Created
June 9, 2025 17:44
-
-
Save esco/f0e81730bc25523976b14cc498b0194f to your computer and use it in GitHub Desktop.
Revisions
-
esco created this gist
Jun 9, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,186 @@ ## Card Effect System Documentation ### 1. Introduction This document describes the architecture and workflow of the card effect system within the Yu-Gi-Oh! Web Duel Interface. The system is responsible for handling the activation, chaining, and resolution of card effects according to Yu-Gi-Oh! rules. It integrates declarative effect definitions (partially implemented via YAML/JSON) with the core game engine logic. The system aims to manage: * **Activation:** Determining when and if a card effect can be activated. * **Chaining:** Building a sequence of effects when multiple effects are activated in response to each other. * **Resolution:** Executing the effects in the correct (Last-In, First-Out) order. * **Targeting & Conditions:** Handling player choices for targets and verifying activation conditions. * **State Updates:** Ensuring the game state is correctly modified as effects resolve. ### 2. Core Concepts * **Effect:** A specific action or sequence of actions performed by a card (e.g., "Draw 2 cards", "Destroy 1 monster"). Effects are defined declaratively (partially via YAML/JSON). * **Trigger:** An event or condition in the game that allows a card effect to be activated (e.g., `onSummon`, `onAttackDeclaration`, `onActivation`). * **Condition:** A prerequisite that must be met for an effect to be activated or resolved (e.g., `IsPhase: Main1`, `HasCards: GraveyardPlayer`). * **Targeting:** The process of selecting specific cards, zones, or players that an effect will interact with. Can be `Manual` (player selection) or `Automatic`. * **Action:** A basic operation performed by an effect (e.g., `MoveCard`, `DestroyCards`, `ModifyLifePoints`, `DrawCards`). * **Chain:** A sequence of card effects activated in response to each other. Effects on the chain resolve in reverse order of activation (LIFO). * **Spell Speed:** A rule determining which effects can respond to others on a chain (Spell Speed 1 < Spell Speed 2 < Spell Speed 3). * **Resolution:** The process of executing an effect's actions after any chaining is complete. ### 3. Key Components & Responsibilities * **`GameEngine` (`src/engine/GameEngine.ts`):** * The central authority for game state and rules. * Receives actions (like `ACTIVATE_EFFECT`) via its `dispatch` method. * Validates actions using `RuleValidator`. * Processes valid actions, often delegating to specialized handlers (`ChainResolver`, `EffectExecutor`). * Manages the core game loop (phases, turns). * Emits `STATE_PATCH` events reflecting authoritative state changes. * Emits other domain-specific events (e.g., `ATTACK_TARGETS`, `CHAIN_LINK_ADDED`, `EFFECT_RESOLVED`). * **`ChainResolver` (`src/engine/ChainResolver.ts`):** * Manages the building and resolution of chains. * Tracks chain links, player priority, and chain status (`Idle`, `ChainBuilding`, `WaitingForResponse`, `ChainResolving`). * Calls the `EffectExecutor` to resolve each link in LIFO order. * Emits chain-related events (`CHAIN_LINK_ADDED`, `CHAIN_RESOLVING`, `CHAIN_RESOLVED`). * **`EffectExecutor` (`src/engine/EffectExecutor.ts`):** * Orchestrates the execution of a single card effect. * Retrieves effect definitions via `CardEffectRegistry`. * Handles effect steps sequentially: condition checking, targeting, action execution. * Uses `PromptManager` for player interactions (targeting). * Uses `ActionRegistry`, `FilterRegistry`, `ConditionRegistry` to execute declarative steps. * Provides an `EngineAPI` for actions to interact back with the engine (e.g., queueing state changes or dispatching further actions). * **`CardEffectRegistry` (`src/engine/CardEffectRegistry.ts`):** * Central point for retrieving effect definitions for a given card ID/GUID. * Interfaces with `CardEffectLoader` to load definitions as needed. * **`CardEffectLoader` (`src/engine/cardEffects/CardEffectLoader.ts`):** * Loads card effect definitions (currently from JSON files generated from YAML) on demand. * Uses `CardEffectIndex` to map card IDs to filenames. * Caches loaded effects. * **`EffectContext` (`src/engine/effects/EffectContext.ts`):** * Provides contextual information during an effect's execution (activating player, card, targets, chain link number, etc.). * Holds references to the engine for state queries. * **Registries (`src/engine/effects/*Registry.ts`):** * `ActionRegistry`: Contains functions implementing basic effect actions (MoveCard, DestroyCards, etc.). * `ConditionRegistry`: Contains functions implementing effect conditions. * `FilterRegistry`: Contains functions implementing target filters. * **`PromptManager` (`src/engine/effects/PromptManager.ts`):** * Handles requests for player input during effect resolution (e.g., selecting targets, choosing options). * Interacts with the `GameEngine` to emit prompt events (`PROMPT_PLAYER_ACTION`). * **UI Components (`src/components/`):** * `GameBoardUI`, `Zone`, `HandDisplay`: Capture user interactions (clicks, drags) that trigger effect activations. * `PromptHandler`: Listens for prompt events and displays UI prompts. * `ChainStack`: Visualizes the current chain based on store updates. * `CardInfoPanel`: Displays details of hovered cards. * **Stores (`src/store/`):** * `GameStore`: Maintains the primary game state for React components, updated mainly via `STATE_PATCH` events from the `GameEngine`. Handles resolving prompts initiated by the engine. * `UIStore`: Manages UI-specific state (highlights, animations, toasts). Processes domain events from the engine for visual feedback. ### 4. Effect Activation Flow 1. **User Interaction:** The player interacts with a card (e.g., clicks "Activate" on a card in a `Zone`, drags a Spell from `HandDisplay` to a `Zone`). 2. **Action Dispatch:** The UI component (`Zone.tsx` or `GameBoardUI.tsx` via `useGameInterface`) calls `dispatch` on the `GameEngine` with an `ACTIVATE_EFFECT` action, including `playerId`, `cardId`, potentially `zoneId` (if activating to the field), and `guid` (passcode for effect lookup). 3. **Engine Validation:** `GameEngine.validateAction` checks basic rules (correct phase, player turn, etc.). If invalid, an `ACTION_REJECTED` event is emitted. 4. **Engine Processing:** `GameEngine.processAction` handles the `ACTIVATE_EFFECT` action. * If activating from hand to field (e.g., a Spell), it may first queue/dispatch a `MOVE_CARD` action. * It calls `Actions.activateEffect` to generate the `EFFECT_ACTIVATED` domain event. * Crucially, it initiates the chain process by calling `chainResolver.startChain`. 5. **Chain Initiation:** `ChainResolver.startChain`: * Creates the first `ChainLink` using details from the `EFFECT_ACTIVATED` event (cardId, playerId, effectId/guid, targets). * Adds the link to its internal `chainLinks` array. * Sets its status to `ChainBuilding`. * Sets priority to the *opponent*. * Emits a `CHAIN_LINK_ADDED` event via the `GameEngine`. 6. **Event Propagation:** The `GameEngine` emits `EFFECT_ACTIVATED` and `CHAIN_LINK_ADDED`. 7. **Store Updates:** * `UIStore` receives `CHAIN_LINK_ADDED` and updates the visual `ChainStack`. It might also trigger pulsation/highlight via `EFFECT_ACTIVATED`. * `GameStore` receives the events but primarily updates its internal `chainState` based on `STATE_PATCH` events (which the engine should also emit based on the chain state change, though this link isn't explicit in the provided code but is implied by the architecture). 8. **Priority & Response:** The engine, guided by the `ChainResolver`, now needs to prompt the opponent (or the next player in priority) to respond. This leads to the Chain Building flow. ### 5. Chain Building & Response Flow 1. **Prompt for Response:** After a chain link is added, the `GameEngine` (likely triggered by `ChainResolver` state changes or explicit calls) uses the `PromptManager` to request a response from the player with priority. 2. **Prompt Emission:** `PromptManager.requestChainResponse` (or similar) emits a `PROMPT_PLAYER_ACTION` or `CHAIN_RESPONSE_AVAILABLE` event via the `GameEngine`. 3. **UI Prompt:** * `GameStore` receives the prompt event and updates its state (`isWaiting`, `waitingPrompt`, `currentPrompt`). * `PromptHandler` component renders the prompt (e.g., "Respond with an effect?" Yes/No or card selection). 4. **Player Response:** The user interacts with the prompt. 5. **Resolve Prompt:** The UI calls `GameStore.resolvePrompt`, providing the user's choice ('Pass' or 'Activate' with card details). 6. **Prompt Resolution:** `GameStore.resolvePrompt` calls `PromptManager.resolvePrompt`/`resolveChainResponse`, which fulfills the promise the engine was waiting on. 7. **Engine Receives Response:** The `GameEngine` receives the resolved response. 8. **Dispatch Response Action:** The engine dispatches a `RESPOND_TO_CHAIN` action. 9. **Process Response Action:** `GameEngine.processAction` handles `RESPOND_TO_CHAIN`. * **If 'Pass':** Calls `chainResolver.passPriority`. * If the *other* player had already passed, `chainResolver.resolveChain` is called (Go to Resolution Flow). * If this is the first pass, priority shifts to the other player, and the process repeats from Step 1 for that player. * **If 'Activate':** Calls `chainResolver.addLink` with the new card's details. * A new `CHAIN_LINK_ADDED` event is emitted. * Priority shifts to the other player. * The process repeats from Step 1 for the other player. ### 6. Effect Resolution Flow 1. **Trigger Resolution:** `ChainResolver.resolveChain` is called when both players consecutively pass priority. 2. **Status Update:** `ChainResolver` sets status to `ChainResolving`, emits `CHAIN_RESOLVING` (start) event. 3. **Process Links (LIFO):** `ChainResolver.processChainLinks` starts processing the `chainLinks` array in *reverse* order. 4. **For Each Link:** * Emit `CHAIN_RESOLVING` event for the specific link number. * Create `EffectContext` for the link (includes cardId, playerId, targets, link number). * Call `effectExecutor.executeCardEffect` with the card identifier (GUID/passcode or ID) and context. 5. **Execute Effect:** `EffectExecutor.executeCardEffect`: * Uses `CardEffectRegistry.getEffectsForCard` to load the effect definition (using `CardEffectLoader` if needed). * Iterates through the `effects` steps defined in the loaded effect JSON/YAML. 6. **Execute Step:** `EffectExecutor.executeEffectStep`: * **Conditions:** Checks step `conditions` using `ConditionRegistry`. If fail, skips step. * **Targeting:** If `targeting` exists, calls `EffectExecutor.executeTargeting`. * Gets eligible targets using `FilterRegistry`. * If `Manual`, uses `PromptManager.requestPrompt` to ask the player to select targets. * Stores selected targets in the `EffectContext`. If targeting fails (e.g., not enough valid targets, player cancels), the effect step may fail. * **Actions:** Executes each `action` sequentially using `ActionRegistry.getAction`. 7. **Execute Action:** The specific `ActionFunction` (e.g., `moveCardAction`, `destroyCardsAction`) is called with `params`, `context`, and `api`. 8. **Engine API Interaction:** The `ActionFunction` uses the `EngineAPI` methods (e.g., `api.requestMoveCard`). 9. **Engine Queues/Dispatches:** The `EngineAPI` methods either queue state changes or, more likely based on the code, **dispatch new actions** (e.g., `MOVE_CARD`, `MODIFY_LP`) back to the `GameEngine`. 10. **Inner Dispatch Loop:** The `GameEngine` receives and processes these new actions (`MOVE_CARD`, etc.), generating domain events (`CARD_MOVED`, etc.) and corresponding `STATE_PATCH` events. These are emitted immediately. 11. **Effect Step Completion:** Control returns to `EffectExecutor` after all actions in a step complete. It moves to the next step or finishes the effect. 12. **Link Resolution:** Control returns to `ChainResolver` after `effectExecutor.executeCardEffect` finishes. 13. **Effect Resolved Event:** `ChainResolver` emits `EFFECT_RESOLVED` via the `GameEngine`. 14. **Post-Resolution Handling:** `GameEngine.handleEffectResolvedEvent` checks if the resolved card (e.g., a Normal Spell) should be sent to the Graveyard and dispatches a `MOVE_CARD` action after a delay if necessary. 15. **Next Link:** `ChainResolver.processChainLinks` waits briefly and proceeds to the next link (next highest number). 16. **Chain Completion:** After all links are processed, `ChainResolver.processChainLinks` resolves. 17. **Chain Resolved Event:** `ChainResolver` emits `CHAIN_RESOLVED` event via `GameEngine`, resets its state to `Idle`. ### 7. State Changes Sequence (Simplified Example: Player activates Pot of Greed) 1. **Action:** User clicks "Activate" on Pot of Greed in hand -> `dispatch({ type: 'ACTIVATE_EFFECT', payload: { cardId: 'pog-1', zoneId: 'player-spell-0', guid: '55144522' } })` 2. **Engine:** Validates action. Processes `ACTIVATE_EFFECT`. 3. **Engine:** Queues/Dispatches `MOVE_CARD` (Hand -> Field Zone `player-spell-0`). 4. **Engine:** Processes `MOVE_CARD`, generates `CARD_MOVED` event and `STATE_PATCH` for the move. Emits both. 5. **GameStore/UIStore:** Receive `CARD_MOVED`, `STATE_PATCH`. UIStore triggers animation. GameStore updates state. Card appears face-up in `player-spell-0`. 6. **Engine:** Calls `Actions.activateEffect`, generates `EFFECT_ACTIVATED` event. 7. **Engine:** Calls `chainResolver.startChain`. 8. **ChainResolver:** Creates Chain Link 1 (Pot of Greed). Sets status `ChainBuilding`. Priority to opponent. Emits `CHAIN_LINK_ADDED` via Engine. 9. **Engine:** Emits `EFFECT_ACTIVATED`, `CHAIN_LINK_ADDED`. 10. **Stores:** UIStore updates ChainStack. 11. **Engine:** Prompts opponent to respond (via `PromptManager`). Emits `PROMPT_PLAYER_ACTION`. 12. **AI/Player:** Opponent chooses 'Pass'. 13. **Engine:** `dispatch({ type: 'RESPOND_TO_CHAIN', payload: { responseType: 'Pass', playerId: 'opponent' } })`. 14. **Engine:** Calls `chainResolver.passPriority`. Priority shifts to player. 15. **Engine:** Prompts player to respond. Emits `PROMPT_PLAYER_ACTION`. 16. **UI/Player:** Player chooses 'Pass'. UI calls `GameStore.resolvePrompt`. 17. **Engine:** `dispatch({ type: 'RESPOND_TO_CHAIN', payload: { responseType: 'Pass', playerId: 'player' } })`. 18. **Engine:** Calls `chainResolver.passPriority`. Both passed -> calls `chainResolver.resolveChain`. 19. **ChainResolver:** Sets status `ChainResolving`. Emits `CHAIN_RESOLVING` (start). Starts `processChainLinks`. 20. **ChainResolver:** Processes Link 1 (Pot of Greed). Emits `CHAIN_RESOLVING` (link 1). Calls `effectExecutor.executeCardEffect('55144522', context)`. 21. **EffectExecutor:** Loads Pot of Greed effect (`drawCards` action, count 2). Executes the action. 22. **Action (`drawCardsAction`):** Uses `api.requestDrawCard('player', 2)`. 23. **EngineAPI:** Dispatches `DRAW_CARD` action back to the engine. 24. **Engine:** Processes `DRAW_CARD`. Generates `CARD_DRAWN` event and `STATE_PATCH` to update deck/hand. Emits both. 25. **GameStore/UIStore:** Receive `CARD_DRAWN`, `STATE_PATCH`. Update state. UIStore may animate card draw. 26. **EffectExecutor:** Effect step completes. 27. **ChainResolver:** Receives completion from executor. Emits `EFFECT_RESOLVED` via Engine. 28. **Engine:** Handles `EFFECT_RESOLVED`. Pot of Greed is a Normal Spell, so dispatches `MOVE_CARD` (Field Zone -> Graveyard) *after a delay*. 29. **ChainResolver:** `processChainLinks` finishes. Emits `CHAIN_RESOLVED` via Engine. Resets state to `Idle`. 30. **Engine:** (After delay) Processes `MOVE_CARD` for Pot of Greed -> Graveyard. Generates `CARD_MOVED` and `STATE_PATCH`. Emits both. 31. **GameStore/UIStore:** Receive events, update state, animate card going to GY. ### 8. YAML/JSON Effect Loading * The system uses dynamic imports (`import()`) managed by `CardEffectLoader`. * `CardEffectIndex` maps card identifiers (like passcodes '55144522') to the base filename of their JSON definition (e.g., 'PotOfGreed'). * When `EffectExecutor` needs an effect, it asks `CardEffectRegistry`, which uses the `CardEffectLoader` to load the corresponding JSON file from `src/engine/cardEffects/json/` if not already cached. * (Implicitly) These JSON files are assumed to be generated/compiled from the YAML definitions during a build step, although the compiler itself is not fully implemented in the provided code. * The `GameEngine` attempts to *preload* effects for cards in the initial decks using `CardEffectRegistry.initializeCardEffects` when the `INITIALIZE_GAME` action occurs.