diff --git a/spirit/game/card_effects/trainers.py b/spirit/game/card_effects/trainers.py index 02c9942..7df412b 100644 --- a/spirit/game/card_effects/trainers.py +++ b/spirit/game/card_effects/trainers.py @@ -1129,6 +1129,217 @@ def bench_has_room(board, player_id): and len(bench.children) < effective_bench_capacity(board, player_id) +# --- Brandon (SWSH12) ------------------------------------------------------- + +def brandon_playable(board, player_id) -> bool: + hand = board.find_player_area(player_id, "hand") + return bool(hand) and len(hand.children) == 1 + + +async def brandon(ctx): + """Draw a card for each Benched Pokemon (both yours and your opponent's).""" + count = len(_bench_pokemon(ctx.board, ctx.player_id)) \ + + len(_bench_pokemon(ctx.board, ctx.opponent_id)) + if count > 0: + await ctx.draw_cards(count) + + +# --- Candice (SWSH12) -------------------------------------------------------- + +def is_water_energy_card(card) -> bool: + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return is_energy_card(card) and PokemonTypes.WATER.value in types + + +def candice_predicate(card) -> bool: + return is_water_pokemon(card) or is_water_energy_card(card) + + +# --- Capturing Aroma (SWSH12) ------------------------------------------------ + +async def capturing_aroma(ctx): + """Flip a coin: heads searches for an Evolution Pokemon, tails a Basic + Pokemon; reveal it and put it into your hand. Then, shuffle your deck.""" + heads, = await ctx.flip_coins(1, "Capturing Aroma") + predicate = is_evolution_pokemon if heads else is_basic_pokemon + prompt = "Choose an Evolution Pokémon to put into your hand." if heads \ + else "Choose a Basic Pokémon to put into your hand." + picks = await ctx.search_deck(predicate, count=1, minimum=0, prompt=prompt) + await ctx.put_in_hand(picks, reveal=True) + await ctx.shuffle_deck() + + +# --- Earthen Seal Stone (SWSH12) --------------------------------------------- + +async def star_gravity(ctx): + """Put damage counters on each of the opponent's Pokemon V until its + remaining HP is 100 (VSTAR Power).""" + for pokemon in ctx.opponent_pokemon_in_play(): + if not is_pokemon_v(pokemon.archetype_id): + continue + current = pokemon.get_attribute(AttrID.HP, ctx.max_hp(pokemon)) + if current > 100: + await ctx.deal_damage(current - 100, target=pokemon, + apply_modifiers=False, as_counters=True) + + +# --- Emergency Jelly (SWSH12) ------------------------------------------------- + +async def emergency_jelly(ctx): + """End of each turn: if the holder has 30 HP or less remaining and any + damage counters on it, heal 120 damage from it and discard this card.""" + pokemon = ctx.source + hp = pokemon.get_attribute(AttrID.HP, 0) + max_hp = ctx.max_hp(pokemon) + if hp <= 30 and hp < max_hp: + await ctx.heal(120, target=pokemon) + tool = next((t for t, p in ctx.tools_in_play() if p is pokemon), None) + if tool is not None: + await ctx.discard_cards([tool]) + + +# --- Furisode Girl (SWSH12) --------------------------------------------------- + +async def furisode_girl(ctx): + """Search the deck for a Basic Pokemon and put it onto the Bench; then + shuffle. You may switch that Pokemon with your Active Pokemon.""" + picks = await ctx.search_deck( + is_basic_pokemon, count=1, minimum=0, + prompt="Choose a Basic Pokémon to put onto your Bench.", + ) + if not picks: + await ctx.shuffle_deck() + return + target = picks[0] + await ctx.bench_pokemon(target) + await ctx.shuffle_deck() + if await ctx.ask_yes_no("Switch that Pokémon with your Active Pokémon?"): + await ctx.switch_active(ctx.player_id, target) + + +# --- Lance (SWSH12) ----------------------------------------------------------- + +def is_dragon_pokemon(card) -> bool: + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return is_pokemon_card(card) and PokemonTypes.DRAGON.value in types + + +async def lance(ctx): + """Search the deck for up to 3 Dragon Pokemon, reveal them, and put them + into your hand. Then, shuffle your deck.""" + picks = await ctx.search_deck( + is_dragon_pokemon, count=3, minimum=0, + prompt="Choose up to 3 Dragon Pokémon to put into your hand.", + ) + await ctx.put_in_hand(picks, reveal=True) + await ctx.shuffle_deck() + + +# --- Leafy Camo Poncho (SWSH12) ----------------------------------------------- + +def leafy_camo_poncho_protects(affected_entity, carrier): + holder = carrier_pokemon(carrier) + return holder is not None and affected_entity is holder + + +def leafy_camo_poncho_condition(board, carrier): + holder = carrier_pokemon(carrier) + if holder is None: + return False + subs = subtypes_for(holder.archetype_id) + return "VSTAR" in subs or "VMAX" in subs + + +# --- Primordial Altar (SWSH12) ------------------------------------------------ + +def primordial_altar_condition(board, player_id, stadium): + return deck_nonempty(board, player_id) + + +async def primordial_altar(ctx): + """Once during each player's turn: look at the top card of the deck and + may discard it.""" + top = ctx.deck_top(1) + if not top: + return + card = top[0] + idx = await ctx.present_card_choice( + card, "Discard the top card of your deck?", + ["Discard", "Keep it on top"], + ) + if idx == 0: + await ctx.discard_cards([card]) + + +PRIMORDIAL_ALTAR_ABILITY = Ability( + title="Primordial Altar", + game_text="Once during each player's turn, that player may look at the top card of their deck. They may discard that card.", + activation=Activations.ONCE_PER_TURN, + effect=primordial_altar, + condition=primordial_altar_condition, +) + + +# --- Professor Laventon (SWSH12) ---------------------------------------------- + +def _is_hisuian_pokemon(card) -> bool: + if not is_pokemon_card(card): + return False + name = getattr(def_for(card.archetype_id), "display_name", "") or "" + return "Hisuian" in name + + +def professor_laventon_playable(board, player_id) -> bool: + return any(_is_hisuian_pokemon(c) for c in _discard(board, player_id)) + + +async def professor_laventon(ctx): + """Put up to 3 Pokemon that have "Hisuian" in their names from the + discard pile into your hand.""" + candidates = [c for c in ctx.discard_pile() if _is_hisuian_pokemon(c)] + picks = await ctx.choose_cards( + candidates, 3, minimum=0, + prompt="Choose up to 3 Hisuian Pokémon from your discard pile.", + ) + await ctx.put_in_hand(picks, reveal=False) + + +# --- Quad Stone (SWSH12) ------------------------------------------------------- + +async def quad_stone(ctx): + """Use 1: heal 10 from your Active. Use 4 at once (this + 3 more from + hand): heal all damage from each of your Pokemon.""" + others = [c for c in ctx.hand() + if c is not ctx.source and c.archetype_id == ctx.source.archetype_id] + use_four = False + if len(others) >= 3 and await ctx.ask_yes_no( + "Use 3 more Quad Stone cards from your hand to heal all damage from " + "each of your Pokémon instead of healing 10 from your Active Pokémon?" + ): + picks = await ctx.choose_cards( + others, 3, minimum=3, prompt="Choose 3 more Quad Stone cards to use", + ) + if len(picks) >= 3: + await ctx.discard_cards(picks) + for pokemon in ctx.my_pokemon_in_play(): + await ctx.heal(9999, pokemon) + use_four = True + if not use_four: + active = ctx.my_active() + if active is not None: + await ctx.heal(10, active) + + +# --- Wallace (SWSH12) ---------------------------------------------------------- + +async def wallace(ctx): + """Draw 3 cards. Your opponent may draw a card; if they do, draw 1 more.""" + await ctx.draw_cards(3) + if await ctx.ask_yes_no("Draw a card?", player_id=ctx.opponent_id): + await ctx.draw_cards(1, ctx.opponent_id) + await ctx.draw_cards(1) + + def fossil_search(fossil_predicate, count: int = 2, label: str = "Rare Fossil"): """Search your deck for up to `count` matching fossil cards and put them diff --git a/spirit/game/data_utils.py b/spirit/game/data_utils.py index b413c36..9edd71a 100644 --- a/spirit/game/data_utils.py +++ b/spirit/game/data_utils.py @@ -376,7 +376,8 @@ class PokemonCardDef(CardDefinition): subtypes: Optional[List[str]] = None, attributes: Optional[dict] = None, passive: Optional[Any] = None, - unplayable_from_hand: bool = False + unplayable_from_hand: bool = False, + setup_as_active: bool = False ): super().__init__(guid, key, name, collector_number, set_code, rarity, display_name, searchable_by, subtypes, attributes) # Card-level continuous effect while this Pokemon is top-level in play @@ -384,6 +385,9 @@ class PokemonCardDef(CardDefinition): self.passive = passive # Shedinja: never offered as a hand bench-play (enters play by effect). self.unplayable_from_hand = unplayable_from_hand + # Luxray CZ's Explosiveness: may be placed as the opening Active + # despite its stage (setup only; bench plays stay Basics-only). + self.setup_as_active = setup_as_active # Add Pokemon-specific defaults to extra_attributes self.extra_attributes.update({ diff --git a/spirit/game/models/board.py b/spirit/game/models/board.py index 8ca6055..2802a94 100644 --- a/spirit/game/models/board.py +++ b/spirit/game/models/board.py @@ -487,16 +487,34 @@ class BoardState: """ return bool(self.basic_pokemon_in_hand(player_id)) + def setup_active_candidates(self, player_id: str) -> List['PokemonEntity']: + """Cards playable as the opening Active: Basics first, then hand + Pokemon whose def sets setup_as_active (Luxray CZ's Explosiveness). + The bench offer and mid-game bench plays stay Basics-only.""" + from spirit.game.data_utils import def_for # circular-import guard + candidates = self.basic_pokemon_in_hand(player_id) + hand_area = self.find_player_area(player_id, "hand") + for c in (hand_area.children if hand_area else []): + if isinstance(c, PokemonEntity) and c not in candidates \ + and getattr(def_for(c.archetype_id), "setup_as_active", False): + candidates.append(c) + return candidates + def player_has_any_basic(self, player_id: str) -> bool: """True if the player has a Basic Pokemon anywhere in deck or hand. Guards the mulligan loop against decks that can never produce a legal opening hand (which would otherwise reshuffle forever). """ + from spirit.game.data_utils import def_for # circular-import guard for area_name in ("deck", "hand"): area = self.find_player_area(player_id, area_name) - if area and any(self._is_basic_pokemon(c) for c in area.children): - return True + for c in (area.children if area else []): + if self._is_basic_pokemon(c): + return True + if isinstance(c, PokemonEntity) \ + and getattr(def_for(c.archetype_id), "setup_as_active", False): + return True return False def pokemon_in_play(self, player_id: str) -> List['PokemonEntity']: diff --git a/spirit/game/scripts/cards/BW1/Potion_100.py b/spirit/game/scripts/cards/BW1/Potion_100.py index 960bf90..1f8aead 100644 --- a/spirit/game/scripts/cards/BW1/Potion_100.py +++ b/spirit/game/scripts/cards/BW1/Potion_100.py @@ -1,5 +1,6 @@ from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import heal_item, requires_damaged_pokemon card = ItemCardDef( guid="d73ca4da-dd21-f428-8051-264ab564587c", @@ -7,5 +8,7 @@ card = ItemCardDef( name="com.direwolfdigital.cake.data.archetypes.trainer.Potion.Name", collector_number=100, set_code="BW1", - rarity=Rarities.Common + rarity=Rarities.Common, + condition=requires_damaged_pokemon(), + effect=heal_item(30) ) diff --git a/spirit/game/scripts/cards/CZ/Absol_76.py b/spirit/game/scripts/cards/CZ/Absol_76.py index 1892d5c..5923c6d 100644 --- a/spirit/game/scripts/cards/CZ/Absol_76.py +++ b/spirit/game/scripts/cards/CZ/Absol_76.py @@ -1,6 +1,17 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +import random + +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def lost_claw(ctx): + """70. Put a random card from your opponent's hand in the Lost Zone.""" + await ctx.deal_damage() + hand = ctx.hand(ctx.opponent_id) + if hand: + await ctx.move_to_lost_zone([random.choice(hand)]) + + card = PokemonCardDef( guid="b8a98898-af7d-52a3-a682-ab6a0ff76dae", key="CZ", @@ -28,7 +39,7 @@ card = PokemonCardDef( game_text="Put a random card from your opponent's hand in the Lost Zone.", cost={PokemonTypes.DARKNESS: 1, PokemonTypes.COLORLESS: 2}, damage=70, - effect=unimplemented, + effect=lost_claw, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Aggron_89.py b/spirit/game/scripts/cards/CZ/Aggron_89.py index 9454b85..0b9c269 100644 --- a/spirit/game/scripts/cards/CZ/Aggron_89.py +++ b/spirit/game/scripts/cards/CZ/Aggron_89.py @@ -1,5 +1,29 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.session.passives import Passive + + +class CounterPressPassive(Passive): + """Reflects the damage this Pokemon takes onto the attacker as counters.""" + + async def damage_interceptor(self, ctx, calc, target, carrier): + if not (calc.is_attack and calc.is_opposing and calc.amount > 0): + return None + if target is not carrier: + return None + attacker = calc.attacker + if attacker is not None: + await ctx.deal_damage(calc.amount, target=attacker, + apply_modifiers=False, as_counters=True) + return None + + +async def counter_press(ctx): + """90. During your opponent's next turn, a hit on this Pokemon reflects + as damage counters on the attacker (even if this Pokemon is KO'd).""" + await ctx.deal_damage() + ctx.add_passive_through_opponents_turn(ctx.attacker, CounterPressPassive()) + card = PokemonCardDef( guid="ede06640-e9fd-56bb-bcf4-a84577370ce7", @@ -25,7 +49,7 @@ card = PokemonCardDef( game_text="During your opponent's next turn, if this Pok\u00e9mon is damaged by an attack (even if this Pok\u00e9mon is Knocked Out), put damage counters on the Attacking Pok\u00e9mon equal to the damage done to this Pok\u00e9mon.", cost={PokemonTypes.METAL: 1, PokemonTypes.COLORLESS: 2}, damage=90, - effect=unimplemented, + effect=counter_press, ), Attack( title="Heavy Impact", diff --git a/spirit/game/scripts/cards/CZ/Aron_87.py b/spirit/game/scripts/cards/CZ/Aron_87.py index 2f3acb6..3166114 100644 --- a/spirit/game/scripts/cards/CZ/Aron_87.py +++ b/spirit/game/scripts/cards/CZ/Aron_87.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack card = PokemonCardDef( guid="66eca74a-2caf-5210-a324-f4b2aa277233", @@ -29,7 +30,7 @@ card = PokemonCardDef( game_text="This Pok\u00e9mon also does 10 damage to itself.", cost={PokemonTypes.COLORLESS: 2}, damage=30, - effect=unimplemented, + effect=recoil_attack(10), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Bea_123.py b/spirit/game/scripts/cards/CZ/Bea_123.py index 1f8b916..8f269a8 100644 --- a/spirit/game/scripts/cards/CZ/Bea_123.py +++ b/spirit/game/scripts/cards/CZ/Bea_123.py @@ -1,5 +1,27 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented -from spirit.game.attributes import Rarities +from spirit.game.data_utils import SupporterCardDef +from spirit.game.attributes import AttrID, PokemonTypes, Rarities +from spirit.game.card_effects.trainers import is_energy_card +from spirit.game.card_effects.support_common import distribute_energy + + +def _is_fighting_pokemon(pokemon): + types = pokemon.get_attribute(AttrID.POKEMON_TYPES) or [] + return PokemonTypes.FIGHTING.value in types + + +async def bea(ctx): + """Discard the top 5 of your deck; attach any Energy discarded this way + to your Benched Fighting Pokemon in any way you like.""" + cards = ctx.deck_top(5) + await ctx.discard_cards(cards) + energies = [c for c in cards if is_energy_card(c)] + if not energies: + return + candidates = [p for p in ctx.my_bench() if _is_fighting_pokemon(p)] + if not candidates: + return + await distribute_energy(ctx, energies, candidates) + card = SupporterCardDef( guid="5c6b91c9-4040-5658-81f8-ec1351d97293", @@ -11,5 +33,5 @@ card = SupporterCardDef( collector_number=123, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + effect=bea ) diff --git a/spirit/game/scripts/cards/CZ/Bede_124.py b/spirit/game/scripts/cards/CZ/Bede_124.py index 2bffe0a..a91e5cb 100644 --- a/spirit/game/scripts/cards/CZ/Bede_124.py +++ b/spirit/game/scripts/cards/CZ/Bede_124.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.card_effects.trainers import bede, bede_playable +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities card = SupporterCardDef( @@ -11,5 +12,6 @@ card = SupporterCardDef( collector_number=124, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + effect=bede, + condition=bede_playable ) diff --git a/spirit/game/scripts/cards/CZ/Bellossom_3.py b/spirit/game/scripts/cards/CZ/Bellossom_3.py index 58dea3a..4b53f30 100644 --- a/spirit/game/scripts/cards/CZ/Bellossom_3.py +++ b/spirit/game/scripts/cards/CZ/Bellossom_3.py @@ -1,5 +1,16 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage + + +async def _switch_self(ctx): + bench = ctx.my_bench() + if not bench: + return + target = await ctx.choose_pokemon(bench, "Choose your new Active Pokémon") + if target is not None: + await ctx.switch_active(ctx.player_id, target) + card = PokemonCardDef( guid="d57c93dd-02e6-5ad1-9b55-7ea441dd4e43", @@ -25,7 +36,7 @@ card = PokemonCardDef( cost={PokemonTypes.GRASS: 1, PokemonTypes.COLORLESS: 1}, damage=80, damage_operator="x", - effect=unimplemented, + effect=flip_damage(coins=3, per_heads=80, also=_switch_self), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Bidoof_111.py b/spirit/game/scripts/cards/CZ/Bidoof_111.py index e596a59..ae9269b 100644 --- a/spirit/game/scripts/cards/CZ/Bidoof_111.py +++ b/spirit/game/scripts/cards/CZ/Bidoof_111.py @@ -1,5 +1,11 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_or_nothing +from spirit.game.card_effects.passives_common import prevent_damage_when + + +def _carefree_pred(calc, carrier): + return calc.is_attack and calc.target is carrier and not calc.to_active card = PokemonCardDef( guid="cc5cc393-6005-5fc0-8114-e5d41ba4c6e4", @@ -21,14 +27,14 @@ card = PokemonCardDef( Ability( title="Carefree Countenance", game_text="As long as this Pok\u00e9mon is on your Bench, prevent all damage done to this Pok\u00e9mon by attacks (both yours and your opponent's).", - effect=unimplemented, + passive=prevent_damage_when(_carefree_pred, attacks_only=False), ), Attack( title="Hyper Fang", game_text="Flip a coin. If tails, this attack does nothing.", cost={PokemonTypes.COLORLESS: 2}, damage=30, - effect=unimplemented, + effect=flip_or_nothing(), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Bisharp_93.py b/spirit/game/scripts/cards/CZ/Bisharp_93.py index 3ce745c..9614f98 100644 --- a/spirit/game/scripts/cards/CZ/Bisharp_93.py +++ b/spirit/game/scripts/cards/CZ/Bisharp_93.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import draw_attack card = PokemonCardDef( guid="550b78f5-3e2a-50ce-8353-d1a1191b90a9", @@ -25,7 +26,7 @@ card = PokemonCardDef( game_text="Draw 2 cards.", cost={PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=draw_attack(2), ), Attack( title="Power Edge", diff --git a/spirit/game/scripts/cards/CZ/Calyrex_17.py b/spirit/game/scripts/cards/CZ/Calyrex_17.py index 39f5dff..c712769 100644 --- a/spirit/game/scripts/cards/CZ/Calyrex_17.py +++ b/spirit/game/scripts/cards/CZ/Calyrex_17.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import search_to_hand, heal_targets card = PokemonCardDef( guid="9656d298-ea31-505e-adbd-b6bb0bc42a6c", @@ -23,14 +24,14 @@ card = PokemonCardDef( game_text="You may search your deck for up to 2 cards and put them into your hand. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 2}, damage=30, - effect=unimplemented, + effect=search_to_hand(count=2, minimum=0, reveal=False), ), Attack( title="Bloomshine", game_text="Heal 20 damage from each of your Pok\u00e9mon.", cost={PokemonTypes.GRASS: 1, PokemonTypes.COLORLESS: 2}, damage=90, - effect=unimplemented, + effect=heal_targets(20, scope="each_own"), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Carnivine_12.py b/spirit/game/scripts/cards/CZ/Carnivine_12.py index 1e3a4f2..dc5d2fb 100644 --- a/spirit/game/scripts/cards/CZ/Carnivine_12.py +++ b/spirit/game/scripts/cards/CZ/Carnivine_12.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="d41fce1f-9097-555b-b507-4504bf9d6d06", @@ -22,14 +23,14 @@ card = PokemonCardDef( title="Festering Saliva", game_text="Your opponent's Active Pok\u00e9mon is now Burned and Poisoned.", cost={PokemonTypes.GRASS: 1}, - effect=unimplemented, + effect=condition_attack(SpecialConditions.BURNED, SpecialConditions.POISONED), ), Attack( title="Bind Down", game_text="During your opponent's next turn, the Defending Pok\u00e9mon can't retreat.", cost={PokemonTypes.COLORLESS: 2}, damage=40, - effect=unimplemented, + effect=condition_attack(no_retreat=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/CharizardVSTAR_19.py b/spirit/game/scripts/cards/CZ/CharizardVSTAR_19.py index fa128a4..3c1f761 100644 --- a/spirit/game/scripts/cards/CZ/CharizardVSTAR_19.py +++ b/spirit/game/scripts/cards/CZ/CharizardVSTAR_19.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, has_damage, self_energy_discard_attack card = PokemonCardDef( guid="ffd5fc1c-42d7-5ff9-9140-707e461a7d8a", @@ -25,14 +26,15 @@ card = PokemonCardDef( cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1}, damage=130, damage_operator="+", - effect=unimplemented, + effect=bonus_if(has_damage("self"), 100), ), Attack( title="Star Blaze", game_text="Discard 2 Energy from this Pok\u00e9mon. (You can't use more than 1 VSTAR Power in a game.)", cost={PokemonTypes.FIRE: 3, PokemonTypes.COLORLESS: 1}, damage=320, - effect=unimplemented, + vstar=True, + effect=self_energy_discard_attack(count=2), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/CharizardV_18.py b/spirit/game/scripts/cards/CZ/CharizardV_18.py index fb9f7ab..bbf19cd 100644 --- a/spirit/game/scripts/cards/CZ/CharizardV_18.py +++ b/spirit/game/scripts/cards/CZ/CharizardV_18.py @@ -1,6 +1,17 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def incinerate(ctx): + """Before doing damage, discard all Tools from the opponent's Active.""" + defender = ctx.defender + if defender is not None and not ctx.effects_blocked(defender): + tools = [t for t, p in ctx.tools_in_play() if p is defender] + if tools: + await ctx.discard_cards(tools) + await ctx.deal_damage() + + card = PokemonCardDef( guid="7c859d59-2894-5783-8c83-60fb3049e133", key="CZ", @@ -23,7 +34,7 @@ card = PokemonCardDef( game_text="Before doing damage, discard all Pok\u00e9mon Tools from your opponent's Active Pok\u00e9mon.", cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1}, damage=90, - effect=unimplemented, + effect=incinerate, ), Attack( title="Heat Blast", diff --git a/spirit/game/scripts/cards/CZ/Chatot_112.py b/spirit/game/scripts/cards/CZ/Chatot_112.py index ab4349b..7ba3619 100644 --- a/spirit/game/scripts/cards/CZ/Chatot_112.py +++ b/spirit/game/scripts/cards/CZ/Chatot_112.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import discard_then_draw card = PokemonCardDef( guid="f1fbc56e-ecba-5831-94b7-573419e85f04", @@ -23,7 +24,7 @@ card = PokemonCardDef( title="Cycle Draw", game_text="Discard a card from your hand. If you do, draw 2 cards.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=discard_then_draw(1, 2, optional=True), ), Attack( title="Flap", diff --git a/spirit/game/scripts/cards/CZ/CrushingHammer_125.py b/spirit/game/scripts/cards/CZ/CrushingHammer_125.py index 74aa3d7..050584b 100644 --- a/spirit/game/scripts/cards/CZ/CrushingHammer_125.py +++ b/spirit/game/scripts/cards/CZ/CrushingHammer_125.py @@ -1,5 +1,21 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.trainers import opponent_has_energy_attached + + +async def crushing_hammer(ctx): + """Flip a coin. If heads, discard an Energy from 1 of your opponent's Pokémon.""" + results = await ctx.flip_coins(1, "Crushing Hammer") + if not results or not results[0]: + return + candidates = [p for p in ctx.opponent_pokemon_in_play() if ctx.attached_energies(p)] + if not candidates: + return + target = await ctx.choose_pokemon(candidates, "Choose 1 of your opponent's Pokémon") + if target is None: + return + await ctx.discard_energy_from(target, 1) + card = ItemCardDef( guid="fdc218bf-196c-5954-bb04-bca1ec80a911", @@ -11,5 +27,6 @@ card = ItemCardDef( collector_number=125, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=crushing_hammer, + condition=opponent_has_energy_attached ) diff --git a/spirit/game/scripts/cards/CZ/DiggingDuo_126.py b/spirit/game/scripts/cards/CZ/DiggingDuo_126.py index c01b855..b4694ce 100644 --- a/spirit/game/scripts/cards/CZ/DiggingDuo_126.py +++ b/spirit/game/scripts/cards/CZ/DiggingDuo_126.py @@ -1,6 +1,19 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities + +async def digging_duo(ctx): + """Flip a coin: heads look at bottom 8, tails bottom 3; put 1 into hand.""" + heads, = await ctx.flip_coins(1, "Digging Duo") + bottom = ctx.deck()[:8 if heads else 3] + if bottom: + picks = await ctx.choose_cards( + bottom, 1, prompt="Choose a card to put into your hand.", + ) + await ctx.put_in_hand(picks, reveal=False) + await ctx.shuffle_deck() + + card = SupporterCardDef( guid="bba06028-290f-5617-8d42-9f4927565f2f", key="CZ", @@ -11,5 +24,5 @@ card = SupporterCardDef( collector_number=126, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=digging_duo, ) diff --git a/spirit/game/scripts/cards/CZ/Ditto_107.py b/spirit/game/scripts/cards/CZ/Ditto_107.py index 95e7329..8d7b13b 100644 --- a/spirit/game/scripts/cards/CZ/Ditto_107.py +++ b/spirit/game/scripts/cards/CZ/Ditto_107.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.pokemon import SuddenTransformationPassive card = PokemonCardDef( guid="d561efa6-d36b-5004-b823-04531ff6bbdb", @@ -21,7 +22,7 @@ card = PokemonCardDef( Ability( title="Sudden Transformation", game_text="This Pok\u00e9mon can use the attacks of any Basic Pok\u00e9mon in your discard pile, except for Pok\u00e9mon with a Rule Box (Pok\u00e9mon V, Pok\u00e9mon-GX, etc. have Rule Boxes). (You still need the necessary Energy to use each attack.)", - effect=unimplemented, + passive=SuddenTransformationPassive(), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Dragalge_82.py b/spirit/game/scripts/cards/CZ/Dragalge_82.py index 498012b..dce0b09 100644 --- a/spirit/game/scripts/cards/CZ/Dragalge_82.py +++ b/spirit/game/scripts/cards/CZ/Dragalge_82.py @@ -1,5 +1,19 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions + + +def _evolved_this_turn(ctx) -> bool: + state = ctx.session.turn_state + return state.entered_play_turn.get(ctx.attacker.entity_id) == state.turn_number + + +async def rocket_poison(ctx): + """Your opponent's Active Pokémon is now Poisoned. If this Pokémon + evolved from Skrelp during this turn, put 8 damage counters instead of 1.""" + counters = 8 if _evolved_this_turn(ctx) else 1 + await ctx.apply_special_condition( + ctx.defender, SpecialConditions.POISONED, poison_counters=counters) + card = PokemonCardDef( guid="5b049f74-2e13-5394-bb2d-7ea832d36233", @@ -23,7 +37,7 @@ card = PokemonCardDef( title="Rocket Poison", game_text="Your opponent's Active Pok\u00e9mon is now Poisoned. If this Pok\u00e9mon evolved from Skrelp during this turn, put 8 damage counters on that Pok\u00e9mon instead of 1 during Pok\u00e9mon Checkup.", cost={PokemonTypes.DARKNESS: 1}, - effect=unimplemented, + effect=rocket_poison, ), Attack( title="Razor Fin", diff --git a/spirit/game/scripts/cards/CZ/Dubwool_122.py b/spirit/game/scripts/cards/CZ/Dubwool_122.py index f5a8aa5..a03698b 100644 --- a/spirit/game/scripts/cards/CZ/Dubwool_122.py +++ b/spirit/game/scripts/cards/CZ/Dubwool_122.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage, snipe_attack card = PokemonCardDef( guid="78aee80e-9456-52a3-b2e2-55268181d44b", @@ -24,7 +25,7 @@ card = PokemonCardDef( game_text="This attack also does 10 damage to 1 of your Benched Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.COLORLESS: 1}, damage=40, - effect=unimplemented, + effect=snipe_attack(10, pool="bench", count=1, side="mine", also_base=True), ), Attack( title="Rolling Dash", @@ -32,7 +33,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 2}, damage=60, damage_operator="+", - effect=unimplemented, + effect=flip_damage(until_tails=True, base=60, per_heads=30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/DuraludonVMAX_104.py b/spirit/game/scripts/cards/CZ/DuraludonVMAX_104.py index 7e2287b..a2f1d37 100644 --- a/spirit/game/scripts/cards/CZ/DuraludonVMAX_104.py +++ b/spirit/game/scripts/cards/CZ/DuraludonVMAX_104.py @@ -1,5 +1,22 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.passives_common import prevent_damage_when +from spirit.game.models.board import BoardState +from spirit.game.session.effects import is_special_energy + + +def _skyscraper_pred(calc, carrier): + if calc.target is not carrier: + return False + attacker = calc.attacker + return attacker is not None and any( + is_special_energy(e) for e in BoardState.attached_energies(attacker) + ) + + +async def g_max_pulverization(ctx): + await ctx.deal_damage(ignore_target_effects=True) + card = PokemonCardDef( guid="2b92d3d9-e5bd-5026-88c1-3a99f9c8eb82", @@ -21,14 +38,14 @@ card = PokemonCardDef( Ability( title="Skyscraper", game_text="Prevent all damage done to this Pok\u00e9mon by attacks from your opponent's Pok\u00e9mon that have Special Energy attached.", - effect=unimplemented, + passive=prevent_damage_when(_skyscraper_pred), ), Attack( title="G-Max Pulverization", game_text="This attack's damage isn't affected by any effects on your opponent's Active Pok\u00e9mon.", cost={PokemonTypes.FIGHTING: 1, PokemonTypes.METAL: 2}, damage=220, - effect=unimplemented, + effect=g_max_pulverization, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/DuraludonV_103.py b/spirit/game/scripts/cards/CZ/DuraludonV_103.py index 1f3e772..711df0f 100644 --- a/spirit/game/scripts/cards/CZ/DuraludonV_103.py +++ b/spirit/game/scripts/cards/CZ/DuraludonV_103.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.passives_common import debuff_defender_attacks card = PokemonCardDef( guid="87d64657-cb5b-58a6-82c6-11ff214e6cb1", @@ -27,7 +28,7 @@ card = PokemonCardDef( game_text="During your opponent's next turn, the Defending Pok\u00e9mon's attacks do 30 less damage (before applying Weakness and Resistance).", cost={PokemonTypes.FIGHTING: 1, PokemonTypes.METAL: 2}, damage=140, - effect=unimplemented, + effect=debuff_defender_attacks(30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Dusclops_63.py b/spirit/game/scripts/cards/CZ/Dusclops_63.py index 849342e..a44319a 100644 --- a/spirit/game/scripts/cards/CZ/Dusclops_63.py +++ b/spirit/game/scripts/cards/CZ/Dusclops_63.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="fc815258-d645-5146-95f7-6a78443cd444", @@ -25,7 +26,7 @@ card = PokemonCardDef( game_text="Your opponent's Active Pok\u00e9mon is now Confused.", cost={PokemonTypes.PSYCHIC: 1}, damage=30, - effect=unimplemented, + effect=condition_attack(SpecialConditions.CONFUSED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Eelektrik_48.py b/spirit/game/scripts/cards/CZ/Eelektrik_48.py index 1eed6f1..f985f8b 100644 --- a/spirit/game/scripts/cards/CZ/Eelektrik_48.py +++ b/spirit/game/scripts/cards/CZ/Eelektrik_48.py @@ -1,5 +1,17 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Triggers +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions + + +async def ad_hoc_shock(ctx): + """On evolve: you may flip a coin. If heads, the opponent's Active is now Paralyzed.""" + if not await ctx.ask_yes_no("Flip a coin?"): + return + results = await ctx.flip_coins(1, "Ad Hoc Shock") + if not results or not results[0]: + return + defender = ctx.defender + if defender is not None: + await ctx.apply_special_condition(defender, SpecialConditions.PARALYZED) card = PokemonCardDef( guid="6806e5e1-1032-5db4-8978-3841d7bd6a7d", @@ -22,7 +34,8 @@ card = PokemonCardDef( Ability( title="Ad Hoc Shock", game_text="When you play this Pok\u00e9mon from your hand to evolve 1 of your Pok\u00e9mon during your turn, you may flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.", - effect=unimplemented, + trigger=Triggers.ON_EVOLVE, + effect=ad_hoc_shock, ), Attack( title="Static Shock", diff --git a/spirit/game/scripts/cards/CZ/EeveeV_108.py b/spirit/game/scripts/cards/CZ/EeveeV_108.py index 72e7ad4..4b8f0ae 100644 --- a/spirit/game/scripts/cards/CZ/EeveeV_108.py +++ b/spirit/game/scripts/cards/CZ/EeveeV_108.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, defender_is_v card = PokemonCardDef( guid="1e67d21d-d7f8-5954-b35d-c208ee2eb6e0", @@ -29,7 +30,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 3}, damage=80, damage_operator="+", - effect=unimplemented, + effect=bonus_if(defender_is_v, 80), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ElesasSparkle_147.py b/spirit/game/scripts/cards/CZ/ElesasSparkle_147.py index c311e92..860c84b 100644 --- a/spirit/game/scripts/cards/CZ/ElesasSparkle_147.py +++ b/spirit/game/scripts/cards/CZ/ElesasSparkle_147.py @@ -1,5 +1,34 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef, subtypes_for from spirit.game.attributes import Rarities +from spirit.game.card_effects.trainers import is_energy_card + + +def _is_fusion_strike_energy(card): + return is_energy_card(card) and "Fusion Strike" in subtypes_for(card.archetype_id) + + +async def elesas_sparkle(ctx): + """Choose up to 2 of your Fusion Strike Pokemon. For each of those + Pokemon, search your deck for a Fusion Strike Energy card and attach it + to that Pokemon. Then, shuffle your deck.""" + candidates = [ + p for p in ctx.my_pokemon_in_play() + if "Fusion Strike" in subtypes_for(p.archetype_id) + ] + if candidates: + targets = await ctx.choose_cards( + candidates, 2, minimum=0, + prompt="Choose up to 2 of your Fusion Strike Pokémon.", + ) + for pokemon in targets: + picks = await ctx.search_deck( + _is_fusion_strike_energy, count=1, minimum=0, + prompt="Choose a Fusion Strike Energy card to attach.", + ) + for card in picks: + await ctx.attach_energy(card, pokemon) + await ctx.shuffle_deck() + card = SupporterCardDef( guid="c616a72f-74b1-5f40-bbbc-6337bdcf607c", @@ -11,5 +40,5 @@ card = SupporterCardDef( collector_number=147, set_code="CZ", rarity=Rarities.RareUltra, - effect=unimplemented + effect=elesas_sparkle ) diff --git a/spirit/game/scripts/cards/CZ/Emolga_47.py b/spirit/game/scripts/cards/CZ/Emolga_47.py index 2a47f3c..25dd49b 100644 --- a/spirit/game/scripts/cards/CZ/Emolga_47.py +++ b/spirit/game/scripts/cards/CZ/Emolga_47.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import condition_attack +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions card = PokemonCardDef( guid="3b51afab-da44-58d4-935d-a30e014ed572", @@ -23,7 +24,7 @@ card = PokemonCardDef( game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.", cost={PokemonTypes.LIGHTNING: 1}, damage=30, - effect=unimplemented, + effect=condition_attack(SpecialConditions.PARALYZED, flip=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Enamorus_67.py b/spirit/game/scripts/cards/CZ/Enamorus_67.py index cb4df81..3e8ea95 100644 --- a/spirit/game/scripts/cards/CZ/Enamorus_67.py +++ b/spirit/game/scripts/cards/CZ/Enamorus_67.py @@ -1,5 +1,17 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if + + +async def draining_kiss(ctx): + """20. Heal 20 damage from this Pokémon.""" + await ctx.deal_damage() + await ctx.heal(20, ctx.attacker) + + +def _same_hand_size(ctx): + return ctx.hand_size() == ctx.hand_size(ctx.opponent_id) + card = PokemonCardDef( guid="fa3103ab-be3e-5b66-a8f1-ea0e4e94d521", @@ -23,7 +35,7 @@ card = PokemonCardDef( game_text="Heal 20 damage from this Pok\u00e9mon.", cost={PokemonTypes.PSYCHIC: 1}, damage=20, - effect=unimplemented, + effect=draining_kiss, ), Attack( title="Loving Sympathy", @@ -31,7 +43,7 @@ card = PokemonCardDef( cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 2}, damage=70, damage_operator="+", - effect=unimplemented, + effect=bonus_if(_same_hand_size, 70), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/EnergyRetrieval_127.py b/spirit/game/scripts/cards/CZ/EnergyRetrieval_127.py index 4749f08..73fabde 100644 --- a/spirit/game/scripts/cards/CZ/EnergyRetrieval_127.py +++ b/spirit/game/scripts/cards/CZ/EnergyRetrieval_127.py @@ -1,4 +1,6 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.support_common import recover_from_discard, requires_discard +from spirit.game.card_effects.trainers import is_basic_energy_card +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities card = ItemCardDef( @@ -11,5 +13,6 @@ card = ItemCardDef( collector_number=127, set_code="CZ", rarity=Rarities.Common, - effect=unimplemented + condition=requires_discard(is_basic_energy_card), + effect=recover_from_discard(is_basic_energy_card, count=2, minimum=1, to="hand"), ) diff --git a/spirit/game/scripts/cards/CZ/EnergySearch_128.py b/spirit/game/scripts/cards/CZ/EnergySearch_128.py index 70be7eb..fbe7e40 100644 --- a/spirit/game/scripts/cards/CZ/EnergySearch_128.py +++ b/spirit/game/scripts/cards/CZ/EnergySearch_128.py @@ -1,4 +1,6 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.support_common import search_to_hand +from spirit.game.card_effects.trainers import is_basic_energy_card +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities card = ItemCardDef( @@ -11,5 +13,6 @@ card = ItemCardDef( collector_number=128, set_code="CZ", rarity=Rarities.Common, - effect=unimplemented + effect=search_to_hand(is_basic_energy_card, count=1, minimum=0, reveal=True, + prompt="Choose a basic Energy card to put into your hand."), ) diff --git a/spirit/game/scripts/cards/CZ/EnergySwitch_129.py b/spirit/game/scripts/cards/CZ/EnergySwitch_129.py index 8bd0fe2..381d989 100644 --- a/spirit/game/scripts/cards/CZ/EnergySwitch_129.py +++ b/spirit/game/scripts/cards/CZ/EnergySwitch_129.py @@ -1,6 +1,21 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.trainers import is_basic_energy_card +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities + +def energy_switch_condition(board, player_id): + pokemon = board.pokemon_in_play(player_id) + if len(pokemon) < 2: + return False + return any(any(is_basic_energy_card(e) for e in p.children) for p in pokemon) + + +async def energy_switch(ctx): + """Move a basic Energy from 1 of your Pokemon to another of your Pokemon.""" + pokemon = ctx.my_pokemon_in_play() + await ctx.move_energy_freely(pokemon, pokemon, predicate=is_basic_energy_card, max_count=1) + + card = ItemCardDef( guid="654fda60-9a95-54fd-9f0d-a4df9c7d55a6", key="CZ", @@ -11,5 +26,6 @@ card = ItemCardDef( collector_number=129, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + condition=energy_switch_condition, + effect=energy_switch, ) diff --git a/spirit/game/scripts/cards/CZ/Entei_21.py b/spirit/game/scripts/cards/CZ/Entei_21.py index 8a0e834..644b64e 100644 --- a/spirit/game/scripts/cards/CZ/Entei_21.py +++ b/spirit/game/scripts/cards/CZ/Entei_21.py @@ -1,5 +1,16 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.passives_common import retreat_free_when +from spirit.game.card_effects.pokemon import energy_provides_type +from spirit.game.models.board import BoardState + + +def _has_fire_energy(pokemon, carrier): + return pokemon is carrier and any( + energy_provides_type(e, PokemonTypes.FIRE.value) + for e in BoardState.attached_energies(pokemon) + ) + card = PokemonCardDef( guid="fae64169-d66c-5a75-a3ec-3aad1982f203", @@ -21,7 +32,7 @@ card = PokemonCardDef( Ability( title="Explosive Heat Dash", game_text="If this Pok\u00e9mon has any Fire Energy attached, it has no Retreat Cost.", - effect=unimplemented, + passive=retreat_free_when(_has_fire_energy), ), Attack( title="Claw Slash", diff --git a/spirit/game/scripts/cards/CZ/Exeggcute_57.py b/spirit/game/scripts/cards/CZ/Exeggcute_57.py index 29ebb87..d167a3a 100644 --- a/spirit/game/scripts/cards/CZ/Exeggcute_57.py +++ b/spirit/game/scripts/cards/CZ/Exeggcute_57.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="1997382b-9f81-567f-96c6-d989cb94a6b9", @@ -24,7 +25,7 @@ card = PokemonCardDef( game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.", cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=condition_attack(SpecialConditions.PARALYZED, flip=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Exeggutor_58.py b/spirit/game/scripts/cards/CZ/Exeggutor_58.py index bee293d..9eba1fe 100644 --- a/spirit/game/scripts/cards/CZ/Exeggutor_58.py +++ b/spirit/game/scripts/cards/CZ/Exeggutor_58.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per, count_energy card = PokemonCardDef( guid="08260c2c-df89-5676-96ef-75f2b9a433b8", @@ -24,7 +25,9 @@ card = PokemonCardDef( title="Powerful Storm", game_text="This attack does 20 damage for each Energy attached to all of your Pok\u00e9mon.", cost={PokemonTypes.PSYCHIC: 1}, - effect=unimplemented, + damage=20, + damage_operator="x", + effect=damage_per(count_energy("mine"), 20), ), Attack( title="Stampede", diff --git a/spirit/game/scripts/cards/CZ/FriendsinHisui_130.py b/spirit/game/scripts/cards/CZ/FriendsinHisui_130.py index 8829810..d8d2723 100644 --- a/spirit/game/scripts/cards/CZ/FriendsinHisui_130.py +++ b/spirit/game/scripts/cards/CZ/FriendsinHisui_130.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import draw_attack card = SupporterCardDef( guid="d48141ff-3e7c-5905-8dc4-7bee4e9abd02", @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=130, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=draw_attack(3) ) diff --git a/spirit/game/scripts/cards/CZ/FriendsinHisui_148.py b/spirit/game/scripts/cards/CZ/FriendsinHisui_148.py index 2552156..226fc04 100644 --- a/spirit/game/scripts/cards/CZ/FriendsinHisui_148.py +++ b/spirit/game/scripts/cards/CZ/FriendsinHisui_148.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import draw_attack card = SupporterCardDef( guid="b8aab8e6-e710-5acc-90d0-075282434464", @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=148, set_code="CZ", rarity=Rarities.RareUltra, - effect=unimplemented + effect=draw_attack(3) ) diff --git a/spirit/game/scripts/cards/CZ/FriendsinSinnoh_131.py b/spirit/game/scripts/cards/CZ/FriendsinSinnoh_131.py index d5a1ba0..bc62f18 100644 --- a/spirit/game/scripts/cards/CZ/FriendsinSinnoh_131.py +++ b/spirit/game/scripts/cards/CZ/FriendsinSinnoh_131.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import draw_attack card = SupporterCardDef( guid="f869e481-4a46-5266-901b-900e2022f3ab", @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=131, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=draw_attack(3) ) diff --git a/spirit/game/scripts/cards/CZ/FriendsinSinnoh_149.py b/spirit/game/scripts/cards/CZ/FriendsinSinnoh_149.py index 194f5e4..1b91448 100644 --- a/spirit/game/scripts/cards/CZ/FriendsinSinnoh_149.py +++ b/spirit/game/scripts/cards/CZ/FriendsinSinnoh_149.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import draw_attack card = SupporterCardDef( guid="d73c8c29-ff5d-587d-befd-99ef44c401ba", @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=149, set_code="CZ", rarity=Rarities.RareUltra, - effect=unimplemented + effect=draw_attack(3) ) diff --git a/spirit/game/scripts/cards/CZ/GalarianMeowth_84.py b/spirit/game/scripts/cards/CZ/GalarianMeowth_84.py index 35c709d..d2851e3 100644 --- a/spirit/game/scripts/cards/CZ/GalarianMeowth_84.py +++ b/spirit/game/scripts/cards/CZ/GalarianMeowth_84.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_bonus card = PokemonCardDef( guid="e86de1ae-68bb-51f3-8a82-b9104424ad7f", @@ -25,7 +26,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 1}, damage=10, damage_operator="+", - effect=unimplemented, + effect=flip_bonus(20), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/GalarianMrMime_30.py b/spirit/game/scripts/cards/CZ/GalarianMrMime_30.py index 64d945d..8184463 100644 --- a/spirit/game/scripts/cards/CZ/GalarianMrMime_30.py +++ b/spirit/game/scripts/cards/CZ/GalarianMrMime_30.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import search_to_hand +from spirit.game.session.effects import is_item_card card = PokemonCardDef( guid="48322948-2b21-5927-a13f-b358e3344f77", @@ -27,7 +29,7 @@ card = PokemonCardDef( title="Find It", game_text="Search your deck for an Item card, reveal it, and put it into your hand. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 2}, - effect=unimplemented, + effect=search_to_hand(is_item_card, count=1, minimum=0, reveal=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/GalarianPerrserker_85.py b/spirit/game/scripts/cards/CZ/GalarianPerrserker_85.py index 001a46e..f9f476d 100644 --- a/spirit/game/scripts/cards/CZ/GalarianPerrserker_85.py +++ b/spirit/game/scripts/cards/CZ/GalarianPerrserker_85.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_bonus card = PokemonCardDef( guid="56239d56-0d3b-5411-a04d-1c26d1eae253", @@ -26,7 +27,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 1, PokemonTypes.COLORLESS: 1}, damage=30, damage_operator="+", - effect=unimplemented, + effect=flip_bonus(60), ), Attack( title="Slash", diff --git a/spirit/game/scripts/cards/CZ/Girafarig_61.py b/spirit/game/scripts/cards/CZ/Girafarig_61.py index f0b41c0..2d809e3 100644 --- a/spirit/game/scripts/cards/CZ/Girafarig_61.py +++ b/spirit/game/scripts/cards/CZ/Girafarig_61.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack +from spirit.game.card_effects.support_common import draw_attack card = PokemonCardDef( guid="16bda894-1118-547a-9b79-738880e7035f", @@ -23,14 +25,14 @@ card = PokemonCardDef( title="Double Draw", game_text="Draw 2 cards.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=draw_attack(2), ), Attack( title="Psybeam", game_text="Your opponent's Active Pok\u00e9mon is now Confused.", cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1}, damage=30, - effect=unimplemented, + effect=condition_attack(SpecialConditions.CONFUSED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/GlaceonV_38.py b/spirit/game/scripts/cards/CZ/GlaceonV_38.py index c1f53c4..88c096b 100644 --- a/spirit/game/scripts/cards/CZ/GlaceonV_38.py +++ b/spirit/game/scripts/cards/CZ/GlaceonV_38.py @@ -1,5 +1,12 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.pokemon import energy_provides_type +from spirit.game.card_effects.support_common import search_attach_energy + + +def _is_water_energy(card): + return energy_provides_type(card, PokemonTypes.WATER.value) + card = PokemonCardDef( guid="d05205ad-77c3-5629-8d38-9e9878b8182b", @@ -23,7 +30,7 @@ card = PokemonCardDef( game_text="Search your deck for a Water Energy card and attach it to this Pok\u00e9mon. Then, shuffle your deck.", cost={PokemonTypes.WATER: 1}, damage=30, - effect=unimplemented, + effect=search_attach_energy(predicate=_is_water_energy, count=1, to_self=True), ), Attack( title="Freezing Wind", diff --git a/spirit/game/scripts/cards/CZ/Gloom_2.py b/spirit/game/scripts/cards/CZ/Gloom_2.py index 6561c7d..f31e909 100644 --- a/spirit/game/scripts/cards/CZ/Gloom_2.py +++ b/spirit/game/scripts/cards/CZ/Gloom_2.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="accabfca-c8d4-571d-b5e3-43380d1f987e", @@ -24,7 +25,7 @@ card = PokemonCardDef( game_text="Your opponent's Active Pok\u00e9mon is now Confused and Poisoned.", cost={PokemonTypes.GRASS: 1, PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=condition_attack(SpecialConditions.CONFUSED, SpecialConditions.POISONED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/GreatBall_132.py b/spirit/game/scripts/cards/CZ/GreatBall_132.py index 8e1ae0b..fd3a5cd 100644 --- a/spirit/game/scripts/cards/CZ/GreatBall_132.py +++ b/spirit/game/scripts/cards/CZ/GreatBall_132.py @@ -1,5 +1,22 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.session.effects import is_pokemon_card + + +async def great_ball(ctx): + """Look at the top 7 cards of your deck. You may reveal a Pokemon you + find there and put it into your hand. Shuffle the other cards back.""" + top = ctx.deck_top(7) + candidates = [c for c in top if is_pokemon_card(c)] + # No matches still shows the looked-at cards (nothing selectable). + picks = await ctx.choose_cards( + candidates, 1, minimum=0, + prompt="You may put a Pokémon into your hand.", + display_cards=top, + ) + await ctx.put_in_hand(picks, reveal=True) + await ctx.shuffle_deck() + card = ItemCardDef( guid="86f22ea3-8a9a-5100-b3e1-a5be7f53f923", @@ -11,5 +28,5 @@ card = ItemCardDef( collector_number=132, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=great_ball ) diff --git a/spirit/game/scripts/cards/CZ/GreedentV_120.py b/spirit/game/scripts/cards/CZ/GreedentV_120.py index 0bb61a6..f533719 100644 --- a/spirit/game/scripts/cards/CZ/GreedentV_120.py +++ b/spirit/game/scripts/cards/CZ/GreedentV_120.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack +from spirit.game.card_effects.support_common import draw_attack card = PokemonCardDef( guid="aab2eb28-3de4-5543-80f3-f25e0ebbbbe9", @@ -23,14 +25,14 @@ card = PokemonCardDef( game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.", cost={PokemonTypes.COLORLESS: 2}, damage=40, - effect=unimplemented, + effect=condition_attack(SpecialConditions.PARALYZED, flip=True), ), Attack( title="Nom-Nom-Nom Incisors", game_text="Draw 3 cards.", cost={PokemonTypes.COLORLESS: 3}, damage=120, - effect=unimplemented, + effect=draw_attack(3), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Grubbin_15.py b/spirit/game/scripts/cards/CZ/Grubbin_15.py index ebd19b7..adbbf25 100644 --- a/spirit/game/scripts/cards/CZ/Grubbin_15.py +++ b/spirit/game/scripts/cards/CZ/Grubbin_15.py @@ -1,5 +1,8 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_or_nothing +from spirit.game.card_effects.support_common import attach_from_discard +from spirit.game.card_effects.pokemon import is_lightning_energy card = PokemonCardDef( guid="da2d4201-db35-5408-bf85-f3d0dad17d1e", @@ -22,14 +25,17 @@ card = PokemonCardDef( title="Energize", game_text="Attach a Lightning Energy card from your discard pile to this Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=attach_from_discard( + predicate=is_lightning_energy, count=1, target="self", + prompt="Choose a Lightning Energy card to attach.", + ), ), Attack( title="Surprise Attack", game_text="Flip a coin. If tails, this attack does nothing.", cost={PokemonTypes.COLORLESS: 3}, damage=50, - effect=unimplemented, + effect=flip_or_nothing(), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Gumshoos_118.py b/spirit/game/scripts/cards/CZ/Gumshoos_118.py index 62015a6..e9ca53b 100644 --- a/spirit/game/scripts/cards/CZ/Gumshoos_118.py +++ b/spirit/game/scripts/cards/CZ/Gumshoos_118.py @@ -1,5 +1,15 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if + + +def _stakeout_bonus(ctx): + active = ctx.opponent_active() + if active is None: + return False + turn_state = ctx.session.turn_state + return turn_state.became_active_turn.get(active.entity_id) == turn_state.turn_number - 1 + card = PokemonCardDef( guid="e70c910b-dcf4-5666-9b5d-db5ece37d83f", @@ -25,7 +35,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 2}, damage=30, damage_operator="+", - effect=unimplemented, + effect=bonus_if(_stakeout_bonus, 120), ), Attack( title="Lunge Out", diff --git a/spirit/game/scripts/cards/CZ/HattereneVMAX_66.py b/spirit/game/scripts/cards/CZ/HattereneVMAX_66.py index 8247f94..dc21730 100644 --- a/spirit/game/scripts/cards/CZ/HattereneVMAX_66.py +++ b/spirit/game/scripts/cards/CZ/HattereneVMAX_66.py @@ -1,5 +1,31 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack +from spirit.game.card_effects.support_common import requires_damaged_pokemon + + +async def witchs_domain(ctx): + """Once during your turn, you may move up to 2 damage counters from your + Pokemon to your opponent's Active Pokemon.""" + candidates = [p for p in ctx.my_pokemon_in_play() + if p.get_attribute(AttrID.HP, 0) < ctx.max_hp(p)] + if not candidates: + return + if not await ctx.ask_yes_no( + "Move up to 2 damage counters from your Pokémon to your " + "opponent's Active Pokémon?" + ): + return + source = await ctx.choose_pokemon( + candidates, "Choose 1 of your Pokémon to move damage counters from" + ) + if source is None: + return + target = ctx.opponent_active() + if target is None: + return + await ctx.move_damage_counters(source, target, max_count=2) + card = PokemonCardDef( guid="57f38c45-b660-582a-8e3e-318e595a8bea", @@ -23,14 +49,16 @@ card = PokemonCardDef( Ability( title="Witch's Domain", game_text="Once during your turn, you may move up to 2 damage counters from your Pok\u00e9mon to your opponent's Active Pok\u00e9mon.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + condition=requires_damaged_pokemon("mine"), + effect=witchs_domain, ), Attack( title="G-Max Smite", game_text="Your opponent's Active Pok\u00e9mon is now Confused.", cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 2}, damage=150, - effect=unimplemented, + effect=condition_attack(SpecialConditions.CONFUSED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/HattereneV_65.py b/spirit/game/scripts/cards/CZ/HattereneV_65.py index 569dad9..143c87b 100644 --- a/spirit/game/scripts/cards/CZ/HattereneV_65.py +++ b/spirit/game/scripts/cards/CZ/HattereneV_65.py @@ -1,5 +1,24 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import switch_self_attack +from spirit.game.card_effects.trainers import is_energy_card + + +async def horoscope(ctx): + """Look at the top 3 cards of your deck. You may attach any number of + Energy cards you find there to this Pokemon. Put the other cards back.""" + top = ctx.deck_top(3) + if not top: + return + energies = [c for c in top if is_energy_card(c)] + picks = await ctx.choose_cards( + energies, max(len(energies), 1), minimum=0, + prompt="Choose Energy cards to attach to this Pokémon.", + display_cards=top, + ) + for card in picks: + await ctx.attach_energy(card, ctx.attacker) + card = PokemonCardDef( guid="81227f69-d3c2-58fa-98e1-25ea982eb0ba", @@ -23,14 +42,14 @@ card = PokemonCardDef( title="Horoscope", game_text="Look at the top 3 cards of your deck. You may attach any number of Energy cards you find there to this Pok\u00e9mon. Put the other cards back in any order.", cost={PokemonTypes.PSYCHIC: 1}, - effect=unimplemented, + effect=horoscope, ), Attack( title="Teleportation Burst", game_text="Switch this Pok\u00e9mon with 1 of your Benched Pok\u00e9mon.", cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 2}, damage=80, - effect=unimplemented, + effect=switch_self_attack(damage=80, optional=False), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Heliolisk_50.py b/spirit/game/scripts/cards/CZ/Heliolisk_50.py index cb9f882..d4d942a 100644 --- a/spirit/game/scripts/cards/CZ/Heliolisk_50.py +++ b/spirit/game/scripts/cards/CZ/Heliolisk_50.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack card = PokemonCardDef( guid="889fffbc-c90f-5f38-8e62-a2c1a40e0268", @@ -29,7 +30,7 @@ card = PokemonCardDef( game_text="This Pok\u00e9mon also does 50 damage to itself.", cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 2}, damage=150, - effect=unimplemented, + effect=recoil_attack(50), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Hoopa_83.py b/spirit/game/scripts/cards/CZ/Hoopa_83.py index 31da421..1962c07 100644 --- a/spirit/game/scripts/cards/CZ/Hoopa_83.py +++ b/spirit/game/scripts/cards/CZ/Hoopa_83.py @@ -1,6 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def assault_gate(ctx): + """If this Pokemon didn't move Bench->Active this turn, this attack does nothing; damage ignores Weakness.""" + if not ctx.entered_active_this_turn(ctx.attacker): + return + await ctx.deal_damage(ignore_weakness=True) + card = PokemonCardDef( guid="27413188-f69d-55ba-ab2b-4f72c7337aa1", key="CZ", @@ -20,10 +27,10 @@ card = PokemonCardDef( abilities=[ Attack( title="Assault Gate", - game_text="If this Pok\u00e9mon didn't move from the Bench to the Active Spot this turn, this attack does nothing. This attack's damage isn't affected by Weakness.", + game_text="If this Pokémon didn't move from the Bench to the Active Spot this turn, this attack does nothing. This attack's damage isn't affected by Weakness.", cost={PokemonTypes.DARKNESS: 1}, damage=90, - effect=unimplemented, + effect=assault_gate, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Hop_133.py b/spirit/game/scripts/cards/CZ/Hop_133.py index f9de0cc..aa5ef49 100644 --- a/spirit/game/scripts/cards/CZ/Hop_133.py +++ b/spirit/game/scripts/cards/CZ/Hop_133.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.card_effects.trainers import hop +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities card = SupporterCardDef( @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=133, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + effect=hop ) diff --git a/spirit/game/scripts/cards/CZ/Koffing_75.py b/spirit/game/scripts/cards/CZ/Koffing_75.py index 6ddb0f3..a34af97 100644 --- a/spirit/game/scripts/cards/CZ/Koffing_75.py +++ b/spirit/game/scripts/cards/CZ/Koffing_75.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="cd07c0d1-fd80-5ebe-bbea-12232fa13f95", @@ -23,7 +24,7 @@ card = PokemonCardDef( game_text="Your opponent's Active Pok\u00e9mon is now Poisoned.", cost={PokemonTypes.DARKNESS: 1, PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=condition_attack(SpecialConditions.POISONED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Kricketot_10.py b/spirit/game/scripts/cards/CZ/Kricketot_10.py index 24773cc..320958d 100644 --- a/spirit/game/scripts/cards/CZ/Kricketot_10.py +++ b/spirit/game/scripts/cards/CZ/Kricketot_10.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_bonus card = PokemonCardDef( guid="134d56d1-fa37-5ec6-a7bf-441abd394007", @@ -24,7 +25,7 @@ card = PokemonCardDef( cost={PokemonTypes.GRASS: 1}, damage=10, damage_operator="+", - effect=unimplemented, + effect=flip_bonus(20), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Krokorok_79.py b/spirit/game/scripts/cards/CZ/Krokorok_79.py index f8445c6..80a41ff 100644 --- a/spirit/game/scripts/cards/CZ/Krokorok_79.py +++ b/spirit/game/scripts/cards/CZ/Krokorok_79.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import mill_attack card = PokemonCardDef( guid="335d48e9-4d6e-52be-aaa4-19ca779b2fbd", @@ -28,7 +29,7 @@ card = PokemonCardDef( title="Dredge Up", game_text="Discard the top 3 cards of your opponent's deck.", cost={PokemonTypes.COLORLESS: 3}, - effect=unimplemented, + effect=mill_attack(3), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/KyogreV_37.py b/spirit/game/scripts/cards/CZ/KyogreV_37.py index 5aab96b..6023874 100644 --- a/spirit/game/scripts/cards/CZ/KyogreV_37.py +++ b/spirit/game/scripts/cards/CZ/KyogreV_37.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import snipe_attack card = PokemonCardDef( guid="4403b192-7c0c-5597-a32d-56a4686d0b6b", @@ -22,14 +23,14 @@ card = PokemonCardDef( title="Dual Splash", game_text="This attack does 50 damage to 2 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.WATER: 1, PokemonTypes.COLORLESS: 2}, - effect=unimplemented, + effect=snipe_attack(50, pool="any", count=2, side="opponent"), ), Attack( title="Aqua Typhoon", game_text="During your next turn, this Pok\u00e9mon can't use Aqua Typhoon.", cost={PokemonTypes.WATER: 1, PokemonTypes.COLORLESS: 3}, damage=210, - effect=unimplemented, + locks_next_turn=True, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Kyogre_36.py b/spirit/game/scripts/cards/CZ/Kyogre_36.py index 1ef5dba..61b3ca9 100644 --- a/spirit/game/scripts/cards/CZ/Kyogre_36.py +++ b/spirit/game/scripts/cards/CZ/Kyogre_36.py @@ -1,5 +1,28 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.pokemon import energy_provides_type +from spirit.game.card_effects.support_common import search_attach_energy + + +def _is_water_energy(card): + return energy_provides_type(card, PokemonTypes.WATER.value) + + +async def dynamic_wave(ctx): + """Put 3 Energy attached to this Pokemon into hand; 180 to 1 of opponent's Pokemon.""" + attached = ctx.attached_energies(ctx.attacker) + if attached: + picks = await ctx.choose_cards( + attached, 3, + prompt="Choose 3 Energy attached to this Pokémon to put into your hand", + ) + await ctx.put_in_hand(picks, reveal=False) + target = await ctx.choose_pokemon( + ctx.opponent_pokemon_in_play(), "Choose 1 of your opponent's Pokémon" + ) + if target is not None: + await ctx.deal_damage(180, target=target) + card = PokemonCardDef( guid="32672775-3b7e-5d18-a914-d5eaced8cdc9", @@ -22,13 +45,13 @@ card = PokemonCardDef( title="Wave Summoning", game_text="Search your deck for a Water Energy card and attach it to this Pok\u00e9mon. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=search_attach_energy(predicate=_is_water_energy, count=1, to_self=True), ), Attack( title="Dynamic Wave", game_text="Put 3 Energy attached to this Pok\u00e9mon into your hand. This attack does 180 damage to 1 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.WATER: 3, PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=dynamic_wave, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Lairon_88.py b/spirit/game/scripts/cards/CZ/Lairon_88.py index 0612e52..f2d8ca6 100644 --- a/spirit/game/scripts/cards/CZ/Lairon_88.py +++ b/spirit/game/scripts/cards/CZ/Lairon_88.py @@ -1,6 +1,15 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def wreak_havoc(ctx): + """80. Flip a coin until tails; discard opponent's top deck card per heads.""" + await ctx.deal_damage() + heads = await ctx.flip_until_tails(ctx.ability.title) + if heads: + await ctx.discard_cards(ctx.deck_top(heads, player_id=ctx.opponent_id)) + + card = PokemonCardDef( guid="76a0f9a8-6eea-5977-87ed-453818144afb", key="CZ", @@ -30,7 +39,7 @@ card = PokemonCardDef( game_text="Flip a coin until you get tails. For each heads, discard the top card of your opponent's deck.", cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 2}, damage=80, - effect=unimplemented, + effect=wreak_havoc, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/LeafeonVSTAR_14.py b/spirit/game/scripts/cards/CZ/LeafeonVSTAR_14.py index 2911aa4..544d62c 100644 --- a/spirit/game/scripts/cards/CZ/LeafeonVSTAR_14.py +++ b/spirit/game/scripts/cards/CZ/LeafeonVSTAR_14.py @@ -1,5 +1,38 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.passives_common import TakesLessPassive + + +def _ivy_star_condition(board, player_id, pokemon): + opponent_id = next(p for p in board.player_ids if p != player_id) + bench = board.find_player_area(opponent_id, "bench") + return bool(board.active_pokemon(opponent_id)) and bool(bench and bench.children) + + +async def ivy_star(ctx): + """You may switch 1 of your opponent's Benched Pokémon with their Active + Pokémon.""" + opp_active = ctx.opponent_active() + opp_bench = ctx.opponent_bench() + if opp_active is None or not opp_bench or ctx.effects_blocked(opp_active): + return + if not await ctx.ask_yes_no( + "Switch 1 of your opponent's Benched Pokémon with their Active Pokémon?" + ): + return + target = await ctx.choose_pokemon( + opp_bench, "Choose the opponent's new Active Pokémon" + ) + if target is not None: + await ctx.switch_active(ctx.opponent_id, target) + + +async def leaf_guard(ctx): + """180. During your opponent's next turn, this Pokémon takes 30 less + damage from attacks (after applying Weakness and Resistance).""" + await ctx.deal_damage() + ctx.add_passive_through_opponents_turn(ctx.attacker, TakesLessPassive(30)) + card = PokemonCardDef( guid="1c418037-55c4-561c-b98b-acb0a20099c0", @@ -22,14 +55,16 @@ card = PokemonCardDef( Ability( title="Ivy Star", game_text="During your turn, you may switch 1 of your opponent's Benched Pok\u00e9mon with their Active Pok\u00e9mon. (You can't use more than 1 VSTAR Power in a game.)", - effect=unimplemented, + vstar=True, + condition=_ivy_star_condition, + effect=ivy_star, ), Attack( title="Leaf Guard", game_text="During your opponent's next turn, this Pok\u00e9mon takes 30 less damage from attacks (after applying Weakness and Resistance).", cost={PokemonTypes.GRASS: 2, PokemonTypes.COLORLESS: 1}, damage=180, - effect=unimplemented, + effect=leaf_guard, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/LeafeonV_13.py b/spirit/game/scripts/cards/CZ/LeafeonV_13.py index dfa4004..31667c3 100644 --- a/spirit/game/scripts/cards/CZ/LeafeonV_13.py +++ b/spirit/game/scripts/cards/CZ/LeafeonV_13.py @@ -1,5 +1,14 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.passives_common import TakesLessPassive + + +async def leaf_guard(ctx): + """30. During your opponent's next turn, this Pokémon takes 30 less + damage from attacks (after applying Weakness and Resistance).""" + await ctx.deal_damage() + ctx.add_passive_through_opponents_turn(ctx.attacker, TakesLessPassive(30)) + card = PokemonCardDef( guid="af49d502-c4b6-5599-bde4-b5c95992105a", @@ -23,14 +32,14 @@ card = PokemonCardDef( game_text="During your opponent's next turn, this Pok\u00e9mon takes 30 less damage from attacks (after applying Weakness and Resistance).", cost={PokemonTypes.GRASS: 1}, damage=30, - effect=unimplemented, + effect=leaf_guard, ), Attack( title="Slashing Strike", game_text="During your next turn, this Pok\u00e9mon can't use Slashing Strike.", cost={PokemonTypes.GRASS: 2, PokemonTypes.COLORLESS: 1}, damage=180, - effect=unimplemented, + locks_next_turn=True, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Leon_134.py b/spirit/game/scripts/cards/CZ/Leon_134.py index 2d05093..6c21781 100644 --- a/spirit/game/scripts/cards/CZ/Leon_134.py +++ b/spirit/game/scripts/cards/CZ/Leon_134.py @@ -1,5 +1,16 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.session.passives import TurnDamageModifier + + +async def leon_effect(ctx): + """This turn, your Pokemon's attacks do 30 more damage to the opponent's Active (before W/R).""" + ctx.add_turn_damage_modifier(TurnDamageModifier(30, ctx.player_id)) + for pokemon in ctx.my_pokemon_in_play(): + await ctx.add_stat_visualization( + pokemon, "Positive", "DamageDealtIncreased", card_text="+30 damage" + ) + card = SupporterCardDef( guid="f8fa0778-c5f3-5966-a0f2-e8aa20803a1e", @@ -11,5 +22,5 @@ card = SupporterCardDef( collector_number=134, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + effect=leon_effect ) diff --git a/spirit/game/scripts/cards/CZ/Liepard_78.py b/spirit/game/scripts/cards/CZ/Liepard_78.py index 06cc108..7595141 100644 --- a/spirit/game/scripts/cards/CZ/Liepard_78.py +++ b/spirit/game/scripts/cards/CZ/Liepard_78.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage card = PokemonCardDef( guid="2e9bf2ed-ca51-5c17-8f91-a9057b71cedb", @@ -25,7 +26,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 2}, damage=40, damage_operator="x", - effect=unimplemented, + effect=flip_damage(coins=3, per_heads=40), ), Attack( title="Claw Slash", diff --git a/spirit/game/scripts/cards/CZ/LostVacuum_135.py b/spirit/game/scripts/cards/CZ/LostVacuum_135.py index 055ae71..0fb30bc 100644 --- a/spirit/game/scripts/cards/CZ/LostVacuum_135.py +++ b/spirit/game/scripts/cards/CZ/LostVacuum_135.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.trainers import lost_vacuum, lost_vacuum_playable +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities card = ItemCardDef( @@ -11,5 +12,6 @@ card = ItemCardDef( collector_number=135, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=lost_vacuum, + condition=lost_vacuum_playable ) diff --git a/spirit/game/scripts/cards/CZ/Lunatone_62.py b/spirit/game/scripts/cards/CZ/Lunatone_62.py index 67e2ebb..43664d0 100644 --- a/spirit/game/scripts/cards/CZ/Lunatone_62.py +++ b/spirit/game/scripts/cards/CZ/Lunatone_62.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.pokemon import read_the_wind +from spirit.game.card_effects.attacks_common import damage_per, count_energy card = PokemonCardDef( guid="2629a956-6b0c-58b5-a372-91927fa03c96", @@ -23,7 +25,7 @@ card = PokemonCardDef( title="Cycle Draw", game_text="Discard a card from your hand. If you do, draw 3 cards.", cost={PokemonTypes.PSYCHIC: 1}, - effect=unimplemented, + effect=read_the_wind, ), Attack( title="Moon Kinesis", @@ -31,7 +33,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 3}, damage=30, damage_operator="+", - effect=unimplemented, + effect=damage_per(count_energy("self", energy_type=PokemonTypes.PSYCHIC.value), 30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Luvdisc_35.py b/spirit/game/scripts/cards/CZ/Luvdisc_35.py index c9cfad1..5d64521 100644 --- a/spirit/game/scripts/cards/CZ/Luvdisc_35.py +++ b/spirit/game/scripts/cards/CZ/Luvdisc_35.py @@ -1,6 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def emotional_draw(ctx): + """Shuffle your hand into your deck. Then, draw 5 cards.""" + await ctx.shuffle_into_deck(ctx.hand(), ctx.player_id) + await ctx.draw_cards(5) + + card = PokemonCardDef( guid="0367b96d-ec56-5b5f-96f8-e5ee7689be85", key="CZ", @@ -22,7 +29,7 @@ card = PokemonCardDef( title="Emotional Draw", game_text="Shuffle your hand into your deck. Then, draw 5 cards.", cost={PokemonTypes.WATER: 1}, - effect=unimplemented, + effect=emotional_draw, ), Attack( title="Tackle", diff --git a/spirit/game/scripts/cards/CZ/Luxio_41.py b/spirit/game/scripts/cards/CZ/Luxio_41.py index be300c3..5f8bf99 100644 --- a/spirit/game/scripts/cards/CZ/Luxio_41.py +++ b/spirit/game/scripts/cards/CZ/Luxio_41.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import snipe_attack card = PokemonCardDef( guid="affeecef-93e0-5191-bdf3-abec1ff30a6e", @@ -23,7 +24,7 @@ card = PokemonCardDef( title="Jumping Kick", game_text="This attack does 30 damage to 1 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.LIGHTNING: 1}, - effect=unimplemented, + effect=snipe_attack(30, pool="any", count=1), ), Attack( title="Head Bolt", diff --git a/spirit/game/scripts/cards/CZ/Luxio_42.py b/spirit/game/scripts/cards/CZ/Luxio_42.py index e8e9459..76dffb0 100644 --- a/spirit/game/scripts/cards/CZ/Luxio_42.py +++ b/spirit/game/scripts/cards/CZ/Luxio_42.py @@ -1,5 +1,15 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import has_tool + + +async def shorting_spark(ctx): + """90 to each opponent Pokemon with a Pokemon Tool attached (Bench takes no W/R).""" + active = ctx.opponent_active() + for target in ctx.opponent_pokemon_in_play(): + if has_tool(target): + await ctx.deal_damage(90, target=target, apply_modifiers=target is active) + card = PokemonCardDef( guid="323885a3-01bb-580d-98ad-49079cb9379f", @@ -23,7 +33,7 @@ card = PokemonCardDef( title="Shorting Spark", game_text="This attack does 90 damage to each of your opponent's Pok\u00e9mon that has a Pok\u00e9mon Tool attached. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.LIGHTNING: 1}, - effect=unimplemented, + effect=shorting_spark, ), Attack( title="Bite", diff --git a/spirit/game/scripts/cards/CZ/Luxray_43.py b/spirit/game/scripts/cards/CZ/Luxray_43.py index b3b102b..d9b0f4c 100644 --- a/spirit/game/scripts/cards/CZ/Luxray_43.py +++ b/spirit/game/scripts/cards/CZ/Luxray_43.py @@ -1,5 +1,15 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import snipe_attack, bonus_if, has_damage + + +async def _switch_self(ctx): + bench = ctx.my_bench() + if not bench: + return + target = await ctx.choose_pokemon(bench, "Choose your new Active Pokémon") + if target is not None: + await ctx.switch_active(ctx.player_id, target) card = PokemonCardDef( guid="8b0a6d3e-8365-5861-85de-47ec0ed75b5f", @@ -21,17 +31,17 @@ card = PokemonCardDef( abilities=[ Attack( title="Electrostep", - game_text="This attack does 40 damage to 1 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.) Switch this Pok\u00e9mon with 1 of your Benched Pok\u00e9mon.", + game_text="This attack does 40 damage to 1 of your opponent's Pokémon. (Don't apply Weakness and Resistance for Benched Pokémon.) Switch this Pokémon with 1 of your Benched Pokémon.", cost={PokemonTypes.LIGHTNING: 1}, - effect=unimplemented, + effect=snipe_attack(40, pool="any", count=1, also=_switch_self), ), Attack( title="Scar Strikes", - game_text="If your opponent's Active Pok\u00e9mon already has any damage counters on it, this attack does 100 more damage.", + game_text="If your opponent's Active Pokémon already has any damage counters on it, this attack does 100 more damage.", cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 1}, damage=100, damage_operator="+", - effect=unimplemented, + effect=bonus_if(has_damage("defender"), 100), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Luxray_44.py b/spirit/game/scripts/cards/CZ/Luxray_44.py index 3feb969..f6b1b7d 100644 --- a/spirit/game/scripts/cards/CZ/Luxray_44.py +++ b/spirit/game/scripts/cards/CZ/Luxray_44.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import search_to_hand +from spirit.game.session.effects import is_trainer_card card = PokemonCardDef( guid="ef8ca3ef-7c49-55f9-8a3b-0bf871f8e524", @@ -18,18 +20,21 @@ card = PokemonCardDef( weakness_type=PokemonTypes.FIGHTING, evolves_from="com.direwolfdigital.cake.data.archetypes.pokemon.Luxio.Name", family_id=403, + setup_as_active=True, abilities=[ Ability( title="Explosiveness", game_text="If this Pok\u00e9mon is in your hand when you are setting up to play, you may put it face down as your Active Pok\u00e9mon.", - effect=unimplemented, ), Attack( title="Seeking Fang", game_text="Search your deck for up to 2 Trainer cards, reveal them, and put them into your hand. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 1}, damage=50, - effect=unimplemented, + effect=search_to_hand( + is_trainer_card, count=2, minimum=0, + prompt="Choose up to 2 Trainer cards to put into your hand.", + ), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Lycanroc_74.py b/spirit/game/scripts/cards/CZ/Lycanroc_74.py index d754535..c46b70a 100644 --- a/spirit/game/scripts/cards/CZ/Lycanroc_74.py +++ b/spirit/game/scripts/cards/CZ/Lycanroc_74.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per, count_energy card = PokemonCardDef( guid="e9a82ef0-4501-5963-a606-5cbc18edaff7", @@ -25,7 +26,7 @@ card = PokemonCardDef( cost={PokemonTypes.FIGHTING: 1, PokemonTypes.COLORLESS: 1}, damage=240, damage_operator="-", - effect=unimplemented, + effect=damage_per(count_energy(scope="defender"), per=-80, base=240), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Metang_90.py b/spirit/game/scripts/cards/CZ/Metang_90.py index 23c6ece..ae1c281 100644 --- a/spirit/game/scripts/cards/CZ/Metang_90.py +++ b/spirit/game/scripts/cards/CZ/Metang_90.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage card = PokemonCardDef( guid="dab72893-000a-5cf9-ae14-fe1a2ce534ff", @@ -26,7 +27,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 1, PokemonTypes.COLORLESS: 1}, damage=30, damage_operator="+", - effect=unimplemented, + effect=flip_damage(coins=2, bonus_per_heads=30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/MewV_60.py b/spirit/game/scripts/cards/CZ/MewV_60.py index 35b61be..e8f62ea 100644 --- a/spirit/game/scripts/cards/CZ/MewV_60.py +++ b/spirit/game/scripts/cards/CZ/MewV_60.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.card_effects.pokemon import energy_mix, psychic_leap +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -23,14 +24,14 @@ card = PokemonCardDef( title="Energy Mix", game_text="Search your deck for an Energy card and attach it to 1 of your Fusion Strike Pok\u00e9mon. Then, shuffle your deck.", cost={PokemonTypes.PSYCHIC: 1}, - effect=unimplemented, + effect=energy_mix, ), Attack( title="Psychic Leap", game_text="You may shuffle this Pok\u00e9mon and all attached cards into your deck.", cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1}, damage=70, - effect=unimplemented, + effect=psychic_leap, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Mewtwo_59.py b/spirit/game/scripts/cards/CZ/Mewtwo_59.py index 6788290..37b1148 100644 --- a/spirit/game/scripts/cards/CZ/Mewtwo_59.py +++ b/spirit/game/scripts/cards/CZ/Mewtwo_59.py @@ -1,5 +1,12 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, opponent_prizes_taken_at_least +from spirit.game.card_effects.pokemon import energy_provides_type +from spirit.game.card_effects.support_common import attach_from_discard + + +def _is_psychic_energy(card): + return energy_provides_type(card, PokemonTypes.PSYCHIC.value) card = PokemonCardDef( guid="2577cc87-9e1b-5938-89f6-4dc23574caf9", @@ -23,7 +30,10 @@ card = PokemonCardDef( title="Psypump", game_text="Attach up to 2 Psychic Energy cards from your discard pile to 1 of your Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=attach_from_discard( + predicate=_is_psychic_energy, count=2, target="choice", minimum=0, + prompt="Choose up to 2 Psychic Energy cards from your discard pile to attach.", + ), ), Attack( title="Limit Break", @@ -31,7 +41,7 @@ card = PokemonCardDef( cost={PokemonTypes.PSYCHIC: 2, PokemonTypes.COLORLESS: 1}, damage=90, damage_operator="+", - effect=unimplemented, + effect=bonus_if(opponent_prizes_taken_at_least(3), 90), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Nessa_136.py b/spirit/game/scripts/cards/CZ/Nessa_136.py index 08f4f79..c7293e2 100644 --- a/spirit/game/scripts/cards/CZ/Nessa_136.py +++ b/spirit/game/scripts/cards/CZ/Nessa_136.py @@ -1,5 +1,17 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented -from spirit.game.attributes import Rarities +from spirit.game.data_utils import SupporterCardDef +from spirit.game.attributes import PokemonTypes, Rarities, AttrID +from spirit.game.card_effects.support_common import recover_from_discard, requires_discard, is_energy +from spirit.game.session.effects import is_water_pokemon + + +def is_water_energy_card(card) -> bool: + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return is_energy(card) and PokemonTypes.WATER.value in types + + +def _nessa_predicate(card) -> bool: + return is_water_pokemon(card) or is_water_energy_card(card) + card = SupporterCardDef( guid="910d43ac-a842-55d1-81b5-f09de43ee5d8", @@ -11,5 +23,9 @@ card = SupporterCardDef( collector_number=136, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + effect=recover_from_discard( + _nessa_predicate, count=4, minimum=1, reveal=False, to="hand", + prompt="Choose up to 4 Water Pokémon and Water Energy cards", + ), + condition=requires_discard(_nessa_predicate), ) diff --git a/spirit/game/scripts/cards/CZ/Oddish_1.py b/spirit/game/scripts/cards/CZ/Oddish_1.py index b753553..b349dfa 100644 --- a/spirit/game/scripts/cards/CZ/Oddish_1.py +++ b/spirit/game/scripts/cards/CZ/Oddish_1.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage card = PokemonCardDef( guid="fdac5939-1fcb-51c2-844a-581984efe448", @@ -24,7 +25,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 1}, damage=10, damage_operator="x", - effect=unimplemented, + effect=flip_damage(coins=2, per_heads=10), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Oranguru_119.py b/spirit/game/scripts/cards/CZ/Oranguru_119.py index 21e8410..146dc18 100644 --- a/spirit/game/scripts/cards/CZ/Oranguru_119.py +++ b/spirit/game/scripts/cards/CZ/Oranguru_119.py @@ -1,5 +1,36 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, TRAINER_EFFECTS_BY_GUID, unimplemented from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.session.effects import is_supporter_card + + +def _runnable_supporter(card): + if not is_supporter_card(card): + return False + effect = TRAINER_EFFECTS_BY_GUID.get((card.archetype_id or "").lower()) + return effect is not None and effect is not unimplemented + + +def _primate_acting_condition(board, player_id, pokemon): + opponent_id = next(p for p in board.player_ids if p != player_id) + discard = board.find_player_area(opponent_id, "discard") + return bool(discard) and any(_runnable_supporter(c) for c in discard.children) + + +async def primate_acting(ctx): + """Choose a Supporter card from the opponent's discard pile and use its + effect as the effect of this attack.""" + candidates = [c for c in ctx.discard_pile(ctx.opponent_id) if _runnable_supporter(c)] + if not candidates: + return + picked = await ctx.choose_cards( + candidates, 1, minimum=1, + prompt="Choose a Supporter card from your opponent's discard pile", + ) + if not picked: + return + effect = TRAINER_EFFECTS_BY_GUID[(picked[0].archetype_id or "").lower()] + await effect(ctx) + card = PokemonCardDef( guid="eafa4d0e-eb5e-5eea-a370-7b74de785952", @@ -22,7 +53,8 @@ card = PokemonCardDef( title="Primate Acting", game_text="Choose a Supporter card from your opponent's discard pile and use the effect of that card as the effect of this attack.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + condition=_primate_acting_condition, + effect=primate_acting, ), Attack( title="Hammer In", diff --git a/spirit/game/scripts/cards/CZ/Pangoro_80.py b/spirit/game/scripts/cards/CZ/Pangoro_80.py index fe1669c..84d32ef 100644 --- a/spirit/game/scripts/cards/CZ/Pangoro_80.py +++ b/spirit/game/scripts/cards/CZ/Pangoro_80.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack card = PokemonCardDef( guid="4ceae16b-44a9-5072-adaa-0b1ed27fef8a", @@ -29,7 +30,7 @@ card = PokemonCardDef( game_text="This Pok\u00e9mon also does 30 damage to itself.", cost={PokemonTypes.DARKNESS: 2, PokemonTypes.COLORLESS: 1}, damage=160, - effect=unimplemented, + effect=recoil_attack(30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Pawniard_91.py b/spirit/game/scripts/cards/CZ/Pawniard_91.py index 50899d7..eeaeecb 100644 --- a/spirit/game/scripts/cards/CZ/Pawniard_91.py +++ b/spirit/game/scripts/cards/CZ/Pawniard_91.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.card_effects.attacks_common import recoil_attack +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -21,10 +22,10 @@ card = PokemonCardDef( abilities=[ Attack( title="Reckless Charge", - game_text="This Pok\u00e9mon also does 10 damage to itself.", + game_text="This Pokémon also does 10 damage to itself.", cost={PokemonTypes.METAL: 1}, damage=30, - effect=unimplemented, + effect=recoil_attack(10), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Pikachu_160.py b/spirit/game/scripts/cards/CZ/Pikachu_160.py index b90d783..d78abc3 100644 --- a/spirit/game/scripts/cards/CZ/Pikachu_160.py +++ b/spirit/game/scripts/cards/CZ/Pikachu_160.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.card_effects.attacks_common import recoil_attack +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -20,10 +21,10 @@ card = PokemonCardDef( abilities=[ Attack( title="Wild Charge", - game_text="This Pok\u00e9mon also does 30 damage to itself.", + game_text="This Pokémon also does 30 damage to itself.", cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1}, damage=90, - effect=unimplemented, + effect=recoil_attack(30), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Pincurchin_56.py b/spirit/game/scripts/cards/CZ/Pincurchin_56.py index 254e503..884a5b6 100644 --- a/spirit/game/scripts/cards/CZ/Pincurchin_56.py +++ b/spirit/game/scripts/cards/CZ/Pincurchin_56.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage +from spirit.game.card_effects.support_common import search_to_bench card = PokemonCardDef( guid="715f3cd8-7b81-5d2b-9884-283de7d5c1a3", @@ -22,7 +24,7 @@ card = PokemonCardDef( title="Call for Family", game_text="Search your deck for up to 2 Basic Pok\u00e9mon and put them onto your Bench. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=search_to_bench(count=2), ), Attack( title="Continuous Tumble", @@ -30,7 +32,7 @@ card = PokemonCardDef( cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 1}, damage=30, damage_operator="+", - effect=unimplemented, + effect=flip_damage(until_tails=True, bonus_per_heads=30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/PokBall_137.py b/spirit/game/scripts/cards/CZ/PokBall_137.py index 82521f8..f1cefd4 100644 --- a/spirit/game/scripts/cards/CZ/PokBall_137.py +++ b/spirit/game/scripts/cards/CZ/PokBall_137.py @@ -1,15 +1,29 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.session.effects import is_pokemon_card + + +async def poke_ball(ctx): + """Flip a coin. If heads, search the deck for a Pokemon, reveal it, and + put it into your hand. Then, shuffle your deck.""" + if (await ctx.flip_coins(1, "Poké Ball"))[0]: + picks = await ctx.search_deck( + is_pokemon_card, count=1, minimum=0, + prompt="Choose a Pokémon to put into your hand.", + ) + await ctx.put_in_hand(picks, reveal=True) + await ctx.shuffle_deck() + card = ItemCardDef( guid="d4644be3-57c5-546d-aeec-4cb076dd48ed", key="CZ", name="com.direwolfdigital.cake.data.archetypes.trainer.PokBall.Name", display_name="Poké Ball", - searchable_by=["Pok\u00c3\u00a9 Ball", "Item"], + searchable_by=["Poké Ball", "Item"], subtypes=["Item"], collector_number=137, set_code="CZ", rarity=Rarities.Common, - effect=unimplemented + effect=poke_ball ) diff --git a/spirit/game/scripts/cards/CZ/PokmonCatcher_138.py b/spirit/game/scripts/cards/CZ/PokmonCatcher_138.py index a7287f7..285184f 100644 --- a/spirit/game/scripts/cards/CZ/PokmonCatcher_138.py +++ b/spirit/game/scripts/cards/CZ/PokmonCatcher_138.py @@ -1,15 +1,33 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.trainers import opponent_has_bench + + +async def pokemon_catcher(ctx): + """Flip a coin. If heads, switch 1 of the opponent's Benched Pokemon + with their Active Pokemon.""" + if not (await ctx.flip_coins(1, "Pokémon Catcher"))[0]: + return + bench = ctx.opponent_bench() + if not bench: + return + target = await ctx.choose_pokemon( + bench, "Choose your opponent's new Active Pokémon" + ) + if target is not None: + await ctx.switch_active(ctx.opponent_id, target) + card = ItemCardDef( guid="14cbcefb-fac6-56ca-8a73-9d93e81c7699", key="CZ", name="com.direwolfdigital.cake.data.archetypes.trainer.PokmonCatcher.Name", display_name="Pokémon Catcher", - searchable_by=["Pok\u00c3\u00a9mon Catcher", "Item"], + searchable_by=["Pokémon Catcher", "Item"], subtypes=["Item"], collector_number=138, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=pokemon_catcher, + condition=opponent_has_bench ) diff --git a/spirit/game/scripts/cards/CZ/Potion_139.py b/spirit/game/scripts/cards/CZ/Potion_139.py index 29ddb7e..b536a1c 100644 --- a/spirit/game/scripts/cards/CZ/Potion_139.py +++ b/spirit/game/scripts/cards/CZ/Potion_139.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import heal_item, requires_damaged_pokemon card = ItemCardDef( guid="0673acf4-d7f4-52c1-87cd-67e9eda9f4e1", @@ -11,5 +12,6 @@ card = ItemCardDef( collector_number=139, set_code="CZ", rarity=Rarities.Common, - effect=unimplemented + condition=requires_damaged_pokemon(), + effect=heal_item(30) ) diff --git a/spirit/game/scripts/cards/CZ/ProfessorsResearch_150.py b/spirit/game/scripts/cards/CZ/ProfessorsResearch_150.py index 9649ffa..9540e21 100644 --- a/spirit/game/scripts/cards/CZ/ProfessorsResearch_150.py +++ b/spirit/game/scripts/cards/CZ/ProfessorsResearch_150.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.card_effects.trainers import professors_research +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities card = SupporterCardDef( @@ -11,5 +12,5 @@ card = SupporterCardDef( collector_number=150, set_code="CZ", rarity=Rarities.RareUltra, - effect=unimplemented + effect=professors_research ) diff --git a/spirit/game/scripts/cards/CZ/Purrloin_77.py b/spirit/game/scripts/cards/CZ/Purrloin_77.py index cbbca94..86954f6 100644 --- a/spirit/game/scripts/cards/CZ/Purrloin_77.py +++ b/spirit/game/scripts/cards/CZ/Purrloin_77.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage card = PokemonCardDef( guid="5b63994a-3cb9-56f4-85d5-cb399a8373b6", @@ -24,7 +25,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 1}, damage=10, damage_operator="x", - effect=unimplemented, + effect=flip_damage(coins=3, per_heads=10), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/RadiantCharizard_20.py b/spirit/game/scripts/cards/CZ/RadiantCharizard_20.py index a1342e3..53b7803 100644 --- a/spirit/game/scripts/cards/CZ/RadiantCharizard_20.py +++ b/spirit/game/scripts/cards/CZ/RadiantCharizard_20.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.card_effects.pokemon import ExcitedHeartPassive +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -20,15 +21,15 @@ card = PokemonCardDef( abilities=[ Ability( title="Excited Heart", - game_text="This Pok\u00e9mon's attacks cost Colorless less for each Prize card your opponent has taken.", - effect=unimplemented, + game_text="This Pokémon's attacks cost Colorless less for each Prize card your opponent has taken.", + passive=ExcitedHeartPassive(), ), Attack( title="Combustion Blast", - game_text="During your next turn, this Pok\u00e9mon can't use Combustion Blast.", + game_text="During your next turn, this Pokémon can't use Combustion Blast.", cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 4}, damage=250, - effect=unimplemented, + locks_next_turn=True, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/RadiantCharjabug_51.py b/spirit/game/scripts/cards/CZ/RadiantCharjabug_51.py index 0291ba6..91881bd 100644 --- a/spirit/game/scripts/cards/CZ/RadiantCharjabug_51.py +++ b/spirit/game/scripts/cards/CZ/RadiantCharjabug_51.py @@ -1,5 +1,15 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Triggers, is_pokemon_v from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import snipe_attack + + +async def shocking_block(ctx): + """Whenever any player attaches an Energy from hand to their Pokemon V, put 2 damage counters on it.""" + receiver = ctx.energy_receiver + if receiver is None or not is_pokemon_v(receiver.archetype_id): + return + await ctx.deal_damage(20, target=receiver, apply_modifiers=False, as_counters=True) + card = PokemonCardDef( guid="b6f25036-ac03-5190-ba5e-f4f448c4528c", @@ -21,13 +31,14 @@ card = PokemonCardDef( Ability( title="Shocking Block", game_text="Whenever any player attaches an Energy card from their hand to 1 of their Pok\u00e9mon V, put 2 damage counters on that Pok\u00e9mon.", - effect=unimplemented, + trigger=Triggers.ON_ENERGY_ATTACHED, + effect=shocking_block, ), Attack( title="Linear Attack", game_text="This attack does 30 damage to 1 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.LIGHTNING: 1}, - effect=unimplemented, + effect=snipe_attack(30, pool="any", count=1, side="opponent"), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/RadiantEternatus_105.py b/spirit/game/scripts/cards/CZ/RadiantEternatus_105.py index 5cd3fca..21aa8f2 100644 --- a/spirit/game/scripts/cards/CZ/RadiantEternatus_105.py +++ b/spirit/game/scripts/cards/CZ/RadiantEternatus_105.py @@ -1,5 +1,24 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Triggers from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import search_to_bench +from spirit.game.card_effects.trainers import is_pokemon_vmax +from spirit.game.session.effects import is_pokemon_card + + +def _is_vmax(card): + return is_pokemon_card(card) and is_pokemon_vmax(card.archetype_id) + + +async def climactic_gate(ctx): + """You may search up to 2 Pokemon VMAX to the Bench; using it ends the turn.""" + if not await ctx.ask_yes_no( + "Search your deck for up to 2 Pokémon VMAX and put them onto " + "your Bench? If you do, your turn ends." + ): + return + await search_to_bench(predicate=_is_vmax, count=2)(ctx) + ctx.ends_turn = True + card = PokemonCardDef( guid="d799e888-b9fa-5bb8-b022-acc672128675", @@ -20,7 +39,8 @@ card = PokemonCardDef( Ability( title="Climactic Gate", game_text="When you play this Pok\u00e9mon from your hand onto your Bench during your turn, you may search your deck for up to 2 Pok\u00e9mon VMAX and put them onto your Bench. Then, shuffle your deck. If you use this Ability, your turn ends.", - effect=unimplemented, + trigger=Triggers.ON_PLAY, + effect=climactic_gate, ), Attack( title="Power Beam", diff --git a/spirit/game/scripts/cards/CZ/Raihan_140.py b/spirit/game/scripts/cards/CZ/Raihan_140.py index e4ffad8..a85c4d0 100644 --- a/spirit/game/scripts/cards/CZ/Raihan_140.py +++ b/spirit/game/scripts/cards/CZ/Raihan_140.py @@ -1,5 +1,21 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import attach_from_discard +from spirit.game.card_effects.trainers import is_basic_energy_card + + +async def _raihan_search(ctx, picks): + found = await ctx.search_deck( + count=1, minimum=0, + prompt="Search your deck for a card and put it into your hand.", + ) + await ctx.put_in_hand(found, reveal=False) + await ctx.shuffle_deck() + + +def _raihan_condition(board, player_id): + return bool(board.turn_state.kos_by_attack_last_turn.get(player_id)) + card = SupporterCardDef( guid="cc0d13f8-938c-5a84-96d8-e9cc96b22bb1", @@ -11,5 +27,10 @@ card = SupporterCardDef( collector_number=140, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented + condition=_raihan_condition, + effect=attach_from_discard( + predicate=is_basic_energy_card, count=1, target="choice", + prompt="Attach a basic Energy card from your discard pile to 1 of your Pokémon.", + then=_raihan_search, + ), ) diff --git a/spirit/game/scripts/cards/CZ/RareCandy_141.py b/spirit/game/scripts/cards/CZ/RareCandy_141.py index 0afd292..3138428 100644 --- a/spirit/game/scripts/cards/CZ/RareCandy_141.py +++ b/spirit/game/scripts/cards/CZ/RareCandy_141.py @@ -1,5 +1,50 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented -from spirit.game.attributes import Rarities +from spirit.game.data_utils import ItemCardDef, evolves_from +from spirit.game.attributes import Rarities, AttrID, PokemonStage +from spirit.game.session.effects import is_basic_pokemon, is_pokemon_card + + +def _stage2_matches(hand_cards, logic_name): + return [ + c for c in hand_cards + if is_pokemon_card(c) + and c.get_attribute(AttrID.STAGE) == PokemonStage.STAGE2.value + and evolves_from(c.archetype_id, logic_name) + ] + + +def _turn_eligible_basics(board, player_id): + turn_state = getattr(board, "turn_state", None) + if turn_state is None: + return [] + return [ + p for p in board.pokemon_in_play(player_id) + if is_basic_pokemon(p) and turn_state.may_evolve_target(p.entity_id) + ] + + +def _rare_candy_condition(board, player_id): + return bool(_turn_eligible_basics(board, player_id)) + + +async def _rare_candy(ctx): + """Choose a Basic Pokemon in play; if you have a Stage 2 in hand that evolves from it, put that card onto it, skipping the Stage 1.""" + candidates = _turn_eligible_basics(ctx.board, ctx.player_id) + if not candidates: + return + target = await ctx.choose_pokemon(candidates, "Choose a Basic Pokémon in play") + if target is None: + return + logic_name = target.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) + stage2_hand = _stage2_matches(ctx.hand(), logic_name) if logic_name else [] + if not stage2_hand: + return + picks = await ctx.choose_cards( + stage2_hand, 1, prompt="Choose a Stage 2 Pokémon to evolve into", + ) + if not picks: + return + await ctx.evolve_pokemon(target, picks[0]) + card = ItemCardDef( guid="963902d0-48a5-507a-ae11-816234a0cd76", @@ -11,5 +56,6 @@ card = ItemCardDef( collector_number=141, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=_rare_candy, + condition=_rare_candy_condition, ) diff --git a/spirit/game/scripts/cards/CZ/RayquazaVMAX_101.py b/spirit/game/scripts/cards/CZ/RayquazaVMAX_101.py index e6f753e..bee5c46 100644 --- a/spirit/game/scripts/cards/CZ/RayquazaVMAX_101.py +++ b/spirit/game/scripts/cards/CZ/RayquazaVMAX_101.py @@ -1,5 +1,39 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID +from spirit.game.card_effects.trainers import is_basic_energy_card + + +async def azure_pulse(ctx): + """Once per turn: you may discard your hand and draw 3 cards.""" + if not await ctx.ask_yes_no("Discard your hand and draw 3 cards?"): + return + await ctx.discard_cards(ctx.hand()) + await ctx.draw_cards(3) + + +def _is_basic_type(card, type_value): + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return is_basic_energy_card(card) and type_value in types + + +async def max_burst(ctx): + """You may discard any amount of basic Fire or Lightning Energy from + this Pokemon; +80 damage for each card discarded this way.""" + attached = [ + e for e in ctx.attached_energies(ctx.attacker) + if _is_basic_type(e, PokemonTypes.FIRE.value) + or _is_basic_type(e, PokemonTypes.LIGHTNING.value) + ] + picks = [] + if attached: + picks = await ctx.choose_cards( + attached, len(attached), minimum=0, + prompt="Discard any amount of basic Fire or Lightning Energy from this Pokémon.", + ) + if picks: + await ctx.discard_cards(picks) + await ctx.deal_damage(20 + 80 * len(picks)) + card = PokemonCardDef( guid="c902b0f4-1b29-5b8a-a6f5-80d8b80e4639", @@ -21,15 +55,16 @@ card = PokemonCardDef( Ability( title="Azure Pulse", game_text="Once during your turn, you may discard your hand and draw 3 cards.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + effect=azure_pulse, ), Attack( title="Max Burst", - game_text="You may discard any amount of basic Fire Energy or any amount of basic Lightning Energy from this Pok\u00e9mon. This attack does 80 more damage for each card you discarded in this way.", + game_text="You may discard any amount of basic Fire Energy or any amount of basic Lightning Energy from this Pokémon. This attack does 80 more damage for each card you discarded in this way.", cost={PokemonTypes.FIRE: 1, PokemonTypes.LIGHTNING: 1}, damage=20, damage_operator="+", - effect=unimplemented, + effect=max_burst, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/RayquazaVMAX_102.py b/spirit/game/scripts/cards/CZ/RayquazaVMAX_102.py index ba2379e..06a17bb 100644 --- a/spirit/game/scripts/cards/CZ/RayquazaVMAX_102.py +++ b/spirit/game/scripts/cards/CZ/RayquazaVMAX_102.py @@ -1,5 +1,39 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID +from spirit.game.card_effects.trainers import is_basic_energy_card + + +async def azure_pulse(ctx): + """Once per turn: you may discard your hand and draw 3 cards.""" + if not await ctx.ask_yes_no("Discard your hand and draw 3 cards?"): + return + await ctx.discard_cards(ctx.hand()) + await ctx.draw_cards(3) + + +def _is_basic_type(card, type_value): + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return is_basic_energy_card(card) and type_value in types + + +async def max_burst(ctx): + """You may discard any amount of basic Fire or Lightning Energy from + this Pokemon; +80 damage for each card discarded this way.""" + attached = [ + e for e in ctx.attached_energies(ctx.attacker) + if _is_basic_type(e, PokemonTypes.FIRE.value) + or _is_basic_type(e, PokemonTypes.LIGHTNING.value) + ] + picks = [] + if attached: + picks = await ctx.choose_cards( + attached, len(attached), minimum=0, + prompt="Discard any amount of basic Fire or Lightning Energy from this Pokémon.", + ) + if picks: + await ctx.discard_cards(picks) + await ctx.deal_damage(20 + 80 * len(picks)) + card = PokemonCardDef( guid="42d2e5b0-e92a-5d96-9e6b-429164987df7", @@ -21,15 +55,16 @@ card = PokemonCardDef( Ability( title="Azure Pulse", game_text="Once during your turn, you may discard your hand and draw 3 cards.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + effect=azure_pulse, ), Attack( title="Max Burst", - game_text="You may discard any amount of basic Fire Energy or any amount of basic Lightning Energy from this Pok\u00e9mon. This attack does 80 more damage for each card you discarded in this way.", + game_text="You may discard any amount of basic Fire Energy or any amount of basic Lightning Energy from this Pokémon. This attack does 80 more damage for each card you discarded in this way.", cost={PokemonTypes.FIRE: 1, PokemonTypes.LIGHTNING: 1}, damage=20, damage_operator="+", - effect=unimplemented, + effect=max_burst, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/RayquazaV_100.py b/spirit/game/scripts/cards/CZ/RayquazaV_100.py index 8c5a454..a4e6ea8 100644 --- a/spirit/game/scripts/cards/CZ/RayquazaV_100.py +++ b/spirit/game/scripts/cards/CZ/RayquazaV_100.py @@ -1,5 +1,40 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import mill_attack +from spirit.game.card_effects.trainers import is_basic_energy_card +from spirit.game.card_effects.pokemon import energy_provides_type + + +async def spiral_burst(ctx): + """20, +80 for each basic Fire or basic Lightning Energy discarded from + this Pokémon (up to 2 of one type).""" + attacker = ctx.attacker + attached = ctx.attached_energies(attacker) + fire = [c for c in attached if is_basic_energy_card(c) + and energy_provides_type(c, PokemonTypes.FIRE.value)] + lightning = [c for c in attached if is_basic_energy_card(c) + and energy_provides_type(c, PokemonTypes.LIGHTNING.value)] + discarded = [] + if fire and lightning: + choice = await ctx.choose( + "Choose up to 2 Energy to discard from this Pokémon:", + ["Basic Fire Energy", "Basic Lightning Energy"], + ) + pool = fire if choice == 0 else lightning + discarded = await ctx.choose_cards( + pool, min(2, len(pool)), minimum=0, + prompt="Choose up to 2 Energy to discard", + ) + elif fire or lightning: + pool = fire or lightning + discarded = await ctx.choose_cards( + pool, min(2, len(pool)), minimum=0, + prompt="Choose up to 2 Energy to discard", + ) + if discarded: + await ctx.discard_cards(discarded) + await ctx.deal_damage(20 + 80 * len(discarded)) + card = PokemonCardDef( guid="cab0d1d1-5756-59e7-8693-34e498b89845", @@ -22,15 +57,15 @@ card = PokemonCardDef( game_text="Discard the top 2 cards of your deck.", cost={PokemonTypes.LIGHTNING: 1}, damage=40, - effect=unimplemented, + effect=mill_attack(2, opponent=False), ), Attack( title="Spiral Burst", - game_text="You may discard up to 2 basic Fire Energy or up to 2 basic Lightning Energy from this Pok\u00e9mon. This attack does 80 more damage for each card you discarded in this way.", + game_text="You may discard up to 2 basic Fire Energy or up to 2 basic Lightning Energy from this Pokémon. This attack does 80 more damage for each card you discarded in this way.", cost={PokemonTypes.FIRE: 1, PokemonTypes.LIGHTNING: 1}, damage=20, damage_operator="+", - effect=unimplemented, + effect=spiral_burst, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/RegigigasVSTAR_114.py b/spirit/game/scripts/cards/CZ/RegigigasVSTAR_114.py index b93f815..6ee9a94 100644 --- a/spirit/game/scripts/cards/CZ/RegigigasVSTAR_114.py +++ b/spirit/game/scripts/cards/CZ/RegigigasVSTAR_114.py @@ -1,5 +1,37 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import lock_all_attacks +from spirit.game.session.effects import full_stack + + +def _star_guardian_condition(board, player_id, pokemon): + opponent_id = next(p for p in board.player_ids if p != player_id) + prizes = board.find_player_area(opponent_id, "prizePile") + bench = board.find_player_area(opponent_id, "bench") + return bool(prizes) and len(prizes.children) == 1 and bool(bench and bench.children) + + +async def star_guardian(ctx): + """VSTAR Power: if the opponent has exactly 1 Prize left, you may make + them discard 1 of their Benched Pokemon and all attached cards.""" + if not await ctx.ask_yes_no( + "Choose 1 of your opponent's Benched Pokémon? They discard that " + "Pokémon and all attached cards." + ): + return + target = await ctx.choose_pokemon( + ctx.opponent_bench(), "Choose 1 of your opponent's Benched Pokémon" + ) + if target is None: + return + await ctx.discard_cards(full_stack(target)) + + +async def giga_impact(ctx): + """230. During your next turn, this Pokemon can't attack.""" + await ctx.deal_damage() + lock_all_attacks(ctx, ctx.attacker) + card = PokemonCardDef( guid="0e4aa482-8295-5b75-b012-daf58cb96397", @@ -22,14 +54,16 @@ card = PokemonCardDef( Ability( title="Star Guardian", game_text="During your turn, if your opponent has exactly 1 Prize card remaining, you may choose 1 of your opponent's Benched Pok\u00e9mon. They discard that Pok\u00e9mon and all attached cards. (You can't use more than 1 VSTAR Power in a game.)", - effect=unimplemented, + vstar=True, + condition=_star_guardian_condition, + effect=star_guardian, ), Attack( title="Giga Impact", game_text="During your next turn, this Pok\u00e9mon can't attack.", cost={PokemonTypes.COLORLESS: 3}, damage=230, - effect=unimplemented, + effect=giga_impact, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/RegigigasV_113.py b/spirit/game/scripts/cards/CZ/RegigigasV_113.py index 61e3e95..327c997 100644 --- a/spirit/game/scripts/cards/CZ/RegigigasV_113.py +++ b/spirit/game/scripts/cards/CZ/RegigigasV_113.py @@ -1,5 +1,11 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import damage_per, damage_counters_on + + +async def _confuse_self(ctx): + await ctx.apply_special_condition(ctx.attacker, SpecialConditions.CONFUSED) + card = PokemonCardDef( guid="1da3abc5-6669-50ab-bbe9-e80900566bdc", @@ -29,7 +35,8 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 3}, damage=100, damage_operator="+", - effect=unimplemented, + effect=damage_per(damage_counters_on("self"), 10, base=100, + also=_confuse_self), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/RescueCarrier_142.py b/spirit/game/scripts/cards/CZ/RescueCarrier_142.py index 36de496..52af5df 100644 --- a/spirit/game/scripts/cards/CZ/RescueCarrier_142.py +++ b/spirit/game/scripts/cards/CZ/RescueCarrier_142.py @@ -1,5 +1,12 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented -from spirit.game.attributes import Rarities +from spirit.game.data_utils import ItemCardDef +from spirit.game.attributes import Rarities, AttrID +from spirit.game.session.effects import is_pokemon_card +from spirit.game.card_effects.support_common import recover_from_discard, requires_discard + + +def _low_hp_pokemon(card): + return is_pokemon_card(card) and (card.get_attribute(AttrID.HP, 999) or 0) <= 90 + card = ItemCardDef( guid="0205ee20-a659-547a-9f16-f0eb96fe47d6", @@ -11,5 +18,9 @@ card = ItemCardDef( collector_number=142, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=recover_from_discard( + _low_hp_pokemon, count=2, reveal=False, + prompt="Put up to 2 Pokémon with 90 HP or less from your discard pile into your hand", + ), + condition=requires_discard(_low_hp_pokemon), ) diff --git a/spirit/game/scripts/cards/CZ/Rockruff_73.py b/spirit/game/scripts/cards/CZ/Rockruff_73.py index 5fdfab7..4da5503 100644 --- a/spirit/game/scripts/cards/CZ/Rockruff_73.py +++ b/spirit/game/scripts/cards/CZ/Rockruff_73.py @@ -1,6 +1,23 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def invite_out(ctx): + """Flip a coin. If heads, switch 1 of your opponent's Benched Pokémon + with their Active Pokémon.""" + heads = (await ctx.flip_coins(1, "Invite Out"))[0] + if not heads: + return + active = ctx.opponent_active() + bench = ctx.opponent_bench() + if active is None or not bench or ctx.effects_blocked(active): + return + target = await ctx.choose_pokemon( + bench, "Choose the opponent's new Active Pokémon" + ) or bench[0] + await ctx.switch_active(ctx.opponent_id, target) + + card = PokemonCardDef( guid="498a4795-0542-573f-96e5-54f951ec5e36", key="CZ", @@ -22,7 +39,7 @@ card = PokemonCardDef( title="Invite Out", game_text="Flip a coin. If heads, switch 1 of your opponent's Benched Pok\u00e9mon with their Active Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=invite_out, ), Attack( title="Smash Kick", diff --git a/spirit/game/scripts/cards/CZ/RotomVSTAR_46.py b/spirit/game/scripts/cards/CZ/RotomVSTAR_46.py index c716db1..38dd146 100644 --- a/spirit/game/scripts/cards/CZ/RotomVSTAR_46.py +++ b/spirit/game/scripts/cards/CZ/RotomVSTAR_46.py @@ -1,5 +1,41 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID, TrainerType + + +def _is_pokemon_tool_card(card): + return card.get_attribute(AttrID.TRAINER_TYPE) in ( + TrainerType.POKEMON_TOOL.value, TrainerType.POKEMON_TOOL_F.value, + ) + + +def _conversion_star_condition(board, player_id, pokemon): + hand = board.find_player_area(player_id, "hand") + return bool(hand) and bool(hand.children) + + +async def conversion_star(ctx): + """VSTAR Power: discard any number of cards from your hand, then draw that many.""" + hand = ctx.hand() + discarded = await ctx.discard_from_hand( + len(hand), minimum=0, prompt="Discard any number of cards from your hand", + ) if hand else [] + if discarded: + await ctx.draw_cards(len(discarded)) + + +async def scrap_pulse(ctx): + """80. Put any number of Pokémon Tool cards from discard into the Lost Zone; +40 damage per card moved this way.""" + tools = [c for c in ctx.discard_pile() if _is_pokemon_tool_card(c)] + picks = [] + if tools: + picks = await ctx.choose_cards( + tools, len(tools), minimum=0, + prompt="Put any number of Pokémon Tool cards from your discard pile in the Lost Zone.", + ) + if picks: + await ctx.move_to_lost_zone(picks) + await ctx.deal_damage(80 + 40 * len(picks)) + card = PokemonCardDef( guid="cbd69b89-554e-5b7b-96c8-bbc2d3da9271", @@ -22,7 +58,10 @@ card = PokemonCardDef( Ability( title="Conversion Star", game_text="During your turn, you may use this Ability. Discard any number of cards from your hand. Then, draw that many cards. (You can't use more than 1 VSTAR Power in a game.)", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + vstar=True, + condition=_conversion_star_condition, + effect=conversion_star, ), Attack( title="Scrap Pulse", @@ -30,7 +69,7 @@ card = PokemonCardDef( cost={PokemonTypes.LIGHTNING: 2}, damage=80, damage_operator="+", - effect=unimplemented, + effect=scrap_pulse, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/RotomV_45.py b/spirit/game/scripts/cards/CZ/RotomV_45.py index 54ecf1a..970be48 100644 --- a/spirit/game/scripts/cards/CZ/RotomV_45.py +++ b/spirit/game/scripts/cards/CZ/RotomV_45.py @@ -1,5 +1,31 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID, TrainerType + + +def _is_pokemon_tool_card(card): + return card.get_attribute(AttrID.TRAINER_TYPE) in ( + TrainerType.POKEMON_TOOL.value, TrainerType.POKEMON_TOOL_F.value, + ) + + +async def instant_charge(ctx): + """Draw 3 cards.""" + await ctx.draw_cards(3) + + +async def scrap_short(ctx): + """40. Put any number of Pokémon Tool cards from discard into the Lost Zone; +40 damage per card moved this way.""" + tools = [c for c in ctx.discard_pile() if _is_pokemon_tool_card(c)] + picks = [] + if tools: + picks = await ctx.choose_cards( + tools, len(tools), minimum=0, + prompt="Put any number of Pokémon Tool cards from your discard pile in the Lost Zone.", + ) + if picks: + await ctx.move_to_lost_zone(picks) + await ctx.deal_damage(40 + 40 * len(picks)) + card = PokemonCardDef( guid="0633fc5d-4359-5595-b7b4-529f83a182ce", @@ -21,15 +47,17 @@ card = PokemonCardDef( Ability( title="Instant Charge", game_text="Once during your turn, you may draw 3 cards. If you use this Ability, your turn ends.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + ends_turn=True, + effect=instant_charge, ), Attack( title="Scrap Short", - game_text="Put any number of Pok\u00e9mon Tool cards from your discard pile in the Lost Zone. This attack does 40 more damage for each card you put in the Lost Zone in this way.", + game_text="Put any number of Pokémon Tool cards from your discard pile in the Lost Zone. This attack does 40 more damage for each card you put in the Lost Zone in this way.", cost={PokemonTypes.LIGHTNING: 2}, damage=40, damage_operator="+", - effect=unimplemented, + effect=scrap_short, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Salandit_27.py b/spirit/game/scripts/cards/CZ/Salandit_27.py index dcfd1f3..6af399e 100644 --- a/spirit/game/scripts/cards/CZ/Salandit_27.py +++ b/spirit/game/scripts/cards/CZ/Salandit_27.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import search_to_bench card = PokemonCardDef( guid="7e4f8f3a-88e9-549c-820f-fa5c1fc82134", @@ -22,7 +23,7 @@ card = PokemonCardDef( title="Call for Family", game_text="Search your deck for a Basic Pok\u00e9mon and put it onto your Bench. Then, shuffle your deck.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=search_to_bench(), ), Attack( title="Scratch", diff --git a/spirit/game/scripts/cards/CZ/Salazzle_28.py b/spirit/game/scripts/cards/CZ/Salazzle_28.py index b35e809..b8db483 100644 --- a/spirit/game/scripts/cards/CZ/Salazzle_28.py +++ b/spirit/game/scripts/cards/CZ/Salazzle_28.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, SpecialConditions +from spirit.game.card_effects.attacks_common import condition_attack card = PokemonCardDef( guid="abc20637-a966-5e78-ba8d-1a14787347c1", @@ -24,14 +25,14 @@ card = PokemonCardDef( game_text="Your opponent's Active Pok\u00e9mon is now Confused.", cost={PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=condition_attack(SpecialConditions.CONFUSED), ), Attack( title="Super Singe", game_text="Your opponent's Active Pok\u00e9mon is now Burned.", cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 1}, damage=60, - effect=unimplemented, + effect=condition_attack(SpecialConditions.BURNED), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Scizor_86.py b/spirit/game/scripts/cards/CZ/Scizor_86.py index b7db4a1..0baf3a2 100644 --- a/spirit/game/scripts/cards/CZ/Scizor_86.py +++ b/spirit/game/scripts/cards/CZ/Scizor_86.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_bonus, bonus_if, active_is +from spirit.game.session.effects import is_basic_pokemon card = PokemonCardDef( guid="66f9da28-828f-5625-8278-048a8d4908bd", @@ -26,7 +28,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 1}, damage=30, damage_operator="+", - effect=unimplemented, + effect=flip_bonus(30), ), Attack( title="Dangerous Claws", @@ -34,7 +36,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 3}, damage=80, damage_operator="+", - effect=unimplemented, + effect=bonus_if(active_is(is_basic_pokemon), 80), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Scyther_6.py b/spirit/game/scripts/cards/CZ/Scyther_6.py index f08ccf0..9005f92 100644 --- a/spirit/game/scripts/cards/CZ/Scyther_6.py +++ b/spirit/game/scripts/cards/CZ/Scyther_6.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import discard_opponent_energy_attack card = PokemonCardDef( guid="b6551aa8-2839-5327-a3a3-d59bb2425325", @@ -23,7 +24,7 @@ card = PokemonCardDef( game_text="Discard a Special Energy from your opponent's Active Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 1}, damage=10, - effect=unimplemented, + effect=discard_opponent_energy_attack(count=1, special_only=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Shaymin_115.py b/spirit/game/scripts/cards/CZ/Shaymin_115.py index e811174..8d5ac76 100644 --- a/spirit/game/scripts/cards/CZ/Shaymin_115.py +++ b/spirit/game/scripts/cards/CZ/Shaymin_115.py @@ -1,5 +1,7 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import recover_from_discard +from spirit.game.card_effects.trainers import is_energy_card card = PokemonCardDef( guid="39282b0d-10bb-5d36-82e9-e1cf9b108c56", @@ -23,7 +25,10 @@ card = PokemonCardDef( title="Gather Flowers", game_text="Shuffle up to 2 Energy cards from your discard pile into your deck.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=recover_from_discard( + predicate=is_energy_card, count=2, to="deck_shuffle", + prompt="Choose up to 2 Energy cards to shuffle into your deck.", + ), ), Attack( title="Rear Kick", diff --git a/spirit/game/scripts/cards/CZ/Shinx_40.py b/spirit/game/scripts/cards/CZ/Shinx_40.py index 9e46efd..170f36a 100644 --- a/spirit/game/scripts/cards/CZ/Shinx_40.py +++ b/spirit/game/scripts/cards/CZ/Shinx_40.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack card = PokemonCardDef( guid="7ce453e5-44ab-509c-9390-5814f08eb066", @@ -23,7 +24,7 @@ card = PokemonCardDef( game_text="This Pok\u00e9mon also does 10 damage to itself.", cost={PokemonTypes.LIGHTNING: 1}, damage=30, - effect=unimplemented, + effect=recoil_attack(10), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/SimisearVSTAR_23.py b/spirit/game/scripts/cards/CZ/SimisearVSTAR_23.py index 07e3779..a6b2f05 100644 --- a/spirit/game/scripts/cards/CZ/SimisearVSTAR_23.py +++ b/spirit/game/scripts/cards/CZ/SimisearVSTAR_23.py @@ -1,5 +1,22 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per, count_discard +from spirit.game.card_effects.trainers import is_energy_card + + +async def fireball_fever(ctx): + """You may discard up to 5 cards from the top of your deck: 40 more damage each.""" + top = ctx.deck_top(5) + picks = [] + if top: + picks = await ctx.choose_cards( + top, len(top), minimum=0, display_cards=top, + prompt="Choose up to 5 cards to discard from the top of your deck", + ) + if picks: + await ctx.discard_cards(picks) + await ctx.deal_damage(40 + 40 * len(picks)) + card = PokemonCardDef( guid="b5ae9386-ac08-5865-a660-4a767e26f2a6", @@ -25,7 +42,7 @@ card = PokemonCardDef( cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 2}, damage=40, damage_operator="+", - effect=unimplemented, + effect=fireball_fever, ), Attack( title="Ember Star", @@ -33,7 +50,8 @@ card = PokemonCardDef( cost={PokemonTypes.FIRE: 1}, damage=30, damage_operator="x", - effect=unimplemented, + vstar=True, + effect=damage_per(count_discard("mine", pred=is_energy_card), 30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/SimisearV_22.py b/spirit/game/scripts/cards/CZ/SimisearV_22.py index d53b112..e63be72 100644 --- a/spirit/game/scripts/cards/CZ/SimisearV_22.py +++ b/spirit/game/scripts/cards/CZ/SimisearV_22.py @@ -1,5 +1,22 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per, count_energy +from spirit.game.card_effects.support_common import distribute_energy +from spirit.game.card_effects.trainers import is_basic_energy_card + + +async def bursting_power(ctx): + await ctx.deal_damage() + energies = [c for c in ctx.hand() if is_basic_energy_card(c)] + if not energies: + return + picks = await ctx.choose_cards( + energies, 2, minimum=0, + prompt="Choose up to 2 basic Energy cards to attach to your Pokémon", + ) + if picks: + await distribute_energy(ctx, picks, ctx.my_pokemon_in_play()) + card = PokemonCardDef( guid="d876a79c-0f49-508d-a68d-c883e02733f1", @@ -20,18 +37,18 @@ card = PokemonCardDef( abilities=[ Attack( title="Bursting Power", - game_text="You may attach up to 2 basic Energy cards from your hand to your Pok\u00e9mon in any way you like.", + game_text="You may attach up to 2 basic Energy cards from your hand to your Pokémon in any way you like.", cost={PokemonTypes.FIRE: 1}, damage=20, - effect=unimplemented, + effect=bursting_power, ), Attack( title="Flare Juggling", - game_text="This attack does 30 more damage for each Energy attached to your opponent's Active Pok\u00e9mon.", + game_text="This attack does 30 more damage for each Energy attached to your opponent's Active Pokémon.", cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 2}, damage=90, damage_operator="+", - effect=unimplemented, + effect=damage_per(count_energy("defender"), 30, base=90), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/SkySealStone_143.py b/spirit/game/scripts/cards/CZ/SkySealStone_143.py index 2a8bed3..261805a 100644 --- a/spirit/game/scripts/cards/CZ/SkySealStone_143.py +++ b/spirit/game/scripts/cards/CZ/SkySealStone_143.py @@ -1,7 +1,41 @@ -from spirit.game.data_utils import PokemonToolCardDef, unimplemented +from spirit.game.data_utils import ( + Ability, Activations, PokemonToolCardDef, is_pokemon_v, subtypes_for, +) from spirit.game.attributes import Rarities + +def _basic_pokemon_v(attacker): + subs = subtypes_for(attacker.archetype_id) + return "Basic" in subs and is_pokemon_v(attacker.archetype_id) + + +async def star_order(ctx): + """This turn: +1 prize when a Basic Pokemon V's attack damage KOs the opponent's Active VSTAR/VMAX.""" + board = ctx.session.board_state + + def _active_vstar_or_vmax(target): + subs = subtypes_for(target.archetype_id) + return ("VSTAR" in subs or "VMAX" in subs) \ + and board.active_pokemon(target.owning_player_id) is target + + ctx.add_extra_prize_watcher(_basic_pokemon_v, _active_vstar_or_vmax) + + card = PokemonToolCardDef( + granted_abilities=[ + Ability( + "Star Order", + "During your turn, you may use this Ability. During this turn, if " + "your opponent's Active Pok\u00e9mon VSTAR or Active Pok\u00e9mon VMAX is " + "Knocked Out by damage from an attack from your Basic Pok\u00e9mon V, " + "take 1 more Prize card. " + "(You can't use more than 1 VSTAR Power in a game.)", + activation=Activations.ONCE_PER_TURN, + vstar=True, + condition=lambda board, player_id, pokemon: is_pokemon_v(pokemon.archetype_id), + effect=star_order, + ), + ], guid="0ed77619-3741-58ab-baf7-3a65c6d2f529", key="CZ", name="com.direwolfdigital.cake.data.archetypes.trainer.SkySealStone.Name", @@ -11,5 +45,4 @@ card = PokemonToolCardDef( collector_number=143, set_code="CZ", rarity=Rarities.RareHolo, - effect=unimplemented ) diff --git a/spirit/game/scripts/cards/CZ/Solrock_69.py b/spirit/game/scripts/cards/CZ/Solrock_69.py index 8f9e8e0..0d396cf 100644 --- a/spirit/game/scripts/cards/CZ/Solrock_69.py +++ b/spirit/game/scripts/cards/CZ/Solrock_69.py @@ -1,5 +1,44 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID, CardType + +LUNATONE_NAME = "com.direwolfdigital.cake.data.archetypes.pokemon.Lunatone.Name" + + +def _is_psychic_energy_card(card): + types = card.get_attribute(AttrID.POKEMON_TYPES) or [] + return card.get_attribute(AttrID.CARD_TYPE) == CardType.ENERGY.value \ + and PokemonTypes.PSYCHIC.value in types + + +def _my_lunatones(pokemon_list): + return [p for p in pokemon_list + if p.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) == LUNATONE_NAME] + + +def sun_energy_condition(board, player_id, pokemon): + if not _my_lunatones(board.pokemon_in_play(player_id)): + return False + discard = board.find_player_area(player_id, "discard") + return bool(discard) and any(_is_psychic_energy_card(c) for c in discard.children) + + +async def sun_energy(ctx): + lunatones = _my_lunatones(ctx.my_pokemon_in_play()) + cards = [c for c in ctx.discard_pile() if _is_psychic_energy_card(c)] + if not lunatones or not cards: + return + if not await ctx.ask_yes_no( + "Attach a Psychic Energy card from your discard pile to 1 of your Lunatone?" + ): + return + picks = await ctx.choose_cards(cards, 1, prompt="Choose a Psychic Energy card to attach") + if not picks: + return + target = await ctx.choose_pokemon( + lunatones, "Choose a Lunatone to attach the Energy to" + ) or lunatones[0] + await ctx.attach_energy(picks[0], target) + card = PokemonCardDef( guid="31cc27d1-0c76-5d6d-b710-3aea82fdc598", @@ -21,7 +60,9 @@ card = PokemonCardDef( Ability( title="Sun Energy", game_text="Once during your turn, you may attach a Psychic Energy card from your discard pile to 1 of your Lunatone.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + condition=sun_energy_condition, + effect=sun_energy, ), Attack( title="Spinning Attack", @@ -29,4 +70,4 @@ card = PokemonCardDef( damage=50, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Starly_110.py b/spirit/game/scripts/cards/CZ/Starly_110.py index 815e9ac..d14021f 100644 --- a/spirit/game/scripts/cards/CZ/Starly_110.py +++ b/spirit/game/scripts/cards/CZ/Starly_110.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_or_nothing card = PokemonCardDef( guid="d301f037-c6ed-59dc-9e55-2d5121cfee8f", @@ -24,7 +25,7 @@ card = PokemonCardDef( game_text="Flip a coin. If tails, this attack does nothing.", cost={PokemonTypes.COLORLESS: 1}, damage=30, - effect=unimplemented, + effect=flip_or_nothing(), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/StoutlandV_116.py b/spirit/game/scripts/cards/CZ/StoutlandV_116.py index 85200d1..166380f 100644 --- a/spirit/game/scripts/cards/CZ/StoutlandV_116.py +++ b/spirit/game/scripts/cards/CZ/StoutlandV_116.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.card_effects.pokemon import double_dip_fangs, wild_tackle +from spirit.game.data_utils import PokemonCardDef, Attack from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -20,17 +21,17 @@ card = PokemonCardDef( abilities=[ Attack( title="Double Dip Fangs", - game_text="If your opponent's Basic Pok\u00e9mon is Knocked Out by damage from this attack, take 1 more Prize card.", + game_text="If your opponent's Basic Pokémon is Knocked Out by damage from this attack, take 1 more Prize card.", cost={PokemonTypes.COLORLESS: 3}, damage=40, - effect=unimplemented, + effect=double_dip_fangs, ), Attack( title="Wild Tackle", - game_text="This Pok\u00e9mon also does 30 damage to itself.", + game_text="This Pokémon also does 30 damage to itself.", cost={PokemonTypes.COLORLESS: 4}, damage=200, - effect=unimplemented, + effect=wild_tackle, ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Switch_144.py b/spirit/game/scripts/cards/CZ/Switch_144.py index 71793c7..730dbbc 100644 --- a/spirit/game/scripts/cards/CZ/Switch_144.py +++ b/spirit/game/scripts/cards/CZ/Switch_144.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.trainers import player_has_bench, switch +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities card = ItemCardDef( @@ -11,5 +12,6 @@ card = ItemCardDef( collector_number=144, set_code="CZ", rarity=Rarities.Common, - effect=unimplemented + effect=switch, + condition=player_has_bench ) diff --git a/spirit/game/scripts/cards/CZ/Tangrowth_5.py b/spirit/game/scripts/cards/CZ/Tangrowth_5.py index aa508d0..5c3bb3f 100644 --- a/spirit/game/scripts/cards/CZ/Tangrowth_5.py +++ b/spirit/game/scripts/cards/CZ/Tangrowth_5.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.support_common import heal_attack card = PokemonCardDef( guid="0972f524-f2e8-53b6-9f18-083660780922", @@ -24,7 +25,7 @@ card = PokemonCardDef( game_text="Heal 30 damage from this Pok\u00e9mon.", cost={PokemonTypes.GRASS: 1, PokemonTypes.COLORLESS: 1}, damage=50, - effect=unimplemented, + effect=heal_attack(30, target="self"), ), Attack( title="Hammer In", diff --git a/spirit/game/scripts/cards/CZ/TapuLele_64.py b/spirit/game/scripts/cards/CZ/TapuLele_64.py index 0294e15..c8f767f 100644 --- a/spirit/game/scripts/cards/CZ/TapuLele_64.py +++ b/spirit/game/scripts/cards/CZ/TapuLele_64.py @@ -1,5 +1,19 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per +from spirit.game.card_effects.support_common import heal_attack +from spirit.game.session.legal_actions import energy_provided_count + + +def _both_actives_energy(ctx) -> int: + total = 0 + for pokemon in (ctx.my_active(), ctx.opponent_active()): + if pokemon is None: + continue + for energy in ctx.attached_energies(pokemon): + total += energy_provided_count(energy) + return total + card = PokemonCardDef( guid="74defd7b-ac79-59df-a51d-9f8e3803f68a", @@ -24,14 +38,14 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 2}, damage=20, damage_operator="x", - effect=unimplemented, + effect=damage_per(_both_actives_energy, 20), ), Attack( title="Spiral Drain", game_text="Heal 30 damage from this Pok\u00e9mon.", cost={PokemonTypes.PSYCHIC: 2, PokemonTypes.COLORLESS: 1}, damage=100, - effect=unimplemented, + effect=heal_attack(amount=30, target="self"), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Tauros_106.py b/spirit/game/scripts/cards/CZ/Tauros_106.py index f471550..91fd836 100644 --- a/spirit/game/scripts/cards/CZ/Tauros_106.py +++ b/spirit/game/scripts/cards/CZ/Tauros_106.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, has_damage card = PokemonCardDef( guid="526514b5-22a1-5a59-9466-a59108ab2d91", @@ -27,7 +28,8 @@ card = PokemonCardDef( title="Adrena-Tackle", game_text="If this Pok\u00e9mon has no damage counters on it, this attack does nothing.", cost={PokemonTypes.COLORLESS: 3}, - effect=unimplemented, + damage=180, + effect=bonus_if(has_damage("self"), 0, else_nothing=True), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/TrekkingShoes_145.py b/spirit/game/scripts/cards/CZ/TrekkingShoes_145.py index cca90eb..0e9c7dc 100644 --- a/spirit/game/scripts/cards/CZ/TrekkingShoes_145.py +++ b/spirit/game/scripts/cards/CZ/TrekkingShoes_145.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.trainers import trekking_shoes, deck_nonempty card = ItemCardDef( guid="907ef9c5-8fe9-5606-9442-e02595c3a5ea", @@ -11,5 +12,6 @@ card = ItemCardDef( collector_number=145, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + condition=deck_nonempty, + effect=trekking_shoes, ) diff --git a/spirit/game/scripts/cards/CZ/UltraBall_146.py b/spirit/game/scripts/cards/CZ/UltraBall_146.py index f821158..7d29a48 100644 --- a/spirit/game/scripts/cards/CZ/UltraBall_146.py +++ b/spirit/game/scripts/cards/CZ/UltraBall_146.py @@ -1,4 +1,5 @@ -from spirit.game.data_utils import ItemCardDef, unimplemented +from spirit.game.card_effects.trainers import hand_size_at_least, ultra_ball +from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities card = ItemCardDef( @@ -11,5 +12,6 @@ card = ItemCardDef( collector_number=146, set_code="CZ", rarity=Rarities.Uncommon, - effect=unimplemented + effect=ultra_ball, + condition=hand_size_at_least(3) ) diff --git a/spirit/game/scripts/cards/CZ/Volcanion_26.py b/spirit/game/scripts/cards/CZ/Volcanion_26.py index a26ff92..1e01e22 100644 --- a/spirit/game/scripts/cards/CZ/Volcanion_26.py +++ b/spirit/game/scripts/cards/CZ/Volcanion_26.py @@ -1,5 +1,8 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, count_energy + +_WATER_ENERGY_ON_SELF = count_energy("self", energy_type=PokemonTypes.WATER) card = PokemonCardDef( guid="201678ff-c7bd-5a5c-8318-b16d195b297a", @@ -29,7 +32,7 @@ card = PokemonCardDef( cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1}, damage=80, damage_operator="+", - effect=unimplemented, + effect=bonus_if(lambda ctx: _WATER_ENERGY_ON_SELF(ctx) > 0, 80), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Volcarona_25.py b/spirit/game/scripts/cards/CZ/Volcarona_25.py index 86b063d..6de6f4b 100644 --- a/spirit/game/scripts/cards/CZ/Volcarona_25.py +++ b/spirit/game/scripts/cards/CZ/Volcarona_25.py @@ -1,4 +1,4 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -29,7 +29,7 @@ card = PokemonCardDef( game_text="During your next turn, this Pok\u00e9mon can't attack.", cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 1}, damage=120, - effect=unimplemented, + locks_next_turn=True, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Volo_151.py b/spirit/game/scripts/cards/CZ/Volo_151.py index a7c52ec..370d2c3 100644 --- a/spirit/game/scripts/cards/CZ/Volo_151.py +++ b/spirit/game/scripts/cards/CZ/Volo_151.py @@ -1,5 +1,27 @@ -from spirit.game.data_utils import SupporterCardDef, unimplemented +from spirit.game.data_utils import SupporterCardDef, is_pokemon_v from spirit.game.attributes import Rarities +from spirit.game.models.board import PokemonEntity +from spirit.game.session.effects import full_stack + + +def _volo_condition(board, player_id): + bench = board.find_player_area(player_id, "bench") + return bool(bench) and any( + isinstance(c, PokemonEntity) and is_pokemon_v(c.archetype_id) + for c in bench.children + ) + + +async def volo(ctx): + """Discard 1 of your Benched Pokemon V and all attached cards.""" + candidates = [p for p in ctx.my_bench() if is_pokemon_v(p.archetype_id)] + target = await ctx.choose_pokemon( + candidates, "Choose 1 of your Benched Pokémon V to discard" + ) + if target is None: + return + await ctx.discard_cards(full_stack(target)) + card = SupporterCardDef( guid="73176109-6ee0-5c9a-b4e9-87bf5447d447", @@ -11,5 +33,6 @@ card = SupporterCardDef( collector_number=151, set_code="CZ", rarity=Rarities.RareUltra, - effect=unimplemented + condition=_volo_condition, + effect=volo, ) diff --git a/spirit/game/scripts/cards/CZ/Wailmer_31.py b/spirit/game/scripts/cards/CZ/Wailmer_31.py index 195f066..7c5ccf0 100644 --- a/spirit/game/scripts/cards/CZ/Wailmer_31.py +++ b/spirit/game/scripts/cards/CZ/Wailmer_31.py @@ -1,6 +1,12 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def nap(ctx): + """Heal 30 damage from this Pokémon.""" + await ctx.heal(30, ctx.attacker) + + card = PokemonCardDef( guid="b0250e76-ad91-5b56-aaab-bb7ecc361b83", key="CZ", @@ -22,7 +28,7 @@ card = PokemonCardDef( title="Nap", game_text="Heal 30 damage from this Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=nap, ), Attack( title="Water Gun", diff --git a/spirit/game/scripts/cards/CZ/Wailord_32.py b/spirit/game/scripts/cards/CZ/Wailord_32.py index 2c9585d..7593033 100644 --- a/spirit/game/scripts/cards/CZ/Wailord_32.py +++ b/spirit/game/scripts/cards/CZ/Wailord_32.py @@ -1,6 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def bubble_drain(ctx): + """80. Heal 30 damage from this Pokémon.""" + await ctx.deal_damage() + await ctx.heal(30, ctx.attacker) + + card = PokemonCardDef( guid="67c92019-6ea5-5152-8523-2b3fed96ba98", key="CZ", @@ -24,7 +31,7 @@ card = PokemonCardDef( game_text="Heal 30 damage from this Pok\u00e9mon.", cost={PokemonTypes.COLORLESS: 3}, damage=80, - effect=unimplemented, + effect=bubble_drain, ), Attack( title="Heavy Impact", diff --git a/spirit/game/scripts/cards/CZ/Wooloo_121.py b/spirit/game/scripts/cards/CZ/Wooloo_121.py index b738a5c..81175d0 100644 --- a/spirit/game/scripts/cards/CZ/Wooloo_121.py +++ b/spirit/game/scripts/cards/CZ/Wooloo_121.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage card = PokemonCardDef( guid="adc2afc3-dca5-52ee-bb98-17ba2e65953b", @@ -24,7 +25,7 @@ card = PokemonCardDef( cost={PokemonTypes.COLORLESS: 1}, damage=30, damage_operator="x", - effect=unimplemented, + effect=flip_damage(until_tails=True, per_heads=30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Yanma_8.py b/spirit/game/scripts/cards/CZ/Yanma_8.py index 7c0ea2b..6994d58 100644 --- a/spirit/game/scripts/cards/CZ/Yanma_8.py +++ b/spirit/game/scripts/cards/CZ/Yanma_8.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_all_opponents card = PokemonCardDef( guid="c468307d-1d94-50b5-8009-902658cf8cc7", @@ -22,7 +23,7 @@ card = PokemonCardDef( title="Swoop Across", game_text="This attack does 10 damage to each of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + effect=damage_all_opponents(10), ), Attack( title="Cutting Wind", diff --git a/spirit/game/scripts/cards/CZ/Yanmega_9.py b/spirit/game/scripts/cards/CZ/Yanmega_9.py index 0632ac6..4cdbab0 100644 --- a/spirit/game/scripts/cards/CZ/Yanmega_9.py +++ b/spirit/game/scripts/cards/CZ/Yanmega_9.py @@ -1,5 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import snipe_attack, lock_all_attacks + + +async def jet_wing(ctx): + """160. During your next turn, this Pokémon can't attack.""" + await ctx.deal_damage() + lock_all_attacks(ctx, ctx.attacker) + card = PokemonCardDef( guid="50f16030-82a5-5fc2-96a1-28a3d3ccd7c6", @@ -24,14 +32,14 @@ card = PokemonCardDef( game_text="This attack also does 20 damage to 1 of your opponent's Benched Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)", cost={PokemonTypes.COLORLESS: 1}, damage=20, - effect=unimplemented, + effect=snipe_attack(20, pool="bench", count=1, side="opponent", also_base=True), ), Attack( title="Jet Wing", game_text="During your next turn, this Pok\u00e9mon can't attack.", cost={PokemonTypes.GRASS: 2, PokemonTypes.COLORLESS: 1}, damage=160, - effect=unimplemented, + effect=jet_wing, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZacianVSTAR_96.py b/spirit/game/scripts/cards/CZ/ZacianVSTAR_96.py index 275b567..50f4fec 100644 --- a/spirit/game/scripts/cards/CZ/ZacianVSTAR_96.py +++ b/spirit/game/scripts/cards/CZ/ZacianVSTAR_96.py @@ -1,5 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack + + +async def break_edge(ctx): + """200. Ignores Weakness, Resistance, and effects on the opponent's Active.""" + await ctx.deal_damage(ignore_weakness=True, ignore_resistance=True, + ignore_target_effects=True) + card = PokemonCardDef( guid="affcb5be-2ebd-587a-9d04-76a65a38e2a2", @@ -25,14 +33,15 @@ card = PokemonCardDef( game_text="This attack's damage isn't affected by Weakness or Resistance, or by any effects on your opponent's Active Pok\u00e9mon.", cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 1}, damage=200, - effect=unimplemented, + effect=break_edge, ), Attack( title="Sword Star", game_text="This Pok\u00e9mon also does 30 damage to itself. (You can't use more than 1 VSTAR Power in a game.)", cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 2}, damage=310, - effect=unimplemented, + vstar=True, + effect=recoil_attack(30), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZacianV_95.py b/spirit/game/scripts/cards/CZ/ZacianV_95.py index b29119b..9f43ef1 100644 --- a/spirit/game/scripts/cards/CZ/ZacianV_95.py +++ b/spirit/game/scripts/cards/CZ/ZacianV_95.py @@ -1,5 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if, defender_is_vmax + + +async def piercing_strike(ctx): + """40. Ignores Weakness, Resistance, and effects on the opponent's Active.""" + await ctx.deal_damage(ignore_weakness=True, ignore_resistance=True, + ignore_target_effects=True) + card = PokemonCardDef( guid="731c79e4-cce6-5993-b2a2-1bdd01e1b6a0", @@ -24,7 +32,7 @@ card = PokemonCardDef( game_text="This attack's damage isn't affected by Weakness or Resistance, or by any effects on your opponent's Active Pok\u00e9mon.", cost={PokemonTypes.METAL: 1}, damage=40, - effect=unimplemented, + effect=piercing_strike, ), Attack( title="Behemoth Blade", @@ -32,7 +40,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 1}, damage=100, damage_operator="+", - effect=unimplemented, + effect=bonus_if(defender_is_vmax, 160), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Zacian_94.py b/spirit/game/scripts/cards/CZ/Zacian_94.py index 4a5e7f3..3f5b973 100644 --- a/spirit/game/scripts/cards/CZ/Zacian_94.py +++ b/spirit/game/scripts/cards/CZ/Zacian_94.py @@ -1,6 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def battle_legion(ctx): + """20+10 per Benched Pokemon; ignores Weakness and effects on the Active.""" + amount = 20 + 10 * len(ctx.my_bench()) + await ctx.deal_damage(amount, ignore_weakness=True, ignore_target_effects=True) + + card = PokemonCardDef( guid="5fb73471-acf0-589f-85db-f5100a774a14", key="CZ", @@ -25,7 +32,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 1}, damage=20, damage_operator="+", - effect=unimplemented, + effect=battle_legion, ), Attack( title="Slicing Blade", diff --git a/spirit/game/scripts/cards/CZ/ZamazentaVSTAR_99.py b/spirit/game/scripts/cards/CZ/ZamazentaVSTAR_99.py index 91dfbe2..8bbfb90 100644 --- a/spirit/game/scripts/cards/CZ/ZamazentaVSTAR_99.py +++ b/spirit/game/scripts/cards/CZ/ZamazentaVSTAR_99.py @@ -1,5 +1,22 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import lock_all_attacks +from spirit.game.card_effects.passives_common import takes_less_passive + + +async def shield_star(ctx): + """VSTAR Power: during opponent's next turn, your Pokemon take 100 less + damage from their attacks (after W/R); covers Pokemon played later too.""" + shield = takes_less_passive(100, protects="team") + for pokemon in ctx.my_pokemon_in_play(): + ctx.add_passive_through_opponents_turn(pokemon, shield) + + +async def giga_impact(ctx): + """220. During your next turn, this Pokemon can't attack.""" + await ctx.deal_damage() + lock_all_attacks(ctx, ctx.attacker) + card = PokemonCardDef( guid="ad9a6f18-d2fa-5436-a862-93ac3087a39b", @@ -23,14 +40,16 @@ card = PokemonCardDef( Ability( title="Shield Star", game_text="During your turn, you may use this Ability. During your opponent's next turn, all of your Pok\u00e9mon take 100 less damage from attacks from your opponent's Pok\u00e9mon (after applying Weakness and Resistance). (This includes Pok\u00e9mon that come into play during this turn or during your opponent's next turn.) (You can't use more than 1 VSTAR Power in a game.)", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + vstar=True, + effect=shield_star, ), Attack( title="Giga Impact", game_text="During your next turn, this Pok\u00e9mon can't attack.", cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 1}, damage=220, - effect=unimplemented, + effect=giga_impact, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZamazentaV_98.py b/spirit/game/scripts/cards/CZ/ZamazentaV_98.py index a76abd1..49576ae 100644 --- a/spirit/game/scripts/cards/CZ/ZamazentaV_98.py +++ b/spirit/game/scripts/cards/CZ/ZamazentaV_98.py @@ -1,5 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability, Activations from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import damage_per, count_prizes_taken + + +async def regal_stance(ctx): + """Once per turn: discard your hand and draw 5 cards. Ends your turn.""" + await ctx.discard_cards(ctx.hand()) + await ctx.draw_cards(5) + card = PokemonCardDef( guid="5b024777-3cb0-519c-8ab2-73f338a044e6", @@ -22,7 +30,9 @@ card = PokemonCardDef( Ability( title="Regal Stance", game_text="Once during your turn, you may discard your hand and draw 5 cards. If you use this Ability, your turn ends.", - effect=unimplemented, + activation=Activations.ONCE_PER_TURN, + ends_turn=True, + effect=regal_stance, ), Attack( title="Revenge Blast", @@ -30,7 +40,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 1, PokemonTypes.COLORLESS: 2}, damage=120, damage_operator="+", - effect=unimplemented, + effect=damage_per(count_prizes_taken("opponent"), 30, base=120), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/CZ/Zamazenta_97.py b/spirit/game/scripts/cards/CZ/Zamazenta_97.py index aad8648..1ff4f45 100644 --- a/spirit/game/scripts/cards/CZ/Zamazenta_97.py +++ b/spirit/game/scripts/cards/CZ/Zamazenta_97.py @@ -1,5 +1,21 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import bonus_if +from spirit.game.card_effects.pokemon import is_energy_card +from spirit.game.session.passives import Passive + + +class MetalShieldPassive(Passive): + def modify_damage_taken(self, calc, carrier): + if not (calc.is_attack and calc.is_opposing and calc.target is carrier): + return + if any(is_energy_card(c) for c in carrier.children): + calc.amount = max(0, calc.amount - 30) + + +def _kos_suffered_last_turn(ctx): + return ctx.kos_suffered_last_turn() > 0 + card = PokemonCardDef( guid="c7a2fabe-fc1b-5678-bbf6-dc7149451a87", @@ -22,7 +38,7 @@ card = PokemonCardDef( Ability( title="Metal Shield", game_text="If this Pok\u00e9mon has any Energy attached, it takes 30 less damage from attacks (after applying Weakness and Resistance).", - effect=unimplemented, + passive=MetalShieldPassive(), ), Attack( title="Retaliate", @@ -30,7 +46,7 @@ card = PokemonCardDef( cost={PokemonTypes.METAL: 2, PokemonTypes.COLORLESS: 1}, damage=100, damage_operator="+", - effect=unimplemented, + effect=bonus_if(_kos_suffered_last_turn, 120), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Zarude_16.py b/spirit/game/scripts/cards/CZ/Zarude_16.py index 2b0c208..d9900fb 100644 --- a/spirit/game/scripts/cards/CZ/Zarude_16.py +++ b/spirit/game/scripts/cards/CZ/Zarude_16.py @@ -1,5 +1,20 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import flip_damage + + +async def drag_off(ctx): + """Switch 1 of the opponent's Benched Pokémon with their Active; 20 + damage to the new Active Pokémon.""" + bench = ctx.opponent_bench() + if not bench: + return + target = await ctx.choose_pokemon( + bench, "Choose your opponent's new Active Pokémon" + ) + if target is not None and await ctx.switch_active(ctx.opponent_id, target): + await ctx.deal_damage(20, target=target) + card = PokemonCardDef( guid="55826f80-1168-573d-a0e6-2487a0e1ca90", @@ -22,7 +37,7 @@ card = PokemonCardDef( title="Drag Off", game_text="Switch 1 of your opponent's Benched Pok\u00e9mon with their Active Pok\u00e9mon. This attack does 20 damage to the new Active Pok\u00e9mon.", cost={PokemonTypes.GRASS: 1}, - effect=unimplemented, + effect=drag_off, ), Attack( title="Triple Whip", @@ -30,7 +45,7 @@ card = PokemonCardDef( cost={PokemonTypes.GRASS: 2}, damage=70, damage_operator="x", - effect=unimplemented, + effect=flip_damage(coins=3, per_heads=70), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZeraoraVMAX_54.py b/spirit/game/scripts/cards/CZ/ZeraoraVMAX_54.py index 1b40ac7..a249327 100644 --- a/spirit/game/scripts/cards/CZ/ZeraoraVMAX_54.py +++ b/spirit/game/scripts/cards/CZ/ZeraoraVMAX_54.py @@ -1,5 +1,13 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented -from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.data_utils import PokemonCardDef, Attack, Ability +from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID +from spirit.game.card_effects.attacks_common import damage_per, count_in_play, self_energy_discard_attack + + +def _has_ability(pokemon) -> bool: + abilities = pokemon.get_attribute(AttrID.PIE_ABILITIES) or [] + return any(isinstance(e, dict) and e.get("abilityType") in ("PokeAbility", "PokePower") + for e in abilities) + card = PokemonCardDef( guid="68e23f1f-d284-5f60-88ca-fc8115f93aea", @@ -25,14 +33,14 @@ card = PokemonCardDef( cost={PokemonTypes.LIGHTNING: 2}, damage=60, damage_operator="x", - effect=unimplemented, + effect=damage_per(count_in_play("opponent", _has_ability), 60), ), Attack( title="Max Fist", game_text="Discard 2 Energy from this Pok\u00e9mon.", cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1}, damage=240, - effect=unimplemented, + effect=self_energy_discard_attack(count=2), ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZeraoraVSTAR_55.py b/spirit/game/scripts/cards/CZ/ZeraoraVSTAR_55.py index 111fd9e..f3a48af 100644 --- a/spirit/game/scripts/cards/CZ/ZeraoraVSTAR_55.py +++ b/spirit/game/scripts/cards/CZ/ZeraoraVSTAR_55.py @@ -1,6 +1,26 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities + +async def crushing_beat(ctx): + """190. You may discard a Stadium in play.""" + await ctx.deal_damage() + if ctx.stadium_in_play() and await ctx.ask_yes_no("Discard the Stadium in play?"): + await ctx.discard_stadium() + + +async def lightning_storm_star(ctx): + """VSTAR Power: choose an opponent's Pokemon 4 times, 60 raw damage each.""" + for _ in range(4): + targets = ctx.opponent_pokemon_in_play() + if not targets: + break + target = await ctx.choose_pokemon( + targets, "Choose 1 of your opponent's Pokémon" + ) or targets[0] + await ctx.deal_damage(60, target=target, apply_modifiers=False) + + card = PokemonCardDef( guid="f9d18688-fd81-5039-a1a3-5b897cbc9bad", key="CZ", @@ -24,13 +44,14 @@ card = PokemonCardDef( game_text="You may discard a Stadium in play.", cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1}, damage=190, - effect=unimplemented, + effect=crushing_beat, ), Attack( title="Lightning Storm Star", game_text="Choose 1 of your opponent's Pok\u00e9mon 4 times. (You can choose the same Pok\u00e9mon more than once.) For each time you chose a Pok\u00e9mon, do 60 damage to it. This damage isn't affected by Weakness or Resistance. (You can't use more than 1 VSTAR Power in a game.)", cost={PokemonTypes.LIGHTNING: 3, PokemonTypes.COLORLESS: 1}, - effect=unimplemented, + vstar=True, + effect=lightning_storm_star, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/ZeraoraV_53.py b/spirit/game/scripts/cards/CZ/ZeraoraV_53.py index c3b93d9..07678f0 100644 --- a/spirit/game/scripts/cards/CZ/ZeraoraV_53.py +++ b/spirit/game/scripts/cards/CZ/ZeraoraV_53.py @@ -1,4 +1,4 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack, Ability from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities card = PokemonCardDef( @@ -28,7 +28,7 @@ card = PokemonCardDef( game_text="During your next turn, this Pok\u00e9mon can't attack.", cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1}, damage=190, - effect=unimplemented, + locks_next_turn=True, ), ], ) \ No newline at end of file diff --git a/spirit/game/scripts/cards/CZ/Zeraora_52.py b/spirit/game/scripts/cards/CZ/Zeraora_52.py index 33f01b1..eda23e4 100644 --- a/spirit/game/scripts/cards/CZ/Zeraora_52.py +++ b/spirit/game/scripts/cards/CZ/Zeraora_52.py @@ -1,5 +1,6 @@ -from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented +from spirit.game.data_utils import PokemonCardDef, Attack from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities +from spirit.game.card_effects.attacks_common import recoil_attack card = PokemonCardDef( guid="5b5f104b-acdc-5ff8-a933-36c815d33749", @@ -20,10 +21,10 @@ card = PokemonCardDef( abilities=[ Attack( title="Wild Charge", - game_text="This Pok\u00e9mon also does 20 damage to itself.", + game_text="This Pokémon also does 20 damage to itself.", cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 1}, damage=70, - effect=unimplemented, + effect=recoil_attack(20), ), ], -) \ No newline at end of file +) diff --git a/spirit/game/scripts/cards/SWSH1/HyperPotion_166.py b/spirit/game/scripts/cards/SWSH1/HyperPotion_166.py index 746aa80..dccdcba 100644 --- a/spirit/game/scripts/cards/SWSH1/HyperPotion_166.py +++ b/spirit/game/scripts/cards/SWSH1/HyperPotion_166.py @@ -1,16 +1,29 @@ from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities -from spirit.game.card_effects.support_common import requires_in_play from spirit.game.models.board import BoardState +from spirit.game.session.legal_actions import energy_provided_count -def _has_two_energy(pokemon): - return len(BoardState.attached_energies(pokemon)) >= 2 +def _has_two_energy(board, pokemon): + return sum( + energy_provided_count(energy, board) + for energy in BoardState.attached_energies(pokemon) + ) >= 2 + + +def _hyper_potion_playable(board, player_id): + return any( + _has_two_energy(board, pokemon) + for pokemon in board.pokemon_in_play(player_id) + ) async def hyper_potion(ctx): """Heal 120 from 1 of your Pokemon with >=2 Energy attached; if healed, discard 2 Energy from it.""" - candidates = [p for p in ctx.my_pokemon_in_play() if _has_two_energy(p)] + candidates = [ + pokemon for pokemon in ctx.my_pokemon_in_play() + if _has_two_energy(ctx.board, pokemon) + ] if not candidates: return target = await ctx.choose_pokemon( @@ -20,7 +33,9 @@ async def hyper_potion(ctx): return healed = await ctx.heal(120, target) if healed: - await ctx.discard_energy_from(target, 2, prompt="Discard 2 Energy") + await ctx.discard_energy_units_from( + target, 2, prompt="Discard 2 Energy" + ) card = ItemCardDef( @@ -34,5 +49,5 @@ card = ItemCardDef( set_code="SWSH1", rarity=Rarities.Uncommon, effect=hyper_potion, - condition=requires_in_play(_has_two_energy), + condition=_hyper_potion_playable, ) diff --git a/spirit/game/scripts/cards/SWSH12/Brandon_151.py b/spirit/game/scripts/cards/SWSH12/Brandon_151.py index aac5ab1..86c32d1 100644 --- a/spirit/game/scripts/cards/SWSH12/Brandon_151.py +++ b/spirit/game/scripts/cards/SWSH12/Brandon_151.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import brandon, brandon_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=151, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=brandon, + condition=brandon_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/Brandon_188.py b/spirit/game/scripts/cards/SWSH12/Brandon_188.py index b66ef86..b9a8415 100644 --- a/spirit/game/scripts/cards/SWSH12/Brandon_188.py +++ b/spirit/game/scripts/cards/SWSH12/Brandon_188.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import brandon, brandon_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=188, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=brandon, + condition=brandon_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/Brandon_203.py b/spirit/game/scripts/cards/SWSH12/Brandon_203.py index 9e0ff55..1ca0a44 100644 --- a/spirit/game/scripts/cards/SWSH12/Brandon_203.py +++ b/spirit/game/scripts/cards/SWSH12/Brandon_203.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import brandon, brandon_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=203, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=brandon, + condition=brandon_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/Candice_152.py b/spirit/game/scripts/cards/SWSH12/Candice_152.py index 3b20ddf..76d0ffe 100644 --- a/spirit/game/scripts/cards/SWSH12/Candice_152.py +++ b/spirit/game/scripts/cards/SWSH12/Candice_152.py @@ -1,3 +1,5 @@ +from spirit.game.card_effects.support_common import look_at_top +from spirit.game.card_effects.trainers import candice_predicate from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +12,9 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=152, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=look_at_top( + 7, take=7, predicate=candice_predicate, rest="shuffle", minimum=0, + prompt="Choose any number of Water Pokémon and Water Energy cards to put into your hand", + ) ) diff --git a/spirit/game/scripts/cards/SWSH12/Candice_189.py b/spirit/game/scripts/cards/SWSH12/Candice_189.py index a63ccf6..d58296a 100644 --- a/spirit/game/scripts/cards/SWSH12/Candice_189.py +++ b/spirit/game/scripts/cards/SWSH12/Candice_189.py @@ -1,3 +1,5 @@ +from spirit.game.card_effects.support_common import look_at_top +from spirit.game.card_effects.trainers import candice_predicate from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +12,9 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=189, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=look_at_top( + 7, take=7, predicate=candice_predicate, rest="shuffle", minimum=0, + prompt="Choose any number of Water Pokémon and Water Energy cards to put into your hand", + ) ) diff --git a/spirit/game/scripts/cards/SWSH12/Candice_204.py b/spirit/game/scripts/cards/SWSH12/Candice_204.py index ab26496..e253ac8 100644 --- a/spirit/game/scripts/cards/SWSH12/Candice_204.py +++ b/spirit/game/scripts/cards/SWSH12/Candice_204.py @@ -1,3 +1,5 @@ +from spirit.game.card_effects.support_common import look_at_top +from spirit.game.card_effects.trainers import candice_predicate from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +12,9 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=204, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=look_at_top( + 7, take=7, predicate=candice_predicate, rest="shuffle", minimum=0, + prompt="Choose any number of Water Pokémon and Water Energy cards to put into your hand", + ) ) diff --git a/spirit/game/scripts/cards/SWSH12/CapturingAroma_153.py b/spirit/game/scripts/cards/SWSH12/CapturingAroma_153.py index 65845c0..f9297cf 100644 --- a/spirit/game/scripts/cards/SWSH12/CapturingAroma_153.py +++ b/spirit/game/scripts/cards/SWSH12/CapturingAroma_153.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import capturing_aroma from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = ItemCardDef( subtypes=["Item"], collector_number=153, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=capturing_aroma ) diff --git a/spirit/game/scripts/cards/SWSH12/EarthenSealStone_154.py b/spirit/game/scripts/cards/SWSH12/EarthenSealStone_154.py index 4aca597..5fcc06c 100644 --- a/spirit/game/scripts/cards/SWSH12/EarthenSealStone_154.py +++ b/spirit/game/scripts/cards/SWSH12/EarthenSealStone_154.py @@ -1,13 +1,24 @@ -from spirit.game.data_utils import PokemonToolCardDef -from spirit.game.attributes import Rarities +from spirit.game.card_effects.trainers import star_gravity +from spirit.game.data_utils import Attack, PokemonToolCardDef, is_pokemon_v +from spirit.game.attributes import PokemonTypes, Rarities card = PokemonToolCardDef( + granted_abilities=[ + Attack( + title="Star Gravity", + game_text="Put damage counters on each of your opponent's Pokémon V until its remaining HP is 100. (You can't use more than 1 VSTAR Power in a game.)", + cost={PokemonTypes.COLORLESS: 3}, + vstar=True, + condition=lambda board, player_id, pokemon: is_pokemon_v(pokemon.archetype_id), + effect=star_gravity, + ), + ], guid="a616915a-5b34-5ccc-ba24-f2753f8618db", key="SWSH12", name="com.direwolfdigital.cake.data.archetypes.trainer.EarthenSealStone.Name", display_name="Earthen Seal Stone", - searchable_by=["Earthen Seal Stone", "Item", "Pok\u00c3\u00a9mon Tool"], - subtypes=["Item", "Pok\u00e9mon Tool"], + searchable_by=["Earthen Seal Stone", "Item", "Pokémon Tool"], + subtypes=["Item", "Pokémon Tool"], collector_number=154, set_code="SWSH12", rarity=Rarities.RareHolo diff --git a/spirit/game/scripts/cards/SWSH12/EmergencyJelly_155.py b/spirit/game/scripts/cards/SWSH12/EmergencyJelly_155.py index 4b9b847..3a67cec 100644 --- a/spirit/game/scripts/cards/SWSH12/EmergencyJelly_155.py +++ b/spirit/game/scripts/cards/SWSH12/EmergencyJelly_155.py @@ -1,13 +1,22 @@ -from spirit.game.data_utils import PokemonToolCardDef +from spirit.game.card_effects.trainers import emergency_jelly +from spirit.game.data_utils import Ability, PokemonToolCardDef, Triggers from spirit.game.attributes import Rarities card = PokemonToolCardDef( + granted_abilities=[ + Ability( + title="Emergency Jelly", + game_text="At the end of each turn, if the Pokémon this card is attached to has 30 HP or less remaining and has any damage counters on it, heal 120 damage from it. If you healed any damage in this way, discard this card.", + trigger=Triggers.BETWEEN_TURNS, + effect=emergency_jelly, + ), + ], guid="f87b4537-160d-59a5-b2a3-d82de56ae563", key="SWSH12", name="com.direwolfdigital.cake.data.archetypes.trainer.EmergencyJelly.Name", display_name="Emergency Jelly", - searchable_by=["Emergency Jelly", "Item", "Pok\u00c3\u00a9mon Tool"], - subtypes=["Item", "Pok\u00e9mon Tool"], + searchable_by=["Emergency Jelly", "Item", "Pokémon Tool"], + subtypes=["Item", "Pokémon Tool"], collector_number=155, set_code="SWSH12", rarity=Rarities.Uncommon diff --git a/spirit/game/scripts/cards/SWSH12/EnergySwitch_212.py b/spirit/game/scripts/cards/SWSH12/EnergySwitch_212.py index b48d64e..fdf6c9f 100644 --- a/spirit/game/scripts/cards/SWSH12/EnergySwitch_212.py +++ b/spirit/game/scripts/cards/SWSH12/EnergySwitch_212.py @@ -1,6 +1,21 @@ +from spirit.game.card_effects.trainers import is_basic_energy_card from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities + +def energy_switch_condition(board, player_id): + pokemon = board.pokemon_in_play(player_id) + if len(pokemon) < 2: + return False + return any(any(is_basic_energy_card(e) for e in p.children) for p in pokemon) + + +async def energy_switch(ctx): + """Move a basic Energy from 1 of your Pokemon to another of your Pokemon.""" + pokemon = ctx.my_pokemon_in_play() + await ctx.move_energy_freely(pokemon, pokemon, predicate=is_basic_energy_card, max_count=1) + + card = ItemCardDef( guid="fc2796ee-150b-596b-886e-38fec26f03b5", key="SWSH12", @@ -10,5 +25,7 @@ card = ItemCardDef( subtypes=["Item"], collector_number=212, set_code="SWSH12", - rarity=Rarities.RareSecret + rarity=Rarities.RareSecret, + condition=energy_switch_condition, + effect=energy_switch ) diff --git a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_157.py b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_157.py index a0b7106..a61df47 100644 --- a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_157.py +++ b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_157.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import bench_has_room, furisode_girl from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=157, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=furisode_girl, + condition=bench_has_room ) diff --git a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_190.py b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_190.py index 08f1e0c..2cf3c80 100644 --- a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_190.py +++ b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_190.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import bench_has_room, furisode_girl from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=190, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=furisode_girl, + condition=bench_has_room ) diff --git a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_205.py b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_205.py index 8d1ab8f..646182c 100644 --- a/spirit/game/scripts/cards/SWSH12/FurisodeGirl_205.py +++ b/spirit/game/scripts/cards/SWSH12/FurisodeGirl_205.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import bench_has_room, furisode_girl from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=205, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=furisode_girl, + condition=bench_has_room ) diff --git a/spirit/game/scripts/cards/SWSH12/GymTrainer_158.py b/spirit/game/scripts/cards/SWSH12/GymTrainer_158.py index 58800bd..9451391 100644 --- a/spirit/game/scripts/cards/SWSH12/GymTrainer_158.py +++ b/spirit/game/scripts/cards/SWSH12/GymTrainer_158.py @@ -1,5 +1,8 @@ from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import conditional_draw + +gym_trainer = conditional_draw(2, 2, lambda ctx: bool(ctx.kos_suffered_last_turn())) card = SupporterCardDef( guid="2ea91b83-cdc7-50d3-ba22-1ff0f692463a", @@ -10,5 +13,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=158, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=gym_trainer ) diff --git a/spirit/game/scripts/cards/SWSH12/GymTrainer_191.py b/spirit/game/scripts/cards/SWSH12/GymTrainer_191.py index 16b3ec4..c5791f7 100644 --- a/spirit/game/scripts/cards/SWSH12/GymTrainer_191.py +++ b/spirit/game/scripts/cards/SWSH12/GymTrainer_191.py @@ -1,5 +1,8 @@ from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities +from spirit.game.card_effects.support_common import conditional_draw + +gym_trainer = conditional_draw(2, 2, lambda ctx: bool(ctx.kos_suffered_last_turn())) card = SupporterCardDef( guid="98a19d72-4605-5821-b9cb-cd6b2d1e82b4", @@ -10,5 +13,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=191, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=gym_trainer ) diff --git a/spirit/game/scripts/cards/SWSH12/Lance_159.py b/spirit/game/scripts/cards/SWSH12/Lance_159.py index 88d44b7..72f4115 100644 --- a/spirit/game/scripts/cards/SWSH12/Lance_159.py +++ b/spirit/game/scripts/cards/SWSH12/Lance_159.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import lance from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=159, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=lance ) diff --git a/spirit/game/scripts/cards/SWSH12/Lance_192.py b/spirit/game/scripts/cards/SWSH12/Lance_192.py index 67347e0..9ed1ec9 100644 --- a/spirit/game/scripts/cards/SWSH12/Lance_192.py +++ b/spirit/game/scripts/cards/SWSH12/Lance_192.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import lance from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=192, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=lance ) diff --git a/spirit/game/scripts/cards/SWSH12/Lance_206.py b/spirit/game/scripts/cards/SWSH12/Lance_206.py index e54f315..3410753 100644 --- a/spirit/game/scripts/cards/SWSH12/Lance_206.py +++ b/spirit/game/scripts/cards/SWSH12/Lance_206.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import lance from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=206, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=lance ) diff --git a/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_160.py b/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_160.py index 35c0c8f..e7b5e55 100644 --- a/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_160.py +++ b/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_160.py @@ -1,13 +1,22 @@ +from spirit.game.card_effects.passives_common import trainer_effect_shield_passive +from spirit.game.card_effects.trainers import ( + leafy_camo_poncho_condition, leafy_camo_poncho_protects, +) from spirit.game.data_utils import PokemonToolCardDef from spirit.game.attributes import Rarities card = PokemonToolCardDef( + passive=trainer_effect_shield_passive( + supporters_only=True, + protects=leafy_camo_poncho_protects, + condition=leafy_camo_poncho_condition, + ), guid="14c6c9a5-bc6e-5f01-bc67-e19b73e759c6", key="SWSH12", name="com.direwolfdigital.cake.data.archetypes.trainer.LeafyCamoPoncho.Name", display_name="Leafy Camo Poncho", - searchable_by=["Leafy Camo Poncho", "Item", "Pok\u00c3\u00a9mon Tool"], - subtypes=["Item", "Pok\u00e9mon Tool"], + searchable_by=["Leafy Camo Poncho", "Item", "Pokémon Tool"], + subtypes=["Item", "Pokémon Tool"], collector_number=160, set_code="SWSH12", rarity=Rarities.Uncommon diff --git a/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_214.py b/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_214.py index d7378c1..5d4788c 100644 --- a/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_214.py +++ b/spirit/game/scripts/cards/SWSH12/LeafyCamoPoncho_214.py @@ -1,13 +1,22 @@ +from spirit.game.card_effects.passives_common import trainer_effect_shield_passive +from spirit.game.card_effects.trainers import ( + leafy_camo_poncho_condition, leafy_camo_poncho_protects, +) from spirit.game.data_utils import PokemonToolCardDef from spirit.game.attributes import Rarities card = PokemonToolCardDef( + passive=trainer_effect_shield_passive( + supporters_only=True, + protects=leafy_camo_poncho_protects, + condition=leafy_camo_poncho_condition, + ), guid="c643bc27-8b1f-54ed-be34-cf41ece44d63", key="SWSH12", name="com.direwolfdigital.cake.data.archetypes.trainer.LeafyCamoPoncho.Name", display_name="Leafy Camo Poncho", - searchable_by=["Leafy Camo Poncho", "Item", "Pok\u00c3\u00a9mon Tool"], - subtypes=["Item", "Pok\u00e9mon Tool"], + searchable_by=["Leafy Camo Poncho", "Item", "Pokémon Tool"], + subtypes=["Item", "Pokémon Tool"], collector_number=214, set_code="SWSH12", rarity=Rarities.RareSecret diff --git a/spirit/game/scripts/cards/SWSH12/PrimordialAltar_161.py b/spirit/game/scripts/cards/SWSH12/PrimordialAltar_161.py index f79b05f..ad99773 100644 --- a/spirit/game/scripts/cards/SWSH12/PrimordialAltar_161.py +++ b/spirit/game/scripts/cards/SWSH12/PrimordialAltar_161.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import PRIMORDIAL_ALTAR_ABILITY from spirit.game.data_utils import StadiumCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = StadiumCardDef( subtypes=["Stadium"], collector_number=161, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + ability=PRIMORDIAL_ALTAR_ABILITY, ) diff --git a/spirit/game/scripts/cards/SWSH12/ProfessorLaventon_162.py b/spirit/game/scripts/cards/SWSH12/ProfessorLaventon_162.py index 3caccf8..1b6375b 100644 --- a/spirit/game/scripts/cards/SWSH12/ProfessorLaventon_162.py +++ b/spirit/game/scripts/cards/SWSH12/ProfessorLaventon_162.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import professor_laventon, professor_laventon_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=162, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=professor_laventon, + condition=professor_laventon_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/QuadStone_163.py b/spirit/game/scripts/cards/SWSH12/QuadStone_163.py index 7ef67c3..86da13b 100644 --- a/spirit/game/scripts/cards/SWSH12/QuadStone_163.py +++ b/spirit/game/scripts/cards/SWSH12/QuadStone_163.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import quad_stone from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = ItemCardDef( subtypes=["Item"], collector_number=163, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=quad_stone ) diff --git a/spirit/game/scripts/cards/SWSH12/Serena_164.py b/spirit/game/scripts/cards/SWSH12/Serena_164.py index 9f7bf9e..9160b94 100644 --- a/spirit/game/scripts/cards/SWSH12/Serena_164.py +++ b/spirit/game/scripts/cards/SWSH12/Serena_164.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import serena, serena_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=164, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=serena, + condition=serena_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/Serena_207.py b/spirit/game/scripts/cards/SWSH12/Serena_207.py index 996d8f8..1a35f3f 100644 --- a/spirit/game/scripts/cards/SWSH12/Serena_207.py +++ b/spirit/game/scripts/cards/SWSH12/Serena_207.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import serena, serena_playable from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,7 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=207, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=serena, + condition=serena_playable ) diff --git a/spirit/game/scripts/cards/SWSH12/Wallace_166.py b/spirit/game/scripts/cards/SWSH12/Wallace_166.py index 30b5237..cfe772e 100644 --- a/spirit/game/scripts/cards/SWSH12/Wallace_166.py +++ b/spirit/game/scripts/cards/SWSH12/Wallace_166.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import wallace from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=166, set_code="SWSH12", - rarity=Rarities.Uncommon + rarity=Rarities.Uncommon, + effect=wallace ) diff --git a/spirit/game/scripts/cards/SWSH12/Wallace_194.py b/spirit/game/scripts/cards/SWSH12/Wallace_194.py index 226a99f..fd38535 100644 --- a/spirit/game/scripts/cards/SWSH12/Wallace_194.py +++ b/spirit/game/scripts/cards/SWSH12/Wallace_194.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import wallace from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=194, set_code="SWSH12", - rarity=Rarities.RareUltra + rarity=Rarities.RareUltra, + effect=wallace ) diff --git a/spirit/game/scripts/cards/SWSH12/Wallace_208.py b/spirit/game/scripts/cards/SWSH12/Wallace_208.py index 178deec..682ea8f 100644 --- a/spirit/game/scripts/cards/SWSH12/Wallace_208.py +++ b/spirit/game/scripts/cards/SWSH12/Wallace_208.py @@ -1,3 +1,4 @@ +from spirit.game.card_effects.trainers import wallace from spirit.game.data_utils import SupporterCardDef from spirit.game.attributes import Rarities @@ -10,5 +11,6 @@ card = SupporterCardDef( subtypes=["Supporter"], collector_number=208, set_code="SWSH12", - rarity=Rarities.RareRainbow + rarity=Rarities.RareRainbow, + effect=wallace ) diff --git a/spirit/game/scripts/cards/SWSH35/HyperPotion_54.py b/spirit/game/scripts/cards/SWSH35/HyperPotion_54.py index 4207410..02e049d 100644 --- a/spirit/game/scripts/cards/SWSH35/HyperPotion_54.py +++ b/spirit/game/scripts/cards/SWSH35/HyperPotion_54.py @@ -1,16 +1,29 @@ from spirit.game.data_utils import ItemCardDef from spirit.game.attributes import Rarities -from spirit.game.card_effects.support_common import requires_in_play from spirit.game.models.board import BoardState +from spirit.game.session.legal_actions import energy_provided_count -def _has_two_energy(pokemon): - return len(BoardState.attached_energies(pokemon)) >= 2 +def _has_two_energy(board, pokemon): + return sum( + energy_provided_count(energy, board) + for energy in BoardState.attached_energies(pokemon) + ) >= 2 + + +def _hyper_potion_playable(board, player_id): + return any( + _has_two_energy(board, pokemon) + for pokemon in board.pokemon_in_play(player_id) + ) async def hyper_potion(ctx): """Heal 120 from 1 of your Pokemon with >=2 Energy attached; if healed, discard 2 Energy from it.""" - candidates = [p for p in ctx.my_pokemon_in_play() if _has_two_energy(p)] + candidates = [ + pokemon for pokemon in ctx.my_pokemon_in_play() + if _has_two_energy(ctx.board, pokemon) + ] if not candidates: return target = await ctx.choose_pokemon( @@ -20,7 +33,9 @@ async def hyper_potion(ctx): return healed = await ctx.heal(120, target) if healed: - await ctx.discard_energy_from(target, 2, prompt="Discard 2 Energy") + await ctx.discard_energy_units_from( + target, 2, prompt="Discard 2 Energy" + ) card = ItemCardDef( @@ -34,5 +49,5 @@ card = ItemCardDef( set_code="SWSH35", rarity=Rarities.Uncommon, effect=hyper_potion, - condition=requires_in_play(_has_two_energy), + condition=_hyper_potion_playable, ) diff --git a/spirit/game/session/effects.py b/spirit/game/session/effects.py index d54384b..2f727ed 100644 --- a/spirit/game/session/effects.py +++ b/spirit/game/session/effects.py @@ -10,7 +10,7 @@ inline before any choreography is flushed. import logging import random from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, cast - +from .legal_actions import energy_provided_count from spirit.game.attributes import ( AbilityTypes, AttrID, @@ -609,6 +609,18 @@ class EffectContext: """Registers a TurnDamageModifier (expires_after_turn None = this turn).""" self.session.turn_state.damage_modifiers.append(mod) + def add_extra_prize_watcher(self, attacker_predicate=None, + target_predicate=None, prizes: int = 1) -> None: + """This-turn bonus-prize watch (Star Order): when this player's attack + KOs by damage a Pokemon passing target_predicate and the attacker + passes attacker_predicate, resolve_knockouts adds `prizes` to the take.""" + self.session.turn_state.extra_prize_watchers.append({ + "player_id": self.player_id, + "attacker_predicate": attacker_predicate, + "target_predicate": target_predicate, + "prizes": prizes, + }) + def add_temporary_passive(self, target, passive, expires_after_turn: Optional[int] = None) -> None: """Attaches an effect-granted passive to `target` (expires_after_turn @@ -1329,6 +1341,53 @@ class EffectContext: await self.discard_cards(picked) return picked + async def discard_energy_units_from( + self, pokemon: PokemonEntity, amount: int, + predicate: Optional[Callable[[CardEntity], bool]] = None, + prompt: str = "Choose Energy to discard", + ) -> List[CardEntity]: + """Discard cards providing at least ``amount`` attached Energy. + + Use this for card text that counts Energy as a provided value (for + example, Hyper Potion's "discard 2 Energy"). ``discard_energy_from`` + remains the physical-card-count primitive for text that says "Energy + cards" or otherwise requires a specific number of card entities. + """ + + energies = [ + energy for energy in self.attached_energies(pokemon) + if predicate is None or predicate(energy) + ] + total = sum( + energy_provided_count(energy, self.board) for energy in energies + ) + if amount <= 0 or total < amount: + return [] + + if total <= amount: + picked = energies + else: + picked_ids = await self.session.prompt_energy_unit_picker( + self.player_id, + self.source.entity_id, + energies, + amount, + prompt, + ) + by_id = {energy.entity_id: energy for energy in energies} + picked = [by_id[entity_id] for entity_id in picked_ids + if entity_id in by_id] + + paid = sum(energy_provided_count(energy, self.board) for energy in picked) + if paid < amount: + logging.warning( + f"[Effects {self.game_id}] Energy payment {amount} resolved " + f"with only {paid}; no cards discarded." + ) + return [] + await self.discard_cards(picked) + return picked + async def move_to_lost_zone(self, cards: List[CardEntity]): """Moves cards to their owner's Lost Zone (a public zone).""" await self._move_to_public_pile(cards, "lostZone") @@ -2174,6 +2233,10 @@ async def resolve_triggered_ability( if ctx_setup is not None: ctx_setup(ctx) await ability.effect(ctx) + # Same rule as activated abilities: a declined "you may" queues no + # messages and must not end the turn (Climactic Gate). + if getattr(ability, "ends_turn", False) and ctx._messages: + ctx.ends_turn = True await _send_ability_brackets(session, ctx, pokemon, ability, _ko_depth=_ko_depth) return ctx diff --git a/spirit/game/session/game_session.py b/spirit/game/session/game_session.py index fcf4518..c4dc480 100644 --- a/spirit/game/session/game_session.py +++ b/spirit/game/session/game_session.py @@ -1350,6 +1350,79 @@ class GameSession: valid, count, min(min_to_select, len(valid)), forced, ) + async def prompt_energy_unit_picker( + self, + player_id: str, + source_entity_id: str, + energies: List[EnergyEntity], + amount: int, + prompt: str = "Choose Energy to discard", + ) -> List[str]: + """Pick attached Energy cards that provide at least ``amount`` Energy. + + This is deliberately distinct from ``prompt_entity_picker``'s card + count. The retreat-cost node tallies each selected card's live + ENERGY_INFO value, so one Double Turbo Energy satisfies an amount of + two while effects that explicitly count Energy *cards* keep using the + ordinary entity picker. + """ + if not energies or amount <= 0: + return [] + + def enough(entity_ids: List[str]) -> bool: + selected = set(entity_ids) + return sum( + energy_provided_count(energy, self.board_state) + for energy in energies if energy.entity_id in selected + ) >= amount + + def automatic_payment() -> List[str]: + # Prefer the fewest physical cards for AI play and malformed-client + # fallback; any overpayment is the whole selected Energy card. + picked: List[str] = [] + paid = 0 + for energy in sorted( + energies, + key=lambda e: energy_provided_count(e, self.board_state), + reverse=True, + ): + picked.append(energy.entity_id) + paid += energy_provided_count(energy, self.board_state) + if paid >= amount: + break + return picked if paid >= amount else [] + + player = self.players[player_id] + if isinstance(player, AIPlayer): + return automatic_payment() + + valid = [energy.entity_id for energy in energies] + node = { + "name": SelectionKind.RETREAT_COST_ENTITY_LIST.value, + "selected": True, + "targetPrompt": {"id": prompt}, + "validTargets": valid, + # The pip tray gates Done on valueToSelect. numberToSelect merely + # caps physical cards, so an amount-2 Double Turbo payment may + # finish after selecting one card. + "numberToSelect": amount, + "minimumToSelect": -1, + "valueToSelect": amount, + "forced": True, + } + picked = await self._run_pick_offer( + player_id, source_entity_id, node, + SelectionKind.RETREAT_COST_ENTITY_LIST.value, + valid, amount, 1, True, + ) + if enough(picked): + return picked + logging.warning( + f"[Session {self.game_id}] Energy payment {amount} underpaid by " + f"{picked}; applying a legal automatic payment." + ) + return automatic_payment() + async def prompt_damage_counter_placement( self, player_id: str, @@ -1582,12 +1655,26 @@ class GameSession: count = prize_value(pokemon.archetype_id) for passive, carrier in passive_pairs: count = passive.modify_prizes_for_knockout(pokemon, ctx, count, carrier) + # This-turn bonus-prize watchers (Star Order): attack-damage KOs + # only, evaluated pre-move so Active-spot predicates still hold. + taker_id = self._opponent_id(owner_id) + if _damage_ko(pokemon) and ctx.attacker.owning_player_id == taker_id: + for watcher in self.turn_state.extra_prize_watchers: + if watcher["player_id"] != taker_id: + continue + attacker_ok = watcher.get("attacker_predicate") + target_ok = watcher.get("target_predicate") + if attacker_ok is not None and not attacker_ok(ctx.attacker): + continue + if target_ok is not None and not target_ok(pokemon): + continue + count += watcher.get("prizes", 1) mode = next( (m for passive, carrier in passive_pairs for m in [passive.prize_destination(pokemon, ctx, carrier)] if m), "hand", ) - prize_plans.append((self._opponent_id(owner_id), max(0, count), mode)) + prize_plans.append((taker_id, max(0, count), mode)) ko_from_attack = _damage_ko(pokemon) \ and ctx.attacker.owning_player_id != owner_id for ally in self.board_state.pokemon_in_play(owner_id): @@ -2744,7 +2831,7 @@ class GameSession: while True: needs_mulligan = [ pid for pid in self._turn_order() - if not board.has_basic_pokemon_in_hand(pid) + if not board.setup_active_candidates(pid) ] if not needs_mulligan: break @@ -3116,7 +3203,7 @@ class GameSession: return False description = entry["selectableAction"]["description"] if description == ACTION_PLAY_POKEMON: - await self._execute_play_basic(player_id, card) + return bool(await self._execute_play_basic(player_id, card)) elif description == ACTION_PLAY_ENERGY: await self._execute_attach_energy(player_id, card, entry, target_ids) elif description == ACTION_ATTACH_TOOL: @@ -3183,16 +3270,19 @@ class GameSession: [card], ) await self.fire_pokemon_benched_triggers(player_id, card) - await self._fire_triggered_abilities(player_id, card, Triggers.ON_PLAY) + return await self._fire_triggered_abilities( + player_id, card, Triggers.ON_PLAY) async def _fire_triggered_abilities(self, player_id: str, card, trigger: str, - ctx_setup=None): + ctx_setup=None) -> bool: """Runs a card's abilities matching `trigger` (on-play, on-evolve, on-knocked-out, between-turns, turn-drawn, taken-as-prize); scans the entity's PIE_ABILITIES plus the definition's declared abilities (trainers carry no PIE slot -- Dream Ball's prize hook). resolve_knockouts already flushed any knockouts the effect caused, - so the extra call here is empty-safe.""" + so the extra call here is empty-safe. Returns True when a resolved + trigger ends the turn (Climactic Gate's "your turn ends").""" + ends_turn = False seen = set() abilities = [] for entry in card.get_attribute(AttrID.PIE_ABILITIES) or []: @@ -3222,6 +3312,9 @@ class GameSession: self.turn_state.used_named_abilities.add(ability.shared_once_per_turn) if trigger_ctx is not None and trigger_ctx.knockouts: await self.resolve_knockouts(trigger_ctx) + if trigger_ctx is not None and trigger_ctx.ends_turn: + ends_turn = True + return ends_turn async def fire_energy_attached_triggers(self, attaching_player_id: str, energy, receiver): @@ -4417,7 +4510,7 @@ class GameSession: try: if isinstance(player, AIPlayer): - basics = board.basic_pokemon_in_hand(player_id) + basics = board.setup_active_candidates(player_id) if basics: await self._place_setup_card(player_id, basics[0].entity_id, active_area) return @@ -4426,7 +4519,7 @@ class GameSession: for _ in range(MAX_SELECTION_RETRIES): if active_area.children: break - basics = board.basic_pokemon_in_hand(player_id) + basics = board.setup_active_candidates(player_id) if not basics: logging.error( f"[Session {self.game_id}] {player.screen_name} has no Basic " diff --git a/spirit/game/session/legal_actions.py b/spirit/game/session/legal_actions.py index f96181f..90dcac6 100644 --- a/spirit/game/session/legal_actions.py +++ b/spirit/game/session/legal_actions.py @@ -155,6 +155,10 @@ class TurnState: # player_id -> attack titles that player declared on THEIR previous turn # ("If 1 of your Pokemon used Yoga Loop during your last turn..."). attack_titles_prev_turn_by_player: Dict[str, List[str]] = field(default_factory=dict) + # This-turn bonus-prize watches (Sky Seal Stone's Star Order): + # {player_id, attacker_predicate, target_predicate, prizes}; consulted by + # resolve_knockouts on attack-damage KOs, cleared every begin_turn. + extra_prize_watchers: List[Dict[str, Any]] = field(default_factory=list) def begin_turn(self, player_id: str, board: Optional[Any] = None): """Advances to the next turn, resets the once-per-turn flags, rotates @@ -207,6 +211,7 @@ class TurnState: if entry[0] >= self.turn_number } self.ignore_target_effects_entities = set() + self.extra_prize_watchers = [] if board is not None: board.temporary_passives = [ tp for tp in (getattr(board, "temporary_passives", None) or []) diff --git a/spirit/tools/effect_smoke.py b/spirit/tools/effect_smoke.py index 59feb43..ef85456 100644 --- a/spirit/tools/effect_smoke.py +++ b/spirit/tools/effect_smoke.py @@ -139,12 +139,17 @@ def pick_filler_basic() -> Any: def pick_filler_item() -> Optional[Any]: - """A vanilla Item card (no effect) so 'other Item in hand' conditions hold.""" + """A plain Item card so 'other Item in hand' conditions hold. + + Exact ItemCardDef only (a Tool/Fossil subclass carries board machinery + that breaks filler roles); its effect never runs, so effect-less defs + merely sort first now that every real Item is scripted. + """ from spirit.game.data_utils import ItemCardDef candidates = [d for d in CARD_DEFS_BY_GUID.values() - if isinstance(d, ItemCardDef) and d.effect is None - and d.condition is None] - candidates.sort(key=lambda d: d.guid) + if type(d) is ItemCardDef and d.condition is None + and getattr(d, "passive", None) is None] + candidates.sort(key=lambda d: (d.effect is not None, d.guid)) return candidates[0] if candidates else None diff --git a/spirit/tools/engine_selftest.py b/spirit/tools/engine_selftest.py index 73fd2fc..c3952fa 100644 --- a/spirit/tools/engine_selftest.py +++ b/spirit/tools/engine_selftest.py @@ -50,6 +50,7 @@ from spirit.game.session.legal_actions import ( ACTION_RETREAT, ACTION_USE_ABILITY, ACTION_USE_ATTACK, + ACTION_USE_TRAINER, TurnState, compute_legal_actions, ) @@ -939,6 +940,61 @@ async def test_modify_energy_provided(): {PokemonTypes.COLORLESS.value} +async def test_hyper_potion_double_turbo_energy(): + hyper_guid = card_loader.cards_by_stem["HyperPotion_166"] + hyper = CARD_DEFS_BY_GUID[hyper_guid.lower()] + alt_guid = card_loader.cards_by_stem["HyperPotion_54"] + alt_hyper = CARD_DEFS_BY_GUID[alt_guid.lower()] + rig = Rig(hyper, FILLER, ENERGY_GUIDS, ITEM) + e = rig.setup("trainer") + board = rig.board + active = e["p1_active"] + trainer = e["target"] + energies = board.attached_energies(active) + assert len(energies) >= 2 + + # Leave one physical Energy card attached. As a basic Energy it provides + # only one unit, so neither printing may be offered. + energy = energies[0] + deck = board.find_player_area(P1, "deck") + for extra in energies[1:]: + board.move_card(extra.entity_id, deck.entity_id) + + def hyper_is_offered(): + return any( + entry["entityID"] == trainer.entity_id + and entry["selectableAction"]["description"] == ACTION_USE_TRAINER + for entry in compute_legal_actions( + board, rig.session.turn_state, P1, rig.session.game_id + ) + ) + + assert not hyper_is_offered(), \ + "one Energy card providing one Energy must not enable Hyper Potion" + assert not alt_hyper.condition(board, P1) + + # Give that same physical card Double Turbo's [[C, C]] provided value. + # Legality and payment must count two Energy units, not two entities. + energy.set_attribute( + AttrID.ENERGY_INFO, + {"options": [[PokemonTypes.COLORLESS.value, + PokemonTypes.COLORLESS.value]]}, + ) + assert hyper_is_offered(), \ + "one Double Turbo Energy must enable Hyper Potion" + assert alt_hyper.condition(board, P1), \ + "both Hyper Potion printings must share provided-Energy legality" + + max_hp = passives.effective_max_hp(board, active) + active.set_attribute(AttrID.HP, max_hp - 50) + await resolve_trainer_effect(rig.session, P1, trainer) + assert active.get_attribute(AttrID.HP) == max_hp, \ + "Hyper Potion heals the damaged target" + discard = board.find_player_area(P1, "discard") + assert energy.parent is discard, \ + "the one Double Turbo card pays and is discarded as two Energy" + + async def test_on_move_to_active_once(): rig, e = new_rig() session = rig.session @@ -1845,6 +1901,79 @@ async def test_usable_despite_conditions(): ABILITIES_BY_ID.pop(slow_id, None) +async def test_on_play_trigger_ends_turn(): + rig, e = new_rig() + pokemon = e["p1_active"] + saved = pokemon.get_attribute(AttrID.PIE_ABILITIES) + + async def _draw_one(c): + await c.draw_cards(1) + + ability = Ability("Test Gate", trigger=Triggers.ON_PLAY, + ends_turn=True, effect=_draw_one) + aid = register_ability(ability) + try: + pokemon.set_attribute(AttrID.PIE_ABILITIES, [{"abilityID": aid}]) + over = await rig.session._fire_triggered_abilities( + P1, pokemon, Triggers.ON_PLAY) + assert over is True, "resolved ON_PLAY ends_turn trigger must end the turn" + finally: + ABILITIES_BY_ID.pop(aid, None) + + async def _noop(c): + pass + + ability = Ability("Test Gate Declined", trigger=Triggers.ON_PLAY, + ends_turn=True, effect=_noop) + aid = register_ability(ability) + try: + pokemon.set_attribute(AttrID.PIE_ABILITIES, [{"abilityID": aid}]) + over = await rig.session._fire_triggered_abilities( + P1, pokemon, Triggers.ON_PLAY) + assert over is False, "a declined/no-op trigger must not end the turn" + finally: + ABILITIES_BY_ID.pop(aid, None) + pokemon.set_attribute(AttrID.PIE_ABILITIES, saved) + + +async def test_extra_prize_watchers(): + rig, e = new_rig() + board, ts = rig.board, rig.session.turn_state + neutralize_wr(e) + board.deal_from_deck(P1, "prizePile", 6) + attacker, target = e["p1_active"], e["p2_active"] + ctx = attack_ctx(rig, e) + ctx.add_extra_prize_watcher(lambda a: False, None) # never matches + ctx.add_extra_prize_watcher( + lambda a: a is attacker, lambda t: t is target, prizes=1) + await ctx.deal_damage(1000) + await rig.session.resolve_knockouts(ctx) + assert len(board.find_player_area(P1, "prizePile").children) == 4, \ + "matching watcher adds +1 to the take; non-matching adds nothing" + ts.begin_turn(P2, board) + assert ts.extra_prize_watchers == [], "watchers are this-turn only" + + +async def test_setup_as_active(): + from spirit.game.data_utils import def_for + luxray = def_for("ef8ca3ef-7c49-55f9-8a3b-0bf871f8e524") # CZ Luxray + assert luxray is not None and getattr(luxray, "setup_as_active", False), \ + "CZ Luxray must carry setup_as_active" + rig = Rig(luxray, FILLER, ENERGY_GUIDS, ITEM) + rig.setup("pokemon") + board = rig.board + in_hand = rig.to_area(rig.pull_guid(P1, luxray.guid), P1, "hand") + assert in_hand is not None + basics = board.basic_pokemon_in_hand(P1) + assert in_hand not in basics, \ + "Stage 2 stays out of the Basic scans (bench offer / bench plays)" + candidates = board.setup_active_candidates(P1) + assert in_hand in candidates, "setup_as_active joins the opening-Active offer" + assert candidates[:len(basics)] == basics, \ + "Basics stay first (the AI pick prefers a true Basic)" + assert board.player_has_any_basic(P1) + + TESTS = [ test_temp_passive_prevents_then_expires, test_temp_passive_cleared_on_leave, @@ -1873,6 +2002,7 @@ TESTS = [ test_prize_hooks, test_move_damage_counters, test_modify_energy_provided, + test_hyper_potion_double_turbo_energy, test_on_move_to_active_once, test_on_ally_knocked_out, test_evolution_gate_passives, @@ -1905,6 +2035,9 @@ TESTS = [ test_retreat_cost_board_param, test_ignore_target_effects_turn_flag, test_usable_despite_conditions, + test_on_play_trigger_ends_turn, + test_extra_prize_watchers, + test_setup_as_active, ]