/**
 * @typedef {import("./types.js").RunData} RunData
 * @typedef {import("../grid/types.js").Position} Position
 * @typedef {import("../units/Unit.js").Unit} Unit
 * @typedef {import("../ui/combat-hud.d.ts").CombatState} CombatState
 * @typedef {import("../ui/combat-hud.d.ts").UnitStatus} UnitStatus
 * @typedef {import("../ui/combat-hud.d.ts").QueueEntry} QueueEntry
 */

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { VoxelGrid } from "../grid/VoxelGrid.js";
import { VoxelManager } from "../grid/VoxelManager.js";
import { UnitManager } from "../managers/UnitManager.js";
import { CaveGenerator } from "../generation/CaveGenerator.js";
import { RuinGenerator } from "../generation/RuinGenerator.js";
import { InputManager } from "./InputManager.js";
import { MissionManager } from "../managers/MissionManager.js";
import { TurnSystem } from "../systems/TurnSystem.js";
import { MovementSystem } from "../systems/MovementSystem.js";
import { SkillTargetingSystem } from "../systems/SkillTargetingSystem.js";
import { EffectProcessor } from "../systems/EffectProcessor.js";
import { SeededRandom } from "../utils/SeededRandom.js";
import { skillRegistry } from "../managers/SkillRegistry.js";
import { InventoryManager } from "../managers/InventoryManager.js";
import { InventoryContainer } from "../models/InventoryContainer.js";
import { itemRegistry } from "../managers/ItemRegistry.js";
import { narrativeManager } from "../managers/NarrativeManager.js";

// Class definitions will be lazy-loaded when startLevel is called

/**
 * Main game loop managing rendering, input, and game state.
 * @class
 */
export class GameLoop {
  constructor() {
    /** @type {boolean} */
    this.isRunning = false;

    /** @type {Object|null} Cached skill tree template */
    this._skillTreeTemplate = null;
    /** @type {number | null} */
    this.animationFrameId = null;
    /** @type {boolean} */
    this.isPaused = false;

    // 1. Core Systems
    /** @type {THREE.Scene} */
    this.scene = new THREE.Scene();
    /** @type {THREE.PerspectiveCamera | null} */
    this.camera = null;
    /** @type {THREE.WebGLRenderer | null} */
    this.renderer = null;
    /** @type {OrbitControls | null} */
    this.controls = null;
    /** @type {InputManager | null} */
    this.inputManager = null;

    /** @type {VoxelGrid | null} */
    this.grid = null;
    /** @type {VoxelManager | null} */
    this.voxelManager = null;
    /** @type {UnitManager | null} */
    this.unitManager = null;

    // Combat Logic Systems
    /** @type {TurnSystem | null} */
    this.turnSystem = null;
    /** @type {MovementSystem | null} */
    this.movementSystem = null;
    /** @type {SkillTargetingSystem | null} */
    this.skillTargetingSystem = null;
    /** @type {EffectProcessor | null} */
    this.effectProcessor = null;

    // Inventory System
    /** @type {InventoryManager | null} */
    this.inventoryManager = null;

    // AbortController for cleaning up event listeners
    /** @type {AbortController | null} */
    this.turnSystemAbortController = null;

    /** @type {Map<string, THREE.Mesh>} */
    this.unitMeshes = new Map();
    /** @type {Set<THREE.Mesh>} */
    this.movementHighlights = new Set();
    /** @type {Set<THREE.Mesh>} */
    this.spawnZoneHighlights = new Set();
    /** @type {Set<THREE.Mesh>} */
    this.rangeHighlights = new Set();
    /** @type {Set<THREE.Mesh>} */
    this.aoeReticle = new Set();
    /** @type {RunData | null} */
    this.runData = null;
    /** @type {Position[]} */
    this.playerSpawnZone = [];
    /** @type {Position[]} */
    this.enemySpawnZone = [];

    // Input Logic State
    /** @type {number} */
    this.lastMoveTime = 0;
    /** @type {number} */
    this.moveCooldown = 120; // ms between cursor moves
    /** @type {"MOVEMENT" | "TARGETING"} */
    this.selectionMode = "MOVEMENT"; // MOVEMENT, TARGETING
    /** @type {MissionManager} */
    this.missionManager = new MissionManager(this); // Init Mission Manager

    // Deployment State
    /** @type {{ selectedUnitIndex: number; deployedUnits: Map<number, Unit> }} */
    this.deploymentState = {
      selectedUnitIndex: -1,
      deployedUnits: new Map(), // Map<Index, UnitInstance>
    };

    /** @type {import("./GameStateManager.js").GameStateManagerClass | null} */
    this.gameStateManager = null;

    // Skill Targeting State
    /** @type {"IDLE" | "SELECTING_MOVE" | "TARGETING_SKILL" | "EXECUTING_SKILL"} */
    this.combatState = "IDLE";
    /** @type {string | null} */
    this.activeSkillId = null;
  }

  /**
   * Initializes the game loop with Three.js setup.
   * @param {HTMLElement} container - DOM element to attach the renderer to
   */
  init(container) {
    // Setup Three.js
    this.camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    this.camera.position.set(20, 20, 20);
    this.camera.lookAt(0, 0, 0);

    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setClearColor(0x111111);
    container.appendChild(this.renderer.domElement);

    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    this.controls.dampingFactor = 0.05;

    // --- INSTANTIATE COMBAT SYSTEMS ---
    this.turnSystem = new TurnSystem();
    this.movementSystem = new MovementSystem();
    // SkillTargetingSystem will be initialized in startLevel when grid/unitManager are ready

    // --- INITIALIZE INVENTORY SYSTEM ---
    // Create stashes (InventoryManager will be initialized in startLevel after itemRegistry loads)
    const runStash = new InventoryContainer("RUN_LOOT");
    const hubStash = new InventoryContainer("HUB_VAULT");
    // Initialize InventoryManager with itemRegistry (will load items in startLevel)
    this.inventoryManager = new InventoryManager(
      itemRegistry,
      runStash,
      hubStash
    );

    // --- SETUP INPUT MANAGER ---
    this.inputManager = new InputManager(
      this.camera,
      this.scene,
      this.renderer.domElement
    );

    // Bind Buttons (Events)
    this.inputManager.addEventListener("gamepadbuttondown", (e) =>
      this.handleButtonInput(e.detail)
    );
    this.inputManager.addEventListener("keydown", (e) =>
      this.handleKeyInput(e.detail)
    );
    this.inputManager.addEventListener("hover", (e) =>
      this.onCursorHover(e.detail.voxelPosition)
    );

    // Default Validator: Movement Logic (Will be overridden in startLevel)
    this.inputManager.setValidator(this.validateCursorMove.bind(this));

    const ambient = new THREE.AmbientLight(0xffffff, 0.6);
    const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
    dirLight.position.set(10, 20, 10);
    this.scene.add(ambient);
    this.scene.add(dirLight);

    window.addEventListener("resize", () => {
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
    });

    this.animate = this.animate.bind(this);
  }

  /**
   * Validation Logic for Standard Movement.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {false | Position} - False if invalid, or adjusted position object
   */
  validateCursorMove(x, y, z) {
    if (!this.grid) return true; // Allow if grid not ready

    // 1. Basic Bounds Check
    if (!this.grid.isValidBounds({ x, y: 0, z })) return false;

    // 2. Scan Column for Surface (Climb/Drop Logic)
    let bestY = null;

    if (this.isWalkable(x, y, z)) bestY = y;
    else if (this.isWalkable(x, y + 1, z)) bestY = y + 1;
    else if (this.isWalkable(x, y - 1, z)) bestY = y - 1;
    else if (this.isWalkable(x, y - 2, z)) bestY = y - 2;

    if (bestY !== null) {
      return { x, y: bestY, z };
    }

    return false;
  }

  /**
   * Validation Logic for Deployment Phase.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {false | Position} - False if invalid, or valid spawn position
   */
  validateDeploymentCursor(x, y, z) {
    if (!this.grid || this.playerSpawnZone.length === 0) return false;

    // Check if the target X,Z is inside the spawn zone list
    const validSpot = this.playerSpawnZone.find((t) => t.x === x && t.z === z);

    if (validSpot) {
      // Snap Y to the valid floor height defined in the zone
      return { x: validSpot.x, y: validSpot.y, z: validSpot.z };
    }

    return false; // Cursor cannot leave the spawn zone
  }

  /**
   * Checks if a position is walkable.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {boolean} - True if walkable
   */
  isWalkable(x, y, z) {
    if (this.grid.getCell(x, y, z) !== 0) return false;
    if (this.grid.getCell(x, y - 1, z) === 0) return false;
    if (this.grid.getCell(x, y + 1, z) !== 0) return false;
    return true;
  }

  /**
   * Validates an interaction target position.
   * @param {number} x - X coordinate
   * @param {number} y - Y coordinate
   * @param {number} z - Z coordinate
   * @returns {boolean} - True if valid
   */
  validateInteractionTarget(x, y, z) {
    if (!this.grid) return true;
    return this.grid.isValidBounds({ x, y, z });
  }

  /**
   * Handles gamepad button input.
   * @param {{ buttonIndex: number; gamepadIndex: number }} detail - Button input detail
   */
  handleButtonInput(detail) {
    if (detail.buttonIndex === 0) {
      // A / Cross
      this.triggerSelection();
    }
  }

