diff --git a/VERSION b/VERSION index e33692ab..169f19b4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.10.1 \ No newline at end of file +1.11.0 \ No newline at end of file diff --git a/acp/package.json b/acp/package.json index af19ae15..196377a3 100644 --- a/acp/package.json +++ b/acp/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.10.1", + "version": "1.11.0", "description": "", "main": "index.js", "scripts": { diff --git a/client/assets/mnml.awards.svg b/client/assets/mnml.awards.svg new file mode 100644 index 00000000..d18f8dc7 --- /dev/null +++ b/client/assets/mnml.awards.svg @@ -0,0 +1,2684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/assets/mnml.logo.text.svg b/client/assets/mnml.logo.text.svg new file mode 100644 index 00000000..578fac9b --- /dev/null +++ b/client/assets/mnml.logo.text.svg @@ -0,0 +1,2568 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/client/assets/styles/account.less b/client/assets/styles/account.less index c920c8dc..21ea145f 100644 --- a/client/assets/styles/account.less +++ b/client/assets/styles/account.less @@ -3,16 +3,25 @@ div { padding-right: 1em; + // display: flex; + // flex-flow: column; + line-height: 2em; + } + + h3 { + // text-transform: uppercase; + margin-bottom: 0.5em; } button { width: 100%; + height: 2.5em; display: block; } input { width: 100%; - height: 3em; + height: 2.5em; display: block; } diff --git a/client/assets/styles/colours.less b/client/assets/styles/colours.less index 16e972af..4d93f6fc 100644 --- a/client/assets/styles/colours.less +++ b/client/assets/styles/colours.less @@ -5,7 +5,7 @@ @white: #f5f5f5; // whitesmoke @purple: #9355b5; // 6lack - that far cover @yellow: #ffa100; -@silver: #c0c0c0; +@silver: #2c2c2c; @black: black; @gray: #222; diff --git a/client/assets/styles/instance.less b/client/assets/styles/instance.less index 52ad8fea..86e97a9f 100644 --- a/client/assets/styles/instance.less +++ b/client/assets/styles/instance.less @@ -54,11 +54,7 @@ button { &.highlight { - color: black; background: @silver; - // border: 1px solid @white; (this bangs around the vbox) - - // overwrite the classes on white svg elements svg { stroke-width: 0.75em; } diff --git a/client/assets/styles/menu.less b/client/assets/styles/menu.less index e197781e..670a86be 100644 --- a/client/assets/styles/menu.less +++ b/client/assets/styles/menu.less @@ -75,25 +75,11 @@ flex: 1; border-top: 0; border: 0.1em solid #222; - &:not(:last-child) { - border-right: 0; - } - &:last-child { float: right; } } } - - .login { - display: flex; - flex-flow: column; - - .terms { - display: inline; - margin: 0 1em; - } - } } section { @@ -108,62 +94,14 @@ section { padding-right: 1em; } - .list { - letter-spacing: 0.25em; - text-transform: uppercase; - display: grid; - // grid-template-columns: repeat(4, 1fr); - grid-template-columns: 1fr 1fr; - grid-gap: 1em; - flex-flow: row wrap; - align-items: flex-end; - button { - border-radius: 0.25em; - // height: 3em; - } - - &.sub { - grid-template-columns: 1fr; - } - - &.play { - grid-template-columns: repeat(2, 1fr); - align-items: flex-start; - - &.rejoin { - grid-template-columns: 1fr; - } - - button.ready:enabled { - color: forestgreen; - border-color: forestgreen; - - &:hover { - background: forestgreen; - color: black; - border-color: forestgreen; - } - } - - // // all green - // button.ready:enabled { - // background: forestgreen; - // color: black; - // border-color: forestgreen; - - // &:hover { - // color: forestgreen; - // border-color: forestgreen; - // background: 0; - // } - // } - } - } - .panes { display: grid; grid-template-columns: repeat(2, 1fr); } +} + +.list { + margin-bottom: 2em; figure { letter-spacing: 0.25em; @@ -172,61 +110,92 @@ section { display: flex; flex-flow: column; } + + letter-spacing: 0.25em; + text-transform: uppercase; + display: grid; + // grid-template-columns: repeat(4, 1fr); + grid-template-columns: 1fr 1fr; + grid-gap: 1em; + flex-flow: row wrap; + align-items: flex-end; + + &.sub { + grid-template-columns: 1fr; + } + + &.play { + grid-template-columns: repeat(2, 1fr); + align-items: flex-start; + + &.rejoin { + grid-template-columns: 1fr; + } + + button.ready:enabled { + color: forestgreen; + border-color: forestgreen; + + &:hover { + background: forestgreen; + color: black; + border-color: forestgreen; + } + } + + // // all green + // button.ready:enabled { + // background: forestgreen; + // color: black; + // border-color: forestgreen; + + // &:hover { + // color: forestgreen; + // border-color: forestgreen; + // background: 0; + // } + // } + } } -.demo { - margin-top: 1em; - display: block; +.login { + display: flex; + flex-flow: column; + + .terms { + display: inline; + margin: 0 1em; + } button { - pointer-events: none; + padding: 0 0.5em; + margin-top: 1em; + } +} + +.options { + grid-area: hdr; + + display: flex; + + .logo { + flex: 0 1 10%; + margin-right: 1em; + border: none; } - section { - margin-bottom: 0.5em; - - div:first-child { - padding-right: 1em; - } - } - - .construct-section { - .construct-list { - height: 25em; - grid-area: unset; - - .instance-construct { - // border: 0; - } - } - } - - .colour-info { - grid-area: vinfo; - display: flex; - align-items: center; - - div { - display: flex; - } - - svg { - flex: 1; - height: 1em; - } - } - - .game-demo { - .game { - height: 25em; - display: flex; - flex-flow: column; - - .game-construct { - flex: 1; - } + button { + flex: 1; + border-top: 0; + border: 0.1em solid #222; + &:last-child { + float: right; } } } +.intro { + text-align: center; + font-size: 0.8em; +} diff --git a/client/assets/styles/styles.less b/client/assets/styles/styles.less index b22ee13d..aa852df9 100644 --- a/client/assets/styles/styles.less +++ b/client/assets/styles/styles.less @@ -27,23 +27,6 @@ html body { overflow-y: hidden; } -#mnml { - /* this is the sweet nectar to keep it full page*/ - height: 100vh; - max-height: 100vh; - min-height: 100vh; - - /* stops inspector going skitz*/ - overflow-x: hidden; - // overflow-y: hidden; -} - -// @media (min-width: 1921px) { -// html, body, #mnml { -// font-size: 16pt; -// } -// } - html { box-sizing: border-box; margin: 0; @@ -108,11 +91,37 @@ dl { padding: 0.5em 1em; + /* this is the sweet nectar to keep it full page*/ + height: 100vh; + max-height: 100vh; + min-height: 100vh; + + /* stops inspector going skitz*/ + overflow-x: hidden; + // overflow-y: hidden; + + &.animations-test { aside button { font-size: 50%; } } + + &.front-page { + display: block; + + main { + padding: 0 25%; + } + + .logo { + margin: 2em 0; + } + + .list { + margin-bottom: 0; + } + } } main { @@ -129,7 +138,7 @@ button, input { box-sizing: border-box; font-size: 1em; flex: 1; - border-radius: 0.5em; + border-radius: 0; line-height: 2em; padding-right: 0.1em; padding-left: 0.1em; @@ -150,9 +159,12 @@ button, input { &:focus { /*colour necesary to bash skellington*/ - outline: 0; } + + // &:active { + // filter: url("#noiseFilter"); + // } } a { @@ -261,28 +273,12 @@ figure.gray { display: none; } -header { - .options { - font-size: 200%; - } - - button { - height: 2em; - // border-radius: 0.1em; - border: none; - border-radius: 0; - } -} - .options { button { &.highlight { color: @white; box-shadow: inset 0px 5px 0px 0px @white; - border: 0; } - - border: none; } } @@ -300,11 +296,20 @@ li { } .logo { - height: 2em; - background-image: url("../../assets/mnml.logo.trim.svg"); + height: 4em; + filter: url("#noiseFilter"); + background-image: url("../../assets/mnml.logo.text.svg"); background-size: contain; background-repeat: no-repeat; - background-position: left; + background-position: center; +} + +.awards { + height: 100%; + background-image: url("../../assets/mnml.awards.svg"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; } .discord-btn { @@ -316,8 +321,13 @@ li { .mnni { background-image: url("./../mnni.svg"); + filter: url("#noiseFilter"); } +// .highlight { +// filter: url("#noiseFilter"); +// } + .avatar { grid-area: avatar; object-fit: contain; @@ -328,6 +338,10 @@ li { // pointer-events: none; } +header { + // font-size: 1.2em; +} + #clipboard { width: 1px; height: 1px; @@ -359,4 +373,8 @@ li { } } +#noise { + height: 0; +} + @import 'styles.mobile.less'; diff --git a/client/assets/styles/styles.mobile.less b/client/assets/styles/styles.mobile.less index 27c8fa8a..e713faab 100644 --- a/client/assets/styles/styles.mobile.less +++ b/client/assets/styles/styles.mobile.less @@ -7,6 +7,12 @@ font-size: 8pt; padding: 0; + &.front-page { + main { + padding: 0 0.5em; + } + } + .instance { grid-template-areas: "vbox vbox" @@ -164,12 +170,21 @@ // portrait menu or small size vertical in landscape -@media (max-width: 550px) and (max-height: 800px) { +@media (max-width: 550px) and (max-height: 800px) and (orientation: portrait) { #mnml { grid-template-columns: 1fr; grid-template-rows: 1fr; grid-template-areas: - "main" + "main"; + + &.front-page { + display: block; + + main { + padding: 0 0.5em; + } + } + } section { @@ -264,6 +279,9 @@ } .info-combiner { + max-height: 7em; + overflow-y: scroll; + .info { display: none; } diff --git a/client/assets/styles/vbox.less b/client/assets/styles/vbox.less index aab8da8e..e217b100 100644 --- a/client/assets/styles/vbox.less +++ b/client/assets/styles/vbox.less @@ -147,16 +147,11 @@ } &.highlight { - color: black; background: @silver; // overwrite the classes on white svg elements svg { stroke-width: 0.75em; } - - .white { - stroke: black; - } } } diff --git a/client/package.json b/client/package.json index c840b666..a0e5ba9f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.10.1", + "version": "1.11.0", "description": "", "main": "index.js", "scripts": { diff --git a/client/src/actions.jsx b/client/src/actions.jsx index e99bba62..a49d8278 100644 --- a/client/src/actions.jsx +++ b/client/src/actions.jsx @@ -1,4 +1,5 @@ export const setAccount = value => ({ type: 'SET_ACCOUNT', value }); +export const setAuthenticated = value => ({ type: 'SET_AUTHENTICATED', value }); export const setAnimating = value => ({ type: 'SET_ANIMATING', value }); export const setAnimFocus = value => ({ type: 'SET_ANIM_FOCUS', value }); @@ -7,8 +8,6 @@ export const setAnimSource = value => ({ type: 'SET_ANIM_SOURCE', value }); export const setAnimTarget = value => ({ type: 'SET_ANIM_TARGET', value }); export const setResolution = value => ({ type: 'SET_RESOLUTION', value }); -export const setDemo = value => ({ type: 'SET_DEMO', value }); - export const setChatShow = value => ({ type: 'SET_CHAT_SHOW', value }); export const setChatWheel = value => ({ type: 'SET_CHAT_WHEEL', value }); export const setInstanceChat = value => ({ type: 'SET_INSTANCE_CHAT', value }); diff --git a/client/src/app.jsx b/client/src/app.jsx index 1599c1ab..47a08e62 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -33,6 +33,10 @@ const ws = createSocket(events); ws.connect(); events.setWs(ws); +if (process.env.NODE_ENV !== 'development') { + LogRocket.init('yh0dy3/mnml'); +} + const App = () => ( {window.Stripe diff --git a/client/src/components/account.top.jsx b/client/src/components/account.top.jsx index de983dd7..b2500d62 100644 --- a/client/src/components/account.top.jsx +++ b/client/src/components/account.top.jsx @@ -152,11 +152,9 @@ class AccountStatus extends Component { return (
+ {subInfo()}
- {subInfo()} -
-
- +

Email

