Merge branch 'release/1.6.6'

This commit is contained in:
ntr 2019-10-27 14:57:32 +11:00
commit f7426c199b
17 changed files with 151 additions and 74 deletions

View File

@ -2,6 +2,23 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [1.6.5] - 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
### Changed
- 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",
"version": "1.6.5",
"version": "1.6.6",
"description": "",
"main": "index.js",
"scripts": {

View File

@ -144,7 +144,15 @@ aside {
&:active, &.confirming {
background: @red;
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

@ -1,12 +1,12 @@
{
"name": "mnml-client",
"version": "1.6.5",
"version": "1.6.6",
"description": "",
"main": "index.js",
"scripts": {
"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",
"build": "parcel build index.html",
"build": "parcel build index.html --no-source-maps",
"scss": "node-sass --watch assets/scss -o assets/styles",
"lint": "eslint --fix --ext .jsx src/",
"test": "echo \"Error: no test specified\" && exit 1"

View File

@ -1,6 +1,7 @@
const anime = require('animejs').default;
function wiggle(id, idle) {
if (!idle) return true;
const duration = 300;
const target = document.getElementById(id);
const x = window.innerWidth * 0.01 * (Math.round(Math.random()) ? Math.random() : -Math.random());

View File

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

View File

@ -8,16 +8,23 @@ const addState = connect(
const {
ws,
game,
account,
} = state;
function sendAbandon() {
return ws.sendInstanceAbandon(game.instance);
}
function sendDraw() {
return ws.sendGameOfferDraw(game.id);
}
return {
game,
account,
sendAbandon,
sendDraw,
};
},
function receiveDispatch(dispatch) {
@ -34,13 +41,18 @@ const addState = connect(
function GameCtrlTopBtns(args) {
const {
game,
account,
leave,
sendAbandon,
sendDraw,
} = args;
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 => {
e.stopPropagation();
@ -48,16 +60,29 @@ function GameCtrlTopBtns(args) {
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 abandonText = abandonState ? 'Confirm' : 'Abandon';
const abandonAction = abandonState ? sendAbandon : abandonStateTrue;
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 (
<div class="instance-ctrl-btns">
{finished ? leaveBtn : abandonBtn}
{abandonBtn}
{drawBtn}
</div>
);
}

View File

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

View File

@ -68,6 +68,11 @@ function Scoreboard(args) {
};
const winner = player.score === 'Win';
const chatText = chat
? chat
: player.draw_offered
? 'draw'
: '\u00A0';
if (!isPlayer) {
const nameClass = `name ${player.img ? 'subscriber' : ''}`;
@ -77,7 +82,7 @@ function Scoreboard(args) {
<div class="score">{scoreText()}</div>
<div class={nameClass}>{player.name}</div>
<Img img={player.img} id={player.id} />
<div class="msg">{chat || '\u00A0'}</div>
<div class="msg">{chatText}</div>
</div>
);
}
@ -87,7 +92,7 @@ function Scoreboard(args) {
return (
<div class={boxClass}>
<div class="msg">{chat || '\u00A0'}</div>
<div class="msg">{chatText}</div>
<div class="score">{scoreText()}</div>
<div class={nameClass}>{player.name}</div>
<Img img={player.img} id={player.id} />

View File

@ -123,6 +123,11 @@ function createSocket(events) {
events.setActiveSkill(null);
}
function sendGameOfferDraw(gameId) {
send(['GameOfferDraw', { game_id: gameId }]);
events.setActiveSkill(null);
}
function sendGameTarget(gameId, constructId, skillId) {
send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]);
events.setActiveSkill(null);
@ -362,6 +367,7 @@ function createSocket(events) {
sendGameReady,
sendGameSkill,
sendGameSkillClear,
sendGameOfferDraw,
sendGameTarget,
sendInstanceAbandon,

View File

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

View File

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

View File

@ -27,7 +27,7 @@ pub enum Phase {
Start,
Skill,
Resolve,
Finish,
Finished,
}
#[derive(Debug,Clone,Serialize,Deserialize)]
@ -325,6 +325,30 @@ impl Game {
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> {
self.player_by_id(player_id)?;
if self.phase != Phase::Skill {
@ -564,15 +588,18 @@ impl Game {
// }
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> {
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 {
self.phase = Phase::Finish;
self.phase = Phase::Finished;
// self.log.push(format!("Game finished."));
// {
@ -594,7 +621,7 @@ impl Game {
}
pub fn upkeep(mut self) -> Game {
if self.phase == Phase::Finish {
if self.phase == Phase::Finished {
return self;
}
@ -903,6 +930,15 @@ pub fn game_skill(tx: &mut Transaction, account: &Account, game_id: Uuid, constr
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> {
let mut game = game_get(tx, game_id)?;
@ -1051,7 +1087,7 @@ mod tests {
game = game.resolve_phase_start();
assert!([Phase::Skill, Phase::Finish].contains(&game.phase));
assert!([Phase::Skill, Phase::Finished].contains(&game.phase));
return;
}
@ -1117,7 +1153,7 @@ mod tests {
game = game.resolve_phase_start();
assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned());
assert!(game.phase == Phase::Finish);
assert!(game.phase == Phase::Finished);
}
#[test]
@ -1423,7 +1459,7 @@ mod tests {
assert!(game.skill_phase_finished());
game = game.resolve_phase_start();
assert!([Phase::Skill, Phase::Finish].contains(&game.phase));
assert!([Phase::Skill, Phase::Finished].contains(&game.phase));
// kill a construct
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_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| {
p.vbox.balance_add(bits.into());
p.set_ready(false);
p.vbox.fill();
});
@ -329,28 +335,18 @@ impl Instance {
}
fn finish_condition(&mut self) -> bool {
// tennis
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;
self.players.iter().any(|p| p.score == Score::Win)
}
pub fn finish(&mut self) -> &mut Instance {
self.phase = InstancePhase::Finished;
for player in self.players.iter() {
if player.score == Score::Win {
self.winner = Some(player.id);
}
}
self
}
@ -412,38 +408,14 @@ impl Instance {
}
// if you don't win, you lose
// ties can happen if both players forfeit
// in this case we just finish the game and
// dock them 10k mmr
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();
// ties can happen if both players agree to a draw
// or ticks fire and knock everybody out
if let Some(winner) = game.winner() {
let winner = self.players.iter_mut()
.find(|p| p.id == winner_id)
.find(|p| p.id == winner.id)
.unwrap();
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());
}
winner.score = winner.score.add_win(&Score::Zero);
};
if self.all_games_finished() {
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)?;
// return the game until it's finished
if game.phase != Phase::Finish {
if game.phase != Phase::Finished {
return Ok(RpcMessage::GameState(game))
}
}

View File

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

View File

@ -22,7 +22,7 @@ use account::{Account};
use account;
use construct::{Construct};
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 item::{Item, ItemInfoCtr, item_info};
use mtx;
@ -90,6 +90,7 @@ pub enum RpcRequest {
GameReady { id: Uuid },
GameSkill { game_id: Uuid, construct_id: Uuid, target_construct_id: Uuid, skill: Skill },
GameSkillClear { game_id: Uuid },
GameOfferDraw { game_id: Uuid },
AccountState {},
AccountShop {},
@ -218,6 +219,9 @@ impl Connection {
RpcRequest::GameReady { 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 {} =>
Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)),