Merge branch 'develop' into face-imgs

This commit is contained in:
ntr 2019-10-27 21:13:20 +11:00
commit d8d5528469
33 changed files with 235 additions and 133 deletions

View File

@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## [1.6.6] - 2019-10-27
# Added
- Offering of draws
- Neither player receives a point if they agree to a draw
- Bots automatically agree to draws
## [1.6.5] - 2019-10-25
# Fixed
- Stripe being blocked no longer causes unrecoverable error
- Automatic ready up is now throttled after abandons
- Player width styling
# Changed
- Improved wiggle animation
- Intercept is now considered defensive by bots
- Password restrictions relaxed
## [1.6.4] - 2019-10-24 ## [1.6.4] - 2019-10-24
### Changed ### Changed
- Animations processing on client side reduced. - Animations processing on client side reduced.

View File

@ -1 +1 @@
1.6.5 1.6.6

View File

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

View File

@ -150,4 +150,22 @@ button {
&.green { &.green {
border-color: @green; border-color: @green;
} }
} }
@keyframes rgb {
0% {
color: @red;
}
25% {
color: @white;
}
50% {
color: @blue;
}
75% {
color: @white;
}
100% {
color: @green;
}
}

View File

@ -144,7 +144,15 @@ aside {
&:active, &.confirming { &:active, &.confirming {
background: @red; background: @red;
color: black; color: black;
border: 2px solid black; border: 2px solid @red;
}
}
.draw:not([disabled]) {
&:active, &.confirming {
background: @gray-hover;
color: black;
border: 2px solid @gray-hover;
} }
} }

View File

@ -437,11 +437,12 @@
#targeting, .resolving-skill { #targeting, .resolving-skill {
width: calc(100% - 1em); width: calc(100% - 1em);
} }
.player {
width: calc(100% - 1em);
bottom: 3em;
height: calc(50% - 3em);
}
} }
.player {
width: calc(100% - 1em);
bottom: 3em;
height: calc(50% - 3em);
}
} }

View File

@ -90,6 +90,11 @@
.login { .login {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
.terms {
display: inline;
margin: 0 1em;
}
} }
} }

View File

