FEAT!: implement all SWSH -> SIT format

This commit is contained in:
Brandon Nguyen
2026-07-13 18:25:21 -07:00
parent 4c07223c42
commit f7a4ce6d6e
2327 changed files with 33324 additions and 6696 deletions

View File

@@ -676,6 +676,21 @@ def discard_for_bonus(source: str = "hand", predicate=None, max_count: int = 1,
return effect
def smokescreen_attack(damage: Optional[int] = None, also=None):
"""Printed (or damage) damage; "during your opponent's next turn, if the
Defending Pokemon tries to attack, your opponent flips a coin. If tails,
that attack doesn't happen" (Smokescreen / Sand Attack / Blinding Beam).
The rider no-ops versus an attack-effect-shielded target."""
async def effect(ctx):
await ctx.deal_damage(damage)
defender = ctx.defender
if defender is not None and not ctx.effects_blocked(defender):
ctx.require_attack_flip(defender)
if also is not None:
await also(ctx)
return effect
async def discard_random_from_hand(ctx, player_id: Optional[str] = None,
count: int = 1) -> list:
"""Discards `count` random cards from a player's hand (reveals via the

View File

@@ -14,11 +14,12 @@ preds (no_retreat / ability_lock / healing_block / retreat_free) take
from typing import Any, Callable, Optional
from spirit.game.attributes import AttrID
from spirit.game.attributes import AttrID, TrainerType
from spirit.game.session.passives import (
Passive,
TurnDamageModifier,
carrier_pokemon,
effective_max_hp,
)
@@ -207,7 +208,7 @@ class RetreatDiscountPassive(Passive):
self.amount = amount
self.protects = _protects_pred(target_pred)
def modify_retreat_cost(self, cost, pokemon, carrier):
def modify_retreat_cost(self, cost, pokemon, carrier, board):
if not self.protects(pokemon, carrier):
return cost
return max(0, cost - self.amount)
@@ -225,7 +226,7 @@ class RetreatFreeWhenPassive(Passive):
def __init__(self, pred):
self.pred = pred
def modify_retreat_cost(self, cost, pokemon, carrier):
def modify_retreat_cost(self, cost, pokemon, carrier, board):
return 0 if self.pred(pokemon, carrier) else cost
@@ -435,6 +436,158 @@ def attack_effect_shield_passive(protects="carrier") -> Passive:
return AttackEffectShieldPassive(protects)
class FlipPreventDamagePassive(Passive):
""""If any damage is done to this Pokemon by attacks, flip a coin. If
heads, prevent that damage" (Infiltrator / Primate Dexterity)."""
def __init__(self, title="", protects="carrier"):
self.title = title
self.protects = _protects_pred(protects)
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 not self.protects(target, carrier):
return None
heads = await ctx.flip_coins(1, self.title,
source=carrier_pokemon(carrier) or carrier)
return 0 if heads and heads[0] else None
def flip_prevent_damage_passive(title="") -> Passive:
"""Flip-to-prevent shield: heads prevents the whole attack hit; the flip
choreographs inside the attack bracket (queued on the attack ctx)."""
return FlipPreventDamagePassive(title)
class GutsSurvivePassive(Passive):
""""If this Pokemon would be Knocked Out by damage from an attack, [flip a
coin -- if heads,] it is not Knocked Out and its remaining HP becomes
`hp_floor`" (Guts; Sturdy with flip=False + require_full_hp=True)."""
def __init__(self, hp_floor=10, title="Guts", flip=True,
require_full_hp=False, protects="carrier"):
self.hp_floor = hp_floor
self.title = title
self.flip = flip
self.require_full_hp = require_full_hp
self.protects = _protects_pred(protects)
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 not self.protects(target, carrier):
return None
current = target.get_attribute(AttrID.HP, 0)
if calc.amount < current:
return None # would not Knock Out
if self.require_full_hp and current < effective_max_hp(calc.board, target):
return None
if self.flip:
heads = await ctx.flip_coins(1, self.title,
source=carrier_pokemon(carrier) or carrier)
if not (heads and heads[0]):
return None
return max(0, current - self.hp_floor)
def guts_survive_passive(hp_floor=10, title="Guts", flip=True,
require_full_hp=False) -> Passive:
"""KO-survive interceptor: only when the hit would KO; heads (or always
with flip=False) rewrites the dealt amount so remaining HP = hp_floor."""
return GutsSurvivePassive(hp_floor, title, flip, require_full_hp)
class TrainerEffectShieldPassive(Passive):
"""Shields the carrier's owner from opposing trainer-card effects
(Dew Guard: "prevent all effects of that card done to you or your hand")."""
def __init__(self, supporters_only=True, protects=None, while_active=False,
condition=None):
self.supporters_only = supporters_only
# protects(affected_entity_or_None, carrier): scopes the shield to
# matching direct objects (None = the whole side, legacy behavior).
self.protects = protects
self.while_active = while_active
self.condition = condition
def blocks_trainer_effects(self, affected_player_id, trainer_card,
trainer_type, carrier, affected_entity=None,
board=None):
if self.supporters_only and trainer_type != TrainerType.SUPPORTER.value:
return False
if affected_player_id != carrier.owning_player_id:
return False
if self.while_active \
and not is_in_active_spot(carrier_pokemon(carrier) or carrier):
return False
if self.condition is not None and not self.condition(board, carrier):
return False
if self.protects is not None:
return bool(self.protects(affected_entity, carrier))
return True
def trainer_effect_shield_passive(supporters_only=True, protects=None,
while_active=False, condition=None) -> Passive:
"""Dew Guard shape; the engine already scopes it to effects a DIFFERENT
player's trainer card does to the shielded side. protects/while_active/
condition(board, carrier) narrow the shield (Princess's Curtain, Baffling)."""
return TrainerEffectShieldPassive(supporters_only, protects, while_active,
condition)
class SupporterReplacementPassive(Passive):
"""Opposing Supporter cards resolve `replacement` instead of their own
effect (Shifty Substitution: "each Supporter card in your opponent's hand
has the effect 'Draw 3 cards.'")."""
def __init__(self, replacement, opponents_only=True, while_active=True):
self.replacement = replacement
self.opponents_only = opponents_only
self.while_active = while_active
def replace_supporter_effect(self, card, player_id, carrier):
if self.opponents_only and player_id == carrier.owning_player_id:
return None
if self.while_active:
holder = carrier_pokemon(carrier) or carrier
if not is_in_active_spot(holder):
return None
return self.replacement
def replace_opponent_supporters(replacement, while_active=True) -> Passive:
"""Shifty Substitution shape: `replacement` is an async def effect(ctx)
run as the opponent's Supporter effect (trainer ctx, shields apply)."""
return SupporterReplacementPassive(replacement, while_active=while_active)
class EnergyAttachTaxPassive(Passive):
"""Opposing manual energy attaches flip a coin; tails discards the energy
instead of attaching (Slimy Room; per text the turn attachment is NOT
used up)."""
def __init__(self, opponents_only=True, while_active=True):
self.opponents_only = opponents_only
self.while_active = while_active
def taxes_energy_attach(self, attaching_player_id, energy, target, carrier):
if self.opponents_only \
and attaching_player_id == carrier.owning_player_id:
return False
if self.while_active:
holder = carrier_pokemon(carrier) or carrier
if not is_in_active_spot(holder):
return False
return True
def energy_attach_tax_passive(opponents_only=True, while_active=True) -> Passive:
"""Slimy Room shape: taxes the opponent's manual attach with a coin flip."""
return EnergyAttachTaxPassive(opponents_only, while_active)
# ======================================================================
# GROUP B -- temporary-shield effect factories (expiring temp passives)
# ======================================================================
@@ -602,7 +755,7 @@ class SelfRetreatCostRaisePassive(Passive):
def __init__(self, extra):
self.extra = extra
def modify_retreat_cost(self, cost, pokemon, carrier):
def modify_retreat_cost(self, cost, pokemon, carrier, board):
if carrier_pokemon(carrier) is not pokemon:
return cost
return cost + self.extra

View File

@@ -9,19 +9,22 @@ from spirit.game.attributes import (
SpecialConditions,
)
from spirit.game.data_utils import (
ABILITIES_BY_ID, Ability, Attack, ability_id_for, def_for, is_pokemon_v,
subtypes_for,
ABILITIES_BY_ID, Ability, Attack, ability_id_for, def_for, has_rule_box,
is_pokemon_v, subtypes_for,
)
from spirit.game.session.constants import BENCH_CAPACITY
from spirit.game.session.effects import (
full_stack,
is_basic_pokemon,
is_colorless_no_rule_box,
is_pokemon_card,
is_special_energy,
is_supporter_card,
is_trainer_card,
)
from spirit.game.session.passives import Passive, carrier_pokemon
from spirit.game.session.passives import (
Passive, carrier_pokemon, effective_bench_capacity,
)
from spirit.game.models.board import PokemonEntity
@@ -40,7 +43,8 @@ class MysteriousNestPassive(Passive):
async def read_the_wind(ctx):
"""Discard a card from your hand. If you do, draw 3 cards."""
if await ctx.discard_from_hand(1, minimum=0, prompt="Discard a card from your hand"):
# No "you may": with a card in hand the discard is mandatory.
if await ctx.discard_from_hand(1, minimum=1, prompt="Discard a card from your hand"):
await ctx.draw_cards(3)
@@ -193,6 +197,26 @@ class ExcitedHeartPassive(Passive):
return cost
# --- Ditto (PGO): Sudden Transformation ------------------------------------
class SuddenTransformationPassive(Passive):
"""May use the attacks of Basic non-Rule-Box Pokemon in the owner's
discard pile (energy costs still apply)."""
def granted_attacks(self, board, pokemon, carrier):
if carrier is not pokemon:
return []
discard = board.find_player_area(pokemon.owning_player_id, "discard")
attacks = []
for card in (discard.children if discard else []):
if not is_basic_pokemon(card) or has_rule_box(card.archetype_id):
continue
for ability in getattr(def_for(card.archetype_id), "abilities", None) or []:
if isinstance(ability, Attack):
attacks.append(ability)
return attacks
# --- Raikou (VIV): Amazing Shot -------------------------------------------
async def amazing_shot(ctx):
@@ -902,3 +926,239 @@ async def teraspark(ctx):
ctx, ctx.opponent_bench(), 40,
"Choose a Benched Pokémon to take 40 damage",
)
# --- Word of Ruin / Doom Curse (delayed Knock Out) ------------------------
async def delayed_knockout(ctx):
"""At the end of your opponent's next turn, the Defending Pokemon will
be Knocked Out (dropped if it leaves the Active spot or evolves)."""
target = ctx.defender
if target is None or ctx.effects_blocked(target):
return
ctx.visual_targets = [target.entity_id]
owner_id = target.owning_player_id
target_id = target.entity_id
def _guard(board):
active = board.active_pokemon(owner_id) if owner_id else None
return active is not None and active.entity_id == target_id
async def _fire(session):
pokemon = session.board_state.get_entity(target_id)
if isinstance(pokemon, PokemonEntity):
await session._resolve_raw_knockout(pokemon)
ctx.schedule_at_checkup(1, _fire, guard=_guard)
# --- Top Entry / Emergency Entry (bench straight from the turn draw) ------
def top_entry(draw_count=0):
"""If drawn at the beginning of your turn and your Bench isn't full, you
may put this Pokemon onto your Bench (Emergency Entry also draws)."""
async def effect(ctx):
source = ctx.source
owner = source.owning_player_id or ctx.player_id
bench = ctx.board.find_player_area(owner, "bench")
if not bench \
or len(bench.children) >= effective_bench_capacity(ctx.board, owner):
return
if not await ctx.ask_yes_no("Put this Pokémon onto your Bench?"):
return
if await ctx.bench_pokemon(source) and draw_count:
await ctx.draw_cards(draw_count)
return effect
# --- Origin Forme Dialga VSTAR (BRS): Star Chronos ------------------------
async def star_chronos(ctx):
"""220. Take another turn after this one. (Skip Pokemon Checkup.)"""
await ctx.deal_damage()
ctx.take_extra_turn()
# --- Medicham V (EVS): Yoga Loop ------------------------------------------
def yoga_loop_condition(board, player_id, pokemon):
"""Unusable if any of your Pokemon used Yoga Loop during your last turn."""
prev = board.turn_state.attack_titles_prev_turn_by_player.get(player_id) or []
return "Yoga Loop" not in prev
async def yoga_loop(ctx):
"""2 damage counters on 1 opposing Pokemon; a KO grants an extra turn."""
target = await ctx.choose_pokemon(
ctx.opponent_pokemon_in_play(), "Choose 1 of your opponent's Pokémon"
)
if target is None:
return
await ctx.deal_damage(20, target=target, as_counters=True)
if target in ctx.knockouts:
ctx.take_extra_turn()
# --- Jumpluff (EVS): Fluffy Barrage ---------------------------------------
class FluffyBarragePassive(Passive):
"""May attack twice each turn (the printed KO sentence is timing reminder
text, not a condition): the first attack keeps the turn, and legal
actions collapse to attacks-only afterwards, so the player either attacks
again or clicks End Turn."""
def attack_keeps_turn(self, attacker, ability, ctx, carrier):
if attacker is not carrier:
return False
uses = [e for e in ctx.session.turn_state.attacks_used
if e[0] == carrier.entity_id]
return len(uses) == 1
# --- Devolution (Rewind Beam / Downgrading Beam / Curse of Devolution) ----
def devolvable(pokemon) -> bool:
"""An evolved in-play Pokemon with its previous stage tucked underneath."""
evolves_from = pokemon.get_attribute(AttrID.EVOLUTION_LOGIC_FROM)
return bool(evolves_from) and any(
isinstance(c, PokemonEntity)
and c.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) == evolves_from
for c in pokemon.children
)
def devolve_depth(pokemon) -> int:
"""How many evolution cards can be peeled off the stack."""
depth, current = 0, pokemon
while True:
evolves_from = current.get_attribute(AttrID.EVOLUTION_LOGIC_FROM)
nxt = next(
(c for c in current.children
if isinstance(c, PokemonEntity)
and c.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) == evolves_from),
None,
) if evolves_from else None
if nxt is None:
return depth
depth, current = depth + 1, nxt
async def rewind_beam(ctx):
"""180. Devolve the opposing evolved Active: highest Stage card to hand."""
await ctx.deal_damage()
defender = ctx.defender
if defender is not None and devolvable(defender) and not ctx.effects_blocked(defender):
await ctx.devolve_pokemon(defender, steps=1, destination="hand")
async def downgrading_beam(ctx):
"""Devolve 1 opposing evolved Pokemon, removing any number of Evolution
cards; the opponent shuffles them into their deck."""
candidates = [p for p in ctx.opponent_pokemon_in_play()
if devolvable(p) and not ctx.effects_blocked(p)]
if not candidates:
return
target = await ctx.choose_pokemon(
candidates, "Choose 1 of your opponent's evolved Pokémon"
)
if target is None:
return
owner_id = target.owning_player_id
depth = devolve_depth(target)
steps = 1
if depth > 1:
steps = 1 + await ctx.choose(
"How many Evolution cards will you remove?",
[str(n + 1) for n in range(depth)],
)
removed = await ctx.devolve_pokemon(target, steps=steps, destination="deck")
if removed:
await ctx.shuffle_deck(owner_id)
async def curse_of_devolution(ctx):
"""On evolve: you may devolve 1 opposing Benched evolved Pokemon."""
candidates = [p for p in ctx.opponent_bench()
if devolvable(p) and not ctx.effects_blocked(p)]
if not candidates:
return
if not await ctx.ask_yes_no("Devolve 1 of your opponent's Benched Pokémon?"):
return
target = await ctx.choose_pokemon(
candidates, "Choose 1 of your opponent's Benched evolved Pokémon"
)
if target is not None:
await ctx.devolve_pokemon(target, steps=1, destination="hand")
# --- Identity swaps (Stance Change / V Transformation / Phantom Transformation)
def _same_name(entity, other) -> bool:
return entity.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) \
== other.get_attribute(AttrID.EVOLUTION_LOGIC_NAME)
def stance_change_condition(board, player_id, pokemon):
"""A same-named card (an Aegislash) sits in the hand."""
hand = board.find_player_area(player_id, "hand")
return bool(hand) and any(_same_name(c, pokemon) for c in hand.children)
async def stance_change(ctx):
"""Switch this Pokemon with an Aegislash in your hand; everything remains."""
candidates = [c for c in ctx.hand() if _same_name(c, ctx.source)]
picks = await ctx.choose_cards(
candidates, 1, minimum=1,
prompt="Choose an Aegislash to switch with this Pokémon",
)
if picks:
await ctx.identity_swap(ctx.source, picks[0], destination="hand")
def _is_basic_pokemon_v(card) -> bool:
return is_basic_pokemon(card) and is_pokemon_v(card.archetype_id)
def v_transformation_condition(board, player_id, pokemon):
discard = board.find_player_area(player_id, "discard")
return bool(discard) and any(_is_basic_pokemon_v(c) for c in discard.children)
async def v_transformation(ctx):
"""Switch this Pokemon with a Basic Pokemon V from the discard pile."""
candidates = [c for c in ctx.discard_pile() if _is_basic_pokemon_v(c)]
picks = await ctx.choose_cards(
candidates, 1, minimum=1,
prompt="Choose a Basic Pokémon V to switch with this Pokémon",
)
if picks:
await ctx.identity_swap(ctx.source, picks[0], destination="discard")
def _phantom_candidate(card, zoroark) -> bool:
return (
is_pokemon_card(card)
and card.get_attribute(AttrID.STAGE) == PokemonStage.STAGE1.value
and not _same_name(card, zoroark)
)
def phantom_transformation_condition(board, player_id, pokemon):
discard = board.find_player_area(player_id, "discard")
return bool(discard) and any(
_phantom_candidate(c, pokemon) for c in discard.children)
async def phantom_transformation(ctx):
"""Discard this Pokemon and all attached cards; a Stage 1 (non-Zoroark)
from the discard pile takes its place as a fresh Pokemon."""
candidates = [c for c in ctx.discard_pile()
if _phantom_candidate(c, ctx.source)]
picks = await ctx.choose_cards(
candidates, 1, minimum=1,
prompt="Choose a Stage 1 Pokémon to put in this Pokémon's place",
)
if picks:
await ctx.identity_swap(ctx.source, picks[0], destination="discard",
transfer=False)

