diff --git a/common/utilities.js b/common/utilities.js index 21d8bde..ae95672 100644 --- a/common/utilities.js +++ b/common/utilities.js @@ -16,6 +16,9 @@ let excluded = [ //messages that should not be logged 'Ladder sent', 'attacking', 'idle', + 'Badge list sent', + 'Badges owned sent', + 'Updated badge selection', 'Combat log sent', 'Spy log sent', diff --git a/public/content/task_list.md b/public/content/task_list.md index 5a61f91..8c4da58 100644 --- a/public/content/task_list.md +++ b/public/content/task_list.md @@ -39,6 +39,7 @@ Potential And Confirmed Bugs Wishlist --- +* Mercenaries. * In-game events. * Hire a graphic designer. * Implement nations (player alliances) (sending items/gold). @@ -64,4 +65,4 @@ Badge Ideas * Bug Hunter (Reward List: Hegemon) * Alliance exclusive badges * Donater / Supporter - +* Unknown / error badge diff --git a/public/img/badges/alpha_tester.png b/public/img/badges/alpha_tester.png new file mode 100644 index 0000000..76b2a30 Binary files /dev/null and b/public/img/badges/alpha_tester.png differ diff --git a/public/img/badges/capture_the_flag.png b/public/img/badges/capture_the_flag.png new file mode 100644 index 0000000..c0685d6 Binary files /dev/null and b/public/img/badges/capture_the_flag.png differ diff --git a/public/img/badges/combat_master.png b/public/img/badges/combat_master.png new file mode 100644 index 0000000..257acd9 Binary files /dev/null and b/public/img/badges/combat_master.png differ diff --git a/public/img/badges/gold_horde.png b/public/img/badges/gold_horde.png new file mode 100644 index 0000000..bba4aa5 Binary files /dev/null and b/public/img/badges/gold_horde.png differ diff --git a/public/img/badges/king_of_the_hill.png b/public/img/badges/king_of_the_hill.png new file mode 100644 index 0000000..773438f Binary files /dev/null and b/public/img/badges/king_of_the_hill.png differ diff --git a/public/styles/shared.css b/public/styles/shared.css index b49cdfa..ddbd759 100644 --- a/public/styles/shared.css +++ b/public/styles/shared.css @@ -337,12 +337,17 @@ pre { .rainbowText { -webkit-background-clip: text; -webkit-text-fill-color: transparent; - background-image: -webkit-gradient(linear, left top, left bottom, + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0.00, red), color-stop(16%, orange), color-stop(32%, yellow), color-stop(48%, green), color-stop(60%, blue), color-stop(76%, indigo), - color-stop(1.00, violet)); - } \ No newline at end of file + color-stop(1.00, violet) + ); +} + +.highlight { + background-color: #1a253a; +} \ No newline at end of file diff --git a/server/badge_statistics.json b/server/badge_statistics.json new file mode 100644 index 0000000..62bd328 --- /dev/null +++ b/server/badge_statistics.json @@ -0,0 +1,32 @@ +{ + "Alpha Tester": { + "filename": "alpha_tester.png", + "description": "Awarded to everyone who joined before or on the 7th of June, 2019 (AEST).", + "visible": true, + "earnable": false + }, + "Capture The Flag": { + "filename": "capture_the_flag.png", + "description": "This badge is stolen by successful attacks.", + "visible": true, + "earnable": true + }, + "Combat Master": { + "filename": "combat_master.png", + "description": "You have successfully attacked 100 times.", + "visible": true, + "earnable": true + }, + "Gold Horde": { + "filename": "gold_horde.png", + "description": "You purchased this badge for 500 gold.", + "visible": true, + "earnable": true + }, + "King Of The Hill": { + "filename": "king_of_the_hill.png", + "description": "You held your position at the top of the game ladder for a whole day.", + "visible": true, + "earnable": true + } +} \ No newline at end of file diff --git a/server/badges.js b/server/badges.js new file mode 100644 index 0000000..c0c1ae7 --- /dev/null +++ b/server/badges.js @@ -0,0 +1,132 @@ +//environment variables +require('dotenv').config(); + +//utilities +let { log } = require('../common/utilities.js'); + +let { logActivity } = require('./utilities.js'); + +const getBadgesStatistics = (cb) => { + //TODO: apiVisible field + return cb(undefined, { 'statistics': require('./badge_statistics.json') }); +}; + +const getBadgesOwned = (connection, id, cb) => { + let query = 'SELECT name, active FROM badges WHERE accountId = ?;'; + connection.query(query, [id], (err, results) => { + if (err) throw err; + + let ret = {}; //names, active + + Object.keys(results).map((key) => { + if (ret[results[key].name] !== undefined) { + log('WARNING: Invalid database state, badges owned', id, JSON.stringify(results)); + } + ret[results[key].name] = { active: results[key].active }; + }); + + return cb(undefined, { 'owned': ret }); + }); +} + +const listRequest = (connection) => (req, res) => { + getBadgesStatistics((err, results) => { + if (err) throw err; + + res.status(200).json(results); + res.end(); + + log('Badge list sent'); + }); +} + +const ownedRequest = (connection) => (req, res) => { + //validate the credentials + let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;'; + connection.query(query, [req.body.id, req.body.token], (err, credentials) => { + if (err) throw err; + + if (credentials[0].total !== 1) { + res.status(400).write(log('Invalid badges owned credentials', JSON.stringify(body), body.id, body.token)); + res.end(); + return; + } + + //get stats and owned + getBadgesStatistics((err, badgesStatistics) => { + if (err) throw err; + + getBadgesOwned(connection, req.body.id, (err, badgesOWned) => { + if (err) throw err; + + res.status(200).json(Object.assign({}, badgesStatistics, badgesOWned)); + res.end(); + + log('Badges owned sent', req.body.id); + }); + }); + }); +} + +const selectActiveBadge = (connection) => (req, res) => { + //validate the credentials + let query = 'SELECT COUNT(*) AS total FROM sessions WHERE accountId = ? AND token = ?;'; + connection.query(query, [req.body.id, req.body.token], (err, credentials) => { + if (err) throw err; + + if (credentials[0].total !== 1) { + res.status(400).write(log('Invalid active badge select credentials', JSON.stringify(body), body.id, body.token)); + res.end(); + return; + } + + //check to see if the player owns this badge + getBadgesOwned(connection, req.body.id, (err, { owned }) => { + if (err) throw err; + + if (req.body.name !== null && !owned[req.body.name]) { + res.status(400).write('You don\'t own that badge'); + res.end(); + return; + } + + //zero out the user's selection + let query = 'UPDATE badges SET active = FALSE WHERE accountId = ?;'; + connection.query(query, [req.body.id], (err) => { + if (err) throw err; + + //update the user's selection + let query = 'UPDATE badges SET active = TRUE WHERE accountId = ? AND name = ?;'; + connection.query(query, [req.body.id, req.body.name], (err) => { + if (err) throw err; + + //re-grab the owned badges (with updated info) + getBadgesOwned(connection, req.body.id, (err, results) => { + if (err) throw err; + + res.status(200).json(results); + res.end(); + + log('Updated badge selection', req.body.id, req.body.name); + logActivity(connection, req.body.id); + }); + }); + }); + }); + }); +}; + +const rewardBadge = (connection, id, badgeName) => { + //TODO: constants as badge/equipment names? + let query = 'INSERT IGNORE badges (accountId, name) VALUES (?, ?);'; + connection.query(query, [id, badgeName], (err) => { + if (err) throw err; + }); +}; + +module.exports = { + listRequest: listRequest, + ownedRequest: ownedRequest, + selectActiveBadge: selectActiveBadge, + rewardBadge: rewardBadge +}; \ No newline at end of file diff --git a/server/equipment.js b/server/equipment.js index edd534b..a256d0e 100644 --- a/server/equipment.js +++ b/server/equipment.js @@ -4,7 +4,7 @@ require('dotenv').config(); //utilities let { log } = require('../common/utilities.js'); -let { getEquipmentStatistics, getOwned, isAttacking, isSpying, logActivity } = require('./utilities.js'); +let { getEquipmentStatistics, getEquipmentOwned, isAttacking, isSpying, logActivity } = require('./utilities.js'); const equipmentRequest = (connection) => (req, res) => { //validate the credentials @@ -28,7 +28,7 @@ const equipmentRequest = (connection) => (req, res) => { return; } - return getOwned(connection, req.body.id, (err, ownedObj) => { + return getEquipmentOwned(connection, req.body.id, (err, ownedObj) => { if (err) { res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); res.end(); @@ -56,7 +56,7 @@ const equipmentRequest = (connection) => (req, res) => { }); case 'owned': - return getOwned(connection, req.body.id, (err, obj) => { + return getEquipmentOwned(connection, req.body.id, (err, obj) => { if (err) { res.status(400).write(log(err, req.body.id, req.body.token, req.body.field)); } else { @@ -172,10 +172,10 @@ const purchaseRequest = (connection) => (req, res) => { if (err) throw err; //return the new owned data - getOwned(connection, req.body.id, (err, results) => { + getEquipmentOwned(connection, req.body.id, (err, results) => { if (err) throw err; - res.status(200).json(Object.assign(results)); + res.status(200).json(Object.assign(results)); //TODO: Why is assign here? res.end(); log('Purchase made', req.body.id, req.body.token, req.body.type, req.body.name); @@ -265,7 +265,7 @@ const sellRequest = (connection) => (req, res) => { if (err) throw err; //return the new owned data - getOwned(connection, req.body.id, (err, results) => { + getEquipmentOwned(connection, req.body.id, (err, results) => { if (err) throw err; res.status(200).json(Object.assign(results)); diff --git a/server/index.js b/server/index.js index 596560a..d235026 100644 --- a/server/index.js +++ b/server/index.js @@ -66,6 +66,11 @@ app.post('/equipmentrequest', equipment.equipmentRequest(connection)); app.post('/equipmentpurchaserequest', equipment.purchaseRequest(connection)); app.post('/equipmentsellrequest', equipment.sellRequest(connection)); +let badges = require('./badges.js'); +app.post('/badgeslistrequest', badges.listRequest(connection)); +app.post('/badgesownedrequest', badges.ownedRequest(connection)); +app.post('/badgeselectactiverequest', badges.selectActiveBadge(connection)); + //static directories app.use('/content', express.static(path.resolve(__dirname + '/../public/content')) ); app.use('/img', express.static(path.resolve(__dirname + '/../public/img')) ); diff --git a/server/utilities.js b/server/utilities.js index ba79a0b..75fd8ff 100644 --- a/server/utilities.js +++ b/server/utilities.js @@ -9,7 +9,7 @@ const getEquipmentStatistics = (cb) => { return cb(undefined, { 'statistics': require('./equipment_statistics.json') }); }; -const getOwned = (connection, id, cb) => { +const getEquipmentOwned = (connection, id, cb) => { let query = 'SELECT name, quantity FROM equipment WHERE accountId = ?;'; connection.query(query, [id], (err, results) => { if (err) throw err; @@ -95,7 +95,7 @@ const logActivity = (connection, id) => { module.exports = { getEquipmentStatistics: getEquipmentStatistics, - getOwned: getOwned, + getEquipmentOwned: getEquipmentOwned, isAttacking: isAttacking, isSpying: isSpying, logActivity: logActivity diff --git a/sql/create_database_structure.sql b/sql/create_database_structure.sql index f526261..067b569 100644 --- a/sql/create_database_structure.sql +++ b/sql/create_database_structure.sql @@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS equipmentStolen ( pastSpyingId INTEGER UNSIGNED, - name VARCHAR(50), + name VARCHAR(50), #TODO: make this NOT NULL quantity INTEGER, type VARCHAR(50), @@ -181,3 +181,15 @@ CREATE TABLE IF NOT EXISTS equipmentStolen ( CONSTRAINT FOREIGN KEY fk_pastSpyingId(pastSpyingId) REFERENCES pastSpying(id) ON UPDATE CASCADE ON DELETE CASCADE ); +#badge system +CREATE TABLE IF NOT EXISTS badges ( + id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE, + td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + + accountId INTEGER UNSIGNED, + + name VARCHAR(50) NOT NULL, + active BOOLEAN NOT NULL DEFAULT FALSE, + + CONSTRAINT FOREIGN KEY fk_accountId(accountId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/components/app.jsx b/src/components/app.jsx index 0941d1d..be55400 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -71,6 +71,7 @@ export default class App extends React.Component { import('./pages/ladder.jsx')} /> import('./pages/combat_log.jsx')} /> import('./pages/spying_log.jsx')} /> + import('./pages/badge_select.jsx')} /> import('./pages/task_list.jsx')} /> import('./pages/patron_list.jsx')} /> diff --git a/src/components/pages/badge_select.jsx b/src/components/pages/badge_select.jsx new file mode 100644 index 0000000..34e49d9 --- /dev/null +++ b/src/components/pages/badge_select.jsx @@ -0,0 +1,51 @@ +import React from 'react'; + +//panels +import CommonLinks from '../panels/common_links.jsx'; +import BadgeSelectPanel from '../panels/badge_select.jsx'; + +class BadgeSelect 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}

+
+ +

Badge Select

+

Click on your favourite badge!

+ this.setState({ fetch: fn }) } /> +
+
+
+ ); + } + + setWarning(s) { + this.setState({ warning: s }); + } +}; + +export default BadgeSelect; \ No newline at end of file diff --git a/src/components/panels/badge.jsx b/src/components/panels/badge.jsx new file mode 100644 index 0000000..23b495f --- /dev/null +++ b/src/components/panels/badge.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +class Badge extends React.Component { + constructor(props) { + super(props); + + this.state = { + // + }; + } + + render() { + let realSize = typeof(this.props.size) === 'number' ? this.props.number : this.parseSize(this.props.size); + + return ( + {this.props.name} + ); + } + + parseSize(sizeString) { + if (sizeString === 'small') return 12; + if (sizeString === 'medium') return 20; + return 100; + } +}; + +export default Badge; \ No newline at end of file diff --git a/src/components/panels/badge_select.jsx b/src/components/panels/badge_select.jsx new file mode 100644 index 0000000..2ab3d2a --- /dev/null +++ b/src/components/panels/badge_select.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import Badge from './badge.jsx'; + +class BadgeSelect extends React.Component { + constructor(props) { + super(props); + + this.state = { + data: {} + }; + + if (props.getFetch) { + props.getFetch(() => this.sendRequest('/badgesownedrequest')); + } + } + + render() { + if (!this.state.data.owned) { + return ( +

Loading badges...

+ ); + } + + //are none selected? + let anySelected = Object.keys(this.state.data.owned).reduce((accumulator, name) => accumulator || this.state.data.owned[name].active, false); + + return ( +
+
+
this.sendRequest('/badgeselectactiverequest', { name: null }) }> +

