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
- 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 =\
* ws gzip encoding
* mobile info page
* Graphics
* Img
* Skill Icons
* Buttons / General UI Theming
* Front Page
## SOON
* Graphical status effects instead of text
* Improve colour contrast / buttons
* supporter gold name in instance (anyone whos put any money into game)

View File

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

View File

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

View File

@ -2457,9 +2457,9 @@
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="0.53357639"
inkscape:cx="411.32817"
inkscape:cy="1018.5983"
inkscape:zoom="0.53546627"
inkscape:cx="561.25984"
inkscape:cy="793.70079"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
@ -2539,8 +2539,8 @@
transform="translate(-472.60042,755.1467)"
style="display:inline">
<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"
id="rect5015"
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="rect5144"
width="297"
height="420"
x="472.60043"
@ -2548,7 +2548,7 @@
<g
id="g4946"
style="stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none"
transform="translate(0,24.99999)">
transform="translate(0,2.7646743)">
<path
inkscape:connector-curvature="0"
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" />
</g>
<g
id="g5001"
transform="translate(25)">
id="g5142"
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
inkscape:connector-curvature="0"
id="path4948"
d="m 522.60042,-435.14668 v -75.00001 h 50 v 75.00001 h -50"
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" />
<path
inkscape:connector-curvature="0"
id="path4978"
d="m 532.60042,-500.14669 h 30 v 55 h -30 z"
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" />
<path
inkscape:connector-curvature="0"
id="path4988"
d="m 542.60042,-490.1467 h 10 v 35 h -10 v -35"
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" />
id="path5003"
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:2.9685142;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<g
id="g5123"
transform="matrix(0.23529412,0,0,0.23529412,411.40033,-377.05346)"
style="stroke:#f5f5f5;stroke-width:12.75;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<path
inkscape:connector-curvature="0"
id="path5068"
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: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>
<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>
</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 {
from {
background: @black;

View File

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

View File

@ -11,7 +11,7 @@
@media (max-width: 1920px) {
.instance .info table td svg {
height: 50%;
// height: 50%;
stroke-width: 8px;
}
@ -33,15 +33,57 @@
}
.instance .info {
/*font-size: 75%;*/
margin: 0 0 0 1em;
grid-area: info;
display: flex;
flex-flow: column;
// white-space: pre-wrap;
> *:first-child {
margin-bottom: 1em;
display: grid;
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 {
animation: win 2s ease-in-out 0s 1;
}
.cancelled {
color: white;
}
}
.tutorial {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
.vbox {
margin-bottom: 2em;
line-height: 0;
.vbox-hdr {
margin-bottom: 1em;
height: 2em;
@ -53,7 +53,7 @@
&, &:hover, &:active {
background: @red;
color: black;
border: 1px solid black;
border: 2px solid black;
}
}
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",
"version": "1.8.1",
"version": "1.8.3",
"description": "",
"main": "index.js",
"scripts": {

View File

@ -1,5 +1,4 @@
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 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 setReclaiming = value => ({ type: 'SET_RECLAIMING', 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 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 setTeamPage = value => ({ type: 'SET_TEAM_PAGE', value });

View File

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

View File

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

View File

@ -36,7 +36,6 @@ const addState = connect(
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([]));

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,6 @@ const addState = connect(
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([]));

View File

@ -9,15 +9,22 @@ const shapes = require('./shapes');
class InfoComponent extends preact.Component {
shouldComponentUpdate(newProps) {
shouldComponentUpdate(newProps, newState) {
if (newProps.tutorial !== this.props.tutorial) return true;
// We don't care about info during tutorial
if (newProps.tutorial && this.props.instance.time_control === 'Practice'
&& this.props.instance.rounds.length === 1) return false;
if (newProps.info !== this.props.info) return true;
if (newState.comboItem !== this.state.comboItem) return true;
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) {
const {
// Variables that will change
@ -33,7 +40,7 @@ class InfoComponent extends preact.Component {
setInfo,
setTutorialNull,
} = args;
const { comboItem } = this.state;
function Info() {
if (tutorial) {
const tutorialStageInfo = tutorialStage(tutorial, ws, setTutorialNull, instance);
@ -42,7 +49,7 @@ class InfoComponent extends preact.Component {
if (!info) return false;
if (info.includes('constructName')) {
return (
<div>
<div class='info-item'>
<h2> {info.replace('constructName ', '')} </h2>
<p> This is the name of your construct. <br />
Names are randomly generated and are purely cosmetic. <br />
@ -54,7 +61,7 @@ class InfoComponent extends preact.Component {
if (info.includes('constructAvatar')) {
return (
<div>
<div class='info-item'>
<h2> {info.replace('constructAvatar ', '')} </h2>
<p> This is your construct avatar. <br />
Avatars are randomly generated and are purely cosmetic. <br />
@ -63,7 +70,9 @@ class InfoComponent extends preact.Component {
</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;
const isSkill = fullInfo.skill;
const isSpec = fullInfo.spec;
@ -75,16 +84,17 @@ class InfoComponent extends preact.Component {
};
if (isSkill || isSpec) {
let infoName = info;
let infoName = fullInfo.item;
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
let itemSourceInfo = itemSource.length && !isSpec
? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}`
: false;
let header = null;
if (!itemSource.length) header = isSkill ? <h3> SKILL </h3> : <h3> SPEC </h3>;
if (itemSourceInfo) {
while (itemSourceInfo.includes('Plus')) itemSourceInfo = itemSourceInfo.replace('Plus', '+');
const itemRegEx = /(Red|Blue|Green)/;
@ -100,7 +110,7 @@ class InfoComponent extends preact.Component {
const thresholds = isSpec ? specThresholds(player, fullInfo, info) : null;
return (
<div class={isSkill ? 'info-skill' : 'info-spec'}>
<div class={isSkill ? 'info-item' : 'info-item'}>
<h2>{infoName} {fullInfo.cost}b</h2>
{header}
{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;
const generalNotes = (
<div>
@ -137,22 +147,33 @@ class InfoComponent extends preact.Component {
const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info));
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 (
<table class="combos">
<tbody>
{vboxCombos.map((c, i) =>
<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>
<div class="combos">
{comboTable}
</div>
);
}
};
return (
<div class='info' >
<div class='info' onMouseOver={() => this.setState({ comboItem: null })}>
<Info />
<Combos />
</div>

View File

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

View File

@ -17,16 +17,11 @@ const addState = connect(
return ws.sendInstanceReady(instance.id);
}
function sendAbandon() {
return ws.sendInstanceAbandon(instance.id);
}
return {
instance,
chatShow,
account,
sendAbandon,
sendReady,
};
},
@ -48,22 +43,12 @@ function InstanceCtrlBtns(args) {
chatShow,
account,
sendAbandon,
sendReady,
setChatShow,
} = args;
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 (
<div class="instance-ctrl-btns">
<button disabled={!account.subscribed} onClick={() => setChatShow(!chatShow)}>Chat</button>

View File

@ -36,7 +36,6 @@ const addState = connect(
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([]));

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ module.exports = {
Colours - 1b<br />
Skills - 2b<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: {
item: 'READY',

View File

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

View File

@ -9,7 +9,6 @@ function setupKeys(store) {
key('esc', () => store.dispatch(actions.setCombiner([])));
key('esc', () => store.dispatch(actions.setReclaiming(false)));
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.setItemEquip(null)));
key('esc', () => store.dispatch(actions.setItemUnequip([])));

View File

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

View File

@ -151,6 +151,10 @@ function createSocket(events) {
send(['InstanceQueue', {}]);
}
function sendInstanceLeave() {
send(['InstanceLeave', {}]);
}
function sendInstanceInvite() {
send(['InstanceInvite', {}]);
}
@ -274,8 +278,16 @@ function createSocket(events) {
Pong: onPong,
Demo: onDemo,
QueueRequested: () => events.notify('PVP queue request received.'),
QueueJoined: () => events.notify('You have joined the PVP queue.'),
// QueueRequested: () => events.notify('PVP queue request received.'),
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.'),
InviteRequested: () => events.notify('PVP invite request received.'),
Invite: code => events.setInvite(code),
@ -318,11 +330,8 @@ function createSocket(events) {
return handlers[msgType](params);
}
let attempts = 1;
// Connection opened
function onOpen() {
attempts = 0;
toast.info({
message: 'connected',
position: 'topRight',
@ -341,21 +350,12 @@ function createSocket(events) {
}
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);
toast.warning({
message: 'disconnected',
position: 'topRight',
});
return setTimeout(connect, attempts * 1000);
return setTimeout(connect, 5000);
}
function connect() {
@ -402,6 +402,7 @@ function createSocket(events) {
sendInstanceInvite,
sendInstanceJoin,
sendInstanceChat,
sendInstanceLeave,
sendVboxAccept,
sendVboxAcceptEquip,

View File

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

View File

@ -266,7 +266,7 @@ function effectInfo(i) {
}
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 '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}%`;
@ -282,6 +282,7 @@ function effectInfo(i) {
case 'Restrict': return 'Disable construct from casting any red skills';
case 'Stun': return 'Stunned construct cannot use skills';
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 'Silence': return 'Disable construct from casting any blue skills';
case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; //

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ use http::MnmlHttpError;
use names::{name as generate_name};
use construct::{Construct, ConstructSkeleton, construct_spawn};
use instance::{Instance, instance_delete};
use instance;
use mtx::{Mtx, FREE_MTX};
use pg::Db;
use img;
@ -21,6 +22,7 @@ use failure::Error;
use failure::{err_msg, format_err};
static PASSWORD_MIN_LEN: usize = 3;
static PASSWORD_ROUNDS: u32 = 10;
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Account {
@ -69,11 +71,11 @@ pub fn chat_wheel(_db: &Db, _id: Uuid) -> Result<Vec<String>, Error> {
return Ok(vec![
"gg".to_string(),
"glhf".to_string(),
"hmm".to_string(),
"ok".to_string(),
"ez".to_string(),
"rekt".to_string(),
"thx".to_string(),
"nice".to_string(),
"wow".to_string(),
"wp".to_string(),
"ok".to_string(),
"...".to_string(),
])
}
@ -225,8 +227,7 @@ pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password:
return Err(MnmlHttpError::BadRequest);
}
let rounds = 8;
let password = hash(&password, rounds)?;
let password = hash(&password, PASSWORD_ROUNDS)?;
let query = "
UPDATE accounts
@ -326,10 +327,13 @@ pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result<
return Err(MnmlHttpError::AccountNameNotProvided);
}
if name.len() > 20 {
return Err(MnmlHttpError::AccountNameUnacceptable);
}
let id = Uuid::new_v4();
let img = Uuid::new_v4();
let rounds = 12;
let password = hash(&password, rounds)?;
let password = hash(&password, PASSWORD_ROUNDS)?;
let mut rng = thread_rng();
let token: String = iter::repeat(())
@ -502,3 +506,25 @@ pub fn img_check(account: &Account) -> Result<Uuid, Error> {
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,
Block,
Buff,
Sustain,
Counter,
Curse,
Haste,
Hybrid,
Intercept,
Invert,
Counter,
Pure,
Purge,
Reflect,
Slow,
Restrict,
Stun,
Intercept,
Vulnerable,
Silence,
Slow,
Stun,
Sustain,
Vulnerable,
Wither, // Reduce green dmg (healing) taken
// electric is the buff that applies
@ -99,21 +100,25 @@ impl Effect {
pub fn modifications(&self) -> Vec<Stat> {
match self {
Effect::Vulnerable => vec![Stat::RedDamageTaken],
// Bases
Effect::Block => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
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],
// 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![],
}
}
@ -128,6 +133,7 @@ impl Effect {
Effect::Haste |
Effect::Slow |
Effect::Hybrid |
Effect::Pure |
Effect::Wither => value.pct(match meta {
Some(EffectMeta::Multiplier(d)) => d,
_ => 100,
@ -135,7 +141,10 @@ impl Effect {
Effect::Absorption => value + match meta {
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::Regen => Some(Colour::Green),
Effect::Siphon => Some(Colour::Blue),
Effect::Pure => Some(Colour::Green),
Effect::Ko => None,
}

View File

@ -60,6 +60,7 @@ pub enum Event {
Invite(Id),
Join(Id, String),
Joined(Id),
Leave(Id),
Chat(Id, Uuid, String),
ChatClear(Id, Uuid),
@ -249,6 +250,7 @@ impl Events {
// or flag the requester as pvp ready
let requester = self.clients.get_mut(&id).unwrap();
requester.pvp = true;
requester.tx.send(RpcMessage::QueueJoined(()))?;
info!("joined game queue id={:?} account={:?}", requester.id, requester.account);
return Ok(());
},
@ -305,6 +307,18 @@ impl Events {
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) => {
// set the chat state of this connection
{

View File

@ -635,15 +635,15 @@ impl Game {
for player in self.players.iter_mut() {
if !player.ready {
player.set_ready(true);
player.add_warning();
info!("upkeep: {:} warned", player.name);
if player.warnings >= 3 {
player.forfeit();
info!("upkeep: {:} forfeited", player.name);
//todo
// self.resolved.push(forfeit)
// self.log.push(format!("{:} forfeited.", player.name));
}
// player.add_warning();
// info!("upkeep: {:} warned", player.name);
// if player.warnings >= 3 {
// player.forfeit();
// info!("upkeep: {:} forfeited", player.name);
// //todo
// // self.resolved.push(forfeit)
// // self.log.push(format!("{:} forfeited.", player.name));
// }
}
}
@ -1622,6 +1622,6 @@ mod tests {
game.players[0].set_ready(true);
game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap());
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,
#[fail(display="account name unavailable")]
AccountNameUnavailable,
#[fail(display="account name is unacceptable. 20 char max")]
AccountNameUnacceptable,
#[fail(display="account not found")]
AccountNotFound,
#[fail(display="password does not match")]
@ -122,6 +124,7 @@ impl From<MnmlHttpError> for IronError {
MnmlHttpError::AccountNameNotProvided |
MnmlHttpError::AccountNameUnavailable |
MnmlHttpError::AccountNameUnacceptable |
MnmlHttpError::AccountNotFound |
MnmlHttpError::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();
let bits = match self.rounds.len() > 0 {
true => 12 + 6 * self.rounds.len(),
true => 30,
false => 0,
};
self.players.iter_mut().for_each(|p| {
p.vbox.balance_add(bits.into());
p.vbox.balance_add(bits);
p.set_ready(false);
p.vbox.fill();
});

View File

@ -712,7 +712,7 @@ impl Item {
// Skills <- need to move effect mulltipliers into skills
Item::Amplify|
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_duration()),
@ -824,15 +824,17 @@ impl Item {
Item::Purge|
Item::PurgePlus |
Item::PurgePlusPlus => format!(
"Remove buffs from target construct.
"Remove all effects from target construct.
Applies purge disabling target green skills for {:?}T.",
self.into_skill().unwrap().effect()[0].get_duration()),
Item::Purify|
Item::PurifyPlus |
Item::PurifyPlusPlus => format!(
"Remove debuffs and heals for {:?}% GreenPower per debuff removed.",
self.into_skill().unwrap().multiplier()),
"Remove all effects and heals for {:?}% GreenPower per effect removed.
Applies Pure increasing healing taken by {:?}%.",
self.into_skill().unwrap().multiplier(),
self.into_skill().unwrap().effect()[0].get_multiplier() - 100),
Item::Reflect|
Item::ReflectPlus |
@ -898,11 +900,10 @@ impl Item {
Item::Bash|
Item::BashPlus |
Item::BashPlusPlus => format!(
"Bash the target increasing the cooldowns of target skills by 1T.
Deals {:?}% RedPower as red damage and 45% more damage per cooldown increased.
Stuns for {:?}T.",
self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(),
self.into_skill().unwrap().effect()[0].get_duration()),
"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.",
self.into_skill().unwrap().effect()[0].get_duration(),
self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier()),
Item::Strike|
Item::StrikePlus |
@ -1318,25 +1319,18 @@ pub fn get_combos() -> Vec<Combo> {
Combo { components: Item::Intercept.combo(), item: Item::Intercept },
Combo { components: Item::InterceptPlus.combo(), item: Item::InterceptPlus },
Combo { components: Item::InterceptPlusPlus.combo(), item: Item::InterceptPlusPlus },
Combo { components: Item::Triage.combo(), item: Item::Triage },
Combo { components: Item::TriagePlus.combo(), item: Item::TriagePlus },
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::AbsorbPlus.combo(), item: Item::AbsorbPlus },
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::HybridPlus.combo(), item: Item::HybridPlus },
Combo { components: Item::HybridPlusPlus.combo(), item: Item::HybridPlusPlus },
Combo { components: Item::Amplify.combo(), item: Item::Amplify },
Combo { components: Item::AmplifyPlus.combo(), item: Item::AmplifyPlus },
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::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::CursePlus.combo(), item: Item::CursePlus },
Combo { components: Item::CursePlusPlus.combo(), item: Item::CursePlusPlus },
Combo { components: Item::Decay.combo(), item: Item::Decay },
Combo { components: Item::DecayPlus.combo(), item: Item::DecayPlus },
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::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::BreakPlus.combo(), item: Item::BreakPlus },
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::BanishPlus.combo(), item: Item::BanishPlus },
Combo { components: Item::BanishPlusPlus.combo(), item: Item::BanishPlusPlus },

View File

@ -62,15 +62,18 @@ use std::path::{Path};
use fern::colors::{Color, ColoredLevelConfig};
use crossbeam_channel::{unbounded};
#[derive(Serialize)]
struct JsonLog {
time: String,
module: String,
level: String,
msg: String,
}
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()
.error(Color::Red)
.warn(Color::Yellow)
@ -90,31 +93,16 @@ pub fn setup_logger() -> Result<(), fern::InitError> {
message = message,
));
})
.chain(fern::log_file("/var/log/mnml/mnml.log")?)
.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()
.level_for("postgres", log::LevelFilter::Info)
.level_for("ws", log::LevelFilter::Warn)
.level_for("iron", log::LevelFilter::Info)
.level(log::LevelFilter::Info)
.chain(term)
.chain(json)
.chain(syslog)
.apply()?;
Ok(())

View File

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

View File

@ -77,11 +77,11 @@ fn handle_notification(n: Notification, pool: &PgPool, events: &Sender<Event>) {
let msg = match n.action {
Action::Delete => {
warn!("unimplemented delete notification {:?}", n);
// warn!("unimplemented delete notification {:?}", n);
None
},
Action::Insert => {
warn!("unimplemented insert notification {:?}", n);
// warn!("unimplemented insert notification {:?}", n);
None
},
Action::Update => match n.table {
@ -92,7 +92,7 @@ fn handle_notification(n: Notification, pool: &PgPool, events: &Sender<Event>) {
Table::Games =>
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
},
},

View File

@ -63,7 +63,6 @@ pub struct Player {
pub constructs: Vec<Construct>,
pub bot: bool,
pub ready: bool,
pub warnings: u8,
pub draw_offered: bool,
pub score: Score,
}
@ -85,7 +84,6 @@ impl Player {
constructs,
bot: false,
ready: false,
warnings: 0,
draw_offered: false,
score: Score::Zero,
})
@ -100,7 +98,6 @@ impl Player {
constructs,
bot: false,
ready: false,
warnings: 0,
draw_offered: false,
score: Score::Zero,
}
@ -134,11 +131,6 @@ impl Player {
self
}
pub fn add_warning(&mut self) -> &mut Player {
self.warnings += 1;
self
}
pub fn forfeit(&mut self) -> &mut Player {
for construct in self.constructs.iter_mut() {
construct.force_ko();

View File

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

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 {
for Resolution { source, target, event, stages: _ } in resolutions.clone() {
let mut source = game.construct_by_id(source.id).unwrap().clone();
let mut target = game.construct_by_id(target.id).unwrap().clone();
for Resolution { source: event_source, target: event_target, event, stages: _ } in resolutions.clone() {
let mut source = game.construct_by_id(event_source.id).unwrap().clone();
let mut target = game.construct_by_id(event_target.id).unwrap().clone();
match event {
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();
resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly));
}
@ -505,7 +506,7 @@ pub enum Event {
AoeSkill { skill: Skill },
Skill { skill: Skill },
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 },
// skill not necessary but makes it neater as all events are arrays in js
Ko (),
@ -1022,6 +1023,14 @@ impl Skill {
meta: Some(EffectMeta::Skill(Skill::TriageTickPlus)), tick: None}],
Skill::TriagePlusPlus => vec![ConstructEffect {effect: Effect::Triage, duration: 4,
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);
},
@ -1630,7 +1639,7 @@ fn electrocute(source: &mut Construct, target: &mut Construct, mut results: Reso
Some(eff) => {
let ce = source.effects.remove(eff);
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));
}
None => ()
@ -1701,7 +1710,7 @@ fn absorption(source: &mut Construct, target: &mut Construct, mut results: Resol
let ce = target.effects.remove(absorb_index);
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));
return results;;
}
@ -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 {
results.push(Resolution::new(source, target).event(Event::Skill { skill }).stages(EventStages::StartEnd));
while let Some(i) = target.effects
.iter()
.position(|ce| {
if let Some(c) = ce.effect.colour() {
c == Colour::Green
} 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));
}
if target.effects.len() > 0 {
target.effects.clear();
results.push(Resolution::new(source, target)
.event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() })
.stages(EventStages::PostOnly));
}
let effect = skill.effect()[0];
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;
}
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));
let amount = source.green_power().pct(skill.multiplier());
while let Some(i) = target.effects
.iter()
.position(|ce| {
if let Some(c) = ce.effect.colour() {
[Colour::Red, Colour::Blue].contains(&c)
} 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));
target.deal_green_damage(skill, amount)
if target.effects.len() > 0 {
let amount = source.green_power().pct(skill.multiplier().saturating_mul(target.effects.len() as u64));
target.effects.clear();
results.push(Resolution::new(source, target)
.event(Event::Removal { skill, effect: None, construct_effects: target.effects.clone() })
.stages(EventStages::PostOnly));
target.deal_green_damage(skill, amount)
.into_iter()
.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;
}

View File

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