diff --git a/VERSION b/VERSION index ee672d89..ed21137e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.1 \ No newline at end of file +1.10.0 \ No newline at end of file diff --git a/WORKLOG.md b/WORKLOG.md index 9ea59bde..d254b298 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -1,10 +1,45 @@ # WORK WORK ## NOW -*PRODUCTION* - +_ntr_ * can't reset password without knowing password =\ +* skip faceoff on server side +* change cooldowns to delay & recharge + - delay is cooldown before skill can first be used + - recharge is cooldown after using skill + - every x speed reduces delay of skills +* audio + * animation effects + * vbox combine / buy / equip etc + * background music +* effects rework +Siphon = +[ + DamageBlue(50%), + Apply( + Siphon(2T) + - Siphoning(2T) + ), +] + +Hexagon Set +- Pick Colour +- Random Walk +- Draw hex +- Increase intensity for each visit + +_mashy_ +* floating combat text combat (start opposite hp and float towards it) to speed up animations +* represent construct colours during game phase (try %bar or dots) +* buy from preview if you have the required bases in vbox / inventory + - a "buy" becomes available under the current info / preview section + - clicking the buy automatically purchases / combine items + - could also be used to upgrade already equipped skills / specs + - e.g. an equipped white power spec could be upgraded by clicking under preview + - if this was added we could reduce inventory size to 3 and rearrange vbox (see mockup img) + +_external_ * Graphics * Img * Skill Icons @@ -15,42 +50,13 @@ - Speed (e.g. boots) - Life (e.g. heart) -* reduce inventory size and consolidate vbox and inventory on left side - -* audio - * background music - * animation effects - * vbox combine / buy / equip etc - -* reclaim change colour from red (clashes with red items) - -* represent construct colours during game phase (coloured border?) +_tba_ * supporter gold name in instance (anyone whos put any money into game) - * Give the bots some ai / make stronger so its a challenge for new people to beat - train a few games of with some round losses to get them into the game -* Speed up animations slightly (3s per normal event too long) - - Improve combat text to start at the opposite end of construct and float towards health stats - - Show combat text for skill cast possibly? Watch some pokemans etc for modern combat smoothing - -* skip faceoff on server side - ## SOON -* buy from preview if you have the required bases in vbox / inventory - - a "buy" becomes available under the current info / preview section - - clicking the buy automatically purchases / combine items - - could also be used to upgrade already equipped skills / specs - - e.g. an equipped white power spec could be upgraded by clicking under preview - - if this was added we could reduce inventory size to 3 and rearrange vbox (see mockup img) - - -* change cooldowns to delay & recharge - - delay is cooldown before skill can first be used - - recharge is cooldown after using skill - - every x speed reduces delay of skills - * combo rework - reduce number of items for creating t2/t3 items from 3 -> 2 - add lost complexity by adding skill spec items @@ -61,18 +67,14 @@ - Strike + SpeedRR -> StrikeSpeed (strike has Y% more speed) - Strike + LifeRR -> StrikeLife (Strike recharges X% of damage as red life) - - Can also work as module style passive keystones + - Can also work as module style passive keystones * troll life -> dmg -> Invert life spec? - * prince of peace + * prince of peace * bonus healing / no damage -> Heal power spec? * fuck magic -> Some sort of reflect spec? * empower on ko -> Amplify + Power spec * elo + leaderboards -* reconnect based on time delta - -* ACP - * essential ## LATER @@ -95,8 +97,8 @@ - Working as cooldowns in reverse by building up to the skill rather than waiting * constants -* (maybe) return of the combat log (last few events with condensed descriptions) - - click in to scroll +* return of the combat log (last few events with condensed descriptions) + - button to switch context or overlay the combatlog * mnml tv diff --git a/acp/package.json b/acp/package.json index ca3b6f27..7a6a5902 100644 --- a/acp/package.json +++ b/acp/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.9.1", + "version": "1.10.0", "description": "", "main": "index.js", "scripts": { diff --git a/client/assets/styles/account.less b/client/assets/styles/account.less index a52e6e5b..c920c8dc 100644 --- a/client/assets/styles/account.less +++ b/client/assets/styles/account.less @@ -44,7 +44,7 @@ } &[disabled] { - border: 1px solid @yellow; + border: 0.1em solid @yellow; color: @yellow; background: black; } diff --git a/client/assets/styles/colours.less b/client/assets/styles/colours.less index a1ae5dcb..16e972af 100644 --- a/client/assets/styles/colours.less +++ b/client/assets/styles/colours.less @@ -5,6 +5,7 @@ @white: #f5f5f5; // whitesmoke @purple: #9355b5; // 6lack - that far cover @yellow: #ffa100; +@silver: #c0c0c0; @black: black; @gray: #222; diff --git a/client/assets/styles/controls.less b/client/assets/styles/controls.less index 0bdb4b59..a5938475 100644 --- a/client/assets/styles/controls.less +++ b/client/assets/styles/controls.less @@ -60,7 +60,7 @@ aside { width: 0.25em; max-width: 0.25em; - margin: 0 1em 0 0; + margin: 0 0.5em 0 0; border: none; } @@ -150,13 +150,13 @@ aside { &:hover { color: @red; border-color: @red; - border: 2px solid @red; + border: 0.1em solid @red; }; &:active, &.confirming { background: @red; color: black; - border: 2px solid @red; + border: 0.1em solid @red; } } @@ -164,7 +164,7 @@ aside { &:active, &.confirming { background: @gray-hover; color: black; - border: 2px solid @gray-hover; + border: 0.1em solid @gray-hover; } } diff --git a/client/assets/styles/game.less b/client/assets/styles/game.less index 4ddacc63..1746abea 100644 --- a/client/assets/styles/game.less +++ b/client/assets/styles/game.less @@ -27,6 +27,7 @@ position: absolute; bottom: 0; + margin-bottom: 0.5em; height: 50%; .avatar { @@ -36,6 +37,11 @@ height: 100%; width: 100%; } + + .combat-text { + left: 15%; + top: 20%; + } } .opponent { @@ -71,6 +77,11 @@ "avatar" } + .combat-text { + left: 15%; + top: 100%; + } + .effects { align-self: flex-start; } @@ -144,21 +155,30 @@ } } + .combat-text { + position: absolute; + z-index: 10; + svg { + display: inline; + height: 1em; + margin-right: 0.1em + } + span { + background-color: black; + } + } + .skills { z-index: 2; button { width: 100%; - height: 2em; + height: 3em; + line-height: 1; margin-right: 1em; - span { - background-color: black; - } + background-color: black; } button.active { - background: #2c2c2c; - span { - background-color: #2c2c2c; - } + background-color: #2c2c2c; } } @@ -199,7 +219,7 @@ } &.ko-transition { - animation: target-ko 1s ease-in-out 0s 1; + animation: target-ko 1.3s ease-in-out 0s 1; } &.ko { @@ -236,8 +256,8 @@ #targeting, .resolving-skill { position: absolute; - top: 35%; - height: 15%; + top: calc(35% + 0.5em); // calc for 0.5em top gap + height: calc(15% - 1em); // calc for 0.5em + 0.5em top / bottom gap width: calc(90% - 1.25em); z-index: 2; span { @@ -266,9 +286,7 @@ padding-right: 1em; text-align: center; z-index: 2; - span { - background-color: black; - } + background-color: black; svg { display: inline; height: 1em; @@ -279,7 +297,7 @@ /* some stupid bug in chrome makes it fill the entire screen */ @media screen and (-webkit-min-device-pixel-ratio:0) { #targeting { - max-height: 10em; + // max-height: 10em; } } diff --git a/client/assets/styles/instance.less b/client/assets/styles/instance.less index e54bf31e..52ad8fea 100644 --- a/client/assets/styles/instance.less +++ b/client/assets/styles/instance.less @@ -1,129 +1,21 @@ -.instance { - overflow: hidden; - display: grid; - grid-template-columns: 1fr minmax(min-content, 1fr); - grid-template-rows: min-content 1fr; - - grid-template-areas: - "vbox info" - "constructs constructs"; -} - -@media (max-width: 1920px) { - .instance .info table td svg { - // height: 50%; - stroke-width: 8px; - } - - .instance svg { - height: 1.5em; - } -} - -.instance .top { - grid-area: top; -} - .instance.lobby { align-content: center; } -.scoreboard { - flex: 1; -} - -.instance .info { - margin: 0 0 0 1em; - grid-area: info; - +.instance { + overflow: hidden; display: grid; - grid-template-rows: 13em min-content; + grid-template-rows: min-content 1fr; + grid-template-areas: - "item" - "combos"; + "vbox" + "constructs"; - .combos { - display: grid; - grid-template-columns: repeat(6, 1fr); - align-content: center; - - .table-button { - display: grid; - text-align: center; - align-content: center; - border-bottom: 2px solid #222; - - grid-template-areas: - "item" - "ingr"; - - cursor: pointer; - &:hover { - color: whitesmoke; - background-color: @gray; - } - - .item { - border-top: 2px solid #222; - border-bottom: 2px solid #222; - flex: 1; - grid-area: item; - font-weight: bold; - } - - div { - border-right: 2px solid #222; - svg { - vertical-align: middle; - } - } - - &:first-child { - div { - border-left: 2px solid #222; - } - } - } + .constructs { + grid-area: constructs; } } -.instance .info h2 { - text-transform: uppercase; -} - -.instance .info svg { - display: inline; - height: 1em; -} - -.instance .info figure { - display: inline; - height: 0.5em; - - svg { - margin-right: 0.5em; - } -} - -.instance .info figcaption { - font-size: 1em; - display: inline-block; - vertical-align: middle; -} - - -.instance .constructs { - grid-area: constructs; -} - -.instance .equip { - grid-area: equip; -} - -.instance .equip .skills { - border-right-width: 0; -} - @keyframes action { 0% { color: palegoldenrod; @@ -135,10 +27,6 @@ /* CONSTRUCT LIST */ -.construct-list { - grid-area: constructs; - display: flex; -} .instance-construct { flex: 1; @@ -152,20 +40,22 @@ "stats "; /*padding: 0.5em;*/ - border: 2px solid #222; + border: 0.1em solid #222; border-left-width: 0; -} - -.instance-construct:first-child { - margin-left: 0; - border-left-width: 1px; + &:first-child { + margin-left: 0; + border-left-width: 1px; + } } .construct-list { + grid-area: constructs; + display: flex; + button { &.highlight { color: black; - background: @white; + background: @silver; // border: 1px solid @white; (this bangs around the vbox) // overwrite the classes on white svg elements @@ -264,95 +154,10 @@ } } -/* Equipment */ -.equip { - display: flex; - margin: 1.5em 0; - text-align: center; -} - -.equip h3 { - margin-bottom: 0.5em; - text-transform: uppercase; - font-weight: bold; - letter-spacing: 0.1em; -} - -.equip .specs { - flex: 1; - border: 2px solid #222; -} - -.equip .items { - display: flex; - flex: 1 0 100%; - justify-content: space-around; -} - .label { flex: 1 0 100%; } -// .equipping { -// position: relative; -// } - -// .equipping::before { -// content: ''; -// position: absolute; -// top: 2px; -// left: 50%; -// width: 100%; -// height: 2px; -// transform-origin: center; -// background-color: whitesmoke; -// animation: equipping-skill 2s infinite ease-out alternate; -// opacity: 0; -// } - -// .equipping::after { -// content: ''; -// position: absolute; -// bottom: 2px; -// left: 50%; -// width: 100%; -// height: 2px; -// transform-origin: center; -// background-color: whitesmoke; -// animation: equipping-skill 2s infinite ease-out alternate; -// opacity: 0; -// animation-delay: 0.75s -// } - -// @keyframes equipping-skill { -// from { -// transform: translate(-50%, 0) scaleX(0); -// } - -// to { -// transform: translate(-50%, 0) scaleX(0.75); -// opacity: 1; -// } -// } - -// .equip-spec { -// position: relative; -// stroke: #333; -// } - -// .equip-spec::after { -// content: ''; -// position: absolute; -// bottom: 2px; -// left: 50%; -// width: 100%; -// height: 2px; -// transform-origin: center; -// background-color: whitesmoke; -// animation: equipping-skill 2s infinite ease-out alternate; -// opacity: 0; -// } - .equipping, .receiving { animation: eq 0.75s cubic-bezier(0, 0, 1, 1) 0s infinite alternate; } @@ -484,9 +289,4 @@ to { color: @yellow; } -} - -/* Mobile Nav*/ -.instance-nav { display: none; } - -.vbox-arrow-mobile { display: none } +} \ No newline at end of file diff --git a/client/assets/styles/instance.mobile.less b/client/assets/styles/instance.mobile.less deleted file mode 100644 index 3b68cc7f..00000000 --- a/client/assets/styles/instance.mobile.less +++ /dev/null @@ -1,29 +0,0 @@ -// tablet / ipad -@media (max-width: 1100px) { - .instance { - grid-template-columns: 1fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "vbox" - "constructs"; - - .info { - display: none; - } - } -} - -@media (max-width: 800px) { - .instance { - font-size: 6pt; - grid-template-columns: 1fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "vbox" - "constructs"; - - .info { - display: none; - } - } -} \ No newline at end of file diff --git a/client/assets/styles/menu.less b/client/assets/styles/menu.less index 1ce06f73..e197781e 100644 --- a/client/assets/styles/menu.less +++ b/client/assets/styles/menu.less @@ -74,7 +74,7 @@ button { flex: 1; border-top: 0; - border: 2px solid #222; + border: 0.1em solid #222; &:not(:last-child) { border-right: 0; } diff --git a/client/assets/styles/player.less b/client/assets/styles/player.less index d731e418..bdcf2477 100644 --- a/client/assets/styles/player.less +++ b/client/assets/styles/player.less @@ -42,6 +42,7 @@ .msg { grid-area: msg; + text-transform: uppercase; color: @white; } diff --git a/client/assets/styles/skeleton.css b/client/assets/styles/skeleton.css index 13bb3549..037e1747 100644 --- a/client/assets/styles/skeleton.css +++ b/client/assets/styles/skeleton.css @@ -176,7 +176,6 @@ input[type="button"] { /*padding: 0 2em;*/ color: #555; text-align: center; - font-size: 11px; font-weight: 600; line-height: 38px; letter-spacing: .1rem; diff --git a/client/assets/styles/styles.less b/client/assets/styles/styles.less index 3a9edf37..b22ee13d 100644 --- a/client/assets/styles/styles.less +++ b/client/assets/styles/styles.less @@ -8,8 +8,6 @@ @import 'vbox.less'; @import 'game.less'; @import 'player.less'; -@import 'styles.mobile.less'; -@import 'instance.mobile.less'; html body { margin: 0; @@ -23,6 +21,7 @@ html body { -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; + -webkit-tap-highlight-color: transparent; overflow-x: hidden; overflow-y: hidden; @@ -33,7 +32,6 @@ html body { height: 100vh; max-height: 100vh; min-height: 100vh; - /*padding: 0 20%;*/ /* stops inspector going skitz*/ overflow-x: hidden; @@ -125,14 +123,19 @@ button, input { font-family: 'Jura'; color: whitesmoke; height: auto; - border-width: 2px; + border-width: 0.1em; border-color: @gray-exists; letter-spacing: 0.25em; box-sizing: border-box; - font-size: 100%; + font-size: 1em; flex: 1; border-radius: 0.5em; line-height: 2em; + padding-right: 0.1em; + padding-left: 0.1em; + padding-bottom: 0.1em; + padding-top: 0.1em; + /*the transitions */ transition-property: border-color, color, background; @@ -250,6 +253,7 @@ figure.gray { svg { height: 1em; + stroke-width: 1em; } } @@ -354,3 +358,5 @@ li { flex: 1; } } + +@import 'styles.mobile.less'; diff --git a/client/assets/styles/styles.mobile.less b/client/assets/styles/styles.mobile.less index e46e31a0..27c8fa8a 100644 --- a/client/assets/styles/styles.mobile.less +++ b/client/assets/styles/styles.mobile.less @@ -1,26 +1,47 @@ -@media (max-width: 1000px) { +@media (max-width: 800px) { body { overflow-y: initial; } #mnml { font-size: 8pt; - padding: 0.25em; + padding: 0; .instance { - grid-template-columns: 1fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "vbox" - "constructs"; + "vbox vbox" + "constructs constructs"; + + font-size: 7.5pt; + + .stats { + div:nth-child(4n) { + margin: 0; + } + } svg { stroke-width: 1.25em; } + } .game { + font-size: 7.5pt; + .stats { + svg { + stroke-width: 1.5em; + } + + div:nth-child(4n) { + margin: 0 0.25em; + } + div { + padding: 0 0.25em; + } + + } + .team, #targeting, .resolving-skill { width: calc(90% - 3em); } @@ -29,7 +50,6 @@ grid-template-columns: 1fr; grid-template-rows: min-content 1fr; - .avatar { grid-area: initial; position: absolute; @@ -45,23 +65,28 @@ } .skills { - button[disabled] { - display: none; + display: flex; + button { + font-size: 1em; + letter-spacing: 0.1em; + margin-right: 0; } + + } .effects { - font-size: 1em; + font-size: 1.1em; } .skill-description { - font-size: 0.8em; svg { height: 1em; } } .player { + width: calc(90%); .game-construct { grid-template-areas: "left" @@ -78,7 +103,12 @@ } } + .resolving-skill { + width: calc(90%); + } + .opponent { + width: calc(90%); .game-construct { grid-template-rows: 2fr min-content; grid-template-rows: 1fr; @@ -100,21 +130,31 @@ .instance-construct { position: relative; + grid-template-columns: 1fr 1fr; + grid-template-rows: min-content min-content 1fr min-content; + grid-template-areas: + "skills skills" + "specs specs" + "avatar name" + "stats stats "; + .skills, .specs { font-size: 75%; } - .avatar { - grid-area: initial; - position: absolute; - top: 0; - height: 100%; - width: 100%; - z-index: -1; + .stats { + svg { + height: 1em; + } + } + + .name { + align-self: center; } } aside { + font-size: 75%; button { margin-bottom: 0.5em; } @@ -123,8 +163,8 @@ } -// portrait menu -@media (max-width: 600px) { +// portrait menu or small size vertical in landscape +@media (max-width: 550px) and (max-height: 800px) { #mnml { grid-template-columns: 1fr; grid-template-rows: 1fr; @@ -166,7 +206,7 @@ grid-template-columns: repeat(2, 1fr); button:not(:last-child) { - border: 2px solid #222; + border: 0.1em solid #222; } button.logo { @@ -191,9 +231,107 @@ } } + .stats { + font-size: 6pt; + } + + .skill-description { + font-size: 6pt; + } + section { .list { grid-template-columns: 1fr; } } -} \ No newline at end of file +} + +@media (max-width: 600px) { + .vbox { + grid-template-rows: min-content min-content 1fr; + grid-template-columns: min-content 1fr min-content 1fr; + grid-template-areas: + "store-hdr store-hdr stash-hdr stash-hdr" + "store store stash stash" + "store store info-combiner info-combiner"; + + > div { + padding: 0.25em; + } + + .combos { + display: none; + } + + .info-combiner { + .info { + display: none; + } + + .combiner { + margin: 0; + width: 100%; + height: 100%; + } + } + + .stash { + border: 0; + border-top: 0.1em solid @gray; + border-right: 0.1em solid @gray; + border-bottom: 0.1em solid @gray; + } + + .stash-hdr { + border: 0; + border-left: 0.1em solid @gray; + border-right: 0.1em solid @gray; + + display: grid; + grid-template-rows: min-content min-content; + grid-template-columns: 1fr 1fr; + + h3 { + margin: 0; + } + } + + .store { + border: 0; + border-top: 0.1em solid @gray; + border-bottom: 0.1em solid @gray; + border-right: 0.1em solid @gray; + } + + .store-hdr { + display: grid; + grid-template-columns: min-content min-content 1fr; + + > * { + margin-right: 1em; + } + + grid-template-areas: + "hdr bits btn"; + + h1 { + grid-area: hdr; + margin-bottom: 0.25em; + } + + .bits { + grid-area: bits; + } + + button { + grid-area: btn; + } + } + + .store-hdr, .stash-hdr { + button { + margin: 0; + } + } + } +} diff --git a/client/assets/styles/vbox.less b/client/assets/styles/vbox.less index ea0d9fd7..aab8da8e 100644 --- a/client/assets/styles/vbox.less +++ b/client/assets/styles/vbox.less @@ -1,6 +1,84 @@ .vbox { - margin-bottom: 2em; - line-height: 0; + align-content: space-between; + grid-area: vbox; + display: grid; + grid-template-rows: 3fr minmax(min-content, 2fr); + grid-template-columns: 1fr 4fr 6fr minmax(min-content, 2fr);; + grid-template-areas: + "store-hdr store info-combiner combos" + "stash-hdr stash info-combiner combos"; + margin-bottom: 1em; + + // immediate children + > div { + padding: 0.5em; + } + + label { + line-height: 0; + } + + .store { + grid-area: store; + border-right: 0.15em solid @gray; + border-top: 0.15em solid @gray; + } + + .store-hdr { + grid-area: store-hdr; + display: flex; + flex-flow: column; + + text-align: center; + border-left: 0.15em solid @gray; + border-top: 0.15em solid @gray; + + h1 { + margin-bottom: 0.5em; + } + + button { + line-height: 1.6; + letter-spacing: 0.15em; + background-color: #421010; + &:hover { + background-color: @red; + + } + } + } + + .stash { + grid-area: stash; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 0.5em 1em; + + align-items: center; + border: 0.15em solid @gray; + border-left: 0; + } + + .stash-hdr { + grid-area: stash-hdr; + display: flex; + flex-flow: column; + + text-align: center; + border: 0.15em solid @gray; + border-right: 0; + + h2 { + margin-bottom: 0.5em; + } + + button { + line-height: 1.6; + letter-spacing: 0.15em; + border-width: 0.1em; + } + } + .vbox-hdr { margin-bottom: 1em; height: 2em; @@ -19,14 +97,12 @@ grid-template-columns: repeat(3, 1fr); grid-gap: 0.5em 1em; align-items: center; - margin-bottom: 0.5em; } .vbox-btn { width: 100%; margin: 0; background-color: @gray-box; - height: 3em; line-height: 1em; border-width: 0; @@ -34,26 +110,18 @@ color: white; } - &.reclaim { - height: auto; - - &:hover { - color: @red; - }; - } - &[disabled] { background: black; - border-width: 1px; + border-width: 0.1em; }; } - .reclaiming { + .Refunding { button:not([disabled]) { &, &:hover, &:active { background: @red; color: black; - border: 2px solid black; + border: 0.1em solid black; } } svg { @@ -65,21 +133,22 @@ } button { - height: 4em; + height: 3.5em; margin: 0; width: 100%; // text-transform: none; - &.empty { border-style: dashed; } + &.fade { + opacity: 0.4; + } + &.highlight { color: black; - background: @white; - // border: 1px solid @white; (this bangs around the vbox) - + background: @silver; // overwrite the classes on white svg elements svg { stroke-width: 0.75em; @@ -94,7 +163,7 @@ // figures don't scale well figure { svg { - height: 2em; + height: 1.5em; stroke-width: 0.5em; } @@ -102,63 +171,155 @@ line-height: initial; } } + + .info-combiner { + grid-area: info-combiner; + display: grid; + grid-template-areas: + "info" + "combiner"; + grid-template-rows: min-content 1fr; + + .info { + grid-area: info; + line-height: 1.6; + height: 100%; + + h2 { + text-transform: uppercase; + } + + svg { + display: inline; + height: 1em; + } + + figure { + display: inline; + height: 0.5em; + svg { + margin-right: 0.5em; + } + } + + figcaption { + font-size: 1em; + display: inline-block; + vertical-align: middle; + } + + margin-left: 1em; + } + + .combiner { + grid-area: combiner; + margin: 1em 0.5em; + width: 50%; + line-height: 1.3; + font-size: 1.25em; + letter-spacing: 0.1em; + border: 0.1em solid @gray-exists; + &:hover { + border: 0.1em solid @gray-hover; + } + } + // align-self: flex-end; + } + + .combos { + display: grid; + grid-area: combos; + margin-left: 0.5em; + margin-right: 0.5em; + grid-template-rows: min-content min-content; + width: 15.5em; + grid-template-areas: + "comboHeader" + "comboList"; + + h2 { + text-transform: uppercase; + } + + svg { + display: inline; + height: 1em; + } + + figure { + display: inline; + height: 0.5em; + svg { + margin-right: 0.5em; + } + } + + figcaption { + font-size: 1em; + display: inline-block; + vertical-align: middle; + } + + + .combo-header { + text-align: center; + } + + .combo-list { + display: grid; + grid-template-rows: min-content min-content min-content; + grid-template-columns: min-content min-content; + grid-gap: 0.5em; + margin-top: 0.5em; + width: 15.5em; + + .table-button { + display: grid; + text-align: center; + align-content: center; + grid-template-areas: + "item" + "ingr"; + + cursor: pointer; + &:hover { + color: whitesmoke; + background-color: @gray; + } + + .item { + border-top: 0.1em solid #222; + border-bottom: 0.1em solid #222; + flex: 1; + grid-area: item; + font-weight: bold; + div { + width: 5em; + } + } + + + div { + border-left: 0.1em solid #222; + border-right: 0.1em solid #222; + height: 1.75em; + width: 7.5em; + svg { + vertical-align: middle; + } + &:last-child { + border-bottom: 0.1em solid #222; + } + } + } + } + } } -/* VBOX */ -.vbox { - align-content: space-between; - grid-area: vbox; - display: grid; - grid-template-rows: min-content min-content min-content; - grid-template-columns: 1fr min-content 1fr; - grid-template-areas: - "vbox varrow inventory" - "vbox varrow combiner"; -} - -.vbox-inventory { - grid-area: inventory; -} - -.vbox-combiner { - grid-area: combiner; - display: flex; - flex-flow: column; - justify-content: flex-end; -} - -.vbox-arrow, .vbox-arrow-mobile { - display: flex; - justify-content:center; - align-content:center; - flex-direction:column; - - margin: 1em 0.25em 0 0.25em; - grid-area: varrow; - font-size: 2em; - color: @gray-hint; -} - -.vbox-combiner button { - flex: 0; -} - - -.vbox-hdr { - display: flex; -} - -.vbox-hdr h3 { - flex: 1; -} - -.vbox-hdr .bits { - font-size: 2em; - line-height: 1em; - animation: bits 1s ease-out; -} - -.arrow { - grid-area: arrow; - color: @gray-hint; -} +@media (min-width: 2000px) { + .vbox { + button { + height: 4.5em; + } + } +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index b5585de2..4cbd135c 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.9.1", + "version": "1.10.0", "description": "", "main": "index.js", "scripts": { diff --git a/client/src/actions.jsx b/client/src/actions.jsx index 87d0cf51..e426f70b 100644 --- a/client/src/actions.jsx +++ b/client/src/actions.jsx @@ -23,7 +23,9 @@ export const setConstructRename = value => ({ type: 'SET_CONSTRUCT_RENAME', valu export const setGame = value => ({ type: 'SET_GAME', value }); export const setGameSkillInfo = value => ({ type: 'SET_GAME_SKILL_INFO', value }); export const setGameEffectInfo = value => ({ type: 'SET_GAME_EFFECT_INFO', value }); + export const setInfo = value => ({ type: 'SET_INFO', value }); +export const setComboPreview = value => ({ type: 'SET_COMBO_PREVIEW', value }); export const setEmail = value => ({ type: 'SET_EMAIL', value }); export const setInvite = value => ({ type: 'SET_INVITE', value }); @@ -35,7 +37,6 @@ export const setMtxActive = value => ({ type: 'SET_MTX_ACTIVE', value }); export const setNav = value => ({ type: 'SET_NAV', value }); export const setPing = value => ({ type: 'SET_PING', value }); export const setPlayer = value => ({ type: 'SET_PLAYER', value }); -export const setReclaiming = value => ({ type: 'SET_RECLAIMING', value }); export const setShowLog = value => ({ type: 'SET_SHOW_LOG', value }); export const setShop = value => ({ type: 'SET_SHOP', value }); export const setSubscription = value => ({ type: 'SET_SUBSCRIPTION', value }); @@ -48,7 +49,9 @@ export const setTeamSelect = value => ({ type: 'SET_TEAM_SELECT', value: Array.f export const setTutorial = value => ({ type: 'SET_TUTORIAL', value }); export const setTutorialGame = value => ({ type: 'SET_TUTORIAL_GAME', value }); +export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value: value }); +export const setVboxCombiner = value => ({ type: 'SET_VBOX_COMBINER', value }); export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value }); -export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value }); +export const setVboxInfo = value => ({ type: 'SET_VBOX_INFO', value }); export const setWs = value => ({ type: 'SET_WS', value }); diff --git a/client/src/animations.utils.jsx b/client/src/animations.utils.jsx index 21b38925..461abe84 100644 --- a/client/src/animations.utils.jsx +++ b/client/src/animations.utils.jsx @@ -128,11 +128,11 @@ function getText(resolution) { let { amount } = event; let css = ''; if (colour === 'Green') { - css = 'green-damage'; + css = 'green'; amount *= -1; } - if (colour === 'Red') css = 'red-damage'; - if (colour === 'Blue') css = 'blue-damage'; + if (colour === 'Red') css = 'red'; + if (colour === 'Blue') css = 'blue'; const mitigationText = mitigation ? `(${mitigation})` @@ -142,7 +142,7 @@ function getText(resolution) { if (type === 'Healing') { const { amount, overhealing } = event; - return { text: `${amount} (${overhealing} OH)`, css: 'green-damage' }; + return { text: `${amount} (${overhealing} OH)`, css: 'green' }; } if (type === 'Inversion') { @@ -160,9 +160,9 @@ function getText(resolution) { if (type === 'Recharge') { const { red, blue } = event; - if (red > 0 && blue > 0) return { text: [`+${red}R +${blue}B`, ''], css: 'rb-damage' }; - if (red > 0) return { text: [`+${red}R`, ''], css: 'red-damage' }; - if (blue > 0) return { text: [`+${blue}B`, ''], css: 'blue-damage' }; + if (red > 0 && blue > 0) return { text: `+${red}R +${blue}B`, css: 'rb' }; + if (red > 0) return { text: `+${red}R`, css: 'red' }; + if (blue > 0) return { text: `+${blue}B`, css: 'blue' }; return nullText; } @@ -178,12 +178,14 @@ function getText(resolution) { const { green, red, blue } = resolution.target; const { text, css, effects } = generatePostSkill(); + const skill = resolution.event[1] ? resolution.event[1].skill : null; return { css, text, effects, life: { green, red, blue }, constructId: resolution.target.id, + skill, }; } diff --git a/client/src/app.jsx b/client/src/app.jsx index 0d668980..1599c1ab 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -8,7 +8,6 @@ const { createStore, combineReducers } = require('redux'); const { StripeProvider } = require('react-stripe-elements'); const reducers = require('./reducers'); -const actions = require('./actions'); const setupKeys = require('./keyboard'); const createSocket = require('./socket'); const registerEvents = require('./events'); diff --git a/client/src/components/account.status.jsx b/client/src/components/account.status.jsx index 99e5014e..1353deff 100644 --- a/client/src/components/account.status.jsx +++ b/client/src/components/account.status.jsx @@ -33,11 +33,9 @@ const addState = connect( function accountPage() { dispatch(actions.setGame(null)); dispatch(actions.setInstance(null)); - dispatch(actions.setReclaiming(false)); dispatch(actions.setActiveSkill(null)); dispatch(actions.setInfo(null)); dispatch(actions.setItemUnequip([])); - dispatch(actions.setVboxHighlight([])); return dispatch(actions.setNav('account')); } diff --git a/client/src/components/anims/slay.jsx b/client/src/components/anims/slay.jsx index 31bc57d2..a65de8cd 100644 --- a/client/src/components/anims/slay.jsx +++ b/client/src/components/anims/slay.jsx @@ -4,9 +4,6 @@ const anime = require('animejs').default; const { connect } = require('preact-redux'); const { TIMES } = require('../../constants'); -const { randomPoints } = require('../../utils'); - -const duration = TIMES.TARGET_DURATION_MS; const addState = connect( function receiveState(state) { @@ -95,17 +92,17 @@ class Slay extends Component { this.animations.push(anime({ targets: '#slay', opacity: [ - { value: 1, delay: TIMES.TARGET_DELAY_MS, duration: TIMES.TARGET_DURATION_MS * 0.3 }, + { value: 1, delay: TIMES.TARGET_DELAY_MS, duration: TIMES.TARGET_DURATION_MS * 0.2 }, { value: 0, - delay: TIMES.TARGET_DURATION_MS * 0.7 + TIMES.POST_SKILL_DURATION_MS * 0.8, - duration: TIMES.POST_SKILL_DURATION_MS * 0.2, + delay: TIMES.TARGET_DURATION_MS + TIMES.POST_SKILL_DURATION_MS * 0.2, + duration: TIMES.POST_SKILL_DURATION_MS * 0.3, }], translateY: 0, translateX: 0, loop: false, delay: TIMES.TARGET_DELAY_MS, - duration: (duration * 1 / 2), + duration: (TIMES.TARGET_DURATION_MS * 0.5), easing: 'easeInQuad', })); @@ -113,15 +110,15 @@ class Slay extends Component { targets: ['#slayFilter feTurbulence', '#slayFilter feDisplacementMap'], baseFrequency: 10, scale: 100, - delay: (TIMES.TARGET_DELAY_MS + duration * 1 / 2), - duration: (duration * 1 / 2), + delay: (TIMES.TARGET_DELAY_MS + TIMES.TARGET_DURATION_MS * 0.5), + duration: (TIMES.TARGET_DURATION_MS * 0.5), easing: 'easeInQuad', })); this.animations.push(anime({ targets: '#sword', opacity: 0, - delay: (TIMES.TARGET_DELAY_MS + duration + TIMES.POST_SKILL_DURATION_MS * 0.7), + delay: (TIMES.TARGET_DELAY_MS + TIMES.TARGET_DURATION_MS), })); const projectiles = document.querySelectorAll('#slay circle'); @@ -130,7 +127,7 @@ class Slay extends Component { targets: proj, cx: Math.random() * 250 + 25, cy: Math.random() * 200 - 100, - delay: (TIMES.TARGET_DELAY_MS + duration + TIMES.POST_SKILL_DURATION_MS * 0.7), + delay: (TIMES.TARGET_DELAY_MS + TIMES.TARGET_DURATION_MS + TIMES.POST_SKILL_DURATION_MS * 0.2), duration: (TIMES.POST_SKILL_DURATION_MS * 0.3), easing: 'easeInQuad', })); diff --git a/client/src/components/construct.jsx b/client/src/components/construct.jsx index c39b219c..34453f9e 100644 --- a/client/src/components/construct.jsx +++ b/client/src/components/construct.jsx @@ -19,7 +19,7 @@ const addState = connect( ); class ConstructAvatar extends Component { - constructor(props) { + constructor() { super(); // The animation ids are a check to ensure that animations are not repeated // When a new construct animation is communicated with state it will have a corresponding Id @@ -96,26 +96,16 @@ class ConstructAvatar extends Component { } } - -const addStateText = connect( - function receiveState(state) { - const { animText } = state; - return { animText }; - } -); - -function constructText(props) { - const { construct, animText } = props; +function ConstructText(props) { + const { construct } = props; if (!construct) return false; - const text = animText && animText.constructId === construct.id - ? animText.text - : construct.name; + const text = construct.name; return

