function readGameSettings(){ $('.setting').attr('disabled', false); let fieldSize = parseInt($('#fieldsize_select :selected').val()); let turnTime = parseInt($('#turntime_select :selected').val()); let mixed = $('#mixed_check').is(':checked'); let silent = $('#silent_check').is(':checked'); gameSettings.fieldSize = fieldSize; gameSettings.turnTime = turnTime; gameSettings.mixed = mixed; gameSettings.silent = silent; if (mixed){ let maxIndex = fieldSize * fieldSize - 1; let indices = []; for (let i = 0; i < maxIndex * 0.1; i++){ let index; do{ index = Math.floor(p.random(0, maxIndex)); } while (indices.find(i => i === index) != null); indices.push(index); } gameSettings.emptyFieldIndices = indices; } socket.emit('game-settings', gameSettings); } let gameSettings = {}; class Game{ constructor(players){ this.turn = ''; this.playerHues = {'neutral': 0}; this.fields = []; this.fieldCount = gameSettings.fieldSize; this.spreads = []; this.spreadsToRemove = []; this.bufferedSlots = []; let i = 0; for (let x = 0; x < this.fieldCount; x++){ for (let y = 0; y < this.fieldCount; y++){ let isEmpty = false; if (gameSettings.mixed) isEmpty = gameSettings.emptyFieldIndices.find(ei => ei === i) != null; this.fields.push(new Field(x, y, this.fieldCount, isEmpty)); i++; } } this.fields.forEach(f => f.setup(this.fields)); this.leaderboard = new Leaderboard(); this.countdown = new Countdown(); this.fieldMarginFactor = 0.1; socket.on('player-colors', (lobby, colors) => this.setPlayerHues(colors)); socket.on('current-turn', (lobby, turnId) => this.setTurnPlayer(lobby.clients.filter(c => c.isPlayer), turnId)); socket.on('set-slot', (lobby, fieldsIndex, slotsIndex, id) => this.setSlot(fieldsIndex, slotsIndex, id)); this.players = players; } get winCount(){ return Math.round(this.fields.filter(f => !f.isEmpty).length * 0.7); } //Width and height of game field get size(){ return Math.min(p.width, p.height - this.leaderboard.height); } //Upper left corner of game field get pos(){ return {x: (p.width - this.size) / 2, y: (p.height + this.leaderboard.height - this.size) / 2}; } get hasTurn(){ return this.turn === socket.id } get isSpectator(){ return this.players.find(p => p.id === socket.id) == null } get data(){ let data = { pH: this.playerHues, f: [] } this.fields.forEach((f, fi) => { if (!f.isNeutral){ let slots = []; f.slots.forEach((s, si) => { if (s.isFilled) slots.push(si); }); data.f.push({fi: fi, id: f.ownerId, sl: slots, ie: f.isEmpty}); } }); return data; } get players(){ return this._players; } set players(players){ this.leaderboard.setBarsFromPlayers(players); this._players = players; this.fields.forEach(f => { if (players.find(p => p.id === f.ownerId) == null && !f.isNeutral){ f.slots.forEach(s => s.isFilled = false); f.ownerId = 'neutral'; } }); } setPlayerHues(colors){ for (let key in colors){ this.playerHues[key] = colors[key]; let graphics = p.createGraphics(100, 100); graphics.colorMode(p.HSB); graphics.tint(colors[key], 100, 100); graphics.image(gemContentImage, 0, 0, 100, 100); gemContentGraphics[colors[key]] = graphics; } } display(){ this.fields.forEach(f => f.display()); this.spreads.forEach(s => s.display()); this.leaderboard.display(); this.countdown.display(); } update(){ this.spreads.forEach(s => s.update()); this.spreads = this.spreads.filter((s, i) => this.spreadsToRemove.find(r => r === i) == null); if (!this.spreads.length && this.spreadsToRemove.length){ this.sendReady(); } this.spreadsToRemove = []; if (this.countdown.isFinished) this.skipTurn(); if (!this.winId) this.checkWin(); } skipTurn(){ if (!this.countdown.isChecked && this.hasTurn && !this.doneTurn){ this.countdown.isChecked = true; this.doneTurn = true; socket.emit('set-slot', null, null); } } checkWin(){ this.players.forEach((p, i, a) => { let count = this.fields.filter(f => f.ownerId === p.id).length; if (count >= this.winCount || (a.length === 1 && !productionMode)){ console.log('Player ' + p.name + ' (' + p.id + ') won the game!'); this.winId = p.id; refreshGamePlayers(this.players); this.countdown.stop(); //$('#play_again').show(); } }); } setTurnPlayer(players, turnId){ this.turn = turnId; this.doneTurn = false; if (!this.winId) this.countdown.start(); refreshGamePlayers(players); } setSlot(fieldIndex, slotIndex, id){ if (fieldIndex == null || slotIndex == null){ this.sendReady(); return; } if (this.spreads.length){ this.bufferedSlots.push({fieldIndex: fieldIndex, slotIndex: slotIndex, id: id}); return; } if (id === socket.id) this.firstTurnMade = true this.fields.forEach(f => f.slots.forEach(s => s.isHighlighted = false)); let slot = this.fields[fieldIndex].slots[slotIndex]; slot.field.ownerId = id; slot.isFilled = true; if (this.players[0]) if (socket.id === this.players[0].id) socket.emit('game-data', this.data) if (slot.field.allSlotsFilled){ this.createSpreads(slot.field); } else { if (!gameSettings.silent) slot.isHighlighted = true; this.sendReady(); } this.countdown.stop(); } setAllSlots(data){ data.f.forEach(f => { this.fields[f.fi].ownerId = f.id; this.fields[f.fi].isEmpty = f.ie; this.fields[f.fi].slots.forEach((s, i) => { if (f.sl.find(si => si === i) != null){ s.isFilled = true; } }) }); this.fields.forEach(f => { if (f.allSlotsFilled) this.createSpreads(f); }); } sendReady(){ if (this.bufferedSlots.length){ let slot = this.bufferedSlots[0]; this.bufferedSlots.splice(0, 1); this.setSlot(slot.fieldIndex, slot.slotIndex, slot.id); } else { let isDead = this.fields.filter(f => f.isMine).length === 0 && this.firstTurnMade; socket.emit('ready-for-turn', isDead); } } createSpreads(field){ field.slots.forEach(s => { let spread = new Spread(s); this.spreads.push(spread); }); } onMouseDown(){ this.fields.filter(f => f.isMouseOver).forEach(f => f.onMouseDown()); } onPlayerLeft(id){ } } class Field{ constructor(x, y, count, isEmpty){ this.gridPos = {x: x, y: y}; this.count = count; this.isEmpty = isEmpty; this.slots = []; this.ownerId = 'neutral'; } get isTop(){ return this.gridPos.y === 0; } get isRight(){ return this.gridPos.x === this.count - 1; } get isBottom(){ return this.gridPos.y === this.count - 1; } get isLeft(){ return this.gridPos.x === 0; } get size(){ return game.size / (game.fieldCount + (game.fieldCount + 1) * game.fieldMarginFactor); } get margin(){ return game.fieldMarginFactor * this.size; } //Upper left corner of field get pos(){ let x = game.pos.x + this.margin + this.gridPos.x * (this.size + this.margin); let y = game.pos.y + this.margin + this.gridPos.y * (this.size + this.margin); return {x: x, y: y}; } get isMouseOver(){ return p.mouseX > this.pos.x && p.mouseX < this.pos.x + this.size && p.mouseY > this.pos.y && p.mouseY < this.pos.y + this.size; } get isNeutral(){ return this.ownerId === 'neutral'; } get isEnemy(){ return this.ownerId !== socket.id && !this.isNeutral; } get isMine(){ return this.ownerId === socket.id; } get allSlotsFilled(){ return this.slots.find(s => !s.isFilled) == null; } setup(allFields){ if (!this.isEmpty){ for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 2){ let x = Math.round(Math.sin(angle)); let y = Math.round(Math.cos(angle)); if (this.getPartner(x, y, allFields)) this.slots.push(new Slot(x, y)); } this.slots.forEach(s => s.field = this); } } getPartner(x, y, allFields){ return allFields.find(f => f.gridPos.x === this.gridPos.x + x && f.gridPos.y === this.gridPos.y + y && !f.isEmpty ); } display(){ if (!this.isEmpty){ p.noStroke(); p.fill(0, 0, 5); let size = this.size / 3; p.rect(this.pos.x + size, this.pos.y + size, size, size); } this.slots.forEach(s => s.display()); let s = this.isNeutral ? 0 : 50; p.stroke(100); p.strokeWeight(1); p.fill(game.playerHues[this.ownerId], s, 60, 0.3); p.rect(this.pos.x, this.pos.y, this.size, this.size); } onMouseDown(){ this.slots.filter(s => s.isMouseOver).forEach(s => s.onMouseDown()); } } class Slot{ constructor(x, y){ this.gridPos = {x: x, y: y}; this.field = null; this.isFilled = null; this.isHighlighted = false; } get size(){ return this.field.size / 3; } //Upper left corner of slot get pos(){ let x = this.field.pos.x + (this.gridPos.x + 1) * this.size; let y = this.field.pos.y + (this.gridPos.y + 1) * this.size; return {x: x, y: y}; } get isMouseOver(){ return p.mouseX > this.pos.x && p.mouseX < this.pos.x + this.size && p.mouseY > this.pos.y && p.mouseY < this.pos.y + this.size; } get color(){ let h = game.playerHues[this.field.ownerId]; let s = this.isFilled ? 100 : 0; let b = this.isMouseOver && !this.isFilled ? 90 : 70; return p.color(h, s, b); } get partner(){ let fieldIndex = game.fields.findIndex(f => f.gridPos.x === this.field.gridPos.x + this.gridPos.x && f.gridPos.y === this.field.gridPos.y + this.gridPos.y ); let slotIndex = game.fields[fieldIndex].slots.findIndex(s => s.gridPos.x === -this.gridPos.x && s.gridPos.y === -this.gridPos.y ); return game.fields[fieldIndex].slots[slotIndex]; } display(){ p.fill(this.color); p.noStroke(); p.rect(this.pos.x, this.pos.y, this.size, this.size); if (this.isFilled){ p.stroke(0, 0, 0); p.strokeWeight(3); let x = this.pos.x + this.size / 2; let y = this.pos.y + this.size / 2; p.line(this.pos.x, y, this.pos.x + this.size, y); p.line(x, this.pos.y, x, this.pos.y + this.size); } if (this.isHighlighted){ p.stroke(0); p.fill(0); let hs = this.size / 2; p.ellipse(this.pos.x + hs, this.pos.y + hs, hs, hs); } } onMouseDown(){ if (!this.field.isEnemy && game.hasTurn && !game.doneTurn && !this.isFilled){ let slotsIndex = this.field.slots.indexOf(this); let fieldsIndex = game.fields.indexOf(this.field); socket.emit('set-slot', fieldsIndex, slotsIndex); game.countdown.stop(); game.doneTurn = true; } } } class Spread{ constructor(slot){ this.slot = slot; this.ownerId = slot.field.ownerId; this.color = slot.color; this.moveProgress = 0; this.waitTime = 0; this.moving = false; } get startPos(){ let x = this.slot.field.pos.x + this.slot.size; let y = this.slot.field.pos.y + this.slot.size; return p.createVector(x, y); } get endPos(){ let x = this.slot.partner.field.pos.x + this.slot.size; let y = this.slot.partner.field.pos.y + this.slot.size; return p.createVector(x, y); } get pos(){ let step = this.smoothStep(0, 1, this.moveProgress); return p5.Vector.lerp(this.startPos, this.endPos, step); } get size(){ return this.slot.size; } display(){ p.noStroke(); let x = this.pos.x + this.size / 2; let y = this.pos.y + this.size / 2; p.fill(0, 0, 0); p.ellipse(x, y, this.size * 0.9, this.size * 0.9); let c = p.frameCount % 10 >= 5 && !this.moving ? p.color(0, 0, 0) : this.color; p.fill(c); p.ellipse(x, y, this.size * 0.5, this.size * 0.5); } update(){ this.waitTime += 1 / p.frameRate(); if (this.waitTime >= 0.2 && !this.moving){ this.moving = true; this.slot.isFilled = false; this.slot.field.ownerId = 'neutral'; } if (this.moving){ this.moveProgress += 1.5 / p.frameRate(); } if (this.moveProgress >= 1) this.fillEndSlot(); } fillEndSlot(){ game.spreadsToRemove.push(game.spreads.indexOf(this)); let endSlot = this.slot.partner; let buffered = false; if (endSlot.isFilled){ if (endSlot.field.allSlotsFilled){ buffered = true; } else endSlot.field.slots.filter(s => !s.isFilled)[0].isFilled = true; } else { endSlot.isFilled = true; } endSlot.field.ownerId = this.ownerId; if (endSlot.field.allSlotsFilled && !buffered) game.createSpreads(endSlot.field); } smoothStep(start, end, t){ function clamp(x, lowLimit, upLimit){ if (x < lowLimit) x = lowLimit; if (x > upLimit) x = upLimit; return x; } t = clamp((t - start) / (end - start), 0, 1); return t * t * t * (t * (t * 6 - 15) + 10); } } class Countdown{ constructor(){ this.limit = new Date().getTime(); } get time(){ return (this.limit - new Date().getTime()) / 1000; } get isFinished(){ return this.time <= 0; } display(){ if (!this.isFinished){ let panelW = (p.width - game.size) / 2; p.stroke(200); p.fill(150); p.textSize(panelW / 4); let x = panelW / 2; let y = game.pos.y + game.size / 2; let rounded = Math.floor(this.time); p.text(rounded, x, y); } } start(){ this.limit = new Date().getTime() + gameSettings.turnTime * 1000; this.isChecked = false; } stop(){ this.limit = 0; } }