use rand::prelude::*; use uuid::Uuid; // timekeeping use chrono::prelude::*; use chrono::Duration; // Db Commons use failure::Error; use failure::err_msg; use construct::{Construct, ConstructEffect, Stat, EffectMeta}; use skill::{Skill, Cast}; use effect::{Effect}; use player::{Player}; use instance::{TimeControl}; pub type Disable = Vec; pub type Immunity = Vec; pub type Direction = (i8, i8); #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Game { pub id: Uuid, pub player_constructs: usize, pub player_num: usize, pub players: Vec, pub phase: Phase, pub stack: Vec, events: Vec, pub resolutions: Vec>, pub instance: Option, pub time_control: TimeControl, pub phase_start: DateTime, pub phase_end: Option>, } impl Game { pub fn new() -> Game { return Game { id: Uuid::new_v4(), player_constructs: 0, player_num: 0, players: vec![], phase: Phase::Start, stack: vec![], events: vec![], resolutions: vec![], instance: None, time_control: TimeControl::Standard, phase_end: None, phase_start: Utc::now(), }; } pub fn redact(mut self, account: Uuid) -> Game { self.players = self.players.into_iter() .map(|p| p.redact(account)) .collect(); self.stack .retain(|s| s.player == account); self } pub fn set_time_control(&mut self, tc: TimeControl) -> &mut Game { self.time_control = tc; self.phase_end = Some(tc.lobby_timeout()); self } pub fn set_player_num(&mut self, size: usize) -> &mut Game { self.player_num = size; self } pub fn set_player_constructs(&mut self, size: usize) -> &mut Game { self.player_constructs = size; self } pub fn set_instance(&mut self, id: Uuid) -> &mut Game { self.instance = Some(id); self } pub fn player_add(&mut self, mut player: Player) -> Result<&mut Game, Error> { if self.players.len() == self.player_num { return Err(err_msg("maximum number of players")); } if self.players.iter().any(|t| t.id == player.id) { return Err(err_msg("player already in game")); } if player.constructs.iter().all(|c| c.skills.len() == 0) { info!("WARNING: {:?} has no skills and has forfeited {:?}", player.name, self.id); // self.log.push(format!("{:} has forfeited the game", player.name)); player.forfeit(); } // let player_description = player.constructs.iter().map(|c| c.name.clone()).collect::>().join(", "); // self.log.push(format!("{:} has joined the game. [{:}]", player.name, player_description)); self.players.push(player); Ok(self) } // handle missing player properly pub fn player_by_id(&mut self, id: Uuid) -> Result<&mut Player, Error> { self.players .iter_mut() .find(|t| t.id == id) .ok_or(format_err!("{:?} not in game", id)) } pub fn construct_by_id(&mut self, id: Uuid) -> Option<&mut Construct> { match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == id)) { Some(player) => player.constructs.iter_mut().find(|c| c.id == id), None => None, } } pub fn construct(&self, id: Uuid) -> &Construct { self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == id)) .unwrap() .constructs .iter() .find(|c| c.id == id) .unwrap() } pub fn can_start(&self) -> bool { return self.players.len() == self.player_num && self.players.iter().all(|t| t.constructs.len() == self.player_constructs) } pub fn start(mut self) -> Game { // both forfeit ddue to no skills if self.finished() { 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) } fn skill_phase_start(mut self, resolution_animation_ms: i64) -> Game { if ![Phase::Start, Phase::Resolve].contains(&self.phase) { panic!("game not in Resolve or start phase {:?}", self.phase); } self.phase = Phase::Skill; self.phase_start = Utc::now() .checked_add_signed(Duration::milliseconds(resolution_animation_ms)) .expect("could not set phase start"); self.phase_end = self.time_control.game_phase_end(resolution_animation_ms); for player in self.players.iter_mut() { // everything disabled or forfeiting etc if player.skills_required() == 0 { continue; } player.set_ready(false); // displayed on client for construct in player.constructs.iter_mut() { for i in 0..construct.skills.len() { if let Some(_d) = construct.disabled(construct.skills[i].skill) { // info!("{:?} disabled {:?}", construct.skills[i].skill, d); construct.skills[i].disabled = true; } else { construct.skills[i].disabled = false; } } } } self.pve_add_skills(); if self.skill_phase_finished() { // pve game where both bots will have readied up return self.resolve_phase_start() } self } fn pve_add_skills(&mut self) -> &mut Game { let mut pve_skills = vec![]; let mut rng = thread_rng(); for mobs in self.players .iter() .filter(|t| t.bot) { let player_player = self.players.iter().find(|t| t.id != mobs.id).unwrap(); for mob in mobs.constructs.iter() { let skill = mob.mob_select_skill(); // info!("{:?} {:?}", mob.name, skill); match skill { Some(s) => { // the mut marks it as being able to be called // more than once let mut find_target = || { match s.defensive() { true => &mobs.constructs[rng.gen_range(0, mobs.constructs.len())], false => &player_player.constructs[rng.gen_range(0, player_player.constructs.len())], } }; let mut target = find_target(); while target.is_ko() { target = find_target(); } pve_skills.push((mobs.id, mob.id, target.id, s)); }, None => continue, }; } } for (player_id, mob_id, target_id, s) in pve_skills { match self.add_skill(player_id, mob_id, target_id, s) { Ok(_) => (), Err(e) => { info!("{:?}", self.construct_by_id(mob_id)); panic!("{:?} unable to add pve mob skill {:?}", e, s); }, } self.player_ready(player_id).unwrap(); } self } pub fn add_skill(&mut self, player_id: Uuid, source: Uuid, target: Uuid, skill: Skill) -> Result<&mut Game, Error> { // check player in game self.player_by_id(player_id)?; if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } // target checks { let target = match self.construct_by_id(target) { Some(c) => c, None => return Err(err_msg("target construct not in game")), }; // fixme for rez if target.is_ko() { return Err(err_msg("target construct is ko")); } } // construct checks { let construct = match self.construct_by_id(source) { Some(c) => c, None => return Err(err_msg("construct not in game")), }; if construct.is_ko() { return Err(err_msg("construct is ko")); } // check the construct has the skill if !construct.knows(skill) { return Err(err_msg("construct does not have that skill")); } if construct.skill_on_cd(skill).is_some() { return Err(err_msg("abiltity on cooldown")); } // check here as well so uncastable spells don't go on the stack if let Some(disable) = construct.disabled(skill) { return Err(format_err!("skill disabled {:?}", disable)); } } // replace construct skill if let Some(s) = self.stack.iter_mut().position(|s| s.source == source) { self.stack.remove(s); } let skill = Cast::new(source, player_id, target, skill); self.stack.push(skill); return Ok(self); } pub fn offer_draw(mut self, player_id: Uuid) -> Result { 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); } pub fn concede(mut self, player_id: Uuid) -> Result { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } self.player_by_id(player_id)? .forfeit(); return Ok(self.finish()); } pub fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { 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")); } let mut game_state = self.clone(); self.stack.retain(|s| game_state.construct_by_id(s.source).unwrap().account != player_id); return Ok(self); } pub fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } self.player_by_id(player_id)? .set_ready(true); Ok(self) } pub fn skill_phase_finished(&self) -> bool { self.players.iter().all(|t| t.ready) // self.players.iter() // // for every player // .all(|t| self.stack.iter() // // the number of skills they have cast // .filter(|s| s.player == t.id).collect::>() // // should equal the number required this turn // .len() == t.skills_required() // ) } pub fn resolve_phase_start(mut self) -> Game { if self.phase != Phase::Skill { panic!("game not in skill phase"); } self.phase = Phase::Resolve; self.resolutions.push(vec![]); // self.log.push("".to_string()); self.resolve_stack() } fn stack_sort_speed(&mut self) -> &mut 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); s.speed = speed; } }); sorted.sort_unstable_by_key(|s| s.speed); self.stack = sorted; self } fn resolve_stack(mut self) -> Game { if self.phase != Phase::Resolve { panic!("game not in Resolve phase"); } // find their statuses with ticks let mut ticks = self.players .iter() .flat_map(|p| p.constructs.iter() .flat_map( |c| c.effects .iter() .cloned() .filter_map(|e| e.meta) .filter_map(move |m| match m { EffectMeta::CastTick { source, target, skill, speed, amount: _, id } => Some( Cast::new(source, c.account, target, skill) .set_speed(speed) .set_id(id) ), _ => None, }) ) ).collect::>(); // add them to the stack self.stack.append(&mut ticks); self.stack_sort_speed(); // temp vec of this round's resolving skills // because need to check cooldown use before pushing them into the complete list let mut r_animation_ms = 0; while let Some(cast) = self.stack.pop() { self.new_resolve(cast); }; self.progress_durations(); // go through the whole most recent round and modify delays of the resolutions let last = self.resolutions.len() - 1; let mut iter = self.resolutions[last].iter_mut().peekable(); while let Some(res) = iter.next() { res.set_delay(iter.peek()); r_animation_ms += res.delay; } if self.finished() { return self.finish() } self.skill_phase_start(r_animation_ms) } 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(); if let Some(t) = target_player.intercepting() { return vec![Cast { target: t.id, ..cast }]; } let casts = match cast.skill.aoe() { true => self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == cast.target)) .unwrap() .constructs .iter() .map(|c| Cast { target: c.id, ..cast }) .collect(), false => vec![cast], }; return casts; } fn new_resolve(&mut self, cast: Cast) -> &mut Game { self.events = vec![]; self.resolve(cast); // sort the stack again in case speeds have changed self.stack_sort_speed(); self } fn resolve(&mut self, cast: Cast) -> &mut Game { if self.finished() { return self } // match tick skills with the effect on the target // if no match is found the effect must have been removed during this turn // and the skill should no longer resolve if cast.skill.is_tick() { let effect_match = self.construct(cast.target).effects.iter() .filter_map(|ce| ce.get_tick_id()) .find(|id| cast.id == *id) .is_some(); if !effect_match { return self; } } // If the skill is disabled for source nothing else will happen if let Some(effects) = self.construct(cast.source).disabled(cast.skill) { self.add_resolution(&cast, &Event::Disable { construct: cast.source, effects }); return self; } // hastestrike / hybridblast for skill in self.construct(cast.source).additional_skills(cast.skill) { self.resolve(Cast { skill, ..cast }); } 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 { if cast.skill.cast_animation() { self.action(cast, Action::Cast); } if cast.skill.aoe() { self.action(cast, Action::HitAoe); } } for cast in casts { self.execute(cast); } self } fn execute(&mut self, cast: Cast) -> &mut Game { if self.construct(cast.target).is_ko() { self.add_resolution(&cast, &Event::TargetKo { construct: cast.target }); return self; } if let Some(immunity) = self.construct(cast.target).immune(cast.skill) { self.add_resolution(&cast, &Event::Immune { construct: cast.target, effects: immunity }); return self; } // maybe this should be done with a status immunity if self.construct(cast.target).affected(Effect::Reflect) && cast.skill.colours().contains(&Colour::Blue) && !cast.skill.is_tick() { self.add_resolution(&cast, &Event::Reflection { construct: cast.target }); // both reflecting, show it and bail if self.construct(cast.source).affected(Effect::Reflect) { self.add_resolution(&cast, &Event::Reflection { construct: cast.source }); return self; } return self.resolve(Cast { target: cast.source, ..cast }); } if !cast.skill.aoe() { self.action(cast, Action::Hit); } cast.resolve(self); self } pub fn action(&mut self, cast: Cast, action: Action) -> &mut 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), Action::Effect { construct, effect } => self.effect(construct, effect), Action::Remove { construct, effect } => self.effect_remove(construct, effect), Action::RemoveAll { construct } => self.remove_all(construct), Action::IncreaseCooldowns { construct, turns } => self.increase_cooldowns(construct, turns), Action::SetEffectMeta { construct, amount, effect } => self.effect_meta(construct, effect, amount), }; // this event is now considered to have happened // for chronological ordering it is added to the resolution list // before extra processing on it begins for event in new_events { self.add_resolution(&cast, &event); let casts = match event { Event::Damage { construct, colour: _, amount: _, mitigation: _, display: _ } => self.construct_by_id(construct).unwrap().damage_trigger_casts(&cast, &event), Event::Cast { construct, skill, player: _, target: _, direction: _ } => { self.construct_by_id(construct).unwrap().skill_set_cd(skill); vec![] } Event::Ko { construct } => self.construct_by_id(construct).unwrap().on_ko(&cast, &event), _ => vec![], }; self.events.push(event); for cast in casts { self.resolve(cast); } } self } fn add_resolution(&mut self, cast: &Cast, event: &Event) -> &mut Game { let last = self.resolutions.len() - 1; // println!("{:?}", event); self.resolutions[last].push(Resolution::new(cast.clone(), event.clone())); self } pub fn value(&self, value: Value) -> usize { match value { Value::Stat { construct, stat } => self.construct(construct).stat(stat), Value::Cooldowns { construct } => self.construct(construct).stat(Stat::Cooldowns), Value::Effects { construct } => self.construct(construct).stat(Stat::EffectsCount), Value::ColourSkills { construct, colour } => self.construct(construct).stat(Stat::Skills(colour)), Value::DamageReceived { construct } => self.events.iter().fold(0, |dmg, e| match e { Event::Damage { construct: event_construct, amount, mitigation, colour: _, display: _ } => match construct == *event_construct { true => amount + mitigation, false => dmg, } _ => dmg, }), Value::Removals { construct } => self.events.iter().fold(0, |dmg, e| match e { Event::Damage { construct: event_construct, amount, mitigation:_, colour: _event_colour, display: _ } => match construct == *event_construct { true => dmg + amount, false => dmg, } _ => dmg, }), 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) } } fn hit(&mut self, cast: Cast) -> Event { Event::Hit { construct: cast.target, player: cast.player, direction: self.direction(cast) } } fn hit_aoe(&mut self, cast: Cast) -> Event { let construct = self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == cast.target)) .unwrap() .constructs .iter() .map(|c| c.id) .collect(); Event::HitAoe { construct, player: cast.player, direction: self.direction(cast) } } fn damage(&mut self, construct: Uuid, amount: usize, colour: Colour) -> Vec { self.construct_by_id(construct).unwrap().damage(amount, colour) } fn heal(&mut self, construct: Uuid, amount: usize, colour: Colour) -> Vec { self.construct_by_id(construct).unwrap().healing(amount, colour) } fn effect(&mut self, construct: Uuid, effect: ConstructEffect) -> Vec { self.construct_by_id(construct).unwrap().effect_add(effect) } fn effect_remove(&mut self, construct: Uuid, effect: Effect) -> Vec { self.construct_by_id(construct).unwrap().effect_remove(effect) } fn effect_meta(&mut self, construct: Uuid, effect: Effect, amount: usize) -> Vec { self.construct_by_id(construct).unwrap().effect_meta(effect, amount) } // could be remove by colour etc fn remove_all(&mut self, construct: Uuid) -> Vec { self.construct_by_id(construct).unwrap().remove_all() } fn increase_cooldowns(&mut self, construct: Uuid, turns: usize) -> Vec { self.construct_by_id(construct).unwrap().increase_cooldowns(turns) } fn direction(&mut self, cast: Cast) -> (i8, i8) { let i = self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == cast.source)) .unwrap().constructs .iter() .position(|c| c.id == cast.source) .unwrap() as i8; let j = self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == cast.target)) .unwrap().constructs .iter() .position(|c| c.id == cast.target) .unwrap() as i8; let x = j - i; let target = self.construct_by_id(cast.target).unwrap(); // is the caster player account same as target player account side of screen let y = match cast.player == target.account { true => 0, false => 1 }; (x, y) } fn progress_durations(&mut self) -> &mut Game { let last = self.resolutions.len() - 1; let casters = self.resolutions[last].iter() .filter_map(|r| match r.event { Event::Cast { construct: caster, player: _, direction: _, skill, target: _ } => match skill.base_cd().is_some() { true => Some(caster), false => None, }, _ => None, }) .collect::>(); for player in self.players.iter_mut() { for construct in player.constructs.iter_mut() { if construct.is_ko() { continue; } // cooldowns are set at the end of a resolution if !casters.contains(&construct.id) { construct.reduce_cooldowns(); }; // always reduce durations construct.reduce_effect_durations(); } } self } pub fn finished(&self) -> bool { self.phase == Phase::Finished || self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) } pub fn winner(&self) -> Option<&Player> { 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::Finished; // self.log.push(format!("Game finished.")); // { // let winner = self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())); // match winner { // Some(w) => self.log.push(format!("Winner: {:}", w.name)), // None => self.log.push(format!("Game was drawn.")), // }; // } self } fn phase_timed_out(&self) -> bool { match self.phase_end { Some(t) => Utc::now().signed_duration_since(t).num_milliseconds() > 0, None => false, } } pub fn upkeep(mut self) -> Game { if self.phase == Phase::Finished { return self; } if !self.phase_timed_out() { return self; } info!("upkeep {:?} {:} vs {:}", self.id, self.players[0].name, self.players[1].name); for player in self.players.iter_mut() { if !player.ready { player.set_ready(true); // player.add_warning(); // info!("upkeep: {:} warned", player.name); // if player.warnings >= 3 { // player.forfeit(); // info!("upkeep: {:} forfeited", player.name); // //todo // // self.Resolutions.push(forfeit) // // self.log.push(format!("{:} forfeited.", player.name)); // } } } self = self.resolve_phase_start(); self } } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Phase { Start, Skill, Resolve, Finished, } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Colour { Red, Blue, Green, } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Value { Stat { construct: Uuid, stat: Stat }, Cooldowns { construct: Uuid }, ColourSkills { construct: Uuid, colour: Colour }, Effects { construct: Uuid }, Removals { construct: Uuid }, DamageReceived { construct: Uuid }, 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 }, Effect { construct: Uuid, effect: ConstructEffect }, Remove { construct: Uuid, effect: Effect }, RemoveAll { construct: Uuid }, IncreaseCooldowns { construct: Uuid, turns: usize }, SetEffectMeta { construct: Uuid, amount: usize, effect: Effect }, } #[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] pub struct Resolution { pub skill: Skill, pub speed: usize, pub focus: Vec, pub event: Event, pub delay: i64, } impl Resolution { pub fn new(cast: Cast, event: Event) -> Resolution { // maybe map events construct_ids let focus = match event { Event::HitAoe { construct: _, player: _, direction: _ } => vec![cast.source], _ => vec![cast.source, cast.target], }; Resolution { skill: cast.skill, speed: cast.speed, delay: 0, // set at the end of the resolve phase because of changes depending on what's before/after focus, event, } } pub fn set_delay(&mut self, next: Option<&&mut Resolution>) -> &mut Resolution { self.delay = self.event.delay(next); self } } #[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] pub enum Event { Cast { construct: Uuid, player: Uuid, target: Uuid, skill: Skill, direction: Direction }, Hit { construct: Uuid, player: Uuid, direction: Direction }, HitAoe { construct: Vec, player: Uuid, direction: Direction }, Damage { construct: Uuid, amount: usize, mitigation: usize, colour: Colour, display: EventConstruct }, Effect { construct: Uuid, effect: Effect, duration: u8, display: EventConstruct }, Removal { construct: Uuid, effect: Effect, display: EventConstruct }, Meta { construct: Uuid, effect: Effect, meta: EffectMeta }, Healing { construct: Uuid, amount: usize, overhealing: usize, colour: Colour, display: EventConstruct }, Inversion { construct: Uuid }, Reflection { construct: Uuid }, Disable { construct: Uuid, effects: Vec }, Immune { construct: Uuid, effects: Vec }, TargetKo { construct: Uuid }, Ko { construct: Uuid }, CooldownIncrease { construct: Uuid, turns: usize }, CooldownDecrease { construct: Uuid, turns: usize }, Forfeit (), } impl Event { fn delay(&self, next: Option<&&mut Resolution>) -> i64 { let source_overlapping = 500; let target_overlapping = 900; let source_duration = 1000; let target_duration = 1500; let combat_text_duration = 1300; let animation = self.animation(); let next_animation = match next { Some(r) => r.event.animation(), None => Animation::Skip, }; // Exhaustively match all types so they're all properly considered match animation { Animation::Source => { match next_animation { Animation::Target | Animation::Text => source_overlapping, Animation::Source | Animation::Skip => source_duration, } }, Animation::Target => { match next_animation { Animation::Text => target_overlapping, Animation::Source | Animation::Target | Animation::Skip => target_duration, } }, Animation::Text => combat_text_duration, Animation::Skip => 0, } } fn animation(&self) -> Animation { match self { Event::Cast { construct: _, direction: _, player: _, target: _, skill: _ } => Animation::Source, Event::Hit { construct: _, direction: _, player: _ } | Event::HitAoe { construct: _, direction: _, player: _ } => Animation::Target, Event::Damage { construct: _, amount: _, mitigation: _, colour: _, display: _ } | Event::Effect { construct: _, effect: _, duration: _, display: _ } | Event::Removal { construct: _, effect: _, display: _ } | Event::Healing { construct: _, amount: _, overhealing: _, colour: _, display: _ } | Event::Inversion { construct: _ } | Event::Reflection { construct: _ } | Event::Ko { construct: _ } | Event::CooldownIncrease { construct: _, turns: _ } | Event::CooldownDecrease { construct: _, turns: _ } => Animation::Text, Event::TargetKo { construct: _ } | Event::Disable { construct: _, effects: _ } | Event::Immune { construct: _, effects: _ } | Event::Meta { construct: _, effect: _, meta: _ } | Event::Forfeit() => Animation::Skip, } } } #[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] pub enum Animation { Source, Target, Text, Skip, } // used to show the progress of a construct // while the resolutions are animating #[derive(Debug,Clone,PartialEq,Serialize,Deserialize)] pub struct EventConstruct { id: Uuid, red: usize, green: usize, blue: usize, effects: Vec, } impl EventConstruct { pub fn new(construct: &Construct) -> EventConstruct { EventConstruct { id: construct.id, red: construct.stat(Stat::RedLife), green: construct.stat(Stat::GreenLife), blue: construct.stat(Stat::BlueLife), effects: construct.effects.iter().cloned().filter(|ce| !ce.effect.hidden()).collect(), } } } #[cfg(test)] mod tests { use game::*; use construct::*; use util::IntPct; fn create_test_game() -> Game { let mut x = Construct::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::Stun) .learn(Skill::Attack) .learn(Skill::Block) .learn(Skill::Counter) .learn(Skill::Siphon) .learn(Skill::Purify) .learn(Skill::Amplify) .learn(Skill::Stun) .learn(Skill::Ruin) .learn(Skill::Block) .learn(Skill::Sleep) .learn(Skill::Decay); let mut y = Construct::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::Stun) .learn(Skill::Attack) .learn(Skill::Block) .learn(Skill::Counter) .learn(Skill::Siphon) .learn(Skill::Purify) .learn(Skill::Amplify) .learn(Skill::Stun) .learn(Skill::Block); 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(); assert!(game.can_start()); game = game.start(); return game.resolve_phase_start(); } fn create_2v2_test_game() -> Game { let mut i = Construct::new() .named(&"pretaliate".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut j = Construct::new() .named(&"poy sian".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut x = Construct::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut y = Construct::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut game = Game::new(); game .set_player_num(2) .set_player_constructs(2); let i_player_id = Uuid::new_v4(); i.account = i_player_id; j.account = i_player_id; let i_player = Player::new(i_player_id, None, &"ntr".to_string(), vec![i, j]); let x_player_id = Uuid::new_v4(); x.account = x_player_id; y.account = x_player_id; let x_player = Player::new(x_player_id, None, &"mashy".to_string(), vec![x, y]); game .player_add(i_player).unwrap() .player_add(x_player).unwrap(); assert!(game.can_start()); game = game.start(); return game.resolve_phase_start(); } #[test] fn phase_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.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); 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(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finished].contains(&game.phase)); 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] fn stun_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::Stun).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::Stun).unwrap(); 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(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].affected(Effect::Stun)); assert!(game.player_by_id(y_player.id).unwrap().skills_required() == 0); } #[test] fn ko_resolution_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.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().red_power.force(1000000000); game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().speed.force(1000000000); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } // just in case // remove all mitigation game.player_by_id(x_player.id).unwrap().construct_by_id(x_construct.id).unwrap().red_life.force(0); game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Stun).unwrap(); 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(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned()); assert!(game.phase == Phase::Finished); } #[test] fn 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(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); 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()); game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Attack).unwrap(); 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(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_none()); // second round // now we block and it should go back on cd game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Stun).unwrap(); 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(); assert!(game.player_by_id(x_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::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()); } // #[test] // fn attack_actions_test() { // let cast = Cast::new(Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4(), Skill::Attack); // let actions = cast.actions(Game::); // match actions[0] { // Action::Cast => (), // _ => panic!("{:?}", actions), // }; // match actions[1] { // Action::Hit => (), // _ => panic!("{:?}", actions), // }; // match actions[2] { // Action::Damage { construct: _, amount: _, colour } => { // assert_eq!(colour, Colour::Red); // }, // _ => panic!("{:?}", actions), // }; // } // #[test] // fn heal_test() { // let mut x = Construct::new() // .named(&"muji".to_string()) // .learn(Skill::Heal); // let mut y = Construct::new() // .named(&"camel".to_string()) // .learn(Skill::Heal); // x.deal_red_damage(Skill::Attack, 5); // heal(&mut y, &mut x, vec![], Skill::Heal); // } // #[test] // fn decay_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"camel".to_string()); // decay(&mut x, &mut y, vec![], Skill::Decay); // assert!(y.effects.iter().any(|e| e.effect == Effect::Decay)); // y.reduce_effect_durations(); // let _decay = y.effects.iter().find(|e| e.effect == Effect::Decay); // // assert!(y.green_life() == y.green_life().saturating_sub(decay.unwrap().tick.unwrap().amount)); // } // #[test] // fn block_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"camel".to_string()); // // ensure it doesn't have 0 pd // x.red_power.force(100); // y.green_life.force(500); // block(&mut y.clone(), &mut y, vec![], Skill::Block); // assert!(y.effects.iter().any(|e| e.effect == Effect::Block)); // attack(&mut x, &mut y, vec![], Skill::Attack); // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Damage { amount, mitigation: _, colour: _, skill: _ } => // assert!(amount < x.red_power().pct(Skill::Attack.multiplier())), // _ => panic!("not damage"), // }; // } // #[test] // fn sustain_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"camel".to_string()); // x.red_power.force(10000000000000); // multiplication of int max will cause overflow // y.green_life.force(1024); // make tests more flexible if we change stats // sustain(&mut y.clone(), &mut y, vec![], Skill::Sustain); // assert!(y.affected(Effect::Sustain)); // ruin(&mut x, &mut y, vec![], Skill::Ruin); // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Immunity { skill: _, immunity } => assert!(immunity.contains(&Effect::Sustain)), // _ => panic!("not immune cluthc"), // }; // attack(&mut x, &mut y, vec![], Skill::Attack); // assert!(y.green_life() == 1); // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Damage { amount, mitigation: _, colour: _, skill: _ } => assert_eq!(amount, 1023), // _ => panic!("not damage"), // }; // } // #[test] // fn invert_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"camel".to_string()); // // give red shield but reduce to 0 // y.red_life.force(64); // y.red_life.reduce(64); // x.red_power.force(512); // invert(&mut y.clone(), &mut y, vec![], Skill::Invert); // assert!(y.affected(Effect::Invert)); // // heal should deal green damage // heal(&mut x, &mut y, vec![], Skill::Heal); // assert!(y.green_life() < 1024); // // attack should heal and recharge red shield // attack(&mut x, &mut y, vec![], Skill::Attack); // // match resolutions.remove(0).event { // // Event::Inversion { skill } => assert_eq!(skill, Skill::Attack), // // _ => panic!("not inversion"), // //}; // match resolutions.remove(0).event { // Event::Heal { skill: _, overhealing: _, amount } => assert!(amount > 0), // _ => panic!("not healing from inversion"), // }; // match resolutions.remove(0).event { // Event::Recharge { skill: _, red, blue: _ } => assert!(red > 0), // _ => panic!("not recharge from inversion"), // }; // } // #[test] // fn reflect_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"camel".to_string()); // reflect(&mut y.clone(), &mut y, vec![], Skill::Reflect); // assert!(y.affected(Effect::Reflect)); // let mut vec![]; // cast_actions(Skill::Blast, &mut x, &mut y, resolutions); // assert!(x.green_life() < 1024); // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Reflection { skill } => assert_eq!(skill, Skill::Blast), // _ => panic!("not reflection"), // }; // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Damage { amount, mitigation: _, colour: _, skill: _ } => assert!(amount > 0), // _ => panic!("not damage"), // }; // } // #[test] // fn triage_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"pretaliation".to_string()); // // ensure it doesn't have 0 sd // x.blue_power.force(50); // // remove all mitigation // y.red_life.force(0); // y.blue_life.force(0); // y.deal_red_damage(Skill::Attack, 5); // let prev_hp = y.green_life(); // triage(&mut x, &mut y, vec![], Skill::Triage); // assert!(y.effects.iter().any(|e| e.effect == Effect::Triage)); // assert!(y.green_life() > prev_hp); // } // #[test] // fn recharge_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"pretaliation".to_string()); // y.red_life.force(50); // y.blue_life.force(50); // y.deal_red_damage(Skill::Attack, 5); // y.deal_blue_damage(Skill::Blast, 5); // recharge(&mut x, &mut y, vec![], Skill::Recharge); // resolutions.remove(0); // let Event { source: _, target: _, event, stages: _ } = resolutions.remove(0); // match event { // Event::Recharge { red, blue, skill: _ } => { // assert!(red == 5); // assert!(blue == 5); // } // _ => panic!("result was not recharge"), // } // } // #[test] // fn silence_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // silence(&mut x.clone(), &mut x, vec![], Skill::Silence); // assert!(x.effects.iter().any(|e| e.effect == Effect::Silence)); // assert!(x.disabled(Skill::Silence).is_some()); // } // #[test] // fn amplify_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // x.blue_power.force(50); // amplify(&mut x.clone(), &mut x, vec![], Skill::Amplify); // assert!(x.effects.iter().any(|e| e.effect == Effect::Amplify)); // assert_eq!(x.blue_power(), 75); // } // #[test] // fn purify_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // decay(&mut x.clone(), &mut x, vec![], Skill::Decay); // assert!(x.effects.iter().any(|e| e.effect == Effect::Decay)); // purify(&mut x.clone(), &mut x, vec![], Skill::Purify); // assert!(!x.effects.iter().any(|e| e.effect == Effect::Decay)); // } // #[test] // fn bash_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"pretaliation".to_string()) // .learn(Skill::Stun); // let stun_cd = y.skills.iter().find(|cs| cs.skill == Skill::Stun).unwrap().cd.unwrap(); // bash(&mut x, &mut y, vec![], Skill::Bash); // assert!(!x.effects.iter().any(|e| e.effect == Effect::Stun)); // assert!(y.skills.iter().any(|cs| cs.skill == Skill::Stun && cs.cd.unwrap() == stun_cd + 1)); // } // #[test] // fn purge_test() { // let mut x = Construct::new() // .named(&"muji".to_string()); // let mut y = Construct::new() // .named(&"pretaliation".to_string()) // .learn(Skill::Heal) // .learn(Skill::HealPlus); // purge(&mut x, &mut y, vec![], Skill::Purge); // // 2 turns at lvl 1 // assert!(y.effects.iter().any(|e| e.effect == Effect::Purge && e.duration == 2)); // assert!(y.disabled(Skill::Heal).is_some()); // } // } // #[test] // fn counter_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(y_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { // game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns(); // } // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Counter).is_some() { // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); // } // game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Counter).unwrap(); // 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(); // // don't get stunned but not really stunning ¯\_(ツ)_/¯ // assert!(game.player_by_id(x_player.id).unwrap().constructs[0].is_stunned() == false); // // riposte // assert_eq!(game.player_by_id(y_player.id).unwrap().constructs[0].green_life(), ( // y_construct.green_life() + y_construct.red_life() - x_construct.red_power().pct(Skill::CounterAttack.multiplier()))); // } // #[test] // fn electrify_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(); // // one shot the target construct (should still get debuffed) // game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().red_power.force(1000000000); // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Electrify); // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Electrify).is_some() { // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); // } // // apply buff // game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Electrify).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::Electric)); // // attack and receive debuff // 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(); // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Electrocute)); // } // // #[test] // // fn absorb_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::Absorb); // // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Absorb).is_some() { // // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); // // } // // // apply buff // // game.add_skill(x_player.id, x_construct.id, x_construct.id, Skill::Absorb).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::Absorb)); // // // attack and receive debuff // // game.add_skill(y_player.id, y_construct.id, x_construct.id, Skill::TestAttack).unwrap(); // // game.player_ready(x_player.id).unwrap(); // // game.player_ready(y_player.id).unwrap(); // // game = game.resolve_phase_start(); // // info!("{:#?}", game); // // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Absorption)); // // } // #[test] // fn aoe_test() { // let mut game = create_2v2_test_game(); // let i_player = game.players[0].clone(); // let x_player = game.players[1].clone(); // let i_construct = i_player.constructs[0].clone(); // let j_construct = i_player.constructs[1].clone(); // let x_construct = x_player.constructs[0].clone(); // let y_construct = x_player.constructs[1].clone(); // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Ruin); // 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(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap(); // game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); // game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Ruin).unwrap(); // game.add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap(); // game.player_ready(i_player.id).unwrap(); // game.player_ready(x_player.id).unwrap(); // assert!(game.skill_phase_finished()); // game = game.resolve_phase_start(); // let ruins = game.Resolutions // .last().unwrap() // .into_iter() // .filter(|r| { // let Resolution { source, target: _, Resolution, stages: _ } = r; // match source.id == x_construct.id { // true => match Resolution { // Resolution::Effect { effect, duration, skill: _, construct_effects: _ } => { // assert!(*effect == Effect::Stun); // assert!(*duration == 1); // true // }, // Resolution::AoeSkill { skill: _ } => false, // Resolution::Damage { amount: _, mitigation: _, colour: _, skill: _ } => false, // _ => panic!("ruin result not effect {:?}", Resolution), // } // false => false, // } // }) // .count(); // assert!(ruins == 2); // } // #[test] // fn intercept_test() { // let mut game = create_2v2_test_game(); // let i_player = game.players[0].clone(); // let x_player = game.players[1].clone(); // let i_construct = i_player.constructs[0].clone(); // let j_construct = i_player.constructs[1].clone(); // let x_construct = x_player.constructs[0].clone(); // let y_construct = x_player.constructs[1].clone(); // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Intercept); // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Intercept).is_some() { // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); // } // game.add_skill(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap(); // game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); // game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Intercept).unwrap(); // game.add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap(); // game.player_ready(i_player.id).unwrap(); // game.player_ready(x_player.id).unwrap(); // game = game.resolve_phase_start(); // assert!(game.Resolutions.len() == 4); // while let Some(r) = game.Resolutions.last().unwrap().pop() { // let Resolution { source , target, Resolution: _, stages: _ } = r; // if [i_construct.id, j_construct.id].contains(&source.id) { // assert!(target.id == x_construct.id); // } // } // } #[test] fn ko_pve_test() { let mut game = create_2v2_test_game(); let i_player = game.players[0].clone(); let x_player = game.players[1].clone(); let i_construct = i_player.constructs[0].clone(); let j_construct = i_player.constructs[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = x_player.constructs[1].clone(); game.add_skill(i_player.id, i_construct.id, x_construct.id, Skill::Attack).unwrap() .add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap() .add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Attack).unwrap() .add_skill(x_player.id, y_construct.id, i_construct.id, Skill::Attack).unwrap() .player_ready(i_player.id).unwrap() .player_ready(x_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); 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(usize::max_value()); assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); // add some more skills game.add_skill(i_player.id, j_construct.id, x_construct.id, Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, j_construct.id, Skill::Attack).unwrap(); game.add_skill(x_player.id, y_construct.id, j_construct.id, Skill::Attack).unwrap(); assert!(game.add_skill(x_player.id, x_construct.id, i_construct.id, Skill::Attack).is_err()); game.player_ready(i_player.id).unwrap(); game.player_ready(x_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); return; } #[test] fn upkeep_test() { 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.upkeep(); // assert!(game.players[1].warnings == 1); } #[test] fn attack_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.add_skill(player_id, source, target, Skill::Attack).unwrap(); game.resolve_phase_start(); } #[test] fn bash_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::Bash)); } #[test] fn slay_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.new_resolve(Cast::new(source, player_id, target, Skill::Slay)); 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 > 0 && colour == Colour::Red, _ => false, })); assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct, colour, amount, mitigation: _, display: _ } => construct == target && amount > 0 && colour == Colour::Red, _ => false, })); } #[test] fn purify_test() { let mut game = create_2v2_test_game(); let source_player_id = game.players[0].id; let target_player_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.new_resolve(Cast::new(source, source_player_id, target, Skill::Decay)); // don't mention 3 we volvo now assert!(game.players[1].constructs[0].effects.len() == 3); game.new_resolve(Cast::new(target, target_player_id, target, Skill::Purify)); assert!(game.players[1].constructs[0].effects.len() == 1); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; assert!(resolutions.iter().any(|r| match r.event { Event::Effect { construct, effect, duration: _, display: _ } => construct == target && effect == Effect::Pure, _ => false, })); // Check for healing here } #[test] fn invert_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.new_resolve(Cast::new(source, player_id, target, Skill::Strike)); game.new_resolve(Cast::new(source, player_id, target, Skill::Invert)); game.new_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::Healing { construct, colour, amount, overhealing: _, display: _, } => construct == target && amount > 0 && colour == Colour::Green, _ => false, })); assert!(resolutions.iter().any(|r| match r.event { Event::Healing { construct, colour, amount, overhealing: _, display: _ } => construct == target && amount > 0 && colour == Colour::Red, _ => false, })); } #[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(); let player_id = game.players[0].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.new_resolve(Cast::new(source, player_id, target, Skill::Siphon)); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; // siphon should // apply effect // damage target // heal source assert!(resolutions.iter().any(|r| match r.event { Event::Effect { construct, effect, duration: _, display: _ } => construct == target && effect == Effect::Siphon, _ => false, })); assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct, colour, amount, mitigation: _, display: _ } => construct == target && amount > 0 && colour == Colour::Blue, _ => false, })); assert!(resolutions.iter().any(|r| match r.event { Event::Healing { construct, colour, amount: _, overhealing: _, display: _ } => construct == source && colour == Colour::Green, _ => false, })); game = game.resolve_phase_start(); // 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]; let damage_events = resolutions.iter().filter(|r| match r.event { Event::Damage { construct: _, colour: _, amount: _, mitigation: _, display: _ } => true, _ => 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"); assert!(resolutions.iter().any(|r| match r.event { Event::Healing { construct, colour, amount, overhealing, display: _ } => { construct == source && (amount + overhealing) == siphon_dmg && colour == Colour::Green }, _ => false, })); } #[test] fn reflect_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::Reflect)); 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 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, })); 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]; let electrocute_dmg_events = resolutions.iter().filter(|r| match r.event { Event::Damage { construct: _, colour: _, amount: _, mitigation: _, display: _ } => match r.skill { Skill::Electrocute => true, _ => false }, _ => false, }).count(); let effect_events = resolutions.iter().filter(|r| match r.event { Event::Effect { construct, effect, duration: _, display: _ } => construct == source && effect == Effect::Electrocute, _ => false, }).count(); assert!(effect_events == 2); assert!(electrocute_dmg_events == 1); // second electrocute application deals no damage } #[test] fn electrocute_ko_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.players[1].constructs[0].green_life.force(1); 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]; // println!("{:#?}", resolutions); assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct, colour, amount, mitigation: _, display: _ } => r.skill == Skill::Electrocute && construct == source && amount > 0 && colour == Colour::Blue, _ => false, })); let effect_events = resolutions.iter().filter(|r| match r.event { Event::Effect { construct, effect, duration: _, display: _ } => construct == source && effect == Effect::Electrocute, _ => false, }).count(); // println!("{:?}", effect_events); assert!(effect_events == 1); } #[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(); 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::Absorb)); 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: _ } => { assert!(construct == target && amount > 0 && colour == Colour::Blue && r.skill == Skill::Blast); resolutions.iter().any(|r| match r.event { Event::Meta { construct, effect, meta } => construct == target && effect == Effect::Absorption && { match meta { EffectMeta::AddedDamage(added_dmg) => added_dmg == amount + mitigation, _ => false, } }, _ => false, }) }, _ => false, })); } #[test] fn absorb_silence_test() { let mut game = create_2v2_test_game(); let player_id = game.players[0].id; let target_player_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.new_resolve(Cast::new(target, player_id, target, Skill::Absorb)); assert!(game.construct_by_id(target).unwrap().affected(Effect::Absorb)); game.new_resolve(Cast::new(source, target_player_id, target, Skill::Silence)); assert!(game.construct_by_id(target).unwrap().affected(Effect::Silence)); assert!(game.construct_by_id(target).unwrap().affected(Effect::Absorption)); } #[test] fn absorb_multi_damage_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.new_resolve(Cast::new(source, player_id, target, Skill::Blast)); game.new_resolve(Cast::new(source, player_id, target, Skill::Blast)); // Abosrb restores blue life here game.new_resolve(Cast::new(source, player_id, target, Skill::Absorb)); game.new_resolve(Cast::new(source, player_id, target, Skill::Blast)); /*assert!(match game.players[1].constructs[0].effects[0].meta { Some(EffectMeta::AddedDamage(d)) => d, _ => 0 // 320 base blue power and 125 base blue life } == 320.pct(Skill::Blast.multiplier()) - 125);*/ } #[test] fn multi_reflect_test() { let mut game = create_2v2_test_game(); let player_id = game.players[0].id; let target_player_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.new_resolve(Cast::new(source, player_id, source, Skill::Reflect)); game.new_resolve(Cast::new(target, target_player_id, target, Skill::Reflect)); game.new_resolve(Cast::new(source, player_id, target, Skill::Blast)); assert!(game.players[0].constructs[0].is_ko() == false); assert!(game.players[1].constructs[0].is_ko() == false); } #[test] fn multi_counter_test() { let mut game = create_2v2_test_game(); let player_id = game.players[0].id; let target_player_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.new_resolve(Cast::new(source, player_id, source, Skill::Counter)); game.new_resolve(Cast::new(target, target_player_id, target, Skill::Counter)); game.new_resolve(Cast::new(source, player_id, target, Skill::Attack)); assert!(game.players[0].constructs[0].is_ko() == false); assert!(game.players[1].constructs[0].is_ko() == false); } #[test] fn intercept_test() { let mut game = create_2v2_test_game(); let player_id = game.players[0].id; let other_player_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; let interceptor = game.players[1].constructs[1].id; // Cast intercept game.new_resolve(Cast::new(interceptor, other_player_id, interceptor, Skill::Intercept)); // Enemy casts skill on target which as a teammate intercepting game.new_resolve(Cast::new(source, player_id, target, Skill::Attack)); // Intercepting teammate attacks someone on same team game.new_resolve(Cast::new(interceptor, other_player_id, target, Skill::Attack)); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; // There should be no damage events on the target assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct, colour, amount, mitigation, display: _ } => construct == target && (amount > 0 || mitigation > 0) && colour == Colour::Red, _ => false, }) == false); // Should be damage events on the interceptor assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct, colour, amount, mitigation, display: _ } => construct == interceptor && (amount > 0 || mitigation > 0) && colour == Colour::Red, _ => false, })); } #[test] fn sustain_test() { // Standard case where construct gets ko from a big hit let mut game = create_2v2_test_game(); let player = game.players[0].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.players[0].constructs[0].red_power.force(1000000); game.new_resolve(Cast::new(source, player, target, Skill::Attack)); assert!(game.players[1].constructs[0].is_ko() == true); // Sustain case where construct survives let mut game = create_2v2_test_game(); let player = game.players[0].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.players[0].constructs[0].red_power.force(1000000); game.new_resolve(Cast::new(source, player, target, Skill::Sustain)); game.new_resolve(Cast::new(source, player, target, Skill::Attack)); assert!(game.players[1].constructs[0].is_ko() == false); } #[test] fn tick_consistency_test() { let mut game = create_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.new_resolve(Cast::new(source, player_id, target, Skill::Siphon)); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; let (siphon_speed, siphon_dmg) = resolutions.iter() .filter(|r| r.skill == Skill::Siphon) .find_map(|r| match r.event { Event::Damage { construct: _, colour: _, amount, mitigation, display: _ } => Some((r.speed, amount + mitigation)), _ => None, }) .unwrap(); game.progress_durations(); // pretend it's a new turn game.new_resolve(Cast::new(source, player_id, source, Skill::HastePlusPlus)); game = game.resolve_phase_start(); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; let (siphon_tick_speed, siphon_tick_dmg) = resolutions.iter() .filter(|r| r.skill == Skill::SiphonTick) .find_map(|r| match r.event { Event::Damage { construct: _, colour: _, amount, mitigation, display: _ } => Some((r.speed, amount + mitigation)), _ => None, }) .unwrap(); assert!(siphon_tick_speed > 0); assert!(siphon_speed > 0); assert_eq!(siphon_tick_dmg, siphon_dmg); assert_eq!(siphon_tick_speed, siphon_speed); } #[test] fn tick_removal_test() { let mut game = create_test_game(); let player_id = game.players[0].id; let opponent_id = game.players[1].id; let source = game.players[0].constructs[0].id; let target = game.players[1].constructs[0].id; game.add_skill(player_id, source, target, Skill::Siphon).unwrap(); game.player_ready(player_id).unwrap(); game.player_ready(opponent_id).unwrap(); game = game.resolve_phase_start(); game.add_skill(player_id, source, target, Skill::Purify).unwrap(); game.player_ready(player_id).unwrap(); game.player_ready(opponent_id).unwrap(); game = game.resolve_phase_start(); // println!("{:#?}", game.resolutions); let last = game.resolutions.len() - 1; let resolutions = &game.resolutions[last]; // There should be no damage events on the target assert!(resolutions.iter().any(|r| match r.event { Event::Damage { construct: _, colour: _, amount: _, mitigation: _, display: _ } => true, _ => false, }) == false); } }