diff --git a/src/actions/abilities/mechanic.rs b/src/actions/abilities/mechanic.rs index d35abe65..ac8d06d6 100644 --- a/src/actions/abilities/mechanic.rs +++ b/src/actions/abilities/mechanic.rs @@ -95,6 +95,7 @@ pub enum AbilityMechanic { amount: u32, }, PoisonOpponentActive, + RemoveRandomSpecialConditionFromActive, HealActiveYourPokemon { amount: u32, }, diff --git a/src/actions/apply_abilities_action.rs b/src/actions/apply_abilities_action.rs index 2194095e..9f31e3de 100644 --- a/src/actions/apply_abilities_action.rs +++ b/src/actions/apply_abilities_action.rs @@ -1,7 +1,7 @@ use core::panic; use log::debug; -use rand::rngs::StdRng; +use rand::{rngs::StdRng, Rng}; use crate::{ actions::{ @@ -14,7 +14,7 @@ use crate::{ }, effects::TurnEffect, hooks::is_ultra_beast, - models::{Card, EnergyType, StatusCondition}, + models::{Card, EnergyType, PlayedCard, StatusCondition}, State, }; @@ -164,6 +164,9 @@ fn forecast_ability_by_mechanic( amount, } => discard_energy_to_increase_type_damage(*discard_energy, *attack_type, *amount), AbilityMechanic::PoisonOpponentActive => poison_opponent_active(), + AbilityMechanic::RemoveRandomSpecialConditionFromActive => { + remove_random_special_condition_from_active() + } AbilityMechanic::HealActiveYourPokemon { amount } => heal_active_your_pokemon(*amount), AbilityMechanic::SwitchOutOpponentActiveToBench { .. } => { switch_out_opponent_active_to_bench() @@ -473,6 +476,31 @@ fn poison_opponent_active() -> Outcomes { }) } +fn remove_random_special_condition_from_active() -> Outcomes { + Outcomes::single_fn(|rng, state, action| { + let active = state.get_active_mut(action.actor); + let conditions = active_special_conditions(active); + if conditions.is_empty() { + return; + } + let condition = conditions[rng.gen_range(0..conditions.len())]; + active.clear_status_condition(condition); + }) +} + +fn active_special_conditions(active: &PlayedCard) -> Vec { + [ + active.is_poisoned().then_some(StatusCondition::Poisoned), + active.is_paralyzed().then_some(StatusCondition::Paralyzed), + active.is_asleep().then_some(StatusCondition::Asleep), + active.is_burned().then_some(StatusCondition::Burned), + active.is_confused().then_some(StatusCondition::Confused), + ] + .into_iter() + .flatten() + .collect() +} + fn coin_flip_sleep_opponent_active() -> Outcomes { Outcomes::binary_coin( Box::new(|_, state, action| { diff --git a/src/actions/apply_attack_action.rs b/src/actions/apply_attack_action.rs index 5f66a07f..f7dd509e 100644 --- a/src/actions/apply_attack_action.rs +++ b/src/actions/apply_attack_action.rs @@ -212,6 +212,10 @@ fn forecast_effect_attack_by_mechanic( *damage_per_heads, ), Mechanic::SelfHeal { amount } => self_heal_attack(*amount, attack), + Mechanic::HealOneYourPokemon { amount } => heal_one_your_pokemon_attack(*amount), + Mechanic::CoinFlipSelfHeal { amount } => { + coin_flip_self_heal_attack(attack.fixed_damage, *amount) + } Mechanic::SelfChargeActive { energies } => { self_charge_active_from_energies(attack.fixed_damage, energies.clone()) } @@ -1568,6 +1572,32 @@ fn draw_and_damage_outcome(damage: u32, amount: u8) -> Outcomes { }) } +fn heal_one_your_pokemon_attack(amount: u32) -> Outcomes { + Outcomes::single_fn(move |_rng, state, action| { + let choices = state + .enumerate_in_play_pokemon(action.actor) + .filter(|(_, pokemon)| pokemon.is_damaged()) + .map(|(in_play_idx, _)| SimpleAction::Heal { + in_play_idx, + amount, + cure_status: false, + }) + .collect::>(); + if !choices.is_empty() { + state.move_generation_stack.push((action.actor, choices)); + } + }) +} + +fn coin_flip_self_heal_attack(damage: u32, heal: u32) -> Outcomes { + Outcomes::binary_coin( + active_damage_effect_mutation(damage, move |_, state, action| { + state.get_active_mut(action.actor).heal(heal); + }), + active_damage_mutation(damage), + ) +} + /// Generic attack that deals bonus damage if the Pokémon has enough energy of a specific type attached. /// Used by attacks like Hydro Pump, Hydro Bazooka, and Blazing Beatdown. fn extra_energy_attack( diff --git a/src/actions/attacks/mechanic.rs b/src/actions/attacks/mechanic.rs index 02733cc6..80a0491a 100644 --- a/src/actions/attacks/mechanic.rs +++ b/src/actions/attacks/mechanic.rs @@ -22,6 +22,12 @@ pub enum Mechanic { SelfHeal { amount: u32, }, + HealOneYourPokemon { + amount: u32, + }, + CoinFlipSelfHeal { + amount: u32, + }, SearchToHandByEnergy { energy_type: EnergyType, }, diff --git a/src/actions/effect_ability_mechanic_map.rs b/src/actions/effect_ability_mechanic_map.rs index 2317269d..726039cb 100644 --- a/src/actions/effect_ability_mechanic_map.rs +++ b/src/actions/effect_ability_mechanic_map.rs @@ -418,7 +418,10 @@ pub static EFFECT_ABILITY_MECHANIC_MAP: LazyLock> = Lazy Mechanic::CoinFlipDiscardEnergyFromOpponentActive, ); // map.insert("Flip a coin. If heads, discard a random card from your opponent's hand.", todo_implementation); - // map.insert("Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pokémon by attacks.", todo_implementation); + map.insert( + "Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pokémon by attacks.", + Mechanic::DamageAndCardEffect { + opponent: false, + effect: CardEffect::PreventAllDamageAndEffects, + duration: 1, + coin_flip: true, + }, + ); map.insert("Flip a coin. If heads, during your opponent's next turn, prevent all damage from—and effects of—attacks done to this Pokémon.", Mechanic::DamageAndCardEffect { opponent: false, effect: CardEffect::PreventAllDamageAndEffects, @@ -1853,7 +1861,14 @@ pub static EFFECT_MECHANIC_MAP: LazyLock> = Lazy // map.insert("Heal 10 damage from each of your Pokémon.", todo_implementation); // map.insert("Heal 20 damage from each of your [P] Pokémon.", todo_implementation); // map.insert("Heal 30 damage from 1 of your Benched Pokémon.", todo_implementation); - // map.insert("Heal 30 damage from 1 of your Pokémon.", todo_implementation); + map.insert( + "Heal 30 damage from 1 of your Pokémon.", + Mechanic::HealOneYourPokemon { amount: 30 }, + ); + map.insert( + "Flip a coin. If heads, heal 60 damage from this Pokémon.", + Mechanic::CoinFlipSelfHeal { amount: 60 }, + ); // map.insert("Heal 30 damage from each of your Pokémon.", todo_implementation); // map.insert("If Durant is on your Bench, this attack does 30 more damage.", todo_implementation); map.insert( diff --git a/src/move_generation/move_generation_abilities.rs b/src/move_generation/move_generation_abilities.rs index 08e7e8c7..5bccbfb6 100644 --- a/src/move_generation/move_generation_abilities.rs +++ b/src/move_generation/move_generation_abilities.rs @@ -120,6 +120,9 @@ fn can_use_ability_by_mechanic( !card.ability_used && card.attached_energy.contains(discard_energy) } AbilityMechanic::PoisonOpponentActive => _in_play_index == 0 && !card.ability_used, + AbilityMechanic::RemoveRandomSpecialConditionFromActive => { + can_use_remove_random_special_condition_from_active(state, card) + } AbilityMechanic::HealActiveYourPokemon { .. } => !card.ability_used, AbilityMechanic::SwitchOutOpponentActiveToBench { require_active } => { let opponent = (state.current_player + 1) % 2; @@ -190,6 +193,19 @@ fn can_use_switch_active_typed_with_bench( .is_some() } +fn can_use_remove_random_special_condition_from_active(state: &State, card: &PlayedCard) -> bool { + !card.ability_used + && state + .maybe_get_active(state.current_player) + .is_some_and(|active| { + active.is_poisoned() + || active.is_paralyzed() + || active.is_asleep() + || active.is_burned() + || active.is_confused() + }) +} + fn can_use_heal_one_your_pokemon_ex_and_discard_random_energy( state: &State, card: &PlayedCard, diff --git a/tests/pokemon.rs b/tests/pokemon.rs index 52e6462d..d98fb675 100644 --- a/tests/pokemon.rs +++ b/tests/pokemon.rs @@ -10,6 +10,8 @@ mod bonsly_teary_attack_test; mod camerupt_eruption_test; #[path = "pokemon/castform_test.rs"] mod castform_test; +#[path = "pokemon/chansey_blissey_test.rs"] +mod chansey_blissey_test; #[path = "pokemon/charmeleon_ignition_test.rs"] mod charmeleon_ignition_test; #[path = "pokemon/comfey_flower_shield_test.rs"] diff --git a/tests/pokemon/chansey_blissey_test.rs b/tests/pokemon/chansey_blissey_test.rs new file mode 100644 index 00000000..8a2c6e83 --- /dev/null +++ b/tests/pokemon/chansey_blissey_test.rs @@ -0,0 +1,172 @@ +use deckgym::{ + actions::{Action, SimpleAction}, + card_ids::CardId, + database::get_card_by_enum, + models::{Card, EnergyType, PlayedCard, StatusCondition, TrainerCard}, + test_support::get_initialized_game, +}; + +fn played_card_with_base_hp(card_id: CardId, base_hp: u32) -> PlayedCard { + let card = get_card_by_enum(card_id); + PlayedCard::new(card, 0, base_hp, vec![], false, vec![]) +} + +fn trainer_from_id(card_id: CardId) -> TrainerCard { + match get_card_by_enum(card_id) { + Card::Trainer(trainer) => trainer, + _ => panic!("Expected trainer card"), + } +} + +fn play_will(game: &mut deckgym::Game<'static>, actor: usize) { + game.apply_action(&Action { + actor, + action: SimpleAction::Play { + trainer_card: trainer_from_id(CardId::A4156Will), + }, + is_stack: false, + }); +} + +#[test] +fn test_chansey_bind_wound_heals_one_of_your_pokemon() { + let mut game = get_initialized_game(0); + let mut state = game.get_state_clone(); + + state.set_board( + vec![ + PlayedCard::from_id(CardId::B3127Chansey) + .with_energy(vec![EnergyType::Colorless, EnergyType::Colorless]), + PlayedCard::from_id(CardId::A1001Bulbasaur).with_damage(40), + ], + vec![PlayedCard::from_id(CardId::A1053Squirtle)], + ); + state.current_player = 0; + state.turn_count = 3; + game.set_state(state); + + game.apply_action(&Action { + actor: 0, + action: SimpleAction::Attack(0), + is_stack: false, + }); + + let (actor, choices) = game.get_state_clone().generate_possible_actions(); + assert_eq!(actor, 0); + let heal_bench = choices + .iter() + .find(|choice| { + matches!( + choice.action, + SimpleAction::Heal { + in_play_idx: 1, + amount: 30, + cure_status: false + } + ) + }) + .expect("Bind Wound should offer a heal choice for damaged benched Pokemon") + .clone(); + + game.apply_action(&heal_bench); + + assert_eq!(game.get_state_clone().get_remaining_hp(0, 1), 60); +} + +#[test] +fn test_chansey_scrunch_heads_prevents_next_attack_damage() { + let mut game = get_initialized_game(0); + let mut state = game.get_state_clone(); + + state.set_board( + vec![PlayedCard::from_id(CardId::A4131Chansey) + .with_energy(vec![EnergyType::Colorless, EnergyType::Colorless])], + vec![PlayedCard::from_id(CardId::A1001Bulbasaur) + .with_energy(vec![EnergyType::Grass, EnergyType::Colorless])], + ); + state.current_player = 0; + state.turn_count = 3; + state.hands[0] = vec![Card::Trainer(trainer_from_id(CardId::A4156Will))]; + game.set_state(state); + + play_will(&mut game, 0); + game.apply_action(&Action { + actor: 0, + action: SimpleAction::Attack(0), + is_stack: false, + }); + + let mut state = game.get_state_clone(); + state.current_player = 1; + game.set_state(state); + + game.apply_action(&Action { + actor: 1, + action: SimpleAction::Attack(0), + is_stack: false, + }); + + assert_eq!(game.get_state_clone().get_active(0).get_remaining_hp(), 100); +} + +#[test] +fn test_blissey_happiness_supplement_removes_active_special_condition() { + let mut game = get_initialized_game(0); + let mut state = game.get_state_clone(); + + state.set_board( + vec![ + PlayedCard::from_id(CardId::A1001Bulbasaur), + PlayedCard::from_id(CardId::B3128Blissey), + ], + vec![PlayedCard::from_id(CardId::A1053Squirtle)], + ); + state.apply_status_condition(0, 0, StatusCondition::Poisoned); + state.current_player = 0; + state.turn_count = 3; + game.set_state(state); + + let (_actor, choices) = game.get_state_clone().generate_possible_actions(); + let ability = choices + .iter() + .find(|choice| matches!(choice.action, SimpleAction::UseAbility { in_play_idx: 1 })) + .expect("Blissey should be able to cure a condition from the Active Pokemon") + .clone(); + + game.apply_action(&ability); + + assert!(!game.get_state_clone().get_active(0).is_poisoned()); +} + +#[test] +fn test_blissey_ex_happy_punch_heads_heals_self_after_damage() { + let mut game = get_initialized_game(0); + let mut state = game.get_state_clone(); + + state.set_board( + vec![PlayedCard::from_id(CardId::PA098BlisseyEx) + .with_energy(vec![ + EnergyType::Colorless, + EnergyType::Colorless, + EnergyType::Colorless, + EnergyType::Colorless, + ]) + .with_damage(80)], + vec![played_card_with_base_hp(CardId::A1001Bulbasaur, 200)], + ); + state.current_player = 0; + state.turn_count = 3; + state.hands[0] = vec![Card::Trainer(trainer_from_id(CardId::A4156Will))]; + game.set_state(state); + + play_will(&mut game, 0); + game.apply_action(&Action { + actor: 0, + action: SimpleAction::Attack(0), + is_stack: false, + }); + + let state = game.get_state_clone(); + assert_eq!(state.get_active(1).get_remaining_hp(), 100); + assert_eq!(state.get_active(0).get_remaining_hp(), 160); +}