View File

@@ -5,7 +5,8 @@ from spirit.game.attributes import (
)
from spirit.game.models.board import PokemonEntity
from spirit.game.data_utils import (
Ability, Activations, has_rule_box, is_pokemon_v, subtypes_for,
Ability, Activations, Attack, def_for, has_rule_box, is_pokemon_v,
subtypes_for,
)
from spirit.game.session.constants import BENCH_CAPACITY
from spirit.game.session.effects import (
@@ -22,7 +23,9 @@ from spirit.game.session.passives import (
Passive,
TurnDamageModifier,
carrier_pokemon,
effective_bench_capacity,
effective_max_hp,
trainer_play_blocked,
)
@@ -584,6 +587,39 @@ async def battle_vip_pass(ctx):
await ctx.shuffle_deck()
def dream_ball_playable(board, player_id):
"""Dream Ball never plays from hand: its window is the prize-take prompt."""
return False
async def dream_ball(ctx):
"""Search your deck for a Pokemon and put it onto your Bench."""
picks = await ctx.search_deck(
is_pokemon_card, count=1, minimum=0,
prompt="Choose a Pokémon to put onto your Bench.",
)
for card in picks:
await ctx.bench_pokemon(card)
await ctx.shuffle_deck()
async def dream_ball_prize_window(ctx):
"""Taken as a face-down Prize: you may play it before it settles in hand."""
ctx.suppress_announce = True
board, pid = ctx.board, ctx.player_id
bench = board.find_player_area(pid, "bench")
if not bench or len(bench.children) >= effective_bench_capacity(board, pid):
return
if not ctx.deck(pid):
return
if trainer_play_blocked(board, pid, ctx.source):
return
if not await ctx.ask_yes_no("Play Dream Ball? Search your deck for a "
"Pokémon and put it onto your Bench."):
return
await ctx.session._execute_play_trainer(pid, ctx.source)
async def power_tablet(ctx):
"""This turn, your Fusion Strike Pokemon's attacks do 30 more damage to
the opponent's Active Pokemon (before Weakness and Resistance)."""
@@ -681,6 +717,30 @@ class LostCityPassive(Passive):
return "lostZone"
class CollapsedStadiumPassive(Passive):
"""Each player can't have more than 4 Benched Pokemon (excess is discarded
by enforce_bench_capacity when this comes into play)."""
def bench_capacity(self, player_id, carrier):
return 4
async def gapejaw_bog_watch(ctx):
"""Gapejaw Bog: 2 damage counters on any Basic just benched from hand."""
pokemon = ctx.benched_pokemon
if pokemon is None:
return
await ctx.deal_damage(20, target=pokemon, apply_modifiers=False,
as_counters=True)
class SkatersParkPassive(Passive):
"""Basic Energy paid for either player's retreat goes to hand instead."""
def retreat_cost_destination(self, pokemon, energy, carrier):
return "hand" if is_basic_energy_card(energy) else None
# ======================================================================
# Lost Zone Box + Regigigas decks
# ======================================================================
@@ -897,17 +957,56 @@ async def hisuian_heavy_ball(ctx):
await ctx.look_at_prizes_take_basic()
# --- Peonia (CRE, Supporter) ----------------------------------------------
def peonia_playable(board, player_id) -> bool:
prizes = board.find_player_area(player_id, "prizePile")
return bool(prizes and prizes.children)
async def peonia(ctx):
"""Put up to 3 Prize cards into your hand. Then, for each Prize card put
into your hand this way, put a card from your hand face down as a Prize."""
taken = await ctx.take_prizes(3, minimum=1, check_win=False)
if not taken:
return
picks = await ctx.choose_cards(
list(ctx.hand()), len(taken), minimum=len(taken),
prompt=f"Choose {len(taken)} card(s) to put face down as Prize cards",
)
await ctx.put_in_prizes(picks)
# --- Air Balloon (SSH, Pokemon Tool) -------------------------------------
class AirBalloonPassive(Passive):
"""The Retreat Cost of the holder is [C][C] less."""
def modify_retreat_cost(self, cost, pokemon, carrier):
def modify_retreat_cost(self, cost, pokemon, carrier, board):
if carrier_pokemon(carrier) is pokemon:
return cost - 2
return cost
# --- Memory Capsule (SWSH4) ------------------------------------------------
class MemoryCapsulePassive(Passive):
"""The holder can use any attack from its tucked previous Evolutions
(energy costs still apply)."""
def granted_attacks(self, board, pokemon, carrier):
if carrier_pokemon(carrier) is not pokemon:
return []
attacks = []
for child in pokemon.children:
if not isinstance(child, PokemonEntity):
continue
for ability in getattr(def_for(child.archetype_id), "abilities", None) or []:
if isinstance(ability, Attack):
attacks.append(ability)
return attacks
# --- Training Court (RCL, Stadium) ---------------------------------------
def training_court_condition(board, player_id, stadium):
@@ -927,6 +1026,44 @@ async def training_court(ctx):
await ctx.put_in_hand(picks, reveal=False)
# --- Thorton (SWSH11) -------------------------------------------------------
def _basic_pokemon_in_play(board, player_id):
"""In-play Basics; fossils count (Basic Pokemon in play, Trainer CARD_TYPE)."""
from spirit.game.attributes import AttrID, PokemonStage
from spirit.game.models.board import PokemonEntity
return [
p for p in board.pokemon_in_play(player_id)
if isinstance(p, PokemonEntity)
and p.get_attribute(AttrID.STAGE) == PokemonStage.BASIC.value
]
def thorton_condition(board, player_id):
if not any(is_basic_pokemon(c) for c in _discard(board, player_id)):
return False
return bool(_basic_pokemon_in_play(board, player_id))
async def thorton(ctx):
"""Choose a Basic Pokemon in your discard pile and switch it with 1 of
your Basic Pokemon in play; everything remains on the new Pokemon."""
candidates = [c for c in ctx.discard_pile() if is_basic_pokemon(c)]
in_play = _basic_pokemon_in_play(ctx.board, ctx.player_id)
if not candidates or not in_play:
return
picks = await ctx.choose_cards(
candidates, 1, minimum=1,
prompt="Choose a Basic Pokémon from your discard pile",
)
if not picks:
return
target = await ctx.choose_pokemon(
in_play, "Choose 1 of your Basic Pokémon in play to switch it with")
if target is not None:
await ctx.identity_swap(target, picks[0], destination="discard")
TRAINING_COURT_ABILITY = Ability(
title="Training Court",
game_text="Once during each player's turn, that player may put a basic Energy card from their discard pile into their hand.",
@@ -934,3 +1071,80 @@ TRAINING_COURT_ABILITY = Ability(
effect=training_court,
condition=training_court_condition,
)
# --- Fossils (Unidentified Fossil / Rare Fossil) ---------------------------
class FossilBodyPassive(Passive):
"""Fossil rules text: this card can't retreat; Rare Fossil additionally
can't be affected by Special Conditions."""
def __init__(self, blocks_conditions: bool = False):
self.condition_immune = blocks_conditions
def blocks_retreat(self, pokemon, carrier):
return pokemon is carrier
def blocks_special_conditions(self, target, condition, carrier):
return self.condition_immune and target is carrier
async def fossil_discard(ctx):
"""Discard this fossil from play; attached cards go to the discard too."""
fossil = ctx.source
was_active = fossil is ctx.my_active()
discard = ctx.board.find_player_area(ctx.player_id, "discard")
if discard is not None and discard.entity_id not in ctx.visual_targets:
ctx.visual_targets.append(discard.entity_id)
await ctx.discard_cards([c for c in full_stack(fossil) if c is not fossil])
await ctx.discard_cards([fossil])
if was_active:
async def _promote():
if not await ctx.session._promote_new_active(ctx.player_id):
screen_name = ctx.session.players[ctx.player_id].screen_name
await ctx.session.end_game(
ctx.opponent_id, f"{screen_name} has no Pokémon left"
)
ctx.deferred_actions.append(_promote)
def fossil_discard_ability() -> Ability:
""""At any time during your turn, you may discard this card from play."
Fresh instance per print: ability_id derives from the owning card GUID."""
return Ability(
"Discard",
"At any time during your turn, you may discard this card from play.",
activation=Activations.ONCE_PER_TURN,
effect=fossil_discard,
)
def is_rare_fossil(card) -> bool:
return getattr(def_for(card.archetype_id), "display_name", None) == "Rare Fossil"
def bench_has_room(board, player_id):
bench = board.find_player_area(player_id, "bench")
return bench is not None \
and len(bench.children) < effective_bench_capacity(board, player_id)
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
onto your Bench; then shuffle (Relicanth's Fossil Search, Cara Liss)."""
async def effect(ctx):
bench = ctx.board.find_player_area(ctx.player_id, "bench")
space = effective_bench_capacity(ctx.board, ctx.player_id) \
- len(bench.children) if bench else 0
if space <= 0:
return
picks = await ctx.search_deck(
fossil_predicate, count=min(count, space), minimum=0,
prompt=f"Choose up to {min(count, space)} {label} cards to put "
f"onto your Bench.",
)
for card in picks:
await ctx.bench_pokemon(card)
await ctx.shuffle_deck()
return effect

View File

@@ -123,6 +123,25 @@ class Triggers:
# BEFORE the KO'd stack moves (energies still attached); ctx carries
# ko_pokemon / ko_from_attack / ko_attacker.
ON_ALLY_KNOCKED_OUT = "on_ally_knocked_out"
# Either player manually put a Basic from hand onto their Bench (Gapejaw
# Bog); ctx carries benching_player_id / benched_pokemon.
ON_POKEMON_BENCHED = "on_pokemon_benched"
# This card was drawn by the beginning-of-turn draw (Lombre "Top Entry");
# fires after the card lands in hand (approximation of "before you put it
# into your hand").
ON_TURN_DRAWN = "on_turn_drawn"
# This card was just taken as a face-down Prize into hand (Dream Ball).
ON_TAKEN_AS_PRIZE = "on_taken_as_prize"
# Another of the owner's Pokemon evolved via a hand-played evolution card
# (Eevee "Resonant Evolution"); ctx carries evolved_pokemon / evolved_from.
ON_ALLY_EVOLVED = "on_ally_evolved"
# Fires for each of the turn player's in-play Pokemon after their turn
# ends, before the checkup (Radiant Venusaur "Sunny Bloom").
END_OF_TURN = "end_of_turn"
# This card was discarded from its owner's hand by an OPPOSING player's
# effect (Amoonguss "Surprise Spores"); fires via the acting ctx's
# deferred_actions, after that effect's choreography flushes.
ON_DISCARDED_FROM_HAND = "on_discarded_from_hand"
class Activations:
@@ -241,7 +260,8 @@ class Attack(Ability):
vstar: bool = False,
locks_next_turn: bool = False,
condition: Optional[Callable] = None,
usable_first_turn: bool = False
usable_first_turn: bool = False,
usable_despite_conditions: bool = False
):
super().__init__(title, game_text, ability_type, effect, vstar=vstar,
condition=condition)
@@ -252,6 +272,8 @@ class Attack(Ability):
self.locks_next_turn = locks_next_turn
# Exempt from the "going first can't attack on turn 1" rule (Indeedee).
self.usable_first_turn = usable_first_turn
# Offered even while the user is Asleep/Paralyzed (Windup Arm-style).
self.usable_despite_conditions = usable_despite_conditions
def to_dict(self) -> dict:
d = super().to_dict()
@@ -431,7 +453,8 @@ class TrainerCardDef(CardDefinition):
the `unimplemented` marker, or None for cards with no effect scripted.
`condition(board, player_id) -> bool` gates when the card may be played
at all (e.g. Ultra Ball needs 2 other cards in hand to discard).
at all (e.g. Ultra Ball needs 2 other cards in hand to discard); a
3-arg `condition(board, player_id, card)` also gets the hand copy (Nugget).
"""
def __init__(
self,
@@ -444,6 +467,7 @@ class TrainerCardDef(CardDefinition):
trainer_type: TrainerType,
effect: Optional[Any] = None,
condition: Optional[Callable] = None,
abilities: Optional[List[Ability]] = None,
display_name: Optional[str] = None,
searchable_by: Optional[List[str]] = None,
subtypes: Optional[List[str]] = None,
@@ -452,6 +476,13 @@ class TrainerCardDef(CardDefinition):
super().__init__(guid, key, name, collector_number, set_code, rarity, display_name, searchable_by, subtypes, attributes)
self.effect = effect
self.condition = condition
# Trainers have no PIE_ABILITIES slot; declared abilities register for
# the session's trigger scans only (Dream Ball's ON_TAKEN_AS_PRIZE).
self.abilities: List[Ability] = abilities or []
for idx, a in enumerate(self.abilities):
if not a.ability_id:
a.ability_id = ability_id_for(guid, idx + 100) # slot 0 = StadiumCardDef.ability
ABILITIES_BY_ID[a.ability_id] = a
if effect is not None:
TRAINER_EFFECTS_BY_GUID[guid.lower()] = effect
self.extra_attributes.update({
@@ -464,6 +495,33 @@ class ItemCardDef(TrainerCardDef):
kwargs['trainer_type'] = TrainerType.ITEM
super().__init__(**kwargs)
class FossilItemCardDef(ItemCardDef):
"""Item played as if it were a Basic Colorless Pokemon (fossils).
The archetype stays a Trainer-Item (real PTCGO shape: deck-builder
grouping, Item locks, mulligan/setup exclusion); plays_as_pokemon makes
the board entity a PokemonEntity so it benches, evolves, takes damage and
offers its PIE abilities like a Basic while in play.
"""
plays_as_pokemon = True
def __init__(self, hp: int, passive: Optional[Any] = None, **kwargs):
super().__init__(**kwargs)
self.passive = passive
self.extra_attributes.update({
str(AttrID.HP.value): {"type": "int", "value": hp},
str(AttrID.STAGE.value): {"type": "int", "value": PokemonStage.BASIC.value},
str(AttrID.POKEMON_TYPES.value): {
"type": "json", "value": json.dumps([PokemonTypes.COLORLESS.value])
},
str(AttrID.RETREAT_COST.value): {"type": "int", "value": 0},
str(AttrID.PIE_ABILITIES.value): {
"type": "json",
"value": json.dumps([a.to_dict() for a in self.abilities]),
},
})
class SupporterCardDef(TrainerCardDef):
def __init__(self, **kwargs):
kwargs['trainer_type'] = TrainerType.SUPPORTER

View File

@@ -256,6 +256,10 @@ def create_card_entity(card_obj: Card, owning_player_id: Optional[str] = None, e
elif c_type == CardType.ENERGY.value:
return EnergyEntity(card_obj, owning_player_id, entity_id)
else:
from spirit.game.data_utils import def_for # circular-import guard
if getattr(def_for(card_obj.guid), "plays_as_pokemon", False):
# Fossils: Trainer archetype, Pokemon entity on the board.
return PokemonEntity(card_obj, owning_player_id, entity_id)
return TrainerEntity(card_obj, owning_player_id, entity_id)
@@ -452,8 +456,11 @@ class BoardState:
@staticmethod
def _is_basic_pokemon(entity: BoardEntity) -> bool:
# Fossils (Trainer archetypes played as Pokemon) don't count for
# mulligans or setup placement.
return (
isinstance(entity, PokemonEntity)
and entity.card_obj.get_attribute_value(AttrID.CARD_TYPE) == CardType.POKEMON.value
and entity.card_obj.get_attribute_value(AttrID.STAGE) == PokemonStage.BASIC.value
)
@@ -643,9 +650,10 @@ class BoardState:
continue
bench.children.sort(key=self.bench_slot_of)
occupied = {self.bench_slot_of(c) for c in bench.children}
slots = bench.get_attribute(AttrID.AREA_SLOTS) or BENCH_SLOT_COUNT
bench.set_attribute(
AttrID.AREA_EMPTY_SLOTS,
[s for s in range(BENCH_SLOT_COUNT) if s not in occupied],
[s for s in range(slots) if s not in occupied],
)
def serialize(self, viewer_id: Optional[str] = None) -> Dict[str, Any]:

View File

@@ -1,4 +1,5 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.card_effects.passives_common import flip_protection
from spirit.game.data_utils import PokemonCardDef, Attack, Ability
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
card = PokemonCardDef(
@@ -24,7 +25,7 @@ card = PokemonCardDef(
title="Star Protection",
game_text="Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pok\u00e9mon by attacks.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=flip_protection(prevent=True),
),
],
)

View File

@@ -1,4 +1,5 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.card_effects.passives_common import flip_protection
from spirit.game.data_utils import PokemonCardDef, Attack, Ability
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
card = PokemonCardDef(
@@ -23,7 +24,7 @@ card = PokemonCardDef(
title="Star Protection",
game_text="Flip a coin. If heads, during your opponent's next turn, prevent all damage done to this Pok\u00e9mon by attacks.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=flip_protection(prevent=True),
),
],
)

View File

@@ -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, requires_discard
from spirit.game.card_effects.attacks_common import damage_per, count_energy
card = PokemonCardDef(
guid="03fc082d-f894-5f17-96af-f266f3ae3d82",
@@ -23,7 +25,8 @@ card = PokemonCardDef(
title="Temporal Backflow",
game_text="Put a card from your discard pile into your hand.",
cost={PokemonTypes.METAL: 1},
effect=unimplemented,
condition=requires_discard(),
effect=recover_from_discard(count=1, minimum=1, reveal=False, to="hand"),
),
Attack(
title="Metal Blast",
@@ -31,7 +34,7 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 3},
damage=60,
damage_operator="+",
effect=unimplemented,
effect=damage_per(count_energy("self", energy_type=PokemonTypes.METAL), 20, base=60),
),
],
)

View File

@@ -1,5 +1,22 @@
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.passives_common import prevent_damage_when
def _max_balloon_shield(calc, carrier):
if calc.target is not carrier:
return False
attacker = calc.attacker
return attacker is not None and \
attacker.get_attribute(AttrID.STAGE) == PokemonStage.BASIC.value
async def _max_balloon(ctx):
await ctx.deal_damage()
ctx.add_passive_through_opponents_turn(
ctx.attacker, prevent_damage_when(_max_balloon_shield)
)
card = PokemonCardDef(
guid="a3f7a335-9942-5930-9395-5f6d4d26754e",
@@ -25,7 +42,7 @@ card = PokemonCardDef(
game_text="During your opponent's next turn, prevent all damage done to this Pok\u00e9mon by attacks from Basic Pok\u00e9mon.",
cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 2},
damage=160,
effect=unimplemented,
effect=_max_balloon,
),
],
)

View File

@@ -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, SpecialConditions
from spirit.game.card_effects.attacks_common import condition_attack, flip_or_nothing
from spirit.game.card_effects.passives_common import apply_protection
async def _fly_success(ctx):
await ctx.deal_damage()
await apply_protection(ctx, prevent=True, effects_too=True)
card = PokemonCardDef(
guid="f13a80b5-f37a-54ae-8277-1ed8c8cc6e15",
@@ -21,17 +29,17 @@ card = PokemonCardDef(
abilities=[
Attack(
title="Thunder Shock",
game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.",
game_text="Flip a coin. If heads, your opponent's Active Pokémon is now Paralyzed.",
cost={PokemonTypes.LIGHTNING: 1},
damage=20,
effect=unimplemented,
effect=condition_attack(SpecialConditions.PARALYZED, flip=True),
),
Attack(
title="Fly",
game_text="Flip a coin. If tails, this attack does nothing. If heads, during your opponent's next turn, prevent all damage from and effects of attacks done to this Pok\u00e9mon.",
game_text="Flip a coin. If tails, this attack does nothing. If heads, during your opponent's next turn, prevent all damage from and effects of attacks done to this Pokémon.",
cost={PokemonTypes.COLORLESS: 3},
damage=120,
effect=unimplemented,
effect=flip_or_nothing(then=_fly_success),
),
],
)
)

View File

@@ -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 mill_scaled_damage
from spirit.game.card_effects.support_common import is_energy
card = PokemonCardDef(
guid="9affb786-a38e-5429-84b8-37ebec853940",
@@ -24,7 +26,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 2, PokemonTypes.COLORLESS: 1},
damage=80,
damage_operator="x",
effect=unimplemented,
effect=mill_scaled_damage(5, 80, pred=is_energy),
),
Attack(
title="Massive Rend",

View File

@@ -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 self_energy_discard_attack, snipe_attack
card = PokemonCardDef(
guid="24eeb34e-c276-5fec-b384-33e4d9670532",
@@ -23,14 +24,14 @@ card = PokemonCardDef(
title="Sacred Fire",
game_text="This attack does 50 damage to 1 of your opponent's Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.FIRE: 1, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=snipe_attack(50, pool="any", count=1),
),
Attack(
title="Fire Blast",
game_text="Discard an Energy from this Pok\u00e9mon.",
cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1},
damage=120,
effect=unimplemented,
effect=self_energy_discard_attack(count=1),
),
],
)

View File

@@ -1,5 +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
from spirit.game.card_effects.pokemon import is_energy_card
async def aqua_storm(ctx):
"""Discard the top 5 of your deck; 50 damage per Energy card discarded
this way to each of 2 chosen opposing Benched Pokémon (no W/R)."""
milled = ctx.deck_top(5)
await ctx.discard_cards(milled)
energy_count = sum(1 for c in milled if is_energy_card(c))
bench = ctx.opponent_bench()
if not bench:
return
targets = await ctx.choose_cards(
bench, 2, prompt="Choose 2 of your opponent's Benched Pokémon",
)
if energy_count <= 0:
return
amount = 50 * energy_count
for target in targets:
await ctx.deal_damage(amount, target=target, apply_modifiers=False)
card = PokemonCardDef(
guid="b6921130-84f9-5a4b-b273-47fba72812f9",
@@ -22,7 +43,7 @@ card = PokemonCardDef(
title="Aqua Storm",
game_text="Discard the top 5 cards of your deck, and then choose 2 of your opponent's Benched Pok\u00e9mon. This attack does 50 damage for each Energy card you discarded in this way to each of those Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.WATER: 2, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=aqua_storm,
),
Attack(
title="Surf",

View File

@@ -1,5 +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
from spirit.game.card_effects.attacks_common import damage_per, lock_all_attacks
from spirit.game.session.legal_actions import energy_provided_count
def _both_actives_energy(ctx):
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
async def _deep_crush(ctx):
await ctx.deal_damage()
lock_all_attacks(ctx, ctx.attacker)
card = PokemonCardDef(
guid="2b73e21f-77dd-5462-8012-bea5eb1a13f1",
@@ -25,14 +43,14 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 2},
damage=20,
damage_operator="x",
effect=unimplemented,
effect=damage_per(_both_actives_energy, 20),
),
Attack(
title="Deep Crush",
game_text="During your next turn, this Pok\u00e9mon can't attack.",
cost={PokemonTypes.COLORLESS: 4},
damage=160,
effect=unimplemented,
effect=_deep_crush,
),
],
)

View File

@@ -1,5 +1,14 @@
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 snipe_attack
async def lunar_pain(ctx):
"""Double the number of damage counters on each of your opponent's Pokemon."""
for pokemon in ctx.opponent_pokemon_in_play():
current = (ctx.max_hp(pokemon) - pokemon.get_attribute(AttrID.HP, 0)) // 10
if current > 0:
await ctx.deal_damage(current * 10, target=pokemon, as_counters=True)
card = PokemonCardDef(
guid="4fd81eeb-9609-5689-b7c6-1c7892dd1f28",
@@ -24,14 +33,14 @@ card = PokemonCardDef(
title="Lunar Pain",
game_text="Double the number of damage counters on each of your opponent's Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 2},
effect=unimplemented,
effect=lunar_pain,
),
Attack(
title="Psychic Shot",
game_text="This attack also does 30 damage to 1 of your opponent's Benched Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 2},
damage=130,
effect=unimplemented,
effect=snipe_attack(30, also_base=True),
),
],
)

View File

@@ -1,5 +1,26 @@
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.session.effects import is_item_card
from spirit.game.card_effects.pokemon import in_active_spot
async def mysterious_tail(ctx):
"""You may look at the top 6 cards of your deck, reveal an Item card you
find there, and put it into your hand. Shuffle the rest back."""
if not await ctx.ask_yes_no("Look at the top 6 cards of your deck?"):
return
top = ctx.deck_top(6)
candidates = [c for c in top if is_item_card(c)]
picks = []
if candidates:
picks = await ctx.choose_cards(
candidates, 1, minimum=0,
prompt="Choose an Item card to put into your hand.",
display_cards=top,
)
await ctx.put_in_hand(picks, reveal=True)
await ctx.shuffle_deck()
card = PokemonCardDef(
guid="637c92b7-bbaa-5279-b62d-0dc204ee4e6c",
@@ -22,7 +43,9 @@ card = PokemonCardDef(
Ability(
title="Mysterious Tail",
game_text="Once during your turn, if this Pok\u00e9mon is in the Active Spot, you may look at the top 6 cards of your deck, reveal an Item card you find there, and put it into your hand. Shuffle the other cards back into your deck.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
condition=in_active_spot,
effect=mysterious_tail,
),
Attack(
title="Psyshot",

View File

@@ -1,5 +1,26 @@
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.session.effects import is_item_card
from spirit.game.card_effects.pokemon import in_active_spot
async def mysterious_tail(ctx):
"""You may look at the top 6 cards of your deck, reveal an Item card you
find there, and put it into your hand. Shuffle the rest back."""
if not await ctx.ask_yes_no("Look at the top 6 cards of your deck?"):
return
top = ctx.deck_top(6)
candidates = [c for c in top if is_item_card(c)]
picks = []
if candidates:
picks = await ctx.choose_cards(
candidates, 1, minimum=0,
prompt="Choose an Item card to put into your hand.",
display_cards=top,
)
await ctx.put_in_hand(picks, reveal=True)
await ctx.shuffle_deck()
card = PokemonCardDef(
guid="37a0065c-8bb7-5076-aa62-100bb1c1896e",
@@ -22,7 +43,9 @@ card = PokemonCardDef(
Ability(
title="Mysterious Tail",
game_text="Once during your turn, if this Pok\u00e9mon is in the Active Spot, you may look at the top 6 cards of your deck, reveal an Item card you find there, and put it into your hand. Shuffle the other cards back into your deck.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
condition=in_active_spot,
effect=mysterious_tail,
),
Attack(
title="Psyshot",

View File

@@ -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
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities, AttrID, TrainerType
from spirit.game.session.passives import Passive
from spirit.game.card_effects.passives_common import is_in_active_spot, boost_own_next_turn
class AbsoluteSpacePassive(Passive):
def blocks_trainer_play(self, card, player_id, carrier):
if player_id == carrier.owning_player_id:
return False
if not is_in_active_spot(carrier):
return False
return card.get_attribute(AttrID.TRAINER_TYPE) == TrainerType.STADIUM.value
card = PokemonCardDef(
guid="70959369-220f-5338-bdef-cfebe8044717",
@@ -21,7 +33,7 @@ card = PokemonCardDef(
Ability(
title="Absolute Space",
game_text="As long as this Pok\u00e9mon is in the Active Spot, your opponent can't play any Stadium cards from their hand.",
effect=unimplemented,
passive=AbsoluteSpacePassive(),
),
Attack(
title="Overdrive Smash",
@@ -29,7 +41,7 @@ card = PokemonCardDef(
cost={PokemonTypes.WATER: 1, PokemonTypes.COLORLESS: 2},
damage=80,
damage_operator="+",
effect=unimplemented,
effect=boost_own_next_turn(80, attack_title="Overdrive Smash"),
),
],
)

View File

@@ -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="3966cba0-7b69-5294-a19d-ed9721139381",
@@ -28,7 +29,7 @@ card = PokemonCardDef(
game_text="Flip a coin. If tails, this Pok\u00e9mon also does 10 damage to itself.",
cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 1},
damage=30,
effect=unimplemented,
effect=flip_damage(tails_self_damage=10),
),
],
)

View File

@@ -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.trainers import professors_research
card = SupporterCardDef(
guid="86d26aad-0a8b-5aa8-bec2-82c4ec9104a2",
@@ -11,5 +12,5 @@ card = SupporterCardDef(
collector_number=23,
set_code="CEL25",
rarity=Rarities.RareHolo,
effect=unimplemented
effect=professors_research
)

View File

@@ -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 spread_damage, bonus_if, named_in_play
card = PokemonCardDef(
guid="1134864a-2e23-5bd8-ae2b-16b9f2c992d9",
@@ -22,7 +23,7 @@ card = PokemonCardDef(
title="Scorching Wind",
game_text="This attack does 20 damage to each of your opponent's Benched Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.COLORLESS: 2},
effect=unimplemented,
effect=spread_damage(20, side="opponent"),
),
Attack(
title="Black Flame",
@@ -30,7 +31,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1},
damage=80,
damage_operator="+",
effect=unimplemented,
effect=bonus_if(named_in_play("Zekrom"), 80),
),
],
)

View File

@@ -1,5 +1,36 @@
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.trainers import is_basic_energy_card
def rush_in_condition(board, player_id, pokemon):
return pokemon is not board.active_pokemon(player_id)
async def rush_in(ctx):
"""Once during your turn, if this Pokémon is on your Bench, you may
switch it with your Active Pokémon."""
await ctx.switch_active(ctx.player_id, ctx.source)
async def solar_geyser(ctx):
"""Printed damage, then attach up to 2 basic Energy cards from your
discard pile to 1 of your Benched Pokémon."""
await ctx.deal_damage()
cards = [c for c in ctx.discard_pile() if is_basic_energy_card(c)]
bench = ctx.my_bench()
if not cards or not bench:
return
picks = await ctx.choose_cards(
cards, 2, minimum=1,
prompt="Choose up to 2 basic Energy cards from your discard pile to attach",
)
if not picks:
return
target = await ctx.choose_pokemon(bench, "Choose a Benched Pokémon") or bench[0]
for card in picks:
await ctx.attach_energy(card, target)
card = PokemonCardDef(
guid="9f4f4d4d-f28f-532a-a1da-09ec3186a1a5",
@@ -23,14 +54,16 @@ card = PokemonCardDef(
Ability(
title="Rush In",
game_text="Once during your turn, if this Pok\u00e9mon is on your Bench, you may switch it with your Active Pok\u00e9mon.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
condition=rush_in_condition,
effect=rush_in,
),
Attack(
title="Solar Geyser",
game_text="Attach up to 2 basic Energy cards from your discard pile to 1 of your Benched Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 2},
damage=100,
effect=unimplemented,
effect=solar_geyser,
),
],
)

View File

@@ -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 spread_damage
card = PokemonCardDef(
guid="fa92a5b1-0306-59dd-b692-cdebfc9985c9",
@@ -24,7 +25,7 @@ card = PokemonCardDef(
game_text="This attack also does 30 damage to each of your opponent's Benched Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.WATER: 3},
damage=160,
effect=unimplemented,
effect=spread_damage(30, side="opponent", also_base=True),
),
],
)

View File

@@ -1,5 +1,45 @@
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 (
AttrID, CLIENT_POKEMON_TYPE_NAMES, PokemonTypes, PokemonStage, Rarities,
)
from spirit.game.card_effects.trainers import is_basic_energy_card
async def breath_of_life(ctx):
"""Search up to 3 basic Energy of different types and attach them to your
Pokémon in any way you like. Then, shuffle your deck."""
deck_cards = list(ctx.deck(ctx.player_id))
reps = []
labels = {}
seen_types = []
for card in deck_cards:
if not is_basic_energy_card(card):
continue
types = card.get_attribute(AttrID.POKEMON_TYPES) or []
if not types or types[0] in seen_types:
continue
seen_types.append(types[0])
reps.append(card)
labels[card.entity_id] = f"{CLIENT_POKEMON_TYPE_NAMES[PokemonTypes(types[0])]} Energy"
if not reps:
await ctx.shuffle_deck()
return
picks = await ctx.choose_cards(
reps, 3, minimum=0,
prompt="Choose up to 3 basic Energy cards of different types.",
display_cards=deck_cards,
)
for energy in picks:
label = labels[energy.entity_id]
target = await ctx.choose_pokemon(
ctx.my_pokemon_in_play(), f"Choose a Pokémon to attach {label} to"
)
if target is not None:
await ctx.attach_energy(energy, target)
await ctx.shuffle_deck()
card = PokemonCardDef(
guid="bf180f7a-320b-507f-80e6-dede7ace3b13",
@@ -22,7 +62,7 @@ card = PokemonCardDef(
title="Breath of Life",
game_text="Search your deck for up to 3 basic Energy cards of different types and attach them to your Pok\u00e9mon in any way you like. Then, shuffle your deck.",
cost={PokemonTypes.COLORLESS: 2},
effect=unimplemented,
effect=breath_of_life,
),
Attack(
title="Aurora Horns",

View File

@@ -1,5 +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
from spirit.game.session.effects import is_special_energy
async def cry_of_destruction(ctx):
"""Discard up to 3 Special Energy from your opponent's Pokemon."""
energies = []
for pokemon in ctx.opponent_pokemon_in_play():
if ctx.effects_blocked(pokemon):
continue
energies.extend(e for e in ctx.attached_energies(pokemon) if is_special_energy(e))
if not energies:
return
picks = await ctx.choose_cards(
energies, min(3, len(energies)), minimum=0,
prompt="Discard up to 3 Special Energy from your opponent's Pokémon.",
)
await ctx.discard_cards(picks)
card = PokemonCardDef(
guid="904e53b8-7d9f-56b2-9258-ddd922439997",
@@ -23,7 +41,7 @@ card = PokemonCardDef(
title="Cry of Destruction",
game_text="Discard up to 3 Special Energy from your opponent's Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 2},
effect=unimplemented,
effect=cry_of_destruction,
),
Attack(
title="Dark Feather",

View File

@@ -1,5 +1,14 @@
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 AttrID, PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.pokemon import is_energy_card
from spirit.game.card_effects.support_common import search_attach_energy
from spirit.game.card_effects.attacks_common import damage_per, count_energy
def _is_psychic_energy_card(card):
types = card.get_attribute(AttrID.POKEMON_TYPES) or []
return is_energy_card(card) and PokemonTypes.PSYCHIC.value in types
card = PokemonCardDef(
guid="a61f827e-fa94-56e2-9dab-cf53202828b1",
@@ -21,7 +30,9 @@ card = PokemonCardDef(
Ability(
title="Roar of the Sword",
game_text="Once during your turn, you may search your deck for a Psychic Energy card and attach it to 1 of your Pok\u00e9mon. Then, shuffle your deck. If you use this Ability, your turn ends.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
ends_turn=True,
effect=search_attach_energy(predicate=_is_psychic_energy_card, count=1, distribute=False),
),
Attack(
title="Storm Slash",
@@ -29,7 +40,7 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 3},
damage=60,
damage_operator="+",
effect=unimplemented,
effect=damage_per(count_energy("self", energy_type=PokemonTypes.PSYCHIC), 30, base=60),
),
],
)

View File

@@ -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.passives_common import takes_less_passive
from spirit.game.card_effects.pokemon import is_pokemon_vmax
_is_fighting = lambda p: PokemonTypes.FIGHTING.value in (p.get_attribute(AttrID.POKEMON_TYPES) or [])
_protects_fighting_team = lambda target, carrier: (
target.owning_player_id == carrier.owning_player_id and _is_fighting(target)
)
_attacker_is_vmax = lambda attacker: is_pokemon_vmax(attacker.archetype_id)
card = PokemonCardDef(
guid="f4b957a8-0f26-5aeb-b968-294fdf0bee7e",
@@ -21,7 +29,10 @@ card = PokemonCardDef(
Ability(
title="Growl of the Shield",
game_text="All of your Fighting Pok\u00e9mon take 20 less damage from attacks from your opponent's Pok\u00e9mon VMAX (after applying Weakness and Resistance). You can't apply more than 1 Growl of the Shield Ability at a time.",
effect=unimplemented,
passive=takes_less_passive(
20, protects=_protects_fighting_team,
attacker_pred=_attacker_is_vmax, stack_key="GrowlOfTheShield",
),
),
Attack(
title="Heavy Impact",

View File

@@ -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, named_in_play
async def field_crush(ctx):
"""30 damage; if your opponent has a Stadium in play, discard it."""
await ctx.deal_damage()
stadium = ctx.stadium_in_play()
if stadium is not None and stadium.owning_player_id == ctx.opponent_id:
await ctx.discard_stadium()
card = PokemonCardDef(
guid="09b5c42b-c34c-54dc-a807-f4049b33d6dc",
@@ -23,7 +33,7 @@ card = PokemonCardDef(
game_text="If your opponent has a Stadium in play, discard it.",
cost={PokemonTypes.COLORLESS: 2},
damage=30,
effect=unimplemented,
effect=field_crush,
),
Attack(
title="White Thunder",
@@ -31,7 +41,7 @@ card = PokemonCardDef(
cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1},
damage=80,
damage_operator="+",
effect=unimplemented,
effect=bonus_if(named_in_play("Reshiram"), 80),
),
],
)

View File

@@ -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 flip_protection
card = PokemonCardDef(
guid="3e3bc7ff-4b50-57ff-9cd1-fbdfd61ee1fc",
@@ -22,7 +23,7 @@ card = PokemonCardDef(
title="Bustle",
game_text="Flip a coin. If heads, during your opponent's next turn, prevent all damage from and effects of attacks done to this Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=flip_protection(prevent=True, effects_too=True),
),
Attack(
title="Slap",

View File

@@ -1,5 +1,27 @@
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 count_energy, flip_or_nothing
from spirit.game.card_effects.support_common import search_attach_energy
from spirit.game.card_effects.trainers import is_grass_energy_card
_grass_attached = count_energy("self", energy_type=PokemonTypes.GRASS)
growing_tall = flip_or_nothing(then=search_attach_energy(
is_grass_energy_card, count=5,
prompt="Choose up to 5 Grass Energy cards to attach to your Pokémon.",
))
async def head_swing(ctx):
"""30 to 1 opposing Pokemon per Grass Energy attached (no W/R on Bench)."""
amount = 30 * _grass_attached(ctx)
if amount <= 0:
return
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(amount, target=target)
card = PokemonCardDef(
guid="010594a7-0853-5833-933f-009a6aea83f6",
@@ -22,13 +44,13 @@ card = PokemonCardDef(
title="Growing Tall",
game_text="Flip a coin. If heads, search your deck for up to 5 Grass Energy cards and attach them to your Pok\u00e9mon in any way you like. Then, shuffle your deck.",
cost={PokemonTypes.GRASS: 1},
effect=unimplemented,
effect=growing_tall,
),
Attack(
title="Head Swing",
game_text="This attack does 30 damage to 1 of your opponent's Pok\u00e9mon for each Grass Energy attached to this Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.COLORLESS: 3},
effect=unimplemented,
effect=head_swing,
),
],
)

View File

@@ -1,5 +1,27 @@
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 count_energy, flip_or_nothing
from spirit.game.card_effects.support_common import search_attach_energy
from spirit.game.card_effects.trainers import is_grass_energy_card
_grass_attached = count_energy("self", energy_type=PokemonTypes.GRASS)
growing_tall = flip_or_nothing(then=search_attach_energy(
is_grass_energy_card, count=5,
prompt="Choose up to 5 Grass Energy cards to attach to your Pokémon.",
))
async def head_swing(ctx):
"""30 to 1 opposing Pokemon per Grass Energy attached (no W/R on Bench)."""
amount = 30 * _grass_attached(ctx)
if amount <= 0:
return
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(amount, target=target)
card = PokemonCardDef(
guid="4076648a-3fa3-560e-8533-22af1c518294",
@@ -22,13 +44,13 @@ card = PokemonCardDef(
title="Growing Tall",
game_text="Flip a coin. If heads, search your deck for up to 5 Grass Energy cards and attach them to your Pok\u00e9mon in any way you like. Then, shuffle your deck.",
cost={PokemonTypes.GRASS: 1},
effect=unimplemented,
effect=growing_tall,
),
Attack(
title="Head Swing",
game_text="This attack does 30 damage to 1 of your opponent's Pok\u00e9mon for each Grass Energy attached to this Pok\u00e9mon. (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.COLORLESS: 3},
effect=unimplemented,
effect=head_swing,
),
],
)

View File

@@ -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.card_effects.support_common import search_to_hand
from spirit.game.data_utils import PokemonCardDef, Attack, Ability
from spirit.game.attributes import AttrID, PokemonTypes, PokemonStage, Rarities
async def super_fang(ctx):
"""Damage counters on the opponent's Active until its remaining HP is 10."""
target = ctx.defender
if target is None:
return
hp = target.get_attribute(AttrID.HP, 0)
if hp > 10:
await ctx.deal_damage(hp - 10, target=target, as_counters=True)
card = PokemonCardDef(
guid="2d7be742-d405-5c3c-befb-5ef93a4db3ea",
@@ -23,13 +35,16 @@ card = PokemonCardDef(
title="Chase Up",
game_text="Search your deck for a card and put it into your hand. Then, shuffle your deck.",
cost={PokemonTypes.DARKNESS: 1},
effect=unimplemented,
effect=search_to_hand(
None, count=1, reveal=False,
prompt="Choose a card to put into your hand.",
),
),
Attack(
title="Super Fang",
game_text="Put damage counters on your opponent's Active Pok\u00e9mon until its remaining HP is 10.",
game_text="Put damage counters on your opponent's Active Pokémon until its remaining HP is 10.",
cost={PokemonTypes.COLORLESS: 3},
effect=unimplemented,
effect=super_fang,
),
],
)
)

View File

@@ -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="5f958813-ec88-5319-99da-07def6d246ce",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
game_text="Flip a coin. If tails, this attack does nothing.",
cost={PokemonTypes.COLORLESS: 2},
damage=50,
effect=unimplemented,
effect=flip_or_nothing(),
),
],
)

View File

@@ -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 count_energy, flip_damage
from spirit.game.card_effects.passives_common import flip_prevent_damage_passive
card = PokemonCardDef(
guid="822d008a-f144-56e1-ae7e-3779ccfd515c",
@@ -22,7 +24,7 @@ card = PokemonCardDef(
Ability(
title="Primate Dexterity",
game_text="If any damage is done to this Pok\u00e9mon by attacks, flip a coin. If heads, prevent that damage.",
effect=unimplemented,
passive=flip_prevent_damage_passive("Primate Dexterity"),
),
Attack(
title="Full Tilt Fling",
@@ -30,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 1},
damage=60,
damage_operator="x",
effect=unimplemented,
effect=flip_damage(coins_from=count_energy("self"), per_heads=60),
),
],
)

View File

@@ -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 heal_attack
card = PokemonCardDef(
guid="f26bde5e-3a31-57e4-9922-90109bcac0ab",
@@ -24,14 +26,15 @@ card = PokemonCardDef(
game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed and Poisoned.",
cost={PokemonTypes.COLORLESS: 1},
damage=20,
effect=unimplemented,
effect=condition_attack(
SpecialConditions.PARALYZED, SpecialConditions.POISONED, flip=True),
),
Attack(
title="Absorb",
game_text="Heal 50 damage from this Pok\u00e9mon.",
cost={PokemonTypes.GRASS: 1, PokemonTypes.COLORLESS: 1},
damage=50,
effect=unimplemented,
effect=heal_attack(amount=50),
),
],
)

View File

@@ -1,5 +1,14 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, def_for
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.passives_common import team_damage_boost_passive
from spirit.game.session.effects import is_basic_pokemon, is_water_pokemon
def _ice_symbol_attacker(pokemon):
if not (is_basic_pokemon(pokemon) and is_water_pokemon(pokemon)):
return False
definition = def_for(pokemon.archetype_id)
return getattr(definition, "display_name", "") != "Articuno"
card = PokemonCardDef(
guid="cde48857-1d2a-5303-8d56-23de5aba41b2",
@@ -21,7 +30,7 @@ card = PokemonCardDef(
Ability(
title="Ice Symbol",
game_text="Your Basic Water Pok\u00e9mon's attacks, except any Articuno, do 10 more damage to your opponent's Active Pok\u00e9mon (before applying Weakness and Resistance).",
effect=unimplemented,
passive=team_damage_boost_passive(10, attacker_pred=_ice_symbol_attacker),
),
Attack(
title="Freezing Wind",

View File

@@ -1,5 +1,18 @@
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.passives_common import Passive, carrier_pokemon, is_in_active_spot
class ReassuringDamPassive(Passive):
def blocks_discard(self, card, carrier):
pokemon = carrier_pokemon(carrier)
if pokemon is None or is_in_active_spot(pokemon):
return False
if card.owning_player_id != pokemon.owning_player_id:
return False
parent = getattr(card, "parent", None)
return bool(parent) and parent.get_attribute(AttrID.NAME) == "deck"
card = PokemonCardDef(
guid="b836a69f-4559-5d3c-aba3-bdd242fdcf57",
@@ -21,8 +34,8 @@ card = PokemonCardDef(
abilities=[
Ability(
title="Reassuring Dam",
game_text="As long as this Pok\u00e9mon is on your Bench, cards in your deck can't be discarded by effects of your opponent's attacks, Abilities, Item cards, or Supporter cards.",
effect=unimplemented,
game_text="As long as this Pokémon is on your Bench, cards in your deck can't be discarded by effects of your opponent's attacks, Abilities, Item cards, or Supporter cards.",
passive=ReassuringDamPassive(),
),
Attack(
title="Hammer In",
@@ -30,4 +43,4 @@ card = PokemonCardDef(
damage=80,
),
],
)
)

View File

@@ -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="f1d171c0-781c-56dc-a3af-4701a785ac44",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
game_text="This Pok\u00e9mon also does 10 damage to itself.",
cost={PokemonTypes.COLORLESS: 3},
damage=50,
effect=unimplemented,
effect=recoil_attack(10),
),
],
)

View File

@@ -1,5 +1,36 @@
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
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
async def blanche(ctx):
"""Draw 2. If you drew any, flip a coin; heads attaches a Water Energy from your discard to a Benched Pokemon."""
drawn = await ctx.draw_cards(2)
if not drawn:
return
flips = await ctx.flip_coins(1, "Blanche")
if not flips[0]:
return
bench = ctx.my_bench()
if not bench:
return
energies = [c for c in ctx.discard_pile() if _is_water_energy_card(c)]
if not energies:
return
picks = await ctx.choose_cards(
energies, 1, minimum=1,
prompt="Choose a Water Energy card to attach",
)
if not picks:
return
target = await ctx.choose_pokemon(bench, "Choose a Benched Pokémon") or bench[0]
await ctx.attach_energy(picks[0], target)
card = SupporterCardDef(
guid="b89d3384-9761-5044-b628-1bf669a74f9b",
@@ -11,5 +42,5 @@ card = SupporterCardDef(
collector_number=64,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=blanche,
)

View File

@@ -1,5 +1,36 @@
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
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
async def blanche(ctx):
"""Draw 2. If you drew any, flip a coin; heads attaches a Water Energy from your discard to a Benched Pokemon."""
drawn = await ctx.draw_cards(2)
if not drawn:
return
flips = await ctx.flip_coins(1, "Blanche")
if not flips[0]:
return
bench = ctx.my_bench()
if not bench:
return
energies = [c for c in ctx.discard_pile() if _is_water_energy_card(c)]
if not energies:
return
picks = await ctx.choose_cards(
energies, 1, minimum=1,
prompt="Choose a Water Energy card to attach",
)
if not picks:
return
target = await ctx.choose_pokemon(bench, "Choose a Benched Pokémon") or bench[0]
await ctx.attach_energy(picks[0], target)
card = SupporterCardDef(
guid="cd61417a-2231-5a88-bf24-355dcb158eae",
@@ -11,5 +42,5 @@ card = SupporterCardDef(
collector_number=82,
set_code="PGO",
rarity=Rarities.RareRainbow,
effect=unimplemented
effect=blanche,
)

View File

@@ -1,5 +1,21 @@
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.support_common import search_attach_energy
from spirit.game.card_effects.attacks_common import damage_per, count_energy
_vitality_spring_search = search_attach_energy(count=6, distribute=True, shuffle=True)
async def vitality_spring(ctx):
"""Once during your turn, you may search up to 6 Energy and attach them
in any way you like. If you do, your turn ends."""
if await ctx.ask_yes_no(
"Search your deck for up to 6 Energy cards and attach them to "
"your Pokémon in any way you like?"
):
await _vitality_spring_search(ctx)
ctx.ends_turn = True
card = PokemonCardDef(
guid="49dab7f1-a23f-5747-954d-9305cf47d5df",
@@ -22,7 +38,8 @@ card = PokemonCardDef(
Ability(
title="Vitality Spring",
game_text="Once during your turn, you may search your deck for up to 6 Energy cards and attach them to your Pok\u00e9mon in any way you like. Then, shuffle your deck. If you use this Ability, your turn ends.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
effect=vitality_spring,
),
Attack(
title="Hydro Pump",
@@ -30,7 +47,9 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 4},
damage=90,
damage_operator="+",
effect=unimplemented,
effect=damage_per(
count_energy("self", energy_type=PokemonTypes.WATER), 30, base=90
),
),
],
)

View File

@@ -1,5 +1,16 @@
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
async def enriching_egg(ctx):
"""Heal all damage from 1 of your Benched Pokemon."""
bench = [p for p in ctx.my_bench() if p.get_attribute(AttrID.HP, 0) < ctx.max_hp(p)]
if not bench:
return
target = await ctx.choose_pokemon(bench, "Choose a Benched Pokémon to heal")
if target is not None:
await ctx.heal(ctx.max_hp(target) - target.get_attribute(AttrID.HP, 0), target)
card = PokemonCardDef(
guid="a1534f88-20e1-5912-88f1-0642734752be",
@@ -23,7 +34,7 @@ card = PokemonCardDef(
title="Enriching Egg",
game_text="Heal all damage from 1 of your Benched Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=enriching_egg,
),
Attack(
title="Zen Headbutt",

View File

@@ -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="d9bf59c3-dd42-583a-9a26-c2dd2eb69d91",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
title="Split Bomb",
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.FIRE: 1, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=snipe_attack(50, pool="any", count=2),
),
Attack(
title="Heat Blast",

View File

@@ -1,5 +1,37 @@
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 Rarities, PokemonTypes, AttrID
from spirit.game.card_effects.trainers import is_basic_energy_card
async def candela(ctx):
"""Draw 2. If you drew any cards, flip a coin; heads attaches a Fire
Energy card from your discard pile to 1 of your Benched Pokemon."""
drawn = await ctx.draw_cards(2)
if drawn <= 0:
return
heads = (await ctx.flip_coins(1, "Candela"))[0]
if not heads:
return
bench = ctx.my_bench()
energies = [
c for c in ctx.discard_pile()
if is_basic_energy_card(c)
and PokemonTypes.FIRE.value in (c.get_attribute(AttrID.POKEMON_TYPES) or [])
]
if not bench or not energies:
return
picks = await ctx.choose_cards(
energies, 1, minimum=1,
prompt="Choose a Fire Energy card from your discard pile.",
)
if not picks:
return
target = await ctx.choose_pokemon(
bench, "Choose a Benched Pokémon to attach Fire Energy to"
)
if target is not None:
await ctx.attach_energy(picks[0], target)
card = SupporterCardDef(
guid="e4edecca-d6f5-5615-95ff-18f243103caf",
@@ -11,5 +43,5 @@ card = SupporterCardDef(
collector_number=65,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=candela
)

View File

@@ -1,5 +1,37 @@
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 Rarities, PokemonTypes, AttrID
from spirit.game.card_effects.trainers import is_basic_energy_card
async def candela(ctx):
"""Draw 2. If you drew any cards, flip a coin; heads attaches a Fire
Energy card from your discard pile to 1 of your Benched Pokemon."""
drawn = await ctx.draw_cards(2)
if drawn <= 0:
return
heads = (await ctx.flip_coins(1, "Candela"))[0]
if not heads:
return
bench = ctx.my_bench()
energies = [
c for c in ctx.discard_pile()
if is_basic_energy_card(c)
and PokemonTypes.FIRE.value in (c.get_attribute(AttrID.POKEMON_TYPES) or [])
]
if not bench or not energies:
return
picks = await ctx.choose_cards(
energies, 1, minimum=1,
prompt="Choose a Fire Energy card from your discard pile.",
)
if not picks:
return
target = await ctx.choose_pokemon(
bench, "Choose a Benched Pokémon to attach Fire Energy to"
)
if target is not None:
await ctx.attach_energy(picks[0], target)
card = SupporterCardDef(
guid="71160c61-af09-5d56-aafd-b1886d76a0d4",
@@ -11,5 +43,5 @@ card = SupporterCardDef(
collector_number=83,
set_code="PGO",
rarity=Rarities.RareRainbow,
effect=unimplemented
effect=candela
)

View File

@@ -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_targets
card = PokemonCardDef(
guid="8506860a-4fe4-5cf8-ae12-ed16c472b45b",
@@ -22,7 +23,7 @@ card = PokemonCardDef(
title="Delicious Egg",
game_text="Heal 30 damage from 1 of your Benched Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=heal_targets(30, "bench_choice"),
),
Attack(
title="Gentle Slap",

View File

@@ -1,5 +1,26 @@
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 self_energy_discard_attack
from spirit.game.card_effects.pokemon import energy_provides_type
from spirit.game.data_utils import PokemonCardDef, Attack, Ability
from spirit.game.attributes import AttrID, PokemonTypes, PokemonStage, Rarities
from spirit.game.session.passives import Passive, active_passives
class BurnBrightlyPassive(Passive):
def modify_energy_provided(self, options, energy, holder, board):
if holder is None or energy.get_attribute(AttrID.IS_SPECIAL_ENERGY):
return options
if not energy_provides_type(energy, PokemonTypes.FIRE.value):
return options
if any(len(option) >= 2 for option in options):
return options
active_here = any(
isinstance(p, BurnBrightlyPassive) and c.owning_player_id == holder.owning_player_id
for p, c in active_passives(board)
)
if not active_here:
return options
return [list(option) * 2 for option in options]
card = PokemonCardDef(
guid="0102ad8f-c8f6-5a87-ad03-66c7fe4282b3",
@@ -22,14 +43,14 @@ card = PokemonCardDef(
Ability(
title="Burn Brightly",
game_text="Each basic Fire Energy attached to your Pok\u00e9mon provides FireFire Energy. You can't apply more than 1 Burn Brightly Ability at a time.",
effect=unimplemented,
passive=BurnBrightlyPassive(),
),
Attack(
title="Flare Blitz",
game_text="Discard all Fire Energy from this Pok\u00e9mon.",
cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 2},
damage=170,
effect=unimplemented,
effect=self_energy_discard_attack(all_energy=True, energy_type=PokemonTypes.FIRE),
),
],
)

View File

@@ -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 energy_provides_type
from spirit.game.card_effects.support_common import search_attach_energy
card = PokemonCardDef(
guid="83d1e4c7-0f9e-54ea-abc8-e4abe32cabdd",
@@ -23,7 +25,11 @@ card = PokemonCardDef(
game_text="Search your deck for a Fire Energy card and attach it to this Pok\u00e9mon. Then, shuffle your deck.",
cost={PokemonTypes.FIRE: 1},
damage=10,
effect=unimplemented,
effect=search_attach_energy(
predicate=lambda c: energy_provides_type(c, PokemonTypes.FIRE.value),
count=1, to_self=True,
prompt="Choose a Fire Energy card to attach.",
),
),
],
)

View File

@@ -1,4 +1,5 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.card_effects.attacks_common import self_energy_discard_attack
from spirit.game.data_utils import PokemonCardDef, Attack, Ability
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
card = PokemonCardDef(
@@ -29,7 +30,7 @@ card = PokemonCardDef(
game_text="Discard a Fire Energy from this Pok\u00e9mon.",
cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 2},
damage=100,
effect=unimplemented,
effect=self_energy_discard_attack(count=1, energy_type=PokemonTypes.FIRE),
),
],
)

View File

@@ -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.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
async def counter(ctx):
"""20 damage, +that much more if this Pokemon was damaged by an attack during the opponent's last turn."""
bonus = ctx.damage_taken_last_turn(ctx.attacker)
await ctx.deal_damage(20 + bonus)
card = PokemonCardDef(
guid="a636cb92-fb8b-563d-a978-eb976e5123a6",
@@ -24,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1},
damage=20,
damage_operator="+",
effect=unimplemented,
effect=counter,
),
Attack(
title="Dynamic Punch",
@@ -32,7 +40,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1, PokemonTypes.COLORLESS: 2},
damage=90,
damage_operator="+",
effect=unimplemented,
effect=condition_attack(SpecialConditions.CONFUSED, flip=True, heads_bonus_damage=90),
),
],
)

View File

@@ -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.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
async def counter(ctx):
"""20 damage, +that much more if this Pokemon was damaged by an attack during the opponent's last turn."""
bonus = ctx.damage_taken_last_turn(ctx.attacker)
await ctx.deal_damage(20 + bonus)
card = PokemonCardDef(
guid="b840bfb9-657c-5d73-88ad-6fce80654bd6",
@@ -24,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1},
damage=20,
damage_operator="+",
effect=unimplemented,
effect=counter,
),
Attack(
title="Dynamic Punch",
@@ -32,7 +40,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1, PokemonTypes.COLORLESS: 2},
damage=90,
damage_operator="+",
effect=unimplemented,
effect=condition_attack(SpecialConditions.CONFUSED, flip=True, heads_bonus_damage=90),
),
],
)

