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",
"version": "1.10.1",
"version": "1.11.0",
"description": "",
"main": "index.js",
"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 {
padding-right: 1em;
// display: flex;
// flex-flow: column;
line-height: 2em;
}
h3 {
// text-transform: uppercase;
margin-bottom: 0.5em;
}
button {
width: 100%;
height: 2.5em;
display: block;
}
input {
width: 100%;
height: 3em;
height: 2.5em;
display: block;
}

View File

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

View File

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

View File

@ -75,25 +75,11 @@
flex: 1;
border-top: 0;
border: 0.1em solid #222;
&:not(:last-child) {
border-right: 0;
}
&:last-child {
float: right;
}
}
}
.login {
display: flex;
flex-flow: column;
.terms {
display: inline;
margin: 0 1em;
}
}
}
section {
@ -108,62 +94,14 @@ section {
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 {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
.list {
margin-bottom: 2em;
figure {
letter-spacing: 0.25em;
@ -172,61 +110,92 @@ section {
display: flex;
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 {
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 {
margin-bottom: 0.5em;
div:first-child {
padding-right: 1em;
}
}
.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;
}
button {
flex: 1;
border-top: 0;
border: 0.1em solid #222;
&:last-child {
float: right;
}
}
}
.intro {
text-align: center;
font-size: 0.8em;
}

View File

@ -27,23 +27,6 @@ html body {
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 {
box-sizing: border-box;
margin: 0;
@ -108,11 +91,37 @@ dl {
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 {
aside button {
font-size: 50%;
}
}
&.front-page {
display: block;
main {
padding: 0 25%;
}
.logo {
margin: 2em 0;
}
.list {
margin-bottom: 0;
}
}
}
main {
@ -129,7 +138,7 @@ button, input {
box-sizing: border-box;
font-size: 1em;
flex: 1;
border-radius: 0.5em;
border-radius: 0;
line-height: 2em;
padding-right: 0.1em;
padding-left: 0.1em;
@ -150,9 +159,12 @@ button, input {
&:focus {
/*colour necesary to bash skellington*/
outline: 0;
}
// &:active {
// filter: url("#noiseFilter");
// }
}
a {
@ -261,28 +273,12 @@ figure.gray {
display: none;
}
header {
.options {
font-size: 200%;
}
button {
height: 2em;
// border-radius: 0.1em;
border: none;
border-radius: 0;
}
}
.options {
button {
&.highlight {
color: @white;
box-shadow: inset 0px 5px 0px 0px @white;
border: 0;
}
border: none;
}
}
@ -300,11 +296,20 @@ li {
}
.logo {
height: 2em;
background-image: url("../../assets/mnml.logo.trim.svg");
height: 4em;
filter: url("#noiseFilter");
background-image: url("../../assets/mnml.logo.text.svg");
background-size: contain;
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 {
@ -316,8 +321,13 @@ li {
.mnni {
background-image: url("./../mnni.svg");
filter: url("#noiseFilter");
}
// .highlight {
// filter: url("#noiseFilter");
// }
.avatar {
grid-area: avatar;
object-fit: contain;
@ -328,6 +338,10 @@ li {
// pointer-events: none;
}
header {
// font-size: 1.2em;
}
#clipboard {
width: 1px;
height: 1px;
@ -359,4 +373,8 @@ li {
}
}
#noise {
height: 0;
}
@import 'styles.mobile.less';

View File

@ -7,6 +7,12 @@
font-size: 8pt;
padding: 0;
&.front-page {
main {
padding: 0 0.5em;
}
}
.instance {
grid-template-areas:
"vbox vbox"
@ -164,12 +170,21 @@
// 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 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas:
"main"
"main";
&.front-page {
display: block;
main {
padding: 0 0.5em;
}
}
}
section {
@ -264,6 +279,9 @@
}
.info-combiner {
max-height: 7em;
overflow-y: scroll;
.info {
display: none;
}

View File

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

View File

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

View File

@ -1,4 +1,5 @@
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 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 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 setChatWheel = value => ({ type: 'SET_CHAT_WHEEL', value });
export const setInstanceChat = value => ({ type: 'SET_INSTANCE_CHAT', value });

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,8 @@ const { ConstructAnimation } = require('./animations');
const addState = connect(
function receiveState(state) {
const { animSource, animTarget, resolution, account } = state;
return { animSource, animTarget, resolution, account };
const { animating, animSource, animTarget, resolution, account } = state;
return { animating, animSource, animTarget, resolution, account };
}
);
@ -43,6 +43,7 @@ class ConstructAvatar extends Component {
}
onClick() {
if (this.props.animating) return false;
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;
// a different text object and text construct
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

View File

@ -10,6 +10,7 @@ const addState = connect(
function receiveState(state) {
const {
ws,
authenticated,
account,
game,
instance,
@ -17,6 +18,7 @@ const addState = connect(
} = state;
return {
authenticated,
account,
game,
instance,
@ -28,6 +30,7 @@ const addState = connect(
function Controls(args) {
const {
game,
authenticated,
account,
instance,
nav,
@ -38,6 +41,7 @@ function Controls(args) {
if (game) return <GameCtrl />;
if (instance) return <InstanceCtrl />;
if (!authenticated) return false;
if (nav === 'play' || nav === 'shop' || nav === 'reshape' || !nav) return <PlayCtrl />
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 [type, event] = resolution.event;
if (type === 'Ko') return <h1><span>KO!</span></h1>;
if (type === 'Disable') {
const { disable } = 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;
switch (type) {
case 'Damage': {
const { amount, mitigation, colour } = event;
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;
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>;
if (type === 'Reflection') return <h1><span>REFLECT</span></h1>;
if (type === 'Effect') {
case 'Effect': {
const { effect, duration } = event;
return <h1><span>+{effect} {duration}T</span></h1>;
}
if (type === 'Removal') {
case 'Removal': {
const { effect } = event;
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 (
<div class="combat-text">

View File

@ -59,11 +59,16 @@ class GameConstruct extends preact.Component {
player,
} = 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 = () => {
if (resolution) {
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 '';
};

View File

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

View File

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

View File

@ -4,27 +4,38 @@ const { connect } = require('preact-redux');
const Main = require('./main');
// const Nav = require('./nav');
const Controls = require('./controls');
const FrontPage = require('./front.page');
const Noise = require('./noise');
const addState = connect(
({ game, instance }) => ({ game, instance })
({ game, instance, authenticated }) => ({ game, instance, authenticated })
);
function Mnml(args) {
const {
game,
instance,
authenticated,
} = args;
const rotateClass = (game || instance) && window.innerHeight < 900 && window.innerWidth < window.innerHeight
? 'show'
: '';
if (!authenticated && !instance && !game) return (
<div id="mnml" class='front-page'>
<Noise />
<FrontPage />
<div id="rotate" class={rotateClass} ></div>
</div>
);
return (
<div id="mnml">
<Main />
<Controls />
<div id="rotate" class={rotateClass} >
</div>
<Noise />
<div id="rotate" class={rotateClass} ></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="combo-header">
<h2>COMBOS</h2>
Combine colours and items.
</div>
<div class="combo-list"
onMouseOver={e => e.stopPropagation()}

View File

@ -5,10 +5,9 @@ const Login = require('./welcome.login');
const Register = require('./welcome.register');
const Help = require('./welcome.help');
// const About = require('./welcome.about');
const Demo = require('./demo');
function Welcome() {
const page = this.state.page || 'register';
const page = this.state.page || 'login';
const pageEl = () => {
if (page === 'login') return <Login />;
@ -17,65 +16,32 @@ function Welcome() {
return false;
};
const news = (
<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 />;
const form = <div>{pageEl()}</div>;
return (
<main class="menu welcome">
<header>
<div class="options">
<button
onClick={() => this.setState({ page: 'login' })}
class='logo login-btn'>
&nbsp;
</button>
<button
class={`login-btn ${page === 'login' ? 'highlight' : ''}`}
disabled={page === 'login'}
onClick={() => this.setState({ page: 'login' })}>
Login
</button>
<button
class={`login-btn ${page === 'register' ? 'highlight' : ''}`}
disabled={page === 'register'}
onClick={() => this.setState({ page: 'register' })}>
Register
</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}
<header>
<div class="options">
<button
class={`login-btn ${page === 'login' ? 'highlight' : ''}`}
disabled={page === 'login'}
onClick={() => this.setState({ page: 'login' })}>
Login
</button>
<button
class={`login-btn ${page === 'register' ? 'highlight' : ''}`}
disabled={page === 'register'}
onClick={() => this.setState({ page: 'register' })}>
Register
</button>
<button
class={`login-btn ${page === 'help' ? 'highlight' : ''}`}
disabled={page === 'help'}
onClick={() => this.setState({ page: 'help' })}>
Help
</button>
</div>
</main>
{form}
</header>
);
}

View File

@ -27,7 +27,6 @@ function registerEvents(store) {
function clearTutorial() {
store.dispatch(actions.setTutorial(null));
localStorage.setItem('tutorial-complete', true);
}
@ -35,7 +34,6 @@ function registerEvents(store) {
store.dispatch(actions.setTutorialGame(null));
}
function setPing(ping) {
store.dispatch(actions.setPing(ping));
}
@ -106,16 +104,21 @@ function registerEvents(store) {
}
function setAccount(account) {
if (account && process.env.NODE_ENV !== 'development') {
LogRocket.init('yh0dy3/mnml');
LogRocket.identify(account.id, account);
store.dispatch(actions.setAccount(account));
}
if (window.Notification) {
window.Notification.requestPermission();
}
function setAuthenticated(account) {
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.setTutorial(null));
store.dispatch(actions.setAuthenticated(true));
}
function setEmail(email) {
@ -180,18 +183,14 @@ function registerEvents(store) {
const player = v.players.find(p => p.id === account.id);
store.dispatch(actions.setPlayer(player));
if (tutorial) tutorialVbox(player, store, tutorial);
if (v.phase === 'Finished') {
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));
}
@ -207,94 +206,6 @@ function registerEvents(store) {
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() {
const { ws } = store.getState();
const cmds = querystring.parse(location.hash);
@ -303,6 +214,11 @@ function registerEvents(store) {
return true;
}
function startTutorial() {
store.dispatch(actions.setTutorial(1));
}
window.addEventListener('hashchange', urlHashChange, false);
return {
@ -313,11 +229,11 @@ function registerEvents(store) {
clearTutorial,
clearTutorialGame,
setAccount,
setAuthenticated,
setAccountInstances,
setActiveItem,
setActiveSkill,
setChatWheel,
setDemo,
setConstructList,
setNewConstruct,
setGame,
@ -333,6 +249,8 @@ function registerEvents(store) {
setSubscription,
setWs,
startTutorial,
urlHashChange,
notify,

View File

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

View File

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

View File

@ -115,8 +115,9 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 1) {
return (
<div class='info-item'>
<h2>Tutorial</h2>
<p> Welcome to the vbox phase tutorial.</p>
<h1>Welcome to MNML</h1>
<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> Buy the two colours from the store to continue. </p>
</div>
@ -126,9 +127,9 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 2) {
return (
<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> 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>
);
}
@ -137,11 +138,11 @@ function tutorialStage(tutorial, clearTutorial, instance) {
const constructOne = instance.players[0].constructs[0].name;
return (
<div class='info-item'>
<h2>Tutorial</h2>
<h2>Equipping Items</h2>
<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> 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>
);
}
@ -149,7 +150,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 4) {
return (
<div class='info-item'>
<h2>Tutorial</h2>
<h2>Specialisations</h2>
<p> You can also buy specialisation items for your constructs. <br />
Specialisation items increase stats including power, speed and life. </p>
<p> Buy the specialisation item from the store to continue. </p>
@ -160,11 +161,12 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 5) {
return (
<div class='info-item'>
<h2>Tutorial</h2>
<h2>Specialisations</h2>
<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> Click the specialisation item in the stash.<br />
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>
);
}
@ -174,11 +176,12 @@ function tutorialStage(tutorial, clearTutorial, instance) {
const constructThree = instance.players[0].constructs[2].name;
return (
<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> 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 />
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>
);
}
@ -186,7 +189,7 @@ function tutorialStage(tutorial, clearTutorial, instance) {
if (tutorial === 7) {
return (
<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> Bits are your currency for buying items. <br />
You can refill the store by pressing the refill button for 2b. <br />
@ -203,22 +206,22 @@ function tutorialStage(tutorial, clearTutorial, instance) {
return (
<div class='info-item'>
<h2>Tutorial</h2>
<h2>GLHF</h2>
<p>That completes the VBOX Tutorial.</p>
<p>Press <b>READY</b> to progress to the <b>GAME PHASE</b> <br />
You can continue creating new items to upgrade your constructs further. </p>
<p>Press the green <b>READY</b> button in the bottom right to progress to the <b>GAME PHASE</b> <br />
or continue creating new items to upgrade your constructs further. </p>
</div>
);
}
return false;
};
const classes = tutorial === 8 ? 'focus' : '';
const text = tutorial === 8 ? 'Continue' : 'Skip Tutorial'
const exitTutorial = <button
class={classes}
onClick={e => e.stopPropagation()}
onMouseDown={exit}> {text} </button>;
const exitTutorial = tutorial === 8 ?
<button
class='focus'
onClick={e => e.stopPropagation()}
onMouseDown={exit}> Continue </button>
: null;
return (
<div class='tutorial'>

View File

@ -241,30 +241,15 @@ function convertItem(v) {
function effectInfo(i) {
// FIX ME
return 'effect info to be fixed';
/*const hybridBlast = 25;
const hybridBlast = 25;
const hasteStrike = 30;
function multiplier(s) { // Update later to use server info in future
if (s === 'CounterAttack') return 120;
if (s === 'CounterAttack+') return 160;
if (s === 'CounterAttack++') return 230;
if (s === 'DecayTick') return 33;
if (s === 'DecayTick+') return 45;
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;
if (s === 'CounterAttack') return 115;
if (s === 'CounterAttack+') return 130;
if (s === 'CounterAttack++') return 160;
if (s === 'Electrocute') return 80;
if (s === 'Electrocute+') return 90;
if (s === 'Electrocute++') return 100;
return 0;
}
@ -289,16 +274,16 @@ function effectInfo(i) {
case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`;
case 'Silence': return 'Disable construct from casting any blue skills';
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 '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 '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 'Siphon': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower + GreenPower as blue damage each turn, healing the caster.`;
case 'Triage': return `Construct will be healed for ${i.meta[1].amount} green life each turn.`;
case 'Siphon': return `Construct will take ${i.meta[1].amount} blue damage each turn, healing the caster.`;
default: return 'Missing Effect Text';
}*/
}
}
module.exports = {

View File

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

View File

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

View File

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

View File

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

View File

@ -135,20 +135,6 @@ impl Game {
.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 {
return self.players.len() == self.player_num
&& 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> {
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 {
return Err(err_msg("game not in skill phase"));
}
@ -398,6 +388,11 @@ impl Game {
let mut sorted = self.stack.clone();
sorted.iter_mut()
.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() {
let caster = self.construct_by_id(s.source).unwrap();
let speed = caster.skill_speed(s.skill);
@ -462,7 +457,15 @@ impl Game {
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()
.find(|t| t.constructs.iter().any(|c| c.id == cast.target))
.unwrap();
@ -508,17 +511,19 @@ impl Game {
self.resolve(Cast { skill, ..cast });
}
// for aoe events send the source / target animations before each set of casts
if cast.skill.aoe() {
if cast.skill.cast_animation() {
let event = self.cast(cast);
self.add_resolution(&cast, &event);
let casts = self.modify_cast(cast);
let castable = casts
.iter()
.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 {
self.execute(cast);
}
@ -550,6 +555,10 @@ impl Game {
return self.resolve(Cast { target: cast.source, ..cast });
}
if !cast.skill.aoe() {
self.action(cast, Action::Hit);
}
cast.resolve(self);
self
@ -559,6 +568,7 @@ impl Game {
let new_events = match action {
Action::Cast => vec![self.cast(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::Heal { construct, amount, colour } => self.heal(construct, amount, colour),
@ -642,11 +652,14 @@ impl Game {
Value::TickDamage { construct, effect } =>
self.construct(construct).stat(Stat::TickDamage(effect)),
// 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 {
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 },
DamageReceived { construct: Uuid, colour: Colour },
TickDamage { construct: Uuid, effect: Effect },
// Affected { construct: Uuid, effect: Effect }, // not an int :(
}
#[derive(Debug,Clone,PartialEq)]
pub enum Action {
Hit,
HitAoe,
Cast,
Heal { construct: Uuid, amount: usize, colour: Colour },
Damage { construct: Uuid, amount: usize, colour: Colour },
@ -1033,6 +1048,7 @@ mod tests {
.learn(Skill::Siphon)
.learn(Skill::Amplify)
.learn(Skill::Stun)
.learn(Skill::Ruin)
.learn(Skill::Block)
.learn(Skill::Sleep)
.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());
}
#[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)]
// mod tests {
// 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]
// fn counter_test() {
// let mut game = create_test_game();
@ -1712,56 +1705,6 @@ mod tests {
// 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]
// // fn absorb_test() {
// // let mut game = create_test_game();
@ -2015,7 +1958,7 @@ mod tests {
let mut game = create_2v2_test_game();
game.players[0].set_ready(true);
game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap());
game = game.upkeep();
game.upkeep();
// assert!(game.players[1].warnings == 1);
}
@ -2026,7 +1969,7 @@ mod tests {
let source = game.players[0].constructs[0].id;
let target = game.players[1].constructs[0].id;
game.add_skill(player_id, source, target, Skill::Attack).unwrap();
game = game.resolve_phase_start();
game.resolve_phase_start();
}
#[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]
fn siphon_test() {
let mut game = create_2v2_test_game();
@ -2158,6 +2138,9 @@ mod tests {
// 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));
let last = game.resolutions.len() - 1;
let resolutions = &game.resolutions[last];
@ -2166,7 +2149,75 @@ mod tests {
_ => false,
}).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);
// 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]
@ -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]
fn absorb_test() {
let mut game = create_2v2_test_game();
@ -2376,6 +2506,8 @@ mod tests {
})
.unwrap();
assert!(siphon_tick_speed > 0);
assert!(siphon_speed > 0);
assert_eq!(siphon_tick_dmg, siphon_dmg);
assert_eq!(siphon_tick_speed, siphon_speed);
}

View File

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

View File

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

View File

@ -31,6 +31,11 @@ pub fn bot_player() -> Player {
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 {
let mut rng = thread_rng();
let mut game = Game::new();

View File

@ -1,7 +1,6 @@
use std::collections::{HashMap};
use uuid::Uuid;
use rand::prelude::*;
use failure::Error;
use failure::err_msg;
@ -130,7 +129,7 @@ impl Player {
}
pub fn autobuy(&mut self) -> &mut Player {
let mut rng = thread_rng();
// let mut rng = thread_rng();
// skill buying phase
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) {
if !self.skill.aoe() {
if self.skill.cast_animation() {
game.action(self, Action::Cast);
}
game.action(self, Action::Hit);
}
match self.skill {
Skill::Attack => attack(self, game, Attack::Base),
Skill::Block => block(self, game, Block::Base),
@ -193,9 +186,6 @@ impl Cast {
Skill::TriageTick => triage_tick(self, game),
};
// actions.append(&mut rest);
// return actions;
}
}
@ -647,6 +637,9 @@ impl Skill {
Skill::Heal |
Skill::HealPlus |
Skill::HealPlusPlus |
Skill::Hybrid |
Skill::HybridPlus |
Skill::HybridPlusPlus |
Skill::Absorb |
Skill::AbsorbPlus |
Skill::AbsorbPlusPlus |
@ -722,7 +715,7 @@ impl Skill {
.collect::<Vec<Colour>>();
}
fn base(&self) -> Skill {
fn _base(&self) -> Skill {
let bases = [Item::Attack, Item::Stun, Item::Buff, Item::Debuff, Item::Block];
match self.components()
.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,
Action::Damage {
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,
Action::Effect {
construct: cast.target,
@ -1685,19 +1684,27 @@ impl 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());
game.action(cast,
Action::Remove {
construct: cast.source,
effect: Effect::Electrocute,
}
);
game.action(cast,
Action::Effect {
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 }) },
},
);
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) {
@ -1847,11 +1854,16 @@ fn link(cast: Cast, game: &mut Game, values: Link) {
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,
Action::Damage {
construct: cast.target,
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 {
construct: cast.target,
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 }) },
}
);
if game.affected(cast.target, Effect::Triaged) { return; }
game.action(cast,
Action::Heal {
construct: cast.target,
@ -2208,7 +2223,7 @@ fn triage_tick(cast: Cast, game: &mut Game) {
Action::Heal {
construct: cast.target,
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,

View File

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

View File

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

View File

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

View File

@ -42,7 +42,17 @@ impl Account {
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 = "
SELECT id, name, balance, subscribed, img
FROM accounts

View File

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

View File

@ -72,7 +72,7 @@ enum ConstructShapes {
Line,
V,
Tri,
Plus,
// Plus,
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})\" />",
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 => (),
}
}

View File

@ -15,6 +15,7 @@ extern crate serde_cbor;
#[macro_use] extern crate failure;
extern crate fern;
extern crate log_panics;
#[macro_use] extern crate log;
extern crate stripe;
@ -47,6 +48,8 @@ mod pg;
mod events;
pub mod rpc;
mod warden;
mod user_authenticated;
mod user_anonymous;
use std::thread::{spawn};
use std::path::{Path};
@ -55,6 +58,8 @@ use crossbeam_channel::{unbounded};
#[cfg(unix)]
pub fn setup_logger() -> Result<(), fern::InitError> {
log_panics::init();
let formatter = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER,
hostname: None,
@ -117,11 +122,11 @@ pub fn start() {
let warden_tick_tx = warden_tx.clone();
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
// 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 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));
}
pub fn listen(rx: Receiver<Mail>) -> SmtpTransport {
pub fn listen(_rx: Receiver<Mail>) -> SmtpTransport {
let sender = env::var("MAIL_ADDRESS")
.expect("MAIL_ADDRESS must be set");
@ -271,7 +271,7 @@ pub fn listen(rx: Receiver<Mail>) -> SmtpTransport {
let domain = env::var("MAIL_DOMAIN")
.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))
.credentials(Credentials::new(sender, password))
.smtp_utf8(true)
@ -281,19 +281,6 @@ pub fn listen(rx: Receiver<Mail>) -> SmtpTransport {
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
// mailer.close();
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 = "
DELETE
FROM mtx

View File

@ -22,7 +22,7 @@ use mnml_core::mob::instance_mobs;
use mnml_core::vbox::{ItemType, VboxIndices};
use mnml_core::item::Item;
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 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 = "
DELETE
FROM constructs
@ -163,7 +163,7 @@ pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Res
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 = "
SELECT data
FROM constructs
@ -290,7 +290,7 @@ pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {
return Ok(game);
}
pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> {
pub fn _game_select(db: &Db, id: Uuid) -> Result<Game, Error> {
let query = "
SELECT *
FROM games
@ -312,7 +312,7 @@ pub fn select(db: &Db, id: Uuid) -> Result<Game, Error> {
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 = "
SELECT data
FROM games
@ -694,6 +694,35 @@ pub fn instance_practice(tx: &mut Transaction, account: &Account) -> Result<Inst
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> {
let mut instance = Instance::new()
// TODO generate nice game names
@ -786,7 +815,7 @@ pub fn instance_game_finished(tx: &mut Transaction, game: &Game, instance_id: Uu
Ok(())
}
pub fn bot_instance() -> Instance {
pub fn _bot_instance() -> Instance {
let mut instance = Instance::new();
let bot_player = bot_player();
@ -804,26 +833,6 @@ pub fn bot_instance() -> 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> {
let instance = instance_get(tx, instance_id)?
.vbox_refill(account.id)?;

View File

@ -1,6 +1,6 @@
use mnml_core::item::ItemInfoCtr;
use mnml_core::instance::ChatState;
use mnml_core::item::item_info;
use std::collections::HashMap;
use std::time::{Instant};
use std::thread::{spawn};
@ -8,12 +8,8 @@ use std::thread::{spawn};
use std::str;
use uuid::Uuid;
use rand::prelude::*;
use failure::Error;
use failure::err_msg;
use serde_cbor::{from_slice, to_vec};
use serde_cbor::{to_vec};
use cookie::Cookie;
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::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;
use events::{Event};
use user_anonymous::{Anonymous};
use user_authenticated::{Authorised};
use mnml_core::construct::{Construct};
use mnml_core::game::{Game};
use mnml_core::player::Player;
use mnml_core::vbox::{ItemType};
use mnml_core::item::Item;
use mnml_core::skill::Skill;
use mnml_core::mob::{anim_test_game};
use mnml_core::instance::{Instance};
use mtx;
use mail;
use payments;
use mail::Email;
use pg::{Db};
use pg::{PgPool};
@ -68,13 +44,12 @@ use http::{AUTH_CLEAR, TOKEN_HEADER};
#[derive(Debug,Clone,Serialize)]
pub enum RpcMessage {
AccountState(Account),
AccountAuthenticated(Account),
AccountConstructs(Vec<Construct>),
AccountTeam(Vec<Construct>),
AccountInstances(Vec<Instance>),
AccountShop(mtx::Shop),
Demo(Vec<Player>),
ConstructSpawn(Construct),
GameState(Game),
ItemInfo(ItemInfoCtr),
@ -87,6 +62,7 @@ pub enum RpcMessage {
SubscriptionState(Option<Subscription>),
Pong(()),
StartTutorial(()),
QueueRequested(()),
QueueJoined(()),
@ -149,194 +125,32 @@ pub enum RpcRequest {
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 {
pub id: usize,
pub id: Uuid,
pub ws: CbSender<RpcMessage>,
pool: PgPool,
stripe: StripeClient,
account: Option<Account>,
// account: Option<Account>,
user: Box<dyn User>,
events: CbSender<Event>,
}
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
// use it to modify outgoing messages, update subs, serialize in some way...
fn send(&self, msg: RpcMessage) -> Result<(), Error> {
let msg = match self.account {
Some(ref a) => match msg {
RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(a.id)),
RpcMessage::AccountInstances(v) =>
RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(a.id)).collect()),
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(a.id)),
_ => msg,
},
None => msg,
let msg = match msg {
RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(self.id)),
RpcMessage::AccountInstances(v) =>
RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(self.id)).collect()),
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(self.id)),
_ => msg,
};
self.ws.send(msg).unwrap();
@ -351,50 +165,8 @@ impl Connection {
// when it encounters errors
impl Handler for Connection {
fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> {
info!("websocket connected account={:?}", self.account);
// 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();
}
let db = self.pool.get().unwrap();
self.user.connected(&db, &self.events, &self.ws).unwrap();
Ok(())
}
@ -404,23 +176,9 @@ impl Handler for Connection {
let begin = Instant::now();
let db_connection = self.pool.get().unwrap();
match self.receive(msg, &db_connection, begin) {
Ok(reply) => {
// if the user queries the state of something
// 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();
match self.user.receive(msg, &db_connection, begin, &self.events, &self.stripe) {
Ok(msg) => {
self.user.send(msg, &self.events, &self.ws).unwrap();
},
Err(e) => {
warn!("{:?}", e);
@ -434,7 +192,7 @@ impl Handler for Connection {
}
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();
}
@ -462,21 +220,19 @@ impl Handler for Connection {
if cookie.name() == TOKEN_HEADER {
let db = self.pool.get().unwrap();
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(),
}
}
};
};
}
Ok(res)
}
}
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 {
max_connections: 10_000,
..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(
Connection {
id: rng.gen::<usize>(),
account: None,
id,
ws: tx,
pool: pool.clone(),
stripe: stripe.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 uuid::Uuid;
use crossbeam_channel::{tick, Sender, Receiver};
// Db Commons
@ -24,13 +22,12 @@ use pg::{
pvp,
};
type Id = usize;
type Pair = (PvpRequest, PvpRequest);
pub enum GameEvent {
Upkeep,
Finish(Uuid),
// Finish(Uuid),
Match(Pair),
}
@ -76,10 +73,10 @@ impl Warden {
match msg {
GameEvent::Upkeep => self.on_upkeep(),
GameEvent::Match(pair) => self.on_match(pair),
GameEvent::Finish(id) => {
info!("game finished id={:?}", id);
Ok(())
},
// GameEvent::Finish(id) => {
// info!("game finished id={:?}", id);
// Ok(())
// },
}
}
@ -131,7 +128,7 @@ impl Warden {
fn fetch_games(mut tx: Transaction) -> Result<Transaction, Error> {
let games = games_need_upkeep(&mut tx)?;
for mut game in games {
for game in games {
let game = game.upkeep();
match game_update(&mut tx, &game) {
Ok(_) => (),
@ -146,7 +143,7 @@ fn fetch_games(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();
if let Some(game) = new_game {