Skip to content

Instantly share code, notes, and snippets.

@esco
Created June 9, 2025 17:44
Show Gist options
  • Select an option

  • Save esco/f0e81730bc25523976b14cc498b0194f to your computer and use it in GitHub Desktop.

Select an option

Save esco/f0e81730bc25523976b14cc498b0194f to your computer and use it in GitHub Desktop.

Revisions

  1. esco created this gist Jun 9, 2025.
    186 changes: 186 additions & 0 deletions CARD_EFFECT_SYSTEM.md
    Original 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.