Implemented stats page

This commit is contained in:
2019-06-07 17:09:18 +10:00
parent c0d018ac4a
commit 9f508a4aca
11 changed files with 181 additions and 14 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ Major
* Write the instructions for badges. * Write the instructions for badges.
* Implement countdown timers for combat and training. * Implement countdown timers for combat and training.
* Implement referral links. * Implement referral links.
* Implement admin panel / stats page. * Implement admin panel.
* Implement bug tracker. * Implement bug tracker.
Minor Minor
+2 -2
View File
@@ -8,7 +8,7 @@ let CronJob = require('cron').CronJob;
let { logDiagnostics } = require('./diagnostics.js'); let { logDiagnostics } = require('./diagnostics.js');
let { log } = require('../common/utilities.js'); let { log } = require('../common/utilities.js');
let { getStatistics, isAttacking, logActivity } = require('./utilities.js'); let { getEquipmentStatistics, isAttacking, logActivity } = require('./utilities.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)
@@ -197,7 +197,7 @@ const runCombatTick = (connection) => {
if (err) throw err; if (err) throw err;
//get the global equipment stats //get the global equipment stats
getStatistics((err, { statistics }) => { getEquipmentStatistics((err, { statistics }) => {
if (err) throw err; if (err) throw err;
//get the combat boosts from equipment, from highest to lowest //get the combat boosts from equipment, from highest to lowest
+5 -5
View File
@@ -4,7 +4,7 @@ require('dotenv').config();
//utilities //utilities
let { log } = require('../common/utilities.js'); let { log } = require('../common/utilities.js');
let { getStatistics, getOwned, isAttacking, isSpying, logActivity } = require('./utilities.js'); let { getEquipmentStatistics, getOwned, isAttacking, isSpying, logActivity } = require('./utilities.js');
const equipmentRequest = (connection) => (req, res) => { const equipmentRequest = (connection) => (req, res) => {
//validate the credentials //validate the credentials
@@ -21,7 +21,7 @@ const equipmentRequest = (connection) => (req, res) => {
//if no field received, send everything //if no field received, send everything
if (!req.body.field) { if (!req.body.field) {
//compose the returned objects //compose the returned objects
return getStatistics((err, statisticsObj) => { return getEquipmentStatistics((err, statisticsObj) => {
if (err) { if (err) {
res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); res.status(400).write(log(err, req.body.id, req.body.token, req.body.field));
res.end(); res.end();
@@ -45,7 +45,7 @@ const equipmentRequest = (connection) => (req, res) => {
//send specific fields //send specific fields
switch(req.body.field) { switch(req.body.field) {
case 'statistics': case 'statistics':
return getStatistics((err, obj) => { return getEquipmentStatistics((err, obj) => {
if (err) { if (err) {
res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); res.status(400).write(log(err, req.body.id, req.body.token, req.body.field));
} else { } else {
@@ -117,7 +117,7 @@ const purchaseRequest = (connection) => (req, res) => {
} }
//get the stats for all objects //get the stats for all objects
getStatistics((err, { statistics }) => { getEquipmentStatistics((err, { statistics }) => {
if (err) throw err; if (err) throw err;
//valid parameters //valid parameters
@@ -235,7 +235,7 @@ const sellRequest = (connection) => (req, res) => {
} }
//get the stats for all objects //get the stats for all objects
getStatistics((err, { statistics }) => { getEquipmentStatistics((err, { statistics }) => {
if (err) throw err; if (err) throw err;
//valid parameters //valid parameters
+4
View File
@@ -26,6 +26,10 @@ let connection = connectToDatabase(); //uses .env
let diagnostics = require('./diagnostics.js'); let diagnostics = require('./diagnostics.js');
diagnostics.runDailyDiagnostics(connection); diagnostics.runDailyDiagnostics(connection);
//game statistics
let statistics = require('./statistics.js');
app.post('/statisticsrequest', statistics.statisticsRequest(connection));
//handle accounts //handle accounts
let accounts = require('./accounts.js'); let accounts = require('./accounts.js');
app.post('/signuprequest', accounts.signupRequest(connection)); app.post('/signuprequest', accounts.signupRequest(connection));
+3 -3
View File
@@ -8,7 +8,7 @@ let CronJob = require('cron').CronJob;
let { logDiagnostics } = require('./diagnostics.js'); let { logDiagnostics } = require('./diagnostics.js');
let { log } = require('../common/utilities.js'); let { log } = require('../common/utilities.js');
let { getStatistics, isSpying, isAttacking, logActivity } = require('./utilities.js'); //TODO: rename getStatistics to getEquipmentStatistics let { getEquipmentStatistics, isSpying, isAttacking, logActivity } = require('./utilities.js');
const spyRequest = (connection) => (req, res) => { const spyRequest = (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)
@@ -292,7 +292,7 @@ const spyStealEquipmentInner = (connection, attackerId, defenderId, attackingUni
connection.query(query, [defenderId], (err, results) => { connection.query(query, [defenderId], (err, results) => {
if (err) throw err; if (err) throw err;
getStatistics((err, { statistics }) => { getEquipmentStatistics((err, { statistics }) => {
if (err) throw err; if (err) throw err;
//don't steal certain items //don't steal certain items
@@ -349,7 +349,7 @@ const spyStealEquipmentInner = (connection, attackerId, defenderId, attackingUni
}; };
const removeForEachSoldier = (results, soldiers, cb) => { const removeForEachSoldier = (results, soldiers, cb) => {
getStatistics((err, { statistics }) => { getEquipmentStatistics((err, { statistics }) => {
if (err) throw err; if (err) throw err;
results.sort((a, b) => statistics[a.type][a.name].combatBoost < statistics[b.type][b.name].combatBoost); results.sort((a, b) => statistics[a.type][a.name].combatBoost < statistics[b.type][b.name].combatBoost);
+47
View File
@@ -0,0 +1,47 @@
//environment variables
require('dotenv').config();
const round = (x) => Math.round(x * 100) / 100;
const statisticsRequest = (connection) => (req, res) => {
let query = 'SELECT COUNT(*) AS playerCount, SUM(gold) / COUNT(*) AS goldAverage FROM profiles;';
connection.query(query, (err, results) => {
if (err) throw err;
let playerCount = results[0].playerCount;
let goldAverage = results[0].goldAverage;
//determine the correct tick rate based on the current gold average
//NOTE: copy/pasted
let tickRate = (() => {
if (results[0].goldAverage < 120) return 5;
if (results[0].goldAverage < 130) return 15;
if (results[0].goldAverage < 140) return 30;
return 60; //slow it way down
})();
let nextTick = tickRate - (new Date()).getMinutes() % tickRate;
let query = 'SELECT COUNT(*) AS activity FROM accounts WHERE lastActivityTime >= DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY);';
connection.query(query, (err, results) => {
if (err) throw err;
let activity = results[0].activity;
let activePercentage = round(activity / playerCount * 100);
res.status(200).json({
'Player Count': playerCount,
'Active Players': activity,
'Active Percentage': { string: `${activePercentage}%`, color: activePercentage >= 10 ? 'lightgreen' : activePercentage >= 5 ? 'yellow' : 'red'},
'Gold Average': `${round(goldAverage)}`,
'Tick Rate': `${tickRate} minutes`,
'Next Tick': `${nextTick} minute${nextTick === 1 ? '' : 's'} from now`
});
res.end();
});
});
};
module.exports = {
statisticsRequest: statisticsRequest
};
+2 -2
View File
@@ -4,7 +4,7 @@ require('dotenv').config();
//utilities //utilities
let { log } = require('../common/utilities.js'); let { log } = require('../common/utilities.js');
const getStatistics = (cb) => { const getEquipmentStatistics = (cb) => {
//TODO: apiVisible field //TODO: apiVisible field
return cb(undefined, { 'statistics': require('./equipment_statistics.json') }); return cb(undefined, { 'statistics': require('./equipment_statistics.json') });
}; };
@@ -94,7 +94,7 @@ const logActivity = (connection, id) => {
}; };
module.exports = { module.exports = {
getStatistics: getStatistics, getEquipmentStatistics: getEquipmentStatistics,
getOwned: getOwned, getOwned: getOwned,
isAttacking: isAttacking, isAttacking: isAttacking,
isSpying: isSpying, isSpying: isSpying,
+1
View File
@@ -76,6 +76,7 @@ export default class App extends React.Component {
<LazyRoute path='/patronlist' component={() => import('./pages/patron_list.jsx')} /> <LazyRoute path='/patronlist' component={() => import('./pages/patron_list.jsx')} />
<LazyRoute path='/news/:postId' component={() => import('./pages/news.jsx')} /> <LazyRoute path='/news/:postId' component={() => import('./pages/news.jsx')} />
<LazyRoute path='/rules' component={() => import('./pages/rules.jsx')} /> <LazyRoute path='/rules' component={() => import('./pages/rules.jsx')} />
<LazyRoute path='/statistics' component={() => import('./pages/statistics.jsx')} />
<LazyRoute path='*' component={() => import('./pages/page_not_found.jsx')} /> <LazyRoute path='*' component={() => import('./pages/page_not_found.jsx')} />
</Switch> </Switch>
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
//panels
import CommonLinks from '../panels/common_links.jsx';
import StatisticsPanel from '../panels/statistics.jsx';
class Statistics extends React.Component {
constructor(props) {
super(props);
this.state = {
warning: '', //TODO: unified warning?
fetch: null
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.state.fetch();
}
render() {
let warningStyle = {
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
return (
<div className='page'>
<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'>Game Statistics</h1>
<StatisticsPanel setWarning={this.setWarning.bind(this)} getFetch={ (fn) => this.setState({ fetch: fn }) } />
</div>
</div>
</div>
);
}
setWarning(s) {
this.setState({ warning: s });
}
};
export default Statistics;
+4 -1
View File
@@ -37,6 +37,7 @@ class CommonLinks extends React.Component {
<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>
<p className='mobile centered'><Link to='/rules' onClick={this.props.onClickRules}>Rules</Link></p> <p className='mobile centered'><Link to='/rules' onClick={this.props.onClickRules}>Rules</Link></p>
<p className='mobile centered'><Link to='/statistics' onClick={this.props.onClickStatistics}>Game Stats</Link></p>
<Extra /> <Extra />
@@ -53,6 +54,7 @@ class CommonLinks extends React.Component {
<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>
<p className='mobile centered'><Link to='/rules' onClick={this.props.onClickRules}>Rules</Link></p> <p className='mobile centered'><Link to='/rules' onClick={this.props.onClickRules}>Rules</Link></p>
<p className='mobile centered'><Link to='/statistics' onClick={this.props.onClickStatistics}>Game Stats</Link></p>
<Extra /> <Extra />
</div> </div>
@@ -75,7 +77,8 @@ CommonLinks.propTypes = {
onClickSpyingLog: PropTypes.func, onClickSpyingLog: PropTypes.func,
onClickTaskList: PropTypes.func, onClickTaskList: PropTypes.func,
onClickPatronList: PropTypes.func, onClickPatronList: PropTypes.func,
onClickRules: PropTypes.func onClickRules: PropTypes.func,
onClickStatistics: PropTypes.func
}; };
function mapStoreToProps(store) { function mapStoreToProps(store) {
+62
View File
@@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
class Statistics extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {}
};
if (props.getFetch) {
props.getFetch(() => this.sendRequest('/statisticsrequest'));
}
}
render() {
return (
<div className='panel table noCollapse'>
{Object.keys(this.state.data).map((key) => <div key={key} className='row'>
<p className='col'>{key}:</p>
<p className='col'>{typeof(this.state.data[key]) === 'object' ? <span style={{color: this.state.data[key].color}}>{this.state.data[key].string}</span> : <span>{this.state.data[key]}</span>}</p>
<div className='col mobile hide' />
</div>)}
</div>
);
}
sendRequest(url, args = {}) { //send a unified request, using my credentials
//TODO: move sendRequest() into it's own module
//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({ data: 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({
//NOTE: No id or token needed for statistics
...args
}));
}
};
Statistics.propTypes = {
setWarning: PropTypes.func,
getFetch: PropTypes.func
};
export default Statistics;