diff --git a/public/content/task_list.md b/public/content/task_list.md index 74083a0..862b904 100644 --- a/public/content/task_list.md +++ b/public/content/task_list.md @@ -5,7 +5,7 @@ Major * Write the instructions for badges. * Implement countdown timers for combat and training. * Implement referral links. -* Implement admin panel / stats page. +* Implement admin panel. * Implement bug tracker. Minor diff --git a/server/combat.js b/server/combat.js index de3e355..4e03de7 100644 --- a/server/combat.js +++ b/server/combat.js @@ -8,7 +8,7 @@ let CronJob = require('cron').CronJob; let { logDiagnostics } = require('./diagnostics.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) => { //verify the attacker's credentials (only the attacker can launch an attack) @@ -197,7 +197,7 @@ const runCombatTick = (connection) => { if (err) throw err; //get the global equipment stats - getStatistics((err, { statistics }) => { + getEquipmentStatistics((err, { statistics }) => { if (err) throw err; //get the combat boosts from equipment, from highest to lowest diff --git a/server/equipment.js b/server/equipment.js index 5fd4b13..edd534b 100644 --- a/server/equipment.js +++ b/server/equipment.js @@ -4,7 +4,7 @@ require('dotenv').config(); //utilities 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) => { //validate the credentials @@ -21,7 +21,7 @@ const equipmentRequest = (connection) => (req, res) => { //if no field received, send everything if (!req.body.field) { //compose the returned objects - return getStatistics((err, statisticsObj) => { + return getEquipmentStatistics((err, statisticsObj) => { if (err) { res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); res.end(); @@ -45,7 +45,7 @@ const equipmentRequest = (connection) => (req, res) => { //send specific fields switch(req.body.field) { case 'statistics': - return getStatistics((err, obj) => { + return getEquipmentStatistics((err, obj) => { if (err) { res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); } else { @@ -117,7 +117,7 @@ const purchaseRequest = (connection) => (req, res) => { } //get the stats for all objects - getStatistics((err, { statistics }) => { + getEquipmentStatistics((err, { statistics }) => { if (err) throw err; //valid parameters @@ -235,7 +235,7 @@ const sellRequest = (connection) => (req, res) => { } //get the stats for all objects - getStatistics((err, { statistics }) => { + getEquipmentStatistics((err, { statistics }) => { if (err) throw err; //valid parameters diff --git a/server/index.js b/server/index.js index bcf1adc..f6113af 100644 --- a/server/index.js +++ b/server/index.js @@ -26,6 +26,10 @@ let connection = connectToDatabase(); //uses .env let diagnostics = require('./diagnostics.js'); diagnostics.runDailyDiagnostics(connection); +//game statistics +let statistics = require('./statistics.js'); +app.post('/statisticsrequest', statistics.statisticsRequest(connection)); + //handle accounts let accounts = require('./accounts.js'); app.post('/signuprequest', accounts.signupRequest(connection)); diff --git a/server/spying.js b/server/spying.js index ff544bf..9f7600d 100644 --- a/server/spying.js +++ b/server/spying.js @@ -8,7 +8,7 @@ let CronJob = require('cron').CronJob; let { logDiagnostics } = require('./diagnostics.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) => { //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) => { if (err) throw err; - getStatistics((err, { statistics }) => { + getEquipmentStatistics((err, { statistics }) => { if (err) throw err; //don't steal certain items @@ -349,7 +349,7 @@ const spyStealEquipmentInner = (connection, attackerId, defenderId, attackingUni }; const removeForEachSoldier = (results, soldiers, cb) => { - getStatistics((err, { statistics }) => { + getEquipmentStatistics((err, { statistics }) => { if (err) throw err; results.sort((a, b) => statistics[a.type][a.name].combatBoost < statistics[b.type][b.name].combatBoost); diff --git a/server/statistics.js b/server/statistics.js new file mode 100644 index 0000000..8d7aae8 --- /dev/null +++ b/server/statistics.js @@ -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 +}; \ No newline at end of file diff --git a/server/utilities.js b/server/utilities.js index b4b3871..ba79a0b 100644 --- a/server/utilities.js +++ b/server/utilities.js @@ -4,7 +4,7 @@ require('dotenv').config(); //utilities let { log } = require('../common/utilities.js'); -const getStatistics = (cb) => { +const getEquipmentStatistics = (cb) => { //TODO: apiVisible field return cb(undefined, { 'statistics': require('./equipment_statistics.json') }); }; @@ -94,7 +94,7 @@ const logActivity = (connection, id) => { }; module.exports = { - getStatistics: getStatistics, + getEquipmentStatistics: getEquipmentStatistics, getOwned: getOwned, isAttacking: isAttacking, isSpying: isSpying, diff --git a/src/components/app.jsx b/src/components/app.jsx index 0027965..0941d1d 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -76,6 +76,7 @@ export default class App extends React.Component { import('./pages/patron_list.jsx')} /> import('./pages/news.jsx')} /> import('./pages/rules.jsx')} /> + import('./pages/statistics.jsx')} /> import('./pages/page_not_found.jsx')} /> diff --git a/src/components/pages/statistics.jsx b/src/components/pages/statistics.jsx new file mode 100644 index 0000000..d62ebbd --- /dev/null +++ b/src/components/pages/statistics.jsx @@ -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 ( +
+
+
+ +
+ +
+
+

{this.state.warning}

+
+ +

Game Statistics

+ this.setState({ fetch: fn }) } /> +
+
+
+ ); + } + + setWarning(s) { + this.setState({ warning: s }); + } +}; + +export default Statistics; \ No newline at end of file diff --git a/src/components/panels/common_links.jsx b/src/components/panels/common_links.jsx index f868306..642933f 100644 --- a/src/components/panels/common_links.jsx +++ b/src/components/panels/common_links.jsx @@ -37,6 +37,7 @@ class CommonLinks extends React.Component {

Task List

Patron List

Rules

+

Game Stats

@@ -53,6 +54,7 @@ class CommonLinks extends React.Component {

Task List

Patron List

Rules

+

Game Stats

@@ -75,7 +77,8 @@ CommonLinks.propTypes = { onClickSpyingLog: PropTypes.func, onClickTaskList: PropTypes.func, onClickPatronList: PropTypes.func, - onClickRules: PropTypes.func + onClickRules: PropTypes.func, + onClickStatistics: PropTypes.func }; function mapStoreToProps(store) { diff --git a/src/components/panels/statistics.jsx b/src/components/panels/statistics.jsx new file mode 100644 index 0000000..24b5acc --- /dev/null +++ b/src/components/panels/statistics.jsx @@ -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 ( +
+ {Object.keys(this.state.data).map((key) =>
+

{key}:

+

{typeof(this.state.data[key]) === 'object' ? {this.state.data[key].string} : {this.state.data[key]}}

+
+
)} +
+ ); + } + + 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; \ No newline at end of file