View File

@@ -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.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
async def counter(ctx):
"""20 damage, +that much more if this Pokemon was damaged by an attack during the opponent's last turn."""
bonus = ctx.damage_taken_last_turn(ctx.attacker)
await ctx.deal_damage(20 + bonus)
card = PokemonCardDef(
guid="a6cded83-22e8-57ab-bae6-f51a1a627c0e",
@@ -24,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1},
damage=20,
damage_operator="+",
effect=unimplemented,
effect=counter,
),
Attack(
title="Dynamic Punch",
@@ -32,7 +40,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 1, PokemonTypes.COLORLESS: 2},
damage=90,
damage_operator="+",
effect=unimplemented,
effect=condition_attack(SpecialConditions.CONFUSED, flip=True, heads_bonus_damage=90),
),
],
)

View File

@@ -1,5 +1,6 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.data_utils import PokemonCardDef, Ability
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.pokemon import SuddenTransformationPassive
card = PokemonCardDef(
guid="b37b1ba4-3f8a-555a-bf98-caaccf0e8dfe",
@@ -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(),
),
],
)

View File

@@ -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.attacks_common import lock_all_attacks
from spirit.game.card_effects.pokemon import energy_provides_type
from spirit.game.card_effects.support_common import look_top_attach_energy
def _is_water_or_lightning_energy(card):
return (
energy_provides_type(card, PokemonTypes.WATER.value)
or energy_provides_type(card, PokemonTypes.LIGHTNING.value)
)
async def giga_impact(ctx):
"""250 damage. During your next turn, this Pokemon can't attack."""
await ctx.deal_damage()
lock_all_attacks(ctx, ctx.attacker)
draconic_star = look_top_attach_energy(12, predicate=_is_water_or_lightning_energy)
card = PokemonCardDef(
guid="58fa5388-ecc3-5215-ae28-39216771ceae",
@@ -23,13 +42,14 @@ card = PokemonCardDef(
game_text="During your next turn, this Pok\u00e9mon can't attack.",
cost={PokemonTypes.WATER: 1, PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 2},
damage=250,
effect=unimplemented,
effect=giga_impact,
),
Attack(
title="Draconic Star",
game_text="Look at the top 12 cards of your deck and attach any number of Water or Lightning Energy cards you find there to your Pok\u00e9mon in any way you like. Shuffle the other cards back into your deck. (You can't use more than 1 VSTAR Power in a game.)",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
vstar=True,
effect=draconic_star,
),
],
)

