diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6107c0..904fc8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,87 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [1.6.7] - 2019-10-27 +# Added +- Step by step tutorial + - Will activate during the learn game for the first round + - There is a button which will exit tutorial so you can continune the normal practice mode + +- Skill combo previews + - You can now preview what item combos will create! + - Click into the item in the info section table (top right) and it will be replaced with the new item + +# Changed +- Vbox phase + - Made general performance improvements + - Removed the default info state (should be smoother to navigate now) + - When combining or selecting base items (skills/specs) the base item info won't be replaced with other vbox items + - Added info text for construct names and avatars + - Changed a number of info descriptions for clarity + +- Game phase + - Made general performance improvements + - Now has default tutorial text for the first round (tells the player to select skills and then the targets) + +- Moved the login page demo to a new info tab, increased the speed of demo and it now creates random combos + +- Banish + - Cooldown reduced to 2T (was 3T) + +- Bash + - Skill multiplier reduced from 65/95/140 -> 45/65/100 + +- Blast + - Damage multiplier reduced 110/145/210 -> 105/140/200 + +- Block + - Previously reduced red damage taken by 50% + - Now reduces red damage and blue damage taken by 65% + +- Buff + - Previously increased red power and speed stat by 25% + - Now increases red power, blue power and speed stat by 30% + - Increased duration from 2T -> 3T + +- Counter + - No longer applies block effect + - CounterAttack multiplier increased from 70/95/120 -> 120/160/230 + - Will now animate an attack animation on counter attack + +- Curse + - Now created by combining Debuff + RB was Debuff + RG + - Fixed issue where curse was only increasing blue damage taken instead of red and blue + - Fixed an issue where the animation would not display if it occured already + +- Haste + - Cooldown reduced to 1T (was 2T) + - Duration increased from 2/3/4T -> 3/4/5T + - HasteStrike damage multiplier changed from 30% -> 60% + +- Hybrid + - Cooldown reduced to 1T (was 3T) + - Duration increased from 2/3/4T -> 3/4/5T + - HybridBlast damage multiplier changed from 25% -> 50% + +- Invert + - Now created by combining Debuff + RG was debuff + RB + +- Silence + - Cooldown reduced to 2T (was 3T) + +- Siphon + - Skill multiplier increased from 20/25/30 -> 25/30/40 + - Reworded part of description "Construct heals self for 100% of damage dealt to target construct GreenLife." + +- Slay + - Self healing is now 50% of damage dealt to green life (was 100%) + - Damage multiplier increased from (40/60/90)% -> (45/65/100)% + +- Sleep + - Cooldown reduced to 2T (was 3T) + - Skill multiplier reduced from 240/300/400 -> 200/290/400 + + ## [1.6.6] - 2019-10-27 # Added - Offering of draws diff --git a/client/assets/styles/instance.less b/client/assets/styles/instance.less index 461f8eb0..0f05fc8b 100644 --- a/client/assets/styles/instance.less +++ b/client/assets/styles/instance.less @@ -446,6 +446,17 @@ } } +.tutorial { + button { + width: 100%; + } + + button.focus { + animation: co 0.75s cubic-bezier(0, 0, 1, 1) 0s infinite alternate; + } + +} + @keyframes faceoff { from { color: @black; diff --git a/client/src/actions.jsx b/client/src/actions.jsx index 8c58ed0d..4b9d11ac 100644 --- a/client/src/actions.jsx +++ b/client/src/actions.jsx @@ -50,6 +50,9 @@ export const setTeam = value => ({ type: 'SET_TEAM', value: Array.from(value) }) export const setTeamPage = value => ({ type: 'SET_TEAM_PAGE', value }); export const setTeamSelect = value => ({ type: 'SET_TEAM_SELECT', value: Array.from(value) }); +export const setTutorial = value => ({ type: 'SET_TUTORIAL', value }); +export const setTutorialGame = value => ({ type: 'SET_TUTORIAL_GAME', value }); + export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value }); export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value }); diff --git a/client/src/components/animations.jsx b/client/src/components/animations.jsx index 84953719..42b640fa 100644 --- a/client/src/components/animations.jsx +++ b/client/src/components/animations.jsx @@ -39,7 +39,6 @@ const Stun = require('./anims/stun'); const Triage = require('./anims/triage'); const TriageTick = require('./anims/triage.tick'); -const actions = require('../actions'); const { removeTier } = require('../utils'); @@ -47,10 +46,16 @@ const addState = connect( function receiveState(state) { const { animTarget } = state; return { animTarget }; - }, + } ); class ConstructAnimation extends Component { + shouldComponentUpdate(nextProps) { + if (nextProps.animTarget !== this.props.animTarget) return true; + if (nextProps.construct !== this.props.construct) return true; + return false; + } + render(props) { const { animTarget, @@ -70,7 +75,7 @@ class ConstructAnimation extends Component { // find target animation - const chooseAnim = (animSkill) => { + const chooseAnim = () => { switch (animSkill) { // Attack base case 'Attack': return ; @@ -124,7 +129,7 @@ class ConstructAnimation extends Component { case 'Reflect': return ; default: return false; - }; + } }; const anim = chooseAnim(animSkill); diff --git a/client/src/components/anims/blast.jsx b/client/src/components/anims/blast.jsx index 21d985fa..c11bdc95 100644 --- a/client/src/components/anims/blast.jsx +++ b/client/src/components/anims/blast.jsx @@ -36,7 +36,7 @@ class Blast extends Component { render() { return ( - + diff --git a/client/src/components/anims/counter.jsx b/client/src/components/anims/counter.jsx index 0ae9d709..ec7a0399 100644 --- a/client/src/components/anims/counter.jsx +++ b/client/src/components/anims/counter.jsx @@ -21,13 +21,13 @@ class Counter extends Component { render({ team }) { return ( - + @@ -60,7 +60,7 @@ class Counter extends Component { this.animations.push(anime({ targets: ['#counter'], rotateX: 180, - delay: TIMES.TARGET_DELAY_MS * 2, + delay: TIMES.TARGET_DELAY_MS + TIMES.TARGET_DURATION_MS / 3, duration: TIMES.TARGET_DURATION_MS / 2, easing: 'easeOutSine', })); diff --git a/client/src/components/anims/curse.jsx b/client/src/components/anims/curse.jsx index 0747b1c8..d186a4d3 100644 --- a/client/src/components/anims/curse.jsx +++ b/client/src/components/anims/curse.jsx @@ -21,9 +21,9 @@ class Curse extends Component { render() { return ( @@ -36,15 +36,15 @@ class Curse extends Component { - - - + + + - - - + + + ); @@ -61,12 +61,27 @@ class Curse extends Component { })); this.animations.push(anime({ - targets: ['#curse circle'], - r: 0, + targets: ['#curseCircleOne', '#curseFilterOne'], + r: 30, easing: 'easeInOutSine', - delay: TIMES.TARGET_DURATION_MS / 2, + delay: TIMES.TARGET_DELAY_MS, + duration: TIMES.TARGET_DURATION_MS, + })); + + this.animations.push(anime({ + targets: ['#curseCircleTwo', '#curseFilterTwo'], + r: 60, + easing: 'easeInOutSine', + delay: TIMES.TARGET_DELAY_MS, + duration: TIMES.TARGET_DURATION_MS, + })); + + this.animations.push(anime({ + targets: ['#curseCircleThree', '#curseFilterThree'], + r: 90, + easing: 'easeInOutSine', + delay: TIMES.TARGET_DELAY_MS, duration: TIMES.TARGET_DURATION_MS, - direction: 'reverse', })); } diff --git a/client/src/components/anims/debuff.jsx b/client/src/components/anims/debuff.jsx index 0f68d33d..14bbab7a 100644 --- a/client/src/components/anims/debuff.jsx +++ b/client/src/components/anims/debuff.jsx @@ -21,7 +21,7 @@ class Debuff extends Component { render() { return ( diff --git a/client/src/components/anims/hex.jsx b/client/src/components/anims/hex.jsx index c279225d..fb31b373 100644 --- a/client/src/components/anims/hex.jsx +++ b/client/src/components/anims/hex.jsx @@ -23,7 +23,7 @@ class Hex extends Component { render() { return ( diff --git a/client/src/components/anims/intercept.jsx b/client/src/components/anims/intercept.jsx index 1edb45a1..5049febf 100644 --- a/client/src/components/anims/intercept.jsx +++ b/client/src/components/anims/intercept.jsx @@ -25,7 +25,7 @@ class Intercept extends Component { render() { return ( diff --git a/client/src/components/anims/triage.tick.jsx b/client/src/components/anims/triage.tick.jsx index afeb6034..ecda83f1 100644 --- a/client/src/components/anims/triage.tick.jsx +++ b/client/src/components/anims/triage.tick.jsx @@ -37,8 +37,8 @@ class TriageTick extends Component { render() { return ( diff --git a/client/src/components/anims/wiggle.jsx b/client/src/components/anims/wiggle.jsx index 5886a47d..8a54b096 100644 --- a/client/src/components/anims/wiggle.jsx +++ b/client/src/components/anims/wiggle.jsx @@ -6,7 +6,7 @@ function wiggle(id, idle) { const target = document.getElementById(id); const x = window.innerWidth * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random()); const y = window.innerHeight * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random()); - + const originalX = parseFloat(idle.animations[0].currentValue); const originalY = parseFloat(idle.animations[1].currentValue); // console.log(x, y); diff --git a/client/src/components/construct.jsx b/client/src/components/construct.jsx index 334ba19e..19e9fd20 100644 --- a/client/src/components/construct.jsx +++ b/client/src/components/construct.jsx @@ -29,12 +29,13 @@ class ConstructAvatar extends Component { } render() { - const { construct } = this.props; + const { construct, mouseOver } = this.props; return (
@@ -60,42 +61,36 @@ class ConstructAvatar extends Component { this.resetAnimations(); } - shouldComponentUpdate(newProps) { - const { animSource, animTarget, animText, construct } = newProps; - - if (construct !== this.props.construct) { - return true; - } - - if (animText && animText !== this.props.animText && animText.constructId === construct.id) { + componentDidUpdate(prevProps) { + const { animSource, animTarget, animText, construct } = this.props; + // a different text object and text construct + if (animText && animText !== prevProps.animText && animText.constructId === construct.id) { return wiggle(construct.id, this.idle); } - if (animSource === this.props.animSource && animTarget === this.props.animTarget) { - // console.warn(construct.name, 'thinks its same props') - return false; - } - - // something has changed - // what do? - - // this is the source - if (animSource && animSource.constructId === construct.id) { - // console.warn(construct.name, 'should update') + // different source object and source construct + if (animSource && animSource !== prevProps.animSource && animSource.constructId === construct.id) { return sourceCast(animSource.constructId, animSource.direction, this.idle); } - - // this is the target - if (animTarget && animTarget.constructId.includes(construct.id)) { - // console.warn(construct.name, 'should update') + // different target object and target construct + if (animTarget && animTarget !== prevProps.animTarget && animTarget.constructId.includes(construct.id)) { switch (animTarget.skill) { case 'Banish': return banish(construct.id, this.idle); case 'Invert': return invert(construct.id, this.idle); default: return null; } } + return true; + } + + shouldComponentUpdate(newProps) { + const { animSource, animTarget, animText, construct } = newProps; + if (animSource !== this.props.animSource) return true; + if (animTarget !== this.props.animTarget) return true; + if (animText !== this.props.animText) return true; + if (construct !== this.props.construct) return true; return false; } } diff --git a/client/src/components/demo.jsx b/client/src/components/demo.jsx index 57c814ee..45dcf71d 100644 --- a/client/src/components/demo.jsx +++ b/client/src/components/demo.jsx @@ -1,11 +1,11 @@ const { connect } = require('preact-redux'); const preact = require('preact'); -const actions = require('../actions'); +// const actions = require('../actions'); const shapes = require('./shapes'); const { ConstructAvatar } = require('./construct'); -const { ConstructAnimation } = require('./animations'); +// const { ConstructAnimation } = require('./animations'); const addState = connect( function receiveState(state) { @@ -20,15 +20,15 @@ const addState = connect( itemInfo, demo, }; - }, + } - function receiveDispatch(dispatch) { +/* function receiveDispatch(dispatch) { function setAnimTarget(anim) { dispatch(actions.setAnimTarget(anim)); } return { setAnimTarget }; - } + } */ ); @@ -38,12 +38,12 @@ function Demo(args) { itemInfo, account, - setAnimTarget, + // setAnimTarget, } = args; if (!demo || !itemInfo.items.length || account) return false; - const { combiner, items, equipping, equipped, players } = demo; + const { combiner, items, equipping, equipped, players, combo } = demo; const vboxDemo = () => { function inventoryBtn(i, j) { @@ -90,7 +90,7 @@ function Demo(args) { VBOX PHASE {shapes.Red()} {shapes.Green()} {shapes.Blue()}

- Combine the colour base items with an array of skills and specialisations to build powerful variants. + Combine colours with base skills and specialisations to build an array of powerful variants.

 
@@ -122,7 +122,7 @@ function Demo(args) {
{equipped - ? + ? : } @@ -179,10 +179,10 @@ function Demo(args) { }; return ( -
+
+ {gameDemo()} {vboxDemo()} {vboxConstructs()} - {gameDemo()}
); } diff --git a/client/src/components/faceoff.jsx b/client/src/components/faceoff.jsx index 251f10e6..ea3858cd 100644 --- a/client/src/components/faceoff.jsx +++ b/client/src/components/faceoff.jsx @@ -1,34 +1,25 @@ const preact = require('preact'); const { connect } = require('preact-redux'); -const actions = require('../actions'); - const { ConstructAvatar } = require('./construct'); -const Controls = require('./controls'); const addState = connect( function receiveState(state) { const { - ws, instance, account, } = state; - function sendInstanceReady() { - return ws.sendInstanceReady(instance.id); - } - return { instance, account, - sendInstanceReady, }; - }, + } ); function FaceoffConstruct(args) { const { - construct + construct, } = args; return ( @@ -39,75 +30,82 @@ function FaceoffConstruct(args) {
- ) -} - -function Faceoff(props) { - const { - instance, - account, - sendInstanceReady, - } = props; - - if (!instance) return
...
; - - const otherTeam = instance.players.find(t => t.id !== account.id); - const playerTeam = instance.players.find(t => t.id === account.id); - - function PlayerTeam(team) { - const constructs = team.constructs.map((c, i) => - ); - - const winner = instance.winner === team.id; - const classes = `team player ${winner ? 'winner' : team.ready ? 'ready' : ''}` - return ( -
- {constructs} -
- ); - } - - - function OpponentTeam(team) { - const constructs = team.constructs.map((c, i) => - ); - - const winner = instance.winner === team.id; - const classes = `team opponent ${winner ? 'winner' : team.ready ? 'ready' : ''}` - - return ( -
- {constructs} -
- ); - } - function faceoffText() { - if (!instance.winner) { - return ( -
-
{otherTeam.name}
-
vs
-
{playerTeam.name}
-
- ); - } - const winner = instance.winner === playerTeam.id ? playerTeam : otherTeam; - return ( -
-
{winner.name}
-
wins
-
- ) - - } - - return ( -
- {OpponentTeam(otherTeam)} - {faceoffText()} - {PlayerTeam(playerTeam)} -
); } +class Faceoff extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.instance !== this.props.instance) return true; + if (newProps.account !== this.props.account) return true; + return false; + } + + render(props) { + const { + instance, + account, + } = props; + + if (!instance) return
...
; + + const otherTeam = instance.players.find(t => t.id !== account.id); + const playerTeam = instance.players.find(t => t.id === account.id); + + function PlayerTeam(team) { + const constructs = team.constructs.map((c, i) => + ); + + const winner = instance.winner === team.id; + const classes = `team player ${winner ? 'winner' : team.ready ? 'ready' : ''}`; + return ( +
+ {constructs} +
+ ); + } + + + function OpponentTeam(team) { + const constructs = team.constructs.map((c, i) => + ); + + const winner = instance.winner === team.id; + const classes = `team opponent ${winner ? 'winner' : team.ready ? 'ready' : ''}`; + + return ( +
+ {constructs} +
+ ); + } + function faceoffText() { + if (!instance.winner) { + return ( +
+
{otherTeam.name}
+
vs
+
{playerTeam.name}
+
+ ); + } + const winner = instance.winner === playerTeam.id ? playerTeam : otherTeam; + return ( +
+
{winner.name}
+
wins
+
+ ) + + } + + return ( +
+ {OpponentTeam(otherTeam)} + {faceoffText()} + {PlayerTeam(playerTeam)} +
+ ); + } +} + module.exports = addState(Faceoff); diff --git a/client/src/components/game.construct.jsx b/client/src/components/game.construct.jsx index 186a55ef..75a1677d 100644 --- a/client/src/components/game.construct.jsx +++ b/client/src/components/game.construct.jsx @@ -24,6 +24,7 @@ const addState = connect( animText, gameSkillInfo, itemInfo, + tutorialGame, } = state; function selectSkillTarget(targetConstructId) { @@ -33,10 +34,6 @@ const addState = connect( return false; } - // intercept self casting skills - if (activeSkill && activeSkill.skill.self_targeting) { - ws.sendGameSkill(game.id, activeSkill.constructId, null, activeSkill.skill.skill); - } return { game, account, @@ -47,6 +44,7 @@ const addState = connect( selectSkillTarget, gameSkillInfo, itemInfo, + tutorialGame, }; }, @@ -55,7 +53,11 @@ const addState = connect( dispatch(actions.setGameEffectInfo(info)); } - return { setGameEffectInfo }; + function setTutorialGameClear(activeSkill, tutorialGame) { + if (activeSkill && tutorialGame) dispatch(actions.setTutorialGame(null)); + } + + return { setGameEffectInfo, setTutorialGameClear }; } ); @@ -83,20 +85,36 @@ class GameConstruct extends Component { this.resolvedLength = 0; } + shouldComponentUpdate(newProps) { + if (newProps.activeSkill !== this.props.activeSkill) return true; + if (newProps.animFocus !== this.props.animFocus) return true; + if (newProps.animText !== this.props.animText) return true; + if (newProps.animating !== this.props.animating) return true; + if (newProps.construct !== this.props.construct) return true; + if (newProps.player !== this.props.player) return true; + if (newProps.tutorialGame !== this.props.tutorialGame) return true; + if (newProps.gameSkillInfo !== this.props.gameSkillInfo) return true; + return false; + } + render() { const { - i, + // Changing state variables + activeSkill, + animFocus, + animText, animating, construct, player, - activeSkill, - selectSkillTarget, - animFocus, - animText, - - setGameEffectInfo, + tutorialGame, gameSkillInfo, + // Constants + i, itemInfo, + // Functions + selectSkillTarget, + setTutorialGameClear, + setGameEffectInfo, } = this.props; const ko = construct.green_life.value === 0 ? 'ko' : ''; @@ -142,11 +160,14 @@ class GameConstruct extends Component { > {c.effect} - {c.duration}T) : null; return (
{effects}
); - } + }; return (
selectSkillTarget(construct.id)} + onClick={() => { + selectSkillTarget(construct.id); + setTutorialGameClear(activeSkill, tutorialGame); + }} style={ activeSkill ? { cursor: 'pointer' } : {}} class={`game-construct ${ko} ${classes}`} >
diff --git a/client/src/components/game.ctrl.btns.jsx b/client/src/components/game.ctrl.btns.jsx index 3918b5ca..557e0915 100644 --- a/client/src/components/game.ctrl.btns.jsx +++ b/client/src/components/game.ctrl.btns.jsx @@ -46,7 +46,6 @@ const addState = connect( function quit() { dispatch(actions.setNav('transition')); dispatch(actions.setGame(null)); - dispatch(actions.setInstance(null)); } function setChatShow(v) { diff --git a/client/src/components/game.ctrl.btns.top.jsx b/client/src/components/game.ctrl.btns.top.jsx index 8e09718c..55c4caa0 100644 --- a/client/src/components/game.ctrl.btns.top.jsx +++ b/client/src/components/game.ctrl.btns.top.jsx @@ -8,6 +8,7 @@ const addState = connect( const { ws, game, + animating, account, } = state; @@ -25,17 +26,19 @@ const addState = connect( sendAbandon, sendDraw, + animating, }; }, + function receiveDispatch(dispatch) { function leave() { dispatch(actions.setNav('play')); dispatch(actions.setGame(null)); - dispatch(actions.setInstance(null)); } return { leave }; } + ); function GameCtrlTopBtns(args) { @@ -46,6 +49,7 @@ function GameCtrlTopBtns(args) { leave, sendAbandon, sendDraw, + animating, } = args; const finished = game && game.phase === 'Finished'; @@ -70,14 +74,14 @@ function GameCtrlTopBtns(args) { const abandonText = abandonState ? 'Confirm' : 'Abandon'; const abandonAction = abandonState ? sendAbandon : abandonStateTrue; - const abandonBtn = ; + const abandonBtn = ; const drawClasses = `draw ${drawState || drawOffered ? 'confirming' : ''}`; const drawText = drawOffered ? 'Offered' : drawState ? 'Draw' : 'Offer'; const drawAction = drawState ? sendDraw : drawStateTrue; - const drawBtn = ; + const drawBtn = ; return (
diff --git a/client/src/components/info.component.jsx b/client/src/components/info.component.jsx index 3aebbc14..0666010a 100644 --- a/client/src/components/info.component.jsx +++ b/client/src/components/info.component.jsx @@ -1,207 +1,163 @@ const preact = require('preact'); -const range = require('lodash/range'); const reactStringReplace = require('react-string-replace'); +const specThresholds = require('./info.thresholds'); const { INFO } = require('./../constants'); const { convertItem, removeTier } = require('../utils'); +const { tutorialStage } = require('../tutorial.utils'); const shapes = require('./shapes'); -function InfoComponent(args) { - const { - itemInfo, - player, - info, - } = args; - // args.info = 'Life'; - // const { info } = args; +class InfoComponent extends preact.Component { + shouldComponentUpdate(newProps) { + 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; + return false; + } - function Info() { - if (!info) { - return ( -
-

VBOX phase

-

Strengthen and specialise your constructs by equipping items to them.

-

Double click to purchase items in the VBOX and move them to your INVENTORY.

-

- Combine a SKILL or SPEC with 2 COLOURS to create an item.
- Combine 3 of the same item to upgrade it.
- Click an item and then click a construct to equip that item to it.
-

-

Click the READY button for the GAME PHASE.

-
- ); - } - const fullInfo = itemInfo.items.find(i => i.item === info) || INFO[info]; - if (!fullInfo) return false; - const isSkill = fullInfo.skill; - const isSpec = fullInfo.spec; + render(args) { + const { + // Variables that will change + info, + tutorial, - if (isSkill) { - const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/; - const infoDescription = reactStringReplace(fullInfo.description, regEx, match => shapes[match]()); - const itemSource = itemInfo.combos.filter(c => c.item === removeTier(info)); - const itemSourceInfo = itemSource.length - ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` - : false; - const itemRegEx = /(Red|Blue|Green)/; - const itemSourceDescription = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); - const speed =
Speed {shapes.SpeedStat()} multiplier {fullInfo.speed * 4}%
; - const cooldown = fullInfo.cooldown ? `${fullInfo.cooldown} Turn delay` : null; - return ( -
-

{fullInfo.item} - {fullInfo.cost}b

-

SKILL

- {itemSourceDescription} -
{cooldown}
-
{infoDescription}
- {speed} -
- ); - } + // Static + player, // Only used for colour calcs which will be update if info changes + ws, + itemInfo, + instance, // Only used for instance id + // functions + setInfo, + setTutorialNull, + } = args; - if (isSpec) { - let red = 0; - let blue = 0; - let green = 0; - player.constructs.forEach(construct => { - red += construct.colours.red; - blue += construct.colours.blue; - green += construct.colours.green; - }); - const teamColours = { red, blue, green }; - - const colourReqs = fullInfo.values.bonuses || []; - - const thresholds = colourReqs.map((bonus, i) => { - const colours = ['red', 'green', 'blue']; - const colourGoals = { - red: [], - green: [], - blue: [], - }; - const overFlow = []; - - colours.forEach(c => { - const colourReq = bonus.req[c]; - if (colourReqs === 0) return false; - - const start = i === 0 - ? 0 - : colourReqs[i - 1].req[c]; - - const dots = range(start, colourReq).map(j => { - const unmet = teamColours[c] < j + 1; - - - const reqClass = unmet - ? 'unmet' - : ''; - - if (j - start > 4) { - overFlow.push( -
- {shapes.vboxColour(c)} -
- ); - } else { - colourGoals[c].push( -
- {shapes.vboxColour(c)} -
- ); - } - - return true; - }); - - return dots; - }); - - const reqsMet = colours.every(c => teamColours[c] >= bonus.req[c]); - - const reqClass = reqsMet - ? '' - : 'unmet'; - - const goals = colours.map((c, j) => { - if (colourGoals[c].length) { - return ( -
{colourGoals[c]}
- ); - } - return false; - }); - const bonusObj = info.includes('Life') - ?
+ {bonus.bonus}
- :
+ {bonus.bonus}%
; - const overFlowObj = overFlow.length ?
{overFlow}
: null; + function Info() { + if (tutorial) { + const tutorialStageInfo = tutorialStage(tutorial, ws, setTutorialNull, instance); + if (tutorialStageInfo) return tutorialStageInfo; + } + if (!info) return false; + if (info.includes('constructName')) { return ( -
- {goals} - {overFlowObj} - {bonusObj} +
+

{info.replace('constructName ', '')}

+

This is the name of your construct.
+ Names are randomly generated and are purely cosmetic.
+ You can change change your construct name in the RESHAPE tab outside of games. +

+
+ ); + } + + if (info.includes('constructAvatar')) { + return ( +
+

{info.replace('constructAvatar ', '')}

+

This is your construct avatar.
+ Avatars are randomly generated and are purely cosmetic.
+ You can change your construct avatar in the RESHAPE tab outside of games. +

); - }); - const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat)/; - const infoDescription = reactStringReplace(fullInfo.description, regEx, match => shapes[match]()); - const itemSource = itemInfo.combos.filter(c => c.item === info); - const itemSourceInfo = itemSource.length - ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` - : false; - const itemRegEx = /(Red|Blue|Green)/; - const itemSourceDescription = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); - const infoText = info.replace('Plus', '+'); + } + const fullInfo = itemInfo.items.find(i => i.item === info) || INFO[info]; + if (!fullInfo) return false; + const isSkill = fullInfo.skill; + const isSpec = fullInfo.spec; - return ( -
-

{infoText} - {fullInfo.cost}b

-

SPEC

- {itemSourceDescription} -
{infoDescription}
-
+ const itemDescription = () => { + const regEx = /(RedPower|BluePower|GreenPower|RedLife|BlueLife|GreenLife|SpeedStat|LIFE|SPEED|POWER)/; + const infoDescription = reactStringReplace(fullInfo.description, regEx, m => shapes[m]()); + return
{reactStringReplace(infoDescription, '\n', () =>
)}
; + }; + + if (isSkill || isSpec) { + let infoName = info; + while (infoName.includes('Plus')) infoName = infoName.replace('Plus', '+'); + + const header = isSkill ?

SKILL

:

SPEC

; + + const itemSource = itemInfo.combos.filter(c => c.item === removeTier(info)); + let itemSourceInfo = itemSource.length + ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` + : false; + + if (itemSourceInfo) { + while (itemSourceInfo.includes('Plus')) itemSourceInfo = itemSourceInfo.replace('Plus', '+'); + const itemRegEx = /(Red|Blue|Green)/; + itemSourceInfo = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); + } + + const cooldown = isSkill && fullInfo.cooldown ?
{fullInfo.cooldown} Turn delay
: null; + + const speed = isSkill + ?
Speed {shapes.SpeedStat()} multiplier {fullInfo.speed * 4}%
+ : null; + + const thresholds = isSpec ? specThresholds(player, fullInfo, info) : null; + + return ( +
+

{infoName} {fullInfo.cost}b

+ {header} + {itemSourceInfo} + {cooldown} + {itemDescription()} + {speed} {thresholds}
+ ); + } + const cost = fullInfo.cost ? `- ${fullInfo.cost}b` : false; + return ( +
+

{fullInfo.item} {cost}

+ {itemDescription()}
); } - const cost = fullInfo.cost ? `- ${fullInfo.cost}b` : false; + + function Combos() { + if (tutorial && instance.time_control === 'Practice' && instance.rounds.length === 1) return false; + const generalNotes = ( +
+

General

+

+ You can preview combos by clicking the combined item when it appears in this section.
+ Click the READY button to start the GAME PHASE. +

+
+ ); + if (!player) return generalNotes; + if (!info) return generalNotes; + + const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info)); + if (vboxCombos.length > 6 || vboxCombos.length === 0) return generalNotes; + + return ( + + + {vboxCombos.map((c, i) => + + + {c.components.map((u, j) => )} + + )} + +
setInfo(c.item)} >{convertItem(c.item)}{convertItem(u)}
+ ); + } + return ( -
-

{fullInfo.item} {cost}

-
{fullInfo.description}
+
+ +
); } - - function Combos() { - if (!player) return false; - if (!info) return false; - - const vboxCombos = itemInfo.combos.filter(c => c.components.includes(info)); - if (vboxCombos.length > 6) return false; - - return ( - - - {vboxCombos.map((c, i) => - - - {c.components.map((u, j) => )} - - )} - -
{convertItem(c.item)}{convertItem(u)}
- ); - } - - return ( -
- - -
- ); } module.exports = InfoComponent; diff --git a/client/src/components/info.container.jsx b/client/src/components/info.container.jsx index 5f8639b8..aec4b282 100644 --- a/client/src/components/info.container.jsx +++ b/client/src/components/info.container.jsx @@ -13,6 +13,7 @@ const addState = connect( instance, player, account, + tutorial, } = state; return { @@ -23,8 +24,22 @@ const addState = connect( instance, player, account, + tutorial, }; }, + + function receiveDispatch(dispatch) { + function setTutorialNull() { + dispatch(actions.setTutorial(null)); + } + + function setInfo(info) { + dispatch(actions.setInfo(info)); + } + return { setTutorialNull, setInfo }; + } + + ); module.exports = addState(Info); diff --git a/client/src/components/info.thresholds.jsx b/client/src/components/info.thresholds.jsx new file mode 100644 index 00000000..7136a855 --- /dev/null +++ b/client/src/components/info.thresholds.jsx @@ -0,0 +1,96 @@ +const preact = require('preact'); +const range = require('lodash/range'); +const shapes = require('./shapes'); + +function specThresholds(player, fullInfo, info) { + let red = 0; + let blue = 0; + let green = 0; + player.constructs.forEach(construct => { + red += construct.colours.red; + blue += construct.colours.blue; + green += construct.colours.green; + }); + const teamColours = { red, blue, green }; + + const colourReqs = fullInfo.values.bonuses || []; + + const thresholds = colourReqs.map((bonus, i) => { + const colours = ['red', 'green', 'blue']; + const colourGoals = { + red: [], + green: [], + blue: [], + }; + const overFlow = []; + + colours.forEach(c => { + const colourReq = bonus.req[c]; + if (colourReqs === 0) return false; + + const start = i === 0 + ? 0 + : colourReqs[i - 1].req[c]; + + const dots = range(start, colourReq).map(j => { + const unmet = teamColours[c] < j + 1; + + + const reqClass = unmet + ? 'unmet' + : ''; + + if (j - start > 4) { + overFlow.push( +
+ {shapes.vboxColour(c)} +
+ ); + } else { + colourGoals[c].push( +
+ {shapes.vboxColour(c)} +
+ ); + } + + return true; + }); + + return dots; + }); + + const reqsMet = colours.every(c => teamColours[c] >= bonus.req[c]); + + const reqClass = reqsMet + ? '' + : 'unmet'; + + const goals = colours.map((c, j) => { + if (colourGoals[c].length) { + return ( +
{colourGoals[c]}
+ ); + } + return false; + }); + const bonusObj = info.includes('Life') + ?
+ {bonus.bonus}
+ :
+ {bonus.bonus}%
; + const overFlowObj = overFlow.length ?
{overFlow}
: null; + return ( +
+ {goals} + {overFlowObj} + {bonusObj} +
+ ); + }); + return ( +
+ {thresholds} +
+ ); +} + +module.exports = specThresholds; diff --git a/client/src/components/instance.component.jsx b/client/src/components/instance.component.jsx index abd2e9da..b5e98674 100644 --- a/client/src/components/instance.component.jsx +++ b/client/src/components/instance.component.jsx @@ -7,18 +7,19 @@ const Hammer = require('hammerjs'); const Vbox = require('./vbox.component'); const InfoContainer = require('./info.container'); const InstanceConstructsContainer = require('./instance.constructs'); -// const EquipmentContainer = require('./instance.equip'); const Faceoff = require('./faceoff'); const actions = require('../actions'); const addState = connect( function receiveState(state) { - const { instance, + const { + instance, nav, navInstance, } = state; - return { instance, + return { + instance, nav, navInstance, }; @@ -53,10 +54,14 @@ const addState = connect( ); class Instance extends Component { + shouldComponentUpdate(newProps) { + if (newProps.instance !== this.props.instance) return true; + return false; + } + render(args) { const { instance, - setInfo, clearItems, } = args; @@ -76,7 +81,7 @@ class Instance extends Component { } return ( -
setInfo(null)}> +
diff --git a/client/src/components/instance.constructs.jsx b/client/src/components/instance.constructs.jsx index bc7e005f..1e010061 100644 --- a/client/src/components/instance.constructs.jsx +++ b/client/src/components/instance.constructs.jsx @@ -8,6 +8,7 @@ const { STATS } = require('../utils'); const { ConstructAvatar } = require('./construct'); const actions = require('../actions'); const { removeTier } = require('../utils'); +const { tutorialConstructDisplay, tutorialShouldDisableEquip } = require('../tutorial.utils.jsx'); const addState = connect( function receiveState(state) { @@ -18,8 +19,8 @@ const addState = connect( account, itemInfo, itemEquip, - activeConstruct, navInstance, + tutorial, } = state; function sendVboxApply(constructId, i) { @@ -38,8 +39,8 @@ const addState = connect( itemInfo, itemEquip, navInstance, - activeConstruct, sendUnequip, + tutorial, }; }, @@ -56,10 +57,6 @@ const addState = connect( dispatch(actions.setActiveConstruct(value)); } - function clearInfo() { - return dispatch(actions.setInfo(null)); - } - function setItemEquip(v) { return dispatch(actions.setItemEquip(v)); } @@ -68,29 +65,45 @@ const addState = connect( return dispatch(actions.setItemUnequip(v)); } - return { quit, clearInfo, setInfo, setActiveConstruct, setItemUnequip, setItemEquip }; + return { quit, setInfo, setActiveConstruct, setItemUnequip, setItemEquip }; } ); function Construct(props) { const { - itemEquip, + // Changing state variables construct, + iter, + itemEquip, + instance, + mobileVisible, player, + tutorial, + // Static Info + itemInfo, + // Function Calls sendVboxApply, + sendUnequip, setActiveConstruct, setItemUnequip, setItemEquip, - itemInfo, setInfo, - sendUnequip, - mobileVisible, } = props; + const { vbox } = player; + + const duplicateSkill = construct.skills.length !== 0 && construct.skills.every(sk => { + if (!itemEquip && itemEquip !== 0) return false; + if (!sk) return false; + return sk.skill === vbox.bound[itemEquip]; + }); + const tutorialDisableEquip = tutorialShouldDisableEquip(tutorial, iter, instance, construct); + function onClick(e) { e.stopPropagation(); e.preventDefault(); + if (duplicateSkill || tutorialDisableEquip) return true; if (itemEquip !== null) sendVboxApply(construct.id, itemEquip); setItemEquip(null); return setActiveConstruct(construct); @@ -102,7 +115,6 @@ function Construct(props) { return setInfo(info); } - const { vbox } = player; const skillList = itemInfo.items.filter(v => v.skill).map(v => v.item); const specList = itemInfo.items.filter(v => v.spec).map(v => v.item); @@ -130,8 +142,8 @@ function Construct(props) { return true; } - // const action = skill ? '' : 'action'; - const equipping = skillList.includes(vbox.bound[itemEquip]) && !skill; + const equipping = skillList.includes(vbox.bound[itemEquip]) && !skill + && !tutorialDisableEquip && !duplicateSkill; const border = () => { if (!skill) return ''; const borderFn = buttons[removeTier(skill.skill)]; @@ -166,8 +178,6 @@ function Construct(props) { ); } - const specInfo = itemInfo.items.find(i => i.item === s); - function specClick(e) { e.stopPropagation(); setItemUnequip([construct.id, s]); @@ -211,11 +221,11 @@ function Construct(props) { }); const classes = `instance-construct ${mobileVisible ? 'visible' : ''}`; - + const avatarMouseOver = e => hoverInfo(e, `constructAvatar ${construct.name}`); return ( -
- -

{construct.name}

+
+ +

hoverInfo(e, `constructName ${construct.name}`)}>{construct.name}

hoverInfo(e, 'constructSkills')} > {skills}
@@ -229,50 +239,69 @@ function Construct(props) { ); } -function InstanceConstructs(props) { - const { - activeConstruct, - itemEquip, - player, - instance, - // clearInfo, - setInfo, - setActiveConstruct, +class InstanceConstructs extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.itemEquip !== this.props.itemEquip) return true; + if (newProps.tutorial !== this.props.tutorial) return true; + if (newProps.navInstance !== this.props.navInstance) return true; + // JSON or Array objects + if (newProps.player !== this.props.player) return true; + if (newProps.instance !== this.props.instance) return true; + return false; + } - sendVboxApply, - itemInfo, - setVboxHighlight, - setItemUnequip, - setItemEquip, - sendUnequip, - navInstance, - } = props; + render(props) { + const { + // Changing state variables + itemEquip, + instance, + navInstance, + player, + tutorial, + // Static data + itemInfo, + // Function calls + setInfo, + setActiveConstruct, + sendVboxApply, + setVboxHighlight, + setItemUnequip, + setItemEquip, + sendUnequip, + } = props; - if (!player) return false; - if (instance.phase === 'Lobby') return false; + if (!player) return false; + if (instance.phase === 'Lobby') return false; - const constructs = player.constructs.map((c, i) => Construct({ - construct: c, - activeConstruct, - itemEquip, - setItemUnequip, - setItemEquip, - player, - sendVboxApply, - setInfo, - setActiveConstruct, - itemInfo, - setVboxHighlight, - sendUnequip, - mobileVisible: navInstance === i + 1, - })); + const constructs = range(0, 3).map(i => { + const tutorialConstruct = tutorialConstructDisplay(player, instance, tutorial, navInstance, i); + if (tutorialConstruct) return (tutorialConstruct); - const classes = `construct-list`; - return ( -
setActiveConstruct(null)}> - {constructs} -
- ); + return Construct({ + iter: i, + construct: player.constructs[i], + itemEquip, + instance, + setItemUnequip, + setItemEquip, + player, + sendVboxApply, + setInfo, + setActiveConstruct, + itemInfo, + setVboxHighlight, + sendUnequip, + tutorial, + mobileVisible: navInstance === i + 1, + }); + }); + + return ( +
setActiveConstruct(null)}> + {constructs} +
+ ); + } } module.exports = addState(InstanceConstructs); diff --git a/client/src/components/instance.ctrl.top.btns.jsx b/client/src/components/instance.ctrl.top.btns.jsx index 528e5d0e..108485f7 100644 --- a/client/src/components/instance.ctrl.top.btns.jsx +++ b/client/src/components/instance.ctrl.top.btns.jsx @@ -8,6 +8,7 @@ const addState = connect( const { ws, instance, + tutorial, } = state; function sendAbandon() { @@ -16,15 +17,16 @@ const addState = connect( return { instance, - + tutorial, sendAbandon, }; }, function receiveDispatch(dispatch) { - function leave() { + function leave(tutorial) { dispatch(actions.setNav('play')); dispatch(actions.setGame(null)); dispatch(actions.setInstance(null)); + if (tutorial) dispatch(actions.setTutorial(1)); } return { leave }; @@ -37,6 +39,7 @@ function InstanceTopBtns(args) { leave, sendAbandon, + tutorial, } = args; const finished = instance && instance.phase === 'Finished'; @@ -53,7 +56,7 @@ function InstanceTopBtns(args) { const abandonAction = abandonState ? sendAbandon : abandonStateTrue; const abandonBtn = ; - const leaveBtn = ; + const leaveBtn = ; return (
diff --git a/client/src/components/instance.equip.jsx b/client/src/components/instance.equip.jsx deleted file mode 100644 index ef4ac2e3..00000000 --- a/client/src/components/instance.equip.jsx +++ /dev/null @@ -1,144 +0,0 @@ -const { connect } = require('preact-redux'); -const preact = require('preact'); -const range = require('lodash/range'); - -const actions = require('../actions'); -const shapes = require('./shapes'); -const { convertItem } = require('./../utils'); - -const addState = connect( - function receiveState(state) { - const { account, activeConstruct, itemInfo, info, ws, instance, player, itemUnequip } = state; - - function sendUnequip(constructId, item) { - return ws.sendVboxUnequip(instance.id, constructId, item); - } - - return { player, itemInfo, instance, info, sendUnequip, activeConstruct, itemUnequip }; - }, - - function receiveDispatch(dispatch) { - function setInfo(item) { - dispatch(actions.setInfo(item)); - } - - function clearInfo() { - return dispatch(actions.setInfo(null)); - } - - function setItemEquip(v) { - return dispatch(actions.setItemEquip(v)); - } - - function setItemUnequip(v) { - return dispatch(actions.setItemUnequip(v)); - } - - return { setInfo, setItemEquip, setItemUnequip, clearInfo }; - } - -); - -function Equipment(props) { - const { - player, - instance, - - itemUnequip, - setItemEquip, - setItemUnequip, - activeConstruct, - - itemInfo, - sendUnequip, - - setInfo, - } = props; - - if (instance.phase === 'Lobby') return false; - - const { vbox } = player; - - const fullInfo = itemInfo.items.find(i => i.item === itemUnequip); - const isSkill = fullInfo && fullInfo.skill; - const isSpec = fullInfo && fullInfo.spec; - - function skillClick(e, i) { - if (itemUnequip && activeConstruct) return false; - // const value = vbox.bound[i]; - setItemEquip(i); - return false; - } - - function unequipClick(e) { - e.stopPropagation(); - if (!itemUnequip) return false; - if (!activeConstruct) return false; - setItemUnequip([]); - return sendUnequip(activeConstruct.id, itemUnequip); - } - - function hoverInfo(e, info) { - e.stopPropagation(); - return setInfo(info); - } - - const skillClass = isSkill ? 'skills highlight' : 'skills'; - const specClass = isSpec ? 'specs highlight' : 'specs'; - - const skills = range(0, 9).map(i => { - const item = convertItem(vbox.bound[i]); - const skillInfo = itemInfo.items.find(i => i.item === item); - if (skillInfo && skillInfo.skill) { - return ( - - ); - } - return false; - }); - - const specs = range(0, 9).map(i => { - const item = convertItem(vbox.bound[i]); - const specInfo = itemInfo.items.find(i => i.item === item); - if (specInfo && specInfo.spec) { - return ( -
skillClick(e, i)} onMouseOver={e => hoverInfo(e, item)} > - {shapes[item]()} -
{item ? item : '-'}
-
- ); - } - return false; - }); - - if (skills.every(s => !s)) skills.push(); - if (specs.every(s => !s)) { - specs.push( -
- {shapes.diamond('gray')} -
 
-
- ); - } - - return ( -
-
unequipClick(e)} onMouseOver={e => hoverInfo(e, 'equipSkills')} > -

Skills

-
- {skills} -
-
-
unequipClick(e)} onMouseOver={e => hoverInfo(e, 'equipSpecs')} > -

Specs

-
- {specs} -
-
-
- ); -} - -module.exports = addState(Equipment); diff --git a/client/src/components/main.jsx b/client/src/components/main.jsx index ab923182..b03e0069 100644 --- a/client/src/components/main.jsx +++ b/client/src/components/main.jsx @@ -16,35 +16,45 @@ const addState = connect( } ); -function Main(props) { - const { - game, - instance, - account, - nav, - } = props; - - if (!account) { - return ; +class Main extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.game !== this.props.game) return true; + if (newProps.instance !== this.props.instance) return true; + if (newProps.account !== this.props.account) return true; + if (newProps.nav !== this.props.nav) return true; + return false; } - if (game) { - return ; + render(props) { + const { + game, + instance, + account, + nav, + } = props; + + if (!account) { + return ; + } + + if (game) { + return ; + } + + if (instance) { + return ; + } + + if (nav === 'transition') return false; + + return ( +
+
+ + +
+ ); } - - if (instance) { - return ; - } - - if (nav === 'transition') return false; - - return ( -
-
- - -
- ); } module.exports = addState(Main); diff --git a/client/src/components/mnml.jsx b/client/src/components/mnml.jsx index 5c4ae51b..60dad587 100644 --- a/client/src/components/mnml.jsx +++ b/client/src/components/mnml.jsx @@ -10,11 +10,21 @@ const addState = connect( state => ({ showNav: state.showNav }) ); -const Mnml = ({ showNav }) => -
-
- -
-
; +class Mnml extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.showNav !== this.props.showNav) return true; + return false; + } + + render(args) { + return ( +
+
+ +
+
+ ); + } +} module.exports = addState(Mnml); diff --git a/client/src/components/particles.config.js b/client/src/components/particles.config.js deleted file mode 100644 index 651cae39..00000000 --- a/client/src/components/particles.config.js +++ /dev/null @@ -1,41 +0,0 @@ -const config = { - particles: { - number: { value: 300, density: { enable: true, value_area: 800 } }, - color: { value: '#ffffff' }, - shape: { - type: 'circle', - stroke: { width: 0, color: '#000000' }, - polygon: { nb_sides: 5 }, - image: { src: 'img/github.svg', width: 100, height: 100 } - }, - opacity: { - value: 0.5, - random: false, - anim: { enable: false, speed: 1, opacity_min: 0.1, sync: false }, - }, - size: { - value: 3, - random: true, - anim: { enable: false, speed: 40, size_min: 0.1, sync: false }, - }, - line_linked: { - enable: true, - distance: 150, - color: '#ffffff', - opacity: 0.4, - width: 1, - }, - move: { - enable: true, - speed: 6, - direction: 'none', - random: false, - straight: false, - out_mode: 'out', - bounce: false, - attract: { enable: false, rotateX: 600, rotateY: 1200 }, - }, - }, -}; - -module.exports = config; diff --git a/client/src/components/play.jsx b/client/src/components/play.jsx index 40f5b657..2059f774 100644 --- a/client/src/components/play.jsx +++ b/client/src/components/play.jsx @@ -139,39 +139,41 @@ function Play(args) { ; const list = () => { - if (!instances.length) return ( -
-
- -
Matchmaking
-
- {inviteBtn()} -
- -
Practice MNML
-
-
- -
Join the Community
-
-
- ); + if (!instances.length) { + return ( +
+
+ +
Matchmaking
+
+ {inviteBtn()} +
+ +
Practice MNML
+
+
+ +
Join the Community
+
+
+ ); + } return (
-
+
); - } + }; return (
@@ -201,9 +203,9 @@ function Play(args) {
- Join our Discord server to find opponents and talk to the devs.
- Message @ntr or @mashy for some credits to get started.
- Tutorial Playthrough on YouTube + Join our Discord server to find opponents and talk to the devs.
+ Message @ntr or @mashy for some credits to get started.
+ Tutorial Playthrough on YouTube

diff --git a/client/src/components/shapes.jsx b/client/src/components/shapes.jsx index a8a20ed8..454e51fe 100644 --- a/client/src/components/shapes.jsx +++ b/client/src/components/shapes.jsx @@ -35,7 +35,9 @@ module.exports = { BluePower: () => circle(['blue']), SpeedStat: () => triangle(['white']), - + POWER: () => circle(['white']), + LIFE: () => square(['white']), + SPEED: () => triangle(['white']), // specs // Base diff --git a/client/src/components/targeting.arrows.jsx b/client/src/components/targeting.arrows.jsx index c9e0f57c..b615517b 100644 --- a/client/src/components/targeting.arrows.jsx +++ b/client/src/components/targeting.arrows.jsx @@ -9,8 +9,8 @@ const shapes = require('./shapes'); const { effectInfo, removeTier } = require('../utils'); const addState = connect( - ({ game, account, animSkill, animating, itemInfo, gameEffectInfo }) => - ({ game, account, animSkill, animating, itemInfo, gameEffectInfo }) + ({ game, account, animSkill, animating, itemInfo, gameEffectInfo, tutorialGame }) => + ({ game, account, animSkill, animating, itemInfo, gameEffectInfo, tutorialGame }) ); class TargetSvg extends Component { @@ -27,10 +27,42 @@ class TargetSvg extends Component { }, 500); } + shouldComponentUpdate(newProps, newState) { + if (newProps.game !== this.props.game) return true; + if (newProps.account !== this.props.account) return true; + if (newProps.animating !== this.props.animating) return true; + if (newProps.animSkill !== this.props.animSkill) return true; + if (newProps.gameEffectInfo !== this.props.gameEffectInfo) return true; + if (newProps.tutorialGame !== this.props.tutorialGame) return true; + if (newState.width !== this.state.width) return true; + if (newState.height !== this.state.height) return true; + return false; + } + render(props, state) { - const { game, account, animating, animSkill, itemInfo, gameEffectInfo } = props; + const { + // Changing State Variables + account, + animating, + animSkill, + game, + gameEffectInfo, + tutorialGame, + // Static + itemInfo, + } = props; const { width, height } = state; if (!game) return false; // game will be null when battle ends + if (game.phase === 'Finished') return false; // Clear everything if its over (in case of abandon) + + // First time joining game phase + if (tutorialGame) { + return ( +
+

Select your skills, click on targets and then hit ready.

+
+ ); + } // Whenever someones looking at effects throw it up here if (gameEffectInfo) { diff --git a/client/src/components/vbox.component.jsx b/client/src/components/vbox.component.jsx index 266ae0b2..c2c1a51a 100644 --- a/client/src/components/vbox.component.jsx +++ b/client/src/components/vbox.component.jsx @@ -1,8 +1,9 @@ const preact = require('preact'); +const { connect } = require('preact-redux'); const range = require('lodash/range'); const countBy = require('lodash/countBy'); const without = require('lodash/without'); -const { connect } = require('preact-redux'); + const { removeTier } = require('../utils'); const shapes = require('./shapes'); const actions = require('../actions'); @@ -20,7 +21,7 @@ const addState = connect( itemInfo, itemUnequip, navInstance, - info, + tutorial, } = state; function sendVboxDiscard() { @@ -57,7 +58,7 @@ const addState = connect( itemUnequip, sendItemUnequip, navInstance, - info, + tutorial, }; }, @@ -93,350 +94,360 @@ const addState = connect( ); -function Vbox(args) { - const { - combiner, - navInstance, - instance, - itemInfo, - player, - reclaiming, - sendVboxAccept, - sendVboxCombine, - sendVboxDiscard, - sendVboxReclaim, - - setCombiner, - setInfo, - - vboxSelected, - setVboxSelected, - - setItemEquip, - itemUnequip, - sendItemUnequip, - - setReclaiming, - info, - } = args; - - if (!player) return false; - const { vbox } = player; - const vboxSelecting = vboxSelected.length; - - function combinerChange(newCombiner) { - setCombiner(newCombiner); - - if (newCombiner.length === 1) { - setItemEquip(newCombiner[0]); - } else { - setItemEquip(null); - } - - return true; +class Vbox extends preact.Component { + shouldComponentUpdate(newProps) { + // Single variable props + if (newProps.combiner !== this.props.combiner) return true; + if (newProps.itemUnequip !== this.props.itemUnequip) return true; + if (newProps.reclaiming !== this.props.reclaiming) return true; + if (newProps.navInstance !== this.props.navInstance) return true; + if (newProps.tutorial !== this.props.tutorial) return true; + if (newProps.vboxSelected !== this.props.vboxSelected) return true; + if (newProps.player !== this.props.player) return true; + if (newProps.instance !== this.props.instance) return true; + return false; } - // - // VBOX - // - function vboxHover(e, v) { - if (v) { - if (info !== v) setInfo(v); - e.stopPropagation(); - } - return true; - } + render(args) { + const { + // Changing state variables + combiner, + itemUnequip, + player, + reclaiming, + tutorial, + navInstance, + vboxSelected, + instance, - function clearVboxSelected() { - setVboxSelected([]); - } + // Static + itemInfo, + // Function Calls + sendItemUnequip, + sendVboxAccept, + sendVboxCombine, + sendVboxDiscard, + sendVboxReclaim, + setVboxSelected, + setItemEquip, + setInfo, + setCombiner, + setReclaiming, + } = args; - function vboxBuySelected() { - if (!vboxSelecting) return false; - document.activeElement.blur(); - clearVboxSelected(); - sendVboxAccept(vboxSelected[0], vboxSelected[1]); - return true; - } + if (!player) return false; + const { vbox } = player; + const vboxSelecting = vboxSelected.length; - function availableBtn(v, group, index) { - if (!v) return ; - const tutorial = instance.time_control === 'Practice' - && instance.rounds.length === 1 - && group === 0 - && combiner.length === 0 - && vboxSelected.length === 0 - && vbox.bits > 10 - && vbox.free[0].filter(c => c).length > 4 - ? 'combo-border' : null; + function combinerChange(newCombiner) { + setCombiner(newCombiner); - const selected = vboxSelected[0] === group && vboxSelected[1] === index; - - // state not yet set in double click handler - function onDblClick(e) { - clearVboxSelected(); - sendVboxAccept(group, index); - e.stopPropagation(); - } - - function onClick(e) { - e.stopPropagation(); - setItemEquip(null); - setCombiner([]); - - if (selected) return clearVboxSelected(); - return setVboxSelected([group, index]); - } - - const combinerItems = combiner.map(j => vbox.bound[j]); - const combinerCount = countBy(combinerItems, co => co); - - const comboHighlight = combinerItems.length > 0 && itemInfo.combos.some(combo => { - if (combo.components.includes(v)) { - return combinerItems.every(c => { - if (!combo.components.includes(c)) return false; - const comboCount = countBy(combo.components, co => co); - if (combinerCount[c] > comboCount[c]) return false; - if (c === v && combinerCount[c] + 1 > comboCount[c]) return false; - return true; - }); - } return false; - }) ? 'combo-border' : ''; - - const classes = `${v.toLowerCase()} ${selected ? 'highlight' : ''} ${comboHighlight} ${tutorial}`; - - if (shapes[v]) { - return ( - - ); - } - - return ( - - ); - } - - - function vboxElement() { - return ( -
setReclaiming(false)} - onClick={e => e.stopPropagation()} - onMouseOver={e => hoverInfo(e, 'vbox')}> -
-

