Added badge selection

This commit is contained in:
2019-06-08 10:20:10 +10:00
parent d8a7dd4933
commit 2a27572562
21 changed files with 396 additions and 15 deletions
+3
View File
@@ -16,6 +16,9 @@ let excluded = [ //messages that should not be logged
'Ladder sent', 'Ladder sent',
'attacking', 'attacking',
'idle', 'idle',
'Badge list sent',
'Badges owned sent',
'Updated badge selection',
'Combat log sent', 'Combat log sent',
'Spy log sent', 'Spy log sent',
+2 -1
View File
@@ -39,6 +39,7 @@ Potential And Confirmed Bugs
Wishlist Wishlist
--- ---
* Mercenaries.
* In-game events. * In-game events.
* Hire a graphic designer. * Hire a graphic designer.
* Implement nations (player alliances) (sending items/gold). * Implement nations (player alliances) (sending items/gold).
@@ -64,4 +65,4 @@ Badge Ideas
* Bug Hunter (Reward List: Hegemon) * Bug Hunter (Reward List: Hegemon)
* Alliance exclusive badges * Alliance exclusive badges
* Donater / Supporter * Donater / Supporter
* Unknown / error badge
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+8 -3
View File
@@ -337,12 +337,17 @@ pre {
.rainbowText { .rainbowText {
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -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(0.00, red),
color-stop(16%, orange), color-stop(16%, orange),
color-stop(32%, yellow), color-stop(32%, yellow),
color-stop(48%, green), color-stop(48%, green),
color-stop(60%, blue), color-stop(60%, blue),
color-stop(76%, indigo), color-stop(76%, indigo),
color-stop(1.00, violet)); color-stop(1.00, violet)
} );
}
.highlight {
background-color: #1a253a;
}
+32
View File
@@ -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
}
}
+132
View File
@@ -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
};
+6 -6
View File
@@ -4,7 +4,7 @@ require('dotenv').config();
//utilities //utilities
let { log } = require('../common/utilities.js'); 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) => { const equipmentRequest = (connection) => (req, res) => {
//validate the credentials //validate the credentials
@@ -28,7 +28,7 @@ const equipmentRequest = (connection) => (req, res) => {
return; return;
} }
return getOwned(connection, req.body.id, (err, ownedObj) => { return getEquipmentOwned(connection, req.body.id, (err, ownedObj) => {
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();
@@ -56,7 +56,7 @@ const equipmentRequest = (connection) => (req, res) => {
}); });
case 'owned': case 'owned':
return getOwned(connection, req.body.id, (err, obj) => { return getEquipmentOwned(connection, req.body.id, (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 {
@@ -172,10 +172,10 @@ const purchaseRequest = (connection) => (req, res) => {
if (err) throw err; if (err) throw err;
//return the new owned data //return the new owned data
getOwned(connection, req.body.id, (err, results) => { getEquipmentOwned(connection, req.body.id, (err, results) => {
if (err) throw err; 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(); res.end();
log('Purchase made', req.body.id, req.body.token, req.body.type, req.body.name); 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; if (err) throw err;
//return the new owned data //return the new owned data
getOwned(connection, req.body.id, (err, results) => { getEquipmentOwned(connection, req.body.id, (err, results) => {
if (err) throw err; if (err) throw err;
res.status(200).json(Object.assign(results)); res.status(200).json(Object.assign(results));
+5
View File
@@ -66,6 +66,11 @@ app.post('/equipmentrequest', equipment.equipmentRequest(connection));
app.post('/equipmentpurchaserequest', equipment.purchaseRequest(connection)); app.post('/equipmentpurchaserequest', equipment.purchaseRequest(connection));
app.post('/equipmentsellrequest', equipment.sellRequest(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 //static directories
app.use('/content', express.static(path.resolve(__dirname + '/../public/content')) ); app.use('/content', express.static(path.resolve(__dirname + '/../public/content')) );
app.use('/img', express.static(path.resolve(__dirname + '/../public/img')) ); app.use('/img', express.static(path.resolve(__dirname + '/../public/img')) );
+2 -2
View File
@@ -9,7 +9,7 @@ const getEquipmentStatistics = (cb) => {
return cb(undefined, { 'statistics': require('./equipment_statistics.json') }); 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 = ?;'; let query = 'SELECT name, quantity FROM equipment WHERE accountId = ?;';
connection.query(query, [id], (err, results) => { connection.query(query, [id], (err, results) => {
if (err) throw err; if (err) throw err;
@@ -95,7 +95,7 @@ const logActivity = (connection, id) => {
module.exports = { module.exports = {
getEquipmentStatistics: getEquipmentStatistics, getEquipmentStatistics: getEquipmentStatistics,
getOwned: getOwned, getEquipmentOwned: getEquipmentOwned,
isAttacking: isAttacking, isAttacking: isAttacking,
isSpying: isSpying, isSpying: isSpying,
logActivity: logActivity logActivity: logActivity
+13 -1
View File
@@ -173,7 +173,7 @@ CREATE TABLE IF NOT EXISTS equipmentStolen (
pastSpyingId INTEGER UNSIGNED, pastSpyingId INTEGER UNSIGNED,
name VARCHAR(50), name VARCHAR(50), #TODO: make this NOT NULL
quantity INTEGER, quantity INTEGER,
type VARCHAR(50), 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 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
);
+1
View File
@@ -71,6 +71,7 @@ export default class App extends React.Component {
<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='/spyinglog' component={() => import('./pages/spying_log.jsx')} /> <LazyRoute path='/spyinglog' component={() => import('./pages/spying_log.jsx')} />
<LazyRoute path='/badges' component={() => import('./pages/badge_select.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')} />
+51
View File
@@ -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 (
<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'>Badge Select</h1>
<p className='centered'>Click on your favourite badge!</p>
<BadgeSelectPanel setWarning={this.setWarning.bind(this)} getFetch={ (fn) => this.setState({ fetch: fn }) } />
</div>
</div>
</div>
);
}
setWarning(s) {
this.setState({ warning: s });
}
};
export default BadgeSelect;
+27
View File
@@ -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 (
<img {...this.props} src={`/img/badges/${this.props.filename}`} alt={this.props.name} width={realSize} height={realSize} />
);
}
parseSize(sizeString) {
if (sizeString === 'small') return 12;
if (sizeString === 'medium') return 20;
return 100;
}
};
export default Badge;
+110
View File
@@ -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 (
<p className='panel'>Loading badges...</p>
);
}
//are none selected?
let anySelected = Object.keys(this.state.data.owned).reduce((accumulator, name) => accumulator || this.state.data.owned[name].active, false);
return (
<div className='panel table'>
<div key={name}>
<div className={`panel row${!anySelected ? ' highlight' : ''}`} style={{padding: 10, minHeight: 120}} onClick={ () => this.sendRequest('/badgeselectactiverequest', { name: null }) }>
<p className={'col centered'} style={{alignSelf: 'center'}}>No Badge</p>
</div>
<div className='row'>
<hr className='col mobile show' />
</div>
</div>
{Object.keys(this.state.data.owned).map((name) =>
<div key={name}>
<div className={`panel row${this.state.data.owned[name].active ? ' highlight' : ''}`} style={{padding: 10}} onClick={ () => this.sendRequest('/badgeselectactiverequest', { name: name }) }>
<div className={'col centered'} style={{ minWidth: 110 }}>
<Badge name={name} filename={this.state.data.statistics[name].filename} />
</div>
<p className={'col'} style={{flex: 4, alignSelf: 'center'}}>{this.state.data.statistics[name].description}</p>
</div>
<div className='row'>
<hr className='col mobile show' />
</div>
</div>
)}
</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({ 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;
+1 -1
View File
@@ -13,7 +13,7 @@ class CombatLogRecord extends React.Component {
render() { render() {
return ( return (
<div className='table noCollapse'> <div className='panel table noCollapse'>
<hr /> <hr />
<div className='break' /> <div className='break' />
<div className='row'> <div className='row'>
+2
View File
@@ -30,6 +30,7 @@ class CommonLinks extends React.Component {
<div className='panel'> <div className='panel'>
<p className='mobile centered'><Link to='/profile' onClick={this.props.onClickProfile}>Your Kingdom</Link></p> <p className='mobile centered'><Link to='/profile' onClick={this.props.onClickProfile}>Your Kingdom</Link></p>
<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='/badges' onClick={this.props.onClickBadges}>Your Badges</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='/spyinglog' onClick={this.props.onClickSpyingLog}>Espionage Log</Link></p>
@@ -72,6 +73,7 @@ CommonLinks.propTypes = {
onClickPasswordRecover: PropTypes.func, onClickPasswordRecover: PropTypes.func,
onClickProfile: PropTypes.func, onClickProfile: PropTypes.func,
onClickEquipment: PropTypes.func, onClickEquipment: PropTypes.func,
onClickBadges: PropTypes.func,
onClickLadder: PropTypes.func, onClickLadder: PropTypes.func,
onClickCombatLog: PropTypes.func, onClickCombatLog: PropTypes.func,
onClickSpyingLog: PropTypes.func, onClickSpyingLog: PropTypes.func,
+1 -1
View File
@@ -13,7 +13,7 @@ class SpyingLogRecord extends React.Component {
render() { render() {
return ( return (
<div className='table noCollapse'> <div className='panel table noCollapse'>
<hr /> <hr />
<div className='break' /> <div className='break' />
<div className='row'> <div className='row'>