View File

@@ -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.attacks_common import lock_all_attacks
from spirit.game.card_effects.pokemon import energy_provides_type
from spirit.game.card_effects.support_common import look_top_attach_energy
def _is_water_or_lightning_energy(card):
return (
energy_provides_type(card, PokemonTypes.WATER.value)
or energy_provides_type(card, PokemonTypes.LIGHTNING.value)
)
async def giga_impact(ctx):
"""250 damage. During your next turn, this Pokemon can't attack."""
await ctx.deal_damage()
lock_all_attacks(ctx, ctx.attacker)
draconic_star = look_top_attach_energy(12, predicate=_is_water_or_lightning_energy)
card = PokemonCardDef(
guid="a56db22e-4b35-56f3-ba94-732e42148400",
@@ -23,13 +42,14 @@ card = PokemonCardDef(
game_text="During your next turn, this Pok\u00e9mon can't attack.",
cost={PokemonTypes.WATER: 1, PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 2},
damage=250,
effect=unimplemented,
effect=giga_impact,
),
Attack(
title="Draconic Star",
game_text="Look at the top 12 cards of your deck and attach any number of Water or Lightning Energy cards you find there to your Pok\u00e9mon in any way you like. Shuffle the other cards back into your deck. (You can't use more than 1 VSTAR Power in a game.)",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
vstar=True,
effect=draconic_star,
),
],
)

