Merge branch 'account' into develop

This commit is contained in:
ntr 2019-08-17 17:52:12 +10:00
commit ee60167d1e
57 changed files with 3297 additions and 749 deletions

View File

@ -1,6 +1,15 @@
# WORK WORK
## NOW
*PRODUCTION*
* ACP
* essential
* error log
* account lookup w/ pw reset
* nice to have
* bot game grind
* serde serialize privatise
* stripe prod
* mobile styles
@ -10,6 +19,11 @@
* graphs n shit
* acp init
* DO postgres
* make our own toasts / msg pane
* send account_instances on players update
* only clear effects on post_resolve
electrify doesn't work if you ko the construct
## SOON
*SERVER*

9
acp/.babelrc Normal file
View File

@ -0,0 +1,9 @@
{
"presets": [
"es2015",
"react"
],
"plugins": [
["transform-react-jsx", { "pragma":"preact.h" }]
]
}

1501
acp/.eslintrc.js Normal file

File diff suppressed because it is too large Load Diff

5
acp/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
package-lock.json
node_modules/
dist/
.cache/
assets/molecules

19
acp/acp.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>mnml - acp</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black>
<meta name="description" content="mnml pvp tbs">
<meta name="author" content="ntr@smokestack.io">
<link rel="stylesheet" href="./node_modules/izitoast/dist/css/iziToast.min.css"></script>
<link href="https://fonts.googleapis.com/css?family=Jura" rel="stylesheet">
</head>
</head>
<body>
<noscript>js is required to run mnml</noscript>
</body>
<script src="./acp.js"></script>
</html>

18
acp/acp.js Normal file
View File

@ -0,0 +1,18 @@
require('./../client/assets/styles/normalize.css');
require('./../client/assets/styles/skeleton.css');
require('./../client/assets/styles/styles.less');
require('./../client/assets/styles/menu.less');
require('./../client/assets/styles/acp.less');
require('./../client/assets/styles/nav.less');
require('./../client/assets/styles/footer.less');
require('./../client/assets/styles/account.less');
require('./../client/assets/styles/controls.less');
require('./../client/assets/styles/instance.less');
require('./../client/assets/styles/vbox.less');
require('./../client/assets/styles/game.less');
require('./../client/assets/styles/styles.mobile.css');
require('./../client/assets/styles/instance.mobile.css');
// kick it off
require('./src/acp');

49
acp/package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "mnml-client",
"version": "0.2.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "parcel watch acp.html --no-hmr --out-dir /var/lib/mnml/public/current",
"build": "parcel build acp.html",
"lint": "eslint --fix --ext .jsx src/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"dependencies": {
"anime": "^0.1.2",
"animejs": "^3.0.1",
"async": "^2.6.2",
"borc": "^2.0.3",
"docco": "^0.7.0",
"izitoast": "^1.4.0",
"keymaster": "^1.6.2",
"linkstate": "^1.1.1",
"lodash": "^4.17.15",
"node-sass": "^4.12.0",
"parcel": "^1.12.3",
"preact": "^8.4.2",
"preact-compat": "^3.19.0",
"preact-context": "^1.1.3",
"preact-redux": "^2.1.0",
"react-stripe-elements": "^3.0.0",
"redux": "^4.0.0"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-plugin-module-resolver": "^3.2.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"eslint": "^5.6.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-react": "^7.11.1",
"jest": "^18.0.0",
"less": "^3.9.0"
},
"alias": {
"react": "preact-compat",
"react-dom": "preact-compat"
}
}

36
acp/src/acp.game.list.jsx Normal file
View File

@ -0,0 +1,36 @@
const preact = require('preact');
const { Component } = require('preact');
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const actions = require('./actions');
const addState = connect(
function receiveState(state) {
const {
games,
} = state;
return {
games
};
},
);
function AcpGameList(args) {
const {
games,
} = args;
if (!games) return false;
return (
<table>
<tbody>
{games.map((g, i) => <tr key={i}><td>{JSON.stringify(g)}</td></tr>)}
</tbody>
</table>
)
}
module.exports = addState(AcpGameList);

34
acp/src/acp.jsx Normal file
View File

