Merge branch 'release/1.3.0'

This commit is contained in:
ntr 2019-09-06 17:45:17 +10:00
commit 1b95e171a8
49 changed files with 968 additions and 645 deletions

View File

@ -1 +1 @@
1.2.0 1.3.0

View File

@ -10,9 +10,9 @@
* treats * treats
* constructs jiggle when clicked * constructs jiggle when clicked
* background colour changes depending on time of day * background colour changes depending on time of day
* hit animation wobble
* bug fixes * combat text scale + translate
* pvp 1st round doesn't resolve until warden timer completes * susbcriber gold name in instance
* bot game grind * bot game grind
* stress test * stress test
@ -22,11 +22,8 @@
* make our own toasts / msg pane * make our own toasts / msg pane
* send account_instances on players update * send account_instances on players update
* convert PlusPlus to ++ or rename
* add speed to descriptions * add speed to descriptions
* add components to description e.g. Strike (red circle red circle attack)
* clear skill (if currently targetted) * clear skill (if currently targetted)
* increase power to speed up early rounds
* only login / logout / register http * only login / logout / register http

View File

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

View File

@ -6,7 +6,6 @@ MNML_PATH=$(realpath "$DIR/../")
VERSION=$(<"$MNML_PATH/VERSION") VERSION=$(<"$MNML_PATH/VERSION")
SERVER_BIN_DIR="/usr/local/mnml/bin" SERVER_BIN_DIR="/usr/local/mnml/bin"
CLIENT_DIST_DIR="/var/lib/mnml/client" CLIENT_DIST_DIR="/var/lib/mnml/client"
CLIENT_PUBLIC_DIR="/var/lib/mnml/public/current" CLIENT_PUBLIC_DIR="/var/lib/mnml/public/current"

View File