View File

@@ -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="f56edb89-68c2-555a-844a-fb0460efae15",
@@ -19,10 +20,10 @@ card = PokemonCardDef(
abilities=[
Attack(
title="Hyper Beam",
game_text="Discard an Energy from your opponent's Active Pok\u00e9mon.",
game_text="Discard an Energy from your opponent's Active Pokémon.",
cost={PokemonTypes.WATER: 1, PokemonTypes.LIGHTNING: 1},
damage=60,
effect=unimplemented,
effect=discard_opponent_energy_attack(count=1),
),
Attack(
title="Buster Tail",
@@ -30,4 +31,4 @@ card = PokemonCardDef(
damage=160,
),
],
)
)

View File

@@ -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="f9dbadd3-d1f8-5518-b84b-4ce04911567c",
@@ -19,10 +20,10 @@ card = PokemonCardDef(
abilities=[
Attack(
title="Hyper Beam",
game_text="Discard an Energy from your opponent's Active Pok\u00e9mon.",
game_text="Discard an Energy from your opponent's Active Pokémon.",
cost={PokemonTypes.WATER: 1, PokemonTypes.LIGHTNING: 1},
damage=60,
effect=unimplemented,
effect=discard_opponent_energy_attack(count=1),
),
Attack(
title="Buster Tail",
@@ -30,4 +31,4 @@ card = PokemonCardDef(
damage=160,
),
],
)
)

