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

538 lines
18 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 { getEquipmentStatistics, isSpying, isAttacking, logActivity } = require('./utilities.js');
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();
logActivity(connection, req.body.id);
});
});
});
});
});
};
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) => {
//verify the user's 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 log credentials', req.body.id, req.body.token));
res.end();
return;
}
//grab the spying log and equipment stolen based on the id
let query = 'SELECT pastSpying.id AS id, pastSpying.eventTime AS eventTime, pastSpying.attackerId AS attackerId, pastSpying.defenderId AS defenderId, atk.username AS attackerUsername, def.username AS defenderUsername, pastSpying.attackingUnits AS attackingUnits, pastSpying.success AS success, pastSpying.spoilsGold AS spoilsGold, equipmentStolen.name AS equipmentStolenName, equipmentStolen.type AS equipmentStolenType, equipmentStolen.quantity AS equipmentStolenQuantity FROM pastSpying LEFT JOIN equipmentStolen ON pastSpying.id = equipmentStolen.pastSpyingId LEFT JOIN accounts AS atk ON pastSpying.attackerId = atk.id LEFT JOIN accounts AS def ON pastSpying.defenderId = def.id WHERE pastSpying.attackerId = ? OR pastSpying.defenderId = ? ORDER BY eventTime DESC LIMIT ?, ?;';
connection.query(query, [req.body.id, req.body.id, req.body.start, req.body.length], (err, results) => {
if (err) throw err;
//build the sendable data structure (delete names from successful events when you're the losing defender, etc.)
let ret = [];
results.forEach((result) => {
//appending equipment stolen
if (ret[result.id]) {
ret[result.id].equipmentStolen.push({
name: result.equipmentStolenName,
type: result.equipmentStolenType,
quantity: result.equipmentStolenQuantity
});
return;
}
let hideData = req.body.id === result.defenderId && (result.success === 'success' || result.success === 'ineffective');
//creating a new entry
ret[result.id] = {
eventTime: result.eventTime,
attacker: hideData ? null : result.attackerUsername,
defender: result.defenderUsername,
attackingUnits: hideData ? null : result.attackingUnits,
success: result.success,
spoilsGold: result.spoilsGold,
equipmentStolen: result.equipmentStolenName ? [{
name: result.equipmentStolenName,
type: result.equipmentStolenType,
quantity: result.equipmentStolenQuantity
}] : []
};
});
//remove null fields
ret = ret.filter(x => x);
//send the build structure
res.status(200).json(ret);
res.end();
log('Spy log sent', JSON.stringify(ret));
});
});
};
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 (failure)
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.2) : 0; //50% chance of stealing gold
let query = 'INSERT INTO pastSpying (eventTime, attackerId, defenderId, attackingUnits, success, spoilsGold) VALUES (?, ?, ?, ?, ?, ?);';
connection.query(query, [pendingSpying.eventTime, pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits, spoilsGold ? 'success' : 'ineffective', 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;
getEquipmentStatistics((err, { statistics }) => {
if (err) throw err;
//don't steal certain items
results = results.filter(item => statistics[item.type][item.name].stealable);
//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) => {
getEquipmentStatistics((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);
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);
if (items.length) {
updateSuccessStatus(connection, 'success', pastSpyingId); //QOL improvement
}
};
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));
});
});
};
const updateSuccessStatus = (connection, status, pastSpyingId) => {
let query = 'UPDATE pastSpying SET success = ? WHERE id = ?;';
connection.query(query, [status, pastSpyingId], (err) => {
if (err) throw err;
log('Success status updated', pastSpyingId, status);
});
}
module.exports = {
spyRequest: spyRequest,
spyStatusRequest: spyStatusRequest,
spyLogRequest: spyLogRequest,
runSpyTick: runSpyTick
};
//TODO: move balance variables to an external file (.env?)