Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/actions/abilities/mechanic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub enum AbilityMechanic {
},
SearchRandomPokemonFromDeck,
MoveDamageFromOneYourPokemonToThisPokemon,
DiscardOpponentActiveToolsAndDiscardSelf,
PreventFirstAttack,
ElectromagneticWall,
InfiltratingInspection,
Expand Down
22 changes: 21 additions & 1 deletion src/actions/apply_abilities_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rand::{rngs::StdRng, Rng};
use crate::{
actions::{
abilities::AbilityMechanic,
apply_action_helpers::{handle_damage, Mutation},
apply_action_helpers::{handle_damage, handle_knockouts, Mutation},
effect_ability_mechanic_map::ability_mechanic_from_effect,
outcomes::Outcomes,
shared_mutations::pokemon_search_outcomes,
Expand Down Expand Up @@ -133,6 +133,7 @@ fn forecast_ability_by_mechanic(
AbilityMechanic::MoveDamageFromOneYourPokemonToThisPokemon => {
Outcomes::single(dusknoir_shadow_void(in_play_idx))
}
AbilityMechanic::DiscardOpponentActiveToolsAndDiscardSelf => dismantling_keys(in_play_idx),
AbilityMechanic::PreventFirstAttack => {
panic!("PreventFirstAttack is a passive ability")
}
Expand Down Expand Up @@ -609,6 +610,25 @@ fn dusknoir_shadow_void(dusknoir_idx: usize) -> Mutation {
})
}

fn dismantling_keys(klefki_idx: usize) -> Outcomes {
Outcomes::single_fn(move |_rng, state, action| {
let opponent = (action.actor + 1) % 2;
if state
.maybe_get_active(opponent)
.is_none_or(|active| !active.has_tool_attached())
{
return;
}

state.discard_tool(opponent, 0);
handle_knockouts(state, (action.actor, klefki_idx), false);

if state.in_play_pokemon[action.actor][klefki_idx].is_some() {
state.discard_from_play(action.actor, klefki_idx);
}
})
}

fn umbreon_dark_chase(_: &mut StdRng, state: &mut State, action: &Action) {
// Once during your turn, if this Pokémon is in the Active Spot, you may switch in 1 of your opponent's Benched Pokémon that has damage on it to the Active Spot.
debug!("Umbreon ex's Dark Chase: Switching in opponent's damaged benched Pokemon");
Expand Down
23 changes: 23 additions & 0 deletions src/actions/apply_attack_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,15 @@ fn forecast_effect_attack_by_mechanic(
attack.fixed_damage,
*damage_per_supporter,
),
Mechanic::ExtraDamagePerPokemonTypeInDiscard {
energy_type,
damage_per_pokemon,
} => extra_damage_per_pokemon_type_in_discard_attack(
state,
attack.fixed_damage,
*energy_type,
*damage_per_pokemon,
),
Mechanic::ExtraDamagePerOwnPoint { damage_per_point } => {
extra_damage_per_own_point_attack(state, attack.fixed_damage, *damage_per_point)
}
Expand Down Expand Up @@ -3012,6 +3021,20 @@ fn extra_damage_per_supporter_in_discard_attack(
active_damage_doutcome(total_damage)
}

fn extra_damage_per_pokemon_type_in_discard_attack(
state: &State,
base_damage: u32,
energy_type: EnergyType,
damage_per_pokemon: u32,
) -> Outcomes {
let pokemon_count = state.discard_piles[state.current_player]
.iter()
.filter(|card| matches!(card, Card::Pokemon(pokemon) if pokemon.energy_type == energy_type))
.count() as u32;
let total_damage = base_damage + (pokemon_count * damage_per_pokemon);
active_damage_doutcome(total_damage)
}

/// Mega Manectric ex - Lightning Accelerator: Extra damage per point you have gotten
fn extra_damage_per_own_point_attack(
state: &State,
Expand Down
4 changes: 4 additions & 0 deletions src/actions/attacks/mechanic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ pub enum Mechanic {
ExtraDamagePerSupporterInDiscard {
damage_per_supporter: u32,
},
ExtraDamagePerPokemonTypeInDiscard {
energy_type: EnergyType,
damage_per_pokemon: u32,
},
ExtraDamagePerOwnPoint {
damage_per_point: u32,
},
Expand Down
5 changes: 4 additions & 1 deletion src/actions/effect_ability_mechanic_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ pub static EFFECT_ABILITY_MECHANIC_MAP: LazyLock<HashMap<&'static str, AbilityMe
energy_type: EnergyType::Grass,
},
);
// map.insert("Once during your turn, if this Pokémon is on your Bench, you may discard all Pokémon Tools from your opponent's Active Pokémon. If you do, discard this Pokémon.", todo_implementation);
map.insert(
"Once during your turn, if this Pokémon is on your Bench, you may discard all Pokémon Tools from your opponent's Active Pokémon. If you do, discard this Pokémon.",
AbilityMechanic::DiscardOpponentActiveToolsAndDiscardSelf,
);
map.insert(
"Once during your turn, if this Pokémon is on your Bench, you may switch it with your Active Pokémon.",
AbilityMechanic::SwitchThisBenchWithActive,
Expand Down
8 changes: 7 additions & 1 deletion src/actions/effect_mechanic_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,13 @@ pub static EFFECT_MECHANIC_MAP: LazyLock<HashMap<&'static str, Mechanic>> = Lazy
},
);
// map.insert("This attack also does 50 damage to 1 of your opponent's Benched Pokémon.", todo_implementation);
// map.insert("This attack does 20 more damage for each [P] Pokémon in your discard pile.", todo_implementation);
map.insert(
"This attack does 20 more damage for each [P] Pokémon in your discard pile.",
Mechanic::ExtraDamagePerPokemonTypeInDiscard {
energy_type: EnergyType::Psychic,
damage_per_pokemon: 20,
},
);

// Promo-B
// map.insert("If this Pokémon has any [P] Energy attached, this attack does 40 more damage. This attack's damage isn't affected by any effects on your opponent's Active Pokémon.", todo_implementation);
Expand Down
13 changes: 13 additions & 0 deletions src/move_generation/move_generation_abilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ fn can_use_ability_by_mechanic(
AbilityMechanic::MoveDamageFromOneYourPokemonToThisPokemon => {
can_use_dusknoir_shadow_void(state, _in_play_index)
}
AbilityMechanic::DiscardOpponentActiveToolsAndDiscardSelf => {
can_use_dismantling_keys(state, _in_play_index, card)
}
AbilityMechanic::PreventFirstAttack => false,
AbilityMechanic::ElectromagneticWall => false,
AbilityMechanic::InfiltratingInspection => false,
Expand Down Expand Up @@ -240,6 +243,16 @@ fn can_use_dusknoir_shadow_void(state: &State, dusknoir_idx: usize) -> bool {
.any(|(i, p)| p.is_damaged() && i != dusknoir_idx)
}

fn can_use_dismantling_keys(state: &State, in_play_idx: usize, card: &PlayedCard) -> bool {
if in_play_idx == 0 || card.ability_used {
return false;
}
let opponent = (state.current_player + 1) % 2;
state
.maybe_get_active(opponent)
.is_some_and(|active| active.has_tool_attached())
}

fn can_use_crobat_cunning_link(state: &State, card: &PlayedCard) -> bool {
if card.ability_used {
return false;
Expand Down
4 changes: 4 additions & 0 deletions tests/pokemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ mod emboar_flare_storm_test;
mod gallade_test;
#[path = "pokemon/grovyle_slicing_snipe_test.rs"]
mod grovyle_slicing_snipe_test;
#[path = "pokemon/houndstone_last_respects_test.rs"]
mod houndstone_last_respects_test;
#[path = "pokemon/jolteon_ex_test.rs"]
mod jolteon_ex_test;
#[path = "pokemon/klefki_dismantling_keys_test.rs"]
mod klefki_dismantling_keys_test;
#[path = "pokemon/legacy_ability_logic_test.rs"]
mod legacy_ability_logic_test;
#[path = "pokemon/lucario_b3_test.rs"]
Expand Down
57 changes: 57 additions & 0 deletions tests/pokemon/houndstone_last_respects_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use deckgym::{
actions::{Action, SimpleAction},
card_ids::CardId,
database::get_card_by_enum,
models::{Card, EnergyType, PlayedCard},
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 use_last_respects(discard_piles: [Vec<Card>; 2]) -> u32 {
let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();

state.current_player = 0;
state.set_board(
vec![PlayedCard::from_id(CardId::B2a053Houndstone)
.with_energy(vec![EnergyType::Psychic, EnergyType::Colorless])],
vec![played_card_with_base_hp(CardId::A1001Bulbasaur, 200)],
);
state.discard_piles = discard_piles;
game.set_state(state);

game.apply_action(&Action {
actor: 0,
action: SimpleAction::Attack(0),
is_stack: false,
});

game.get_state_clone().get_active(1).get_remaining_hp()
}

#[test]
fn test_houndstone_last_respects_does_base_damage_without_psychic_pokemon_in_discard() {
let remaining_hp = use_last_respects([vec![], vec![]]);

assert_eq!(remaining_hp, 150);
}

#[test]
fn test_houndstone_last_respects_counts_only_own_psychic_pokemon_in_discard() {
let remaining_hp = use_last_respects([
vec![
get_card_by_enum(CardId::A1128Mewtwo),
get_card_by_enum(CardId::A1130Ralts),
get_card_by_enum(CardId::A1120Gastly),
get_card_by_enum(CardId::A1001Bulbasaur),
get_card_by_enum(CardId::PA005PokeBall),
],
vec![get_card_by_enum(CardId::A1131Kirlia)],
]);

assert_eq!(remaining_hp, 90);
}
128 changes: 128 additions & 0 deletions tests/pokemon/klefki_dismantling_keys_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use deckgym::{
actions::{Action, SimpleAction},
card_ids::CardId,
database::get_card_by_enum,
models::PlayedCard,
test_support::get_initialized_game,
};

fn has_use_ability(actions: &[Action], in_play_idx: usize) -> bool {
actions
.iter()
.any(|action| matches!(action.action, SimpleAction::UseAbility { in_play_idx: idx } if idx == in_play_idx))
}

#[test]
fn test_klefki_dismantling_keys_discards_opponent_active_tool_and_self() {
let rocky_helmet = get_card_by_enum(CardId::A2148RockyHelmet);
let klefki = get_card_by_enum(CardId::B1120Klefki);

let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();
state.current_player = 0;
state.set_board(
vec![
PlayedCard::from_id(CardId::A1001Bulbasaur),
PlayedCard::from_id(CardId::B1120Klefki),
],
vec![PlayedCard::from_id(CardId::A1033Charmander).with_tool(rocky_helmet.clone())],
);
game.set_state(state);

let (actor, actions) = game.get_state_clone().generate_possible_actions();
assert_eq!(actor, 0);
assert!(has_use_ability(&actions, 1));

let ability_action = actions
.iter()
.find(|action| matches!(action.action, SimpleAction::UseAbility { in_play_idx: 1 }))
.expect("Dismantling Keys should be available from the Bench")
.clone();
game.apply_action(&ability_action);

let state = game.get_state_clone();
assert!(state.get_active(1).attached_tool.is_none());
assert!(state.discard_piles[1].contains(&rocky_helmet));
assert!(state.in_play_pokemon[0][1].is_none());
assert!(state.discard_piles[0].contains(&klefki));
assert_eq!(state.points, [0, 0]);
}

#[test]
fn test_klefki_dismantling_keys_requires_bench_and_opponent_active_tool() {
let rocky_helmet = get_card_by_enum(CardId::A2148RockyHelmet);

let mut active_klefki_game = get_initialized_game(0);
let mut active_klefki_state = active_klefki_game.get_state_clone();
active_klefki_state.current_player = 0;
active_klefki_state.set_board(
vec![PlayedCard::from_id(CardId::B1120Klefki)],
vec![PlayedCard::from_id(CardId::A1033Charmander).with_tool(rocky_helmet)],
);
active_klefki_game.set_state(active_klefki_state);

let (_actor, actions) = active_klefki_game
.get_state_clone()
.generate_possible_actions();
assert!(!has_use_ability(&actions, 0));

let mut no_tool_game = get_initialized_game(0);
let mut no_tool_state = no_tool_game.get_state_clone();
no_tool_state.current_player = 0;
no_tool_state.set_board(
vec![
PlayedCard::from_id(CardId::A1001Bulbasaur),
PlayedCard::from_id(CardId::B1120Klefki),
],
vec![PlayedCard::from_id(CardId::A1033Charmander)],
);
no_tool_game.set_state(no_tool_state);

let (_actor, actions) = no_tool_game.get_state_clone().generate_possible_actions();
assert!(!has_use_ability(&actions, 1));
}

#[test]
fn test_klefki_dismantling_keys_resolves_ko_from_lost_giant_cape() {
let giant_cape = get_card_by_enum(CardId::A2147GiantCape);

let mut game = get_initialized_game(0);
let mut state = game.get_state_clone();
state.current_player = 0;
state.turn_count = 3;
state.points = [0, 0];
state.set_board(
vec![
PlayedCard::from_id(CardId::A1001Bulbasaur),
PlayedCard::from_id(CardId::B1120Klefki),
],
vec![
PlayedCard::from_id(CardId::A1001Bulbasaur)
.with_tool(giant_cape.clone())
.with_damage(80),
PlayedCard::from_id(CardId::A1053Squirtle),
],
);
game.set_state(state);

let ability_action = game
.get_state_clone()
.generate_possible_actions()
.1
.into_iter()
.find(|action| matches!(action.action, SimpleAction::UseAbility { in_play_idx: 1 }))
.expect("Dismantling Keys should be available from the Bench");
game.apply_action(&ability_action);

let state = game.get_state_clone();
assert!(state.in_play_pokemon[1][0].is_none());
assert!(state.discard_piles[1].contains(&giant_cape));
assert!(state.in_play_pokemon[0][1].is_none());
assert_eq!(state.points[0], 1);

let (actor, actions) = state.generate_possible_actions();
assert_eq!(actor, 1);
assert!(actions
.iter()
.any(|action| matches!(action.action, SimpleAction::Activate { player: 1, .. })));
}
Loading