Merge tag '1.11.0' into develop

1.11.0
This commit is contained in:
ntr 2020-01-03 18:38:07 +10:00
commit 02b25f92b7
59 changed files with 6663 additions and 1303 deletions

View File

@ -1 +1 @@
1.10.1 1.11.0

View File

@ -1,6 +1,6 @@
{ {
"name": "mnml-client", "name": "mnml-client",
"version": "1.10.1", "version": "1.11.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -3,16 +3,25 @@
div { div {
padding-right: 1em; padding-right: 1em;
// display: flex;
// flex-flow: column;
line-height: 2em;
}
h3 {
// text-transform: uppercase;
margin-bottom: 0.5em;
} }
button { button {
width: 100%; width: 100%;
height: 2.5em;
display: block; display: block;
} }
input { input {
width: 100%; width: 100%;
height: 3em; height: 2.5em;
display: block; display: block;
} }

View File

@ -5,7 +5,7 @@
@white: #f5f5f5; // whitesmoke @white: #f5f5f5; // whitesmoke
@purple: #9355b5; // 6lack - that far cover @purple: #9355b5; // 6lack - that far cover
@yellow: #ffa100; @yellow: #ffa100;
@silver: #c0c0c0; @silver: #2c2c2c;
@black: black; @black: black;
@gray: #222; @gray: #222;

View File

@ -54,11 +54,7 @@
button { button {
&.highlight { &.highlight {
color: black;
background: @silver; background: @silver;
// border: 1px solid @white; (this bangs around the vbox)
// overwrite the classes on white svg elements
svg { svg {
stroke-width: 0.75em; stroke-width: 0.75em;
} }

View File

@ -75,25 +75,11 @@
flex: 1; flex: 1;
border-top: 0; border-top: 0;
border: 0.1em solid #222; border: 0.1em solid #222;
&:not(:last-child) {
border-right: 0;
}
&:last-child { &:last-child {
float: right; float: right;
} }
} }
} }
.login {
display: flex;
flex-flow: column;
.terms {
display: inline;
margin: 0 1em;
}
}
} }
section { section {
@ -108,62 +94,14 @@ section {
padding-right: 1em; padding-right: 1em;
} }
.list {
letter-spacing: 0.25em;
text-transform: uppercase;
display: grid;
// grid-template-columns: repeat(4, 1fr);
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
flex-flow: row wrap;
align-items: flex-end;
button {
border-radius: 0.25em;
// height: 3em;
}
&.sub {
grid-template-columns: 1fr;
}
&.play {
grid-template-columns: repeat(2, 1fr);
align-items: flex-start;
&.rejoin {
grid-template-columns: 1fr;
}
button.ready:enabled {
color: forestgreen;
border-color: forestgreen;
&:hover {
background: forestgreen;
color: black;
border-color: forestgreen;
}
}
// // all green
// button.ready:enabled {
// background: forestgreen;
// color: black;
// border-color: forestgreen;
// &:hover {
// color: forestgreen;
// border-color: forestgreen;
// background: 0;
// }
// }
}
}
.panes { .panes {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
}
.list {
margin-bottom: 2em;
figure { figure {
letter-spacing: 0.25em; letter-spacing: 0.25em;
@ -172,61 +110,92 @@ section {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
} }
letter-spacing: 0.25em;
text-transform: uppercase;
display: grid;
// grid-template-columns: repeat(4, 1fr);
grid-template-columns: 1fr 1fr;
grid-gap: 1em;
flex-flow: row wrap;
align-items: flex-end;
&.sub {
grid-template-columns: 1fr;
}
&.play {
grid-template-columns: repeat(2, 1fr);
align-items: flex-start;
&.rejoin {
grid-template-columns: 1fr;
}
button.ready:enabled {
color: forestgreen;
border-color: forestgreen;
&:hover {
background: forestgreen;
color: black;
border-color: forestgreen;
}
}
// // all green
// button.ready:enabled {
// background: forestgreen;
// color: black;
// border-color: forestgreen;
// &:hover {
// color: forestgreen;
// border-color: forestgreen;
// background: 0;
// }
// }
}
} }
.demo {
margin-top: 1em;
display: block; .login {
display: flex;
flex-flow: column;
.terms {
display: inline;
margin: 0 1em;
}
button { button {
pointer-events: none; padding: 0 0.5em;
margin-top: 1em;
}
}
.options {
grid-area: hdr;
display: flex;
.logo {
flex: 0 1 10%;
margin-right: 1em;
border: none;
} }
section { button {
margin-bottom: 0.5em; flex: 1;
border-top: 0;
div:first-child { border: 0.1em solid #222;
padding-right: 1em; &:last-child {
} float: right;
}
.construct-section {
.construct-list {
height: 25em;
grid-area: unset;
.instance-construct {
// border: 0;
}
}
}
.colour-info {
grid-area: vinfo;
display: flex;
align-items: center;
div {
display: flex;
}
svg {
flex: 1;
height: 1em;
}
}
.game-demo {
.game {
height: 25em;
display: flex;
flex-flow: column;
.game-construct {
flex: 1;
}
} }
} }
} }
.intro {
text-align: center;
font-size: 0.8em;
}

View File

@ -27,23 +27,6 @@ html body {
overflow-y: hidden; overflow-y: hidden;
} }
#mnml {
/* this is the sweet nectar to keep it full page*/
height: 100vh;
max-height: 100vh;
min-height: 100vh;
/* stops inspector going skitz*/
overflow-x: hidden;
// overflow-y: hidden;
}
// @media (min-width: 1921px) {
// html, body, #mnml {
// font-size: 16pt;
// }
// }
html { html {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@ -108,11 +91,37 @@ dl {
padding: 0.5em 1em; padding: 0.5em 1em;
/* this is the sweet nectar to keep it full page*/
height: 100vh;
max-height: 100vh;
min-height: 100vh;
/* stops inspector going skitz*/
overflow-x: hidden;
// overflow-y: hidden;
&.animations-test { &.animations-test {
aside button { aside button {
font-size: 50%; font-size: 50%;
} }
} }
&.front-page {
display: block;
main {
padding: 0 25%;
}
.logo {
margin: 2em 0;
}
.list {
margin-bottom: 0;
}
}
} }
main { main {
@ -129,7 +138,7 @@ button, input {
box-sizing: border-box; box-sizing: border-box;
font-size: 1em; font-size: 1em;
flex: 1; flex: 1;
border-radius: 0.5em; border-radius: 0;
line-height: 2em; line-height: 2em;
padding-right: 0.1em; padding-right: 0.1em;
padding-left: 0.1em; padding-left: 0.1em;
@ -150,9 +159,12 @@ button, input {
&:focus { &:focus {
/*colour necesary to bash skellington*/ /*colour necesary to bash skellington*/
outline: 0; outline: 0;
} }
// &:active {
// filter: url("#noiseFilter");
// }
} }
a { a {
@ -261,28 +273,12 @@ figure.gray {
display: none; display: none;
} }
header {
.options {
font-size: 200%;
}
button {
height: 2em;
// border-radius: 0.1em;
border: none;
border-radius: 0;
}
}
.options { .options {
button { button {
&.highlight { &.highlight {
color: @white; color: @white;
box-shadow: inset 0px 5px 0px 0px @white; box-shadow: inset 0px 5px 0px 0px @white;
border: 0;
} }
border: none;
} }
} }
@ -300,11 +296,20 @@ li {
} }
.logo { .logo {
height: 2em; height: 4em;
background-image: url("../../assets/mnml.logo.trim.svg"); filter: url("#noiseFilter");
background-image: url("../../assets/mnml.logo.text.svg");
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: left; background-position: center;
}
.awards {
height: 100%;
background-image: url("../../assets/mnml.awards.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
} }
.discord-btn { .discord-btn {
@ -316,8 +321,13 @@ li {
.mnni { .mnni {
background-image: url("./../mnni.svg"); background-image: url("./../mnni.svg");
filter: url("#noiseFilter");
} }
// .highlight {
// filter: url("#noiseFilter");
// }
.avatar { .avatar {
grid-area: avatar; grid-area: avatar;
object-fit: contain; object-fit: contain;
@ -328,6 +338,10 @@ li {
// pointer-events: none; // pointer-events: none;
} }
header {
// font-size: 1.2em;
}
#clipboard { #clipboard {
width: 1px; width: 1px;
height: 1px; height: 1px;
@ -359,4 +373,8 @@ li {
} }
} }
#noise {
height: 0;
}
@import 'styles.mobile.less'; @import 'styles.mobile.less';

View File

@ -7,6 +7,12 @@
font-size: 8pt; font-size: 8pt;
padding: 0; padding: 0;
&.front-page {
main {
padding: 0 0.5em;
}
}
.instance { .instance {
grid-template-areas: grid-template-areas:
"vbox vbox" "vbox vbox"
@ -164,12 +170,21 @@
// portrait menu or small size vertical in landscape // portrait menu or small size vertical in landscape
@media (max-width: 550px) and (max-height: 800px) { @media (max-width: 550px) and (max-height: 800px) and (orientation: portrait) {
#mnml { #mnml {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-template-areas: grid-template-areas:
"main" "main";
&.front-page {
display: block;
main {
padding: 0 0.5em;
}
}
} }
section { section {
@ -264,6 +279,9 @@
} }
.info-combiner { .info-combiner {
max-height: 7em;
overflow-y: scroll;
.info { .info {
display: none; display: none;
} }

View File

@ -147,16 +147,11 @@
} }
&.highlight { &.highlight {
color: black;
background: @silver; background: @silver;
// overwrite the classes on white svg elements // overwrite the classes on white svg elements
svg { svg {
stroke-width: 0.75em; stroke-width: 0.75em;
} }
.white {
stroke: black;
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "mnml-client", "name": "mnml-client",
"version": "1.10.1", "version": "1.11.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -1,4 +1,5 @@
export const setAccount = value => ({ type: 'SET_ACCOUNT', value }); export const setAccount = value => ({ type: 'SET_ACCOUNT', value });
export const setAuthenticated = value => ({ type: 'SET_AUTHENTICATED', value });
export const setAnimating = value => ({ type: 'SET_ANIMATING', value }); export const setAnimating = value => ({ type: 'SET_ANIMATING', value });
export const setAnimFocus = value => ({ type: 'SET_ANIM_FOCUS', value }); export const setAnimFocus = value => ({ type: 'SET_ANIM_FOCUS', value });
@ -7,8 +8,6 @@ export const setAnimSource = value => ({ type: 'SET_ANIM_SOURCE', value });
export const setAnimTarget = value => ({ type: 'SET_ANIM_TARGET', value }); export const setAnimTarget = value => ({ type: 'SET_ANIM_TARGET', value });
export const setResolution = value => ({ type: 'SET_RESOLUTION', value }); export const setResolution = value => ({ type: 'SET_RESOLUTION', value });
export const setDemo = value => ({ type: 'SET_DEMO', value });
export const setChatShow = value => ({ type: 'SET_CHAT_SHOW', value }); export const setChatShow = value => ({ type: 'SET_CHAT_SHOW', value });
export const setChatWheel = value => ({ type: 'SET_CHAT_WHEEL', value }); export const setChatWheel = value => ({ type: 'SET_CHAT_WHEEL', value });
export const setInstanceChat = value => ({ type: 'SET_INSTANCE_CHAT', value }); export const setInstanceChat = value => ({ type: 'SET_INSTANCE_CHAT', value });

View File

@ -33,6 +33,10 @@ const ws = createSocket(events);
ws.connect(); ws.connect();
events.setWs(ws); events.setWs(ws);
if (process.env.NODE_ENV !== 'development') {
LogRocket.init('yh0dy3/mnml');
}
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
{window.Stripe {window.Stripe

View File

@ -152,11 +152,9 @@ class AccountStatus extends Component {
return ( return (
<section class='account top' onClick={tlClick}> <section class='account top' onClick={tlClick}>
{subInfo()}
<div> <div>
{subInfo()} <h3>Email</h3>
</div>
<div>
<label for="email">Email Settings:</label>
<dl> <dl>
<dt>Recovery Email</dt> <dt>Recovery Email</dt>
<dd>{email ? email.email : 'No email set'}</dd> <dd>{email ? email.email : 'No email set'}</dd>
@ -174,6 +172,7 @@ class AccountStatus extends Component {
<button onClick={() => sendSetEmail(emailState)}>Update</button> <button onClick={() => sendSetEmail(emailState)}>Update</button>
</div> </div>
<div> <div>
<h3>Password</h3>
<label for="current">Password:</label> <label for="current">Password:</label>
<input <input
class="login-input" class="login-input"
@ -208,6 +207,7 @@ class AccountStatus extends Component {
</button> </button>
</div> </div>
<div> <div>
<h3>Other</h3>
<figure> <figure>
<figcaption>spawn new construct</figcaption> <figcaption>spawn new construct</figcaption>
<button onClick={() => sendConstructSpawn()} type="submit"> <button onClick={() => sendConstructSpawn()} type="submit">

View File

@ -1,34 +1,17 @@
const preact = require('preact'); const preact = require('preact');
const { Component } = require('preact'); const { Component } = require('preact');
const anime = require('animejs').default; const anime = require('animejs').default;
const times = require('lodash/times');
const { TIMES } = require('../../constants'); const { TIMES } = require('../../constants');
function projectile(x, y, radius, colour) { const GREEN = '#1FF01F';
return ( const RED = '#a52a2a';
<circle
cx={x}
cy={y}
stroke="none"
r={radius}
fill={colour}
/>
);
}
function sword(colour) {
return (
<polygon points='150,150 100,75, 150,300, 200,75' stroke="none" fill={colour} id="sword" filter="url(#slayFilter)"></polygon>
);
}
class Slay extends Component { class Slay extends Component {
constructor() { constructor() {
super(); super();
this.animations = []; this.animations = [];
this.colour = '#a52a2a';
const points = new Array(30).fill(0);
this.charges = points.map(() => projectile(150, 420, 7, '#1FF01F'));
} }
render() { render() {
@ -39,13 +22,16 @@ class Slay extends Component {
id="slay" id="slay"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 300"> viewBox="0 0 300 300">
<filter id="slayFilter"> {times(10, () => (
<feGaussianBlur stdDeviation="4"/> <ellipse
<feTurbulence type="turbulence" baseFrequency="0.001" numOctaves="3" result="turbulence"/> cx={anime.random(100, 200)}
<feDisplacementMap in2="turbulence" in="SourceGraphic" scale="1" xChannelSelector="A" yChannelSelector="A"/> cy={anime.random(-60, -30)}
</filter> stroke="none"
{sword(this.colour)} rx={anime.random(5, 10)}
{this.charges} ry={10}
fill={RED}
/>
))}
</svg> </svg>
); );
} }
@ -65,60 +51,26 @@ class Slay extends Component {
anime.set('#slay', { anime.set('#slay', {
rotate, rotate,
});
anime.set('#slay', {
translateY: -1 * (window.innerHeight) * 0.35,
translateX: 0,
});
anime.set('#slayFilter feDisplacementMap', {
scale: 0,
});
anime.set('#sword', {
fill: this.colour,
opacity: 1, opacity: 1,
}); });
this.animations.push(anime({ anime.set('#slay ellipse',{
targets: '#slay', fill: RED,
opacity: [ })
{ value: 1, duration: TIMES.TARGET_DURATION_MS * 0.2 },
{ value: 0, delay: TIMES.TARGET_DURATION_MS * 0.6, duration: TIMES.TARGET_DURATION_MS * 0.2 },
],
translateY: 0,
translateX: 0,
loop: false,
easing: 'easeInQuad',
}));
this.animations.push(anime({ this.animations.push(anime({
targets: ['#slayFilter feTurbulence', '#slayFilter feDisplacementMap'], targets: ['#slay ellipse'],
baseFrequency: 10, cx: 150,
scale: 100, cy: 325,
delay: TIMES.TARGET_DURATION_MS * 0.6, duration: TIMES.TARGET_DURATION_MS * 0.2,
duration: TIMES.TARGET_DURATION_MS * 0.3, duration: TIMES.TARGET_DURATION_MS * 0.4,
easing: 'easeInQuad', easing: 'easeOutQuad',
direction: 'alternate',
})); }));
this.animations.push(anime({ setTimeout(() => anime.set('#slay ellipse',{
targets: '#sword', fill: GREEN,
opacity: 0, }), TIMES.TARGET_DURATION_MS * 0.5);
delay: TIMES.TARGET_DURATION_MS * 0.9,
}));
const projectiles = document.querySelectorAll('#slay circle');
projectiles.forEach(proj => {
this.animations.push(anime({
targets: proj,
cx: Math.random() * 250 + 25,
cy: Math.random() * 200 - 100,
delay: TIMES.TARGET_DURATION_MS * 0.7,
duration: TIMES.TARGET_DURATION_MS * 0.3,
easing: 'easeInQuad',
}));
});
} }
componentWillUnmount() { componentWillUnmount() {

View File

@ -40,6 +40,7 @@ class Strike extends Component {
height: [200, 10, 0], height: [200, 10, 0],
width: [20, 400, 0], width: [20, 400, 0],
duration: TIMES.TARGET_DURATION_MS, duration: TIMES.TARGET_DURATION_MS,
delay: TIMES.TARGET_DURATION_MS * 0.2,
})); }));
this.animations.push(anime({ this.animations.push(anime({

View File

@ -13,8 +13,8 @@ const { ConstructAnimation } = require('./animations');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { animSource, animTarget, resolution, account } = state; const { animating, animSource, animTarget, resolution, account } = state;
return { animSource, animTarget, resolution, account }; return { animating, animSource, animTarget, resolution, account };
} }
); );
@ -43,6 +43,7 @@ class ConstructAvatar extends Component {
} }
onClick() { onClick() {
if (this.props.animating) return false;
return this.animations.push(wiggle(this.props.construct.id, this.idle)); return this.animations.push(wiggle(this.props.construct.id, this.idle));
} }
@ -65,7 +66,10 @@ class ConstructAvatar extends Component {
const { animSource, animTarget, resolution, construct, account } = this.props; const { animSource, animTarget, resolution, construct, account } = this.props;
// a different text object and text construct // a different text object and text construct
if (resolution && resolution !== prevProps.resolution && resolution.event[1].construct === construct.id) { if (resolution && resolution !== prevProps.resolution && resolution.event[1].construct === construct.id) {
return wiggle(construct.id, this.idle); const type = resolution.event[0];
// only trigger the wiggle on damage and ko events rather than spam it on everything
// also stops wiggle triggering when invert effect is applied
if (['Damage', 'Ko'].includes(type)) return wiggle(construct.id, this.idle);
} }
// different source object and source construct // different source object and source construct

View File

@ -10,6 +10,7 @@ const addState = connect(
function receiveState(state) { function receiveState(state) {
const { const {
ws, ws,
authenticated,
account, account,
game, game,
instance, instance,
@ -17,6 +18,7 @@ const addState = connect(
} = state; } = state;
return { return {
authenticated,
account, account,
game, game,
instance, instance,
@ -28,6 +30,7 @@ const addState = connect(
function Controls(args) { function Controls(args) {
const { const {
game, game,
authenticated,
account, account,
instance, instance,
nav, nav,
@ -38,6 +41,7 @@ function Controls(args) {
if (game) return <GameCtrl />; if (game) return <GameCtrl />;
if (instance) return <InstanceCtrl />; if (instance) return <InstanceCtrl />;
if (!authenticated) return false;
if (nav === 'play' || nav === 'shop' || nav === 'reshape' || !nav) return <PlayCtrl /> if (nav === 'play' || nav === 'shop' || nav === 'reshape' || !nav) return <PlayCtrl />
if (nav === 'team' || nav === 'account') return <TeamCtrl /> if (nav === 'team' || nav === 'account') return <TeamCtrl />

View File

@ -1,190 +0,0 @@
const { connect } = require('preact-redux');
const preact = require('preact');
// const actions = require('../actions');
const shapes = require('./shapes');
const { ConstructAvatar } = require('./construct');
// const { ConstructAnimation } = require('./animations');
const addState = connect(
function receiveState(state) {
const {
account,
itemInfo,
demo,
} = state;
return {
account,
itemInfo,
demo,
};
}
/* function receiveDispatch(dispatch) {
function setAnimTarget(anim) {
dispatch(actions.setAnimTarget(anim));
}
return { setAnimTarget };
} */
);
function Demo(args) {
const {
demo,
itemInfo,
account,
// setAnimTarget,
} = args;
if (!demo || !itemInfo.items.length || account) return false;
const { combiner, items, equipping, equipped, players, combo } = demo;
const vboxDemo = () => {
function stashBtn(i, j) {
if (!i) return <button disabled class='empty' >&nbsp;</button>;
const highlighted = combiner.indexOf(j) > -1;
const classes = `${highlighted ? 'highlight' : ''}`;
if (shapes[i]) {
return <button class={classes} key={j}>{shapes[i]()}</button>;
}
return <button class={classes}>{i}</button>;
}
function combinerBtn() {
let text = '';
if (combiner.length < 3) {
for (let i = 0; i < 3; i++) {
if (combiner.length > i) {
text += '■ ';
} else {
text += '▫ ';
}
}
} else {
text = 'combine';
}
return (
<button
class='vbox-btn'
disabled={combiner.length !== 3}>
{text}
</button>
);
}
function stashElement() {
return (
<div class="vbox">
<div class='vbox-section'>
<h2 class='colour-info'>
VBOX PHASE {shapes.Red()} {shapes.Green()} {shapes.Blue()}
</h2>
<p>
Combine colours with base skills and specialisations to build an array of powerful variants.
</p>
</div>
<div>&nbsp;</div>
<div class='vbox-section'>
<div class='vbox-items'>
{items.map((i, j) => stashBtn(i, j))}
</div>
{combinerBtn()}
</div>
</div>
);
}
return (
<div class="news vbox-demo">
{stashElement()}
</div>
);
};
const vboxConstructs = () => {
const btnClass = equipping
? 'equipping empty gray'
: 'empty gray';
const constructEl = c => (
<div class="instance-construct">
<h2 class="name" >{c.name}</h2>
<ConstructAvatar construct={c} />
<div class="skills">
{equipped
? <button>{combo}</button>
: <button disabled={!equipping} class={btnClass}>SKILL</button>
}
<button disabled={!equipping} class={btnClass}>SKILL</button>
<button disabled={!equipping} class={btnClass}>SKILL</button>
</div>
<div class="specs">
</div>
<div class="stats">
</div>
</div>
);
return (
<section class="construct-section">
<div>
<h2>CONSTRUCTS</h2>
<p><b>Constructs</b> are the units you control. They are reset every game and their initial appearance is randomly generated.</p>
<p><b>Skills</b> and <b>Specs</b> you create in the <b>VBOX Phase</b> are equipped to your constructs to create a build.</p>
</div>
<div class='construct-list'>
{constructEl(players[0].constructs[0])}
</div>
</section>
);
};
const gameDemo = () => {
return (
<section class="game-demo">
<div>
<h2>COMBAT PHASE</h2>
<p>Battle your opponent using dynamic team builds from the VBOX phase.</p>
<p>The skills crafted can be used to damage the opponent or support your team.</p>
<p>Simultaneous turn based combat: each team picks targets for their skills during this phase.</p>
<p>The damage dealt by skills, cast order and construct life depend on your decisions in the VBOX phase.</p>
</div>
<div class="game">
<div class="game-construct">
<div class="left"></div>
<div class="right">
<ConstructAvatar construct={players[1].constructs[0]} />
</div>
</div>
<div></div>
<div class="game-construct">
<div class="left"></div>
<div class="right">
<ConstructAvatar construct={players[1].constructs[1]} />
</div>
</div>
</div>
</section>
);
};
return (
<section class='demo news top'>
{gameDemo()}
{vboxDemo()}
{vboxConstructs()}
</section>
);
}
module.exports = addState(Demo);

View File

@ -0,0 +1,85 @@
// const { connect } = require('preact-redux');
const preact = require('preact');
const { connect } = require('preact-redux');
const { errorToast, infoToast } = require('../utils');
const actions = require('./../actions');
const VERSION = process.env.npm_package_version;
const Welcome = require('./welcome');
const addState = connect(
function receiveState(state) {
const {
ws,
account,
} = state;
function sendInstancePractice() {
ws.sendInstancePractice();
}
return {
account,
sendInstancePractice,
};
},
);
function Play(args) {
const {
account,
sendInstancePractice,
} = args;
const news = (
<div class="list">
<div class="intro">
<p> MNML is a turn-based 1v1 strategy game in an abstract setting. </p>
<p>
Build a unique team of 3 constructs from a range of skills and specialisations.<br />
Outplay your opponent across multiple rounds by adapting to an always shifting meta. <br />
</p>
</div>
<div class="awards"></div>
</div>
);
const list = () => {
return (
<div class='list play'>
<figure>
<button
class="ready"
onClick={() => sendInstancePractice()}>
Play
</button>
<figcaption>Learn MNML</figcaption>
</figure>
<figure>
<button
class='discord-btn'
onClick={() => window.open('https://discord.gg/YJJgurM') }>
&nbsp;
</button>
<figcaption>Join the Community</figcaption>
</figure>
</div>
);
};
return (
<main>
<div class="logo"/>
<hr />
{list()}
<hr />
<Welcome />
<hr />
{news}
</main>
);
}
module.exports = addState(Play);

View File

@ -41,36 +41,38 @@ class AnimText extends preact.Component {
const generateAnimText = () => { const generateAnimText = () => {
const [type, event] = resolution.event; const [type, event] = resolution.event;
if (type === 'Ko') return <h1><span>KO!</span></h1>; switch (type) {
if (type === 'Disable') { case 'Damage': {
const { disable } = event; const { amount, mitigation, colour } = event;
return <h1><span>{disable}</span></h1>;
}
if (type === 'Immunity') return <h1><span>IMMUNE</span></h1>;
if (type === 'Damage') {
const { mitigation, colour } = event;
let { amount } = event;
amount *= -1;
const mitigationText = mitigation ? `(${mitigation})` : ''; const mitigationText = mitigation ? `(${mitigation})` : '';
return <h1><span class={colour.toLowerCase()}>{amount} {mitigationText} </span></h1>; return <h1><span class={colour.toLowerCase()}>-{amount} {mitigationText} </span></h1>;
} }
if (type === 'Healing') { case 'Healing': {
const { amount, overhealing, colour } = event; const { amount, overhealing, colour } = event;
return <h1><span class={colour.toLowerCase()}>{amount} ({overhealing} OH)</span></h1>; const overHealingText = overhealing ? `(${overhealing} OH)` : '';
return <h1><span class={colour.toLowerCase()}>+{amount} {overHealingText}</span></h1>;
} }
if (type === 'Inversion') return <h1><span>INVERT</span></h1>; case 'Effect': {
if (type === 'Reflection') return <h1><span>REFLECT</span></h1>;
if (type === 'Effect') {
const { effect, duration } = event; const { effect, duration } = event;
return <h1><span>+{effect} {duration}T</span></h1>; return <h1><span>+{effect} {duration}T</span></h1>;
} }
if (type === 'Removal') { case 'Removal': {
const { effect } = event; const { effect } = event;
if (!effect) return <h1><span>Effect Removal</span></h1>; if (!effect) return <h1><span>Effect Removal</span></h1>;
return <h1><span>{effect}</span></h1>; return <h1><span>-{effect}</span></h1>;
}
case 'Ko': return <h1><span>KO!</span></h1>;
case 'Reflection': return <h1><span>REFLECT</span></h1>;
default: return false;
} }
return false;
}; };
// We don't send inversion / disable / immune event text
/* case 'Inversion': return <h1><span>INVERT</span></h1>;
case 'Disable': {
const { disable } = event;
return <h1><span>{disable}</span></h1>;
}
case 'Immunity': return <h1><span>IMMUNE</span></h1>; */
return ( return (
<div class="combat-text"> <div class="combat-text">

View File

@ -59,11 +59,16 @@ class GameConstruct extends preact.Component {
player, player,
} = this.props; } = this.props;
const ko = construct.green_life.value === 0 ? 'ko' : ''; // construct green_life comes from game state and won't update during animations
// treat the construct as ko for the remainder of the anims if ko event occurs
const ko = construct.green_life.value === 0 || this.ko ? 'ko' : '';
const koEvent = () => { const koEvent = () => {
if (resolution) { if (resolution) {
const [type, variant] = resolution.event; const [type, variant] = resolution.event;
if (variant.construct === construct.id && type === 'Ko') return 'ko-transition'; if (variant.construct === construct.id && type === 'Ko') {
this.ko = true;
return 'ko-transition';
}
} }
return ''; return '';
}; };

View File

@ -8,6 +8,7 @@ const addState = connect(
const { const {
ws, ws,
account, account,
authenticated,
nav, nav,
} = state; } = state;
@ -22,6 +23,7 @@ const addState = connect(
return { return {
account, account,
authenticated,
nav, nav,
sendInstanceState, sendInstanceState,
@ -48,6 +50,7 @@ const addState = connect(
function Header(args) { function Header(args) {
const { const {
account, account,
authenticated,
nav, nav,
sendAccountStates, sendAccountStates,
@ -56,6 +59,8 @@ function Header(args) {
if (!account) return false; if (!account) return false;
if (!authenticated) return false;
function navTo(p) { function navTo(p) {
return setNav(p); return setNav(p);
} }
@ -68,11 +73,6 @@ function Header(args) {
return ( return (
<header> <header>
<div class="options"> <div class="options">
<button
onClick={() => navTo('play')}
class='logo login-btn'>
&nbsp;
</button>
<button <button
onClick={() => navTo('play')} onClick={() => navTo('play')}
class={`login-btn ${nav === 'play' ? 'highlight' : ''}`}> class={`login-btn ${nav === 'play' ? 'highlight' : ''}`}>

View File

@ -23,6 +23,7 @@ const addState = connect(
function Top(args) { function Top(args) {
const { const {
nav, nav,
authenticated,
} = args; } = args;
if (nav === 'account') return <AccountTop />; if (nav === 'account') return <AccountTop />;

View File

@ -4,27 +4,38 @@ const { connect } = require('preact-redux');
const Main = require('./main'); const Main = require('./main');
// const Nav = require('./nav'); // const Nav = require('./nav');
const Controls = require('./controls'); const Controls = require('./controls');
const FrontPage = require('./front.page');
const Noise = require('./noise');
const addState = connect( const addState = connect(
({ game, instance }) => ({ game, instance }) ({ game, instance, authenticated }) => ({ game, instance, authenticated })
); );
function Mnml(args) { function Mnml(args) {
const { const {
game, game,
instance, instance,
authenticated,
} = args; } = args;
const rotateClass = (game || instance) && window.innerHeight < 900 && window.innerWidth < window.innerHeight const rotateClass = (game || instance) && window.innerHeight < 900 && window.innerWidth < window.innerHeight
? 'show' ? 'show'
: ''; : '';
if (!authenticated && !instance && !game) return (
<div id="mnml" class='front-page'>
<Noise />
<FrontPage />
<div id="rotate" class={rotateClass} ></div>
</div>
);
return ( return (
<div id="mnml"> <div id="mnml">
<Main /> <Main />
<Controls /> <Controls />
<div id="rotate" class={rotateClass} > <Noise />
</div> <div id="rotate" class={rotateClass} ></div>
</div> </div>
); );
} }

View File

@ -0,0 +1,75 @@
const preact = require('preact');
const { Component } = require('preact');
const anime = require('animejs').default;
class Noise extends Component {
constructor() {
super();
this.animations = [];
}
render() {
return (
<svg
version="1.1"
id="noise"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400">
<filter id='noiseFilter'>
<feTurbulence type="turbulence" baseFrequency="0.2" numOctaves="2" result="turbulence"></feTurbulence>
<feDisplacementMap in2="turbulence" in="SourceGraphic" scale="2" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
</filter>
</svg>
);
}
componentDidMount() {
this.animations.push(anime({
targets: ['#noiseFilter feTurbulence', '#noiseFilter feDisplacementMap'],
easing: 'linear',
loop: true,
keyframes: [
{
baseFrequency: 0.5,
duration: () => anime.random(1000, 2000),
},
],
}));
this.animations.push(anime({
targets: ['#noiseFilter feDisplacementMap'],
easing: 'linear',
loop: true,
keyframes: [
{
scale: 2,
duration: () => anime.random(2000, 5000),
},
{
scale: 4,
duration: () => anime.random(150, 250),
},
{
scale: 2,
duration: () => anime.random(100, 150),
},
{
scale: 4,
duration: () => anime.random(150, 250),
},
],
}));
}
// this is necessary because
// skipping / timing / unmounting race conditions
// can cause the animations to cut short, this will ensure the values are reset
// because preact will recycle all these components
componentWillUnmount() {
for (let i = this.animations.length - 1; i >= 0; i--) {
this.animations[i].reset();
}
}
}
module.exports = Noise;

View File

@ -71,7 +71,6 @@ class Combos extends preact.Component {
<div class="combos"> <div class="combos">
<div class="combo-header"> <div class="combo-header">
<h2>COMBOS</h2> <h2>COMBOS</h2>
Combine colours and items.
</div> </div>
<div class="combo-list" <div class="combo-list"
onMouseOver={e => e.stopPropagation()} onMouseOver={e => e.stopPropagation()}

View File

@ -5,10 +5,9 @@ const Login = require('./welcome.login');
const Register = require('./welcome.register'); const Register = require('./welcome.register');
const Help = require('./welcome.help'); const Help = require('./welcome.help');
// const About = require('./welcome.about'); // const About = require('./welcome.about');
const Demo = require('./demo');
function Welcome() { function Welcome() {
const page = this.state.page || 'register'; const page = this.state.page || 'login';
const pageEl = () => { const pageEl = () => {
if (page === 'login') return <Login />; if (page === 'login') return <Login />;
@ -17,65 +16,32 @@ function Welcome() {
return false; return false;
}; };
const news = ( const form = <div>{pageEl()}</div>;
<div class="news">
<p> Welcome to mnml.</p>
<p> MNML is a turn-based 1v1 strategy game in an abstract setting. </p>
<p>
Build a unique team of 3 constructs from a range of skills and specialisations.<br />
Outplay your opponent in multiple rounds by adapting to an always shifting meta. <br />
Simple rules, complex interactions and unique mechanics.<br />
</p>
<p> Free to play, no pay to win. Register to start playing.<br /></p>
<a href='https://www.youtube.com/watch?v=VtZLlkpJuS8'>Tutorial Playthrough on YouTube</a>
</div>
);
const main = (['login', 'register', 'help'].includes(page))
? <section>{news}{pageEl()}</section>
: <Demo />;
return ( return (
<main class="menu welcome"> <header>
<header> <div class="options">
<div class="options"> <button
<button class={`login-btn ${page === 'login' ? 'highlight' : ''}`}
onClick={() => this.setState({ page: 'login' })} disabled={page === 'login'}
class='logo login-btn'> onClick={() => this.setState({ page: 'login' })}>
&nbsp; Login
</button> </button>
<button <button
class={`login-btn ${page === 'login' ? 'highlight' : ''}`} class={`login-btn ${page === 'register' ? 'highlight' : ''}`}
disabled={page === 'login'} disabled={page === 'register'}
onClick={() => this.setState({ page: 'login' })}> onClick={() => this.setState({ page: 'register' })}>
Login Register
</button> </button>
<button <button
class={`login-btn ${page === 'register' ? 'highlight' : ''}`} class={`login-btn ${page === 'help' ? 'highlight' : ''}`}
disabled={page === 'register'} disabled={page === 'help'}
onClick={() => this.setState({ page: 'register' })}> onClick={() => this.setState({ page: 'help' })}>
Register Help
</button> </button>
<button
class={`login-btn ${page === 'info' ? 'highlight' : ''}`}
disabled={page === 'info'}
onClick={() => this.setState({ page: 'info' })}>
Info
</button>
<button
class={`login-btn ${page === 'help' ? 'highlight' : ''}`}
disabled={page === 'help'}
onClick={() => this.setState({ page: 'help' })}>
Help
</button>
</div>
</header>
<div class="top">
{main}
</div> </div>
</main> {form}
</header>
); );
} }

View File

@ -27,7 +27,6 @@ function registerEvents(store) {
function clearTutorial() { function clearTutorial() {
store.dispatch(actions.setTutorial(null)); store.dispatch(actions.setTutorial(null));
localStorage.setItem('tutorial-complete', true);
} }
@ -35,7 +34,6 @@ function registerEvents(store) {
store.dispatch(actions.setTutorialGame(null)); store.dispatch(actions.setTutorialGame(null));
} }
function setPing(ping) { function setPing(ping) {
store.dispatch(actions.setPing(ping)); store.dispatch(actions.setPing(ping));
} }
@ -106,16 +104,21 @@ function registerEvents(store) {
} }
function setAccount(account) { function setAccount(account) {
if (account && process.env.NODE_ENV !== 'development') { store.dispatch(actions.setAccount(account));
LogRocket.init('yh0dy3/mnml'); }
LogRocket.identify(account.id, account);
if (window.Notification) { function setAuthenticated(account) {
window.Notification.requestPermission(); if (account && window.Notification) {
} window.Notification.requestPermission();
}
if (process.env.NODE_ENV !== 'development') {
LogRocket.identify(account.id, account);
} }
store.dispatch(actions.setAccount(account)); store.dispatch(actions.setAccount(account));
store.dispatch(actions.setTutorial(null));
store.dispatch(actions.setAuthenticated(true));
} }
function setEmail(email) { function setEmail(email) {
@ -180,18 +183,14 @@ function registerEvents(store) {
const player = v.players.find(p => p.id === account.id); const player = v.players.find(p => p.id === account.id);
store.dispatch(actions.setPlayer(player)); store.dispatch(actions.setPlayer(player));
if (tutorial) tutorialVbox(player, store, tutorial);
if (v.phase === 'Finished') { if (v.phase === 'Finished') {
ws.sendAccountInstances(); ws.sendAccountInstances();
} }
// instance.mobile.less hides info at @media 1000
if (localStorage.getItem('tutorial-complete') || window.innerWidth <= 1100) {
store.dispatch(actions.setTutorial(null));
} else if (v.time_control === 'Practice' && v.rounds.length === 1 && tutorial) {
tutorialVbox(player, store, tutorial);
}
} }
return store.dispatch(actions.setInstance(v)); return store.dispatch(actions.setInstance(v));
} }
@ -207,94 +206,6 @@ function registerEvents(store) {
return store.dispatch(actions.setItemInfo(v)); return store.dispatch(actions.setItemInfo(v));
} }
function setDemo(d) {
const vboxDemo = {
players: d,
combiner: [],
equipped: false,
equipping: false,
};
const startDemo = () => {
const { account, itemInfo } = store.getState();
if (account) return false;
if (!itemInfo || itemInfo.items.length === 0) return setTimeout(startDemo, 500);
store.dispatch(actions.setAnimTarget(null));
const bases = ['Attack', 'Stun', 'Buff', 'Debuff', 'Block'];
const combo = sample(itemInfo.combos.filter(i => bases.some(b => i.components.includes(b))));
vboxDemo.combo = combo.item;
vboxDemo.items = combo.components;
store.dispatch(actions.setDemo(vboxDemo));
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0] }))), 500);
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1] }))), 1000);
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1, 2] }))), 1500);
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: [vboxDemo.combo, '', ''] }))), 2500);
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0], items: [vboxDemo.combo, '', ''], equipping: true }))), 3000);
setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: ['', '', ''], equipped: true, equipping: false }))), 4000);
setTimeout(() => {
return store.dispatch(actions.setAnimTarget({
skill: sample(itemInfo.items.filter(i => i.skill)).item,
constructId: d[1].constructs[0].id,
player: false,
direction: 0,
}));
}, 500);
setTimeout(() => {
return store.dispatch(actions.setAnimTarget({
skill: sample(itemInfo.items.filter(i => i.skill)).item,
constructId: d[1].constructs[1].id,
player: true,
direction: 0,
}));
}, 3000);
return setTimeout(startDemo, 5000);
};
startDemo();
}
// store.subscribe(setInfo);
// store.on('SET_INFO', setInfo);
// events.on('SET_PLAYER', setInstance);
// events.on('SEND_SKILL', function skillActive(gameId, constructId, targetConstructId, skill) {
// ws.sendGameSkill(gameId, constructId, targetConstructId, skill);
// setConstructStatusUpdate(constructId, skill, targetConstructId);
// });
// events.on('CONSTRUCT_ACTIVE', function constructActiveCb(construct) {
// for (let i = 0; i < constructs.length; i += 1) {
// if (constructs[i].id === construct.id) constructs[i].active = !constructs[i].active;
// }
// return setConstructs(constructs);
// });
/* function errorPrompt(type) {
const message = errMessages[type];
const OK_BUTTON = '<button type="submit">OK</button>';
toast.error({
theme: 'dark',
color: 'black',
timeout: false,
drag: false,
position: 'center',
maxWidth: window.innerWidth / 2,
close: false,
buttons: [
[OK_BUTTON, (instance, thisToast) => instance.hide({ transitionOut: 'fadeOut' }, thisToast)],
],
message,
});
} */
// setup / localstorage
function urlHashChange() { function urlHashChange() {
const { ws } = store.getState(); const { ws } = store.getState();
const cmds = querystring.parse(location.hash); const cmds = querystring.parse(location.hash);
@ -303,6 +214,11 @@ function registerEvents(store) {
return true; return true;
} }
function startTutorial() {
store.dispatch(actions.setTutorial(1));
}
window.addEventListener('hashchange', urlHashChange, false); window.addEventListener('hashchange', urlHashChange, false);
return { return {
@ -313,11 +229,11 @@ function registerEvents(store) {
clearTutorial, clearTutorial,
clearTutorialGame, clearTutorialGame,
setAccount, setAccount,
setAuthenticated,
setAccountInstances, setAccountInstances,
setActiveItem, setActiveItem,
setActiveSkill, setActiveSkill,
setChatWheel, setChatWheel,
setDemo,
setConstructList, setConstructList,
setNewConstruct, setNewConstruct,
setGame, setGame,
@ -333,6 +249,8 @@ function registerEvents(store) {
setSubscription, setSubscription,
setWs, setWs,
startTutorial,
urlHashChange, urlHashChange,
notify, notify,

View File

@ -10,6 +10,7 @@ function createReducer(defaultState, actionType) {
/* eslint-disable key-spacing */ /* eslint-disable key-spacing */
module.exports = { module.exports = {
account: createReducer(null, 'SET_ACCOUNT'), account: createReducer(null, 'SET_ACCOUNT'),
authenticated: createReducer(null, 'SET_AUTHENTICATED'),
activeItem: createReducer(null, 'SET_ACTIVE_VAR'), activeItem: createReducer(null, 'SET_ACTIVE_VAR'),
activeSkill: createReducer(null, 'SET_ACTIVE_SKILL'), activeSkill: createReducer(null, 'SET_ACTIVE_SKILL'),
@ -20,8 +21,6 @@ module.exports = {
resolution: createReducer(null, 'SET_RESOLUTION'), resolution: createReducer(null, 'SET_RESOLUTION'),
demo: createReducer(null, 'SET_DEMO'),
chatShow: createReducer(null, 'SET_CHAT_SHOW'), chatShow: createReducer(null, 'SET_CHAT_SHOW'),
chatWheel: createReducer([], 'SET_CHAT_WHEEL'), chatWheel: createReducer([], 'SET_CHAT_WHEEL'),

View File

@ -256,10 +256,6 @@ function createSocket(events) {
events.setItemInfo(info); events.setItemInfo(info);
} }
function onDemo(v) {
events.setDemo(v);
}
let pongTimeout; let pongTimeout;
function onPong() { function onPong() {
events.setPing(Date.now() - ping); events.setPing(Date.now() - ping);
@ -274,6 +270,7 @@ function createSocket(events) {
// this object wraps the reply types to a function // this object wraps the reply types to a function
const handlers = { const handlers = {
AccountState: onAccount, AccountState: onAccount,
AccountAuthenticated: account => events.setAuthenticated(account),
AccountConstructs: onAccountConstructs, AccountConstructs: onAccountConstructs,
AccountTeam: onAccountTeam, AccountTeam: onAccountTeam,
AccountInstances: onAccountInstances, AccountInstances: onAccountInstances,
@ -285,7 +282,6 @@ function createSocket(events) {
InstanceState: onInstanceState, InstanceState: onInstanceState,
ItemInfo: onItemInfo, ItemInfo: onItemInfo,
Pong: onPong, Pong: onPong,
Demo: onDemo,
// QueueRequested: () => events.notify('PVP queue request received.'), // QueueRequested: () => events.notify('PVP queue request received.'),
QueueRequested: () => true, QueueRequested: () => true,
@ -304,6 +300,8 @@ function createSocket(events) {
ChatWheel: wheel => events.setChatWheel(wheel), ChatWheel: wheel => events.setChatWheel(wheel),
// Joining: () => events.notify('Searching for instance...'), // Joining: () => events.notify('Searching for instance...'),
StartTutorial: () => events.startTutorial(),
Processing: () => true, Processing: () => true,
Error: errHandler, Error: errHandler,
}; };

View File

@ -115,8 +115,9 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 1) { if (tutorial === 1) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h1>Welcome to MNML</h1>
<p> Welcome to the vbox phase tutorial.</p> <p> This is the <b>VBOX Phase</b> tutorial.</p>
<p> In the <b>VBOX Phase</b> you customise your constructs' skills and specialisations. </p>
<p> Colours are used to create powerful combinations with base items. </p> <p> Colours are used to create powerful combinations with base items. </p>
<p> Buy the two colours from the store to continue. </p> <p> Buy the two colours from the store to continue. </p>
</div> </div>
@ -126,9 +127,9 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 2) { if (tutorial === 2) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Combining Items</h2>
<p> You start the game with the base <b>Attack</b> skill item. </p> <p> You start the game with the base <b>Attack</b> skill item. </p>
<p> Highlight all three items then click combine.</p> <p> Highlight the <b>Attack</b> and the two <b> colours</b> then click <b> combine</b> </p>
</div> </div>
); );
} }
@ -137,11 +138,11 @@ function tutorialStage(tutorial, clearTutorial, instance) {
const constructOne = instance.players[0].constructs[0].name; const constructOne = instance.players[0].constructs[0].name;
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Equipping Items</h2>
<p> The first construct on your team is <b>{constructOne}</b>. </p> <p> The first construct on your team is <b>{constructOne}</b>. </p>
<p> Skill items can be equipped to your constructs to be used in the combat phase. </p> <p> Skill items can be equipped to your constructs to be used in the combat phase. </p>
<p> Click your new skill from the stash. <br /> <p> Click your new skill from the stash. <br />
Once selected click the flashing <b>SKILL</b> slot to equip the skill. </p> Once selected click the flashing <b>SKILL</b> slot or the construct img to equip the skill. </p>
</div> </div>
); );
} }
@ -149,7 +150,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 4) { if (tutorial === 4) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Specialisations</h2>
<p> You can also buy specialisation items for your constructs. <br /> <p> You can also buy specialisation items for your constructs. <br />
Specialisation items increase stats including power, speed and life. </p> Specialisation items increase stats including power, speed and life. </p>
<p> Buy the specialisation item from the store to continue. </p> <p> Buy the specialisation item from the store to continue. </p>
@ -160,11 +161,12 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 5) { if (tutorial === 5) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Specialisations</h2>
<p> Equipping specialisation items will increase the stats of your constructs.</p> <p> Equipping specialisation items will increase the stats of your constructs.</p>
<p> These can also be combined with colours for further specialisation. </p> <p> These can also be combined with colours for further specialisation. </p>
<p> Click the specialisation item in the stash.<br /> <p> Click the specialisation item in the stash.<br />
Once selected click the flashing <b>SPEC</b> slot to equip the specialisation. </p> Once selected click the flashing <b>SPEC</b> slot to equip the specialisation. </p>
<p> <b>PRO TIP:</b> while selecting an item in the shop, click on your construct to buy and equip in one step. </p>
</div> </div>
); );
} }
@ -174,11 +176,12 @@ function tutorialStage(tutorial, clearTutorial, instance) {
const constructThree = instance.players[0].constructs[2].name; const constructThree = instance.players[0].constructs[2].name;
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Skills</h2>
<p> You have now created a construct with an upgraded skill and base spec. </p> <p> You have now created a construct with an upgraded skill and base spec. </p>
<p> The goal is to create three powerful constructs for combat. </p> <p> The goal is to create three powerful constructs for combat. </p>
<p> Equip your other constructs <b>{constructTwo}</b> and <b>{constructThree}</b> with the Attack skill. <br /> <p> Equip your other constructs <b>{constructTwo}</b> and <b>{constructThree}</b> with the Attack skill. <br />
Ensure each construct has a single skill to continue. </p> Ensure each construct has a single skill to continue. </p>
<p> <b>PRO TIP:</b> Select a skill or spec on a construct and click another construct to swap it. </p>
</div> </div>
); );
} }
@ -186,7 +189,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 7) { if (tutorial === 7) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>Economy</h2>
<p> Each round you start with 30 bits and a store full of different skills, specs and colours. </p> <p> Each round you start with 30 bits and a store full of different skills, specs and colours. </p>
<p> Bits are your currency for buying items. <br /> <p> Bits are your currency for buying items. <br />
You can refill the store by pressing the refill button for 2b. <br /> You can refill the store by pressing the refill button for 2b. <br />
@ -203,22 +206,22 @@ function tutorialStage(tutorial, clearTutorial, instance) {
return ( return (
<div class='info-item'> <div class='info-item'>
<h2>Tutorial</h2> <h2>GLHF</h2>
<p>That completes the VBOX Tutorial.</p> <p>That completes the VBOX Tutorial.</p>
<p>Press <b>READY</b> to progress to the <b>GAME PHASE</b> <br /> <p>Press the green <b>READY</b> button in the bottom right to progress to the <b>GAME PHASE</b> <br />
You can continue creating new items to upgrade your constructs further. </p> or continue creating new items to upgrade your constructs further. </p>
</div> </div>
); );
} }
return false; return false;
}; };
const classes = tutorial === 8 ? 'focus' : ''; const exitTutorial = tutorial === 8 ?
const text = tutorial === 8 ? 'Continue' : 'Skip Tutorial' <button
const exitTutorial = <button class='focus'
class={classes} onClick={e => e.stopPropagation()}
onClick={e => e.stopPropagation()} onMouseDown={exit}> Continue </button>
onMouseDown={exit}> {text} </button>; : null;
return ( return (
<div class='tutorial'> <div class='tutorial'>

View File

@ -241,30 +241,15 @@ function convertItem(v) {
function effectInfo(i) { function effectInfo(i) {
// FIX ME // FIX ME
return 'effect info to be fixed'; const hybridBlast = 25;
/*const hybridBlast = 25;
const hasteStrike = 30; const hasteStrike = 30;
function multiplier(s) { // Update later to use server info in future function multiplier(s) { // Update later to use server info in future
if (s === 'CounterAttack') return 120; if (s === 'CounterAttack') return 115;
if (s === 'CounterAttack+') return 160; if (s === 'CounterAttack+') return 130;
if (s === 'CounterAttack++') return 230; if (s === 'CounterAttack++') return 160;
if (s === 'Electrocute') return 80;
if (s === 'DecayTick') return 33; if (s === 'Electrocute+') return 90;
if (s === 'DecayTick+') return 45; if (s === 'Electrocute++') return 100;
if (s === 'DecayTick++') return 70;
if (s === 'SiphonTick') return 25;
if (s === 'SiphonTick+') return 30;
if (s === 'SiphonTick++') return 40;
if (s === 'TriageTick') return 75;
if (s === 'TriageTick+') return 110;
if (s === 'TriageTick++') return 140;
if (s === 'Electrocute' || s === 'ElectrocuteTick') return 80;
if (s === 'Electrocute+' || s === 'ElectrocuteTick+') return 100;
if (s === 'Electrocute++' || s === 'ElectrocuteTick++') return 130;
return 0; return 0;
} }
@ -289,16 +274,16 @@ function effectInfo(i) {
case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`; case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`;
case 'Silence': return 'Disable construct from casting any blue skills'; case 'Silence': return 'Disable construct from casting any blue skills';
case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; // case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; //
case 'Decay': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`; // case 'Decay': return `Construct will take ${i.meta[1].amount} blue damage each turn.`; //
case 'Electric': return `Attacks against this construct will apply Electrocute dealing ${multiplier(i.meta[1])}% of construct BluePower as blue damage each turn.`; case 'Electric': return `Attacks against this construct will apply Electrocute dealing ${multiplier(i.meta[1])}% of construct BluePower as blue damage each turn.`;
case 'Electrocute': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`; case 'Electrocute': return `Construct will take ${i.meta[1].amount} blue damage each turn.`;
case 'Absorb': return 'If construct takes damage, Absorption will be applied increasing RedPower and BluePower based on damage taken.'; case 'Absorb': return 'If construct takes damage, Absorption will be applied increasing RedPower and BluePower based on damage taken.';
case 'Absorption': return `Increasing construct RedPower and BluePower by ${i.meta[1]}`; case 'Absorption': return `Increasing construct RedPower and BluePower by ${i.meta[1]}`;
case 'Triage': return `Construct will be healed for ${multiplier(i.tick.skill)}% of caster's GreenPower each turn.`; case 'Triage': return `Construct will be healed for ${i.meta[1].amount} green life each turn.`;
case 'Siphon': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower + GreenPower as blue damage each turn, healing the caster.`; case 'Siphon': return `Construct will take ${i.meta[1].amount} blue damage each turn, healing the caster.`;
default: return 'Missing Effect Text'; default: return 'Missing Effect Text';
}*/ }
} }
module.exports = { module.exports = {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mnml_core" name = "mnml_core"
version = "1.10.0" version = "1.11.0"
authors = ["ntr <ntr@smokestack.io>", "mashy <mashy@mnml.gg>"] authors = ["ntr <ntr@smokestack.io>", "mashy <mashy@mnml.gg>"]
[dependencies] [dependencies]

View File

@ -1,6 +1,5 @@
# FIXME # FIXME
check silence skill multiplier
game ready not auto starting resolve phase game ready not auto starting resolve phase
cooldowns set after cast remove big header and move to rhs of news pane
cooldowns reduced after 1 complete cast add big logo w/ noise when you mouseover stuff etc

View File

@ -1,5 +1,3 @@
use std::iter;
use uuid::Uuid; use uuid::Uuid;
use rand::prelude::*; use rand::prelude::*;
@ -435,12 +433,14 @@ impl Construct {
} }
pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct { pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct {
// println!("{:?} {:?} skill cooldown set", self.name, skill);
// tests force resolve some skills // tests force resolve some skills
// which cause the game to attempt to put them on cd // which cause the game to attempt to put them on cd
// even though the construct doesn't know the skill // even though the construct doesn't know the skill
if let Some(i) = self.skills.iter().position(|s| s.skill == skill) { if let Some(i) = self.skills.iter().position(|s| s.skill == skill) {
self.skills.remove(i); self.skills.remove(i);
self.skills.push(ConstructSkill::new(skill)); self.skills.insert(i, ConstructSkill::new(skill));
} }
self self
@ -493,11 +493,13 @@ impl Construct {
self.effects = self.effects.clone().into_iter().filter_map(|mut effect| { self.effects = self.effects.clone().into_iter().filter_map(|mut effect| {
effect.duration = effect.duration.saturating_sub(1); effect.duration = effect.duration.saturating_sub(1);
// println!("{:?}", effect);
if effect.duration == 0 { if effect.duration == 0 {
return None; return None;
} }
// info!("reduced effect {:?}", effect); info!("reduced effect {:?}", effect);
return Some(effect); return Some(effect);
}).collect::<Vec<ConstructEffect>>(); }).collect::<Vec<ConstructEffect>>();
@ -946,7 +948,10 @@ impl Construct {
if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] } if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] }
if let Some(p) = self.effects.iter().position(|ce| ce.effect == effect) { if let Some(p) = self.effects.iter().position(|ce| ce.effect == effect) {
self.effects.remove(p); let ce = self.effects.remove(p);
if ce.effect.hidden() { return vec![] }
return vec![Event::Removal { return vec![Event::Removal {
construct: self.id, construct: self.id,
effect: effect, effect: effect,
@ -982,11 +987,13 @@ impl Construct {
if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] } if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] }
while let Some(ce) = self.effects.pop() { while let Some(ce) = self.effects.pop() {
removals.push(Event::Removal { if !ce.effect.hidden() {
construct: self.id, removals.push(Event::Removal {
effect: ce.effect, construct: self.id,
display: EventConstruct::new(self), effect: ce.effect,
}); display: EventConstruct::new(self),
});
}
} }
return removals; return removals;
@ -1012,7 +1019,7 @@ impl Construct {
match meta { match meta {
Some(EffectMeta::CastOnHit(skill)) => Some(EffectMeta::CastOnHit(skill)) =>
casts.push(Cast::new(self.id, self.account, cast.target, *skill)), casts.push(Cast::new(self.id, self.account, cast.source, *skill)),
_ => panic!("no electrify skill {:?}", meta), _ => panic!("no electrify skill {:?}", meta),
}; };
} }
@ -1119,9 +1126,9 @@ mod tests {
construct.apply_modifiers(&player_colours); construct.apply_modifiers(&player_colours);
assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(15));
assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(50)); assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(24));
assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(70)); assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(37));
return; return;
} }
@ -1170,7 +1177,7 @@ mod tests {
let colours = Colours::from_construct(&construct); let colours = Colours::from_construct(&construct);
assert!(colours.red == 4); assert!(colours.red == 4);
assert!(colours.blue == 20); assert!(colours.blue == 10);
assert!(colours.green == 2); assert!(colours.green == 2);
} }
@ -1191,9 +1198,9 @@ mod tests {
construct.apply_modifiers(&player_colours); construct.apply_modifiers(&player_colours);
assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(15));
assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(25)); assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(10));
assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(25)); assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(10));
return; return;
} }

View File

@ -75,38 +75,19 @@ impl Effect {
match self { match self {
Effect::Banish => true, Effect::Banish => true,
// delete sustain immunitiy??? // these provide immunity for the ticks
/*Effect::Sustain => [ // the base skills will still resolve
Skill::Stun, // but they have early return checks
Skill::Silence, // to ensure the effect is reapplied but damage is not
Skill::SilencePlus,
Skill::SilencePlusPlus,
Skill::Ruin,
Skill::RuinPlus,
Skill::RuinPlusPlus,
Skill::Restrict,
Skill::RestrictPlus,
Skill::RestrictPlusPlus
].contains(&skill),*/
Effect::Siphoned => [ Effect::Siphoned => [
Skill::Siphon,
Skill::SiphonPlus,
Skill::SiphonPlusPlus,
Skill::SiphonTick, Skill::SiphonTick,
].contains(&skill), ].contains(&skill),
Effect::Decayed => [ Effect::Decayed => [
Skill::Decay,
Skill::DecayPlus,
Skill::DecayPlusPlus,
Skill::DecayTick, Skill::DecayTick,
].contains(&skill), ].contains(&skill),
Effect::Triaged => [ Effect::Triaged => [
Skill::Triage,
Skill::TriagePlus,
Skill::TriagePlusPlus,
Skill::TriageTick, Skill::TriageTick,
].contains(&skill), ].contains(&skill),

View File

@ -135,20 +135,6 @@ impl Game {
.unwrap() .unwrap()
} }
pub fn update_construct(&mut self, construct: &mut Construct) -> &mut Game {
match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == construct.id)) {
Some(player) => {
let index = player.constructs.iter().position(|t| t.id == construct.id).unwrap();
player.constructs.remove(index);
player.constructs.push(construct.clone());
player.constructs.sort_unstable_by_key(|c| c.id);
},
None => panic!("construct not in game"),
};
self
}
pub fn can_start(&self) -> bool { pub fn can_start(&self) -> bool {
return self.players.len() == self.player_num return self.players.len() == self.player_num
&& self.players.iter().all(|t| t.constructs.len() == self.player_constructs) && self.players.iter().all(|t| t.constructs.len() == self.player_constructs)
@ -350,7 +336,11 @@ impl Game {
pub fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { pub fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> {
self.player_by_id(player_id)?; let player = self.player_by_id(player_id)?;
if player.ready {
return Err(err_msg("cannot clear skills while ready"));
}
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase")); return Err(err_msg("game not in skill phase"));
} }
@ -398,6 +388,11 @@ impl Game {
let mut sorted = self.stack.clone(); let mut sorted = self.stack.clone();
sorted.iter_mut() sorted.iter_mut()
.for_each(|s| { .for_each(|s| {
// we do not modify the speed of ticks
// as they are considered to be pinned to the speed
// that they were initially cast
if !s.skill.is_tick() { if !s.skill.is_tick() {
let caster = self.construct_by_id(s.source).unwrap(); let caster = self.construct_by_id(s.source).unwrap();
let speed = caster.skill_speed(s.skill); let speed = caster.skill_speed(s.skill);
@ -462,7 +457,15 @@ impl Game {
self.skill_phase_start(r_animation_ms) self.skill_phase_start(r_animation_ms)
} }
fn modify_cast(&self, cast: Cast) -> Vec<Cast> { fn modify_cast(&self, mut cast: Cast) -> Vec<Cast> {
// reassign the speeds based on the caster
// for test purposes
if !cast.skill.is_tick() {
let speed = self.construct(cast.source).skill_speed(cast.skill);
cast.speed = speed;
}
let target_player = self.players.iter() let target_player = self.players.iter()
.find(|t| t.constructs.iter().any(|c| c.id == cast.target)) .find(|t| t.constructs.iter().any(|c| c.id == cast.target))
.unwrap(); .unwrap();
@ -508,17 +511,19 @@ impl Game {
self.resolve(Cast { skill, ..cast }); self.resolve(Cast { skill, ..cast });
} }
// for aoe events send the source / target animations before each set of casts let casts = self.modify_cast(cast);
if cast.skill.aoe() {
if cast.skill.cast_animation() { let castable = casts
let event = self.cast(cast); .iter()
self.add_resolution(&cast, &event); .any(|c| !self.construct(c.target).is_ko() && !self.construct(c.target).immune(c.skill).is_some());
if castable {
self.action(cast, Action::Cast);
if cast.skill.aoe() {
self.action(cast, Action::HitAoe);
} }
let event = self.hit_aoe(cast);
self.add_resolution(&cast, &event);
} }
let casts = self.modify_cast(cast);
for cast in casts { for cast in casts {
self.execute(cast); self.execute(cast);
} }
@ -550,6 +555,10 @@ impl Game {
return self.resolve(Cast { target: cast.source, ..cast }); return self.resolve(Cast { target: cast.source, ..cast });
} }
if !cast.skill.aoe() {
self.action(cast, Action::Hit);
}
cast.resolve(self); cast.resolve(self);
self self
@ -559,6 +568,7 @@ impl Game {
let new_events = match action { let new_events = match action {
Action::Cast => vec![self.cast(cast)], Action::Cast => vec![self.cast(cast)],
Action::Hit => vec![self.hit(cast)], Action::Hit => vec![self.hit(cast)],
Action::HitAoe => vec![self.hit_aoe(cast)],
Action::Damage { construct, amount, colour } => self.damage(construct, amount, colour), Action::Damage { construct, amount, colour } => self.damage(construct, amount, colour),
Action::Heal { construct, amount, colour } => self.heal(construct, amount, colour), Action::Heal { construct, amount, colour } => self.heal(construct, amount, colour),
@ -642,11 +652,14 @@ impl Game {
Value::TickDamage { construct, effect } => Value::TickDamage { construct, effect } =>
self.construct(construct).stat(Stat::TickDamage(effect)), self.construct(construct).stat(Stat::TickDamage(effect)),
// Skills { construct: Uuid, colour: Colour }, // Skills { construct: Uuid, colour: Colour },
} }
} }
pub fn affected(&self, construct: Uuid, effect: Effect) -> bool {
self.construct(construct).affected(effect)
}
fn cast(&mut self, cast: Cast) -> Event { fn cast(&mut self, cast: Cast) -> Event {
Event::Cast { construct: cast.source, player: cast.player, target: cast.target, skill: cast.skill, direction: self.direction(cast) } Event::Cast { construct: cast.source, player: cast.player, target: cast.target, skill: cast.skill, direction: self.direction(cast) }
} }
@ -843,11 +856,13 @@ pub enum Value {
Removals { construct: Uuid }, Removals { construct: Uuid },
DamageReceived { construct: Uuid, colour: Colour }, DamageReceived { construct: Uuid, colour: Colour },
TickDamage { construct: Uuid, effect: Effect }, TickDamage { construct: Uuid, effect: Effect },
// Affected { construct: Uuid, effect: Effect }, // not an int :(
} }
#[derive(Debug,Clone,PartialEq)] #[derive(Debug,Clone,PartialEq)]
pub enum Action { pub enum Action {
Hit, Hit,
HitAoe,
Cast, Cast,
Heal { construct: Uuid, amount: usize, colour: Colour }, Heal { construct: Uuid, amount: usize, colour: Colour },
Damage { construct: Uuid, amount: usize, colour: Colour }, Damage { construct: Uuid, amount: usize, colour: Colour },
@ -1033,6 +1048,7 @@ mod tests {
.learn(Skill::Siphon) .learn(Skill::Siphon)
.learn(Skill::Amplify) .learn(Skill::Amplify)
.learn(Skill::Stun) .learn(Skill::Stun)
.learn(Skill::Ruin)
.learn(Skill::Block) .learn(Skill::Block)
.learn(Skill::Sleep) .learn(Skill::Sleep)
.learn(Skill::Decay); .learn(Skill::Decay);
@ -1251,6 +1267,32 @@ mod tests {
assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none());
} }
#[test]
fn ruin_cooldown_test() {
let mut game = create_test_game();
let x_player = game.players[0].clone();
let y_player = game.players[1].clone();
let x_construct = x_player.constructs[0].clone();
let y_construct = y_player.constructs[0].clone();
while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Ruin).is_some() {
game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns();
}
game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Ruin).unwrap();
game.player_ready(x_player.id).unwrap();
game.player_ready(y_player.id).unwrap();
game = game.resolve_phase_start();
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Ruin).is_some());
}
// #[cfg(test)] // #[cfg(test)]
// mod tests { // mod tests {
// use skill::*; // use skill::*;
@ -1595,55 +1637,6 @@ mod tests {
// } // }
// } // }
#[test]
fn sleep_cooldown_test() {
let mut game = create_test_game();
let x_player = game.players[0].clone();
let y_player = game.players[1].clone();
let x_construct = x_player.constructs[0].clone();
let y_construct = y_player.constructs[0].clone();
for _n in 1..10 {
// should auto progress back to skill phase
assert!(game.phase == Phase::Skill);
// Sleep 2T CD
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none());
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some());
game.player_ready(x_player.id).unwrap();
game.player_ready(y_player.id).unwrap();
game = game.resolve_phase_start();
// Sleep 1T CD
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none());
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some());
game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Decay).unwrap();
// game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap();
game.player_ready(x_player.id).unwrap();
game.player_ready(y_player.id).unwrap();
game = game.resolve_phase_start();
// Sleep 0T CD (we use it here)
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none());
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_none());
game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Sleep).unwrap();
game.player_ready(x_player.id).unwrap();
game.player_ready(y_player.id).unwrap();
game = game.resolve_phase_start();
// Sleep back to 2T CD
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none());
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some());
}
}
// #[test] // #[test]
// fn counter_test() { // fn counter_test() {
// let mut game = create_test_game(); // let mut game = create_test_game();
@ -1712,56 +1705,6 @@ mod tests {
// assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Electrocute)); // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Electrocute));
// } // }
// // #[test]
// // fn link_test() {
// // let mut game = create_test_game();
// // let x_player = game.players[0].clone();
// // let y_player = game.players[1].clone();
// // let x_construct = x_player.constructs[0].clone();
// // let y_construct = y_player.constructs[0].clone();
// // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Link);
// // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Link).is_some() {
// // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns();
// // }
// // // apply buff
// // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Link).unwrap();
// // game.player_ready(x_player.id).unwrap();
// // game.player_ready(y_player.id).unwrap();
// // game = game.resolve_phase_start();
// // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Link));
// // let Resolution { source: _, target: _, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap();
// // match Resolution {
// // Resolution::Effect { effect, skill: _, duration: _, construct_effects: _ } => assert_eq!(effect, Effect::Link),
// // _ => panic!("not siphon"),
// // };
// // let Resolution { source: _, target: _, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap();
// // match Resolution {
// // Resolution::Recharge { red: _, blue: _, skill: _ } => (),
// // _ => panic!("link result was not recharge"),
// // }
// // // attack and receive link hit
// // game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap();
// // game.player_ready(x_player.id).unwrap();
// // game.player_ready(y_player.id).unwrap();
// // game = game.resolve_phase_start();
// // let Resolution { source: _, target, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap();
// // assert_eq!(target.id, y_construct.id);
// // match Resolution {
// // Resolution::Damage { amount, skill: _, mitigation: _, colour: _} =>
// // assert_eq!(amount, x_construct.red_power().pct(Skill::Attack.multiplier()) >> 1),
// // _ => panic!("not damage link"),
// // };
// // }
// // #[test] // // #[test]
// // fn absorb_test() { // // fn absorb_test() {
// // let mut game = create_test_game(); // // let mut game = create_test_game();
@ -2015,7 +1958,7 @@ mod tests {
let mut game = create_2v2_test_game(); let mut game = create_2v2_test_game();
game.players[0].set_ready(true); game.players[0].set_ready(true);
game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap()); game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap());
game = game.upkeep(); game.upkeep();
// assert!(game.players[1].warnings == 1); // assert!(game.players[1].warnings == 1);
} }
@ -2026,7 +1969,7 @@ mod tests {
let source = game.players[0].constructs[0].id; let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id; let target = game.players[1].constructs[0].id;
game.add_skill(player_id, source, target, Skill::Attack).unwrap(); game.add_skill(player_id, source, target, Skill::Attack).unwrap();
game = game.resolve_phase_start(); game.resolve_phase_start();
} }
#[test] #[test]
@ -2122,6 +2065,43 @@ mod tests {
})); }));
} }
#[test]
fn link_test() {
let mut game = create_2v2_test_game();
let player_id = game.players[0].id;
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.players[1].constructs[0].blue_life.force(0);
game.new_resolve(Cast::new(source, player_id, target, Skill::Link));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Damage { construct, colour, amount, mitigation: _, display: _ } =>
construct == target && amount == 320.pct(50) && colour == Colour::Blue,
_ => false,
}));
game = game.resolve_phase_start();
game.new_resolve(Cast::new(source, player_id, target, Skill::Triage));
game.new_resolve(Cast::new(source, player_id, target, Skill::Link));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Damage { construct, colour, amount, mitigation: _, display: _ } =>
construct == target && amount == 320.pct(75) && colour == Colour::Blue,
_ => false,
}));
}
#[test] #[test]
fn siphon_test() { fn siphon_test() {
let mut game = create_2v2_test_game(); let mut game = create_2v2_test_game();
@ -2158,6 +2138,9 @@ mod tests {
// que ota? // que ota?
game.resolve(Cast::new(source, player_id, target, Skill::Siphon)); game.resolve(Cast::new(source, player_id, target, Skill::Siphon));
game.resolve(Cast::new(source, player_id, target, Skill::Siphon));
game.resolve(Cast::new(source, player_id, target, Skill::Siphon));
let last = game.resolutions.len() - 1; let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last]; let resolutions = &game.resolutions[last];
@ -2166,7 +2149,75 @@ mod tests {
_ => false, _ => false,
}).count(); }).count();
let effect_events = resolutions.iter().filter(|r| match r.event {
Event::Effect { construct, effect, duration: _, display: _ } =>
construct == target && effect == Effect::Siphon,
_ => false,
}).count();
// Deal siphon dmg once
assert_eq!(damage_events, 1); assert_eq!(damage_events, 1);
// 3 new applications of siphon
assert_eq!(effect_events, 3);
// Siphon + Siphoned
assert!(game.players[1].constructs[0].effects.len() == 2);
}
#[test]
fn hybrid_test() {
let mut game = create_2v2_test_game();
let player_id = game.players[0].id;
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.players[1].constructs[0].blue_life.force(0);
game.resolve(Cast::new(source, player_id, source, Skill::Hybrid));
game.resolve(Cast::new(source, player_id, target, Skill::Siphon));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.skill {
Skill::HybridBlast => true,
_ => false
}));
assert!(resolutions.iter().filter(|r| match r.event {
Event::Damage { construct: _, colour: _, amount: _, mitigation: _, display: _ } => true,
_ => false,
}).count() == 2);
let _siphon_dmg = resolutions.iter().find_map(|r| match r.skill {
Skill::Siphon => {
match r.event {
Event::Damage { construct: _, colour: _, amount, mitigation: _, display: _ } => Some(amount),
_ => None,
}
},
_ => None
}).expect("no siphon dmg");
// let hybrid_dmg = resolutions.iter().find_map(|r| match r.skill {
// Skill::HybridBlast => {
// match r.event {
// Event::Damage { construct: _, colour: _, amount, mitigation: _, display: _ } => Some(amount),
// _ => None,
// }
// },
// _ => None
// }).expect("no hybrid dmg");
// assert!(resolutions.iter().any(|r| match r.event {
// Event::Healing { construct, colour, amount, overhealing, display: _ } => {
// construct == source && (amount + overhealing) == siphon_dmg && colour == Colour::Green
// // this works
// // construct == source && (amount + overhealing) == (siphon_dmg + hybrid_dmg) && colour == Colour::Green
// },
// _ => false,
// }));
} }
#[test] #[test]
@ -2189,6 +2240,85 @@ mod tests {
})); }));
} }
#[test]
fn electrify_test() {
let mut game = create_2v2_test_game();
let player_id = game.players[0].id;
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.resolve(Cast::new(source, player_id, target, Skill::Electrify));
game.resolve(Cast::new(source, player_id, target, Skill::Blast));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Damage { construct, colour, amount, mitigation: _, display: _ } =>
construct == source && amount > 0 && colour == Colour::Blue,
_ => false,
}));
}
#[test]
fn triage_test() {
let mut game = create_2v2_test_game();
let player_id = game.players[0].id;
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.resolve(Cast::new(source, player_id, target, Skill::Strike));
game.resolve(Cast::new(source, player_id, target, Skill::Triage));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Healing { construct, colour, amount, overhealing: _, display: _ } =>
construct == target && amount > 0 && colour == Colour::Green,
_ => false,
}));
// it's hidden
// assert!(resolutions.iter().any(|r| match r.event {
// Event::Effect { construct, effect, duration: _, display: _ } =>
// construct == target && effect == Effect::Triaged,
// _ => false,
// }));
game.progress_durations(); // pretend it's a new turn
game = game.resolve_phase_start();
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Healing { construct, colour, amount: _, overhealing, display: _ } =>
construct == target && overhealing > 0 && colour == Colour::Green,
_ => false,
}));
}
#[test]
fn counter_test() {
let mut game = create_2v2_test_game();
let player_id = game.players[0].id;
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.resolve(Cast::new(source, player_id, target, Skill::Counter));
game.resolve(Cast::new(source, player_id, target, Skill::Strike));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
assert!(resolutions.iter().any(|r| match r.event {
Event::Damage { construct, colour, amount, mitigation: _, display: _ } =>
construct == source && amount > 0 && colour == Colour::Red,
_ => false,
}));
}
#[test] #[test]
fn absorb_test() { fn absorb_test() {
let mut game = create_2v2_test_game(); let mut game = create_2v2_test_game();
@ -2376,6 +2506,8 @@ mod tests {
}) })
.unwrap(); .unwrap();
assert!(siphon_tick_speed > 0);
assert!(siphon_speed > 0);
assert_eq!(siphon_tick_dmg, siphon_dmg); assert_eq!(siphon_tick_dmg, siphon_dmg);
assert_eq!(siphon_tick_speed, siphon_speed); assert_eq!(siphon_tick_speed, siphon_speed);
} }

