Merge branch 'release/1.12.0'

This commit is contained in:
ntr 2020-01-10 12:34:24 +10:00
commit d1606e9117
32 changed files with 460 additions and 150 deletions

View File

@ -1 +1 @@
1.11.2 1.12.0

View File

@ -3,32 +3,16 @@
_ntr_ _ntr_
* can't reset password without knowing password =\ * can't reset password without knowing password =\
* change cooldowns to delay & recharge
- delay is cooldown before skill can first be used
- recharge is cooldown after using skill
- every x speed reduces delay of skills
* audio * audio
* animation effects * animation effects
* vbox combine / buy / equip etc * vbox combine / buy / equip etc
* background music * background music
* effects rework
Siphon =
[
DamageBlue(50%),
Apply(
Siphon(2T)
- Siphoning(2T)
),
]
Hexagon Set
- Pick Colour
- Random Walk
- Draw hex
- Increase intensity for each visit
_mashy_ _mashy_
* combat text
- last 3-4 text events (damage / heal / disable etc)
* rebalance * rebalance
* speed specs * speed specs
* life specs * life specs
@ -111,6 +95,11 @@ _tba_
* treats * treats
* client animation bpm * client animation bpm
* background colour changes depending on time of day * background colour changes depending on time of day
Hexagon Set
- Pick Colour
- Random Walk
- Draw hex
- Increase intensity for each visit
# Mechanics # Mechanics
* 10d chaos maths, not rock paper scissors * 10d chaos maths, not rock paper scissors

View File

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

View File