e.target.scrollIntoView(true)}>VBOX

-
hoverInfo(e, 'bits')} >{vbox.bits}b
-
-
- {range(0, 6).map(i => availableBtn(vbox.free[0][i], 0, i))} -
-
- {range(0, 3).map(i => availableBtn(vbox.free[1][i], 1, i))} - {range(0, 3).map(i => availableBtn(vbox.free[2][i], 2, i))} -
- -
- ); - } - - // - // INVENTORY - // - function reclaimClick(e) { - e.stopPropagation(); - return setReclaiming(!reclaiming); - } - - const inventoryClass = `vbox-section ${reclaiming ? 'reclaiming' : ''}`; - - function inventoryBtn(v, i) { - const inventoryHighlight = vboxSelecting || itemUnequip.length; - - if (!v && v !== 0) { - return ; - } - - const tutorial = instance.time_control === 'Practice' - && instance.rounds.length === 1 - && i === 0 - && combiner.length === 0 - && vboxSelected.length === 0 - && vbox.bits === 16 - && vbox.bound.length === 5 - && vbox.free[0].filter(c => c).length === 4 - ? 'combo-border' : null; - - const combinerItems = combiner.map(j => vbox.bound[j]); - const combinerCount = countBy(combinerItems, co => co); - - const comboHighlight = combinerItems.length > 0 && itemInfo.combos.some(combo => { - if (combo.components.includes(v)) { - return combinerItems.every(c => { - if (!combo.components.includes(c)) return false; - const comboCount = countBy(combo.components, co => co); - if (combinerCount[c] > comboCount[c]) return false; - if (c === v && combinerCount[c] + 1 > comboCount[c]) return false; - return true; - }); - } return false; - }) ? 'combo-border' : ''; - - function onClick(e) { - if (vboxSelecting) clearVboxSelected(); - if (reclaiming) return sendVboxReclaim(i); - - // 4 things selected - if (combiner.length > 2) return combinerChange([i]); - - // removing - const combinerIndex = combiner.indexOf(i); - if (combinerIndex > -1) { - return combinerChange(without(combiner, i)); + if (newCombiner.length === 1) { + setItemEquip(newCombiner[0]); + } else { + setItemEquip(null); } - combiner.push(i); - - if (!comboHighlight) { - return combinerChange([i]); - } - - return combinerChange(combiner); - } - - const highlighted = combiner.indexOf(i) > -1; - const border = buttons[removeTier(v)] ? buttons[removeTier(v)]() : ''; - const classes = `${highlighted ? 'highlight' : border} ${comboHighlight} ${tutorial}`; - if (shapes[v]) { - return ( - - ); - } - - return ( - - ); - } - - function combinerBtn() { - let text = ''; - let comboItem = ''; - if (combiner.length < 3) { - for (let i = 0; i < 3; i++) { - if (combiner.length > i) { - text += '■ '; - } else { - text += '▫ '; - } - } - } else { - // 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 combinerCount = countBy(combinerItems, co => co); - const comboItemObj = itemInfo.combos.find(combo => combinerItems.every(c => { - if (!combo.components.includes(c)) return false; - const comboCount = countBy(combo.components, co => co); - if (combinerCount[c] > comboCount[c]) return false; - return true; - })); - comboItem = comboItemObj ? comboItemObj.item : 'refine'; - comboItem = comboItem.replace('Plus', '+'); - text = `Combine - ${comboItem}`; - } - - return ( - - ); - } - - function inventoryElement() { - function inventoryClick(e) { - e.stopPropagation(); - setReclaiming(false); - if (vboxSelecting) return vboxBuySelected(); - if (itemUnequip.length) return sendItemUnequip(itemUnequip); return true; } - return ( -
e.stopPropagation()} - style={vboxSelecting || itemUnequip.length ? { cursor: 'pointer' } : null} - onMouseOver={e => hoverInfo(e, 'inventory')}> -
-