View File

@@ -1,6 +1,20 @@
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 whiny_voice(ctx):
"""Choose a random card from your opponent's hand. Your opponent reveals
that card and shuffles it into their deck."""
hand = ctx.hand(ctx.opponent_id)
if not hand:
return
card = random.choice(hand)
await ctx.reveal_cards([card])
await ctx.shuffle_into_deck([card], ctx.opponent_id)
card = PokemonCardDef(
guid="d61727a9-46c1-5dfc-b296-1b53d72a7352",
key="PGO",
@@ -22,7 +36,7 @@ card = PokemonCardDef(
title="Whiny Voice",
game_text="Choose a random card from your opponent's hand. Your opponent reveals that card and shuffles it into their deck.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=whiny_voice,
),
Attack(
title="Tackle",

View File

@@ -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.support_common import search_to_bench
from spirit.game.session.effects import is_basic_pokemon
async def egg_incubator(ctx):
"""Flip a coin. Heads: search a Basic Pokemon onto your Bench, shuffle.
Tails: put this card on the bottom of your deck instead of the discard pile."""
heads = (await ctx.flip_coins(1, "Egg Incubator"))[0]
if heads:
await search_to_bench(
predicate=is_basic_pokemon, count=1,
prompt="Choose a Basic Pokémon to put onto your Bench.",
)(ctx)
else:
await ctx.put_on_bottom_of_deck(ctx.source)
card = ItemCardDef(
guid="10bee995-3c57-5f12-98d3-3f1b45c62180",
@@ -11,5 +27,5 @@ card = ItemCardDef(
collector_number=66,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=egg_incubator,
)

View File

@@ -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.support_common import search_to_bench
from spirit.game.session.effects import is_basic_pokemon
async def egg_incubator(ctx):
"""Flip a coin. Heads: search a Basic Pokemon onto your Bench, shuffle.
Tails: put this card on the bottom of your deck instead of the discard pile."""
heads = (await ctx.flip_coins(1, "Egg Incubator"))[0]
if heads:
await search_to_bench(
predicate=is_basic_pokemon, count=1,
prompt="Choose a Basic Pokémon to put onto your Bench.",
)(ctx)
else:
await ctx.put_on_bottom_of_deck(ctx.source)
card = ItemCardDef(
guid="440d3bd9-bc70-5512-ba16-a634259fc77d",
@@ -11,5 +27,5 @@ card = ItemCardDef(
collector_number=87,
set_code="PGO",
rarity=Rarities.RareSecret,
effect=unimplemented
effect=egg_incubator,
)

View File

@@ -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 bonus_if
def _entered_active_this_turn(ctx):
return ctx.entered_active_this_turn(ctx.attacker)
card = PokemonCardDef(
guid="c5904577-7950-5bbb-b720-30dc82474c01",
@@ -25,7 +31,7 @@ card = PokemonCardDef(
cost={PokemonTypes.WATER: 1},
damage=20,
damage_operator="+",
effect=unimplemented,
effect=bonus_if(_entered_active_this_turn, 90),
),
Attack(
title="Slash",

View File

@@ -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.attacks_common import mill_attack
async def wreak_havoc(ctx):
"""Flip a coin until you get tails. For each heads, discard the top 2
cards of your opponent's deck."""
heads = await ctx.flip_until_tails("Wreak Havoc")
if heads:
await ctx.discard_cards(ctx.deck_top(heads * 2, player_id=ctx.opponent_id))
card = PokemonCardDef(
guid="d9f36c0f-95d4-5b0b-90c7-f3e460535e53",
@@ -23,14 +32,14 @@ card = PokemonCardDef(
title="Wreak Havoc",
game_text="Flip a coin until you get tails. For each heads, discard the top 2 cards of your opponent's deck.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=wreak_havoc,
),
Attack(
title="Wild Splash",
game_text="Discard the top 5 cards of your deck.",
cost={PokemonTypes.WATER: 2, PokemonTypes.COLORLESS: 2},
damage=230,
effect=unimplemented,
effect=mill_attack(5, opponent=False),
),
],
)

View File

@@ -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_pokemon_card
card = PokemonCardDef(
guid="cc24f629-0c9d-51da-9bbb-0bd69465899f",
@@ -23,7 +25,10 @@ card = PokemonCardDef(
title="Summoning Aroma",
game_text="Search your deck for up to 2 Pok\u00e9mon, reveal them, and put them into your hand. Then, shuffle your deck.",
cost={PokemonTypes.GRASS: 1},
effect=unimplemented,
effect=search_to_hand(
is_pokemon_card, count=2, reveal=True,
prompt="Choose up to 2 Pokémon to put into your hand.",
),
),
Attack(
title="Razor Leaf",

View File

@@ -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="8a31d277-f608-586a-81b1-4c44d9a63f12",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.",
cost={PokemonTypes.WATER: 1},
damage=20,
effect=unimplemented,
effect=condition_attack(SpecialConditions.PARALYZED, flip=True),
),
Attack(
title="Surf",

View File

@@ -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="8771ac8b-2fbd-51ca-a017-543027ff58b7",
@@ -24,7 +25,7 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 1},
damage=10,
damage_operator="+",
effect=unimplemented,
effect=flip_bonus(10),
),
],
)

View File

@@ -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="81288712-7261-5003-9462-a3e56043a648",
@@ -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),
),
],
)

View File

@@ -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.session.effects import is_pokemon_card
async def lure_module_effect(ctx):
"""Each player reveals the top 3 of their deck; Pokemon found go to hand,
the rest is shuffled back."""
for pid in (ctx.player_id, ctx.opponent_id):
top = ctx.deck_top(3, player_id=pid)
if not top:
continue
await ctx.reveal_cards(top)
matches = [c for c in top if is_pokemon_card(c)]
if matches:
await ctx.put_in_hand(matches, reveal=False)
await ctx.shuffle_deck(player_id=pid)
card = ItemCardDef(
guid="d17d2b10-6400-5d79-9ae6-63504e64447f",
@@ -11,5 +27,5 @@ card = ItemCardDef(
collector_number=67,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=lure_module_effect
)

View File

@@ -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.session.effects import is_pokemon_card
async def lure_module_effect(ctx):
"""Each player reveals the top 3 of their deck; Pokemon found go to hand,
the rest is shuffled back."""
for pid in (ctx.player_id, ctx.opponent_id):
top = ctx.deck_top(3, player_id=pid)
if not top:
continue
await ctx.reveal_cards(top)
matches = [c for c in top if is_pokemon_card(c)]
if matches:
await ctx.put_in_hand(matches, reveal=False)
await ctx.shuffle_deck(player_id=pid)
card = ItemCardDef(
guid="132ad4d9-8492-544c-afd9-f8899fd7ac12",
@@ -11,5 +27,5 @@ card = ItemCardDef(
collector_number=88,
set_code="PGO",
rarity=Rarities.RareSecret,
effect=unimplemented
effect=lure_module_effect
)

View File

@@ -1,5 +1,15 @@
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.support_common import search_to_hand
from spirit.game.card_effects.attacks_common import damage_per, count_discard
def _is_magikarp(card):
return card.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) == "Magikarp"
def _is_magikarp_or_gyarados(card):
return card.get_attribute(AttrID.EVOLUTION_LOGIC_NAME) in ("Magikarp", "Gyarados")
card = PokemonCardDef(
guid="9c7cea7e-3a5f-51fd-9b32-4edaa5613811",
@@ -22,7 +32,7 @@ card = PokemonCardDef(
title="Lively Grouping",
game_text="Search your deck for any number of Magikarp, reveal them, and put them into your hand. Then, shuffle your deck.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=search_to_hand(_is_magikarp, count=60, minimum=0, reveal=True),
),
Attack(
title="Raging Fin",
@@ -30,7 +40,7 @@ card = PokemonCardDef(
cost={PokemonTypes.COLORLESS: 2},
damage=10,
damage_operator="+",
effect=unimplemented,
effect=damage_per(count_discard("mine", pred=_is_magikarp_or_gyarados), 30, base=10),
),
],
)

View File

@@ -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 damage_per, count_energy
def _extra_metal_energy(ctx) -> int:
return max(0, count_energy("self", energy_type=PokemonTypes.METAL)(ctx) - 3)
card = PokemonCardDef(
guid="a5db3f2a-bd2a-5f4c-bd92-e5639bb833a5",
@@ -26,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.METAL: 3},
damage=160,
damage_operator="+",
effect=unimplemented,
effect=damage_per(_extra_metal_energy, 60, base=160, cap=280),
),
],
)

View File

@@ -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 damage_per, count_energy
def _extra_metal_energy(ctx) -> int:
return max(0, count_energy("self", energy_type=PokemonTypes.METAL)(ctx) - 3)
card = PokemonCardDef(
guid="28f9a8c6-31ad-5713-9fe3-6d6a54c5e278",
@@ -26,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.METAL: 3},
damage=160,
damage_operator="+",
effect=unimplemented,
effect=damage_per(_extra_metal_energy, 60, base=160, cap=280),
),
],
)

View File

@@ -1,5 +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
from spirit.game.card_effects.trainers import is_metal_energy_card
async def _arm_charge(ctx):
await ctx.deal_damage()
energies = [c for c in ctx.hand() if is_metal_energy_card(c)]
if not energies:
return
if not await ctx.ask_yes_no(
"Attach a Metal Energy card from your hand to this Pokémon?"
):
return
picked = await ctx.choose_cards(
energies, 1, minimum=1, prompt="Choose a Metal Energy card to attach"
)
if picked:
await ctx.attach_energy(picked[0], ctx.attacker)
card = PokemonCardDef(
guid="b77472f2-c926-5367-b469-8e680bc0e647",
@@ -24,7 +42,7 @@ card = PokemonCardDef(
game_text="You may attach a Metal Energy card from your hand to this Pok\u00e9mon.",
cost={PokemonTypes.METAL: 2},
damage=50,
effect=unimplemented,
effect=_arm_charge,
),
Attack(
title="Mega Punch",

View File

@@ -1,5 +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
from spirit.game.card_effects.trainers import is_metal_energy_card
async def _arm_charge(ctx):
await ctx.deal_damage()
energies = [c for c in ctx.hand() if is_metal_energy_card(c)]
if not energies:
return
if not await ctx.ask_yes_no(
"Attach a Metal Energy card from your hand to this Pokémon?"
):
return
picked = await ctx.choose_cards(
energies, 1, minimum=1, prompt="Choose a Metal Energy card to attach"
)
if picked:
await ctx.attach_energy(picked[0], ctx.attacker)
card = PokemonCardDef(
guid="415f4d10-3d01-5d2e-a550-fde9f4534c5b",
@@ -24,7 +42,7 @@ card = PokemonCardDef(
game_text="You may attach a Metal Energy card from your hand to this Pok\u00e9mon.",
cost={PokemonTypes.METAL: 2},
damage=50,
effect=unimplemented,
effect=_arm_charge,
),
Attack(
title="Mega Punch",

View File

@@ -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="1eb170c9-255f-5716-af07-873244bb18c4",
@@ -31,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.METAL: 1, PokemonTypes.COLORLESS: 2},
damage=30,
damage_operator="+",
effect=unimplemented,
effect=flip_damage(coins=2, bonus_per_heads=90),
),
],
)

View File

@@ -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="2112d529-f8b5-5117-948d-fb1c17a49bf7",
@@ -24,7 +25,7 @@ card = PokemonCardDef(
game_text="Flip a coin. If heads, your opponent's Active Pok\u00e9mon is now Paralyzed.",
cost={PokemonTypes.COLORLESS: 2},
damage=20,
effect=unimplemented,
effect=condition_attack(SpecialConditions.PARALYZED, flip=True),
),
],
)

View File

@@ -1,5 +1,27 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, is_pokemon_v
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.pokemon import energy_provides_type
async def psy_purge(ctx):
energies = [
e for p in ctx.my_pokemon_in_play() for e in ctx.attached_energies(p)
if energy_provides_type(e, PokemonTypes.PSYCHIC.value)
]
picks = await ctx.choose_cards(
energies, 3, minimum=0,
prompt="Discard up to 3 Psychic Energy from your Pokémon.",
)
await ctx.discard_cards(picks)
if picks:
await ctx.deal_damage(90 * len(picks))
async def star_raid(ctx):
for pokemon in ctx.opponent_pokemon_in_play():
if is_pokemon_v(pokemon.archetype_id):
await ctx.deal_damage(120, target=pokemon, apply_modifiers=False)
card = PokemonCardDef(
guid="87ec5586-6ef2-5aae-a1bc-c7303bcaa2da",
@@ -26,13 +48,14 @@ card = PokemonCardDef(
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
damage=90,
damage_operator="x",
effect=unimplemented,
effect=psy_purge,
),
Attack(
title="Star Raid",
game_text="This attack does 120 damage to each of your opponent's Pok\u00e9mon V. This damage isn't affected by Weakness or Resistance. (You can't use more than 1 VSTAR Power in a game.)",
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
vstar=True,
effect=star_raid,
),
],
)

View File

@@ -1,5 +1,27 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, is_pokemon_v
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.pokemon import energy_provides_type
async def psy_purge(ctx):
energies = [
e for p in ctx.my_pokemon_in_play() for e in ctx.attached_energies(p)
if energy_provides_type(e, PokemonTypes.PSYCHIC.value)
]
picks = await ctx.choose_cards(
energies, 3, minimum=0,
prompt="Discard up to 3 Psychic Energy from your Pokémon.",
)
await ctx.discard_cards(picks)
if picks:
await ctx.deal_damage(90 * len(picks))
async def star_raid(ctx):
for pokemon in ctx.opponent_pokemon_in_play():
if is_pokemon_v(pokemon.archetype_id):
await ctx.deal_damage(120, target=pokemon, apply_modifiers=False)
card = PokemonCardDef(
guid="761bc299-300a-5c20-92b2-dfccdbf1b307",
@@ -26,13 +48,14 @@ card = PokemonCardDef(
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
damage=90,
damage_operator="x",
effect=unimplemented,
effect=psy_purge,
),
Attack(
title="Star Raid",
game_text="This attack does 120 damage to each of your opponent's Pok\u00e9mon V. This damage isn't affected by Weakness or Resistance. (You can't use more than 1 VSTAR Power in a game.)",
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
vstar=True,
effect=star_raid,
),
],
)

View File

@@ -1,5 +1,27 @@
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, unimplemented
from spirit.game.data_utils import PokemonCardDef, Attack, Ability, is_pokemon_v
from spirit.game.attributes import PokemonTypes, PokemonStage, Rarities
from spirit.game.card_effects.pokemon import energy_provides_type
async def psy_purge(ctx):
energies = [
e for p in ctx.my_pokemon_in_play() for e in ctx.attached_energies(p)
if energy_provides_type(e, PokemonTypes.PSYCHIC.value)
]
picks = await ctx.choose_cards(
energies, 3, minimum=0,
prompt="Discard up to 3 Psychic Energy from your Pokémon.",
)
await ctx.discard_cards(picks)
if picks:
await ctx.deal_damage(90 * len(picks))
async def star_raid(ctx):
for pokemon in ctx.opponent_pokemon_in_play():
if is_pokemon_v(pokemon.archetype_id):
await ctx.deal_damage(120, target=pokemon, apply_modifiers=False)
card = PokemonCardDef(
guid="54612974-e4ee-5bff-86c8-fb5a59135b84",
@@ -26,13 +48,14 @@ card = PokemonCardDef(
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
damage=90,
damage_operator="x",
effect=unimplemented,
effect=psy_purge,
),
Attack(
title="Star Raid",
game_text="This attack does 120 damage to each of your opponent's Pok\u00e9mon V. This damage isn't affected by Weakness or Resistance. (You can't use more than 1 VSTAR Power in a game.)",
cost={PokemonTypes.PSYCHIC: 1, PokemonTypes.COLORLESS: 1},
effect=unimplemented,
vstar=True,
effect=star_raid,
),
],
)

View File

@@ -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 _transfer_break(ctx):
await ctx.deal_damage()
bench = ctx.my_bench()
energies = ctx.attached_energies(ctx.attacker)
if not bench or not energies:
return
picked = await ctx.choose_cards(energies, 1, minimum=1, prompt="Choose an Energy to move")
if not picked:
return
target = await ctx.choose_pokemon(
bench, "Choose the Benched Pokémon to move the Energy to"
)
if target is not None:
await ctx.move_energy(picked[0], target)
card = PokemonCardDef(
guid="09d83eb7-1d28-555c-ba85-a8ad58e6d7e1",
key="PGO",
@@ -29,7 +46,7 @@ card = PokemonCardDef(
game_text="Move an Energy from this Pok\u00e9mon to 1 of your Benched Pok\u00e9mon.",
cost={PokemonTypes.PSYCHIC: 2, PokemonTypes.COLORLESS: 1},
damage=160,
effect=unimplemented,
effect=_transfer_break,
),
],
)

View File

@@ -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 _transfer_break(ctx):
await ctx.deal_damage()
bench = ctx.my_bench()
energies = ctx.attached_energies(ctx.attacker)
if not bench or not energies:
return
picked = await ctx.choose_cards(energies, 1, minimum=1, prompt="Choose an Energy to move")
if not picked:
return
target = await ctx.choose_pokemon(
bench, "Choose the Benched Pokémon to move the Energy to"
)
if target is not None:
await ctx.move_energy(picked[0], target)
card = PokemonCardDef(
guid="17788c8e-c675-52e8-aef1-79996fb495ed",
key="PGO",
@@ -29,7 +46,7 @@ card = PokemonCardDef(
game_text="Move an Energy from this Pok\u00e9mon to 1 of your Benched Pok\u00e9mon.",
cost={PokemonTypes.PSYCHIC: 2, PokemonTypes.COLORLESS: 1},
damage=160,
effect=unimplemented,
effect=_transfer_break,
),
],
)

View File

@@ -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, def_for
from spirit.game.attributes import AttrID, PokemonTypes, PokemonStage, Rarities
from spirit.game.session.effects import is_basic_pokemon
from spirit.game.card_effects.passives_common import team_damage_boost_passive
def _flare_symbol_attacker(pokemon) -> bool:
if not is_basic_pokemon(pokemon):
return False
types = pokemon.get_attribute(AttrID.POKEMON_TYPES) or []
if PokemonTypes.FIRE.value not in types:
return False
definition = def_for(pokemon.archetype_id)
name = getattr(definition, "display_name", None) or ""
return name != "Moltres"
card = PokemonCardDef(
guid="b3cf08b6-fbf4-5c63-ad13-b353e030ec75",
@@ -21,7 +35,7 @@ card = PokemonCardDef(
Ability(
title="Flare Symbol",
game_text="Your Basic Fire Pok\u00e9mon's attacks, except any Moltres, do 10 more damage to your opponent's Active Pok\u00e9mon (before applying Weakness and Resistance).",
effect=unimplemented,
passive=team_damage_boost_passive(10, attacker_pred=_flare_symbol_attacker),
),
Attack(
title="Fire Wing",

View File

@@ -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="5b3ac0ad-0387-56bf-8874-885dbe59735d",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
title="Nap",
game_text="Heal 20 damage from this Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=heal_attack(20),
),
Attack(
title="Peck",

View File

@@ -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="4e6cf096-db9c-5189-a3c9-0d1676c55ca7",
@@ -29,7 +30,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIRE: 2, PokemonTypes.COLORLESS: 1},
damage=50,
damage_operator="+",
effect=unimplemented,
effect=flip_bonus(50),
),
],
)

