mirror of
https://github.com/Bratah123/Spirit-PTCGO.git
synced 2026-09-09 05:05:30 -05:00
FEAT: Handle pile reordering & add missing effect packet headers
Fixes animation on cards that re-order piles such as Rotom Phone
This commit is contained in:
@@ -663,8 +663,7 @@ async def rotom_phone(ctx):
|
||||
)
|
||||
if not picks:
|
||||
return
|
||||
await ctx.shuffle_deck()
|
||||
await ctx.put_on_top_of_deck(picks[0])
|
||||
await ctx.shuffle_deck_below(picks[0])
|
||||
|
||||
|
||||
async def fan_of_waves(ctx):
|
||||
|
||||
@@ -2,6 +2,15 @@ from typing import Any, Dict, List, Optional
|
||||
from spirit.game.attributes import GameSequence
|
||||
from spirit.network.message_names import OutboundMsg
|
||||
|
||||
|
||||
class NestedSequence:
|
||||
"""A child Start/Stop bracket embedded inside a parent game sequence."""
|
||||
|
||||
def __init__(self, name, messages: List[Dict[str, Any]]):
|
||||
self.name: str = getattr(name, "value", name)
|
||||
self.messages: List[Dict[str, Any]] = messages
|
||||
|
||||
|
||||
def _build_msg(name: str, value: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Helper to build standard Warg Protocol polymorphic JSON envelopes."""
|
||||
return {
|
||||
|
||||
@@ -34,6 +34,7 @@ from spirit.game.data_utils import (
|
||||
)
|
||||
from spirit.game.models.board import BoardEntity, CardEntity, EnergyEntity, PokemonEntity
|
||||
from spirit.network.message_names import OutboundMsg
|
||||
from spirit.game.game_sequence_packets import NestedSequence
|
||||
from .constants import PROMPT_NO, PROMPT_YES
|
||||
from .passives import (
|
||||
TempPassive,
|
||||
@@ -1203,7 +1204,6 @@ class EffectContext:
|
||||
# bare "Draw" stack, which misses the path table (default linear
|
||||
# curve); nested moves animate FromDeck|ToHand with k.z's stagger --
|
||||
# the same top-of-deck arc as the initial deal.
|
||||
from .game_session import NestedSequence # circular-import guard
|
||||
for move in moved:
|
||||
self._queue(self.session._entity_introduced_msg(move["card"]),
|
||||
viewer_id=pid, bracket=GameSequence.DRAW.value)
|
||||
@@ -1609,14 +1609,22 @@ class EffectContext:
|
||||
), bracket=bracket)
|
||||
return len(cards)
|
||||
|
||||
def _queue_pile_reordered(self, pile: BoardEntity):
|
||||
"""Synchronizes the complete pile order without revealing card attributes."""
|
||||
self._queue(self.session._build_msg(
|
||||
OutboundMsg.PILE_REORDERED.value,
|
||||
{"gameID": self.game_id, "entityID": pile.entity_id,
|
||||
"children": [card.entity_id for card in pile.children]},
|
||||
), bracket=GameSequence.GROUPED_MOVE.value)
|
||||
|
||||
async def reorder_deck_top(self, count: int,
|
||||
player_id: Optional[str] = None,
|
||||
prompt: str = "Rearrange the cards on top of your deck",
|
||||
) -> List[CardEntity]:
|
||||
"""Looks at the top `count` deck cards and puts them back in any
|
||||
order (ordered browser, owner-only; the opponent learns nothing --
|
||||
hidden-zone browser cards re-hide on close and no moves are sent).
|
||||
Returns the new top order (topmost first)."""
|
||||
"""Privately chooses the top cards' order and synchronizes the pile.
|
||||
|
||||
Returns the new top order (topmost first); card faces stay hidden.
|
||||
"""
|
||||
pid = player_id or self.player_id
|
||||
top = self.deck_top(count, pid)
|
||||
if len(top) <= 1:
|
||||
@@ -1626,7 +1634,7 @@ class EffectContext:
|
||||
prompt=prompt, ordered=True,
|
||||
)
|
||||
by_id = {c.entity_id: c for c in top}
|
||||
order = [by_id[i] for i in picked_ids if i in by_id]
|
||||
order = [by_id[i] for i in dict.fromkeys(picked_ids) if i in by_id]
|
||||
for card in top:
|
||||
if card not in order:
|
||||
order.append(card)
|
||||
@@ -1636,17 +1644,50 @@ class EffectContext:
|
||||
# First pick = new top; top of the deck is the LAST child.
|
||||
for card in reversed(order):
|
||||
deck.children.append(card)
|
||||
self._queue_pile_reordered(deck)
|
||||
return order
|
||||
|
||||
async def shuffle_deck_below(self, card: CardEntity) -> bool:
|
||||
"""Shuffles the other deck cards and animates the chosen card onto the top."""
|
||||
owner = card.owning_player_id or self.player_id
|
||||
deck = self.board.find_player_area(owner, "deck")
|
||||
if not deck or card not in deck.children:
|
||||
return False
|
||||
position = deck.children.index(card)
|
||||
if position == len(deck.children) - 1:
|
||||
movement = GameSequence.MOVE_FROM_TOP_OF_DECK
|
||||
elif position == 0:
|
||||
movement = GameSequence.MOVE_FROM_BOTTOM_OF_DECK
|
||||
else:
|
||||
movement = GameSequence.MOVE_FROM_MIDDLE_OF_DECK
|
||||
remaining = [child for child in deck.children if child is not card]
|
||||
random.shuffle(remaining)
|
||||
deck.children[:] = remaining + [card]
|
||||
# The stock deck-motion paths require TrainerCard in the sequence stack.
|
||||
self._queue(self.session._build_msg(
|
||||
OutboundMsg.SHUFFLED.value,
|
||||
{"gameID": self.game_id, "entityID": deck.entity_id},
|
||||
), bracket=GameSequence.TRAINER_CARD.value)
|
||||
# r.h consumes exactly one EntityMoved, then animates a sleeved card.
|
||||
self._queue(NestedSequence(movement, [self.session._entity_moved_msg(
|
||||
card.entity_id, deck.entity_id, len(deck.children) - 1,
|
||||
)]), bracket=GameSequence.TRAINER_CARD.value)
|
||||
self._queue_pile_reordered(deck)
|
||||
return True
|
||||
|
||||
async def put_on_top_of_deck(self, card: CardEntity) -> bool:
|
||||
"""Puts a card on top of its owner's deck."""
|
||||
owner = card.owning_player_id or self.player_id
|
||||
deck = self.board.find_player_area(owner, "deck")
|
||||
if not deck or self._energy_removal_blocked(card):
|
||||
return False
|
||||
same_pile = card.parent_id == deck.entity_id
|
||||
position = len(deck.children)
|
||||
if not self.board.move_card(card.entity_id, deck.entity_id):
|
||||
return False
|
||||
if same_pile:
|
||||
self._queue_pile_reordered(deck)
|
||||
return True
|
||||
self._queue(
|
||||
self.session._entity_moved_msg(card.entity_id, deck.entity_id, position),
|
||||
bracket=GameSequence.GROUPED_MOVE.value,
|
||||
@@ -1659,8 +1700,12 @@ class EffectContext:
|
||||
deck = self.board.find_player_area(owner, "deck")
|
||||
if not deck or self._energy_removal_blocked(card):
|
||||
return False
|
||||
same_pile = card.parent_id == deck.entity_id
|
||||
if not self.board.move_card(card.entity_id, deck.entity_id, 0):
|
||||
return False
|
||||
if same_pile:
|
||||
self._queue_pile_reordered(deck)
|
||||
return True
|
||||
self._queue(
|
||||
self.session._entity_moved_msg(card.entity_id, deck.entity_id, 0),
|
||||
bracket=GameSequence.GROUPED_MOVE.value,
|
||||
|
||||
@@ -73,6 +73,7 @@ from .constants import (
|
||||
TEXT_ATTACH_TAX_DISCARD,
|
||||
)
|
||||
from spirit.network.message_names import OutboundMsg
|
||||
from spirit.game.game_sequence_packets import NestedSequence
|
||||
from spirit.game.attributes import (
|
||||
AttrID,
|
||||
CLIENT_SPECIAL_CONDITION_NAMES,
|
||||
@@ -150,22 +151,6 @@ class GameOver(Exception):
|
||||
"""Raised once the game has been decided; unwinds the gameplay sequence."""
|
||||
|
||||
|
||||
class NestedSequence:
|
||||
"""A child Start/Stop bracket embedded inside a parent game sequence.
|
||||
|
||||
The client's SequenceParser keeps a stack of in-progress sequences: a
|
||||
StartSequence (exempt from the envelope sequence-ID check) pushes a child,
|
||||
its inner messages must ride the CHILD's sequenceID, and its StopSequence
|
||||
folds the child into the parent as a single sequence command. This is how
|
||||
GroupedMove batches EntityMoved commands so they animate together instead
|
||||
of one-by-one (e.g. a mulliganed hand returning to the deck at once).
|
||||
"""
|
||||
|
||||
def __init__(self, name, messages: List[Dict[str, Any]]):
|
||||
self.name: str = getattr(name, "value", name)
|
||||
self.messages: List[Dict[str, Any]] = messages
|
||||
|
||||
|
||||
class GameOptions:
|
||||
"""
|
||||
Represents the gameplay options and match metadata sent to the client
|
||||
|
||||
@@ -581,6 +581,23 @@ class OutboundMsg(str, Enum):
|
||||
CUSTOM_CHOICE_REQUIRED = "CustomChoiceRequired"
|
||||
NOTIFY_GAME_CHAT = "NotifyGameChat"
|
||||
|
||||
# Unimplemented message emissions: client_source/coredll/core/*Effect.cs.
|
||||
ANIMATION_DELAY_EFFECT = "AnimationDelayEffect"
|
||||
BLINK_EFFECT = "BlinkEffect"
|
||||
PHASE_CHANGE_EFFECT = "PhaseChangeEffect"
|
||||
WAIT_FOR_TARGET_ON_EFFECT = "WaitForTargetOnEffect"
|
||||
WAIT_FOR_TARGET_OFF_EFFECT = "WaitForTargetOffEffect"
|
||||
|
||||
# Unimplemented message emissions: client_source/pie/pie-src/*Effect.cs.
|
||||
WAITING_FOR_OPPONENT_EFFECT = "WaitingForOpponentEffect"
|
||||
DONE_WAITING_FOR_OPPONENT_EFFECT = "DoneWaitingForOpponentEffect"
|
||||
POST_ACTION_PHASE_EFFECT = "PostActionPhaseEffect"
|
||||
ROCK_PAPER_SCISSORS_EFFECT = "RockPaperScissorsEffect"
|
||||
|
||||
# Unimplemented message emissions: dwd.core.match.interaction.messages.incoming.
|
||||
PLAYER_INTERACTED_WITH_ENTITY_EFFECT = "PlayerInteractedWithEntityEffect"
|
||||
PLAYER_STOPPED_INTERACTING_EFFECT = "PlayerStoppedInteractingEffect"
|
||||
|
||||
# Setup phase (post coin flip): entity movement + reveal + mulligan
|
||||
ENTITY_MOVED = "EntityMoved"
|
||||
ENTITY_INTRODUCED = "EntityIntroduced"
|
||||
@@ -591,8 +608,13 @@ class OutboundMsg(str, Enum):
|
||||
# Return=true tucks it back afterwards, Return=false leaves it in the
|
||||
# multiPresentArea for a following attach move (l.a) to consume.
|
||||
REVEAL_CARD_TO_ALL_EFFECT = "RevealCardToAllEffect"
|
||||
# Unimplemented message emissions: client_source/pie/pie-src/RevealCardsTo*Effect.cs.
|
||||
REVEAL_CARDS_TO_ALL_EFFECT = "RevealCardsToAllEffect"
|
||||
REVEAL_CARDS_TO_PLAYER_EFFECT = "RevealCardsToPlayerEffect"
|
||||
MULLIGAN_CHOICE_REQUIRED = "MulliganChoiceRequired"
|
||||
SHUFFLED = "Shuffled"
|
||||
# Source: PileReordered.cs; children contains the complete pile, bottom first.
|
||||
PILE_REORDERED = "PileReordered"
|
||||
# Deck-lift animation trigger (S.I); required in HandShuffledAndMovedToDeck.
|
||||
PLACE_ON_BOTTOM = "PlaceOnBottom"
|
||||
# Full-screen carousel revealing a player's mulliganed hand(s) to the opponent.
|
||||
@@ -609,10 +631,32 @@ class OutboundMsg(str, Enum):
|
||||
# knockout replacement): pick one entity, no action tree.
|
||||
SELECTION_WITH_TARGETS_REQUIRED = "SelectionWithTargetsRequired"
|
||||
|
||||
# Unimplemented message emissions: client_source/pie/pie-src/Evolve*Effect.cs.
|
||||
EVOLVE_EFFECT = "EvolveEffect"
|
||||
EVOLVE_WITH_CONTEXT_EFFECT = "EvolveWithContextEffect"
|
||||
# Unimplemented message emission: pie.code.pie.gameModules.playmat.messages.
|
||||
ENERGY_SWAP_EFFECT = "EnergySwapEffect"
|
||||
|
||||
# Unimplemented condition messages (mechanics currently use attribute updates).
|
||||
ADD_SPECIAL_CONDITION_EFFECT = "AddSpecialConditionEffect"
|
||||
REMOVE_SPECIAL_CONDITION_EFFECT = "RemoveSpecialConditionEffect"
|
||||
BURN_EFFECT = "BurnEffect"
|
||||
CONFUSE_EFFECT = "ConfuseEffect"
|
||||
PARALYZE_EFFECT = "ParalyzeEffect"
|
||||
POISON_EFFECT = "PoisonEffect"
|
||||
SLEEP_EFFECT = "SleepEffect"
|
||||
REMOVE_BURN_EFFECT = "RemoveBurnEffect"
|
||||
REMOVE_CONFUSE_EFFECT = "RemoveConfuseEffect"
|
||||
REMOVE_PARALYZE_EFFECT = "RemoveParalyzeEffect"
|
||||
REMOVE_POISON_EFFECT = "RemovePoisonEffect"
|
||||
REMOVE_SLEEP_EFFECT = "RemoveSleepEffect"
|
||||
|
||||
# Attack choreography (inside an "Attack" sequence bracket, in order):
|
||||
# ability-begin marker (L.o), damage popup + lunge trigger (m.m),
|
||||
# HP attribute update, ability-finished marker (k.U).
|
||||
ABILITY_PLAYED_EFFECT = "AbilityPlayedEffect"
|
||||
# Unimplemented message emission: client_source/pie/pie-src/CakeAbilitySelectedEffect.cs.
|
||||
CAKE_ABILITY_SELECTED_EFFECT = "CakeAbilitySelectedEffect"
|
||||
CAKE_ATTACK_EFFECT = "CakeAttackEffect"
|
||||
# Prevented-hit barrier choreography: {source, targets, wasDamage}.
|
||||
SHIELD_TARGETS_EFFECT = "ShieldTargetsEffect"
|
||||
@@ -627,6 +671,11 @@ class OutboundMsg(str, Enum):
|
||||
ABILITY_FINISHED_EFFECT = "AbilityFinishedEffect"
|
||||
# Named entity parameter for a bracket executor (e.g. Evolve needs "From"/"Into").
|
||||
ENTITY_ID_DATA_EFFECT = "EntityIDDataEffect"
|
||||
# Unimplemented message emissions: dwd.core.match.sequence.dataEffects.
|
||||
ENTITY_ID_LIST_DATA_EFFECT = "EntityIDListDataEffect"
|
||||
INT_DATA_EFFECT = "IntDataEffect"
|
||||
SOURCE_TARGET_DATA_EFFECT = "SourceTargetDataEffect"
|
||||
STRING_DATA_EFFECT = "StringDataEffect"
|
||||
# Orb-of-light targets: the Attack executor injects the r.u projectile group
|
||||
# from the playmat's attack-source entity to these targets.
|
||||
NON_DAMAGING_TARGETS_EFFECT = "NonDamagingTargetsEffect"
|
||||
@@ -635,6 +684,15 @@ class OutboundMsg(str, Enum):
|
||||
VSTAR_POWER_USED_EFFECT = "VSTARPowerUsedEffect"
|
||||
# GX equivalent of the VSTAR marker flip (handler b.M).
|
||||
GX_ATTACK_USED_EFFECT = "GXAttackUsedEffect"
|
||||
|
||||
# Unimplemented generic combat messages: sausage-core/dwd/core/match/effects and core.
|
||||
ATTACK_EFFECT = "AttackEffect"
|
||||
CLEAR_ATTACK_EFFECT = "ClearAttackEffect"
|
||||
SWAP_ATTACKER_EFFECT = "SwapAttackerEffect"
|
||||
DEFEND_EFFECT = "DefendEffect"
|
||||
CLEAR_DEFEND_EFFECT = "ClearDefendEffect"
|
||||
SWAP_DEFENDER_EFFECT = "SwapDefenderEffect"
|
||||
|
||||
# End-of-game dialog (handler D.Z): winner/loser account IDs + reward info.
|
||||
GAME_COMPLETED_MESSAGE = "GameCompletedMessage"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user