e.target.scrollIntoView(true)}>INVENTORY

+ // + // VBOX + // + function vboxHover(e, v) { + if (v) { + e.stopPropagation(); + if (vboxSelected[0]) return true; // There is a base skill or spec selected in the vbox + if (combiner.length !== 0) { + const base = combiner.find(c => !['Red', 'Blue', 'Green'].includes(vbox.bound[c])); + if (base || base === 0) return true; + } + setInfo(v); + } + return true; + } + + function clearVboxSelected() { + setVboxSelected([]); + } + + function vboxBuySelected() { + if (!vboxSelecting) return false; + document.activeElement.blur(); + clearVboxSelected(); + sendVboxAccept(vboxSelected[0], vboxSelected[1]); + return true; + } + + function availableBtn(v, group, index) { + if (!v) return ; + const selected = vboxSelected[0] === group && vboxSelected[1] === index; + + // state not yet set in double click handler + function onDblClick(e) { + clearVboxSelected(); + sendVboxAccept(group, index); + e.stopPropagation(); + } + + function onClick(e) { + e.stopPropagation(); + setItemEquip(null); + setCombiner([]); + + if (selected) return clearVboxSelected(); + setInfo(vbox.free[group][index]); + return setVboxSelected([group, index]); + } + + const combinerItems = combiner.map(j => vbox.bound[j]); + const combinerCount = countBy(combinerItems, co => co); + + const comboHighlight = combinerItems.length > 0 && itemInfo.combos.some(combo => { + if (combo.components.includes(v)) { + return combinerItems.every(c => { + if (!combo.components.includes(c)) return false; + const comboCount = countBy(combo.components, co => co); + if (combinerCount[c] > comboCount[c]) return false; + if (c === v && combinerCount[c] + 1 > comboCount[c]) return false; + return true; + }); + } return false; + }) ? 'combo-border' : ''; + + const classes = `${v.toLowerCase()} ${selected ? 'highlight' : ''} ${comboHighlight}`; + + if (shapes[v]) { + return ( + ); + } + + return ( + + ); + } + + + function vboxElement() { + return ( +
setReclaiming(false)} + onClick={e => e.stopPropagation()}> +
+