View File

@ -11,7 +11,6 @@ use chrono::prelude::*;
use chrono::Duration; use chrono::Duration;
use player::{Player, Score}; use player::{Player, Score};
use mob::{bot_player, instance_mobs};
use game::{Game}; use game::{Game};
use item::{Item}; use item::{Item};
use vbox; use vbox;
@ -144,8 +143,8 @@ impl Instance {
.collect::<Vec<Uuid>>() .collect::<Vec<Uuid>>()
} }
// time out lobbies that have been open too long
pub fn upkeep(mut self) -> (Instance, Option<Game>) { pub fn upkeep(mut self) -> (Instance, Option<Game>) {
// time out lobbies that have been open too long
if self.phase == InstancePhase::Lobby && self.phase_timed_out() { if self.phase == InstancePhase::Lobby && self.phase_timed_out() {
self.finish(); self.finish();
return (self, None); return (self, None);
@ -504,6 +503,7 @@ impl Instance {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use mob::{bot_player, instance_mobs};
#[test] #[test]
fn instance_pve_test() { fn instance_pve_test() {

View File

@ -716,7 +716,7 @@ impl Item {
Item::HealPlusPlus | Item::HealPlusPlus |
Item::Triage | Item::Triage |
Item::TriagePlus | Item::TriagePlus |
Item::TriagePlusPlus | Item::TriagePlusPlus |
Item::Break | Item::Break |
Item::BreakPlus | Item::BreakPlus |
Item::BreakPlusPlus | Item::BreakPlusPlus |
@ -735,7 +735,7 @@ impl Item {
Item::Invert | Item::Invert |
Item::InvertPlus | Item::InvertPlus |
Item::InvertPlusPlus | Item::InvertPlusPlus |
Item::Decay | Item::Decay |
Item::DecayPlus | Item::DecayPlus |
Item::DecayPlusPlus | Item::DecayPlusPlus |
Item::Siphon| Item::Siphon|
@ -817,7 +817,7 @@ impl Item {
Item::AmplifyPlus => vec![Item::Amplify, Item::Amplify], Item::AmplifyPlus => vec![Item::Amplify, Item::Amplify],
Item::AmplifyPlusPlus => vec![Item::AmplifyPlus, Item::AmplifyPlus], Item::AmplifyPlusPlus => vec![Item::AmplifyPlus, Item::AmplifyPlus],
Item::HastePlus => vec![Item::Haste, Item::Haste], Item::HastePlus => vec![Item::Haste, Item::Haste],
Item::HastePlusPlus => vec![Item::HastePlus, Item::HastePlus], Item::HastePlusPlus => vec![Item::HastePlus, Item::HastePlus],
@ -832,9 +832,9 @@ impl Item {
Item::Curse => vec![Item::Debuff, Item::Red, Item::Blue], Item::Curse => vec![Item::Debuff, Item::Red, Item::Blue],
Item::Decay => vec![Item::Debuff, Item::Green, Item::Blue], Item::Decay => vec![Item::Debuff, Item::Green, Item::Blue],
Item::PurgePlus => vec![Item::Purge, Item::Purge], Item::PurgePlus => vec![Item::Purge, Item::Purge],
Item::PurgePlusPlus => vec![Item::PurgePlus, Item::PurgePlus], Item::PurgePlusPlus => vec![Item::PurgePlus, Item::PurgePlus],
Item::InvertPlus => vec![Item::Invert, Item::Invert], Item::InvertPlus => vec![Item::Invert, Item::Invert],
Item::InvertPlusPlus => vec![Item::InvertPlus, Item::InvertPlus], Item::InvertPlusPlus => vec![Item::InvertPlus, Item::InvertPlus],
@ -843,10 +843,10 @@ impl Item {
Item::SilencePlus => vec![Item::Silence, Item::Silence], Item::SilencePlus => vec![Item::Silence, Item::Silence],
Item::SilencePlusPlus => vec![Item::SilencePlus, Item::SilencePlus], Item::SilencePlusPlus => vec![Item::SilencePlus, Item::SilencePlus],
Item::CursePlus => vec![Item::Curse, Item::Curse], Item::CursePlus => vec![Item::Curse, Item::Curse],
Item::CursePlusPlus => vec![Item::CursePlus, Item::CursePlus], Item::CursePlusPlus => vec![Item::CursePlus, Item::CursePlus],
Item::DecayPlus => vec![Item::Decay, Item::Decay], Item::DecayPlus => vec![Item::Decay, Item::Decay],
Item::DecayPlusPlus => vec![Item::DecayPlus, Item::DecayPlus], Item::DecayPlusPlus => vec![Item::DecayPlus, Item::DecayPlus],
@ -1427,20 +1427,13 @@ mod tests {
assert_eq!(Item::StrikePlus.components(), vec![ assert_eq!(Item::StrikePlus.components(), vec![
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
]); ]);
assert_eq!(Item::StrikePlusPlus.components(), vec![ assert_eq!(Item::StrikePlusPlus.components(), vec![
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
Item::Red, Item::Red, Item::Attack,
]); ]);
} }

View File

@ -31,6 +31,11 @@ pub fn bot_player() -> Player {
Player::new(bot_id, None, &name(), constructs).set_bot(true) Player::new(bot_id, None, &name(), constructs).set_bot(true)
} }
pub fn anon_player(id: Uuid) -> Player {
let constructs = instance_mobs(id);
Player::new(id, None, &"player".to_string(), constructs)
}
pub fn anim_test_game(skill: Skill) -> Game { pub fn anim_test_game(skill: Skill) -> Game {
let mut rng = thread_rng(); let mut rng = thread_rng();
let mut game = Game::new(); let mut game = Game::new();

View File

@ -1,7 +1,6 @@
use std::collections::{HashMap}; use std::collections::{HashMap};
use uuid::Uuid; use uuid::Uuid;
use rand::prelude::*;
use failure::Error; use failure::Error;
use failure::err_msg; use failure::err_msg;
@ -130,7 +129,7 @@ impl Player {
} }
pub fn autobuy(&mut self) -> &mut Player { pub fn autobuy(&mut self) -> &mut Player {
let mut rng = thread_rng(); // let mut rng = thread_rng();
// skill buying phase // skill buying phase
while self.constructs.iter().any(|c| c.skills.len() < 3) { while self.constructs.iter().any(|c| c.skills.len() < 3) {

View File

@ -42,13 +42,6 @@ impl Cast {
} }
pub fn resolve(self, game: &mut Game) { pub fn resolve(self, game: &mut Game) {
if !self.skill.aoe() {
if self.skill.cast_animation() {
game.action(self, Action::Cast);
}
game.action(self, Action::Hit);
}
match self.skill { match self.skill {
Skill::Attack => attack(self, game, Attack::Base), Skill::Attack => attack(self, game, Attack::Base),
Skill::Block => block(self, game, Block::Base), Skill::Block => block(self, game, Block::Base),
@ -193,9 +186,6 @@ impl Cast {
Skill::TriageTick => triage_tick(self, game), Skill::TriageTick => triage_tick(self, game),
}; };
// actions.append(&mut rest);
// return actions;
} }
} }
@ -647,6 +637,9 @@ impl Skill {
Skill::Heal | Skill::Heal |
Skill::HealPlus | Skill::HealPlus |
Skill::HealPlusPlus | Skill::HealPlusPlus |
Skill::Hybrid |
Skill::HybridPlus |
Skill::HybridPlusPlus |
Skill::Absorb | Skill::Absorb |
Skill::AbsorbPlus | Skill::AbsorbPlus |
Skill::AbsorbPlusPlus | Skill::AbsorbPlusPlus |
@ -722,7 +715,7 @@ impl Skill {
.collect::<Vec<Colour>>(); .collect::<Vec<Colour>>();
} }
fn base(&self) -> Skill { fn _base(&self) -> Skill {
let bases = [Item::Attack, Item::Stun, Item::Buff, Item::Debuff, Item::Block]; let bases = [Item::Attack, Item::Stun, Item::Buff, Item::Debuff, Item::Block];
match self.components() match self.components()
.iter() .iter()
@ -996,6 +989,10 @@ fn siphon(cast: Cast, game: &mut Game, values: Siphon) {
} }
); );
// should only reapply the dot if they have already been hit by the dmg
// from either this or the tick
if game.affected(cast.target, Effect::Siphoned) { return; }
game.action(cast, game.action(cast,
Action::Damage { Action::Damage {
construct: cast.target, construct: cast.target,
@ -1587,6 +1584,8 @@ fn decay(cast: Cast, game: &mut Game, values: Decay) {
} }
); );
if game.affected(cast.target, Effect::Decayed) { return; }
game.action(cast, game.action(cast,
Action::Effect { Action::Effect {
construct: cast.target, construct: cast.target,
@ -1685,19 +1684,27 @@ impl Electrocute {
fn electrocute(cast: Cast, game: &mut Game, values: Electrocute) { fn electrocute(cast: Cast, game: &mut Game, values: Electrocute) {
let amount = game.value(Value::Stat { construct: cast.source, stat: Stat::BluePower }).pct(values.damage_multiplier()); let amount = game.value(Value::Stat { construct: cast.source, stat: Stat::BluePower }).pct(values.damage_multiplier());
game.action(cast,
Action::Remove {
construct: cast.source,
effect: Effect::Electrocute,
}
);
game.action(cast, game.action(cast,
Action::Effect { Action::Effect {
construct: cast.target, construct: cast.target,
effect: ConstructEffect { effect: Effect::Electric, duration: values.duration(), meta: effect: ConstructEffect { effect: Effect::Electrocute, duration: values.duration(), meta:
Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::ElectrocuteTick, speed: cast.speed, amount }) }, Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::ElectrocuteTick, speed: cast.speed, amount }) },
}, },
); );
game.action(cast,
Action::Damage {
construct: cast.target,
colour: Colour::Blue,
amount,
},
);
game.action(cast,
Action::Remove {
construct: cast.source,
effect: Effect::Electric,
}
);
} }
fn electrocute_tick(cast: Cast, game: &mut Game) { fn electrocute_tick(cast: Cast, game: &mut Game) {
@ -1847,11 +1854,16 @@ fn link(cast: Cast, game: &mut Game, values: Link) {
effect: ConstructEffect { effect: Effect::Stun, duration: values.duration(), meta: None }, effect: ConstructEffect { effect: Effect::Stun, duration: values.duration(), meta: None },
} }
); );
let bp = game.value(Value::Stat { construct: cast.source, stat: Stat::BluePower }).pct(values.blue_dmg_base());
let links = game.value(Value::Effects { construct: cast.target });
let amount = bp.pct(100 + 100usize.saturating_mul(links));
game.action(cast, game.action(cast,
Action::Damage { Action::Damage {
construct: cast.target, construct: cast.target,
colour: Colour::Blue, colour: Colour::Blue,
amount: game.value(Value::Effects { construct: cast.target }).pct(values.blue_dmg_base()), amount,
}, },
); );
} }
@ -2089,7 +2101,7 @@ fn sleep(cast: Cast, game: &mut Game, values: Sleep) {
Action::Heal { Action::Heal {
construct: cast.target, construct: cast.target,
amount: game.value(Value::Stat { construct: cast.source, stat: Stat::GreenPower }).pct(values.green_heal_multi()), amount: game.value(Value::Stat { construct: cast.source, stat: Stat::GreenPower }).pct(values.green_heal_multi()),
colour: Colour::Blue, colour: Colour::Green,
}, },
); );
} }
@ -2188,6 +2200,9 @@ fn triage(cast: Cast, game: &mut Game, values: Triage) {
Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::TriageTick, speed: cast.speed, amount }) }, Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::TriageTick, speed: cast.speed, amount }) },
} }
); );
if game.affected(cast.target, Effect::Triaged) { return; }
game.action(cast, game.action(cast,
Action::Heal { Action::Heal {
construct: cast.target, construct: cast.target,
@ -2208,7 +2223,7 @@ fn triage_tick(cast: Cast, game: &mut Game) {
Action::Heal { Action::Heal {
construct: cast.target, construct: cast.target,
colour: Colour::Green, colour: Colour::Green,
amount: game.value(Value::TickDamage { construct: cast.target, effect: Effect::Electrocute }), amount: game.value(Value::TickDamage { construct: cast.target, effect: Effect::Triage }),
} }
); );
game.action(cast, game.action(cast,

View File

@ -1,5 +1,3 @@
use uuid::Uuid;
use std::iter; use std::iter;
use std::collections::HashMap; use std::collections::HashMap;
@ -11,9 +9,6 @@ use rand::distributions::{WeightedIndex};
use failure::Error; use failure::Error;
use failure::err_msg; use failure::err_msg;
use instance::{Instance};
use construct::{Colours};
use item::*; use item::*;
pub type VboxIndices = Option<HashMap<ItemType, Vec<String>>>; pub type VboxIndices = Option<HashMap<ItemType, Vec<String>>>;
@ -40,9 +35,9 @@ const STARTING_ATTACK_COUNT: usize = 3;
impl Vbox { impl Vbox {
pub fn new() -> Vbox { pub fn new() -> Vbox {
let mut colours: HashMap<String, Item> = HashMap::new(); let colours: HashMap<String, Item> = HashMap::new();
let mut skills: HashMap<String, Item> = HashMap::new(); let skills: HashMap<String, Item> = HashMap::new();
let mut specs: HashMap<String, Item> = HashMap::new(); let specs: HashMap<String, Item> = HashMap::new();
let store = [ let store = [
(ItemType::Colours, colours), (ItemType::Colours, colours),
@ -220,6 +215,7 @@ impl Vbox {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use construct::{Colours};
#[test] #[test]
fn combine_test() { fn combine_test() {

View File

@ -1,6 +1,6 @@
{ {
"name": "mnml-ops", "name": "mnml-ops",
"version": "1.10.1", "version": "1.11.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mnml" name = "mnml"
version = "1.10.1" version = "1.11.0"
authors = ["ntr <ntr@smokestack.io>"] authors = ["ntr <ntr@smokestack.io>"]
[dependencies] [dependencies]
@ -25,6 +25,7 @@ dotenv = "0.9.0"
log = "0.4" log = "0.4"
fern = { version = "0.5", features = ["colored", "syslog-4"] } fern = { version = "0.5", features = ["colored", "syslog-4"] }
syslog = "4" syslog = "4"
log-panics = "2"
iron = "0.6" iron = "0.6"
bodyparser = "0.8" bodyparser = "0.8"

View File

@ -42,7 +42,17 @@ impl Account {
false => None, false => None,
}; };
Ok(Player::new(self.id, Some(self.img), &self.name, constructs)) Ok(Player::new(self.id, img, &self.name, constructs))
}
pub fn anonymous() -> Account {
Account {
id: Uuid::new_v4(),
img: Uuid::new_v4(),
name: "you".to_string(),
balance: 0,
subscribed: false,
}
} }
} }
@ -93,7 +103,7 @@ pub fn chat_wheel(_db: &Db, _id: Uuid) -> Result<Vec<String>, Error> {
]) ])
} }
pub fn select_name(db: &Db, name: &String) -> Result<Account, Error> { pub fn _select_name(db: &Db, name: &String) -> Result<Account, Error> {
let query = " let query = "
SELECT id, name, balance, subscribed, img SELECT id, name, balance, subscribed, img
FROM accounts FROM accounts

View File

@ -6,7 +6,7 @@ use std::time;
use uuid::Uuid; use uuid::Uuid;
use failure::Error; use failure::Error;
use failure::{err_msg, format_err}; use failure::{format_err};
use crossbeam_channel::{Sender, Receiver}; use crossbeam_channel::{Sender, Receiver};
@ -15,10 +15,9 @@ use names;
use rpc::RpcMessage; use rpc::RpcMessage;
use warden::{GameEvent}; use warden::{GameEvent};
use mail::Mail;
pub type EventsTx = Sender<Event>; pub type EventsTx = Sender<Event>;
type Id = usize; type Id = Uuid;
// this is pretty heavyweight // this is pretty heavyweight
// but it makes the ergonomics easy // but it makes the ergonomics easy
@ -35,7 +34,6 @@ pub struct Events {
pub tx: Sender<Event>, pub tx: Sender<Event>,
rx: Receiver<Event>, rx: Receiver<Event>,
mail: Sender<Mail>,
warden: Sender<GameEvent>, warden: Sender<GameEvent>,
clients: HashMap<Id, WsClient>, clients: HashMap<Id, WsClient>,
} }
@ -43,7 +41,7 @@ pub struct Events {
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub enum Event { pub enum Event {
// ws lifecycle // ws lifecycle
Connect(Id, Option<Account>, Sender<RpcMessage>), Connect(Id, Account, Sender<RpcMessage>),
Disconnect(Id), Disconnect(Id),
Subscribe(Id, Uuid), Subscribe(Id, Uuid),
Unsubscribe(Id, Uuid), Unsubscribe(Id, Uuid),
@ -64,7 +62,6 @@ pub enum Event {
struct WsClient { struct WsClient {
id: Id, id: Id,
account: Option<Uuid>,
tx: Sender<RpcMessage>, tx: Sender<RpcMessage>,
subs: HashSet<Uuid>, subs: HashSet<Uuid>,
chat: Option<(Uuid, String)>, chat: Option<(Uuid, String)>,
@ -73,12 +70,11 @@ struct WsClient {
} }
impl Events { impl Events {
pub fn new(tx: Sender<Event>, rx: Receiver<Event>, warden: Sender<GameEvent>, mail: Sender<Mail>) -> Events { pub fn new(tx: Sender<Event>, rx: Receiver<Event>, warden: Sender<GameEvent>) -> Events {
Events { Events {
tx, tx,
rx, rx,
warden, warden,
mail,
clients: HashMap::new(), clients: HashMap::new(),
} }
} }
@ -102,30 +98,13 @@ impl Events {
} }
} }
fn get_client(&mut self, id: Id) -> Result<&mut WsClient, Error> {
match self.clients.get_mut(&id) {
Some(c) => Ok(c),
None => Err(format_err!("connection not found id={:?}", id)),
}
}
fn remove_client(&mut self, id: Id) {
self.clients.remove(&id);
}
fn event(&mut self, msg: Event) -> Result<(), Error> { fn event(&mut self, msg: Event) -> Result<(), Error> {
match msg { match msg {
Event::Connect(id, account, tx) => { Event::Connect(id, account, tx) => {
info!("connect id={:?} account={:?}", id, account); info!("connect id={:?} account={:?}", id, account);
let account_id = match account {
Some(a) => Some(a.id),
None => None,
};
let client = WsClient { id, let client = WsClient { id,
tx, tx,
account: account_id,
subs: HashSet::new(), subs: HashSet::new(),
pvp: false, pvp: false,
invite: None, invite: None,
@ -164,7 +143,7 @@ impl Events {
trace!("unsubscribe id={:?} object={:?}", id, obj); trace!("unsubscribe id={:?} object={:?}", id, obj);
match self.clients.get_mut(&id) { match self.clients.get_mut(&id) {
Some(mut client) => { Some(client) => {
client.subs.remove(&obj); client.subs.remove(&obj);
trace!("unsubscribe subscriptions removed={:?}", client.subs.len()); trace!("unsubscribe subscriptions removed={:?}", client.subs.len());
Ok(()) Ok(())
@ -183,13 +162,10 @@ impl Events {
if client.subs.contains(&id) { if client.subs.contains(&id) {
subs += 1; subs += 1;
let redacted = match client.account { let redacted = match msg {
Some(a) => match msg { RpcMessage::InstanceState(ref i) => RpcMessage::InstanceState(i.clone().redact(client.id)),
RpcMessage::InstanceState(ref i) => RpcMessage::InstanceState(i.clone().redact(a)), RpcMessage::GameState(ref i) => RpcMessage::GameState(i.clone().redact(client.id)),
RpcMessage::GameState(ref i) => RpcMessage::GameState(i.clone().redact(a)), _ => msg.clone(),
_ => msg.clone(),
}
None => msg.clone(),
}; };
match client.tx.send(redacted) { match client.tx.send(redacted) {
@ -204,7 +180,9 @@ impl Events {
if !dead.is_empty() { if !dead.is_empty() {
trace!("dead connections={:?}", dead.len()); trace!("dead connections={:?}", dead.len());
dead.iter().for_each(|id| self.remove_client(*id)); for id in dead.iter() {
self.clients.remove(id);
}
} }
trace!("push subscribers={:?}", subs); trace!("push subscribers={:?}", subs);
@ -218,18 +196,14 @@ impl Events {
let c = self.clients.get(&id) let c = self.clients.get(&id)
.ok_or(format_err!("connection not found id={:?}", id))?; .ok_or(format_err!("connection not found id={:?}", id))?;
if let None = c.account { info!("pvp queue request id={:?} account={:?}", c.id, c.id);
return Err(err_msg("cannot join pvp queue anonymously"));
}
info!("pvp queue request id={:?} account={:?}", c.id, c.account);
} }
// create the req for the already queued opponent // create the req for the already queued opponent
if let Some(opp_req) = match self.clients.iter_mut().find(|(c_id, c)| c.pvp && **c_id != id) { if let Some(opp_req) = match self.clients.iter_mut().find(|(c_id, c)| c.pvp && **c_id != id) {
Some((q_id, q)) => { Some((q_id, q)) => {
q.pvp = false; q.pvp = false;
Some(PvpRequest { id: *q_id, account: q.account.unwrap(), tx: q.tx.clone() }) Some(PvpRequest { id: *q_id, account: q.id, tx: q.tx.clone() })
}, },
None => None, None => None,
} { } {
@ -237,7 +211,7 @@ impl Events {
let c = self.clients.get_mut(&id) let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?; .ok_or(format_err!("connection not found id={:?}", id))?;
let player_req = PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }; let player_req = PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() };
self.warden.send(GameEvent::Match((opp_req, player_req)))?; self.warden.send(GameEvent::Match((opp_req, player_req)))?;
return Ok(()) return Ok(())
@ -247,7 +221,7 @@ impl Events {
let requester = self.clients.get_mut(&id).unwrap(); let requester = self.clients.get_mut(&id).unwrap();
requester.pvp = true; requester.pvp = true;
requester.tx.send(RpcMessage::QueueJoined(()))?; requester.tx.send(RpcMessage::QueueJoined(()))?;
info!("joined game queue id={:?} account={:?}", requester.id, requester.account); info!("joined game queue id={:?} account={:?}", requester.id, requester.id);
return Ok(()); return Ok(());
}, },
@ -256,12 +230,8 @@ impl Events {
let c = self.clients.get_mut(&id) let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?; .ok_or(format_err!("connection not found id={:?}", id))?;
if let None = c.account {
return Err(err_msg("cannot join pvp queue anonymously"));
}
let code = names::name().split_whitespace().collect::<Vec<&str>>().join("-"); let code = names::name().split_whitespace().collect::<Vec<&str>>().join("-");
info!("pvp invite request id={:?} account={:?} code={:?}", c.id, c.account, code); info!("pvp invite request id={:?} account={:?} code={:?}", c.id, c.id, code);
c.invite = Some(code.clone()); c.invite = Some(code.clone());
c.tx.send(RpcMessage::Invite(code))?; c.tx.send(RpcMessage::Invite(code))?;
return Ok(()); return Ok(());
@ -272,11 +242,7 @@ impl Events {
let c = self.clients.get(&id) let c = self.clients.get(&id)
.ok_or(format_err!("connection not found id={:?}", id))?; .ok_or(format_err!("connection not found id={:?}", id))?;
if let None = c.account { info!("pvp join request id={:?} account={:?} code={:?}", c.id, c.id, code);
return Err(err_msg("cannot join pvp queue anonymously"));
}
info!("pvp join request id={:?} account={:?} code={:?}", c.id, c.account, code);
let inv = self.clients.iter() let inv = self.clients.iter()
.filter(|(_id, c)| c.invite.is_some()) .filter(|(_id, c)| c.invite.is_some())
@ -284,10 +250,10 @@ impl Events {
Some(ref c) => *c == code, Some(ref c) => *c == code,
None => false, None => false,
}) })
.map(|(_id, c)| PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }) .map(|(_id, c)| PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() })
.ok_or(format_err!("invite expired code={:?}", code))?; .ok_or(format_err!("invite expired code={:?}", code))?;
let join = PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }; let join = PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() };
self.warden.send(GameEvent::Match((join, inv)))?; self.warden.send(GameEvent::Match((join, inv)))?;
return Ok(()); return Ok(());
@ -310,7 +276,7 @@ impl Events {
c.pvp = false; c.pvp = false;
c.tx.send(RpcMessage::QueueLeft(()))?; c.tx.send(RpcMessage::QueueLeft(()))?;
info!("left game queue id={:?} account={:?}", c.id, c.account); info!("left game queue id={:?} account={:?}", c.id, c.id);
return Ok(()); return Ok(());
}, },
@ -337,12 +303,11 @@ impl Events {
// now collect all listeners of this instance // now collect all listeners of this instance
let chat_state: HashMap<Uuid, String> = self.clients.iter() let chat_state: HashMap<Uuid, String> = self.clients.iter()
.filter(|(_id, c)| c.account.is_some())
.filter(|(_id, c)| match c.chat { .filter(|(_id, c)| match c.chat {
Some(ref chat) => chat.0 == instance, Some(ref chat) => chat.0 == instance,
None => false, None => false,
}) })
.map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1)) .map(|(_id, c)| (c.id, c.chat.clone().unwrap().1))
.collect(); .collect();
return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state))); return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state)));
@ -357,12 +322,11 @@ impl Events {
} }
let chat_state: HashMap<Uuid, String> = self.clients.iter() let chat_state: HashMap<Uuid, String> = self.clients.iter()
.filter(|(_id, c)| c.account.is_some())
.filter(|(_id, c)| match c.chat { .filter(|(_id, c)| match c.chat {
Some(ref chat) => chat.0 == instance, Some(ref chat) => chat.0 == instance,
None => false, None => false,
}) })
.map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1)) .map(|(_id, c)| (c.id, c.chat.clone().unwrap().1))
.collect(); .collect();
return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state))); return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state)));

View File

@ -111,7 +111,7 @@ pub fn json_response(status: status::Status, response: Json) -> Response {
return Response::with((content_type, status, json)); return Response::with((content_type, status, json));
} }
pub fn json_object(status: status::Status, object: String) -> Response { pub fn _json_object(status: status::Status, object: String) -> Response {
let content_type = "application/json".parse::<Mime>().unwrap(); let content_type = "application/json".parse::<Mime>().unwrap();
return Response::with((content_type, status, object)); return Response::with((content_type, status, object));
} }

View File

@ -72,7 +72,7 @@ enum ConstructShapes {
Line, Line,
V, V,
Tri, Tri,
Plus, // Plus,
Blank, Blank,
} }
@ -203,9 +203,7 @@ pub fn shapes_write(id: Uuid) -> Result<Uuid, Error> {
write!(&mut svg, "<path stroke=\"{fill}\" stroke-width=\"{width}\" d=\"M{x0} {y0}L 0 0 M{x1} {y1}L 0 0 M{x2} {y2}L 0 0 \" transform=\"translate({x_translate}, {y_translate}) rotate({rotation})\" />", write!(&mut svg, "<path stroke=\"{fill}\" stroke-width=\"{width}\" d=\"M{x0} {y0}L 0 0 M{x1} {y1}L 0 0 M{x2} {y2}L 0 0 \" transform=\"translate({x_translate}, {y_translate}) rotate({rotation})\" />",
fill = colour, width = width, x0 = x0, y0 = y0, x1 = x1, y1 = y1, x2 = x2, y2 = y2, rotation = rotation, x_translate = -x_translate, y_translate = -y_translate)?; fill = colour, width = width, x0 = x0, y0 = y0, x1 = x1, y1 = y1, x2 = x2, y2 = y2, rotation = rotation, x_translate = -x_translate, y_translate = -y_translate)?;
}, },
ConstructShapes::Plus => { // ConstructShapes::Plus => { },
},
ConstructShapes::Blank => (), ConstructShapes::Blank => (),
} }
} }

