You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
579 lines
16 KiB
579 lines
16 KiB
2 years ago
|
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{
|
||
2 years ago
|
index = Math.floor(p.random(0, maxIndex));
|
||
2 years ago
|
} 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(){
|
||
2 years ago
|
return Math.round(this.fields.filter(f => !f.isEmpty).length * 0.7);
|
||
2 years ago
|
}
|
||
|
|
||
|
//Width and height of game field
|
||
|
get size(){
|
||
2 years ago
|
return Math.min(p.width, p.height - this.leaderboard.height);
|
||
2 years ago
|
}
|
||
|
|
||
|
//Upper left corner of game field
|
||
|
get pos(){
|
||
2 years ago
|
return {x: (p.width - this.size) / 2, y: (p.height + this.leaderboard.height - this.size) / 2};
|
||
2 years ago
|
}
|
||
|
|
||
|
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];
|
||
2 years ago
|
let graphics = p.createGraphics(100, 100);
|
||
|
graphics.colorMode(p.HSB);
|
||
2 years ago
|
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(){
|
||
2 years ago
|
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;
|
||
2 years ago
|
}
|
||
|
|
||
|
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){
|
||
2 years ago
|
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));
|
||
2 years ago
|
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){
|
||
2 years ago
|
p.noStroke();
|
||
|
p.fill(0, 0, 5);
|
||
2 years ago
|
let size = this.size / 3;
|
||
2 years ago
|
p.rect(this.pos.x + size, this.pos.y + size, size, size);
|
||
2 years ago
|
}
|
||
|
|
||
|
this.slots.forEach(s => s.display());
|
||
|
|
||
|
let s = this.isNeutral ? 0 : 50;
|
||
2 years ago
|
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);
|
||
2 years ago
|
}
|
||
|
|
||
|
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(){
|
||
2 years ago
|
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;
|
||
2 years ago
|
}
|
||
|
|
||
|
get color(){
|
||
|
let h = game.playerHues[this.field.ownerId];
|
||
|
let s = this.isFilled ? 100 : 0;
|
||
|
let b = this.isMouseOver && !this.isFilled ? 90 : 70;
|
||
2 years ago
|
return p.color(h, s, b);
|
||
2 years ago
|
}
|
||
|
|
||
|
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(){
|
||
|
|
||
2 years ago
|
p.fill(this.color);
|
||
|
p.noStroke();
|
||
|
p.rect(this.pos.x, this.pos.y, this.size, this.size);
|
||
2 years ago
|
|
||
|
if (this.isFilled){
|
||
2 years ago
|
p.stroke(0, 0, 0);
|
||
|
p.strokeWeight(3);
|
||
2 years ago
|
let x = this.pos.x + this.size / 2;
|
||
|
let y = this.pos.y + this.size / 2;
|
||
2 years ago
|
p.line(this.pos.x, y, this.pos.x + this.size, y);
|
||
|
p.line(x, this.pos.y, x, this.pos.y + this.size);
|
||
2 years ago
|
}
|
||
|
|
||
|
if (this.isHighlighted){
|
||
2 years ago
|
p.stroke(0);
|
||
|
p.fill(0);
|
||
2 years ago
|
let hs = this.size / 2;
|
||
2 years ago
|
p.ellipse(this.pos.x + hs, this.pos.y + hs, hs, hs);
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
|
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;
|
||
2 years ago
|
return p.createVector(x, y);
|
||
2 years ago
|
}
|
||
|
|
||
|
get endPos(){
|
||
|
let x = this.slot.partner.field.pos.x + this.slot.size;
|
||
|
let y = this.slot.partner.field.pos.y + this.slot.size;
|
||
2 years ago
|
return p.createVector(x, y);
|
||
2 years ago
|
}
|
||
|
|
||
|
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(){
|
||
|
|
||
2 years ago
|
p.noStroke();
|
||
2 years ago
|
let x = this.pos.x + this.size / 2;
|
||
|
let y = this.pos.y + this.size / 2;
|
||
2 years ago
|
p.fill(0, 0, 0);
|
||
|
p.ellipse(x, y, this.size * 0.9, this.size * 0.9);
|
||
2 years ago
|
|
||
2 years ago
|
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);
|
||
2 years ago
|
}
|
||
|
|
||
|
update(){
|
||
2 years ago
|
this.waitTime += 1 / p.frameRate();
|
||
2 years ago
|
if (this.waitTime >= 0.2 && !this.moving){
|
||
|
this.moving = true;
|
||
|
this.slot.isFilled = false;
|
||
|
this.slot.field.ownerId = 'neutral';
|
||
|
}
|
||
|
if (this.moving){
|
||
2 years ago
|
this.moveProgress += 1.5 / p.frameRate();
|
||
2 years ago
|
}
|
||
|
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){
|
||
2 years ago
|
let panelW = (p.width - game.size) / 2;
|
||
|
p.stroke(200);
|
||
|
p.fill(150);
|
||
|
p.textSize(panelW / 4);
|
||
2 years ago
|
let x = panelW / 2;
|
||
|
let y = game.pos.y + game.size / 2;
|
||
2 years ago
|
let rounded = Math.floor(this.time);
|
||
|
p.text(rounded, x, y);
|
||
2 years ago
|
}
|
||
|
}
|
||
|
|
||
|
start(){
|
||
|
this.limit = new Date().getTime() + gameSettings.turnTime * 1000;
|
||
|
this.isChecked = false;
|
||
|
}
|
||
|
|
||
|
stop(){
|
||
|
this.limit = 0;
|
||
|
}
|
||
|
|
||
|
}
|