From c88162ef03cd8f7548240e3aa4aa78725a497fbd Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Wed, 5 Jun 2019 14:38:35 +1000 Subject: [PATCH] Finished spying system --- common/utilities.js | 1 + server/spying.js | 64 ++++++++- src/components/app.jsx | 3 +- src/components/pages/spying_log.jsx | 148 ++++++++++++++++++++ src/components/panels/common_links.jsx | 2 + src/components/panels/paged_spying_log.jsx | 90 ++++++++++++ src/components/panels/spying_log_record.jsx | 78 +++++++++++ 7 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 src/components/pages/spying_log.jsx create mode 100644 src/components/panels/paged_spying_log.jsx create mode 100644 src/components/panels/spying_log_record.jsx diff --git a/common/utilities.js b/common/utilities.js index 7a008d1..2a34149 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -18,6 +18,7 @@ let excluded = [ //messages that should not be logged 'idle', 'Combat log sent', + 'Spy log sent', 'News sent', 'News sent (singular)', diff --git a/server/spying.js b/server/spying.js index 01757f9..b2a0cae 100644 --- a/server/spying.js +++ b/server/spying.js @@ -103,9 +103,64 @@ const spyStatusRequest = (connection) => (req, res) => { }; const spyLogRequest = (connection) => (req, res) => { - //TODO - res.status(400).write(log('Not yet implemented', 'spyLogRequest')); - res.end(); + //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'; + + //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) => { @@ -157,7 +212,7 @@ const spyGameplayLogic = (connection, pendingSpying) => { //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 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) => { @@ -458,3 +513,4 @@ module.exports = { runSpyTick: runSpyTick }; +//TODO: move balance variables to an external file (.env?) \ No newline at end of file diff --git a/src/components/app.jsx b/src/components/app.jsx index 947b026..0027965 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -67,9 +67,10 @@ export default class App extends React.Component { import('./pages/password_reset.jsx')} /> import('./pages/profile.jsx')} /> + import('./pages/equipment.jsx')} /> import('./pages/ladder.jsx')} /> import('./pages/combat_log.jsx')} /> - import('./pages/equipment.jsx')} /> + import('./pages/spying_log.jsx')} /> import('./pages/task_list.jsx')} /> import('./pages/patron_list.jsx')} /> diff --git a/src/components/pages/spying_log.jsx b/src/components/pages/spying_log.jsx new file mode 100644 index 0000000..5e16aba --- /dev/null +++ b/src/components/pages/spying_log.jsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import queryString from 'query-string'; +import PropTypes from 'prop-types'; + +//panels +import CommonLinks from '../panels/common_links.jsx'; +import PagedSpyingLog from '../panels/paged_spying_log.jsx'; + +class SpyingLog extends React.Component { + constructor(props) { + super(props); + + let params = queryString.parse(props.location.search); + + this.state = { + params: params, + start: parseInt(params.log) || 0, + length: parseInt(params.length) || 20, + + fetch: null, + + warning: '' + }; + } + + componentDidMount() { + if (!this.props.loggedIn) { + this.props.history.replace('/login'); + } + } + + componentDidUpdate(prevProps, prevState, snapshot) { + if (JSON.stringify(this.state) !== JSON.stringify(prevState)) { + this.state.fetch(); + } + } + + render() { + let warningStyle = { + display: this.state.warning.length > 0 ? 'flex' : 'none' + }; + + let ButtonHeader = this.buttonHeader.bind(this); + + return ( +
+
+ +
+ +
+
+

{this.state.warning}

+
+ +

Espionage Log

+ + + + +
+
+ + ); + } + + buttonHeader() { + return ( +
+
+ +
+
+ +
+
+ ); + } + + increment() { + let start = this.state.start + this.state.length; + + this.props.history.push(`${this.props.location.pathname}?log=${start}`); + } + + decrement() { + let start = Math.max(0, this.state.start - this.state.length); + + //don't decrement too far + if (start === this.state.start) { + return; + } + + this.props.history.push(`${this.props.location.pathname}?log=${start}`); + } + + //bound callbacks + getFetch(fn) { + this.setState({ fetch: fn }); + } + + onReceived(data) { + if (data.length === 0) { + let start = Math.max(0, this.state.start - this.state.length); + + //don't decrement too far + if (start === this.state.start) { + return; + } + + this.props.history.replace(`${this.props.location.pathname}?log=${start}`); + } + } + + setWarning(s) { + this.setState({ warning: s }); + } +}; + +SpyingLog.propTypes = { + username: PropTypes.string.isRequired, + loggedIn: PropTypes.bool.isRequired +}; + +const mapStoreToProps = (store) => { + return { + username: store.account.username, + loggedIn: store.account.id !== 0 + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + // + }; +}; + +SpyingLog = connect(mapStoreToProps, mapDispatchToProps)(SpyingLog); + +export default SpyingLog; \ No newline at end of file diff --git a/src/components/panels/common_links.jsx b/src/components/panels/common_links.jsx index 114f8e5..f868306 100644 --- a/src/components/panels/common_links.jsx +++ b/src/components/panels/common_links.jsx @@ -32,6 +32,7 @@ class CommonLinks extends React.Component {

Your Equipment

Attack (Game Ladder)

Combat Log

+

Espionage Log

Change Password

Task List

Patron List

@@ -71,6 +72,7 @@ CommonLinks.propTypes = { onClickEquipment: PropTypes.func, onClickLadder: PropTypes.func, onClickCombatLog: PropTypes.func, + onClickSpyingLog: PropTypes.func, onClickTaskList: PropTypes.func, onClickPatronList: PropTypes.func, onClickRules: PropTypes.func diff --git a/src/components/panels/paged_spying_log.jsx b/src/components/panels/paged_spying_log.jsx new file mode 100644 index 0000000..15d9608 --- /dev/null +++ b/src/components/panels/paged_spying_log.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { withRouter, Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import SpyingLogRecord from './spying_log_record.jsx'; + +class PagedSpyingLog extends React.Component { + constructor(props) { + super(props); + + this.state = { + // + }; + + if (props.getFetch) { + props.getFetch(() => this.sendRequest('/spylogrequest', {start: props.start, length: props.length})); + } + } + + render() { + return ( +
+ {Object.keys(this.state).map((key) => )} +
+ ); + } + + //gameplay functions + sendRequest(url, args = {}) { //send a unified request, using my credentials + //build the XHR + let xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let json = JSON.parse(xhr.responseText); + + //on success + this.setState(json); + + if (this.props.onReceived) { + this.props.onReceived(json); + } + } + else if (xhr.status === 400 && this.props.setWarning) { + this.props.setWarning(xhr.responseText); + } + } + }; + + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(JSON.stringify({ + id: this.props.id, + token: this.props.token, + ...args + })); + } +}; + +PagedSpyingLog.propTypes = { + id: PropTypes.number.isRequired, + token: PropTypes.number.isRequired, + + username: PropTypes.string.isRequired, + start: PropTypes.number.isRequired, + length: PropTypes.number.isRequired, + + setWarning: PropTypes.func, + getFetch: PropTypes.func, + onReceived: PropTypes.func +}; + +const mapStoreToProps = (store) => { + return { + id: store.account.id, + token: store.account.token + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + // + }; +}; + +PagedSpyingLog = connect(mapStoreToProps, mapDispatchToProps)(PagedSpyingLog); + +export default withRouter(PagedSpyingLog); \ No newline at end of file diff --git a/src/components/panels/spying_log_record.jsx b/src/components/panels/spying_log_record.jsx new file mode 100644 index 0000000..434c375 --- /dev/null +++ b/src/components/panels/spying_log_record.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { withRouter, Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +class SpyingLogRecord extends React.Component { + constructor(props) { + super(props); + + this.state = { + // + }; + } + + render() { + return ( +
+
+
+
+

{this.parseDate(this.props.eventTime)}

+

Atk: {this.prettyName(this.props.attacker)} ({this.props.attackingUnits ? this.props.attackingUnits : '???'} units)

+

Def: {this.prettyName(this.props.defender)}

+
+ +
+

Result: {this.capitalizeFirstLetter(this.props.success)}

+

Gold Stolen: {this.props.spoilsGold}

+
+
+ + {Object.keys(this.props.equipmentStolen).map((key) => { + return ( +
+

Stolen: {this.props.equipmentStolen[key].name}

+

{this.props.equipmentStolen[key].type}

+

Total: {this.props.equipmentStolen[key].quantity}

+
+ ); + })} +
+ ); + } + + prettyName(name) { + //make the enemy name a link + if (name === this.props.username) { + return name; + } else if (name) { + return ({name}); + } else { + return (???); + } + } + + parseDate(eventTime) { + let month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + let date = new Date(eventTime); + return `${date.getDate()} ${month[date.getMonth()]}`; + } + + capitalizeFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } +}; + +SpyingLogRecord.propTypes = { + username: PropTypes.string.isRequired, + eventTime: PropTypes.string.isRequired, + attacker: PropTypes.string, + defender: PropTypes.string.isRequired, + attackingUnits: PropTypes.number, + success: PropTypes.string.isRequired, + spoilsGold: PropTypes.number.isRequired, + equipmentStolen: PropTypes.array.isRequired +}; + +export default withRouter(SpyingLogRecord); \ No newline at end of file