aether-shards/specs/EffectProcessor.spec.md
Matthew Mone f04905044d Implement EffectProcessor and related systems for enhanced game mechanics
Introduce the EffectProcessor class to manage game state changes through various effects, including damage, healing, and status application. Define type specifications for effects, conditions, and passive abilities in Effects.d.ts. Add a comprehensive JSON registry for passive skills and item effects, enhancing gameplay dynamics. Update the GameLoop and TurnSystem to integrate the EffectProcessor, ensuring proper handling of environmental hazards and passive effects during combat. Enhance testing coverage for the EffectProcessor and environmental interactions to validate functionality and performance.
2025-12-30 20:50:11 -08:00

8.7 KiB

Effect Processor Specification: The Game Logic Engine

This document defines the architecture for the Effect Processor, the central system responsible for executing all changes to the game state (Damage, Healing, Movement, Spawning).

1. System Overview

The EffectProcessor is a stateless logic engine. It takes a Definition (What to do) and a Context (Who is doing it to whom), and applies the necessary mutations to the UnitManager or VoxelGrid.

Architectural Role

  • Input: EffectDefinition (JSON), Source (Unit), Target (Unit/Tile).
  • Output: State Mutation (HP changed, Unit moved) + EffectResult (Log data).
  • Pattern: Strategy Pattern. Each effect_type maps to a specific Handler Function.

2. Integration Points

A. Calling the Processor

The Processor is never called directly by the UI. It is invoked by:

  1. SkillManager: When an Active Skill is executed.
  2. EventSystem: When a Passive Item triggers (e.g., "On Hit -> Apply Burn").
  3. Environmental Hazard: When a unit starts their turn on Fire/Acid.

B. Dependencies

The Processor requires injection of:

  • VoxelGrid: To check collision, modify terrain, or move units.
  • UnitManager: To find neighbors (Chain Lightning) or spawn tokens (Turrets).
  • RNG: A seeded random number generator for damage variance and status chances.

3. Data Structure (JSON Schema)

Every effect in the game must adhere to this structure.

/**
 * Effects.ts
 * Type definitions for the Game Logic Engine (Effects, Passives, and Triggers).
 */

// =============================================================================
// 1. EFFECT DEFINITIONS (The "What")
// =============================================================================

/**
 * List of all valid actions the EffectProcessor can execute.
 */
export type EffectType =
  // Combat
  | "DAMAGE"
  | "HEAL"
  | "CHAIN_DAMAGE"
  | "REDIRECT_DAMAGE"
  | "PREVENT_DEATH"

  // Status & Stats
  | "APPLY_STATUS"
  | "REMOVE_STATUS"
  | "REMOVE_ALL_DEBUFFS"
  | "APPLY_BUFF"
  | "GIVE_AP"
  | "ADD_CHARGE"
  | "ADD_SHIELD"
  | "CONVERT_DAMAGE_TO_HEAL"
  | "DYNAMIC_BUFF"

  // Movement & Physics
  | "TELEPORT"
  | "MOVE_TO_TARGET"
  | "SWAP_POSITIONS"
  | "PHYSICS_PULL"
  | "PUSH"

  // World & Spawning
  | "SPAWN_OBJECT"
  | "SPAWN_HAZARD"
  | "SPAWN_LOOT"
  | "MODIFY_TERRAIN"
  | "DESTROY_VOXEL"
  | "DESTROY_OBJECTS"
  | "REVEAL_OBJECTS"
  | "COLLECT_LOOT"

  // Meta / Logic
  | "REPEAT_SKILL"
  | "CANCEL_EVENT"
  | "REDUCE_COST"
  | "BUFF_SPAWN"
  | "MODIFY_AOE";

/**
 * A generic container for parameters used by Effect Handlers.
 * In a strict system, this would be a Union of specific interfaces,
 * but for JSON deserialization, a loose interface is often more flexible.
 */
export interface EffectParams {
  // -- Combat Magnitude --
  power?: number; // Base amount (Damage/Heal)
  attribute?: string; // Stat to scale off (e.g., "strength", "magic")
  scaling?: number; // Multiplier for attribute (Default: 1.0)
  element?: "PHYSICAL" | "FIRE" | "ICE" | "SHOCK" | "VOID" | "TECH";

  // -- Chaining --
  bounces?: number;
  decay?: number;
  synergy_trigger?: string; // Status ID that triggers bonus effect

  // -- Status/Buffs --
  status_id?: string;
  duration?: number;
  stat?: string; // For Buffs
  value?: number; // For Buffs/Mods
  chance?: number; // 0.0 to 1.0 (Proc chance)

  // -- Physics --
  force?: number; // Distance
  destination?: "TARGET" | "BEHIND_TARGET" | "ADJACENT_TO_TARGET";

  // -- World --
  object_id?: string; // Unit ID to spawn
  hazard_id?: string;
  tag?: string; // Filter for objects (e.g. "COVER")
  range?: number; // AoE radius

  // -- Logic --
  percentage?: number; // 0.0 - 1.0
  amount?: number; // Flat amount (AP/Charge)
  amount_range?: [number, number]; // [min, max]
  set_hp?: number; // Hard set HP value
  shape?: "CIRCLE" | "LINE" | "CONE" | "SINGLE";
  size?: number;
  multiplier?: number;
}

/**
 * The runtime payload passed to EffectProcessor.process().
 * Combines the Type and the Params.
 */
export interface EffectDefinition extends EffectParams {
  type: EffectType;