@ -0,0 +1,34 @@
const preact = require('preact');
// const logger = require('redux-diff-logger');
const { Provider, connect } = require('preact-redux');
const { createStore, combineReducers } = require('redux');
const reducers = require('./reducers');
const actions = require('./actions');
const Main = require('./acp.main');
// Redux Store
const store = createStore(
combineReducers(reducers),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
document.fonts.load('16pt "Jura"').then(() => {
const Acp = () => (
<Provider store={store}>
<div id="mnml" class="acp">
<nav>
<h1>acp</h1>
<hr/>
</nav>
<Main />
<aside></aside>
</div>
</Provider>
);
// eslint-disable-next-line
preact.render(<Acp />, document.body);
});

173
acp/src/acp.main.jsx Normal file
View File

@ -0,0 +1,173 @@
const preact = require('preact');
const { Component } = require('preact');
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const actions = require('./actions');
const { postData, errorToast } = require('./../../client/src/utils');
const AcpGameList = require('./acp.game.list');
const AcpUser = require('./acp.user');
const addState = connect(
function receiveState(state) {
const {
account,
user,
msg,
} = state;
return {
account,
user,
msg,
};
},
function receiveDispatch(dispatch) {
function setUser(user) {
dispatch(actions.setUser(user));
}
function setGames(list) {
dispatch(actions.setGames(list));
}
function setMsg(msg) {
dispatch(actions.setMsg(msg));
}
return {
setUser,
setMsg,
setGames,
};
}
);
class AcpMain extends Component {
constructor(props) {
super(props);
this.state = {
id: null,
name: null,
};
}
render(args, state) {
const {
setGames,
setUser,
setMsg,
msg,
} = args;
const {
name,
id,
} = state;
const reset = () => {
setMsg(null);
this.setState({ id: null, name: null });
};
const getUser = () => {
reset();
postData('/acp/user', { id, name })
.then(res => res.json())
.then(data => {
if (data.error) return setMsg(data.error);
setUser(data);
})
.catch(error => errorToast(error));
};
const gameList = () => {
reset();
postData('/acp/game/list', { number: 20 })
.then(res => res.json())
.then(data => {
if (data.error) return setMsg(data.error);
console.log(data);
setGames(data.data);
})
.catch(error => errorToast(error));
};
const gameOpen = () => {
reset();
postData('/acp/game/open')
.then(res => res.json())
.then(data => {
if (data.error) return setMsg(data.error);
console.log(data);
setGames(data);
})
.catch(error => errorToast(error));
};
return (
<main class='menu'>
<div class="top">
<div class="msg">{msg}</div>
<AcpUser />
<AcpGameList />
</div>
<div class="bottom acp list">
<div>
<label for="current">Username:</label>
<input
class="login-input"
type="text"
name="name"
value={this.state.name}
onInput={linkState(this, 'name')}
placeholder="name"
/>
<input
class="login-input"
type="text"
name="userid"
value={this.state.id}
onInput={linkState(this, 'id')}
placeholder="id"
/>
<button
onClick={getUser}>
Search
</button>
</div>
<div>
<label for="current">Game:</label>
<input
class="login-input"
type="text"
name="userid"
value={this.state.id}
onInput={linkState(this, 'id')}
placeholder="id"
/>
<button
onClick={getUser}>
Search
</button>
<button
onClick={gameList}>
Last 20
</button>
<button
onClick={gameOpen}>
Open
</button>
</div>
</div>
</main>
);
}
}
module.exports = addState(AcpMain);

97
acp/src/acp.user.jsx Normal file
View File

@ -0,0 +1,97 @@
const preact = require('preact');
const { Component } = require('preact');
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const { postData, errorToast } = require('./../../client/src/utils');
const actions = require('./actions');
const addState = connect(
function receiveState(state) {
const {
user,
} = state;
return {
user,
};
},
function receiveDispatch(dispatch) {
function setUser(user) {
dispatch(actions.setUser(user));
}
function setMsg(msg) {
dispatch(actions.setMsg(msg));
}
return {
setUser,
setMsg,
};
}
);
function AcpUser(args) {
const {
user,
setUser,
setMsg,
} = args;
const {
credits,
} = this.state;
if (!user) return false;
const reset = () => {
setMsg(null);
this.setState({ credits: null });
};
const addCredits = () => {
reset();
postData('/acp/user/credits', { id: user.id, credits })
.then(res => res.json())
.then(data => {
if (data.error) return setMsg(data.error);
return setUser(data);
})
.catch(error => setMsg(error));
};
return (
<div class="user">
<dl>
<h1>{user.name}</h1>
<dt>Id</dt>
<dd>{user.id}</dd>
<dt>Credits</dt>
<dd>{user.balance}</dd>
<dt>Subscribed</dt>
<dd>{user.subscribed.toString()}</dd>
</dl>
<div>
<label for="current">Add Credits:</label>
<input
class="login-input"
type="number"
name="credits"
value={credits}
onInput={linkState(this, 'credits')}
placeholder="credits"
/>
<button
onClick={addCredits}>
Add Credits
</button>
</div>
<div>
<h2>Constructs</h2>
</div>
</div>
);
}
module.exports = addState(AcpUser);

4
acp/src/actions.jsx Normal file
View File

@ -0,0 +1,4 @@
export const setAccount = value => ({ type: 'SET_ACCOUNT', value });
export const setUser = value => ({ type: 'SET_USER', value });
export const setMsg = value => ({ type: 'SET_MSG', value });
export const setGames = value => ({ type: 'SET_GAMES', value });

18
acp/src/reducers.jsx Normal file
View File

@ -0,0 +1,18 @@
function createReducer(defaultState, actionType) {
return function reducer(state = defaultState, action) {
switch (action.type) {
case actionType:
return action.value;
default:
return state;
}
};
}
/* eslint-disable key-spacing */
module.exports = {
account: createReducer(null, 'SET_ACCOUNT'),
user: createReducer(null, 'SET_USER'),
msg: createReducer(null, 'SET_MSG'),
games: createReducer([], 'SET_GAMES'),
};

View File

@ -12,3 +12,9 @@ cd $MNML_PATH/client
rm -rf dist
npm i
npm run build
echo "Building acp version $VERSION"
cd $MNML_PATH/acp
rm -rf dist
npm i
npm run build

View File

@ -6,8 +6,12 @@ MNML_PATH=$(realpath "$DIR/../")
VERSION=$(<"$MNML_PATH/VERSION")
SERVER_BIN_DIR="/usr/local/mnml/bin"
CLIENT_DIST_DIR="/var/lib/mnml/client"
CLIENT_PUBLIC_DIR="/var/lib/mnml/public/current"
CLIENT_PUBLIC_DIR="/var/lib/mnml/public/client"
ACP_DIST_DIR="/var/lib/mnml/acp"
ACP_PUBLIC_DIR="/var/lib/mnml/public/acp"
# server updates
echo "syncing server $VERSION "
@ -19,6 +23,11 @@ ssh -q mnml ls -lah "$SERVER_BIN_DIR"
echo "syncing client $VERSION"
rsync -a --delete --delete-excluded "$MNML_PATH/client/dist/" mnml:"$CLIENT_DIST_DIR/$VERSION/"
ssh -q mnml ln -nfs "$CLIENT_DIST_DIR/$VERSION" "$CLIENT_PUBLIC_DIR"
# acp updates
echo "syncing acp $VERSION"
rsync -a --delete --delete-excluded "$MNML_PATH/acp/dist/" mnml:"$ACP_DIST_DIR/$VERSION/"
ssh -q mnml ln -nfs "$ACP_DIST_DIR/$VERSION" "$ACP_PUBLIC_DIR"
ssh -q mnml ls -lah "/var/lib/mnml/public"
echo "restarting mnml service"

View File

@ -0,0 +1,55 @@
@import 'colours.less';
.account {
margin-top: 2em;
grid-area: bottom;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 0 1em;
button {
display: block;
// height: 3em;
width: 75%;
}
input {
width: 75%;
height: 3em;
display: block;
}
.list {
letter-spacing: 0.25em;
text-transform: uppercase;
figure {
font-size: 125%;
display: flex;
flex-flow: column;
button {
width: 100%;
}
}
}
}
.stripe-btn {
background: @yellow;
color: black;
border-radius: 2px;
border-width: 0;
&:active, &:focus, &:hover {
color: black;
}
&[disabled] {
border: 1px solid @yellow;
color: @yellow;
background: black;
}
}

View File

@ -0,0 +1,24 @@
#mnml.acp {
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
.bottom {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.top {
padding: 0;
}
input {
display: block;
}
.user {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}

View File

@ -4,6 +4,7 @@
@blue: #3050f8;
@white: #f5f5f5; // whitesmoke
@purple: #9355b5; // 6lack - that far cover
@yellow: #ffa100;
@black: black;
@gray: #222;
@ -30,3 +31,33 @@ svg {
stroke: @blue;
}
}
.green {
color: @green;
stroke: @green;
}
.red {
color: @red;
stroke: @red;
}
.red-fill {
fill: @red;
}
.blue {
color: @blue;
stroke: @blue;
stroke-linecap: round;
}
.gray {
color: #333;
stroke: #333;
}
.white {
color: @white;
stroke: @white;
}

View File

@ -1,10 +1,22 @@
aside {
grid-area: ctrl;
display: grid;
grid-template-areas:
"timer controls"
"timer controls"
"timer controls";
grid-template-columns: min-content 1fr;
grid-template-rows: 1fr 1fr 1fr;
grid-gap: 0.5em 0;
padding: 1em;
padding: 1em 1em 1em 0;
// fix chrome being inconsistent
table {
height: 100%;
}
div {
text-align: right;
@ -28,6 +40,30 @@ aside {
border-color: forestgreen;
}
}
.timer-container {
grid-area: timer;
display: flex;
flex: 1 1 100%;
height: 100%;
min-height: 100%;
width: 0.25em;
max-width: 0.25em;
margin: 0 1em 0 0;
border: none;
}
.timer {
background: whitesmoke;
transition-property: all;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
}
}
.ready-btn:hover, .ready-btn:focus, .ready-btn:active {

View File

@ -0,0 +1,51 @@
footer {
display: none;
flex-flow: row wrap;
grid-area: footer;
margin: 0;
button {
margin: 0;
border: none;
background: #222;
font-size: 1.5em;
padding: 0.25em;
&[disabled] {
color: #444;
}
&:not(:last-child) {
background: #222;
border-right: 1px solid black;
}
.ready {
background: forestgreen;
color: black;
}
}
.timer-container {
display: flex;
flex: 1 1 100%;
width: 100%;
height: 0.25em;
max-height: 0.25em;
border: none;
margin: 1em 0 0 0;
}
.timer {
background: whitesmoke;
transition-property: all;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
}
}
#nav-btn, #instance-nav {
display: none;
}

View File

@ -1,3 +1,4 @@
@import 'colours.less';
/* GAME */
.game {
@ -322,15 +323,6 @@
pointer-events: none;
}
/*@keyframes rotate {
0% {
transform: rotateY(0deg);
}
100% {
transform: rotateY(50deg);
}
}
*/
.resolving .skills button {
opacity: 0;
}
@ -338,80 +330,9 @@
.skill-animation {
opacity: 0;
stroke-width: 5px;
height: 5em;
}
/*
COMBAT ANIMATIONS
*/
/* ----------------------------------------------
* Generated by Animista on 2019-5-21 11:38:30
* w: http://animista.net, t: @cssanimista
* ---------------------------------------------- */
/*.attack-cast {
-webkit-animation: attack-cast 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
animation: attack-cast 0.5s cubic-bezier(0.550, 0.085, 0.680, 0.530) both;
}
@-webkit-keyframes attack-cast {
0% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
100% {
-webkit-transform: translateY(-5em);
transform: translateY(-5em);
opacity: 0;
}
}
@keyframes attack-cast {
0% {
-webkit-transform: translateY(0);
transform: translateY(0);
opacity: 1;
}
100% {
-webkit-transform: translateY(-5em);
transform: translateY(-5em);
opacity: 0;
}
}
.attack-hit {
-webkit-animation: attack-hit 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
animation: attack-hit 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
@-webkit-keyframes attack-hit {
0% {
-webkit-transform: translateY(-5em) translateX(5em);
transform: translateY(-5em) translateX(5em);
opacity: 0;
}
100% {
-webkit-transform: translateY(0) translateX(0);
transform: translateY(0) translateX(0);
opacity: 1;
}
}
@keyframes attack-hit {
0% {
-webkit-transform: translateY(-5em) translateX(5em);
transform: translateY(-5em) translateX(5em);
opacity: 0;
}
100% {
-webkit-transform: translateY(0) translateX(0);
transform: translateY(0) translateX(0);
opacity: 1;
}
}
*/
@media (max-width: 1000px) {
.game {
grid-template-areas:

View File

@ -426,6 +426,12 @@
margin-top: 1em;
grid-area: playername;
}
.team {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
max-height: 100%;
}
}
/* Mobile Nav*/

View File

@ -0,0 +1,90 @@
@import 'colours.less';
.menu {
height: 100%;
display: grid;
grid-template-rows: minmax(min-content, 2fr) 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"top"
"bottom";
.top {
grid-area: top;
padding: 0 0 0.5em 2em;
border-bottom: 0.1em solid #222;
box-sizing: border-box;
}
.bottom {
grid-area: bottom;
}
.team {
display: grid;
grid-area: top;
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
max-height: 100%;
.team-select:not(:nth-child(3n)) {
margin-right: 0.5em;
}
.construct {
flex: 1 1 33%;
display: flex;
flex-flow: column;
text-align: center;
justify-content: center;
border: 1px solid black;
transition-property: border;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
button:not(:last-child) {
margin-bottom: 1em;
}
.avatar {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
height: 100%;
}
}
}
.inventory {
margin-top: 2em;
grid-area: bottom;
display: grid;
grid-template-columns: 1fr 1fr;
h1 {
margin-bottom: 0.5em;
}
.list {
letter-spacing: 0.25em;
text-transform: uppercase;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 1em;
flex-flow: row wrap;
align-items: flex-end;
}
figure {
font-size: 125%;
display: flex;
flex-flow: column;
}
}
}

View File

@ -0,0 +1,99 @@
nav {
grid-area: nav;
padding-left: 2em;
margin-right: 2em;
max-height: 100%;
h1 {
margin-bottom: 0.5em;
letter-spacing: 0.05em;
}
h2:first-child {
margin: 0;
}
h2 {
margin-top: 2em;
}
hr {
margin: 1em 0;
border-color: #444;
}
button {
text-overflow: ellipsis;
display: block;
color: #888;
flex: 1 1 100%;
padding: 0;
margin: 0;
border-width: 0px;
}
button.active {
color: whitesmoke;
}
button[disabled], button[disabled]:hover {
color: #333;
text-decoration: none;
}
button:hover {
color: whitesmoke;
text-decoration: underline;
}
button:focus, button:active {
color: whitesmoke;
}
.account-info {
display: flex;
}
.account-header {
letter-spacing: 0.05em;
flex: 1;
display: inline;
}
.account-info svg {
margin: 0.5em 0 0 1em;
height: 1em;
background-color: black;
stroke: whitesmoke;
}
.ping-path {
fill: none;
stroke-width: 4px;
stroke-dasharray: 121, 242;
animation: saw 2s infinite linear;
transition-property: stroke-color;
transition-duration: 0.25s;
transition-timing-function: ease;
}
.ping-text {
margin-left: 1em;
min-width: 3em;
display: inline-block;
}
.play-btn {
font-size: 150%;
}
}
@keyframes saw {
0% {
stroke-dashoffset: 363;
}
100% {
stroke-dashoffset: 0;
}
}

View File

@ -1,11 +1,6 @@
@import 'colours.less';
/*
GLOBAL
*/
html, body, #mnml {
/*width: 100%;*/
margin: 0;
background-color: black;
@ -48,6 +43,9 @@ html {
h1 {
font-size: 2em;
margin: 0;
margin-bottom: 0.5em;
letter-spacing: 0.05em;
}
h2 {
@ -66,6 +64,7 @@ h4 {
}
hr {
color: #222;
margin: 1.5em 0;
width: 100%;
}
@ -85,70 +84,11 @@ figure {
"nav main ctrl";
}
nav {
grid-area: nav;
padding-left: 2em;
margin-right: 2em;
max-height: 100%;
}
nav h2:first-child {
margin: 0;
}
nav h2 {
margin-top: 2em;
}
nav hr {
margin: 1em 0;
border-color: #444;
}
nav button {
text-overflow: ellipsis;
display: block;
color: #888;
flex: 1 1 100%;
padding: 0;
margin: 0;
border-width: 0px;
}
nav button.active {
color: whitesmoke;
}
nav button[disabled], nav button[disabled]:hover {
color: #333;
text-decoration: none;
}
nav button:hover {
color: whitesmoke;
text-decoration: underline;
}
nav button:focus, nav button:active {
color: whitesmoke;
}
main {
padding: 1em;
grid-area: main;
}
tr.right:focus, tr.right:hover {
box-shadow: inset -0.5em 0 0 0 whitesmoke;
}
tr {
transition-property: color, background;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
}
button, input {
font-family: 'Jura';
color: whitesmoke;
@ -166,22 +106,34 @@ button, input {
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
}
button:hover {
color: whitesmoke;
border-color: @gray-hover;
}
&:hover {
color: whitesmoke;
border-color: @gray-hover;
}
button:focus {
/*colour necesary to bash skellington*/
color: @gray-focus;
border-color: @gray-focus;
&:focus {
/*colour necesary to bash skellington*/
color: @gray-focus;
border-color: @gray-focus;
}
}
a {
color: whitesmoke;
font-size: 150%;
text-decoration: none;
&:hover {
color: whitesmoke;
border-color: @gray-hover;
}
&:focus {
/*colour necesary to bash skellington*/
color: @gray-focus;
border-color: @gray-focus;
}
}
svg {
@ -191,19 +143,6 @@ svg {
height: 2em;
}
.skill-animation {
height: 5em;
}
.team .avatar {
object-fit: contain;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
pointer-events: none;
}
table {
table-layout: fixed;
width: 100%;
@ -246,64 +185,19 @@ button[disabled] {
border-color: #222;
}
/*
COLOURS
*/
.green {
color: #1FF01F;
stroke: #1FF01F;
}
.red {
color: #a52a2a;
stroke: #a52a2a;
}
.red-fill {
fill: #a52a2a;
}
.blue {
color: #3050f8;
stroke: #3050f8;
stroke-linecap: round;
}
.yellow {
color: #FFD123;
stroke: #FFD123;
}
.purple {
color: #A25AC1;
stroke: #A25AC1;
}
.cyan {
color: #6AD1BF;
stroke: #6AD1BF;
}
.gray {
color: #333;
stroke: #333;
}
.white {
color: whitesmoke;
stroke: whitesmoke;
}
/*
LOGIN
*/
.login {
width: 25%;
width: 50%;
display: flex;
flex-flow: column;
margin-bottom: 2em;
h2 {
margin-bottom: 0.5em;
}
}
#mnml input, #mnml select {
@ -316,242 +210,6 @@ button[disabled] {
border-color: #888;
}
/*
account
*/
header {
display: flex;
flex-flow: row;
grid-area: hd;
margin-bottom: 1.5em;
}
.account {
margin: 1em 0;
}
.header-title {
flex: 1;
letter-spacing: 0.05em;
}
.account-status {
display: flex;
}
.account-header {
letter-spacing: 0.05em;
flex: 1;
display: inline;
}
.account-status svg {
margin: 0.5em 0 0 1em;
height: 1em;
background-color: black;
stroke: whitesmoke;
}
.ping-path {
fill: none;
stroke-width: 4px;
stroke-dasharray: 121, 242;
animation: saw 2s infinite linear;
transition-property: stroke-color;
transition-duration: 0.25s;
transition-timing-function: ease;
}
.ping-text {
margin-left: 1em;
min-width: 3em;
display: inline-block;
}
@keyframes saw {
0% {
stroke-dashoffset: 363;
}
100% {
stroke-dashoffset: 0;
}
}
/*
TEAM
*/
.team {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
max-height: 100%;
}
.menu-construct {
margin: 0.5em;
flex: 1 1 auto;
display: flex;
flex-flow: column;
text-align: center;
justify-content: center;
border: 1px solid black;
transition-property: border;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
button:not(:last-child) {
margin-bottom: 1em;
}
.avatar {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
pointer-events: none;
height: 100%;
}
}
.spawn-btn.menu-construct {
border: 1px solid #333;
color: #333;
display: flex;
flex-flow: row wrap;
align-content: center;
justify-content: center;
}
.spawn-btn.menu-construct.selected {
color: whitesmoke;
}
.spawn-btn.menu-construct:hover {
border: 1px solid whitesmoke;
}
.spawn-btn input {
flex: 1 1 100%;
width: 100%;
margin: 1em;
}
.spawn-btn button {
flex: 1 1 100%;
margin: 0.5em 1em;
padding: 0 0.5em;
}
.spawn-btn input[disabled], .spawn-btn button[disabled] {
opacity: 0
}
.play {
height: 100%;
display: grid;
grid-template-rows: minmax(min-content, 2fr) 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"team"
"inventory";
.team {
grid-area: team;
/* poor man's <hr>*/
padding: 0.5em 2em 0 0;
border-bottom: 0.1em solid #444;
}
.menu-construct {
flex: 1 0 33%;
}
.inventory {
margin: 2em 2em 0 0;
grid-area: inventory;
display: grid;
grid-template-columns: 1fr 1fr;
.list {
letter-spacing: 0.25em;
text-transform: uppercase;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 1em;
flex-flow: row wrap;
align-items: flex-end;
}
figure {
font-size: 125%;
display: flex;
flex-flow: column;
}
}
}
.menu-instance-btn {
flex: 1 1 100%;
font-size: 1.2em;
}
.stripe-btn {
width: 100%;
padding: 0 0.5em;
margin: 0.25em 0;
background: whitesmoke;
color: black;
border-radius: 2px;
}
.stripe-btn:hover {
color: black;
}
.play-btn {
font-size: 150%;
}
.refresh-btn {
border: 1px solid #222;
float: right;
font-size: 1.5em;
}
.create-form {
grid-area: create;
flex: 1;
justify-self: flex-end;
display: flex;
flex-flow: row wrap;
margin-bottom: 1.5em;
}
.create-form button {
flex: 1;
font-size: 1.5em;
}
.create-form button:first-child {
margin-right: 1em;
}
.create-form button:hover, .create-form button:focus {
border-color: whitesmoke;
text-decoration: none;
}
figure.gray {
color: #333;
stroke-color: #333;
@ -562,56 +220,9 @@ figure.gray {
fill: none;
}
main .top button {
width: 100%;
}
.timer-container {
display: flex;
flex: 1 1 100%;
width: 100%;
height: 0.25em;
max-height: 0.25em;
border: none;
margin: 1em 0 0 0;
}
.timer {
background: whitesmoke;
transition-property: all;
transition-duration: 0.25s;
transition-delay: 0;
transition-timing-function: ease;
}
footer {
display: none;
flex-flow: row wrap;
grid-area: footer;
margin: 0;
}
footer button {
margin: 0;
border: none;
background: #222;
font-size: 1.5em;
padding: 0.25em;
}
footer button:disabled {
color: #444;
}
footer button:not(:last-child) {
background: #222;
border-right: 1px solid black;
}
footer button .ready {
background: forestgreen;
color: black;
.credits {
color: @yellow;
font-weight: 800;
}
@media (max-width: 1500px) {
@ -624,10 +235,6 @@ footer button .ready {
}
}
#nav-btn, #instance-nav {
display: none;
}
.mobile-title {
display: none;
}
}

View File

@ -50,7 +50,7 @@
main {
overflow-x: hidden;
padding: 0;
padding: 0 0.5em;
}
.login {

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>mnml.gg</title>
<title>mnml - abstract strategy</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name=apple-mobile-web-app-capable content=yes>

View File

@ -1,8 +1,12 @@
require('./assets/styles/styles.less');
require('./assets/styles/menu.less');
require('./assets/styles/nav.less');
require('./assets/styles/footer.less');
require('./assets/styles/account.less');
require('./assets/styles/controls.less');
require('./assets/styles/instance.less');
require('./assets/styles/vbox.less');
require('./assets/styles/game.less');
require('./assets/styles/controls.less');
require('./assets/styles/styles.mobile.css');
require('./assets/styles/instance.mobile.css');

View File

@ -21,6 +21,7 @@
"docco": "^0.7.0",
"izitoast": "^1.4.0",
"keymaster": "^1.6.2",
"linkstate": "^1.1.1",
"lodash": "^4.17.15",
"node-sass": "^4.12.0",
"parcel": "^1.12.3",

View File

@ -0,0 +1,151 @@
const preact = require('preact');
const { Component } = require('preact')
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const SpawnButton = require('./spawn.button');
const { postData, errorToast, infoToast } = require('../utils');
const actions = require('../actions');
const addState = connect(
function receiveState(state) {
const {
account,
ping,
ws,
} = state;
function setPassword(current, password) {
postData('/account/password', { current, password })
.then(res => res.json())
.then(data => {
if (!data.success) return errorToast(data.error_message);
infoToast('Password changed. Reloading...')
setTimeout(() => window.location.reload(), 5000);
})
.catch(error => errorToast(error));
}
function logout() {
postData('/account/logout').then(() => window.location.reload(true));
}
function sendConstructSpawn(name) {
return ws.sendMtxConstructSpawn(name);
}
return {
account,
ping,
logout,
setPassword,
sendConstructSpawn,
};
},
);
class AccountStatus extends Component {
constructor(props) {
super(props);
this.state = {
setPassword: { current: '', password: '', confirm: ''},
};
}
render(args) {
const {
account,
ping,
logout,
setPassword,
sendConstructSpawn,
} = args;
if (!account) return null;
const passwordsEqual = () =>
this.state.setPassword.password === this.state.setPassword.confirm;
const setPasswordDisabled = () => {
const { current, password, confirm } = this.state.setPassword;
return !(passwordsEqual() && password && current && confirm);
}
return (
<section class='account'>
<div>
<h1>{account.name}</h1>
<dl>
<dt>Subscription</dt>
<dd>{account.subscribed ? 'some date' : 'unsubscribed'}</dd>
</dl>
<button><a href={`mailto:support@mnml.gg?subject=Account%20Support:%20${account.name}`}> support</a></button>
<button onClick={() => logout()}>Logout</button>
</div>
<div>
<label for="email">Email:</label>
<dl>
<dt>Current Email</dt>
<dd>{account.email ? account.email : 'No email set'}</dd>
<dt>Status</dt>
<dd>{account.email_confirmed ? 'Confirmed' : 'Unconfirmed'}</dd>
</dl>
<input
class="login-input"
type="email"
name="email"
placeholder="new email"
/>
<button>Update</button>
</div>
<div>
<label for="current">Password:</label>
<input
class="login-input"
type="password"
name="current"
value={this.state.setPassword.current}
onInput={linkState(this, 'setPassword.current')}
placeholder="current"
/>
<input
class="login-input"
type="password"
name="new"
value={this.state.setPassword.password}
onInput={linkState(this, 'setPassword.password')}
placeholder="new password"
/>
<input
class="login-input"
type="password"
name="confirm"
value={this.state.setPassword.confirm}
onInput={linkState(this, 'setPassword.confirm')}
placeholder="confirm"
/>
<button
disabled={setPasswordDisabled()}
onClick={() => setPassword(this.state.setPassword.current, this.state.setPassword.password)}>
Set Password
</button>
</div>
<div class="list">
<figure>
<figcaption>spawn new construct</figcaption>
<button onClick={() => sendConstructSpawn()} type="submit">
¤50
</button>
</figure>
</div>
</section>
);
}
}
module.exports = addState(AccountStatus);

View File

@ -0,0 +1,33 @@
const { connect } = require('preact-redux');
const preact = require('preact');
const Team = require('./team');
const AccountManagement = require('./account.management');
const addState = connect(
function receiveState(state) {
const {
ws,
account,
} = state;
return {
account,
};
},
);
function Account(args) {
const {
account,
} = args;
return (
<main class="menu">
<Team />
<AccountManagement />
</main>
);
}
module.exports = addState(Account);

View File

@ -1,9 +1,7 @@
const { connect } = require('preact-redux');
const preact = require('preact');
const { Elements, injectStripe } = require('react-stripe-elements');
const { saw } = require('./shapes');
const { postData } = require('./../utils');
const actions = require('../actions');
function pingColour(ping) {
@ -12,55 +10,6 @@ function pingColour(ping) {
return 'red';
}
function BitsBtn(args) {
const {
stripe,
account,
} = args;
function subscribeClick(e) {
stripe.redirectToCheckout({
items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}],
successUrl: 'http://localhost/api/payments/success',
cancelUrl: 'http://localhost/api/payments/cancel',
clientReferenceId: account.id
});
}
function bitsClick(e) {
stripe.redirectToCheckout({
items: [{sku: 'sku_FHUfNEhWQaVDaT', quantity: 1}],
successUrl: 'http://localhost/payments/success',
cancelUrl: 'http://localhost/payments/cancel',
clientReferenceId: account.id
});
}
const subscription = account.subscribed
? <h3 class="account-header">Subscribed</h3>
: <button
onClick={subscribeClick}
class="stripe-btn"
role="link">
Subscribe
</button>;
return (
<div>
<div id="error-message"></div>
{subscription}
<button
onClick={bitsClick}
class="stripe-btn"
role="link">
Get Credits
</button>
</div>
);
}
const StripeBitsBtn = injectStripe(BitsBtn);
const addState = connect(
function receiveState(state) {
const {
@ -68,23 +17,28 @@ const addState = connect(
ping,
} = state;
function logout() {
postData('/logout').then(() => window.location.reload(true));
}
return {
account,
ping,
logout,
};
},
function receiveDispatch(dispatch) {
function selectConstructs() {
return dispatch(actions.setNav('team'));
function accountPage() {
dispatch(actions.setGame(null));
dispatch(actions.setInstance(null));
dispatch(actions.setCombiner([]));
dispatch(actions.setReclaiming(false));
dispatch(actions.setActiveSkill(null));
dispatch(actions.setActiveConstruct(null));
dispatch(actions.setInfo(null));
dispatch(actions.setItemEquip(null));
dispatch(actions.setItemUnequip([]));
dispatch(actions.setVboxHighlight([]));
return dispatch(actions.setNav('account'));
}
return {
selectConstructs,
accountPage,
};
}
);
@ -94,25 +48,20 @@ function AccountStatus(args) {
const {
account,
ping,
logout,
selectConstructs,
accountPage,
} = args;
if (!account) return null;
return (
<div class="account">
<div class="account-status">
<div class="account-status">
<div class="account-info">
<h2 class="account-header">{account.name}</h2>
{saw(pingColour(ping))}
<div class="ping-text">{ping}ms</div>
</div>
<h3 class="account-header">{`¤${account.balance}`}</h3>
<Elements>
<StripeBitsBtn account={account} />
</Elements>
<button onClick={() => selectConstructs()}>Constructs </button>
<button onClick={() => logout()}>Logout</button>
<h3 class="account-header credits">{`¤${account.balance}`}</h3>
<button onClick={() => accountPage()}> account</button>
</div>
);
}

View File

@ -10,12 +10,14 @@ const addState = connect(
function receiveState(state) {
const {
ws,
account,
game,
instance,
nav,
} = state;
return {
account,
game,
instance,
nav,
@ -26,15 +28,18 @@ const addState = connect(
function Controls(args) {
const {
game,
account,
instance,
nav,
sendGameReady,
} = args;
if (!account) return false;
if (game) return <GameCtrl />;
if (instance) return <InstanceCtrl />;
if (nav === 'play' || !nav) return <PlayCtrl />
if (nav === 'team' || !nav) return <TeamCtrl />
if (nav === 'team' || nav === 'account') return <TeamCtrl />
return false;
}

View File

@ -20,16 +20,22 @@ const addState = connect(
return ws.sendGameReady(game.id);
}
function getInstanceState() {
return ws.sendInstanceState(game.instance);
}
return {
game,
sendReady,
account,
getInstanceState,
animating,
};
},
function receiveDispatch(dispatch) {
function quit() {
dispatch(actions.setNav('transition'));
dispatch(actions.setGame(null));
dispatch(actions.setInstance(null));
}
@ -74,6 +80,7 @@ function Controls(args) {
animating,
sendReady,
quit,
getInstanceState,
} = args;
if (!game) return false;
@ -81,11 +88,43 @@ function Controls(args) {
const opponent = game.players.find(t => t.id !== account.id);
const player = game.players.find(t => t.id === account.id);
function quitClick() {
getInstanceState();
quit();
}
const zero = Date.parse(game.phase_start);
const now = Date.now();
const end = Date.parse(game.phase_end);
const timerPct = game.phase_end
? ((now - zero) / (end - zero) * 100)
: 100;
const displayColour = !game.phase_end
? '#222'
: player.ready
? 'forestgreen'
: timerPct > 80
? 'red'
: 'whitesmoke';
const timerStyles = {
height: `${timerPct > 100 ? 100 : timerPct}%`,
background: displayColour,
};
const timer = (
<div class="timer-container">
<div class="timer" style={timerStyles} >&nbsp;</div>
</div>
);
const readyBtn = <button disabled={animating} class="ready" onClick={() => sendReady()}>Ready</button>;
const quitBtn = <button disabled={animating} onClick={() => quit()}>Back</button>;
const quitBtn = <button disabled={animating} onClick={quitClick}>Back</button>;
return (
<aside class="controls">
{timer}
{scoreboard(game, opponent, true)}
{game.phase === 'Finish' ? quitBtn : readyBtn}
{scoreboard(game, player)}

View File

@ -66,8 +66,35 @@ function Controls(args) {
const opponent = instance.players.find(t => t.id !== account.id);
const player = instance.players.find(t => t.id === account.id);
const zero = Date.parse(instance.phase_start);
const now = Date.now();
const end = Date.parse(instance.phase_end);
const timerPct = instance.phase_end
? ((now - zero) / (end - zero) * 100)
: 100;
const displayColour = !instance.phase_end
? '#222'
: player.ready
? 'forestgreen'
: timerPct > 80
? 'red'
: 'whitesmoke';
const timerStyles = {
height: `${timerPct > 100 ? 100 : timerPct}%`,
background: displayColour,
};
const timer = (
<div class="timer-container">
<div class="timer" style={timerStyles} >&nbsp;</div>
</div>
);
return (
<aside class="controls">
{timer}
{scoreboard(instance, opponent, true)}
<button class="ready" onClick={() => sendReady()}>Ready</button>
{scoreboard(instance, player)}

View File

@ -1,8 +1,11 @@
const { connect } = require('preact-redux');
const { Elements } = require('react-stripe-elements');
const preact = require('preact');
const toast = require('izitoast');
const actions = require('./../actions');
const StripeBtns = require('./stripe.buttons');
const addState = connect(
function receiveState(state) {
@ -63,15 +66,18 @@ function Inventory(args) {
return (
<div class="inventory">
<div>
<h1>¤ {account.balance}</h1>
<h1>Shop</h1>
<Elements>
<StripeBtns account={account} />
</Elements>
<div class='list'>
{shop.owned.map(useMtx)}
{shop.available.map(availableMtx)}
</div>
</div>
<div>
<h1>Shop</h1>
<h1 class="credits">¤ {account.balance}</h1>
<div class='list'>
{shop.available.map(availableMtx)}
{shop.owned.map(useMtx)}
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@
const preact = require('preact');
const { Component } = require('preact')
const { connect } = require('preact-redux');
const linkState = require('linkstate').default;
const { postData, errorToast } = require('../utils');
@ -11,10 +12,10 @@ const addState = connect(
ws
} = state;
function submitLogin(name, password) {
postData('/login', { name, password })
postData('/account/login', { name, password })
.then(res => res.json())
.then(data => {
if (!data.success) return errorToast(data.error_message);
if (data.error) return errorToast(data.error);
console.log(data.response);
ws.connect();
})
@ -22,10 +23,10 @@ const addState = connect(
}
function submitRegister(name, password, code) {
postData('/register', { name, password, code })
postData('/account/register', { name, password, code })
.then(res => res.json())
.then(data => {
if (!data.success) return errorToast(data.error_message);
if (data.error) return errorToast(data.error);
console.log(data.response);
ws.connect();
})
@ -43,87 +44,122 @@ class Login extends Component {
constructor(props) {
super(props);
this.state = { name: '', password: '', code: ''};
this.state = {
login: { name: '', password: '', code: ''},
register: { name: '', password: '', confirm: '', code: ''},
};
this.nameInput = this.nameInput.bind(this);
this.passwordInput = this.passwordInput.bind(this);
this.codeInput = this.codeInput.bind(this);
this.loginSubmit = this.loginSubmit.bind(this);
this.registerSubmit = this.registerSubmit.bind(this);
}
nameInput(event) {
this.setState({ name: event.target.value });
}
passwordInput(event) {
this.setState({ password: event.target.value });
}
codeInput(event) {
this.setState({ code: event.target.value });
}
loginSubmit(event) {
event.preventDefault();
console.log(this.state);
this.props.submitLogin(this.state.name, this.state.password);
this.setState({ name: '', password: '' });
this.props.submitLogin(this.state.login.name, this.state.login.password);
this.setState({ login: { name: '', password: '' }});
}
registerSubmit(event) {
event.preventDefault();
this.props.submitRegister(this.state.name, this.state.password, this.state.code);
console.log(this.state);
this.setState({ name: '', password: '', code: ''});
this.props.submitRegister(this.state.register.name, this.state.register.password, this.state.register.code);
this.setState({ register: { name: '', password: '', confirm: '', code: ''}});
}
render() {
const registerConfirm = () =>
this.state.register.password === this.state.register.confirm;
const loginDisabled = () => {
const { password, name } = this.state.login;
return !(password && name);
}
const registerDisabled = () => {
const { password, name, code } = this.state.register;
return !(registerConfirm() && password && name && code);
}
return (
<main>
<h1 class="mobile-title" >mnml.gg</h1>
<h1>mnml.gg</h1>
<div class="login">
<div>mnml is an abstract turn based strategy game</div>
<div>free to play</div>
<div>no email required</div>
</div>
<div class="login">
<h2>Login</h2>
<label for="username">Username</label>
<input
class="login-input"
type="email"
placeholder="username"
tabIndex={1}
value={this.state.name}
onInput={this.nameInput}
value={this.state.login.name}
onInput={linkState(this, 'login.name')}
/>
<label for="password">Password</label>
<input
class="login-input"
type="password"
placeholder="password"
tabIndex={2}
value={this.state.password}
onInput={this.passwordInput}
/>
<input
class="login-input"
type="text"
placeholder="code"
tabIndex={3}
value={this.state.code}
onInput={this.codeInput}
value={this.state.login.password}
onInput={linkState(this, 'login.password')}
/>
<button
class="login-btn"
tabIndex={4}
disabled={loginDisabled()}
onClick={this.loginSubmit}>
Login
</button>
</div>
<div class="login">
<h2>Register</h2>
<label for="username">Username</label>
<input
class="login-input"
type="email"
placeholder="username"
value={this.state.register.name}
onInput={linkState(this, 'register.name')}
/>
<label for="password">Password - min 12 chars</label>
<input
class="login-input"
type="password"
placeholder="password"
value={this.state.register.password}
onInput={linkState(this, 'register.password')}
/>
<label for="confirm">Confirm Password</label>
<input
class={`${registerConfirm() ? '' : 'red'} login-input`}
type="password"
placeholder="confirm"
value={this.state.register.confirm}
onInput={linkState(this, 'register.confirm')}
/>
<label for="code">Access Code</label>
<input
class="login-input"
type="text"
placeholder="code"
value={this.state.register.code}
onInput={linkState(this, 'register.code')}
/>
<button
class="login-btn"
tabIndex={5}
disabled={registerDisabled()}
onClick={this.registerSubmit}>
Register
</button>
<button
class="login-btn"
onClick={() => document.location.assign('https://discord.gg/YJJgurM')}>
Discord + Invites
Discord + Codes
</button>
</div>
</main>

View File

@ -7,6 +7,7 @@ const Game = require('./game');
const Instance = require('./instance.component');
const Team = require('./team');
const Play = require('./play');
const Account = require('./account.page');
const addState = connect(
state => {
@ -38,6 +39,7 @@ function Main(props) {
if (nav === 'transition') return false;
if (nav === 'play') return <Play />;
if (nav === 'team') return <Team />;
if (nav === 'account') return <Account />;
return (
<Play />

View File

@ -69,6 +69,8 @@ function Nav(args) {
hideNav,
} = args;
if (!account) return false;
function navTo(p) {
return setNav(p);
}
@ -90,6 +92,7 @@ function Nav(args) {
const canJoin = team.some(c => !c);
return (
<nav onClick={hideNav} >
<h1 class="header-title">mnml.gg</h1>

View File

@ -27,7 +27,8 @@ function JoinButtons(args) {
} = args;
return (
<aside>
<aside class='play-ctrl'>
<div class="timer-container"></div>
<button
class='pvp ready'
onClick={() => sendInstanceQueue()}

View File

@ -97,7 +97,7 @@ function Play(args) {
if (mtxActive === 'Rename') return setConstructRename(construct.id);
return sendConstructAvatarReroll(construct.id);
}}
class="menu-construct" >
class="construct">
<ConstructAvatar construct={construct} />
{constructName}
{confirm}
@ -107,8 +107,8 @@ function Play(args) {
});
return (
<main class="play">
<div class="team">
<main class="menu">
<div class="team top">
{constructPanels}
</div>
<Inventory />

View File

@ -0,0 +1,54 @@
const preact = require('preact');
const { injectStripe } = require('react-stripe-elements');
function BitsBtn(args) {
const {
stripe,
account,
} = args;
function subscribeClick() {
stripe.redirectToCheckout({
items: [{ plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1 }],
successUrl: 'http://localhost',
cancelUrl: 'http://localhost',
clientReferenceId: account.id,
});
}
function bitsClick() {
stripe.redirectToCheckout({
items: [{ sku: 'sku_FHUfNEhWQaVDaT', quantity: 1 }],
successUrl: 'http://localhost',
cancelUrl: 'http://localhost',
clientReferenceId: account.id,
});
}
const subscription = account.subscribed
? <button
class="stripe-btn"
disabled>
Subscribed
</button>
: <button
onClick={subscribeClick}
class="stripe-btn"
role="link">
Subscribe
</button>;
return (
<div class='list'>
{subscription}
<button
onClick={bitsClick}
class="stripe-btn"
role="link">
Get Credits
</button>
<div id="error-message"></div>
</div>
);
}
module.exports = injectStripe(BitsBtn);

View File

@ -29,6 +29,7 @@ function TeamCtrl(args) {
return (
<aside>
<div class="timer-container"></div>
<button
class='ready'
disabled={teamSelect.some(c => !c)}

View File

@ -1,11 +1,8 @@
const preact = require('preact');
const { connect } = require('preact-redux');
const range = require('lodash/range');
const actions = require('./../actions');
const { COLOURS } = require('./../utils');
const { stringSort } = require('./../utils');
const SpawnButton = require('./spawn.button');
const { ConstructAvatar } = require('./construct');
const addState = connect(
@ -19,7 +16,6 @@ const addState = connect(
return {
constructs,
teamSelect,
sendConstructSpawn,
};
},
function receiveDispatch(dispatch) {
@ -31,7 +27,6 @@ const addState = connect(
setTeam,
};
}
);
function Team(args) {
@ -39,7 +34,6 @@ function Team(args) {
constructs,
teamSelect,
setTeam,
sendConstructSpawn,
} = args;
if (!constructs) return <div></div>;
@ -70,29 +64,19 @@ function Team(args) {
return (
<div
key={construct.id}
class="menu-construct"
class="construct team-select"
style={ { 'border-color': borderColour || 'whitesmoke' } }
onClick={() => selectConstruct(construct.id)} >
<div class="controls">
<h2>
{construct.name}
</h2>
</div>
<ConstructAvatar construct={construct} />
<h2>{construct.name}</h2>
</div>
);
});
const spawnButtonsNum = (3 - constructs.length % 3);
const spawnButtons = range(spawnButtonsNum)
.map(i => <SpawnButton key={constructs.length + i} spawn={() => sendConstructSpawn()} />);
return (
<main class="team">
<section class="team top">
{constructPanels}
{spawnButtons}
</main>
</section>
);
}

View File

@ -213,6 +213,15 @@ function errorToast(message) {
});
}
function infoToast(message) {
toast.info({
position: 'topRight',
drag: false,
close: false,
message,
});
}
function convertItem(v) {
if (['Red', 'Green', 'Blue'].includes(v)) {
return (
@ -233,6 +242,7 @@ module.exports = {
postData,
convertItem,
errorToast,
infoToast,
NULL_UUID,
STATS,
COLOURS,

View File

@ -1,5 +1,3 @@
error_log /var/log/mnml/nginx.log;
upstream mnml_http {
server 127.0.0.1:40000;
}

View File

@ -1,7 +1,12 @@
upstream mnml_dev {
server 0.0.0.0:41337;
upstream mnml_http {
server 127.0.0.1:40000;
}
upstream mnml_ws {
server 127.0.0.1:40055;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
@ -9,25 +14,19 @@ map $http_upgrade $connection_upgrade {
# DEV
server {
root /var/lib/mnml/public/;
index index.html;
server_name dev.mnml.gg; # managed by Certbot
location / {
root /var/lib/mnml/public/current;
index index.html;
try_files $uri $uri/ index.html;
}
location /imgs/ {
root /var/lib/mnml/public/;
try_files $uri $uri/ =404;
}
listen [::]:443;
ssl on;
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/dev.mnml.gg/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/dev.mnml.gg/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
location /api/ws {
proxy_pass http://mnml_dev;
proxy_pass http://mnml_ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
@ -35,15 +34,35 @@ server {
}
location /api/ {
proxy_pass http://mnml_dev;
proxy_pass http://mnml_http;
proxy_read_timeout 600s;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mnml.gg/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mnml.gg/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
server_name grep.live;
location / {
root /var/lib/mnml/public/current/;
index acp.html;
try_files $uri $uri/ acp.html;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/grep.live/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/grep.live/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# http -> https
server {
server_name dev.mnml.gg;
return 301 https://$host$request_uri;
}

View File

@ -29,6 +29,7 @@ iron = "0.6"
bodyparser = "0.8"
persistent = "0.4"
router = "0.6"
mount = "0.4"
cookie = "0.12"
crossbeam-channel = "0.3"
ws = "0.8"

View File

@ -8,7 +8,7 @@ use serde_cbor::{from_slice};
use postgres::transaction::Transaction;
use net::MnmlHttpError;
use http::MnmlHttpError;
use names::{name as generate_name};
use construct::{Construct, construct_recover, construct_spawn};
use instance::{Instance, instance_delete};
@ -29,6 +29,23 @@ pub struct Account {
pub subscribed: bool,
}
impl<'a> TryFrom<postgres::rows::Row<'a>> for Account {
type Error = Error;
fn try_from(row: postgres::rows::Row) -> Result<Self, Error> {
let id: Uuid = row.get("id");
let db_balance: i64 = row.get("balance");
let balance = u32::try_from(db_balance)
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
let subscribed: bool = row.get("subscribed");
let name: String = row.get("name");
Ok(Account { id, name, balance, subscribed })
}
}
pub fn select(db: &Db, id: Uuid) -> Result<Account, Error> {
let query = "
SELECT id, name, balance, subscribed
@ -42,13 +59,23 @@ pub fn select(db: &Db, id: Uuid) -> Result<Account, Error> {
let row = result.iter().next()
.ok_or(format_err!("account not found {:?}", id))?;
let db_balance: i64 = row.get(2);
let balance = u32::try_from(db_balance)
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
Account::try_from(row)
}
let subscribed: bool = row.get(3);
pub fn select_name(db: &Db, name: &String) -> Result<Account, Error> {
let query = "
SELECT id, name, balance, subscribed
FROM accounts
WHERE name = $1;
";
Ok(Account { id, name: row.get(1), balance, subscribed })
let result = db
.query(query, &[&name])?;
let row = result.iter().next()
.ok_or(format_err!("account not found name={:?}", name))?;
Account::try_from(row)
}
pub fn from_token(db: &Db, token: String) -> Result<Account, Error> {
@ -65,15 +92,7 @@ pub fn from_token(db: &Db, token: String) -> Result<Account, Error> {
let row = result.iter().next()
.ok_or(err_msg("invalid token"))?;
let id: Uuid = row.get(0);
let name: String = row.get(1);
let subscribed: bool = row.get(2);
let db_balance: i64 = row.get(3);
let balance = u32::try_from(db_balance)
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
Ok(Account { id, name, balance, subscribed })
Account::try_from(row)
}
pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<Account, MnmlHttpError> {
@ -101,23 +120,16 @@ pub fn login(tx: &mut Transaction, name: &String, password: &String) -> Result<A
},
};
let id: Uuid = row.get(0);
let hash: String = row.get(1);
let name: String = row.get(2);
let db_balance: i64 = row.get(3);
let subscribed: bool = row.get(4);
if !verify(password, &hash)? {
return Err(MnmlHttpError::PasswordNotMatch);
}
let balance = u32::try_from(db_balance)
.or(Err(format_err!("user {:?} has unparsable balance {:?}", id, db_balance)))?;
Ok(Account { id, name, balance, subscribed })
Account::try_from(row)
.or(Err(MnmlHttpError::ServerError))
}
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, MnmlHttpError> {
let mut rng = thread_rng();
let token: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
@ -136,11 +148,73 @@ pub fn new_token(tx: &mut Transaction, id: Uuid) -> Result<String, Error> {
.query(query, &[&token, &id])?;
result.iter().next()
.ok_or(format_err!("account not updated {:?}", id))?;
.ok_or(MnmlHttpError::Unauthorized)?;
Ok(token)
}
pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN {
return Err(MnmlHttpError::PasswordUnacceptable);
}
let query = "
SELECT id, password
FROM accounts
WHERE id = $1
";
let result = tx
.query(query, &[&id])?;
let row = match result.iter().next() {
Some(row) => row,
None => {
let mut rng = thread_rng();
let garbage: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(64)
.collect();
// verify garbage to prevent timing attacks
verify(garbage.clone(), &garbage).ok();
return Err(MnmlHttpError::AccountNotFound);
},
};
let id: Uuid = row.get(0);
let db_pw: String = row.get(1);
if !verify(current, &db_pw)? {
return Err(MnmlHttpError::PasswordNotMatch);
}
let rounds = 8;
let password = hash(&password, rounds)?;
let query = "
UPDATE accounts
SET password = $1, updated_at = now()
WHERE id = $2
RETURNING id, name;
";
let result = tx
.query(query, &[&password, &id])?;
let row = match result.iter().next() {
Some(row) => row,
None => return Err(MnmlHttpError::DbError),
};
let name: String = row.get(1);
info!("password updated name={:?} id={:?}", name, id);
new_token(tx, id)
}
pub fn credit(tx: &mut Transaction, id: Uuid, credits: i64) -> Result<String, Error> {
let query = "
UPDATE accounts

125
server/src/acp.rs Normal file
View File

@ -0,0 +1,125 @@
use iron::prelude::*;
use iron::status;
use iron::{BeforeMiddleware};
use persistent::Read;
use router::Router;
use serde::{Deserialize};
use uuid::Uuid;
use account;
use game;
use http::{State, MnmlHttpError, json_object};
struct AcpMiddleware;
impl BeforeMiddleware for AcpMiddleware {
fn before(&self, req: &mut Request) -> IronResult<()> {
match req.extensions.get::<account::Account>() {
Some(a) => {
if ["ntr", "mashy"].contains(&a.name.to_ascii_lowercase().as_ref()) {
return Ok(());
}
return Err(MnmlHttpError::Unauthorized.into());
},
None => Err(MnmlHttpError::Unauthorized.into()),
}
}
}
#[derive(Debug,Clone,Deserialize)]
struct GetUser {
name: Option<String>,
id: Option<Uuid>,
}
fn acp_user(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<GetUser>>() {
Ok(Some(b)) => b,
_ => return Err(MnmlHttpError::BadRequest.into()),
};
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let user = match params.id {
Some(id) => account::select(&db, id)
.or(Err(MnmlHttpError::NotFound))?,
None => match params.name {
Some(n) => account::select_name(&db, &n)
.or(Err(MnmlHttpError::NotFound))?,
None => return Err(MnmlHttpError::BadRequest.into()),
}
};
Ok(json_object(status::Ok, serde_json::to_string(&user).unwrap()))
}
#[derive(Debug,Clone,Deserialize)]
struct GetGame {
id: Uuid,
}
fn acp_game(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<GetGame>>() {
Ok(Some(b)) => b,
_ => return Err(MnmlHttpError::BadRequest.into()),
};
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let game = game::select(&db, params.id)
.or(Err(MnmlHttpError::NotFound))?;
Ok(json_object(status::Ok, serde_json::to_string(&game).unwrap()))
}
#[derive(Debug,Clone,Deserialize)]
struct GameList {
number: u32,
}
fn game_list(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<GameList>>() {
Ok(Some(b)) => b,
_ => return Err(MnmlHttpError::BadRequest.into()),
};
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let list = game::list(&db, params.number)
.or(Err(MnmlHttpError::ServerError))?;
Ok(json_object(status::Ok, serde_json::to_string(&list).unwrap()))
}
fn game_open(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
let list = game::games_need_upkeep(&mut tx)
.or(Err(MnmlHttpError::ServerError))?;
tx.commit()
.or(Err(MnmlHttpError::ServerError))?;
Ok(json_object(status::Ok, serde_json::to_string(&list).unwrap()))
}
pub fn acp_mount() -> Chain {
let mut router = Router::new();
router.post("user", acp_user, "acp_user");
router.post("game", acp_game, "acp_game");
router.post("game/list", game_list, "acp_game_list");
router.post("game/open", game_open, "acp_game_open");
let mut chain = Chain::new(router);
chain.link_before(AcpMiddleware);
chain
}

View File

@ -12,6 +12,7 @@ use failure::Error;
use failure::err_msg;
use account::Account;
use pg::Db;
use construct::{Construct};
use skill::{Skill, Cast, Resolution, Event, resolution_steps};
@ -655,6 +656,55 @@ pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {
return Ok(game);
}
pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> {
let query = "
SELECT *
FROM games
WHERE id = $1;
";
let result = db
.query(query, &[&id])?;
let returned = match result.iter().next() {
Some(row) => row,
None => return Err(err_msg("game not found")),
};
// tells from_slice to cast into a construct
let game_bytes: Vec<u8> = returned.get("data");
let game = from_slice::<Game>(&game_bytes)?;
return Ok(game);
}
pub fn list(db: &Db, number: u32) -> Result<Vec<Game>, Error> {
let query = "
SELECT data
FROM games
ORDER BY created_at
LIMIT $1;
";
let result = db
.query(query, &[&number])?;
let mut list = vec![];
for row in result.into_iter() {
let bytes: Vec<u8> = row.get(0);
match from_slice::<Game>(&bytes) {
Ok(i) => list.push(i),
Err(e) => {
warn!("{:?}", e);
}
};
}
return Ok(list);
}
pub fn games_need_upkeep(tx: &mut Transaction) -> Result<Vec<Game>, Error> {
let query = "
SELECT data, id

View File

@ -9,15 +9,17 @@ use iron::mime::Mime;
use iron::{typemap, BeforeMiddleware,AfterMiddleware};
use persistent::Read;
use router::Router;
use mount::{Mount};
use serde::{Serialize, Deserialize};
use acp;
use account;
use pg::PgPool;
use payments::{stripe};
pub const TOKEN_HEADER: &str = "x-auth-token";
pub const AUTH_CLEAR: &str =
"x-auth-token=; HttpOnly; SameSite=Strict; Max-Age=-1;";
"x-auth-token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=-1;";
#[derive(Clone, Copy, Fail, Debug, Serialize, Deserialize)]
pub enum MnmlHttpError {
@ -30,6 +32,8 @@ pub enum MnmlHttpError {
Unauthorized,
#[fail(display="bad request")]
BadRequest,
#[fail(display="not found")]
NotFound,
#[fail(display="account name taken or invalid")]
AccountNameNotProvided,
#[fail(display="account name not provided")]
@ -58,42 +62,40 @@ impl From<postgres::Error> for MnmlHttpError {
}
}
impl From<r2d2::Error> for MnmlHttpError {
fn from(_err: r2d2::Error) -> Self {
MnmlHttpError::DbError
}
}
impl From<failure::Error> for MnmlHttpError {
fn from(_err: failure::Error) -> Self {
fn from(err: failure::Error) -> Self {
warn!("{:?}", err);
MnmlHttpError::ServerError
}
}
#[derive(Serialize, Deserialize)]
struct JsonResponse {
response: Option<String>,
success: bool,
error_message: Option<String>
#[serde(rename_all(serialize = "lowercase"))]
pub enum Json {
Error(String),
Message(String),
}
impl JsonResponse {
fn success(response: String) -> Self {
JsonResponse { response: Some(response), success: true, error_message: None }
}
fn error(msg: String) -> Self {
JsonResponse { response: None, success: false, error_message: Some(msg) }
}
}
fn iron_response (status: status::Status, message: String) -> Response {
pub fn json_response(status: status::Status, response: Json) -> Response {
let content_type = "application/json".parse::<Mime>().unwrap();
let msg = match status {
status::Ok => JsonResponse::success(message),
_ => JsonResponse::error(message)
};
let msg_out = serde_json::to_string(&msg).unwrap();
return Response::with((content_type, status, msg_out));
let json = serde_json::to_string(&response).unwrap();
return Response::with((content_type, status, json));
}
pub fn json_object(status: status::Status, object: String) -> Response {
let content_type = "application/json".parse::<Mime>().unwrap();
return Response::with((content_type, status, object));
}
impl From<MnmlHttpError> for IronError {
fn from(m_err: MnmlHttpError) -> Self {
let (err, res) = match m_err {
let (err, status) = match m_err {
MnmlHttpError::ServerError |
MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError),
@ -107,8 +109,12 @@ impl From<MnmlHttpError> for IronError {
MnmlHttpError::InvalidCode |
MnmlHttpError::TokenDoesNotMatch |
MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized),
MnmlHttpError::NotFound => (m_err.compat(), status::NotFound),
};
IronError { error: Box::new(err), response: iron_response(res, m_err.to_string()) }
let response = json_response(status, Json::Error(m_err.to_string()));
IronError { error: Box::new(err), response }
}
}
@ -163,10 +169,15 @@ fn token_res(token: String) -> Response {
let v = Cookie::build(TOKEN_HEADER, token)
.http_only(true)
.same_site(SameSite::Strict)
.path("/")
.max_age(Duration::weeks(1)) // 1 week aligns with db set
.finish();
let mut res = iron_response(status::Ok, "token_res".to_string());
let mut res = json_response(
status::Ok,
Json::Message("authenticated".to_string())
);
res.headers.set(SetCookie(vec![v.to_string()]));
return res;
@ -220,7 +231,7 @@ fn login(req: &mut Request) -> IronResult<Response> {
match account::login(&mut tx, &params.name, &params.password) {
Ok(a) => {
let token = account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::ServerError))?;
let token = account::new_token(&mut tx, a.id)?;
tx.commit().or(Err(MnmlHttpError::ServerError))?;
Ok(token_res(token))
},
@ -238,11 +249,11 @@ fn logout(req: &mut Request) -> IronResult<Response> {
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
account::new_token(&mut tx, a.id).or(Err(MnmlHttpError::Unauthorized))?;
account::new_token(&mut tx, a.id)?;
tx.commit().or(Err(MnmlHttpError::ServerError))?;
let mut res = iron_response(status::Ok, "logout".to_string());
let mut res = json_response(status::Ok, Json::Message("logged out".to_string()));
res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()]));
Ok(res)
@ -251,6 +262,33 @@ fn logout(req: &mut Request) -> IronResult<Response> {
}
}
#[derive(Debug,Clone,Deserialize)]
struct SetPassword {
current: String,
password: String,
}
fn set_password(req: &mut Request) -> IronResult<Response> {
let state = req.get::<Read<State>>().unwrap();
let params = match req.get::<bodyparser::Struct<SetPassword>>() {
Ok(Some(b)) => b,
_ => return Err(IronError::from(MnmlHttpError::BadRequest)),
};
match req.extensions.get::<account::Account>() {
Some(a) => {
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
let token = account::set_password(&mut tx, a.id, &params.current, &params.password)?;
tx.commit().or(Err(MnmlHttpError::ServerError))?;
Ok(token_res(token))
},
None => Err(IronError::from(MnmlHttpError::Unauthorized)),
}
}
const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10;
@ -261,18 +299,33 @@ pub struct State {
impl Key for State { type Value = State; }
pub fn start(pool: PgPool) {
fn account_mount() -> Router {
let mut router = Router::new();
// auth
router.post("/api/login", login, "login");
router.post("/api/logout", logout, "logout");
router.post("/api/register", register, "register");
router.post("login", login, "login");
router.post("logout", logout, "logout");
router.post("register", register, "register");
router.post("password", set_password, "set_password");
router.post("email", logout, "email");
// payments
router.post("/api/payments/stripe", stripe, "stripe");
router
}
let mut chain = Chain::new(router);
fn payment_mount() -> Router {
let mut router = Router::new();
router.post("stripe", stripe, "stripe");
router
}
pub fn start(pool: PgPool) {
let mut mounts = Mount::new();
mounts.mount("/api/account/", account_mount());
mounts.mount("/api/payments/", payment_mount());
mounts.mount("/api/acp/", acp::acp_mount());
let mut chain = Chain::new(mounts);
chain.link(Read::<State>::both(State { pool }));
chain.link_before(Read::<bodyparser::MaxBodyLength>::one(MAX_BODY_LENGTH));
chain.link_before(AuthMiddleware);

View File

@ -396,9 +396,11 @@ impl Instance {
// if you don't win, you lose
// ties can happen if both players forfeit
// in this case we just finish the game and
// dock them 10k mmr
let winner_id = match game.winner() {
Some(w) => w.id,
None => Uuid::nil(),
None => return Ok(self.finish()),
};
for player in game.players.iter() {

View File

@ -23,12 +23,14 @@ extern crate iron;
extern crate bodyparser;
extern crate persistent;
extern crate router;
extern crate mount;
extern crate cookie;
extern crate ws;
extern crate crossbeam_channel;
mod account;
mod acp;
mod construct;
mod effect;
mod game;
@ -38,7 +40,7 @@ mod img;
mod mob;
mod mtx;
mod names;
mod net;
mod http;
mod payments;
mod pg;
mod player;
@ -98,7 +100,7 @@ fn main() {
let pg_pool = pool.clone();
spawn(move || net::start(http_pool));
spawn(move || http::start(http_pool));
spawn(move || warden.listen());
spawn(move || warden::upkeep_tick(warden_tick_tx));
spawn(move || pg::listen(pg_pool, pg_events_tx));

View File

@ -1,7 +1,7 @@
use rand::prelude::*;
use rand::{thread_rng};
const FIRSTS: [&'static str; 47] = [
const FIRSTS: [&'static str; 50] = [
"artificial",
"ambient",
"borean",
@ -20,10 +20,13 @@ const FIRSTS: [&'static str; 47] = [
"fierce",
"fossilised",
"frozen",
"gravitational",
"jovian",
"inverted",
"leafy",
"lurking",
"limitless",
"magnetic",
"metallic",
"mossy",
"mighty",
@ -37,6 +40,7 @@ const FIRSTS: [&'static str; 47] = [
"oxygenated",
"oscillating",
"ossified",
"orbiting",
"piscine",
"purified",
"recalcitrant",
@ -46,18 +50,18 @@ const FIRSTS: [&'static str; 47] = [
"supercooled",
"subsonic",
"synthetic",
"sweet",
"terrestrial",
"weary",
];
const LASTS: [&'static str; 52] = [
const LASTS: [&'static str; 55] = [
"artifact",
"assembly",
"carbon",
"console",
"construct",
"craft",
"core",
"design",
"drone",
"distortion",
@ -77,6 +81,8 @@ const LASTS: [&'static str; 52] = [
"lifeform",
"landmass",
"lens",
"mantle",
"magnetism",
"mechanism",
"mountain",
"nectar",

View File

@ -1,6 +1,6 @@
use std::io::Read;
use net::State;
use http::State;
use iron::prelude::*;
use iron::response::HttpResponse;
use iron::status;
@ -14,7 +14,7 @@ use failure::err_msg;
use stripe::{Event, EventObject, CheckoutSession, SubscriptionStatus};
use net::{MnmlHttpError};
use http::{MnmlHttpError};
use pg::{PgPool};
use account;

View File

@ -26,7 +26,7 @@ use pg::{Db};
use pg::{PgPool};
use skill::{Skill, dev_resolve, Resolutions};
use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip};
use net::{AUTH_CLEAR, TOKEN_HEADER};
use http::{AUTH_CLEAR, TOKEN_HEADER};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub enum RpcMessage {