Finished spying system

This commit is contained in:
2019-06-05 14:38:35 +10:00
parent e1c5d20289
commit c88162ef03
7 changed files with 381 additions and 5 deletions
+1
View File
@@ -18,6 +18,7 @@ let excluded = [ //messages that should not be logged
'idle', 'idle',
'Combat log sent', 'Combat log sent',
'Spy log sent',
'News sent', 'News sent',
'News sent (singular)', 'News sent (singular)',
+60 -4
View File
@@ -103,9 +103,64 @@ const spyStatusRequest = (connection) => (req, res) => {
}; };
const spyLogRequest = (connection) => (req, res) => { const spyLogRequest = (connection) => (req, res) => {
//TODO //verify the user's credentials
res.status(400).write(log('Not yet implemented', 'spyLogRequest')); let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;';
res.end(); 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) => { const runSpyTick = (connection) => {
@@ -157,7 +212,7 @@ const spyGameplayLogic = (connection, pendingSpying) => {
//more spies reduces the chances of being seen? Counter intuitive //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. 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) { if (Math.random() <= chanceSeen) {
let query = 'INSERT INTO pastSpying (eventTime, attackerId, defenderId, attackingUnits, success, spoilsGold) VALUES (?, ?, ?, ?, "failure", 0);'; 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) => { connection.query(query, [pendingSpying.eventTime, pendingSpying.attackerId, pendingSpying.defenderId, pendingSpying.attackingUnits], (err) => {
@@ -458,3 +513,4 @@ module.exports = {
runSpyTick: runSpyTick runSpyTick: runSpyTick
}; };
//TODO: move balance variables to an external file (.env?)
+2 -1
View File
@@ -67,9 +67,10 @@ export default class App extends React.Component {
<LazyRoute path='/passwordreset' component={() => import('./pages/password_reset.jsx')} /> <LazyRoute path='/passwordreset' component={() => import('./pages/password_reset.jsx')} />
<LazyRoute path='/profile' component={() => import('./pages/profile.jsx')} /> <LazyRoute path='/profile' component={() => import('./pages/profile.jsx')} />
<LazyRoute path='/equipment' component={() => import('./pages/equipment.jsx')} />
<LazyRoute path='/ladder' component={() => import('./pages/ladder.jsx')} /> <LazyRoute path='/ladder' component={() => import('./pages/ladder.jsx')} />
<LazyRoute path='/combatlog' component={() => import('./pages/combat_log.jsx')} /> <LazyRoute path='/combatlog' component={() => import('./pages/combat_log.jsx')} />
<LazyRoute path='/equipment' component={() => import('./pages/equipment.jsx')} /> <LazyRoute path='/spyinglog' component={() => import('./pages/spying_log.jsx')} />
<LazyRoute path='/tasklist' component={() => import('./pages/task_list.jsx')} /> <LazyRoute path='/tasklist' component={() => import('./pages/task_list.jsx')} />
<LazyRoute path='/patronlist' component={() => import('./pages/patron_list.jsx')} /> <LazyRoute path='/patronlist' component={() => import('./pages/patron_list.jsx')} />
+148
View File
@@ -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 (
<div className='sidePanelPage'>
<div className='sidePanel'>
<CommonLinks />
</div>
<div className='mainPanel'>
<div className='warning' style={warningStyle}>
<p>{this.state.warning}</p>
</div>
<h1 className='centered'>Espionage Log</h1>
<ButtonHeader />
<PagedSpyingLog
setWarning={this.setWarning.bind(this)}
username={this.props.username}
start={this.state.start}
length={this.state.length}
getFetch={this.getFetch.bind(this)}
onReceived={this.onReceived.bind(this)}
/>
<ButtonHeader />
</div>
</div>
);
}
buttonHeader() {
return (
<div className='table noCollapse'>
<div className='row'>
<button className='col' onClick={ this.decrement.bind(this) }>{'< Back'}</button>
<div className='col hide mobile' />
<div className='col hide mobile' />
<button className='col' onClick={ this.increment.bind(this) }>{'Next >'}</button>
</div>
</div>
);
}
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;
+2
View File
@@ -32,6 +32,7 @@ class CommonLinks extends React.Component {
<p className='mobile centered'><Link to='/equipment' onClick={this.props.onClickEquipment}>Your Equipment</Link></p> <p className='mobile centered'><Link to='/equipment' onClick={this.props.onClickEquipment}>Your Equipment</Link></p>
<p className='mobile centered'><Link to='/ladder' onClick={this.props.onClickLadder}>Attack (Game Ladder)</Link></p> <p className='mobile centered'><Link to='/ladder' onClick={this.props.onClickLadder}>Attack (Game Ladder)</Link></p>
<p className='mobile centered'><Link to='/combatlog' onClick={this.props.onClickCombatLog}>Combat Log</Link></p> <p className='mobile centered'><Link to='/combatlog' onClick={this.props.onClickCombatLog}>Combat Log</Link></p>
<p className='mobile centered'><Link to='/spyinglog' onClick={this.props.onClickSpyingLog}>Espionage Log</Link></p>
<p className='mobile centered'><Link to='/passwordchange' onClick={this.props.onClickPasswordChange}>Change Password</Link></p> <p className='mobile centered'><Link to='/passwordchange' onClick={this.props.onClickPasswordChange}>Change Password</Link></p>
<p className='mobile centered'><Link to='/tasklist' onClick={this.props.onClickTaskList}>Task List</Link></p> <p className='mobile centered'><Link to='/tasklist' onClick={this.props.onClickTaskList}>Task List</Link></p>
<p className='mobile centered'><Link to='/patronlist' onClick={this.props.onClickPatronList}>Patron List</Link></p> <p className='mobile centered'><Link to='/patronlist' onClick={this.props.onClickPatronList}>Patron List</Link></p>
@@ -71,6 +72,7 @@ CommonLinks.propTypes = {
onClickEquipment: PropTypes.func, onClickEquipment: PropTypes.func,
onClickLadder: PropTypes.func, onClickLadder: PropTypes.func,
onClickCombatLog: PropTypes.func, onClickCombatLog: PropTypes.func,
onClickSpyingLog: PropTypes.func,
onClickTaskList: PropTypes.func, onClickTaskList: PropTypes.func,
onClickPatronList: PropTypes.func, onClickPatronList: PropTypes.func,
onClickRules: PropTypes.func onClickRules: PropTypes.func
@@ -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 (
<div>
{Object.keys(this.state).map((key) => <SpyingLogRecord key={key} username={this.props.username} {...this.state[key]} />)}
</div>
);
}
//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);
@@ -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 (
<div className='table noCollapse'>
<hr />
<div className='break' />
<div className='row'>
<p className='col truncate'>{this.parseDate(this.props.eventTime)}</p>
<p className='col truncate'>Atk: {this.prettyName(this.props.attacker)} ({this.props.attackingUnits ? this.props.attackingUnits : '???'} units)</p>
<p className='col truncate'>Def: {this.prettyName(this.props.defender)}</p>
</div>
<div className='row'>
<p className='col truncate'><span className='mobile hide'>Result: </span>{this.capitalizeFirstLetter(this.props.success)}</p>
<p className='col truncate'>Gold Stolen: {this.props.spoilsGold}</p>
<div className='col' />
</div>
{Object.keys(this.props.equipmentStolen).map((key) => {
return (
<div key={key} className='row'>
<p className='col truncate'><span className='mobile hide' style={{color:'red'}}>Stolen: </span>{this.props.equipmentStolen[key].name}</p>
<p className='col truncate'>{this.props.equipmentStolen[key].type}</p>
<p className='col truncate'><span className='mobile hide'>Total: </span>{this.props.equipmentStolen[key].quantity}</p>
</div>
);
})}
</div>
);
}
prettyName(name) {
//make the enemy name a link
if (name === this.props.username) {
return name;
} else if (name) {
return (<Link to={`/profile?username=${name}`}>{name}</Link>);
} else {
return (<span style={{color:'red'}}>???</span>);
}
}
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);