Recovery Email
{email ? email.email : 'No email set'}
@@ -174,6 +172,7 @@ class AccountStatus extends Component {
+

Password

+

Other

spawn new construct
; - const highlighted = combiner.indexOf(j) > -1; - const classes = `${highlighted ? 'highlight' : ''}`; - - if (shapes[i]) { - return ; - } - - return ; - } - - function combinerBtn() { - let text = ''; - - if (combiner.length < 3) { - for (let i = 0; i < 3; i++) { - if (combiner.length > i) { - text += 'â–  '; - } else { - text += 'â–« '; - } - } - } else { - text = 'combine'; - } - - return ( - - ); - } - - function stashElement() { - return ( -
-
-

- VBOX PHASE {shapes.Red()} {shapes.Green()} {shapes.Blue()} -

-

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

-
-
 
-
-
- {items.map((i, j) => stashBtn(i, j))} -
- {combinerBtn()} -
-
- ); - } - - return ( -
- {stashElement()} -
- ); - }; - - const vboxConstructs = () => { - const btnClass = equipping - ? 'equipping empty gray' - : 'empty gray'; - - const constructEl = c => ( -
-

{c.name}

- -
- {equipped - ? - : - } - - -
-
-
-
-
-
- ); - - return ( -
-
-

CONSTRUCTS

-

Constructs are the units you control. They are reset every game and their initial appearance is randomly generated.

-

Skills and Specs you create in the VBOX Phase are equipped to your constructs to create a build.

-
-
- {constructEl(players[0].constructs[0])} -
-
- ); - }; - - const gameDemo = () => { - return ( -
-
-

COMBAT PHASE

-

Battle your opponent using dynamic team builds from the VBOX phase.

-

The skills crafted can be used to damage the opponent or support your team.

-

Simultaneous turn based combat: each team picks targets for their skills during this phase.

-

The damage dealt by skills, cast order and construct life depend on your decisions in the VBOX phase.

-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
- ); - }; - - return ( -
- {gameDemo()} - {vboxDemo()} - {vboxConstructs()} -
- ); -} - -module.exports = addState(Demo); diff --git a/client/src/components/front.page.jsx b/client/src/components/front.page.jsx new file mode 100644 index 00000000..32cf3e16 --- /dev/null +++ b/client/src/components/front.page.jsx @@ -0,0 +1,85 @@ +// const { connect } = require('preact-redux'); +const preact = require('preact'); +const { connect } = require('preact-redux'); + +const { errorToast, infoToast } = require('../utils'); +const actions = require('./../actions'); + +const VERSION = process.env.npm_package_version; + +const Welcome = require('./welcome'); + +const addState = connect( + function receiveState(state) { + const { + ws, + account, + } = state; + + function sendInstancePractice() { + ws.sendInstancePractice(); + } + + return { + account, + sendInstancePractice, + }; + }, +); + +function Play(args) { + const { + account, + sendInstancePractice, + } = args; + + const news = ( +
+
+

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

+

+ Build a unique team of 3 constructs from a range of skills and specialisations.
+ Outplay your opponent across multiple rounds by adapting to an always shifting meta.
+

