use rand::prelude::*; use uuid::Uuid; // Db Commons use serde_cbor::{from_slice, to_vec}; use postgres::transaction::Transaction; use failure::Error; use failure::err_msg; use account::Account; use rpc::{GameStateParams, GameSkillParams}; use cryp::{Cryp}; use skill::{Skill, Cast, ResolutionResult}; use player::{Player}; use instance::{instance_game_finished, global_game_finished}; pub type Log = Vec; #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Team { pub id: Uuid, pub player: Option, pub bot: bool, cryps: Vec, } impl Team { pub fn new(account: Uuid) -> Team { return Team { id: account, player: None, cryps: vec![], bot: false, }; } pub fn set_bot(&mut self) -> &mut Team { self.bot = true; self } fn skills_required(&self) -> usize { let required = self.cryps.iter() .filter(|c| !c.is_ko()) .filter(|c| c.available_skills().len() > 0) .collect::>().len(); // println!("{:} requires {:} skills this turn", self.id, required); return required; } pub fn set_cryps(&mut self, mut cryps: Vec) -> &mut Team { cryps.sort_unstable_by_key(|c| c.id); self.cryps = cryps; self } pub fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> { self.cryps.iter_mut().find(|c| c.id == id) } } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Phase { Start, Skill, Target, Resolve, Finish, } #[derive(Debug,Clone,Copy,Serialize,Deserialize)] pub enum GameMode { Normal, Pvp, Zone3v2Attack, Zone2v2Caster, Zone3v3MeleeMiniboss, Zone3v3HealerBoss, } #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Game { pub id: Uuid, pub team_size: usize, pub team_num: usize, pub teams: Vec, pub phase: Phase, pub stack: Vec, pub resolved: Vec, pub log: Vec, pub instance: Option, pub mode: GameMode, } impl Game { pub fn new() -> Game { return Game { id: Uuid::new_v4(), team_size: 0, team_num: 0, teams: vec![], phase: Phase::Start, stack: vec![], resolved: vec![], log: vec![], instance: None, mode: GameMode::Normal, }; } pub fn set_team_num(&mut self, size: usize) -> &mut Game { self.team_num = size; self } pub fn set_team_size(&mut self, size: usize) -> &mut Game { self.team_size = size; self } pub fn set_instance(&mut self, id: Uuid) -> &mut Game { self.instance = Some(id); self } pub fn set_mode(&mut self, mode: GameMode) -> &mut Game { self.mode = mode; self } pub fn joinable(&self) -> bool { self.can_start() } pub fn team_add(&mut self, team: Team) -> Result<&mut Game, Error> { if self.teams.len() == self.team_num { return Err(err_msg("maximum number of teams")); } if self.teams.iter().any(|t| t.id == team.id) { return Err(err_msg("team already in game")); } if team.cryps.iter().all(|c| c.skills.len() == 0) { return Err(err_msg("your cryps have no skills")); } let team_description = team.cryps.iter().map(|c| c.name.clone()).collect::>().join(", "); self.log.push(format!("{:} has joined the game.", team_description)); self.teams.push(team); Ok(self) } // handle missing team properly fn team_by_id(&mut self, id: Uuid) -> &mut Team { match self.teams.iter_mut().find(|t| t.id == id) { Some(t) => t, None => panic!("id not in game {:}", id), } } fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> { match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == id)) { Some(team) => team.cryps.iter_mut().find(|c| c.id == id), None => None, } } fn all_cryps(&self) -> Vec { self.teams.clone() .into_iter() .flat_map( |t| t.cryps .into_iter()) .collect::>() } fn update_cryp(&mut self, cryp: &mut Cryp) -> &mut Game { match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == cryp.id)) { Some(team) => { let index = team.cryps.iter().position(|t| t.id == cryp.id).unwrap(); team.cryps.remove(index); team.cryps.push(cryp.clone()); team.cryps.sort_unstable_by_key(|c| c.id); }, None => panic!("cryp not in game"), }; self } fn can_start(&self) -> bool { return self.teams.len() == self.team_num && self.teams.iter().all(|t| t.cryps.len() == self.team_size) } pub fn start(&mut self) -> &mut Game { self.log.push("Game starting...".to_string()); self.skill_phase_start(); self } fn skill_phase_start(&mut self) -> &mut Game { self.log.push("".to_string()); if ![Phase::Start, Phase::Resolve].contains(&self.phase) { panic!("game not in Resolve or start phase"); } self.phase = Phase::Skill; self.stack.clear(); self.pve_add_skills(); if self.skill_phase_finished() { self.resolve_phase_start(); } self } fn pve_add_skills(&mut self) -> &mut Game { for mobs in self.teams .clone() .into_iter() .filter(|t| t.bot) { let player_team = self.teams.iter().find(|t| t.id != mobs.id).unwrap().clone(); for mob in mobs.cryps.iter() { let skill = mob.mob_select_skill(); // println!("{:?} {:?}", mob.name, skill); match skill { Some(s) => { let mut rng = thread_rng(); // the mut marks it as being able to be called // more than once let mut find_target = || { match s.defensive() { true => &mobs.cryps[rng.gen_range(0, mobs.cryps.len())], false => &player_team.cryps[rng.gen_range(0, player_team.cryps.len())], } }; let mut target = find_target(); while target.is_ko() { target = find_target(); } match self.add_skill(mobs.id, mob.id, Some(target.id), s) { Ok(_) => (), Err(e) => println!("{:?} could not add pve skill", e), } // println!("{:?}", cast); }, None => continue, }; } } self } fn add_skill(&mut self, team_id: Uuid, source_cryp_id: Uuid, target_cryp_id: Option, skill: Skill) -> Result { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } let final_target_id = match skill.self_targeting() { true => source_cryp_id, false => match target_cryp_id { Some(t) => t, None => return Err(err_msg("skill requires a target")), } }; // target checks { let target = match self.cryp_by_id(final_target_id) { Some(c) => c, None => return Err(err_msg("target cryp not in game")), }; // fixme for rez if target.is_ko() { return Err(err_msg("target cryp is ko")); } } // cryp checks { let cryp = match self.cryp_by_id(source_cryp_id) { Some(c) => c, None => return Err(err_msg("cryp not in game")), }; if cryp.is_ko() { return Err(err_msg("cryp is ko")); } // check the cryp has the skill if !cryp.knows(skill) { return Err(err_msg("cryp does not have that skill")); } if cryp.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 let check = cryp.disabled(skill); if check.disabled { return Err(format_err!("skill disabled {:?}", check.effects)); } } // replace cryp skill if let Some(s) = self.stack.iter_mut().position(|s| s.source_cryp_id == source_cryp_id) { self.stack.remove(s); } let skill = Cast::new(source_cryp_id, team_id, final_target_id, skill); let skill_id = skill.id; self.stack.push(skill); return Ok(skill_id); } fn skill_phase_finished(&self) -> bool { self.teams.iter() // for every team .all(|t| self.stack.iter() // the number of skills they have cast .filter(|s| s.source_team_id == t.id).collect::>() // should equal the number required this turn .len() == t.skills_required() ) } // requires no input // just do it fn resolve_phase_start(&mut self) -> &mut Game { if self.phase != Phase::Skill { panic!("game not in skill phase"); } assert!(self.skill_phase_finished()); self.phase = Phase::Resolve; self.log.push("".to_string()); self.resolve_skills() } fn log_resolution(&mut self, source: &mut Cryp, target: &mut Cryp, cast: &Cast) -> &mut Game { match cast.resolution.disable.disabled { true => { self.log.push(format!("{:} {:?} {:} disabled {:?}", source.name, cast.skill, target.name, cast.resolution.disable.effects)); return self; }, false => (), }; for result in cast.resolution.results.iter() { match result { ResolutionResult::Damage { amount, mitigation, category: _, immunity } => { match immunity.immune { true => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", cast.resolution.speed, source.name, cast.skill, target.name, immunity.effects)), false => self.log.push(format!("[{:}] {:} {:?} {:} {:} ({:} mitigated)", cast.resolution.speed, source.name, cast.skill, target.name, amount, mitigation)), } }, ResolutionResult::Healing { amount, overhealing, immunity } => { match immunity.immune { true => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", cast.resolution.speed, source.name, cast.skill, target.name, immunity.effects)), false => self.log.push(format!("[{:}] {:} {:?} {:} {:} ({:}OH)", cast.resolution.speed, source.name, cast.skill, target.name, amount, overhealing)), } }, ResolutionResult::Effect { effect, duration, immunity } => { match immunity.immune { true => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", cast.resolution.speed, source.name, cast.skill, target.name, immunity.effects)), false => self.log.push(format!("[{:}] {:} {:?} {:} {:?} {:}T", cast.resolution.speed, source.name, cast.skill, target.name, effect, duration)), } }, ResolutionResult::Removal { effect, immunity } => { match immunity.immune { true => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", cast.resolution.speed, source.name, cast.skill, target.name, immunity.effects)), false => self.log.push(format!("[{:}] {:?} removed {:} {:?}", cast.resolution.speed, source.name, target.name, effect)), } }, ResolutionResult::Recharge { red, blue, immunity } => { match immunity.immune { true => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", cast.resolution.speed, source.name, cast.skill, target.name, immunity.effects)), false => self.log.push(format!("[{:}] {:} {:?} {:} {:}R {:}B", cast.resolution.speed, source.name, cast.skill, target.name, red, blue)), } }, ResolutionResult::Evasion { skill: _, evasion_rating } => { self.log.push(format!("[{:}] {:} {:?} {:} evaded ({:}%)", cast.resolution.speed, source.name, cast.skill, target.name, evasion_rating)); }, } } self } fn stack_sort_speed(&mut self) -> &mut Game { let mut sorted = self.stack.clone(); sorted.sort_unstable_by_key(|s| -> u64 { let caster = self.cryp_by_id(s.source_cryp_id).unwrap(); caster.skill_speed(s.skill) }); self.stack = sorted; self.stack.reverse(); self } fn resolve_skills(&mut self) -> &mut Game { if self.phase != Phase::Resolve { panic!("game not in Resolve phase"); } // find their statuses with ticks let mut ticks = self.all_cryps() .iter() .flat_map( |c| c.effects .iter() .cloned() .filter_map(|e| e.tick)) .collect::>(); // add them to the stack self.stack.append(&mut ticks); self.stack_sort_speed(); // update the stack with the resolved skills for mut skill in self.stack.clone() { // println!("{:} resolving ", skill); let mut source = self.cryp_by_id(skill.source_cryp_id).unwrap().clone(); let mut target = self.cryp_by_id(skill.target_cryp_id).unwrap().clone(); skill.set_resolution(&mut source, &mut target); self.log_resolution(&mut source, &mut target, &skill); self.resolved.push(skill); if target.is_ko() && !target.ko_logged { self.log.push(format!("{:} KO", target.name)); target.effects.clear(); target.ko_logged = true; } if source.is_ko() && !source.ko_logged { self.log.push(format!("{:} KO", source.name)); source.effects.clear(); source.ko_logged = true; } self.update_cryp(&mut source); self.update_cryp(&mut target); // find a way to // resort the stack after each cast because // the cryp speed may have changed // self.stack_sort_speed(); }; // println!("{:#?}", self.resolved); // now Resolve has all been assigned // handle cooldowns and statuses self.progress_durations(); if self.finished() { return self.finish() } self.skill_phase_start() } fn progress_durations(&mut self) -> &mut Game { for mut cryp in self.all_cryps() { // println!("progressing durations for {:}", cryp.name); if cryp.is_ko() { continue; } // only reduce cooldowns if no cd was used // have to borrow self for the skill check { if let Some(skill) = self.stack.iter_mut().find(|s| s.source_cryp_id == cryp.id) { if skill.used_cooldown() { cryp.skill_set_cd(skill.skill); } else { cryp.reduce_cooldowns(); } } else { cryp.reduce_cooldowns(); } } // always reduce durations cryp.reduce_effect_durations(&mut self.log); self.update_cryp(&mut cryp); } self } pub fn finished(&self) -> bool { self.teams.iter().any(|t| t.cryps.iter().all(|c| c.is_ko())) } pub fn winner(&self) -> Option<&Team> { self.teams.iter().find(|t| t.cryps.iter().any(|c| !c.is_ko())) } fn finish(&mut self) -> &mut Game { self.phase = Phase::Finish; self.log.push(format!("Game finished.")); self.stack.clear(); { let winner = self.teams.iter().find(|t| t.cryps.iter().any(|c| !c.is_ko())); match winner { Some(w) => self.log.push(format!("Winner: {:}", w.id)), None => self.log.push(format!("Game was drawn.")), }; } self } } pub fn game_skill(params: GameSkillParams, tx: &mut Transaction, account: &Account) -> Result { let query = " SELECT * FROM games WHERE id = $1 "; let result = tx .query(query, &[¶ms.game_id])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("game not found")), }; // tells from_slice to cast into a cryp let game_bytes: Vec = returned.get("data"); let mut game = from_slice::(&game_bytes)?; if game.phase != Phase::Skill { return Err(err_msg("game not in skill phase")) } game.add_skill(account.id, params.cryp_id, params.target_cryp_id, params.skill)?; if game.skill_phase_finished() { game.resolve_phase_start(); } game_update(&game, tx)?; Ok(game) } // pub fn game_target(params: GameTargetParams, tx: &mut Transaction, account: &Account) -> Result { // let query = " // SELECT * // FROM games // WHERE id = $1 // "; // let result = tx // .query(query, &[¶ms.game_id])?; // let returned = match result.iter().next() { // Some(row) => row, // None => return Err(err_msg("game not found")), // }; // // tells from_slice to cast into a cryp // let game_bytes: Vec = returned.get("data"); // let mut game = from_slice::(&game_bytes)?; // game.add_target(account.id, params.cryp_id, params.skill_id)?; // if game.target_phase_finished() { // game.resolve_phase_start(); // } // game_update(game, tx)?; // Ok(game) // } pub fn game_write(game: &Game, tx: &mut Transaction) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " INSERT INTO games (id, data) VALUES ($1, $2) RETURNING id; "; let result = tx .query(query, &[&game.id, &game_bytes])?; result.iter().next().ok_or(format_err!("no game written"))?; // println!("{:} wrote game", game.id); return Ok(()); } pub fn game_state(params: GameStateParams, tx: &mut Transaction, _account: &Account) -> Result { return game_get(tx, params.id) } pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result { let query = " SELECT * FROM games WHERE id = $1 "; let result = tx .query(query, &[&id])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("game not found")), }; // tells from_slice to cast into a cryp let game_bytes: Vec = returned.get("data"); let game = from_slice::(&game_bytes)?; return Ok(game); } pub fn game_global_startup(tx: &mut Transaction) -> Result<(), Error> { if game_global_get(tx).is_ok() { println!("global mm game exists"); return Ok(()); } let mut game = Game::new(); game .set_team_num(2) .set_team_size(3) .set_mode(GameMode::Pvp); game_write(&game, tx)?; let query = " INSERT INTO matchmaking (id, game) VALUES ($1, $2) RETURNING id; "; let result = tx .query(query, &[&Uuid::nil(), &game.id])?; result.iter().next().ok_or(format_err!("no game written"))?; println!("{:} wrote global mm startup", game.id); return Ok(()); } pub fn game_global_set(tx: &mut Transaction, game: &Game) -> Result<(), Error> { let query = " UPDATE matchmaking SET game = $1 WHERE id = $2 RETURNING id, game; "; let result = tx .query(query, &[&game.id, &Uuid::nil()])?; result.iter() .next() .ok_or(err_msg("could not set global game mm"))?; return Ok(()); } pub fn game_global_get(tx: &mut Transaction) -> Result { let query = " SELECT * from games WHERE id = ( SELECT game FROM matchmaking WHERE id = $1 ); "; let delete_query = " DELETE from matchmaking; "; let result = tx .query(query, &[&Uuid::nil()])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("game not found")), }; // tells from_slice to cast into a cryp let game_bytes: Vec = returned.get("data"); let game = match from_slice::(&game_bytes) { Ok(g) => g, Err(_) => { tx.query(delete_query, &[])?; return Err(err_msg("matchmaking game was invalid")) } }; return Ok(game); } pub fn game_update(game: &Game, tx: &mut Transaction) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " UPDATE games SET data = $1 WHERE id = $2 RETURNING id, data; "; let result = tx .query(query, &[&game_bytes, &game.id])?; result.iter().next().ok_or(format_err!("game {:?} could not be written", game))?; if game.finished() { if let Some(i) = game.instance { match i == Uuid::nil() { true => global_game_finished(tx, &game)?, false => instance_game_finished(tx, &game, i)?, } } } return Ok(()); } // pub fn game_pve_new(cryp_ids: Vec, mode: GameMode, tx: &mut Transaction, account: &Account) -> Result { // if cryp_ids.len() == 0 { // return Err(err_msg("no cryps selected")); // } // let cryps = cryp_ids // .iter() // .map(|id| cryp_get(tx, *id, account.id)) // .collect::, Error>>()?; // if cryps.len() > 3 { // return Err(err_msg("team size too large (3 max)")); // } // // create the game // let mut game = Game::new(); // // let game_id = game.id; // game; // .set_team_num(2) // .set_team_size(cryps.len()) // .set_mode(mode); // // create the mob team // let mob_team = generate_mob_team(mode, &cryps); // // add the players // let mut plr_team = Team::new(account.id); // plr_team // .set_cryps(cryps); // game // .team_add(plr_team)? // .team_add(mob_team)?; // game.start(); // return Ok(game); // } // pub fn game_pve(params: GamePveParams, tx: &mut Transaction, account: &Account) -> Result { // let game = game_pve_new(params.cryp_ids, GameMode::Normal, tx, account)?; // // persist // game_write(&game, tx)?; // Ok(game) // } pub fn game_instance_new(tx: &mut Transaction, player: Player, game_id: Uuid) -> Result { // create the game let mut game = Game::new(); game.id = game_id; game .set_team_num(2) .set_team_size(3) .set_instance(player.instance) .set_mode(GameMode::Pvp); // create the initiators team let mut team = Team::new(player.account); team.set_cryps(player.cryps); game.team_add(team)?; // persist game_write(&game, tx)?; Ok(game) } pub fn game_instance_join(tx: &mut Transaction, player: Player, game_id: Uuid) -> Result { let mut game = game_get(tx, game_id)?; let mut team = Team::new(player.account); team.set_cryps(player.cryps); game.team_add(team)?; if game.can_start() { game.start(); } println!("{:?} game joined", game.id); game_update(&game, tx)?; Ok(game) } #[cfg(test)] mod tests { use game::*; use cryp::*; fn create_test_game() -> Game { let x = Cryp::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::TestStun) .learn(Skill::TestTouch) .learn(Skill::TestBlock) .learn(Skill::TestParry) .learn(Skill::TestSiphon) .learn(Skill::Empower) .learn(Skill::Stun) .learn(Skill::Block); let y = Cryp::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::TestStun) .learn(Skill::TestTouch) .learn(Skill::TestBlock) .learn(Skill::TestParry) .learn(Skill::TestSiphon) .learn(Skill::Empower) .learn(Skill::Stun) .learn(Skill::Block); let mut game = Game::new(); game .set_team_num(2) .set_team_size(1); let x_team_id = Uuid::new_v4(); let mut x_team = Team::new(x_team_id); x_team .set_cryps(vec![x]); let y_team_id = Uuid::new_v4(); let mut y_team = Team::new(y_team_id); y_team .set_cryps(vec![y]); game .team_add(x_team).unwrap() .team_add(y_team).unwrap(); assert!(game.can_start()); game.start(); return game; } fn create_2v2_test_game() -> Game { let i = Cryp::new() .named(&"pretaliate".to_string()) .learn(Skill::Attack) .learn(Skill::TestTouch); let j = Cryp::new() .named(&"poy sian".to_string()) .learn(Skill::Attack) .learn(Skill::TestTouch); let x = Cryp::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::TestTouch); let y = Cryp::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::TestTouch); let mut game = Game::new(); game .set_team_num(2) .set_team_size(2); let i_team_id = Uuid::new_v4(); let mut i_team = Team::new(i_team_id); i_team .set_cryps(vec![i,j]); let x_team_id = Uuid::new_v4(); let mut x_team = Team::new(x_team_id); x_team .set_cryps(vec![x,y]); game .team_add(i_team).unwrap() .team_add(x_team).unwrap(); assert!(game.can_start()); game.start(); return game; } #[test] fn phase_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::Attack).unwrap(); game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::Attack).unwrap(); assert!(game.skill_phase_finished()); game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); return; } #[test] fn stun_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let _x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::TestStun).unwrap(); game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); assert!(game.skill_phase_finished()); game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.team_by_id(y_team.id).cryps[0].is_stunned()); assert!(game.team_by_id(y_team.id).skills_required() == 0); } #[test] fn ko_resolution_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); game.team_by_id(y_team.id).cryp_by_id(y_cryp.id).unwrap().red_damage.force(u64::max_value()); game.team_by_id(y_team.id).cryp_by_id(y_cryp.id).unwrap().speed.force(u64::max_value()); // just in case // remove all mitigation game.team_by_id(x_team.id).cryp_by_id(x_cryp.id).unwrap().red_shield.force(0); let _x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::TestStun).unwrap(); game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::Attack).unwrap(); assert!(game.skill_phase_finished()); game.resolve_phase_start(); assert!(!game.team_by_id(y_team.id).cryps[0].is_stunned()); assert!(game.phase == Phase::Finish); } #[test] fn cooldown_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_none()); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Stun).is_some()); assert!(game.team_by_id(x_team.id).cryps[0].skill_on_cd(Skill::Block).is_none()); let _x_stun_id = game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Stun).is_none()); // second round // now we block and it should go back on cd let _x_block_id = game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::Stun).unwrap(); let _y_touch_id = game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.resolve_phase_start(); assert!(game.team_by_id(x_team.id).cryps[0].skill_on_cd(Skill::Stun).is_some()); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_none()); } #[test] fn parry_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let _x_block_id = game.add_skill(x_team.id, x_cryp.id, None, Skill::TestParry).unwrap(); game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::TestStun).unwrap(); game.resolve_phase_start(); // should not be stunned because of parry assert!(game.team_by_id(x_team.id).cryps[0].is_stunned() == false); } #[test] fn siphon_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let _x_siphon_id = game.add_skill(x_team.id, x_cryp.id, Some(y_cryp.id), Skill::TestSiphon).unwrap(); let _y_touch_id = game.add_skill(y_team.id, y_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.resolve_phase_start(); game.add_skill(x_team.id, x_cryp.id, None, Skill::TestBlock).unwrap(); game.add_skill(y_team.id, y_cryp.id, None, Skill::TestBlock).unwrap(); game.resolve_phase_start(); assert!(game.resolved.iter().any(|r| r.skill == Skill::SiphonTick)); } #[test] fn ko_pve_test() { let mut game = create_2v2_test_game(); let i_team = game.teams[0].clone(); let x_team = game.teams[1].clone(); let i_cryp = i_team.cryps[0].clone(); let j_cryp = i_team.cryps[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = x_team.cryps[1].clone(); game.add_skill(i_team.id, i_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(i_team.id, j_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(x_team.id, x_cryp.id, Some(i_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(x_team.id, y_cryp.id, Some(i_cryp.id), Skill::TestTouch).unwrap(); assert!(game.skill_phase_finished()); game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); // kill a cryp game.team_by_id(i_team.id).cryp_by_id(i_cryp.id).unwrap().hp.reduce(u64::max_value()); assert!(game.team_by_id(i_team.id).skills_required() == 1); assert!(game.team_by_id(x_team.id).skills_required() == 2); // add some more skills game.add_skill(i_team.id, j_cryp.id, Some(x_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(x_team.id, x_cryp.id, Some(j_cryp.id), Skill::TestTouch).unwrap(); game.add_skill(x_team.id, y_cryp.id, Some(j_cryp.id), Skill::TestTouch).unwrap(); assert!(game.add_skill(x_team.id, x_cryp.id, Some(i_cryp.id), Skill::TestTouch).is_err()); assert!(game.skill_phase_finished()); game.resolve_phase_start(); assert!(game.team_by_id(i_team.id).skills_required() == 1); assert!(game.team_by_id(x_team.id).skills_required() == 2); return; } }