@ -1,9 +1,6 @@
@import 'colours.less'; @import 'colours.less';
.account { .account {
margin-top: 2em;
grid-area: bottom;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 0 1em; grid-gap: 0 1em;

View File

@ -8,10 +8,17 @@ aside {
"timer controls"; "timer controls";
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
grid-template-rows: 1fr 1fr 1fr; grid-template-rows: 1fr;
grid-gap: 0.5em 0;
padding: 1em 1em 1em 0; padding-left: 1em;
.controls {
grid-area: controls;
display: grid;
grid-auto-rows: 1fr;
grid-gap: 0.5em 0;
}
// fix chrome being inconsistent // fix chrome being inconsistent
table { table {

View File

@ -1,9 +1,9 @@
@import 'colours.less'; @import 'colours.less';
.instance { .instance {
overflow-x: hidden; overflow: hidden;
display: grid; display: grid;
grid-template-columns: 2fr minmax(min-content, 1fr); grid-template-columns: 1fr minmax(min-content, 1fr);
grid-template-rows: min-content 1fr; grid-template-rows: min-content 1fr;
grid-template-areas: grid-template-areas:
@ -63,10 +63,16 @@
.instance .info figure { .instance .info figure {
display: inline; display: inline;
height: 0.5em; height: 0.5em;
svg {
margin-right: 0.5em;
}
} }
.instance .info figcaption { .instance .info figcaption {
font-size: 1em; font-size: 1em;
display: inline-block;
vertical-align: middle;
} }

View File

@ -4,16 +4,16 @@
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-rows: minmax(min-content, 2fr) 1fr; grid-template-rows: min-content 1fr 2fr;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-areas: grid-template-areas:
"hdr"
"top" "top"
"bottom"; "bottom";
.top { .top {
grid-area: top; grid-area: top;
padding: 0 0 0.5em 2em;
border-bottom: 0.1em solid #222; border-bottom: 0.1em solid #222;
box-sizing: border-box; box-sizing: border-box;
} }
@ -24,9 +24,10 @@
.team { .team {
display: grid; display: grid;
grid-area: top; grid-area: bottom;
grid-template-columns: repeat(auto-fill, minmax(min-content, 33%)); grid-template-columns: repeat(auto-fill, minmax(min-content, 33%));
max-height: 100%; max-height: 100%;
margin-top: 1em;
.team-select:not(:nth-child(3n)) { .team-select:not(:nth-child(3n)) {
margin-right: 0.5em; margin-right: 0.5em;
@ -61,9 +62,6 @@
} }
.inventory { .inventory {
margin-top: 2em;
grid-area: bottom;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
@ -71,6 +69,10 @@
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.news {
padding-right: 1em;
}
.list { .list {
letter-spacing: 0.25em; letter-spacing: 0.25em;
text-transform: uppercase; text-transform: uppercase;
@ -87,4 +89,21 @@
flex-flow: column; flex-flow: column;
} }
} }
.options {
grid-area: hdr;
button {
width: 25%;
border-top: 0;
border: 1px solid #222;
&:not(:last-child) {
border-right: 0;
}
&:last-child {
float: right;
}
}
}
} }

View File

@ -67,6 +67,7 @@ hr {
color: #222; color: #222;
margin: 1.5em 0; margin: 1.5em 0;
width: 100%; width: 100%;
border-top: 1px solid #222;
} }
figure { figure {
@ -84,16 +85,17 @@ dl {
#mnml { #mnml {
display: grid; display: grid;
grid-template-columns: minmax(min-content, 1fr) 8fr 1fr; grid-template-columns: 9fr 1fr;
grid-template-rows: min-content 1fr min-content; grid-template-rows: min-content min-content 1fr;
grid-template-areas: grid-template-areas:
"hd hd ctrl" "hdr ctrl"
"nav main ctrl" "main ctrl"
"nav main ctrl"; "main ctrl";
padding: 0.5em 1em;
} }
main { main {
padding: 1em;
grid-area: main; grid-area: main;
} }
@ -198,12 +200,6 @@ button[disabled] {
*/ */
.welcome { .welcome {
.highlight {
color: black;
background: @white;
border: 1px solid @white;
}
.login { .login {
width: 50%; width: 50%;
display: flex; display: flex;
@ -212,12 +208,8 @@ button[disabled] {
} }
.options { .options {
width: 50%;
display: flex; display: flex;
flex-flow: row; width: 50%;
button {
flex: 1;
}
} }
h2 { h2 {
@ -263,3 +255,47 @@ figure.gray {
.mobile-title { .mobile-title {
display: none; display: none;
} }
header {
.options {
font-size: 150%;
}
button {
height: 2em;
}
}
.options {
button {
&.highlight {
color: @white;
box-shadow: inset 0px 5px 0px 0px @white;
border: 0;
&:first-child {
border-left: 1px solid #444;
}
&:last-child {
border-right: 1px solid #444;
}
}
border: 1px solid #444;
flex: 1;
}
}
nav {
display: none;
}
ul {
margin-bottom: 1em;
list-style: inside;
}
li {
margin-bottom: 0.5em;
}

View File

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

View File

@ -1,4 +1,3 @@
const { removeTier } = require('./utils');
const { TIMES } = require('./constants'); const { TIMES } = require('./constants');
function none() { function none() {
@ -60,10 +59,8 @@ function getObjects(resolution, stages, game, account) {
? null ? null
: createSourceAnim(); : createSourceAnim();
const skill = removeTier(event.skill);
const animTarget = { const animTarget = {
skill, skill: event.skill,
constructId: targetting(), constructId: targetting(),
player: playerTeamIds.includes(resolution.target.id), player: playerTeamIds.includes(resolution.target.id),
direction, direction,

View File

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

View File

@ -151,19 +151,9 @@ class AccountStatus extends Component {
} }
return ( return (
<section class='account' onClick={tlClick}> <section class='account top' onClick={tlClick}>
<div> <div>
<h1>{account.name}</h1> {subInfo()}
<div class="list">
<figure>
<figcaption>spawn new construct</figcaption>
<button onClick={() => sendConstructSpawn()} type="submit">
¤50
</button>
</figure>
</div>
<button onClick={() => logout()}>Logout</button>
<button><a href={`mailto:humans@mnml.gg?subject=Account%20Support:%20${account.name}`}> support</a></button>
</div> </div>
<div> <div>
<label for="email">Email Settings:</label> <label for="email">Email Settings:</label>
@ -216,7 +206,16 @@ class AccountStatus extends Component {
</button> </button>
</div> </div>
<div> <div>
{subInfo()} <div class="list">
<figure>
<figcaption>spawn new construct</figcaption>
<button onClick={() => sendConstructSpawn()} type="submit">
¤50
</button>
</figure>
</div>
<button onClick={() => logout()}>Logout</button>
<button><a href={`mailto:humans@mnml.gg?subject=Account%20Support:%20${account.name}`}> support</a></button>
</div> </div>
</section> </section>
); );

View File

@ -40,6 +40,8 @@ const Triage = require('./anims/triage');
const TriageTick = require('./anims/triage.tick'); const TriageTick = require('./anims/triage.tick');
const actions = require('../actions'); const actions = require('../actions');
const { removeTier } = require('../utils');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
@ -63,12 +65,12 @@ class ConstructAnimation extends Component {
direction, direction,
constructId, constructId,
} = animTarget; } = animTarget;
const animSkill = removeTier(skill);
if (!constructId.includes(construct.id)) return false; if (!constructId.includes(construct.id)) return false;
// find target animation // find target animation
const chooseAnim = (skill) => { const chooseAnim = (animSkill) => {
switch (skill) { switch (animSkill) {
// Attack base // Attack base
case 'Attack': return <Attack direction={direction}/>; case 'Attack': return <Attack direction={direction}/>;
case 'Blast': return <Blast direction={direction}/>; case 'Blast': return <Blast direction={direction}/>;
@ -123,7 +125,7 @@ class ConstructAnimation extends Component {
}; };
}; };
const anim = chooseAnim(skill); const anim = chooseAnim(animSkill);
if (!anim) return false; if (!anim) return false;
return ( return (

View File

@ -0,0 +1,89 @@
const preact = require('preact');
const { connect } = require('preact-redux');
const actions = require('./../actions');
const { COLOURS } = require('./../utils');
const { ConstructAvatar } = require('./construct');
const addState = connect(
function receiveState(state) {
const { ws, constructs, teamPage, teamSelect } = state;
function sendConstructSpawn(name) {
return ws.sendMtxConstructSpawn(name);
}
return {
constructs,
teamPage,
teamSelect,
};
},
function receiveDispatch(dispatch) {
function setTeam(constructIds) {
dispatch(actions.setTeamSelect(constructIds));
}
return {
setTeam,
};
}
);
function Collection(args) {
const {
constructs,
teamPage,
teamSelect,
setTeam,
} = args;
if (!constructs) return <div></div>;
// redux limitation + suggested workaround
// so much for dumb components
function selectConstruct(id) {
// remove
const i = teamSelect.findIndex(sid => sid === id);
if (i > -1) {
teamSelect[i] = null;
return setTeam(teamSelect);
}
// window insert
const insert = teamSelect.findIndex(j => j === null);
if (insert === -1) return setTeam([id, null, null]);
teamSelect[insert] = id;
return setTeam(teamSelect);
}
console.log(constructs.length);
const dispConstructs = constructs.length >= ((teamPage + 1) * 6)
? constructs.slice(teamPage * 6, (teamPage + 1) * 6)
: constructs.slice(teamPage * 6, constructs.length);
const constructPanels = dispConstructs.map(construct => {
const colour = teamSelect.indexOf(construct.id);
const selected = colour > -1;
const borderColour = selected ? COLOURS[colour] : '#000000';
return (
<div
key={construct.id}
class="construct team-select"
style={ { 'border-color': borderColour || 'whitesmoke' } }
onClick={() => selectConstruct(construct.id)} >
<ConstructAvatar construct={construct} />
<h2>{construct.name}</h2>
</div>
);
});
return (
<section class="team">
{constructPanels}
</section>
);
}
module.exports = addState(Collection);

View File

@ -125,9 +125,11 @@ function Controls(args) {
return ( return (
<aside class="controls"> <aside class="controls">
{timer} {timer}
<div class="controls">
{scoreboard(game, opponent, true)} {scoreboard(game, opponent, true)}
{game.phase === 'Finish' ? quitBtn : readyBtn} {game.phase === 'Finish' ? quitBtn : readyBtn}
{scoreboard(game, player)} {scoreboard(game, player)}
</div>
</aside> </aside>
); );
} }

View File

@ -1,40 +1,96 @@
// eslint-disable-next-line
const preact = require('preact');
const { connect } = require('preact-redux'); const { connect } = require('preact-redux');
const preact = require('preact');
const { saw } = require('./shapes'); const actions = require('../actions');
const actions = require('./../actions');
function pingColour(ping) {
if (ping < 100) return 'forestgreen';
if (ping < 200) return 'yellow';
return 'red';
}
const addState = connect( const addState = connect(
({ account, ping, showNav }) => { function receiveState(state) {
return { account, ping }; const {
ws,
account,
nav,
} = state;
function sendInstanceState(instance) {
return ws.sendInstanceState(instance.id);
}
function sendAccountStates() {
ws.sendEmailState();
ws.sendSubscriptionState();
}
return {
account,
nav,
sendInstanceState,
sendAccountStates,
};
}, },
function receiveDispatch(dispatch) {
function setNav(place) {
dispatch(actions.setGame(null));
dispatch(actions.setInstance(null));
dispatch(actions.setCombiner([]));
dispatch(actions.setReclaiming(false));
dispatch(actions.setActiveSkill(null));
dispatch(actions.setActiveConstruct(null));
dispatch(actions.setInfo(null));
dispatch(actions.setItemEquip(null));
dispatch(actions.setItemUnequip([]));
dispatch(actions.setVboxHighlight([]));
return dispatch(actions.setNav(place));
}
return {
setNav,
};
}
); );
function renderHeader(args) { function Header(args) {
const { account, ping } = args; const {
account,
nav,
const accountStatus = account sendAccountStates,
? (<div class="header-status"> setNav,
<h1 class="header-username">{account.name}</h1> } = args;
{saw(pingColour(ping))}
<div class="ping-text">{ping}ms</div> if (!account) return false;
</div>)
: ''; function navTo(p) {
return setNav(p);
}
function accountClick() {
sendAccountStates();
navTo('account');
}
return ( return (
<header> <header>
<h1 class="header-title">mnml.gg</h1> <div class="options">
{accountStatus} <button
onClick={() => navTo('play')}
class={`login-btn ${nav === 'play' ? 'highlight' : ''}`}>
MNML
</button>
<button
onClick={() => navTo('shop')}
class={`login-btn ${nav === 'shop' ? 'highlight' : ''}`}>
Shop
</button>
<button
onClick={accountClick}
class={`login-btn ${nav === 'account' ? 'highlight' : ''}`}>
{account.name}
</button>
</div>
</header> </header>
); );
} }
module.exports = addState(Header);
module.exports = addState(renderHeader);

View File

@ -9,7 +9,6 @@ const shapes = require('./shapes');
function InfoComponent(args) { function InfoComponent(args) {
const { const {
itemInfo, itemInfo,
combiner,
player, player,
info, info,
} = args; } = args;
@ -18,7 +17,21 @@ function InfoComponent(args) {
// const { info } = args; // const { info } = args;
function Info() { function Info() {
if (!info) return false; if (!info) {
return (
<div>
<h2>VBOX phase</h2>
<p>double clicking items in the <b>VBOX</b> will purchase and move them to your <b>INVENTORY</b>.</p>
<p>
hover over an item to see its effects and combinations.<br />
combine a <b>SKILL</b> or <b>SPEC</b> with 2 <b>COLOURS</b> to create an item.<br />
combine 3 of the same item to upgrade it.<br />
click an item and then click a construct to equip that item to it.<br />
</p>
<p>click the <b>READY</b> button on the right to progress to the <b>GAME PHASE</b>.</p>
</div>
);
}
const fullInfo = itemInfo.items.find(i => i.item === info) || INFO[info]; const fullInfo = itemInfo.items.find(i => i.item === info) || INFO[info];
if (!fullInfo) return false; if (!fullInfo) return false;
const isSkill = fullInfo.skill; const isSkill = fullInfo.skill;
@ -28,16 +41,10 @@ function InfoComponent(args) {
const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/; const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/;
const infoDescription = reactStringReplace(fullInfo.description, regEx, match => shapes[match]()); const infoDescription = reactStringReplace(fullInfo.description, regEx, match => shapes[match]());
const itemSource = itemInfo.combos.filter(c => c.item === info);
const itemSourceInfo = itemSource.length
? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}`
: false;
const itemRegEx = /(Red|Blue|Green)/;
const itemSourceDescription = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]());
return ( return (
<div class="info-skill"> <div class="info-skill">
<h2>{fullInfo.item}</h2> <h2>{fullInfo.item}</h2>
<h3>{itemSourceDescription}</h3> <h3>SKILL</h3>
<div>{infoDescription}</div> <div>{infoDescription}</div>
</div> </div>
); );
@ -132,6 +139,7 @@ function InfoComponent(args) {
return ( return (
<div class="info-spec"> <div class="info-spec">
<h2>{info}</h2> <h2>{info}</h2>
<h3>SPEC</h3>
<div>{infoDescription}</div> <div>{infoDescription}</div>
<div class="thresholds"> <div class="thresholds">
{thresholds} {thresholds}
@ -150,30 +158,11 @@ function InfoComponent(args) {
function Combos() { function Combos() {
if (!player) return false; if (!player) return false;
// show recipe for what's in combiner
if (combiner.some(u => u !== null)) {
const filteredCombos = itemInfo.combos
.filter(combo => combiner.every(u => u === null
|| combo.components.includes(player.vbox.bound[u])));
if (filteredCombos.length > 6) return false;
return (
<table>
<tbody>
{filteredCombos.map((c, i) =>
<tr key={i} >
<td class="highlight" >{convertItem(c.item)}</td>
{c.components.map((u, j) => <td key={j}>{convertItem(u)}</td>)}
</tr>
)}
</tbody>
</table>
);
}
if (!info) return false; if (!info) return false;
const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info)); const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info));
if (vboxCombos.length > 6) return false; if (vboxCombos.length > 6) return false;
return ( return (
<table class="combos"> <table class="combos">
<tbody> <tbody>

View File

@ -98,7 +98,7 @@ function Construct(props) {
const skill = construct.skills[i]; const skill = construct.skills[i];
const s = skill const s = skill
? skill.skill ? skill.skill
: (<span class="gray">+</span>); : (<span class="gray">SKILL</span>);
function skillClick(e) { function skillClick(e) {
if (!skill) return false; if (!skill) return false;

View File

@ -93,11 +93,13 @@ function Controls(args) {
); );
return ( return (
<aside class="controls"> <aside>
{timer} {timer}
<div class="controls">
{scoreboard(instance, opponent, true)} {scoreboard(instance, opponent, true)}
<button class="ready" onClick={() => sendReady()}>Ready</button> <button class="ready" onClick={() => sendReady()}>Ready</button>
{scoreboard(instance, player)} {scoreboard(instance, player)}
</div>
</aside> </aside>
); );
} }

View File

@ -1,87 +0,0 @@
const { connect } = require('preact-redux');
const { Elements } = require('react-stripe-elements');
const preact = require('preact');
const toast = require('izitoast');
const actions = require('./../actions');
const StripeBtns = require('./stripe.buttons');
const addState = connect(
function receiveState(state) {
const {
ws,
account,
shop,
} = state;
function mtxBuy(mtx) {
return ws.sendMtxBuy(mtx.variant);
}
return {
account,
shop,
mtxBuy,
};
},
function receiveDispatch(dispatch) {
function setMtxActive(mtx) {
dispatch(actions.setConstructRename(null));
dispatch(actions.setMtxActive(mtx));
return true;
}
return {
setMtxActive,
};
}
);
function Inventory(args) {
const {
account,
shop,
setMtxActive,
mtxBuy,
} = args;
if (!shop) return false;
const useMtx = (item, i) => (
<figure key={i} onClick={() => setMtxActive(item)} >
<figcaption>{item}</figcaption>
<button disabled={account.balance === 0}>¤1</button>
</figure>
);
const availableMtx = (item, i) => (
<figure key={i} onClick={() => mtxBuy(item)} >
<figcaption>{item.variant}</figcaption>
<button disabled={account.balance < item.credits}>¤{item.credits}</button>
</figure>
);
return (
<div class="inventory">
<div>
<h1>Shop</h1>
<Elements>
<StripeBtns account={account} />
</Elements>
<div class='list'>
{shop.available.map(availableMtx)}
</div>
</div>
<div>
<h1 class="credits">¤ {account.balance}</h1>
<div class='list'>
{shop.owned.map(useMtx)}
</div>
</div>
</div>
);
}
module.exports = addState(Inventory);

View File

@ -0,0 +1,30 @@
const { connect } = require('preact-redux');
const preact = require('preact');
const actions = require('./../actions');
const Team = require('./team');
const Collection = require('./collection');
const addState = connect(
function receiveState(state) {
const {
nav,
} = state;
return {
nav,
};
},
);
function Bottom(args) {
const {
nav,
} = args;
if (nav === 'account') return <Collection />;
return <Team />;
}
module.exports = addState(Bottom);

View File

@ -5,14 +5,14 @@ const { connect } = require('preact-redux');
const Welcome = require('./welcome'); const Welcome = require('./welcome');
const Game = require('./game'); const Game = require('./game');
const Instance = require('./instance.component'); const Instance = require('./instance.component');
const Team = require('./team'); const Header = require('./header');
const Play = require('./play'); const Top = require('./main.top');
const Account = require('./account.page'); const Bottom = require('./main.bottom');
const addState = connect( const addState = connect(
state => { state => {
const { game, instance, account, nav, team, constructs } = state; const { game, instance, account, nav } = state;
return { game, instance, account, nav, team, constructs }; return { game, instance, account, nav };
} }
); );
@ -37,12 +37,13 @@ function Main(props) {
} }
if (nav === 'transition') return false; if (nav === 'transition') return false;
if (nav === 'play') return <Play />;
if (nav === 'team') return <Team />;
if (nav === 'account') return <Account />;
return ( return (
<Play /> <main class="menu">
<Header />
<Top />
<Bottom />
</main>
); );
} }

View File

@ -0,0 +1,34 @@
const { connect } = require('preact-redux');
const preact = require('preact');
const actions = require('./../actions');
const AccountTop = require('./account.top');
const Play = require('./play');
const Shop = require('./shop');
const addState = connect(
function receiveState(state) {
const {
nav,
} = state;
return {
nav,
};
},
);
function Top(args) {
const {
nav,
} = args;
if (nav === 'account') return <AccountTop />;
if (nav === 'play') return <Play />
if (nav === 'shop') return <Shop />
return false;
}
module.exports = addState(Top);

View File

@ -1,7 +1,6 @@
const preact = require('preact'); const preact = require('preact');
const { connect } = require('preact-redux'); const { connect } = require('preact-redux');
// const Header = require('./header');
const Main = require('./main'); const Main = require('./main');
const Nav = require('./nav'); const Nav = require('./nav');
const Controls = require('./controls'); const Controls = require('./controls');

View File

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

View File

@ -3,7 +3,14 @@ const { connect } = require('preact-redux');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { ws } = state; const {
ws,
instances,
} = state;
function sendInstanceState(id) {
ws.sendInstanceState(id);
}
function sendInstancePractice() { function sendInstancePractice() {
ws.sendInstancePractice(); ws.sendInstancePractice();
@ -14,6 +21,9 @@ const addState = connect(
} }
return { return {
instances,
sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
}; };
@ -22,13 +32,33 @@ const addState = connect(
function JoinButtons(args) { function JoinButtons(args) {
const { const {
instances,
sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
} = args; } = args;
if (instances.length) {
return ( return (
<aside class='play-ctrl'> <aside class='play-ctrl'>
<div class="timer-container"></div> <div class="timer-container"></div>
<div class="controls">
<button
class='pvp ready full'
onClick={() => sendInstanceState(instances[0].id)}
type="submit">
Rejoin
</button>
</div>
</aside>
);
}
return (
<aside class='play-ctrl'>
<div class="timer-container"></div>
<div class="controls">
<button <button
class='pvp ready' class='pvp ready'
onClick={() => sendInstanceQueue()} onClick={() => sendInstanceQueue()}
@ -39,8 +69,9 @@ function JoinButtons(args) {
class='practice ready' class='practice ready'
onClick={() => sendInstancePractice()} onClick={() => sendInstancePractice()}
type="submit"> type="submit">
Practice Learn
</button> </button>
</div>
</aside> </aside>
); );
} }

View File

@ -1,118 +1,128 @@
const { connect } = require('preact-redux'); // const { connect } = require('preact-redux');
const preact = require('preact'); const preact = require('preact');
const { connect } = require('preact-redux');
const { Elements } = require('react-stripe-elements');
const Header = require('./header');
const Team = require('./team');
const StripeBtns = require('./stripe.buttons');
const { stringSort } = require('./../utils');
const { ConstructAvatar } = require('./construct');
const actions = require('./../actions'); const actions = require('./../actions');
const Inventory = require('./inventory');
const idSort = stringSort('id'); const VERSION = process.env.npm_package_version;
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { const {
ws, ws,
constructs, account,
constructRename, shop,
team,
mtxActive,
} = state; } = state;
function sendInstancePractice() { function mtxBuy(mtx) {
return ws.sendInstancePractice(); return ws.sendMtxBuy(mtx.variant);
}
function sendConstructAvatarReroll(id) {
console.log('using', mtxActive, 'on', id);
return ws.sendMtxApply(id, mtxActive, '');
}
function sendConstructRename(id, name) {
ws.sendMtxApply(id, 'Rename', name);
} }
return { return {
constructs, account,
mtxActive, shop,
constructRename, mtxBuy,
team,
sendConstructRename,
sendInstancePractice,
sendConstructAvatarReroll,
}; };
}, },
function receiveDispatch(dispatch) { function receiveDispatch(dispatch) {
function setConstructRename(id) { function setMtxActive(mtx) {
dispatch(actions.setConstructRename(id)); dispatch(actions.setConstructRename(null));
dispatch(actions.setMtxActive(mtx));
return true;
} }
function clearMtxRename() { function setNav(place) {
dispatch(actions.setConstructRename(null)); return dispatch(actions.setNav(place));
dispatch(actions.setMtxActive(null));
} }
return { return {
clearMtxRename, setMtxActive,
setConstructRename, setNav,
}; };
} }
); );
function Play(args) { function Play(args) {
const { const {
team, account,
constructRename, shop,
clearMtxRename, mtxBuy,
setConstructRename,
sendConstructRename, setMtxActive,
mtxActive, setNav,
sendConstructAvatarReroll,
} = args; } = args;
const constructPanels = team if (!shop) return false;
.map(construct => {
const constructName = constructRename === construct.id
? <input id='renameInput' type="text" style="text-align: center" placeholder="enter a new name"></input>
: <h2>{construct.name}</h2>;
const confirm = constructRename === construct.id const useMtx = (item, i) => (
? <button onClick={() => sendConstructRename(construct.id, document.getElementById('renameInput').value)}> <figure key={i} onClick={() => setMtxActive(item)} >
Confirm <figcaption>{item}</figcaption>
</button> <button disabled={account.balance === 0}>¤1</button>
: false; </figure>
const cancel = constructRename === construct.id
? <button onClick={() => clearMtxRename()}>
Cancel
</button>
: false;
return (
<div
key={construct.id}
style={ mtxActive ? { cursor: 'pointer' } : {}}
onClick={() => {
if (!mtxActive) return false;
if (mtxActive === 'Rename') return setConstructRename(construct.id);
return sendConstructAvatarReroll(construct.id);
}}
class="construct">
<ConstructAvatar construct={construct} />
{constructName}
{confirm}
{cancel}
</div>
); );
});
const availableMtx = (item, i) => (
<figure key={i} onClick={() => mtxBuy(item)} >
<figcaption>{item.variant}</figcaption>
<button disabled={account.balance < item.credits}>¤{item.credits}</button>
</figure>
);
const subscription = account.subscribed
? <button
class="stripe-btn"
disabled>
Subscribed
</button>
: <button
onClick={() => setNav('shop')}
class="stripe-btn"
role="link">
Subscribe
</button>;
return ( return (
<main class="menu"> <div class="inventory top">
<div class="team top"> <div class="news">
{constructPanels} <h1>mnml v{VERSION}</h1>
<p>use the buttons on the right to join an instance.</p>
<p>
select <b>PVP</b> to play against other players.<br />
click <b>LEARN</b> to practice the game without time controls.
</p>
<p>
if you enjoy the game please support its development by <b>subscribing</b> or purchasing <b>credits</b>.<br />
glhf
</p>
<p>--ntr & mashy</p>
</div>
<div>
<h1 class="credits">¤ {account.balance}</h1>
<div class='list'>
{subscription}
<button
onClick={() => setNav('shop')}
class="stripe-btn"
role="link">
Get Credits
</button>
<div id="error-message"></div>
</div>
<div class='list'>
{shop.owned.map(useMtx)}
</div>
<div class='list'>
{shop.available.map(availableMtx)}
</div>
</div>
</div> </div>
<Inventory />
</main>
); );
} }

View File

@ -42,7 +42,7 @@ module.exports = {
None: () => None: () =>
<figure> <figure>
{diamond(['gray'])} {diamond(['gray'])}
<figcaption>&nbsp;</figcaption> <figcaption>SPEC</figcaption>
</figure>, </figure>,

View File

@ -0,0 +1,58 @@
// const { connect } = require('preact-redux');
const preact = require('preact');
const { connect } = require('preact-redux');
const { Elements } = require('react-stripe-elements');
const StripeBtns = require('./stripe.buttons');
const actions = require('./../actions');
const addState = connect(
function receiveState(state) {
const {
ws,
account,
} = state;
return {
account,
};
},
);
function Shop(args) {
const {
account,
} = args;
return (
<div class="inventory top">
<div class="news">
<h1>support the game</h1>
<p>
<b>credits</b> are in game currency that can be used to purchase:
<ul>
<li>img sets</li>
<li>construct renames</li>
<li>new constructs</li>
</ul>
</p>
<p>
<b>subscriptions</b> grant extra benefits:
<ul>
<li>additional credits</li>
<li>chat wheel (soon )</li>
<li>account icons (soon )</li>
</ul>
</p>
</div>
<div>
<h1 class="credits">¤ {account.balance}</h1>
<Elements>
<StripeBtns account={account} />
</Elements>
</div>
</div>
);
}
module.exports = addState(Shop);

View File

@ -6,8 +6,16 @@ function subPlan() {
return 'prod_FWSA8RoyMMV3st'; return 'prod_FWSA8RoyMMV3st';
} }
function bitsSku() { function bitsSku(d) {
if (window.location.host === 'mnml.gg') return 'sku_Fjdu7zOy3sLGc5'; if (window.location.host === 'mnml.gg') {
if (d === 50) return 'sku_Fl5tLCWogUsgus';
if (d === 20) return 'sku_Fl5qegnxYRv7Cy';
if (d === 10) return 'sku_Fl5qVosoDsUVgy';
if (d === 5) return 'sku_Fjdu7zOy3sLGc5';
// !!!!
return 'sku_Fjdu7zOy3sLGc5';
}
return 'sku_FjuNxONdWewjH2'; return 'sku_FjuNxONdWewjH2';
} }
@ -16,6 +24,7 @@ function BitsBtn(args) {
stripe, stripe,
account, account,
} = args; } = args;
function subscribeClick() { function subscribeClick() {
stripe.redirectToCheckout({ stripe.redirectToCheckout({
items: [{ plan: subPlan(), quantity: 1 }], items: [{ plan: subPlan(), quantity: 1 }],
@ -25,9 +34,9 @@ function BitsBtn(args) {
}); });
} }
function bitsClick() { function bitsClick(d) {
stripe.redirectToCheckout({ stripe.redirectToCheckout({
items: [{ sku: bitsSku(), quantity: 1 }], items: [{ sku: bitsSku(d), quantity: 1 }],
successUrl: window.location.origin, successUrl: window.location.origin,
cancelUrl: window.location.origin, cancelUrl: window.location.origin,
clientReferenceId: account.id, clientReferenceId: account.id,
@ -48,16 +57,30 @@ function BitsBtn(args) {
</button>; </button>;
return ( return (
<div>
<div class='list'> <div class='list'>
{subscription} {subscription}
<button </div>
onClick={bitsClick} <div class='list'>
class="stripe-btn" <figure onClick={() => bitsClick(5)} >
role="link"> <figcaption>$5 AUD</figcaption>
Get Credits <button class="stripe-btn">¤50</button>
</button> </figure>
<figure onClick={() => bitsClick(10)} >
<figcaption>$10 AUD</figcaption>
<button class="stripe-btn">¤110</button>
</figure>
<figure onClick={() => bitsClick(20)} >
<figcaption>$20 AUD</figcaption>
<button class="stripe-btn">¤250</button>
</figure>
<figure onClick={() => bitsClick(50)} >
<figcaption>$50 AUD</figcaption>
<button class="stripe-btn">¤660</button>
</figure>
<div id="error-message"></div> <div id="error-message"></div>
</div> </div>
</div>
); );
} }

View File

@ -52,7 +52,7 @@ function TeamCtrl(args) {
class='ready' class='ready'
disabled={teamSelect.some(c => !c)} disabled={teamSelect.some(c => !c)}
onClick={sendAccountSetTeam}> onClick={sendAccountSetTeam}>
Set Team Set
</button> </button>
<div class="team-page-ctrl"> <div class="team-page-ctrl">
<button onClick={() => setTeamPage(teamPage - 1)} disabled={disableDecrement}> &laquo; </button> <button onClick={() => setTeamPage(teamPage - 1)} disabled={disableDecrement}> &laquo; </button>

View File

@ -1,88 +1,112 @@
const preact = require('preact');
const { connect } = require('preact-redux'); const { connect } = require('preact-redux');
const preact = require('preact');
const actions = require('./../actions');
const { COLOURS } = require('./../utils');
const { ConstructAvatar } = require('./construct'); const { ConstructAvatar } = require('./construct');
const actions = require('./../actions');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { ws, constructs, teamPage, teamSelect } = state; const {
ws,
constructs,
constructRename,
team,
mtxActive,
} = state;
function sendConstructSpawn(name) { function sendInstancePractice() {
return ws.sendMtxConstructSpawn(name); return ws.sendInstancePractice();
}
function sendConstructAvatarReroll(id) {
console.log('using', mtxActive, 'on', id);
return ws.sendMtxApply(id, mtxActive, '');
}
function sendConstructRename(id, name) {
ws.sendMtxApply(id, 'Rename', name);
} }
return { return {
constructs, constructs,
teamPage, mtxActive,
teamSelect, constructRename,
team,
sendConstructRename,
sendInstancePractice,
sendConstructAvatarReroll,
}; };
}, },
function receiveDispatch(dispatch) { function receiveDispatch(dispatch) {
function setTeam(constructIds) { function setConstructRename(id) {
dispatch(actions.setTeamSelect(constructIds)); dispatch(actions.setConstructRename(id));
}
function clearMtxRename() {
dispatch(actions.setConstructRename(null));
dispatch(actions.setMtxActive(null));
} }
return { return {
setTeam, clearMtxRename,
setConstructRename,
}; };
} }
); );
function Team(args) { function Team(args) {
const { const {
constructs, team,
teamPage, constructRename,
teamSelect, clearMtxRename,
setTeam, setConstructRename,
sendConstructRename,
mtxActive,
sendConstructAvatarReroll,
} = args; } = args;
if (!constructs) return <div></div>; const constructPanels = team
.map(construct => {
const constructName = constructRename === construct.id
? <input id='renameInput' type="text" style="text-align: center" placeholder="enter a new name"></input>
: <h2>{construct.name}</h2>;
// redux limitation + suggested workaround const confirm = constructRename === construct.id
// so much for dumb components ? <button onClick={() => sendConstructRename(construct.id, document.getElementById('renameInput').value)}>
function selectConstruct(id) { Confirm
// remove </button>
const i = teamSelect.findIndex(sid => sid === id); : false;
if (i > -1) {
teamSelect[i] = null;
return setTeam(teamSelect);
}
// window insert const cancel = constructRename === construct.id
const insert = teamSelect.findIndex(j => j === null); ? <button onClick={() => clearMtxRename()}>
if (insert === -1) return setTeam([id, null, null]); Cancel
teamSelect[insert] = id; </button>
return setTeam(teamSelect); : false;
}
console.log(constructs.length);
const dispConstructs = constructs.length >= ((teamPage + 1) * 6)
? constructs.slice(teamPage * 6, (teamPage + 1) * 6)
: constructs.slice(teamPage * 6, constructs.length);
const constructPanels = dispConstructs.map(construct => {
const colour = teamSelect.indexOf(construct.id);
const selected = colour > -1;
const borderColour = selected ? COLOURS[colour] : '#000000';
return ( return (
<div <div
key={construct.id} key={construct.id}
class="construct team-select" style={ mtxActive ? { cursor: 'pointer' } : {}}
style={ { 'border-color': borderColour || 'whitesmoke' } } onClick={() => {
onClick={() => selectConstruct(construct.id)} > if (!mtxActive) return false;
if (mtxActive === 'Rename') return setConstructRename(construct.id);
return sendConstructAvatarReroll(construct.id);
}}
class="construct">
<ConstructAvatar construct={construct} /> <ConstructAvatar construct={construct} />
<h2>{construct.name}</h2> {constructName}
{confirm}
{cancel}
</div> </div>
); );
}); });
return ( return (
<section class="team top"> <div class="team">
{constructPanels} {constructPanels}
</section> </div>
); );
} }

View File

@ -14,7 +14,6 @@ const addState = connect(
player, player,
combiner, combiner,
reclaiming, reclaiming,
vboxHighlight,
vboxSelected, vboxSelected,
itemInfo, itemInfo,
itemUnequip, itemUnequip,
@ -49,7 +48,6 @@ const addState = connect(
sendVboxCombine, sendVboxCombine,
sendVboxDiscard, sendVboxDiscard,
sendVboxReclaim, sendVboxReclaim,
vboxHighlight,
vboxSelected, vboxSelected,
itemInfo, itemInfo,
itemUnequip, itemUnequip,
@ -70,10 +68,6 @@ const addState = connect(
return dispatch(actions.setInfo(item)); return dispatch(actions.setInfo(item));
} }
function setVboxHighlight(v) {
return dispatch(actions.setVboxHighlight(v));
}
function setVboxSelected(v) { function setVboxSelected(v) {
return dispatch(actions.setVboxSelected(v)); return dispatch(actions.setVboxSelected(v));
} }
@ -86,7 +80,6 @@ const addState = connect(
setCombiner, setCombiner,
setReclaiming, setReclaiming,
setInfo, setInfo,
setVboxHighlight,
setVboxSelected, setVboxSelected,
setItemEquip, setItemEquip,
}; };
@ -105,7 +98,6 @@ function Vbox(args) {
sendVboxCombine, sendVboxCombine,
sendVboxDiscard, sendVboxDiscard,
sendVboxReclaim, sendVboxReclaim,
// vboxHighlight,
setCombiner, setCombiner,
setInfo, setInfo,
@ -118,19 +110,12 @@ function Vbox(args) {
sendItemUnequip, sendItemUnequip,
setReclaiming, setReclaiming,
setVboxHighlight,
} = args; } = args;
if (!player) return false; if (!player) return false;
const { vbox } = player; const { vbox } = player;
const vboxSelecting = vboxSelected.length; const vboxSelecting = vboxSelected.length;
// function setHighlight(type) {
// if (type === 'skill') return setVboxHighlight(itemInfo.items.filter(v => v.skill).map(v => v.item));
// if (type === 'spec') return setVboxHighlight(itemInfo.items.filter(v => v.spec).map(v => v.item));
// return false;
// }
function combinerChange(newCombiner) { function combinerChange(newCombiner) {
setCombiner(newCombiner); setCombiner(newCombiner);
@ -140,20 +125,7 @@ function Vbox(args) {
setItemEquip(null); setItemEquip(null);
} }
if (newCombiner.every(c => c === null)) return setVboxHighlight([]);
const combinerValues = newCombiner.map(cv => player.vbox.bound[cv]).filter(cv => cv);
const filteredCombos = itemInfo.combos
.filter(combo => combinerValues.every(u => combo.components.includes(u)));
const comboValues = itemInfo.items.filter(v => {
if (!filteredCombos.some(c => c.components.includes(v.item))) return false;
if (!['Red', 'Green', 'Blue'].includes(v.item) && combinerValues.includes(v.item)) return false;
return true; return true;
});
return setVboxHighlight(comboValues.map(v => v.item));
} }
// //
@ -192,6 +164,7 @@ function Vbox(args) {
function onClick(e) { function onClick(e) {
e.stopPropagation(); e.stopPropagation();
setCombiner([]);
if (selected) return clearVboxSelected(); if (selected) return clearVboxSelected();
return setVboxSelected([group, index]); return setVboxSelected([group, index]);
} }
@ -203,7 +176,8 @@ function Vbox(args) {
<button <button
class={classes} class={classes}
onMouseOver={e => vboxHover(e, v)} onMouseOver={e => vboxHover(e, v)}
onClick={onClick} onMouseDown={onClick}
onClick={e => e.stopPropagation()}
onDblClick={onDblClick} > onDblClick={onDblClick} >
{shapes[v]()} {shapes[v]()}
</button> </button>
@ -213,7 +187,8 @@ function Vbox(args) {
return ( return (
<button <button
class={classes} class={classes}
onClick={onClick} onMouseDown={onClick}
onClick={e => e.stopPropagation()}
onDblClick={onDblClick} onDblClick={onDblClick}
onMouseOver={e => vboxHover(e, v)}> onMouseOver={e => vboxHover(e, v)}>
{v} {v}
@ -225,7 +200,8 @@ function Vbox(args) {
function vboxElement() { function vboxElement() {
return ( return (
<div class='vbox-vbox' <div class='vbox-vbox'
onClick={() => setReclaiming(false)} onMouseDown={() => setReclaiming(false)}
onClick={e => e.stopPropagation()}
onMouseOver={e => hoverInfo(e, 'vbox')}> onMouseOver={e => hoverInfo(e, 'vbox')}>
<div class="vbox-hdr"> <div class="vbox-hdr">
<h3 onTouchStart={e => e.target.scrollIntoView(true)}>VBOX</h3> <h3 onTouchStart={e => e.target.scrollIntoView(true)}>VBOX</h3>
@ -241,7 +217,8 @@ function Vbox(args) {
<button <button
class='vbox-btn' class='vbox-btn'
onMouseOver={e => hoverInfo(e, 'refill')} onMouseOver={e => hoverInfo(e, 'refill')}
onClick={() => sendVboxDiscard()}> onClick={e => e.stopPropagation()}
onMouseDown={() => sendVboxDiscard()}>
refill - 2b refill - 2b
</button> </button>
</div> </div>
@ -251,32 +228,6 @@ function Vbox(args) {
// //
// INVENTORY // INVENTORY
// //
// const boundTds = range(0, 9).map(i => {
// const value = vbox.bound[i];
// if (combiner.indexOf(i) > -1) {
// return (
// <td
// key={i}>
// &nbsp;
// </td>
// );
// }
// const highlighted = value && vboxHighlight.includes(value);
// return (
// <td
// key={i}
// class={`${highlighted ? 'highlight' : ''}`}
// onMouseOver={(e) => vboxHover(e, value)}
// onClick={e => boundClick(e, i, highlighted) }>
// {convertItem(value)}
// </td>
// );
// });
function reclaimClick(e) { function reclaimClick(e) {
e.stopPropagation(); e.stopPropagation();
return setReclaiming(!reclaiming); return setReclaiming(!reclaiming);
@ -292,6 +243,7 @@ function Vbox(args) {
} }
function onClick(e) { function onClick(e) {
if (vboxSelecting) clearVboxSelected();
if (reclaiming) return sendVboxReclaim(i); if (reclaiming) return sendVboxReclaim(i);
const combinerIndex = combiner.indexOf(i); const combinerIndex = combiner.indexOf(i);
@ -310,7 +262,8 @@ function Vbox(args) {
<button <button
class={classes} class={classes}
onMouseOver={e => vboxHover(e, v)} onMouseOver={e => vboxHover(e, v)}
onClick={onClick}> onClick={e => e.stopPropagation()}
onMouseDown={onClick}>
{shapes[v]()} {shapes[v]()}
</button> </button>
); );
@ -319,7 +272,8 @@ function Vbox(args) {
return ( return (
<button <button
class={classes} class={classes}
onClick={onClick} onMouseDown={onClick}
onClick={e => e.stopPropagation()}
onMouseOver={e => vboxHover(e, v)}> onMouseOver={e => vboxHover(e, v)}>
{v} {v}
</button> </button>
@ -346,7 +300,8 @@ function Vbox(args) {
class='vbox-btn' class='vbox-btn'
disabled={combiner.length !== 3} disabled={combiner.length !== 3}
onMouseOver={e => hoverInfo(e, 'refine')} onMouseOver={e => hoverInfo(e, 'refine')}
onClick={() => sendVboxCombine()}> onClick={e => e.stopPropagation()}
onMouseDown={() => sendVboxCombine()}>
{text} {text}
</button> </button>
); );
@ -363,7 +318,8 @@ function Vbox(args) {
return ( return (
<div class={inventoryClass} <div class={inventoryClass}
onClick={inventoryClick} onMouseDown={inventoryClick}
onClick={e => e.stopPropagation()}
style={vboxSelecting || itemUnequip.length ? { cursor: 'pointer' } : null} style={vboxSelecting || itemUnequip.length ? { cursor: 'pointer' } : null}
onMouseOver={e => hoverInfo(e, 'inventory')}> onMouseOver={e => hoverInfo(e, 'inventory')}>
<div class="vbox-hdr"> <div class="vbox-hdr">
@ -371,7 +327,8 @@ function Vbox(args) {
<button <button
class='vbox-btn reclaim' class='vbox-btn reclaim'
onMouseOver={e => hoverInfo(e, 'reclaim')} onMouseOver={e => hoverInfo(e, 'reclaim')}
onClick={reclaimClick}> onClick={e => e.stopPropagation()}
onMouseDown={reclaimClick}>
reclaim reclaim
</button> </button>
</div> </div>

View File

@ -3,7 +3,7 @@ const eachSeries = require('async/eachSeries');
const actions = require('./actions'); const actions = require('./actions');
const { TIMES } = require('./constants'); const { TIMES } = require('./constants');
const animations = require('./animations.utils'); const animations = require('./animations.utils');
const { infoToast, errorToast } = require('./utils'); const { infoToast, errorToast, removeTier } = require('./utils');
function registerEvents(store) { function registerEvents(store) {
function notify(msg) { function notify(msg) {
@ -69,13 +69,12 @@ function registerEvents(store) {
const timeout = animations.getTime(sequence); const timeout = animations.getTime(sequence);
const anims = animations.getObjects(r, sequence, game, account); const anims = animations.getObjects(r, sequence, game, account);
const text = animations.getText(r, sequence); const text = animations.getText(r, sequence);
store.dispatch(actions.setAnimFocus(animations.getFocusTargets(r, game))); store.dispatch(actions.setAnimFocus(animations.getFocusTargets(r, game)));
if (sequence.includes('START_SKILL')) store.dispatch(actions.setAnimSource(anims.animSource)); if (sequence.includes('START_SKILL')) store.dispatch(actions.setAnimSource(anims.animSource));
if (sequence.includes('END_SKILL')) { if (sequence.includes('END_SKILL')) {
store.dispatch(actions.setAnimTarget(anims.animTarget)); store.dispatch(actions.setAnimTarget(anims.animTarget));
if (!['Banish', 'Invert'].includes(anims.animTarget.skill)) store.dispatch(actions.setAnimCb(cb)); if (!['Banish', 'Invert'].includes(removeTier(anims.animTarget.skill))) store.dispatch(actions.setAnimCb(cb));
} }
if (sequence.includes('POST_SKILL')) { if (sequence.includes('POST_SKILL')) {
// timeout to prevent text classes from being added too soon // timeout to prevent text classes from being added too soon
@ -90,7 +89,7 @@ function registerEvents(store) {
store.dispatch(actions.setAnimText(null)); store.dispatch(actions.setAnimText(null));
store.dispatch(actions.setAnimFocus([])); store.dispatch(actions.setAnimFocus([]));
if (!sequence.includes('END_SKILL') if (!sequence.includes('END_SKILL')
|| ['Banish', 'Invert'].includes(anims.animTarget.skill)) return cb(); || ['Banish', 'Invert'].includes(removeTier(anims.animTarget.skill))) return cb();
return true; return true;
}, timeout); }, timeout);
}, err => { }, err => {

View File

@ -50,7 +50,6 @@ module.exports = {
teamPage: createReducer(0, 'SET_TEAM_PAGE'), teamPage: createReducer(0, 'SET_TEAM_PAGE'),
teamSelect: createReducer([null, null, null], 'SET_TEAM_SELECT'), teamSelect: createReducer([null, null, null], 'SET_TEAM_SELECT'),
vboxHighlight: createReducer([], 'SET_VBOX_HIGHLIGHT'),
vboxSelected: createReducer([], 'SET_VBOX_SELECTED'), vboxSelected: createReducer([], 'SET_VBOX_SELECTED'),
ws: createReducer(null, 'SET_WS'), ws: createReducer(null, 'SET_WS'),

View File

@ -200,7 +200,7 @@ function createSocket(events) {
let pongTimeout; let pongTimeout;
function onPong() { function onPong() {
events.setPing(Date.now() - ping); events.setPing(Date.now() - ping);
pongTimeout = setTimeout(sendPing, 1000); // pongTimeout = setTimeout(sendPing, 1000);
} }
// ------------- // -------------

View File

@ -101,7 +101,7 @@
## urls will be written to each interval. ## urls will be written to each interval.
# urls = ["unix:///var/run/influxdb.sock"] # urls = ["unix:///var/run/influxdb.sock"]
# urls = ["udp://127.0.0.1:8089"] # urls = ["udp://127.0.0.1:8089"]
urls = ["http://mnml-prod-elk:8086"] urls = ["http://mnml-prod-mn:8086"]
## The target database for metrics; will be created as needed. ## The target database for metrics; will be created as needed.
## For UDP url endpoint database needs to be configured on server side. ## For UDP url endpoint database needs to be configured on server side.

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mnml" name = "mnml"
version = "1.2.0" version = "1.3.0"
authors = ["ntr <ntr@smokestack.io>"] authors = ["ntr <ntr@smokestack.io>"]
[dependencies] [dependencies]

View File

@ -10,7 +10,7 @@ use postgres::transaction::Transaction;
use http::MnmlHttpError; use http::MnmlHttpError;
use names::{name as generate_name}; use names::{name as generate_name};
use construct::{Construct, construct_recover, construct_spawn}; use construct::{Construct, ConstructSkeleton, construct_spawn};
use instance::{Instance, instance_delete}; use instance::{Instance, instance_delete};
use mtx::{Mtx, FREE_MTX}; use mtx::{Mtx, FREE_MTX};
use pg::Db; use pg::Db;
@ -350,23 +350,20 @@ pub fn constructs(tx: &mut Transaction, account: &Account) -> Result<Vec<Constru
let result = tx let result = tx
.query(query, &[&account.id])?; .query(query, &[&account.id])?;
let constructs: Result<Vec<Construct>, _> = result.iter() let mut constructs = result.iter()
.map(|row| { .filter_map(|row| {
let construct_bytes: Vec<u8> = row.get(0); let construct_bytes: Vec<u8> = row.get(0);
match from_slice::<Construct>(&construct_bytes) { match from_slice::<ConstructSkeleton>(&construct_bytes) {
Ok(c) => Ok(c), Ok(s) => Some(s),
Err(_e) => construct_recover(construct_bytes, tx), Err(e) => {
warn!("{:?}", e);
None
},
} }
}) })
.collect(); .map(|sk| Construct::from_skeleton(&sk))
.collect::<Vec<Construct>>();
// catch any errors
if constructs.is_err() {
warn!("{:?}", constructs);
return Err(err_msg("could not deserialise a construct"));
}
let mut constructs = constructs.unwrap();
constructs.sort_by_key(|c| c.id); constructs.sort_by_key(|c| c.id);
return Ok(constructs); return Ok(constructs);
} }
@ -382,23 +379,19 @@ pub fn team(tx: &mut Transaction, account: &Account) -> Result<Vec<Construct>, E
let result = tx let result = tx
.query(query, &[&account.id])?; .query(query, &[&account.id])?;
let constructs: Result<Vec<Construct>, _> = result.iter() let mut constructs = result.iter()
.map(|row| { .filter_map(|row| {
let construct_bytes: Vec<u8> = row.get(0); let construct_bytes: Vec<u8> = row.get(0);
match from_slice::<Construct>(&construct_bytes) { match from_slice::<ConstructSkeleton>(&construct_bytes) {
Ok(c) => Ok(c), Ok(s) => Some(s),
Err(_e) => construct_recover(construct_bytes, tx), Err(e) => {
warn!("{:?}", e);
None
},
} }
}) })
.collect(); .map(|sk| Construct::from_skeleton(&sk))
.collect::<Vec<Construct>>();
// catch any errors
if constructs.is_err() {
warn!("{:?}", constructs);
return Err(err_msg("could not deserialise a construct"));
}
let mut constructs = constructs.unwrap();
if constructs.len() != 3 { if constructs.len() != 3 {
return Err(format_err!("team not size 3 account={:?}", account)); return Err(format_err!("team not size 3 account={:?}", account));

View File

@ -187,9 +187,10 @@ impl ConstructStat {
} }
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct ConstructRecover { pub struct ConstructSkeleton {
pub id: Uuid, pub id: Uuid,
pub account: Uuid, pub account: Uuid,
pub img: Uuid,
pub name: String, pub name: String,
} }
@ -220,13 +221,13 @@ impl Construct {
id, id,
account: id, account: id,
img: Uuid::new_v4(), img: Uuid::new_v4(),
red_power: ConstructStat { base: 256, value: 256, max: 256, stat: Stat::RedPower }, red_power: ConstructStat { base: 320, value: 320, max: 320, stat: Stat::RedPower },
red_life: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::RedLife }, red_life: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::RedLife },
blue_power: ConstructStat { base: 256, value: 256, max: 256, stat: Stat::BluePower }, blue_power: ConstructStat { base: 320, value: 320, max: 320, stat: Stat::BluePower },
blue_life: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::BlueLife }, blue_life: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::BlueLife },
green_power: ConstructStat { base: 256, value: 256, max: 256, stat: Stat::GreenPower }, green_power: ConstructStat { base: 320, value: 300, max: 300, stat: Stat::GreenPower },
green_life: ConstructStat { base: 1024, value: 1024, max: 1024, stat: Stat::GreenLife }, green_life: ConstructStat { base: 950, value: 950, max: 950, stat: Stat::GreenLife },
speed: ConstructStat { base: 128, value: 128, max: 128, stat: Stat::Speed }, speed: ConstructStat { base: 100, value: 100, max: 100, stat: Stat::Speed },
// evasion: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::Evasion }, // evasion: ConstructStat { base: 0, value: 0, max: 0, stat: Stat::Evasion },
skills: vec![], skills: vec![],
effects: vec![], effects: vec![],
@ -236,6 +237,27 @@ impl Construct {
}; };
} }
pub fn from_skeleton(skeleton: &ConstructSkeleton) -> Construct {
return Construct {
id: skeleton.id,
account: skeleton.id,
img: skeleton.img,
name: skeleton.name.clone(),
.. Construct::new()
};
}
pub fn to_skeleton(&self) -> ConstructSkeleton {
ConstructSkeleton {
id: self.id,
account: self.id,
img: self.img,
name: self.name.clone(),
}
}
pub fn named(mut self, name: &String) -> Construct { pub fn named(mut self, name: &String) -> Construct {
self.name = name.clone(); self.name = name.clone();
self self
@ -827,10 +849,9 @@ pub fn construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result
let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?; let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?;
let construct_bytes: Vec<u8> = result.get(0); let construct_bytes: Vec<u8> = result.get(0);
let construct = from_slice::<Construct>(&construct_bytes) let skeleton = from_slice::<ConstructSkeleton>(&construct_bytes)?;
.or_else(|_| construct_recover(construct_bytes, tx))?;
return Ok(construct); return Ok(Construct::from_skeleton(&skeleton));
} }
pub fn construct_select(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> { pub fn construct_select(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> {
@ -847,10 +868,9 @@ pub fn construct_select(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Res
let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?; let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?;
let construct_bytes: Vec<u8> = result.get(0); let construct_bytes: Vec<u8> = result.get(0);
let construct = from_slice::<Construct>(&construct_bytes) let skeleton = from_slice::<ConstructSkeleton>(&construct_bytes)?;
.or_else(|_| construct_recover(construct_bytes, tx))?;
return Ok(construct); return Ok(Construct::from_skeleton(&skeleton));
} }
pub fn construct_spawn(tx: &mut Transaction, account: Uuid, name: String, team: bool) -> Result<Construct, Error> { pub fn construct_spawn(tx: &mut Transaction, account: Uuid, name: String, team: bool) -> Result<Construct, Error> {
@ -878,7 +898,7 @@ pub fn construct_spawn(tx: &mut Transaction, account: Uuid, name: String, team:
} }
pub fn construct_write(tx: &mut Transaction, construct: Construct) -> Result<Construct, Error> { pub fn construct_write(tx: &mut Transaction, construct: Construct) -> Result<Construct, Error> {
let construct_bytes = to_vec(&construct)?; let construct_bytes = to_vec(&construct.to_skeleton())?;
let query = " let query = "
UPDATE constructs UPDATE constructs
@ -897,20 +917,6 @@ pub fn construct_write(tx: &mut Transaction, construct: Construct) -> Result<Con
return Ok(construct); return Ok(construct);
} }
pub fn construct_recover(construct_bytes: Vec<u8>, tx: &mut Transaction) -> Result<Construct, Error> {
let c = from_slice::<ConstructRecover>(&construct_bytes)?;
let mut construct = Construct::new()
.named(&c.name)
.set_account(c.account);
construct.id = c.id;
info!("recovered construct {:?}", c.name);
return construct_write(tx, construct);
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use construct::*; use construct::*;

View File

@ -132,12 +132,12 @@ impl Events {
}, },
Event::Subscribe(id, obj) => { Event::Subscribe(id, obj) => {
info!("subscribe id={:?} object={:?}", id, obj); trace!("subscribe id={:?} object={:?}", id, obj);
match self.clients.get_mut(&id) { match self.clients.get_mut(&id) {
Some(client) => { Some(client) => {
client.subs.insert(obj); client.subs.insert(obj);
info!("client={:?} subscriptions={:?}", id, client.subs.len()); trace!("client={:?} subscriptions={:?}", id, client.subs.len());
Ok(()) Ok(())
}, },
None => return Err(format_err!("unknown client {:?}", id)) None => return Err(format_err!("unknown client {:?}", id))
@ -145,12 +145,12 @@ impl Events {
}, },
Event::Unsubscribe(id, obj) => { Event::Unsubscribe(id, obj) => {
info!("unsubscribe id={:?} object={:?}", id, obj); trace!("unsubscribe id={:?} object={:?}", id, obj);
match self.clients.get_mut(&id) { match self.clients.get_mut(&id) {
Some(mut client) => { Some(mut client) => {
client.subs.remove(&obj); client.subs.remove(&obj);
info!("unsubscribe subscriptions removed={:?}", client.subs.len()); trace!("unsubscribe subscriptions removed={:?}", client.subs.len());
Ok(()) Ok(())
}, },
None => return Err(format_err!("unknown client {:?}", id)) None => return Err(format_err!("unknown client {:?}", id))
@ -158,7 +158,7 @@ impl Events {
}, },
Event::Push(id, msg) => { Event::Push(id, msg) => {
info!("push id={:?}", id); trace!("push id={:?}", id);
let mut subs = 0; let mut subs = 0;
let mut dead = vec![]; let mut dead = vec![];
@ -177,11 +177,11 @@ impl Events {
} }
if !dead.is_empty() { if !dead.is_empty() {
info!("dead connections={:?}", dead.len()); trace!("dead connections={:?}", dead.len());
dead.iter().for_each(|id| self.remove_client(*id)); dead.iter().for_each(|id| self.remove_client(*id));
} }
info!("push subscribers={:?}", subs); trace!("push subscribers={:?}", subs);
Ok(()) Ok(())
}, },

View File

@ -1152,7 +1152,8 @@ mod tests {
// should not be stunned because of counter // should not be stunned because of counter
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].is_stunned() == false); assert!(game.player_by_id(x_player.id).unwrap().constructs[0].is_stunned() == false);
// riposte // riposte
assert_eq!(game.player_by_id(y_player.id).unwrap().constructs[0].green_life(), (1024 - x_construct.red_power().pct(Skill::CounterAttack.multiplier()))); assert_eq!(game.player_by_id(y_player.id).unwrap().constructs[0].green_life(), (
y_construct.green_life() - x_construct.red_power().pct(Skill::CounterAttack.multiplier())));
} }
#[test] #[test]
@ -1235,7 +1236,7 @@ mod tests {
assert_eq!(target.id, y_construct.id); assert_eq!(target.id, y_construct.id);
match event { match event {
Event::Damage { amount, skill: _, mitigation: _, colour: _} => Event::Damage { amount, skill: _, mitigation: _, colour: _} =>
assert_eq!(amount, 256.pct(Skill::Attack.multiplier()) >> 1), assert_eq!(amount, x_construct.red_power().pct(Skill::Attack.multiplier()) >> 1),
_ => panic!("not damage link"), _ => panic!("not damage link"),
}; };
} }

View File

@ -53,16 +53,15 @@ pub fn invader_write(id: Uuid) -> Result<Uuid, Error> {
colours.push("none".to_string()); colours.push("none".to_string());
// add up to 100 for % // add up to 100 for %
// [3] is the transparent colour
let weights = [ let weights = [
5, 5,
5, 5,
65, 65,
25, 25, // the transparent colour
]; ];
let colour_dist = WeightedIndex::new(&weights)?; let colour_dist = WeightedIndex::new(&weights)?;
write!(&mut svg, "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' viewBox='-250 -250 1000 1000' width='500' height='500'><g>")?; write!(&mut svg, "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' viewBox='-250 -250 1000 1000' width='375' height='375'><g>")?;
for i in 0..50 { for i in 0..50 {
let x = (i % 5) * 50; let x = (i % 5) * 50;
let y = (i / 5) * 50; let y = (i / 5) * 50;

View File

@ -76,7 +76,7 @@ impl TimeControl {
pub fn lobby_timeout(&self) -> DateTime<Utc> { pub fn lobby_timeout(&self) -> DateTime<Utc> {
Utc::now() Utc::now()
.checked_add_signed(Duration::minutes(5)) .checked_add_signed(Duration::seconds(30))
.expect("could not set phase end") .expect("could not set phase end")
} }
@ -366,7 +366,7 @@ impl Instance {
} }
fn current_game_id(&self) -> Option<Uuid> { fn current_game_id(&self) -> Option<Uuid> {
if self.phase == InstancePhase::Lobby { if self.phase != InstancePhase::InProgress {
return None; return None;
} }

View File

@ -497,152 +497,235 @@ pub enum Skill {
// Evade, // actively evade // Evade, // actively evade
// Nightmare, // Nightmare,
// Sleep, // Sleep,
Amplify, Amplify,
#[serde(rename = "Amplify+")]
AmplifyPlus, AmplifyPlus,
#[serde(rename = "Amplify++")]
AmplifyPlusPlus, AmplifyPlusPlus,
Absorb,
#[serde(rename = "Absorb+")]
AbsorbPlus,
#[serde(rename = "Absorb++")]
AbsorbPlusPlus,
Banish, Banish,
#[serde(rename = "Banish+")]
BanishPlus, BanishPlus,
#[serde(rename = "Banish++")]
BanishPlusPlus, BanishPlusPlus,
Bash, Bash,
#[serde(rename = "Bash+")]
BashPlus, BashPlus,
#[serde(rename = "Bash++")]
BashPlusPlus, BashPlusPlus,
Blast, Blast,
#[serde(rename = "Blast+")]
BlastPlus, BlastPlus,
#[serde(rename = "Blast++")]
BlastPlusPlus, BlastPlusPlus,
Chaos, Chaos,
#[serde(rename = "Chaos+")]
ChaosPlus, ChaosPlus,
#[serde(rename = "Chaos++")]
ChaosPlusPlus, ChaosPlusPlus,
Sustain, Sustain,
#[serde(rename = "Sustain+")]
SustainPlus, SustainPlus,
#[serde(rename = "Sustain++")]
SustainPlusPlus, SustainPlusPlus,
Electrify, Electrify,
#[serde(rename = "Electrify+")]
ElectrifyPlus, ElectrifyPlus,
#[serde(rename = "Electrify++")]
ElectrifyPlusPlus, ElectrifyPlusPlus,
Electrocute,
ElectrocutePlus,
ElectrocutePlusPlus,
ElectrocuteTick,
ElectrocuteTickPlus,
ElectrocuteTickPlusPlus,
Curse, Curse,
#[serde(rename = "Curse+")]
CursePlus, CursePlus,
#[serde(rename = "Curse++")]
CursePlusPlus, CursePlusPlus,
Decay, // dot Decay,
#[serde(rename = "Decay+")]
DecayPlus, DecayPlus,
#[serde(rename = "Decay++")]
DecayPlusPlus, DecayPlusPlus,
DecayTick, // dot
DecayTickPlus,
DecayTickPlusPlus,
Haste,
HastePlus,
HastePlusPlus,
HasteStrike,
Heal,
HealPlus,
HealPlusPlus,
Hex, Hex,
#[serde(rename = "Hex+")]
HexPlus, HexPlus,
#[serde(rename = "Hex++")]
HexPlusPlus, HexPlusPlus,
Absorption, Haste,
AbsorptionPlus, #[serde(rename = "Haste+")]
AbsorptionPlusPlus, HastePlus,
Absorb, #[serde(rename = "Haste++")]
AbsorbPlus, HastePlusPlus,
AbsorbPlusPlus,
HybridBlast, Heal,
#[serde(rename = "Heal+")]
HealPlus,
#[serde(rename = "Heal++")]
HealPlusPlus,
Hybrid, Hybrid,
#[serde(rename = "Hybrid+")]
HybridPlus, HybridPlus,
#[serde(rename = "Hybrid++")]
HybridPlusPlus, HybridPlusPlus,
Invert, Invert,
#[serde(rename = "Invert+")]
InvertPlus, InvertPlus,
#[serde(rename = "Invert++")]
InvertPlusPlus, InvertPlusPlus,
Counter, // avoid all damage Counter,
#[serde(rename = "Counter+")]
CounterPlus, CounterPlus,
#[serde(rename = "Counter++")]
CounterPlusPlus, CounterPlusPlus,
Purge, Purge,
#[serde(rename = "Purge+")]
PurgePlus, PurgePlus,
#[serde(rename = "Purge++")]
PurgePlusPlus, PurgePlusPlus,
Purify, Purify,
#[serde(rename = "Purify+")]
PurifyPlus, PurifyPlus,
#[serde(rename = "Purify++")]
PurifyPlusPlus, PurifyPlusPlus,
Recharge,
RechargePlus,
RechargePlusPlus,
Reflect, Reflect,
#[serde(rename = "Reflect+")]
ReflectPlus, ReflectPlus,
#[serde(rename = "Reflect++")]
ReflectPlusPlus, ReflectPlusPlus,
CounterAttack, Recharge,
CounterAttackPlus, #[serde(rename = "Recharge+")]
CounterAttackPlusPlus, RechargePlus,
#[serde(rename = "Recharge++")]
RechargePlusPlus,
Ruin, Ruin,
#[serde(rename = "Ruin+")]
RuinPlus, RuinPlus,
#[serde(rename = "Ruin++")]
RuinPlusPlus, RuinPlusPlus,
Link, Link,
#[serde(rename = "Link+")]
LinkPlus, LinkPlus,
#[serde(rename = "Link++")]
LinkPlusPlus, LinkPlusPlus,
Silence, Silence,
#[serde(rename = "Silence+")]
SilencePlus, SilencePlus,
#[serde(rename = "Silence++")]
SilencePlusPlus, SilencePlusPlus,
Siphon,
SiphonPlus,
SiphonPlusPlus,
SiphonTick,
SiphonTickPlus,
SiphonTickPlusPlus,
Slay, Slay,
#[serde(rename = "Slay+")]
SlayPlus, SlayPlus,
#[serde(rename = "Slay++")]
SlayPlusPlus, SlayPlusPlus,
Sleep, Sleep,
#[serde(rename = "Sleep+")]
SleepPlus, SleepPlus,
#[serde(rename = "Sleep++")]
SleepPlusPlus, SleepPlusPlus,
Restrict, Restrict,
#[serde(rename = "Restrict+")]
RestrictPlus, RestrictPlus,
#[serde(rename = "Restrict++")]
RestrictPlusPlus, RestrictPlusPlus,
Strike, Strike,
#[serde(rename = "Strike+")]
StrikePlus, StrikePlus,
#[serde(rename = "Strike++")]
StrikePlusPlus, StrikePlusPlus,
Siphon,
#[serde(rename = "Siphon+")]
SiphonPlus,
#[serde(rename = "Siphon++")]
SiphonPlusPlus,
Intercept, Intercept,
#[serde(rename = "Intercept+")]
InterceptPlus, InterceptPlus,
#[serde(rename = "Intercept++")]
InterceptPlusPlus, InterceptPlusPlus,
Break, // no damage stun, adds vulnerable Break,
#[serde(rename = "Break+")]
BreakPlus, BreakPlus,
#[serde(rename = "Break++")]
BreakPlusPlus, BreakPlusPlus,
Triage, // hot Triage,
#[serde(rename = "Triage+")]
TriagePlus, TriagePlus,
#[serde(rename = "Triage++")]
TriagePlusPlus, TriagePlusPlus,
Absorption,
#[serde(rename = "Absorption+")]
AbsorptionPlus,
#[serde(rename = "Absorption++")]
AbsorptionPlusPlus,
CounterAttack,
#[serde(rename = "CounterAttack+")]
CounterAttackPlus,
#[serde(rename = "CounterAttack++")]
CounterAttackPlusPlus,
Electrocute,
#[serde(rename = "Electrocute+")]
ElectrocutePlus,
#[serde(rename = "Electrocute++")]
ElectrocutePlusPlus,
ElectrocuteTick,
#[serde(rename = "ElectrocuteTick+")]
ElectrocuteTickPlus,
#[serde(rename = "ElectrocuteTick++")]
ElectrocuteTickPlusPlus,
DecayTick, // dot
#[serde(rename = "DecayTick+")]
DecayTickPlus,
#[serde(rename = "DecayTick++")]
DecayTickPlusPlus,
HasteStrike,
HybridBlast,
SiphonTick,
#[serde(rename = "SiphonTick+")]
SiphonTickPlus,
#[serde(rename = "SiphonTick++")]
SiphonTickPlusPlus,
TriageTick, TriageTick,
#[serde(rename = "TriageTick+")]
TriageTickPlus, TriageTickPlus,
#[serde(rename = "TriageTick++")]
TriageTickPlusPlus, TriageTickPlusPlus,
} }
@ -1675,13 +1758,14 @@ fn purge(source: &mut Construct, target: &mut Construct, mut results: Resolution
.event(Event::Removal { effect: ce.effect, construct_effects: target.effects.clone() })); .event(Event::Removal { effect: ce.effect, construct_effects: target.effects.clone() }));
} }
let mut turns = 0; let mut turns = 1;
for cs in target.skills.iter_mut() { for cs in target.skills.iter_mut() {
if Effect::Purge.disables_skill(cs.skill) { if Effect::Purge.disables_skill(cs.skill) {
turns += 1; turns += 1;
} }
} }
if turns > 0 {
if turns > 1 {
let mut effect = skill.effect()[0]; let mut effect = skill.effect()[0];
effect.duration = effect.duration * turns; effect.duration = effect.duration * turns;
results.push(Resolution::new(source, target).event(target.add_effect(skill, effect)).stages(EventStages::PostOnly)); results.push(Resolution::new(source, target).event(target.add_effect(skill, effect)).stages(EventStages::PostOnly));
@ -1789,6 +1873,7 @@ mod tests {
.named(&"camel".to_string()); .named(&"camel".to_string());
x.red_power.force(10000000000000); // multiplication of int max will cause overflow x.red_power.force(10000000000000); // multiplication of int max will cause overflow
y.green_life.force(1024); // make tests more flexible if we change stats
sustain(&mut y.clone(), &mut y, vec![], Skill::Sustain); sustain(&mut y.clone(), &mut y, vec![], Skill::Sustain);
assert!(y.affected(Effect::Sustain)); assert!(y.affected(Effect::Sustain));
@ -1885,6 +1970,8 @@ mod tests {
let mut y = Construct::new() let mut y = Construct::new()
.named(&"camel".to_string()); .named(&"camel".to_string());
x.blue_power.force(256);
x.green_life.force(1024);
x.green_life.reduce(512); x.green_life.reduce(512);
let mut results = resolve(Skill::Siphon, &mut x, &mut y, vec![]); let mut results = resolve(Skill::Siphon, &mut x, &mut y, vec![]);

View File

@ -1,6 +1,5 @@
use construct::{Stat, Colours}; use construct::{Stat, Colours};
use util::{IntPct}; use util::{IntPct};
use std::cmp;
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct SpecBonus { pub struct SpecBonus {
@ -29,9 +28,11 @@ impl SpecValues {
self.bonuses.iter().fold(self.base, |acc, s| acc + s.get_bonus(c)) self.bonuses.iter().fold(self.base, |acc, s| acc + s.get_bonus(c))
} }
/*
pub fn calc_multi (&self, c: &Colours) -> u64 { pub fn calc_multi (&self, c: &Colours) -> u64 {
self.multi * (c.red + c.green + c.blue) as u64 self.multi * (c.red + c.green + c.blue) as u64
} }
*/
} }
@ -711,10 +712,8 @@ impl Spec {
Spec::SpeedBBPlusPlus | Spec::SpeedBBPlusPlus |
Spec::SpeedRGPlusPlus | Spec::SpeedRGPlusPlus |
Spec::SpeedGBPlusPlus | Spec::SpeedGBPlusPlus |
Spec::SpeedRBPlusPlus => modified + { Spec::SpeedRBPlusPlus => modified + base.pct(self.values().max_value(player_colours)),
base.pct(cmp::min(self.values().calc_multi(construct_colours),
self.values().max_value(player_colours)))
},
// Flat bonus // Flat bonus
Spec::Life => modified + self.values().base, Spec::Life => modified + self.values().base,
Spec::LifeRR| Spec::LifeRR|
@ -734,10 +733,7 @@ impl Spec {
Spec::LifeBBPlusPlus | Spec::LifeBBPlusPlus |
Spec::LifeRGPlusPlus | Spec::LifeRGPlusPlus |
Spec::LifeGBPlusPlus | Spec::LifeGBPlusPlus |
Spec::LifeRBPlusPlus => modified + { Spec::LifeRBPlusPlus => modified + self.values().max_value(player_colours),
cmp::min(self.values().calc_multi(construct_colours),
self.values().max_value(player_colours))
},
} }
} }
} }