+
+
+
+ ); + + const list = () => { + return ( +
+
+ +
Learn MNML
+
+
+ +
Join the Community
+
+
+ ); + }; + + return ( +
+
+ ); +} + +module.exports = addState(Play); diff --git a/client/src/components/game.construct.anim.text.jsx b/client/src/components/game.construct.anim.text.jsx index 1311d744..8b982cd2 100644 --- a/client/src/components/game.construct.anim.text.jsx +++ b/client/src/components/game.construct.anim.text.jsx @@ -41,36 +41,38 @@ class AnimText extends preact.Component { const generateAnimText = () => { const [type, event] = resolution.event; - if (type === 'Ko') return

KO!

; - if (type === 'Disable') { - const { disable } = event; - return

{disable}

; - } - if (type === 'Immunity') return

IMMUNE

; - if (type === 'Damage') { - const { mitigation, colour } = event; - let { amount } = event; - amount *= -1; + switch (type) { + case 'Damage': { + const { amount, mitigation, colour } = event; const mitigationText = mitigation ? `(${mitigation})` : ''; - return

{amount} {mitigationText}

; + return

-{amount} {mitigationText}

; } - if (type === 'Healing') { + case 'Healing': { const { amount, overhealing, colour } = event; - return

{amount} ({overhealing} OH)

; + const overHealingText = overhealing ? `(${overhealing} OH)` : ''; + return

+{amount} {overHealingText}

; } - if (type === 'Inversion') return

INVERT

; - if (type === 'Reflection') return

REFLECT

; - if (type === 'Effect') { + case 'Effect': { const { effect, duration } = event; return

+{effect} {duration}T

; } - if (type === 'Removal') { + case 'Removal': { const { effect } = event; if (!effect) return

Effect Removal

; - return

{effect}

; + return

-{effect}

; + } + case 'Ko': return

KO!

; + case 'Reflection': return

REFLECT

; + default: return false; } - return false; }; + // We don't send inversion / disable / immune event text + /* case 'Inversion': return

INVERT

; + case 'Disable': { + const { disable } = event; + return

{disable}

; + } + case 'Immunity': return

IMMUNE

; */ return (
diff --git a/client/src/components/game.construct.jsx b/client/src/components/game.construct.jsx index 68e53cad..a5c163c2 100644 --- a/client/src/components/game.construct.jsx +++ b/client/src/components/game.construct.jsx @@ -59,11 +59,16 @@ class GameConstruct extends preact.Component { player, } = this.props; - const ko = construct.green_life.value === 0 ? 'ko' : ''; + // construct green_life comes from game state and won't update during animations + // treat the construct as ko for the remainder of the anims if ko event occurs + const ko = construct.green_life.value === 0 || this.ko ? 'ko' : ''; const koEvent = () => { if (resolution) { const [type, variant] = resolution.event; - if (variant.construct === construct.id && type === 'Ko') return 'ko-transition'; + if (variant.construct === construct.id && type === 'Ko') { + this.ko = true; + return 'ko-transition'; + } } return ''; }; diff --git a/client/src/components/header.jsx b/client/src/components/header.jsx index 734b1761..bcc286a6 100644 --- a/client/src/components/header.jsx +++ b/client/src/components/header.jsx @@ -8,6 +8,7 @@ const addState = connect( const { ws, account, + authenticated, nav, } = state; @@ -22,6 +23,7 @@ const addState = connect( return { account, + authenticated, nav, sendInstanceState, @@ -48,6 +50,7 @@ const addState = connect( function Header(args) { const { account, + authenticated, nav, sendAccountStates, @@ -56,6 +59,8 @@ function Header(args) { if (!account) return false; + if (!authenticated) return false; + function navTo(p) { return setNav(p); } @@ -68,11 +73,6 @@ function Header(args) { return (
- - - - - -
-
-
- {main} +
+
+ + +
- + {form} +
); } diff --git a/client/src/events.jsx b/client/src/events.jsx index 240d9bf1..11e791ea 100644 --- a/client/src/events.jsx +++ b/client/src/events.jsx @@ -27,7 +27,6 @@ function registerEvents(store) { function clearTutorial() { store.dispatch(actions.setTutorial(null)); - localStorage.setItem('tutorial-complete', true); } @@ -35,7 +34,6 @@ function registerEvents(store) { store.dispatch(actions.setTutorialGame(null)); } - function setPing(ping) { store.dispatch(actions.setPing(ping)); } @@ -106,16 +104,21 @@ function registerEvents(store) { } function setAccount(account) { - if (account && process.env.NODE_ENV !== 'development') { - LogRocket.init('yh0dy3/mnml'); - LogRocket.identify(account.id, account); + store.dispatch(actions.setAccount(account)); + } - if (window.Notification) { - window.Notification.requestPermission(); - } + function setAuthenticated(account) { + if (account && window.Notification) { + window.Notification.requestPermission(); + } + + if (process.env.NODE_ENV !== 'development') { + LogRocket.identify(account.id, account); } store.dispatch(actions.setAccount(account)); + store.dispatch(actions.setTutorial(null)); + store.dispatch(actions.setAuthenticated(true)); } function setEmail(email) { @@ -180,18 +183,14 @@ function registerEvents(store) { const player = v.players.find(p => p.id === account.id); store.dispatch(actions.setPlayer(player)); + if (tutorial) tutorialVbox(player, store, tutorial); + if (v.phase === 'Finished') { ws.sendAccountInstances(); } - - // instance.mobile.less hides info at @media 1000 - if (localStorage.getItem('tutorial-complete') || window.innerWidth <= 1100) { - store.dispatch(actions.setTutorial(null)); - } else if (v.time_control === 'Practice' && v.rounds.length === 1 && tutorial) { - tutorialVbox(player, store, tutorial); - } } + return store.dispatch(actions.setInstance(v)); } @@ -207,94 +206,6 @@ function registerEvents(store) { return store.dispatch(actions.setItemInfo(v)); } - function setDemo(d) { - - const vboxDemo = { - players: d, - combiner: [], - equipped: false, - equipping: false, - }; - - const startDemo = () => { - const { account, itemInfo } = store.getState(); - if (account) return false; - if (!itemInfo || itemInfo.items.length === 0) return setTimeout(startDemo, 500); - store.dispatch(actions.setAnimTarget(null)); - const bases = ['Attack', 'Stun', 'Buff', 'Debuff', 'Block']; - const combo = sample(itemInfo.combos.filter(i => bases.some(b => i.components.includes(b)))); - vboxDemo.combo = combo.item; - vboxDemo.items = combo.components; - store.dispatch(actions.setDemo(vboxDemo)); - - - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0] }))), 500); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1] }))), 1000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0, 1, 2] }))), 1500); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: [vboxDemo.combo, '', ''] }))), 2500); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [0], items: [vboxDemo.combo, '', ''], equipping: true }))), 3000); - setTimeout(() => store.dispatch(actions.setDemo(Object.assign({}, vboxDemo, { combiner: [], items: ['', '', ''], equipped: true, equipping: false }))), 4000); - - setTimeout(() => { - return store.dispatch(actions.setAnimTarget({ - skill: sample(itemInfo.items.filter(i => i.skill)).item, - constructId: d[1].constructs[0].id, - player: false, - direction: 0, - })); - }, 500); - - setTimeout(() => { - return store.dispatch(actions.setAnimTarget({ - skill: sample(itemInfo.items.filter(i => i.skill)).item, - constructId: d[1].constructs[1].id, - player: true, - direction: 0, - })); - }, 3000); - - return setTimeout(startDemo, 5000); - }; - - startDemo(); - } - - // store.subscribe(setInfo); - // store.on('SET_INFO', setInfo); - - // events.on('SET_PLAYER', setInstance); - - // events.on('SEND_SKILL', function skillActive(gameId, constructId, targetConstructId, skill) { - // ws.sendGameSkill(gameId, constructId, targetConstructId, skill); - // setConstructStatusUpdate(constructId, skill, targetConstructId); - // }); - - // events.on('CONSTRUCT_ACTIVE', function constructActiveCb(construct) { - // for (let i = 0; i < constructs.length; i += 1) { - // if (constructs[i].id === construct.id) constructs[i].active = !constructs[i].active; - // } - // return setConstructs(constructs); - // }); - - /* function errorPrompt(type) { - const message = errMessages[type]; - const OK_BUTTON = ''; - toast.error({ - theme: 'dark', - color: 'black', - timeout: false, - drag: false, - position: 'center', - maxWidth: window.innerWidth / 2, - close: false, - buttons: [ - [OK_BUTTON, (instance, thisToast) => instance.hide({ transitionOut: 'fadeOut' }, thisToast)], - ], - message, - }); - } */ - // setup / localstorage - function urlHashChange() { const { ws } = store.getState(); const cmds = querystring.parse(location.hash); @@ -303,6 +214,11 @@ function registerEvents(store) { return true; } + function startTutorial() { + store.dispatch(actions.setTutorial(1)); + } + + window.addEventListener('hashchange', urlHashChange, false); return { @@ -313,11 +229,11 @@ function registerEvents(store) { clearTutorial, clearTutorialGame, setAccount, + setAuthenticated, setAccountInstances, setActiveItem, setActiveSkill, setChatWheel, - setDemo, setConstructList, setNewConstruct, setGame, @@ -333,6 +249,8 @@ function registerEvents(store) { setSubscription, setWs, + startTutorial, + urlHashChange, notify, diff --git a/client/src/reducers.jsx b/client/src/reducers.jsx index 36a81eea..c30dec0a 100644 --- a/client/src/reducers.jsx +++ b/client/src/reducers.jsx @@ -10,6 +10,7 @@ function createReducer(defaultState, actionType) { /* eslint-disable key-spacing */ module.exports = { account: createReducer(null, 'SET_ACCOUNT'), + authenticated: createReducer(null, 'SET_AUTHENTICATED'), activeItem: createReducer(null, 'SET_ACTIVE_VAR'), activeSkill: createReducer(null, 'SET_ACTIVE_SKILL'), @@ -20,8 +21,6 @@ module.exports = { resolution: createReducer(null, 'SET_RESOLUTION'), - demo: createReducer(null, 'SET_DEMO'), - chatShow: createReducer(null, 'SET_CHAT_SHOW'), chatWheel: createReducer([], 'SET_CHAT_WHEEL'), diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 210a0700..30e16b59 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -256,10 +256,6 @@ function createSocket(events) { events.setItemInfo(info); } - function onDemo(v) { - events.setDemo(v); - } - let pongTimeout; function onPong() { events.setPing(Date.now() - ping); @@ -274,6 +270,7 @@ function createSocket(events) { // this object wraps the reply types to a function const handlers = { AccountState: onAccount, + AccountAuthenticated: account => events.setAuthenticated(account), AccountConstructs: onAccountConstructs, AccountTeam: onAccountTeam, AccountInstances: onAccountInstances, @@ -285,7 +282,6 @@ function createSocket(events) { InstanceState: onInstanceState, ItemInfo: onItemInfo, Pong: onPong, - Demo: onDemo, // QueueRequested: () => events.notify('PVP queue request received.'), QueueRequested: () => true, @@ -304,6 +300,8 @@ function createSocket(events) { ChatWheel: wheel => events.setChatWheel(wheel), // Joining: () => events.notify('Searching for instance...'), + StartTutorial: () => events.startTutorial(), + Processing: () => true, Error: errHandler, }; diff --git a/client/src/tutorial.utils.jsx b/client/src/tutorial.utils.jsx index 740aa867..90c0ded5 100644 --- a/client/src/tutorial.utils.jsx +++ b/client/src/tutorial.utils.jsx @@ -115,8 +115,9 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (tutorial === 1) { return (
-

Tutorial

-

Welcome to the vbox phase tutorial.

+

Welcome to MNML

+

This is the VBOX Phase tutorial.

+

In the VBOX Phase you customise your constructs' skills and specialisations.

Colours are used to create powerful combinations with base items.

Buy the two colours from the store to continue.

@@ -126,9 +127,9 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (tutorial === 2) { return (
-

Tutorial

+

Combining Items

You start the game with the base Attack skill item.

-

Highlight all three items then click combine.

+

Highlight the Attack and the two colours then click combine

); } @@ -137,11 +138,11 @@ function tutorialStage(tutorial, clearTutorial, instance) { const constructOne = instance.players[0].constructs[0].name; return (
-

Tutorial

+

Equipping Items

The first construct on your team is {constructOne}.

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

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

+ Once selected click the flashing SKILL slot or the construct img to equip the skill.

); } @@ -149,7 +150,7 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (tutorial === 4) { return (
-

Tutorial

+

Specialisations

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

Buy the specialisation item from the store to continue.

@@ -160,11 +161,12 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (tutorial === 5) { return (
-

Tutorial

+

Specialisations

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 stash.
Once selected click the flashing SPEC slot to equip the specialisation.

+

PRO TIP: while selecting an item in the shop, click on your construct to buy and equip in one step.

); } @@ -174,11 +176,12 @@ function tutorialStage(tutorial, clearTutorial, instance) { const constructThree = instance.players[0].constructs[2].name; return (
-

Tutorial

+

Skills

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

The goal is to create three powerful constructs for combat.

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

+

PRO TIP: Select a skill or spec on a construct and click another construct to swap it.

); } @@ -186,7 +189,7 @@ function tutorialStage(tutorial, clearTutorial, instance) { if (tutorial === 7) { return (
-

Tutorial

+

Economy

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.
@@ -203,22 +206,22 @@ function tutorialStage(tutorial, clearTutorial, instance) { return (

-

Tutorial

+

GLHF

That completes the VBOX Tutorial.

-

Press READY to progress to the GAME PHASE
- You can continue creating new items to upgrade your constructs further.

+

Press the green READY button in the bottom right to progress to the GAME PHASE
+ or continue creating new items to upgrade your constructs further.

); } return false; }; - const classes = tutorial === 8 ? 'focus' : ''; - const text = tutorial === 8 ? 'Continue' : 'Skip Tutorial' - const exitTutorial = ; + const exitTutorial = tutorial === 8 ? + + : null; return (
diff --git a/client/src/utils.jsx b/client/src/utils.jsx index 349679c5..b2c715fb 100644 --- a/client/src/utils.jsx +++ b/client/src/utils.jsx @@ -241,30 +241,15 @@ function convertItem(v) { function effectInfo(i) { // FIX ME - return 'effect info to be fixed'; - - /*const hybridBlast = 25; + const hybridBlast = 25; const hasteStrike = 30; function multiplier(s) { // Update later to use server info in future - if (s === 'CounterAttack') return 120; - if (s === 'CounterAttack+') return 160; - if (s === 'CounterAttack++') return 230; - - if (s === 'DecayTick') return 33; - if (s === 'DecayTick+') return 45; - if (s === 'DecayTick++') return 70; - - if (s === 'SiphonTick') return 25; - if (s === 'SiphonTick+') return 30; - if (s === 'SiphonTick++') return 40; - - if (s === 'TriageTick') return 75; - if (s === 'TriageTick+') return 110; - if (s === 'TriageTick++') return 140; - - if (s === 'Electrocute' || s === 'ElectrocuteTick') return 80; - if (s === 'Electrocute+' || s === 'ElectrocuteTick+') return 100; - if (s === 'Electrocute++' || s === 'ElectrocuteTick++') return 130; + if (s === 'CounterAttack') return 115; + if (s === 'CounterAttack+') return 130; + if (s === 'CounterAttack++') return 160; + if (s === 'Electrocute') return 80; + if (s === 'Electrocute+') return 90; + if (s === 'Electrocute++') return 100; return 0; } @@ -289,16 +274,16 @@ function effectInfo(i) { case 'Vulnerable': return `Construct will take ${i.meta[1] - 100}% increased red damage`; case 'Silence': return 'Disable construct from casting any blue skills'; case 'Wither': return `Construct will take ${100 - i.meta[1]}% reduced healing`; // - case 'Decay': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`; // + case 'Decay': return `Construct will take ${i.meta[1].amount} blue damage each turn.`; // case 'Electric': return `Attacks against this construct will apply Electrocute dealing ${multiplier(i.meta[1])}% of construct BluePower as blue damage each turn.`; - case 'Electrocute': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower as blue damage each turn.`; + case 'Electrocute': return `Construct will take ${i.meta[1].amount} blue damage each turn.`; case 'Absorb': return 'If construct takes damage, Absorption will be applied increasing RedPower and BluePower based on damage taken.'; case 'Absorption': return `Increasing construct RedPower and BluePower by ${i.meta[1]}`; - case 'Triage': return `Construct will be healed for ${multiplier(i.tick.skill)}% of caster's GreenPower each turn.`; - case 'Siphon': return `Construct will take ${multiplier(i.tick.skill)}% of caster's BluePower + GreenPower as blue damage each turn, healing the caster.`; + case 'Triage': return `Construct will be healed for ${i.meta[1].amount} green life each turn.`; + case 'Siphon': return `Construct will take ${i.meta[1].amount} blue damage each turn, healing the caster.`; default: return 'Missing Effect Text'; - }*/ + } } module.exports = { diff --git a/core/Cargo.toml b/core/Cargo.toml index 57a24193..64372ae4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml_core" -version = "1.10.0" +version = "1.11.0" authors = ["ntr ", "mashy "] [dependencies] diff --git a/core/fixme.md b/core/fixme.md index 8223a821..b9888748 100644 --- a/core/fixme.md +++ b/core/fixme.md @@ -1,6 +1,5 @@ # FIXME -check silence skill multiplier game ready not auto starting resolve phase -cooldowns set after cast -cooldowns reduced after 1 complete cast +remove big header and move to rhs of news pane +add big logo w/ noise when you mouseover stuff etc diff --git a/core/src/construct.rs b/core/src/construct.rs index 47c60095..5eb26e17 100644 --- a/core/src/construct.rs +++ b/core/src/construct.rs @@ -1,5 +1,3 @@ -use std::iter; - use uuid::Uuid; use rand::prelude::*; @@ -435,12 +433,14 @@ impl Construct { } pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct { + // println!("{:?} {:?} skill cooldown set", self.name, skill); + // tests force resolve some skills // which cause the game to attempt to put them on cd // even though the construct doesn't know the skill if let Some(i) = self.skills.iter().position(|s| s.skill == skill) { self.skills.remove(i); - self.skills.push(ConstructSkill::new(skill)); + self.skills.insert(i, ConstructSkill::new(skill)); } self @@ -493,11 +493,13 @@ impl Construct { self.effects = self.effects.clone().into_iter().filter_map(|mut effect| { effect.duration = effect.duration.saturating_sub(1); + // println!("{:?}", effect); + if effect.duration == 0 { return None; } - // info!("reduced effect {:?}", effect); + info!("reduced effect {:?}", effect); return Some(effect); }).collect::>(); @@ -946,7 +948,10 @@ impl Construct { if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] } if let Some(p) = self.effects.iter().position(|ce| ce.effect == effect) { - self.effects.remove(p); + let ce = self.effects.remove(p); + + if ce.effect.hidden() { return vec![] } + return vec![Event::Removal { construct: self.id, effect: effect, @@ -982,11 +987,13 @@ impl Construct { if self.is_ko() { return vec![Event::TargetKo { construct: self.id }] } while let Some(ce) = self.effects.pop() { - removals.push(Event::Removal { - construct: self.id, - effect: ce.effect, - display: EventConstruct::new(self), - }); + if !ce.effect.hidden() { + removals.push(Event::Removal { + construct: self.id, + effect: ce.effect, + display: EventConstruct::new(self), + }); + } } return removals; @@ -1012,7 +1019,7 @@ impl Construct { match meta { Some(EffectMeta::CastOnHit(skill)) => - casts.push(Cast::new(self.id, self.account, cast.target, *skill)), + casts.push(Cast::new(self.id, self.account, cast.source, *skill)), _ => panic!("no electrify skill {:?}", meta), }; } @@ -1119,9 +1126,9 @@ mod tests { construct.apply_modifiers(&player_colours); - assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); - assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(50)); - assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(70)); + assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(15)); + assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(24)); + assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(37)); return; } @@ -1170,7 +1177,7 @@ mod tests { let colours = Colours::from_construct(&construct); assert!(colours.red == 4); - assert!(colours.blue == 20); + assert!(colours.blue == 10); assert!(colours.green == 2); } @@ -1191,9 +1198,9 @@ mod tests { construct.apply_modifiers(&player_colours); - assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(35)); - assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(25)); - assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(25)); + assert!(construct.red_power.value == construct.red_power.base + construct.red_power.base.pct(15)); + assert!(construct.green_power.value == construct.green_power.base + construct.green_power.base.pct(10)); + assert!(construct.blue_power.value == construct.blue_power.base + construct.blue_power.base.pct(10)); return; } diff --git a/core/src/effect.rs b/core/src/effect.rs index 7db563f8..c1d98d39 100644 --- a/core/src/effect.rs +++ b/core/src/effect.rs @@ -75,38 +75,19 @@ impl Effect { match self { Effect::Banish => true, - // delete sustain immunitiy??? - /*Effect::Sustain => [ - Skill::Stun, - Skill::Silence, - Skill::SilencePlus, - Skill::SilencePlusPlus, - Skill::Ruin, - Skill::RuinPlus, - Skill::RuinPlusPlus, - Skill::Restrict, - Skill::RestrictPlus, - Skill::RestrictPlusPlus - ].contains(&skill),*/ - + // these provide immunity for the ticks + // the base skills will still resolve + // but they have early return checks + // to ensure the effect is reapplied but damage is not Effect::Siphoned => [ - Skill::Siphon, - Skill::SiphonPlus, - Skill::SiphonPlusPlus, Skill::SiphonTick, ].contains(&skill), Effect::Decayed => [ - Skill::Decay, - Skill::DecayPlus, - Skill::DecayPlusPlus, Skill::DecayTick, ].contains(&skill), Effect::Triaged => [ - Skill::Triage, - Skill::TriagePlus, - Skill::TriagePlusPlus, Skill::TriageTick, ].contains(&skill), diff --git a/core/src/game.rs b/core/src/game.rs index 6724e12b..fcea46ab 100644 --- a/core/src/game.rs +++ b/core/src/game.rs @@ -135,20 +135,6 @@ impl Game { .unwrap() } - pub fn update_construct(&mut self, construct: &mut Construct) -> &mut Game { - match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == construct.id)) { - Some(player) => { - let index = player.constructs.iter().position(|t| t.id == construct.id).unwrap(); - player.constructs.remove(index); - player.constructs.push(construct.clone()); - player.constructs.sort_unstable_by_key(|c| c.id); - }, - None => panic!("construct not in game"), - }; - - self - } - pub fn can_start(&self) -> bool { return self.players.len() == self.player_num && self.players.iter().all(|t| t.constructs.len() == self.player_constructs) @@ -350,7 +336,11 @@ impl Game { pub fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { - self.player_by_id(player_id)?; + let player = self.player_by_id(player_id)?; + if player.ready { + return Err(err_msg("cannot clear skills while ready")); + } + if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } @@ -398,6 +388,11 @@ impl Game { let mut sorted = self.stack.clone(); sorted.iter_mut() .for_each(|s| { + + // we do not modify the speed of ticks + // as they are considered to be pinned to the speed + // that they were initially cast + if !s.skill.is_tick() { let caster = self.construct_by_id(s.source).unwrap(); let speed = caster.skill_speed(s.skill); @@ -462,7 +457,15 @@ impl Game { self.skill_phase_start(r_animation_ms) } - fn modify_cast(&self, cast: Cast) -> Vec { + fn modify_cast(&self, mut cast: Cast) -> Vec { + + // reassign the speeds based on the caster + // for test purposes + if !cast.skill.is_tick() { + let speed = self.construct(cast.source).skill_speed(cast.skill); + cast.speed = speed; + } + let target_player = self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == cast.target)) .unwrap(); @@ -508,17 +511,19 @@ impl Game { self.resolve(Cast { skill, ..cast }); } - // for aoe events send the source / target animations before each set of casts - if cast.skill.aoe() { - if cast.skill.cast_animation() { - let event = self.cast(cast); - self.add_resolution(&cast, &event); + let casts = self.modify_cast(cast); + + let castable = casts + .iter() + .any(|c| !self.construct(c.target).is_ko() && !self.construct(c.target).immune(c.skill).is_some()); + + if castable { + self.action(cast, Action::Cast); + if cast.skill.aoe() { + self.action(cast, Action::HitAoe); } - let event = self.hit_aoe(cast); - self.add_resolution(&cast, &event); } - let casts = self.modify_cast(cast); for cast in casts { self.execute(cast); } @@ -550,6 +555,10 @@ impl Game { return self.resolve(Cast { target: cast.source, ..cast }); } + if !cast.skill.aoe() { + self.action(cast, Action::Hit); + } + cast.resolve(self); self @@ -559,6 +568,7 @@ impl Game { let new_events = match action { Action::Cast => vec![self.cast(cast)], Action::Hit => vec![self.hit(cast)], + Action::HitAoe => vec![self.hit_aoe(cast)], Action::Damage { construct, amount, colour } => self.damage(construct, amount, colour), Action::Heal { construct, amount, colour } => self.heal(construct, amount, colour), @@ -642,11 +652,14 @@ impl Game { Value::TickDamage { construct, effect } => self.construct(construct).stat(Stat::TickDamage(effect)), - // Skills { construct: Uuid, colour: Colour }, } } + pub fn affected(&self, construct: Uuid, effect: Effect) -> bool { + self.construct(construct).affected(effect) + } + fn cast(&mut self, cast: Cast) -> Event { Event::Cast { construct: cast.source, player: cast.player, target: cast.target, skill: cast.skill, direction: self.direction(cast) } } @@ -843,11 +856,13 @@ pub enum Value { Removals { construct: Uuid }, DamageReceived { construct: Uuid, colour: Colour }, TickDamage { construct: Uuid, effect: Effect }, + // Affected { construct: Uuid, effect: Effect }, // not an int :( } #[derive(Debug,Clone,PartialEq)] pub enum Action { Hit, + HitAoe, Cast, Heal { construct: Uuid, amount: usize, colour: Colour }, Damage { construct: Uuid, amount: usize, colour: Colour }, @@ -1033,6 +1048,7 @@ mod tests { .learn(Skill::Siphon) .learn(Skill::Amplify) .learn(Skill::Stun) + .learn(Skill::Ruin) .learn(Skill::Block) .learn(Skill::Sleep) .learn(Skill::Decay); @@ -1251,6 +1267,32 @@ mod tests { assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); } + + #[test] + fn ruin_cooldown_test() { + let mut game = create_test_game(); + + let x_player = game.players[0].clone(); + let y_player = game.players[1].clone(); + + let x_construct = x_player.constructs[0].clone(); + let y_construct = y_player.constructs[0].clone(); + + while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Ruin).is_some() { + game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); + } + + game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Ruin).unwrap(); + + game.player_ready(x_player.id).unwrap(); + game.player_ready(y_player.id).unwrap(); + + game = game.resolve_phase_start(); + + assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Ruin).is_some()); + } + + // #[cfg(test)] // mod tests { // use skill::*; @@ -1595,55 +1637,6 @@ mod tests { // } // } - - #[test] - fn sleep_cooldown_test() { - let mut game = create_test_game(); - - let x_player = game.players[0].clone(); - let y_player = game.players[1].clone(); - - let x_construct = x_player.constructs[0].clone(); - let y_construct = y_player.constructs[0].clone(); - - - for _n in 1..10 { - // should auto progress back to skill phase - assert!(game.phase == Phase::Skill); - - // Sleep 2T CD - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); - - game.player_ready(x_player.id).unwrap(); - game.player_ready(y_player.id).unwrap(); - game = game.resolve_phase_start(); - - // Sleep 1T CD - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); - - game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Decay).unwrap(); - // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); - game.player_ready(x_player.id).unwrap(); - game.player_ready(y_player.id).unwrap(); - game = game.resolve_phase_start(); - - // Sleep 0T CD (we use it here) - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_none()); - - game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Sleep).unwrap(); - game.player_ready(x_player.id).unwrap(); - game.player_ready(y_player.id).unwrap(); - game = game.resolve_phase_start(); - - // Sleep back to 2T CD - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Decay).is_none()); - assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Sleep).is_some()); - } - } - // #[test] // fn counter_test() { // let mut game = create_test_game(); @@ -1712,56 +1705,6 @@ mod tests { // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Electrocute)); // } - // // #[test] - // // fn link_test() { - // // let mut game = create_test_game(); - - // // let x_player = game.players[0].clone(); - // // let y_player = game.players[1].clone(); - - // // let x_construct = x_player.constructs[0].clone(); - // // let y_construct = y_player.constructs[0].clone(); - - // // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Link); - - // // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Link).is_some() { - // // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); - // // } - - // // // apply buff - // // game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Link).unwrap(); - // // game.player_ready(x_player.id).unwrap(); - // // game.player_ready(y_player.id).unwrap(); - // // game = game.resolve_phase_start(); - // // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Link)); - - // // let Resolution { source: _, target: _, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap(); - // // match Resolution { - // // Resolution::Effect { effect, skill: _, duration: _, construct_effects: _ } => assert_eq!(effect, Effect::Link), - // // _ => panic!("not siphon"), - // // }; - - // // let Resolution { source: _, target: _, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap(); - // // match Resolution { - // // Resolution::Recharge { red: _, blue: _, skill: _ } => (), - // // _ => panic!("link result was not recharge"), - // // } - - // // // attack and receive link hit - // // game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::Attack).unwrap(); - // // game.player_ready(x_player.id).unwrap(); - // // game.player_ready(y_player.id).unwrap(); - // // game = game.resolve_phase_start(); - - // // let Resolution { source: _, target, Resolution, stages: _ } = game.Resolutions.last.unwrap().pop().unwrap(); - // // assert_eq!(target.id, y_construct.id); - // // match Resolution { - // // Resolution::Damage { amount, skill: _, mitigation: _, colour: _} => - // // assert_eq!(amount, x_construct.red_power().pct(Skill::Attack.multiplier()) >> 1), - // // _ => panic!("not damage link"), - // // }; - // // } - // // #[test] // // fn absorb_test() { // // let mut game = create_test_game(); @@ -2015,7 +1958,7 @@ mod tests { let mut game = create_2v2_test_game(); game.players[0].set_ready(true); game.phase_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap()); - game = game.upkeep(); + game.upkeep(); // assert!(game.players[1].warnings == 1); } @@ -2026,7 +1969,7 @@ mod tests { let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.add_skill(player_id, source, target, Skill::Attack).unwrap(); - game = game.resolve_phase_start(); + game.resolve_phase_start(); } #[test] @@ -2122,6 +2065,43 @@ mod tests { })); } + + + #[test] + fn link_test() { + let mut game = create_2v2_test_game(); + let player_id = game.players[0].id; + let source = game.players[0].constructs[0].id; + let target = game.players[1].constructs[0].id; + game.players[1].constructs[0].blue_life.force(0); + + game.new_resolve(Cast::new(source, player_id, target, Skill::Link)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Damage { construct, colour, amount, mitigation: _, display: _ } => + construct == target && amount == 320.pct(50) && colour == Colour::Blue, + _ => false, + })); + + game = game.resolve_phase_start(); + game.new_resolve(Cast::new(source, player_id, target, Skill::Triage)); + game.new_resolve(Cast::new(source, player_id, target, Skill::Link)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Damage { construct, colour, amount, mitigation: _, display: _ } => + construct == target && amount == 320.pct(75) && colour == Colour::Blue, + _ => false, + })); + + + } + #[test] fn siphon_test() { let mut game = create_2v2_test_game(); @@ -2158,6 +2138,9 @@ mod tests { // que ota? game.resolve(Cast::new(source, player_id, target, Skill::Siphon)); + game.resolve(Cast::new(source, player_id, target, Skill::Siphon)); + game.resolve(Cast::new(source, player_id, target, Skill::Siphon)); + let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; @@ -2166,7 +2149,75 @@ mod tests { _ => false, }).count(); + let effect_events = resolutions.iter().filter(|r| match r.event { + Event::Effect { construct, effect, duration: _, display: _ } => + construct == target && effect == Effect::Siphon, + _ => false, + }).count(); + + // Deal siphon dmg once assert_eq!(damage_events, 1); + // 3 new applications of siphon + assert_eq!(effect_events, 3); + // Siphon + Siphoned + assert!(game.players[1].constructs[0].effects.len() == 2); + } + + #[test] + fn hybrid_test() { + let mut game = create_2v2_test_game(); + let player_id = game.players[0].id; + let source = game.players[0].constructs[0].id; + let target = game.players[1].constructs[0].id; + + game.players[1].constructs[0].blue_life.force(0); + + + game.resolve(Cast::new(source, player_id, source, Skill::Hybrid)); + game.resolve(Cast::new(source, player_id, target, Skill::Siphon)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.skill { + Skill::HybridBlast => true, + _ => false + })); + + assert!(resolutions.iter().filter(|r| match r.event { + Event::Damage { construct: _, colour: _, amount: _, mitigation: _, display: _ } => true, + _ => false, + }).count() == 2); + + + let _siphon_dmg = resolutions.iter().find_map(|r| match r.skill { + Skill::Siphon => { + match r.event { + Event::Damage { construct: _, colour: _, amount, mitigation: _, display: _ } => Some(amount), + _ => None, + } + }, + _ => None + }).expect("no siphon dmg"); + + // let hybrid_dmg = resolutions.iter().find_map(|r| match r.skill { + // Skill::HybridBlast => { + // match r.event { + // Event::Damage { construct: _, colour: _, amount, mitigation: _, display: _ } => Some(amount), + // _ => None, + // } + // }, + // _ => None + // }).expect("no hybrid dmg"); + + // assert!(resolutions.iter().any(|r| match r.event { + // Event::Healing { construct, colour, amount, overhealing, display: _ } => { + // construct == source && (amount + overhealing) == siphon_dmg && colour == Colour::Green + // // this works + // // construct == source && (amount + overhealing) == (siphon_dmg + hybrid_dmg) && colour == Colour::Green + // }, + // _ => false, + // })); } #[test] @@ -2189,6 +2240,85 @@ mod tests { })); } + #[test] + fn electrify_test() { + let mut game = create_2v2_test_game(); + let player_id = game.players[0].id; + let source = game.players[0].constructs[0].id; + let target = game.players[1].constructs[0].id; + + game.resolve(Cast::new(source, player_id, target, Skill::Electrify)); + game.resolve(Cast::new(source, player_id, target, Skill::Blast)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Damage { construct, colour, amount, mitigation: _, display: _ } => + construct == source && amount > 0 && colour == Colour::Blue, + _ => false, + })); + } + + #[test] + fn triage_test() { + let mut game = create_2v2_test_game(); + let player_id = game.players[0].id; + let source = game.players[0].constructs[0].id; + let target = game.players[1].constructs[0].id; + + game.resolve(Cast::new(source, player_id, target, Skill::Strike)); + game.resolve(Cast::new(source, player_id, target, Skill::Triage)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Healing { construct, colour, amount, overhealing: _, display: _ } => + construct == target && amount > 0 && colour == Colour::Green, + _ => false, + })); + + // it's hidden + // assert!(resolutions.iter().any(|r| match r.event { + // Event::Effect { construct, effect, duration: _, display: _ } => + // construct == target && effect == Effect::Triaged, + // _ => false, + // })); + + game.progress_durations(); // pretend it's a new turn + game = game.resolve_phase_start(); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Healing { construct, colour, amount: _, overhealing, display: _ } => + construct == target && overhealing > 0 && colour == Colour::Green, + _ => false, + })); + } + + #[test] + fn counter_test() { + let mut game = create_2v2_test_game(); + let player_id = game.players[0].id; + let source = game.players[0].constructs[0].id; + let target = game.players[1].constructs[0].id; + + game.resolve(Cast::new(source, player_id, target, Skill::Counter)); + game.resolve(Cast::new(source, player_id, target, Skill::Strike)); + + let last = game.resolutions.len() - 1; + let resolutions = &game.resolutions[last]; + + assert!(resolutions.iter().any(|r| match r.event { + Event::Damage { construct, colour, amount, mitigation: _, display: _ } => + construct == source && amount > 0 && colour == Colour::Red, + _ => false, + })); + } + #[test] fn absorb_test() { let mut game = create_2v2_test_game(); @@ -2376,6 +2506,8 @@ mod tests { }) .unwrap(); + assert!(siphon_tick_speed > 0); + assert!(siphon_speed > 0); assert_eq!(siphon_tick_dmg, siphon_dmg); assert_eq!(siphon_tick_speed, siphon_speed); } diff --git a/core/src/instance.rs b/core/src/instance.rs index 27d09bc0..79467015 100644 --- a/core/src/instance.rs +++ b/core/src/instance.rs @@ -11,7 +11,6 @@ use chrono::prelude::*; use chrono::Duration; use player::{Player, Score}; -use mob::{bot_player, instance_mobs}; use game::{Game}; use item::{Item}; use vbox; @@ -144,8 +143,8 @@ impl Instance { .collect::>() } + // time out lobbies that have been open too long pub fn upkeep(mut self) -> (Instance, Option) { - // time out lobbies that have been open too long if self.phase == InstancePhase::Lobby && self.phase_timed_out() { self.finish(); return (self, None); @@ -504,6 +503,7 @@ impl Instance { #[cfg(test)] mod tests { use super::*; + use mob::{bot_player, instance_mobs}; #[test] fn instance_pve_test() { diff --git a/core/src/item.rs b/core/src/item.rs index 43b2a73e..185b1832 100644 --- a/core/src/item.rs +++ b/core/src/item.rs @@ -716,7 +716,7 @@ impl Item { Item::HealPlusPlus | Item::Triage | Item::TriagePlus | - Item::TriagePlusPlus | + Item::TriagePlusPlus | Item::Break | Item::BreakPlus | Item::BreakPlusPlus | @@ -735,7 +735,7 @@ impl Item { Item::Invert | Item::InvertPlus | Item::InvertPlusPlus | - Item::Decay | + Item::Decay | Item::DecayPlus | Item::DecayPlusPlus | Item::Siphon| @@ -817,7 +817,7 @@ impl Item { Item::AmplifyPlus => vec![Item::Amplify, Item::Amplify], Item::AmplifyPlusPlus => vec![Item::AmplifyPlus, Item::AmplifyPlus], - + Item::HastePlus => vec![Item::Haste, Item::Haste], Item::HastePlusPlus => vec![Item::HastePlus, Item::HastePlus], @@ -832,9 +832,9 @@ impl Item { Item::Curse => vec![Item::Debuff, Item::Red, Item::Blue], Item::Decay => vec![Item::Debuff, Item::Green, Item::Blue], - Item::PurgePlus => vec![Item::Purge, Item::Purge], + Item::PurgePlus => vec![Item::Purge, Item::Purge], Item::PurgePlusPlus => vec![Item::PurgePlus, Item::PurgePlus], - + Item::InvertPlus => vec![Item::Invert, Item::Invert], Item::InvertPlusPlus => vec![Item::InvertPlus, Item::InvertPlus], @@ -843,10 +843,10 @@ impl Item { Item::SilencePlus => vec![Item::Silence, Item::Silence], Item::SilencePlusPlus => vec![Item::SilencePlus, Item::SilencePlus], - + Item::CursePlus => vec![Item::Curse, Item::Curse], Item::CursePlusPlus => vec![Item::CursePlus, Item::CursePlus], - + Item::DecayPlus => vec![Item::Decay, Item::Decay], Item::DecayPlusPlus => vec![Item::DecayPlus, Item::DecayPlus], @@ -1427,20 +1427,13 @@ mod tests { assert_eq!(Item::StrikePlus.components(), vec![ Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack, - Item::Red, Item::Red, Item::Attack, ]); assert_eq!(Item::StrikePlusPlus.components(), vec![ Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack, - Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack, Item::Red, Item::Red, Item::Attack, - Item::Red, Item::Red, Item::Attack, - - Item::Red, Item::Red, Item::Attack, - Item::Red, Item::Red, Item::Attack, - Item::Red, Item::Red, Item::Attack, ]); } diff --git a/core/src/mob.rs b/core/src/mob.rs index 479acb43..5d774c13 100644 --- a/core/src/mob.rs +++ b/core/src/mob.rs @@ -31,6 +31,11 @@ pub fn bot_player() -> Player { Player::new(bot_id, None, &name(), constructs).set_bot(true) } +pub fn anon_player(id: Uuid) -> Player { + let constructs = instance_mobs(id); + Player::new(id, None, &"player".to_string(), constructs) +} + pub fn anim_test_game(skill: Skill) -> Game { let mut rng = thread_rng(); let mut game = Game::new(); diff --git a/core/src/player.rs b/core/src/player.rs index 5e5dde9c..21556a0b 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap}; use uuid::Uuid; -use rand::prelude::*; use failure::Error; use failure::err_msg; @@ -130,7 +129,7 @@ impl Player { } pub fn autobuy(&mut self) -> &mut Player { - let mut rng = thread_rng(); + // let mut rng = thread_rng(); // skill buying phase while self.constructs.iter().any(|c| c.skills.len() < 3) { diff --git a/core/src/skill.rs b/core/src/skill.rs index 6599ba96..91aba876 100644 --- a/core/src/skill.rs +++ b/core/src/skill.rs @@ -42,13 +42,6 @@ impl Cast { } pub fn resolve(self, game: &mut Game) { - if !self.skill.aoe() { - if self.skill.cast_animation() { - game.action(self, Action::Cast); - } - game.action(self, Action::Hit); - } - match self.skill { Skill::Attack => attack(self, game, Attack::Base), Skill::Block => block(self, game, Block::Base), @@ -193,9 +186,6 @@ impl Cast { Skill::TriageTick => triage_tick(self, game), }; - - // actions.append(&mut rest); - // return actions; } } @@ -647,6 +637,9 @@ impl Skill { Skill::Heal | Skill::HealPlus | Skill::HealPlusPlus | + Skill::Hybrid | + Skill::HybridPlus | + Skill::HybridPlusPlus | Skill::Absorb | Skill::AbsorbPlus | Skill::AbsorbPlusPlus | @@ -722,7 +715,7 @@ impl Skill { .collect::>(); } - fn base(&self) -> Skill { + fn _base(&self) -> Skill { let bases = [Item::Attack, Item::Stun, Item::Buff, Item::Debuff, Item::Block]; match self.components() .iter() @@ -996,6 +989,10 @@ fn siphon(cast: Cast, game: &mut Game, values: Siphon) { } ); + // should only reapply the dot if they have already been hit by the dmg + // from either this or the tick + if game.affected(cast.target, Effect::Siphoned) { return; } + game.action(cast, Action::Damage { construct: cast.target, @@ -1587,6 +1584,8 @@ fn decay(cast: Cast, game: &mut Game, values: Decay) { } ); + if game.affected(cast.target, Effect::Decayed) { return; } + game.action(cast, Action::Effect { construct: cast.target, @@ -1685,19 +1684,27 @@ impl Electrocute { fn electrocute(cast: Cast, game: &mut Game, values: Electrocute) { let amount = game.value(Value::Stat { construct: cast.source, stat: Stat::BluePower }).pct(values.damage_multiplier()); - game.action(cast, - Action::Remove { - construct: cast.source, - effect: Effect::Electrocute, - } - ); game.action(cast, Action::Effect { construct: cast.target, - effect: ConstructEffect { effect: Effect::Electric, duration: values.duration(), meta: + effect: ConstructEffect { effect: Effect::Electrocute, duration: values.duration(), meta: Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::ElectrocuteTick, speed: cast.speed, amount }) }, }, ); + game.action(cast, + Action::Damage { + construct: cast.target, + colour: Colour::Blue, + amount, + }, + ); + + game.action(cast, + Action::Remove { + construct: cast.source, + effect: Effect::Electric, + } + ); } fn electrocute_tick(cast: Cast, game: &mut Game) { @@ -1847,11 +1854,16 @@ fn link(cast: Cast, game: &mut Game, values: Link) { effect: ConstructEffect { effect: Effect::Stun, duration: values.duration(), meta: None }, } ); + + let bp = game.value(Value::Stat { construct: cast.source, stat: Stat::BluePower }).pct(values.blue_dmg_base()); + let links = game.value(Value::Effects { construct: cast.target }); + let amount = bp.pct(100 + 100usize.saturating_mul(links)); + game.action(cast, Action::Damage { construct: cast.target, colour: Colour::Blue, - amount: game.value(Value::Effects { construct: cast.target }).pct(values.blue_dmg_base()), + amount, }, ); } @@ -2089,7 +2101,7 @@ fn sleep(cast: Cast, game: &mut Game, values: Sleep) { Action::Heal { construct: cast.target, amount: game.value(Value::Stat { construct: cast.source, stat: Stat::GreenPower }).pct(values.green_heal_multi()), - colour: Colour::Blue, + colour: Colour::Green, }, ); } @@ -2188,6 +2200,9 @@ fn triage(cast: Cast, game: &mut Game, values: Triage) { Some(EffectMeta::CastTick { source: cast.source, target: cast.target, skill: Skill::TriageTick, speed: cast.speed, amount }) }, } ); + + if game.affected(cast.target, Effect::Triaged) { return; } + game.action(cast, Action::Heal { construct: cast.target, @@ -2208,7 +2223,7 @@ fn triage_tick(cast: Cast, game: &mut Game) { Action::Heal { construct: cast.target, colour: Colour::Green, - amount: game.value(Value::TickDamage { construct: cast.target, effect: Effect::Electrocute }), + amount: game.value(Value::TickDamage { construct: cast.target, effect: Effect::Triage }), } ); game.action(cast, diff --git a/core/src/vbox.rs b/core/src/vbox.rs index 71a37ce2..6110f2ec 100644 --- a/core/src/vbox.rs +++ b/core/src/vbox.rs @@ -1,5 +1,3 @@ -use uuid::Uuid; - use std::iter; use std::collections::HashMap; @@ -11,9 +9,6 @@ use rand::distributions::{WeightedIndex}; use failure::Error; use failure::err_msg; -use instance::{Instance}; -use construct::{Colours}; - use item::*; pub type VboxIndices = Option>>; @@ -40,9 +35,9 @@ const STARTING_ATTACK_COUNT: usize = 3; impl Vbox { pub fn new() -> Vbox { - let mut colours: HashMap = HashMap::new(); - let mut skills: HashMap = HashMap::new(); - let mut specs: HashMap = HashMap::new(); + let colours: HashMap = HashMap::new(); + let skills: HashMap = HashMap::new(); + let specs: HashMap = HashMap::new(); let store = [ (ItemType::Colours, colours), @@ -220,6 +215,7 @@ impl Vbox { #[cfg(test)] mod tests { use super::*; + use construct::{Colours}; #[test] fn combine_test() { diff --git a/ops/package.json b/ops/package.json index d4b90bb7..2fc06500 100644 --- a/ops/package.json +++ b/ops/package.json @@ -1,6 +1,6 @@ { "name": "mnml-ops", - "version": "1.10.1", + "version": "1.11.0", "description": "", "main": "index.js", "scripts": { diff --git a/server/Cargo.toml b/server/Cargo.toml index 4b409c11..2dfe13f5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml" -version = "1.10.1" +version = "1.11.0" authors = ["ntr "] [dependencies] @@ -25,6 +25,7 @@ dotenv = "0.9.0" log = "0.4" fern = { version = "0.5", features = ["colored", "syslog-4"] } syslog = "4" +log-panics = "2" iron = "0.6" bodyparser = "0.8" diff --git a/server/src/account.rs b/server/src/account.rs index 7373d0f6..08d1ca74 100644 --- a/server/src/account.rs +++ b/server/src/account.rs @@ -42,7 +42,17 @@ impl Account { false => None, }; - Ok(Player::new(self.id, Some(self.img), &self.name, constructs)) + Ok(Player::new(self.id, img, &self.name, constructs)) + } + + pub fn anonymous() -> Account { + Account { + id: Uuid::new_v4(), + img: Uuid::new_v4(), + name: "you".to_string(), + balance: 0, + subscribed: false, + } } } @@ -93,7 +103,7 @@ pub fn chat_wheel(_db: &Db, _id: Uuid) -> Result, Error> { ]) } -pub fn select_name(db: &Db, name: &String) -> Result { +pub fn _select_name(db: &Db, name: &String) -> Result { let query = " SELECT id, name, balance, subscribed, img FROM accounts diff --git a/server/src/events.rs b/server/src/events.rs index 0a347868..8208c821 100644 --- a/server/src/events.rs +++ b/server/src/events.rs @@ -6,7 +6,7 @@ use std::time; use uuid::Uuid; use failure::Error; -use failure::{err_msg, format_err}; +use failure::{format_err}; use crossbeam_channel::{Sender, Receiver}; @@ -15,10 +15,9 @@ use names; use rpc::RpcMessage; use warden::{GameEvent}; -use mail::Mail; pub type EventsTx = Sender; -type Id = usize; +type Id = Uuid; // this is pretty heavyweight // but it makes the ergonomics easy @@ -35,7 +34,6 @@ pub struct Events { pub tx: Sender, rx: Receiver, - mail: Sender, warden: Sender, clients: HashMap, } @@ -43,7 +41,7 @@ pub struct Events { #[derive(Debug,Clone)] pub enum Event { // ws lifecycle - Connect(Id, Option, Sender), + Connect(Id, Account, Sender), Disconnect(Id), Subscribe(Id, Uuid), Unsubscribe(Id, Uuid), @@ -64,7 +62,6 @@ pub enum Event { struct WsClient { id: Id, - account: Option, tx: Sender, subs: HashSet, chat: Option<(Uuid, String)>, @@ -73,12 +70,11 @@ struct WsClient { } impl Events { - pub fn new(tx: Sender, rx: Receiver, warden: Sender, mail: Sender) -> Events { + pub fn new(tx: Sender, rx: Receiver, warden: Sender) -> Events { Events { tx, rx, warden, - mail, clients: HashMap::new(), } } @@ -102,30 +98,13 @@ impl Events { } } - fn get_client(&mut self, id: Id) -> Result<&mut WsClient, Error> { - match self.clients.get_mut(&id) { - Some(c) => Ok(c), - None => Err(format_err!("connection not found id={:?}", id)), - } - } - - fn remove_client(&mut self, id: Id) { - self.clients.remove(&id); - } - fn event(&mut self, msg: Event) -> Result<(), Error> { match msg { Event::Connect(id, account, tx) => { info!("connect id={:?} account={:?}", id, account); - let account_id = match account { - Some(a) => Some(a.id), - None => None, - }; - let client = WsClient { id, tx, - account: account_id, subs: HashSet::new(), pvp: false, invite: None, @@ -164,7 +143,7 @@ impl Events { trace!("unsubscribe id={:?} object={:?}", id, obj); match self.clients.get_mut(&id) { - Some(mut client) => { + Some(client) => { client.subs.remove(&obj); trace!("unsubscribe subscriptions removed={:?}", client.subs.len()); Ok(()) @@ -183,13 +162,10 @@ impl Events { if client.subs.contains(&id) { subs += 1; - let redacted = match client.account { - Some(a) => match msg { - RpcMessage::InstanceState(ref i) => RpcMessage::InstanceState(i.clone().redact(a)), - RpcMessage::GameState(ref i) => RpcMessage::GameState(i.clone().redact(a)), - _ => msg.clone(), - } - None => msg.clone(), + let redacted = match msg { + RpcMessage::InstanceState(ref i) => RpcMessage::InstanceState(i.clone().redact(client.id)), + RpcMessage::GameState(ref i) => RpcMessage::GameState(i.clone().redact(client.id)), + _ => msg.clone(), }; match client.tx.send(redacted) { @@ -204,7 +180,9 @@ impl Events { if !dead.is_empty() { trace!("dead connections={:?}", dead.len()); - dead.iter().for_each(|id| self.remove_client(*id)); + for id in dead.iter() { + self.clients.remove(id); + } } trace!("push subscribers={:?}", subs); @@ -218,18 +196,14 @@ impl Events { let c = self.clients.get(&id) .ok_or(format_err!("connection not found id={:?}", id))?; - if let None = c.account { - return Err(err_msg("cannot join pvp queue anonymously")); - } - - info!("pvp queue request id={:?} account={:?}", c.id, c.account); + info!("pvp queue request id={:?} account={:?}", c.id, c.id); } // create the req for the already queued opponent if let Some(opp_req) = match self.clients.iter_mut().find(|(c_id, c)| c.pvp && **c_id != id) { Some((q_id, q)) => { q.pvp = false; - Some(PvpRequest { id: *q_id, account: q.account.unwrap(), tx: q.tx.clone() }) + Some(PvpRequest { id: *q_id, account: q.id, tx: q.tx.clone() }) }, None => None, } { @@ -237,7 +211,7 @@ impl Events { let c = self.clients.get_mut(&id) .ok_or(format_err!("connection not found id={:?}", id))?; - let player_req = PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }; + let player_req = PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() }; self.warden.send(GameEvent::Match((opp_req, player_req)))?; return Ok(()) @@ -247,7 +221,7 @@ impl Events { let requester = self.clients.get_mut(&id).unwrap(); requester.pvp = true; requester.tx.send(RpcMessage::QueueJoined(()))?; - info!("joined game queue id={:?} account={:?}", requester.id, requester.account); + info!("joined game queue id={:?} account={:?}", requester.id, requester.id); return Ok(()); }, @@ -256,12 +230,8 @@ impl Events { let c = self.clients.get_mut(&id) .ok_or(format_err!("connection not found id={:?}", id))?; - if let None = c.account { - return Err(err_msg("cannot join pvp queue anonymously")); - } - let code = names::name().split_whitespace().collect::>().join("-"); - info!("pvp invite request id={:?} account={:?} code={:?}", c.id, c.account, code); + info!("pvp invite request id={:?} account={:?} code={:?}", c.id, c.id, code); c.invite = Some(code.clone()); c.tx.send(RpcMessage::Invite(code))?; return Ok(()); @@ -272,11 +242,7 @@ impl Events { let c = self.clients.get(&id) .ok_or(format_err!("connection not found id={:?}", id))?; - if let None = c.account { - return Err(err_msg("cannot join pvp queue anonymously")); - } - - info!("pvp join request id={:?} account={:?} code={:?}", c.id, c.account, code); + info!("pvp join request id={:?} account={:?} code={:?}", c.id, c.id, code); let inv = self.clients.iter() .filter(|(_id, c)| c.invite.is_some()) @@ -284,10 +250,10 @@ impl Events { Some(ref c) => *c == code, None => false, }) - .map(|(_id, c)| PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }) + .map(|(_id, c)| PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() }) .ok_or(format_err!("invite expired code={:?}", code))?; - let join = PvpRequest { id: c.id, account: c.account.unwrap(), tx: c.tx.clone() }; + let join = PvpRequest { id: c.id, account: c.id, tx: c.tx.clone() }; self.warden.send(GameEvent::Match((join, inv)))?; return Ok(()); @@ -310,7 +276,7 @@ impl Events { c.pvp = false; c.tx.send(RpcMessage::QueueLeft(()))?; - info!("left game queue id={:?} account={:?}", c.id, c.account); + info!("left game queue id={:?} account={:?}", c.id, c.id); return Ok(()); }, @@ -337,12 +303,11 @@ impl Events { // now collect all listeners of this instance let chat_state: HashMap = self.clients.iter() - .filter(|(_id, c)| c.account.is_some()) .filter(|(_id, c)| match c.chat { Some(ref chat) => chat.0 == instance, None => false, }) - .map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1)) + .map(|(_id, c)| (c.id, c.chat.clone().unwrap().1)) .collect(); return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state))); @@ -357,12 +322,11 @@ impl Events { } let chat_state: HashMap = self.clients.iter() - .filter(|(_id, c)| c.account.is_some()) .filter(|(_id, c)| match c.chat { Some(ref chat) => chat.0 == instance, None => false, }) - .map(|(_id, c)| (c.account.unwrap(), c.chat.clone().unwrap().1)) + .map(|(_id, c)| (c.id, c.chat.clone().unwrap().1)) .collect(); return self.event(Event::Push(instance, RpcMessage::InstanceChat(chat_state))); diff --git a/server/src/http.rs b/server/src/http.rs index 9c244317..88e7b51b 100644 --- a/server/src/http.rs +++ b/server/src/http.rs @@ -111,7 +111,7 @@ pub fn json_response(status: status::Status, response: Json) -> Response { return Response::with((content_type, status, json)); } -pub fn json_object(status: status::Status, object: String) -> Response { +pub fn _json_object(status: status::Status, object: String) -> Response { let content_type = "application/json".parse::().unwrap(); return Response::with((content_type, status, object)); } diff --git a/server/src/img.rs b/server/src/img.rs index 705d86cd..ef14b0ec 100644 --- a/server/src/img.rs +++ b/server/src/img.rs @@ -72,7 +72,7 @@ enum ConstructShapes { Line, V, Tri, - Plus, + // Plus, Blank, } @@ -203,9 +203,7 @@ pub fn shapes_write(id: Uuid) -> Result { write!(&mut svg, "", fill = colour, width = width, x0 = x0, y0 = y0, x1 = x1, y1 = y1, x2 = x2, y2 = y2, rotation = rotation, x_translate = -x_translate, y_translate = -y_translate)?; }, - ConstructShapes::Plus => { - - }, + // ConstructShapes::Plus => { }, ConstructShapes::Blank => (), } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 954197e4..b8e9060c 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -15,6 +15,7 @@ extern crate serde_cbor; #[macro_use] extern crate failure; extern crate fern; +extern crate log_panics; #[macro_use] extern crate log; extern crate stripe; @@ -47,6 +48,8 @@ mod pg; mod events; pub mod rpc; mod warden; +mod user_authenticated; +mod user_anonymous; use std::thread::{spawn}; use std::path::{Path}; @@ -55,6 +58,8 @@ use crossbeam_channel::{unbounded}; #[cfg(unix)] pub fn setup_logger() -> Result<(), fern::InitError> { + log_panics::init(); + let formatter = syslog::Formatter3164 { facility: syslog::Facility::LOG_USER, hostname: None, @@ -117,11 +122,11 @@ pub fn start() { let warden_tick_tx = warden_tx.clone(); let (mail_tx, mail_rx) = unbounded(); - let http_mail_tx = mail_tx.clone(); + let _http_mail_tx = mail_tx.clone(); // create a clone of the tx so ws handler can tell events // about connection status - let events = events::Events::new(events_tx, events_rx, events_warden_tx, mail_tx); + let events = events::Events::new(events_tx, events_rx, events_warden_tx); let warden = warden::Warden::new(warden_tx, warden_rx, events.tx.clone(), pool.clone()); let pg_pool = pool.clone(); diff --git a/server/src/mail.rs b/server/src/mail.rs index 0fdffc7b..d5083021 100644 --- a/server/src/mail.rs +++ b/server/src/mail.rs @@ -261,7 +261,7 @@ pub fn set(tx: &mut Transaction, account: Uuid, email: &String) -> Result<(Uuid, return Ok((id, confirm_token)); } -pub fn listen(rx: Receiver) -> SmtpTransport { +pub fn listen(_rx: Receiver) -> SmtpTransport { let sender = env::var("MAIL_ADDRESS") .expect("MAIL_ADDRESS must be set"); @@ -271,7 +271,7 @@ pub fn listen(rx: Receiver) -> SmtpTransport { let domain = env::var("MAIL_DOMAIN") .expect("MAIL_DOMAIN must be set"); - let mut mailer = SmtpClient::new_simple("smtp.gmail.com").unwrap() + let mailer = SmtpClient::new_simple("smtp.gmail.com").unwrap() .hello_name(ClientId::Domain(domain)) .credentials(Credentials::new(sender, password)) .smtp_utf8(true) @@ -281,19 +281,6 @@ pub fn listen(rx: Receiver) -> SmtpTransport { info!("mail connected"); - // loop { - // match rx.recv() { - // Ok(m) => match send_mail(&mut mailer, m) { - // Ok(r) => info!("{:?}", r), - // Err(e) => warn!("{:?}", e), - // }, - // Err(e) => { - // error!("{:?}", e); - // panic!("mail thread cannot continue"); - // }, - // }; - // } - // Explicitly close the SMTP transaction as we enabled connection reuse // mailer.close(); return mailer; diff --git a/server/src/mtx.rs b/server/src/mtx.rs index 5fc66763..7968c468 100644 --- a/server/src/mtx.rs +++ b/server/src/mtx.rs @@ -86,7 +86,7 @@ impl Mtx { } } - pub fn delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> { + pub fn _delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> { let query = " DELETE FROM mtx diff --git a/server/src/pg.rs b/server/src/pg.rs index 7a9f9495..97444249 100644 --- a/server/src/pg.rs +++ b/server/src/pg.rs @@ -22,7 +22,7 @@ use mnml_core::mob::instance_mobs; use mnml_core::vbox::{ItemType, VboxIndices}; use mnml_core::item::Item; use mnml_core::skill::Skill; -use mnml_core::mob::bot_player; +use mnml_core::mob::{bot_player, anon_player}; use mnml_core::instance::{Instance, TimeControl}; use events::{Event}; @@ -143,7 +143,7 @@ pub fn listen(pool: PgPool, events: Sender) -> Result<(), Error> { } -pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<(), Error> { +pub fn _construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<(), Error> { let query = " DELETE FROM constructs @@ -163,7 +163,7 @@ pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Res return Ok(()); } -pub fn construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result { +pub fn _construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result { let query = " SELECT data FROM constructs @@ -290,7 +290,7 @@ pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result { return Ok(game); } -pub fn select(db: &Db, id: Uuid) -> Result { +pub fn _game_select(db: &Db, id: Uuid) -> Result { let query = " SELECT * FROM games @@ -312,7 +312,7 @@ pub fn select(db: &Db, id: Uuid) -> Result { return Ok(game); } -pub fn list(db: &Db, number: u32) -> Result, Error> { +pub fn _game_list(db: &Db, number: u32) -> Result, Error> { let query = " SELECT data FROM games @@ -694,6 +694,35 @@ pub fn instance_practice(tx: &mut Transaction, account: &Account) -> Result Result { + let bot = bot_player(); + let bot_id = bot.id; + + // generate imgs for the client to see + for c in bot.constructs.iter() { + img::shapes_write(c.img)?; + } + + let mut instance = Instance::new() + .set_time_control(TimeControl::Practice) + .set_name(bot.name.clone())?; + + let player = anon_player(account.id); + + for c in player.constructs.iter() { + img::shapes_write(c.img)?; + } + + instance.add_player(player.clone())?; + instance.add_player(bot)?; + + instance.player_ready(bot_id)?; + // skip faceoff + instance.player_ready(player.id)?; + + Ok(instance) +} + pub fn pvp(tx: &mut Transaction, a: &Account, b: &Account) -> Result { let mut instance = Instance::new() // TODO generate nice game names @@ -786,7 +815,7 @@ pub fn instance_game_finished(tx: &mut Transaction, game: &Game, instance_id: Uu Ok(()) } -pub fn bot_instance() -> Instance { +pub fn _bot_instance() -> Instance { let mut instance = Instance::new(); let bot_player = bot_player(); @@ -804,26 +833,6 @@ pub fn bot_instance() -> Instance { return instance; } -pub fn demo() -> Result, Error> { - let bot = bot_player(); - - // generate bot imgs for the client to see - for c in bot.constructs.iter() { - img::shapes_write(c.img)?; - }; - - let bot2 = bot_player(); - - // generate bot imgs for the client to see - for c in bot2.constructs.iter() { - img::shapes_write(c.img)?; - }; - - - Ok(vec![bot, bot2]) -} - - pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result { let instance = instance_get(tx, instance_id)? .vbox_refill(account.id)?; diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 9fd4e824..7ab789f1 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,6 +1,6 @@ use mnml_core::item::ItemInfoCtr; use mnml_core::instance::ChatState; -use mnml_core::item::item_info; + use std::collections::HashMap; use std::time::{Instant}; use std::thread::{spawn}; @@ -8,12 +8,8 @@ use std::thread::{spawn}; use std::str; use uuid::Uuid; -use rand::prelude::*; - use failure::Error; -use failure::err_msg; - -use serde_cbor::{from_slice, to_vec}; +use serde_cbor::{to_vec}; use cookie::Cookie; use stripe::{Client as StripeClient, Subscription}; @@ -22,44 +18,24 @@ use crossbeam_channel::{unbounded, Sender as CbSender}; use ws::{Builder, CloseCode, Message, Handler, Request, Response, Settings, Sender as WsSender}; use ws::deflate::DeflateHandler; -use pg::{ - demo, - game_concede, - game_offer_draw, - game_ready, - game_skill, - game_skill_clear, - game_state, - instance_abandon, - instance_practice, - instance_ready, - instance_state, - vbox_apply, - vbox_buy, - vbox_combine, - vbox_refill, - vbox_refund, - vbox_unequip, -}; - use account::{Account}; use account; use events::{Event}; +use user_anonymous::{Anonymous}; +use user_authenticated::{Authorised}; + use mnml_core::construct::{Construct}; use mnml_core::game::{Game}; -use mnml_core::player::Player; use mnml_core::vbox::{ItemType}; use mnml_core::item::Item; use mnml_core::skill::Skill; -use mnml_core::mob::{anim_test_game}; + use mnml_core::instance::{Instance}; use mtx; -use mail; -use payments; use mail::Email; use pg::{Db}; use pg::{PgPool}; @@ -68,13 +44,12 @@ use http::{AUTH_CLEAR, TOKEN_HEADER}; #[derive(Debug,Clone,Serialize)] pub enum RpcMessage { AccountState(Account), + AccountAuthenticated(Account), AccountConstructs(Vec), AccountTeam(Vec), AccountInstances(Vec), AccountShop(mtx::Shop), - Demo(Vec), - ConstructSpawn(Construct), GameState(Game), ItemInfo(ItemInfoCtr), @@ -87,6 +62,7 @@ pub enum RpcMessage { SubscriptionState(Option), Pong(()), + StartTutorial(()), QueueRequested(()), QueueJoined(()), @@ -149,194 +125,32 @@ pub enum RpcRequest { VboxRefund { instance_id: Uuid, index: String }, } +pub trait User { + fn receive(&mut self, data: Vec, db: &Db, begin: Instant, events: &CbSender, stripe: &StripeClient) -> Result; + fn connected(&mut self, db: &Db, events: &CbSender, ws: &CbSender) -> Result<(), Error>; + fn send(&mut self, msg: RpcMessage, events: &CbSender, ws: &CbSender) -> Result<(), Error>; +} + struct Connection { - pub id: usize, + pub id: Uuid, pub ws: CbSender, pool: PgPool, stripe: StripeClient, - account: Option, + // account: Option, + user: Box, events: CbSender, } impl Connection { - fn receive(&self, data: Vec, db: &Db, begin: Instant) -> Result { - // cast the msg to this type to receive method name - match from_slice::(&data) { - Ok(v) => { - - // non authenticated - // non transactional reqs - match v { - RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())), - RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())), - RpcRequest::DevResolve { skill } => - return Ok(RpcMessage::GameState(anim_test_game(skill))), - _ => (), - }; - - // check for authorization now - let account = match self.account { - Some(ref account) => account, - None => return Err(err_msg("auth required")), - }; - - let request = v.clone(); - - let response = match v { - // evented but authorization required - RpcRequest::InstanceQueue {} => { - self.events.send(Event::Queue(self.id))?; - Ok(RpcMessage::QueueRequested(())) - }, - RpcRequest::InstanceInvite {} => { - self.events.send(Event::Invite(self.id))?; - Ok(RpcMessage::InviteRequested(())) - }, - RpcRequest::InstanceJoin { code } => { - self.events.send(Event::Join(self.id, code))?; - Ok(RpcMessage::Joining(())) - }, - RpcRequest::InstanceLeave {} => { - self.events.send(Event::Leave(self.id))?; - Ok(RpcMessage::Processing(())) - }, - - RpcRequest::InstanceChat { instance_id, index } => { - if !account.subscribed { - return Err(err_msg("subscribe to unlock chat")) - } - - let wheel = account::chat_wheel(&db, account.id)?; - - if let Some(c) = wheel.get(index) { - self.events.send(Event::Chat(self.id, instance_id, c.to_string()))?; - } else { - return Err(err_msg("invalid chat index")); - } - - Ok(RpcMessage::Processing(())) - }, - _ => { - // all good, let's make a tx and process - let mut tx = db.transaction()?; - - let res = match v { - RpcRequest::AccountState {} => - Ok(RpcMessage::AccountState(account.clone())), - RpcRequest::AccountConstructs {} => - Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &account)?)), - RpcRequest::AccountInstances {} => - Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, account)?)), - RpcRequest::AccountSetTeam { ids } => - Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &account, ids)?)), - - RpcRequest::EmailState {} => - Ok(RpcMessage::EmailState(mail::select_account(&db, account.id)?)), - - RpcRequest::SubscriptionState {} => - Ok(RpcMessage::SubscriptionState(payments::account_subscription(&db, &self.stripe, &account)?)), - - // RpcRequest::AccountShop {} => - // Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)), - - // RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account), - - RpcRequest::GameState { id } => - Ok(RpcMessage::GameState(game_state(&mut tx, account, id)?)), - - RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } => - Ok(RpcMessage::GameState(game_skill(&mut tx, account, game_id, construct_id, target_construct_id, skill)?)), - - RpcRequest::GameSkillClear { game_id } => - Ok(RpcMessage::GameState(game_skill_clear(&mut tx, account, game_id)?)), - - RpcRequest::GameReady { id } => - Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)), - - RpcRequest::GameConcede { game_id } => - Ok(RpcMessage::GameState(game_concede(&mut tx, account, game_id)?)), - - RpcRequest::GameOfferDraw { game_id } => - Ok(RpcMessage::GameState(game_offer_draw(&mut tx, account, game_id)?)), - - RpcRequest::InstancePractice {} => - Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)), - - // these two can return GameState or InstanceState - RpcRequest::InstanceReady { instance_id } => - Ok(instance_ready(&mut tx, account, instance_id)?), - RpcRequest::InstanceState { instance_id } => - Ok(instance_state(&mut tx, instance_id)?), - RpcRequest::InstanceAbandon { instance_id } => - Ok(instance_abandon(&mut tx, account, instance_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)?)), - - RpcRequest::VboxCombine { instance_id, inv_indices, vbox_indices } => - Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, account, instance_id, inv_indices, vbox_indices)?)), - - RpcRequest::VboxRefill { instance_id } => - Ok(RpcMessage::InstanceState(vbox_refill(&mut tx, account, instance_id)?)), - - 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)?)), - - RpcRequest::VboxUnequipApply { instance_id, construct_id, target, target_construct_id } => - Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, account, instance_id, construct_id, target, Some(target_construct_id))?)), - - RpcRequest::MtxConstructSpawn {} => - Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, account)?)), - - RpcRequest::MtxConstructApply { mtx, construct_id, name } => - Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, account, mtx, construct_id, name)?)), - - RpcRequest::MtxAccountApply { mtx } => - Ok(RpcMessage::AccountState(mtx::account_apply(&mut tx, account, mtx)?)), - - RpcRequest::MtxBuy { mtx } => - Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, account, mtx)?)), - - RpcRequest::SubscriptionEnding { ending } => - Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, &self.stripe, account, ending)?)), - - _ => Err(format_err!("unknown request request={:?}", request)), - }; - - tx.commit()?; - res - } - }; - - info!("request={:?} account={:?} duration={:?}", request, account.name, begin.elapsed()); - - return response; - }, - Err(e) => { - warn!("{:?}", e); - Err(err_msg("invalid message")) - }, - } - } - // this is where last minute processing happens // use it to modify outgoing messages, update subs, serialize in some way... fn send(&self, msg: RpcMessage) -> Result<(), Error> { - let msg = match self.account { - Some(ref a) => match msg { - RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(a.id)), - RpcMessage::AccountInstances(v) => - RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(a.id)).collect()), - RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(a.id)), - _ => msg, - }, - None => msg, + let msg = match msg { + RpcMessage::InstanceState(v) => RpcMessage::InstanceState(v.redact(self.id)), + RpcMessage::AccountInstances(v) => + RpcMessage::AccountInstances(v.into_iter().map(|i| i.redact(self.id)).collect()), + RpcMessage::GameState(v) => RpcMessage::GameState(v.redact(self.id)), + _ => msg, }; self.ws.send(msg).unwrap(); @@ -351,50 +165,8 @@ impl Connection { // when it encounters errors impl Handler for Connection { fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> { - info!("websocket connected account={:?}", self.account); - - // tell events we have connected - self.events.send(Event::Connect(self.id, self.account.clone(), self.ws.clone())).unwrap(); - - // if user logged in do some prep work - if let Some(ref a) = self.account { - self.send(RpcMessage::AccountState(a.clone())).unwrap(); - self.events.send(Event::Subscribe(self.id, a.id)).unwrap(); - - // check if they have an image that needs to be generated - account::img_check(&a).unwrap(); - - let db = self.pool.get().unwrap(); - let mut tx = db.transaction().unwrap(); - - // send account constructs - let account_constructs = account::constructs(&mut tx, a).unwrap(); - self.send(RpcMessage::AccountConstructs(account_constructs)).unwrap(); - - // get account instances - // and send them to the client - let account_instances = account::account_instances(&mut tx, a).unwrap(); - self.send(RpcMessage::AccountInstances(account_instances)).unwrap(); - - let shop = mtx::account_shop(&mut tx, &a).unwrap(); - self.send(RpcMessage::AccountShop(shop)).unwrap(); - - let team = account::team(&mut tx, &a).unwrap(); - self.send(RpcMessage::AccountTeam(team)).unwrap(); - - let wheel = account::chat_wheel(&db, a.id).unwrap(); - self.send(RpcMessage::ChatWheel(wheel)).unwrap(); - - if let Some(instance) = account::tutorial(&mut tx, &a).unwrap() { - self.send(RpcMessage::InstanceState(instance)).unwrap(); - } - - // tx should do nothing - tx.commit().unwrap(); - } else { - self.send(RpcMessage::Demo(demo().unwrap())).unwrap(); - } - + let db = self.pool.get().unwrap(); + self.user.connected(&db, &self.events, &self.ws).unwrap(); Ok(()) } @@ -404,23 +176,9 @@ impl Handler for Connection { let begin = Instant::now(); let db_connection = self.pool.get().unwrap(); - match self.receive(msg, &db_connection, begin) { - Ok(reply) => { - // if the user queries the state of something - // we tell events to push updates to them - match reply { - RpcMessage::AccountState(ref v) => { - self.account = Some(v.clone()); - self.events.send(Event::Subscribe(self.id, v.id)).unwrap() - }, - RpcMessage::GameState(ref v) => - self.events.send(Event::Subscribe(self.id, v.id)).unwrap(), - RpcMessage::InstanceState(ref v) => - self.events.send(Event::Subscribe(self.id, v.id)).unwrap(), - _ => (), - }; - - self.send(reply).unwrap(); + match self.user.receive(msg, &db_connection, begin, &self.events, &self.stripe) { + Ok(msg) => { + self.user.send(msg, &self.events, &self.ws).unwrap(); }, Err(e) => { warn!("{:?}", e); @@ -434,7 +192,7 @@ impl Handler for Connection { } fn on_close(&mut self, _: CloseCode, _: &str) { - info!("websocket disconnected account={:?}", self.account); + info!("websocket disconnected id={:?}", self.id); self.events.send(Event::Disconnect(self.id)).unwrap(); } @@ -462,21 +220,19 @@ impl Handler for Connection { if cookie.name() == TOKEN_HEADER { let db = self.pool.get().unwrap(); match account::from_token(&db, &cookie.value().to_string()) { - Ok(a) => self.account = Some(a), + Ok(a) => self.user = Box::new(Authorised { id: a.id, account: a }), Err(_) => return unauth(), } } }; - }; + } Ok(res) } } pub fn start(pool: PgPool, events_tx: CbSender, stripe: StripeClient) { - let mut rng = thread_rng(); - - let ws = Builder::new() + let _ws = Builder::new() .with_settings(Settings { max_connections: 10_000, ..Settings::default() @@ -506,14 +262,17 @@ pub fn start(pool: PgPool, events_tx: CbSender, stripe: StripeClient) { } }); + let anon_account = Account::anonymous(); + let id = anon_account.id; + DeflateHandler::new( Connection { - id: rng.gen::(), - account: None, + id, ws: tx, pool: pool.clone(), stripe: stripe.clone(), events: events_tx.clone(), + user: Box::new(Anonymous { id, account: anon_account, game: None, instance: None }) } ) }) diff --git a/server/src/user_anonymous.rs b/server/src/user_anonymous.rs new file mode 100644 index 00000000..a24d9911 --- /dev/null +++ b/server/src/user_anonymous.rs @@ -0,0 +1,160 @@ +use std::time::Instant; +use uuid::Uuid; + +use failure::Error; +use failure::err_msg; + +use crossbeam_channel::{Sender as CbSender}; + +use serde_cbor::{from_slice}; + +use stripe::{Client as StripeClient}; + +use account::{Account}; +use pg::{Db}; +use pg; +use events::{Event}; +use rpc::{RpcMessage, RpcRequest, User}; + +use mnml_core::game::Game; +use mnml_core::item::item_info; +use mnml_core::instance::Instance; + +#[derive(Debug,Clone)] +pub struct Anonymous { + pub account: Account, + pub id: Uuid, + pub instance: Option, + pub game: Option, +} + +impl User for Anonymous { + fn send(&mut self, msg: RpcMessage, _events: &CbSender, ws: &CbSender) -> Result<(), Error> { + // if the user queries the state of something + // we tell events to push updates to them + match msg { + RpcMessage::GameState(ref v) => + self.game = Some(v.clone()), + RpcMessage::InstanceState(ref v) => + self.instance = Some(v.clone()), + _ => (), + }; + + ws.send(msg)?; + + Ok(()) + } + + fn connected(&mut self, _db: &Db, events: &CbSender, ws: &CbSender) -> Result<(), Error> { + info!("anonymous connection"); + + self.send(RpcMessage::AccountState(self.account.clone()), events, ws)?; + self.send(RpcMessage::StartTutorial(()), events, ws)?; + + Ok(()) + } + + fn receive(&mut self, data: Vec, _db: &Db, _begin: Instant, _events: &CbSender, _stripe: &StripeClient) -> Result { + match from_slice::(&data) { + Ok(v) => { + let get_instance = || { + match self.instance { + Some(ref i) => Ok(i.clone()), + None => return Err(err_msg("instance missing")), + } + }; + + let get_game = || { + match self.game { + Some(ref i) => Ok(i.clone()), + None => return Err(err_msg("game missing")), + } + }; + + match v { + RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())), + + RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())), + RpcRequest::AccountInstances {} => return Ok(RpcMessage::Pong(())), + + RpcRequest::InstancePractice {} => + Ok(RpcMessage::InstanceState(pg::instance_demo(&self.account)?)), + + RpcRequest::InstanceReady { instance_id: _ } => { + match get_instance()?.player_ready(self.account.id)? { + Some(g) => Ok(RpcMessage::GameState(g)), + None => Ok(RpcMessage::InstanceState(get_instance()?)), + } + }, + + RpcRequest::InstanceState { instance_id: _ } => + Ok(RpcMessage::InstanceState(get_instance()?)), + + RpcRequest::InstanceAbandon { instance_id: _ } => { + let mut instance = get_instance()?; + instance.finish(); + Ok(RpcMessage::InstanceState(instance)) + }, + + RpcRequest::VboxBuy { instance_id: _, group, index, construct_id } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_buy(self.account.id, group, index, construct_id)?)), + + RpcRequest::VboxApply { instance_id: _, construct_id, index } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_apply(self.account.id, index, construct_id)?)), + + RpcRequest::VboxCombine { instance_id: _, inv_indices, vbox_indices } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_combine(self.account.id, inv_indices, vbox_indices)?)), + + RpcRequest::VboxRefill { instance_id: _ } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_refill(self.account.id)?)), + + RpcRequest::VboxRefund { instance_id: _, index } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_refund(self.account.id, index)?)), + + RpcRequest::VboxUnequip { instance_id: _, construct_id, target } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_unequip(self.account.id, target, construct_id, None)?)), + + RpcRequest::VboxUnequipApply { instance_id: _, construct_id, target, target_construct_id } => + Ok(RpcMessage::InstanceState(get_instance()?.vbox_unequip(self.account.id, target, construct_id, Some(target_construct_id))?)), + + RpcRequest::GameState { id: _ } => + Ok(RpcMessage::GameState(get_game()?)), + + RpcRequest::GameSkill { game_id: _, construct_id, target_construct_id, skill } => { + let mut game = get_game()?; + game.add_skill(self.account.id, construct_id, target_construct_id, skill)?; + Ok(RpcMessage::GameState(game)) + }, + + RpcRequest::GameSkillClear { game_id: _ } => { + let mut game = get_game()?; + game.clear_skill(self.account.id)?; + Ok(RpcMessage::GameState(game)) + }, + + RpcRequest::GameReady { id: _ } => { + let mut game = get_game()?; + game.player_ready(self.account.id)?; + if game.skill_phase_finished() { + game = game.resolve_phase_start(); + } + + Ok(RpcMessage::GameState(game)) + }, + + RpcRequest::GameConcede { game_id: _ } => + Ok(RpcMessage::GameState(get_game()?.concede(self.account.id)?)), + + RpcRequest::GameOfferDraw { game_id: _ } => + Ok(RpcMessage::GameState(get_game()?.offer_draw(self.account.id)?)), + + _ => Err(format_err!("unhandled anonymous request request={:?}", v)), + } + }, + Err(e) => { + warn!("{:?}", e); + Err(err_msg("invalid message")) + }, + } + } +} diff --git a/server/src/user_authenticated.rs b/server/src/user_authenticated.rs new file mode 100644 index 00000000..3cf25be8 --- /dev/null +++ b/server/src/user_authenticated.rs @@ -0,0 +1,265 @@ +use mnml_core::mob::anim_test_game; +use mnml_core::item::item_info; +use std::time::Instant; +use uuid::Uuid; + +use failure::Error; +use failure::err_msg; + +use crossbeam_channel::{Sender as CbSender}; +use stripe::{Client as StripeClient}; + +use serde_cbor::{from_slice}; + +use pg::{ + game_concede, + game_offer_draw, + game_ready, + game_skill, + game_skill_clear, + game_state, + instance_abandon, + instance_practice, + instance_ready, + instance_state, + vbox_apply, + vbox_buy, + vbox_combine, + vbox_refill, + vbox_refund, + vbox_unequip, +}; + +use account::{Account}; +use account; +use events::{Event}; + +use mtx; +use mail; +use payments; +use pg::{Db}; +use rpc::{RpcMessage, RpcRequest, User}; + + +#[derive(Debug,Clone)] +pub struct Authorised { + pub account: Account, + pub id: Uuid +} + +impl User for Authorised { + fn send(&mut self, msg: RpcMessage, events: &CbSender, ws: &CbSender) -> Result<(), Error> { + // if the user queries the state of something + // we tell events to push updates to them + match msg { + RpcMessage::AccountState(ref v) => { + events.send(Event::Subscribe(self.id, v.id))? + }, + RpcMessage::GameState(ref v) => + events.send(Event::Subscribe(self.id, v.id))?, + RpcMessage::InstanceState(ref v) => + events.send(Event::Subscribe(self.id, v.id))?, + _ => (), + }; + + ws.send(msg)?; + + Ok(()) + } + + fn connected(&mut self, db: &Db, events: &CbSender, ws: &CbSender) -> Result<(), Error> { + info!("authenticated connection account={:?}", self.account); + let a = &self.account; + + ws.send(RpcMessage::AccountAuthenticated(a.clone()))?; + // tell events we have connected + events.send(Event::Connect(self.id, a.clone(), ws.clone()))?; + + ws.send(RpcMessage::AccountState(a.clone()))?; + events.send(Event::Subscribe(self.id, a.id))?; + + // check if they have an image that needs to be generated + account::img_check(&a)?; + + let mut tx = db.transaction()?; + + // send account constructs + let account_constructs = account::constructs(&mut tx, &a)?; + ws.send(RpcMessage::AccountConstructs(account_constructs))?; + + // get account instances + // and send them to the client + let account_instances = account::account_instances(&mut tx, &a)?; + ws.send(RpcMessage::AccountInstances(account_instances))?; + + let shop = mtx::account_shop(&mut tx, &a)?; + ws.send(RpcMessage::AccountShop(shop))?; + + let team = account::team(&mut tx, &a)?; + ws.send(RpcMessage::AccountTeam(team))?; + + let wheel = account::chat_wheel(&db, a.id)?; + ws.send(RpcMessage::ChatWheel(wheel))?; + + if let Some(instance) = account::tutorial(&mut tx, &a)? { + ws.send(RpcMessage::InstanceState(instance))?; + } + + // tx should do nothing + tx.commit()?; + + Ok(()) + } + + fn receive(&mut self, data: Vec, db: &Db, begin: Instant, events: &CbSender, stripe: &StripeClient) -> Result { + // cast the msg to this type to receive method name + match from_slice::(&data) { + Ok(v) => { + let request = v.clone(); + let response = match v { + RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())), + RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())), + RpcRequest::DevResolve { skill } => + return Ok(RpcMessage::GameState(anim_test_game(skill))), + + RpcRequest::InstanceQueue {} => { + events.send(Event::Queue(self.id))?; + Ok(RpcMessage::QueueRequested(())) + }, + RpcRequest::InstanceInvite {} => { + events.send(Event::Invite(self.id))?; + Ok(RpcMessage::InviteRequested(())) + }, + RpcRequest::InstanceJoin { code } => { + events.send(Event::Join(self.id, code))?; + Ok(RpcMessage::Joining(())) + }, + RpcRequest::InstanceLeave {} => { + events.send(Event::Leave(self.id))?; + Ok(RpcMessage::Processing(())) + }, + + RpcRequest::InstanceChat { instance_id, index } => { + if !self.account.subscribed { + return Err(err_msg("subscribe to unlock chat")) + } + + let wheel = account::chat_wheel(&db, self.account.id)?; + + if let Some(c) = wheel.get(index) { + events.send(Event::Chat(self.id, instance_id, c.to_string()))?; + } else { + return Err(err_msg("invalid chat index")); + } + + Ok(RpcMessage::Processing(())) + }, + _ => { + // all good, let's make a tx and process + let mut tx = db.transaction()?; + + let res = match v { + RpcRequest::AccountState {} => + Ok(RpcMessage::AccountState(self.account.clone())), + RpcRequest::AccountConstructs {} => + Ok(RpcMessage::AccountConstructs(account::constructs(&mut tx, &self.account)?)), + RpcRequest::AccountInstances {} => + Ok(RpcMessage::AccountInstances(account::account_instances(&mut tx, &self.account)?)), + RpcRequest::AccountSetTeam { ids } => + Ok(RpcMessage::AccountTeam(account::set_team(&mut tx, &self.account, ids)?)), + + RpcRequest::EmailState {} => + Ok(RpcMessage::EmailState(mail::select_account(&db, self.account.id)?)), + + RpcRequest::SubscriptionState {} => + Ok(RpcMessage::SubscriptionState(payments::account_subscription(db, stripe, &self.account)?)), + + // RpcRequest::AccountShop {} => + // Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)), + + // RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account), + + RpcRequest::GameState { id } => + Ok(RpcMessage::GameState(game_state(&mut tx, &self.account, id)?)), + + RpcRequest::GameSkill { game_id, construct_id, target_construct_id, skill } => + Ok(RpcMessage::GameState(game_skill(&mut tx, &self.account, game_id, construct_id, target_construct_id, skill)?)), + + RpcRequest::GameSkillClear { game_id } => + Ok(RpcMessage::GameState(game_skill_clear(&mut tx, &self.account, game_id)?)), + + RpcRequest::GameReady { id } => + Ok(RpcMessage::GameState(game_ready(&mut tx, &self.account, id)?)), + + RpcRequest::GameConcede { game_id } => + Ok(RpcMessage::GameState(game_concede(&mut tx, &self.account, game_id)?)), + + RpcRequest::GameOfferDraw { game_id } => + Ok(RpcMessage::GameState(game_offer_draw(&mut tx, &self.account, game_id)?)), + + RpcRequest::InstancePractice {} => + Ok(RpcMessage::InstanceState(instance_practice(&mut tx, &self.account)?)), + + // these two can return GameState or InstanceState + RpcRequest::InstanceReady { instance_id } => + Ok(instance_ready(&mut tx, &self.account, instance_id)?), + RpcRequest::InstanceState { instance_id } => + Ok(instance_state(&mut tx, instance_id)?), + RpcRequest::InstanceAbandon { instance_id } => + Ok(instance_abandon(&mut tx, &self.account, instance_id)?), + + RpcRequest::VboxBuy { instance_id, group, index, construct_id } => + Ok(RpcMessage::InstanceState(vbox_buy(&mut tx, &self.account, instance_id, group, index, construct_id)?)), + + RpcRequest::VboxApply { instance_id, construct_id, index } => + Ok(RpcMessage::InstanceState(vbox_apply(&mut tx, &self.account, instance_id, construct_id, index)?)), + + RpcRequest::VboxCombine { instance_id, inv_indices, vbox_indices } => + Ok(RpcMessage::InstanceState(vbox_combine(&mut tx, &self.account, instance_id, inv_indices, vbox_indices)?)), + + RpcRequest::VboxRefill { instance_id } => + Ok(RpcMessage::InstanceState(vbox_refill(&mut tx, &self.account, instance_id)?)), + + RpcRequest::VboxRefund { instance_id, index } => + Ok(RpcMessage::InstanceState(vbox_refund(&mut tx, &self.account, instance_id, index)?)), + + RpcRequest::VboxUnequip { instance_id, construct_id, target } => + Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, &self.account, instance_id, construct_id, target, None)?)), + + RpcRequest::VboxUnequipApply { instance_id, construct_id, target, target_construct_id } => + Ok(RpcMessage::InstanceState(vbox_unequip(&mut tx, &self.account, instance_id, construct_id, target, Some(target_construct_id))?)), + + RpcRequest::MtxConstructSpawn {} => + Ok(RpcMessage::ConstructSpawn(mtx::new_construct(&mut tx, &self.account)?)), + + RpcRequest::MtxConstructApply { mtx, construct_id, name } => + Ok(RpcMessage::AccountTeam(mtx::apply(&mut tx, &self.account, mtx, construct_id, name)?)), + + RpcRequest::MtxAccountApply { mtx } => + Ok(RpcMessage::AccountState(mtx::account_apply(&mut tx, &self.account, mtx)?)), + + RpcRequest::MtxBuy { mtx } => + Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, &self.account, mtx)?)), + + RpcRequest::SubscriptionEnding { ending } => + Ok(RpcMessage::SubscriptionState(payments::subscription_ending(&mut tx, stripe, &self.account, ending)?)), + + _ => Err(format_err!("unknown request request={:?}", request)), + }; + + tx.commit()?; + res + } + }; + + info!("request={:?} account={:?} duration={:?}", request, self.account.name, begin.elapsed()); + + return response; + }, + Err(e) => { + warn!("{:?}", e); + Err(err_msg("invalid message")) + }, + } + } +} diff --git a/server/src/warden.rs b/server/src/warden.rs index 1e6a729a..644e78db 100644 --- a/server/src/warden.rs +++ b/server/src/warden.rs @@ -1,7 +1,5 @@ use std::time::{Duration}; -use uuid::Uuid; - use crossbeam_channel::{tick, Sender, Receiver}; // Db Commons @@ -24,13 +22,12 @@ use pg::{ pvp, }; -type Id = usize; type Pair = (PvpRequest, PvpRequest); pub enum GameEvent { Upkeep, - Finish(Uuid), + // Finish(Uuid), Match(Pair), } @@ -76,10 +73,10 @@ impl Warden { match msg { GameEvent::Upkeep => self.on_upkeep(), GameEvent::Match(pair) => self.on_match(pair), - GameEvent::Finish(id) => { - info!("game finished id={:?}", id); - Ok(()) - }, + // GameEvent::Finish(id) => { + // info!("game finished id={:?}", id); + // Ok(()) + // }, } } @@ -131,7 +128,7 @@ impl Warden { fn fetch_games(mut tx: Transaction) -> Result { let games = games_need_upkeep(&mut tx)?; - for mut game in games { + for game in games { let game = game.upkeep(); match game_update(&mut tx, &game) { Ok(_) => (), @@ -146,7 +143,7 @@ fn fetch_games(mut tx: Transaction) -> Result { } fn fetch_instances(mut tx: Transaction) -> Result { - for mut instance in instances_need_upkeep(&mut tx)? { + for instance in instances_need_upkeep(&mut tx)? { let (instance, new_game) = instance.upkeep(); if let Some(game) = new_game {