Merge branch 'release/1.8.3'

This commit is contained in:
ntr 2019-11-13 21:51:55 +11:00
commit ef790b2f73
56 changed files with 1744 additions and 342 deletions

View File

@ -1,4 +1,29 @@
## [1.8.1] - 2019-11-07 ## [1.8.3] - 2019-11-10
### Added
- Preview combos by hovering over recipes
- Condensed recipe display
### Fixed
- Construct display on info / demo page
- Case where a skill could send multiple ko events to target
- Resizing of vbox when you buy / create certain items
### Changed
- Automatically shows a preview of combo item when you have 3 items selected for combining
- Only highlight the first available item slot when equipping
- Amplify no longer increase GreenPower
- Purify
- Now removes all effects on target
- Applies "Pure" increasing healing taken
- Purge
- Now removes all effects on target
## [1.8.2] - 2019-11-10
### Fixed
- Duplicate button issue in reshape tab
## [1.8.1] - 2019-11-09
### Fixed ### Fixed
- An issue where skills would not be put on cooldown after being used. - An issue where skills would not be put on cooldown after being used.

View File

@ -1 +1 @@
1.8.1 1.8.3

View File

@ -6,9 +6,15 @@
* can't reset password without knowing password =\ * can't reset password without knowing password =\
* ws gzip encoding * ws gzip encoding
* mobile info page * Graphics
* Img
* Skill Icons
* Buttons / General UI Theming
* Front Page
## SOON ## SOON
* Graphical status effects instead of text
* Improve colour contrast / buttons
* supporter gold name in instance (anyone whos put any money into game) * supporter gold name in instance (anyone whos put any money into game)
@ -59,7 +65,7 @@
* Items * Items
* instead of red noise, red and black bar gradient * instead of red noise, red and black bar gradient
* eth adapter * eth adapter
*sets* *sets*
* illusions * illusions
* vaporwave * vaporwave
* crop circles * crop circles

View File

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

View File

@ -14,6 +14,7 @@ npm i
npm run build npm run build
cp tos.html dist/ cp tos.html dist/
cp changelog.html dist/
# echo "Building acp version $VERSION" # echo "Building acp version $VERSION"
# cd $MNML_PATH/acp # cd $MNML_PATH/acp

View File