View File

@ -15,6 +15,7 @@ extern crate serde_cbor;
#[macro_use] extern crate failure; #[macro_use] extern crate failure;
extern crate fern; extern crate fern;
extern crate log_panics;
#[macro_use] extern crate log; #[macro_use] extern crate log;
extern crate stripe; extern crate stripe;
@ -47,6 +48,8 @@ mod pg;
mod events; mod events;
pub mod rpc; pub mod rpc;
mod warden; mod warden;
mod user_authenticated;
mod user_anonymous;
use std::thread::{spawn}; use std::thread::{spawn};
use std::path::{Path}; use std::path::{Path};
@ -55,6 +58,8 @@ use crossbeam_channel::{unbounded};
#[cfg(unix)] #[cfg(unix)]
pub fn setup_logger() -> Result<(), fern::InitError> { pub fn setup_logger() -> Result<(), fern::InitError> {
log_panics::init();
let formatter = syslog::Formatter3164 { let formatter = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER, facility: syslog::Facility::LOG_USER,
hostname: None, hostname: None,
@ -117,11 +122,11 @@ pub fn start() {
let warden_tick_tx = warden_tx.clone(); let warden_tick_tx = warden_tx.clone();
let (mail_tx, mail_rx) = unbounded(); let (mail_tx, mail_rx) = unbounded();
let http_mail_tx = mail_tx.clone(); let _http_mail_tx = mail_tx.clone();
// create a clone of the tx so ws handler can tell events // create a clone of the tx so ws handler can tell events
// about connection status // about connection status
let events = events::Events::new(events_tx, events_rx, events_warden_tx, mail_tx); let events = events::Events::new(events_tx, events_rx, events_warden_tx);
let warden = warden::Warden::new(warden_tx, warden_rx, events.tx.clone(), pool.clone()); let warden = warden::Warden::new(warden_tx, warden_rx, events.tx.clone(), pool.clone());
let pg_pool = pool.clone(); let pg_pool = pool.clone();

View File

@ -261,7 +261,7 @@ pub fn set(tx: &mut Transaction, account: Uuid, email: &String) -> Result<(Uuid,
return Ok((id, confirm_token)); return Ok((id, confirm_token));
} }
pub fn listen(rx: Receiver<Mail>) -> SmtpTransport { pub fn listen(_rx: Receiver<Mail>) -> SmtpTransport {
let sender = env::var("MAIL_ADDRESS") let sender = env::var("MAIL_ADDRESS")
.expect("MAIL_ADDRESS must be set"); .expect("MAIL_ADDRESS must be set");
@ -271,7 +271,7 @@ pub fn listen(rx: Receiver<Mail>) -> SmtpTransport {
let domain = env::var("MAIL_DOMAIN") let domain = env::var("MAIL_DOMAIN")
.expect("MAIL_DOMAIN must be set"); .expect("MAIL_DOMAIN must be set");
let mut mailer = SmtpClient::new_simple("smtp.gmail.com").unwrap() let mailer = SmtpClient::new_simple("smtp.gmail.com").unwrap()
.hello_name(ClientId::Domain(domain)) .hello_name(ClientId::Domain(domain))
.credentials(Credentials::new(sender, password)) .credentials(Credentials::new(sender, password))
.smtp_utf8(true) .smtp_utf8(true)
@ -281,19 +281,6 @@ pub fn listen(rx: Receiver<Mail>) -> SmtpTransport {
info!("mail connected"); info!("mail connected");
// loop {
// match rx.recv() {
// Ok(m) => match send_mail(&mut mailer, m) {
// Ok(r) => info!("{:?}", r),
// Err(e) => warn!("{:?}", e),
// },
// Err(e) => {
// error!("{:?}", e);
// panic!("mail thread cannot continue");
// },
// };
// }
// Explicitly close the SMTP transaction as we enabled connection reuse // Explicitly close the SMTP transaction as we enabled connection reuse
// mailer.close(); // mailer.close();
return mailer; return mailer;

View File

@ -86,7 +86,7 @@ impl Mtx {
} }
} }
pub fn delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> { pub fn _delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
let query = " let query = "
DELETE DELETE
FROM mtx FROM mtx

View File

@ -22,7 +22,7 @@ use mnml_core::mob::instance_mobs;
use mnml_core::vbox::{ItemType, VboxIndices}; use mnml_core::vbox::{ItemType, VboxIndices};
use mnml_core::item::Item; use mnml_core::item::Item;
use mnml_core::skill::Skill; use mnml_core::skill::Skill;
use mnml_core::mob::bot_player; use mnml_core::mob::{bot_player, anon_player};
use mnml_core::instance::{Instance, TimeControl}; use mnml_core::instance::{Instance, TimeControl};
use events::{Event}; use events::{Event};
@ -143,7 +143,7 @@ pub fn listen(pool: PgPool, events: Sender<Event>) -> Result<(), Error> {
} }
pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<(), Error> { pub fn _construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<(), Error> {
let query = " let query = "
DELETE DELETE
FROM constructs FROM constructs
@ -163,7 +163,7 @@ pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Res
return Ok(()); return Ok(());
} }
pub fn construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> { pub fn _construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> {
let query = " let query = "
SELECT data SELECT data
FROM constructs FROM constructs
@ -290,7 +290,7 @@ pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {
return Ok(game); return Ok(game);
} }
pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> { pub fn _game_select(db: &Db, id: Uuid) -> Result<Game, Error> {
let query = " let query = "
SELECT * SELECT *
FROM games FROM games
@ -312,7 +312,7 @@ pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> {
return Ok(game); return Ok(game);
} }
pub fn list(db: &Db, number: u32) -> Result<Vec<Game>, Error> { pub fn _game_list(db: &Db, number: u32) -> Result<Vec<Game>, Error> {
let query = " let query = "
SELECT data SELECT data
FROM games FROM games
@ -694,6 +694,35 @@ pub fn instance_practice(tx: &mut Transaction, account: &Account) -> Result<Inst
Ok(instance) Ok(instance)
} }
pub fn instance_demo(account: &Account) -> Result<Instance, Error> {
let bot = bot_player();
let bot_id = bot.id;
// generate imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
}
let mut instance = Instance::new()
.set_time_control(TimeControl::Practice)
.set_name(bot.name.clone())?;
let player = anon_player(account.id);
for c in player.constructs.iter() {
img::shapes_write(c.img)?;
}
instance.add_player(player.clone())?;
instance.add_player(bot)?;
instance.player_ready(bot_id)?;
// skip faceoff
instance.player_ready(player.id)?;
Ok(instance)
}
pub fn pvp(tx: &mut Transaction, a: &Account, b: &Account) -> Result<Instance, Error> { pub fn pvp(tx: &mut Transaction, a: &Account, b: &Account) -> Result<Instance, Error> {
let mut instance = Instance::new() let mut instance = Instance::new()
// TODO generate nice game names // TODO generate nice game names
@ -786,7 +815,7 @@ pub fn instance_game_finished(tx: &mut Transaction, game: &Game, instance_id: Uu
Ok(()) Ok(())
} }
pub fn bot_instance() -> Instance { pub fn _bot_instance() -> Instance {
let mut instance = Instance::new(); let mut instance = Instance::new();
let bot_player = bot_player(); let bot_player = bot_player();
@ -804,26 +833,6 @@ pub fn bot_instance() -> Instance {
return instance; return instance;
} }
pub fn demo() -> Result<Vec<Player>, Error> {
let bot = bot_player();
// generate bot imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
};
let bot2 = bot_player();
// generate bot imgs for the client to see
for c in bot2.constructs.iter() {
img::shapes_write(c.img)?;
};
Ok(vec![bot, bot2])
}
pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<Instance, Error> { pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)? let instance = instance_get(tx, instance_id)?
.vbox_refill(account.id)?; .vbox_refill(account.id)?;

View File

@ -1,6 +1,6 @@
use mnml_core::item::ItemInfoCtr; use mnml_core::item::ItemInfoCtr;
use mnml_core::instance::ChatState; use mnml_core::instance::ChatState;
use mnml_core::item::item_info;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant}; use std::time::{Instant};
use std::thread::{spawn}; use std::thread::{spawn};
@ -8,12 +8,8 @@ use std::thread::{spawn};
use std::str; use std::str;
use uuid::Uuid; use uuid::Uuid;
use rand::prelude::*;
use failure::Error; use failure::Error;
use failure::err_msg; use serde_cbor::{to_vec};
use serde_cbor::{from_slice, to_vec};
use cookie::Cookie; use cookie::Cookie;
use stripe::{Client as StripeClient, Subscription}; use stripe::{Client as StripeClient, Subscription};
@ -22,44 +18,24 @@ use crossbeam_channel::{unbounded, Sender as CbSender};
use ws::{Builder, CloseCode, Message, Handler, Request, Response, Settings, Sender as WsSender}; use ws::{Builder, CloseCode, Message, Handler, Request, Response, Settings, Sender as WsSender};
use ws::deflate::DeflateHandler; use ws::deflate::DeflateHandler;
use pg::{
demo,
game_concede,
game_offer_draw,
game_ready,
game_skill,
game_skill_clear,
game_state,
instance_abandon,
instance_practice,
instance_ready,
instance_state,
vbox_apply,
vbox_buy,
vbox_combine,
vbox_refill,
vbox_refund,
vbox_unequip,
};
use account::{Account}; use account::{Account};
use account; use account;
use events::{Event}; use events::{Event};
use user_anonymous::{Anonymous};
use user_authenticated::{Authorised};
use mnml_core::construct::{Construct}; use mnml_core::construct::{Construct};
use mnml_core::game::{Game}; use mnml_core::game::{Game};
use mnml_core::player::Player;
use mnml_core::vbox::{ItemType}; use mnml_core::vbox::{ItemType};
use mnml_core::item::Item; use mnml_core::item::Item;
use mnml_core::skill::Skill; use mnml_core::skill::Skill;
use mnml_core::mob::{anim_test_game};
use mnml_core::instance::{Instance}; use mnml_core::instance::{Instance};
use mtx; use mtx;
use mail;
use payments;
use mail::Email; use mail::Email;
use pg::{Db}; use pg::{Db};
use pg::{PgPool}; use pg::{PgPool};
@ -68,13 +44,12 @@ use http::{AUTH_CLEAR, TOKEN_HEADER};
#[derive(Debug,Clone,Serialize)] #[derive(Debug,Clone,Serialize)]
pub enum RpcMessage { pub enum RpcMessage {
AccountState(Account), AccountState(Account),
AccountAuthenticated(Account),
AccountConstructs(Vec<Construct>), AccountConstructs(Vec<Construct>),
AccountTeam(Vec<Construct>), AccountTeam(Vec<Construct>),
AccountInstances(Vec<Instance>), AccountInstances(Vec<Instance>),
AccountShop(mtx::Shop), AccountShop(mtx::Shop),
Demo(Vec<Player>),
ConstructSpawn(Construct), ConstructSpawn(Construct),
GameState(Game), GameState(Game),
ItemInfo(ItemInfoCtr), ItemInfo(ItemInfoCtr),
@ -87,6 +62,7 @@ pub enum RpcMessage {
SubscriptionState(Option<Subscription>), SubscriptionState(Option<Subscription>),
Pong(()), Pong(()),
StartTutorial(()),
QueueRequested(()), QueueRequested(()),
QueueJoined(()), QueueJoined(()),
@ -149,194 +125,32 @@ pub enum RpcRequest {
VboxRefund { instance_id: Uuid, index: String }, VboxRefund { instance_id: Uuid, index: String },
} }
pub trait User {
fn receive(&mut self, data: Vec<u8>, db: &Db, begin: Instant, events: &CbSender<Event>, stripe: &StripeClient) -> Result<RpcMessage, Error>;
fn connected(&mut self, db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error>;
fn send(&mut self, msg: RpcMessage, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error>;
}
struct Connection { struct Connection {
pub id: usize, pub id: Uuid,
pub ws: CbSender<RpcMessage>, pub ws: CbSender<RpcMessage>,
pool: PgPool, pool: PgPool,
stripe: StripeClient, stripe: StripeClient,
account: Option<Account>, // account: Option<Account>,
user: Box<dyn User>,
events: CbSender<Event>, events: CbSender<Event>,
} }
impl Connection { impl Connection {
fn receive(&self, data: Vec<u8>, db: &Db, begin: Instant) -> Result<RpcMessage, Error> {
// cast the msg to this type to receive method name
match from_slice::<RpcRequest>(&data) {
Ok(v) => {
// non authenticated
// non transactional reqs
match v {
RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())),
RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())),
RpcRequest::DevResolve { skill } =>
return Ok(RpcMessage::GameState(anim_test_game(skill))),
_ => (),
};
// check for authorization now
let account = match self.account {
Some(ref account) => account,
None => return Err(err_msg("auth required")),
};
let request = v.clone();
let response = match v {
// evented but authorization required
RpcRequest::InstanceQueue {} => {
self.events.send(Event::Queue(self.id))?;
Ok(RpcMessage::QueueRequested(()))
},
RpcRequest::InstanceInvite {} => {
self.events.send(Event::Invite(self.id))?;
Ok(RpcMessage::InviteRequested(()))
},
RpcRequest::InstanceJoin { code } => {
self.events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(()))
},
RpcRequest::InstanceLeave {} => {
self.events.send(Event::Leave(self.id))?;
Ok(RpcMessage::Processing(()))
},
RpcRequest::InstanceChat { instance_id, index } => {
if !account.subscribed {
return Err(err_msg("subscribe to unlock chat"))
}
let wheel = account::chat_wheel(&db, account.id)?;
if let Some(c) = wheel.get(index) {
self.events.send(Event::Chat(self.id, instance_id, c.to_string()))?;
} else {
return Err(err_msg("invalid chat index"));
}
Ok(RpcMessage::Processing(()))
},
_ => {
// all good, let's make a tx and process
let mut tx = db.transaction()?;
let res = match v {
RpcRequest::AccountState {} =>
Ok(RpcMessage::AccountState(account.clone())),
RpcRequest::AccountConstructs {} =>
Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &account)?)),
RpcRequest::AccountInstances {} =>
Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, account)?)),
RpcRequest::AccountSetTeam { ids } =>
Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &account, ids)?)),
RpcRequest::EmailState {} =>
Ok(RpcMessage::EmailState(mail::select_account(&db, account.id)?)),
RpcRequest::SubscriptionState {} =>
Ok(RpcMessage::SubscriptionState(payments::account_subscription(&db, &self.stripe, &account)?)),
// RpcRequest::AccountShop {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),
// RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account),
RpcRequest::GameState { id } =>
Ok(RpcMessage::GameState(game_state(&mut tx, account, id)?)),
RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } =>
Ok(RpcMessage::GameState(game_skill(&mut tx, account, game_id, construct_id, target_construct_id, skill)?)),
RpcRequest::GameSkillClear { game_id } =>
Ok(RpcMessage::GameState(game_skill_clear(&mut tx, account, game_id)?)),
RpcRequest::GameReady { id } =>
Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)),
RpcRequest::GameConcede { game_id } =>
Ok(RpcMessage::GameState(game_concede(&mut tx, account, game_id)?)),
RpcRequest::GameOfferDraw { game_id } =>
Ok(RpcMessage::GameState(game_offer_draw(&mut tx, account, game_id)?)),
RpcRequest::InstancePractice {} =>
Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)),
// these two can return GameState or InstanceState
RpcRequest::InstanceReady { instance_id } =>
Ok(instance_ready(&mut tx, account, instance_id)?),
RpcRequest::InstanceState { instance_id } =>
Ok(instance_state(&mut tx, instance_id)?),
RpcRequest::InstanceAbandon { instance_id } =>
Ok(instance_abandon(&mut tx, account, instance_id)?),
RpcRequest::VboxBuy { instance_id, group, index, construct_id } =>
Ok(RpcMessage::InstanceState(vbox_buy(&mut tx, account, instance_id, group, index, construct_id)?)),
RpcRequest::VboxApply { instance_id, construct_id, index } =>
Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, account, instance_id, construct_id, index)?)),
RpcRequest::VboxCombine { instance_id, inv_indices, vbox_indices } =>
Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, account, instance_id, inv_indices, vbox_indices)?)),
RpcRequest::VboxRefill { instance_id } =>
Ok(RpcMessage::InstanceState(vbox_refill(&mut tx, account, instance_id)?)),
RpcRequest::VboxRefund { instance_id, index } =>
Ok(RpcMessage::InstanceState(vbox_refund(&mut tx, account, instance_id, index)?)),
RpcRequest::VboxUnequip { instance_id, construct_id, target } =>
Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target, None)?)),
RpcRequest::VboxUnequipApply { instance_id, construct_id, target, target_construct_id } =>
Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target, Some(target_construct_id))?)),
RpcRequest::MtxConstructSpawn {} =>
Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, account)?)),
RpcRequest::MtxConstructApply { mtx, construct_id, name } =>
Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, account, mtx, construct_id, name)?)),
RpcRequest::MtxAccountApply { mtx } =>
Ok(RpcMessage::AccountState(mtx::account_apply(&mut tx, account, mtx)?)),
RpcRequest::MtxBuy { mtx } =>
Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, account, mtx)?)),
RpcRequest::SubscriptionEnding { ending } =>
Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, &self.stripe, account, ending)?)),
_ => Err(format_err!("unknown request request={:?}", request)),
};
tx.commit()?;
res
}
};
info!("request={:?} account={:?} duration={:?}", request, account.name, begin.elapsed());
return response;
},
Err(e) => {
warn!("{:?}", e);
Err(err_msg("invalid message"))
},
}
}
// this is where last minute processing happens // this is where last minute processing happens
// use it to modify outgoing messages, update subs, serialize in some way... // use it to modify outgoing messages, update subs, serialize in some way...
fn send(&self, msg: RpcMessage) -> Result<(), Error> { fn send(&self, msg: RpcMessage) -> Result<(), Error> {
let msg = match self.account { let msg = match msg {
Some(ref a) => match msg { RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(self.id)),
RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(a.id)), RpcMessage::AccountInstances(v) =>
RpcMessage::AccountInstances(v) => RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(self.id)).collect()),
RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(a.id)).collect()), RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(self.id)),
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(a.id)), _ => msg,
_ => msg,
},
None => msg,
}; };
self.ws.send(msg).unwrap(); self.ws.send(msg).unwrap();
@ -351,50 +165,8 @@ impl Connection {
// when it encounters errors // when it encounters errors
impl Handler for Connection { impl Handler for Connection {
fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> { fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> {
info!("websocket connected account={:?}", self.account); let db = self.pool.get().unwrap();
self.user.connected(&db, &self.events, &self.ws).unwrap();
// tell events we have connected
self.events.send(Event::Connect(self.id, self.account.clone(), self.ws.clone())).unwrap();
// if user logged in do some prep work
if let Some(ref a) = self.account {
self.send(RpcMessage::AccountState(a.clone())).unwrap();
self.events.send(Event::Subscribe(self.id, a.id)).unwrap();
// check if they have an image that needs to be generated
account::img_check(&a).unwrap();
let db = self.pool.get().unwrap();
let mut tx = db.transaction().unwrap();
// send account constructs
let account_constructs = account::constructs(&mut tx, a).unwrap();
self.send(RpcMessage::AccountConstructs(account_constructs)).unwrap();
// get account instances
// and send them to the client
let account_instances = account::account_instances(&mut tx, a).unwrap();
self.send(RpcMessage::AccountInstances(account_instances)).unwrap();
let shop = mtx::account_shop(&mut tx, &a).unwrap();
self.send(RpcMessage::AccountShop(shop)).unwrap();
let team = account::team(&mut tx, &a).unwrap();
self.send(RpcMessage::AccountTeam(team)).unwrap();
let wheel = account::chat_wheel(&db, a.id).unwrap();
self.send(RpcMessage::ChatWheel(wheel)).unwrap();
if let Some(instance) = account::tutorial(&mut tx, &a).unwrap() {
self.send(RpcMessage::InstanceState(instance)).unwrap();
}
// tx should do nothing
tx.commit().unwrap();
} else {
self.send(RpcMessage::Demo(demo().unwrap())).unwrap();
}
Ok(()) Ok(())
} }
@ -404,23 +176,9 @@ impl Handler for Connection {
let begin = Instant::now(); let begin = Instant::now();
let db_connection = self.pool.get().unwrap(); let db_connection = self.pool.get().unwrap();
match self.receive(msg, &db_connection, begin) { match self.user.receive(msg, &db_connection, begin, &self.events, &self.stripe) {
Ok(reply) => { Ok(msg) => {
// if the user queries the state of something self.user.send(msg, &self.events, &self.ws).unwrap();
// we tell events to push updates to them
match reply {
RpcMessage::AccountState(ref v) => {
self.account = Some(v.clone());
self.events.send(Event::Subscribe(self.id, v.id)).unwrap()
},
RpcMessage::GameState(ref v) =>
self.events.send(Event::Subscribe(self.id, v.id)).unwrap(),
RpcMessage::InstanceState(ref v) =>
self.events.send(Event::Subscribe(self.id, v.id)).unwrap(),
_ => (),
};
self.send(reply).unwrap();
}, },
Err(e) => { Err(e) => {
warn!("{:?}", e); warn!("{:?}", e);
@ -434,7 +192,7 @@ impl Handler for Connection {
} }
fn on_close(&mut self, _: CloseCode, _: &str) { fn on_close(&mut self, _: CloseCode, _: &str) {
info!("websocket disconnected account={:?}", self.account); info!("websocket disconnected id={:?}", self.id);
self.events.send(Event::Disconnect(self.id)).unwrap(); self.events.send(Event::Disconnect(self.id)).unwrap();
} }
@ -462,21 +220,19 @@ impl Handler for Connection {
if cookie.name() == TOKEN_HEADER { if cookie.name() == TOKEN_HEADER {
let db = self.pool.get().unwrap(); let db = self.pool.get().unwrap();
match account::from_token(&db, &cookie.value().to_string()) { match account::from_token(&db, &cookie.value().to_string()) {
Ok(a) => self.account = Some(a), Ok(a) => self.user = Box::new(Authorised { id: a.id, account: a }),
Err(_) => return unauth(), Err(_) => return unauth(),
} }
} }
}; };
}; }
Ok(res) Ok(res)
} }
} }
pub fn start(pool: PgPool, events_tx: CbSender<Event>, stripe: StripeClient) { pub fn start(pool: PgPool, events_tx: CbSender<Event>, stripe: StripeClient) {
let mut rng = thread_rng(); let _ws = Builder::new()
let ws = Builder::new()
.with_settings(Settings { .with_settings(Settings {
max_connections: 10_000, max_connections: 10_000,
..Settings::default() ..Settings::default()
@ -506,14 +262,17 @@ pub fn start(pool: PgPool, events_tx: CbSender<Event>, stripe: StripeClient) {
} }
}); });
let anon_account = Account::anonymous();
let id = anon_account.id;
DeflateHandler::new( DeflateHandler::new(
Connection { Connection {
id: rng.gen::<usize>(), id,
account: None,
ws: tx, ws: tx,
pool: pool.clone(), pool: pool.clone(),
stripe: stripe.clone(), stripe: stripe.clone(),
events: events_tx.clone(), events: events_tx.clone(),
user: Box::new(Anonymous { id, account: anon_account, game: None, instance: None })
} }
) )
}) })

