From bbf0f170d7296d496561d28cbc00b220ee082bd2 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Wed, 29 May 2019 06:55:23 +1000 Subject: [PATCH] Combat is working --- common/utilities.js | 4 +- server/combat.js | 128 ++++++++++++++++++++---- server/index.js | 1 + sql/create_database_structure.sql | 2 +- src/components/panels/attack_button.jsx | 36 ++----- 5 files changed, 125 insertions(+), 46 deletions(-) diff --git a/common/utilities.js b/common/utilities.js index c5d1582..788431a 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -14,8 +14,8 @@ let excluded = [ //messages that should not be logged 'Profile sent', 'Ladder sent', -// 'attacking', -// 'idle' + 'attacking', + 'idle' ]; const log = (msg, ...args) => { diff --git a/server/combat.js b/server/combat.js index b646c6d..8d51099 100644 --- a/server/combat.js +++ b/server/combat.js @@ -1,6 +1,9 @@ //environment variables require('dotenv').config(); +//libraries +let CronJob = require('cron').CronJob; + //utilities let { log } = require('../common/utilities.js'); @@ -53,12 +56,17 @@ const attackRequest = (connection) => (req, res) => { } //create the pending attack value - let query = 'INSERT INTO pendingCombat (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 10 * ? SECOND), ?, ?, ?);'; + let query = 'INSERT INTO pendingCombat (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 60 * ? SECOND), ?, ?, ?);'; connection.query(query, [attackingUnits, attackerId, defenderId, attackingUnits], (err) => { if (err) throw err; - res.status(200).write(log(`Your soldiers are on their way to attack ${req.body.defender}`, req.body.attacker, req.body.defender)); + res.status(200).json({ + status: 'attacking', + defender: req.body.defender + }); res.end(); + + log(`attacking ${req.body.defender}`, req.body.attacker, req.body.defender) }); }); }); @@ -67,37 +75,123 @@ const attackRequest = (connection) => (req, res) => { } const attackStatusRequest = (connection) => (req, res) => { - isAttacking(connection, req.body.username, (isAttacking) => { - res.status(200).write(log(isAttacking ? 'attacking' : 'idle', req.body.username)); + isAttacking(connection, req.body.username, (isAttacking, defender) => { + res.status(200).json({ + status: log(isAttacking ? 'attacking' : 'idle', req.body.username, defender), + defender: defender + }); + res.end(); }); } -const isAttacking = (connection, username, cb) => { - let query = 'SELECT * FROM pendingCombat WHERE attackerId IN (SELECT id FROM accounts WHERE username = ?);'; - connection.query(query, [username], (err, results) => { +const runCombatTick = (connection) => { + let combatTick = new CronJob('* * * * * *', () => { + //find each pending combat + let query = 'SELECT * FROM pendingCombat WHERE eventTime < CURRENT_TIMESTAMP();'; + connection.query(query, (err, results) => { + if (err) throw err; + + results.forEach((pendingCombat) => { + //get the defender's undefended status + isAttacking(connection, pendingCombat.defenderId, (undefended) => { + //get the defending unit count, gold + let query = 'SELECT soldiers, recruits, gold FROM profiles WHERE accountId = ?;'; + + connection.query(query, [pendingCombat.defenderId], (err, results) => { + if (err) throw err; + + let defendingUnits; + if (!undefended && results[0].soldiers > 0) { + defendingUnits = results[0].soldiers; + } else { + defendingUnits = results[0].recruits; + } + + //determine the victor + let rand = Math.random() * (pendingCombat.attackingUnits + defendingUnits * (undefended ? 0.25 : 1)); + let victor = rand <= pendingCombat.attackingUnits ? 'attacker' : 'defender'; + + //determine the spoils and casualties + let spoilsGold = Math.floor(results[0].gold * (victor === 'attacker' ? 0.1 : 0.02)); + let casualtiesVictor = Math.floor((pendingCombat.attackingUnits >= 10 ? pendingCombat.attackingUnits - 10 : 0) * (victor === 'attacker' ? 0.05 : 0.1)); + + //save the combat + let query = 'INSERT INTO pastCombat (eventTime, attackerId, defenderId, attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);'; + connection.query(query, [pendingCombat.eventTime, pendingCombat.attackerId, pendingCombat.defenderId, pendingCombat.attackingUnits, defendingUnits, undefended, victor, spoilsGold, casualtiesVictor], (err) => { + if (err) throw err; + + //update the attacker profile + let query = 'UPDATE profiles SET gold = gold + ?, soldiers = soldiers - ? WHERE id = ?;'; + connection.query(query, [spoilsGold, casualtiesVictor, pendingCombat.attackerId], (err) => { + if (err) throw err; + + //update the defender profile + let query = 'UPDATE profiles SET gold = gold - ? WHERE id = ?;'; + connection.query(query, [spoilsGold, pendingCombat.defenderId], (err) => { + if (err) throw err; + + //delete the pending combat + let query = 'DELETE FROM pendingCombat WHERE id = ?;'; + connection.query(query, [pendingCombat.id], (err) => { + if (err) throw err; + + log('Combat executed', pendingCombat.attackerId, pendingCombat.defenderId, victor); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + combatTick.start(); +} + +const isAttacking = (connection, user, cb) => { + let query; + + if (typeof(user) === 'string') { + query = 'SELECT * FROM pendingCombat WHERE attackerId IN (SELECT id FROM accounts WHERE username = ?);'; + } else if (typeof(user) === 'number') { + query = 'SELECT * FROM pendingCombat WHERE attackerId = ?;'; + } + + connection.query(query, [user], (err, results) => { if (err) throw err; - return cb(results.length !== 0); + if (results.length === 0) { + cb(false); + } else { + //get the username of the person being attacked + let query = 'SELECT username FROM accounts WHERE id = ?;'; + connection.query(query, [results[0].defenderId], (err, results) => { + if (err) throw err; + cb(true, results[0].username); + }); + } }); } module.exports = { attackRequest: attackRequest, - attackStatusRequest: attackStatusRequest + attackStatusRequest: attackStatusRequest, + runCombatTick: runCombatTick } /* > You can attack another player using your soldiers (it doesn't work without soldiers). -* Doing so takes time, up to 10 seconds for every soldier you have. -* Combat takes place at the end of the time delay, at which point you can attack people again (after reloading the page). +> Doing so takes time, up to 10 seconds for every soldier you have. +> Combat takes place at the end of the time delay, at which point you can attack people again (after reloading the page). > While attacking, you are undefended. -* While undefended, your recruits act as combatants, otherwise your soldiers do. -* The chance of success is determined by the ratio of each side's combatant strength. -* Recruits have a strength equal to 0.25 times that of a soldier. -* On a success, you steal 10% of the target's gold. On a failure, you steal 2% of the target's gold. -* The attacking force will lose a percentage, rounded down, of their units - 5% on a success, 10% on a failure (edit: excluding the first 10 units). -* If the server resets (which happens alot) combat still progresses as expected. +> While undefended, your recruits act as combatants, otherwise your soldiers do. +> The chance of success is determined by the ratio of each side's combatant strength. +> Recruits have a strength equal to 0.25 times that of a soldier. +> On a success, you steal 10% of the target's gold. On a failure, you steal 2% of the target's gold. +> The attacking force will lose a percentage, rounded down, of their units - 5% on a success, 10% on a failure (edit: excluding the first 10 units). +> If the server resets (which happens alot) combat still progresses as expected. * All combat is logged and presented to the player. */ diff --git a/server/index.js b/server/index.js index 5f4b011..fc8b5b3 100644 --- a/server/index.js +++ b/server/index.js @@ -43,6 +43,7 @@ profiles.runGoldTick(connection); let combat = require('./combat.js'); app.post('/attackrequest', combat.attackRequest(connection)); app.post('/attackstatusrequest', combat.attackStatusRequest(connection)); +combat.runCombatTick(connection); //static directories app.use('/styles', express.static(path.resolve(__dirname + '/../public/styles')) ); diff --git a/sql/create_database_structure.sql b/sql/create_database_structure.sql index 789c713..d6aa9b5 100644 --- a/sql/create_database_structure.sql +++ b/sql/create_database_structure.sql @@ -80,7 +80,7 @@ CREATE TABLE IF NOT EXISTS pastCombat ( attackerId INTEGER UNSIGNED, defenderId INTEGER UNSIGNED, attackingUnits INTEGER UNSIGNED, - defindingUnits INTEGER UNSIGNED, + defendingUnits INTEGER UNSIGNED, undefended BOOLEAN, diff --git a/src/components/panels/attack_button.jsx b/src/components/panels/attack_button.jsx index 5fbff95..72adfe8 100644 --- a/src/components/panels/attack_button.jsx +++ b/src/components/panels/attack_button.jsx @@ -1,8 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; - -import { setAttackDisabled } from '../../actions/combat.js'; class AttackButton extends React.Component { constructor(props) { @@ -17,11 +14,11 @@ class AttackButton extends React.Component { render() { if (this.state.message !== '') { return ( -

{this.state.message}

+

{this.state.message}

); } else { return ( - + ); } } @@ -34,7 +31,10 @@ class AttackButton extends React.Component { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { - this.setState({message: xhr.responseText}); + let json = JSON.parse(xhr.responseText); + if (json.status === 'attacking') { + this.setState({ message: `Your soldiers are attacking ${json.defender}` }); + } } else if (xhr.status === 400) { if (this.props.setWarning) { this.props.setWarning(xhr.responseText); @@ -64,8 +64,9 @@ class AttackButton extends React.Component { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { - if (xhr.responseText === 'attacking') { - this.props.setDisabled(true); + let json = JSON.parse(xhr.responseText); + if (json.status === 'attacking') { + this.setState({ message: `Your soldiers are attacking ${json.defender}` }); } } } @@ -85,24 +86,7 @@ AttackButton.propTypes = { setWarning: PropTypes.func, attacker: PropTypes.string.isRequired, defender: PropTypes.string.isRequired, - token: PropTypes.number.isRequired, - - disabled: PropTypes.bool.isRequired, - setDisabled: PropTypes.func.isRequired + token: PropTypes.number.isRequired }; -function mapStoreToProps(store) { - return { - disabled: store.combat.attackDisabled - } -} - -function mapDispatchToProps(dispatch) { - return { - setDisabled: (disabled) => dispatch(setAttackDisabled(disabled)) - } -} - -AttackButton = connect(mapStoreToProps, mapDispatchToProps)(AttackButton); - export default AttackButton; \ No newline at end of file