mnml/core/src/game.rs
2020-01-10 12:34:15 +10:00

2528 lines
89 KiB
Rust

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<Effect>;
pub type Immunity = Vec<Effect>;
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<Player>,
pub phase: Phase,
pub stack: Vec<Cast>,
events: Vec<Event>,
pub resolutions: Vec<Vec<Resolution>>,
pub instance: Option<Uuid>,
pub time_control: TimeControl,
pub phase_start: DateTime<Utc>,
pub phase_end: Option<DateTime<Utc>>,
}
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::<Vec<String>>().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<Game, Error> {
if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase"));
}
{
let player = self.player_by_id(player_id)?;
player.draw_offered = true;
}
// bots automatically accept draws
for player in self.players.iter_mut() {
if player.bot {
player.draw_offered = true;
}
}
if self.players.iter().all(|p| p.draw_offered) {
return Ok(self.finish());
}
return Ok(self);
}
pub fn concede(mut self, player_id: Uuid) -> Result<Game, Error> {
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::<Vec<&Cast>>()
// // 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("<Resolve Phase>".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: _ } =>
Some(Cast::new(source, c.account, target, skill).set_speed(speed)),
_ => None,
})
)
).collect::<Vec<Cast>>();
// 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<Cast> {
// 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 }
// 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<Event> {
self.construct_by_id(construct).unwrap().damage(amount, colour)
}
fn heal(&mut self, construct: Uuid, amount: usize, colour: Colour) -> Vec<Event> {
self.construct_by_id(construct).unwrap().healing(amount, colour)
}
fn effect(&mut self, construct: Uuid, effect: ConstructEffect) -> Vec<Event> {
self.construct_by_id(construct).unwrap().effect_add(effect)
}
fn effect_remove(&mut self, construct: Uuid, effect: Effect) -> Vec<Event> {
self.construct_by_id(construct).unwrap().effect_remove(effect)
}
fn effect_meta(&mut self, construct: Uuid, effect: Effect, amount: usize) -> Vec<Event> {
self.construct_by_id(construct).unwrap().effect_meta(effect, amount)
}
// could be remove by colour etc
fn remove_all(&mut self, construct: Uuid) -> Vec<Event> {
self.construct_by_id(construct).unwrap().remove_all()
}
fn increase_cooldowns(&mut self, construct: Uuid, turns: usize) -> Vec<Event> {
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::<Vec<Uuid>>();
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 game: {:} vs {:}", 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<Uuid>,
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<Uuid>, 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<Effect> },
Immune { construct: Uuid, effects: Vec<Effect> },
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<ConstructEffect>,
}
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::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::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 tick_removal_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();
// // make the purify construct super fast so it beats out decay
// game.construct_by_id(y_construct.id).unwrap().speed.force(10000000);
// game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Decay);
// while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Decay).is_some() {
// game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns();
// }
// game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Siphon);
// while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Siphon).is_some() {
// game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns();
// }
// game.construct_by_id(y_construct.id).unwrap().learn_mut(Skill::Purify);
// while game.construct_by_id(y_construct.id).unwrap().skill_on_cd(Skill::Purify).is_some() {
// game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns();
// }
// // apply buff
// game.add_skill(x_player.id, x_construct.id, y_construct.id, Skill::Decay).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::Decay));
// let Resolution { source: _, target: _, Resolution, stages: _ } = game.Resolutions.last().unwrap().pop().unwrap();
// match Resolution {
// Resolution::Damage { amount: _, skill, mitigation: _, colour: _ } => assert_eq!(skill, Skill::DecayTick),
// _ => panic!("not decay"),
// };
// game.Resolutions.clear();
// // remove
// game.add_skill(y_player.id, y_construct.id, y_construct.id, Skill::Purify).unwrap();
// game.player_ready(x_player.id).unwrap();
// game.player_ready(y_player.id).unwrap();
// game = game.resolve_phase_start();
// while let Some(Resolution { source: _, target: _, Resolution, stages: _ }) = game.Resolutions.last().unwrap().pop() {
// match Resolution {
// Resolution::Damage { amount: _, skill: _, mitigation: _, colour: _ } =>
// panic!("{:?} damage Resolution", Resolution),
// _ => (),
// }
// };
// game.add_skill(y_player.id, x_construct.id, y_construct.id, Skill::Siphon).unwrap();
// game.player_ready(x_player.id).unwrap();
// game.player_ready(y_player.id).unwrap();
// game = game.resolve_phase_start();
// game.Resolutions.clear();
// game.add_skill(y_player.id, y_construct.id, y_construct.id, Skill::Purify).unwrap();
// game.player_ready(x_player.id).unwrap();
// game.player_ready(y_player.id).unwrap();
// game = game.resolve_phase_start();
// while let Some(Resolution { source: _, target: _, Resolution, stages: _ }) = game.Resolutions.last().unwrap().pop() {
// match Resolution {
// Resolution::Damage { amount: _, skill: _, mitigation: _, colour: _ } =>
// panic!("{:#?} {:#?} damage Resolution", game.Resolutions, Resolution),
// _ => (),
// }
// };
// }
#[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 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,
}));
/* 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 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);
}
}