View File

@ -0,0 +1,160 @@
use std::time::Instant;
use uuid::Uuid;
use failure::Error;
use failure::err_msg;
use crossbeam_channel::{Sender as CbSender};
use serde_cbor::{from_slice};
use stripe::{Client as StripeClient};
use account::{Account};
use pg::{Db};
use pg;
use events::{Event};
use rpc::{RpcMessage, RpcRequest, User};
use mnml_core::game::Game;
use mnml_core::item::item_info;
use mnml_core::instance::Instance;
#[derive(Debug,Clone)]
pub struct Anonymous {
pub account: Account,
pub id: Uuid,
pub instance: Option<Instance>,
pub game: Option<Game>,
}
impl User for Anonymous {
fn send(&mut self, msg: RpcMessage, _events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> {
// if the user queries the state of something
// we tell events to push updates to them
match msg {
RpcMessage::GameState(ref v) =>
self.game = Some(v.clone()),
RpcMessage::InstanceState(ref v) =>
self.instance = Some(v.clone()),
_ => (),
};
ws.send(msg)?;
Ok(())
}
fn connected(&mut self, _db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> {
info!("anonymous connection");
self.send(RpcMessage::AccountState(self.account.clone()), events, ws)?;
self.send(RpcMessage::StartTutorial(()), events, ws)?;
Ok(())
}
fn receive(&mut self, data: Vec<u8>, _db: &Db, _begin: Instant, _events: &CbSender<Event>, _stripe: &StripeClient) -> Result<RpcMessage, Error> {
match from_slice::<RpcRequest>(&data) {
Ok(v) => {
let get_instance = || {
match self.instance {
Some(ref i) => Ok(i.clone()),
None => return Err(err_msg("instance missing")),
}
};
let get_game = || {
match self.game {
Some(ref i) => Ok(i.clone()),
None => return Err(err_msg("game missing")),
}
};
match v {
RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())),
RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())),
RpcRequest::AccountInstances {} => return Ok(RpcMessage::Pong(())),
RpcRequest::InstancePractice {} =>
Ok(RpcMessage::InstanceState(pg::instance_demo(&self.account)?)),
RpcRequest::InstanceReady { instance_id: _ } => {
match get_instance()?.player_ready(self.account.id)? {
Some(g) => Ok(RpcMessage::GameState(g)),
None => Ok(RpcMessage::InstanceState(get_instance()?)),
}
},
RpcRequest::InstanceState { instance_id: _ } =>
Ok(RpcMessage::InstanceState(get_instance()?)),
RpcRequest::InstanceAbandon { instance_id: _ } => {
let mut instance = get_instance()?;
instance.finish();
Ok(RpcMessage::InstanceState(instance))
},
RpcRequest::VboxBuy { instance_id: _, group, index, construct_id } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_buy(self.account.id, group, index, construct_id)?)),
RpcRequest::VboxApply { instance_id: _, construct_id, index } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_apply(self.account.id, index, construct_id)?)),
RpcRequest::VboxCombine { instance_id: _, inv_indices, vbox_indices } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_combine(self.account.id, inv_indices, vbox_indices)?)),
RpcRequest::VboxRefill { instance_id: _ } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_refill(self.account.id)?)),
RpcRequest::VboxRefund { instance_id: _, index } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_refund(self.account.id, index)?)),
RpcRequest::VboxUnequip { instance_id: _, construct_id, target } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_unequip(self.account.id, target, construct_id, None)?)),
RpcRequest::VboxUnequipApply { instance_id: _, construct_id, target, target_construct_id } =>
Ok(RpcMessage::InstanceState(get_instance()?.vbox_unequip(self.account.id, target, construct_id, Some(target_construct_id))?)),
RpcRequest::GameState { id: _ } =>
Ok(RpcMessage::GameState(get_game()?)),
RpcRequest::GameSkill { game_id: _, construct_id, target_construct_id, skill } => {
let mut game = get_game()?;
game.add_skill(self.account.id, construct_id, target_construct_id, skill)?;
Ok(RpcMessage::GameState(game))
},
RpcRequest::GameSkillClear { game_id: _ } => {
let mut game = get_game()?;
game.clear_skill(self.account.id)?;
Ok(RpcMessage::GameState(game))
},
RpcRequest::GameReady { id: _ } => {
let mut game = get_game()?;
game.player_ready(self.account.id)?;
if game.skill_phase_finished() {
game = game.resolve_phase_start();
}
Ok(RpcMessage::GameState(game))
},
RpcRequest::GameConcede { game_id: _ } =>
Ok(RpcMessage::GameState(get_game()?.concede(self.account.id)?)),
RpcRequest::GameOfferDraw { game_id: _ } =>
Ok(RpcMessage::GameState(get_game()?.offer_draw(self.account.id)?)),
_ => Err(format_err!("unhandled anonymous request request={:?}", v)),
}
},
Err(e) => {
warn!("{:?}", e);
Err(err_msg("invalid message"))
},
}
}
}

