Merge branch 'release/1.6.0'

This commit is contained in:
ntr 2019-10-18 17:40:31 +11:00
commit 0b55484c14
36 changed files with 601 additions and 74 deletions

View File

@ -2,6 +2,40 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [1.6.0] - 2019-10-18
### Added
- Subscriber chat!
### Changed
- Made available skill / effect information during the combat phase.
- Highlighting a skill replace the effect area with the skill description including speed multiplier.
- Highlighting an effect will replace the targetting arrow / anim skill text with effect info.
- You can now preview combinations before you create them
- After selecting the three items for a combo hover over the combine button for info
- Damage formula for Slay and Siphon reworked
- Slay now deals red damage based RedPower and GreenPower. Previously only based on RedPower.
- Siphon now deals blue damage based BluePower and GreenPower. Previously only based on BluePower.
### Fixed
- Matchmaking bug where server matches you with yourself
## [1.5.6] - 2019-10-17
We've updated the UI during the vbox / buy phase to give a better indication of valid actions.
### Changed
- Borders for skill combo's represent the base colours.
- Heal (GG) has a green border, Siphon (BG) has an alternating blue / green border etc.
- Borders are shown for items in inventory and as equipped skills during both phases.
- Improvements to making item combo's
- If you select an item in your inventory it will now highlight other items that are valid for combining.
- This includes items that can be bought and in your inventory.
- Improved the indicator for where to click for equipping and buying items where its valid.
- Now slowly flashes between black and grey, previously changed the border once.
## [1.5.5] - 2019-10-15
### Changed
* Purge

View File

@ -1 +1 @@
1.5.6
1.6.0

View File

@ -3,10 +3,7 @@
*PRODUCTION*
* border colours for skills e.g. strike red border, slay half red half green
* rename vbox to shop
* combat phase info system
* drag and drop buy / equip / unequip items
* mobile styles
* mobile info page
@ -16,6 +13,11 @@
* can't reset password without knowing password =\
* Invert recharge
* serde serialize privatise
* chat
* Convert spec 'Plus' -> '+' when it appears as combo text in combiner and in info text
## SOON
* equip from shop (buy and equip without putting in your inventory) for bases
@ -23,7 +25,6 @@
* bot game grind
* ACP
* essential
* serde serialize privatise
* msg pane / chatwheel
* audio
* treats
@ -71,6 +72,7 @@ $$$
* Highlight (dota) colour
* fx colours + styles
* ??? (PROBS NOT) drag and drop buy / equip / unequip items ???
* modules
* troll life -> dmg
* prince of peace

View File

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

View File