  // Optional Override Condition for the Effect itself
  // (Distinct from the Passive Trigger condition)
  condition?: ConditionDefinition;

  // Conditional Multiplier (e.g. Execute damage)
  conditional_multiplier?: {
    condition: string; // Condition Tag
    value: number;
  };
}

// =============================================================================
// 2. PASSIVE DEFINITIONS (The "When")
// =============================================================================

/**
 * Triggers that the EventSystem listens for.
 */
export type TriggerType =
  // Stat Calculation Hooks
  | "ON_STAT_CALC"
  | "STATIC_STAT_MOD"
  | "ON_SKILL_COST_CALC"
  | "ON_ATTACK_CALC"
  | "ON_DAMAGE_CALC"

  // Combat Events
  | "ON_ATTACK_HIT"
  | "ON_SKILL_HIT"
  | "ON_SKILL_CAST"
  | "ON_DAMAGED"
  | "ON_ALLY_DAMAGED"
  | "ON_DAMAGE_DEALT"
  | "ON_HEAL_DEALT"
  | "ON_HEAL_OVERFLOW"
  | "ON_KILL"
  | "ON_LETHAL_DAMAGE"
  | "ON_SYNERGY_TRIGGER"

  // Turn Lifecycle
  | "ON_TURN_START"
  | "ON_TURN_END"
  | "ON_ACTION_COMPLETE"

  // World Events
  | "ON_MOVE_COMPLETE"
  | "ON_HAZARD_TICK"
  | "ON_TRAP_TRIGGER"
  | "ON_OBJECT_DESTROYED"
  | "ON_SPAWN_OBJECT"
  | "ON_LEVEL_START"

  // Meta / UI
  | "GLOBAL_SHOP_PRICE"
  | "ON_SKILL_TARGETING"
  | "AURA_UPDATE";

/**
 * How a stat modifier interacts with the base value.
 */
export type ModifierType =
  | "ADD"
  | "MULTIPLY"
  | "ADD_PER_ADJACENT_ENEMY"
  | "ADD_PER_DESTROYED_VOXEL"
  | "ADD_STAT"
  | "MULTIPLY_DAMAGE";

/**
 * An entry in `passive_registry.json`.
 * Defines a rule: "WHEN [Trigger] IF [Condition] THEN [Action]".
 */
export interface PassiveDefinition {
  id: string;
  name: string;
  description: string;

  // -- The Trigger --
  trigger: TriggerType;

  // -- The Check --
  condition?: ConditionDefinition;
  limit?: number; // Max times per run/turn

  // -- The Effect (Action) --
  // Maps to EffectDefinition.type
  action?: EffectType;

  // -- The Parameters --
  // Merged into EffectDefinition
  params?: EffectParams;

  // -- For Stat Modifiers (simpler than full Effects) --
  stat?: string;
  modifier_type?: ModifierType;
  value?: number;
  range?: number; // For Aura/Per-Adjacent checks

  // -- For Complex Stat Lists --
  modifiers?: { stat: string; value: number }[];

  // -- For Auras --
  target?: "ALLIES" | "ENEMIES" | "SELF";
}

// =============================================================================
// 3. CONDITIONS (The "If")
// =============================================================================

export type ConditionType =
  | "SOURCE_IS_ADJACENT"
  | "IS_ADJACENT"
  | "IS_BASIC_ATTACK"
  | "SKILL_TAG" // value: string
  | "DAMAGE_TYPE" // value: string
  | "TARGET_TAG" // value: string
  | "OWNER_IS_SELF"
  | "IS_FLANKING"
  | "TARGET_LOCKED"
  | "DID_NOT_ATTACK"
  | "TARGET_HP_LOW"
  | "IS_MECHANICAL";

export interface ConditionDefinition {
  type: ConditionType;
  value?: string | number | boolean;
}

4. Handler Specifications

Handler: DAMAGE

  • Logic: FinalDamage = (BasePower + (Source[Attribute] * Scaling)) - Target.Defense.
  • Element Check: If Target has Resistance/Weakness to element, modify FinalDamage.
  • Result: Target.currentHP -= FinalDamage.

Handler: CHAIN_DAMAGE

  • Logic: Apply DAMAGE to primary target. Then, scan for N nearest enemies within Range R. Apply DAMAGE * Decay to them.
  • Synergy: If condition.target_status is present on a target, the chain may branch or deal double damage.

Handler: TELEPORT

  • Logic: Validate destination tile (must be Air and Unoccupied). Update Unit.position and VoxelGrid.unitMap.
  • Visuals: Trigger "Vanish" VFX at old pos, "Appear" VFX at new pos.

Handler: MODIFY_TERRAIN

  • Logic: Update VoxelGrid ID at Target coordinates.
  • Use Case: Sapper's "Breach Charge" turns ID_WALL into ID_AIR.
  • Safety: Check VoxelGrid.isDestructible(). Do not destroy bedrock.

5. Conditions of Acceptance (CoA)

CoA 1: Attribute Scaling

  • Given a Damage Effect with power: 10 and attribute: "magic", if Source has magic: 5, the output damage must be 15.

CoA 2: Conditional Logic

  • Given an Effect with condition: { target_status: "WET" }, if the target does not have the "WET" status, the effect must not execute (return early).

CoA 3: State Mutation

  • When APPLY_STATUS is executed, the Target unit's statusEffects array must contain the new ID with the correct duration.

CoA 4: Physics Safety

  • When PUSH is executed, the system must check VoxelGrid.isSolid() behind the target. If a wall exists, the unit must not move into the wall (optionally take "Smash" damage instead).