Merge tag '1.11.0' into develop
1.11.0
This commit is contained in:
commit
02b25f92b7
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mnml-client",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
2684
client/assets/mnml.awards.svg
Normal file
2684
client/assets/mnml.awards.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 106 KiB |
2568
client/assets/mnml.logo.text.svg
Normal file
2568
client/assets/mnml.logo.text.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 95 KiB |
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
@white: #f5f5f5; // whitesmoke
|
||||
@purple: #9355b5; // 6lack - that far cover
|
||||
@yellow: #ffa100;
|
||||
@silver: #c0c0c0;
|
||||
@silver: #2c2c2c;
|
||||
|
||||
@black: black;
|
||||
@gray: #222;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,7 +94,23 @@ section {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.list {
|
||||
.panes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 2em;
|
||||
|
||||
figure {
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
font-size: 125%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
display: grid;
|
||||
@ -117,10 +119,6 @@ section {
|
||||
grid-gap: 1em;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-end;
|
||||
button {
|
||||
border-radius: 0.25em;
|
||||
// height: 3em;
|
||||
}
|
||||
|
||||
&.sub {
|
||||
grid-template-columns: 1fr;
|
||||
@ -158,75 +156,46 @@ section {
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.panes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
figure {
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
font-size: 125%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 0.5em;
|
||||
.options {
|
||||
grid-area: hdr;
|
||||
|
||||
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;
|
||||
.logo {
|
||||
flex: 0 1 10%;
|
||||
margin-right: 1em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
button {
|
||||
flex: 1;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.game-demo {
|
||||
.game {
|
||||
height: 25em;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.game-construct {
|
||||
flex: 1;
|
||||
}
|
||||
border-top: 0;
|
||||
border: 0.1em solid #222;
|
||||
&:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.intro {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -147,16 +147,11 @@
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
color: black;
|
||||
background: @silver;
|
||||
// overwrite the classes on white svg elements
|
||||
svg {
|
||||
stroke-width: 0.75em;
|
||||
}
|
||||
|
||||
.white {
|
||||
stroke: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mnml-client",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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
|
||||
|
||||
@ -152,11 +152,9 @@ class AccountStatus extends Component {
|
||||
|
||||
return (
|
||||
<section class='account top' onClick={tlClick}>
|
||||
<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">
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 />
|
||||
|
||||
|
||||
@ -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' > </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> </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);
|
||||
85
client/src/components/front.page.jsx
Normal file
85
client/src/components/front.page.jsx
Normal 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') }>
|
||||
|
||||
</button>
|
||||
<figcaption>Join the Community</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div class="logo"/>
|
||||
<hr />
|
||||
{list()}
|
||||
<hr />
|
||||
<Welcome />
|
||||
<hr />
|
||||
{news}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = addState(Play);
|
||||
@ -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">
|
||||
|
||||
@ -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 '';
|
||||
};
|
||||
|
||||
@ -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'>
|
||||
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navTo('play')}
|
||||
class={`login-btn ${nav === 'play' ? 'highlight' : ''}`}>
|
||||
|
||||
@ -23,6 +23,7 @@ const addState = connect(
|
||||
function Top(args) {
|
||||
const {
|
||||
nav,
|
||||
authenticated,
|
||||
} = args;
|
||||
|
||||
if (nav === 'account') return <AccountTop />;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
75
client/src/components/noise.jsx
Normal file
75
client/src/components/noise.jsx
Normal 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;
|
||||
@ -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()}
|
||||
|
||||
@ -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,35 +16,11 @@ 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'>
|
||||
|
||||
</button>
|
||||
<button
|
||||
class={`login-btn ${page === 'login' ? 'highlight' : ''}`}
|
||||
disabled={page === 'login'}
|
||||
@ -58,12 +33,6 @@ function Welcome() {
|
||||
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'}
|
||||
@ -71,11 +40,8 @@ function Welcome() {
|
||||
Help
|
||||
</button>
|
||||
</div>
|
||||
{form}
|
||||
</header>
|
||||
<div class="top">
|
||||
{main}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
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,17 +183,13 @@ 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,
|
||||
|
||||
@ -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'),
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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}
|
||||
const exitTutorial = tutorial === 8 ?
|
||||
<button
|
||||
class='focus'
|
||||
onClick={e => e.stopPropagation()}
|
||||
onMouseDown={exit}> {text} </button>;
|
||||
onMouseDown={exit}> Continue </button>
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div class='tutorial'>
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,12 +987,14 @@ impl Construct {
|
||||
if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] }
|
||||
|
||||
while let Some(ce) = self.effects.pop() {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
|
||||
382
core/src/game.rs
382
core/src/game.rs
@ -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
|
||||
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() {
|
||||
if cast.skill.cast_animation() {
|
||||
let event = self.cast(cast);
|
||||
self.add_resolution(&cast, &event);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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>>()
|
||||
}
|
||||
|
||||
pub fn upkeep(mut self) -> (Instance, Option<Game>) {
|
||||
// time out lobbies that have been open too long
|
||||
pub fn upkeep(mut self) -> (Instance, Option<Game>) {
|
||||
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() {
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mnml-ops",
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)),
|
||||
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(),
|
||||
}
|
||||
None => 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)));
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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 => (),
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)?;
|
||||
|
||||
@ -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)),
|
||||
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(a.id)).collect()),
|
||||
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(a.id)),
|
||||
RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(self.id)).collect()),
|
||||
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(self.id)),
|
||||
_ => msg,
|
||||
},
|
||||
None => 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();
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
160
server/src/user_anonymous.rs
Normal file
160
server/src/user_anonymous.rs
Normal 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"))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
265
server/src/user_authenticated.rs
Normal file
265
server/src/user_authenticated.rs
Normal 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"))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user