  /**
   * Handles keyboard input.
   * @param {string} code - Key code
   */
  handleKeyInput(code) {
    if (code === "Space" || code === "Enter") {
      this.triggerSelection();
    }
    if (code === "Escape" || code === "KeyB") {
      // Cancel skill targeting
      if (this.combatState === "TARGETING_SKILL") {
        this.cancelSkillTargeting();
      }
    }
    if (code === "Tab") {
      this.selectionMode =
        this.selectionMode === "MOVEMENT" ? "TARGETING" : "MOVEMENT";
      const validator =
        this.selectionMode === "MOVEMENT"
          ? this.validateCursorMove.bind(this)
          : this.validateInteractionTarget.bind(this);

      this.inputManager.setValidator(validator);
    }
    if (code === "KeyC") {
      // Open character sheet for active unit
      this.openCharacterSheet();
    }
    if (code === "KeyM") {
      // Movement mode hotkey
      if (this.gameStateManager?.currentState === "STATE_COMBAT") {
        this.onMovementClicked();
      }
    }
    // Number key hotkeys for skills (1-5)
    if (
      this.gameStateManager?.currentState === "STATE_COMBAT" &&
      this.turnSystem
    ) {
      const activeUnit = this.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team === "PLAYER") {
        const skills = activeUnit.actions || [];
        let skillIndex = -1;

        // Map key codes to skill indices (1-5)
        if (code === "Digit1" || code === "Numpad1") {
          skillIndex = 0;
        } else if (code === "Digit2" || code === "Numpad2") {
          skillIndex = 1;
        } else if (code === "Digit3" || code === "Numpad3") {
          skillIndex = 2;
        } else if (code === "Digit4" || code === "Numpad4") {
          skillIndex = 3;
        } else if (code === "Digit5" || code === "Numpad5") {
          skillIndex = 4;
        }

        if (skillIndex >= 0 && skillIndex < skills.length) {
          const skill = skills[skillIndex];
          if (skill && skill.id) {
            this.onSkillClicked(skill.id);
          }
        }
      }
    }
  }

  /**
   * Opens the character sheet for the currently active unit.
   */
  openCharacterSheet() {
    if (!this.turnSystem) return;

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit || activeUnit.team !== "PLAYER") {
      // If no active unit or not player unit, try to get first player unit
      if (this.unitManager) {
        const playerUnits = this.unitManager
          .getAllUnits()
          .filter((u) => u.team === "PLAYER");
        if (playerUnits.length > 0) {
          this._dispatchOpenCharacterSheet(playerUnits[0]);
        }
      }
      return;
    }

    this._dispatchOpenCharacterSheet(activeUnit);
  }

  /**
   * Dispatches open-character-sheet event for a unit.
   * @param {Unit|string} unitOrId - Unit object or unit ID
   * @private
   */
  _dispatchOpenCharacterSheet(unitOrId) {
    // Get full unit object if ID was provided
    let unit = unitOrId;
    if (typeof unitOrId === "string" && this.unitManager) {
      unit = this.unitManager.getUnitById(unitOrId);
    }

    if (!unit) {
      console.warn("Cannot open character sheet: unit not found");
      return;
    }

    // Get inventory from runData or empty array
    const inventory = this.runData?.inventory || [];

    // Determine if read-only (enemy turn or restricted)
    const activeUnit = this.turnSystem?.getActiveUnit();
    const isReadOnly =
      this.combatState === "TARGETING_SKILL" ||
      (activeUnit && activeUnit.team !== "PLAYER");

    window.dispatchEvent(
      new CustomEvent("open-character-sheet", {
        detail: {
          unit: unit,
          readOnly: isReadOnly,
          inventory: inventory,
        },
      })
    );
  }

  /**
   * Called by UI when a unit is clicked in the Roster.
   * @param {number} index - The index of the unit in the squad to select.
   */
  selectDeploymentUnit(index) {
    this.deploymentState.selectedUnitIndex = index;
    console.log(`Deployment: Selected Unit Index ${index}`);
  }

  /**
   * Triggers selection action at cursor position.
   */
  triggerSelection() {
    const cursor = this.inputManager.getCursorPosition();
    console.log("Action at:", cursor);

    if (
      this.gameStateManager &&
      this.gameStateManager.currentState === "STATE_DEPLOYMENT"
    ) {
      const selIndex = this.deploymentState.selectedUnitIndex;

      if (selIndex !== -1) {
        // Attempt to deploy OR move the selected unit
        const unitDef = this.runData.squad[selIndex];
        const existingUnit = this.deploymentState.deployedUnits.get(selIndex);

        const resultUnit = this.deployUnit(unitDef, cursor, existingUnit);

        if (resultUnit) {
          // Track it
          this.deploymentState.deployedUnits.set(selIndex, resultUnit);

          // Notify UI
          window.dispatchEvent(
            new CustomEvent("deployment-update", {
              detail: {
                deployedIndices: Array.from(
                  this.deploymentState.deployedUnits.keys()
                ),
              },
            })
          );
        }
      } else {
        console.log("No unit selected.");
      }
    } else if (
      this.gameStateManager &&
      this.gameStateManager.currentState === "STATE_COMBAT"
    ) {
      // Handle combat actions based on state
      if (this.combatState === "TARGETING_SKILL") {
        this.handleSkillTargeting(cursor);
      } else {
        // Default to movement
        this.handleCombatMovement(cursor);
      }
    }
  }

  /**
   * Handles movement in combat state.
   * Delegates to MovementSystem.
   * @param {Position} targetPos - Target position to move to
   */
  async handleCombatMovement(targetPos) {
    if (!this.movementSystem || !this.turnSystem) return;

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit || activeUnit.team !== "PLAYER") {
      console.log("Not a player's turn or unit not found");
      return;
    }

    // DELEGATE to MovementSystem
    const success = await this.movementSystem.executeMove(
      activeUnit,
      targetPos
    );

    if (success) {
      // Update unit mesh position
      const mesh = this.unitMeshes.get(activeUnit.id);
      if (mesh) {
        // Floor surface is at pos.y - 0.5, unit should be 0.6 above: pos.y + 0.1
        mesh.position.set(
          activeUnit.position.x,
          activeUnit.position.y + 0.1,
          activeUnit.position.z
        );
      }

      console.log(
        `Moved ${activeUnit.name} to ${activeUnit.position.x},${activeUnit.position.y},${activeUnit.position.z}`
      );

      // Update combat state and movement highlights
      this.updateCombatState().catch(console.error);

      // NOTE: Do NOT auto-end turn when AP reaches 0 after movement.
      // The player should explicitly click "End Turn" to end their turn.
      // Even if the unit has no AP left, they may want to use skills or wait.
    }
  }

  /**
   * Handles skill click from CombatHUD.
   * Enters TARGETING_SKILL state and shows skill range.
   * @param {string} skillId - Skill ID
   */
  onSkillClicked(skillId) {
    if (!this.turnSystem || !this.skillTargetingSystem) return;

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit || activeUnit.team !== "PLAYER") return;

    // If clicking the same skill that's already active, cancel targeting
    if (
      this.combatState === "TARGETING_SKILL" &&
      this.activeSkillId === skillId
    ) {
      this.cancelSkillTargeting();
      return;
    }

    // Find skill in unit's actions
    const skill = (activeUnit.actions || []).find((a) => a.id === skillId);
    if (!skill) {
      console.warn(`Skill ${skillId} not found in unit actions`);
      return;
    }

    // Validate unit has AP
    if (activeUnit.currentAP < (skill.costAP || 0)) {
      console.log("Insufficient AP");
      return;
    }

    // Enter targeting mode
    this.combatState = "TARGETING_SKILL";
    this.activeSkillId = skillId;

    // Clear movement highlights and show skill range (only valid targets)
    this.clearMovementHighlights();
    const skillDef = this.skillTargetingSystem.getSkillDef(skillId);
    if (skillDef && this.voxelManager && this.skillTargetingSystem) {
      // Check if this is a teleport skill with unlimited range (range = -1)
      const isTeleportSkill =
        skillDef.effects && skillDef.effects.some((e) => e.type === "TELEPORT");
      const hasUnlimitedRange =
        skillDef.range === -1 || skillDef.range === Infinity;

      let allTilesInRange = [];

      if (isTeleportSkill && hasUnlimitedRange) {
        // For teleport with unlimited range, scan all valid tiles in the grid
        // Range is limited only by line of sight
        if (this.grid && this.grid.size) {
          for (let x = 0; x < this.grid.size.x; x++) {
            for (let y = 0; y < this.grid.size.y; y++) {
              for (let z = 0; z < this.grid.size.z; z++) {
                if (this.grid.isValidBounds({ x, y, z })) {
                  allTilesInRange.push({ x, y, z });
                }
              }
            }
          }
        }
      } else {
        // For normal skills, get tiles within range
        for (
          let x = activeUnit.position.x - skillDef.range;
          x <= activeUnit.position.x + skillDef.range;
          x++
        ) {
          for (
            let y = activeUnit.position.y - skillDef.range;
            y <= activeUnit.position.y + skillDef.range;
            y++
          ) {
            for (
              let z = activeUnit.position.z - skillDef.range;
              z <= activeUnit.position.z + skillDef.range;
              z++
            ) {
              const dist =
                Math.abs(x - activeUnit.position.x) +
                Math.abs(y - activeUnit.position.y) +
                Math.abs(z - activeUnit.position.z);
              if (dist <= skillDef.range) {
                // Check if position is valid bounds
                if (this.grid && this.grid.isValidBounds({ x, y, z })) {
                  allTilesInRange.push({ x, y, z });
                }
              }
            }
          }
        }
      }

      // Filter to only valid targets using validation and collect obstruction data
      const validTilesWithObstruction = [];
      allTilesInRange.forEach((tilePos) => {
        const validation = this.skillTargetingSystem.validateTarget(
          activeUnit,
          tilePos,
          skillId
        );
        if (validation.valid) {
          validTilesWithObstruction.push({
            pos: tilePos,
            obstruction: validation.obstruction || 0,
          });
        }
      });

      // Highlight only valid targets with obstruction-based dimming
      this.voxelManager.highlightTilesWithObstruction(
        validTilesWithObstruction,
        "RED_OUTLINE"
      );
    }

    // Update combat state to refresh UI (show cancel button)
    this.updateCombatState().catch(console.error);

    console.log(`Entering targeting mode for skill: ${skillId}`);
  }

  /**
   * Handles skill targeting when in TARGETING_SKILL state.
   * Validates target and executes skill if valid.
   * @param {Position} targetPos - Target position
   */
  handleSkillTargeting(targetPos) {
    if (!this.turnSystem || !this.skillTargetingSystem || !this.activeSkillId) {
      return;
    }

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit || activeUnit.team !== "PLAYER") {
      return;
    }

    // Validate target
    const validation = this.skillTargetingSystem.validateTarget(
      activeUnit,
      targetPos,
      this.activeSkillId
    );

    if (validation.valid) {
      this.executeSkill(this.activeSkillId, targetPos);
    } else {
      // Audio: Error Buzz
      console.log(`Invalid target: ${validation.reason}`);
    }
  }

  /**
   * Executes a skill at the target position.
   * Deducts costs, processes effects, and cleans up.
   * @param {string} skillId - Skill ID
   * @param {Position} targetPos - Target position
   */
  async executeSkill(skillId, targetPos) {
    if (!this.turnSystem || !this.skillTargetingSystem) return;

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit) return;

    this.combatState = "EXECUTING_SKILL";

    // 1. Deduct Costs (AP, Cooldown)
    const skill = (activeUnit.actions || []).find((a) => a.id === skillId);
    if (skill) {
      activeUnit.currentAP -= skill.costAP || 0;
      if (skill.cooldown !== undefined) {
        skill.cooldown = (skill.cooldown || 0) + 1; // Set cooldown
      }
    }

    // 2. Get Targets (Units in AoE)
    let targets = this.skillTargetingSystem.getUnitsInAoE(
      activeUnit.position,
      targetPos,
      skillId
    );

    console.log(
      `AoE found ${targets.length} targets at ${targetPos.x},${targetPos.y},${targetPos.z}`
    );
    if (targets.length > 0) {
      targets.forEach((t) => {
        console.log(
          `  - Target: ${t.name} at ${t.position.x},${t.position.y},${t.position.z}`
        );
      });
    }

    // Fallback: If no targets found but there's a unit at the target position, include it
    // This handles cases where the AoE calculation might miss the exact target
    if (targets.length === 0 && this.grid) {
      const unitAtTarget = this.grid.getUnitAt(targetPos);
      if (unitAtTarget) {
        targets = [unitAtTarget];
        console.log(
          `Fallback: Added unit at target position: ${unitAtTarget.name}`
        );
      }
    }

    // 3. Process Effects using EffectProcessor
    const skillDef = this.skillTargetingSystem.getSkillDef(skillId);

    // Process ON_SKILL_CAST passive effects
    if (skillDef) {
      this.processPassiveItemEffects(activeUnit, "ON_SKILL_CAST", {
        skillId: skillId,
        skillDef: skillDef,
      });
    }

    if (skillDef && skillDef.effects && this.effectProcessor) {
      for (const effect of skillDef.effects) {
        // Special handling for TELEPORT - teleports the source unit, not targets
        if (effect.type === "TELEPORT") {
          // Check line of sight - if obstructed, teleport has a chance to fail
          const losResult = this.skillTargetingSystem.hasLineOfSight(
            activeUnit.position,
            targetPos,
            skillDef.ignore_cover || false
          );

          // Calculate failure chance based on obstruction level
          // Obstruction of 0 = 0% failure, obstruction of 1.0 = 100% failure
          const failureChance = losResult.obstruction || 0;
          if (Math.random() < failureChance) {
            console.warn(
              `${
                activeUnit.name
              }'s teleport failed due to obstructed line of sight! (${Math.round(
                failureChance * 100
              )}% obstruction)`
            );
            // Teleport failed - unit stays in place, but AP was already deducted
            // Could optionally refund AP here, but for now we'll just log the failure
            continue; // Skip teleport execution
          }

          // Find walkable Y level for target position
          let walkableY = targetPos.y;
          if (this.movementSystem) {
            const foundY = this.movementSystem.findWalkableY(
              targetPos.x,
              targetPos.z,
              targetPos.y
            );
            if (foundY !== null) {
              walkableY = foundY;
            } else {
              // No walkable position found - teleport fails
              console.warn(
                `${activeUnit.name}'s teleport failed: target position is not walkable`
              );
              continue; // Skip teleport execution
            }
          }
          const teleportDestination = {
            x: targetPos.x,
            y: walkableY,
            z: targetPos.z,
          };

          // Process teleport effect - source unit is teleported to destination
          const result = this.effectProcessor.process(
            effect,
            activeUnit,
            teleportDestination
          );

          if (result.success && result.data) {
            // Update unit mesh position after teleport
            const mesh = this.unitMeshes.get(activeUnit.id);
            if (mesh) {
              // Floor surface is at pos.y - 0.5, unit should be 0.6 above: pos.y + 0.1
              mesh.position.set(
                activeUnit.position.x,
                activeUnit.position.y + 0.1,
                activeUnit.position.z
              );
            }
            console.log(
              `Teleported ${activeUnit.name} to ${activeUnit.position.x},${activeUnit.position.y},${activeUnit.position.z}`
            );
          } else {
            console.warn(`Teleport failed: ${result.error || "Unknown error"}`);
          }
          continue; // Skip normal target processing for TELEPORT
        }

        // Process effect for all targets (for non-TELEPORT effects)
        for (const target of targets) {
          if (!target) continue;
          // Check if unit is alive
          if (typeof target.isAlive === "function" && !target.isAlive())
            continue;
          if (target.currentHealth <= 0) continue;

          // Process ON_SKILL_HIT passive effects (before processing effect)
          this.processPassiveItemEffects(activeUnit, "ON_SKILL_HIT", {
            skillId: skillId,
            skillDef: skillDef,
            target: target,
            effect: effect,
          });

          // Process effect through EffectProcessor
          const result = this.effectProcessor.process(
            effect,
            activeUnit,
            target
          );

          if (result.success) {
            // Log success messages based on effect type
            if (result.data) {
              if (result.data.type === "DAMAGE") {
                console.log(
                  `${activeUnit.name} dealt ${result.data.amount} damage to ${target.name} (${result.data.currentHP}/${target.maxHealth} HP)`
                );
                if (result.data.currentHP <= 0) {
                  console.log(`${target.name} has been defeated!`);
                  // Process ON_KILL passive effects (on source)
                  this.processPassiveItemEffects(activeUnit, "ON_KILL", {
                    target: target,
                    killedUnit: target,
                  });
                  // Handle unit death
                  this.handleUnitDeath(target);
                }
                // Process passive item effects for ON_DAMAGED trigger (on target)
                this.processPassiveItemEffects(target, "ON_DAMAGED", {
                  source: activeUnit,
                  damageAmount: result.data.amount,
                });
                // Process passive item effects for ON_DAMAGE_DEALT trigger (on source)
                this.processPassiveItemEffects(activeUnit, "ON_DAMAGE_DEALT", {
                  target: target,
                  damageAmount: result.data.amount,
                });
              } else if (result.data.type === "HEAL") {
                if (result.data.amount > 0) {
                  console.log(
                    `${activeUnit.name} healed ${target.name} for ${result.data.amount} HP (${result.data.currentHP}/${target.maxHealth} HP)`
                  );
                }
                // Process passive item effects for ON_HEAL_DEALT trigger (on source)
                this.processPassiveItemEffects(activeUnit, "ON_HEAL_DEALT", {
                  target: target,
                  healAmount: result.data.amount,
                });
              } else if (result.data.type === "APPLY_STATUS") {
                console.log(
                  `${activeUnit.name} applied ${result.data.statusId} to ${target.name} for ${result.data.duration} turns`
                );
              } else if (result.data.type === "CHAIN_DAMAGE") {
                // Log chain damage results
                if (result.data.results && result.data.results.length > 0) {
                  const primaryResult = result.data.results[0];
                  console.log(
                    `${activeUnit.name} dealt ${primaryResult.amount} damage to ${target.name} (chain lightning)`
                  );
                  if (
                    result.data.chainTargets &&
                    result.data.chainTargets.length > 0
                  ) {
                    console.log(
                      `Chain lightning bounced to ${result.data.chainTargets.length} additional targets`
                    );
                  }
                }
              }
            }
          } else {
            // Log warnings for failed effects (but don't block other effects)
            if (result.error && result.error !== "Conditions not met") {
              console.warn(`Effect ${effect.type} failed: ${result.error}`);
            }
          }
        }
      }
    }

    console.log(
      `Executed skill ${skillId} at ${targetPos.x},${targetPos.y},${targetPos.z}, hit ${targets.length} targets`
    );

    // 4. Cleanup
    this.combatState = "IDLE";
    this.activeSkillId = null;
    // Clear skill highlights
    if (this.voxelManager) {
      this.voxelManager.clearHighlights();
    }
    // Restore movement highlights if we have an active unit
    if (this.turnSystem) {
      const activeUnit = this.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team === "PLAYER") {
        this.updateMovementHighlights(activeUnit);
      }
    }

    // Update combat state
    this.updateCombatState().catch(console.error);
  }

  /**
   * Handles cursor hover to update AoE preview when targeting skills.
   * @param {THREE.Vector3} pos - Cursor position
   */
  onCursorHover(pos) {
    if (
      this.combatState === "TARGETING_SKILL" &&
      this.activeSkillId &&
      this.turnSystem &&
      this.skillTargetingSystem &&
      this.voxelManager
    ) {
      const activeUnit = this.turnSystem.getActiveUnit();
      if (activeUnit) {
        const cursorPos = { x: pos.x, y: pos.y, z: pos.z };
        const aoeTiles = this.skillTargetingSystem.getAoETiles(
          activeUnit.position,
          cursorPos,
          this.activeSkillId
        );

        // Show AoE reticle
        this.voxelManager.showReticle(aoeTiles);
      }
    }
  }

  /**
   * Cancels skill targeting and returns to IDLE state.
   */
  cancelSkillTargeting() {
    this.combatState = "IDLE";
    this.activeSkillId = null;
    // Clear skill highlights
    if (this.voxelManager) {
      this.voxelManager.clearHighlights();
    }
    // Restore movement highlights if we have an active unit
    if (this.turnSystem) {
      const activeUnit = this.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team === "PLAYER") {
        this.updateMovementHighlights(activeUnit);
      }
    }
    // Update combat state to refresh UI
    this.updateCombatState().catch(console.error);
  }

  /**
   * Handles movement button click from CombatHUD.
   * Returns to movement mode from skill targeting.
   */
  onMovementClicked() {
    if (!this.turnSystem) return;

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit || activeUnit.team !== "PLAYER") return;

    // If we're in skill targeting mode, cancel it and return to movement
    if (this.combatState === "TARGETING_SKILL") {
      this.cancelSkillTargeting();
    } else {
      // If already in movement mode, ensure movement highlights are shown
      this.updateMovementHighlights(activeUnit);
      // Update combat state to refresh UI
      this.updateCombatState().catch(console.error);
    }
  }

  /**
   * Starts a mission by ID.
   * @param {string} missionId - Mission identifier
   * @returns {Promise<void>}
   */
  async startMission(missionId) {
    const mission = await fetch(
      `assets/data/missions/${missionId.toLowerCase()}.json`
    );
    const missionData = await mission.json();
    this.missionManager.startMission(missionData);
  }

  /**
   * Starts a level with the given run data.
   * @param {RunData} runData - Run data containing mission and squad info
   * @param {Object} [options] - Optional configuration
   * @param {boolean} [options.startAnimation=true] - Whether to start the animation loop
   * @returns {Promise<void>}
   */
  async startLevel(runData, options = {}) {
    console.log("GameLoop: Generating Level...");
    this.runData = runData;
    this.isRunning = true;
    this.clearUnitMeshes();
    this.clearMovementHighlights();
    this.clearSpawnZoneHighlights();

    // Reset Deployment State
    this.deploymentState = {
      selectedUnitIndex: -1,
      deployedUnits: new Map(), // Map<Index, UnitInstance>
    };

    this.grid = new VoxelGrid(20, 10, 20);
    const generator = new RuinGenerator(this.grid, runData.seed);
    generator.generate();

    if (generator.generatedAssets.spawnZones) {
      this.playerSpawnZone = generator.generatedAssets.spawnZones.player || [];
      this.enemySpawnZone = generator.generatedAssets.spawnZones.enemy || [];
    }

    if (this.playerSpawnZone.length === 0)
      this.playerSpawnZone.push({ x: 2, y: 1, z: 2 });
    if (this.enemySpawnZone.length === 0)
      this.enemySpawnZone.push({ x: 18, y: 1, z: 18 });

    this.voxelManager = new VoxelManager(this.grid, this.scene);
    this.voxelManager.updateMaterials(generator.generatedAssets);
    this.voxelManager.update();

    // Set up highlight tracking sets
    this.voxelManager.setHighlightSets(this.rangeHighlights, this.aoeReticle);

    if (this.controls) this.voxelManager.focusCamera(this.controls);

    // Create a proper registry with actual class definitions
    const classRegistry = new Map();

    // Lazy-load class definitions
    const [vanguardDef, weaverDef, scavengerDef, tinkerDef, custodianDef] =
      await Promise.all([
        import("../assets/data/classes/vanguard.json", {
          with: { type: "json" },
        }).then((m) => m.default),
        import("../assets/data/classes/aether_weaver.json", {
          with: { type: "json" },
        }).then((m) => m.default),
        import("../assets/data/classes/scavenger.json", {
          with: { type: "json" },
        }).then((m) => m.default),
        import("../assets/data/classes/tinker.json", {
          with: { type: "json" },
        }).then((m) => m.default),
        import("../assets/data/classes/custodian.json", {
          with: { type: "json" },
        }).then((m) => m.default),
      ]);

    // Register all class definitions
    const classDefs = [
      vanguardDef,
      weaverDef,
      scavengerDef,
      tinkerDef,
      custodianDef,
    ];

    for (const classDef of classDefs) {
      if (classDef && classDef.id) {
        // Add type field for compatibility
        classRegistry.set(classDef.id, {
          ...classDef,
          type: "EXPLORER",
        });
      }
    }

    // Create registry object with get method for UnitManager
    const unitRegistry = {
      get: (id) => {
        // Try to get from class registry first
        if (classRegistry.has(id)) {
          return classRegistry.get(id);
        }

        // Fallback for enemy units
        if (id.startsWith("ENEMY_")) {
          return {
            type: "ENEMY",
            name: "Enemy",
            stats: { health: 50, attack: 8, defense: 3, speed: 8 },
            ai_archetype: "BRUISER",
          };
        }

        console.warn(`Unit definition not found: ${id}`);
        return null;
      },
    };

    this.unitManager = new UnitManager(unitRegistry);
    // Store classRegistry reference for accessing class definitions later
    this.classRegistry = classRegistry;

    // WIRING: Connect Systems to Data
    this.movementSystem.setContext(this.grid, this.unitManager);
    this.turnSystem.setContext(this.unitManager);
    // Initialize EffectProcessor with grid, unitManager, and optional RNG (using seed from runData)
    this.effectProcessor = new EffectProcessor(
      this.grid,
      this.unitManager,
      runData.seed ? new SeededRandom(runData.seed) : null
    );
    // Set hazard context for TurnSystem (for environmental hazard processing)
    this.turnSystem.setHazardContext(this.grid, this.effectProcessor);

    // Load skills and initialize SkillTargetingSystem
    // Skip skill loading in test mode (when startAnimation is false) to avoid fetch timeouts
    if (options.startAnimation !== false && skillRegistry.skills.size === 0) {
      await skillRegistry.loadAll();
    }
    this.skillTargetingSystem = new SkillTargetingSystem(
      this.grid,
      this.unitManager,
      skillRegistry
    );

    // Load items for InventoryManager
    if (options.startAnimation !== false && itemRegistry.items.size === 0) {
      await itemRegistry.loadAll();
    }

    // WIRING: Listen for Turn Changes (to update UI/Input state)
    // Create new AbortController for this level - when aborted, listeners are automatically removed
    this.turnSystemAbortController = new AbortController();
    const signal = this.turnSystemAbortController.signal;

    this.turnSystem.addEventListener(
      "turn-start",
      (e) => this._onTurnStart(e.detail),
      { signal }
    );
    this.turnSystem.addEventListener(
      "turn-end",
      (e) => this._onTurnEnd(e.detail),
      { signal }
    );
    this.turnSystem.addEventListener(
      "combat-start",
      () => this._onCombatStart(),
      { signal }
    );
    this.turnSystem.addEventListener("combat-end", () => this._onCombatEnd(), {
      signal,
    });

    this.highlightZones();

    if (this.playerSpawnZone.length > 0) {
      let sumX = 0,
        sumY = 0,
        sumZ = 0;
      for (const spot of this.playerSpawnZone) {
        sumX += spot.x;
        sumY += spot.y;
        sumZ += spot.z;
      }
      const centerX = sumX / this.playerSpawnZone.length;
      const centerY = sumY / this.playerSpawnZone.length;
      const centerZ = sumZ / this.playerSpawnZone.length;

      const start = this.playerSpawnZone[0];
      this.inputManager.setCursor(start.x, start.y, start.z);

      if (this.controls) {
        this.controls.target.set(centerX, centerY, centerZ);
        this.controls.update();
      }
    }

    this.inputManager.setValidator(this.validateDeploymentCursor.bind(this));

    // Only start animation loop if explicitly requested (default true for normal usage)
    if (options.startAnimation !== false) {
      this.animate();
    }
  }

  /**
   * Deploys or moves a unit to a target tile.
   * @param {import("./types.js").SquadMember} unitDef - Unit definition
   * @param {Position} targetTile - Target position
   * @param {Unit | null} [existingUnit] - Existing unit to move, or null to create new
   * @returns {Unit | null} - The deployed/moved unit, or null if failed
   */
  deployUnit(unitDef, targetTile, existingUnit = null) {
    if (
      !this.gameStateManager ||
      this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
    )
      return null;

    const isValid = this.validateDeploymentCursor(
      targetTile.x,
      targetTile.y,
      targetTile.z
    );

    // Check collision
    if (!isValid) {
      console.warn("Invalid spawn zone");
      return null;
    }

    // If tile occupied...
    if (this.grid.isOccupied(targetTile)) {
      // If occupied by SELF (clicking same spot), that's valid, just do nothing
      if (
        existingUnit &&
        existingUnit.position.x === targetTile.x &&
        existingUnit.position.z === targetTile.z
      ) {
        return existingUnit;
      }
      console.warn("Tile occupied");
      return null;
    }

    if (existingUnit) {
      // MOVE logic
      this.grid.moveUnit(existingUnit, targetTile, { force: true }); // Force to bypass standard move checks if any
      // Update Mesh
      const mesh = this.unitMeshes.get(existingUnit.id);
      if (mesh) {
        // Floor surface is at pos.y - 0.5, unit should be 0.6 above: pos.y + 0.1
        mesh.position.set(targetTile.x, targetTile.y + 0.1, targetTile.z);
      }
      console.log(
        `Moved ${existingUnit.name} to ${targetTile.x},${targetTile.y},${targetTile.z}`
      );
      return existingUnit;
    } else {
      // CREATE logic
      const classId = unitDef.classId || unitDef.id;
      const unit = this.unitManager.createUnit(classId, "PLAYER");

      if (!unit) {
        console.error(`Failed to create unit for class: ${classId}`);
        return null;
      }

      // Set character name and class name from unitDef
      if (unitDef.name) unit.name = unitDef.name;
      if (unitDef.className) unit.className = unitDef.className;

      // Restore progression data from roster for Explorers
      if (unit.type === "EXPLORER" && unitDef.id && this.gameStateManager) {
        const rosterUnit = this.gameStateManager.rosterManager.roster.find(
          (r) => r.id === unitDef.id
        );
        if (rosterUnit) {
          // Store roster ID on unit for later saving
          unit.rosterId = unitDef.id;

          // Restore activeClassId first (needed for stat recalculation)
          if (rosterUnit.activeClassId) {
            unit.activeClassId = rosterUnit.activeClassId;
          }

          // Restore classMastery progression
          if (rosterUnit.classMastery) {
            unit.classMastery = JSON.parse(
              JSON.stringify(rosterUnit.classMastery)
            );
            // Recalculate stats based on restored mastery and activeClassId
            if (unit.recalculateBaseStats && unit.activeClassId) {
              const classDef =
                typeof this.unitManager.registry.get === "function"
                  ? this.unitManager.registry.get(unit.activeClassId)
                  : this.unitManager.registry[unit.activeClassId];
              if (classDef) {
                unit.recalculateBaseStats(classDef);
              }
            }
          }

          // Restore currentHealth from roster (preserve HP that was paid for)
          if (
            rosterUnit.currentHealth !== undefined &&
            rosterUnit.currentHealth !== null
          ) {
            // Ensure currentHealth doesn't exceed maxHealth (in case maxHealth increased)
            unit.currentHealth = Math.min(
              rosterUnit.currentHealth,
              unit.maxHealth || 100
            );
            console.log(
              `Restored HP for ${unit.name}: ${unit.currentHealth}/${unit.maxHealth} (from roster)`
            );
          }
        }
      }

      // Preserve portrait/image from unitDef for UI display
      if (unitDef.image) {
        // Normalize path: ensure it starts with / if it doesn't already
        unit.portrait = unitDef.image.startsWith("/")
          ? unitDef.image
          : "/" + unitDef.image;
      } else if (unitDef.portrait) {
        unit.portrait = unitDef.portrait.startsWith("/")
          ? unitDef.portrait
          : "/" + unitDef.portrait;
      }

      // Initialize starting equipment for Explorers
      if (unit.type === "EXPLORER" && this.inventoryManager) {
        // Get class definition from the registry
        let classDef = null;
        if (this.unitManager.registry) {
          classDef =
            typeof this.unitManager.registry.get === "function"
              ? this.unitManager.registry.get(classId)
              : this.unitManager.registry[classId];
        }

        if (
          classDef &&
          typeof unit.initializeStartingEquipment === "function"
        ) {
          unit.initializeStartingEquipment(
            this.inventoryManager.itemRegistry,
            classDef
          );
        }
      }

      // Ensure unit has valid health values
      // Only set to full health if currentHealth is invalid (0 or negative) and wasn't restored from roster
      // This preserves HP that was paid for in the barracks
      if (
        unit.currentHealth <= 0 &&
        (!unit.rosterId ||
          !this.gameStateManager?.rosterManager?.roster.find(
            (r) => r.id === unit.rosterId
          )?.currentHealth)
      ) {
        // Only set to full if we didn't restore from roster (new unit or roster had no saved HP)
        unit.currentHealth = unit.maxHealth || unit.baseStats?.health || 100;
      }
      // Ensure maxHealth is set
      if (!unit.maxHealth) {
        unit.maxHealth = unit.baseStats?.health || 100;
      }

      this.grid.placeUnit(unit, targetTile);
      this.createUnitMesh(unit, targetTile);

      console.log(
        `Deployed ${unit.name} at ${targetTile.x},${targetTile.y},${targetTile.z}`
      );
      return unit;
    }
  }

  /**
   * Finalizes deployment phase and starts combat.
   */
  async finalizeDeployment() {
    if (
      !this.gameStateManager ||
      this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
    )
      return;

    // Get enemy spawns from mission definition
    const missionDef = await this.missionManager?.getActiveMission();
    const enemySpawns = missionDef?.enemy_spawns || [];

    // If no enemy_spawns defined, fall back to default behavior
    if (enemySpawns.length === 0) {
      console.warn("No enemy_spawns defined in mission, using default");
      const enemy = this.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
      if (enemy && this.enemySpawnZone.length > 0) {
        const spot = this.enemySpawnZone[0];
        const walkableY = this.movementSystem?.findWalkableY(
          spot.x,
          spot.z,
          spot.y
        );
        if (walkableY !== null) {
          const walkablePos = { x: spot.x, y: walkableY, z: spot.z };
          if (
            !this.grid.isOccupied(walkablePos) &&
            !this.grid.isSolid(walkablePos)
          ) {
            this.grid.placeUnit(enemy, walkablePos);
            this.createUnitMesh(enemy, walkablePos);
          }
        }
      }
    } else {
      // Spawn enemies according to mission definition
      let totalSpawned = 0;
      const availableSpots = [...this.enemySpawnZone]; // Copy to avoid mutating original

      for (const spawnDef of enemySpawns) {
        const { enemy_def_id, count } = spawnDef;
        let attempts = 0;
        const maxAttempts = availableSpots.length * 2;

        for (
          let i = 0;
          i < count && attempts < maxAttempts && availableSpots.length > 0;
          attempts++
        ) {
          const spotIndex = Math.floor(Math.random() * availableSpots.length);
          const spot = availableSpots[spotIndex];

          if (!spot) continue;

          // Check if position is walkable (not just unoccupied)
          const walkableY = this.movementSystem?.findWalkableY(
            spot.x,
            spot.z,
            spot.y
          );
          if (walkableY === null) continue;

          const walkablePos = { x: spot.x, y: walkableY, z: spot.z };

          // Check if position is not occupied and is walkable (not solid)
          if (
            !this.grid.isOccupied(walkablePos) &&
            !this.grid.isSolid(walkablePos)
          ) {
            const enemy = this.unitManager.createUnit(enemy_def_id, "ENEMY");
            if (enemy) {
              this.grid.placeUnit(enemy, walkablePos);
              this.createUnitMesh(enemy, walkablePos);
              availableSpots.splice(spotIndex, 1);
              totalSpawned++;
              i++; // Only increment if we successfully placed an enemy
            }
          }
        }
      }

      console.log(`Spawned ${totalSpawned} enemies from mission definition`);
    }

    // Switch to standard movement validator for the game
    this.inputManager.setValidator(this.validateCursorMove.bind(this));

    // Clear spawn zone highlights now that deployment is finished
    this.clearSpawnZoneHighlights();

    // Notify GameStateManager about state change
    if (this.gameStateManager) {
      this.gameStateManager.transitionTo("STATE_COMBAT");
    }

    // WIRING: Hand control to TurnSystem
    // Get units from UnitManager (which tracks all units including enemies just spawned)
    const allUnits = this.unitManager.getAllUnits();
    this.turnSystem.startCombat(allUnits);

    // WIRING: Set up MissionManager references
    if (this.missionManager) {
      this.missionManager.setUnitManager(this.unitManager);
      this.missionManager.setTurnSystem(this.turnSystem);
      await this.missionManager.setupActiveMission();
    }

    // WIRING: Listen for mission events
    this._setupMissionEventListeners();

    // Update combat state immediately so UI shows combat HUD
    this.updateCombatState().catch(console.error);

    console.log("Combat Started!");
  }

  /**
   * Initializes all units for combat with starting AP and charge.
   */
  initializeCombatUnits() {
    if (!this.grid) return;

    const allUnits = Array.from(this.grid.unitMap.values());

    allUnits.forEach((unit) => {
      // Set starting AP (default to 10, can be derived from stats later)
      const maxAP = 10; // TODO: Derive from unit stats

      // All units start with full AP when combat begins
      unit.currentAP = maxAP;

      // Initialize charge meter based on speed stat (faster units start with more charge)
      // Charge meter ranges from 0-100, speed-based units get a head start
      const speed = unit.baseStats?.speed || 10;
      // Scale speed (typically 5-20) to charge (0-100)
      // Faster units start closer to 100, slower units start lower
      unit.chargeMeter = Math.min(100, Math.max(0, speed * 5)); // Rough scaling: 10 speed = 50 charge
    });
  }

  /**
   * Clears all unit meshes from the scene.
   */
  clearUnitMeshes() {
    this.unitMeshes.forEach((mesh) => this.scene.remove(mesh));
    this.unitMeshes.clear();
  }

  /**
   * Clears all movement highlight meshes from the scene.
   */
  clearMovementHighlights() {
    this.movementHighlights.forEach((mesh) => {
      this.scene.remove(mesh);
      // Dispose geometry and material to free memory
      if (mesh.geometry) {
        // For LineSegments, geometry might be EdgesGeometry which wraps another geometry
        // Dispose the geometry itself
        mesh.geometry.dispose();
      }
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((mat) => {
            if (mat.map) mat.map.dispose();
            mat.dispose();
          });
        } else {
          if (mesh.material.map) mesh.material.map.dispose();
          mesh.material.dispose();
        }
      }
    });
    this.movementHighlights.clear();
  }

  /**
   * Updates movement highlights for the active player unit.
   * Uses MovementSystem to get reachable tiles.
   * @param {Unit | null} activeUnit - The active unit, or null to clear highlights
   */
  updateMovementHighlights(activeUnit) {
    // Clear existing highlights
    this.clearMovementHighlights();

    // Only show highlights for player units in combat
    if (
      !activeUnit ||
      activeUnit.team !== "PLAYER" ||
      !this.gameStateManager ||
      this.gameStateManager.currentState !== "STATE_COMBAT" ||
      !this.movementSystem
    ) {
      return;
    }

    // DELEGATE to MovementSystem
    const reachablePositions =
      this.movementSystem.getReachableTiles(activeUnit);

    // Create glowing blue outline materials with multiple layers for enhanced glow
    // Outer glow layers (fade outward, decreasing opacity)
    const outerGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x0066ff,
      transparent: true,
      opacity: 0.3,
    });

    const midGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x0088ff,
      transparent: true,
      opacity: 0.5,
    });

    // Inner bright outline (main glow - brightest)
    const highlightMaterial = new THREE.LineBasicMaterial({
      color: 0x00ccff, // Very bright cyan-blue for maximum visibility
      transparent: true,
      opacity: 1.0,
    });

    // Thick inner outline (for thickness simulation)
    const thickMaterial = new THREE.LineBasicMaterial({
      color: 0x00aaff,
      transparent: true,
      opacity: 0.8,
    });

    // Create base plane geometry for the tile
    const baseGeometry = new THREE.PlaneGeometry(1, 1);
    baseGeometry.rotateX(-Math.PI / 2);

    // Create highlight outlines for each reachable position
    reachablePositions.forEach((pos) => {
      // Get the correct floor surface height for this position
      const walkableY = this.movementSystem.findWalkableY(pos.x, pos.z, pos.y);
      if (walkableY === null) return; // Skip if no valid floor found

      // Floor surface is at the walkable Y coordinate (top of the floor block)
      // Adjust by -0.5 to account for voxel centering
      const floorSurfaceY = walkableY - 0.5;

      // Create multiple glow layers for enhanced visibility and fade effect
      // Outer glow (largest, most transparent)
      const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
      outerGlowGeometry.rotateX(-Math.PI / 2);
      const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
      const outerGlowLines = new THREE.LineSegments(
        outerGlowEdges,
        outerGlowMaterial
      );
      outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
      this.scene.add(outerGlowLines);
      this.movementHighlights.add(outerGlowLines);

      // Mid glow (medium size)
      const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
      midGlowGeometry.rotateX(-Math.PI / 2);
      const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
      const midGlowLines = new THREE.LineSegments(
        midGlowEdges,
        midGlowMaterial
      );
      midGlowLines.position.set(pos.x, floorSurfaceY + 0.002, pos.z);
      this.scene.add(midGlowLines);
      this.movementHighlights.add(midGlowLines);

      // Thick inner outline (slightly larger than base for thickness)
      const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
      thickGeometry.rotateX(-Math.PI / 2);
      const thickEdges = new THREE.EdgesGeometry(thickGeometry);
      const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
      thickLines.position.set(pos.x, floorSurfaceY + 0.001, pos.z);
      this.scene.add(thickLines);
      this.movementHighlights.add(thickLines);

      // Main bright outline (exact size, brightest)
      const edgesGeometry = new THREE.EdgesGeometry(baseGeometry);
      const lineSegments = new THREE.LineSegments(
        edgesGeometry,
        highlightMaterial
      );
      // Position exactly on floor surface
      lineSegments.position.set(pos.x, floorSurfaceY, pos.z);
      this.scene.add(lineSegments);
      this.movementHighlights.add(lineSegments);
    });
  }

  /**
   * Creates a visual mesh for a unit.
   * @param {Unit} unit - The unit instance
   * @param {Position} pos - Position to place the mesh
   */
  createUnitMesh(unit, pos) {
    const geometry = new THREE.BoxGeometry(0.6, 1.2, 0.6);

    // Class-based color mapping for player units
    const CLASS_COLORS = {
      CLASS_VANGUARD: 0xff3333, // Red - Tank
      CLASS_TINKER: 0xffaa00, // Orange - Tech/Mechanical
      CLASS_SCAVENGER: 0xaa33ff, // Purple - Rogue/Stealth
      CLASS_CUSTODIAN: 0x33ffaa, // Teal - Healer/Support
      CLASS_BATTLE_MAGE: 0x3333ff, // Blue - Magic Fighter
      CLASS_SAPPER: 0xff6600, // Dark Orange - Explosive
      CLASS_FIELD_ENGINEER: 0x00ffff, // Cyan - Tech Support
      CLASS_WEAVER: 0xff33ff, // Magenta - Magic (Aether Weaver)
      CLASS_AETHER_SENTINEL: 0x33aaff, // Light Blue - Defensive Magic
      CLASS_ARCANE_SCOURGE: 0x6600ff, // Dark Purple - Dark Magic
    };

    let color = 0xcccccc; // Default gray

    if (unit.team === "ENEMY") {
      color = 0x550000; // Dark red for enemies
    } else if (unit.team === "PLAYER") {
      // Get class ID from activeClassId (Explorer units) or extract from unit.id
      let classId = unit.activeClassId;

      // If no activeClassId, try to extract from unit.id (format: "CLASS_VANGUARD_0")
      if (!classId && unit.id.includes("CLASS_")) {
        const parts = unit.id.split("_");
        if (parts.length >= 2) {
          classId = parts[0] + "_" + parts[1];
        }
      }

      // Look up color by class ID
      if (classId && CLASS_COLORS[classId]) {
        color = CLASS_COLORS[classId];
      } else {
        // Fallback: check if unit.id contains any class name
        for (const className of Object.keys(CLASS_COLORS)) {
          const classShortName = className.replace("CLASS_", "");
          if (unit.id.includes(classShortName)) {
            color = CLASS_COLORS[className];
            break;
          }
        }
      }
    }

    const material = new THREE.MeshStandardMaterial({ color: color });
    const mesh = new THREE.Mesh(geometry, material);
    // Floor surface is at pos.y - 0.5 (floor block at pos.y-1, top at pos.y-0.5)
    // Unit should be 0.6 units above floor surface: (pos.y - 0.5) + 0.6 = pos.y + 0.1
    mesh.position.set(pos.x, pos.y + 0.1, pos.z);
    this.scene.add(mesh);
    this.unitMeshes.set(unit.id, mesh);
  }

  /**
   * Highlights spawn zones with visual indicators.
   * Uses multi-layer glow outline style similar to movement highlights.
   */
  highlightZones() {
    // Clear any existing spawn zone highlights
    this.clearSpawnZoneHighlights();

    // Player zone colors (green) - multi-layer glow
    const playerOuterGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x006600,
      transparent: true,
      opacity: 0.3,
    });

    const playerMidGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x008800,
      transparent: true,
      opacity: 0.5,
    });

    const playerHighlightMaterial = new THREE.LineBasicMaterial({
      color: 0x00ff00, // Bright green
      transparent: true,
      opacity: 1.0,
    });

    const playerThickMaterial = new THREE.LineBasicMaterial({
      color: 0x00cc00,
      transparent: true,
      opacity: 0.8,
    });

    // Enemy zone colors (red) - multi-layer glow
    const enemyOuterGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x660000,
      transparent: true,
      opacity: 0.3,
    });

    const enemyMidGlowMaterial = new THREE.LineBasicMaterial({
      color: 0x880000,
      transparent: true,
      opacity: 0.5,
    });

    const enemyHighlightMaterial = new THREE.LineBasicMaterial({
      color: 0xff0000, // Bright red
      transparent: true,
      opacity: 1.0,
    });

    const enemyThickMaterial = new THREE.LineBasicMaterial({
      color: 0xcc0000,
      transparent: true,
      opacity: 0.8,
    });

    // Create base plane geometry for the tile
    const baseGeometry = new THREE.PlaneGeometry(1, 1);
    baseGeometry.rotateX(-Math.PI / 2);

    // Helper function to create multi-layer highlights for a position
    const createHighlights = (pos, materials) => {
      const { outerGlow, midGlow, highlight, thick } = materials;

      // Find walkable Y level (similar to movement highlights)
      let walkableY = pos.y;
      if (this.grid && this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
        for (let checkY = pos.y; checkY >= 0; checkY--) {
          if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
            walkableY = checkY;
            break;
          }
        }
      }

      const floorSurfaceY = walkableY - 0.5;

      // Outer glow (largest, most transparent)
      const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
      outerGlowGeometry.rotateX(-Math.PI / 2);
      const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
      const outerGlowLines = new THREE.LineSegments(outerGlowEdges, outerGlow);
      outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
      this.scene.add(outerGlowLines);
      this.spawnZoneHighlights.add(outerGlowLines);

      // Mid glow (medium size)
      const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
      midGlowGeometry.rotateX(-Math.PI / 2);
      const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
      const midGlowLines = new THREE.LineSegments(midGlowEdges, midGlow);
      midGlowLines.position.set(pos.x, floorSurfaceY + 0.002, pos.z);
      this.scene.add(midGlowLines);
      this.spawnZoneHighlights.add(midGlowLines);

      // Thick inner outline (slightly larger than base for thickness)
      const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
      thickGeometry.rotateX(-Math.PI / 2);
      const thickEdges = new THREE.EdgesGeometry(thickGeometry);
      const thickLines = new THREE.LineSegments(thickEdges, thick);
      thickLines.position.set(pos.x, floorSurfaceY + 0.001, pos.z);
      this.scene.add(thickLines);
      this.spawnZoneHighlights.add(thickLines);

      // Main bright outline (exact size, brightest)
      const edgesGeometry = new THREE.EdgesGeometry(baseGeometry);
      const lineSegments = new THREE.LineSegments(edgesGeometry, highlight);
      lineSegments.position.set(pos.x, floorSurfaceY, pos.z);
      this.scene.add(lineSegments);
      this.spawnZoneHighlights.add(lineSegments);
    };

    // Create highlights for player spawn zone (green)
    const playerMaterials = {
      outerGlow: playerOuterGlowMaterial,
      midGlow: playerMidGlowMaterial,
      highlight: playerHighlightMaterial,
      thick: playerThickMaterial,
    };
    this.playerSpawnZone.forEach((pos) => {
      createHighlights(pos, playerMaterials);
    });

    // Create highlights for enemy spawn zone (red)
    const enemyMaterials = {
      outerGlow: enemyOuterGlowMaterial,
      midGlow: enemyMidGlowMaterial,
      highlight: enemyHighlightMaterial,
      thick: enemyThickMaterial,
    };
    this.enemySpawnZone.forEach((pos) => {
      createHighlights(pos, enemyMaterials);
    });
  }

  /**
   * Clears all spawn zone highlight meshes from the scene.
   */
  clearSpawnZoneHighlights() {
    this.spawnZoneHighlights.forEach((mesh) => {
      this.scene.remove(mesh);
      // Dispose geometry and material to free memory
      if (mesh.geometry) {
        mesh.geometry.dispose();
      }
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((mat) => mat.dispose());
        } else {
          mesh.material.dispose();
        }
      }
    });
    this.spawnZoneHighlights.clear();
  }

  /**
   * Main animation loop.
   */
  animate() {
    if (!this.isRunning) return;
    requestAnimationFrame(this.animate);

    if (this.inputManager) this.inputManager.update();
    if (this.controls) this.controls.update();

    const now = Date.now();
    if (now - this.lastMoveTime > this.moveCooldown) {
      let dx = 0;
      let dz = 0;

      if (
        this.inputManager.isKeyPressed("KeyW") ||
        this.inputManager.isKeyPressed("ArrowUp")
      )
        dz = -1;
      if (
        this.inputManager.isKeyPressed("KeyS") ||
        this.inputManager.isKeyPressed("ArrowDown")
      )
        dz = 1;
      if (
        this.inputManager.isKeyPressed("KeyA") ||
        this.inputManager.isKeyPressed("ArrowLeft")
      )
        dx = -1;
      if (
        this.inputManager.isKeyPressed("KeyD") ||
        this.inputManager.isKeyPressed("ArrowRight")
      )
        dx = 1;

      if (dx !== 0 || dz !== 0) {
        const currentPos = this.inputManager.getCursorPosition();
        const newX = currentPos.x + dx;
        const newZ = currentPos.z + dz;

        this.inputManager.setCursor(newX, currentPos.y, newZ);
        this.lastMoveTime = now;
      }
    }

    const time = Date.now() * 0.002;
    this.unitMeshes.forEach((mesh) => {
      mesh.position.y += Math.sin(time) * 0.002;
    });

    this.renderer.render(this.scene, this.camera);
  }

  /**
   * Pauses the game loop (temporarily stops animation).
   * Can be resumed with resume().
   */
  pause() {
    this.isPaused = true;
    this.isRunning = false;
  }

  /**
   * Resumes the game loop after being paused.
   */
  resume() {
    if (this.isPaused) {
      this.isPaused = false;
      this.isRunning = true;
      this.animate();
    }
  }

  /**
   * Stops the game loop and cleans up resources.
   */
  stop() {
    this.isRunning = false;
    this.isPaused = false;

    // Abort turn system event listeners (automatically removes them via signal)
    if (this.turnSystemAbortController) {
      this.turnSystemAbortController.abort();
      this.turnSystemAbortController = null;
    }

    // Reset turn system state BEFORE ending combat to prevent event cascades
    if (this.turnSystem) {
      // End combat first to stop any ongoing turn advancement
      if (
        this.turnSystem.phase !== "INIT" &&
        this.turnSystem.phase !== "COMBAT_END"
      ) {
        try {
          this.turnSystem.endCombat();
        } catch (e) {
          // Ignore errors
        }
      }

      // Then reset
      if (typeof this.turnSystem.reset === "function") {
        this.turnSystem.reset();
      }
    }

    if (this.inputManager && typeof this.inputManager.detach === "function") {
      this.inputManager.detach();
    }
    if (this.controls) this.controls.dispose();
  }

  /**
   * Updates the combat state in GameStateManager.
   * Called when combat starts or when combat state changes (turn changes, etc.)
   * Uses TurnSystem to get the spec-compliant CombatState, then enriches it for UI.
   */
  async updateCombatState() {
    if (!this.gameStateManager || !this.turnSystem) {
      return;
    }

    // Get spec-compliant combat state from TurnSystem
    const turnSystemState = this.turnSystem.getCombatState();

    if (!turnSystemState.isActive) {
      // Combat not active, clear state
      this.gameStateManager.setCombatState(null);
      return;
    }

    // Get active unit for UI enrichment
    const activeUnit = this.turnSystem.getActiveUnit();

    // Build active unit status if we have an active unit (for UI)
    let unitStatus = null;
    if (activeUnit) {
      // Calculate effective speed (including equipment and skill tree bonuses)
      let effectiveSpeed = activeUnit.baseStats?.speed || 10;

      // Add equipment bonuses if available
      if (activeUnit.loadout && this.inventoryManager) {
        const loadoutSlots = ["mainHand", "offHand", "body", "accessory"];
        for (const slot of loadoutSlots) {
          const itemInstance = activeUnit.loadout[slot];
          if (itemInstance) {
            const itemDef = this.inventoryManager.itemRegistry?.get(
              itemInstance.defId
            );
            if (itemDef && itemDef.stats && itemDef.stats.speed) {
              effectiveSpeed += itemDef.stats.speed;
            }
          }
        }
      }

      // Calculate max AP using formula: 3 + floor(effectiveSpeed/5)
      // We'll add skill tree bonuses to speed below when we generate the skill tree
      let maxAP = 3 + Math.floor(effectiveSpeed / 5);

      // Convert status effects to status icons
      const statuses = (activeUnit.statusEffects || []).map((effect) => ({
        id: effect.id || "unknown",
        icon: effect.icon || "❓",
        turnsRemaining: effect.duration || 0,
        description: effect.description || effect.name || "Status Effect",
      }));

      // Build skills from unit's actions
      const skills = (activeUnit.actions || []).map((action, index) => ({
        id: action.id || `skill_${index}`,
        name: action.name || "Unknown Skill",
        icon: action.icon || "⚔",
        costAP: action.costAP || 0,
        cooldown: action.cooldown || 0,
        isAvailable:
          activeUnit.currentAP >= (action.costAP || 0) &&
          (action.cooldown || 0) === 0,
      }));

      // Add unlocked skill tree skills for Explorer units
      if (
        (activeUnit.type === "EXPLORER" ||
          activeUnit.constructor?.name === "Explorer") &&
        activeUnit.activeClassId &&
        activeUnit.classMastery &&
        this.classRegistry
      ) {
        const mastery = activeUnit.classMastery[activeUnit.activeClassId];
        if (
          mastery &&
          mastery.unlockedNodes &&
          mastery.unlockedNodes.length > 0
        ) {
          try {
            // Get class definition
            const classDef = this.classRegistry.get(activeUnit.activeClassId);
            if (classDef && classDef.skillTreeData) {
              // Generate skill tree (similar to index.js)
              // We'll need to import SkillTreeFactory dynamically or store it
              // For now, let's try to get the skill tree from the skill registry
              const { SkillTreeFactory } = await import(
                "../factories/SkillTreeFactory.js"
              );

              // Load skill tree template (use cache if available)
              let template = this._skillTreeTemplate;
              if (!template) {
                const templateResponse = await fetch(
                  "assets/data/skill_trees/template_standard_30.json"
                );
                if (templateResponse.ok) {
                  template = await templateResponse.json();
                  this._skillTreeTemplate = template; // Cache it
                }
              }

              if (template) {
                const templateRegistry = { [template.id]: template };

                // Convert skillRegistry Map to object for SkillTreeFactory
                const skillMap = Object.fromEntries(skillRegistry.skills);

                // Create factory and generate tree
                const factory = new SkillTreeFactory(
                  templateRegistry,
                  skillMap
                );
                const skillTree = factory.createTree(classDef);

                // Add speed boosts from unlocked nodes to effective speed
                for (const nodeId of mastery.unlockedNodes) {
                  const nodeDef = skillTree.nodes?.[nodeId];
                  if (
                    nodeDef &&
                    nodeDef.type === "STAT_BOOST" &&
                    nodeDef.data &&
                    nodeDef.data.stat === "speed"
                  ) {
                    effectiveSpeed += nodeDef.data.value || 0;
                  }
                }

                // Recalculate maxAP with skill tree bonuses
                maxAP = 3 + Math.floor(effectiveSpeed / 5);

                // Add unlocked ACTIVE_SKILL nodes to skills array
                for (const nodeId of mastery.unlockedNodes) {
                  const nodeDef = skillTree.nodes?.[nodeId];
                  if (
                    nodeDef &&
                    nodeDef.type === "ACTIVE_SKILL" &&
                    nodeDef.data
                  ) {
                    const skillData = nodeDef.data;
                    const skillId = skillData.id || nodeId;

                    // Get full skill definition from registry if available
                    const fullSkill = skillRegistry.skills.get(skillId);

                    // Add skill to skills array (avoid duplicates)
                    if (!skills.find((s) => s.id === skillId)) {
                      // Get costAP from full skill definition
                      const costAP =
                        fullSkill?.costs?.ap || skillData.costAP || 3;
                      const baseCooldown =
                        fullSkill?.cooldown_turns || skillData.cooldown || 0;

                      // Ensure skill exists in unit.actions for cooldown tracking
                      if (!activeUnit.actions) {
                        activeUnit.actions = [];
                      }
                      let existingAction = activeUnit.actions.find(
                        (a) => a.id === skillId
                      );

                      // If action doesn't exist, create it with cooldown 0 (ready to use immediately)
                      if (!existingAction) {
                        existingAction = {
                          id: skillId,
                          name:
                            skillData.name ||
                            fullSkill?.name ||
                            "Unknown Skill",
                          icon: skillData.icon || fullSkill?.icon || "⚔",
                          costAP: costAP,
                          cooldown: 0, // Newly unlocked skills start ready to use
                        };
                        activeUnit.actions.push(existingAction);
                      }

                      // Use current cooldown from the action (which gets decremented by TurnSystem)
                      const currentCooldown = existingAction.cooldown || 0;

                      skills.push({
                        id: skillId,
                        name: existingAction.name,
                        icon: existingAction.icon,
                        costAP: costAP,
                        cooldown: currentCooldown,
                        isAvailable:
                          activeUnit.currentAP >= costAP &&
                          currentCooldown === 0,
                      });
                    }
                  }
                }
              }
            }
          } catch (error) {
            console.warn("Failed to load skill tree for combat HUD:", error);
          }
        }
      }

      // If no skills from actions or skill tree, provide a default attack skill
      if (skills.length === 0) {
        skills.push({
          id: "attack",
          name: "Attack",
          icon: "⚔",
          costAP: 3,
          cooldown: 0,
          isAvailable: activeUnit.currentAP >= 3,
        });
      }

      // Get portrait for active unit (same logic as enrichedQueue)
      let activePortrait = activeUnit.portrait || activeUnit.image;

      // If no portrait and it's a player unit, try to look up by classId
      if (
        !activePortrait &&
        activeUnit.team === "PLAYER" &&
        activeUnit.activeClassId
      ) {
        const CLASS_PORTRAITS = {
          CLASS_VANGUARD: "/assets/images/portraits/vanguard.png",
          CLASS_WEAVER: "/assets/images/portraits/weaver.png",
          CLASS_SCAVENGER: "/assets/images/portraits/scavenger.png",
          CLASS_TINKER: "/assets/images/portraits/tinker.png",
          CLASS_CUSTODIAN: "/assets/images/portraits/custodian.png",
        };
        activePortrait = CLASS_PORTRAITS[activeUnit.activeClassId];
      }

      // Normalize path: ensure it starts with / if it doesn't already
      if (activePortrait && !activePortrait.startsWith("/")) {
        activePortrait = "/" + activePortrait;
      }

      // Fallback to default portraits
      if (!activePortrait) {
        activePortrait =
          activeUnit.team === "PLAYER"
            ? "/assets/images/portraits/default.png"
            : "/assets/images/portraits/enemy.png";
      }

      unitStatus = {
        id: activeUnit.id,
        name: activeUnit.name,
        portrait: activePortrait,
        hp: {
          current: activeUnit.currentHealth,
          max: activeUnit.maxHealth,
        },
        ap: {
          current: activeUnit.currentAP,
          max: maxAP,
        },
        charge: activeUnit.chargeMeter || 0,
        statuses: statuses,
        skills: skills,
      };
    }

    // Build enriched turn queue for UI (with portraits, etc.)
    const enrichedQueue = turnSystemState.turnQueue
      .map((unitId) => {
        const unit = this.unitManager?.activeUnits.get(unitId);
        if (!unit) return null;

        // Try to get portrait from unit property (portrait or image)
        let portrait = unit.portrait || unit.image;

        // If no portrait and it's a player unit, try to look up by classId
        if (!portrait && unit.team === "PLAYER" && unit.activeClassId) {
          // Map of class IDs to portrait paths (matching team-builder CLASS_METADATA)
          const CLASS_PORTRAITS = {
            CLASS_VANGUARD: "/assets/images/portraits/vanguard.png",
            CLASS_WEAVER: "/assets/images/portraits/weaver.png",
            CLASS_SCAVENGER: "/assets/images/portraits/scavenger.png",
            CLASS_TINKER: "/assets/images/portraits/tinker.png",
            CLASS_CUSTODIAN: "/assets/images/portraits/custodian.png",
          };
          portrait = CLASS_PORTRAITS[unit.activeClassId];
        }

        // Normalize path: ensure it starts with / if it doesn't already
        if (portrait && !portrait.startsWith("/")) {
          portrait = "/" + portrait;
        }

        // Fallback to default portraits
        if (!portrait) {
          portrait =
            unit.team === "PLAYER"
              ? "/assets/images/portraits/default.png"
              : "/assets/images/portraits/enemy.png";
        }

        return {
          unitId: unit.id,
          portrait: portrait,
          team: unit.team || "ENEMY",
          initiative: unit.chargeMeter || 0,
        };
      })
      .filter((entry) => entry !== null);

    // Build combat state (enriched for UI, but includes spec fields)
    const combatState = {
      // Spec-compliant fields
      isActive: turnSystemState.isActive,
      round: turnSystemState.round,
      turnQueue: turnSystemState.turnQueue, // string[] as per spec
      activeUnitId: turnSystemState.activeUnitId, // string as per spec
      phase: turnSystemState.phase,

      // UI-enriched fields (for backward compatibility)
      activeUnit: unitStatus, // Object for UI
      enrichedQueue: enrichedQueue, // Objects for UI display
      targetingMode: this.combatState === "TARGETING_SKILL", // True when player is targeting a skill
      activeSkillId: this.activeSkillId || null, // ID of the skill being targeted (for UI toggle state)
      roundNumber: turnSystemState.round, // Alias for UI
    };

    // Update GameStateManager
    this.gameStateManager.setCombatState(combatState);
  }

  /**
   * Ends the current unit's turn and advances the turn queue.
   * Delegates to TurnSystem.
   */
  endTurn() {
    if (!this.turnSystem) {
      return;
    }

    const activeUnit = this.turnSystem.getActiveUnit();
    if (!activeUnit) {
      return;
    }

    // Clear any active skill targeting state and highlights
    if (this.combatState === "TARGETING_SKILL" || this.activeSkillId) {
      this.combatState = "IDLE";
      this.activeSkillId = null;
      // Clear skill targeting highlights (range and AoE reticle)
      if (this.voxelManager) {
        this.voxelManager.clearHighlights();
      }
    }

    // Clear movement highlights
    this.clearMovementHighlights();

    // DELEGATE to TurnSystem
    this.turnSystem.endTurn(activeUnit);

    // Update combat state (TurnSystem will have advanced to next unit)
    this.updateCombatState().catch(console.error);

    // If the next unit is an enemy, trigger AI turn
    const nextUnit = this.turnSystem.getActiveUnit();
    if (nextUnit && nextUnit.team === "ENEMY") {
      // TODO: Trigger AI turn
      console.log(`Enemy ${nextUnit.name}'s turn`);
      // For now, auto-end enemy turns after a delay
      setTimeout(() => {
        this.endTurn();
      }, 1000);
    }
  }

  /**
   * Event handler for turn-start event from TurnSystem.
   * @param {{ unitId: string; unit: Unit }} detail - Turn start event detail
   * @private
   */
  _onTurnStart(detail) {
    const { unit } = detail;
    // Update movement highlights if it's a player's turn
    if (unit.team === "PLAYER") {
      this.updateMovementHighlights(unit);
    } else {
      this.clearMovementHighlights();
    }
  }

  /**
   * Event handler for turn-end event from TurnSystem.
   * @param {{ unitId: string; unit: Unit }} detail - Turn end event detail
   * @private
   */
  _onTurnEnd(detail) {
    // Clear movement highlights when turn ends
    this.clearMovementHighlights();

    // Dispatch TURN_END event to MissionManager
    if (this.missionManager && this.turnSystem) {
      const currentTurn = this.turnSystem.round || 0;
      this.missionManager.updateTurn(currentTurn);
      this.missionManager.onGameEvent("TURN_END", { turn: currentTurn });
    }
  }

  /**
   * Event handler for combat-start event from TurnSystem.
   * @private
   */
  _onCombatStart() {
    // Combat has started
    console.log("TurnSystem: Combat started");
  }

  /**
   * Processes passive item effects for a unit when a trigger event occurs.
   * Integration Point 2: EventSystem for Passive Items
   * @param {Unit} unit - Unit whose items should be checked
   * @param {string} trigger - Trigger event type (e.g., "ON_DAMAGED", "ON_DAMAGE_DEALT", "ON_HEAL_DEALT")
   * @param {Object} context - Event context (source, target, damageAmount, etc.)
   * @private
   */
  processPassiveItemEffects(unit, trigger, context = {}) {
    if (
      !unit ||
      !unit.loadout ||
      !this.effectProcessor ||
      !this.inventoryManager
    ) {
      return;
    }

    // Get all equipped items from loadout
    const equippedItems = [
      unit.loadout.mainHand,
      unit.loadout.offHand,
      unit.loadout.body,
      unit.loadout.accessory,
      ...(unit.loadout.belt || []),
    ].filter(Boolean); // Remove nulls

    // Process each equipped item's passive effects
    for (const itemInstance of equippedItems) {
      if (!itemInstance || !itemInstance.defId) continue;

      const itemDef = this.inventoryManager.itemRegistry.get(
        itemInstance.defId
      );
      if (!itemDef || !itemDef.passives) continue;

      // Check each passive effect
      for (const passive of itemDef.passives) {
        if (passive.trigger !== trigger) continue;

        // Check conditions before processing
        if (!this._checkPassiveConditions(passive, context)) continue;

        // Check chance if present
        if (passive.chance !== undefined) {
          if (!this.rng || Math.random() > passive.chance) continue;
        }

        // Convert passive effect to EffectDefinition format
        const effectDef = this._convertPassiveToEffectDef(passive, context);
        if (!effectDef) continue;

        // Determine target based on passive action
        let target = context.target || context.source || unit;
        if (
          passive.params &&
          passive.params.target === "SOURCE" &&
          context.source
        ) {
          target = context.source;
        } else if (passive.params && passive.params.target === "SELF") {
          target = unit;
        }

        // Process the effect through EffectProcessor
        const result = this.effectProcessor.process(effectDef, unit, target);
        if (result.success && result.data) {
          console.log(
            `Passive effect ${passive.id || "unknown"} triggered on ${
              unit.name
            } (${trigger})`
          );
        }
      }
    }
  }

  /**
   * Converts a passive effect definition to an EffectDefinition format.
   * @param {Object} passive - Passive effect definition
   * @param {Object} context - Event context
   * @returns {Object | null} - EffectDefinition or null if conversion not supported
   * @private
   */
  _convertPassiveToEffectDef(passive, context) {
    if (!passive.action || !passive.params) return null;

    // Map passive actions to EffectProcessor effect types
    const actionMap = {
      DAMAGE: "DAMAGE",
      APPLY_STATUS: "APPLY_STATUS",
      HEAL: "HEAL",
      CHAIN_DAMAGE: "CHAIN_DAMAGE",
      GIVE_AP: "GIVE_AP",
      HEAL_SELF: "HEAL_SELF",
      APPLY_BUFF: "APPLY_BUFF",
    };

    const effectType = actionMap[passive.action];
    if (!effectType) return null; // Unsupported action

    const effectDef = {
      type: effectType,
    };

    // Copy relevant params
    if (passive.params.power !== undefined) {
      effectDef.power = passive.params.power;
    }
    if (passive.params.element) {
      effectDef.element = passive.params.element;
    }
    if (passive.params.status_id) {
      effectDef.status_id = passive.params.status_id;
    }
    if (passive.params.duration !== undefined) {
      effectDef.duration = passive.params.duration;
    }
    if (passive.params.chance !== undefined) {
      effectDef.chance = passive.params.chance;
    }
    // CHAIN_DAMAGE specific params
    if (passive.params.bounces !== undefined) {
      effectDef.bounces = passive.params.bounces;
    }
    if (passive.params.chainRange !== undefined) {
      effectDef.chainRange = passive.params.chainRange;
    }
    if (passive.params.decay !== undefined) {
      effectDef.decay = passive.params.decay;
    }
    if (passive.params.synergy_trigger) {
      effectDef.synergy_trigger = passive.params.synergy_trigger;
    }
    if (passive.condition) {
      effectDef.condition = passive.condition;
    }

    // GIVE_AP specific params
    if (passive.params.amount !== undefined) {
      effectDef.amount = passive.params.amount;
    }

    // HEAL_SELF specific params
    if (passive.params.percentage !== undefined) {
      effectDef.percentage = passive.params.percentage;
    }
    if (context.healAmount !== undefined) {
      effectDef.healAmount = context.healAmount;
    }

    // APPLY_BUFF specific params
    if (passive.params.stat !== undefined) {
      effectDef.stat = passive.params.stat;
    }
    if (passive.params.value !== undefined) {
      effectDef.value = passive.params.value;
    }

    return effectDef;
  }

  /**
   * Checks if passive effect conditions are met.
   * @param {Object} passive - Passive effect definition
   * @param {Object} context - Event context
   * @returns {boolean} - True if conditions are met
   * @private
   */
  _checkPassiveConditions(passive, context) {
    if (!passive.condition) return true; // No conditions means always execute

    const condition = passive.condition;

    // Check SKILL_TAG condition
    if (condition.type === "SKILL_TAG" && condition.value) {
      const skillDef = context.skillDef;
      if (!skillDef || !skillDef.tags) return false;
      if (!skillDef.tags.includes(condition.value)) return false;
    }

    // Check DAMAGE_TYPE condition
    if (condition.type === "DAMAGE_TYPE" && condition.value) {
      const effect = context.effect;
      if (!effect || effect.element !== condition.value) return false;
    }

    // Check TARGET_TAG condition
    if (condition.type === "TARGET_TAG" && condition.value) {
      const target = context.target;
      if (!target || !target.tags) return false;
      if (!target.tags.includes(condition.value)) return false;
    }

    // Check SOURCE_IS_ADJACENT condition
    if (condition.type === "SOURCE_IS_ADJACENT") {
      const source = context.source;
      const target = context.target || context.unit;
      if (!source || !target || !source.position || !target.position)
        return false;
      const dist =
        Math.abs(source.position.x - target.position.x) +
        Math.abs(source.position.y - target.position.y) +
        Math.abs(source.position.z - target.position.z);
      if (dist > 1) return false; // Not adjacent (Manhattan distance > 1)
    }

    // Check IS_ADJACENT condition
    if (condition.type === "IS_ADJACENT") {
      const source = context.source || context.unit;
      const target = context.target;
      if (!source || !target || !source.position || !target.position)
        return false;
      const dist =
        Math.abs(source.position.x - target.position.x) +
        Math.abs(source.position.y - target.position.y) +
        Math.abs(source.position.z - target.position.z);
      if (dist > 1) return false; // Not adjacent
    }

    // Check DID_NOT_ATTACK condition (for ON_TURN_END)
    if (condition.type === "DID_NOT_ATTACK") {
      // This would need to track if the unit attacked this turn
      // For now, we'll assume it's tracked in context
      if (context.didAttack === true) return false;
    }

    return true; // All conditions passed
  }

  /**
   * Handles unit death: removes from grid, dispatches events, and updates MissionManager.
   * @param {Unit} unit - The unit that died
   */
  handleUnitDeath(unit) {
    if (!unit || !this.grid || !this.unitManager) return;

    // Remove unit from grid
    if (unit.position) {
      this.grid.removeUnit(unit.position);
    }

    // Remove unit from UnitManager
    this.unitManager.removeUnit(unit.id);

    // Remove unit mesh from scene
    const mesh = this.unitMeshes.get(unit.id);
    if (mesh) {
      this.scene.remove(mesh);
      this.unitMeshes.delete(unit.id);
      // Dispose geometry and material
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) {
        if (Array.isArray(mesh.material)) {
          mesh.material.forEach((mat) => {
            if (mat.map) mat.map.dispose();
            mat.dispose();
          });
        } else {
          if (mesh.material.map) mesh.material.map.dispose();
          mesh.material.dispose();
        }
      }
    }

    // Dispatch death event to MissionManager
    if (this.missionManager) {
      const eventType = unit.team === "ENEMY" ? "ENEMY_DEATH" : "PLAYER_DEATH";
      const unitDefId = unit.defId || unit.id;
      this.missionManager.onGameEvent(eventType, {
        unitId: unit.id,
        defId: unitDefId,
        team: unit.team,
      });
    }

    console.log(`${unit.name} (${unit.team}) has been removed from combat.`);
  }

  /**
   * Sets up event listeners for mission victory and failure.
   * @private
   */
  _setupMissionEventListeners() {
    // Listen for mission victory
    window.addEventListener("mission-victory", (event) => {
      this._handleMissionVictory(event.detail);
    });

    // Listen for mission failure
    window.addEventListener("mission-failure", (event) => {
      this._handleMissionFailure(event.detail);
    });
  }

  /**
   * Handles mission victory.
   * @param {Object} detail - Victory event detail
   * @private
   */
  _handleMissionVictory(detail) {
    console.log("Mission Victory!", detail);

    // Save Explorer progression back to roster
    this._saveExplorerProgression();

    // Pause the game
    this.isPaused = true;

    // Stop the game loop
    this.stop();

    // Clear the active run from persistence since mission is complete
    if (this.gameStateManager) {
      this.gameStateManager.clearActiveRun();
    }

    // Wait for the outro narrative to complete before transitioning
    // The outro is played in MissionManager.completeActiveMission()
    // We'll listen for the narrative-end event to know when it's done
    const hasOutro =
      this.gameStateManager?.missionManager?.currentMissionDef?.narrative
        ?.outro_success;

    if (hasOutro) {
      console.log("GameLoop: Waiting for outro narrative to complete...");
      const handleNarrativeEnd = () => {
        console.log(
          "GameLoop: Narrative end event received, transitioning to hub"
        );
        narrativeManager.removeEventListener(
          "narrative-end",
          handleNarrativeEnd
        );

        // Small delay after narrative ends to let user see the final message
        setTimeout(() => {
          if (this.gameStateManager) {
            this.gameStateManager.transitionTo("STATE_MAIN_MENU");
          }
        }, 500);
      };

      narrativeManager.addEventListener("narrative-end", handleNarrativeEnd);

      // Fallback timeout: if narrative doesn't end within 30 seconds, transition anyway
      setTimeout(() => {
        console.warn(
          "GameLoop: Narrative end timeout - transitioning to hub anyway"
        );
        narrativeManager.removeEventListener(
          "narrative-end",
          handleNarrativeEnd
        );
        if (this.gameStateManager) {
          this.gameStateManager.transitionTo("STATE_MAIN_MENU");
        }
      }, 30000);
    } else {
      // No outro, transition immediately after a short delay
      console.log("GameLoop: No outro narrative, transitioning to hub");
      setTimeout(() => {
        if (this.gameStateManager) {
          this.gameStateManager.transitionTo("STATE_MAIN_MENU");
        }
      }, 1000);
    }
  }

  /**
   * Saves Explorer progression (classMastery, activeClassId) back to roster.
   * @private
   */
  _saveExplorerProgression() {
    if (!this.unitManager || !this.gameStateManager) return;

    const playerUnits = Array.from(
      this.unitManager.activeUnits.values()
    ).filter((u) => u.team === "PLAYER" && u.type === "EXPLORER");

    for (const unit of playerUnits) {
      // Use rosterId if available, otherwise fall back to unit.id
      const rosterId = unit.rosterId || unit.id;
      if (!rosterId) continue;

      const rosterUnit = this.gameStateManager.rosterManager.roster.find(
        (r) => r.id === rosterId
      );

      if (rosterUnit) {
        // Save classMastery progression
        if (unit.classMastery) {
          rosterUnit.classMastery = JSON.parse(
            JSON.stringify(unit.classMastery)
          );
        }
        // Save activeClassId
        if (unit.activeClassId) {
          rosterUnit.activeClassId = unit.activeClassId;
        }
        // Save equipment/loadout
        if (unit.loadout) {
          rosterUnit.loadout = JSON.parse(JSON.stringify(unit.loadout));
        }
        if (unit.equipment) {
          rosterUnit.equipment = JSON.parse(JSON.stringify(unit.equipment));
        }
        // Save current health
        if (unit.currentHealth !== undefined) {
          rosterUnit.currentHealth = unit.currentHealth;
        }
        console.log(
          `Saved progression for ${unit.name} (roster ID: ${rosterId})`
        );
      }
    }

    // Save roster to persistence
    if (this.gameStateManager.rosterManager) {
      this.gameStateManager._saveRoster();
    }
  }

  /**
   * Handles mission failure.
   * @param {Object} detail - Failure event detail
   * @private
   */
  _handleMissionFailure(detail) {
    console.log("Mission Failed!", detail);

    // Save Explorer progression back to roster (even on failure, progression should persist)
    this._saveExplorerProgression();

    // Pause the game
    this.isPaused = true;

    // Stop the game loop
    this.stop();

    // Clear the active run from persistence since mission is failed
    if (this.gameStateManager) {
      this.gameStateManager.clearActiveRun();
    }

    // TODO: Show failure screen UI
    // For now, just log and transition back to main menu after a delay
    setTimeout(() => {
      if (this.gameStateManager) {
        this.gameStateManager.transitionTo("STATE_MAIN_MENU");
      }
    }, 3000);
  }

  /**
   * Checks DID_NOT_ATTACK condition (for ON_TURN_END)
    if (condition.type === "DID_NOT_ATTACK") {
      // This would need to track if the unit attacked this turn
      // For now, we'll assume it's tracked in context
      if (context.didAttack === true) return false;
    }

    return true;
  }

  /**
   * Event handler for combat-end event from TurnSystem.
   * @private
   */
  _onCombatEnd() {
    // Combat has ended
    console.log("TurnSystem: Combat ended");
    this.clearMovementHighlights();
  }
}