@ -2457,9 +2457,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="1" inkscape:pageopacity="1"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.53357639" inkscape:zoom="0.53546627"
inkscape:cx="411.32817" inkscape:cx="561.25984"
inkscape:cy="1018.5983" inkscape:cy="793.70079"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="true" showgrid="true"
@ -2539,8 +2539,8 @@
transform="translate(-472.60042,755.1467)" transform="translate(-472.60042,755.1467)"
style="display:inline"> style="display:inline">
<rect <rect
style="opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.01528859;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:20.98234558;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect5015" id="rect5144"
width="297" width="297"
height="420" height="420"
x="472.60043" x="472.60043"
@ -2548,7 +2548,7 @@
<g <g
id="g4946" id="g4946"
style="stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none" style="stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none"
transform="translate(0,24.99999)"> transform="translate(0,2.7646743)">
<path <path
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path4913" id="path4913"
@ -2566,28 +2566,69 @@
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g> </g>
<g <g
id="g5001" id="g5142"
transform="translate(25)"> transform="translate(0,-47.235375)">
<g
transform="translate(25)"
id="g5001">
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 522.60042,-435.14668 v -75.00001 h 50 v 75.00001 h -50"
id="path4948"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 532.60042,-500.14669 h 30 v 55 h -30 z"
id="path4978"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 542.60042,-490.1467 h 10 v 35 h -10 v -35"
id="path4988"
inkscape:connector-curvature="0" />
</g>
<path <path
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path4948" id="path5003"
d="m 522.60042,-435.14668 v -75.00001 h 50 v 75.00001 h -50" d="m 697.60042,-460.14669 -25,25.00001 -25,-25.00001 25,25.00001 v -75.00001 h -25"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:3;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path </g>
inkscape:connector-curvature="0" <g
id="path4978" id="g5123"
d="m 532.60042,-500.14669 h 30 v 55 h -30 z" transform="matrix(0.23529412,0,0,0.23529412,411.40033,-377.05346)"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<path <path
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path4988" id="path5068"
d="m 542.60042,-490.1467 h 10 v 35 h -10 v -35" d="m 472.60042,-185.14671 v -49.99998 h 100 v 25 h -100 l 100,24.99998"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5075"
d="m 622.60042,-185.14671 v -49.99998 h 100 v 49.99998 z"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="cccc"
inkscape:connector-curvature="0"
id="path5082"
d="m 772.60042,-235.14669 h 99.99999 -50.00001 l 2e-5,50"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5096"
d="m 922.60041,-185.14671 v 0 -49.99998 h 99.99999 v 49.99998 -24.99998 h -99.99999"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path5103"
d="m 1072.6004,-235.14669 h 100 -50 v 49.99998"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccc"
inkscape:connector-curvature="0"
id="path5110"
d="m 1322.6004,-185.14669 -100,-2e-5 v -24.99998 h 75 -75 v -25 h 100"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g> </g>
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#f5f5f5;stroke-width:2.9685142;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 697.60042,-460.14669 -25,25.00001 -25,-25.00001 25,25.00001 v -75.00001 h -25"
id="path5003"
inkscape:connector-curvature="0" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -122,6 +122,15 @@ svg {
} }
} }
@keyframes border-co {
0% {
border-color: @gray-box;
}
100% {
border-color: @gray-hint;
}
}
@keyframes co { @keyframes co {
from { from {
background: @black; background: @black;

View File

@ -94,7 +94,7 @@
justify-items: center; justify-items: center;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: minmax(min-content, 1fr) min-content; grid-template-rows: min-content 1fr;
grid-template-areas: grid-template-areas:
"left" "left"
"right"; "right";

View File

@ -11,7 +11,7 @@
@media (max-width: 1920px) { @media (max-width: 1920px) {
.instance .info table td svg { .instance .info table td svg {
height: 50%; // height: 50%;
stroke-width: 8px; stroke-width: 8px;
} }
@ -33,15 +33,57 @@
} }
.instance .info { .instance .info {
/*font-size: 75%;*/
margin: 0 0 0 1em; margin: 0 0 0 1em;
grid-area: info; grid-area: info;
display: flex;
flex-flow: column;
// white-space: pre-wrap;
> *:first-child { display: grid;
margin-bottom: 1em; grid-template-rows: 13em min-content;
grid-template-areas:
"item"
"combos";
.combos {
display: grid;
grid-template-columns: repeat(6, 1fr);
align-content: center;
.table-button {
display: grid;
text-align: center;
align-content: center;
border-bottom: 2px solid #222;
grid-template-areas:
"item"
"ingr";
cursor: pointer;
&:hover {
color: whitesmoke;
background-color: @gray;
}
.item {
border-top: 2px solid #222;
border-bottom: 2px solid #222;
flex: 1;
grid-area: item;
font-weight: bold;
}
div {
border-right: 2px solid #222;
svg {
vertical-align: middle;
}
}
&:first-child {
div {
border-left: 2px solid #222;
}
}
}
} }
} }
@ -392,6 +434,10 @@
&.winner { &.winner {
animation: win 2s ease-in-out 0s 1; animation: win 2s ease-in-out 0s 1;
} }
.cancelled {
color: white;
}
} }
.tutorial { .tutorial {

View File

@ -122,6 +122,10 @@ section {
// height: 3em; // height: 3em;
} }
&.sub {
grid-template-columns: 1fr;
}
&.play { &.play {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
align-items: flex-start; align-items: flex-start;

View File

@ -269,7 +269,7 @@ textarea:focus,
select:focus { select:focus {
border: 1px solid #33C3F0; border: 1px solid #33C3F0;
outline: 0; } outline: 0; }
label,
legend { legend {
display: block; display: block;
margin-bottom: .5rem; margin-bottom: .5rem;

View File

@ -1,6 +1,6 @@
.vbox { .vbox {
margin-bottom: 2em; margin-bottom: 2em;
line-height: 0;
.vbox-hdr { .vbox-hdr {
margin-bottom: 1em; margin-bottom: 1em;
height: 2em; height: 2em;
@ -53,7 +53,7 @@
&, &:hover, &:active { &, &:hover, &:active {
background: @red; background: @red;
color: black; color: black;
border: 1px solid black; border: 2px solid black;
} }
} }
svg { svg {

1211
client/changelog.html Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,4 @@
export const setAccount = value => ({ type: 'SET_ACCOUNT', value }); export const setAccount = value => ({ type: 'SET_ACCOUNT', value });
export const setActiveConstruct = value => ({ type: 'SET_ACTIVE_CONSTRUCT', value });
export const setAnimating = value => ({ type: 'SET_ANIMATING', value }); export const setAnimating = value => ({ type: 'SET_ANIMATING', value });
export const setAnimCb = value => ({ type: 'SET_ANIM_CB', value }); export const setAnimCb = value => ({ type: 'SET_ANIM_CB', value });
@ -40,9 +39,9 @@ export const setPing = value => ({ type: 'SET_PING', value });
export const setPlayer = value => ({ type: 'SET_PLAYER', value }); export const setPlayer = value => ({ type: 'SET_PLAYER', value });
export const setReclaiming = value => ({ type: 'SET_RECLAIMING', value }); export const setReclaiming = value => ({ type: 'SET_RECLAIMING', value });
export const setShowLog = value => ({ type: 'SET_SHOW_LOG', value }); export const setShowLog = value => ({ type: 'SET_SHOW_LOG', value });
export const setSkip = value => ({ type: 'SET_SKIP', value });
export const setShop = value => ({ type: 'SET_SHOP', value }); export const setShop = value => ({ type: 'SET_SHOP', value });
export const setSubscription = value => ({ type: 'SET_SUBSCRIPTION', value }); export const setSubscription = value => ({ type: 'SET_SUBSCRIPTION', value });
export const setPvp = value => ({ type: 'SET_PVP', value });
export const setTeam = value => ({ type: 'SET_TEAM', value: Array.from(value) }); export const setTeam = value => ({ type: 'SET_TEAM', value: Array.from(value) });
export const setTeamPage = value => ({ type: 'SET_TEAM_PAGE', value }); export const setTeamPage = value => ({ type: 'SET_TEAM_PAGE', value });

View File

@ -78,7 +78,6 @@ function createSocket(store) {
store.dispatch(actions.setAnimText(null)); store.dispatch(actions.setAnimText(null));
store.dispatch(actions.setAnimating(false)); store.dispatch(actions.setAnimating(false));
store.dispatch(actions.setGameEffectInfo(null)); store.dispatch(actions.setGameEffectInfo(null));
store.dispatch(actions.setSkip(false));
// set the game state so resolutions don't fire twice // set the game state so resolutions don't fire twice
store.dispatch(actions.setGame(game)); store.dispatch(actions.setGame(game));

View File

@ -168,6 +168,9 @@ function getText(resolution) {
if (type === 'Removal') { if (type === 'Removal') {
const { effect, construct_effects: effects } = event; const { effect, construct_effects: effects } = event;
if (!effect) {
return { text: 'Effect Removal', css: '', effects };
}
return { text: `-${effect}`, css: '', effects }; return { text: `-${effect}`, css: '', effects };
} }
return false; return false;

View File

@ -36,7 +36,6 @@ const addState = connect(
dispatch(actions.setCombiner([])); dispatch(actions.setCombiner([]));
dispatch(actions.setReclaiming(false)); dispatch(actions.setReclaiming(false));
dispatch(actions.setActiveSkill(null)); dispatch(actions.setActiveSkill(null));
dispatch(actions.setActiveConstruct(null));
dispatch(actions.setInfo(null)); dispatch(actions.setInfo(null));
dispatch(actions.setItemEquip(null)); dispatch(actions.setItemEquip(null));
dispatch(actions.setItemUnequip([])); dispatch(actions.setItemUnequip([]));

View File

@ -20,6 +20,7 @@ function projectile(x, y, radius, colour) {
cx={x} cx={x}
cy={y} cy={y}
r={radius} r={radius}
stroke="none"
fill={colour} fill={colour}
/> />
); );

View File

@ -80,6 +80,12 @@ class Faceoff extends preact.Component {
} }
function faceoffText() { function faceoffText() {
if (!instance.winner) { if (!instance.winner) {
if (instance.phase === 'Finished') return (
<div class="faceoff-text">
<div class="cancelled"> game cancelled </div>
</div>
);
return ( return (
<div class="faceoff-text"> <div class="faceoff-text">
<div class="opponent-text"> {otherTeam.name} </div> <div class="opponent-text"> {otherTeam.name} </div>

View File

@ -148,7 +148,7 @@ class GameConstruct extends Component {
return ( return (
<div class="skill-description"> <div class="skill-description">
<h2><span> {gameSkillInfo.skill} </span></h2> <h2><span> {gameSkillInfo.skill} </span></h2>
<span>{infoDescription} </span> <span>{infoDescription} </span> <br />
{speed} {speed}
</div>); </div>);
} }

View File

@ -14,7 +14,6 @@ const addState = connect(
account, account,
animating, animating,
activeSkill, activeSkill,
activeConstruct,
} = state; } = state;
function selectSkillTarget(targetConstructId) { function selectSkillTarget(targetConstructId) {
@ -34,7 +33,6 @@ const addState = connect(
account, account,
animating, animating,
activeSkill, activeSkill,
activeConstruct,
selectSkillTarget, selectSkillTarget,
}; };
}, },
@ -44,11 +42,7 @@ const addState = connect(
dispatch(actions.setActiveSkill(constructId, skill)); dispatch(actions.setActiveSkill(constructId, skill));
} }
function setActiveConstruct(construct) { return { setActiveSkill };
dispatch(actions.setActiveConstruct(construct));
}
return { setActiveSkill, setActiveConstruct };
} }
); );
@ -59,7 +53,6 @@ function Game(props) {
account, account,
animating, animating,
setActiveSkill, setActiveSkill,
setActiveConstruct,
} = props; } = props;
if (!game) return <div>...</div>; if (!game) return <div>...</div>;
@ -91,7 +84,6 @@ function Game(props) {
function gameClick(e) { function gameClick(e) {
e.stopPropagation(); e.stopPropagation();
setActiveConstruct(null);
} }
return ( return (

View File

@ -35,7 +35,6 @@ const addState = connect(
dispatch(actions.setCombiner([])); dispatch(actions.setCombiner([]));
dispatch(actions.setReclaiming(false)); dispatch(actions.setReclaiming(false));
dispatch(actions.setActiveSkill(null)); dispatch(actions.setActiveSkill(null));
dispatch(actions.setActiveConstruct(null));
dispatch(actions.setInfo(null)); dispatch(actions.setInfo(null));
dispatch(actions.setItemEquip(null)); dispatch(actions.setItemEquip(null));
dispatch(actions.setItemUnequip([])); dispatch(actions.setItemUnequip([]));

View File

@ -9,15 +9,22 @@ const shapes = require('./shapes');
class InfoComponent extends preact.Component { class InfoComponent extends preact.Component {
shouldComponentUpdate(newProps) { shouldComponentUpdate(newProps, newState) {
if (newProps.tutorial !== this.props.tutorial) return true; if (newProps.tutorial !== this.props.tutorial) return true;
// We don't care about info during tutorial // We don't care about info during tutorial
if (newProps.tutorial && this.props.instance.time_control === 'Practice' if (newProps.tutorial && this.props.instance.time_control === 'Practice'
&& this.props.instance.rounds.length === 1) return false; && this.props.instance.rounds.length === 1) return false;
if (newProps.info !== this.props.info) return true; if (newProps.info !== this.props.info) return true;
if (newState.comboItem !== this.state.comboItem) return true;
return false; return false;
} }
componentDidUpdate(prevProps) {
// Catch case where mouse events don't properly clear state and info changed
if (prevProps.info !== this.props.info && this.state.comboItem) this.setState({ comboItem: null });
}
render(args) { render(args) {
const { const {
// Variables that will change // Variables that will change
@ -33,7 +40,7 @@ class InfoComponent extends preact.Component {
setInfo, setInfo,
setTutorialNull, setTutorialNull,
} = args; } = args;
const { comboItem } = this.state;
function Info() { function Info() {
if (tutorial) { if (tutorial) {
const tutorialStageInfo = tutorialStage(tutorial, ws, setTutorialNull, instance); const tutorialStageInfo = tutorialStage(tutorial, ws, setTutorialNull, instance);
@ -42,19 +49,19 @@ class InfoComponent extends preact.Component {
if (!info) return false; if (!info) return false;
if (info.includes('constructName')) { if (info.includes('constructName')) {
return ( return (
<div> <div class='info-item'>
<h2> {info.replace('constructName ', '')} </h2> <h2> {info.replace('constructName ', '')} </h2>
<p> This is the name of your construct. <br /> <p> This is the name of your construct. <br />
Names are randomly generated and are purely cosmetic. <br /> Names are randomly generated and are purely cosmetic. <br />
You can change change your construct name in the <b>RESHAPE</b> tab outside of games. You can change change your construct name in the <b>RESHAPE</b> tab outside of games.
</p> </p>
</div> </div>
); );
} }
if (info.includes('constructAvatar')) { if (info.includes('constructAvatar')) {
return ( return (
<div> <div class='info-item'>
<h2> {info.replace('constructAvatar ', '')} </h2> <h2> {info.replace('constructAvatar ', '')} </h2>
<p> This is your construct avatar. <br /> <p> This is your construct avatar. <br />
Avatars are randomly generated and are purely cosmetic. <br /> Avatars are randomly generated and are purely cosmetic. <br />
@ -63,7 +70,9 @@ class InfoComponent extends preact.Component {
</div> </div>
); );
} }
const fullInfo = itemInfo.items.find(i => i.item === info) || INFO[info]; const fullInfo = comboItem
? itemInfo.items.find(i => i.item === comboItem) || INFO[comboItem]
: 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;
const isSpec = fullInfo.spec; const isSpec = fullInfo.spec;
@ -75,16 +84,17 @@ class InfoComponent extends preact.Component {
}; };
if (isSkill || isSpec) { if (isSkill || isSpec) {
let infoName = info; let infoName = fullInfo.item;
while (infoName.includes('Plus')) infoName = infoName.replace('Plus', '+'); while (infoName.includes('Plus')) infoName = infoName.replace('Plus', '+');
const header = isSkill ? <h3> SKILL </h3> : <h3> SPEC </h3>; const itemSource = itemInfo.combos.filter(c => c.item === removeTier(fullInfo.item));
const itemSource = itemInfo.combos.filter(c => c.item === removeTier(info)); let itemSourceInfo = itemSource.length && !isSpec
let itemSourceInfo = itemSource.length
? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}`
: false; : false;
let header = null;
if (!itemSource.length) header = isSkill ? <h3> SKILL </h3> : <h3> SPEC </h3>;
if (itemSourceInfo) { if (itemSourceInfo) {
while (itemSourceInfo.includes('Plus')) itemSourceInfo = itemSourceInfo.replace('Plus', '+'); while (itemSourceInfo.includes('Plus')) itemSourceInfo = itemSourceInfo.replace('Plus', '+');
const itemRegEx = /(Red|Blue|Green)/; const itemRegEx = /(Red|Blue|Green)/;
@ -100,7 +110,7 @@ class InfoComponent extends preact.Component {
const thresholds = isSpec ? specThresholds(player, fullInfo, info) : null; const thresholds = isSpec ? specThresholds(player, fullInfo, info) : null;
return ( return (
<div class={isSkill ? 'info-skill' : 'info-spec'}> <div class={isSkill ? 'info-item' : 'info-item'}>
<h2>{infoName} {fullInfo.cost}b</h2> <h2>{infoName} {fullInfo.cost}b</h2>
{header} {header}
{itemSourceInfo} {itemSourceInfo}
@ -120,7 +130,7 @@ class InfoComponent extends preact.Component {
); );
} }
function Combos() { const Combos = () => {
if (tutorial && instance.time_control === 'Practice' && instance.rounds.length === 1) return false; if (tutorial && instance.time_control === 'Practice' && instance.rounds.length === 1) return false;
const generalNotes = ( const generalNotes = (
<div> <div>
@ -137,22 +147,33 @@ class InfoComponent extends preact.Component {
const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info)); const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info));
if (vboxCombos.length > 6 || vboxCombos.length === 0) return generalNotes; if (vboxCombos.length > 6 || vboxCombos.length === 0) return generalNotes;
const comboTable = vboxCombos.map((c, i) => {
const mouseOver = e => {
e.stopPropagation();
this.setState({ comboItem: c.item });
};
const componentTable = (c.components.some(ci => ['Red', 'Blue', 'Green'].includes(ci)))
? [<div key="0">{convertItem(c.components[0])}&nbsp;{convertItem(c.components[1])}</div>,
<div key="1">{convertItem(c.components[2])}</div>]
: c.components.map((u, j) => <div key={j} >{convertItem(u)}</div>);
return (
<div key={i} onMouseOver={mouseOver} class="table-button" onClick={() => setInfo(c.item)}>
<div class="item">
{convertItem(c.item)}
</div>
{componentTable}
</div>
);
});
return ( return (
<table class="combos"> <div class="combos">
<tbody> {comboTable}
{vboxCombos.map((c, i) => </div>
<tr key={i} >
<td class="highlight" onClick={() => setInfo(c.item)} >{convertItem(c.item)}</td>
{c.components.map((u, j) => <td key={j} >{convertItem(u)}</td>)}
</tr>
)}
</tbody>
</table>
); );
} };
return ( return (
<div class='info' > <div class='info' onMouseOver={() => this.setState({ comboItem: null })}>
<Info /> <Info />
<Combos /> <Combos />
</div> </div>

View File

@ -66,10 +66,6 @@ const addState = connect(
dispatch(actions.setInfo(item)); dispatch(actions.setInfo(item));
} }
function setActiveConstruct(value) {
dispatch(actions.setActiveConstruct(value));
}
function setItemEquip(v) { function setItemEquip(v) {
return dispatch(actions.setItemEquip(v)); return dispatch(actions.setItemEquip(v));
} }
@ -78,7 +74,7 @@ const addState = connect(
return dispatch(actions.setItemUnequip(v)); return dispatch(actions.setItemUnequip(v));
} }
return { quit, setInfo, setActiveConstruct, setItemUnequip, setItemEquip }; return { quit, setInfo, setItemUnequip, setItemEquip };
} }
); );
@ -101,7 +97,6 @@ function Construct(props) {
sendVboxAcceptEquip, sendVboxAcceptEquip,
sendVboxUnequipApply, sendVboxUnequipApply,
sendUnequip, sendUnequip,
setActiveConstruct,
setItemUnequip, setItemUnequip,
setItemEquip, setItemEquip,
setInfo, setInfo,
@ -144,7 +139,6 @@ function Construct(props) {
function skillClick(e) { function skillClick(e) {
if (!skill) return false; if (!skill) return false;
setItemUnequip([construct.id, skill.skill]); setItemUnequip([construct.id, skill.skill]);
setActiveConstruct(construct);
e.stopPropagation(); e.stopPropagation();
return true; return true;
} }
@ -152,7 +146,6 @@ function Construct(props) {
function skillDblClick(e) { function skillDblClick(e) {
if (!skill) return false; if (!skill) return false;
sendUnequip(construct.id, skill.skill); sendUnequip(construct.id, skill.skill);
setActiveConstruct(null);
setItemUnequip([]); setItemUnequip([]);
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -160,7 +153,7 @@ function Construct(props) {
} }
const equipping = skillList.includes(vbox.bound[itemEquip]) && !skill const equipping = skillList.includes(vbox.bound[itemEquip]) && !skill
&& !tutorialDisableEquip && !duplicateSkill; && !tutorialDisableEquip && !duplicateSkill && i === construct.skills.length;
const border = () => { const border = () => {
if (!skill) return ''; if (!skill) return '';
const borderFn = buttons[removeTier(skill.skill)]; const borderFn = buttons[removeTier(skill.skill)];
@ -173,7 +166,7 @@ function Construct(props) {
<label onDragStart={ev => { <label onDragStart={ev => {
ev.dataTransfer.setData('text', ''); ev.dataTransfer.setData('text', '');
skillClick(ev); skillClick(ev);
}} key={i} draggable="true" onDragEnd={() => setItemUnequip([])}> }} key={i} draggable={skill} onDragEnd={() => setItemUnequip([])}>
<button <button
key={i} key={i}
disabled={!skill && !equipping} disabled={!skill && !equipping}
@ -191,7 +184,7 @@ function Construct(props) {
const s = construct.specs[i]; const s = construct.specs[i];
if (!s) { if (!s) {
const equipping = specList.includes(vbox.bound[itemEquip]); const equipping = specList.includes(vbox.bound[itemEquip]) && i === construct.specs.length;
const classes = `${equipping ? 'equipping' : 'gray'} empty`; const classes = `${equipping ? 'equipping' : 'gray'} empty`;
return ( return (
<button key={i} class={classes} disabled={!equipping} > <button key={i} class={classes} disabled={!equipping} >
@ -203,12 +196,10 @@ function Construct(props) {
function specClick(e) { function specClick(e) {
e.stopPropagation(); e.stopPropagation();
setItemUnequip([construct.id, s]); setItemUnequip([construct.id, s]);
setActiveConstruct(construct);
} }
function specDblClick(e) { function specDblClick(e) {
sendUnequip(construct.id, s); sendUnequip(construct.id, s);
setActiveConstruct(null);
setItemUnequip([]); setItemUnequip([]);
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -247,7 +238,7 @@ function Construct(props) {
</div>; </div>;
}); });
const classes = `instance-construct`; const classes = 'instance-construct';
const avatarMouseOver = e => hoverInfo(e, `constructAvatar ${construct.name}`); const avatarMouseOver = e => hoverInfo(e, `constructAvatar ${construct.name}`);
return ( return (
<div key={construct.id} class={classes} onClick={onClick} onDragOver={ev => ev.preventDefault()} onDrop={onClick}> <div key={construct.id} class={classes} onClick={onClick} onDragOver={ev => ev.preventDefault()} onDrop={onClick}>
@ -291,7 +282,6 @@ class InstanceConstructs extends preact.Component {
itemInfo, itemInfo,
// Function calls // Function calls
setInfo, setInfo,
setActiveConstruct,
sendVboxApply, sendVboxApply,
sendVboxAcceptEquip, sendVboxAcceptEquip,
sendVboxUnequipApply, sendVboxUnequipApply,
@ -321,7 +311,6 @@ class InstanceConstructs extends preact.Component {
sendVboxAcceptEquip, sendVboxAcceptEquip,
sendVboxUnequipApply, sendVboxUnequipApply,
setInfo, setInfo,
setActiveConstruct,
itemInfo, itemInfo,
setVboxHighlight, setVboxHighlight,
sendUnequip, sendUnequip,
@ -331,7 +320,7 @@ class InstanceConstructs extends preact.Component {
}); });
return ( return (
<div class='construct-list' onClick={() => setActiveConstruct(null)}> <div class='construct-list'>
{constructs} {constructs}
</div> </div>
); );

View File

@ -17,16 +17,11 @@ const addState = connect(
return ws.sendInstanceReady(instance.id); return ws.sendInstanceReady(instance.id);
} }
function sendAbandon() {
return ws.sendInstanceAbandon(instance.id);
}
return { return {
instance, instance,
chatShow, chatShow,
account, account,
sendAbandon,
sendReady, sendReady,
}; };
}, },
@ -48,22 +43,12 @@ function InstanceCtrlBtns(args) {
chatShow, chatShow,
account, account,
sendAbandon,
sendReady, sendReady,
setChatShow, setChatShow,
} = args; } = args;
const finished = instance && instance.phase === 'Finished'; const finished = instance && instance.phase === 'Finished';
// cheeky to make sure nubs don't just abandon their first game
const beingNub = instance.phase_end
&& instance.phase === 'Lobby'
&& Date.parse(instance.phase_end) - Date.now() < 2000;
if (beingNub) {
sendReady();
}
return ( return (
<div class="instance-ctrl-btns"> <div class="instance-ctrl-btns">
<button disabled={!account.subscribed} onClick={() => setChatShow(!chatShow)}>Chat</button> <button disabled={!account.subscribed} onClick={() => setChatShow(!chatShow)}>Chat</button>

View File

@ -36,7 +36,6 @@ const addState = connect(
dispatch(actions.setCombiner([])); dispatch(actions.setCombiner([]));
dispatch(actions.setReclaiming(false)); dispatch(actions.setReclaiming(false));
dispatch(actions.setActiveSkill(null)); dispatch(actions.setActiveSkill(null));
dispatch(actions.setActiveConstruct(null));
dispatch(actions.setInfo(null)); dispatch(actions.setInfo(null));
dispatch(actions.setItemEquip(null)); dispatch(actions.setItemEquip(null));
dispatch(actions.setItemUnequip([])); dispatch(actions.setItemUnequip([]));

View File

@ -14,6 +14,7 @@ const addState = connect(
account, account,
instances, instances,
invite, invite,
pvp,
} = state; } = state;
function sendInstanceState(id) { function sendInstanceState(id) {
@ -32,15 +33,21 @@ const addState = connect(
ws.sendInstanceInvite(); ws.sendInstanceInvite();
} }
function sendInstanceLeave() {
ws.sendInstanceLeave();
}
return { return {
account, account,
instances, instances,
invite, invite,
pvp,
sendInstanceState, sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
sendInstanceInvite, sendInstanceInvite,
sendInstanceLeave,
}; };
}, },
@ -67,11 +74,13 @@ function Play(args) {
account, account,
instances, instances,
invite, invite,
pvp,
sendInstanceState, sendInstanceState,
sendInstanceQueue, sendInstanceQueue,
sendInstancePractice, sendInstancePractice,
sendInstanceInvite, sendInstanceInvite,
sendInstanceLeave,
setNav, setNav,
} = args; } = args;
@ -125,6 +134,31 @@ function Play(args) {
); );
}; };
const pvpBtn = () => {
if (pvp) return (
<figure>
<button
class="ready"
onClick={() => sendInstanceLeave()}
>
Cancel
</button>
<figcaption>Finding Opponent</figcaption>
</figure>
);
return (
<figure>
<button
class="ready"
onClick={() => sendInstanceQueue()}>
PVP
</button>
<figcaption>Matchmaking</figcaption>
</figure>
);
}
const subscription = account.subscribed const subscription = account.subscribed
? <button ? <button
class="yellow-btn" class="yellow-btn"
@ -142,14 +176,7 @@ function Play(args) {
if (!instances.length) { if (!instances.length) {
return ( return (
<div class='list play'> <div class='list play'>
<figure> {pvpBtn()}
<button
class="ready"
onClick={() => sendInstanceQueue()}>
PVP
</button>
<figcaption>Matchmaking</figcaption>
</figure>
{inviteBtn()} {inviteBtn()}
<figure> <figure>
<button <button

View File

@ -56,7 +56,7 @@ function Reshape(args) {
const useMtx = (item, i) => { const useMtx = (item, i) => {
const price = item === 'Rename' ? 5 : 1; const price = item === 'Rename' ? 5 : 1;
return ( return (
<figure key={i} onClick={e => { <figure key={i * 2} onClick={e => {
e.stopPropagation(); e.stopPropagation();
setMtxActive(item); setMtxActive(item);
}}> }}>
@ -67,7 +67,7 @@ function Reshape(args) {
}; };
const availableMtx = (item, i) => ( const availableMtx = (item, i) => (
<figure key={i}> <figure key={i * 2 + 1}>
<figcaption>Enable {item.variant}</figcaption> <figcaption>Enable {item.variant}</figcaption>
<button onClick={() => mtxBuy(item)} disabled={account.balance < item.credits}>¤{item.credits}</button> <button onClick={() => mtxBuy(item)} disabled={account.balance < item.credits}>¤{item.credits}</button>
</figure> </figure>

View File

@ -44,21 +44,26 @@ function BitsBtn(args) {
} }
const subscription = account.subscribed const subscription = account.subscribed
? <button ? <figure>
class="yellow-btn" <figcaption>Thank you for your support</figcaption>
disabled> <button
Subscribed class="yellow-btn"
</button> disabled>
: <button Subscribed
onClick={subscribeClick} </button>
class="yellow-btn" </figure>
role="link"> : <figure onClick={subscribeClick}>
Subscribe <figcaption>¤150 / month + Chat Wheel + more</figcaption>
</button>; <button
class="yellow-btn"
role="link">
Subscribe
</button>
</figure>;
return ( return (
<div> <div>
<div class='list'> <div class='list sub'>
{subscription} {subscription}
</div> </div>
<div class='list'> <div class='list'>

View File

@ -213,6 +213,7 @@ class Vbox extends preact.Component {
const classes = `${v.toLowerCase()} ${selected ? 'highlight' : ''} ${comboHighlight}`; const classes = `${v.toLowerCase()} ${selected ? 'highlight' : ''} ${comboHighlight}`;
const vboxObject = shapes[v] ? shapes[v]() : v; const vboxObject = shapes[v] ? shapes[v]() : v;
const disabled = vbox.bits <= group;
return ( return (
<label draggable='true' <label draggable='true'
onDragStart={ev => { onDragStart={ev => {
@ -223,6 +224,7 @@ class Vbox extends preact.Component {
onDragEnd={clearVboxSelected}> onDragEnd={clearVboxSelected}>
<button <button
class={classes} class={classes}
disabled={disabled}
onMouseOver={e => vboxHover(e, v)} onMouseOver={e => vboxHover(e, v)}
onMouseDown={onClick} onMouseDown={onClick}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
@ -244,7 +246,7 @@ class Vbox extends preact.Component {
onTouchStart={e => e.target.scrollIntoView(true)} onTouchStart={e => e.target.scrollIntoView(true)}
onMouseOver={e => hoverInfo(e, 'vbox')}> VBOX onMouseOver={e => hoverInfo(e, 'vbox')}> VBOX
</h3> </h3>
<div class="bits" onMouseOver={e => hoverInfo(e, 'bits')} >{vbox.bits}b</div> <div class={`bits ${vbox.bits < 3 ? 'red' : false}`} onMouseOver={e => hoverInfo(e, 'bits')} >{vbox.bits}b</div>
</div> </div>
<div class="vbox-colours"> <div class="vbox-colours">
{range(0, 6).map(i => availableBtn(vbox.free[0][i], 0, i))} {range(0, 6).map(i => availableBtn(vbox.free[0][i], 0, i))}
@ -285,7 +287,7 @@ class Vbox extends preact.Component {
const combinerItems = combiner.map(j => vbox.bound[j]); const combinerItems = combiner.map(j => vbox.bound[j]);
const combinerCount = countBy(combinerItems, co => co); const combinerCount = countBy(combinerItems, co => co);
const comboHighlight = combinerItems.length > 0 && itemInfo.combos.some(combo => { const comboItem = itemInfo.combos.find(combo => {
if (combo.components.includes(v)) { if (combo.components.includes(v)) {
return combinerItems.every(c => { return combinerItems.every(c => {
if (!combo.components.includes(c)) return false; if (!combo.components.includes(c)) return false;
@ -295,7 +297,8 @@ class Vbox extends preact.Component {
return true; return true;
}); });
} return false; } return false;
}) ? 'combo-border' : ''; });
const comboHighlight = combinerItems.length > 0 && comboItem ? 'combo-border' : '';
function onClick(type) { function onClick(type) {
if (vboxSelecting) clearVboxSelected(); if (vboxSelecting) clearVboxSelected();
@ -321,6 +324,7 @@ class Vbox extends preact.Component {
} }
combiner.push(i); combiner.push(i);
if (combiner.length === 3) setInfo(comboItem.item);
return combinerChange(combiner); return combinerChange(combiner);
} }

View File

@ -39,7 +39,7 @@ module.exports = {
Colours - 1b<br /> Colours - 1b<br />
Skills - 2b<br /> Skills - 2b<br />
Specs - 3b<br /> Specs - 3b<br />
At the beginning of each round you receive 18 bits increasing by 6 bits per round.</p>, At the beginning of each round you receive 30 bits.</p>,
}, },
ready: { ready: {
item: 'READY', item: 'READY',

View File

@ -30,6 +30,10 @@ function registerEvents(store) {
store.dispatch(actions.setPing(ping)); store.dispatch(actions.setPing(ping));
} }
function setPvp(v) {
store.dispatch(actions.setPvp(v));
}
function setNav(v) { function setNav(v) {
store.dispatch(actions.setNav(v)); store.dispatch(actions.setNav(v));
} }
@ -120,7 +124,6 @@ function registerEvents(store) {
store.dispatch(actions.setAnimText(null)); store.dispatch(actions.setAnimText(null));
store.dispatch(actions.setAnimating(false)); store.dispatch(actions.setAnimating(false));
store.dispatch(actions.setGameEffectInfo(null)); store.dispatch(actions.setGameEffectInfo(null));
store.dispatch(actions.setSkip(false));
// set the game state so resolutions don't fire twice // set the game state so resolutions don't fire twice
store.dispatch(actions.setGame(game)); store.dispatch(actions.setGame(game));
@ -177,7 +180,6 @@ function registerEvents(store) {
function clearInfo() { function clearInfo() {
store.dispatch(actions.setInfo(null)); store.dispatch(actions.setInfo(null));
store.dispatch(actions.setActiveConstruct(null));
console.log('event clear item'); console.log('event clear item');
} }
@ -185,7 +187,6 @@ function registerEvents(store) {
store.dispatch(actions.setCombiner([])); store.dispatch(actions.setCombiner([]));
store.dispatch(actions.setReclaiming(false)); store.dispatch(actions.setReclaiming(false));
store.dispatch(actions.setActiveSkill(null)); store.dispatch(actions.setActiveSkill(null));
store.dispatch(actions.setActiveConstruct(null));
store.dispatch(actions.setInfo(null)); store.dispatch(actions.setInfo(null));
store.dispatch(actions.setItemEquip(null)); store.dispatch(actions.setItemEquip(null));
store.dispatch(actions.setItemUnequip([])); store.dispatch(actions.setItemUnequip([]));
@ -209,16 +210,16 @@ function registerEvents(store) {
} }
function setInstance(v) { function setInstance(v) {
const { account, instance, ws, tutorial } = store.getState(); const { account, ws, tutorial } = store.getState();
if (v) { if (v) {
setInvite(null); setInvite(null);
setPvp(false);
const player = v.players.find(p => p.id === account.id); const player = v.players.find(p => p.id === account.id);
store.dispatch(actions.setPlayer(player)); store.dispatch(actions.setPlayer(player));
if (!instance || v.id !== instance.id) { const skip = v.time_control === 'Practice' && v.phase === 'Lobby';
store.dispatch(actions.setNav('vbox')); if (skip) {
const first = player.constructs[0]; ws.sendInstanceReady(v.id);
store.dispatch(actions.setActiveConstruct(first));
} }
if (v.phase === 'Finished') { if (v.phase === 'Finished') {
@ -364,6 +365,7 @@ function registerEvents(store) {
setItemInfo, setItemInfo,
setInvite, setInvite,
setPing, setPing,
setPvp,
setShop, setShop,
setTeam, setTeam,
setSubscription, setSubscription,

View File

@ -9,7 +9,6 @@ function setupKeys(store) {
key('esc', () => store.dispatch(actions.setCombiner([]))); key('esc', () => store.dispatch(actions.setCombiner([])));
key('esc', () => store.dispatch(actions.setReclaiming(false))); key('esc', () => store.dispatch(actions.setReclaiming(false)));
key('esc', () => store.dispatch(actions.setActiveSkill(null))); key('esc', () => store.dispatch(actions.setActiveSkill(null)));
key('esc', () => store.dispatch(actions.setActiveConstruct(null)));
key('esc', () => store.dispatch(actions.setInfo(null))); key('esc', () => store.dispatch(actions.setInfo(null)));
key('esc', () => store.dispatch(actions.setItemEquip(null))); key('esc', () => store.dispatch(actions.setItemEquip(null)));
key('esc', () => store.dispatch(actions.setItemUnequip([]))); key('esc', () => store.dispatch(actions.setItemUnequip([])));

View File

@ -10,7 +10,6 @@ function createReducer(defaultState, actionType) {
/* eslint-disable key-spacing */ /* eslint-disable key-spacing */
module.exports = { module.exports = {
account: createReducer(null, 'SET_ACCOUNT'), account: createReducer(null, 'SET_ACCOUNT'),
activeConstruct: createReducer(null, 'SET_ACTIVE_CONSTRUCT'),
activeItem: createReducer(null, 'SET_ACTIVE_VAR'), activeItem: createReducer(null, 'SET_ACTIVE_VAR'),
activeSkill: createReducer(null, 'SET_ACTIVE_SKILL'), activeSkill: createReducer(null, 'SET_ACTIVE_SKILL'),
@ -48,8 +47,8 @@ module.exports = {
ping: createReducer(null, 'SET_PING'), ping: createReducer(null, 'SET_PING'),
player: createReducer(null, 'SET_PLAYER'), player: createReducer(null, 'SET_PLAYER'),
reclaiming: createReducer(false, 'SET_RECLAIMING'), reclaiming: createReducer(false, 'SET_RECLAIMING'),
skip: createReducer(false, 'SET_SKIP'),
shop: createReducer(false, 'SET_SHOP'), shop: createReducer(false, 'SET_SHOP'),
pvp: createReducer(null, 'SET_PVP'),
subscription: createReducer(null, 'SET_SUBSCRIPTION'), subscription: createReducer(null, 'SET_SUBSCRIPTION'),

View File

@ -151,6 +151,10 @@ function createSocket(events) {
send(['InstanceQueue', {}]); send(['InstanceQueue', {}]);
} }
function sendInstanceLeave() {
send(['InstanceLeave', {}]);
}
function sendInstanceInvite() { function sendInstanceInvite() {
send(['InstanceInvite', {}]); send(['InstanceInvite', {}]);
} }
@ -274,8 +278,16 @@ function createSocket(events) {
Pong: onPong, Pong: onPong,
Demo: onDemo, Demo: onDemo,
QueueRequested: () => events.notify('PVP queue request received.'), // QueueRequested: () => events.notify('PVP queue request received.'),
QueueJoined: () => events.notify('You have joined the PVP queue.'), QueueRequested: () => true,
QueueJoined: () => {
events.notify('You have joined the PVP queue.');
events.setPvp(true);
},
QueueLeft: () => {
events.notify('You have left the PVP queue.');
events.setPvp(false);
},
QueueFound: () => events.notify('Your PVP game has started.'), QueueFound: () => events.notify('Your PVP game has started.'),
InviteRequested: () => events.notify('PVP invite request received.'), InviteRequested: () => events.notify('PVP invite request received.'),
Invite: code => events.setInvite(code), Invite: code => events.setInvite(code),
@ -318,11 +330,8 @@ function createSocket(events) {
return handlers[msgType](params); return handlers[msgType](params);
} }
let attempts = 1;
// Connection opened // Connection opened
function onOpen() { function onOpen() {
attempts = 0;
toast.info({ toast.info({
message: 'connected', message: 'connected',
position: 'topRight', position: 'topRight',
@ -341,21 +350,12 @@ function createSocket(events) {
} }
function onClose(event) { function onClose(event) {
attempts *= 2;
if (attempts > 10) {
toast.warning({
message: 'unable to connect, refreshing...',
position: 'topRight',
});
setTimeout(() => window.location.reload(true), 2000);
}
console.error('WebSocket closed', event); console.error('WebSocket closed', event);
toast.warning({ toast.warning({
message: 'disconnected', message: 'disconnected',
position: 'topRight', position: 'topRight',
}); });
return setTimeout(connect, attempts * 1000); return setTimeout(connect, 5000);
} }
function connect() { function connect() {
@ -402,6 +402,7 @@ function createSocket(events) {
sendInstanceInvite, sendInstanceInvite,
sendInstanceJoin, sendInstanceJoin,
sendInstanceChat, sendInstanceChat,
sendInstanceLeave,
sendVboxAccept, sendVboxAccept,
sendVboxAcceptEquip, sendVboxAcceptEquip,

View File

@ -109,7 +109,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
const tutorialText = () => { const tutorialText = () => {
if (tutorial === 1) { if (tutorial === 1) {
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> Welcome to the vbox phase tutorial.</p> <p> Welcome to the vbox phase tutorial.</p>
<p> Colours are used to create powerful combinations. </p> <p> Colours are used to create powerful combinations. </p>
@ -121,7 +121,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
if (tutorial === 2) { if (tutorial === 2) {
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> In a normal game you start with three base <b>Attack</b> skill items. </p> <p> In a normal game you start with three base <b>Attack</b> skill items. </p>
<p> The <b>Attack</b> item can be combined with <b>colours</b> to create a new skill. </p> <p> The <b>Attack</b> item can be combined with <b>colours</b> to create a new skill. </p>
@ -135,7 +135,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
if (tutorial === 3) { if (tutorial === 3) {
const constructOne = instance.players[0].constructs[0].name; const constructOne = instance.players[0].constructs[0].name;
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> The first construct on your team is <b>{constructOne}</b>. </p> <p> The first construct on your team is <b>{constructOne}</b>. </p>
<p> Skill items can be equipped to your constructs to be used in the combat phase. </p> <p> Skill items can be equipped to your constructs to be used in the combat phase. </p>
@ -147,7 +147,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
if (tutorial === 4) { if (tutorial === 4) {
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> You can also buy specialisation items for your constructs. <br /> <p> You can also buy specialisation items for your constructs. <br />
Specialisation items increase stats including power, speed and life. </p> Specialisation items increase stats including power, speed and life. </p>
@ -159,7 +159,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
if (tutorial === 5) { if (tutorial === 5) {
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> Equipping specialisation items will increase the stats of your constructs.</p> <p> Equipping specialisation items will increase the stats of your constructs.</p>
<p> These can also be combined with colours for further specialisation. </p> <p> These can also be combined with colours for further specialisation. </p>
@ -173,7 +173,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
const constructTwo = instance.players[0].constructs[1].name; const constructTwo = instance.players[0].constructs[1].name;
const constructThree = instance.players[0].constructs[2].name; const constructThree = instance.players[0].constructs[2].name;
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> You have now created a construct with an upgraded skill and base spec. </p> <p> You have now created a construct with an upgraded skill and base spec. </p>
<p> The goal is to create three powerful constructs for combat. </p> <p> The goal is to create three powerful constructs for combat. </p>
@ -185,7 +185,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
if (tutorial === 7) { if (tutorial === 7) {
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p> Each round you start with a vbox full of different skills, specs and colours. </p> <p> Each round you start with a vbox full of different skills, specs and colours. </p>
<p> Bits are your currency for buying skills, specs and colours from the vbox. <br /> <p> Bits are your currency for buying skills, specs and colours from the vbox. <br />
@ -204,7 +204,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
} }
return ( return (
<div> <div class='info-item'>
<h2>Tutorial</h2> <h2>Tutorial</h2>
<p>You've completed the tutorial! Try to create more skill and spec combinations. </p> <p>You've completed the tutorial! Try to create more skill and spec combinations. </p>
<p>You can unequip skills and specs back into the inventory by double clicking. <br /> <p>You can unequip skills and specs back into the inventory by double clicking. <br />
@ -216,7 +216,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {
}; };
const classes = tutorial === 8 ? 'focus' : ''; const classes = tutorial === 8 ? 'focus' : '';
const text = tutorial === 8 ? 'Continue' : 'Close Tutorial' const text = tutorial === 8 ? 'Continue' : 'Skip Tutorial'
const exitTutorial = <button const exitTutorial = <button
class={classes} class={classes}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
@ -235,5 +235,5 @@ module.exports = {
tutorialConstructDisplay, tutorialConstructDisplay,
tutorialVbox, tutorialVbox,
tutorialStage, tutorialStage,
tutorialShouldDisableEquip tutorialShouldDisableEquip,
}; };

View File

@ -266,7 +266,7 @@ function effectInfo(i) {
} }
switch (i.effect) { switch (i.effect) {
case 'Amplify': return `Increases construct RedPower BluePower GreenPower by ${i.meta[1] - 100}%`; case 'Amplify': return `Increases construct RedPower BluePower by ${i.meta[1] - 100}%`;
case 'Banish': return 'Banished construct cannot cast or take damage'; case 'Banish': return 'Banished construct cannot cast or take damage';
case 'Block': return `Reduces construct red damage and blue damage taken by ${100 - i.meta[1]}%`; case 'Block': return `Reduces construct red damage and blue damage taken by ${100 - i.meta[1]}%`;
case 'Buff': return `Increases construct RedPower BluePower SpeedStat by ${i.meta[1] - 100}%`; case 'Buff': return `Increases construct RedPower BluePower SpeedStat by ${i.meta[1] - 100}%`;
@ -282,6 +282,7 @@ function effectInfo(i) {
case 'Restrict': return 'Disable construct from casting any red skills'; case 'Restrict': return 'Disable construct from casting any red skills';
case 'Stun': return 'Stunned construct cannot use skills'; case 'Stun': return 'Stunned construct cannot use skills';
case 'Intercept': return 'Redirect any skills on team to this target construct'; case 'Intercept': return 'Redirect any skills on team to this target construct';
case 'Pure': return `Construct will take ${i.meta[1] - 100}% increased healing`;
case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`; case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`;
case 'Silence': return 'Disable construct from casting any blue skills'; case 'Silence': return 'Disable construct from casting any blue skills';
case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; // case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; //

View File

@ -1,12 +1,15 @@
[Unit] [Unit]
Description=mnml game server Description=mnml game server
User=mnml
[Service] [Service]
User=mnml
ExecStart=/usr/local/mnml/bin/mnml ExecStart=/usr/local/mnml/bin/mnml
KillMode=process KillMode=process
Restart=on-failure Restart=on-failure
RestartPreventExitStatus=255 RestartPreventExitStatus=255
#StandardOutput=file:/var/log/mnml/mnml.log
StandardOutput=null
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -295,11 +295,13 @@
## Uncomment to remove deprecated metrics. ## Uncomment to remove deprecated metrics.
# fielddrop = ["uptime_format"] # fielddrop = ["uptime_format"]
[[inputs.tail]] # [[inputs.tail]]
files = ["/var/log/mnml/mnml.log"] # files = ["/var/log/mnml/mnml.log"]
name_override = "mnml_log" # name_override = "mnml_log"
data_format = "json" # data_format = "json"
json_time_key = "time" # json_time_key = "time"
json_time_format = "2006-01-02T15:04:05.999999999Z07:00" # json_time_format = "2006-01-02T15:04:05.999999999Z07:00"
json_string_fields = ["level", "module", "msg"] # json_string_fields = ["level", "module", "msg"]
[[inputs.syslog]]
server = "tcp://:6514"

View File

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

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mnml" name = "mnml"
version = "1.8.1" version = "1.8.3"
authors = ["ntr <ntr@smokestack.io>"] authors = ["ntr <ntr@smokestack.io>"]
[dependencies] [dependencies]
@ -23,7 +23,8 @@ failure = "0.1"
dotenv = "0.9.0" dotenv = "0.9.0"
log = "0.4" log = "0.4"
fern = { version = "0.5", features = ["colored"] } fern = { version = "0.5", features = ["colored", "syslog-4"] }
syslog = "4"
iron = "0.6" iron = "0.6"
bodyparser = "0.8" bodyparser = "0.8"

View File

@ -12,6 +12,7 @@ use http::MnmlHttpError;
use names::{name as generate_name}; use names::{name as generate_name};
use construct::{Construct, ConstructSkeleton, construct_spawn}; use construct::{Construct, ConstructSkeleton, construct_spawn};
use instance::{Instance, instance_delete}; use instance::{Instance, instance_delete};
use instance;
use mtx::{Mtx, FREE_MTX}; use mtx::{Mtx, FREE_MTX};
use pg::Db; use pg::Db;
use img; use img;
@ -21,6 +22,7 @@ use failure::Error;
use failure::{err_msg, format_err}; use failure::{err_msg, format_err};
static PASSWORD_MIN_LEN: usize = 3; static PASSWORD_MIN_LEN: usize = 3;
static PASSWORD_ROUNDS: u32 = 10;
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Account { pub struct Account {
@ -69,11 +71,11 @@ pub fn chat_wheel(_db: &Db, _id: Uuid) -> Result<Vec<String>, Error> {
return Ok(vec![ return Ok(vec![
"gg".to_string(), "gg".to_string(),
"glhf".to_string(), "glhf".to_string(),
"hmm".to_string(), "ez".to_string(),
"ok".to_string(),
"rekt".to_string(), "rekt".to_string(),
"thx".to_string(), "wow".to_string(),
"nice".to_string(), "wp".to_string(),
"ok".to_string(),
"...".to_string(), "...".to_string(),
]) ])
} }
@ -225,8 +227,7 @@ pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password:
return Err(MnmlHttpError::BadRequest); return Err(MnmlHttpError::BadRequest);
} }
let rounds = 8; let password = hash(&password, PASSWORD_ROUNDS)?;
let password = hash(&password, rounds)?;
let query = " let query = "
UPDATE accounts UPDATE accounts
@ -326,10 +327,13 @@ pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result<
return Err(MnmlHttpError::AccountNameNotProvided); return Err(MnmlHttpError::AccountNameNotProvided);
} }
if name.len() > 20 {
return Err(MnmlHttpError::AccountNameUnacceptable);
}
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let img = Uuid::new_v4(); let img = Uuid::new_v4();
let rounds = 12; let password = hash(&password, PASSWORD_ROUNDS)?;
let password = hash(&password, rounds)?;
let mut rng = thread_rng(); let mut rng = thread_rng();
let token: String = iter::repeat(()) let token: String = iter::repeat(())
@ -502,3 +506,25 @@ pub fn img_check(account: &Account) -> Result<Uuid, Error> {
false => Ok(account.img), false => Ok(account.img),
} }
} }
pub fn tutorial(tx: &mut Transaction, account: &Account) -> Result<Option<Instance>, Error> {
let query = "
SELECT count(id)
FROM players
WHERE account = $1;
";
let result = tx
.query(query, &[&account.id])?;
let row = result.iter().next()
.ok_or(format_err!("unable to fetch joined games account={:?}", account))?;
let count: i64 = row.get(0);
if count == 0 {
return Ok(Some(instance::instance_practice(tx, account)?));
}
return Ok(None);
}

View File

@ -10,20 +10,21 @@ pub enum Effect {
Banish, Banish,
Block, Block,
Buff, Buff,
Sustain, Counter,
Curse, Curse,
Haste, Haste,
Hybrid, Hybrid,
Intercept,
Invert, Invert,
Counter, Pure,
Purge, Purge,
Reflect, Reflect,
Slow,
Restrict, Restrict,
Stun,
Intercept,
Vulnerable,
Silence, Silence,
Slow,
Stun,
Sustain,
Vulnerable,
Wither, // Reduce green dmg (healing) taken Wither, // Reduce green dmg (healing) taken
// electric is the buff that applies // electric is the buff that applies
@ -99,21 +100,25 @@ impl Effect {
pub fn modifications(&self) -> Vec<Stat> { pub fn modifications(&self) -> Vec<Stat> {
match self { match self {
Effect::Vulnerable => vec![Stat::RedDamageTaken], // Bases
Effect::Block => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken], Effect::Block => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
Effect::Buff => vec![Stat::BluePower, Stat::RedPower, Stat::Speed], Effect::Buff => vec![Stat::BluePower, Stat::RedPower, Stat::Speed],
Effect::Absorption => vec![Stat::RedPower, Stat::BluePower],
Effect::Amplify => vec![Stat::GreenPower, Stat::RedPower, Stat::BluePower],
Effect::Curse => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
Effect::Hybrid => vec![Stat::GreenPower],
Effect::Wither => vec![Stat::GreenDamageTaken],
Effect::Haste => vec![Stat::Speed],
Effect::Slow => vec![Stat::Speed], Effect::Slow => vec![Stat::Speed],
// Power changes
Effect::Absorption => vec![Stat::RedPower, Stat::BluePower],
Effect::Amplify => vec![Stat::RedPower, Stat::BluePower],
Effect::Hybrid => vec![Stat::GreenPower],
// Damage taken changes
Effect::Curse => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
Effect::Pure => vec![Stat::GreenDamageTaken], // increased green taken
Effect::Vulnerable => vec![Stat::RedDamageTaken],
Effect::Wither => vec![Stat::GreenDamageTaken], // reduced green taken
// Speed
Effect::Haste => vec![Stat::Speed],
_ => vec![], _ => vec![],
} }
} }
@ -128,6 +133,7 @@ impl Effect {
Effect::Haste | Effect::Haste |
Effect::Slow | Effect::Slow |
Effect::Hybrid | Effect::Hybrid |
Effect::Pure |
Effect::Wither => value.pct(match meta { Effect::Wither => value.pct(match meta {
Some(EffectMeta::Multiplier(d)) => d, Some(EffectMeta::Multiplier(d)) => d,
_ => 100, _ => 100,
@ -135,7 +141,10 @@ impl Effect {
Effect::Absorption => value + match meta { Effect::Absorption => value + match meta {
Some(EffectMeta::AddedDamage(d)) => d, Some(EffectMeta::AddedDamage(d)) => d,
_ => panic!("absorb meta not damage"), _ => {
warn!("absorb meta not damage");
return 0;
}
}, },
_ => { _ => {
@ -185,6 +194,7 @@ impl Effect {
Effect::Decay => Some(Colour::Blue), Effect::Decay => Some(Colour::Blue),
Effect::Regen => Some(Colour::Green), Effect::Regen => Some(Colour::Green),
Effect::Siphon => Some(Colour::Blue), Effect::Siphon => Some(Colour::Blue),
Effect::Pure => Some(Colour::Green),
Effect::Ko => None, Effect::Ko => None,
} }

View File

@ -60,6 +60,7 @@ pub enum Event {
Invite(Id), Invite(Id),
Join(Id, String), Join(Id, String),
Joined(Id), Joined(Id),
Leave(Id),
Chat(Id, Uuid, String), Chat(Id, Uuid, String),
ChatClear(Id, Uuid), ChatClear(Id, Uuid),
@ -249,6 +250,7 @@ impl Events {
// or flag the requester as pvp ready // or flag the requester as pvp ready
let requester = self.clients.get_mut(&id).unwrap(); let requester = self.clients.get_mut(&id).unwrap();
requester.pvp = true; requester.pvp = true;
requester.tx.send(RpcMessage::QueueJoined(()))?;
info!("joined game queue id={:?} account={:?}", requester.id, requester.account); info!("joined game queue id={:?} account={:?}", requester.id, requester.account);
return Ok(()); return Ok(());
}, },
@ -305,6 +307,18 @@ impl Events {
return Ok(()); return Ok(());
}, },
Event::Leave(id) => {
// check whether request is valid
let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?;
c.pvp = false;
c.tx.send(RpcMessage::QueueLeft(()))?;
info!("left game queue id={:?} account={:?}", c.id, c.account);
return Ok(());
},
Event::Chat(id, instance, msg) => { Event::Chat(id, instance, msg) => {
// set the chat state of this connection // set the chat state of this connection
{ {

View File

@ -469,14 +469,14 @@ impl Game {
let mut resolutions = resolution_steps(&cast, &mut self); let mut resolutions = resolution_steps(&cast, &mut self);
r_animation_ms = resolutions.iter().fold(r_animation_ms, |acc, r| acc + r.clone().get_delay()); r_animation_ms = resolutions.iter().fold(r_animation_ms, |acc, r| acc + r.clone().get_delay());
// the cast itself goes into this temp vec to handle cooldowns // the cast itself goes into this temp vec to handle cooldowns
// if theres no resolution events, the skill didn't trigger (disable etc) // if theres no resolution events, the skill didn't trigger (disable etc)
if resolutions.len() > 0 { if resolutions.len() > 0 {
casts.push(cast); casts.push(cast);
} }
self.resolved.append(&mut resolutions); self.resolved.append(&mut resolutions);
// while let Some(resolution) = resolutions.pop() { // while let Some(resolution) = resolutions.pop() {
@ -635,15 +635,15 @@ impl Game {
for player in self.players.iter_mut() { for player in self.players.iter_mut() {
if !player.ready { if !player.ready {
player.set_ready(true); player.set_ready(true);
player.add_warning(); // player.add_warning();
info!("upkeep: {:} warned", player.name); // info!("upkeep: {:} warned", player.name);
if player.warnings >= 3 { // if player.warnings >= 3 {
player.forfeit(); // player.forfeit();
info!("upkeep: {:} forfeited", player.name); // info!("upkeep: {:} forfeited", player.name);
//todo // //todo
// self.resolved.push(forfeit) // // self.resolved.push(forfeit)
// self.log.push(format!("{:} forfeited.", player.name)); // // self.log.push(format!("{:} forfeited.", player.name));
} // }
} }
} }
@ -1622,6 +1622,6 @@ mod tests {
game.players[0].set_ready(true); game.players[0].set_ready(true);
game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap()); game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap());
game = game.upkeep(); game = game.upkeep();
assert!(game.players[1].warnings == 1); // assert!(game.players[1].warnings == 1);
} }
} }

View File

@ -44,6 +44,8 @@ pub enum MnmlHttpError {
AccountNameNotProvided, AccountNameNotProvided,
#[fail(display="account name unavailable")] #[fail(display="account name unavailable")]
AccountNameUnavailable, AccountNameUnavailable,
#[fail(display="account name is unacceptable. 20 char max")]
AccountNameUnacceptable,
#[fail(display="account not found")] #[fail(display="account not found")]
AccountNotFound, AccountNotFound,
#[fail(display="password does not match")] #[fail(display="password does not match")]
@ -122,6 +124,7 @@ impl From<MnmlHttpError> for IronError {
MnmlHttpError::AccountNameNotProvided | MnmlHttpError::AccountNameNotProvided |
MnmlHttpError::AccountNameUnavailable | MnmlHttpError::AccountNameUnavailable |
MnmlHttpError::AccountNameUnacceptable |
MnmlHttpError::AccountNotFound | MnmlHttpError::AccountNotFound |
MnmlHttpError::BadRequest | MnmlHttpError::BadRequest |
MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest), MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest),

View File

@ -318,12 +318,12 @@ impl Instance {
self.phase_end = self.time_control.vbox_phase_end(); self.phase_end = self.time_control.vbox_phase_end();
let bits = match self.rounds.len() > 0 { let bits = match self.rounds.len() > 0 {
true => 12 + 6 * self.rounds.len(), true => 30,
false => 0, false => 0,
}; };
self.players.iter_mut().for_each(|p| { self.players.iter_mut().for_each(|p| {
p.vbox.balance_add(bits.into()); p.vbox.balance_add(bits);
p.set_ready(false); p.set_ready(false);
p.vbox.fill(); p.vbox.fill();
}); });

View File

@ -712,7 +712,7 @@ impl Item {
// Skills <- need to move effect mulltipliers into skills // Skills <- need to move effect mulltipliers into skills
Item::Amplify| Item::Amplify|
Item::AmplifyPlus | Item::AmplifyPlus |
Item::AmplifyPlusPlus => format!("Increase RedPower BluePower GreenPower by {:?}%. Lasts {:?}T.", Item::AmplifyPlusPlus => format!("Increase RedPower BluePower by {:?}%. Lasts {:?}T.",
self.into_skill().unwrap().effect()[0].get_multiplier() - 100, self.into_skill().unwrap().effect()[0].get_multiplier() - 100,
self.into_skill().unwrap().effect()[0].get_duration()), self.into_skill().unwrap().effect()[0].get_duration()),
@ -824,15 +824,17 @@ impl Item {
Item::Purge| Item::Purge|
Item::PurgePlus | Item::PurgePlus |
Item::PurgePlusPlus => format!( Item::PurgePlusPlus => format!(
"Remove buffs from target construct. "Remove all effects from target construct.
Applies purge disabling target green skills for {:?}T.", Applies purge disabling target green skills for {:?}T.",
self.into_skill().unwrap().effect()[0].get_duration()), self.into_skill().unwrap().effect()[0].get_duration()),
Item::Purify| Item::Purify|
Item::PurifyPlus | Item::PurifyPlus |
Item::PurifyPlusPlus => format!( Item::PurifyPlusPlus => format!(
"Remove debuffs and heals for {:?}% GreenPower per debuff removed.", "Remove all effects and heals for {:?}% GreenPower per effect removed.
self.into_skill().unwrap().multiplier()), Applies Pure increasing healing taken by {:?}%.",
self.into_skill().unwrap().multiplier(),
self.into_skill().unwrap().effect()[0].get_multiplier() - 100),
Item::Reflect| Item::Reflect|
Item::ReflectPlus | Item::ReflectPlus |
@ -898,11 +900,10 @@ impl Item {
Item::Bash| Item::Bash|
Item::BashPlus | Item::BashPlus |
Item::BashPlusPlus => format!( Item::BashPlusPlus => format!(
"Bash the target increasing the cooldowns of target skills by 1T. "Bash the target increasing the cooldowns of target skills by 1T. Stuns target for {:?}T.
Deals {:?}% RedPower as red damage and 45% more damage per cooldown increased. Deals {:?}% RedPower as red damage and 45% more damage per cooldown increased.",
Stuns for {:?}T.", self.into_skill().unwrap().effect()[0].get_duration(),
self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier()),
self.into_skill().unwrap().effect()[0].get_duration()),
Item::Strike| Item::Strike|
Item::StrikePlus | Item::StrikePlus |
@ -1318,25 +1319,18 @@ pub fn get_combos() -> Vec<Combo> {
Combo { components: Item::Intercept.combo(), item: Item::Intercept }, Combo { components: Item::Intercept.combo(), item: Item::Intercept },
Combo { components: Item::InterceptPlus.combo(), item: Item::InterceptPlus }, Combo { components: Item::InterceptPlus.combo(), item: Item::InterceptPlus },
Combo { components: Item::InterceptPlusPlus.combo(), item: Item::InterceptPlusPlus }, Combo { components: Item::InterceptPlusPlus.combo(), item: Item::InterceptPlusPlus },
Combo { components: Item::Triage.combo(), item: Item::Triage }, Combo { components: Item::Triage.combo(), item: Item::Triage },
Combo { components: Item::TriagePlus.combo(), item: Item::TriagePlus }, Combo { components: Item::TriagePlus.combo(), item: Item::TriagePlus },
Combo { components: Item::TriagePlusPlus.combo(), item: Item::TriagePlusPlus }, Combo { components: Item::TriagePlusPlus.combo(), item: Item::TriagePlusPlus },
Combo { components: Item::Link.combo(), item: Item::Link },
Combo { components: Item::LinkPlus.combo(), item: Item::LinkPlus },
Combo { components: Item::LinkPlusPlus.combo(), item: Item::LinkPlusPlus },
Combo { components: Item::Haste.combo(), item: Item::Haste },
Combo { components: Item::HastePlus.combo(), item: Item::HastePlus },
Combo { components: Item::HastePlusPlus.combo(), item: Item::HastePlusPlus },
Combo { components: Item::Absorb.combo(), item: Item::Absorb }, Combo { components: Item::Absorb.combo(), item: Item::Absorb },
Combo { components: Item::AbsorbPlus.combo(), item: Item::AbsorbPlus }, Combo { components: Item::AbsorbPlus.combo(), item: Item::AbsorbPlus },
Combo { components: Item::AbsorbPlusPlus.combo(), item: Item::AbsorbPlusPlus }, Combo { components: Item::AbsorbPlusPlus.combo(), item: Item::AbsorbPlusPlus },
Combo { components: Item::Haste.combo(), item: Item::Haste },
Combo { components: Item::HastePlus.combo(), item: Item::HastePlus },
Combo { components: Item::HastePlusPlus.combo(), item: Item::HastePlusPlus },
Combo { components: Item::Hybrid.combo(), item: Item::Hybrid }, Combo { components: Item::Hybrid.combo(), item: Item::Hybrid },
Combo { components: Item::HybridPlus.combo(), item: Item::HybridPlus }, Combo { components: Item::HybridPlus.combo(), item: Item::HybridPlus },
Combo { components: Item::HybridPlusPlus.combo(), item: Item::HybridPlusPlus }, Combo { components: Item::HybridPlusPlus.combo(), item: Item::HybridPlusPlus },
Combo { components: Item::Amplify.combo(), item: Item::Amplify }, Combo { components: Item::Amplify.combo(), item: Item::Amplify },
Combo { components: Item::AmplifyPlus.combo(), item: Item::AmplifyPlus }, Combo { components: Item::AmplifyPlus.combo(), item: Item::AmplifyPlus },
Combo { components: Item::AmplifyPlusPlus.combo(), item: Item::AmplifyPlusPlus }, Combo { components: Item::AmplifyPlusPlus.combo(), item: Item::AmplifyPlusPlus },
@ -1353,15 +1347,15 @@ pub fn get_combos() -> Vec<Combo> {
Combo { components: Item::SilencePlusPlus.combo(), item: Item::SilencePlusPlus }, Combo { components: Item::SilencePlusPlus.combo(), item: Item::SilencePlusPlus },
Combo { components: Item::Invert.combo(), item: Item::Invert },
Combo { components: Item::InvertPlus.combo(), item: Item::InvertPlus },
Combo { components: Item::InvertPlusPlus.combo(), item: Item::InvertPlusPlus },
Combo { components: Item::Curse.combo(), item: Item::Curse }, Combo { components: Item::Curse.combo(), item: Item::Curse },
Combo { components: Item::CursePlus.combo(), item: Item::CursePlus }, Combo { components: Item::CursePlus.combo(), item: Item::CursePlus },
Combo { components: Item::CursePlusPlus.combo(), item: Item::CursePlusPlus }, Combo { components: Item::CursePlusPlus.combo(), item: Item::CursePlusPlus },
Combo { components: Item::Decay.combo(), item: Item::Decay }, Combo { components: Item::Decay.combo(), item: Item::Decay },
Combo { components: Item::DecayPlus.combo(), item: Item::DecayPlus }, Combo { components: Item::DecayPlus.combo(), item: Item::DecayPlus },
Combo { components: Item::DecayPlusPlus.combo(), item: Item::DecayPlusPlus }, Combo { components: Item::DecayPlusPlus.combo(), item: Item::DecayPlusPlus },
Combo { components: Item::Invert.combo(), item: Item::Invert },
Combo { components: Item::InvertPlus.combo(), item: Item::InvertPlus },
Combo { components: Item::InvertPlusPlus.combo(), item: Item::InvertPlusPlus },
Combo { components: Item::Counter.combo(), item: Item::Counter }, Combo { components: Item::Counter.combo(), item: Item::Counter },
Combo { components: Item::CounterPlus.combo(), item: Item::CounterPlus }, Combo { components: Item::CounterPlus.combo(), item: Item::CounterPlus },
@ -1399,6 +1393,9 @@ pub fn get_combos() -> Vec<Combo> {
Combo { components: Item::Break.combo(), item: Item::Break }, Combo { components: Item::Break.combo(), item: Item::Break },
Combo { components: Item::BreakPlus.combo(), item: Item::BreakPlus }, Combo { components: Item::BreakPlus.combo(), item: Item::BreakPlus },
Combo { components: Item::BreakPlusPlus.combo(), item: Item::BreakPlusPlus }, Combo { components: Item::BreakPlusPlus.combo(), item: Item::BreakPlusPlus },
Combo { components: Item::Link.combo(), item: Item::Link },
Combo { components: Item::LinkPlus.combo(), item: Item::LinkPlus },
Combo { components: Item::LinkPlusPlus.combo(), item: Item::LinkPlusPlus },
Combo { components: Item::Banish.combo(), item: Item::Banish }, Combo { components: Item::Banish.combo(), item: Item::Banish },
Combo { components: Item::BanishPlus.combo(), item: Item::BanishPlus }, Combo { components: Item::BanishPlus.combo(), item: Item::BanishPlus },
Combo { components: Item::BanishPlusPlus.combo(), item: Item::BanishPlusPlus }, Combo { components: Item::BanishPlusPlus.combo(), item: Item::BanishPlusPlus },

View File

@ -62,15 +62,18 @@ use std::path::{Path};
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use crossbeam_channel::{unbounded}; use crossbeam_channel::{unbounded};
#[derive(Serialize)]
struct JsonLog {
time: String,
module: String,
level: String,
msg: String,
}
pub fn setup_logger() -> Result<(), fern::InitError> { pub fn setup_logger() -> Result<(), fern::InitError> {
let formatter = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER,
hostname: None,
process: "mnml".to_owned(),
pid: 0,
};
let syslog = fern::Dispatch::new()
.level(log::LevelFilter::Info)
.chain(syslog::unix(formatter).unwrap());
let colors_line = ColoredLevelConfig::new() let colors_line = ColoredLevelConfig::new()
.error(Color::Red) .error(Color::Red)
.warn(Color::Yellow) .warn(Color::Yellow)
@ -90,31 +93,16 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
message = message, message = message,
)); ));
}) })
.chain(fern::log_file("/var/log/mnml/mnml.log")?)
.chain(std::io::stdout()); .chain(std::io::stdout());
let json = fern::Dispatch::new()
.format(|out, message, record| {
let json = JsonLog {
time: chrono::Local::now().to_rfc3339(),
module: record.target().to_string(),
level: record.level().to_string(),
msg: message.to_string()
};
out.finish(format_args!(
"{}",
serde_json::to_string(&json).unwrap(),
))
})
.chain(fern::log_file("/var/log/mnml/mnml.log")?);
fern::Dispatch::new() fern::Dispatch::new()
.level_for("postgres", log::LevelFilter::Info) .level_for("postgres", log::LevelFilter::Info)
.level_for("ws", log::LevelFilter::Warn) .level_for("ws", log::LevelFilter::Warn)
.level_for("iron", log::LevelFilter::Info) .level_for("iron", log::LevelFilter::Info)
.level(log::LevelFilter::Info) .level(log::LevelFilter::Info)
.chain(term) .chain(term)
.chain(json) .chain(syslog)
.apply()?; .apply()?;
Ok(()) Ok(())

View File

@ -44,7 +44,7 @@ const FIRSTS: [&'static str; 53] = [
"orbiting", "orbiting",
"piscine", "piscine",
"polar", "polar",
"purified", "pure",
"recalcitrant", "recalcitrant",
"rogue", "rogue",
"sealed", "sealed",

View File

@ -77,11 +77,11 @@ fn handle_notification(n: Notification, pool: &PgPool, events: &Sender<Event>) {
let msg = match n.action { let msg = match n.action {
Action::Delete => { Action::Delete => {
warn!("unimplemented delete notification {:?}", n); // warn!("unimplemented delete notification {:?}", n);
None None
}, },
Action::Insert => { Action::Insert => {
warn!("unimplemented insert notification {:?}", n); // warn!("unimplemented insert notification {:?}", n);
None None
}, },
Action::Update => match n.table { Action::Update => match n.table {
@ -92,7 +92,7 @@ fn handle_notification(n: Notification, pool: &PgPool, events: &Sender<Event>) {
Table::Games => Table::Games =>
Some(Event::Push(n.id, RpcMessage::GameState(game::game_get(&mut tx, n.id).unwrap()))), Some(Event::Push(n.id, RpcMessage::GameState(game::game_get(&mut tx, n.id).unwrap()))),
_ => { _ => {
warn!("unimplemented update notification {:?}", n); // warn!("unimplemented update notification {:?}", n);
None None
}, },
}, },

View File

@ -63,7 +63,6 @@ pub struct Player {
pub constructs: Vec<Construct>, pub constructs: Vec<Construct>,
pub bot: bool, pub bot: bool,
pub ready: bool, pub ready: bool,
pub warnings: u8,
pub draw_offered: bool, pub draw_offered: bool,
pub score: Score, pub score: Score,
} }
@ -85,7 +84,6 @@ impl Player {
constructs, constructs,
bot: false, bot: false,
ready: false, ready: false,
warnings: 0,
draw_offered: false, draw_offered: false,
score: Score::Zero, score: Score::Zero,
}) })
@ -100,7 +98,6 @@ impl Player {
constructs, constructs,
bot: false, bot: false,
ready: false, ready: false,
warnings: 0,
draw_offered: false, draw_offered: false,
score: Score::Zero, score: Score::Zero,
} }
@ -134,11 +131,6 @@ impl Player {
self self
} }
pub fn add_warning(&mut self) -> &mut Player {
self.warnings += 1;
self
}
pub fn forfeit(&mut self) -> &mut Player { pub fn forfeit(&mut self) -> &mut Player {
for construct in self.constructs.iter_mut() { for construct in self.constructs.iter_mut() {
construct.force_ko(); construct.force_ko();
@ -358,7 +350,7 @@ impl Player {
} }
self.vbox.bound.push(target); self.vbox.bound.push(target);
if target_construct_id.is_some() { if target_construct_id.is_some() {
let equip_index = self.vbox.bound.len() - 1; let equip_index = self.vbox.bound.len() - 1;
self.vbox_apply(equip_index, target_construct_id.expect("no construct"))?; self.vbox_apply(equip_index, target_construct_id.expect("no construct"))?;

View File

@ -63,7 +63,7 @@ pub enum RpcMessage {
QueueRequested(()), QueueRequested(()),
QueueJoined(()), QueueJoined(()),
QueueCancelled(()), QueueLeft(()),
QueueFound(()), QueueFound(()),
InviteRequested(()), InviteRequested(()),
@ -105,6 +105,7 @@ pub enum RpcRequest {
InstanceInvite {}, InstanceInvite {},
InstanceJoin { code: String }, InstanceJoin { code: String },
InstanceQueue {}, InstanceQueue {},
InstanceLeave {},
InstancePractice {}, InstancePractice {},
InstanceAbandon { instance_id: Uuid }, InstanceAbandon { instance_id: Uuid },
InstanceReady { instance_id: Uuid }, InstanceReady { instance_id: Uuid },
@ -168,6 +169,10 @@ impl Connection {
self.events.send(Event::Join(self.id, code))?; self.events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(())) Ok(RpcMessage::Joining(()))
}, },
RpcRequest::InstanceLeave {} => {
self.events.send(Event::Leave(self.id))?;
Ok(RpcMessage::Processing(()))
},
RpcRequest::InstanceChat { instance_id, index } => { RpcRequest::InstanceChat { instance_id, index } => {
if !account.subscribed { if !account.subscribed {
@ -353,6 +358,10 @@ impl Handler for Connection {
let wheel = account::chat_wheel(&db, a.id).unwrap(); let wheel = account::chat_wheel(&db, a.id).unwrap();
self.send(RpcMessage::ChatWheel(wheel)).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 should do nothing
tx.commit().unwrap(); tx.commit().unwrap();
} else { } else {

View File

@ -299,9 +299,9 @@ pub fn resolve(skill: Skill, source: &mut Construct, target: &mut Construct, mut
} }
fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) -> Resolutions { fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) -> Resolutions {
for Resolution { source, target, event, stages: _ } in resolutions.clone() { for Resolution { source: event_source, target: event_target, event, stages: _ } in resolutions.clone() {
let mut source = game.construct_by_id(source.id).unwrap().clone(); let mut source = game.construct_by_id(event_source.id).unwrap().clone();
let mut target = game.construct_by_id(target.id).unwrap().clone(); let mut target = game.construct_by_id(event_target.id).unwrap().clone();
match event { match event {
Event::Damage { amount, skill, mitigation, colour: c } => { Event::Damage { amount, skill, mitigation, colour: c } => {
@ -352,7 +352,8 @@ fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) ->
_ => (), _ => (),
}; };
if target.is_ko() { if target.is_ko() && event_target.green == 0 {
// Make sure target ko is from this event
target.effects.clear(); target.effects.clear();
resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly)); resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly));
} }
@ -505,7 +506,7 @@ pub enum Event {
AoeSkill { skill: Skill }, AoeSkill { skill: Skill },
Skill { skill: Skill }, Skill { skill: Skill },
Effect { skill: Skill, effect: Effect, duration: u8, construct_effects: Vec<ConstructEffect> }, Effect { skill: Skill, effect: Effect, duration: u8, construct_effects: Vec<ConstructEffect> },
Removal { effect: Effect, construct_effects: Vec<ConstructEffect> }, Removal { skill: Skill, effect: Option<Effect>, construct_effects: Vec<ConstructEffect> },
TargetKo { skill: Skill }, TargetKo { skill: Skill },
// skill not necessary but makes it neater as all events are arrays in js // skill not necessary but makes it neater as all events are arrays in js
Ko (), Ko (),
@ -848,7 +849,7 @@ impl Skill {
Skill::HybridBlast => 50, Skill::HybridBlast => 50,
Skill::HasteStrike => 60, Skill::HasteStrike => 60,
Skill::Absorb=> 95, Skill::Absorb=> 95,
Skill::AbsorbPlus => 120, Skill::AbsorbPlus => 120,
Skill::AbsorbPlusPlus => 155, Skill::AbsorbPlusPlus => 155,
@ -1022,6 +1023,14 @@ impl Skill {
meta: Some(EffectMeta::Skill(Skill::TriageTickPlus)), tick: None}], meta: Some(EffectMeta::Skill(Skill::TriageTickPlus)), tick: None}],
Skill::TriagePlusPlus => vec![ConstructEffect {effect: Effect::Triage, duration: 4, Skill::TriagePlusPlus => vec![ConstructEffect {effect: Effect::Triage, duration: 4,
meta: Some(EffectMeta::Skill(Skill::TriageTickPlusPlus)), tick: None}], meta: Some(EffectMeta::Skill(Skill::TriageTickPlusPlus)), tick: None}],
Skill::Purify => vec![ConstructEffect { effect: Effect::Pure, duration: 2,
meta: Some(EffectMeta::Multiplier(150)), tick: None}],
Skill::PurifyPlus => vec![ConstructEffect { effect: Effect::Pure, duration: 2,
meta: Some(EffectMeta::Multiplier(175)), tick: None}],
Skill::PurifyPlusPlus => vec![ConstructEffect { effect: Effect::Pure, duration: 2,
meta: Some(EffectMeta::Multiplier(200)), tick: None}],
_ => { _ => {
panic!("{:?} no skill effect", self); panic!("{:?} no skill effect", self);
}, },
@ -1039,7 +1048,7 @@ impl Skill {
Skill::Strike=> None, Skill::Strike=> None,
Skill::StrikePlus => None, Skill::StrikePlus => None,
Skill::StrikePlusPlus => None, Skill::StrikePlusPlus => None,
Skill::Counter| Skill::Counter|
Skill::CounterPlus | Skill::CounterPlus |
Skill::CounterPlusPlus => None, // avoid all damage Skill::CounterPlusPlus => None, // avoid all damage
@ -1630,7 +1639,7 @@ fn electrocute(source: &mut Construct, target: &mut Construct, mut results: Reso
Some(eff) => { Some(eff) => {
let ce = source.effects.remove(eff); let ce = source.effects.remove(eff);
results.push(Resolution::new(source, source) results.push(Resolution::new(source, source)
.event(Event::Removal { effect: ce.effect, construct_effects: source.effects.clone() }) .event(Event::Removal { skill, effect: Some(ce.effect), construct_effects: source.effects.clone() })
.stages(EventStages::PostOnly)); .stages(EventStages::PostOnly));
} }
None => () None => ()
@ -1701,7 +1710,7 @@ fn absorption(source: &mut Construct, target: &mut Construct, mut results: Resol
let ce = target.effects.remove(absorb_index); let ce = target.effects.remove(absorb_index);
results.push(Resolution::new(source, target) results.push(Resolution::new(source, target)
.event(Event::Removal { effect: ce.effect, construct_effects: target.effects.clone() }) .event(Event::Removal { skill, effect: Some(ce.effect), construct_effects: target.effects.clone() })
.stages(EventStages::PostOnly)); .stages(EventStages::PostOnly));
return results;; return results;;
} }
@ -1787,7 +1796,7 @@ fn siphon_tick(source: &mut Construct, target: &mut Construct, mut results: Reso
fn link(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { fn link(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0])));
let amount = source.blue_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64)); let amount = source.blue_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64));
target.deal_blue_damage(skill, amount) target.deal_blue_damage(skill, amount)
.into_iter() .into_iter()
@ -1816,58 +1825,33 @@ fn silence(source: &mut Construct, target: &mut Construct, mut results: Resoluti
fn purge(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { fn purge(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd));
while let Some(i) = target.effects if target.effects.len() > 0 {
.iter() target.effects.clear();
.position(|ce| { results.push(Resolution::new(source, target)
if let Some(c) = ce.effect.colour() { .event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() })
c == Colour::Green .stages(EventStages::PostOnly));
} else { }
false
}
}) {
let ce = target.effects.remove(i);
results.push(Resolution::new(source, target)
.event(Event::Removal { effect: ce.effect, construct_effects: target.effects.clone() })
.stages(EventStages::PostOnly));
}
let effect = skill.effect()[0]; let effect = skill.effect()[0];
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));
/*let mut turns = 1;
for cs in target.skills.iter_mut() {
if Effect::Purge.disables_skill(cs.skill) {
turns += 1;
}
}
if turns > 1 {
effect.duration = effect.duration * turns;
}*/
return results; return results;
} }
fn purify(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions { fn purify(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd)); results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd));
let amount = source.green_power().pct(skill.multiplier()); if target.effects.len() > 0 {
while let Some(i) = target.effects let amount = source.green_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64));
.iter() target.effects.clear();
.position(|ce| { results.push(Resolution::new(source, target)
if let Some(c) = ce.effect.colour() { .event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() })
[Colour::Red, Colour::Blue].contains(&c) .stages(EventStages::PostOnly));
} else { target.deal_green_damage(skill, amount)
false
}
}) {
let ce = target.effects.remove(i);
results.push(Resolution::new(source, target)
.event(Event::Removal { effect: ce.effect, construct_effects: target.effects.clone() })
.stages(EventStages::PostOnly));
target.deal_green_damage(skill, amount)
.into_iter() .into_iter()
.for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly))); .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::PostOnly)));
} }
let effect = skill.effect()[0];
results.push(Resolution::new(source, target).event(target.add_effect(skill, effect)).stages(EventStages::PostOnly));
return results; return results;
} }

View File

@ -43,7 +43,7 @@ impl Vbox {
Vbox { Vbox {
free: vec![vec![], vec![], vec![]], free: vec![vec![], vec![], vec![]],
bound: starting_items, bound: starting_items,
bits: 18, bits: 30,
} }
} }