@ -51,6 +51,11 @@
color: @yellow; color: @yellow;
font-weight: bold; font-weight: bold;
} }
.name.subscriber {
// animation: rgb 4s cubic-bezier(0.5, 0, 0.5, 1) 0s infinite alternate;
// font-weight: bold;
}
} }
.chat { .chat {

View File

@ -143,7 +143,7 @@ button, input {
a { a {
color: whitesmoke; color: whitesmoke;
text-decoration: none; // text-decoration: none;
&:hover { &:hover {
color: whitesmoke; color: whitesmoke;

View File

@ -1,12 +1,12 @@
{ {
"name": "mnml-client", "name": "mnml-client",
"version": "1.6.5", "version": "1.6.6",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "parcel watch index.html --out-dir /var/lib/mnml/public/current", "start": "parcel watch index.html --out-dir /var/lib/mnml/public/current",
"anims": "parcel watch animations.html --no-hmr --out-dir /var/lib/mnml/public/current", "anims": "parcel watch animations.html --no-hmr --out-dir /var/lib/mnml/public/current",
"build": "parcel build index.html", "build": "parcel build index.html --no-source-maps",
"scss": "node-sass --watch assets/scss -o assets/styles", "scss": "node-sass --watch assets/scss -o assets/styles",
"lint": "eslint --fix --ext .jsx src/", "lint": "eslint --fix --ext .jsx src/",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"

View File

@ -15,10 +15,6 @@ const registerEvents = require('./events');
const Mnml = require('./components/mnml'); const Mnml = require('./components/mnml');
if (process.env.NODE_ENV !== 'development') {
LogRocket.init('yh0dy3/mnml');
}
function stripeKey() { function stripeKey() {
if (window.location.host === 'mnml.gg') return 'pk_live_fQGrL1uWww2ot8W1G7vTySAv004ygmnMXq'; if (window.location.host === 'mnml.gg') return 'pk_live_fQGrL1uWww2ot8W1G7vTySAv004ygmnMXq';
return 'pk_test_Cb49tTqTXpzk7nEmlGzRrNJg00AU0aNZDj'; return 'pk_test_Cb49tTqTXpzk7nEmlGzRrNJg00AU0aNZDj';
@ -40,9 +36,12 @@ events.setWs(ws);
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
<StripeProvider apiKey={stripeKey()}> {window.Stripe
<Mnml /> ? <StripeProvider apiKey={stripeKey()}>
</StripeProvider> <Mnml />
</StripeProvider>
: <Mnml />
}
</Provider> </Provider>
); );

View File

@ -101,11 +101,13 @@ function AccountBox(args) {
// ); // );
// } // }
const nameClass = `name ${account.subscribed ? 'subscriber' : ''}`;
return ( return (
<div class='player-box bottom'> <div class='player-box bottom'>
<div class="msg">&nbsp;</div> <div class="msg">&nbsp;</div>
<StateAccountAvatar /> <StateAccountAvatar />
<div class="name">{account.name}</div> <div class={nameClass}>{account.name}</div>
<div class="score">&nbsp;</div> <div class="score">&nbsp;</div>
</div> </div>
); );

View File

@ -1,22 +1,24 @@
const anime = require('animejs').default; const anime = require('animejs').default;
function wiggle(id, idle) { function wiggle(id, idle) {
if (!idle) return true;
const duration = 300; const duration = 300;
const target = document.getElementById(id); const target = document.getElementById(id);
const x = window.innerWidth * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random()); const x = window.innerWidth * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
const y = window.innerHeight * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random()); const y = window.innerHeight * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());
const originalX = parseFloat(idle.animations[0].currentValue);
const originalY = parseFloat(idle.animations[1].currentValue);
// console.log(x, y); // console.log(x, y);
return anime({ return anime({
targets: target, targets: target,
rotate: 0, translateX: [originalX + x, originalX - x, originalX],
translateX: [x, -x, 0], translateY: [originalY + y, originalY - y, originalY],
translateY: [y, -y, 0],
duration, duration,
easing: 'easeInOutSine', easing: 'easeInOutSine',
// direction: 'alternate', // direction: 'alternate',
begin: idle.pause, begin: idle.pause,
complete: idle.restart, complete: idle.play,
}); });
} }

View File

@ -75,7 +75,7 @@ function GameCtrlBtns(args) {
} = args; } = args;
if (!game) return false; if (!game) return false;
const finished = game.phase === 'Finish'; const finished = game.phase === 'Finished';
function quitClick() { function quitClick() {
getInstanceState(); getInstanceState();

View File

@ -8,16 +8,23 @@ const addState = connect(
const { const {
ws, ws,
game, game,
account,
} = state; } = state;
function sendAbandon() { function sendAbandon() {
return ws.sendInstanceAbandon(game.instance); return ws.sendInstanceAbandon(game.instance);
} }
function sendDraw() {
return ws.sendGameOfferDraw(game.id);
}
return { return {
game, game,
account,
sendAbandon, sendAbandon,
sendDraw,
}; };
}, },
function receiveDispatch(dispatch) { function receiveDispatch(dispatch) {
@ -34,13 +41,18 @@ const addState = connect(
function GameCtrlTopBtns(args) { function GameCtrlTopBtns(args) {
const { const {
game, game,
account,
leave, leave,
sendAbandon, sendAbandon,
sendDraw,
} = args; } = args;
const finished = game && game.phase === 'Finished'; const finished = game && game.phase === 'Finished';
const { abandonState } = this.state; const { abandonState, drawState } = this.state;
const player = game.players.find(p => p.id === account.id);
const drawOffered = player && player.draw_offered;
const abandonStateTrue = e => { const abandonStateTrue = e => {
e.stopPropagation(); e.stopPropagation();
@ -48,16 +60,29 @@ function GameCtrlTopBtns(args) {
setTimeout(() => this.setState({ abandonState: false }), 2000); setTimeout(() => this.setState({ abandonState: false }), 2000);
}; };
const drawStateTrue = e => {
e.stopPropagation();
this.setState({ drawState: true });
setTimeout(() => this.setState({ drawState: false }), 2000);
};
const abandonClasses = `abandon ${abandonState ? 'confirming' : ''}`; const abandonClasses = `abandon ${abandonState ? 'confirming' : ''}`;
const abandonText = abandonState ? 'Confirm' : 'Abandon'; const abandonText = abandonState ? 'Confirm' : 'Abandon';
const abandonAction = abandonState ? sendAbandon : abandonStateTrue; const abandonAction = abandonState ? sendAbandon : abandonStateTrue;
const abandonBtn = <button class={abandonClasses} disabled={finished} onClick={abandonAction}>{abandonText}</button>; const abandonBtn = <button class={abandonClasses} disabled={finished} onClick={abandonAction}>{abandonText}</button>;
const leaveBtn = <button class='abandon confirming' onClick={leave}>Leave</button>;
const drawClasses = `draw ${drawState || drawOffered ? 'confirming' : ''}`;
const drawText = drawOffered
? 'Offered'
: drawState ? 'Draw' : 'Offer';
const drawAction = drawState ? sendDraw : drawStateTrue;
const drawBtn = <button class={drawClasses} disabled={finished || drawOffered} onClick={drawAction}>{drawText}</button>;
return ( return (
<div class="instance-ctrl-btns"> <div class="instance-ctrl-btns">
{finished ? leaveBtn : abandonBtn} {abandonBtn}
{drawBtn}
</div> </div>
); );
} }

View File

@ -82,7 +82,7 @@ function GameFooter(props) {
const now = Date.now(); const now = Date.now();
const end = Date.parse(game.phase_end); const end = Date.parse(game.phase_end);
const timerPct = ((now - zero) / (end - zero) * 100); const timerPct = ((now - zero) / (end - zero) * 100);
const displayPct = game.phase === 'Finish' || !game.phase_end const displayPct = game.phase === 'Finished' || !game.phase_end
? 0 ? 0
: Math.min(timerPct, 100); : Math.min(timerPct, 100);
@ -108,7 +108,7 @@ function GameFooter(props) {
return ( return (
<footer> <footer>
{timer} {timer}
{game.phase === 'Finish' && quitBtn } {game.phase === 'Finished' && quitBtn }
{game.phase === 'Skill' && readyBtn } {game.phase === 'Skill' && readyBtn }
</footer> </footer>
); );

View File

@ -3,8 +3,6 @@ const { connect } = require('preact-redux');
const { errorToast, infoToast } = require('../utils'); const { errorToast, infoToast } = require('../utils');
const AccountBox = require('./account.box');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { const {

View File

@ -203,6 +203,7 @@ function Play(args) {
<div> <div>
Join our Discord server to find opponents and talk to the devs. <br /> Join our Discord server to find opponents and talk to the devs. <br />
Message <b>@ntr</b> or <b>@mashy</b> for some credits to get started.<br /> Message <b>@ntr</b> or <b>@mashy</b> for some credits to get started.<br />
<a href='https://www.youtube.com/watch?v=VtZLlkpJuS8'>Tutorial Playthrough on YouTube</a>
</div> </div>
<br /> <br />
<div> <div>

View File

@ -68,24 +68,33 @@ function Scoreboard(args) {
}; };
const winner = player.score === 'Win'; const winner = player.score === 'Win';
const chatText = chat
? chat
: player.draw_offered
? 'draw'
: '\u00A0';
if (!isPlayer) { if (!isPlayer) {
const nameClass = `name ${player.img ? 'subscriber' : ''}`;
return ( return (
<div class={`player-box top ${winner ? 'winner' : player.ready ? 'ready' : ''}`}> <div class={`player-box top ${winner ? 'winner' : player.ready ? 'ready' : ''}`}>
<div></div> <div></div>
<div class="score">{scoreText()}</div> <div class="score">{scoreText()}</div>
<div class="name">{player.name}</div> <div class={nameClass}>{player.name}</div>
<Img img={player.img} id={player.id} /> <Img img={player.img} id={player.id} />
<div class="msg">{chat || '\u00A0'}</div> <div class="msg">{chatText}</div>
</div> </div>
); );
} }
const boxClass = `player-box bottom ${winner ? 'winner': player.ready ? 'ready' : ''}`;
const nameClass = `name ${player.img ? 'subscriber' : ''}`;
return ( return (
<div class={`player-box bottom ${winner ? 'winner': player.ready ? 'ready' : ''}`}> <div class={boxClass}>
<div class="msg">{chat || '\u00A0'}</div> <div class="msg">{chatText}</div>
<div class="score">{scoreText()}</div> <div class="score">{scoreText()}</div>
<div class="name">{player.name}</div> <div class={nameClass}>{player.name}</div>
<Img img={player.img} id={player.id} /> <Img img={player.img} id={player.id} />
</div> </div>
); );

View File

@ -48,9 +48,12 @@ function Shop(args) {
</div> </div>
<div> <div>
<h1 class="credits">¤ {account.balance}</h1> <h1 class="credits">¤ {account.balance}</h1>
<Elements> {window.Stripe
<StripeBtns account={account} /> ? <Elements>
</Elements> <StripeBtns account={account} />
</Elements>
: <div>Please unblock Stripe to use the store</div>
}
</div> </div>
</section> </section>
); );

View File

@ -53,7 +53,7 @@ function Welcome() {
<p> <p>
Welcome to mnml. Welcome to mnml.
</p> </p>
<p> Turn-based 1v1 strategy game in an abstract setting. </p> <p> MNML is a turn-based 1v1 strategy game in an abstract setting. </p>
<p> <p>
Build a unique team of 3 constructs from a range of skills and specialisations.<br /> Build a unique team of 3 constructs from a range of skills and specialisations.<br />
Outplay your opponent in multiple rounds by adapting to an always shifting meta. <br /> Outplay your opponent in multiple rounds by adapting to an always shifting meta. <br />
@ -62,6 +62,7 @@ function Welcome() {
<p> <p>
Free to play, no pay to win. Register to start playing.<br /> Free to play, no pay to win. Register to start playing.<br />
</p> </p>
<a href='https://www.youtube.com/watch?v=VtZLlkpJuS8'>Tutorial Playthrough on YouTube</a>
</div> </div>
{pageEl()} {pageEl()}
</section> </section>

View File

@ -58,7 +58,7 @@ function Register(args) {
value={this.state.name} value={this.state.name}
onInput={linkState(this, 'name')} onInput={linkState(this, 'name')}
/> />
<label for="password">Password - min 12 chars</label> <label for="password">Password - min 4 chars</label>
<input <input
class="login-input" class="login-input"
type="password" type="password"
@ -79,9 +79,11 @@ function Register(args) {
<div> <div>
<input <input
type="checkbox" type="checkbox"
name="terms"
id="register-terms"
onInput={linkState(this, 'terms') onInput={linkState(this, 'terms')
}/> }/>
&nbsp; Confirm agreement to terms of service &nbsp; <label class="terms" for="register-terms">Confirm agreement to terms of service.</label>
<button onClick={() => window.open('/tos.html')}>VIEW</button> <button onClick={() => window.open('/tos.html')}>VIEW</button>
</div> </div>
<button <button

View File

@ -127,6 +127,7 @@ function registerEvents(store) {
function setAccount(account) { function setAccount(account) {
if (account) { if (account) {
LogRocket.init('yh0dy3/mnml');
LogRocket.identify(account.id, account); LogRocket.identify(account.id, account);
} }

View File

@ -1,6 +1,8 @@
const toast = require('izitoast'); const toast = require('izitoast');
const cbor = require('borc'); const cbor = require('borc');
const throttle = require('lodash/throttle');
const SOCKET_URL = const SOCKET_URL =
`${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`; `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
@ -121,6 +123,11 @@ function createSocket(events) {
events.setActiveSkill(null); events.setActiveSkill(null);
} }
function sendGameOfferDraw(gameId) {
send(['GameOfferDraw', { game_id: gameId }]);
events.setActiveSkill(null);
}
function sendGameTarget(gameId, constructId, skillId) { function sendGameTarget(gameId, constructId, skillId) {
send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]); send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]);
events.setActiveSkill(null); events.setActiveSkill(null);
@ -360,10 +367,12 @@ function createSocket(events) {
sendGameReady, sendGameReady,
sendGameSkill, sendGameSkill,
sendGameSkillClear, sendGameSkillClear,
sendGameOfferDraw,
sendGameTarget, sendGameTarget,
sendInstanceAbandon, sendInstanceAbandon,
sendInstanceReady, // some weird shit happening in face off
sendInstanceReady: throttle(sendInstanceReady, 500),
sendInstancePractice, sendInstancePractice,
sendInstanceQueue, sendInstanceQueue,
sendInstanceState, sendInstanceState,

View File

@ -63,24 +63,6 @@ server {
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
} }
server {
server_name minimalstudios.com.au;
location / {
root /var/lib/mnml/public/press/;
index index.html;
try_files $uri $uri/ index.html;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/minimalstudios.com.au/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/minimalstudios.com.au/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
# http -> https # http -> https
server { server {
server_name mnml.gg; server_name mnml.gg;
@ -91,8 +73,3 @@ server {
server_name minimal.gg; server_name minimal.gg;
return 301 https://mnml.gg$request_uri; return 301 https://mnml.gg$request_uri;
} }
server {
server_name minimalstudios.com.au;
return 301 https://minimalstudios.com.au$request_uri;
}

View File

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

View File

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

View File

@ -20,7 +20,7 @@ use img;
use failure::Error; use failure::Error;
use failure::{err_msg, format_err}; use failure::{err_msg, format_err};
static PASSWORD_MIN_LEN: usize = 11; static PASSWORD_MIN_LEN: usize = 3;
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Account { pub struct Account {
@ -189,7 +189,7 @@ pub fn new_img(tx: &mut Transaction, id: Uuid) -> Result<Account, Error> {
} }
pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result<String, MnmlHttpError> { pub fn set_password(tx: &mut Transaction, id: Uuid, current: &String, password: &String) -> Result<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN { if password.len() < PASSWORD_MIN_LEN || password.len() > 100 {
return Err(MnmlHttpError::PasswordUnacceptable); return Err(MnmlHttpError::PasswordUnacceptable);
} }
@ -318,7 +318,7 @@ pub fn set_subscribed(tx: &mut Transaction, id: Uuid, subscribed: bool) -> Resul
} }
pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result<String, MnmlHttpError> { pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN { if password.len() < PASSWORD_MIN_LEN || password.len() > 100 {
return Err(MnmlHttpError::PasswordUnacceptable); return Err(MnmlHttpError::PasswordUnacceptable);
} }
@ -328,7 +328,7 @@ pub fn create(name: &String, password: &String, tx: &mut Transaction) -> Result<
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let img = Uuid::new_v4(); let img = Uuid::new_v4();
let rounds = 8; let rounds = 12;
let password = hash(&password, rounds)?; let password = hash(&password, rounds)?;
let mut rng = thread_rng(); let mut rng = thread_rng();

View File

@ -27,7 +27,7 @@ pub enum Phase {
Start, Start,
Skill, Skill,
Resolve, Resolve,
Finish, Finished,
} }
#[derive(Debug,Clone,Serialize,Deserialize)] #[derive(Debug,Clone,Serialize,Deserialize)]
@ -325,6 +325,30 @@ impl Game {
return Ok(self); return Ok(self);
} }
fn offer_draw(mut self, player_id: Uuid) -> Result<Game, Error> {
if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase"));
}
{
let player = self.player_by_id(player_id)?;
player.draw_offered = true;
}
// bots automatically accept draws
for player in self.players.iter_mut() {
if player.bot {
player.draw_offered = true;
}
}
if self.players.iter().all(|p| p.draw_offered) {
return Ok(self.finish());
}
return Ok(self);
}
fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> {
self.player_by_id(player_id)?; self.player_by_id(player_id)?;
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
@ -564,15 +588,18 @@ impl Game {
// } // }
pub fn finished(&self) -> bool { pub fn finished(&self) -> bool {
self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) self.phase == Phase::Finished || self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko()))
} }
pub fn winner(&self) -> Option<&Player> { pub fn winner(&self) -> Option<&Player> {
self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())) match self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) {
true => self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())),
false => None,
}
} }
fn finish(mut self) -> Game { fn finish(mut self) -> Game {
self.phase = Phase::Finish; self.phase = Phase::Finished;
// self.log.push(format!("Game finished.")); // self.log.push(format!("Game finished."));
// { // {
@ -594,7 +621,7 @@ impl Game {
} }
pub fn upkeep(mut self) -> Game { pub fn upkeep(mut self) -> Game {
if self.phase == Phase::Finish { if self.phase == Phase::Finished {
return self; return self;
} }
@ -903,6 +930,15 @@ pub fn game_skill(tx: &mut Transaction, account: &Account, game_id: Uuid, constr
Ok(game) Ok(game)
} }
pub fn game_offer_draw(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> {
let game = game_get(tx, game_id)?
.offer_draw(account.id)?;
game_update(tx, &game)?;
Ok(game)
}
pub fn game_skill_clear(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> { pub fn game_skill_clear(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> {
let mut game = game_get(tx, game_id)?; let mut game = game_get(tx, game_id)?;
@ -1051,7 +1087,7 @@ mod tests {
game = game.resolve_phase_start(); game = game.resolve_phase_start();
assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); assert!([Phase::Skill, Phase::Finished].contains(&game.phase));
return; return;
} }
@ -1117,7 +1153,7 @@ mod tests {
game = game.resolve_phase_start(); game = game.resolve_phase_start();
assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned()); assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned());
assert!(game.phase == Phase::Finish); assert!(game.phase == Phase::Finished);
} }
#[test] #[test]
@ -1423,7 +1459,7 @@ mod tests {
assert!(game.skill_phase_finished()); assert!(game.skill_phase_finished());
game = game.resolve_phase_start(); game = game.resolve_phase_start();
assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); assert!([Phase::Skill, Phase::Finished].contains(&game.phase));
// kill a construct // kill a construct
game.player_by_id(i_player.id).unwrap().construct_by_id(i_construct.id).unwrap().green_life.reduce(u64::max_value()); game.player_by_id(i_player.id).unwrap().construct_by_id(i_construct.id).unwrap().green_life.reduce(u64::max_value());

View File

@ -317,7 +317,13 @@ impl Instance {
self.phase_start = Utc::now(); self.phase_start = Utc::now();
self.phase_end = self.time_control.vbox_phase_end(); self.phase_end = self.time_control.vbox_phase_end();
let bits = match self.rounds.len() > 0 {
true => 12 + 6 * self.rounds.len(),
false => 0,
};
self.players.iter_mut().for_each(|p| { self.players.iter_mut().for_each(|p| {
p.vbox.balance_add(bits.into());
p.set_ready(false); p.set_ready(false);
p.vbox.fill(); p.vbox.fill();
}); });
@ -329,28 +335,18 @@ impl Instance {
} }
fn finish_condition(&mut self) -> bool { fn finish_condition(&mut self) -> bool {
// tennis self.players.iter().any(|p| p.score == Score::Win)
for player in self.players.iter() {
if player.score == Score::Win {
self.winner = Some(player.id);
return true;
}
}
// Game defaults to lose otherwise
if self.rounds.len() < 4 {
return false;
}
// both players afk
if self.players.iter().all(|p| p.score == Score::Zero) {
return true;
}
return false;
} }
pub fn finish(&mut self) -> &mut Instance { pub fn finish(&mut self) -> &mut Instance {
self.phase = InstancePhase::Finished; self.phase = InstancePhase::Finished;
for player in self.players.iter() {
if player.score == Score::Win {
self.winner = Some(player.id);
}
}
self self
} }
@ -412,38 +408,14 @@ impl Instance {
} }
// if you don't win, you lose // if you don't win, you lose
// ties can happen if both players forfeit // ties can happen if both players agree to a draw
// in this case we just finish the game and // or ticks fire and knock everybody out
// dock them 10k mmr if let Some(winner) = game.winner() {
let winner_id = match game.winner() {
Some(w) => w.id,
None => return Ok(self.finish()),
};
let bits = 12 + 6 * self.rounds.len();
{
let loser = self.players.iter()
.find(|p| p.id != winner_id)
.map(|p| p.score)
.unwrap();
let winner = self.players.iter_mut() let winner = self.players.iter_mut()
.find(|p| p.id == winner_id) .find(|p| p.id == winner.id)
.unwrap(); .unwrap();
winner.score = winner.score.add_win(&Score::Zero);
winner.score = winner.score.add_win(&loser); };
winner.vbox.balance_add(bits.into());
}
{
let loser = self.players.iter_mut()
.find(|p| p.id != winner_id)
.unwrap();
loser.score = loser.score.add_loss();
loser.vbox.balance_add(bits.into());
}
if self.all_games_finished() { if self.all_games_finished() {
self.next_round(); self.next_round();
@ -799,7 +771,7 @@ pub fn instance_state(tx: &mut Transaction, instance_id: Uuid) -> Result<RpcMess
let game = game_get(tx, game_id)?; let game = game_get(tx, game_id)?;
// return the game until it's finished // return the game until it's finished
if game.phase != Phase::Finish { if game.phase != Phase::Finished {
return Ok(RpcMessage::GameState(game)) return Ok(RpcMessage::GameState(game))
} }
} }

View File

@ -64,6 +64,7 @@ pub struct Player {
pub bot: bool, pub bot: bool,
pub ready: bool, pub ready: bool,
pub warnings: u8, pub warnings: u8,
pub draw_offered: bool,
pub score: Score, pub score: Score,
} }
@ -85,6 +86,7 @@ impl Player {
bot: false, bot: false,
ready: false, ready: false,
warnings: 0, warnings: 0,
draw_offered: false,
score: Score::Zero, score: Score::Zero,
}) })
} }
@ -99,6 +101,7 @@ impl Player {
bot: false, bot: false,
ready: false, ready: false,
warnings: 0, warnings: 0,
draw_offered: false,
score: Score::Zero, score: Score::Zero,
} }
} }

View File

@ -22,7 +22,7 @@ use account::{Account};
use account; use account;
use construct::{Construct}; use construct::{Construct};
use events::{Event}; use events::{Event};
use game::{Game, game_state, game_skill, game_skill_clear, game_ready}; use game::{Game, game_state, game_skill, game_skill_clear, game_ready, game_offer_draw};
use instance::{Instance, ChatState, instance_state, instance_practice, instance_ready, instance_abandon, demo}; use instance::{Instance, ChatState, instance_state, instance_practice, instance_ready, instance_abandon, demo};
use item::{Item, ItemInfoCtr, item_info}; use item::{Item, ItemInfoCtr, item_info};
use mtx; use mtx;
@ -90,6 +90,7 @@ pub enum RpcRequest {
GameReady { id: Uuid }, GameReady { id: Uuid },
GameSkill { game_id: Uuid, construct_id: Uuid, target_construct_id: Uuid, skill: Skill }, GameSkill { game_id: Uuid, construct_id: Uuid, target_construct_id: Uuid, skill: Skill },
GameSkillClear { game_id: Uuid }, GameSkillClear { game_id: Uuid },
GameOfferDraw { game_id: Uuid },
AccountState {}, AccountState {},
AccountShop {}, AccountShop {},
@ -218,6 +219,9 @@ impl Connection {
RpcRequest::GameReady { id } => RpcRequest::GameReady { id } =>
Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)), Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)),
RpcRequest::GameOfferDraw { game_id } =>
Ok(RpcMessage::GameState(game_offer_draw(&mut tx, account, game_id)?)),
RpcRequest::InstancePractice {} => RpcRequest::InstancePractice {} =>
Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)), Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)),
@ -450,6 +454,7 @@ pub fn start(pool: PgPool, events_tx: CbSender<Event>, stripe: StripeClient) {
} }
// we done // we done
Err(_e) => { Err(_e) => {
// info!("{:?}", e);
break; break;
}, },
}; };

View File

@ -1264,6 +1264,9 @@ impl Skill {
Skill::Invert| Skill::Invert|
Skill::InvertPlus | Skill::InvertPlus |
Skill::InvertPlusPlus | Skill::InvertPlusPlus |
Skill::Intercept|
Skill::InterceptPlus |
Skill::InterceptPlusPlus |
Skill::Counter| Skill::Counter|
Skill::CounterPlus | Skill::CounterPlus |
Skill::CounterPlusPlus | Skill::CounterPlusPlus |
@ -2083,7 +2086,7 @@ mod tests {
let Resolution { source: _, target: _, event, stages: _ } = results.remove(0); let Resolution { source: _, target: _, event, stages: _ } = results.remove(0);
match event { match event {
Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier()) Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::SiphonTick.multiplier())
+ 220.pct(Skill::SiphonTick.multiplier())), + 220.pct(Skill::SiphonTick.multiplier())),
_ => panic!("not damage siphon"), _ => panic!("not damage siphon"),
}; };