View File

@ -0,0 +1,265 @@
use mnml_core::mob::anim_test_game;
use mnml_core::item::item_info;
use std::time::Instant;
use uuid::Uuid;
use failure::Error;
use failure::err_msg;
use crossbeam_channel::{Sender as CbSender};
use stripe::{Client as StripeClient};
use serde_cbor::{from_slice};
use pg::{
game_concede,
game_offer_draw,
game_ready,
game_skill,
game_skill_clear,
game_state,
instance_abandon,
instance_practice,
instance_ready,
instance_state,
vbox_apply,
vbox_buy,
vbox_combine,
vbox_refill,
vbox_refund,
vbox_unequip,
};
use account::{Account};
use account;
use events::{Event};
use mtx;
use mail;
use payments;
use pg::{Db};
use rpc::{RpcMessage, RpcRequest, User};
#[derive(Debug,Clone)]
pub struct Authorised {
pub account: Account,
pub id: Uuid
}
impl User for Authorised {
fn send(&mut self, msg: RpcMessage, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> {
// if the user queries the state of something
// we tell events to push updates to them
match msg {
RpcMessage::AccountState(ref v) => {
events.send(Event::Subscribe(self.id, v.id))?
},
RpcMessage::GameState(ref v) =>
events.send(Event::Subscribe(self.id, v.id))?,
RpcMessage::InstanceState(ref v) =>
events.send(Event::Subscribe(self.id, v.id))?,
_ => (),
};
ws.send(msg)?;
Ok(())
}
fn connected(&mut self, db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> {
info!("authenticated connection account={:?}", self.account);
let a = &self.account;
ws.send(RpcMessage::AccountAuthenticated(a.clone()))?;
// tell events we have connected
events.send(Event::Connect(self.id, a.clone(), ws.clone()))?;
ws.send(RpcMessage::AccountState(a.clone()))?;
events.send(Event::Subscribe(self.id, a.id))?;
// check if they have an image that needs to be generated
account::img_check(&a)?;
let mut tx = db.transaction()?;
// send account constructs
let account_constructs = account::constructs(&mut tx, &a)?;
ws.send(RpcMessage::AccountConstructs(account_constructs))?;
// get account instances
// and send them to the client
let account_instances = account::account_instances(&mut tx, &a)?;
ws.send(RpcMessage::AccountInstances(account_instances))?;
let shop = mtx::account_shop(&mut tx, &a)?;
ws.send(RpcMessage::AccountShop(shop))?;
let team = account::team(&mut tx, &a)?;
ws.send(RpcMessage::AccountTeam(team))?;
let wheel = account::chat_wheel(&db, a.id)?;
ws.send(RpcMessage::ChatWheel(wheel))?;
if let Some(instance) = account::tutorial(&mut tx, &a)? {
ws.send(RpcMessage::InstanceState(instance))?;
}
// tx should do nothing
tx.commit()?;
Ok(())
}
fn receive(&mut self, data: Vec<u8>, db: &Db, begin: Instant, events: &CbSender<Event>, stripe: &StripeClient) -> Result<RpcMessage, Error> {
// cast the msg to this type to receive method name
match from_slice::<RpcRequest>(&data) {
Ok(v) => {
let request = v.clone();
let response = match v {
RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())),
RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())),
RpcRequest::DevResolve { skill } =>
return Ok(RpcMessage::GameState(anim_test_game(skill))),
RpcRequest::InstanceQueue {} => {
events.send(Event::Queue(self.id))?;
Ok(RpcMessage::QueueRequested(()))
},
RpcRequest::InstanceInvite {} => {
events.send(Event::Invite(self.id))?;
Ok(RpcMessage::InviteRequested(()))
},
RpcRequest::InstanceJoin { code } => {
events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(()))
},
RpcRequest::InstanceLeave {} => {
events.send(Event::Leave(self.id))?;
Ok(RpcMessage::Processing(()))
},
RpcRequest::InstanceChat { instance_id, index } => {
if !self.account.subscribed {
return Err(err_msg("subscribe to unlock chat"))
}
let wheel = account::chat_wheel(&db, self.account.id)?;
if let Some(c) = wheel.get(index) {
events.send(Event::Chat(self.id, instance_id, c.to_string()))?;
} else {
return Err(err_msg("invalid chat index"));
}
Ok(RpcMessage::Processing(()))
},
_ => {
// all good, let's make a tx and process
let mut tx = db.transaction()?;
let res = match v {
RpcRequest::AccountState {} =>
Ok(RpcMessage::AccountState(self.account.clone())),
RpcRequest::AccountConstructs {} =>
Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &self.account)?)),
RpcRequest::AccountInstances {} =>
Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, &self.account)?)),
RpcRequest::AccountSetTeam { ids } =>
Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &self.account, ids)?)),
RpcRequest::EmailState {} =>
Ok(RpcMessage::EmailState(mail::select_account(&db, self.account.id)?)),
RpcRequest::SubscriptionState {} =>
Ok(RpcMessage::SubscriptionState(payments::account_subscription(db, stripe, &self.account)?)),
// RpcRequest::AccountShop {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),
// RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account),
RpcRequest::GameState { id } =>
Ok(RpcMessage::GameState(game_state(&mut tx, &self.account, id)?)),
RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } =>
Ok(RpcMessage::GameState(game_skill(&mut tx, &self.account, game_id, construct_id, target_construct_id, skill)?)),
RpcRequest::GameSkillClear { game_id } =>
Ok(RpcMessage::GameState(game_skill_clear(&mut tx, &self.account, game_id)?)),
RpcRequest::GameReady { id } =>
Ok(RpcMessage::GameState(game_ready(&mut tx, &self.account, id)?)),
RpcRequest::GameConcede { game_id } =>
Ok(RpcMessage::GameState(game_concede(&mut tx, &self.account, game_id)?)),
RpcRequest::GameOfferDraw { game_id } =>
Ok(RpcMessage::GameState(game_offer_draw(&mut tx, &self.account, game_id)?)),
RpcRequest::InstancePractice {} =>
Ok(RpcMessage::InstanceState(instance_practice(&mut tx, &self.account)?)),
// these two can return GameState or InstanceState
RpcRequest::InstanceReady { instance_id } =>
Ok(instance_ready(&mut tx, &self.account, instance_id)?),
RpcRequest::InstanceState { instance_id } =>
Ok(instance_state(&mut tx, instance_id)?),
RpcRequest::InstanceAbandon { instance_id } =>
Ok(instance_abandon(&mut tx, &self.account, instance_id)?),
RpcRequest::VboxBuy { instance_id, group, index, construct_id } =>
Ok(RpcMessage::InstanceState(vbox_buy(&mut tx, &self.account, instance_id, group, index, construct_id)?)),
RpcRequest::VboxApply { instance_id, construct_id, index } =>
Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, &self.account, instance_id, construct_id, index)?)),
RpcRequest::VboxCombine { instance_id, inv_indices, vbox_indices } =>
Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, &self.account, instance_id, inv_indices, vbox_indices)?)),
RpcRequest::VboxRefill { instance_id } =>
Ok(RpcMessage::InstanceState(vbox_refill(&mut tx, &self.account, instance_id)?)),
RpcRequest::VboxRefund { instance_id, index } =>
Ok(RpcMessage::InstanceState(vbox_refund(&mut tx, &self.account, instance_id, index)?)),
RpcRequest::VboxUnequip { instance_id, construct_id, target } =>
Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, &self.account, instance_id, construct_id, target, None)?)),
RpcRequest::VboxUnequipApply { instance_id, construct_id, target, target_construct_id } =>
Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, &self.account, instance_id, construct_id, target, Some(target_construct_id))?)),
RpcRequest::MtxConstructSpawn {} =>
Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, &self.account)?)),
RpcRequest::MtxConstructApply { mtx, construct_id, name } =>
Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, &self.account, mtx, construct_id, name)?)),
RpcRequest::MtxAccountApply { mtx } =>
Ok(RpcMessage::AccountState(mtx::account_apply(&mut tx, &self.account, mtx)?)),
RpcRequest::MtxBuy { mtx } =>
Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, &self.account, mtx)?)),
RpcRequest::SubscriptionEnding { ending } =>
Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, stripe, &self.account, ending)?)),
_ => Err(format_err!("unknown request request={:?}", request)),
};
tx.commit()?;
res
}
};
info!("request={:?} account={:?} duration={:?}", request, self.account.name, begin.elapsed());
return response;
},
Err(e) => {
warn!("{:?}", e);
Err(err_msg("invalid message"))
},
}
}
}