{text}

; } module.exports = { ConstructAvatar: addState(ConstructAvatar), - ConstructText: addStateText(constructText), + ConstructText, }; diff --git a/client/src/components/demo.jsx b/client/src/components/demo.jsx index 494015ef..e6bee9eb 100644 --- a/client/src/components/demo.jsx +++ b/client/src/components/demo.jsx @@ -46,7 +46,7 @@ function Demo(args) { const { combiner, items, equipping, equipped, players, combo } = demo; const vboxDemo = () => { - function inventoryBtn(i, j) { + function stashBtn(i, j) { if (!i) return ; const highlighted = combiner.indexOf(j) > -1; const classes = `${highlighted ? 'highlight' : ''}`; @@ -82,7 +82,7 @@ function Demo(args) { ); } - function inventoryElement() { + function stashElement() { return (
@@ -96,7 +96,7 @@ function Demo(args) {
 
- {items.map((i, j) => inventoryBtn(i, j))} + {items.map((i, j) => stashBtn(i, j))}
{combinerBtn()}
@@ -106,7 +106,7 @@ function Demo(args) { return (
- {inventoryElement()} + {stashElement()}
); }; diff --git a/client/src/components/game.construct.jsx b/client/src/components/game.construct.jsx index 4ffede73..347b4511 100644 --- a/client/src/components/game.construct.jsx +++ b/client/src/components/game.construct.jsx @@ -1,17 +1,82 @@ -const { connect } = require('preact-redux'); -const { Component } = require('preact'); const preact = require('preact'); +const { connect } = require('preact-redux'); +const anime = require('animejs').default; const range = require('lodash/range'); const reactStringReplace = require('react-string-replace'); -const { STATS } = require('../utils'); +const { STATS, removeTier } = require('../utils'); const { ConstructAvatar, ConstructText } = require('./construct'); const shapes = require('./shapes'); -const { INFO } = require('./../constants'); +const { INFO, TIMES } = require('./../constants'); const actions = require('../actions'); const SkillBtn = require('./skill.btn'); +const addStateText = connect(({ animText, itemInfo }) => ({ animText, itemInfo })); + +class combatText extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.animText !== this.props.animText) return true; + return false; + } + + componentDidUpdate(prevProps) { + const { animText, construct } = this.props; + if (animText && animText !== prevProps.animText && animText.constructId === construct.id) { + anime({ + targets: '.combat-text', + top: '40%', + duration: TIMES.POST_SKILL_DURATION_MS - 500, + easing: 'easeOutQuad', + }); + } + } + + render(props) { + const { construct, animText, itemInfo } = props; + if (animText && animText.constructId === construct.id) { + const itemSourceDescription = () => { + const itemSource = itemInfo.combos.filter(c => c.item === removeTier(animText.skill)); + const itemSourceInfo = itemSource.length + ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` + : false; + const itemRegEx = /(Red|Blue|Green)/; + return reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); + }; + + const animationTextHtml = () => { + // monkaW hack to make red / blue recharge work nicely + if (animText.css === 'rb') { + const text = animText.text.split(' '); + return ( +

+ {text[0]}  + {text[1]} +

+ ); + } + return ( +

+ {animText.text} +

+ ); + }; + + + return ( +
+

{animText.skill}

+ {itemSourceDescription()} + {animationTextHtml()} +
+ ); + } + return null; + } +} + +const ConstructAnimationText = addStateText(combatText); + const addState = connect( function receiveState(state) { const { @@ -79,7 +144,7 @@ const eventClasses = (animating, animFocus, construct, postSkill) => { return postSkill.css; }; -class GameConstruct extends Component { +class GameConstruct extends preact.Component { constructor() { super(); this.resolvedLength = 0; @@ -120,6 +185,7 @@ class GameConstruct extends Component { const koEvent = animText ? animText.text === 'KO!' && animText.constructId === construct.id : false; const ko = construct.green_life.value === 0 && !koEvent ? 'ko' : ''; const classes = eventClasses(animating, animFocus, construct, animText); + const cssClass = ['ko-transition', 'unfocus'].includes(classes) ? classes : null; const stats = ['RedLife', 'GreenLife', 'BlueLife'].map((s, j) => (
@@ -170,7 +236,7 @@ class GameConstruct extends Component { setTutorialGameClear(activeSkill, tutorialGame); }} style={ activeSkill ? { cursor: 'pointer' } : {}} - class={`game-construct ${ko} ${classes}`} > + class={`game-construct ${ko} ${cssClass}`}>
{crypSkills} {effectBox()} @@ -179,6 +245,7 @@ class GameConstruct extends Component {
{stats}
+
); diff --git a/client/src/components/header.jsx b/client/src/components/header.jsx index b591afe0..734b1761 100644 --- a/client/src/components/header.jsx +++ b/client/src/components/header.jsx @@ -32,11 +32,9 @@ const addState = connect( function setNav(place) { dispatch(actions.setGame(null)); dispatch(actions.setInstance(null)); - dispatch(actions.setReclaiming(false)); dispatch(actions.setActiveSkill(null)); dispatch(actions.setInfo(null)); dispatch(actions.setItemUnequip([])); - dispatch(actions.setVboxHighlight([])); dispatch(actions.setMtxActive(null)); return dispatch(actions.setNav(place)); } diff --git a/client/src/components/info.component.jsx b/client/src/components/info.component.jsx deleted file mode 100644 index 71360599..00000000 --- a/client/src/components/info.component.jsx +++ /dev/null @@ -1,184 +0,0 @@ -const preact = require('preact'); -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'); - - -class InfoComponent extends preact.Component { - shouldComponentUpdate(newProps, newState) { - if (newProps.tutorial !== this.props.tutorial) return true; - // We don't care about info during tutorial - if (newProps.tutorial && this.props.instance.time_control === 'Practice' - && this.props.instance.rounds.length === 1) return false; - if (newProps.info !== this.props.info) return true; - if (newState.comboItem !== this.state.comboItem) return true; - - return false; - } - - componentDidUpdate(prevProps) { - // Catch case where mouse events don't properly clear state and info changed - if (prevProps.info !== this.props.info && this.state.comboItem) this.setState({ comboItem: null }); - } - - render(args) { - const { - // Variables that will change - info, - tutorial, - - // 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; - const { comboItem } = this.state; - function Info() { - if (tutorial) { - const tutorialStageInfo = tutorialStage(tutorial, ws, setTutorialNull, instance); - if (tutorialStageInfo) return tutorialStageInfo; - } - if (!info) return false; - if (info.includes('constructName')) { - return ( -
-

{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 fullInfo = comboItem - ? itemInfo.items.find(i => i.item === comboItem) || INFO[comboItem] - : itemInfo.items.find(i => i.item === info) || INFO[info]; - if (!fullInfo) return false; - const isSkill = fullInfo.skill; - const isSpec = fullInfo.spec; - - 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 = fullInfo.item; - while (infoName.includes('Plus')) infoName = infoName.replace('Plus', '+'); - - const itemSource = itemInfo.combos.filter(c => c.item === removeTier(fullInfo.item)); - - let itemSourceInfo = itemSource.length && !isSpec - ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` - : false; - - let header = null; - if (!itemSource.length) header = isSkill ?

SKILL

:

SPEC

; - 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 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; - - const comboTable = vboxCombos.map((c, i) => { - const mouseOver = e => { - e.stopPropagation(); - this.setState({ comboItem: c.item }); - }; - const componentTable = (c.components.some(ci => ['Red', 'Blue', 'Green'].includes(ci))) - ? [
{convertItem(c.components[0])} {convertItem(c.components[1])}
, -
{convertItem(c.components[2])}
] - : c.components.map((u, j) =>
{convertItem(u)}
); - return ( -
setInfo(c.item)}> -
- {convertItem(c.item)} -
- {componentTable} -
- ); - }); - return ( -
- {comboTable} -
- ); - }; - - return ( -
this.setState({ comboItem: null })}> - - -
- ); - } -} - -module.exports = InfoComponent; diff --git a/client/src/components/info.container.jsx b/client/src/components/info.container.jsx deleted file mode 100644 index 8a8dbb6f..00000000 --- a/client/src/components/info.container.jsx +++ /dev/null @@ -1,43 +0,0 @@ -const { connect } = require('preact-redux'); - -const actions = require('../actions'); -const Info = require('./info.component'); - -const addState = connect( - function receiveState(state) { - const { - ws, - info, - itemInfo, - instance, - player, - account, - tutorial, - } = state; - - return { - ws, - info, - itemInfo, - 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/instance.component.jsx b/client/src/components/instance.component.jsx index 8b6c2377..6a3a3df2 100644 --- a/client/src/components/instance.component.jsx +++ b/client/src/components/instance.component.jsx @@ -2,7 +2,6 @@ const preact = require('preact'); const { connect } = require('preact-redux'); const Vbox = require('./vbox.component'); -const InfoContainer = require('./info.container'); const InstanceConstructsContainer = require('./instance.constructs'); const Faceoff = require('./faceoff'); @@ -11,10 +10,12 @@ const actions = require('../actions'); const addState = connect( function receiveState(state) { const { + comboPreview, instance, nav, } = state; return { + comboPreview, instance, nav, }; @@ -27,14 +28,20 @@ const addState = connect( function clearItems() { - dispatch(actions.setReclaiming(false)); dispatch(actions.setItemUnequip([])); - dispatch(actions.setVboxHighlight([])); - dispatch(actions.setVboxSelected({ shopSelect: [], stashSelect: [] })); + dispatch(actions.setVboxCombiner(null)); + dispatch(actions.setVboxHighlight(null)); + dispatch(actions.setVboxInfo(null)); + dispatch(actions.setVboxSelected({ storeSelect: [], stashSelect: [] })); return true; } + function clearComboPreview() { + return dispatch(actions.setComboPreview(null)); + } + return { + clearComboPreview, setInfo, clearItems, }; @@ -43,7 +50,9 @@ const addState = connect( function Instance(args) { const { + comboPreview, instance, + clearComboPreview, clearItems, } = args; @@ -58,14 +67,14 @@ function Instance(args) { clearItems(); } - function onTouchMove(e) { - e.preventDefault(); + function mouseOver(e) { + e.stopPropagation(); + if (comboPreview) clearComboPreview(); } return ( -
+
-
); diff --git a/client/src/components/instance.constructs.jsx b/client/src/components/instance.constructs.jsx index 59e77b10..5a752394 100644 --- a/client/src/components/instance.constructs.jsx +++ b/client/src/components/instance.constructs.jsx @@ -24,8 +24,8 @@ const addState = connect( tutorial, } = state; - function sendVboxAcceptEquip(constructId) { - return ws.sendVboxAcceptEquip(instance.id, vboxSelected.shopSelect[0][0], vboxSelected.shopSelect[0][1], constructId); + function sendVboxBuyEquip(constructId) { + return ws.sendVboxBuyEquip(instance.id, vboxSelected.storeSelect[0][0], vboxSelected.storeSelect[0][1], constructId); } function sendVboxApply(constructId, i) { @@ -40,7 +40,7 @@ const addState = connect( instance, player, account, - sendVboxAcceptEquip, + sendVboxBuyEquip, sendVboxUnequipApply, sendVboxApply, itemInfo, @@ -60,7 +60,7 @@ const addState = connect( } function setItemUnequip(v) { - dispatch(actions.setVboxSelected({ shopSelect: [], stashSelect: [] })); + dispatch(actions.setVboxSelected({ storeSelect: [], stashSelect: [] })); return dispatch(actions.setItemUnequip(v)); } @@ -83,7 +83,7 @@ function Construct(props) { itemInfo, // Function Calls sendVboxApply, - sendVboxAcceptEquip, + sendVboxBuyEquip, sendVboxUnequipApply, setItemUnequip, setInfo, @@ -91,14 +91,14 @@ function Construct(props) { const { vbox } = player; - const itemEquip = vboxSelected.shopSelect.length === 0 && vboxSelected.stashSelect.length === 1 + const itemEquip = vboxSelected.storeSelect.length === 0 && vboxSelected.stashSelect.length === 1 ? vboxSelected.stashSelect[0] : -1; 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]; + return sk.skill === vbox.stash[itemEquip]; }); const tutorialDisableEquip = tutorialShouldDisableEquip(tutorial, iter, instance, construct); function onClick(e) { @@ -106,7 +106,7 @@ function Construct(props) { e.preventDefault(); if (duplicateSkill || tutorialDisableEquip) return true; if (itemEquip !== -1) return sendVboxApply(construct.id, itemEquip); - if (vboxSelected.shopSelect.length === 1) return sendVboxAcceptEquip(construct.id); + if (vboxSelected.storeSelect.length === 1) return sendVboxBuyEquip(construct.id); if (itemUnequip.length && itemUnequip[0] !== construct.id) return sendVboxUnequipApply(construct.id); setItemUnequip([]); return true; @@ -114,7 +114,7 @@ function Construct(props) { function hoverInfo(e, info) { e.stopPropagation(); if (!info) return false; - if (vboxSelected.shopSelect.length || vboxSelected.stashSelect.length) return false; + if (vboxSelected.storeSelect.length || vboxSelected.stashSelect.length) return false; return setInfo(info); } @@ -129,12 +129,14 @@ function Construct(props) { function skillClick(e) { if (!skill) return false; - setItemUnequip([construct.id, skill.skill, i]); e.stopPropagation(); + if (itemUnequip.length && itemUnequip[0] === construct.id && skill.skill === itemUnequip[1] + && i === itemUnequip[2]) return setItemUnequip([]); + setItemUnequip([construct.id, skill.skill, i]); return true; } - const equipping = skillList.includes(vbox.bound[itemEquip]) && !skill + const equipping = skillList.includes(vbox.stash[itemEquip]) && !skill && !tutorialDisableEquip && !duplicateSkill && i === construct.skills.length; const border = () => { if (!skill) return ''; @@ -167,7 +169,7 @@ function Construct(props) { const s = construct.specs[i]; if (!s) { - const equipping = specList.includes(vbox.bound[itemEquip]) && i === construct.specs.length; + const equipping = specList.includes(vbox.stash[itemEquip]) && i === construct.specs.length; const classes = `${equipping ? 'equipping' : 'gray'} empty`; return ( ); } diff --git a/client/src/components/targeting.arrows.jsx b/client/src/components/targeting.arrows.jsx index f93966c5..bed48261 100644 --- a/client/src/components/targeting.arrows.jsx +++ b/client/src/components/targeting.arrows.jsx @@ -81,21 +81,7 @@ class TargetSvg extends Component { // resolutions happening // just put skill name up - if (animating) { - if (!animSkill) return false; - const itemSource = itemInfo.combos.filter(c => c.item === removeTier(animSkill)); - const itemSourceInfo = itemSource.length - ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` - : false; - const itemRegEx = /(Red|Blue|Green)/; - const itemSourceDescription = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); - return ( -
-

{animSkill}

-
{itemSourceDescription}
-
- ); - } + if (animating) return false; const playerTeam = game.players.find(t => t.id === account.id); const otherTeam = game.players.find(t => t.id !== account.id); @@ -110,8 +96,11 @@ class TargetSvg extends Component { ? playerTeam.constructs.findIndex(c => c.id === cast.target_construct_id) : otherTeam.constructs.findIndex(c => c.id === cast.target_construct_id); + const skillNumber = window.innerWidth <= 800 // mobile styling trigger + ? playerTeam.constructs[source].skills.findIndex(s => s.skill === cast.skill) + : 0; const sourceY = height; - const sourceX = (source * width / 3) + width / 24; + const sourceX = (source * width / 3) + width / 18 + skillNumber * (width / 9); const targetX = (target * width / 3) + width / 6 + (defensive ? width / 64 : 0) + (source * width / 18); diff --git a/client/src/components/vbox.combiner.jsx b/client/src/components/vbox.combiner.jsx new file mode 100644 index 00000000..9c7ddd96 --- /dev/null +++ b/client/src/components/vbox.combiner.jsx @@ -0,0 +1,67 @@ +const preact = require('preact'); +const { connect } = require('preact-redux'); + +const addState = connect(({ vboxCombiner }) => ({ vboxCombiner })); + +class Combiner extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.vbox !== this.props.vbox) return true; + if (newProps.vboxSelected !== this.props.vboxSelected) return true; + if (newProps.vboxBuySelected !== this.props.vboxBuySelected) return true; + if (newProps.sendVboxCombine !== this.props.sendVboxCombine) return true; + if (newProps.vboxCombiner !== this.props.vboxCombiner) return true; + return false; + } + + render(args) { + const { + // passed props + vbox, + vboxSelected, + vboxBuySelected, + sendVboxCombine, + // state props + vboxCombiner, + } = args; + + const { stashSelect, storeSelect } = vboxSelected; + + function cost([group, i]) { + if (group === 'Colours') return 1; + if (group === 'Skills') return 2; + if (group === 'Specs') return 3; + }; + + + if (vboxCombiner) { + const combinerComboText = vboxCombiner.replace('Plus', '+'); + let bits = 0; + storeSelect.forEach(item => bits += cost(item)); + return ( + + ); + } + + if (stashSelect.length === 0 && storeSelect.length === 1) { + const item = storeSelect[0]; + return ( + + ); + } + + return false; + } +} + +module.exports = addState(Combiner); diff --git a/client/src/components/vbox.combos.jsx b/client/src/components/vbox.combos.jsx new file mode 100644 index 00000000..f3c18682 --- /dev/null +++ b/client/src/components/vbox.combos.jsx @@ -0,0 +1,86 @@ +const preact = require('preact'); +const { connect } = require('preact-redux'); + +const { convertItem } = require('../utils'); +const actions = require('../actions'); + +const addState = connect( + function receiveState(state) { + const { + info, instance, itemInfo, tutorial, vboxInfo, + } = state; + return { + info, instance, itemInfo, tutorial, vboxInfo, + }; + }, + + function receiveDispatch(dispatch) { + function setComboPreview(item) { + return dispatch(actions.setComboPreview(item)); + } + + return { + setComboPreview, + }; + } +); + +class Combos extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.info !== this.props.info) return true; + if (newProps.instance !== this.props.instance) return true; + if (newProps.itemInfo !== this.props.itemInfo) return true; + if (newProps.tutorial !== this.props.tutorial) return true; + if (newProps.vboxInfo !== this.props.vboxInfo) return true; + return false; + } + + render(props) { + const { + info, + instance, + itemInfo, + tutorial, + vboxInfo, + setComboPreview, + } = props; + if (tutorial && instance.time_control === 'Practice' && instance.rounds.length === 1) return false; + + const vboxCombos = itemInfo.combos.filter(c => c.components.includes(vboxInfo || info)); + if (vboxCombos.length > 6 || vboxCombos.length === 0) return
; + + const comboTable = vboxCombos.map((c, i) => { + const mouseOver = e => { + e.stopPropagation(); + setComboPreview(c.item); + }; + const componentTable = (c.components.some(ci => ['Red', 'Blue', 'Green'].includes(ci))) + ? [
{convertItem(c.components[0])} {convertItem(c.components[1])}
, +
{convertItem(c.components[2])}
] + : c.components.map((u, j) =>
{convertItem(u)}
); + return ( +
+
+ {convertItem(c.item)} +
+ {componentTable} +
+ ); + }); + return ( +
+
+

COMBOS

+ Combine colours and items. +
+
e.stopPropagation()} + onClick={e => e.stopPropagation()}> + {comboTable} +
+
+ ); + } +} + +module.exports = addState(Combos); diff --git a/client/src/components/vbox.component.jsx b/client/src/components/vbox.component.jsx index a0329268..2f1770da 100644 --- a/client/src/components/vbox.component.jsx +++ b/client/src/components/vbox.component.jsx @@ -1,43 +1,48 @@ 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 forEach = require('lodash/forEach'); -const { removeTier } = require('../utils'); -const shapes = require('./shapes'); const actions = require('../actions'); -const buttons = require('./buttons'); + +const InfoContainer = require('./vbox.info'); +const StashElement = require('./vbox.stash'); +const StoreElement = require('./vbox.store'); +const Combiner = require('./vbox.combiner'); +const Combos = require('./vbox.combos'); + +const { setVboxState } = require('./vbox.utils'); const addState = connect( function receiveState(state) { const { ws, + itemUnequip, instance, player, - reclaiming, + tutorial, vboxSelected, itemInfo, - itemUnequip, - tutorial, } = state; - function sendVboxDiscard() { - return ws.sendVboxDiscard(instance.id); + function sendInstance() { + return ws.sendInstanceState(instance.id); } - function sendVboxAccept(group, index) { + function sendVboxRefill() { + return ws.sendVboxRefill(instance.id); + } + + function sendVboxBuy(group, index) { + if (!(vboxSelected.storeSelect.length === 1 && vboxSelected.stashSelect.length === 0)) return false; document.activeElement.blur(); - return ws.sendVboxAccept(instance.id, group, index); + return ws.sendVboxBuy(instance.id, group, index); } function sendVboxCombine() { - return ws.sendVboxCombine(instance.id, vboxSelected.stashSelect, vboxSelected.shopSelect); + return ws.sendVboxCombine(instance.id, vboxSelected.stashSelect, vboxSelected.storeSelect); } - function sendVboxReclaim(i) { - return ws.sendVboxReclaim(instance.id, i); + function sendVboxRefund(i) { + return ws.sendVboxRefund(instance.id, i); } function sendItemUnequip([constructId, item]) { @@ -45,85 +50,53 @@ const addState = connect( } return { + itemUnequip, instance, player, - reclaiming, - sendVboxAccept, - sendVboxCombine, - sendVboxDiscard, - sendVboxReclaim, - vboxSelected, - itemInfo, - itemUnequip, - sendItemUnequip, tutorial, + vboxSelected, + + itemInfo, + + sendInstance, + sendItemUnequip, + sendVboxBuy, + sendVboxCombine, + sendVboxRefill, + sendVboxRefund, }; }, function receiveDispatch(dispatch) { - function setReclaiming(v) { - dispatch(actions.setItemUnequip([])); - dispatch(actions.setVboxSelected({ shopSelect: [], stashSelect: [] })); - return dispatch(actions.setReclaiming(v)); - } - function setInfo(item) { return dispatch(actions.setInfo(item)); } - function setVboxSelected(v) { + function setTutorial(stage) { + return dispatch(actions.setTutorial(stage)); + } + + function dispatchVboxSelect(v, state) { + setVboxState(dispatch, v, state); dispatch(actions.setItemUnequip([])); - dispatch(actions.setVboxSelected(v)); return dispatch(actions.setVboxSelected(v)); } return { - setReclaiming, + dispatchVboxSelect, setInfo, - setVboxSelected, + setTutorial, }; } ); -function validVboxSelect(vbox, itemInfo, shopSelect, stashSelect) { - if (shopSelect.length === 0 && stashSelect.length === 0) return false; - - const validSelects = []; - - const stashItems = stashSelect.map(j => vbox.bound[j]); - const shopItems = shopSelect.map(j => vbox.free[j[0]][j[1]]); - - const selectedItems = stashItems.concat(shopItems); - const itemCount = countBy(selectedItems, co => co); - - itemInfo.combos.forEach(combo => { - const comboCount = countBy(combo.components, co => co); - const buyCount = countBy(combo.components, co => co); - const valid = selectedItems.every(c => { - if (!combo.components.includes(c)) return false; - if (itemCount[c] > comboCount[c]) return false; - buyCount[c] -= 1; - return true; - }); - if (valid) { - forEach(buyCount, (value, key) => { - if (value > 0 && !validSelects.includes(key)) validSelects.push(key); - }); - } - }); - - return validSelects; -} - class Vbox extends preact.Component { shouldComponentUpdate(newProps) { - // Single variable props if (newProps.itemUnequip !== this.props.itemUnequip) return true; - if (newProps.reclaiming !== this.props.reclaiming) return true; + if (newProps.instance !== this.props.instance) return true; + if (newProps.player !== this.props.player) 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; } @@ -131,310 +104,132 @@ class Vbox extends preact.Component { const { // Changing state variables itemUnequip, + instance, player, - reclaiming, tutorial, vboxSelected, - instance, - // Static itemInfo, // Function Calls + dispatchVboxSelect, sendItemUnequip, - sendVboxAccept, + sendInstance, + sendVboxBuy, sendVboxCombine, - sendVboxDiscard, - sendVboxReclaim, - setVboxSelected, + sendVboxRefill, + sendVboxRefund, setInfo, - setReclaiming, + setTutorial, } = args; if (!player) return false; const { vbox } = player; - const { shopSelect, stashSelect } = vboxSelected; - const vboxSelecting = shopSelect.length === 1 && stashSelect.length === 0; + const { storeSelect, stashSelect } = vboxSelected; - function combinerChange(newStashSelect) { - return setVboxSelected({ shopSelect, stashSelect: newStashSelect }); - } - - const vboxHighlight = validVboxSelect(vbox, itemInfo, shopSelect, stashSelect); - // - // VBOX - // + const setVboxSelected = v => dispatchVboxSelect(v, { itemInfo, itemUnequip, vbox }); + const clearVboxSelected = () => setVboxSelected({ storeSelect: [], stashSelect: [] }); + const vboxBuySelected = () => sendVboxBuy(storeSelect[0][0], storeSelect[0][1]); + const clearTutorial = () => { + setTutorial(null); + sendInstance(); + }; function vboxHover(e, v) { if (v) { e.stopPropagation(); - if (shopSelect.find(c => c[0])) return true; // There is a base skill or spec selected in the vbox - if (stashSelect.length !== 0) { - const base = stashSelect.find(c => !['Red', 'Blue', 'Green'].includes(vbox.bound[c])); - if (base || base === 0) return true; - } + if (stashSelect.length !== 0 || storeSelect.length !== 0) return true; setInfo(v); } return true; } - function clearVboxSelected() { - setVboxSelected({ shopSelect: [], stashSelect: [] }); - } - - function vboxBuySelected() { - if (!vboxSelecting) return false; - document.activeElement.blur(); - sendVboxAccept(shopSelect[0][0], shopSelect[0][1]); - return true; - } - - function availableBtn(v, group, index) { - if (!v) return ; - const selected = shopSelect.length && shopSelect.some(vs => vs[0] === group && vs[1] === index); - - const comboHighlight = vboxHighlight && vboxHighlight.includes(v) ? 'combo-border' : ''; - - function onClick(e) { - e.stopPropagation(); - if (!comboHighlight) setInfo(vbox.free[group][index]); - if (shopSelect.length && shopSelect.some(vs => vs[0] === group && vs[1] === index)) { - return setVboxSelected({ shopSelect: shopSelect.filter(vs => !(vs[0] === group && vs[1] === index)), stashSelect }); - } - - if (!shopSelect.length && !stashSelect.length) return setVboxSelected({ shopSelect: [[group, index]], stashSelect }); - if (comboHighlight !== 'combo-border') { - return setVboxSelected({ shopSelect: [[group, index]], stashSelect: [] }); - } - return setVboxSelected({ shopSelect: [...shopSelect, [group, index]], stashSelect }); - } - - - const classes = `${v.toLowerCase()} ${selected ? 'highlight' : ''} ${comboHighlight}`; - - const vboxObject = shapes[v] ? shapes[v]() : v; - const disabled = vbox.bits <= group; + function storeHdr() { return ( - - ); - } - - - function vboxElement() { - return ( -
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))} -
+
+

e.target.scrollIntoView(true)} + onMouseOver={e => vboxHover(e, 'store')}> STORE +

+

vboxHover(e, 'bits')}> + {vbox.bits}b +

); } - // - // INVENTORY - // - function reclaimClick(e) { - e.stopPropagation(); - return setReclaiming(!reclaiming); - } - - function inventoryBtn(v, i) { - const inventoryHighlight = vboxSelecting || itemUnequip.length; - - if (!v && v !== 0) { - const emptyInvClick = () => { - if (vboxSelecting) return vboxBuySelected(); - return false; - }; - return ; - } - - const comboHighlight = vboxHighlight && vboxHighlight.includes(v) ? 'combo-border' : ''; - - function onClick(type) { - if (reclaiming) return sendVboxReclaim(i); - - const combinerContainsIndex = stashSelect.indexOf(i) > -1; - // removing - if (combinerContainsIndex) { - if (type === 'click') { - return combinerChange(without(stashSelect, i)); - } - return true; - } - - if (!comboHighlight) { - setInfo(vbox.bound[i]); - return setVboxSelected({ shopSelect: [], stashSelect: [i] }); - } - - stashSelect.push(i); - // if (stashSelect.length === 3) setInfo(comboItem.item); - return combinerChange(stashSelect); - } - - const highlighted = stashSelect.indexOf(i) > -1; - const border = buttons[removeTier(v)] ? buttons[removeTier(v)]() : ''; - const classes = `${highlighted ? 'highlight' : border} ${comboHighlight}`; - - const invObject = shapes[v] ? shapes[v]() : v; - - return ( - - ); - } - - function combinerBtn() { - let text = ''; - let mouseEvent = false; - const combineLength = stashSelect.length + shopSelect.length; - if (vboxHighlight && vboxHighlight.length === 0) { - // The selected items can't be combined with additional items therefore valid combo - const stashItems = stashSelect.map(j => vbox.bound[j]); - const shopItems = shopSelect.map(j => vbox.free[j[0]][j[1]]); - const selectedItems = stashItems.concat(shopItems); - const combinerCount = countBy(selectedItems, co => co); - - const comboItemObj = itemInfo.combos.find(combo => selectedItems.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; - })); - let comboItem = comboItemObj ? comboItemObj.item : 'refine'; - setInfo(comboItem); - comboItem = comboItem.replace('Plus', '+'); - let bits = 0; - shopSelect.forEach(item => bits += item[0] + 1); - text = bits - ? `Buy ${comboItem} - ${bits}b` - : `Combine - ${comboItem}`; - if (vbox.bits >= bits) mouseEvent = sendVboxCombine; - } else if (stashSelect.length === 0 && shopSelect.length === 1) { - const item = shopSelect[0]; - text = `Buy ${vbox.free[item[0]][item[1]]} ${item[0] + 1}b`; - mouseEvent = vboxBuySelected; - } else { - for (let i = 0; i < 3; i++) { - if (combineLength > i) { - text += 'â–  '; - } else { - text += 'â–« '; - } - } - } - return ( + function stashHdr() { + const refund = storeSelect.length === 0 && stashSelect.length === 1 + ? itemInfo.items.find(i => i.item === vbox.stash[stashSelect[0]]).cost + : 0; + const tutorialDisabled = tutorial && tutorial < 8 + && instance.time_control === 'Practice' && instance.rounds.length === 1; + const refundBtn = ( ); - } - - function inventoryElement() { - function inventoryClick(e) { - e.stopPropagation(); - if (itemUnequip.length) return sendItemUnequip(itemUnequip); - return true; - } return ( -
e.stopPropagation()} - onDragOver={ev => ev.preventDefault()} - onDrop={inventoryClick} - > -
-

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

- -
- -
- {range(0, 9).map(i => inventoryBtn(vbox.bound[i], i))} -
- {combinerBtn()} +
+

e.target.scrollIntoView(true)} + onMouseOver={e => vboxHover(e, 'stash')}> STASH +

+ {refundBtn}
); } - // // EVERYTHING - // - function hoverInfo(e, newInfo) { - if (shopSelect.find(c => c[0])) return true; - if (stashSelect.length !== 0) { - const base = stashSelect.find(c => !['Red', 'Blue', 'Green'].includes(vbox.bound[c])); - if (base || base === 0) return true; - } - return setInfo(newInfo); - } - - const classes = 'vbox'; return ( -
- {vboxElement()} -
⮞
- {inventoryElement()} +
+ {storeHdr()} + {stashHdr()} + + +
+ + +
+
); } diff --git a/client/src/components/vbox.info.jsx b/client/src/components/vbox.info.jsx new file mode 100644 index 00000000..d2eddbec --- /dev/null +++ b/client/src/components/vbox.info.jsx @@ -0,0 +1,78 @@ +const preact = require('preact'); +const { connect } = require('preact-redux'); + +const { tutorialStage } = require('../tutorial.utils'); +const { genItemInfo } = require('./vbox.utils'); + +const addState = connect( + ({ info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview }) => ({ + info, player, tutorial, vboxInfo, itemInfo, instance, comboPreview, + })); + + +class Info extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.clearTutorial !== this.props.clearTutorial) return true; + if (newProps.info !== this.props.info) return true; + if (newProps.player !== this.props.player) return true; + if (newProps.tutorial !== this.props.tutorial) return true; + if (newProps.vboxInfo !== this.props.vboxInfo) return true; + if (newProps.itemInfo !== this.props.itemInfo) return true; + if (newProps.instance !== this.props.instance) return true; + if (newProps.comboPreview !== this.props.comboPreview) return true; + return false; + } + + render(props) { + const { + // passed props + clearTutorial, + // connect state props + info, + player, + tutorial, + vboxInfo, + itemInfo, + instance, + comboPreview, + } = props; + + // dispaly priority + // tutorial -> comboPreview -> vboxInfo -> info + if (tutorial) { + const tutorialStageInfo = tutorialStage(tutorial, clearTutorial, instance); + if (tutorialStageInfo) return tutorialStageInfo; + } + if (comboPreview) return genItemInfo(comboPreview, itemInfo, player); + if (vboxInfo) return genItemInfo(vboxInfo, itemInfo, player); + + if (!info) return false; + if (info.includes('constructName')) { + return ( +
+

{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. +

+
+ ); + } + + return genItemInfo(info, itemInfo, player, info); + } +} + +module.exports = addState(Info); diff --git a/client/src/components/info.thresholds.jsx b/client/src/components/vbox.info.thresholds.jsx similarity index 95% rename from client/src/components/info.thresholds.jsx rename to client/src/components/vbox.info.thresholds.jsx index 7136a855..9f259f0e 100644 --- a/client/src/components/info.thresholds.jsx +++ b/client/src/components/vbox.info.thresholds.jsx @@ -3,6 +3,7 @@ const range = require('lodash/range'); const shapes = require('./shapes'); function specThresholds(player, fullInfo, info) { + if (!info) return false; let red = 0; let blue = 0; let green = 0; @@ -87,9 +88,12 @@ function specThresholds(player, fullInfo, info) { ); }); return ( -
- {thresholds} +
+
+ {thresholds} +
+ ); } diff --git a/client/src/components/vbox.stash.jsx b/client/src/components/vbox.stash.jsx new file mode 100644 index 00000000..cec94686 --- /dev/null +++ b/client/src/components/vbox.stash.jsx @@ -0,0 +1,132 @@ +const preact = require('preact'); +const { connect } = require('preact-redux'); + +const range = require('lodash/range'); +const without = require('lodash/without'); + +const shapes = require('./shapes'); +const buttons = require('./buttons'); +const { removeTier } = require('../utils'); + +const addState = connect( + ({ itemUnequip, vboxHighlight, vboxSelected }) => ({ itemUnequip, vboxHighlight, vboxSelected })); + +class stashElement extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.sendItemUnequip !== this.props.sendItemUnequip) return true; + if (newProps.setInfo !== this.props.setInfo) return true; + if (newProps.setVboxSelected !== this.props.setVboxSelected) return true; + if (newProps.vbox !== this.props.vbox) return true; + if (newProps.vboxBuySelected !== this.props.vboxBuySelected) return true; + if (newProps.vboxHover !== this.props.vboxHover) return true; + + if (newProps.itemUnequip !== this.props.itemUnequip) return true; + if (newProps.vboxHighlight !== this.props.vboxHighlight) return true; + if (newProps.vboxSelected !== this.props.vboxSelected) return true; + return false; + } + + render(props) { + const { + // passed props + sendItemUnequip, + setInfo, + setVboxSelected, + vbox, + vboxBuySelected, + vboxHover, + // connect state props + itemUnequip, + vboxHighlight, + vboxSelected, + } = props; + + const { storeSelect, stashSelect } = vboxSelected; + + const vboxSelecting = storeSelect.length === 1 && stashSelect.length === 0; + + function stashClick(e) { + e.stopPropagation(); + if (itemUnequip.length) return sendItemUnequip(itemUnequip); + if (vboxSelecting) return vboxBuySelected(); + return true; + } + + function stashBtn(v, i) { + const stashHighlight = vboxSelecting || itemUnequip.length; + + if (!v && v !== 0) { + const emptyInvClick = () => { + if (vboxSelecting) return vboxBuySelected(); + return false; + }; + return ; + } + + const notValidCombo = vboxHighlight && !vboxHighlight.includes(v); + + function onClick(type, e) { + e.stopPropagation(); + const combinerContainsIndex = stashSelect.indexOf(i) > -1; + // removing + if (combinerContainsIndex) { + if (type === 'click') { + return setVboxSelected({ storeSelect, stashSelect: without(stashSelect, i) }); + } + return true; + } + + if (notValidCombo) { + setInfo(vbox.stash[i]); + return setVboxSelected({ storeSelect: [], stashSelect: [i] }); + } + + return setVboxSelected({ storeSelect, stashSelect: [...stashSelect, i] }); + } + + const highlighted = stashSelect.indexOf(i) > -1; + const border = buttons[removeTier(v)] ? buttons[removeTier(v)]() : ''; + const classes = highlighted + ? 'highlight' + : `${border} ${notValidCombo ? 'fade' : ''}`; + + const invObject = shapes[v] ? shapes[v]() : v; + + return ( + + ); + } + + return ( +
e.stopPropagation()} + onDragOver={ev => ev.preventDefault()} + onDrop={stashClick} + > + {range(0, 6).map(i => stashBtn(vbox.stash[i], i.toString()))} +
+ ); + } +} + +module.exports = addState(stashElement); diff --git a/client/src/components/vbox.store.jsx b/client/src/components/vbox.store.jsx new file mode 100644 index 00000000..f4d1878c --- /dev/null +++ b/client/src/components/vbox.store.jsx @@ -0,0 +1,95 @@ +const preact = require('preact'); +const { connect } = require('preact-redux'); +const range = require('lodash/range'); + +const shapes = require('./shapes'); + +const addState = connect(({ vboxHighlight }) => ({ vboxHighlight })); + +class storeElement extends preact.Component { + shouldComponentUpdate(newProps) { + if (newProps.clearVboxSelected !== this.props.clearVboxSelected) return true; + if (newProps.setVboxSelected !== this.props.setVboxSelected) return true; + if (newProps.vbox !== this.props.vbox) return true; + if (newProps.vboxHighlight !== this.props.vboxHighlight) return true; + if (newProps.vboxHover !== this.props.vboxHover) return true; + if (newProps.vboxSelected !== this.props.vboxSelected) return true; + return false; + } + + render(props) { + const { + // passed props + clearVboxSelected, + setVboxSelected, + vbox, + vboxHover, + vboxSelected, + // connect state props + vboxHighlight, + } = props; + + const { storeSelect, stashSelect } = vboxSelected; + + function availableBtn(v, group, index) { + if (!v) return ; + const selected = storeSelect.length && storeSelect.some(vs => vs[0] === group && vs[1] === index); + + const notValidCombo = vboxHighlight && !vboxHighlight.includes(v); + + function onClick(e) { + e.stopPropagation(); + if (storeSelect.length && storeSelect.some(vs => vs[0] === group && vs[1] === index)) { + return setVboxSelected( + { storeSelect: storeSelect.filter(vs => !(vs[0] === group && vs[1] === index)), stashSelect } + ); + } + + if (!storeSelect.length && !stashSelect.length) { + return setVboxSelected({ storeSelect: [[group, index]], stashSelect }); + } + if (notValidCombo) { + return setVboxSelected({ storeSelect: [[group, index]], stashSelect: [] }); + } + return setVboxSelected({ storeSelect: [...storeSelect, [group, index]], stashSelect }); + } + + + const classes = selected + ? `${v.toLowerCase()} highlight` + : `${v.toLowerCase()} ${notValidCombo ? 'fade' : ''}`; + + const vboxObject = shapes[v] ? shapes[v]() : v; + const disabled = vbox.bits <= group; + return ( + + ); + } + + return ( +
e.stopPropagation()}> +
+ {range(0, 6).map(i => availableBtn(vbox.store['Colours'][i], 'Colours', i.toString()))} +
+
+ {range(0, 3).map(i => availableBtn(vbox.store['Skills'][i], 'Skills', i.toString()))} + {range(0, 3).map(i => availableBtn(vbox.store['Specs'][i], 'Specs', i.toString()))} +
+
+ ); + } +} + +module.exports = addState(storeElement); diff --git a/client/src/components/vbox.utils.jsx b/client/src/components/vbox.utils.jsx new file mode 100644 index 00000000..4a4a90d1 --- /dev/null +++ b/client/src/components/vbox.utils.jsx @@ -0,0 +1,133 @@ +const preact = require('preact'); +const countBy = require('lodash/countBy'); +const forEach = require('lodash/forEach'); +const reactStringReplace = require('react-string-replace'); + +const actions = require('../actions'); +const specThresholds = require('./vbox.info.thresholds'); +const { INFO } = require('./../constants'); +const { removeTier } = require('../utils'); +const shapes = require('./shapes'); + +function setVboxState(dispatch, vboxSelected, state) { + const { + itemInfo, + itemUnequip, + vbox, + } = state; + const { storeSelect, stashSelect } = vboxSelected; + + // default returns + let vboxCombiner = false; + let vboxHighlight = false; + + if (storeSelect.length || stashSelect.length) { + vboxHighlight = []; + const stashItems = stashSelect.map(j => vbox.stash[j]); + const shopItems = storeSelect.map(j => vbox.store[j[0]][j[1]]); + + const selectedItems = stashItems.concat(shopItems); + const itemCount = countBy(selectedItems, co => co); + + itemInfo.combos.forEach(combo => { + const comboCount = countBy(combo.components, co => co); + const buyCount = countBy(combo.components, co => co); + const valid = selectedItems.every(c => { + if (!combo.components.includes(c)) return false; + if (itemCount[c] > comboCount[c]) return false; + buyCount[c] -= 1; + return true; + }); + if (valid) { + const fullCombo = combo.components.every(c => itemCount[c] === comboCount[c]); + if (fullCombo) vboxCombiner = combo.item; + + forEach(buyCount, (value, key) => { + if (value > 0 && !vboxHighlight.includes(key)) { + vboxHighlight.push(key); + } + }); + } + }); + } + + + const vboxInfo = () => { + if (vboxCombiner) return vboxCombiner; + if (itemUnequip.length) return itemUnequip[1]; + const stashBase = stashSelect.find(i => !(['Red', 'Blue', 'Green'].includes(vbox.stash[i]))); + if (stashBase > -1) return vbox.stash[stashBase]; + const storeBase = storeSelect.find(j => !(['Red', 'Blue', 'Green'].includes(vbox.store[j[0]][j[1]]))); + if (storeBase) return vbox.store[storeBase[0]][storeBase[1]]; + if (stashSelect.length > 0) return vbox.stash[stashSelect[0]]; + if (storeSelect.length > 0) return vbox.store[storeSelect[0][0]][storeSelect[0][1]]; + return false; + }; + + dispatch(actions.setVboxInfo(vboxInfo())); + dispatch(actions.setVboxCombiner(vboxCombiner)); + dispatch(actions.setVboxHighlight(vboxHighlight)); +} + +function genItemInfo(item, itemInfo, player) { + const fullInfo = itemInfo.items.find(i => i.item === item) || INFO[item]; + const isSkill = fullInfo.skill; + const isSpec = fullInfo.spec; + 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 = fullInfo.item; + while (infoName.includes('Plus')) infoName = infoName.replace('Plus', '+'); + + const itemSource = itemInfo.combos.filter(c => c.item === removeTier(fullInfo.item)); + + let itemSourceInfo = itemSource.length && !isSpec + ? `${itemSource[0].components[0]} ${itemSource[0].components[1]} ${itemSource[0].components[2]}` + : false; + + let header = null; + if (!itemSource.length) header = isSkill ?

SKILL

:

SPEC

; + 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, item) : null; + + return ( +
+

{infoName}

+ {header} + {itemSourceInfo} + {cooldown} + {itemDescription()} + {speed} + {thresholds} +
+ ); + } + return ( +
+

{fullInfo.item}

+ {itemDescription()} +
+ ); +} + +function cost(group) { + if (group === 'Colours') return 1; + if (group === 'Skills') return 2; + if (group === 'Specs') return 3; +}; + +module.exports = { setVboxState, genItemInfo, cost }; diff --git a/client/src/constants.jsx b/client/src/constants.jsx index 5a7a24a3..c6b6dfaf 100644 --- a/client/src/constants.jsx +++ b/client/src/constants.jsx @@ -3,7 +3,7 @@ const preact = require('preact'); const SOURCE_DURATION_MS = 1000; // Time for SOURCE ONLY const TARGET_DELAY_MS = 500; // Used for Source + Target const TARGET_DURATION_MS = 1500; // Time for TARGET ONLY -const POST_SKILL_DURATION_MS = 1000; // Time for all POST +const POST_SKILL_DURATION_MS = 1300; // Time for all POST const SOURCE_AND_TARGET_TOTAL_DURATION = TARGET_DELAY_MS + TARGET_DURATION_MS; // SOURCE + TARGET time module.exports = { @@ -24,22 +24,18 @@ 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.

