//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 };