diff --git a/common/utilities.js b/common/utilities.js index ce0aa97..c5d1582 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -13,7 +13,9 @@ let excluded = [ //messages that should not be logged 'Not enough time has passed', 'Profile sent', - 'Ladder sent' + 'Ladder sent', +// 'attacking', +// 'idle' ]; const log = (msg, ...args) => { diff --git a/public/news/2019-05-29-01.md b/public/news/2019-05-29-01.md index c9168d2..5c88ad6 100644 --- a/public/news/2019-05-29-01.md +++ b/public/news/2019-05-29-01.md @@ -14,7 +14,13 @@ The plan is this: * 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. +* 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). All of these numbers can be adjusted later, but this is the initial gameplan for combat. +Edit: More aspects that I'd like to ensure are: + +* If the server resets (which happens alot) combat still progresses as expected. +* All combat is logged and presented to the player. +* You can only attack one person at a time. + diff --git a/server/combat.js b/server/combat.js index f85a0c2..b646c6d 100644 --- a/server/combat.js +++ b/server/combat.js @@ -5,10 +5,99 @@ require('dotenv').config(); let { log } = require('../common/utilities.js'); const attackRequest = (connection) => (req, res) => { - res.status(400).write(log('Not yet implemented')); - res.end(); + //verify the attacker's credentials + let query = 'SELECT accountId FROM sessions WHERE accountId IN (SELECT id FROM accounts WHERE username = ?) AND token = ?;'; + connection.query(query, [req.body.attacker, req.body.token], (err, results) => { + if (err) throw err; + + if (results.length !== 1) { + res.status(400).write(log('Invalid attack credentials', req.body.attacker, req.body.defender, req.body.token)); + res.end(); + return; + } + + let attackerId = results[0].accountId; + + //verify that the defender exists + let query = '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 credentials', req.body.attacker, req.body.defender)); + res.end(); + return; + } + + let defenderId = results[0].id; + + //verify that the attacker has enough soldiers + let query = 'SELECT soldiers FROM profiles WHERE accountId = ?;'; + connection.query(query, [attackerId], (err, results) => { + if (err) throw err; + + if (results[0].soldiers <= 0) { + res.status(400).write(log('Not enough soldiers', req.body.attacker, req.body.defender, results[0].soldiers)); + res.end(); + return; + } + + let attackingUnits = results[0].soldiers; + + //verify that the attacker is not already attacking someone + isAttacking(connection, req.body.attacker, (isAttacking) => { + if (isAttacking) { + res.status(400).write(log('You are already attacking someone', req.body.attacker, req.body.defender)); + res.end(); + return; + } + + //create the pending attack value + let query = 'INSERT INTO pendingCombat (eventTime, attackerId, defenderId, attackingUnits) VALUES (DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 10 * ? 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.end(); + }); + }); + }); + }); + }); +} + +const attackStatusRequest = (connection) => (req, res) => { + isAttacking(connection, req.body.username, (isAttacking) => { + res.status(200).write(log(isAttacking ? 'attacking' : 'idle', req.body.username)); + 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) => { + if (err) throw err; + + return cb(results.length !== 0); + }); } module.exports = { - attackRequest: attackRequest -} \ No newline at end of file + attackRequest: attackRequest, + attackStatusRequest: attackStatusRequest +} + +/* +> 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). +> 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. +* All combat is logged and presented to the player. +*/ + diff --git a/server/index.js b/server/index.js index 42363de..5f4b011 100644 --- a/server/index.js +++ b/server/index.js @@ -42,6 +42,7 @@ profiles.runGoldTick(connection); let combat = require('./combat.js'); app.post('/attackrequest', combat.attackRequest(connection)); +app.post('/attackstatusrequest', combat.attackStatusRequest(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 7901d96..789c713 100644 --- a/sql/create_database_structure.sql +++ b/sql/create_database_structure.sql @@ -54,4 +54,42 @@ CREATE TABLE IF NOT EXISTS profiles ( lastRecruitTime TIMESTAMP DEFAULT '2019-01-01 00:00:00', CONSTRAINT FOREIGN KEY fk_accountId(accountId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +#combat system +CREATE TABLE IF NOT EXISTS pendingCombat ( + id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE, + td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + + eventTime TIMESTAMP, + + attackerId INTEGER UNSIGNED, + defenderId INTEGER UNSIGNED, + attackingUnits INTEGER UNSIGNED, + + 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 +); + +CREATE TABLE IF NOT EXISTS pastCombat ( + id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE, + td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + + eventTime TIMESTAMP, + + attackerId INTEGER UNSIGNED, + defenderId INTEGER UNSIGNED, + attackingUnits INTEGER UNSIGNED, + defindingUnits INTEGER UNSIGNED, + + undefended BOOLEAN, + + victor ENUM ('attacker', 'defender'), + + spoilsGold INTEGER, + + casualtiesVictor INTEGER, + + 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 ); \ No newline at end of file diff --git a/src/components/pages/profile.jsx b/src/components/pages/profile.jsx index 89a20ed..ed1c3b9 100644 --- a/src/components/pages/profile.jsx +++ b/src/components/pages/profile.jsx @@ -218,7 +218,7 @@ class Profile extends React.Component {

Recruits:

{this.state.recruits}

- +
diff --git a/src/components/panels/attack_button.jsx b/src/components/panels/attack_button.jsx index 049b5df..5fbff95 100644 --- a/src/components/panels/attack_button.jsx +++ b/src/components/panels/attack_button.jsx @@ -8,14 +8,22 @@ class AttackButton extends React.Component { constructor(props) { super(props); this.state = { - // + message: '' }; + + this.sendAttackingStatusRequest(); } render() { - return ( - - ); + if (this.state.message !== '') { + return ( +

{this.state.message}

+ ); + } else { + return ( + + ); + } } sendAttackRequest() { @@ -26,7 +34,7 @@ class AttackButton extends React.Component { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status === 200) { - //DO NOTHING + this.setState({message: xhr.responseText}); } else if (xhr.status === 400) { if (this.props.setWarning) { this.props.setWarning(xhr.responseText); @@ -38,7 +46,8 @@ class AttackButton extends React.Component { xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); xhr.send(JSON.stringify({ attacker: this.props.attacker, - defender: this.props.defender + defender: this.props.defender, + token: this.props.token })); if (this.props.onClick) { @@ -47,6 +56,26 @@ class AttackButton extends React.Component { this.props.setDisabled(true); } + + sendAttackingStatusRequest() { + let xhr = new XMLHttpRequest(); + xhr.open('POST', '/attackstatusrequest', true); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + if (xhr.responseText === 'attacking') { + this.props.setDisabled(true); + } + } + } + } + + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(JSON.stringify({ + username: this.props.attacker + })); + } }; AttackButton.propTypes = { @@ -56,6 +85,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 diff --git a/src/reducers/combat.js b/src/reducers/combat.js index a4397c4..e7fa7f2 100644 --- a/src/reducers/combat.js +++ b/src/reducers/combat.js @@ -1,7 +1,7 @@ import { SET_ATTACK_DISABLED } from '../actions/combat.js'; const initialStore = { - attackDisabled: true + attackDisabled: false }; export function combatReducer(store = initialStore, action) {