, + store: { + item: 'STORE', + description:

Contains items that are available to buy.
+ The store is refilled every round.
Click REFILL to purchase a refill for 2 bits.

, }, - inventory: { - item: 'INVENTORY', + stash: { + item: 'STASH', description:

Holds ITEMS
ITEMS carry over each round.

, }, bits: { item: 'BITS', - description:

The VBOX currency.
- Colours - 1b
- Skills - 2b
- Specs - 3b
- At the beginning of each round you receive 30 bits.

, + description:

Currency to buy items.
At the beginning of each round you receive 30 bits.

, }, ready: { item: 'READY', @@ -49,10 +45,9 @@ module.exports = { item: 'READY', description: 'Ready for the game to begin. When all players are ready the first VBOX PHASE begins.', }, - reclaim: { - item: 'RECLAIM', - description:

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

, + Refund: { + item: 'Refund', + description: 'Refund the listed cost of a single selected item from the stash.', }, refill: { item: 'REFILL', @@ -60,7 +55,7 @@ module.exports = { }, constructSkills: { item: 'SKILLS', - 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.', + description: 'Skills are used by constructs in the game phase.\nBase skills can be bought from the VBOX.\nEquip skills from the stash. Double-click to unequip.', }, constructSpecs: { item: 'SPECS', diff --git a/client/src/events.jsx b/client/src/events.jsx index 02c6af1e..f1b7e158 100644 --- a/client/src/events.jsx +++ b/client/src/events.jsx @@ -26,6 +26,12 @@ function registerEvents(store) { return errorToast(msg); } + function clearTutorial() { + store.dispatch(actions.setTutorial(null)); + localStorage.setItem('tutorial-complete', true); + } + + function setPing(ping) { store.dispatch(actions.setPing(ping)); } @@ -79,7 +85,7 @@ function registerEvents(store) { const newRes = game.resolved.slice(currentGame.resolved.length); return eachSeries(newRes, (r, cb) => { if (!r.event) return cb(); - const timeout = animations.getTime(r.stages); + let timeout = animations.getTime(r.stages); const anims = animations.getObjects(r, game, account); const text = animations.getText(r); store.dispatch(actions.setAnimFocus(animations.getFocusTargets(r, game))); @@ -87,10 +93,12 @@ function registerEvents(store) { if (r.stages.includes('START_SKILL') && anims.animSource) { store.dispatch(actions.setAnimSource(anims.animSource)); + store.dispatch(actions.setAnimText(null)); } if (r.stages.includes('END_SKILL') && anims.animTarget) { store.dispatch(actions.setAnimTarget(anims.animTarget)); + store.dispatch(actions.setAnimText(null)); if (animations.isCbAnim(anims.animSkill)) store.dispatch(actions.setAnimCb(cb)); } @@ -101,8 +109,9 @@ function registerEvents(store) { } else { setTimeout( () => store.dispatch(actions.setAnimText(text)), - timeout - TIMES.POST_SKILL_DURATION_MS + timeout - TIMES.POST_SKILL_DURATION_MS - 700 ); + timeout -= 700; } } @@ -110,7 +119,7 @@ function registerEvents(store) { store.dispatch(actions.setAnimSkill(null)); store.dispatch(actions.setAnimSource(null)); store.dispatch(actions.setAnimTarget(null)); - store.dispatch(actions.setAnimText(null)); + // store.dispatch(actions.setAnimText(null)); store.dispatch(actions.setAnimFocus([])); if (r.stages.includes('END_SKILL') && animations.isCbAnim(anims.animSkill)) return true; return cb(); @@ -179,12 +188,13 @@ function registerEvents(store) { } function clearInstance() { - store.dispatch(actions.setReclaiming(false)); store.dispatch(actions.setActiveSkill(null)); store.dispatch(actions.setInfo(null)); store.dispatch(actions.setItemUnequip([])); - store.dispatch(actions.setVboxHighlight([])); - store.dispatch(actions.setVboxSelected({ shopSelect: [], stashSelect: [] })); + store.dispatch(actions.setVboxCombiner(null)); + store.dispatch(actions.setVboxHighlight(null)); + store.dispatch(actions.setVboxInfo(null)); + store.dispatch(actions.setVboxSelected({ storeSelect: [], stashSelect: [] })); } function setAccountInstances(v) { @@ -294,6 +304,9 @@ function registerEvents(store) { startDemo(); } + // store.subscribe(setInfo); + // store.on('SET_INFO', setInfo); + // events.on('SET_PLAYER', setInstance); // events.on('SEND_SKILL', function skillActive(gameId, constructId, targetConstructId, skill) { @@ -342,6 +355,7 @@ function registerEvents(store) { clearInfo, clearInstance, clearMtxActive, + clearTutorial, setAccount, setAccountInstances, setActiveItem, diff --git a/client/src/keyboard.jsx b/client/src/keyboard.jsx index bb525d47..d9cc79ca 100644 --- a/client/src/keyboard.jsx +++ b/client/src/keyboard.jsx @@ -6,12 +6,13 @@ function setupKeys(store) { key.unbind('esc'); key('esc', () => document.activeElement.blur()); - key('esc', () => store.dispatch(actions.setReclaiming(false))); key('esc', () => store.dispatch(actions.setActiveSkill(null))); key('esc', () => store.dispatch(actions.setInfo(null))); key('esc', () => store.dispatch(actions.setItemUnequip([]))); - key('esc', () => store.dispatch(actions.setVboxHighlight([]))); - key('esc', () => store.dispatch(actions.setVboxSelected({ shopSelect: [], stashSelect: [] }))); + key('esc', () => store.dispatch(actions.setVboxSelected({ storeSelect: [], stashSelect: [] }))); + key('esc', () => store.dispatch(actions.setVboxHighlight(null))); + key('esc', () => store.dispatch(actions.setVboxCombiner(null))); + key('esc', () => store.dispatch(actions.setVboxInfo(null))); key('esc', () => store.dispatch(actions.setMtxActive(null))); } diff --git a/client/src/reducers.jsx b/client/src/reducers.jsx index 1bda4dc7..ef43b31a 100644 --- a/client/src/reducers.jsx +++ b/client/src/reducers.jsx @@ -34,7 +34,6 @@ module.exports = { gameEffectInfo: createReducer(null, 'SET_GAME_EFFECT_INFO'), email: createReducer(null, 'SET_EMAIL'), invite: createReducer(null, 'SET_INVITE'), - info: createReducer(null, 'SET_INFO'), instance: createReducer(null, 'SET_INSTANCE'), instanceChat: createReducer(null, 'SET_INSTANCE_CHAT'), instances: createReducer([], 'SET_INSTANCES'), @@ -44,10 +43,12 @@ module.exports = { nav: createReducer(null, 'SET_NAV'), ping: createReducer(null, 'SET_PING'), player: createReducer(null, 'SET_PLAYER'), - reclaiming: createReducer(false, 'SET_RECLAIMING'), shop: createReducer(false, 'SET_SHOP'), pvp: createReducer(null, 'SET_PVP'), + info: createReducer(null, 'SET_INFO'), + comboPreview: createReducer(null, 'SET_COMBO_PREVIEW'), + subscription: createReducer(null, 'SET_SUBSCRIPTION'), team: createReducer([], 'SET_TEAM'), @@ -57,7 +58,10 @@ module.exports = { tutorial: createReducer(1, 'SET_TUTORIAL'), tutorialGame: createReducer(1, 'SET_TUTORIAL_GAME'), - vboxSelected: createReducer({ shopSelect: [], stashSelect: [] }, 'SET_VBOX_SELECTED'), + vboxSelected: createReducer({ storeSelect: [], stashSelect: [] }, 'SET_VBOX_SELECTED'), + vboxCombiner: createReducer(null, 'SET_VBOX_COMBINER'), + vboxHighlight: createReducer(null, 'SET_VBOX_HIGHLIGHT'), + vboxInfo: createReducer(null, 'SET_VBOX_INFO'), ws: createReducer(null, 'SET_WS'), }; diff --git a/client/src/socket.jsx b/client/src/socket.jsx index a9ec07ba..a99fe130 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -2,6 +2,7 @@ const toast = require('izitoast'); const cbor = require('borc'); const throttle = require('lodash/throttle'); +const groupBy = require('lodash/groupBy'); const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`; @@ -77,13 +78,13 @@ function createSocket(events) { send(['InstanceChat', { instance_id: instanceId, index }]); } - function sendVboxAccept(instanceId, group, index) { - send(['VboxAccept', { instance_id: instanceId, group, index }]); + function sendVboxBuy(instanceId, group, index) { + send(['VboxBuy', { instance_id: instanceId, group, index }]); events.clearInstance(); } - function sendVboxAcceptEquip(instanceId, group, index, constructId) { - send(['VboxAcceptEquip', { instance_id: instanceId, group, index, construct_id: constructId }]); + function sendVboxBuyEquip(instanceId, group, index, constructId) { + send(['VboxBuy', { instance_id: instanceId, group, index, construct_id: constructId }]); events.clearInstance(); } @@ -102,18 +103,20 @@ function createSocket(events) { events.clearInstance(); } - function sendVboxDiscard(instanceId) { - send(['VboxDiscard', { instance_id: instanceId }]); + function sendVboxRefill(instanceId) { + send(['VboxRefill', { instance_id: instanceId }]); events.clearInstance(); } function sendVboxCombine(instanceId, invIndicies, vboxIndicies) { - send(['VboxCombine', { instance_id: instanceId, inv_indices: invIndicies, vbox_indices: vboxIndicies }]); + const formatted = {}; + vboxIndicies.forEach(p => formatted[p[0]] ? formatted[p[0]].push(p[1]) : formatted[p[0]] = [p[1]]); + send(['VboxCombine', { instance_id: instanceId, inv_indices: invIndicies, vbox_indices: formatted }]); events.clearInstance(); } - function sendVboxReclaim(instanceId, index) { - send(['VboxReclaim', { instance_id: instanceId, index }]); + function sendVboxRefund(instanceId, index) { + send(['VboxRefund', { instance_id: instanceId, index }]); events.clearInstance(); } @@ -410,12 +413,12 @@ function createSocket(events) { sendInstanceChat, sendInstanceLeave, - sendVboxAccept, - sendVboxAcceptEquip, + sendVboxBuy, + sendVboxBuyEquip, sendVboxApply, - sendVboxReclaim, + sendVboxRefund, sendVboxCombine, - sendVboxDiscard, + sendVboxRefill, sendVboxUnequip, sendVboxUnequipApply, diff --git a/client/src/tutorial.utils.jsx b/client/src/tutorial.utils.jsx index a06cede2..740aa867 100644 --- a/client/src/tutorial.utils.jsx +++ b/client/src/tutorial.utils.jsx @@ -24,21 +24,26 @@ function tutorialVbox(player, store, tutorial) { if (vbox.bits < 29) { stage += 1; } else { - vbox.free[0] = vbox.free[0].slice(0, 2); - vbox.free[1] = []; - vbox.free[2] = []; - vbox.bound.fill(null, 0, 3); + for (let i = 2; i < 6; i += 1) { + delete vbox.store.Colours[i]; + } + vbox.store.Skills = {}; + vbox.store.Specs = {}; + delete vbox.stash[0]; + delete vbox.stash[1]; + delete vbox.stash[2]; } } if (stage === 2) { - if (!(vbox.bound.slice(0, 3).every(i => i === 'Attack') && vbox.bound.length >= 3)) { + if (!(vbox.stash[0] === 'Attack' && vbox.stash[1] === 'Attack' && vbox.stash[2] === 'Attack')) { stage += 1; } else { - vbox.free[0] = vbox.free[0].slice(0, 2); - vbox.free[1] = []; - vbox.free[2] = []; - vbox.bound.fill(null, 1, 3); + vbox.store.Colours = {}; + vbox.store.Skills = {}; + vbox.store.Specs = {}; + delete vbox.stash[0]; + delete vbox.stash[1]; } } @@ -46,21 +51,24 @@ function tutorialVbox(player, store, tutorial) { 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); + vbox.store.Colours = {}; + vbox.store.Skills = {}; + vbox.store.Specs = {}; + delete vbox.stash[0]; + delete vbox.stash[1]; } } if (stage === 4) { - if (!vbox.free[2][0] || vbox.bits < 24) { + if (!vbox.store.Specs[0] || vbox.bits < 24) { stage += 1; } else { - vbox.free[0] = []; - vbox.free[1] = []; - vbox.free[2] = vbox.free[2].slice(0, 1); - vbox.bound.fill(null, 0, 2); + vbox.store.Colours = {}; + vbox.store.Skills = {}; + delete vbox.store.Specs[1]; + delete vbox.store.Specs[2]; + delete vbox.stash[0]; + delete vbox.stash[1]; } } @@ -68,10 +76,11 @@ function tutorialVbox(player, store, tutorial) { 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); + vbox.store.Colours = {}; + vbox.store.Skills = {}; + vbox.store.Specs = {}; + delete vbox.stash[0]; + delete vbox.stash[1]; } } @@ -79,9 +88,9 @@ function tutorialVbox(player, store, tutorial) { if (player.constructs.every(c => c.skills.length !== 0)) { stage += 1; } else { - vbox.free[0] = []; - vbox.free[1] = []; - vbox.free[2] = []; + vbox.store.Colours = {}; + vbox.store.Skills = {}; + vbox.store.Specs = {}; } } @@ -89,22 +98,18 @@ function tutorialVbox(player, store, tutorial) { if (vbox.bits < 25) { stage += 1; } else { - vbox.free[0] = []; - vbox.free[1] = []; - vbox.free[2] = []; + vbox.store.Colours = {}; + vbox.store.Skills = {}; + vbox.store.Specs = {}; } } store.dispatch(actions.setTutorial(stage)); } -function tutorialStage(tutorial, ws, clearTutorial, instance) { +function tutorialStage(tutorial, 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 exit = () => clearTutorial(); const tutorialText = () => { if (tutorial === 1) { @@ -113,7 +118,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {

Tutorial

Welcome to the vbox phase tutorial.

Colours are used to create powerful combinations with base items.

-

Buy the two colours from the vbox to continue.

+

Buy the two colours from the store to continue.

); } @@ -122,11 +127,8 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) { 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. -

+

You start the game with the base Attack skill item.

+

Highlight all three items then click combine.

); } @@ -138,8 +140,8 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {

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.

+

Click your new skill from the stash.
+ Once selected click the flashing SKILL slot to equip the skill.

); } @@ -150,7 +152,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {

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 to continue.

+

Buy the specialisation item from the store to continue.

); } @@ -161,8 +163,8 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {

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.

+

Click the specialisation item in the stash.
+ Once selected click the flashing SPEC slot to equip the specialisation.

); } @@ -185,13 +187,11 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) { 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. +

Each round you start with 30 bits and a store full of different skills, specs and colours.

+

Bits are your currency for buying items.
+ You can refill the store by pressing the refill button for 2b.

-

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

+

Press the REFILL button to buy new items.

); } @@ -206,9 +206,7 @@ function tutorialStage(tutorial, ws, clearTutorial, instance) {

Tutorial

That completes the VBOX Tutorial.

Press READY to progress to the GAME PHASE
- or continue creating new items to strengthen your constructs further

-

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.

+ You can continue creating new items to upgrade your constructs further.

); } diff --git a/client/src/utils.jsx b/client/src/utils.jsx index a14c684e..550c69e6 100644 --- a/client/src/utils.jsx +++ b/client/src/utils.jsx @@ -197,7 +197,7 @@ function postData(url = '/', data = {}) { cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'include', // include, same-origin, *omit headers: { - Accept: 'application/json', + Buy: 'application/json', 'content-type': 'application/json', }, redirect: 'error', // manual, *follow, error diff --git a/ops/package.json b/ops/package.json index a3094609..63b2a82e 100644 --- a/ops/package.json +++ b/ops/package.json @@ -1,6 +1,6 @@ { "name": "mnml-ops", - "version": "1.9.1", + "version": "1.10.0", "description": "", "main": "index.js", "scripts": { diff --git a/server/Cargo.toml b/server/Cargo.toml index b666e932..fb7f73c9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml" -version = "1.9.1" +version = "1.10.0" authors = ["ntr "] [dependencies] diff --git a/server/src/construct.rs b/server/src/construct.rs index bce0d5c0..3c41ffd1 100644 --- a/server/src/construct.rs +++ b/server/src/construct.rs @@ -323,8 +323,6 @@ impl Construct { } pub fn apply_modifiers(&mut self, player_colours: &Colours) -> &mut Construct { - self.specs.sort_unstable(); - self.red_power.recalculate(&self.specs, player_colours); self.red_life.recalculate(&self.specs, player_colours); self.blue_power.recalculate(&self.specs, player_colours); @@ -600,11 +598,11 @@ impl Construct { self.reduce_green_life(red_remainder); let red_damage_amount = red_current_green_life - self.green_life(); - events.push(Event::Damage { + events.push(Event::Damage { skill, amount: red_damage_amount, mitigation: red_mitigation, - colour: Colour::Red + colour: Colour::Red }); } @@ -629,11 +627,11 @@ impl Construct { self.reduce_green_life(blue_remainder); let blue_damage_amount = blue_current_green_life - self.green_life(); - events.push(Event::Damage { + events.push(Event::Damage { skill, amount: blue_damage_amount, mitigation: blue_mitigation, - colour: Colour::Blue + colour: Colour::Blue }); } } diff --git a/server/src/instance.rs b/server/src/instance.rs index c5ec9c04..65c17b65 100644 --- a/server/src/instance.rs +++ b/server/src/instance.rs @@ -15,7 +15,7 @@ use chrono::prelude::*; use chrono::Duration; use account::Account; -use account; +use vbox; use player::{Player, Score, player_create}; @@ -466,38 +466,38 @@ impl Instance { Ok(()) } - pub fn vbox_discard(mut self, account: Uuid) -> Result { + pub fn vbox_refill(mut self, account: Uuid) -> Result { self.vbox_action_allowed(account)?; self.account_player(account)? - .vbox_discard()?; + .vbox_refill()?; Ok(self) } - pub fn vbox_accept(mut self, account: Uuid, group: usize, index: usize, construct_id: Option) -> Result { + pub fn vbox_buy(mut self, account: Uuid, group: vbox::ItemType, index: String, construct_id: Option) -> Result { self.vbox_action_allowed(account)?; self.account_player(account)? - .vbox_accept(group, index, construct_id)?; + .vbox_buy(group, index, construct_id)?; Ok(self) } - pub fn vbox_combine(mut self, account: Uuid, inv_indices: Vec, vbox_indices: Vec>) -> Result { + pub fn vbox_combine(mut self, account: Uuid, inv_indices: Vec, vbox_indices: vbox::VboxIndices) -> Result { self.vbox_action_allowed(account)?; self.account_player(account)? .vbox_combine(inv_indices, vbox_indices)?; Ok(self) } - pub fn vbox_reclaim(mut self, account: Uuid, index: usize) -> Result { + pub fn vbox_refund(mut self, account: Uuid, index: String) -> Result { self.vbox_action_allowed(account)?; self.account_player(account)? - .vbox_reclaim(index)?; + .vbox_refund(index)?; Ok(self) } - pub fn vbox_apply(mut self, account: Uuid, index: usize, construct_id: Uuid) -> Result { + pub fn vbox_apply(mut self, account: Uuid, index: String, construct_id: Uuid) -> Result { self.vbox_action_allowed(account)?; self.account_player(account)? - .vbox_apply(index, construct_id)?; + .vbox_equip(index, construct_id)?; Ok(self) } @@ -834,19 +834,17 @@ mod tests { fn instance_pve_test() { let mut instance = Instance::new(); - let bot_player = bot_player(); - let bot = bot_player.id; - instance.add_player(bot_player).unwrap(); + let bot = bot_player(); + let bot_one = bot.id; + instance.add_player(bot).unwrap(); - let player_account = Uuid::new_v4(); - let constructs = instance_mobs(player_account); - let player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); - - instance.add_player(player).expect("could not add player"); + let bot = bot_player(); + let bot_two = bot.id; + instance.add_player(bot).unwrap(); assert_eq!(instance.phase, InstancePhase::Lobby); - instance.player_ready(player_account).unwrap(); - instance.player_ready(bot).unwrap(); + instance.player_ready(bot_one).unwrap(); + instance.player_ready(bot_two).unwrap(); assert_eq!(instance.phase, InstancePhase::Finished); } diff --git a/server/src/player.rs b/server/src/player.rs index 6a0a5abd..b0988a3a 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -1,3 +1,5 @@ +use std::collections::{HashMap}; + use uuid::Uuid; use rand::prelude::*; @@ -9,7 +11,7 @@ use failure::err_msg; use account; use account::Account; use construct::{Construct, Colours}; -use vbox::{Vbox}; +use vbox::{Vbox, ItemType, VboxIndices}; use item::{Item, ItemEffect}; use effect::{Effect}; @@ -155,125 +157,162 @@ impl Player { pub fn autobuy(&mut self) -> &mut Player { let mut rng = thread_rng(); - // first check if any constructs have no skills - // if there is one find an item in vbox that gives a skill - while let Some(c) = self.constructs.iter().position(|c| c.skills.len() == 0) { - if let Some(s) = self.vbox.bound.iter().position(|v| v.into_skill().is_some()) { - let construct_id = self.constructs[c].id; - self.vbox_apply(s, construct_id).expect("could not apply"); + // skill buying phase + while self.constructs.iter().any(|c| c.skills.len() < 3) { + // find the construct with the smallest number of skills + let construct_id = match self.constructs.iter().min_by_key(|c| c.skills.len()) { + None => panic!("no constructs in autobuy"), + Some(c) => c.id, + }; + + let i = self.vbox.stash.iter() + .find(|(_i, v)| v.into_skill().is_some()) + .map(|(i, _v)| i.clone()); + + // got a skill in stash + if let Some(i) = i { + // AAAAAAAAAAAAAAAAAAAA + // there's a bad bug here where if this apply fails + // the item in question will be silently dropped + let item = self.vbox.stash.remove(&i).unwrap(); + self.vbox_apply(item, construct_id).ok(); continue; } - info!("no skills available..."); - } + // need to buy one + else { - // now keep buying and applying items cause whynot - // inb4 montecarlo gan + // do we have any colours in store? + let colours = self.vbox.store[&ItemType::Colours].keys() + .cloned() + .collect::>(); - loop { - let (target_construct_i, target_construct_id) = match self.constructs.iter().any(|c| c.skills.len() < 3) { - true => { - let mut target_construct_i = 0; - for (j, c) in self.constructs.iter().enumerate() { - if c.skills.len() < self.constructs[target_construct_i].skills.len() { - target_construct_i = j; + // how about a base skill? + let base = match self.vbox.store[&ItemType::Skills].iter().next() { + Some(b) => Some(b.0.clone()), + None => None, + }; + + // if no: try to refill and start again + match colours.len() < 2 || base.is_none() { + true => match self.vbox_refill() { + Ok(_) => continue, + Err(_) => break, // give up + }, + false => { + let mut vbox_items = HashMap::new(); + vbox_items.insert(ItemType::Colours, colours); + vbox_items.insert(ItemType::Skills, vec![base.unwrap()]); + + match self.vbox_combine(vec![], Some(vbox_items)) { + Ok(_) => continue, + Err(_) => break, // give up } } - (target_construct_i, self.constructs[target_construct_i].id) - }, - false => { - let i = rng.gen_range(0, 3); - (i, self.constructs[i].id) - }, - }; - - let needs_skills = self.constructs[target_construct_i].skills.len() < 3; - let group_i = match needs_skills { - true => 1, - false => 2, - }; - - - let num_colours = self.vbox.bound - .iter() - .filter(|v| [Item::Red, Item::Green, Item::Blue].contains(v)) - .count(); - - if self.vbox.bound.len() < 3 || num_colours < 2 { - if (needs_skills && self.vbox.bits < 4) || self.vbox.bits < 5 { - // info!("insufficient balance"); - break; } - - // get 2 colours and something else - let free_colours = self.vbox.free[0].iter().fold(0, |count, item| { - match item.is_some() { - true => count + 1, - false => count - } - }); - if free_colours < 2 { - break; - } - self.bot_vbox_accept(0).expect("could't accept colour item"); - self.bot_vbox_accept(0).expect("could't accept colour item"); - self.bot_vbox_accept(group_i).expect("could't accept group item"); } + } - // info!("{:?}", self.vbox.bound); - - let skills = [Item::Attack, Item::Block, Item::Buff, Item::Debuff, Item::Stun]; - let combo_i = match group_i { - 1 => self.vbox.bound.iter().position(|v| skills.contains(v)).expect("no skill found"), - 2 => self.vbox.bound.iter().position(|v| v.into_spec().is_some()).expect("no spec found"), - _ => panic!("unknown group_i"), + // spec buying phase + while self.constructs.iter().any(|c| c.specs.len() < 3) { + // find the construct with the smallest number of skills + let construct_id = match self.constructs.iter().min_by_key(|c| c.specs.len()) { + None => panic!("no constructs in autobuy"), + Some(c) => c.id, }; - // first 2 colours can be whatever - self.vbox_combine(vec![0, 1, combo_i], vec![]).ok(); - let item_i = self.vbox.bound.len() - 1; - self.vbox_apply(item_i, target_construct_id).ok(); + let i = self.vbox.stash.iter() + .find(|(_i, v)| v.into_spec().is_some()) + .map(|(i, _v)| i.clone()); + + // got a skill in stash + if let Some(i) = i { + // AAAAAAAAAAAAAAAAAAAA + // there's a bad bug here where if this apply fails + // the item in question will be silently dropped + let item = self.vbox.stash.remove(&i).unwrap(); + self.vbox_apply(item, construct_id).ok(); + continue; + } + // need to buy one + else { + // do we have any colours in store? + let colours = self.vbox.store[&ItemType::Colours].keys() + .cloned() + .collect::>(); + + // how about a base spec? + let base = match self.vbox.store[&ItemType::Specs].iter().next() { + Some(b) => Some(b.0.clone()), + None => None, + }; + + // if no: try to refill and start again + match colours.len() < 2 || base.is_none() { + true => match self.vbox_refill() { + Ok(_) => continue, + Err(_) => break, // give up + }, + false => { + let mut vbox_items = HashMap::new(); + vbox_items.insert(ItemType::Colours, colours); + vbox_items.insert(ItemType::Specs, vec![base.unwrap()]); + + match self.vbox_combine(vec![], Some(vbox_items)) { + Ok(_) => continue, + Err(_) => break, // give up + } + } + } + } } + // upgrading phase + // NYI + return self; } - pub fn vbox_discard(&mut self) -> Result<&mut Player, Error> { + pub fn vbox_refill(&mut self) -> Result<&mut Player, Error> { self.vbox.balance_sub(DISCARD_COST)?; self.vbox.fill(); Ok(self) } - pub fn bot_vbox_accept(&mut self, group: usize) -> Result<&mut Player, Error> { - self.vbox.bot_accept(group)?; + pub fn bot_vbox_accept(&mut self, group: ItemType) -> Result<&mut Player, Error> { + let item = self.vbox.bot_buy(group)?; + self.vbox.stash_add(item, None)?; Ok(self) } - pub fn vbox_accept(&mut self, group: usize, index: usize, construct_id: Option) -> Result<&mut Player, Error> { - self.vbox.accept(group, index, construct_id)?; - if construct_id.is_some() { - let equip_index = self.vbox.bound.len() - 1; - self.vbox_apply(equip_index, construct_id.expect("no construct"))?; - } + pub fn vbox_buy(&mut self, group: ItemType, index: String, construct_id: Option) -> Result<&mut Player, Error> { + let item = self.vbox.buy(group, &index)?; + + match construct_id { + Some(id) => { self.vbox_apply(item, id)?; }, + None => { self.vbox.stash_add(item, None)?; }, + }; + Ok(self) } - pub fn vbox_combine(&mut self, inv_indices: Vec, vbox_indices: Vec>) -> Result<&mut Player, Error> { + pub fn vbox_combine(&mut self, inv_indices: Vec, vbox_indices: VboxIndices) -> Result<&mut Player, Error> { self.vbox.combine(inv_indices, vbox_indices)?; Ok(self) } - pub fn vbox_reclaim(&mut self, index: usize) -> Result<&mut Player, Error> { - self.vbox.reclaim(index)?; + pub fn vbox_refund(&mut self, index: String) -> Result<&mut Player, Error> { + self.vbox.refund(index)?; Ok(self) } - pub fn vbox_apply(&mut self, index: usize, construct_id: Uuid) -> Result<&mut Player, Error> { - if self.vbox.bound.get(index).is_none() { - return Err(format_err!("no item at index {:?}", index)); - } + pub fn vbox_equip(&mut self, index: String, construct_id: Uuid) -> Result<&mut Player, Error> { + let item = self.vbox.stash.remove(&index) + .ok_or(format_err!("no item at index {:?} {:}", self, &index))?; - let item = self.vbox.bound.remove(index); + self.vbox_apply(item, construct_id) + } + pub fn vbox_apply(&mut self, item: Item, construct_id: Uuid) -> Result<&mut Player, Error> { match item.effect() { Some(ItemEffect::Skill) => { let skill = item.into_skill().ok_or(format_err!("item {:?} has no associated skill", item))?; @@ -317,8 +356,8 @@ impl Player { } pub fn vbox_unequip(&mut self, target: Item, construct_id: Uuid, target_construct_id: Option) -> Result<&mut Player, Error> { - if self.vbox.bound.len() >= 9 && !target_construct_id.is_some() { - return Err(err_msg("too many items bound")); + if self.vbox.stash.len() >= 9 && !target_construct_id.is_some() { + return Err(err_msg("too many items stash")); } match target.effect() { @@ -349,13 +388,10 @@ impl Player { construct.apply_modifiers(&player_colours); } - self.vbox.bound.push(target); - - if target_construct_id.is_some() { - let equip_index = self.vbox.bound.len() - 1; - self.vbox_apply(equip_index, target_construct_id.expect("no construct"))?; - } - // self.vbox.bound.sort_unstable(); + match target_construct_id { + Some(cid) => { self.vbox_apply(target, cid)?; }, + None => { self.vbox.stash_add(target, None)?; }, + }; Ok(self) } @@ -408,8 +444,8 @@ mod tests { let player_account = Uuid::new_v4(); let constructs = instance_mobs(player_account); let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); - player.vbox.fill(); + player.vbox.fill(); player.autobuy(); assert!(player.constructs.iter().all(|c| c.skills.len() >= 1)); diff --git a/server/src/rpc.rs b/server/src/rpc.rs index bc0ba493..e27a62dd 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::time::{Instant}; use std::thread::{spawn}; @@ -34,7 +35,7 @@ use mail::Email; use pg::{Db}; use pg::{PgPool}; use skill::{Skill, dev_resolve, Resolutions}; -use vbox::{vbox_accept, vbox_apply, vbox_discard, vbox_combine, vbox_reclaim, vbox_unequip}; +use vbox::{ItemType, vbox_buy, vbox_apply, vbox_refill, vbox_combine, vbox_refund, vbox_unequip}; use http::{AUTH_CLEAR, TOKEN_HEADER}; #[derive(Debug,Clone,Serialize)] @@ -114,14 +115,13 @@ pub enum RpcRequest { InstanceState { instance_id: Uuid }, InstanceChat { instance_id: Uuid, index: usize }, - VboxAccept { instance_id: Uuid, group: usize, index: usize }, - VboxAcceptEquip { instance_id: Uuid, group: usize, index: usize, construct_id: Uuid }, - VboxDiscard { instance_id: Uuid }, - VboxCombine { instance_id: Uuid, inv_indices: Vec, vbox_indices: Vec> }, - VboxApply { instance_id: Uuid, construct_id: Uuid, index: usize }, + VboxBuy { instance_id: Uuid, group: ItemType, index: String, construct_id: Option }, + VboxRefill { instance_id: Uuid }, + VboxCombine { instance_id: Uuid, inv_indices: Vec, vbox_indices: Option>> }, + VboxApply { instance_id: Uuid, construct_id: Uuid, index: String }, VboxUnequip { instance_id: Uuid, construct_id: Uuid, target: Item }, VboxUnequipApply { instance_id: Uuid, construct_id: Uuid, target: Item, target_construct_id: Uuid }, - VboxReclaim { instance_id: Uuid, index: usize }, + VboxRefund { instance_id: Uuid, index: String }, } struct Connection { @@ -245,11 +245,8 @@ impl Connection { RpcRequest::InstanceAbandon { instance_id } => Ok(instance_abandon(&mut tx, account, instance_id)?), - RpcRequest::VboxAccept { instance_id, group, index } => - Ok(RpcMessage::InstanceState(vbox_accept(&mut tx, account, instance_id, group, index, None)?)), - - RpcRequest::VboxAcceptEquip { instance_id, group, index, construct_id } => - Ok(RpcMessage::InstanceState(vbox_accept(&mut tx, account, instance_id, group, index, Some(construct_id))?)), + RpcRequest::VboxBuy { instance_id, group, index, construct_id } => + Ok(RpcMessage::InstanceState(vbox_buy(&mut tx, account, instance_id, group, index, construct_id)?)), RpcRequest::VboxApply { instance_id, construct_id, index } => Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, account, instance_id, construct_id, index)?)), @@ -257,11 +254,11 @@ impl Connection { RpcRequest::VboxCombine { instance_id, inv_indices, vbox_indices } => Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, account, instance_id, inv_indices, vbox_indices)?)), - RpcRequest::VboxDiscard { instance_id } => - Ok(RpcMessage::InstanceState(vbox_discard(&mut tx, account, instance_id)?)), + RpcRequest::VboxRefill { instance_id } => + Ok(RpcMessage::InstanceState(vbox_refill(&mut tx, account, instance_id)?)), - RpcRequest::VboxReclaim { instance_id, index } => - Ok(RpcMessage::InstanceState(vbox_reclaim(&mut tx, account, instance_id, index)?)), + RpcRequest::VboxRefund { instance_id, index } => + Ok(RpcMessage::InstanceState(vbox_refund(&mut tx, account, instance_id, index)?)), RpcRequest::VboxUnequip { instance_id, construct_id, target } => Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target, None)?)), diff --git a/server/src/vbox.rs b/server/src/vbox.rs index 8df14c6d..fdfd1d3d 100644 --- a/server/src/vbox.rs +++ b/server/src/vbox.rs @@ -1,8 +1,9 @@ use uuid::Uuid; use std::iter; +use std::collections::HashMap; -// reclaims +// refunds use rand::prelude::*; use rand::{thread_rng}; use rand::distributions::{WeightedIndex}; @@ -19,30 +20,48 @@ use construct::{Colours}; use item::*; +pub type VboxIndices = Option>>; + #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Vbox { pub bits: usize, - pub free: Vec>>, - pub bound: Vec, + pub store: HashMap>, + pub stash: HashMap, } +#[derive(Debug,Copy,Clone,Serialize,Deserialize,Hash,PartialEq,Eq)] pub enum ItemType { Colours, Skills, Specs, } +const STORE_COLOURS_CAPACITY: usize = 6; +const STORE_SKILLS_CAPACITY: usize = 3; +const STORE_SPECS_CAPACITY: usize = 3; +const STASH_CAPACITY: usize = 6; +const STARTING_ATTACK_COUNT: usize = 3; + impl Vbox { pub fn new() -> Vbox { - let starting_items = vec![ - Item::Attack, - Item::Attack, - Item::Attack, - ]; + let mut colours: HashMap = HashMap::new(); + let mut skills: HashMap = HashMap::new(); + let mut specs: HashMap = HashMap::new(); + + let store = [ + (ItemType::Colours, colours), + (ItemType::Skills, skills), + (ItemType::Colours, specs), + ].iter().cloned().collect(); + + let mut stash = HashMap::new(); + for i in 0..STARTING_ATTACK_COUNT { + stash.insert(i.to_string(), Item::Attack); + } Vbox { - free: vec![vec![], vec![], vec![]], - bound: starting_items, + store, + stash, bits: 30, } } @@ -65,103 +84,130 @@ impl Vbox { pub fn fill(&mut self) -> &mut Vbox { let mut rng = thread_rng(); - self.free = [ItemType::Colours, ItemType::Skills, ItemType::Specs].iter() - .map(|item_type| { - let items = match item_type { - ItemType::Colours => vec![ - (Some(Item::Red), 1), - (Some(Item::Green), 1), - (Some(Item::Blue), 1), - ], - ItemType::Skills => vec![ - (Some(Item::Attack), 1), - (Some(Item::Block), 1), - (Some(Item::Buff), 1), - (Some(Item::Debuff), 1), - (Some(Item::Stun), 1), - ], - ItemType::Specs => vec![ - (Some(Item::Power), 1), - (Some(Item::Life), 1), - (Some(Item::Speed), 1), - ], - }; + let colours = vec![ + (Item::Red, 1), + (Item::Green, 1), + (Item::Blue, 1), + ]; + let colour_dist = WeightedIndex::new(colours.iter().map(|item| item.1)).unwrap(); - let dist = WeightedIndex::new(items.iter().map(|item| item.1)).unwrap(); - iter::repeat_with(|| { - items[dist.sample(&mut rng)].0}).take(match item_type { - ItemType::Colours => 6, - _ => 3, - }).collect::>>() - }) - .collect::>>>(); + let skills = vec![ + (Item::Attack, 1), + (Item::Block, 1), + (Item::Buff, 1), + (Item::Debuff, 1), + (Item::Stun, 1), + ]; + let skill_dist = WeightedIndex::new(skills.iter().map(|item| item.1)).unwrap(); + + let specs = vec![ + (Item::Power, 1), + (Item::Life, 1), + (Item::Speed, 1), + ]; + let spec_dist = WeightedIndex::new(specs.iter().map(|item| item.1)).unwrap(); + + for item_type in [ItemType::Colours, ItemType::Skills, ItemType::Specs].iter() { + let (items, num, dist) = match item_type { + ItemType::Colours => (&colours, STORE_COLOURS_CAPACITY, &colour_dist), + ItemType::Skills => (&skills, STORE_SKILLS_CAPACITY, &skill_dist), + ItemType::Specs => (&specs, STORE_SPECS_CAPACITY, &spec_dist), + }; + + let drops = iter::repeat_with(|| items[dist.sample(&mut rng)].0) + .take(num) + .enumerate() + .map(|(i, item)| (i.to_string(), item)) + .collect::>(); + + self.store.insert(*item_type, drops); + } self } - pub fn accept(&mut self, i: usize, j: usize, construct_id: Option) -> Result<&mut Vbox, Error> { - if self.bound.len() >= 9 && !construct_id.is_some() { - return Err(err_msg("too many items bound")); - } - + pub fn buy(&mut self, item: ItemType, i: &String) -> Result { // check item exists - self.free - .get(i).ok_or(format_err!("no item group at index {:?}", i))? - .get(j).ok_or(format_err!("no item at index {:?}", j))?; + let selection = self.store + .get_mut(&item).ok_or(format_err!("no item group {:?}", item))? + .remove(i).ok_or(format_err!("no item at index {:?} {:}", self, i))?; - // check can purchase - let cost = match self.free[i][j] { - None => 0, - _ => self.free[i][j].unwrap().cost() - }; - self.balance_sub(cost)?; + self.balance_sub(selection.cost())?; - // actually move - match self.free[i][j] { - None => (), - _ => self.bound.push(self.free[i][j].unwrap()) + Ok(selection) + } + + pub fn stash_add(&mut self, item: Item, index: Option<&String>) -> Result { + if self.stash.len() >= STASH_CAPACITY { + return Err(err_msg("stash full")); } - // self.bound.push(self.free[i][j].unwrap()); - self.free[i][j] = None; - // self.bound.sort_unstable(); - Ok(self) + if let Some(index) = index { + if self.stash.contains_key(index) { + return Err(format_err!("slot occupied {:?}", index)); + } + self.stash.insert(index.clone(), item); + return Ok(index.to_string()); + } + + for i in (0..STASH_CAPACITY).map(|i| i.to_string()) { + if !self.stash.contains_key(&i) { + self.stash.insert(i.clone(), item); + return Ok(i); + } + } + + return Err(err_msg("stash full")); } - pub fn bot_accept(&mut self, i: usize) -> Result<&mut Vbox, Error> { - let buy_index = self.free[i].iter().position(|item| item.is_some()); - self.accept(i, buy_index.expect("no valid buys"), None) + pub fn bot_buy(&mut self, item: ItemType) -> Result { + let buy_index = self.store[&item] + .keys() + .next() + .ok_or(format_err!("no item in group {:?}", item))? + .clone(); + + self.buy(item, &buy_index) } - pub fn reclaim(&mut self, i: usize) -> Result<&mut Vbox, Error> { - self.bound.get(i).ok_or(format_err!("no item at index {:?}", i))?; - let reclaimed = self.bound.remove(i); - let refund = reclaimed.cost(); - // info!("reclaiming {:?} for {:?}", refund, reclaimed); + pub fn refund(&mut self, i: String) -> Result<&mut Vbox, Error> { + let refunded = self.stash.remove(&i) + .ok_or(format_err!("no item at index {:?} {:?}", self.stash, i))?; + + let refund = refunded.cost(); + // info!("refunding {:?} for {:?}", refund, refunded); self.balance_add(refund); Ok(self) } - pub fn combine(&mut self, mut inv_indices: Vec, vbox_indicies: Vec>) -> Result<&mut Vbox, Error> { - if !inv_indices.iter().all(|i| self.bound.get(*i).is_some()) { - return Err(err_msg("item missing index")); - } - // try to buy up the vbox indicies and add them to the inventory indicies for combining - for vi in vbox_indicies.iter() { - inv_indices.push(self.bound.len()); - self.accept(vi[0], vi[1], Some(Uuid::nil()))?; - } + pub fn combine(&mut self, stash_indices: Vec, store_indices: Option>>) -> Result<&mut Vbox, Error> { + // find base item for index to insert into + let base_index = stash_indices.iter() + .find(|i| match self.stash.get(i.clone()) { + Some(item) => item.into_skill().is_some(), + None => false, + }); - // have to sort the indices and keep track of the iteration - // because when removing the elements the array shifts - inv_indices.sort_unstable(); - let mut input = inv_indices + let mut input = stash_indices .iter() - .enumerate() - .map(|(i, index)| { - self.bound.remove(index.saturating_sub(i)) - }) - .collect::>(); + .map(|i| self.stash.remove(i) + .ok_or(format_err!("no item at index {:?} {:?}", self.stash, i))) + .collect::, Error>>()?; + + if let Some(store_indices) = store_indices { + let mut purchased = store_indices.iter() + .map(|(g, list)| + list.iter() + .map(|i| self.buy(*g, i)) + .collect::, Error>>() + ) + .collect::>, Error>>()? + .into_iter() + .flatten() + .collect(); + + input.append(&mut purchased); + } // sort the input to align with the combinations // combos are sorted when created @@ -169,40 +215,37 @@ impl Vbox { let combos = get_combos(); 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(); - if self.bound.len() >= 10 { - return Err(err_msg("too many items bound")); - } + self.stash_add(combo.item, base_index)?; + Ok(self) } } -pub fn vbox_discard(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result { +pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result { let instance = instance_get(tx, instance_id)? - .vbox_discard(account.id)?; + .vbox_refill(account.id)?; return instance_update(tx, instance); } -pub fn vbox_accept(tx: &mut Transaction, account: &Account, instance_id: Uuid, group: usize, index: usize, construct_id: Option) -> Result { +pub fn vbox_buy(tx: &mut Transaction, account: &Account, instance_id: Uuid, group: ItemType, index: String, construct_id: Option) -> Result { let instance = instance_get(tx, instance_id)? - .vbox_accept(account.id, group, index, construct_id)?; + .vbox_buy(account.id, group, index, construct_id)?; return instance_update(tx, instance); } -pub fn vbox_combine(tx: &mut Transaction, account: &Account, instance_id: Uuid, inv_indices: Vec, vbox_indices: Vec>) -> Result { +pub fn vbox_combine(tx: &mut Transaction, account: &Account, instance_id: Uuid, stash_indices: Vec, vbox_indices: VboxIndices) -> Result { let instance = instance_get(tx, instance_id)? - .vbox_combine(account.id, inv_indices, vbox_indices)?; + .vbox_combine(account.id, stash_indices, vbox_indices)?; return instance_update(tx, instance); } -pub fn vbox_reclaim(tx: &mut Transaction, account: &Account, instance_id: Uuid, index: usize) -> Result { +pub fn vbox_refund(tx: &mut Transaction, account: &Account, instance_id: Uuid, index: String) -> Result { let instance = instance_get(tx, instance_id)? - .vbox_reclaim(account.id, index)?; + .vbox_refund(account.id, index)?; return instance_update(tx, instance); } -pub fn vbox_apply(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, index: usize) -> Result { +pub fn vbox_apply(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, index: String) -> Result { let instance = instance_get(tx, instance_id)? .vbox_apply(account.id, index, construct_id)?; return instance_update(tx, instance); @@ -221,9 +264,49 @@ mod tests { #[test] fn combine_test() { let mut vbox = Vbox::new(); - vbox.bound = vec![Item::Attack, Item::Green, Item::Green]; - vbox.combine(vec![1,2,0], vec![]).unwrap(); - assert_eq!(vbox.bound[0], Item::Heal); + vbox.stash.insert(0.to_string(), Item::Attack); + vbox.stash.insert(1.to_string(), Item::Green); + vbox.stash.insert(2.to_string(), Item::Green); + vbox.combine(vec![0.to_string(), 1.to_string(), 2.to_string()], None).unwrap(); + assert_eq!(vbox.stash["0"], Item::Heal); + } + + #[test] + fn buy_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + + // cannot rebuy same + vbox.buy(ItemType::Skills, &0.to_string()).unwrap(); + assert!(vbox.store[&ItemType::Skills].get(&0.to_string()).is_none()); + assert!(vbox.buy(ItemType::Skills, &0.to_string()).is_err()); + } + + #[test] + fn capacity_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + vbox.stash_add(Item::Red, None).unwrap(); + vbox.stash_add(Item::Red, None).unwrap(); + vbox.stash_add(Item::Red, None).unwrap(); + assert!(vbox.stash_add(Item::Red, None).is_err()); + } + + #[test] + fn store_and_stash_combine_test() { + let mut vbox = Vbox::new(); + vbox.fill(); + + let mut skill_combine_args = HashMap::new(); + skill_combine_args.insert(ItemType::Colours, vec![0.to_string(), 1.to_string()]); + skill_combine_args.insert(ItemType::Skills, vec![0.to_string()]); + + let mut spec_combine_args = HashMap::new(); + spec_combine_args.insert(ItemType::Colours, vec![2.to_string(), 3.to_string()]); + spec_combine_args.insert(ItemType::Specs, vec![0.to_string()]); + + vbox.combine(vec![], Some(skill_combine_args)).unwrap(); + vbox.combine(vec![], Some(spec_combine_args)).unwrap(); } #[test] @@ -239,10 +322,10 @@ mod tests { } #[test] - fn reclaim_test() { + fn refund_test() { let mut vbox = Vbox::new(); - vbox.bound = vec![Item::Strike]; - vbox.reclaim(0).unwrap(); + vbox.stash.insert(0.to_string(), Item::Strike); + vbox.refund(0.to_string()).unwrap(); assert_eq!(vbox.bits, 32); }