e.target.scrollIntoView(true)} + onMouseOver={e => hoverInfo(e, 'vbox')}> VBOX +

+
hoverInfo(e, 'bits')} >{vbox.bits}b
+
+
+ {range(0, 6).map(i => availableBtn(vbox.free[0][i], 0, i))} +
+
+ {range(0, 3).map(i => availableBtn(vbox.free[1][i], 1, i))} + {range(0, 3).map(i => availableBtn(vbox.free[2][i], 2, i))} +
+
-
- {range(0, 9).map(i => inventoryBtn(vbox.bound[i], i))} + ); + } + + // + // INVENTORY + // + function reclaimClick(e) { + e.stopPropagation(); + return setReclaiming(!reclaiming); + } + + const inventoryClass = `vbox-section ${reclaiming ? 'reclaiming' : ''}`; + + function inventoryBtn(v, i) { + const inventoryHighlight = vboxSelecting || itemUnequip.length; + + if (!v && v !== 0) { + return ; + } + + const combinerItems = combiner.map(j => vbox.bound[j]); + const combinerCount = countBy(combinerItems, co => co); + + const comboHighlight = combinerItems.length > 0 && itemInfo.combos.some(combo => { + if (combo.components.includes(v)) { + return combinerItems.every(c => { + if (!combo.components.includes(c)) return false; + const comboCount = countBy(combo.components, co => co); + if (combinerCount[c] > comboCount[c]) return false; + if (c === v && combinerCount[c] + 1 > comboCount[c]) return false; + return true; + }); + } return false; + }) ? 'combo-border' : ''; + + function onClick(e) { + if (vboxSelecting) clearVboxSelected(); + if (reclaiming) return sendVboxReclaim(i); + + // 4 things selected + if (combiner.length > 2) { + setInfo(vbox.bound[i]); + return combinerChange([i]); + } + // removing + const combinerIndex = combiner.indexOf(i); + if (combinerIndex > -1) { + return combinerChange(without(combiner, i)); + } + + combiner.push(i); + + if (!comboHighlight) { + setInfo(vbox.bound[i]); + return combinerChange([i]); + } + + return combinerChange(combiner); + } + + const highlighted = combiner.indexOf(i) > -1; + const border = buttons[removeTier(v)] ? buttons[removeTier(v)]() : ''; + const classes = `${highlighted ? 'highlight' : border} ${comboHighlight}`; + if (shapes[v]) { + return ( + + ); + } + + return ( + + ); + } + + function combinerBtn() { + let text = ''; + let comboItem = ''; + if (combiner.length < 3) { + for (let i = 0; i < 3; i++) { + if (combiner.length > i) { + text += '■ '; + } else { + text += '▫ '; + } + } + } else { + // 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 combinerCount = countBy(combinerItems, co => co); + const comboItemObj = itemInfo.combos.find(combo => combinerItems.every(c => { + if (!combo.components.includes(c)) return false; + const comboCount = countBy(combo.components, co => co); + if (combinerCount[c] > comboCount[c]) return false; + return true; + })); + comboItem = comboItemObj ? comboItemObj.item : 'refine'; + comboItem = comboItem.replace('Plus', '+'); + text = `Combine - ${comboItem}`; + } + + return ( + + ); + } + + function inventoryElement() { + function inventoryClick(e) { + e.stopPropagation(); + setReclaiming(false); + if (vboxSelecting) return vboxBuySelected(); + if (itemUnequip.length) return sendItemUnequip(itemUnequip); + return true; + } + + return ( +
e.stopPropagation()} + style={vboxSelecting || (itemUnequip.length) ? { cursor: 'pointer' } : null}> +
+