No Badge

+
+
+
+
+
+ + {Object.keys(this.state.data.owned).map((name) => +
+
this.sendRequest('/badgeselectactiverequest', { name: name }) }> +
+ +
+

{this.state.data.statistics[name].description}

+
+
+
+
+
+ )} +
+ ); + } + + //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({ data: Object.assign({}, this.state.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({ + id: this.props.id, + token: this.props.token, + ...args + })); + } +}; + +BadgeSelect.propTypes = { + id: PropTypes.number.isRequired, + token: PropTypes.number.isRequired, + + setWarning: PropTypes.func, + getFetch: PropTypes.func +}; + +const mapStoreToProps = (store) => { + return { + id: store.account.id, + token: store.account.token + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + // + }; +}; + +BadgeSelect = connect(mapStoreToProps, mapDispatchToProps)(BadgeSelect); + +export default BadgeSelect; \ No newline at end of file diff --git a/src/components/panels/combat_log_record.jsx b/src/components/panels/combat_log_record.jsx index f1781b1..fb9f01c 100644 --- a/src/components/panels/combat_log_record.jsx +++ b/src/components/panels/combat_log_record.jsx @@ -13,7 +13,7 @@ class CombatLogRecord extends React.Component { render() { return ( -
+

diff --git a/src/components/panels/common_links.jsx b/src/components/panels/common_links.jsx index 642933f..6ad87bf 100644 --- a/src/components/panels/common_links.jsx +++ b/src/components/panels/common_links.jsx @@ -30,6 +30,7 @@ class CommonLinks extends React.Component {

Your Kingdom

Your Equipment

+

Your Badges

Attack (Game Ladder)

Combat Log

Espionage Log

@@ -72,6 +73,7 @@ CommonLinks.propTypes = { onClickPasswordRecover: PropTypes.func, onClickProfile: PropTypes.func, onClickEquipment: PropTypes.func, + onClickBadges: PropTypes.func, onClickLadder: PropTypes.func, onClickCombatLog: PropTypes.func, onClickSpyingLog: PropTypes.func, diff --git a/src/components/panels/spying_log_record.jsx b/src/components/panels/spying_log_record.jsx index 434c375..65c5a90 100644 --- a/src/components/panels/spying_log_record.jsx +++ b/src/components/panels/spying_log_record.jsx @@ -13,7 +13,7 @@ class SpyingLogRecord extends React.Component { render() { return ( -
+