mirror of
https://github.com/Bratah123/Spirit-PTCGO.git
synced 2026-09-12 14:45:51 -05:00
FEAT: add play targets to scripting engine
This commit is contained in:
@@ -54,6 +54,12 @@ def opponent_has_bench(board, player_id):
|
||||
return bool(opponent) and bool(_bench_pokemon(board, opponent))
|
||||
|
||||
|
||||
def opponent_bench_play_targets(board, player_id, card):
|
||||
"""Public bench targets for a single-target gust trainer."""
|
||||
opponent = _other_player(board, player_id)
|
||||
return _bench_pokemon(board, opponent) if opponent else []
|
||||
|
||||
|
||||
def player_has_bench(board, player_id):
|
||||
return bool(_bench_pokemon(board, player_id))
|
||||
|
||||
@@ -225,7 +231,7 @@ async def marnie(ctx):
|
||||
|
||||
async def bosss_orders(ctx):
|
||||
"""Switch 1 of the opponent's Benched Pokemon with their Active."""
|
||||
target = await ctx.choose_pokemon(
|
||||
target = await ctx.choose_play_target(
|
||||
ctx.opponent_bench(), "Choose the opponent's new Active Pokémon"
|
||||
)
|
||||
if target is not None:
|
||||
|
||||
@@ -552,11 +552,15 @@ class TrainerCardDef(CardDefinition):
|
||||
searchable_by: Optional[List[str]] = None,
|
||||
subtypes: Optional[List[str]] = None,
|
||||
attributes: Optional[dict] = None,
|
||||
foil: Optional[Foil] = None
|
||||
foil: Optional[Foil] = None,
|
||||
play_targets: Optional[Callable] = None,
|
||||
play_target_prompt: str = "Choose a target",
|
||||
):
|
||||
super().__init__(guid, key, name, collector_number, set_code, rarity, display_name, searchable_by, subtypes, attributes, foil)
|
||||
self.effect = effect
|
||||
self.condition = condition
|
||||
self.play_targets = play_targets
|
||||
self.play_target_prompt = play_target_prompt
|
||||
# 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 []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench, opponent_bench_play_targets
|
||||
from spirit.game.data_utils import SupporterCardDef
|
||||
from spirit.game.attributes import Rarities
|
||||
|
||||
@@ -13,5 +13,7 @@ card = SupporterCardDef(
|
||||
set_code="SWSH2",
|
||||
rarity=Rarities.RareHolo,
|
||||
effect=bosss_orders,
|
||||
play_targets=opponent_bench_play_targets,
|
||||
play_target_prompt="Choose the opponent's new Active Pokémon",
|
||||
condition=opponent_has_bench
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench, opponent_bench_play_targets
|
||||
from spirit.game.data_utils import SupporterCardDef
|
||||
from spirit.game.attributes import Rarities
|
||||
|
||||
@@ -13,5 +13,7 @@ card = SupporterCardDef(
|
||||
set_code="SWSH2",
|
||||
rarity=Rarities.RareUltra,
|
||||
effect=bosss_orders,
|
||||
play_targets=opponent_bench_play_targets,
|
||||
play_target_prompt="Choose the opponent's new Active Pokémon",
|
||||
condition=opponent_has_bench
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench, opponent_bench_play_targets
|
||||
from spirit.game.data_utils import SupporterCardDef
|
||||
from spirit.game.attributes import Rarities
|
||||
|
||||
@@ -13,5 +13,7 @@ card = SupporterCardDef(
|
||||
set_code="SWSH2",
|
||||
rarity=Rarities.RareRainbow,
|
||||
effect=bosss_orders,
|
||||
play_targets=opponent_bench_play_targets,
|
||||
play_target_prompt="Choose the opponent's new Active Pokémon",
|
||||
condition=opponent_has_bench
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench, opponent_bench_play_targets
|
||||
from spirit.game.data_utils import SupporterCardDef
|
||||
from spirit.game.attributes import Rarities
|
||||
|
||||
@@ -13,5 +13,7 @@ card = SupporterCardDef(
|
||||
set_code="SWSH45",
|
||||
rarity=Rarities.Rare,
|
||||
effect=bosss_orders,
|
||||
play_targets=opponent_bench_play_targets,
|
||||
play_target_prompt="Choose the opponent's new Active Pokémon",
|
||||
condition=opponent_has_bench
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench
|
||||
from spirit.game.card_effects.trainers import bosss_orders, opponent_has_bench, opponent_bench_play_targets
|
||||
from spirit.game.data_utils import SupporterCardDef
|
||||
from spirit.game.attributes import Rarities
|
||||
|
||||
@@ -13,5 +13,7 @@ card = SupporterCardDef(
|
||||
set_code="SWSH9",
|
||||
rarity=Rarities.RareHolo,
|
||||
effect=bosss_orders,
|
||||
play_targets=opponent_bench_play_targets,
|
||||
play_target_prompt="Choose the opponent's new Active Pokémon",
|
||||
condition=opponent_has_bench
|
||||
)
|
||||
|
||||
@@ -86,7 +86,8 @@ class EffectContext:
|
||||
"""
|
||||
|
||||
def __init__(self, session, player_id: str, source: BoardEntity,
|
||||
ability: Optional[Ability], attached_to: Optional[PokemonEntity] = None):
|
||||
ability: Optional[Ability], attached_to: Optional[PokemonEntity] = None,
|
||||
play_target: Optional[BoardEntity] = None):
|
||||
self.session = session
|
||||
self.board = session.board_state
|
||||
self.game_id = session.game_id
|
||||
@@ -97,6 +98,8 @@ class EffectContext:
|
||||
self.ability = ability
|
||||
# For energy on-attach effects: the Pokemon the card just attached to.
|
||||
self.attached_to = attached_to
|
||||
self.play_target = play_target
|
||||
self._play_target_consumed = False
|
||||
self.knockouts: List[PokemonEntity] = []
|
||||
# Extra prizes the attacker takes for knockouts this attack causes
|
||||
# (e.g. Stoutland V's Double Dip Fangs).
|
||||
@@ -1091,6 +1094,16 @@ class EffectContext:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def choose_play_target(
|
||||
self, candidates: Sequence[CardEntity], prompt: str,
|
||||
) -> Optional[CardEntity]:
|
||||
"""Uses the declared play target once, or prompts for an untargeted play."""
|
||||
if self.play_target is not None and not self._play_target_consumed:
|
||||
self._play_target_consumed = True
|
||||
return next((card for card in candidates if card is self.play_target), None)
|
||||
picks = await self.choose_cards(candidates, 1, prompt=prompt)
|
||||
return picks[0] if picks else None
|
||||
|
||||
async def choose_pokemon(
|
||||
self,
|
||||
candidates: Sequence[PokemonEntity],
|
||||
@@ -2433,7 +2446,8 @@ async def _send_ability_brackets(session, ctx: EffectContext,
|
||||
await session.enforce_bench_capacity()
|
||||
|
||||
|
||||
async def resolve_trainer_effect(session, player_id: str, card) -> Optional[EffectContext]:
|
||||
async def resolve_trainer_effect(session, player_id: str, card,
|
||||
play_target=None) -> Optional[EffectContext]:
|
||||
"""Runs a trainer card's scripted effect and returns its ctx (None when
|
||||
the card has no runnable effect).
|
||||
|
||||
@@ -2461,7 +2475,7 @@ async def resolve_trainer_effect(session, player_id: str, card) -> Optional[Effe
|
||||
+ "; card plays with no effect."
|
||||
)
|
||||
return None
|
||||
ctx = EffectContext(session, player_id, card, None)
|
||||
ctx = EffectContext(session, player_id, card, None, play_target=play_target)
|
||||
ctx.is_trainer_effect = True
|
||||
await effect(ctx)
|
||||
return ctx
|
||||
|
||||
@@ -130,6 +130,7 @@ from .legal_actions import (
|
||||
compute_legal_actions,
|
||||
copy_attack_choice_node,
|
||||
energy_provided_count,
|
||||
trainer_play_target_ids,
|
||||
)
|
||||
|
||||
|
||||
@@ -3727,7 +3728,11 @@ class GameSession:
|
||||
if len(selection) > 1 and isinstance(selection[1], (list, tuple)):
|
||||
for response in selection[1]:
|
||||
if isinstance(response, dict):
|
||||
target_ids.extend(response.get("entityList") or [])
|
||||
entities = response.get("entityList") or []
|
||||
if not isinstance(entities, list) or any(
|
||||
not isinstance(entity_id, str) for entity_id in entities):
|
||||
return None
|
||||
target_ids.extend(entities)
|
||||
return entry, target_ids
|
||||
|
||||
def _validated_target(
|
||||
@@ -3765,7 +3770,17 @@ class GameSession:
|
||||
elif description == ACTION_EVOLVE:
|
||||
await self._execute_evolve(player_id, card, entry, target_ids)
|
||||
elif description == ACTION_USE_TRAINER:
|
||||
return await self._execute_play_trainer(player_id, card)
|
||||
targets = trainer_play_target_ids(self.board_state, player_id, card)
|
||||
play_target = None
|
||||
if targets is not None:
|
||||
if not targets or len(target_ids) > 1:
|
||||
return False
|
||||
if target_ids:
|
||||
if (not isinstance(target_ids[0], str) or target_ids[0] not in targets
|
||||
or self._validated_target(entry, target_ids) != target_ids[0]):
|
||||
return False
|
||||
play_target = self.board_state.get_entity(target_ids[0])
|
||||
return await self._execute_play_trainer(player_id, card, play_target)
|
||||
elif description == ACTION_PLAY_STADIUM:
|
||||
await self._execute_play_stadium(player_id, card)
|
||||
elif description == ACTION_USE_ABILITY:
|
||||
@@ -4740,7 +4755,7 @@ class GameSession:
|
||||
)
|
||||
return incoming
|
||||
|
||||
async def _execute_play_trainer(self, player_id, card) -> bool:
|
||||
async def _execute_play_trainer(self, player_id, card, play_target=None) -> bool:
|
||||
"""Plays an Item/Supporter: revealed onto activeTrainer, effect resolves,
|
||||
then discarded. Returns True when the effect ended the turn (Rotom Bike)."""
|
||||
trainer_area = self.board_state.find_global_area("activeTrainer")
|
||||
@@ -4776,7 +4791,7 @@ class GameSession:
|
||||
|
||||
# Effect dialogs run after placement so both viewers see the card on
|
||||
# the trainer slot while the player decides.
|
||||
ctx = await resolve_trainer_effect(self, player_id, card)
|
||||
ctx = await resolve_trainer_effect(self, player_id, card, play_target=play_target)
|
||||
logging.info(
|
||||
f"[Session {self.game_id}] {self.players[player_id].screen_name} "
|
||||
f"played trainer {card.entity_id}."
|
||||
|
||||
@@ -401,6 +401,17 @@ def _active_immobilized(board: BoardState, player_id: str) -> bool:
|
||||
return any(c in _IMMOBILIZING_CONDITIONS for c in (conditions or []))
|
||||
|
||||
|
||||
def trainer_play_target_ids(board: BoardState, player_id: str, card) -> Optional[List[str]]:
|
||||
"""Returns declared play targets, or None for a trainer without targeting."""
|
||||
selector = getattr(def_for(card.archetype_id), "play_targets", None)
|
||||
if selector is None:
|
||||
return None
|
||||
return list(dict.fromkeys(
|
||||
target.entity_id for target in selector(board, player_id, card)
|
||||
if board.get_entity(target.entity_id) is target
|
||||
))
|
||||
|
||||
|
||||
def compute_legal_actions(
|
||||
board: BoardState,
|
||||
state: TurnState,
|
||||
@@ -489,16 +500,27 @@ def compute_legal_actions(
|
||||
if condition is not None \
|
||||
and not trainer_condition_met(condition, board, player_id, card):
|
||||
continue
|
||||
target_infos = []
|
||||
if trainer_type in (TrainerType.ITEM.value, TrainerType.SUPPORTER.value):
|
||||
targets = trainer_play_target_ids(board, player_id, card)
|
||||
if targets is not None:
|
||||
if not targets:
|
||||
continue
|
||||
node = entity_list_target_info(targets, minimum_to_select=0, forced=False)
|
||||
node["targetPrompt"] = {"id": definition.play_target_prompt}
|
||||
target_infos = [node]
|
||||
if trainer_type == TrainerType.ITEM.value:
|
||||
entries.append(_target_map_entry(
|
||||
game_id, card.entity_id,
|
||||
action_id_for(card.entity_id, "item"), ACTION_USE_TRAINER,
|
||||
target_infos,
|
||||
))
|
||||
elif trainer_type == TrainerType.SUPPORTER.value:
|
||||
if not state.supporter_played and state.turn_number > 1:
|
||||
entries.append(_target_map_entry(
|
||||
game_id, card.entity_id,
|
||||
action_id_for(card.entity_id, "supporter"), ACTION_USE_TRAINER,
|
||||
target_infos,
|
||||
))
|
||||
elif trainer_type == TrainerType.STADIUM.value:
|
||||
if not state.stadium_played and not _same_stadium_in_play(board, card):
|
||||
|
||||
Reference in New Issue
Block a user