e.target.scrollIntoView(true)} + onMouseOver={e => hoverInfo(e, 'inventory')}> INVENTORY +

+ +
+
+ {range(0, 9).map(i => inventoryBtn(vbox.bound[i], i))} +
+ {combinerBtn()}
- {combinerBtn()} + ); + } + + // + // EVERYTHING + // + function hoverInfo(e, newInfo) { + e.stopPropagation(); + return setInfo(newInfo); + } + + const classes = `vbox ${navInstance === 0 ? 'visible' : ''}`; + return ( +
+ {vboxElement()} +
+ {inventoryElement()}
); } - - // - // EVERYTHING - // - function hoverInfo(e, newInfo) { - e.stopPropagation(); - if (info === newInfo) return true; - return setInfo(newInfo); - } - - const classes = `vbox ${navInstance === 0 ? 'visible' : ''}`; - return ( -
- {vboxElement()} -
- {inventoryElement()} -
- ); } module.exports = addState(Vbox); diff --git a/client/src/components/welcome.jsx b/client/src/components/welcome.jsx index 558e5719..2a1bfa67 100644 --- a/client/src/components/welcome.jsx +++ b/client/src/components/welcome.jsx @@ -10,7 +10,6 @@ const Demo = require('./demo'); function Welcome() { const page = this.state.page || 'register'; - const navRegister = () => this.setState({ page: 'register' }); const pageEl = () => { if (page === 'login') return ; if (page === 'register') return ; @@ -18,6 +17,26 @@ function Welcome() { return false; }; + const news = ( +
+

Welcome to mnml.

+ +

MNML is a turn-based 1v1 strategy game in an abstract setting.

+

+ Build a unique team of 3 constructs from a range of skills and specialisations.
+ Outplay your opponent in multiple rounds by adapting to an always shifting meta.
+ Simple rules, complex interactions and unique mechanics.
+

+

Free to play, no pay to win. Register to start playing.

+ + Tutorial Playthrough on YouTube +
+ ); + + const main = (['login', 'register', 'help'].includes(page)) + ?
{news}{pageEl()}
+ : ; + return (
@@ -39,6 +58,12 @@ function Welcome() { onClick={() => this.setState({ page: 'register' })}> Register +
-
-
-

- Welcome to mnml. -

-

MNML is a turn-based 1v1 strategy game in an abstract setting.

-

- Build a unique team of 3 constructs from a range of skills and specialisations.
- Outplay your opponent in multiple rounds by adapting to an always shifting meta.
- Simple rules, complex interactions and unique mechanics.
-

-

- Free to play, no pay to win. Register to start playing.
-

- Tutorial Playthrough on YouTube -
- {pageEl()} -
+ {main}
-
); } diff --git a/client/src/constants.jsx b/client/src/constants.jsx index 97199bb5..bf370e14 100644 --- a/client/src/constants.jsx +++ b/client/src/constants.jsx @@ -26,7 +26,8 @@ module.exports = { INFO: { vbox: { item: 'VBOX', - description:

ITEMS that are available to buy.
The VBOX is refilled every round.
Click REFILL at the bottom to purchase a refill.

, + description:

ITEMS that are available to buy.
+ The VBOX is refilled every round.
Click REFILL at the bottom to purchase a refill.

, }, inventory: { item: 'INVENTORY', @@ -34,7 +35,11 @@ module.exports = { }, bits: { item: 'BITS', - description: 'The VBOX currency.\nColours: 1b\nSkills: 2b\nSpecs: 3b\nAt the beginning of a round you receive 12 + 6 * round_number bits.', + description:

The VBOX currency.
+ Colours - 1b
+ Skills - 2b
+ Specs - 3b
+ At the beginning of each round you receive 18 bits increasing by 6 bits per round.

, }, ready: { item: 'READY', @@ -46,43 +51,32 @@ module.exports = { }, reclaim: { item: 'RECLAIM', - description: 'Reclaim ITEMS for half the purchase cost of their combined ITEMS.\nClick to enable and click ITEM to reclaim.', - }, - refine: { - item: 'COMBINE', - description:

combine the selected items.
hover over an item to see RECIPES.

, + description:

Reclaim items refunding the listed cost of the item.
+ Click to enable and then click the item to reclaim.

, }, refill: { item: 'REFILL', description: 'Refill the VBOX with new items.', }, - equipSkills: { - item: 'QUICK ACCESS - SKILLS', - description: 'Click to select \nClick target CONSTRUCT to equip', - }, - equipSpecs: { - item: 'QUICK ACCESS - SPECS', - description: 'Click to select \nClick target CONSTRUCT to equip', - }, constructSkills: { item: 'SKILLS', - description: 'Skills are used by Constructs in-game.\nClick a SKILL above and select a CONSTRUCT to equip.\nDouble-click to unequip.', + description: 'Skills are used by constructs in the game phase.\nBase skills can be bought from the VBOX.\nEquip skills from the inventory. Double-click to unequip.', }, constructSpecs: { item: 'SPECS', - description: 'SPECS increase the STATS of a CONSTRUCT.\nSPECS have increased effect once they reach a THRESHOLD across your whole team.\nClick a SPEC above and select a CONSTRUCT to equip.\nDouble-click to unequip.', + description: 'Specialisations increase the stats of a construct.\nIncreased effect when your team meets the colour threshold.\nDouble-click to unequip.', }, powerStat: { item: 'POWER', - description: 'Power determines the base damage and healing of your construct skills. Combine POWER specs to increase construct power.', + description: 'Power determines the damage and healing of your construct skills.\nRed power RedPower is used by red skills.\nGreen power GreenPower is used by green skills.\nBlue power BluePower is used by blue skills.\nCombine coloured POWER specs to increase specific construct power.', }, lifeStat: { item: 'LIFE', - description: 'The life of your construct.\n When your construct reaches 0 green life it is knocked out and cannot cast skills.\nRed life mitigates red damage and blue life mitigates blue damage. Combine LIFE specs to increase life totals.', + description: 'The life of your construct.\nWhen your construct reaches 0 GreenLife it is knocked out.\nRed life RedLife mitigates red damage.\nBlue life BlueLife mitigates blue damage.\nCombine coloured LIFE specs to increase specific construct life.', }, speedStat: { item: 'SPEED', - description: 'Speed determines the order in which skills resolve. Combine SPEED specs to increase speed.', + description: 'Speed determines the order in which skills resolve.\nCombine SPEED specs to increase speed.', }, }, }; diff --git a/client/src/events.jsx b/client/src/events.jsx index aaf0fdf8..0cc1e4a0 100644 --- a/client/src/events.jsx +++ b/client/src/events.jsx @@ -8,6 +8,7 @@ const actions = require('./actions'); const { TIMES } = require('./constants'); const animations = require('./animations.utils'); const { infoToast, errorToast } = require('./utils'); +const { tutorialVbox } = require('./tutorial.utils'); function registerEvents(store) { function notify(msg) { @@ -196,7 +197,7 @@ function registerEvents(store) { } function setInstance(v) { - const { account, instance, ws } = store.getState(); + const { account, instance, ws, tutorial } = store.getState(); if (v) { setInvite(null); const player = v.players.find(p => p.id === account.id); @@ -207,10 +208,15 @@ function registerEvents(store) { const first = player.constructs[0]; store.dispatch(actions.setActiveConstruct(first)); } - } - if (v.phase === 'Finished') { - setGame(null); - ws.sendAccountInstances(); + + if (v.phase === 'Finished') { + ws.sendAccountInstances(); + } + if (localStorage.getItem('tutorial-complete')) { + store.dispatch(actions.setTutorial(null)); + } else if (v.time_control === 'Practice' && v.rounds.length === 1 && tutorial) { + tutorialVbox(player, store, tutorial); + } } return store.dispatch(actions.setInstance(v)); @@ -230,48 +236,51 @@ function registerEvents(store) { function setDemo(d) { - const initial = { + const vboxDemo = { players: d, combiner: [], - items: ['Red', 'Red', 'Attack'], equipped: false, equipping: false, }; const startDemo = () => { - const { account } = store.getState(); + const { account, itemInfo } = store.getState(); if (account) return false; - store.dispatch(actions.setDemo(initial)); + if (!itemInfo || itemInfo.items.length === 0) return setTimeout(startDemo, 500); store.dispatch(actions.setAnimTarget(null)); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [0] }))), 2000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [0, 1] }))), 4000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [0, 1, 2] }))), 6000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [], items: ['Strike', '', ''] }))), 8000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [0], items: ['Strike', '', ''], equipping: true }))), 10000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { combiner: [], items: ['', '', ''], equipped: true, equipping: false }))), 12000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, initial, { items: ['', '', ''], equipped: true, equipping: false }))), 12000); + const bases = ['Attack', 'Stun', 'Buff', 'Debuff', 'Block']; + const combo = sample(itemInfo.combos.filter(i => bases.some(b => i.components.includes(b)))); + vboxDemo.combo = combo.item; + vboxDemo.items = combo.components; + store.dispatch(actions.setDemo(vboxDemo)); + + + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0] }))), 500); + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1] }))), 1000); + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1, 2] }))), 1500); + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: [vboxDemo.combo, '', ''] }))), 2500); + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0], items: [vboxDemo.combo, '', ''], equipping: true }))), 3000); + setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: ['', '', ''], equipped: true, equipping: false }))), 4000); setTimeout(() => { - const { itemInfo } = store.getState(); return store.dispatch(actions.setAnimTarget({ skill: sample(itemInfo.items.filter(i => i.skill)).item, constructId: d[1].constructs[0].id, player: false, direction: 0, })); - }, 14000); + }, 500); setTimeout(() => { - const { itemInfo } = store.getState(); return store.dispatch(actions.setAnimTarget({ skill: sample(itemInfo.items.filter(i => i.skill)).item, constructId: d[1].constructs[1].id, player: true, direction: 0, })); - }, 16000); + }, 3000); - setTimeout(startDemo, 20000); + return setTimeout(startDemo, 5000); }; startDemo(); diff --git a/client/src/reducers.jsx b/client/src/reducers.jsx index 2bfec51f..d2d06f43 100644 --- a/client/src/reducers.jsx +++ b/client/src/reducers.jsx @@ -59,6 +59,9 @@ module.exports = { teamPage: createReducer(0, 'SET_TEAM_PAGE'), teamSelect: createReducer([null, null, null], 'SET_TEAM_SELECT'), + tutorial: createReducer(1, 'SET_TUTORIAL'), + tutorialGame: createReducer(1, 'SET_TUTORIAL_GAME'), + vboxSelected: createReducer([], 'SET_VBOX_SELECTED'), ws: createReducer(null, 'SET_WS'), diff --git a/client/src/tutorial.utils.jsx b/client/src/tutorial.utils.jsx new file mode 100644 index 00000000..1cdf0dfb --- /dev/null +++ b/client/src/tutorial.utils.jsx @@ -0,0 +1,236 @@ +const preact = require('preact'); +const actions = require('./actions'); + +function tutorialConstructDisplay(player, instance, tutorial, navInstance, i) { + if (instance.time_control === 'Practice' && instance.rounds.length === 1 && tutorial && tutorial < 6) { + if (tutorial <= 2 || (tutorial > 2 && i > 0)) { + const mobileVisible = navInstance === i + 1; + const classes = `instance-construct ${mobileVisible ? 'visible' : ''}`; + return (
); + } + } + return false; +} + +function tutorialShouldDisableEquip(tutorial, iter, instance, construct) { + return tutorial && tutorial === 6 && iter === 0 && construct.skills.length !== 0 + && instance.time_control === 'Practice' && instance.rounds.length === 1; +} + + +function tutorialVbox(player, store, tutorial) { + let stage = tutorial; + const { vbox } = player; + if (stage === 1) { + if (vbox.bits < 17) { + stage += 1; + } else { + vbox.free[0] = vbox.free[0].slice(0, 2); + vbox.free[1] = []; + vbox.free[2] = []; + vbox.bound.fill(null, 0, 3); + } + } + + if (stage === 2) { + if (!(vbox.bound.slice(0, 3).every(i => i === 'Attack') && vbox.bound.length >= 3)) { + stage += 1; + } else { + vbox.free[0] = vbox.free[0].slice(0, 2); + vbox.free[1] = []; + vbox.free[2] = []; + vbox.bound.fill(null, 1, 3); + } + } + + if (stage === 3) { + if (player.constructs[0].skills.length !== 0) { + stage += 1; + } else { + vbox.free[0] = vbox.free[0].slice(0, 2); + vbox.free[1] = []; + vbox.free[2] = []; + vbox.bound.fill(null, 0, 2); + } + } + + if (stage === 4) { + if (!vbox.free[2][0] || vbox.bits < 12) { + stage += 1; + } else { + vbox.free[0] = []; + vbox.free[1] = []; + vbox.free[2] = vbox.free[2].slice(0, 1); + vbox.bound.fill(null, 0, 2); + } + } + + if (stage === 5) { + if (player.constructs[0].specs.length !== 0) { + stage += 1; + } else { + vbox.free[0] = []; + vbox.free[1] = []; + vbox.free[2] = vbox.free[2].slice(0, 1); + vbox.bound.fill(null, 0, 2); + } + } + + if (stage === 6) { + if (player.constructs.every(c => c.skills.length !== 0)) { + stage += 1; + } else { + vbox.free[0] = []; + vbox.free[1] = []; + vbox.free[2] = []; + } + } + + if (stage === 7) { + if (vbox.bits < 13) { + stage += 1; + } else { + vbox.free[0] = []; + vbox.free[1] = []; + vbox.free[2] = []; + } + } + store.dispatch(actions.setTutorial(stage)); +} + +function tutorialStage(tutorial, ws, clearTutorial, instance) { + if (!(instance.time_control === 'Practice' && instance.rounds.length === 1)) return false; + + const exit = () => { + clearTutorial(); + localStorage.setItem('tutorial-complete', true); + ws.sendInstanceState(instance.id); + }; + + const tutorialText = () => { + if (tutorial === 1) { + return ( +
+

Tutorial

+

Welcome to the vbox phase tutorial.

+

Colours are used to create powerful combinations.

+

Buy two colours from the vbox by double clicking.
+ You can also click the colour once and then click the inventory.

+
+ ); + } + + if (tutorial === 2) { + return ( +
+

Tutorial

+

In a normal game you start with three base Attack skill items.

+

The Attack item can be combined with colours to create a new skill.

+

Select the Attack item along with two colours.
+ Once selected press COMBINE to create a new combo. +

+
+ ); + } + + if (tutorial === 3) { + const constructOne = instance.players[0].constructs[0].name; + return ( +
+

Tutorial

+

The first construct on your team is {constructOne}.

+

Skill items can be equipped to your constructs to be used in the combat phase.

+

Click the newly combined skill item in the top right of the inventory.
+ Once selected click the construct SKILL slot to equip the skill.

+
+ ); + } + + if (tutorial === 4) { + return ( +
+

Tutorial

+

You can also buy specialisation items for your constructs.
+ Specialisation items increase stats including power, speed and life.

+

Buy the specialisation item from the vbox by double clicking.
+ You can also click the specialisation once and then click the inventory.

+
+ ); + } + + if (tutorial === 5) { + return ( +
+

Tutorial

+

Equipping specialisation items will increase the stats of your constructs.

+

These can also be combined with colours for further specialisation.

+

Click the specialisation item in the top right of the inventory.
+ Once selected click the construct SPEC slot to equip the specialisation.

+
+ ); + } + + if (tutorial === 6) { + const constructTwo = instance.players[0].constructs[1].name; + const constructThree = instance.players[0].constructs[2].name; + return ( +
+

Tutorial

+

You have now created a construct with an upgraded skill and base spec.

+

The goal is to create three powerful constructs for combat.

+

Equip your other constructs {constructTwo} and {constructThree} with the Attack skill.
+ Ensure each construct has a single skill to continue.

+
+ ); + } + + if (tutorial === 7) { + return ( +
+

Tutorial

+

Each round you start with a vbox full of different skills, specs and colours.

+

Bits are your currency for buying skills, specs and colours from the vbox.
+ Colours cost 1b, Skills cost 2b and specs cost 3b.
+ You can refill the vbox by pressing the refill button for 2b.
+ After each combat round you get more bits to further upgrade your team. +

+

Press the REFILL button to get a new vbox and continue.

+
+ ); + } + + if (tutorial === 8) { + return ( +
+

Tutorial

+

You've completed the tutorial! Try to create more skill and spec combinations.

+

You can unequip skills and specs back into the inventory by double clicking.
+ Reclaim can be used to refund the cost of items in your inventory.

+

Click the EXIT TUTORIAL button to replace this section with more information.

+
+ ); + } + return false; + }; + + const classes = tutorial === 8 ? 'focus' : ''; + const exitTutorial = ; + + return ( +
+ {tutorialText()} +
+ {exitTutorial} +
+
); +} + +module.exports = { + tutorialConstructDisplay, + tutorialVbox, + tutorialStage, + tutorialShouldDisableEquip +}; diff --git a/client/src/utils.jsx b/client/src/utils.jsx index 88719c34..ec49e659 100644 --- a/client/src/utils.jsx +++ b/client/src/utils.jsx @@ -143,6 +143,7 @@ function randomPoints(numPoints, radius, dimensions) { const removeTier = skill => { if (!skill) return skill; + if (skill.includes('CounterAttack')) return 'CounterAttack'; if (skill.includes('SiphonTick')) return 'SiphonTick'; if (skill.includes('TriageTick')) return 'TriageTick'; if (skill.includes('DecayTick')) return 'DecayTick'; @@ -239,20 +240,18 @@ function convertItem(v) { } 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 === 'CounterAttack') return 120; + if (s === 'CounterAttack+') return 160; + if (s === 'CounterAttack++') return 230; if (s === 'DecayTick') return 33; if (s === 'DecayTick+') return 45; if (s === 'DecayTick++') return 70; - if (s === 'SiphonTick') return 20; - if (s === 'SiphonTick+') return 25; - if (s === 'SiphonTick++') return 30; + if (s === 'SiphonTick') return 25; + if (s === 'SiphonTick+') return 30; + if (s === 'SiphonTick++') return 40; if (s === 'TriageTick') return 75; if (s === 'TriageTick+') return 110; @@ -267,8 +266,8 @@ function effectInfo(i) { 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 'Block': return `Reduces construct red damage and blue damage taken by ${100 - i.meta[1]}%`; + case 'Buff': return `Increases construct RedPower BluePower SpeedStat by ${i.meta[1] - 100}%`; case '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.`; diff --git a/server/src/effect.rs b/server/src/effect.rs index 1e959e50..c2c77392 100644 --- a/server/src/effect.rs +++ b/server/src/effect.rs @@ -100,13 +100,13 @@ impl Effect { pub fn modifications(&self) -> Vec { match self { Effect::Vulnerable => vec![Stat::RedDamageTaken], - Effect::Block => vec![Stat::RedDamageTaken], - Effect::Buff => vec![Stat::RedPower, Stat::Speed], + 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::RedPower, Stat::BluePower], - Effect::Curse => vec![Stat::BlueDamageTaken], + Effect::Curse => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken], Effect::Hybrid => vec![Stat::GreenPower], Effect::Wither => vec![Stat::GreenDamageTaken], diff --git a/server/src/item.rs b/server/src/item.rs index 763e17c2..52b3bb8b 100644 --- a/server/src/item.rs +++ b/server/src/item.rs @@ -584,26 +584,31 @@ impl Item { pub fn into_description(&self) -> String { match self { // colours - Item::Blue => format!("Combine with skills and specs to create upgraded items. \n Deterrents and destruction."), - Item::Green => format!("Combine with skills and specs to create upgraded items.\n Protection and trickery."), - Item::Red => format!("Combine with skills and specs to create upgraded items. \n Speed and chaos."), + Item::Blue => format!("Combine two colours with a white base item to create a new combo. \n Slow speed, magical type. Deterrents and destruction."), + Item::Green => format!("Combine two colours with a white base item to create a new combo.\n Normal speed, healing type. Protection and trickery."), + Item::Red => format!("Combine two colours with a white base item to create a new combo. \n Fast speed, physical type. Chaos and momentum."), // base skills Item::Attack => format!("Deal {:?}% RedPower as red damage.", self.into_skill().unwrap().multiplier()), - Item::Block => format!("Reduce red damage taken by {:?}%.", - 100 - self.into_skill().unwrap().effect()[0].get_multiplier()), + Item::Block => format!("Reduce red damage and blue damage taken by {:?}%. Block lasts {:?}T", + 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), Item::Stun => format!("Stun target construct for {:?}T.", self.into_skill().unwrap().effect()[0].get_duration()), - Item::Buff => format!("Increase target construct RedPower and SpeedStat by {:?}%.", - self.into_skill().unwrap().effect()[0].get_multiplier() - 100), - Item::Debuff => format!("Slows the target reducing SpeedStat by {:?}%.", - 100 - self.into_skill().unwrap().effect()[0].get_multiplier()), + Item::Buff => format!("Increase target construct RedPower BluePower SpeedStat by {:?}%. Buff lasts {:?}T", + self.into_skill().unwrap().effect()[0].get_multiplier() - 100, + self.into_skill().unwrap().effect()[0].get_duration()), + + Item::Debuff => format!("Slows the target reducing SpeedStat by {:?}%. Debuff lasts {:?}T", + 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), + self.into_skill().unwrap().effect()[0].get_duration()), // specs // Base - Item::Power => format!("Increases all power stats by {:?}%. Power determines the base damage and healing of your construct skills.", + Item::Power => format!("Increases all power stats by {:?}%. + Power determines the base damage and healing of your construct skills.", self.into_spec().unwrap().values().base()), Item::Life => format!("Increases construct GreenLife by {:?}. When your construct reaches 0 GreenLife it is knocked out and cannot cast skills.", @@ -713,7 +718,8 @@ impl Item { Item::Banish| Item::BanishPlus | - Item::BanishPlusPlus => format!("Banish target for {:?}T. Deal blue damage and red damage equal to {:?}% target red and blue life. + Item::BanishPlusPlus => format!("Banish target for {:?}T. + Deal blue damage and red damage equal to {:?}% target red and blue life. Banished constructs are immune to all skills and effects.", self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().multiplier()), @@ -725,20 +731,24 @@ impl Item { Item::Chaos| Item::ChaosPlus | Item::ChaosPlusPlus => format!( - "Hits twice for red and blue damage. Damage {:?}% RedPower and BluePower. Randomly deals 0 to 30% more damage.", + "Hits twice for red and blue damage. Damage {:?}% RedPower and BluePower. + Randomly deals 0 to 30% more damage.", self.into_skill().unwrap().multiplier()), Item::Sustain| Item::SustainPlus | - Item::SustainPlusPlus => format!("Construct cannot be KO'd while active. Additionally provides immunity to disables. \ - Lasts {:?}T", - self.into_skill().unwrap().effect()[0].get_duration()), + Item::SustainPlusPlus => format!( + "Construct cannot be KO'd while active and provides immunity to disables. Lasts {:?}T. + Recharges target RedLife based on {:?}% RedPower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), Item::Electrify| Item::ElectrifyPlus | Item::ElectrifyPlusPlus => format!( - "Applies electrify for {:?}T. If a construct with electrify takes damage they will apply an electrocute debuff to the caster dealing {:?}% \ - BluePower as BlueDamage per turn for {:?}T.", + "Applies electrify for {:?}T. + If a construct with electrify takes direct damage they will apply an electrocute debuff to the caster. + Electrocute deals {:?}% BluePower as BlueDamage per turn for {:?}T.", self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_skill().unwrap().multiplier(), self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_duration()), @@ -753,7 +763,8 @@ impl Item { Item::Decay| Item::DecayPlus | Item::DecayPlusPlus => format!( - "Reduces healing taken by {:?}% for {:?}T. Deals blue damage {:?}% BluePower each turn for {:?}T.", + "Reduces healing taken by {:?}% for {:?}T. + Deals blue damage {:?}% BluePower each turn for {:?}T.", 100 - self.into_skill().unwrap().effect()[0].get_multiplier(), self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().effect()[1].get_skill().unwrap().multiplier(), @@ -762,17 +773,20 @@ impl Item { Item::Absorb| Item::AbsorbPlus | Item::AbsorbPlusPlus => format!( - "Gain Absorb for {:?}T. {} Absorption lasts {:?}T.", + "Gain Absorb for {:?}T. When attacked with Absorb you gain Absorption. + Absorption increases RedPower and BluePower based on Damage taken. + Absorption lasts {:?}T.", self.into_skill().unwrap().effect()[0].get_duration(), - "When attacked with Absorb you gain Absorption which increases RedPower and BluePower based on Damage taken.", self.into_skill().unwrap().effect()[0].get_skill().unwrap().effect()[0].get_duration()), Item::Haste| Item::HastePlus | Item::HastePlusPlus => format!( - "Haste increases SpeedStat by {:?}%, Red Attack based skills will strike again dealing {:?}% SpeedStat as red damage. Lasts {:?}T", + "Haste increases SpeedStat by {:?}%. + Red Attack based skills will strike again dealing {:?}% SpeedStat as red damage. + Lasts {:?}T", self.into_skill().unwrap().effect()[0].get_multiplier() - 100, - Skill::HasteStrike.multiplier(), + Skill::HasteStrike.multiplier(), self.into_skill().unwrap().effect()[0].get_duration()), Item::Heal| @@ -782,7 +796,9 @@ impl Item { Item::Hybrid| Item::HybridPlus | Item::HybridPlusPlus => format!( - "Hybrid increases GreenPower by {:?}%, Blue based Attack skills will blast again dealing {:?}% GreenPower as blue damage. Lasts {:?}T.", + "Hybrid increases GreenPower by {:?}%. + Blue based Attack skills will blast again dealing {:?}% GreenPower as blue damage. + Lasts {:?}T.", self.into_skill().unwrap().effect()[0].get_multiplier() - 100, Skill::HybridBlast.multiplier(), self.into_skill().unwrap().effect()[0].get_duration()), @@ -790,20 +806,25 @@ impl Item { Item::Invert| Item::InvertPlus | Item::InvertPlusPlus => format!( - "Reverse healing into damage and damage into healing. Any excess red or blue damage is converted into shield recharge. Lasts {:?}T.", + "Reverse healing into damage and damage into healing. + Any excess red or blue damage is converted into shield recharge. + Lasts {:?}T.", self.into_skill().unwrap().effect()[0].get_duration()), Item::Counter| Item::CounterPlus | - Item::CounterPlusPlus => format!("Applies counter and block {:?}T. Block reduces incoming red damage by {:?}%. - Red damage taken during counter will trigger a counter attack dealing {:?}% RedPower as red damage.", + Item::CounterPlusPlus => format!( + "Applies counter for {:?}T. + Red damage taken during counter will trigger a counter attack. + Counter attack deals {:?}% RedPower as red damage.", self.into_skill().unwrap().effect()[0].get_duration(), - 100 - self.into_skill().unwrap().effect()[1].get_multiplier(), self.into_skill().unwrap().effect()[0].get_skill().unwrap().multiplier()), Item::Purge| Item::PurgePlus | - Item::PurgePlusPlus => format!("Remove buffs from target construct. Applies purge disabling target green skills for {:?}T.", + Item::PurgePlusPlus => format!( + "Remove buffs from target construct. + Applies purge disabling target green skills for {:?}T.", self.into_skill().unwrap().effect()[0].get_duration()), Item::Purify| @@ -815,8 +836,10 @@ impl Item { Item::Reflect| Item::ReflectPlus | Item::ReflectPlusPlus => format!( - "Reflect incoming blue skills to source. Lasts {:?}T.", - self.into_skill().unwrap().effect()[0].get_duration()), + "Reflect incoming blue skills to source. Lasts {:?}T. + Recharges target BlueLife based on {:?}% BluePower.", + self.into_skill().unwrap().effect()[0].get_duration(), + self.into_skill().unwrap().multiplier()), Item::Recharge| Item::RechargePlus | @@ -833,8 +856,8 @@ impl Item { Item::Link| Item::LinkPlus | Item::LinkPlusPlus => format!( - "Form a link with target swapping relative life values. - If the target construct has more green life than caster, {:?}% of green life difference as blue damage to the target and heal to the caster. + "Swap {:?}% of green life difference as blue damage to the target and healing to the caster. + The swap only occurs if the target construct has more green life than caster. Stuns caster for {:?}T in the process.", self.into_skill().unwrap().multiplier(), self.into_skill().unwrap().effect()[0].get_duration()), @@ -842,7 +865,7 @@ impl Item { Item::Silence| Item::SilencePlus | Item::SilencePlusPlus => format!( - "Block the target from using blue skills for {:?}T and deals {:?}% BluePower as blue damage. + "Disable the target from using blue skills for {:?}T and deals {:?}% BluePower as blue damage. Deals 45% more Damage per blue skill on target.", self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().multiplier()), @@ -850,7 +873,8 @@ impl Item { Item::Slay| Item::SlayPlus | Item::SlayPlusPlus => format!( - "Deals {:?}% RedPower + {:?}% GreenPower as red damage and provides self healing based on damage dealt.", + "Deals {:?}% RedPower + {:?}% GreenPower as red damage. + Construct heals self for 50% of damage dealt to target construct GreenLife.", self.into_skill().unwrap().multiplier(), self.into_skill().unwrap().multiplier()), @@ -864,7 +888,8 @@ impl Item { Item::Restrict| Item::RestrictPlus | Item::RestrictPlusPlus => format!( - "Block the target from using red skills for {:?}T and deals {:?}% RedPower as red damage. Deals 35% more damage per red skill on target.", + "Disable the target from using red skills for {:?}T and deals {:?}% RedPower as red damage. + Deals 35% more damage per red skill on target.", self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().multiplier()), @@ -872,7 +897,8 @@ impl Item { 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.", + 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()), @@ -885,14 +911,17 @@ impl Item { Item::Siphon| Item::SiphonPlus | Item::SiphonPlusPlus => format!( - "Deals {:?}% BluePower + {:?}% GreenPower as blue damage each turn and heals caster based on damage dealt. Lasts {:?}T.", + "Deals {:?}% BluePower + {:?}% GreenPower as blue damage each turn. + Construct heals self for 100% of damage dealt to target construct GreenLife. + 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()), Item::Intercept| Item::InterceptPlus | - Item::InterceptPlusPlus => format!("Intercept redirects skills against the team to target, lasts {:?}T.\ + Item::InterceptPlusPlus => format!( + "Intercept redirects skills against the team to target, lasts {:?}T. Recharges RedLife for {:?} RedPower.", self.into_skill().unwrap().effect()[0].get_duration(), self.into_skill().unwrap().multiplier()), @@ -936,10 +965,10 @@ impl Item { Item::AmplifyPlusPlus => vec![Item::AmplifyPlus, Item::AmplifyPlus, Item::AmplifyPlus], Item::Purge => vec![Item::Debuff, Item::Green, Item::Green], // Needs flavour - Item::Invert => vec![Item::Debuff, Item::Red, Item::Blue], + Item::Invert => vec![Item::Debuff, Item::Red, Item::Green], Item::Restrict => vec![Item::Debuff, Item::Red, Item::Red], Item::Silence => vec![Item::Debuff, Item::Blue, Item::Blue], - Item::Curse => vec![Item::Debuff, Item::Red, Item::Green], + Item::Curse => vec![Item::Debuff, Item::Red, Item::Blue], Item::Decay => vec![Item::Debuff, Item::Green, Item::Blue], Item::RestrictPlus => vec![Item::Restrict, Item::Restrict, Item::Restrict], Item::RestrictPlusPlus => vec![Item::RestrictPlus, Item::RestrictPlus, Item::RestrictPlus], diff --git a/server/src/skill.rs b/server/src/skill.rs index 2e98af9b..1f655e6d 100644 --- a/server/src/skill.rs +++ b/server/src/skill.rs @@ -342,7 +342,7 @@ fn post_resolve(_skill: Skill, game: &mut Game, mut resolutions: Resolutions) -> }; if target.is_ko() { - resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly)); + // resolutions.push(Resolution::new(&source, &target).event(Event::Ko()).stages(EventStages::PostOnly)); target.effects.clear(); } @@ -753,9 +753,9 @@ impl Skill { // Attack Base Skill::Attack => 80, // Base - Skill::Blast=> 110, // BB - Skill::BlastPlus => 145, // BB - Skill::BlastPlusPlus => 210, // BB + Skill::Blast=> 105, // BB + Skill::BlastPlus => 140, // BB + Skill::BlastPlusPlus => 200, // BB Skill::Chaos=> 40, // BR Skill::ChaosPlus => 65, // BR @@ -765,13 +765,13 @@ impl Skill { Skill::HealPlus => 185, //GG Skill::HealPlusPlus => 270, //GG - Skill::SiphonTick=> 20, // GB - Skill::SiphonTickPlus => 25, - Skill::SiphonTickPlusPlus => 30, + Skill::SiphonTick=> 25, // GB + Skill::SiphonTickPlus => 30, + Skill::SiphonTickPlusPlus => 40, - Skill::Slay=> 40, // RG - Skill::SlayPlus => 60, - Skill::SlayPlusPlus => 90, + Skill::Slay=> 45, // RG + Skill::SlayPlus => 65, + Skill::SlayPlusPlus => 100, Skill::Strike=> 90, //RR Skill::StrikePlus => 140, @@ -782,9 +782,9 @@ impl Skill { Skill::ElectrocuteTickPlus => 100, Skill::ElectrocuteTickPlusPlus => 130, - Skill::CounterAttack=> 70, - Skill::CounterAttackPlus => 95, - Skill::CounterAttackPlusPlus => 120, + Skill::CounterAttack=> 120, + Skill::CounterAttackPlus => 160, + Skill::CounterAttackPlusPlus => 230, Skill::Purify=> 45, //Green dmg (heal) Skill::PurifyPlus => 70, @@ -803,18 +803,21 @@ impl Skill { Skill::SustainPlusPlus => 230, // Stun Base - Skill::Sleep=> 240, //Green dmg (heal) - Skill::SleepPlus => 300, + Skill::Sleep=> 200, //Green dmg (heal) + Skill::SleepPlus => 290, Skill::SleepPlusPlus => 400, Skill::Banish=> 40, //Green dmg (heal) Skill::BanishPlus => 75, Skill::BanishPlusPlus => 125, - Skill::Bash=> 65, - Skill::BashPlus => 95, - Skill::BashPlusPlus => 140, + Skill::Bash=> 45, + Skill::BashPlus => 65, + Skill::BashPlusPlus => 100, + Skill::Link=> 75, + Skill::LinkPlus => 100, + Skill::LinkPlusPlus => 150, // Debuff Base Skill::DecayTick=> 33, Skill::DecayTickPlus => 45, @@ -827,14 +830,14 @@ impl Skill { Skill::RestrictPlusPlus => 100, // Buff base - Skill::HybridBlast => 25, - Skill::HasteStrike => 30, - Skill::Link=> 75, - Skill::LinkPlus => 100, - Skill::LinkPlusPlus => 150, + Skill::HybridBlast => 50, + + Skill::HasteStrike => 60, + Skill::Intercept=> 80, Skill::InterceptPlus => 110, Skill::InterceptPlusPlus => 150, + Skill::TriageTick=> 75, Skill::TriageTickPlus => 110, Skill::TriageTickPlusPlus => 140, @@ -857,9 +860,9 @@ impl Skill { Skill::BanishPlus => vec![ConstructEffect {effect: Effect::Banish, duration: 2, meta: None, tick: None}], Skill::BanishPlusPlus => vec![ConstructEffect {effect: Effect::Banish, duration: 2, meta: None, tick: None}], Skill::Block => vec![ConstructEffect {effect: Effect::Block, duration: 1, - meta: Some(EffectMeta::Multiplier(50)), tick: None}], - Skill::Buff => vec![ConstructEffect {effect: Effect::Buff, duration: 2, - meta: Some(EffectMeta::Multiplier(125)), tick: None }], + meta: Some(EffectMeta::Multiplier(35)), tick: None}], + Skill::Buff => vec![ConstructEffect {effect: Effect::Buff, duration: 3, + meta: Some(EffectMeta::Multiplier(130)), tick: None }], Skill::Electrify => vec![ConstructEffect {effect: Effect::Electric, duration: 1, meta: Some(EffectMeta::Skill(Skill::Electrocute)), tick: None}], @@ -901,11 +904,11 @@ impl Skill { ConstructEffect {effect: Effect::Decay, duration: 4, meta: Some(EffectMeta::Skill(Skill::DecayTickPlusPlus)), tick: None}], - Skill::Haste => vec![ConstructEffect {effect: Effect::Haste, duration: 2, + Skill::Haste => vec![ConstructEffect {effect: Effect::Haste, duration: 3, meta: Some(EffectMeta::Multiplier(150)), tick: None }], - Skill::HastePlus => vec![ConstructEffect {effect: Effect::Haste, duration: 3, + Skill::HastePlus => vec![ConstructEffect {effect: Effect::Haste, duration: 4, meta: Some(EffectMeta::Multiplier(175)), tick: None }], - Skill::HastePlusPlus => vec![ConstructEffect {effect: Effect::Haste, duration: 4, + Skill::HastePlusPlus => vec![ConstructEffect {effect: Effect::Haste, duration: 5, meta: Some(EffectMeta::Multiplier(225)), tick: None }], Skill::Absorb => vec![ConstructEffect {effect: Effect::Absorb, duration: 2, @@ -919,11 +922,11 @@ impl Skill { Skill::AbsorptionPlus => vec![ConstructEffect {effect: Effect::Absorption, duration: 5, meta: None, tick: None}], Skill::AbsorptionPlusPlus => vec![ConstructEffect {effect: Effect::Absorption, duration: 7, meta: None, tick: None}], - Skill::Hybrid => vec![ConstructEffect {effect: Effect::Hybrid, duration: 2, + Skill::Hybrid => vec![ConstructEffect {effect: Effect::Hybrid, duration: 3, meta: Some(EffectMeta::Multiplier(150)), tick: None }], - Skill::HybridPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 3, + Skill::HybridPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 4, meta: Some(EffectMeta::Multiplier(175)), tick: None }], - Skill::HybridPlusPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 4, + Skill::HybridPlusPlus => vec![ConstructEffect {effect: Effect::Hybrid, duration: 5, meta: Some(EffectMeta::Multiplier(225)), tick: None }], Skill::Invert => vec![ConstructEffect {effect: Effect::Invert, duration: 2, meta: None, tick: None}], @@ -931,17 +934,11 @@ impl Skill { Skill::InvertPlusPlus => vec![ConstructEffect {effect: Effect::Invert, duration: 4, meta: None, tick: None}], Skill::Counter => vec![ConstructEffect {effect: Effect::Counter, duration: 1, - meta: Some(EffectMeta::Skill(Skill::CounterAttack)), tick: None}, - ConstructEffect {effect: Effect::Block, duration: 1, - meta: Some(EffectMeta::Multiplier(60)), tick: None}], + meta: Some(EffectMeta::Skill(Skill::CounterAttack)), tick: None}], Skill::CounterPlus => vec![ConstructEffect {effect: Effect::Counter, duration: 1, - meta: Some(EffectMeta::Skill(Skill::CounterAttackPlus)), tick: None}, - ConstructEffect {effect: Effect::Block, duration: 1, - meta: Some(EffectMeta::Multiplier(40)), tick: None}], + meta: Some(EffectMeta::Skill(Skill::CounterAttackPlus)), tick: None}], Skill::CounterPlusPlus => vec![ConstructEffect {effect: Effect::Counter, duration: 1, - meta: Some(EffectMeta::Skill(Skill::CounterAttackPlusPlus)), tick: None}, - ConstructEffect {effect: Effect::Block, duration: 1, - meta: Some(EffectMeta::Multiplier(20)), tick: None}], + meta: Some(EffectMeta::Skill(Skill::CounterAttackPlusPlus)), tick: None}], Skill::Reflect => vec![ConstructEffect {effect: Effect::Reflect, duration: 1, meta: None, tick: None }], Skill::ReflectPlus => vec![ConstructEffect {effect: Effect::Reflect, duration: 1, meta: None, tick: None }], @@ -1060,7 +1057,7 @@ impl Skill { Skill::AmplifyPlusPlus => Some(1), Skill::Hybrid| Skill::HybridPlus | - Skill::HybridPlusPlus => Some(3), + Skill::HybridPlusPlus => Some(1), Skill::Invert=> Some(2), Skill::InvertPlus => Some(2), @@ -1080,7 +1077,7 @@ impl Skill { Skill::LinkPlus => Some(2), Skill::LinkPlusPlus => Some(2), - Skill::Silence=> Some(3), + Skill::Silence=> Some(2), Skill::SilencePlus => Some(2), Skill::SilencePlusPlus => Some(2), @@ -1094,11 +1091,11 @@ impl Skill { Skill::Banish | Skill::BanishPlus | - Skill::BanishPlusPlus => Some(3), + Skill::BanishPlusPlus => Some(2), - Skill::Haste=> Some(2), - Skill::HastePlus => Some(2), - Skill::HastePlusPlus => Some(2), + Skill::Haste=> Some(1), + Skill::HastePlus => Some(1), + Skill::HastePlusPlus => Some(1), Skill::Reflect | Skill::ReflectPlus | @@ -1116,9 +1113,9 @@ impl Skill { Skill::SlayPlus => None, Skill::SlayPlusPlus => None, - Skill::Sleep=> Some(3), - Skill::SleepPlus => Some(3), - Skill::SleepPlusPlus => Some(3), + Skill::Sleep=> Some(2), + Skill::SleepPlus => Some(2), + Skill::SleepPlusPlus => Some(2), Skill::Sustain | Skill::SustainPlus | @@ -1445,10 +1442,6 @@ fn counter(source: &mut Construct, target: &mut Construct, mut results: Resoluti results.push(Resolution::new(source, target) .event(target.add_effect(skill, skill.effect()[0]))); - results.push(Resolution::new(source, target) - .event(target.add_effect(skill, skill.effect()[1])) - .stages(EventStages::PostOnly)); - return results; } @@ -1456,7 +1449,7 @@ fn counter_attack(source: &mut Construct, target: &mut Construct, mut results: R let amount = source.red_power().pct(skill.multiplier()); target.deal_red_damage(skill, amount) .into_iter() - .for_each(|e| results.push(Resolution::new(source, target).event(e).stages(EventStages::StartPost))); + .for_each(|e| results.push(Resolution::new(source, target).event(e))); return results; } @@ -1489,7 +1482,7 @@ fn slay(source: &mut Construct, target: &mut Construct, mut results: Resolutions match e { Event::Damage { amount, mitigation: _, colour: _, skill: _ } => { results.push(Resolution::new(source, target).event(e)); - let heal = source.deal_green_damage(skill, amount); + let heal = source.deal_green_damage(skill, amount.pct(50)); for h in heal { results.push(Resolution::new(source, source).event(h).stages(EventStages::PostOnly)); }; diff --git a/server/src/vbox.rs b/server/src/vbox.rs index c8df2087..162f48de 100644 --- a/server/src/vbox.rs +++ b/server/src/vbox.rs @@ -169,7 +169,7 @@ impl Vbox { let combo = combos.iter().find(|c| c.components == input).ok_or(err_msg("not a combo"))?; self.bound.push(combo.item); - self.bound.sort_unstable(); + // self.bound.sort_unstable(); Ok(self) }