const uuidv4 = require("uuid").v4;
const WebSocket = require("ws");
const pauseable = require("pauseable");
const argv = require("yargs") // eslint-disable-line
.command(
"server[team]",
"starts the server with team name",
(yargs) => {
yargs.positional("team", {
describe: "Team name",
default: 5000,
});
},
(argv) => {
if (argv.verbose) console.info(`start server of team ${argv.team}`);
}
)
.demandOption(["config-charset", "config-match", "config-scenario", "x"])
.option("config-charset", {
alias: "c",
type: "path",
description: "Path to characters.json",
})
.option("config-match", {
alias: "m",
type: "path",
description: "Path to matchconfig.match",
})
.option("config-scenario", {
alias: "s",
type: "path",
description: "Path to scenarioconfig.scenario",
})
.option("port", {
alias: "p",
type: "port",
description: "Run server on specific port",
})
.option("settings", {
alias: "x",
type: "object",
description: "Optional settings",
})
.option("verbose", {
alias: "v",
type: "boolean",
description: "Run with verbose logging",
})
.help("h")
.alias("h", "help").argv;
var dateFormat = require("dateformat");
var Validator = require("jsonschema").Validator;
var v = new Validator();
const {
messageTypeEnum,
roleEnum,
propertyEnum,
gadgetEnum,
operationEnum,
errorEnum,
victoryEnum,
fieldStateEnum,
genderEnum,
phaseEnum,
metaEnum,
} = require("./enums");
const characters = require(argv.c);
const matchconfig = require(argv.m);
const { scenario } = require(argv.s);
const settings = JSON.parse(argv.x);
const wss = new WebSocket.Server({ port: argv.port ? argv.port : 7007 });
const PHASE_TIMEOUT = matchconfig.turnPhaseLimit * 1000;
const RECONNECT_TIMEOUT = matchconfig.reconnectLimit * 1000;
const PAUSE_TIMEOUT = matchconfig.pauseLimit * 1000;
var messageSchema = {
id: "/message",
type: "object",
properties: {
clientId: {
type: "string",
format:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
},
type: { type: "string", enum: Object.values(messageTypeEnum) },
creationDate: {
type: "string",
format:
"^([1-9]|([012][0-9])|(3[01]))-([0]{0,1}[1-9]|1[012])-dddd (20|21|22|23|[0-1]?d):[0-5]?d:[0-5]?d$",
},
debugMessage: { type: "string" },
},
};
v.addSchema(messageSchema, "/message");
var helloSchema = {
id: "/message/hello",
allOf: [{ $ref: "/message" }],
properties: {
name: { type: "string" },
role: { type: "string", enum: Object.values(roleEnum) },
},
};
var reconnectSchema = {
id: "/message/reconnect",
allOf: [{ $ref: "/message" }],
properties: {
sessionId: {
type: "string",
format:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
},
},
};
var itemChoiceSchema = {
id: "/message/itemChoice",
allOf: [{ $ref: "/message" }],
properties: {
chosenCharacterId: {
type: "string",
format:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
},
chosenGadget: { type: "string", enum: Object.values(gadgetEnum) },
},
};
var equipChoiceSchema = {
id: "/message/equipChoice",
allOf: [{ $ref: "/message" }],
properties: {
chosenCharacterIds: {
type: "array",
items: {
type: "string",
format:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
},
},
chosenGadgets: {
type: "array",
items: {
type: "string",
enum: Object.values(gadgetEnum),
},
},
},
};
var gameOperationSchema = {
id: "/message/gameOperation",
allOf: [{ $ref: "/message" }],
properties: {
operation: {
type: "object",
properties: {
type: {
type: "string",
enum: Object.values(operationEnum),
},
target: {
type: "obejct",
properties: {
x: { type: "number" },
y: { type: "number" },
},
},
characterId: {
type: "string",
format:
"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
},
},
},
},
};
var gameLeaveSchema = {
id: "/message/gameLeave",
allOf: [{ $ref: "/message" }],
properties: {},
};
var metaSchema = {
id: "/message/metaSchema",
allOf: [{ $ref: "/message" }],
properties: {
keys: {
type: "string",
enum: Object.values(metaEnum),
},
},
};
var replaySchema = {
id: "/message/replaySchema",
allOf: [{ $ref: "/message" }],
properties: {},
};
var requestGamePauseSchema = {
id: "/message/requestGamePause",
allOf: [{ $ref: "/message" }],
properties: {
gamePause: { type: "boolean" },
},
};
var sessionId = uuidv4();
// assign id to each character
var characterSettings = [...characters];
var charactersObject = {};
characterSettings.forEach((char) => {
char.characterId = uuidv4();
charactersObject[char.characterId] = char;
});
// phase
var currentPhase = phaseEnum.INIT;
var choosableCharacterIds = characterSettings.map((char) => char.characterId);
var choosableGadgets = Object.values(gadgetEnum).filter(
(gadget) =>
gadget != gadgetEnum.DIAMOND_COLLAR && gadget != gadgetEnum.COCKTAIL
);
// game
var playerOne = {};
var playerTwo = {};
var statistics = [];
var hasReplay = true;
var gameStart = 0,
gameEnd = 0;
var gamePaused = false;
var gameTimeout, itemTimeout, equipTimeout, pauseTimeout;
wss.on("connection", (ws) => {
if (argv.verbose) console.info("new connection");
/**
* Closes the connection to a player and lets the other win.
* @param {Websocket} ws The websocket client of a player
*/
disqualifyPlayer = (ws) => {
ws.close();
sendWinnerMessage(
getOtherPlayer(ws.player).id,
victoryEnum.VICTORY_BY_KICK
);
};
ws.messages = [];
ws.on("message", (message) => {
const jsonMessage = JSON.parse(message);
if (argv.verbose) console.info(jsonMessage);
if (v.validate(jsonMessage, messageSchema)) {
const { clientId, type, creationDate, debugMessage } = jsonMessage;
var player = ws.player;
var spectator = ws.spectator;
switch (type) {
case messageTypeEnum.HELLO: {
if (v.validate(jsonMessage, helloSchema)) {
const { name, role } = jsonMessage;
if (role == roleEnum.PLAYER) {
if (playerOne.connected && playerTwo.connected) {
sendMessage(
ws,
messageTypeEnum.ERROR,
{ reason: errorEnum.GENERAL },
"Server is full"
);
ws.close();
} else {
// player connects
player = {
id: uuidv4(),
name,
role,
sendMessage: (type, data) => sendMessage(ws, type, data),
disqualifyPlayer: () => disqualifyPlayer(ws),
characters: [],
gadgets: [],
connected: true,
cocktrailDrinks: 0,
cocktailSpills: 0,
totalDamage: 0,
};
ws.player = player;
playerOne.connected
? (playerTwo = player)
: (playerOne = player);
sendMessage(ws, messageTypeEnum.HELLO_REPLY, {
sessionId,
level: scenario,
settings: matchconfig,
characterSettings,
});
if (playerOne.connected && playerTwo.connected) {
if (argv.verbose) console.info("game start");
startGame();
}
}
} else if (role == roleEnum.SPECTATOR) {
spectator = {
id: uuidv4(),
name,
role,
sendMessage: (type, data) => sendMessage(ws, type, data),
};
ws.spectator = spectator;
sendMessage(ws, messageTypeEnum.HELLO_REPLY, {
sessionId,
level: scenario,
settings: matchconfig,
characterSettings,
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.RECONNECT: {
if (player && v.validate(jsonMessage, reconnectSchema)) {
if (jsonMessage.sessionId == sessionId) {
player = playerOne.connected ? playerTwo : playerOne;
gamePaused = false;
sendMessage(ws, messageTypeEnum.HELLO_REPLY, {
sessionId,
level: scenario,
settings: matchconfig,
characterSettings,
});
// send phase data
switch (currentPhase) {
case phaseEnum.ITEM_CHOSE: {
sendMessage(ws, messageTypeEnum.REQUEST_ITEM_CHOICE, {
offeredCharacterIds: getOfferedCharacterIds(player),
offeredGadgets: getOfferedGadgets(player),
});
break;
}
case phaseEnum.EQUIP_CHOSE: {
sendMessage(ws, messageTypeEnum.REQUEST_EQUIPMENT_CHOICE, {
chosenCharacterIds: player.characters,
chosenGadgets: player.gadgets,
});
break;
}
}
} else {
// Occurs when an attempt is made to access a session that does not exist using a ReconnectMessage or otherwise
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.SESSION_DOES_NOT_EXIST,
debugMessage: "Incorrect session id",
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.ITEM_CHOICE: {
if (player && v.validate(jsonMessage, itemChoiceSchema)) {
if (currentPhase === phaseEnum.ITEM_CHOSE) {
const { chosenCharacterId, chosenGadget } = jsonMessage;
if (argv.verbose) console.info(chosenCharacterId, chosenGadget);
if (chosenCharacterId ? chosenGadget : !chosenGadget) {
// logical XNOR -> cant be both null or chosen
sendMessage(
ws,
messageTypeEnum.ERROR,
{ reason: errorEnum.ILLEGAL_MESSAGE },
"Either chosenCharacterId or chosenGadget has to be null"
);
return;
}
if (chosenCharacterId) {
if (!player.offeredCharacterIds.includes(chosenCharacterId)) {
sendMessage(
ws,
messageTypeEnum.ERROR,
{ reason: errorEnum.ILLEGAL_MESSAGE },
chosenCharacterId + " was not offered"
);
return;
}
player.characters.push(chosenCharacterId); // add to player
} else if (chosenGadget) {
if (!player.offeredGadgets.includes(chosenGadget)) {
sendMessage(
ws,
messageTypeEnum.ERROR,
{ reason: errorEnum.ILLEGAL_MESSAGE },
chosenGadget + " was not offered"
);
return;
}
player.gadgets.push(chosenGadget); // add to player
}
choosableCharacterIds.push(
...player.offeredCharacterIds.filter(
(id) => id !== chosenCharacterId
)
); // lay back the rest
choosableGadgets.push(
...player.offeredGadgets.filter((id) => id !== chosenGadget)
); // lay back the rest
// if slots are filled
if (player.characters.length + player.gadgets.length < 8) {
sendMessage(ws, messageTypeEnum.REQUEST_ITEM_CHOICE, {
offeredCharacterIds: getOfferedCharacterIds(player),
offeredGadgets: getOfferedGadgets(player),
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect phase",
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.EQUIPMENT_CHOICE: {
if (player && v.validate(jsonMessage, equipChoiceSchema)) {
if (currentPhase === phaseEnum.EQUIP_CHOSE) {
var equipment = jsonMessage.equipment;
var isValid = true; // check if uuids and gadgets are in player pool
for (var uuid in equipment) {
if (!player.characters.includes(uuid)) isValid = false;
for (var gadget of equipment[uuid]) {
if (!player.gadgets.includes(gadget)) isValid = false;
}
}
if (isValid) {
player.equipment = equipment; //set equipment
} else {
// close connection if not valid
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Equipment is not valid",
});
player.disqualifyPlayer();
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect phase",
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.GAME_OPERATION: {
if (player && v.validate(jsonMessage, gameOperationSchema)) {
if (
currentPhase === phaseEnum.GAME &&
player.activeCharacterId === activeCharacterId
) {
var operation = jsonMessage.operation;
if (
operation.characterId == activeCharacterId &&
(operation.type == operationEnum.RETIRE ||
doOperation(player, operation))
) {
// Siegesbedingung überprüfen
if (!gameOver) {
// Send game status message
sendGameStatusMessage();
// Next turn
if (
(gameCharacters[activeCharacterId].mp > 0 ||
gameCharacters[activeCharacterId].ap > 0) &&
operation.type != operationEnum.RETIRE
) {
player.sendMessage(
// request new operation from active character
messageTypeEnum.REQUEST_GAME_OPERATION,
{
characterId: activeCharacterId,
},
"Character turn"
);
} else {
nextTurn();
}
} else {
// Send game status message
sendGameStatusMessage();
determineWinner();
}
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect phase",
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.GAME_LEAVE: {
if (v.validate(jsonMessage, gameLeaveSchema)) {
ws.close();
if (player) {
broadcastMessage(messageTypeEnum.GAME_LEFT, {
leftUserId: player.id,
});
sendWinnerMessage(
getOtherPlayer(ws.player).id,
victoryEnum.VICTORY_BY_LEAVE
);
} else if (spectator) {
spectator.sendMessage({
leftUserId: spectator.id,
});
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.REQUEST_GAME_PAUSE: {
if (player && v.validate(jsonMessage, requestGamePauseSchema)) {
if (jsonMessage.gamePause) {
pauseGame(player);
} else {
resumeGame();
}
broadcastMessage(messageTypeEnum.GAME_PAUSE, {
gamePaused,
serverEnforced: false,
});
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.REQUEST_META_INFORMATION: {
if (v.validate(jsonMessage, metaSchema)) {
const { keys } = jsonMessage;
var information = {};
if (keys.includes(metaEnum.CONFIG_SCENARIO)) {
information[metaEnum.CONFIG_SCENARIO] = scenario;
}
if (keys.includes(metaEnum.CONFIG_MATCH)) {
information[metaEnum.CONFIG_MATCH] = matchconfig;
}
if (keys.includes(metaEnum.CONFIG_CHAR)) {
information[metaEnum.CONFIG_CHAR] = characters;
}
if (keys.includes(metaEnum.FRAC_ONE)) {
information[metaEnum.FRAC_ONE] =
player && player.id == playerTwo.id
? null
: playerOne.characters;
}
if (keys.includes(metaEnum.FRAC_TWO)) {
information[metaEnum.FRAC_TWO] =
player && player.id == playerOne.id
? null
: playerTwo.characters;
}
if (keys.includes(metaEnum.FRAC_NEUTRAL)) {
information[metaEnum.FRAC_NEUTRAL] = player
? null
: npcCharacters;
}
if (keys.includes(metaEnum.GAD_ONE)) {
information[metaEnum.GAD_ONE] =
player && player.id == playerTwo.id ? null : playerOne.gadgets;
}
if (keys.includes(metaEnum.GAD_TWO)) {
information[metaEnum.GAD_TWO] =
player && player.id == playerOne.id ? null : playerTwo.gadgets;
}
sendMessage(ws, messageTypeEnum.META_INFORMATION, {
information,
});
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
case messageTypeEnum.REQUEST_REPLAY: {
if (v.validate(jsonMessage, replaySchema)) {
sendMessage(ws, messageTypeEnum.REPLAY, {
sessionId,
gameStart,
gameEnd,
playerOneId: playerOne.id,
playerTwoId: playerTwo.id,
playerOneName: playerOne.name,
playerTwoName: playerTwo.name,
rounds: currentRound,
level: scenario,
settings: matchconfig,
characterSettings: characters,
messages: ws.messages,
});
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect schema",
});
}
break;
}
}
} else {
sendMessage(ws, messageTypeEnum.ERROR, {
reason: errorEnum.ILLEGAL_MESSAGE,
});
}
});
ws.on("close", (number, reason) => {
var player = ws.player;
if (player) {
player.connected = false;
if (argv.verbose)
console.info(
playerOne.connected
? "playerTwo disconnected"
: "playerOne disconnected",
number,
reason
);
pauseGame(player);
broadcastMessage(messageTypeEnum.GAME_PAUSE, {
gamePaused,
serverEnforced: true,
});
setTimeout(() => {
if (!player.connected) {
broadcastMessage(messageTypeEnum.STATISTICS, {
statistics,
winner: getOtherPlayer(player).id,
reason: victoryEnum.VICTORY_BY_LEAVE,
hasReplay,
});
}
}, RECONNECT_TIMEOUT);
}
});
/**
* Determines the winner.
*/
determineWinner = () => {
var playerOneIPs = 0;
for (let characterId of playerOne.characters) {
playerOneIPs += gameCharacters[characterId].ip;
playerOneIPs +=
gameCharacters[characterId].chips * matchconfig.chipsToIpFactor;
}
var playerTwoIPs = 0;
for (let characterId of playerTwo.characters) {
playerTwoIPs += gameCharacters[characterId].ip;
playerTwoIPs +=
gameCharacters[characterId].chips * matchconfig.chipsToIpFactor;
}
if (playerOneIPs > playerTwoIPs) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_IP);
} else if (playerOneIPs < playerTwoIPs) {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_IP);
}
if (catHandover.id == playerOne.id) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_COLLAR);
} else if (catHandover.id == playerTwo.id) {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_COLLAR);
}
if (playerOne.cocktrailDrinks > playerTwo.cocktrailDrinks) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_DRINKING);
} else if (playerOne.cocktrailDrinks < playerTwo.cocktrailDrinks) {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_DRINKING);
}
if (playerOne.cocktailSpills > playerTwo.cocktailSpills) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_SPILLING);
} else if (playerOne.cocktailSpills < playerTwo.cocktailSpills) {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_SPILLING);
}
if (playerOne.totalDamage > playerTwo.totalDamage) {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_HP);
} else if (playerOne.totalDamage < playerTwo.totalDamage) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_HP);
}
if (Math.round(Math.random())) {
sendWinnerMessage(playerOne.id, victoryEnum.VICTORY_BY_RANDOMNESS);
} else {
sendWinnerMessage(playerTwo.id, victoryEnum.VICTORY_BY_RANDOMNESS);
}
endGame();
};
/**
* Broadcasts the winner message.
* @param {String} playerId The player id of the winner.
* @param {messageTypeEnum} type The win reason.
*/
sendWinnerMessage = (playerId, type) => {
broadcastMessage(messageTypeEnum.STATISTICS, {
statistics,
winner: playerId,
reason: type,
hasReplay,
});
};
/**
* Returns the other player.
* @param {Player} player The player.
* @return {Player} The player's opponent.
*/
getOtherPlayer = (player) => {
return player.id == playerOne.id ? playerTwo : playerOne;
};
/**
* Returns a random player.
* @return {Player} Either playerOne oder playerTwo.
*/
getRandomPlayer = () => {
return Math.round(Math.random()) ? playerOne : playerTwo;
};
/**
* Returns the offered character ids for a player.
* @param {Player} player The player.
* @return {String[]} Array of offered character ids.
*/
getOfferedCharacterIds = (player) => {
var offeredCharacterIds = [];
var num = player.characters.length == 4 ? 0 : 3;
for (var i = 0; i < num; i++) {
let r = Math.round(Math.random() * (choosableCharacterIds.length - 1));
offeredCharacterIds.push(choosableCharacterIds[r]);
choosableCharacterIds.splice(r, 1);
}
player.offeredCharacterIds = offeredCharacterIds;
return offeredCharacterIds;
};
/**
* Returns the offered gadgets.
* @param {Player} player The player.
* @return {String[]} Array of offered gadgets.
*/
getOfferedGadgets = (player) => {
var offeredGadgets = [];
var num = player.gadgets.length == 6 ? 0 : 3;
for (var i = 0; i < num; i++) {
let r = Math.round(Math.random() * (choosableGadgets.length - 1));
offeredGadgets.push(choosableGadgets[r]);
choosableGadgets.splice(r, 1);
}
player.offeredGadgets = offeredGadgets;
return offeredGadgets;
};
});
/**
* Sends a message to a websocket client.
* @param {Websocket} ws The receiving websocket client.
* @param {messageTypeEnum} type The type of the message.
* @param {Object} data The data to send.
* @param {String} debugMessage The debug message to send.
*/
sendMessage = (ws, type, data, debugMessage = "") => {
var message = JSON.stringify({
clientId: ws.player ? ws.player.id : undefined,
type,
creationDate: dateFormat(new Date(), "dd.mm.yyyy hh:MM:ss"),
debugMessage,
...data,
});
ws.messages.push(message);
ws.send(message);
};
/**
* Broadcasts a message to a all websocket clients.
* @param {messageTypeEnum} type The type of the message.
* @param {Object} data The data to send.
* @param {String} debugMessage The debug message to send.
*/
broadcastMessage = (type, data, debugMessage = "") => {
wss.clients.forEach((ws) => sendMessage(ws, type, data, debugMessage));
};
/**
* Ends the game.
*/
endGame = () => {
endGame = dateFormat(new Date(), "dd.mm.yyyy hh:MM:ss");
gameOver = true;
};
/**
* Pauses the game.
*/
pauseGame = (player) => {
gamePaused = true;
itemTimeout.pause();
equipTimeout.pause();
gameTimeout.pause();
pauseTimeout = pauseable.setTimeout(() => {
broadcastMessage(messageTypeEnum.STATISTICS, {
statistics,
winner: getOtherPlayer(player).id,
reason: victoryEnum.VICTORY_BY_LEAVE,
hasReplay,
});
}, PAUSE_TIMEOUT);
};
/**
* Resumes the game.
*/
resumeGame = () => {
gamePaused = false;
itemTimeout.resume();
equipTimeout.resume();
gameTimeout.resume();
pauseTimeout.clear();
};
/**
* Sarts the game.
*/
startGame = () => {
gameStart = dateFormat(new Date(), "dd.mm.yyyy hh:MM:ss");
broadcastMessage(messageTypeEnum.GAME_STARTED, {
playerOneId: playerOne.id,
playerOneName: playerOne.name,
playerTwoId: playerTwo.id,
playerTwoName: playerTwo.name,
sessionId,
});
// Start choosing phase
if (argv.verbose) console.info("item chose phase");
currentPhase = phaseEnum.ITEM_CHOSE;
playerOne.sendMessage(messageTypeEnum.REQUEST_ITEM_CHOICE, {
offeredCharacterIds: getOfferedCharacterIds(playerOne),
offeredGadgets: getOfferedGadgets(playerOne),
});
playerTwo.sendMessage(messageTypeEnum.REQUEST_ITEM_CHOICE, {
offeredCharacterIds: getOfferedCharacterIds(playerTwo),
offeredGadgets: getOfferedGadgets(playerTwo),
});
itemTimeout = pauseable.setTimeout(() => {
// Close connection if slots not filled
if (playerOne.characters.length + playerOne.gadgets.length < 8) {
playerOne.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Not all slots were filled",
});
playerOne.disqualifyPlayer();
}
if (playerTwo.characters.length + playerTwo.gadgets.length < 8) {
playerTwo.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Not all slots were filled",
});
playerTwo.disqualifyPlayer();
}
// Move on to equip phase
if (argv.verbose) console.info("equip chose phase");
currentPhase = phaseEnum.EQUIP_CHOSE;
playerOne.sendMessage(messageTypeEnum.REQUEST_EQUIPMENT_CHOICE, {
chosenCharacterIds: playerOne.characters,
chosenGadgets: playerOne.gadgets,
});
playerTwo.sendMessage(messageTypeEnum.REQUEST_EQUIPMENT_CHOICE, {
chosenCharacterIds: playerTwo.characters,
chosenGadgets: playerTwo.gadgets,
});
equipTimeout = pauseable.setTimeout(() => {
// Close connection if no equipment
if (!playerOne.equipment) {
playerOne.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No equipment",
});
playerOne.disqualifyPlayer();
}
if (!playerTwo.equipment) {
playerTwo.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No equipment",
});
playerTwo.disqualifyPlayer();
}
// Move on to game phase
if (argv.verbose) console.info("game phase");
currentPhase = phaseEnum.GAME;
// Set map
var safeCount = 1;
var safeIndices = [];
var freeFields = [];
for (let row in scenario) {
var fields = [];
for (let col in scenario[row]) {
let state = scenario[row][col];
fields.push({
state,
gadget:
state == fieldStateEnum.BAR_TABLE
? { gadget: gadgetEnum.COCKTAIL, usages: 0, isPoisoned: false }
: null, // place cocktails on bar tables
isDestroyed: false,
isInverted: false,
chipAmount: matchconfig.maxChipsRoulette,
safeIndex: null,
isFoggy: false,
isUpdated: true,
});
if (state == fieldStateEnum.SAFE) {
safeIndices.push(safeCount);
safeCount++;
}
if (
state === fieldStateEnum.FREE ||
state === fieldStateEnum.BAR_SEAT
)
freeFields.push({ x: parseInt(col), y: parseInt(row) }); // add to free fields
}
map.push(fields);
}
//randomize safe indices https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
for (let i = safeIndices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[safeIndices[i], safeIndices[j]] = [safeIndices[j], safeIndices[i]];
}
// set safe indices and combinations
safeIndex = 0;
for (let row in map) {
for (let col in row) {
let field = map[row][col];
field.safeIndex = safeIndices[safeIndex];
safeCombinations.push(field.safeIndex);
// put collar in random safe
if (safeIndex == 0) {
field.gadget = {
gadget: gadgetEnum.DIAMOND_COLLAR,
usages: 0,
};
}
safeIndex++;
}
}
// Set gameCharacters
for (let i = 0; i < npcCount; i++) {
// add random NPCs
if (choosableCharacterIds.length > 0) {
let r = Math.round(
Math.random() * (choosableCharacterIds.length - 1)
);
let randomFreeField = Math.round(
Math.random() * (freeFields.length - 1)
); // get random free field
let characterId = choosableCharacterIds[r];
let { name, features, gender } = charactersObject[characterId];
gameCharacters[characterId] = {
characterId,
name,
gender,
coordinates: freeFields[randomFreeField],
properties: features,
deactivatedProperties: [],
mp: 2,
ap: 1,
hp: 100,
ip: 0,
chips: 10,
gadgets: [],
};
freeFields.splice(randomFreeField, 1);
npcCharacters.push(choosableCharacterIds[r]);
choosableCharacterIds.splice(r, 1);
}
}
setGameCharacters(playerOne, freeFields);
setGameCharacters(playerTwo, freeFields);
// Set cat on random free field
let randomFreeField = Math.round(Math.random() * (freeFields.length - 1));
catCoordinates = freeFields[randomFreeField];
freeFields.splice(randomFreeField, 1);
sendGameStatusMessage(); // send initial game status message
startRound(getRandomPlayer()); //start first round with random player
}, PHASE_TIMEOUT);
}, PHASE_TIMEOUT);
};
/**
* Initiates the characters of a player.
* @param {Player} player The player.
* @param {Field[]} freeFields The remaining free fields on the map.
*/
setGameCharacters = (player, freeFields) => {
for (let characterId in player.equipment) {
let { name, features, gender } = charactersObject[characterId];
let charGadgets = player.equipment[characterId];
let randomFreeField = Math.round(Math.random() * (freeFields.length - 1)); // get random free field
let gadgets = [];
for (let gadget in charGadgets) {
if (gadget == gadgetEnum.WIRETAP_WITH_EARPLUGS) {
gadgets.push({
gadget,
usages: 0,
isActive: null,
isWorking: true,
});
} else {
gadgets.push({
gadget,
usages: 0,
});
}
}
gameCharacters[characterId] = {
characterId,
name,
gender,
coordinates: freeFields[randomFreeField],
properties: features,
deactivatedProperties: [],
mp: 2,
ap: 1,
hp: 100,
ip: 0,
chips: 10,
gadgets,
};
freeFields.splice(randomFreeField, 1); // remove free field
}
};
var npcCount = settings.npcCount ? settings.npcCount : 3;
var npcCharacters = [];
var safeCombinations = [];
var currentRound = 0;
var map = [];
var gameCharacters = {};
var characterOrder = [];
var catCoordinates = null;
var janitorCoordinates = null;
var janitorID = uuidv4();
var catID = uuidv4();
var catHandover = null;
var activeCharacterId = null;
var wireTapChar = null;
var earPlugsChar = null;
var fogTinTime = 0;
var fogTin = null;
var isGameOver = false;
var latestOperations = [];
/**
* Returns the current game state in respect to a specific client.
* @param {Websocket} ws The websocket client.
* @return {Object} The game state.
*/
getGameState = (ws) => {
return {
currentRound,
map,
mySafeCombinations: ws.player ? ws.player.safeCombinations : [],
characters: Object.values(gameCharacters),
catCoordinates,
janitorCoordinates,
};
};
/**
* Broadcasts the current game state.
*/
sendGameStatusMessage = () => {
wss.clients.forEach((ws) => {
sendMessage(
ws,
messageTypeEnum.GAME_STATUS,
{
activeCharacterId,
operations: latestOperations,
state: getGameState(ws),
isGameOver,
},
"Game state"
);
});
latestOperations = [];
};
/**
* Starts a new round
*/
startRound = () => {
currentRound++;
// Janitor
if (currentRound == matchconfig.roundLimit) {
var freeFields = [];
for (let row in map) {
for (let col in row) {
let field = map[row][col];
if (
field == fieldStateEnum.FREE &&
!getCharacterByPosition({ x: col, y: row })
) {
freeFields.push(field);
}
}
}
janitorCoordinates =
freeFields[Math.round(Math.random() * (freeFields.length - 1))]; // spawn on random field
npcCharacters.forEach((characterId) => {
// remove all NPCs
delete gameCharacters[characterId];
});
sendGameStatusMessage();
}
// Fill cocktails and do fireplace dry
for (let row in map) {
for (let col in row) {
let field = map[row][col];
if (field.state === fieldStateEnum.BAR_TABLE) {
field.gadget = {
gadget: gadgetEnum.COCKTAIL,
usages: 0,
isPoisoned: false,
};
} else if (field.state === fieldStateEnum.FIREPLACE) {
getNeighbors({ x: col, y: row }).forEach((neighbor) => {
let neighborChar = getCharacterByPosition(neighbor);
if (
neighborChar &&
neighborChar.properties.includes(propertyEnum.CLAMMY_CLOTHES)
) {
removeProperty(neighborChar, propertyEnum.CLAMMY_CLOTHES);
}
});
}
}
}
// Ausfallwahrscheinlichkeitsprobe and bar seat
for (let char of Object.values(gameCharacters)) {
for (let gadget of char.gadgets) {
if (gadget.gadget == gadgetEnum.WIRETAP_WITH_EARPLUGS) {
if (getChance(char, matchconfig.wiretapWithEarplugsFailChance)) {
gadget.isWorking = false;
gadget.activeOn = null;
wireTapChar = null;
earPlugsChar = null;
}
}
}
if (
map[char.coordinates.y][char.coordinates.x].state ===
fieldStateEnum.BAR_SEAT
) {
char.hp = 100;
}
}
// Überprüfen ob Fog Tin aktiv
if (fogTin) {
fogTinTime++;
if (fogTinTime == 3) {
var fogTinField = map[fogTin.y][fogTin.x];
fogTinField.isFoggy = false;
fogTinIsActive = false;
getNeighbors(fogTin).forEach((coords) => {
let field = map[coords.y][coords.x];
field.isFoggy = false;
});
}
}
// Zugreihenfolge der charaktere
setCharacterOrder();
if (currentRound >= matchconfig.roundLimit) {
characterOrder.splice(
Math.round(Math.random() * (characterOrder.length - 1)),
0,
janitorID
); // random turn place
}
characterOrder.splice(
Math.round(Math.random() * (characterOrder.length - 1)),
0,
catID
); // random turn place
// Erste Zugphase
startTurn(characterOrder[0]);
};
/**
* Sets the order of all characters.
*/
setCharacterOrder = () => {
// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
var a = Object.keys(gameCharacters);
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
characterOrder = a;
};
/**
* Starts a new turn for a specific character.
* @param {String} characterId The id of the character.
*/
startTurn = (characterId) => {
activeCharacterId = characterId;
if (argv.verbose) console.info("Turn: ", characterId);
if (npcCharacters.includes(activeCharacterId)) {
// NPC turn
if (argv.verbose) console.info("NPC turn");
var targetChar = gameCharacters[characterId];
while (targetChar.mp > 0 || targetChar.ap > 0) {
if (argv.verbose) console.info("NPC loop", targetChar.mp, targetChar.ap);
if (targetChar.mp > 0) {
var freeNeighbors = [];
getNeighbors(targetChar.coordinates).forEach((coords) => {
let field = map[coords.y][coords.x];
if (
field.state == fieldStateEnum.FREE ||
field.state == fieldStateEnum.BAR_SEAT
)
freeNeighbors.push(coords);
});
var target =
freeNeighbors[Math.round(Math.random() * (freeNeighbors.length - 1))];
if (argv.verbose) console.info("NPC moves to ", target);
doOperation(null, {
type: operationEnum.MOVEMENT,
target,
characterId,
});
}
if (targetChar.ap > 0) {
if (getGadget(targetChar, gadgetEnum.MOLEDIE)) {
if (argv.verbose) console.info("NPC throws moledie ");
doOperation(null, {
type: operationEnum.GADGET_ACTION,
target: getRandomFreeNeighbor(targetChar.coordinates),
characterId,
});
} else {
targetChar.ap--;
}
}
}
sendGameStatusMessage();
nextTurn();
} else if (characterId == janitorID) {
// janitor turn
if (argv.verbose) console.info("Janitor turn");
var nearestChar = getNearestCharacter(janitorCoordinates);
janitorCoordinates = nearestChar.coordinates; // jump to nearest character
delete gameCharacters[nearestChar.characterId]; // remove character
characterOrder.splice(characterOrder.indexOf(nearestChar.characterId), 1); // remove from turn order
if (Object.keys(gameCharacters).length == 0) {
endGame();
sendGameStatusMessage();
determineWinner();
} else {
sendGameStatusMessage();
nextTurn();
}
} else if (characterId == catID) {
// cat turn
if (argv.verbose) console.info("Cat turn");
var freeNeighbors = [];
getNeighbors(catCoordinates).forEach((coords) => {
let field = map[coords.y][coords.x];
if (
field.state == fieldStateEnum.FREE ||
field.state == fieldStateEnum.BAR_SEAT
)
freeNeighbors.push(coords);
});
var target =
freeNeighbors[Math.round(Math.random() * (freeNeighbors.length - 1))];
var targetField = map[target.y][target.x];
catCoordinates = target;
if (
targetField.gadget &&
targetField.gadget.gadget == gadgetEnum.DIAMOND_COLLAR
) {
endGame();
sendGameStatusMessage();
determineWinner();
} else {
sendGameStatusMessage();
nextTurn();
}
} else {
// player turn
if (argv.verbose) console.info("Player turn");
var player = playerOne.characters.includes(characterId)
? playerOne
: playerTwo; // determine player
player.activeCharacterId = characterId;
prepareTurn(activeCharacterId);
player.sendMessage(
messageTypeEnum.REQUEST_GAME_OPERATION,
{
characterId: activeCharacterId,
},
"Character turn"
);
gameTimeout = pauseable.setTimeout(() => {
if (activeCharacterId == characterId) {
player.strikes++;
player.sendMessage(messageTypeEnum.STRIKE, {
strikeNr: player.strikes,
strikeMax: matchconfig.strikeMaximum,
reason: "Turn took to long",
});
}
if (player.strikes == matchconfig.strikeMaximum) {
player.disqualifyPlayer();
}
}, PHASE_TIMEOUT);
}
};
/**
* Starts the next turn or next round if all characters had their turn.
*/
nextTurn = () => {
if (argv.verbose) console.info("Next turn");
// Next round if last character
if (activeCharacterId == characterOrder.pop()) {
startRound();
} else {
// Next character
var nextCharId =
characterOrder[characterOrder.indexOf(activeCharacterId) + 1];
startTurn(nextCharId);
}
};
/**
* Prepares a characters turn and sets their mps and aps in respect to their properties.
* @param {String} characterId The id of the character.
*/
prepareTurn = (characterId) => {
var character = gameCharacters[characterId];
var mp = 2,
ap = 1;
if (character.properties.includes(propertyEnum.NIMBLENESS)) mp++;
if (character.properties.includes(propertyEnum.SLUGGISHNESS)) mp--;
if (character.properties.includes(propertyEnum.SPRYNESS)) ap++;
if (character.properties.includes(propertyEnum.PONDEROUSNESS)) {
if (Math.round(Math.random())) mp--;
else ap--;
}
if (character.properties.includes(propertyEnum.AGILITY)) {
if (Math.round(Math.random())) mp++;
else ap++;
}
character.mp = mp;
character.ap = ap;
};
/**
* Returns a character (if on field) by position.
* @param {Point} coordinates The position.
* @return {Character} The character. (null if no character on that position)
*/
getCharacterByPosition = (coordinates) => {
for (let char of Object.values(gameCharacters)) {
if (
char.coordinates.x == coordinates.x &&
char.coordinates.y == coordinates.y
) {
return char;
}
}
return null;
};
/**
* Returns true or false for a character with a specific chance in respect to their properties.
* @param {Character} character The character.
* @param {Double} chance The chance.
* @return {Boolean} Random boolean.
*/
getChance = (character, chance) => {
if (
character.properties.includes(propertyEnum.CLAMMY_CLOTHES) ||
character.properties.includes(propertyEnum.CONSTANT_CLAMMY_CLOTHES)
) {
chance /= 2;
}
if (
!(Math.random() <= chance) &&
char.poperties.include(propertyEnum.TRADECRAFT)
) {
return Math.random() <= chance;
}
return true;
};
/**
* Adds damage to a character in respect to their properties. (Decreases hp)
* @param {Character} character The character to damage.
* @param {Integer} damage The amaount of damage.
* @param {Boolean} poison If cause of damage is a poisoned cocktail.
*/
damageCharacter = (character, damage, poison = false) => {
var successful = true;
if (character.properties.includes(propertyEnum.TOUGHNESS) && !poison)
damage /= 2;
var player = playerOne.characters.includes(character.characterId)
? playerOne
: playerTwo;
if (!poison) {
if (
character.properties.includes(propertyEnum.BABYSITTER) &&
getChance(character, matchconfig.babysitterSuccessChance)
) {
// babysitter
var neighbors = [];
getNeighbors(character.coordinates).forEach((neighbor) => {
let neighborChar = getCharacterByPosition(neighbor);
if (
neighborChar &&
player.characters.includes(neighborChar.characterId)
) {
neighbors.push(neighborChar);
}
});
if (neighbors.length > 0) {
character =
neighbors[Math.round(Math.random() * (neighbors.length - 1))];
successful = false;
}
}
}
if (!npcCharacters.includes(character.characterId))
player.totalDamage += damage;
character.hp -= damage;
if (character.hp <= 0) {
// Exfiltration
// Drop Collar
var collar = getGadget(character, gadgetEnum.DIAMOND_COLLAR);
if (collar) {
map[character.coordinates.y][character.coordinates.x].gadget = collar;
removeGadget(character, gadgetEnum.DIAMOND_COLLAR);
}
var seats = [];
var freeSeats = [];
for (let row in map) {
for (let col in row) {
let field = map[row][col];
if ((field.state = fieldStateEnum.BAR_SEAT)) {
if (!getCharacterByPosition({ x: col, y: row }))
freeSeats.push({ x: col, y: row });
seats.push({ x: col, y: row });
}
}
}
var randomSeat;
if (freeSeats.length == 0) {
randomSeat = seats[Math.round(Math.random() * (seats.length - 1))];
getCharacterByPosition(randomSeat).coordinates = getRandomFreeNeighbor(
randomSeat
); // move character
} else {
randomSeat =
freeSeats[Math.round(Math.random() * (freeSeats.length - 1))];
}
latestOperations.push({
type: operationEnum.EXFILTRATION,
successful: true,
target: randomSeat,
characterId: character.characterId,
from: character.coordinates,
});
character.coordinates = randomSeat;
character.hp = 1;
}
return successful;
};
/**
* Adds intelligence points to a character.
* @param {Character} character The character to damage.
* @param {Integer} ip The amaount of intelligence points.
*/
giveCharIP = (character, ip) => {
character.ip += ip;
// if wiretap with earplugs is active
if (wireTapChar && earPlugsChar) {
if (wireTapChar == character.characterId) {
gameCharacters[earPlugsChar].ip += ip;
}
}
};
/**
* Returns specific gadget of a character.
* @param {Character} character The character.
* @param {gadgetEnum} gadget The gadget type.
* @return {Gadget} The gadget if. (Can be null if not found).
*/
getGadget = (char, type) => {
for (let gadget of char.gadgets) {
if (gadget.gadget == type) {
return gadget;
}
}
return null;
};
/**
* Removes a specific gadget from a character.
* @param {Character} character The character to damage.
* @param {gadgetEnum} type The gadget type.
*/
removeGadget = (char, type) => {
char.gadgets = char.gadgets.filter((gadget) => gadget.gadget !== type);
};
/**
* Removes a specific property from a character.
* @param {Character} character The character to damage.
* @param {propertyEnum} type The property type.
*/
removeProperty = (char, type) => {
char.properties = char.properties.filter((prop) => prop !== type);
};
/**
* Returns the distance of to points.
* @param {Point} pos1 The first point.
* @param {Point} pos2 The second point.
* @return {Integer} The distance of those two points.
*/
getDistance = (pos1, pos2) => {
console.log(
"distance ",
pos1,
pos2,
Math.max(Math.abs(pos2.x - pos1.x), Math.abs(pos2.y - pos1.y))
);
return Math.max(Math.abs(pos2.x - pos1.x), Math.abs(pos2.y - pos1.y));
};
/**
* Returns the nearest character to a specific point.
* @param {Point} coordinates The point.
* @param {Integer} maxDist The maximum distance.
* @return {Character} The nearest character.
*/
getNearestCharacter = (coordinates, maxDist) => {
var minDist = maxDist ? maxDist : gameCharacters[0].coordinates;
var minChar = null;
for (let char of Object.values(gameCharacters)) {
let dist = getDistance(coordinates, char.coordinates);
if (dist <= minDist) {
minDist = dist;
minChar = char;
}
}
return minChar;
};
/**
* Returns if one point is a neighbor of the other.
* @param {Point} position The position.
* @param {Point} target The target position.
* @param {Integer} [range=1] The range of neighborhood.
* @return {Boolean} If they are neighbors.
*/
isNeighbor = (position, target, range = 1) => {
return getDistance(position, target) == range;
};
/**
* Returns all neighbors for a specific point.
* @param {Point} coordinates The position.
* @param {Integer} [range=1] The range of neighborhood.
* @return {Point[]} Array of neighbors.
*/
getNeighbors = (coordinates, range = 1) => {
var neighbors = [];
for (let y = -range; y <= range; y++) {
for (let x = -range; x <= range; x++) {
let coords = { x: coordinates.x + x, y: coordinates.y + y };
if (
isCoordinatesValid(coords) &&
coords.x != coordinates.x &&
coords.y != coordinates.y
) {
neighbors.push(coords);
}
}
}
return neighbors;
};
/**
* Returns a random free neighbor for a specific point.
* @param {Point} coordinates The position.
* @param {Integer} [range=1] The range of neighborhood.
* @return {Point} Random free neighbor.
*/
getRandomFreeNeighbor = (coordinates, range) => {
var freeNeighbors = [];
var neighbors = getNeighbors(coordinates, range);
neighbors.forEach((coords) => {
let field = map[coords.y][coords.x];
if (field.state == fieldStateEnum.FREE && !getCharacterByPosition(coords))
freeNeighbors.push(coords);
});
if (freeNeighbors.length > 0) {
return freeNeighbors[
Math.round(Math.random() * (freeNeighbors.length - 1))
];
} else {
return getRandomFreeNeighbor(
neighbors[Math.round(Math.random() * (neighbors.length - 1))],
range
);
}
};
/**
* Returns a random neighbor character for a specific point.
* @param {Point} coordinates The position.
* @param {Integer} [range=1] The range of neighborhood.
* @param {Boolean} [sight=false] If character has to be in sight (important for range > 1).
* @return {Character} Random neighbor character.
*/
getRandomNeighborCharacter = (coordinates, range = 1, sight = false) => {
var freeNeighbors = [];
var neighbors = getNeighbors(coordinates, range);
neighbors.forEach((coords) => {
let field = map[coords.y][coords.x];
if (field.state == fieldStateEnum.FREE) {
let fieldChar = getCharacterByPosition(coords);
if (fieldChar) {
if (sight ? isInSight(coordinates, coords) : true) {
freeNeighbors.push(fieldChar);
}
}
}
});
if (freeNeighbors.length > 0) {
return freeNeighbors[
Math.round(Math.random() * (freeNeighbors.length - 1))
];
} else {
return getRandomNeighborCharacter(
neighbors[Math.round(Math.random() * (neighbors.length - 1))],
range,
sight
);
}
};
/**
* Returns a random character on the map.
* @return {Character} Random character.
*/
getRandomCharacter = () => {
var characters = [];
for (let row in map) {
for (let col in row) {
let fieldChar = getCharacterByPosition({ x: col, y: row });
if (fieldChar) characters.push(fieldChar);
}
}
if (characters.length > 0) {
return characters[Math.round(Math.random() * (characters.length - 1))];
} else {
return null;
}
};
/**
* Return if two points are in sight.
* @param {Point} position The position.
* @param {Point} target The target position.
* @param {Boolean} chars If character block the sight.
* @returns {Boolean} If in sight.
*/
isInSight = (position, target, chars = false) => {
const x0 = position.x,
y0 = position.y;
const x1 = target.x,
y1 = target.y;
var dx = Math.abs(x1 - x0); // https://stackoverflow.com/questions/4672279/bresenham-algorithm-in-javascript
var dy = Math.abs(y1 - y0);
var sx = x0 < x1 ? 1 : -1;
var sy = y0 < y1 ? 1 : -1;
var err = dx - dy;
while (true) {
let field = map[y0][x0];
if (
field.state == fieldStateEnum.WALL ||
field.state == fieldStateEnum.FIREPLACE ||
field.isFoggy ||
chars
? getCharacterByPosition({ x: x0, y: y0 })
: false
) {
return false;
}
if (x0 === x1 && y0 === y1) break;
let e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
return true;
};
/**
* If point is on the map.
* @param {Point} coordinates The coordinates.
* @returns {Boolean} If is on map.
*/
isCoordinatesValid = (coordinates) => {
const { x, y } = coordinates;
return y < map.length && y >= 0 && x < map[0].length && x >= 0;
};
/**
* Perfoms actions which a character can do with certain properties.
* @param {Player} player The player.
* @param {Operation} operation The operation.
* @returns {Boolean} If operation was valid.
*/
doOperation = (player, operation) => {
const {
type,
successful,
target,
characterId,
gadget,
stake,
usedProperty,
from,
} = operation;
if (argv.verbose)
console.info(characterId, " does ", operation, activeCharacterId);
if (characterId == activeCharacterId) {
const char = gameCharacters[characterId];
//validization
if (!char || (player && !player.characters.includes(characterId))) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Not a valid character",
});
return false;
}
if (!isCoordinatesValid(target)) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not on map",
});
return false;
}
const targetField = map[target.y][target.x];
const targetChar = getCharacterByPosition(targetField);
operation.successful = true;
/**
* Performs property action which a character can do.
* @param {Player} player The player.
* @param {Character} char The player's character.
* @param {Point} target The target.
* @param {fieldStateEnum} targetField The target field.
* @param {Character} targetChar The taget character (if on target field).
* @param {propertyEnum} usedProperty The used property.
* @param {operationEnum} operation The operation.
* @return {Boolean} If action was valid.
*/
doPropertyAction = (
player,
char,
target,
targetField,
targetChar,
usedProperty,
operation
) => {
switch (usedProperty) {
case propertyEnum.BANG_AND_BURN: {
if (
!validizeNeighbor(target) ||
!validizeFieldState(fieldStateEnum.ROULETTE_TABLE)
)
return false;
targetField.isDestroyed = true; //the roulette Table is not playable
targetField.isUpdated = true;
break;
}
case propertyEnum.OBSERVATION: {
if (!validizeSight(target, targetChar)) return false;
if (
getChance(char, matchconfig.observationSuccessChance) &&
!player.characters.includes(targetChar.characterId)
) {
if (
targetChar.gadgets.includes(gadgetEnum.POCKET_LITTER) ||
npcCharacters.includes(targetChar.characterId)
) {
operation.isEnemy = false;
} else {
operation.isEnemy = true;
}
}
break;
}
}
latestOperations.push(operation);
char.ap--; // Decrease action points
return true;
};
/**
* Perfoms spy on NPCs or on a safe.
* @param {Player} player The player.
* @param {Character} char The player's character.
* @param {Point} target The target
* @param {Character} targetChar The taget character (if on target field).
* @param {operationEnum} operation The operation.
* @return {Boolean} If action was valid.
*/
doSpyAction = (player, char, target, targetChar, operation) => {
if (!validizeTargetChar()) return false;
// Flaps and Seals
if (
char.properties.includes(propertyEnum.FLAPS_AND_SEALS)
? !validizeNeighbor(target, 2)
: !validizeNeighbor(target)
)
return false;
// Spy on character
if (targetChar) {
if (npcCharacters.includes(targetChar.characterId)) {
// if npc
if (getChance(char, matchconfig.spySuccessChance)) {
var secret =
safeCombinations[
Math.round(Math.random() * (safeCombinations.length - 1))
]; // get random safe combination
if (player.safeCombinations.includes(secret)) {
giveCharIP(char, matchconfig.secretToIpFactor); // add IPs if secret was unknown
}
player.safeCombinations.push(secret); // add secret
}
} else {
operation.isEnemy = true;
operation.successful = false;
}
} else {
// Spy on safe
var secret =
safeCombinations[
Math.round(Math.random() * (safeCombinations.length - 1))
]; // get random safe combination
if (player.safeCombinations.includes(secret)) {
giveCharIP(char, matchconfig.secretToIpFactor); // add IPs if secret was unknown
}
player.safeCombinations.push(secret); // add secret
if (
targetField.gadget &&
targetField.gadget.gadget == gadgetEnum.DIAMOND_COLLAR
) {
char.gadgets.push(targetField.gadget);
}
}
char.ap--;
latestOperations.push(operation);
return true;
};
/**
* Perform move to a specific character.
* @param {Player} player The player.
* @param {Point} target The target.
* @param {Character} char The player's character.
* @param {Character} targetChar The taget character. (if on target field)
* @param {operationEnum} operation The operation.
* @return {Boolean} If action was valid.
*/
doMovementAction = (player, target, char, targetChar, operation) => {
if (!validizeNeighbor(target)) return false;
if (argv.verbose) console.info(char, " moves to ", target);
// extra validization
if (
targetField.state != fieldStateEnum.FREE &&
targetField.state != fieldStateEnum.BAR_SEAT
) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not free or a bar seat",
});
return false;
}
// take gadget
if (targetField.gadget) {
char.gadgets.push(targetField.gadget);
}
// give collar to cat
if (target.x == catCoordinates.x && target.y == catCoordinates.y) {
giveCharIP(char, matchconfig.catIp);
catHandover = player;
endGame();
}
if (targetChar) targetChar.coordinates = char.coordinates; // switch places
char.coordinates = target; // move to target
char.mp--; // decrease move points
latestOperations.push(operation);
return true;
};
/**
* Perfroms roulette/gamble action.
* @param {Player} player The player.
* @param {Point} target The target.
* @param {Character} char The player's character.
* @param {operationEnum} stake The amount of stake.
* @param {fieldStateEnum} targetField The target field.
* @param {operationEnum} operation The operation.
* @return {Boolean} If action was valid.
*/
doGambleAction = (player, target, char, stake, targetField, operation) => {
if (
!validizeFieldState(fieldStateEnum.ROULETTE_TABLE) ||
!validizeNeighbor(target) ||
!validizeStake(stake)
)
return false;
var probability = 18 / 37;
if (targetField.chipAmount > 0) {
switch (char.properties) {
case propertyEnum.LUCKY_DEVIL: {
probability = 23 / 37;
break;
}
case propertyEnum.JINX: {
probability = 13 / 37;
break;
}
}
if (
char.properties.includes(propertyEnum.CLAMMY_CLOTHES) ||
char.properties.includes(propertyEnum.CONSTANT_CLAMMY_CLOTHES)
)
probability /= 2;
if (
targetField.isInverted
? Math.random() > probability
: Math.random() <= probability
) {
char.chips += stake;
targetField.chipAmount = 0;
targetField.isUpdated = true;
} else {
targetField.chipAmount += stake;
targetField.isUpdated = true;
char.chips -= stake;
}
} else {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Operation is not valid",
});
}
char.ap--; // Decrease action points
latestOperations.push(operation);
return true;
};
/**
* Performs gadget action for a specfic character.
* @param {Player} player The player.
* @param {Character} char The player's character.
* @param {Point} target The target.
* @param {Character} targetChar The taget character. (if on target field)
* @param {operationEnum} operation The operation.
* @param {fieldStateEnum} targetField The target field.
* @param {operationEnum} operation The operation.
* @return {Boolean} If action was valid.
*/
doGadgetAction = (
player,
gadget,
target,
char,
targetField,
targetChar,
operation
) => {
switch (gadget) {
case gadgetEnum.COCKTAIL: {
if (
target.x == char.coordinates.x &&
target.y == char.coordinates.y
) {
if (!validizeGadget(gadgetEnum.COCKTAIL) || !validizeTargetChar())
return false;
// drink
var cocktailHp = matchconfig.cocktailHp;
if (char.properties.includes(propertyEnum.ROBUST_STOMACH))
cocktailHp *= 2;
if (getGadget(char, gadgetEnum.COCKTAIL).isPoisoned) {
operation.successful = damageCharacter(char, cocktailHp, true);
} else {
char.hp += cocktailHp;
}
player.cocktrailDrinks++;
removeGadget(char, gadgetEnum.COCKTAIL); // remove gadget
} else {
if (!validizeNeighbor(target) || !validizeSight(target))
return false;
if (targetField.state == fieldStateEnum.BAR_TABLE) {
// take
char.gadgets.push({
gadget: gadgetEnum.COCKTAIL,
usages: 0,
});
targetField.gadget = null;
targetField.isUpdated = true;
} else {
// spill
if (!validizeGadget(gadgetEnum.COCKTAIL) || !validizeTargetChar())
return false;
if (
targetChar.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(character, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nTargetChar = getRandomNeighborCharacter(char.coordinates);
if (nTargetChar) targetChar = nTargetChar;
}
if (getChance(char, matchconfig.cocktailDodgeChance)) {
targetChar.properties.push(propertyEnum.CLAMMY_CLOTHES); // dodge
operation.successful = false;
player.cocktailSpills++;
}
removeGadget(char, gadgetEnum.COCKTAIL); // remove gadget
}
}
break;
}
case gadgetEnum.HAIRDRYER: {
if (!validizeGadget(gadgetEnum.HAIRDRYER)) return false;
if (targetField.state == fieldStateEnum.BAR_TABLE) {
if (!validizeNeighbor(target) || !validizeTargetChar())
return false;
removeProperty(targetChar, propertyEnum.CLAMMY_CLOTHES);
} else {
removeProperty(char, propertyEnum.CLAMMY_CLOTHES);
}
getGadget(char, gadgetEnum.HAIRDRYER).usages++;
break;
}
case gadgetEnum.MOLEDIE: {
if (
!validizeGadget(gadgetEnum.MOLEDIE) ||
!validizeDistance(target, matchconfig.moledieRange) ||
!validizeSight(target)
)
return false;
// get back deactivated properties
char.properties.push(...char.deactivatedProperties);
char.deactivatedProperties = [];
// bounce off
if (!targetChar) targetChar = getNearestCharacter(target);
// throw to other character and deactivate properties
targetChar.gadgets.push({
gadget: gadgetEnum.MOLEDIE,
usages: 0,
});
var newProps = [];
for (let prop of targetChar.properties) {
if (
prop == propertyEnum.TRADECRAFT ||
prop == propertyEnum.FLAPS_AND_SEALS ||
prop == propertyEnum.OBSERVATION
) {
targetChar.deactivatedProperties.push(prop);
} else {
newProps.push(prop);
}
}
targetChar.properties = newProps;
break;
}
case gadgetEnum.TECHNICOLOUR_PRISM: {
if (
!validizeGadget(gadgetEnum.TECHNICOLOUR_PRISM) ||
!validizeNeighbor(target) ||
!validizeFieldState(fieldStateEnum.ROULETTE_TABLE)
)
return false;
targetField.isInverted = true;
targetField.isUpdated = true;
removeGadget(char, gadgetEnum.TECHNICOLOUR_PRISM);
break;
}
case gadgetEnum.BOWLER_BLADE: {
if (
!validizeGadget(gadgetEnum.BOWLER_BLADE) ||
!validizeDistance(target, matchconfig.bowlerBladeRange) ||
!validizeSight(target, true)
)
return false;
if (
targetChar.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(character, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nTargetChar = getRandomNeighborCharacter(
char.coordinates,
matchconfig.bowlerBladeRange,
true
);
if (nTargetChar) targetChar = nTargetChar;
}
// Try to hit target character
if (!getGadget(targetChar, gadgetEnum.MAGNETIC_WATCH)) {
if (getChance(char, matchconfig.bowlerBladeHitChance)) {
damageCharacter(targetChar, matchconfig.bowlerBladeDamage);
} else {
operation.successful = false;
}
}
getRandomFreeNeighbor(targetChar.coordinates).gadget =
gadgetEnum.BOWLER_BLADE; // lay bowler blade on random free neighbor
break;
}
case gadgetEnum.POISON_PILLS: {
if (!validizeGadget(gadgetEnum.POISON_PILLS)) return false;
if (targetChar) {
if (!validizeNeighbor(target) || !validizeTargetChar(target))
return false;
// extra validization
var targetGadget = getGadget(targetChar, gadgetEnum.COCKTAIL);
if (!targetGadget) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target character has no cocktail",
});
return false;
}
targetGadget.isPoisoned = true; //Poison Cocktail
} else {
if (
!validizeNeighbor(target) ||
!validizeFieldState(fieldStateEnum.BAR_TABLE) ||
!validizeFieldGadget(gadgetEnum.COCKTAIL)
)
return false;
targetField.gadget.isPoisoned = true; //Poison Cocktail
targetField.isUpdated = true;
}
var charGadget = getGadget(char, gadgetEnum.POISON_PILLS);
charGadget.usages++;
//check if all Pills are used
if (charGadget.usages == 5) {
removeGadget(char, gadgetEnum.POISON_PILLS); // remove gadget
}
break;
}
case gadgetEnum.LASER_COMPACT: {
if (
!validizeGadget(gadgetEnum.LASER_COMPACT) ||
!validizeSight(target)
)
return false;
// extra validization
if (targetField.state != fieldStateEnum.BAR_TABLE && !targetChar) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is neither bar table nor a character",
});
return false;
}
if (targetChar) {
if (!getGadget(targetChar)) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target character does not have a cocktail",
});
return false;
}
if (getChance(char, matchconfig.laserCompactHitChance)) {
removeGadget(targetChar, gadgetEnum.COCKTAIL);
} else {
operation.successful = false;
}
} else {
if (targetField.gadget.gadget != gadgetEnum.COCKTAIL) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Bar table does not have a cocktail",
});
return false;
}
if (getChance(char, matchconfig.laserCompactHitChance)) {
targetField.gadget = null;
targetField.isUpdated = true;
} else {
operation.successful = false;
}
}
break;
}
case gadgetEnum.ROCKET_PEN: {
if (!validizeGadget(gadgetEnum.ROCKET_PEN) || !validizeSight(target))
return false;
if (targetField.state == fieldStateEnum.WALL) {
targetField.state = fieldStateEnum.FREE; // destroy target wall
targetField.isUpdated = true;
}
if (targetChar) {
if (
targetChar.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(character, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nTargetChar = getRandomCharacter(true);
if (nTargetChar) targetChar = nTargetChar;
}
damageCharacter(targetChar, matchconfig.rocketPenDamage); // deal damage to taget character
}
getNeighbors(target).forEach((coords) => {
let field = map[coords.y][coords.x];
if (field.state == fieldStateEnum.WALL) {
//destroy neighbor walls
field.state = fieldStateEnum.FREE;
}
let fieldChar = getCharacterByPosition(coords);
if (fieldChar) {
if (
fieldChar.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(fieldChar, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nFieldChar = getRandomNeighborCharacter(coords);
if (nFieldChar) fieldChar = nFieldChar;
}
damageCharacter(fieldChar, matchconfig.rocketPenDamage); // deal damage to character
}
});
removeGadget(char, gadgetEnum.ROCKET_PEN); // remove gadget
break;
}
case gadgetEnum.GAS_GLOSS: {
if (
!validizeGadget(gadgetEnum.GAS_GLOSS) ||
!validizeNeighbor(target) ||
!validizeTargetChar()
)
return false;
if (
targetChar.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(character, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nTargetChar = getRandomNeighborCharacter(char.coordinates);
if (nTargetChar) targetChar = nTargetChar;
}
damageCharacter(targetChar, matchconfig.gasGlossDamage); //deal damage to target character
removeGadget(char, gadgetEnum.GAS_GLOSS); // remove gadget
return true;
}
case gadgetEnum.MOTHBALL_POUCH: {
if (
!validizeGadget(gadgetEnum.MOTHBALL_POUCH) ||
!validizeDistance(target, matchconfig.mothballPouchRange) ||
!validizeSight(target) ||
!validizeFieldState(fieldStateEnum.FIREPLACE)
)
return false;
getNeighbors(target).forEach((coords) => {
let neighbor = getCharacterByPosition(coords);
if (neighbor) {
if (
neighbor.properties.includes(propertyEnum.HONEY_TRAP) &&
getChance(neighbor, matchconfig.honeyTrapSuccessChance)
) {
// honey trap
var nNeighbor = getRandomNeighborCharacter(coords);
if (nNeighbor) neighbor = nNeighbor;
}
damageCharacter(neighbor, matchconfig.mothballPouchDamage);
}
});
var charGadget = getGadget(char, gadgetEnum.MOTHBALL_POUCH);
charGadget.usages++;
//check if all moth balls are used
if (charGadget.usages == 5) {
removeGadget(char, gadgetEnum.MOTHBALL_POUCH); // remove gadget
}
break;
}
case gadgetEnum.FOG_TIN: {
if (
!validizeGadget(gadgetEnum.FOG_TIN) ||
!validizeSight(target) ||
!validizeDistance(target, matchconfig.fogTinRange)
)
return false;
// extra validization
if (targetField.state == fieldStateEnum.WALL) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target can not be a " + fieldState,
});
return false;
}
targetField.isFoggy = true;
targetField.isUpdated = true;
getNeighbors(target).forEach((field) => {
field.isFoggy = true;
field.isUpdated = true;
});
fogTinIsActive = true;
fogTin = target;
break;
}
case gadgetEnum.GRAPPLE: {
if (
!validizeGadget(gadgetEnum.GRAPPLE) ||
!validizeDistance(target, matchconfig.grappleRange) ||
!validizeSight(target)
)
return false;
// extra validization
if (
targetField.state != fieldStateEnum.FREE ||
targetField.state != fieldStateEnum.BAR_TABLE
) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not free or a bar table",
});
return false;
}
if (getChance(char, matchconfig.grappleHitChance)) {
char.gadgets.push(targetField.gadget); // add gadget to character
targetField.gadget = null; // remove gadget from field
targetField.isUpdated = true;
} else {
operation.successful = false;
}
getGadget(char, gadgetEnum.MOTHBALL_POUCH).usages++;
break;
}
case gadgetEnum.JETPACK: {
if (
!validizeGadget(gadgetEnum.JETPACK) ||
!validizeFieldState(fieldStateEnum.FREE) ||
validizeTargetChar(target) // character on field
)
return false;
char.coordinates = target; // move to target
removeGadget(char, gadgetEnum.JETPACK);
break;
}
case gadgetEnum.WIRETAP_WITH_EARPLUGS: {
if (!validizeTargetChar() || !validizeNeighbor(target)) return false;
getGadget(char).activeOn = targetChar;
wireTapChar = targetChar.characterId;
earPlugsChar = char.characterId;
break;
}
case gadgetEnum.CHICKEN_FEED: {
if (
!validizeTargetChar() ||
!validizeGadget(gadgetEnum.CHICKEN_FEED) ||
!validizeNeighbor(target)
)
return false;
if (!player.characters.includes(targetChar.characterId)) {
if (char.ip > targetChar.ip) {
giveCharIP(char, char.ip - targetChar.ip);
} else if (char.ip < targetChar.ip) {
giveCharIP(targetChar, targetChar.ip - char.ip);
}
} else {
operation.successful = false;
}
removeGadget(char, gadgetEnum.CHICKEN_FEED);
break;
}
case gadgetEnum.NUGGET: {
if (
!validizeGadget(gadgetEnum.NUGGET) ||
!validizeTargetChar() ||
!validizeNeighbor(target)
)
return false;
if (npcCharacters.includes(targetChar.characterId)) {
// if npc add to players characters
player.characters.push(targetChar.characterId);
player.equipment[targetChar.characterId] = targetChar;
removeGadget(gadgetEnum.NUGGET);
} else {
// else give nugget to target character
targetChar.gadgets.push(getGadget(char, gadgetEnum.NUGGET));
}
break;
}
case gadgetEnum.MIRROR_OF_WILDERNESS: {
if (!validizeTargetChar() || !validizeNeighbor(target)) return false;
if (player.characters.includes(targetChar.characterId)) {
// exchange ips
var tempIp = char.ip;
char.ip = targetChar.ip;
targetChar.ip = tempIp;
} else {
// opponent
if (getChance(char, matchconfig.mirrorSwapChance)) {
// exchange ips
var tempIp = char.ip;
char.ip = targetChar.ip;
targetChar.ip = tempIp;
removeGadget(char, gadgetEnum.MIRROR_OF_WILDERNESS); // remove gadget from character
} else {
operation.successful = false;
}
}
break;
}
default: {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Incorrect operation",
});
return false;
}
}
latestOperations.push(operation);
char.ap--; // Decrease action points
return true;
};
// Validization
/**
* Validizies targetChar
* @return If valid.
*/
validizeTargetChar = () => {
if (!targetChar) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target has no character",
});
return false;
}
return true;
};
/**
* Validizies gadget
* @param {gadgetEnum} gadget The gadget.
* @return If valid.
*/
validizeGadget = (gadget) => {
if (!getGadget(char, gadget)) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Character does not own gadget " + gadget,
});
return false;
}
return true;
};
/**
* Validizes distance between current position and target.
* @param {Point} target The target.
* @param {number} maxDist The maximum distance.
* @return If valid.
*/
validizeDistance = (target, maxDist) => {
if (getDistance(char.coordinates, target) > maxDist) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target out of range",
});
return false;
}
return true;
};
/**
* Validizes sight between current position and target.
* @param {Point} target The target.
* @param {Boolean} chars If characters block the sight.
* @return If valid.
*/
validizeSight = (target, chars) => {
if (!isInSight(char.coordinates, target, chars)) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not in line of sight",
});
return false;
}
return true;
};
/**
* Validizes if field is in specific state.
* @param {fieldStateEnum} fieldState The field state.
* @return If valid.
*/
validizeFieldState = (fieldState) => {
if (targetField.state != fieldState) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not a " + fieldState,
});
return false;
}
return true;
};
/**
* Validizes if gadget is of specific type.
* @param {gadgetEnum} gadget The gadget.
* @return Is valid.
*/
validizeFieldGadget = (gadget) => {
if (targetField.gadget.gadget != gadget) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Field has no " + gadget,
});
return false;
}
};
/**
* Validizes if target is neighbor of current position.
* @param {Point} target The target.
* @param {Integer} [range=1] The range.
* @return Is valid.
*/
validizeNeighbor = (target, range = 1) => {
if (!isNeighbor(char.coordinates, target, range)) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Target is not a neighbor",
});
return false;
}
return true;
};
/**
* Validize stake.
* @param {Integer} stake The stake.
* @return Is valid.
*/
validizeStake = (stake) => {
if (stake > targetField.chipAmount) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Stake is bigger than chip amount",
});
return false;
}
return true;
};
// Performs operation
switch (type) {
case operationEnum.GADGET_ACTION: {
if (char.ap == 0) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No action points left",
});
return false;
}
return doGadgetAction(
player,
gadget,
target,
char,
targetField,
targetChar,
operation
);
}
case operationEnum.MOVEMENT: {
if (char.mp == 0) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No move points left",
});
return false;
}
return doMovementAction(player, target, char, targetChar, operation);
}
case operationEnum.GAMBLE_ACTION: {
if (char.ap == 0) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No action points left",
});
return false;
}
return doGambleAction(
player,
target,
char,
stake,
targetField,
operation
);
}
case operationEnum.PROPERTY_ACTION: {
if (char.ap == 0) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No action points left",
});
return false;
}
return doPropertyAction(
player,
char,
target,
targetField,
targetChar,
usedProperty,
operation
);
}
case operationEnum.SPY_ACTION: {
if (char.ap == 0) {
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "No action points left",
});
return false;
}
return doSpyAction(player, target, char, targetChar, operation);
}
}
} else {
// close connection if not valid
player.sendMessage(messageTypeEnum.ILLEGAL_MESSAGE, {
reason: errorEnum.ILLEGAL_MESSAGE,
debugMessage: "Operation is not valid",
});
player.disqualifyPlayer();
}
};