@ -100,6 +100,12 @@ section {
} }
} }
.block-text {
letter-spacing: 0.25em;
text-transform: uppercase;
text-align: center;
}
.list { .list {
margin-bottom: 2em; margin-bottom: 2em;

View File

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

View File

@ -19,8 +19,8 @@ const addState = connect(
} = state; } = state;
function sendSetPassword(current, password) { function sendSetPassword(password) {
postData('/account/password', { current, password }) postData('/account/password', { password })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.error) return errorToast(data.error); if (data.error) return errorToast(data.error);
@ -74,7 +74,7 @@ class AccountStatus extends Component {
super(props); super(props);
this.state = { this.state = {
passwordState: { current: '', password: '', confirm: ''}, passwordState: { password: '', confirm: ''},
emailState: null, emailState: null,
unsubState: false, unsubState: false,
}; };
@ -105,8 +105,8 @@ class AccountStatus extends Component {
passwordState.password === passwordState.confirm; passwordState.password === passwordState.confirm;
const setPasswordDisabled = () => { const setPasswordDisabled = () => {
const { current, password, confirm } = passwordState; const { password, confirm } = passwordState;
return !(passwordsEqual() && password && current && confirm); return !(passwordsEqual() && password && confirm);
} }
const tlClick = e => { const tlClick = e => {
@ -173,15 +173,7 @@ class AccountStatus extends Component {
</div> </div>
<div> <div>
<h3>Password</h3> <h3>Password</h3>
<label for="current">Password:</label> <label for="current">Set Password:</label>
<input
class="login-input"
type="password"
name="current"
value={passwordState.current}
onInput={linkState(this, 'passwordState.current')}
placeholder="current"
/>
<input <input
class="login-input" class="login-input"
type="password" type="password"
@ -202,7 +194,7 @@ class AccountStatus extends Component {
/> />
<button <button
disabled={setPasswordDisabled()} disabled={setPasswordDisabled()}
onClick={() => sendSetPassword(passwordState.current, passwordState.password)}> onClick={() => sendSetPassword(passwordState.password)}>
Set Password Set Password
</button> </button>
</div> </div>

View File

@ -14,6 +14,7 @@ const addState = connect(
const { const {
ws, ws,
account, account,
tutorial,
} = state; } = state;
function sendInstancePractice() { function sendInstancePractice() {
@ -21,6 +22,7 @@ const addState = connect(
} }
return { return {
promptRegister: tutorial === 99, // see events
account, account,
sendInstancePractice, sendInstancePractice,
}; };
@ -30,6 +32,7 @@ const addState = connect(
function Play(args) { function Play(args) {
const { const {
account, account,
promptRegister,
sendInstancePractice, sendInstancePractice,
} = args; } = args;
@ -47,6 +50,17 @@ function Play(args) {
); );
const list = () => { const list = () => {
if (promptRegister) {
return (
<div class='block-text'>
<p><b>You just won your first round of MNML.</b></p>
<p>Register below to play a real Bo5 against other players, play a practice round, customise your team & more...</p>
<p>glhf</p>
</div>
)
}
return ( return (
<div class='list play'> <div class='list play'>
<figure> <figure>

View File

@ -9,6 +9,7 @@ const addState = connect(
ws, ws,
game, game,
account, account,
authenticated,
chatShow, chatShow,
animating, animating,
} = state; } = state;
@ -33,6 +34,7 @@ const addState = connect(
return { return {
game, game,
account, account,
authenticated,
chatShow, chatShow,
sendAbandon, sendAbandon,
sendGameSkillClear, sendGameSkillClear,
@ -65,6 +67,7 @@ function GameCtrlBtns(args) {
animating, animating,
account, account,
chatShow, chatShow,
authenticated,
getInstanceState, getInstanceState,
sendGameSkillClear, sendGameSkillClear,
@ -77,7 +80,9 @@ function GameCtrlBtns(args) {
const finished = game.phase === 'Finished'; const finished = game.phase === 'Finished';
function quitClick() { function quitClick() {
if (authenticated) {
getInstanceState(); getInstanceState();
}
quit(); quit();
} }

View File

@ -9,6 +9,7 @@ const addState = connect(
ws, ws,
game, game,
animating, animating,
authenticated,
account, account,
} = state; } = state;
@ -27,6 +28,7 @@ const addState = connect(
return { return {
game, game,
account, account,
authenticated,
sendAbandon, sendAbandon,
sendDraw, sendDraw,
@ -50,6 +52,7 @@ function GameCtrlTopBtns(args) {
const { const {
game, game,
account, account,
authenticated,
leave, leave,
sendAbandon, sendAbandon,
@ -82,6 +85,11 @@ function GameCtrlTopBtns(args) {
setTimeout(() => this.setState({ concedeState: false }), 2000); setTimeout(() => this.setState({ concedeState: false }), 2000);
}; };
const authBtn = btn => {
if (authenticated) return btn;
return <button disabled>-</button>
}
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;
@ -102,9 +110,9 @@ function GameCtrlTopBtns(args) {
return ( return (
<div class="instance-ctrl-btns"> <div class="instance-ctrl-btns">
{abandonBtn} {authBtn(abandonBtn)}
{concedeBtn} {authBtn(concedeBtn)}
{drawBtn} {authBtn(drawBtn)}
</div> </div>
); );
} }

View File

@ -7,6 +7,7 @@ const addState = connect(
function receiveState(state) { function receiveState(state) {
const { const {
ws, ws,
authenticated,
instance, instance,
tutorial, tutorial,
} = state; } = state;
@ -17,6 +18,7 @@ const addState = connect(
return { return {
instance, instance,
authenticated,
tutorial, tutorial,
sendAbandon, sendAbandon,
}; };
@ -39,6 +41,7 @@ const addState = connect(
function InstanceTopBtns(args) { function InstanceTopBtns(args) {
const { const {
instance, instance,
authenticated,
leave, leave,
sendAbandon, sendAbandon,
@ -61,9 +64,16 @@ function InstanceTopBtns(args) {
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(tutorial)}>Leave</button>; const leaveBtn = <button class='abandon confirming' onClick={() => leave(tutorial)}>Leave</button>;
const finalBtn = () => {
// disable for tutorial mode
if (!authenticated) return <button disabled='true'>-</button>;
if (finished) return leaveBtn;
return abandonBtn;
}
return ( return (
<div class="instance-ctrl-btns"> <div class="instance-ctrl-btns">
{finished ? leaveBtn : abandonBtn} {finalBtn()}
</div> </div>
); );
} }

View File

@ -232,7 +232,6 @@ 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

@ -96,7 +96,7 @@ function genItemInfo(item, itemInfo, player) {
itemSourceInfo = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]()); itemSourceInfo = reactStringReplace(itemSourceInfo, itemRegEx, match => shapes[match]());
} }
const cooldown = isSkill && fullInfo.cooldown ? <div>{fullInfo.cooldown} Turn delay</div> : null; const cooldown = isSkill && fullInfo.cooldown ? <div>{fullInfo.delay} turn delay, {fullInfo.cooldown} turn cooldown</div> : null;
const speed = isSkill const speed = isSkill
? <div> Speed {shapes.SpeedStat()} multiplier {fullInfo.speed * 4}% </div> ? <div> Speed {shapes.SpeedStat()} multiplier {fullInfo.speed * 4}% </div>

View File

@ -1,13 +1,31 @@
// eslint-disable-next-line // eslint-disable-next-line
const preact = require('preact'); const preact = require('preact');
const { connect } = require('preact-redux');
const Login = require('./welcome.login'); const Login = require('./welcome.login');
const Register = require('./welcome.register'); const Register = require('./welcome.register');
const Help = require('./welcome.help'); const Help = require('./welcome.help');
// const About = require('./welcome.about'); // const About = require('./welcome.about');
function Welcome() { const addState = connect(
const page = this.state.page || 'login'; function receiveState(state) {
const {
tutorial,
} = state;
return {
promptRegister: tutorial === 99, // see events
};
},
);
function Welcome(args) {
const {
promptRegister,
} = args;
const page = this.state.page || promptRegister && 'register' || 'login';
const pageEl = () => { const pageEl = () => {
if (page === 'login') return <Login />; if (page === 'login') return <Login />;
@ -45,4 +63,4 @@ function Welcome() {
); );
} }
module.exports = Welcome; module.exports = addState(Welcome);

View File

@ -71,7 +71,7 @@ module.exports = {
}, },
speedStat: { speedStat: {
item: 'SPEED', item: 'SPEED',
description: 'Speed determines the order in which skills resolve.\nCombine SPEED specs to increase speed.', description: 'Speed determines the order in which skills resolve.\nThe initial delay of skills is reduced by 1 turn for every 250 speed.\nCombine SPEED specs to increase speed.',
}, },
}, },
}; };

View File

@ -218,6 +218,10 @@ function registerEvents(store) {
store.dispatch(actions.setTutorial(1)); store.dispatch(actions.setTutorial(1));
} }
function promptRegister() {
store.dispatch(actions.setTutorial(99));
store.dispatch(actions.setInstance(null));
}
window.addEventListener('hashchange', urlHashChange, false); window.addEventListener('hashchange', urlHashChange, false);
@ -250,6 +254,7 @@ function registerEvents(store) {
setWs, setWs,
startTutorial, startTutorial,
promptRegister,
urlHashChange, urlHashChange,

View File

@ -301,6 +301,7 @@ function createSocket(events) {
// Joining: () => events.notify('Searching for instance...'), // Joining: () => events.notify('Searching for instance...'),
StartTutorial: () => events.startTutorial(), StartTutorial: () => events.startTutorial(),
PromptRegister: () => events.promptRegister(),
Processing: () => true, Processing: () => true,
Error: errHandler, Error: errHandler,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mnml_core" name = "mnml_core"
version = "1.11.2" version = "1.12.0"
authors = ["ntr <ntr@smokestack.io>", "mashy <mashy@mnml.gg>"] authors = ["ntr <ntr@smokestack.io>", "mashy <mashy@mnml.gg>"]
[dependencies] [dependencies]

View File

@ -52,10 +52,14 @@ impl ConstructSkill {
pub fn new(skill: Skill) -> ConstructSkill { pub fn new(skill: Skill) -> ConstructSkill {
ConstructSkill { ConstructSkill {
skill, skill,
cd: skill.base_cd(), cd: skill.delay(),
disabled: false, disabled: false,
} }
} }
pub fn set_cooldown(&mut self, cd: Cooldown) -> () {
self.cd = cd;
}
} }
#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
@ -432,15 +436,34 @@ impl Construct {
self.skills.iter().find(|s| s.skill == skill && s.cd.is_some()) self.skills.iter().find(|s| s.skill == skill && s.cd.is_some())
} }
pub fn set_construct_delays(&mut self) -> () {
// for every multiple of speed threshold delays are reduced by 1 at start of game
let speed_threshold = 250;
let delay_reduction = self.speed.value.wrapping_div(speed_threshold);
self.skills
.iter_mut()
.for_each(|s| match s.skill.delay() {
Some(cd) => match cd.saturating_sub(delay_reduction) {
0 => s.set_cooldown(None),
_ => s.set_cooldown(Some(cd.saturating_sub(delay_reduction)))
},
None => s.set_cooldown(None)
});
}
pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct { pub fn skill_set_cd(&mut self, skill: Skill) -> &mut Construct {
// println!("{:?} {:?} skill cooldown set", self.name, skill); // println!("{:?} {:?} skill cooldown set", self.name, skill);
// tests force resolve some skills // tests force resolve some skills
// which cause the game to attempt to put them on cd // which cause the game to attempt to put them on cd
// even though the construct doesn't know the skill // even though the construct doesn't know the skill
if let Some(i) = self.skills.iter().position(|s| s.skill == skill) { // if let Some(i) = self.skills.iter().position(|s| s.skill == skill) {
self.skills.remove(i); // self.skills.remove(i);
self.skills.insert(i, ConstructSkill::new(skill)); // self.skills.insert(i, ConstructSkill::new(skill));
//}
if let Some(sk) = self.skills.iter_mut().find(|s| s.skill == skill) {
sk.set_cooldown(skill.base_cd());
} }
self self
@ -1218,7 +1241,7 @@ mod tests {
i += 1; i += 1;
} }
assert_eq!(i, Skill::Sleep.base_cd().unwrap()); assert_eq!(i, Skill::Sleep.delay().unwrap());
} }
} }

View File

@ -104,7 +104,6 @@ impl Game {
// let player_description = player.constructs.iter().map(|c| c.name.clone()).collect::<Vec<String>>().join(", "); // let player_description = player.constructs.iter().map(|c| c.name.clone()).collect::<Vec<String>>().join(", ");
// self.log.push(format!("{:} has joined the game. [{:}]", player.name, player_description)); // self.log.push(format!("{:} has joined the game. [{:}]", player.name, player_description));
player.constructs.sort_unstable_by_key(|c| c.id);
self.players.push(player); self.players.push(player);
Ok(self) Ok(self)
@ -140,12 +139,19 @@ impl Game {
&& self.players.iter().all(|t| t.constructs.len() == self.player_constructs) && self.players.iter().all(|t| t.constructs.len() == self.player_constructs)
} }
pub fn start(self) -> Game { pub fn start(mut self) -> Game {
// both forfeit ddue to no skills // both forfeit ddue to no skills
if self.finished() { if self.finished() {
return self.finish(); return self.finish();
} }
self.players
.iter_mut()
.for_each(|p| p.constructs
.iter_mut()
.for_each(|c| c.set_construct_delays())
);
self.skill_phase_start(0) self.skill_phase_start(0)
} }
@ -1163,6 +1169,44 @@ mod tests {
return; return;
} }
#[test]
fn delay_test() {
let mut x = Construct::new()
.named(&"pronounced \"creeep\"".to_string())
.learn(Skill::Ruin);
let mut y = Construct::new()
.named(&"lemongrass tea".to_string())
.learn(Skill::Ruin);
// Ruin has 2 turn cd
// 250 speed = 1 cd delay reduction
x.speed.force(499);
y.speed.force(700);
let mut game = Game::new();
game.set_player_num(2).set_player_constructs(1);
let x_player_id = Uuid::new_v4();
x.account = x_player_id;
let x_player = Player::new(x_player_id, None, &"ntr".to_string(), vec![x]);
let y_player_id = Uuid::new_v4();
y.account = y_player_id;
let y_player = Player::new(y_player_id, None, &"mash".to_string(), vec![y]);
game
.player_add(x_player).unwrap()
.player_add(y_player).unwrap();
game = game.start();
assert!(game.players[0].constructs[0].skill_on_cd(Skill::Ruin).is_some());
assert!(game.players[1].constructs[0].skill_on_cd(Skill::Ruin).is_none());
}
#[test] #[test]
fn stun_test() { fn stun_test() {
let mut game = create_test_game(); let mut game = create_test_game();
@ -1240,7 +1284,7 @@ mod tests {
// should auto progress back to skill phase // should auto progress back to skill phase
assert!(game.phase == Phase::Skill); assert!(game.phase == Phase::Skill);
assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_some()); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_none());
assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none());
game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap();

View File

@ -1362,6 +1362,7 @@ pub struct ItemInfo {
pub values: Option<SpecValues>, pub values: Option<SpecValues>,
pub skill: bool, pub skill: bool,
pub speed: Option<usize>, pub speed: Option<usize>,
pub delay: Cooldown,
pub cooldown: Cooldown, pub cooldown: Cooldown,
pub description: String, pub description: String,
} }
@ -1406,6 +1407,10 @@ pub fn item_info() -> ItemInfoCtr {
Some(s) => s.base_cd(), Some(s) => s.base_cd(),
None => None None => None
}, },
delay: match v.into_skill() {
Some(s) => s.delay(),
None => None
},
}) })
.collect::<Vec<ItemInfo>>(); .collect::<Vec<ItemInfo>>();

View File

@ -65,7 +65,9 @@ pub struct Player {
} }
impl Player { impl Player {
pub fn new(account: Uuid, img: Option<Uuid>, name: &String, constructs: Vec<Construct>) -> Player { pub fn new(account: Uuid, img: Option<Uuid>, name: &String, mut constructs: Vec<Construct>) -> Player {
constructs.sort_unstable_by_key(|c| c.id);
Player { Player {
id: account, id: account,
img, img,

View File

@ -522,6 +522,157 @@ impl Skill {
Skill::SustainPlus | Skill::SustainPlus |
Skill::SustainPlusPlus => Some(1), Skill::SustainPlusPlus => Some(1),
Skill::Intercept => Some(2),
Skill::InterceptPlus => Some(2),
Skill::InterceptPlusPlus => Some(2),
Skill::Electrify |
Skill::ElectrifyPlus |
Skill::ElectrifyPlusPlus => None,
Skill::Absorb |
Skill::AbsorbPlus |
Skill::AbsorbPlusPlus => Some(1),
//-----------
// Never cast directly
//---------
// Trigger
Skill::HybridBlast |
Skill::HasteStrike |
Skill::CounterAttack|
Skill::CounterAttackPlus |
Skill::CounterAttackPlusPlus | // counter
Skill::Electrocute|
Skill::ElectrocutePlus |
Skill::ElectrocutePlusPlus |
Skill::Absorption|
Skill::AbsorptionPlus |
Skill::AbsorptionPlusPlus |
// Ticks
Skill::ElectrocuteTick|
Skill::DecayTick|
Skill::SiphonTick|
Skill::TriageTick => None,
}
}
pub fn delay(&self) -> Cooldown {
match self {
Skill::Attack => None,
Skill::Block => None, // reduce damage
Skill::Buff => None,
Skill::Debuff => None,
Skill::Stun => Some(1),
Skill::Strike=> None,
Skill::StrikePlus => None,
Skill::StrikePlusPlus => None,
Skill::Counter|
Skill::CounterPlus |
Skill::CounterPlusPlus => None,
Skill::Restrict |
Skill::RestrictPlus |
Skill::RestrictPlusPlus => Some(1),
Skill::Bash |
Skill::BashPlus |
Skill::BashPlusPlus => Some(1),
Skill::Heal=> None,
Skill::HealPlus => None,
Skill::HealPlusPlus => None,
Skill::Triage=> None, // hot
Skill::TriagePlus => None, // hot
Skill::TriagePlusPlus => None, // hot
Skill::Break | // no damage stun, adds vulnerable
Skill::BreakPlus |
Skill::BreakPlusPlus => Some(1),
Skill::Blast |
Skill::BlastPlus |
Skill::BlastPlusPlus => None,
Skill::Chaos |
Skill::ChaosPlus |
Skill::ChaosPlusPlus => None,
Skill::Amplify |
Skill::AmplifyPlus |
Skill::AmplifyPlusPlus => Some(1),
Skill::Hybrid |
Skill::HybridPlus |
Skill::HybridPlusPlus => Some(1),
Skill::Invert |
Skill::InvertPlus |
Skill::InvertPlusPlus => Some(2),
Skill::Decay => None, // dot
Skill::DecayPlus => None,
Skill::DecayPlusPlus => None,
Skill::Siphon|
Skill::SiphonPlus |
Skill::SiphonPlusPlus => None,
Skill::Curse |
Skill::CursePlus |
Skill::CursePlusPlus => Some(1),
Skill::Link |
Skill::LinkPlus |
Skill::LinkPlusPlus => Some(1),
Skill::Silence |
Skill::SilencePlus |
Skill::SilencePlusPlus => Some(1),
Skill::Purify |
Skill::PurifyPlus |
Skill::PurifyPlusPlus => None,
Skill::Purge |
Skill::PurgePlus |
Skill::PurgePlusPlus => Some(1),
Skill::Banish |
Skill::BanishPlus |
Skill::BanishPlusPlus => Some(1),
Skill::Haste |
Skill::HastePlus |
Skill::HastePlusPlus => Some(1),
Skill::Reflect |
Skill::ReflectPlus |
Skill::ReflectPlusPlus => None,
Skill::Recharge |
Skill::RechargePlus |
Skill::RechargePlusPlus => None,
Skill::Ruin |
Skill::RuinPlus |
Skill::RuinPlusPlus => Some(2),
Skill::Slay=> None,
Skill::SlayPlus => None,
Skill::SlayPlusPlus => None,
Skill::Sleep |
Skill::SleepPlus |
Skill::SleepPlusPlus => Some(1),
Skill::Sustain |
Skill::SustainPlus |
Skill::SustainPlusPlus => Some(1),
Skill::Intercept => Some(1), Skill::Intercept => Some(1),
Skill::InterceptPlus => Some(1), Skill::InterceptPlus => Some(1),
Skill::InterceptPlusPlus => Some(1), Skill::InterceptPlusPlus => Some(1),

View File

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

View File

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

View File

@ -213,42 +213,42 @@ pub fn new_img(tx: &mut Transaction, id: Uuid) -> Result<Account, Error> {
Account::try_from(row) Account::try_from(row)
} }
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, password: &String) -> Result<String, MnmlHttpError> {
if password.len() < PASSWORD_MIN_LEN || password.len() > 100 { if password.len() < PASSWORD_MIN_LEN || password.len() > 100 {
return Err(MnmlHttpError::PasswordUnacceptable); return Err(MnmlHttpError::PasswordUnacceptable);
} }
let query = " // let query = "
SELECT id, password // SELECT id, password
FROM accounts // FROM accounts
WHERE id = $1 // WHERE id = $1
"; // ";
let result = tx // let result = tx
.query(query, &[&id])?; // .query(query, &[&id])?;
let row = match result.iter().next() { // let row = match result.iter().next() {
Some(row) => row, // Some(row) => row,
None => { // None => {
let mut rng = thread_rng(); // let mut rng = thread_rng();
let garbage: String = iter::repeat(()) // let garbage: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric)) // .map(|()| rng.sample(Alphanumeric))
.take(64) // .take(64)
.collect(); // .collect();
// verify garbage to prevent timing attacks // // verify garbage to prevent timing attacks
verify(garbage.clone(), &garbage).ok(); // verify(garbage.clone(), &garbage).ok();
return Err(MnmlHttpError::AccountNotFound); // return Err(MnmlHttpError::AccountNotFound);
}, // },
}; // };
let id: Uuid = row.get(0); // let id: Uuid = row.get(0);
let db_pw: String = row.get(1); // let db_pw: String = row.get(1);
// return bad request to prevent being logged out // // return bad request to prevent being logged out
if !verify(current, &db_pw)? { // if !verify(current, &db_pw)? {
return Err(MnmlHttpError::BadRequest); // return Err(MnmlHttpError::BadRequest);
} // }
let password = hash(&password, PASSWORD_ROUNDS)?; let password = hash(&password, PASSWORD_ROUNDS)?;

View File

@ -369,7 +369,7 @@ fn recover(req: &mut Request) -> IronResult<Response> {
#[derive(Debug,Clone,Deserialize)] #[derive(Debug,Clone,Deserialize)]
struct SetPassword { struct SetPassword {
current: String, // current: String,
password: String, password: String,
} }
@ -385,7 +385,7 @@ fn set_password(req: &mut Request) -> IronResult<Response> {
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?; let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?; let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
let token = account::set_password(&mut tx, a.id, &params.current, &params.password)?; let token = account::set_password(&mut tx, a.id, &params.password)?;
tx.commit().or(Err(MnmlHttpError::ServerError))?; tx.commit().or(Err(MnmlHttpError::ServerError))?;

View File

@ -42,10 +42,10 @@ pub enum Mail {
fn recover(email: &String, name: &String, token: &String) -> SendableEmail { fn recover(email: &String, name: &String, token: &String) -> SendableEmail {
let body = format!("{:}, let body = format!("{:},
the link below will recover your account. the link below will recover your account.
please change your password immediately in the account page. please change your password immediately in the account page
this link will expire in 48 hours or once used. as this link will expire in 48 hours or once used.
http://mnml.gg/api/account/recover?recover_token={:} https://mnml.gg/api/account/recover?recover_token={:}
glhf glhf
--mnml", name, token); --mnml", name, token);
@ -63,7 +63,7 @@ glhf
fn confirm(email: &String, name: &String, token: &String) -> SendableEmail { fn confirm(email: &String, name: &String, token: &String) -> SendableEmail {
let confirm_body = format!("{:}, let confirm_body = format!("{:},
please click the link below to confirm your email please click the link below to confirm your email
http://mnml.gg/api/account/email/confirm?confirm_token={:} https://mnml.gg/api/account/email/confirm?confirm_token={:}
glhf glhf
--mnml", name, token); --mnml", name, token);

View File

@ -695,11 +695,17 @@ pub fn instance_practice(tx: &mut Transaction, account: &Account) -> Result<Inst
} }
pub fn instance_demo(account: &Account) -> Result<Instance, Error> { pub fn instance_demo(account: &Account) -> Result<Instance, Error> {
let bot = bot_player(); let mut bot = bot_player();
let bot_id = bot.id; let bot_id = bot.id;
// generate imgs for the client to see // generate imgs for the client to see
for c in bot.constructs.iter() { for c in bot.constructs.iter_mut() {
// smash these nubs
c.green_life.force(64);
c.red_life.force(0);
c.blue_life.force(0);
img::shapes_write(c.img)?; img::shapes_write(c.img)?;
} }
@ -709,6 +715,7 @@ pub fn instance_demo(account: &Account) -> Result<Instance, Error> {
let player = anon_player(account.id); let player = anon_player(account.id);
// smash these noobs
for c in player.constructs.iter() { for c in player.constructs.iter() {
img::shapes_write(c.img)?; img::shapes_write(c.img)?;
} }

View File

@ -2,7 +2,6 @@ use mnml_core::item::ItemInfoCtr;
use mnml_core::instance::ChatState; use mnml_core::instance::ChatState;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant};
use std::thread::{spawn}; use std::thread::{spawn};
use std::str; use std::str;
@ -23,7 +22,7 @@ use account;
use events::{Event}; use events::{Event};
use user_anonymous::{Anonymous}; use user_anonymous::{Anonymous};
use user_authenticated::{Authorised}; use user_authenticated::{Authenticated};
use mnml_core::construct::{Construct}; use mnml_core::construct::{Construct};
use mnml_core::game::{Game}; use mnml_core::game::{Game};
@ -37,7 +36,7 @@ use mnml_core::instance::{Instance};
use mtx; use mtx;
use mail::Email; use mail::Email;
use pg::{Db};
use pg::{PgPool}; use pg::{PgPool};
use http::{AUTH_CLEAR, TOKEN_HEADER}; use http::{AUTH_CLEAR, TOKEN_HEADER};
@ -63,6 +62,7 @@ pub enum RpcMessage {
Pong(()), Pong(()),
StartTutorial(()), StartTutorial(()),
PromptRegister(()),
QueueRequested(()), QueueRequested(()),
QueueJoined(()), QueueJoined(()),
@ -126,9 +126,9 @@ pub enum RpcRequest {
} }
pub trait User { pub trait User {
fn receive(&mut self, data: Vec<u8>, db: &Db, begin: Instant, events: &CbSender<Event>, stripe: &StripeClient) -> Result<RpcMessage, Error>; fn receive(&mut self, data: Vec<u8>, stripe: &StripeClient) -> Result<RpcMessage, Error>;
fn connected(&mut self, db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error>; fn connected(&mut self) -> Result<(), Error>;
fn send(&mut self, msg: RpcMessage, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error>; fn send(&mut self, msg: RpcMessage) -> Result<(), Error>;
} }
struct Connection { struct Connection {
@ -165,20 +165,16 @@ impl Connection {
// when it encounters errors // when it encounters errors
impl Handler for Connection { impl Handler for Connection {
fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> { fn on_open(&mut self, _: ws::Handshake) -> ws::Result<()> {
let db = self.pool.get().unwrap(); self.user.connected().unwrap();
self.user.connected(&db, &self.events, &self.ws).unwrap();
Ok(()) Ok(())
} }
fn on_message(&mut self, msg: Message) -> ws::Result<()> { fn on_message(&mut self, msg: Message) -> ws::Result<()> {
match msg { match msg {
Message::Binary(msg) => { Message::Binary(msg) => {
let begin = Instant::now(); match self.user.receive(msg, &self.stripe) {
let db_connection = self.pool.get().unwrap();
match self.user.receive(msg, &db_connection, begin, &self.events, &self.stripe) {
Ok(msg) => { Ok(msg) => {
self.user.send(msg, &self.events, &self.ws).unwrap(); self.user.send(msg).unwrap();
}, },
Err(e) => { Err(e) => {
warn!("{:?}", e); warn!("{:?}", e);
@ -220,7 +216,7 @@ impl Handler for Connection {
if cookie.name() == TOKEN_HEADER { if cookie.name() == TOKEN_HEADER {
let db = self.pool.get().unwrap(); let db = self.pool.get().unwrap();
match account::from_token(&db, &cookie.value().to_string()) { match account::from_token(&db, &cookie.value().to_string()) {
Ok(a) => self.user = Box::new(Authorised { id: a.id, account: a }), Ok(a) => self.user = Box::new(Authenticated::new(a, self.ws.clone(), self.events.clone(), self.pool.clone())),
Err(_) => return unauth(), Err(_) => return unauth(),
} }
} }
@ -268,11 +264,11 @@ pub fn start(pool: PgPool, events_tx: CbSender<Event>, stripe: StripeClient) {
DeflateHandler::new( DeflateHandler::new(
Connection { Connection {
id, id,
ws: tx, ws: tx.clone(),
pool: pool.clone(), pool: pool.clone(),
stripe: stripe.clone(), stripe: stripe.clone(),
events: events_tx.clone(), events: events_tx.clone(),
user: Box::new(Anonymous { id, account: anon_account, game: None, instance: None }) user: Box::new(Anonymous::new(anon_account, tx))
} }
) )
}) })

View File

@ -1,4 +1,3 @@
use std::time::Instant;
use uuid::Uuid; use uuid::Uuid;
use failure::Error; use failure::Error;
@ -9,11 +8,8 @@ use crossbeam_channel::{Sender as CbSender};
use serde_cbor::{from_slice}; use serde_cbor::{from_slice};
use stripe::{Client as StripeClient}; use stripe::{Client as StripeClient};
use account::{Account}; use account::{Account};
use pg::{Db};
use pg; use pg;
use events::{Event};
use rpc::{RpcMessage, RpcRequest, User}; use rpc::{RpcMessage, RpcRequest, User};
use mnml_core::game::Game; use mnml_core::game::Game;
@ -26,10 +22,24 @@ pub struct Anonymous {
pub id: Uuid, pub id: Uuid,
pub instance: Option<Instance>, pub instance: Option<Instance>,
pub game: Option<Game>, pub game: Option<Game>,
ws: CbSender<RpcMessage>,
}
impl Anonymous {
pub fn new(account: Account, ws: CbSender<RpcMessage>) -> Anonymous {
Anonymous {
id: account.id,
account,
ws,
instance: None,
game: None,
}
}
} }
impl User for Anonymous { impl User for Anonymous {
fn send(&mut self, msg: RpcMessage, _events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> { fn send(&mut self, msg: RpcMessage) -> Result<(), Error> {
// if the user queries the state of something // if the user queries the state of something
// we tell events to push updates to them // we tell events to push updates to them
match msg { match msg {
@ -40,21 +50,21 @@ impl User for Anonymous {
_ => (), _ => (),
}; };
ws.send(msg)?; self.ws.send(msg)?;
Ok(()) Ok(())
} }
fn connected(&mut self, _db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> { fn connected(&mut self) -> Result<(), Error> {
info!("anonymous connection"); info!("anonymous connection");
self.send(RpcMessage::AccountState(self.account.clone()), events, ws)?; self.ws.send(RpcMessage::AccountState(self.account.clone()))?;
self.send(RpcMessage::StartTutorial(()), events, ws)?; self.ws.send(RpcMessage::StartTutorial(()))?;
Ok(()) Ok(())
} }
fn receive(&mut self, data: Vec<u8>, _db: &Db, _begin: Instant, _events: &CbSender<Event>, _stripe: &StripeClient) -> Result<RpcMessage, Error> { fn receive(&mut self, data: Vec<u8>, _stripe: &StripeClient) -> Result<RpcMessage, Error> {
match from_slice::<RpcRequest>(&data) { match from_slice::<RpcRequest>(&data) {
Ok(v) => { Ok(v) => {
let get_instance = || { let get_instance = || {
@ -139,6 +149,10 @@ impl User for Anonymous {
game = game.resolve_phase_start(); game = game.resolve_phase_start();
} }
if game.finished() {
self.ws.send(RpcMessage::PromptRegister(()))?;
}
Ok(RpcMessage::GameState(game)) Ok(RpcMessage::GameState(game))
}, },

View File

@ -37,72 +37,90 @@ use events::{Event};
use mtx; use mtx;
use mail; use mail;
use payments; use payments;
use pg::{Db}; use pg::{PgPool};
use rpc::{RpcMessage, RpcRequest, User}; use rpc::{RpcMessage, RpcRequest, User};
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
pub struct Authorised { pub struct Authenticated {
pub account: Account, pub account: Account,
pub id: Uuid pub id: Uuid,
events: CbSender<Event>,
ws: CbSender<RpcMessage>,
pool: PgPool,
} }
impl User for Authorised { impl Authenticated {
fn send(&mut self, msg: RpcMessage, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> { pub fn new(account: Account, ws: CbSender<RpcMessage>, events: CbSender<Event>, pool: PgPool) -> Authenticated {
Authenticated {
id: account.id,
account,
ws,
events,
pool,
}
}
}
impl User for Authenticated {
fn send(&mut self, msg: RpcMessage) -> Result<(), Error> {
// if the user queries the state of something // if the user queries the state of something
// we tell events to push updates to them // we tell events to push updates to them
match msg { match msg {
RpcMessage::AccountState(ref v) => { RpcMessage::AccountState(ref v) => {
events.send(Event::Subscribe(self.id, v.id))? self.events.send(Event::Subscribe(self.id, v.id))?
}, },
RpcMessage::GameState(ref v) => RpcMessage::GameState(ref v) =>
events.send(Event::Subscribe(self.id, v.id))?, self.events.send(Event::Subscribe(self.id, v.id))?,
RpcMessage::InstanceState(ref v) => RpcMessage::InstanceState(ref v) =>
events.send(Event::Subscribe(self.id, v.id))?, self.events.send(Event::Subscribe(self.id, v.id))?,
_ => (), _ => (),
}; };
ws.send(msg)?; self.ws.send(msg)?;
Ok(()) Ok(())
} }
fn connected(&mut self, db: &Db, events: &CbSender<Event>, ws: &CbSender<RpcMessage>) -> Result<(), Error> { fn connected(&mut self) -> Result<(), Error> {
info!("authenticated connection account={:?}", self.account); info!("authenticated connection account={:?}", self.account);
let a = &self.account; let a = &self.account;
ws.send(RpcMessage::AccountAuthenticated(a.clone()))?; self.ws.send(RpcMessage::AccountAuthenticated(a.clone()))?;
// tell events we have connected // tell events we have connected
events.send(Event::Connect(self.id, a.clone(), ws.clone()))?; self.events.send(Event::Connect(self.id, a.clone(), self.ws.clone()))?;
ws.send(RpcMessage::AccountState(a.clone()))?; self.ws.send(RpcMessage::AccountState(a.clone()))?;
events.send(Event::Subscribe(self.id, a.id))?; self.events.send(Event::Subscribe(self.id, a.id))?;
// check if they have an image that needs to be generated // check if they have an image that needs to be generated
account::img_check(&a)?; account::img_check(&a)?;
let db = self.pool.get()?;
let mut tx = db.transaction()?; let mut tx = db.transaction()?;
// send account constructs // send account constructs
let account_constructs = account::constructs(&mut tx, &a)?; let account_constructs = account::constructs(&mut tx, &a)?;
ws.send(RpcMessage::AccountConstructs(account_constructs))?; self.ws.send(RpcMessage::AccountConstructs(account_constructs))?;
// get account instances // get account instances
// and send them to the client // and send them to the client
let account_instances = account::account_instances(&mut tx, &a)?; let account_instances = account::account_instances(&mut tx, &a)?;
ws.send(RpcMessage::AccountInstances(account_instances))?; self.ws.send(RpcMessage::AccountInstances(account_instances))?;
let shop = mtx::account_shop(&mut tx, &a)?; let shop = mtx::account_shop(&mut tx, &a)?;
ws.send(RpcMessage::AccountShop(shop))?; self.ws.send(RpcMessage::AccountShop(shop))?;
let team = account::team(&mut tx, &a)?; let team = account::team(&mut tx, &a)?;
ws.send(RpcMessage::AccountTeam(team))?; self.ws.send(RpcMessage::AccountTeam(team))?;
let wheel = account::chat_wheel(&db, a.id)?; let wheel = account::chat_wheel(&db, a.id)?;
ws.send(RpcMessage::ChatWheel(wheel))?; self.ws.send(RpcMessage::ChatWheel(wheel))?;
if let Some(instance) = account::tutorial(&mut tx, &a)? { if let Some(instance) = account::tutorial(&mut tx, &a)? {
ws.send(RpcMessage::InstanceState(instance))?; self.ws.send(RpcMessage::InstanceState(instance))?;
} }
// tx should do nothing // tx should do nothing
@ -111,8 +129,11 @@ impl User for Authorised {
Ok(()) Ok(())
} }
fn receive(&mut self, data: Vec<u8>, db: &Db, begin: Instant, events: &CbSender<Event>, stripe: &StripeClient) -> Result<RpcMessage, Error> { fn receive(&mut self, data: Vec<u8>, stripe: &StripeClient) -> Result<RpcMessage, Error> {
// cast the msg to this type to receive method name // cast the msg to this type to receive method name
let begin = Instant::now();
let db = self.pool.get()?;
match from_slice::<RpcRequest>(&data) { match from_slice::<RpcRequest>(&data) {
Ok(v) => { Ok(v) => {
let request = v.clone(); let request = v.clone();
@ -123,19 +144,19 @@ impl User for Authorised {
return Ok(RpcMessage::GameState(anim_test_game(skill))), return Ok(RpcMessage::GameState(anim_test_game(skill))),
RpcRequest::InstanceQueue {} => { RpcRequest::InstanceQueue {} => {
events.send(Event::Queue(self.id))?; self.events.send(Event::Queue(self.id))?;
Ok(RpcMessage::QueueRequested(())) Ok(RpcMessage::QueueRequested(()))
}, },
RpcRequest::InstanceInvite {} => { RpcRequest::InstanceInvite {} => {
events.send(Event::Invite(self.id))?; self.events.send(Event::Invite(self.id))?;
Ok(RpcMessage::InviteRequested(())) Ok(RpcMessage::InviteRequested(()))
}, },
RpcRequest::InstanceJoin { code } => { RpcRequest::InstanceJoin { code } => {
events.send(Event::Join(self.id, code))?; self.events.send(Event::Join(self.id, code))?;
Ok(RpcMessage::Joining(())) Ok(RpcMessage::Joining(()))
}, },
RpcRequest::InstanceLeave {} => { RpcRequest::InstanceLeave {} => {
events.send(Event::Leave(self.id))?; self.events.send(Event::Leave(self.id))?;
Ok(RpcMessage::Processing(())) Ok(RpcMessage::Processing(()))
}, },
@ -147,7 +168,7 @@ impl User for Authorised {
let wheel = account::chat_wheel(&db, self.account.id)?; let wheel = account::chat_wheel(&db, self.account.id)?;
if let Some(c) = wheel.get(index) { if let Some(c) = wheel.get(index) {
events.send(Event::Chat(self.id, instance_id, c.to_string()))?; self.events.send(Event::Chat(self.id, instance_id, c.to_string()))?;
} else { } else {
return Err(err_msg("invalid chat index")); return Err(err_msg("invalid chat index"));
} }
@ -172,7 +193,7 @@ impl User for Authorised {
Ok(RpcMessage::EmailState(mail::select_account(&db, self.account.id)?)), Ok(RpcMessage::EmailState(mail::select_account(&db, self.account.id)?)),
RpcRequest::SubscriptionState {} => RpcRequest::SubscriptionState {} =>
Ok(RpcMessage::SubscriptionState(payments::account_subscription(db, stripe, &self.account)?)), Ok(RpcMessage::SubscriptionState(payments::account_subscription(&db, stripe, &self.account)?)),
// RpcRequest::AccountShop {} => // RpcRequest::AccountShop {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)), // Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),

View File

@ -1,6 +1,6 @@
{ {
"name": "mnml-studios", "name": "mnml-studios",
"version": "1.11.2", "version": "1.12.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {