Implemented Capture The Flag

This commit is contained in:
2019-06-16 03:14:12 +10:00
parent 3917aca276
commit c85f9a55a1
10 changed files with 111 additions and 41 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
Priority List Priority List
--- ---
* Capture The Flag / King Of The Hill implemented. * ~~Capture The Flag / King Of The Hill implemented.~~
* ~~Privacy policy / no children under 13.~~ * ~~Privacy policy / no children under 13.~~
* ~~Unsubscribe from promotional emails.~~ * ~~Unsubscribe from promotional emails.~~
* Delete own account (right to be forgotten). * Delete own account (right to be forgotten).
@@ -76,7 +76,7 @@ Badge Ideas
--- ---
* ~~alpha tester~~ * ~~alpha tester~~
* capture the flag * ~~capture the flag~~
* ~~king of the hill~~ * ~~king of the hill~~
* gold horde * gold horde
* ~~Combat Master~~ * ~~Combat Master~~
+5
View File
@@ -5,3 +5,8 @@ _16 June 2019_
Things I've completed so far: Things I've completed so far:
* King Of The Hill badge is now enabled (requires 24 hours at the top of the ladder). * King Of The Hill badge is now enabled (requires 24 hours at the top of the ladder).
* Capture the flag is implemented; user [Ratstail91](/profile?username=Ratstail91) will have it to begin with (that's me!).
Both of these badges are considered experimental, just for the record. I think I managed it, but don't be surprised if I have to roll the database back two days...
P.S. My code is so messy. So, so messy...
+1 -1
View File
@@ -15,7 +15,7 @@
"filename": "capture_the_flag.png", "filename": "capture_the_flag.png",
"description": "This badge is stolen by successful attacks. It is automatically set as your active badge, and you can't change it until it is lost; it also sets your name to yellow, so everyone knows you have it. Only one exists.", "description": "This badge is stolen by successful attacks. It is automatically set as your active badge, and you can't change it until it is lost; it also sets your name to yellow, so everyone knows you have it. Only one exists.",
"visible": true, "visible": true,
"unlockable": null "unlockable": false
}, },
"Combat Master": { "Combat Master": {
"filename": "combat_master.png", "filename": "combat_master.png",
+38
View File
@@ -70,6 +70,16 @@ const selectActiveBadge = (connection) => (req, res) => {
return; return;
} }
//if Capture The Flag is active, don't change the active badge; return badges owned
if (owned["Capture The Flag"]) {
getBadgesOwned(connection, req.body.id, (err, results) => {
if (err) throw err;
res.status(200).json(results);
res.end();
});
return;
}
//zero out the user's selection //zero out the user's selection
let query = 'UPDATE badges SET active = FALSE WHERE accountId = ?;'; let query = 'UPDATE badges SET active = FALSE WHERE accountId = ?;';
connection.query(query, [req.body.id], (err) => { connection.query(query, [req.body.id], (err) => {
@@ -108,6 +118,33 @@ const rewardBadge = (connection, id, badgeName, cb) => {
}); });
}; };
const captureTheFlag = (connection, attackerId, defenderId, skip, cb) => {
//if this is a no-op
if (skip) {
return cb(false);
}
//check to see if the flag belongs to the defender
let query = 'SELECT * FROM badges WHERE accountId = ? AND name = "Capture The Flag" LIMIT 1;';
connection.query(query, [defenderId], (err, results) => {
if (err) throw err;
//does the defender have this badge? If not, return
if (results.length === 0) {
return cb(false);
}
//move the badge between accounts
let query = 'INSERT INTO badges (id, accountId, name, active) VALUES (?, ?, "Capture The Flag", FALSE) ON DUPLICATE KEY UPDATE accountId = VALUES(accountId), active = FALSE;';
connection.query(query, [results[0].id, attackerId], (err) => {
if (err) throw err;
log('Badge moved', attackerId, defenderId);
cb(true);
});
});
};
const runBadgeTicks = (connection) => { const runBadgeTicks = (connection) => {
//Combat Master //Combat Master
let combatMasterBadgeTickJob = new CronJob('0 * * * * *', () => { //once a minute - combats aren't that fast let combatMasterBadgeTickJob = new CronJob('0 * * * * *', () => { //once a minute - combats aren't that fast
@@ -187,5 +224,6 @@ module.exports = {
ownedRequest: ownedRequest, ownedRequest: ownedRequest,
selectActiveBadge: selectActiveBadge, selectActiveBadge: selectActiveBadge,
rewardBadge: rewardBadge, rewardBadge: rewardBadge,
captureTheFlag: captureTheFlag,
runBadgeTicks: runBadgeTicks runBadgeTicks: runBadgeTicks
}; };
+28 -24
View File
@@ -9,6 +9,7 @@ let { logDiagnostics } = require('./diagnostics.js');
let { log } = require('../common/utilities.js'); let { log } = require('../common/utilities.js');
let { getEquipmentStatistics, isAttacking, logActivity } = require('./utilities.js'); let { getEquipmentStatistics, isAttacking, logActivity } = require('./utilities.js');
let { captureTheFlag } = require('./badges.js');
const attackRequest = (connection) => (req, res) => { const attackRequest = (connection) => (req, res) => {
//verify the attacker's credentials (only the attacker can launch an attack) //verify the attacker's credentials (only the attacker can launch an attack)
@@ -234,39 +235,42 @@ const runCombatTick = (connection) => {
let spoilsGold = Math.floor(results[0].gold * (victor === 'attacker' ? 0.1 : 0.02)); let spoilsGold = Math.floor(results[0].gold * (victor === 'attacker' ? 0.1 : 0.02));
let attackerCasualties = Math.floor((pendingCombat.attackingUnits >= 10 ? pendingCombat.attackingUnits : 0) * (victor === 'attacker' ? Math.random() / 5 : Math.random() / 2)); let attackerCasualties = Math.floor((pendingCombat.attackingUnits >= 10 ? pendingCombat.attackingUnits : 0) * (victor === 'attacker' ? Math.random() / 5 : Math.random() / 2));
//save the combat //capture the flag logic
let query = 'INSERT INTO pastCombat (eventTime, attackerId, defenderId, attackingUnits, defendingUnits, undefended, victor, spoilsGold, attackerCasualties) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);'; captureTheFlag(connection, pendingCombat.attackerId, pendingCombat.defenderId, victor !== 'attacker', (flagCaptured) => {
connection.query(query, [pendingCombat.eventTime, pendingCombat.attackerId, pendingCombat.defenderId, pendingCombat.attackingUnits, defendingUnits, undefended, victor, spoilsGold, attackerCasualties], (err) => { //save the combat
if (err) throw err; let query = 'INSERT INTO pastCombat (eventTime, attackerId, defenderId, attackingUnits, defendingUnits, undefended, victor, spoilsGold, attackerCasualties, flagCaptured) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);';
connection.query(query, [pendingCombat.eventTime, pendingCombat.attackerId, pendingCombat.defenderId, pendingCombat.attackingUnits, defendingUnits, undefended, victor, spoilsGold, attackerCasualties, flagCaptured], (err) => {
//update the attacker profile
let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, attackerCasualties, pendingCombat.attackerId], (err) => {
if (err) throw err; if (err) throw err;
//update the defender profile //update the attacker profile
let query = 'UPDATE profiles SET gold = gold - ? WHERE accountId = ?;'; let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE accountId = ?;';
connection.query(query, [spoilsGold, pendingCombat.defenderId], (err) => { connection.query(query, [spoilsGold, attackerCasualties, pendingCombat.attackerId], (err) => {
if (err) throw err; if (err) throw err;
//remove used consumables (moved because callback hell is rediculous) //update the defender profile
removeConsumables(connection, attackerConsumables, pendingCombat.attackingUnits); let query = 'UPDATE profiles SET gold = gold - ? WHERE accountId = ?;';
removeConsumables(connection, defenderConsumables, defendingUnits); connection.query(query, [spoilsGold, pendingCombat.defenderId], (err) => {
//delete the pending combat
let query = 'DELETE FROM pendingCombat WHERE id = ?;';
connection.query(query, [pendingCombat.id], (err) => {
if (err) throw err; if (err) throw err;
log('Combat executed', pendingCombat.attackerId, pendingCombat.defenderId, victor, spoilsGold); //remove used consumables (moved because callback hell is rediculous)
logDiagnostics(connection, 'death', attackerCasualties); removeConsumables(connection, attackerConsumables, pendingCombat.attackingUnits);
removeConsumables(connection, defenderConsumables, defendingUnits);
//clean the database //delete the pending combat
let query = 'DELETE FROM equipment WHERE quantity <= 0;'; let query = 'DELETE FROM pendingCombat WHERE id = ?;';
connection.query(query, (err) => { connection.query(query, [pendingCombat.id], (err) => {
if (err) throw err; if (err) throw err;
log('Cleaned database', 'Combat consumables'); log('Combat executed', pendingCombat.attackerId, pendingCombat.defenderId, victor, spoilsGold);
logDiagnostics(connection, 'death', attackerCasualties);
//clean the database
let query = 'DELETE FROM equipment WHERE quantity <= 0;';
connection.query(query, (err) => {
if (err) throw err;
log('Cleaned database', 'Combat consumables');
});
}); });
}); });
}); });
+6
View File
@@ -46,6 +46,12 @@ const getBadgesOwned = (connection, id, cb) => {
ret[results[key].name] = { active: results[key].active }; ret[results[key].name] = { active: results[key].active };
}); });
//NOTE: check for "Capture The Flag" badge, force it to be the active badge
if ("Capture The Flag" in ret) {
Object.keys(ret).map((key) => ret[key].active = false);
ret["Capture The Flag"].active = true;
}
return cb(undefined, { 'owned': ret }); return cb(undefined, { 'owned': ret });
}); });
} }
+2
View File
@@ -118,6 +118,8 @@ CREATE TABLE IF NOT EXISTS pastCombat (
attackerCasualties INTEGER, attackerCasualties INTEGER,
flagCaptured BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT FOREIGN KEY fk_attackerId(attackerId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT FOREIGN KEY fk_attackerId(attackerId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT FOREIGN KEY fk_defenderId(defenderId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE CONSTRAINT FOREIGN KEY fk_defenderId(defenderId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE
); );
+15 -12
View File
@@ -1,16 +1,19 @@
ALTER TABLE
signups
ADD COLUMN
promotions BOOLEAN DEFAULT FALSE
AFTER
hash
;
ALTER TABLE ALTER TABLE
accounts pastCombat
ADD COLUMN ADD
promotions BOOLEAN DEFAULT FALSE flagCaptured BOOLEAN NOT NULL DEFAULT FALSE
AFTER
hash
; ;
#initialize the server's flag
INSERT INTO badges (accountId, name) VALUES (1, "Capture The Flag"); #my account ID
#move the badge between accounts
#INSERT INTO badges
# (id, accountId)
#VALUES
# (?, ?)
#ON DUPLICATE KEY UPDATE
# accountId = VALUES(accountId)
#;
+7 -1
View File
@@ -21,10 +21,16 @@ class BadgeText extends React.Component {
let style = this.props.centered ? centerStyle : leftStyle; let style = this.props.centered ? centerStyle : leftStyle;
//Capture The Flag forces your name to be yellow
let colorOverride = {};
if (this.props.name === 'Capture The Flag') {
colorOverride.color = 'yellow';
}
return ( return (
<div className={this.props.className} style={{...style, paddingBottom: '0.5em'}}> <div className={this.props.className} style={{...style, paddingBottom: '0.5em'}}>
<Badge name={this.props.name} filename={this.props.filename} size={this.props.size} /> <Badge name={this.props.name} filename={this.props.filename} size={this.props.size} />
<p style={{paddingBottom: 0, ...this.props.style}}>{this.props.children}</p> <p className='truncate' style={{paddingBottom: 0, ...this.props.style, ...colorOverride}}>{this.props.children}</p>
</div> </div>
); );
} }
+7 -1
View File
@@ -2,6 +2,8 @@ import React from 'react';
import { withRouter, Link } from 'react-router-dom'; import { withRouter, Link } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import BadgeText from './badge_text.jsx';
class CombatLogRecord extends React.Component { class CombatLogRecord extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -12,6 +14,10 @@ class CombatLogRecord extends React.Component {
} }
render() { render() {
//NOTE: the badgeText was never meant to be used this way
let badgeFilename = this.props.flagCaptured ? 'capture_the_flag.png' : undefined;
let badgeName = this.props.flagCaptured ? 'Capture The Flag' : undefined;
return ( return (
<div className='panel table noCollapse'> <div className='panel table noCollapse'>
<hr /> <hr />
@@ -23,7 +29,7 @@ class CombatLogRecord extends React.Component {
</div> </div>
<div className='row'> <div className='row'>
<p className='col truncate'><span className='mobile hide'>Victor: </span>{this.capitalizeFirstLetter(this.props.victor)} {this.props.undefended ? '(undefended)' : ''}</p> <BadgeText name={badgeName} filename={badgeFilename} size='small' className='col truncate'><span className='mobile hide'>Victor: </span>{this.capitalizeFirstLetter(this.props.victor)} {this.props.undefended ? '(undefended)' : ''}</BadgeText>
<p className='col truncate'>Gold: {this.props.spoilsGold}</p> <p className='col truncate'>Gold: {this.props.spoilsGold}</p>
<p className='col truncate'>Atk. Deaths: {this.props.attackerCasualties}</p> <p className='col truncate'>Atk. Deaths: {this.props.attackerCasualties}</p>
</div> </div>