View File

@@ -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 condition_attack, damage_per, damage_counters_on
card = PokemonCardDef(
guid="beff1709-f449-5379-a203-cf1395e94da7",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
game_text="During your opponent's next turn, the Defending Pok\u00e9mon can't retreat.",
cost={PokemonTypes.COLORLESS: 3},
damage=50,
effect=unimplemented,
effect=condition_attack(no_retreat=True),
),
Attack(
title="Raging Swing",
@@ -31,7 +32,7 @@ card = PokemonCardDef(
cost={PokemonTypes.FIGHTING: 2, PokemonTypes.COLORLESS: 2},
damage=50,
damage_operator="x",
effect=unimplemented,
effect=damage_per(damage_counters_on("self"), 50),
),
],
)

View File

@@ -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="c049f814-276b-5712-86e2-e33601c1aaca",
@@ -23,7 +24,7 @@ card = PokemonCardDef(
title="Growl",
game_text="During your opponent's next turn, the Defending Pok\u00e9mon's attacks do 20 less damage (before applying Weakness and Resistance).",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=debuff_defender_attacks(20),
),
Attack(
title="Flap",

View File

@@ -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, TrainerType
from spirit.game.card_effects.attacks_common import bonus_if
def _played_supporter_this_turn(ctx):
return ctx.played_trainer_this_turn(lambda r: r[2] == TrainerType.SUPPORTER.value) > 0
card = PokemonCardDef(
guid="65db8eef-73af-5ed6-95d9-d3dfc1863e6d",
@@ -24,7 +30,7 @@ card = PokemonCardDef(
cost={PokemonTypes.LIGHTNING: 1, PokemonTypes.COLORLESS: 2},
damage=30,
damage_operator="+",
effect=unimplemented,
effect=bonus_if(_played_supporter_this_turn, 30),
),
],
)

View File

@@ -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(
@@ -23,7 +24,7 @@ card = PokemonCardDef(
game_text="This Pok\u00e9mon also does 30 damage to itself.",
cost={PokemonTypes.LIGHTNING: 2, PokemonTypes.COLORLESS: 1},
damage=90,
effect=unimplemented,
effect=recoil_attack(30),
),
],
)

View File

@@ -1,5 +1,27 @@
from spirit.game.data_utils import StadiumCardDef, unimplemented
from spirit.game.data_utils import Ability, Activations, StadiumCardDef
from spirit.game.attributes import Rarities
from spirit.game.card_effects.trainers import deck_nonempty, is_item_card
async def pokestop_effect(ctx):
"""That player may discard the top 3 of their deck; any Item cards
discarded this way go into their hand instead."""
top = ctx.deck_top(3)
if not top:
return
await ctx.discard_cards(top)
items = [c for c in top if is_item_card(c)]
if items:
await ctx.put_in_hand(items, reveal=False)
POKESTOP_ABILITY = Ability(
title="PokéStop",
game_text="Once during each player's turn, that player may discard 3 cards from the top of their deck. If a player discarded any Item cards in this way, they put those Item cards into their hand.",
activation=Activations.ONCE_PER_TURN,
effect=pokestop_effect,
condition=lambda board, player_id, stadium: deck_nonempty(board, player_id),
)
card = StadiumCardDef(
guid="57d9c8fc-1ed5-522a-b7de-93f1d280b715",
@@ -11,5 +33,5 @@ card = StadiumCardDef(
collector_number=68,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
ability=POKESTOP_ABILITY,
)

View File

@@ -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 spread_damage
card = PokemonCardDef(
guid="3d5b2fb5-e726-5f34-bdd5-0429a30bcf7e",
@@ -24,7 +25,7 @@ card = PokemonCardDef(
game_text="This attack also does 20 damage to each Benched Pok\u00e9mon (both yours and your opponent's). (Don't apply Weakness and Resistance for Benched Pok\u00e9mon.)",
cost={PokemonTypes.COLORLESS: 1},
damage=20,
effect=unimplemented,
effect=spread_damage(20, side="both", also_base=True),
),
Attack(
title="Tackle",

View File

@@ -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
from spirit.game.card_effects.pokemon import is_energy_card
def _is_water_energy(card):
types = card.get_attribute(AttrID.POKEMON_TYPES) or []
return is_energy_card(card) and PokemonTypes.WATER.value in types
def pump_shot_condition(board, player_id, pokemon):
hand = board.find_player_area(player_id, "hand")
if not hand or not any(_is_water_energy(c) for c in hand.children):
return False
opponent = next((pid for pid in board.player_ids if pid != player_id), None)
if opponent is None:
return False
bench = board.find_player_area(opponent, "bench")
return bool(bench) and len(bench.children) > 0
async def pump_shot(ctx):
"""Discard a Water Energy from hand; put 2 damage counters on 1 of your opponent's Benched Pokemon."""
discarded = await ctx.discard_from_hand(
1, predicate=_is_water_energy,
prompt="Discard a Water Energy card to use Pump Shot",
)
if not discarded:
return
bench = ctx.opponent_bench()
if not bench:
return
target = await ctx.choose_pokemon(
bench, "Choose 1 of your opponent's Benched Pokémon"
)
if target is not None:
await ctx.deal_damage(20, target=target, as_counters=True)
card = PokemonCardDef(
guid="76360f77-185a-5c93-9e8e-b9cc93cc38e9",
@@ -21,14 +57,16 @@ card = PokemonCardDef(
Ability(
title="Pump Shot",
game_text="You must discard a Water Energy card from your hand in order to use this Ability. Once during your turn, you may put 2 damage counters on 1 of your opponent's Benched Pok\u00e9mon.",
effect=unimplemented,
activation=Activations.ONCE_PER_TURN,
condition=pump_shot_condition,
effect=pump_shot,
),
Attack(
title="Torrential Cannon",
game_text="During your next turn, this Pok\u00e9mon can't use Torrential Cannon.",
cost={PokemonTypes.WATER: 2, PokemonTypes.COLORLESS: 1},
damage=170,
effect=unimplemented,
locks_next_turn=True,
),
],
)

View File

@@ -1,5 +1,25 @@
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
from spirit.game.card_effects.attacks_common import condition_attack
def _sunny_bloom_ready(board, player_id, pokemon):
hand = board.find_player_area(player_id, "hand")
deck = board.find_player_area(player_id, "deck")
return bool(deck and deck.children) and hand is not None \
and len(hand.children) < 4
async def sunny_bloom(ctx):
"""End of your turn: you may draw until you have 4 cards in your hand."""
if not _sunny_bloom_ready(ctx.board, ctx.player_id, ctx.source):
ctx.suppress_announce = True
return
if not await ctx.ask_yes_no("Use Sunny Bloom? Draw cards until you have "
"4 cards in your hand."):
ctx.suppress_announce = True
return
await ctx.draw_until(4)
card = PokemonCardDef(
guid="2baf28dd-fef2-52c9-826b-1fbf5017d922",
@@ -21,14 +41,17 @@ card = PokemonCardDef(
Ability(
title="Sunny Bloom",
game_text="Once at the end of your turn (after your attack), you may use this Ability. Draw cards until you have 4 cards in your hand.",
effect=unimplemented,
trigger=Triggers.END_OF_TURN,
condition=_sunny_bloom_ready,
effect=sunny_bloom,
),
Attack(
title="Pollen Hazard",
game_text="Your opponent's Active Pok\u00e9mon is now Burned, Confused, and Poisoned.",
cost={PokemonTypes.GRASS: 2, PokemonTypes.COLORLESS: 1},
damage=90,
effect=unimplemented,
effect=condition_attack(SpecialConditions.BURNED, SpecialConditions.CONFUSED,
SpecialConditions.POISONED),
),
],
)

View File

@@ -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="32e87eba-71ac-50f5-9e8e-46186f5d339b",
@@ -11,5 +56,6 @@ card = ItemCardDef(
collector_number=69,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=_rare_candy,
condition=_rare_candy_condition,
)

View File

@@ -1,6 +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
def _prizes_remaining(board, player_id):
area = board.find_player_area(player_id, "prizePile")
return len(area.children) if area else 0
def _kinda_lazy_condition(board, player_id, pokemon):
return _prizes_remaining(board, player_id) not in (2, 4, 6)
card = PokemonCardDef(
guid="92236790-2c73-56ed-b409-acf2bb56e82e",
key="PGO",
@@ -21,12 +31,12 @@ card = PokemonCardDef(
Ability(
title="Kinda Lazy",
game_text="If you have exactly 2, 4, or 6 Prize cards remaining, this Pok\u00e9mon can't attack.",
effect=unimplemented,
),
Attack(
title="Heavy Impact",
cost={PokemonTypes.COLORLESS: 4},
damage=260,
condition=_kinda_lazy_condition,
),
],
)

View File

@@ -1,6 +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
def _prizes_remaining(board, player_id):
area = board.find_player_area(player_id, "prizePile")
return len(area.children) if area else 0
def _kinda_lazy_condition(board, player_id, pokemon):
return _prizes_remaining(board, player_id) not in (2, 4, 6)
card = PokemonCardDef(
guid="2fa3076d-77d0-50cb-afb3-6c3c81cbdc46",
key="PGO",
@@ -21,12 +31,12 @@ card = PokemonCardDef(
Ability(
title="Kinda Lazy",
game_text="If you have exactly 2, 4, or 6 Prize cards remaining, this Pok\u00e9mon can't attack.",
effect=unimplemented,
),
Attack(
title="Heavy Impact",
cost={PokemonTypes.COLORLESS: 4},
damage=260,
condition=_kinda_lazy_condition,
),
],
)

View File

@@ -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
from spirit.game.card_effects.attacks_common import condition_attack
def _opponent_one_prize_left(board, player_id, pokemon):
opponent = next((p for p in board.player_ids if p != player_id), None)
if not opponent:
return False
area = board.find_player_area(opponent, "prizePile")
return bool(area) and len(area.children) == 1
async def twilight_inspiration(ctx):
"""Take 2 Prize cards (usable only when the opponent has exactly 1 left)."""
await ctx.take_prizes(2)
card = PokemonCardDef(
guid="12412fc3-817b-507d-8705-64286986f951",
@@ -24,13 +38,14 @@ card = PokemonCardDef(
game_text="Both Active Pok\u00e9mon are now Asleep.",
cost={PokemonTypes.COLORLESS: 1},
damage=20,
effect=unimplemented,
effect=condition_attack(SpecialConditions.ASLEEP, both_actives=True),
),
Attack(
title="Twilight Inspiration",
game_text="You can use this attack only if your opponent has exactly 1 Prize card remaining. Take 2 Prize cards.",
cost={PokemonTypes.COLORLESS: 2},
effect=unimplemented,
effect=twilight_inspiration,
condition=_opponent_one_prize_left,
),
],
)

View File

@@ -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.support_common import heal_attack, recover_from_discard, requires_discard
from spirit.game.session.effects import is_item_card
hold_still = heal_attack(30, target="self")
ideal_fishing_day = recover_from_discard(
predicate=is_item_card, count=1, minimum=1, reveal=False, to="hand",
prompt="Choose an Item card to put into your hand.",
)
card = PokemonCardDef(
guid="e0fda57c-5575-5f1b-8484-6d4c3260ec86",
@@ -22,13 +31,14 @@ card = PokemonCardDef(
title="Hold Still",
game_text="Heal 30 damage from this Pok\u00e9mon.",
cost={PokemonTypes.COLORLESS: 1},
effect=unimplemented,
effect=hold_still,
),
Attack(
title="Ideal Fishing Day",
game_text="Put an Item card from your discard pile into your hand.",
cost={PokemonTypes.WATER: 1},
effect=unimplemented,
effect=ideal_fishing_day,
condition=requires_discard(is_item_card),
),
],
)

View File

@@ -1,5 +1,9 @@
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.passives_common import (
no_retreat_passive, is_in_active_spot, opposing_active,
)
card = PokemonCardDef(
guid="5796a3e5-68f2-5556-aee0-2216f67f5f11",
@@ -21,14 +25,16 @@ card = PokemonCardDef(
Ability(
title="Block",
game_text="As long as this Pok\u00e9mon is in the Active Spot, your opponent's Active Pok\u00e9mon can't retreat.",
effect=unimplemented,
passive=no_retreat_passive(
lambda p, c: opposing_active(p, c) and is_in_active_spot(c)
),
),
Attack(
title="Collapse",
game_text="This Pok\u00e9mon is now Asleep.",
cost={PokemonTypes.COLORLESS: 4},
damage=150,
effect=unimplemented,
effect=condition_attack(self_conditions=(SpecialConditions.ASLEEP,)),
),
],
)

View File

@@ -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="2470d314-35d8-54be-b8c4-3f0f504f0765",
@@ -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",

View File

@@ -1,5 +1,38 @@
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 Rarities, PokemonTypes, AttrID
from spirit.game.card_effects.trainers import is_basic_energy_card
def _is_lightning_energy_card(card):
if not is_basic_energy_card(card):
return False
types = card.get_attribute(AttrID.POKEMON_TYPES) or []
return PokemonTypes.LIGHTNING.value in types
async def _spark(ctx):
drawn = await ctx.draw_cards(2)
if drawn <= 0:
return
heads = (await ctx.flip_coins(1, "Spark"))[0]
if not heads:
return
bench = ctx.my_bench()
if not bench:
return
energies = [c for c in ctx.discard_pile() if _is_lightning_energy_card(c)]
if not energies:
return
picks = await ctx.choose_cards(
energies, 1, minimum=1, prompt="Choose a Lightning Energy card from your discard pile.",
)
if not picks:
return
target = await ctx.choose_pokemon(bench, "Choose 1 of your Benched Pokémon")
if target is None:
return
await ctx.attach_energy(picks[0], target)
card = SupporterCardDef(
guid="332538c4-6a38-586d-a9f3-d4c321203813",
@@ -11,5 +44,5 @@ card = SupporterCardDef(
collector_number=70,
set_code="PGO",
rarity=Rarities.Uncommon,
effect=unimplemented
effect=_spark,
)

Some files were not shown because too many files have changed in this diff Show More