This repository has been archived on 2026-04-30. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kingdombattles/server/spying.js
T

461 lines
15 KiB
JavaScript

//environment variables
require('dotenv').config();
//libraries
let CronJob = require('cron').CronJob;
//utilities
let { logDiagnostics } = require('./diagnostics.js');
let { log } = require('../common/utilities.js');
let { getStatistics, isSpying, isAttacking } = require('./utilities.js'); //TODO: rename getStatistics to getEquipmentStatistics
const spyRequest = (connection) => (req, res) => {
//verify the attacker's credentials (only the attacker can launch an attack)
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;';
connection.query(query, [req.body.id, req.body.attacker, req.body.token], (err, results) => {
if (err) throw err;
if (results[0].total !== 1) {
res.status(400).write(log('Invalid spying credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
//verify that the defender's profile exists
let query = 'SELECT accountId FROM profiles WHERE accountId IN (SELECT id FROM accounts WHERE username = ?);';
connection.query(query, [req.body.defender], (err, results) => {
if (err) throw err;
if (results.length !== 1) {
res.status(400).write(log('Invalid defender spying credentials', req.body.id, req.body.attacker, req.body.defender, req.body.token));
res.end();
return;
}
let defenderId = results[0].accountId;
//verify that the attacker has enough spies
let query = 'SELECT spies FROM profiles WHERE accountId = ?;';
connection.query(query, [req.body.id], (err, results) => {
if (err) throw err;
if (results[0].spies <= 0) {
res.status(400).write(log('Not enough spies', req.body.attacker, req.body.defender, results[0].spies));
res.end();
return;
}
let attackingUnits = results[0].spies;
//verify that the attacker is not already spying on someone
isSpying(connection, req.body.attacker, (err, spying) => {
if (err) throw err;
if (spying) {
res.status(400).write(log('You are already spying on someone', req.body.id, req.body.attacker, req.body.token));
res.end();
return;
}
//create the pending spy record
let query = 'INSERT INTO pendingSpying (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 10 * ? MINUTE), ?, ?, ?);';
connection.query(query, [attackingUnits, req.body.id, defenderId, attackingUnits], (err) => {
if (err) throw err;
res.status(200).json({
status: 'spying',
attacker: req.body.attacker,
defender: req.body.defender,
msg: log('Spying', req.body.attacker, req.body.defender) //TODO: am I using this msg parameter anywhere?
});
res.end();
});
});
});
});
});
};
const spyStatusRequest = (connection) => (req, res) => {
//verify the credentials
let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
connection.query(query, [req.body.id, req.body.token], (err, results) => {
if (err) throw err;
if (results[0].total !== 1) {
res.status(400).write(log('Invalid spy status request credentials', req.body.id, req.body.token));
res.end();
return;
}
isSpying(connection, req.body.id, (err, spying, defender) => {
if (err) throw err;
res.status(200).json({
status: spying ? 'spying' : 'idle',
defender: defender
});
res.end();
});
});
};
const spyLogRequest = (connection) => (req, res) => {
//TODO
res.status(400).write(log('Not yet implemented', 'spyLogRequest'));
res.end();
};
const runSpyTick = (connection) => {
//find each pending spy event
let spyTick = new CronJob('* * * * * *', () => {
let query = 'SELECT * FROM pendingSpying WHERE eventTime < CURRENT_TIMESTAMP();';
connection.query(query, (err, results) => {
if (err) throw err;
results.forEach((pendingSpying) => {
//check that the attacker still has enough spies
let query = 'SELECT spies FROM profiles WHERE accountId = ?;';
connection.query(query, [pendingSpying.attackerId], (err, results) => {
if (err) throw err;
if (results[0].spies < pendingSpying.attackingUnits) {
//delete the failed spying
let query = 'DELETE FROM pendingSpying WHERE id = ?;';
connection.query(query, [pendingSpying.id], (err) => {
if (err) throw err;
log('Not enough spies for spying', pendingSpying.attackerId, results[0].spies, pendingSpying.attackingUnits);
});
return;
}
//spy gameplay logic
spyGameplayLogic(connection, pendingSpying);
});
});
});
});
spyTick.start();
};
const spyGameplayLogic = (connection, pendingSpying) => {
//determine how many pairs of defender eyes are available to spot the spies
isAttacking(connection, pendingSpying.defenderId, (err, defenderIsAttacking) => {
if (err) throw err;
isSpying(connection, pendingSpying.defenderId, (err, defenderIsSpying) => {
if (err) throw err;
let query = 'SELECT * FROM profiles WHERE accountId = ?;';
connection.query(query, [pendingSpying.defenderId], (err, results) => {
if (err) throw err;
let totalEyes = results[0].recruits + results[0].soldiers * !defenderIsAttacking + results[0].spies * !defenderIsSpying + results[0].scientists;
//more spies reduces the chances of being seen? Counter intuitive
let chanceSeen = totalEyes / (pendingSpying.attackingUnits * 10); //it takes 10 eyes to guarantee the capture of 1 spy, 50% chance to capture 2 spies, etc.
//if seen
if (Math.random() <= chanceSeen) {
let query = 'INSERT INTO pastSpying (eventTime, attackerId, defenderId, attackingUnits, success, spoilsGold) VALUES (?, ?, ?, ?, "failure", 0);';
connection.query(query, [pendingSpying.eventTime, pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits], (err) => {
if (err) throw err;
//spies die on failure
let query = 'UPDATE profiles SET spies = spies - ? WHERE accountId = ?;';
connection.query(query, [pendingSpying.attackingUnits, pendingSpying.attackerId], (err) => {
if (err) throw err;
//delete from pending
let query = 'DELETE FROM pendingSpying WHERE id = ?;'
connection.query(query, [pendingSpying.id], (err) => {
if (err) throw err;
log('Spy failed', pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits, totalEyes);
});
});
});
} else {
//steal this much gold on success
let spoilsGold = Math.random() >= 0.5 ? Math.floor(results[0].gold * 0.1) : 0; //50% chance of stealing gold
let query = 'INSERT INTO pastSpying (eventTime, attackerId, defenderId, attackingUnits, success, spoilsGold) VALUES (?, ?, ?, ?, "success", ?);';
connection.query(query, [pendingSpying.eventTime, pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits, spoilsGold], (err) => {
if (err) throw err;
let query = 'UPDATE profiles SET gold = gold + ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, pendingSpying.attackerId], (err) => {
if (err) throw err;
let query = 'UPDATE profiles SET gold = gold - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, pendingSpying.defenderId], (err) => {
if (err) throw err;
//delete from pending
let query = 'DELETE FROM pendingSpying WHERE id = ?;'
connection.query(query, [pendingSpying.id], (err) => {
if (err) throw err;
log('Spy succeeded', pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits, totalEyes, spoilsGold);
spyStealEquipment(connection, pendingSpying, spoilsGold);
});
});
});
});
}
});
});;
});
};
const spyStealEquipment = (connection, pendingSpying, spoilsGold) => {
let query = 'SELECT id FROM pastSpying WHERE eventTime = ? AND attackerId = ? AND defenderId = ? AND spoilsGold = ?;'; //make it VERY hard to grab the wrong one
connection.query(query, [pendingSpying.eventTime, pendingSpying.attackerId, pendingSpying.defenderId, spoilsGold], (err, results) => {
if (err) throw err;
let successfulSpies = 0;
for (let i = 0; i < pendingSpying.attackingUnits; i++) {
//50% chance of stealing equipment
if (Math.random() >= 0.5) {
successfulSpies += 1;
}
}
spyStealEquipmentInner(connection, pendingSpying.attackerId, pendingSpying.defenderId, successfulSpies, results[0].id);
});
};
const spyStealEquipmentInner = (connection, attackerId, defenderId, attackingUnits, pastSpyingId) => {
//NOTE: steal equipment that isn't being carried by soldiers
isAttacking(connection, defenderId, (err, attacking) => {
let query = 'SELECT * FROM equipment WHERE accountId = ?;';
connection.query(query, [defenderId], (err, results) => {
if (err) throw err;
//if he's not attacking, skip to the next step
if (!attacking) {
return spyStealEquipmentSelectItemsToSteal(connection, attackerId, defenderId, attackingUnits, results, pastSpyingId);
}
//count the number of weapons/consumable items to be skipped, from strongest to weakest
let query = 'SELECT soldiers FROM profiles WHERE accountId = ?;';
connection.query(query, [defenderId], (err, results) => {
if (err) throw err;
let soldierCount = results[0].soldiers;
//armour
let query = 'SELECT * FROM equipment WHERE accountId = ? AND type = "Armour";';
connection.query(query, [defenderId], (err, armourResults) => {
if (err) throw err;
//NOTE: Armour stays at home - it's never carried by soldiers (don't call removeForEachSoldier)
//weapons
let query = 'SELECT * FROM equipment WHERE accountId = ? AND type = "Weapon";';
connection.query(query, [defenderId], (err, results) => {
if (err) throw err;
removeForEachSoldier(results, soldierCount, (err, weaponResults) => {
if (err) throw err;
//consumables
let query = 'SELECT * FROM equipment WHERE accountId = ? AND type = "Consumable";';
connection.query(query, [defenderId], (err, results) => {
if (err) throw err;
removeForEachSoldier(results, soldierCount, (err, consumableResults) => {
if (err) throw err;
//splice the two arrays back together
let results = weaponResults.concat(consumableResults, armourResults);
spyStealEquipmentSelectItemsToSteal(connection, attackerId, defenderId, attackingUnits, results, pastSpyingId);
});
});
});
});
});
});
});
});
};
const removeForEachSoldier = (results, soldiers, cb) => {
getStatistics((err, { statistics }) => {
if (err) throw err;
results.sort((a, b) => statistics[a.type][a.name].combatBoost < statistics[b.type][b.name].combatBoost);
results = results.map((item) => {
//count downwards
if (item.quantity > soldiers) {
item.quantity -= soldiers;
soldiers = 0;
} else {
soldiers -= item.quantity;
item.quantity = 0;
}
return item;
});
results = results.filter(item => item.quantity > 0 && statistics[item.type][item.name].stealable);
cb(undefined, results);
});
}
const spyStealEquipmentSelectItemsToSteal = (connection, attackerId, defenderId, attackingUnits, results, pastSpyingId) => {
//count the total items
let totalItems = 0;
results.forEach((item) => totalItems += item.quantity);
let items = [];
for (let i = 0; i < attackingUnits; i++) {
//select the specific item to steal
let selection = Math.floor(Math.random() * totalItems);
totalItems -= 1;
//find the exact item that will be stolen (records[0])
let records = results.filter((item) => {
selection -= item.quantity;
return selection < 0;
});
//move to items (quantity = 1)
if (records.length > 0) {
items.unshift({
id: records[0].id,
name: records[0].name,
type: records[0].type,
quantity: 1
});
}
//remove it from results (decrement and/or delete)
for (let i = 0; i < results.length; i++) {
if (results[i].id === items[0].id) {
results[i].quantity -= 1;
if (results[i].quantity <= 0) {
results.splice(i, 1);
}
break;
}
}
//skip the rest
if (results.length <= 0) {
break;
}
}
//collapse the {quantity:1} into {quantity:n}
let collapsedItems = [];
items.forEach((item) => {
if (!collapsedItems[item.id]) {
collapsedItems[item.id] = { ...item };
} else {
collapsedItems[item.id].quantity += item.quantity;
}
});
items = []; //clear
collapsedItems.forEach((record) => {
items.push(record);
});
//next steps
spyStealEquipmentIncrementItemsToInventory(connection, attackerId, items);
spyStealEquipmentDecrementItemsFromInventory(connection, defenderId, items);
recordEquipmentStolen(connection, items, pastSpyingId);
};
const spyStealEquipmentIncrementItemsToInventory = (connection, accountId, items) => {
//add the items to the players's inventory
items.forEach((item) => {
let query = 'SELECT * FROM equipment WHERE accountId = ? AND name = ? AND type = ?;';
connection.query(query, [accountId, item.name, item.type], (err, results) => {
if (err) throw err;
let query;
//if the player has this item, or not
if (results.length > 0) {
query = 'UPDATE equipment SET quantity = quantity + ? WHERE accountId = ? AND name = ? AND type = ?;';
} else {
query = 'INSERT INTO equipment (quantity, accountId, name, type) VALUES (?, ?, ?, ?);';
}
connection.query(query, [item.quantity, accountId, item.name, item.type], (err) => {
if (err) throw err;
});
});
});
//error checking
items.forEach((item) => {
let query = 'SELECT * FROM equipment WHERE accountId = ? AND name = ? AND type = ?;';
connection.query(query, [accountId, item.name, item.type], (err, results) => {
if (err) throw err;
if (results.length > 1) {
log('WARNING: Duplicate items detected', JSON.stringify(results));
}
})
});
};
const spyStealEquipmentDecrementItemsFromInventory = (connection, accountId, items) => {
//remove these items from the player's inventory
items.forEach((item) => {
let query = 'UPDATE equipment SET quantity = quantity - ? WHERE accountId = ? AND id = ?;';
connection.query(query, [item.quantity, accountId, item.id], (err) => {
if (err) throw err;
});
});
//check to see if any quantities are negative
let query = 'SELECT * FROM equipment WHERE quantity < 0;';
connection.query(query, (err, results) => {
if (err) throw err;
if (results.length !== 0) {
log('WARNING: equipment quantity below zero', JSON.stringify(results));
}
});
//clean the database from quantities of 0
query = 'DELETE FROM equipment WHERE accountId = ? AND quantity = 0;';
connection.query(query, [accountId], (err) => {
if (err) throw err;
log('Cleaned database', 'equipment decrement');
});
};
const recordEquipmentStolen = (connection, items, pastSpyingId) => {
//record in the database
let query = 'INSERT INTO equipmentStolen (pastSpyingId, name, type, quantity) VALUES (?, ?, ?, ?);';
items.forEach((item) => {
connection.query(query, [pastSpyingId, item.name, item.type, item.quantity], (err) => {
if (err) throw err;
log('Items stolen', pastSpyingId, JSON.stringify(item));
});
});
};
module.exports = {
spyRequest: spyRequest,
spyStatusRequest: spyStatusRequest,
spyLogRequest: spyLogRequest,
runSpyTick: runSpyTick
};