@ -132,10 +132,16 @@ aside {
flex: 0;
}
.ready, .quit {
.ready {
flex: 1;
font-size: 200%;
}
.quit {
flex: 1;
font-size: 200%;
animation: co 0.75s cubic-bezier(0, 0, 1, 1) 0s infinite alternate;
}
}
.abandon:not([disabled]) {

View File

@ -3,6 +3,7 @@ footer {
flex-flow: row wrap;
grid-area: footer;
margin: 0;
z-index: 10;
button {
margin: 0;

View File

@ -123,7 +123,6 @@
button {
width: 100%;
height: 2em;
height: 25%;
margin-right: 1em;
}
button.active {
@ -204,6 +203,7 @@
.resolving-skill {
grid-area: target;
text-align: center;
align-self: center;
height: auto;
svg {
@ -213,6 +213,17 @@
}
}
.skill-description {
padding-left: 1em;
padding-right: 1em;
text-align: center;
svg {
display: inline;
height: 1em;
margin-right: 0.1em
}
}
/* some stupid bug in chrome makes it fill the entire screen */
@media screen and (-webkit-min-device-pixel-ratio:0) {
#targeting {
@ -396,6 +407,10 @@
.skills button {
font-size: 50%;
}
.skill-description {
font-size: 65%;
}
}
.player {

View File

@ -45,5 +45,9 @@
grid-area: msg;
color: @white;
}
}
.chat {
justify-content: flex-end;
}

View File

@ -14,7 +14,7 @@ html body {
-ms-user-select: none;
overflow-x: hidden;
// overflow-y: hidden;
overflow-y: hidden;
}
#mnml {
@ -26,7 +26,7 @@ html body {
/* stops inspector going skitz*/
overflow-x: hidden;
// overflow-y: hidden;
overflow-y: hidden;
}
// @media (min-width: 1921px) {

View File

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

View File

@ -11,14 +11,22 @@ export const setAnimText = value => ({ type: 'SET_ANIM_TEXT', value });
export const setDemo = value => ({ type: 'SET_DEMO', value });
export const setChatShow = value => ({ type: 'SET_CHAT_SHOW', value });
export const setChatWheel = value => ({ type: 'SET_CHAT_WHEEL', value });
export const setInstanceChat = value => ({ type: 'SET_INSTANCE_CHAT', value });
export const setActiveItem = value => ({ type: 'SET_ACTIVE_VAR', value });
export const setActiveSkill = (constructId, skill) => ({ type: 'SET_ACTIVE_SKILL', value: constructId ? { constructId, skill } : null });
export const setCombiner = value => ({ type: 'SET_COMBINER', value: Array.from(value) });
export const setConstructEditId = value => ({ type: 'SET_CONSTRUCT_EDIT_ID', value });
export const setConstructs = value => ({ type: 'SET_CONSTRUCTS', value });
export const setConstructRename = value => ({ type: 'SET_CONSTRUCT_RENAME', value });
export const setGame = value => ({ type: 'SET_GAME', value });
export const setGameSkillInfo = value => ({ type: 'SET_GAME_SKILL_INFO', value });
export const setGameEffectInfo = value => ({ type: 'SET_GAME_EFFECT_INFO', value });
export const setInfo = value => ({ type: 'SET_INFO', value });
export const setEmail = value => ({ type: 'SET_EMAIL', value });
export const setInvite = value => ({ type: 'SET_INVITE', value });
export const setInstance = value => ({ type: 'SET_INSTANCE', value });

View File

@ -0,0 +1,65 @@
const preact = require('preact');
const { connect } = require('preact-redux');
const actions = require('../actions');
const addState = connect(
function receiveState(state) {
const {
ws,
chatShow,
chatWheel,
instance,
game,
} = state;
function sendInstanceChat(instance, i) {
return ws.sendInstanceChat(instance, i);
}
return {
instance,
game,
chatShow,
chatWheel,
sendInstanceChat,
};
},
function receiveDispatch(dispatch) {
function setChatShow(v) {
dispatch(actions.setChatShow(v));
}
return {
setChatShow,
};
}
);
function Chat(args) {
const {
instance,
game,
chatShow,
chatWheel,
sendInstanceChat,
setChatShow,
} = args;
function onClick(i) {
sendInstanceChat(instance ? instance.id : game && game.id, i);
setChatShow(false);
return true;
}
return (
<div class={`instance-ctrl-btns chat`}>
{chatWheel.map((c, i) => <button key={i} onClick={() => onClick(i)} >{c}</button>)}
</div>
);
}
module.exports = addState(Chat);

View File

@ -2,10 +2,13 @@ const { connect } = require('preact-redux');
const { Component } = require('preact');
const preact = require('preact');
const range = require('lodash/range');
const reactStringReplace = require('react-string-replace');
const { STATS } = require('../utils');
const { ConstructAvatar, ConstructText } = require('./construct');
const shapes = require('./shapes');
const { INFO } = require('./../constants');
const actions = require('../actions');
const SkillBtn = require('./skill.btn');
@ -19,6 +22,8 @@ const addState = connect(
animFocus,
animating,
animText,
gameSkillInfo,
itemInfo,
} = state;
function selectSkillTarget(targetConstructId) {
@ -40,8 +45,19 @@ const addState = connect(
animText,
activeSkill,
selectSkillTarget,
gameSkillInfo,
itemInfo,
};
},
function receiveDispatch(dispatch) {
function setGameEffectInfo(info) {
dispatch(actions.setGameEffectInfo(info));
}
return { setGameEffectInfo };
}
);
const eventClasses = (animating, animFocus, construct, postSkill) => {
@ -77,6 +93,10 @@ class GameConstruct extends Component {
selectSkillTarget,
animFocus,
animText,
setGameEffectInfo,
gameSkillInfo,
itemInfo,
} = this.props;
const ko = construct.green_life.value === 0 ? 'ko' : '';
@ -96,9 +116,33 @@ class GameConstruct extends Component {
let crypSkills = <div></div>;
if (player) crypSkills = (<div class="skills"> {skills} </div>);
function hoverInfo(e, info) {
e.stopPropagation();
return setGameEffectInfo(info);
}
const effectBox = () => {
if (gameSkillInfo && gameSkillInfo.constructId === construct.id) {
const fullInfo = itemInfo.items.find(k => k.item === gameSkillInfo.skill) || INFO[gameSkillInfo.skill];
const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/;
const infoDescription = reactStringReplace(fullInfo.description, regEx, match => shapes[match]());
const speed = <div> Speed {shapes.SpeedStat()} multiplier {fullInfo.speed * 4}% </div>;
return (
<div class="skill-description">
<h2> {gameSkillInfo.skill} </h2>
<div> {infoDescription} </div>
{speed}
</div>);
}
const effects = construct.effects.length
? construct.effects.map(c => <div key={c.effect}>{c.effect} - {c.duration}T</div>)
? construct.effects.map(c =>
<div
key={c.effect}
onMouseOver={e => hoverInfo(e, c)}
onMouseOut={e => hoverInfo(e, null)}
> {c.effect} - {c.duration}T</div>)
: null;
return (<div class="effects"> {effects} </div>);
}
return (
<div
@ -107,7 +151,7 @@ class GameConstruct extends Component {
class={`game-construct ${ko} ${classes}`} >
<div class="left">
{crypSkills}
<div class="effects"> {effects} </div>
{effectBox()}
</div>
<div class="right">
<div class="stats"> {stats} </div>

View File

@ -8,6 +8,8 @@ const addState = connect(
const {
ws,
game,
account,
chatShow,
animating,
} = state;
@ -30,6 +32,8 @@ const addState = connect(
return {
game,
account,
chatShow,
sendAbandon,
sendGameSkillClear,
sendReady,
@ -45,7 +49,14 @@ const addState = connect(
dispatch(actions.setInstance(null));
}
return { quit };
function setChatShow(v) {
dispatch(actions.setChatShow(v));
}
return {
setChatShow,
quit,
};
}
);
@ -53,10 +64,13 @@ function GameCtrlBtns(args) {
const {
game,
animating,
account,
chatShow,
getInstanceState,
sendGameSkillClear,
sendReady,
setChatShow,
quit,
} = args;
@ -73,7 +87,7 @@ function GameCtrlBtns(args) {
return (
<div class="game-ctrl-btns">
<button disabled={true} >Chat</button>
<button disabled={!account.subscribed} onClick={() => setChatShow(!chatShow)}>Chat</button>
<button disabled={animating} onClick={sendGameSkillClear}>Clear</button>
{finished ? quitBtn : readyBtn}
</div>

View File

@ -4,6 +4,7 @@ const { connect } = require('preact-redux');
const actions = require('../actions');
const PlayerBox = require('./player.box');
const Chat = require('./chat');
const GameCtrlButtons = require('./game.ctrl.btns');
const GameCtrlTopButtons = require('./game.ctrl.btns.top');
@ -13,12 +14,16 @@ const addState = connect(
animating,
game,
account,
chatShow,
instanceChat,
} = state;
return {
animating,
game,
account,
chatShow,
instanceChat,
};
},
);
@ -28,6 +33,8 @@ function Controls(args) {
animating,
account,
game,
chatShow,
instanceChat,
} = args;
if (!game) return false;
@ -61,13 +68,17 @@ function Controls(args) {
</div>
);
const bottom = chatShow
? <Chat />
: <PlayerBox player={player} isPlayer={true} chat={instanceChat && instanceChat[player.id]} />;
return (
<aside>
{timer}
<div class="controls instance-ctrl">
<GameCtrlTopButtons />
<PlayerBox player={opponent}/>
<PlayerBox player={player} isPlayer={true} />
<PlayerBox player={opponent} chat={instanceChat && instanceChat[opponent.id]}/>
{bottom}
<GameCtrlButtons />
</div>
</aside>

View File

@ -7,7 +7,9 @@ const addState = connect(
function receiveState(state) {
const {
ws,
chatShow,
instance,
account,
} = state;
function sendReady() {
@ -21,19 +23,34 @@ const addState = connect(
return {
instance,
chatShow,
account,
sendAbandon,
sendReady,
};
},
function receiveDispatch(dispatch) {
function setChatShow(v) {
dispatch(actions.setChatShow(v));
}
return {
setChatShow,
};
}
);
function InstanceCtrlBtns(args) {
const {
instance,
chatShow,
account,
sendAbandon,
sendReady,
setChatShow,
} = args;
const finished = instance && instance.phase === 'Finished';
@ -49,7 +66,7 @@ function InstanceCtrlBtns(args) {
return (
<div class="instance-ctrl-btns">
<button disabled={true} >Chat</button>
<button disabled={!account.subscribed} onClick={() => setChatShow(!chatShow)}>Chat</button>
<button disabled={finished} class="ready" onClick={() => sendReady()}>Ready</button>
</div>
);

View File

@ -1,21 +1,27 @@
const preact = require('preact');
const { connect } = require('preact-redux');
const actions = require('../actions');
const PlayerBox = require('./player.box');
const Chat = require('./chat');
const InstanceCtrlBtns = require('./instance.ctrl.btns');
const InstanceCtrlTopBtns = require('./instance.ctrl.top.btns');
const actions = require('../actions');
const addState = connect(
function receiveState(state) {
const {
ws,
instance,
instanceChat,
account,
chatShow,
} = state;
return {
chatShow,
instance,
instanceChat,
account,
};
},
@ -25,6 +31,8 @@ function Controls(args) {
const {
account,
instance,
instanceChat,
chatShow,
} = args;
if (!instance) return false;
@ -58,13 +66,17 @@ function Controls(args) {
</div>
);
const bottom = chatShow
? <Chat />
: <PlayerBox player={player} isPlayer={true} chat={instanceChat && instanceChat[player.id]} />;
return (
<aside>
{timer}
<div class="controls instance-ctrl">
<InstanceCtrlTopBtns />
<PlayerBox player={opponent} />
<PlayerBox player={player} isPlayer={true} />
<PlayerBox player={opponent} chat={instanceChat && instanceChat[opponent.id]}/>
{bottom}
<InstanceCtrlBtns />
</div>
</aside>

View File

@ -2,7 +2,7 @@ const preact = require('preact');
const { connect } = require('preact-redux');
const Main = require('./main');
const Nav = require('./nav');
// const Nav = require('./nav');
const Controls = require('./controls');
const Footer = require('./footer');
@ -12,7 +12,6 @@ const addState = connect(
const Mnml = ({ showNav }) =>
<div id="mnml" class={showNav ? 'nav-visible' : ''}>
<Nav />
<Main />
<Controls />
<Footer />

View File

@ -56,6 +56,7 @@ function Scoreboard(args) {
const {
isPlayer,
player,
chat,
} = args;
const scoreText = () => {
@ -73,14 +74,14 @@ function Scoreboard(args) {
<div class="score">{scoreText()}</div>
<div class="name">{player.name}</div>
<Img img={player.img} id={player.id} />
<div class="msg">&nbsp;</div>
<div class="msg">{chat || '\u00A0'}</div>
</div>
);
}
return (
<div class={`player-box bottom ${player.ready ? 'ready' : ''}`}>
<div class="msg">&nbsp;</div>
<div class="msg">{chat || '\u00A0'}</div>
<div class="score">{scoreText()}</div>
<div class="name">{player.name}</div>
<Img img={player.img} id={player.id} />

View File

@ -23,18 +23,24 @@ const addState = connect(
dispatch(actions.setActiveSkill(constructId, skill));
}
return { setActiveSkill };
function setGameSkillInfo(info) {
dispatch(actions.setGameSkillInfo(info));
}
return { setActiveSkill, setGameSkillInfo };
}
);
function Skill(props) {
const {
animating,
construct,
game,
i,
activeSkill,
setActiveSkill,
setGameSkillInfo,
} = props;
if (!game) return false;
@ -42,6 +48,11 @@ function Skill(props) {
const s = construct.skills[i];
const ko = construct.green_life.value === 0 ? 'ko' : '';
function hoverInfo(e, info) {
e.stopPropagation();
if (animating) return false;
return setGameSkillInfo(info);
}
if (!s || !game) {
return (
<button
@ -76,6 +87,8 @@ function Skill(props) {
<button
disabled={cdText || s.disabled || ko}
class={`${(targeting || highlight) ? 'active' : ''} ${border}`}
onMouseOver={e => hoverInfo(e, { skill: s.skill, constructId: construct.id })}
onMouseOut={e => hoverInfo(e, null)}
type="submit"
onClick={onClick}>
{s.skill} {cdText}

View File

@ -6,11 +6,11 @@ const reactStringReplace = require('react-string-replace');
const throttle = require('lodash/throttle');
const shapes = require('./shapes');
const { removeTier } = require('../utils');
const { effectInfo, removeTier } = require('../utils');
const addState = connect(
({ game, account, animSkill, animating, itemInfo }) =>
({ game, account, animSkill, animating, itemInfo })
({ game, account, animSkill, animating, itemInfo, gameEffectInfo }) =>
({ game, account, animSkill, animating, itemInfo, gameEffectInfo })
);
class TargetSvg extends Component {
@ -28,10 +28,24 @@ class TargetSvg extends Component {
}
render(props, state) {
const { game, account, animating, animSkill, itemInfo } = props;
const { game, account, animating, animSkill, itemInfo, gameEffectInfo } = props;
const { width, height } = state;
if (!game) return false; // game will be null when battle ends
// Whenever someones looking at effects throw it up here
if (gameEffectInfo) {
const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/;
const infoString = effectInfo(gameEffectInfo);
const infoDescription = reactStringReplace(infoString, regEx, match => shapes[match]());
return (
<div class="resolving-skill">
<h1>{gameEffectInfo.effect}</h1>
<div> {infoDescription} </div>
</div>
);
}
// resolutions happening
// just put skill name up
if (animating) {

View File

@ -20,6 +20,7 @@ const addState = connect(
itemInfo,
itemUnequip,
navInstance,
info,
} = state;
function sendVboxDiscard() {
@ -56,6 +57,7 @@ const addState = connect(
itemUnequip,
sendItemUnequip,
navInstance,
info,
};
},
@ -115,6 +117,7 @@ function Vbox(args) {
sendItemUnequip,
setReclaiming,
info,
} = args;
if (!player) return false;
@ -138,7 +141,7 @@ function Vbox(args) {
//
function vboxHover(e, v) {
if (v) {
setInfo(v);
if (info !== v) setInfo(v);
e.stopPropagation();
}
return true;
@ -329,7 +332,7 @@ function Vbox(args) {
function combinerBtn() {
let text = '';
let comboItem = '';
if (combiner.length < 3) {
for (let i = 0; i < 3; i++) {
if (combiner.length > i) {
@ -339,14 +342,18 @@ function Vbox(args) {
}
}
} else {
text = 'combine';
// Since theres 3 items in combiner and you can't have invalid combos we can preview it
const combinerItems = combiner.map(j => vbox.bound[j]);
const comboItemObj = itemInfo.combos.find(combo => combinerItems.every(c => combo.components.includes(c)));
comboItem = comboItemObj ? comboItemObj.item : 'refine';
text = `Combine - ${comboItem}`;
}
return (
<button
class='vbox-btn'
disabled={combiner.length !== 3}
onMouseOver={e => hoverInfo(e, 'refine')}
onMouseOver={e => hoverInfo(e, comboItem)}
onClick={e => e.stopPropagation()}
onMouseDown={() => sendVboxCombine()}>
{text}
@ -390,9 +397,10 @@ function Vbox(args) {
//
// EVERYTHING
//
function hoverInfo(e, info) {
function hoverInfo(e, newInfo) {
e.stopPropagation();
return setInfo(info);
if (info === newInfo) return true;
return setInfo(newInfo);
}
const classes = `vbox ${navInstance === 0 ? 'visible' : ''}`;

View File

@ -109,7 +109,7 @@ function registerEvents(store) {
store.dispatch(actions.setAnimTarget(null));
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
@ -212,6 +212,14 @@ function registerEvents(store) {
return store.dispatch(actions.setInstance(v));
}
function setInstanceChat(v) {
return store.dispatch(actions.setInstanceChat(v));
}
function setChatWheel(v) {
return store.dispatch(actions.setChatWheel(v));
}
function setItemInfo(v) {
return store.dispatch(actions.setItemInfo(v));
}
@ -318,12 +326,14 @@ function registerEvents(store) {
setAccountInstances,
setActiveItem,
setActiveSkill,
setChatWheel,
setDemo,
setConstructList,
setNewConstruct,
setGame,
setEmail,
setInstance,
setInstanceChat,
setItemInfo,
setInvite,
setPing,

View File

@ -24,15 +24,21 @@ module.exports = {
demo: createReducer(null, 'SET_DEMO'),
chatShow: createReducer(null, 'SET_CHAT_SHOW'),
chatWheel: createReducer([], 'SET_CHAT_WHEEL'),
combiner: createReducer([], 'SET_COMBINER'),
constructs: createReducer([], 'SET_CONSTRUCTS'),
constructEditId: createReducer(null, 'SET_CONSTRUCT_EDIT_ID'),
constructRename: createReducer(null, 'SET_CONSTRUCT_RENAME'),
game: createReducer(null, 'SET_GAME'),
gameSkillInfo: createReducer(null, 'SET_GAME_SKILL_INFO'),
gameEffectInfo: createReducer(null, 'SET_GAME_EFFECT_INFO'),
email: createReducer(null, 'SET_EMAIL'),
invite: createReducer(null, 'SET_INVITE'),
info: createReducer(null, 'SET_INFO'),
instance: createReducer(null, 'SET_INSTANCE'),
instanceChat: createReducer(null, 'SET_INSTANCE_CHAT'),
instances: createReducer([], 'SET_INSTANCES'),
itemEquip: createReducer(null, 'SET_ITEM_EQUIP'),
itemInfo: createReducer({ combos: [], items: [] }, 'SET_ITEM_INFO'),

View File

@ -67,6 +67,10 @@ function createSocket(events) {
send(['InstanceState', { instance_id: instanceId }]);
}
function sendInstanceChat(instanceId, index) {
send(['InstanceChat', { instance_id: instanceId, index }]);
}
function sendVboxAccept(instanceId, group, index) {
send(['VboxAccept', { instance_id: instanceId, group, index }]);
events.clearInstance();
@ -253,8 +257,11 @@ function createSocket(events) {
QueueJoined: () => events.notify('you have joined the pvp queue'),
InviteRequested: () => events.notify('pvp queue request received'),
Invite: code => events.setInvite(code),
InstanceChat: chat => events.setInstanceChat(chat),
ChatWheel: wheel => events.setChatWheel(wheel),
Joining: () => events.notify('searching for instance...'),
Processing: () => true,
Error: errHandler,
};
@ -358,6 +365,7 @@ function createSocket(events) {
sendInstanceState,
sendInstanceInvite,
sendInstanceJoin,
sendInstanceChat,
sendVboxAccept,
sendVboxApply,

View File

@ -237,6 +237,64 @@ function convertItem(v) {
// return;
}
function effectInfo(i) {
console.log(i);
function multiplier(s) { // Update later to use server info in future
if (s === 'CounterAttack') return 70;
if (s === 'CounterAttack+') return 95;
if (s === 'CounterAttack++') return 120;
if (s === 'DecayTick') return 33;
if (s === 'DecayTick+') return 45;
if (s === 'DecayTick++') return 70;
if (s === 'SiphonTick') return 20;
if (s === 'SiphonTick+') return 25;
if (s === 'SiphonTick++') return 30;
if (s === 'TriageTick') return 75;
if (s === 'TriageTick+') return 110;
if (s === 'TriageTick++') return 140;
if (s === 'Electrocute' || s === 'ElectrocuteTick') return 80;
if (s === 'Electrocute+' || s === 'ElectrocuteTick+') return 100;
if (s === 'Electrocute++' || s === 'ElectrocuteTick++') return 130;
return 0;
}
switch (i.effect) {
case 'Amplify': return `Increases construct RedPower and BluePower by ${i.meta[1] - 100}%`;
case 'Banish': return 'Banished construct cannot cast or take damage';
case 'Block': return `Reduces construct red damage taken by ${100 - i.meta[1]}%`;
case 'Buff': return `Increases construct RedPower and SpeedStat by ${i.meta[1] - 100}%`;
case 'Sustain': return 'Construct cannot be KO while active. Additionally provides immunity to disables';
case 'Curse': return `Construct will take ${i.meta[1] - 100}% increased red and blue damage`;
case 'Haste': return `Construct has ${i.meta[1] - 100}% increased SpeedStat. Red attack skills will trigger a HasteStrike dealing 30% SpeedStat as red damage.`;
case 'Hybrid': return `Construct has ${i.meta[1] - 100}% increased GreenPower. Blue attack skills will trigger a HybridBlast dealing 25% GreenPower as red damage.`;
case 'Invert': return 'Reverses damage and healing. Healing will damage this construct and damage will heal.';
case 'Counter': return `Red damage taken by this construct will trigger a CounterAttack. CounterAttack deals ${multiplier(i.meta[1])}% RedPower as red damage.`;
case 'Purge': return 'Disable construct from casting any green skills';
case 'Reflect': return 'Reflect blue skills back to caster';
case 'Slow': return `Reduces construct SpeedStat by ${100 - i.meta[1]}%`;
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 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`;
case 'Silence': return 'Disable construct from casting any blue skills';
case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; //
case 'Decay': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`; //
case 'Electric': return `Attacks against this construct will apply Electrocute dealing ${multiplier(i.meta[1])}% of construct BluePower as blue damage each turn.`;
case 'Electrocute': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`;
case 'Absorb': return 'If construct takes damage, Absorption will be applied increasing RedPower and BluePower based on damage taken.';
case 'Absorption': return `Increasing construct RedPower and BluePower by ${i.meta[1]}`;
case 'Triage': return `Construct will be healed for ${multiplier(i.tick.skill)}% of caster's GreenPower each turn.`;
case 'Siphon': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower + GreenPower as blue damage each turn, healing the caster.`;
default: return 'Missing Effect Text';
}
}
module.exports = {
stringSort,
numSort,
@ -251,4 +309,5 @@ module.exports = {
randomPoints,
removeTier,
match,
effectInfo,
};

View File

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

View File

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

View File

@ -65,6 +65,19 @@ pub fn select(db: &Db, id: Uuid) -> Result<Account, Error> {
Account::try_from(row)
}
pub fn chat_wheel(_db: &Db, _id: Uuid) -> Result<Vec<String>, Error> {
return Ok(vec![
"gl".to_string(),
"hf".to_string(),
"gg".to_string(),
"thx".to_string(),
"nice".to_string(),
"hmm".to_string(),
"ok".to_string(),
"...".to_string(),
])
}
pub fn select_name(db: &Db, name: &String) -> Result<Account, Error> {
let query = "
SELECT id, name, balance, subscribed, img

View File

@ -1,4 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::thread::{spawn, sleep};
use std::time;
// Db Commons
use uuid::Uuid;
@ -58,6 +60,9 @@ pub enum Event {
Invite(Id),
Join(Id, String),
Joined(Id),
Chat(Id, Uuid, String),
ChatClear(Id, Uuid),
}
struct WsClient {
@ -65,6 +70,7 @@ struct WsClient {
account: Option<Uuid>,
tx: Sender<RpcMessage>,
subs: HashSet<Uuid>,
chat: Option<(Uuid, String)>,
pvp: bool,
invite: Option<String>,
}
@ -120,7 +126,15 @@ impl Events {
None => None,
};
let client = WsClient { id, tx, account: account_id, subs: HashSet::new(), pvp: false, invite: None };
let client = WsClient { id,
tx,
account: account_id,
subs: HashSet::new(),
pvp: false,
invite: None,
chat: None,
};
self.clients.insert(id, client);
info!("clients={:?}", self.clients.len());
@ -171,7 +185,17 @@ impl Events {
for (client_id, client) in self.clients.iter() {
if client.subs.contains(&id) {
subs += 1;
match client.tx.send(msg.clone()) {
let redacted = match client.account {
Some(a) => match msg {
RpcMessage::InstanceState(ref i) => RpcMessage::InstanceState(i.clone().redact(a)),
RpcMessage::GameState(ref i) => RpcMessage::GameState(i.clone().redact(a)),
_ => msg.clone(),
}
None => msg.clone(),
};
match client.tx.send(redacted) {
Ok(_) => (),
Err(e) => {
warn!("unable to send msg to client err={:?}", e);
@ -205,7 +229,7 @@ impl Events {
}
// create the req for the already queued opponent
if let Some(opp_req) = match self.clients.iter_mut().find(|(_c_id, c)| c.pvp) {
if let Some(opp_req) = match self.clients.iter_mut().find(|(c_id, c)| c.pvp && **c_id != id) {
Some((q_id, q)) => {
q.pvp = false;
Some(PvpRequest { id: *q_id, account: q.account.unwrap(), tx: q.tx.clone() })
@ -281,6 +305,58 @@ impl Events {
return Ok(());
},
Event::Chat(id, instance, msg) => {
// set the chat state of this connection
{
let c = self.clients.get_mut(&id)
.ok_or(format_err!("connection not found id={:?}", id))?;
if c.chat.is_some() {
return Ok(());
}
c.chat = Some((instance, msg));
let events_tx = self.tx.clone();
spawn(move || {
sleep(time::Duration::from_secs(3));
events_tx.send(Event::ChatClear(id, instance)).unwrap();
});
}
// now collect all listeners of this instance
let chat_state: HashMap<Uuid, String> = self.clients.iter()
.filter(|(_id, c)| c.account.is_some())
.filter(|(_id, c)| match c.chat {
Some(ref chat) => chat.0 == instance,
None => false,
})
.map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1))
.collect();
return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state)));
},
Event::ChatClear(id, instance) => {
{
match self.clients.get_mut(&id) {
Some(c) => c.chat = None,
None => (),
};
}
let chat_state: HashMap<Uuid, String> = self.clients.iter()
.filter(|(_id, c)| c.account.is_some())
.filter(|(_id, c)| match c.chat {
Some(ref chat) => chat.0 == instance,
None => false,
})
.map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1))
.collect();
return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state)));
}
}
}
}

View File

@ -62,6 +62,17 @@ impl Game {
};
}
pub fn redact(mut self, account: Uuid) -> Game {
self.players = self.players.into_iter()
.map(|p| p.redact(account))
.collect();
self.stack
.retain(|s| s.source_player_id == account);
self
}
pub fn set_time_control(&mut self, tc: TimeControl) -> &mut Game {
self.time_control = tc;
self.phase_end = Some(tc.lobby_timeout());
@ -631,8 +642,8 @@ pub fn game_write(tx: &mut Transaction, game: &Game) -> Result<(), Error> {
return Ok(());
}
pub fn game_state(tx: &mut Transaction, _account: &Account, id: Uuid) -> Result<Game, Error> {
return game_get(tx, id)
pub fn game_state(tx: &mut Transaction, account: &Account, id: Uuid) -> Result<Game, Error> {
Ok(game_get(tx, id)?.redact(account.id))
}
pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {

View File

@ -1,4 +1,5 @@
use std::fs::File;
use std::collections::{HashMap};
use uuid::Uuid;
@ -31,6 +32,8 @@ enum InstancePhase {
Finished,
}
pub type ChatState = HashMap<Uuid, String>;
#[derive(Debug,Clone,Serialize,Deserialize)]
struct Round {
game_id: Option<Uuid>,
@ -126,6 +129,14 @@ impl Instance {
}
}
pub fn redact(mut self, account: Uuid) -> Instance {
self.players = self.players.into_iter()
.map(|p| p.redact(account))
.collect();
self
}
fn phase_timed_out(&self) -> bool {
match self.phase_end {
Some(t) => Utc::now().signed_duration_since(t).num_milliseconds() > 0,

View File

@ -850,7 +850,8 @@ impl Item {
Item::Slay|
Item::SlayPlus |
Item::SlayPlusPlus => format!(
"Deals {:?}% RedPower as red damage and provides self healing based on damage dealt.",
"Deals {:?}% RedPower + {:?}% GreenPower as red damage and provides self healing based on damage dealt.",
self.into_skill().unwrap().multiplier(),
self.into_skill().unwrap().multiplier()),
Item::Sleep|
@ -884,7 +885,8 @@ impl Item {
Item::Siphon|
Item::SiphonPlus |
Item::SiphonPlusPlus => format!(
"Deals {:?}% BluePower as blue damage each turn and heals caster based on damage dealt. Lasts {:?}T.",
"Deals {:?}% BluePower + {:?}% GreenPower as blue damage each turn and heals caster based on damage dealt. Lasts {:?}T.",
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()),

View File

@ -103,6 +103,24 @@ impl Player {
}
}
pub fn redact(mut self, account: Uuid) -> Player {
// all g
if account == self.id {
return self;
}
// remove vbox
self.vbox = Vbox::new();
// hide skills
for construct in self.constructs.iter_mut() {
construct.skills = vec![];
construct.specs = vec![];
}
self
}
pub fn set_bot(mut self, bot: bool) -> Player {
self.bot = bot;
self

View File

@ -1,5 +1,7 @@
use std::time::{Instant};
use std::thread::spawn;
use std::thread::{spawn, sleep};
use std::time;
use std::str;
use uuid::Uuid;
@ -21,7 +23,7 @@ use account;
use construct::{Construct};
use events::{Event};
use game::{Game, game_state, game_skill, game_skill_clear, game_ready};
use instance::{Instance, instance_state, instance_practice, instance_ready, instance_abandon, demo};
use instance::{Instance, ChatState, instance_state, instance_practice, instance_ready, instance_abandon, demo};
use item::{Item, ItemInfoCtr, item_info};
use mtx;
use mail;
@ -50,6 +52,8 @@ pub enum RpcMessage {
ItemInfo(ItemInfoCtr),
InstanceState(Instance),
InstanceChat(ChatState),
ChatWheel(Vec<String>),
EmailState(Option<Email>),
SubscriptionState(Option<Subscription>),
@ -66,6 +70,8 @@ pub enum RpcMessage {
Invite(String),
Joining(()),
Processing(()),
Error(String),
}
@ -102,6 +108,7 @@ pub enum RpcRequest {
InstanceAbandon { instance_id: Uuid },
InstanceReady { instance_id: Uuid },
InstanceState { instance_id: Uuid },
InstanceChat { instance_id: Uuid, index: usize },
VboxAccept { instance_id: Uuid, group: usize, index: usize },
VboxDiscard { instance_id: Uuid },
@ -158,6 +165,22 @@ impl Connection {
self.events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(()))
},
RpcRequest::InstanceChat { instance_id, index } => {
if !account.subscribed {
return Err(err_msg("subscribe to unlock chat"))
}
let wheel = account::chat_wheel(&db, account.id)?;
if let Some(c) = wheel.get(index) {
self.events.send(Event::Chat(self.id, instance_id, c.to_string()))?;
} else {
return Err(err_msg("invalid chat index"));
}
Ok(RpcMessage::Processing(()))
},
_ => {
// all good, let's make a tx and process
let mut tx = db.transaction()?;
@ -257,6 +280,25 @@ impl Connection {
},
}
}
// this is where last minute processing happens
// use it to modify outgoing messages, update subs, serialize in some way...
fn send(&self, msg: RpcMessage) -> Result<(), Error> {
let msg = match self.account {
Some(ref a) => match msg {
RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(a.id)),
RpcMessage::AccountInstances(v) =>
RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(a.id)).collect()),
RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(a.id)),
_ => msg,
},
None => msg,
};
self.ws.send(msg).unwrap();
Ok(())
}
}
// we unwrap everything in here cause really
@ -272,7 +314,7 @@ impl Handler for Connection {
// if user logged in do some prep work
if let Some(ref a) = self.account {
self.ws.send(RpcMessage::AccountState(a.clone())).unwrap();
self.send(RpcMessage::AccountState(a.clone())).unwrap();
self.events.send(Event::Subscribe(self.id, a.id)).unwrap();
// check if they have an image that needs to be generated
@ -283,23 +325,26 @@ impl Handler for Connection {
// send account constructs
let account_constructs = account::constructs(&mut tx, a).unwrap();
self.ws.send(RpcMessage::AccountConstructs(account_constructs)).unwrap();
self.send(RpcMessage::AccountConstructs(account_constructs)).unwrap();
// get account instances
// and send them to the client
let account_instances = account::account_instances(&mut tx, a).unwrap();
self.ws.send(RpcMessage::AccountInstances(account_instances)).unwrap();
self.send(RpcMessage::AccountInstances(account_instances)).unwrap();
let shop = mtx::account_shop(&mut tx, &a).unwrap();
self.ws.send(RpcMessage::AccountShop(shop)).unwrap();
self.send(RpcMessage::AccountShop(shop)).unwrap();
let team = account::team(&mut tx, &a).unwrap();
self.ws.send(RpcMessage::AccountTeam(team)).unwrap();
self.send(RpcMessage::AccountTeam(team)).unwrap();
let wheel = account::chat_wheel(&db, a.id).unwrap();
self.send(RpcMessage::ChatWheel(wheel)).unwrap();
// tx should do nothing
tx.commit().unwrap();
} else {
self.ws.send(RpcMessage::Demo(demo().unwrap())).unwrap();
self.send(RpcMessage::Demo(demo().unwrap())).unwrap();
}
Ok(())
@ -327,11 +372,11 @@ impl Handler for Connection {
_ => (),
};
self.ws.send(reply).unwrap();
self.send(reply).unwrap();
},
Err(e) => {
warn!("{:?}", e);
self.ws.send(RpcMessage::Error(e.to_string())).unwrap();
self.send(RpcMessage::Error(e.to_string())).unwrap();
},
};
},

View File

@ -757,13 +757,13 @@ impl Skill {
Skill::HealPlus => 185, //GG
Skill::HealPlusPlus => 270, //GG
Skill::SiphonTick=> 40, // GB
Skill::SiphonTickPlus => 50,
Skill::SiphonTickPlusPlus => 60,
Skill::SiphonTick=> 20, // GB
Skill::SiphonTickPlus => 25,
Skill::SiphonTickPlusPlus => 30,
Skill::Slay=> 70, // RG
Skill::SlayPlus => 115,
Skill::SlayPlusPlus => 180,
Skill::Slay=> 40, // RG
Skill::SlayPlus => 60,
Skill::SlayPlusPlus => 90,
Skill::Strike=> 90, //RR
Skill::StrikePlus => 140,
@ -1420,9 +1420,7 @@ fn break_(source: &mut Construct, target: &mut Construct, mut results: Resolutio
}
fn block(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]))
.stages(EventStages::StartEnd));
results.push(Resolution::new(source, target).event(target.add_effect(skill, skill.effect()[0])));
return results;
}
@ -1473,7 +1471,7 @@ fn restrict(source: &mut Construct, target: &mut Construct, mut results: Resolut
}
fn slay(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let amount = source.red_power().pct(skill.multiplier());
let amount = source.red_power().pct(skill.multiplier()) + source.green_power().pct(skill.multiplier());
let slay_events = target.deal_red_damage(skill, amount);
for e in slay_events {
@ -1734,7 +1732,7 @@ fn siphon(source: &mut Construct, target: &mut Construct, mut results: Resolutio
}
fn siphon_tick(source: &mut Construct, target: &mut Construct, mut results: Resolutions, skill: Skill) -> Resolutions {
let amount = source.blue_power().pct(skill.multiplier());
let amount = source.blue_power().pct(skill.multiplier()) + source.green_power().pct(skill.multiplier());
let siphon_events = target.deal_blue_damage(skill, amount);
for e in siphon_events {
@ -2039,13 +2037,14 @@ mod tests {
.named(&"camel".to_string());
x.blue_power.force(256);
x.green_power.force(220);
x.green_life.force(1024);
x.green_life.reduce(512);
let mut results = resolve(Skill::Siphon, &mut x, &mut y, vec![]);
assert!(y.affected(Effect::Siphon));
assert!(x.green_life() == (512 + 256.pct(Skill::SiphonTick.multiplier())));
assert!(x.green_life() == (512 + 256.pct(Skill::SiphonTick.multiplier()) + 220.pct(Skill::SiphonTick.multiplier())));
let Resolution { source: _, target: _, event, stages: _ } = results.remove(0);
match event {
@ -2055,14 +2054,15 @@ mod tests {
let Resolution { source: _, target: _, event, stages: _ } = results.remove(0);
match event {
Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier())),
Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier())
+ 220.pct(Skill::SiphonTick.multiplier())),
_ => panic!("not damage siphon"),
};
let Resolution { source: _, target, event, stages: _ } = results.remove(0);
match event {
Event::Healing { amount, skill: _, overhealing: _ } => {
assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier()));
assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier()) + 220.pct(Skill::SiphonTick.multiplier()));
assert_eq!(target.id, x.id);
},
_ => panic!("not healing"),