View File

@ -1,7 +1,5 @@
use std::time::{Duration}; use std::time::{Duration};
use uuid::Uuid;
use crossbeam_channel::{tick, Sender, Receiver}; use crossbeam_channel::{tick, Sender, Receiver};
// Db Commons // Db Commons
@ -24,13 +22,12 @@ use pg::{
pvp, pvp,
}; };
type Id = usize;
type Pair = (PvpRequest, PvpRequest); type Pair = (PvpRequest, PvpRequest);
pub enum GameEvent { pub enum GameEvent {
Upkeep, Upkeep,
Finish(Uuid), // Finish(Uuid),
Match(Pair), Match(Pair),
} }
@ -76,10 +73,10 @@ impl Warden {
match msg { match msg {
GameEvent::Upkeep => self.on_upkeep(), GameEvent::Upkeep => self.on_upkeep(),
GameEvent::Match(pair) => self.on_match(pair), GameEvent::Match(pair) => self.on_match(pair),
GameEvent::Finish(id) => { // GameEvent::Finish(id) => {
info!("game finished id={:?}", id); // info!("game finished id={:?}", id);
Ok(()) // Ok(())
}, // },
} }
} }
@ -131,7 +128,7 @@ impl Warden {
fn fetch_games(mut tx: Transaction) -> Result<Transaction, Error> { fn fetch_games(mut tx: Transaction) -> Result<Transaction, Error> {
let games = games_need_upkeep(&mut tx)?; let games = games_need_upkeep(&mut tx)?;
for mut game in games { for game in games {
let game = game.upkeep(); let game = game.upkeep();
match game_update(&mut tx, &game) { match game_update(&mut tx, &game) {
Ok(_) => (), Ok(_) => (),
@ -146,7 +143,7 @@ fn fetch_games(mut tx: Transaction) -> Result<Transaction, Error> {
} }
fn fetch_instances(mut tx: Transaction) -> Result<Transaction, Error> { fn fetch_instances(mut tx: Transaction) -> Result<Transaction, Error> {
for mut instance in instances_need_upkeep(&mut tx)? { for instance in instances_need_upkeep(&mut tx)? {
let (instance, new_game) = instance.upkeep(); let (instance, new_game) = instance.upkeep();
if let Some(game) = new_game { if let Some(game) = new_game {