2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* @typedef {import("./types.js").RunData} RunData
|
|
|
|
|
* @typedef {import("../grid/types.js").Position} Position
|
|
|
|
|
* @typedef {import("../units/Unit.js").Unit} Unit
|
2025-12-22 22:34:43 +00:00
|
|
|
* @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
|
2025-12-22 20:55:41 +00:00
|
|
|
*/
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
import * as THREE from "three";
|
2025-12-19 23:08:54 +00:00
|
|
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
2025-12-19 23:07:36 +00:00
|
|
|
import { VoxelGrid } from "../grid/VoxelGrid.js";
|
|
|
|
|
import { VoxelManager } from "../grid/VoxelManager.js";
|
2025-12-19 23:35:29 +00:00
|
|
|
import { UnitManager } from "../managers/UnitManager.js";
|
2025-12-19 23:07:36 +00:00
|
|
|
import { CaveGenerator } from "../generation/CaveGenerator.js";
|
|
|
|
|
import { RuinGenerator } from "../generation/RuinGenerator.js";
|
2025-12-20 04:58:16 +00:00
|
|
|
import { InputManager } from "./InputManager.js";
|
2025-12-22 04:40:48 +00:00
|
|
|
import { MissionManager } from "../managers/MissionManager.js";
|
2025-12-24 00:22:32 +00:00
|
|
|
import { TurnSystem } from "../systems/TurnSystem.js";
|
|
|
|
|
import { MovementSystem } from "../systems/MovementSystem.js";
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Main game loop managing rendering, input, and game state.
|
|
|
|
|
* @class
|
|
|
|
|
*/
|
2025-12-19 23:07:36 +00:00
|
|
|
export class GameLoop {
|
|
|
|
|
constructor() {
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {boolean} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.isRunning = false;
|
|
|
|
|
|
|
|
|
|
// 1. Core Systems
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.Scene} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.scene = new THREE.Scene();
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.PerspectiveCamera | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.camera = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.WebGLRenderer | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.renderer = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {OrbitControls | null} */
|
2025-12-19 23:08:54 +00:00
|
|
|
this.controls = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {InputManager | null} */
|
2025-12-20 04:58:16 +00:00
|
|
|
this.inputManager = null;
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {VoxelGrid | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.grid = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {VoxelManager | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.voxelManager = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {UnitManager | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.unitManager = null;
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Combat Logic Systems
|
|
|
|
|
/** @type {TurnSystem | null} */
|
|
|
|
|
this.turnSystem = null;
|
|
|
|
|
/** @type {MovementSystem | null} */
|
|
|
|
|
this.movementSystem = null;
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Map<string, THREE.Mesh>} */
|
2025-12-20 00:02:42 +00:00
|
|
|
this.unitMeshes = new Map();
|
2025-12-24 00:22:32 +00:00
|
|
|
/** @type {Set<THREE.Mesh>} */
|
|
|
|
|
this.movementHighlights = new Set();
|
|
|
|
|
/** @type {Set<THREE.Mesh>} */
|
|
|
|
|
this.spawnZoneHighlights = new Set();
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {RunData | null} */
|
2025-12-19 23:07:36 +00:00
|
|
|
this.runData = null;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Position[]} */
|
2025-12-20 00:02:42 +00:00
|
|
|
this.playerSpawnZone = [];
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Position[]} */
|
2025-12-20 00:02:42 +00:00
|
|
|
this.enemySpawnZone = [];
|
2025-12-20 04:58:16 +00:00
|
|
|
|
|
|
|
|
// Input Logic State
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {number} */
|
2025-12-20 04:58:16 +00:00
|
|
|
this.lastMoveTime = 0;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {number} */
|
2025-12-20 04:58:16 +00:00
|
|
|
this.moveCooldown = 120; // ms between cursor moves
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {"MOVEMENT" | "TARGETING"} */
|
2025-12-20 04:58:16 +00:00
|
|
|
this.selectionMode = "MOVEMENT"; // MOVEMENT, TARGETING
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {MissionManager} */
|
2025-12-21 05:04:44 +00:00
|
|
|
this.missionManager = new MissionManager(this); // Init Mission Manager
|
|
|
|
|
|
|
|
|
|
// Deployment State
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {{ selectedUnitIndex: number; deployedUnits: Map<number, Unit> }} */
|
2025-12-21 05:04:44 +00:00
|
|
|
this.deploymentState = {
|
|
|
|
|
selectedUnitIndex: -1,
|
|
|
|
|
deployedUnits: new Map(), // Map<Index, UnitInstance>
|
|
|
|
|
};
|
2025-12-22 20:55:41 +00:00
|
|
|
|
|
|
|
|
/** @type {import("./GameStateManager.js").GameStateManagerClass | null} */
|
|
|
|
|
this.gameStateManager = null;
|
2025-12-19 23:07:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Initializes the game loop with Three.js setup.
|
|
|
|
|
* @param {HTMLElement} container - DOM element to attach the renderer to
|
|
|
|
|
*/
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
2025-12-20 04:58:16 +00:00
|
|
|
this.renderer.setClearColor(0x111111);
|
2025-12-19 23:07:36 +00:00
|
|
|
container.appendChild(this.renderer.domElement);
|
|
|
|
|
|
2025-12-19 23:08:54 +00:00
|
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
2025-12-20 00:02:42 +00:00
|
|
|
this.controls.enableDamping = true;
|
2025-12-19 23:08:54 +00:00
|
|
|
this.controls.dampingFactor = 0.05;
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// --- INSTANTIATE COMBAT SYSTEMS ---
|
|
|
|
|
this.turnSystem = new TurnSystem();
|
|
|
|
|
this.movementSystem = new MovementSystem();
|
|
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
// --- 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)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Default Validator: Movement Logic (Will be overridden in startLevel)
|
|
|
|
|
this.inputManager.setValidator(this.validateCursorMove.bind(this));
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-20 04:58:16 +00:00
|
|
|
* Validation Logic for Standard Movement.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @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
|
2025-12-19 23:07:36 +00:00
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
return false;
|
2025-12-20 04:58:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validation Logic for Deployment Phase.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @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
|
2025-12-20 04:58:16 +00:00
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
validateInteractionTarget(x, y, z) {
|
|
|
|
|
if (!this.grid) return true;
|
|
|
|
|
return this.grid.isValidBounds({ x, y, z });
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Handles gamepad button input.
|
|
|
|
|
* @param {{ buttonIndex: number; gamepadIndex: number }} detail - Button input detail
|
|
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
handleButtonInput(detail) {
|
|
|
|
|
if (detail.buttonIndex === 0) {
|
|
|
|
|
// A / Cross
|
|
|
|
|
this.triggerSelection();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Handles keyboard input.
|
|
|
|
|
* @param {string} code - Key code
|
|
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
handleKeyInput(code) {
|
|
|
|
|
if (code === "Space" || code === "Enter") {
|
|
|
|
|
this.triggerSelection();
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
/**
|
|
|
|
|
* Called by UI when a unit is clicked in the Roster.
|
2025-12-22 04:40:48 +00:00
|
|
|
* @param {number} index - The index of the unit in the squad to select.
|
2025-12-21 05:04:44 +00:00
|
|
|
*/
|
|
|
|
|
selectDeploymentUnit(index) {
|
|
|
|
|
this.deploymentState.selectedUnitIndex = index;
|
|
|
|
|
console.log(`Deployment: Selected Unit Index ${index}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Triggers selection action at cursor position.
|
|
|
|
|
*/
|
2025-12-20 04:58:16 +00:00
|
|
|
triggerSelection() {
|
|
|
|
|
const cursor = this.inputManager.getCursorPosition();
|
|
|
|
|
console.log("Action at:", cursor);
|
|
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
if (
|
|
|
|
|
this.gameStateManager &&
|
|
|
|
|
this.gameStateManager.currentState === "STATE_DEPLOYMENT"
|
|
|
|
|
) {
|
2025-12-21 05:04:44 +00:00
|
|
|
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.");
|
|
|
|
|
}
|
2025-12-24 00:22:32 +00:00
|
|
|
} else if (
|
|
|
|
|
this.gameStateManager &&
|
|
|
|
|
this.gameStateManager.currentState === "STATE_COMBAT"
|
|
|
|
|
) {
|
|
|
|
|
// Handle combat 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) {
|
|
|
|
|
mesh.position.set(
|
|
|
|
|
activeUnit.position.x,
|
|
|
|
|
activeUnit.position.y + 0.6,
|
|
|
|
|
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();
|
2025-12-20 04:58:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Starts a mission by ID.
|
|
|
|
|
* @param {string} missionId - Mission identifier
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
2025-12-21 05:04:44 +00:00
|
|
|
async startMission(missionId) {
|
|
|
|
|
const mission = await fetch(
|
|
|
|
|
`assets/data/missions/${missionId.toLowerCase()}.json`
|
|
|
|
|
);
|
|
|
|
|
const missionData = await mission.json();
|
|
|
|
|
this.missionManager.startMission(missionData);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Starts a level with the given run data.
|
|
|
|
|
* @param {RunData} runData - Run data containing mission and squad info
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
*/
|
2025-12-19 23:07:36 +00:00
|
|
|
async startLevel(runData) {
|
2025-12-20 00:02:42 +00:00
|
|
|
console.log("GameLoop: Generating Level...");
|
2025-12-19 23:07:36 +00:00
|
|
|
this.runData = runData;
|
|
|
|
|
this.isRunning = true;
|
2025-12-20 00:02:42 +00:00
|
|
|
this.clearUnitMeshes();
|
2025-12-24 00:22:32 +00:00
|
|
|
this.clearMovementHighlights();
|
|
|
|
|
this.clearSpawnZoneHighlights();
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
// Reset Deployment State
|
|
|
|
|
this.deploymentState = {
|
|
|
|
|
selectedUnitIndex: -1,
|
|
|
|
|
deployedUnits: new Map(), // Map<Index, UnitInstance>
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
this.grid = new VoxelGrid(20, 10, 20);
|
|
|
|
|
const generator = new RuinGenerator(this.grid, runData.seed);
|
|
|
|
|
generator.generate();
|
|
|
|
|
|
2025-12-20 00:02:42 +00:00
|
|
|
if (generator.generatedAssets.spawnZones) {
|
|
|
|
|
this.playerSpawnZone = generator.generatedAssets.spawnZones.player || [];
|
|
|
|
|
this.enemySpawnZone = generator.generatedAssets.spawnZones.enemy || [];
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
if (this.playerSpawnZone.length === 0)
|
2025-12-20 00:02:42 +00:00
|
|
|
this.playerSpawnZone.push({ x: 2, y: 1, z: 2 });
|
2025-12-20 04:58:16 +00:00
|
|
|
if (this.enemySpawnZone.length === 0)
|
2025-12-20 00:02:42 +00:00
|
|
|
this.enemySpawnZone.push({ x: 18, y: 1, z: 18 });
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-20 00:02:42 +00:00
|
|
|
this.voxelManager = new VoxelManager(this.grid, this.scene);
|
2025-12-19 23:07:36 +00:00
|
|
|
this.voxelManager.updateMaterials(generator.generatedAssets);
|
|
|
|
|
this.voxelManager.update();
|
2025-12-19 23:08:54 +00:00
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
if (this.controls) this.voxelManager.focusCamera(this.controls);
|
2025-12-19 23:07:36 +00:00
|
|
|
|
2025-12-19 23:35:29 +00:00
|
|
|
const mockRegistry = {
|
|
|
|
|
get: (id) => {
|
2025-12-20 00:02:42 +00:00
|
|
|
if (id.startsWith("CLASS_"))
|
2025-12-24 00:22:32 +00:00
|
|
|
return {
|
|
|
|
|
type: "EXPLORER",
|
|
|
|
|
name: id,
|
|
|
|
|
id: id,
|
|
|
|
|
base_stats: { health: 100, attack: 10, defense: 5, speed: 10 },
|
|
|
|
|
growth_rates: {},
|
|
|
|
|
};
|
2025-12-19 23:35:29 +00:00
|
|
|
return {
|
2025-12-20 00:02:42 +00:00
|
|
|
type: "ENEMY",
|
|
|
|
|
name: "Enemy",
|
2025-12-24 00:22:32 +00:00
|
|
|
stats: { health: 50, attack: 8, defense: 3, speed: 8 },
|
2025-12-20 00:02:42 +00:00
|
|
|
ai_archetype: "BRUISER",
|
2025-12-19 23:35:29 +00:00
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
this.unitManager = new UnitManager(mockRegistry);
|
2025-12-24 00:22:32 +00:00
|
|
|
|
|
|
|
|
// WIRING: Connect Systems to Data
|
|
|
|
|
this.movementSystem.setContext(this.grid, this.unitManager);
|
|
|
|
|
this.turnSystem.setContext(this.unitManager);
|
|
|
|
|
|
|
|
|
|
// WIRING: Listen for Turn Changes (to update UI/Input state)
|
|
|
|
|
this.turnSystem.addEventListener("turn-start", (e) =>
|
|
|
|
|
this._onTurnStart(e.detail)
|
|
|
|
|
);
|
|
|
|
|
this.turnSystem.addEventListener("turn-end", (e) =>
|
|
|
|
|
this._onTurnEnd(e.detail)
|
|
|
|
|
);
|
|
|
|
|
this.turnSystem.addEventListener("combat-start", () =>
|
|
|
|
|
this._onCombatStart()
|
|
|
|
|
);
|
|
|
|
|
this.turnSystem.addEventListener("combat-end", () => this._onCombatEnd());
|
|
|
|
|
|
2025-12-20 00:02:42 +00:00
|
|
|
this.highlightZones();
|
|
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
if (this.playerSpawnZone.length > 0) {
|
2025-12-21 05:04:44 +00:00
|
|
|
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;
|
|
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
const start = this.playerSpawnZone[0];
|
|
|
|
|
this.inputManager.setCursor(start.x, start.y, start.z);
|
2025-12-21 05:04:44 +00:00
|
|
|
|
|
|
|
|
if (this.controls) {
|
|
|
|
|
this.controls.target.set(centerX, centerY, centerZ);
|
|
|
|
|
this.controls.update();
|
|
|
|
|
}
|
2025-12-20 04:58:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.inputManager.setValidator(this.validateDeploymentCursor.bind(this));
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
this.animate();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2025-12-21 05:04:44 +00:00
|
|
|
deployUnit(unitDef, targetTile, existingUnit = null) {
|
2025-12-22 05:20:33 +00:00
|
|
|
if (
|
|
|
|
|
!this.gameStateManager ||
|
|
|
|
|
this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
|
|
|
|
|
)
|
|
|
|
|
return null;
|
2025-12-20 00:02:42 +00:00
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
const isValid = this.validateDeploymentCursor(
|
|
|
|
|
targetTile.x,
|
|
|
|
|
targetTile.y,
|
|
|
|
|
targetTile.z
|
2025-12-20 00:02:42 +00:00
|
|
|
);
|
|
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
// 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) {
|
|
|
|
|
mesh.position.set(targetTile.x, targetTile.y + 0.6, targetTile.z);
|
|
|
|
|
}
|
|
|
|
|
console.log(
|
|
|
|
|
`Moved ${existingUnit.name} to ${targetTile.x},${targetTile.y},${targetTile.z}`
|
|
|
|
|
);
|
|
|
|
|
return existingUnit;
|
|
|
|
|
} else {
|
|
|
|
|
// CREATE logic
|
|
|
|
|
const unit = this.unitManager.createUnit(
|
|
|
|
|
unitDef.classId || unitDef.id,
|
|
|
|
|
"PLAYER"
|
|
|
|
|
);
|
|
|
|
|
if (unitDef.name) unit.name = unitDef.name;
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Ensure unit starts with full health
|
|
|
|
|
// Explorer constructor might set health to 0 if classDef is missing base_stats
|
|
|
|
|
if (unit.currentHealth <= 0) {
|
|
|
|
|
unit.currentHealth = unit.maxHealth || unit.baseStats?.health || 100;
|
|
|
|
|
unit.maxHealth = unit.maxHealth || unit.baseStats?.health || 100;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
this.grid.placeUnit(unit, targetTile);
|
|
|
|
|
this.createUnitMesh(unit, targetTile);
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`Deployed ${unit.name} at ${targetTile.x},${targetTile.y},${targetTile.z}`
|
|
|
|
|
);
|
|
|
|
|
return unit;
|
|
|
|
|
}
|
2025-12-20 00:02:42 +00:00
|
|
|
}
|
2025-12-19 23:35:29 +00:00
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Finalizes deployment phase and starts combat.
|
|
|
|
|
*/
|
2025-12-20 00:02:42 +00:00
|
|
|
finalizeDeployment() {
|
2025-12-22 05:20:33 +00:00
|
|
|
if (
|
|
|
|
|
!this.gameStateManager ||
|
|
|
|
|
this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
|
|
|
|
|
)
|
|
|
|
|
return;
|
2025-12-20 00:02:42 +00:00
|
|
|
const enemyCount = 2;
|
|
|
|
|
for (let i = 0; i < enemyCount; i++) {
|
|
|
|
|
const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length);
|
|
|
|
|
const spot = this.enemySpawnZone[spotIndex];
|
|
|
|
|
if (spot && !this.grid.isOccupied(spot)) {
|
|
|
|
|
const enemy = this.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
|
|
|
|
|
this.grid.placeUnit(enemy, spot);
|
|
|
|
|
this.createUnitMesh(enemy, spot);
|
|
|
|
|
this.enemySpawnZone.splice(spotIndex, 1);
|
2025-12-19 23:35:29 +00:00
|
|
|
}
|
2025-12-20 00:02:42 +00:00
|
|
|
}
|
2025-12-20 04:58:16 +00:00
|
|
|
|
|
|
|
|
// Switch to standard movement validator for the game
|
|
|
|
|
this.inputManager.setValidator(this.validateCursorMove.bind(this));
|
2025-12-21 05:04:44 +00:00
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Clear spawn zone highlights now that deployment is finished
|
|
|
|
|
this.clearSpawnZoneHighlights();
|
|
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
// Notify GameStateManager about state change
|
|
|
|
|
if (this.gameStateManager) {
|
|
|
|
|
this.gameStateManager.transitionTo("STATE_COMBAT");
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
// Update combat state immediately so UI shows combat HUD
|
2025-12-22 22:34:43 +00:00
|
|
|
this.updateCombatState();
|
|
|
|
|
|
2025-12-21 05:04:44 +00:00
|
|
|
console.log("Combat Started!");
|
2025-12-20 00:02:42 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Clears all unit meshes from the scene.
|
|
|
|
|
*/
|
2025-12-20 00:02:42 +00:00
|
|
|
clearUnitMeshes() {
|
|
|
|
|
this.unitMeshes.forEach((mesh) => this.scene.remove(mesh));
|
|
|
|
|
this.unitMeshes.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
/**
|
|
|
|
|
* Clears all movement highlight meshes from the scene.
|
|
|
|
|
*/
|
|
|
|
|
clearMovementHighlights() {
|
|
|
|
|
this.movementHighlights.forEach((mesh) => this.scene.remove(mesh));
|
|
|
|
|
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 blue highlight material
|
|
|
|
|
const highlightMaterial = new THREE.MeshBasicMaterial({
|
|
|
|
|
color: 0x0066ff, // Blue color
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0.4,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create geometry for highlights (plane on the ground)
|
|
|
|
|
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
|
|
|
geometry.rotateX(-Math.PI / 2);
|
|
|
|
|
|
|
|
|
|
// Create highlight meshes for each reachable position
|
|
|
|
|
reachablePositions.forEach((pos) => {
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, highlightMaterial);
|
|
|
|
|
// Position just above floor surface (pos.y is the air space, floor surface is at pos.y)
|
|
|
|
|
mesh.position.set(pos.x, pos.y + 0.01, pos.z);
|
|
|
|
|
this.scene.add(mesh);
|
|
|
|
|
this.movementHighlights.add(mesh);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Creates a visual mesh for a unit.
|
|
|
|
|
* @param {Unit} unit - The unit instance
|
|
|
|
|
* @param {Position} pos - Position to place the mesh
|
|
|
|
|
*/
|
2025-12-20 00:02:42 +00:00
|
|
|
createUnitMesh(unit, pos) {
|
|
|
|
|
const geometry = new THREE.BoxGeometry(0.6, 1.2, 0.6);
|
|
|
|
|
let color = 0xcccccc;
|
|
|
|
|
if (unit.id.includes("VANGUARD")) color = 0xff3333;
|
2025-12-20 04:58:16 +00:00
|
|
|
else if (unit.team === "ENEMY") color = 0x550000;
|
2025-12-20 00:02:42 +00:00
|
|
|
const material = new THREE.MeshStandardMaterial({ color: color });
|
|
|
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
|
|
|
mesh.position.set(pos.x, pos.y + 0.6, pos.z);
|
|
|
|
|
this.scene.add(mesh);
|
|
|
|
|
this.unitMeshes.set(unit.id, mesh);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Highlights spawn zones with visual indicators.
|
|
|
|
|
*/
|
2025-12-20 00:02:42 +00:00
|
|
|
highlightZones() {
|
2025-12-24 00:22:32 +00:00
|
|
|
// Clear any existing spawn zone highlights
|
|
|
|
|
this.clearSpawnZoneHighlights();
|
|
|
|
|
|
2025-12-20 00:02:42 +00:00
|
|
|
const highlightMatPlayer = new THREE.MeshBasicMaterial({
|
|
|
|
|
color: 0x00ff00,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0.3,
|
|
|
|
|
});
|
|
|
|
|
const highlightMatEnemy = new THREE.MeshBasicMaterial({
|
|
|
|
|
color: 0xff0000,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0.3,
|
|
|
|
|
});
|
|
|
|
|
const geo = new THREE.PlaneGeometry(1, 1);
|
|
|
|
|
geo.rotateX(-Math.PI / 2);
|
|
|
|
|
this.playerSpawnZone.forEach((pos) => {
|
|
|
|
|
const mesh = new THREE.Mesh(geo, highlightMatPlayer);
|
2025-12-20 04:58:16 +00:00
|
|
|
mesh.position.set(pos.x, pos.y + 0.05, pos.z);
|
2025-12-20 00:02:42 +00:00
|
|
|
this.scene.add(mesh);
|
2025-12-24 00:22:32 +00:00
|
|
|
this.spawnZoneHighlights.add(mesh);
|
2025-12-20 00:02:42 +00:00
|
|
|
});
|
|
|
|
|
this.enemySpawnZone.forEach((pos) => {
|
|
|
|
|
const mesh = new THREE.Mesh(geo, highlightMatEnemy);
|
|
|
|
|
mesh.position.set(pos.x, pos.y + 0.05, pos.z);
|
|
|
|
|
this.scene.add(mesh);
|
2025-12-24 00:22:32 +00:00
|
|
|
this.spawnZoneHighlights.add(mesh);
|
2025-12-19 23:35:29 +00:00
|
|
|
});
|
2025-12-19 23:07:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
/**
|
|
|
|
|
* Clears all spawn zone highlight meshes from the scene.
|
|
|
|
|
*/
|
|
|
|
|
clearSpawnZoneHighlights() {
|
|
|
|
|
this.spawnZoneHighlights.forEach((mesh) => this.scene.remove(mesh));
|
|
|
|
|
this.spawnZoneHighlights.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Main animation loop.
|
|
|
|
|
*/
|
2025-12-19 23:07:36 +00:00
|
|
|
animate() {
|
|
|
|
|
if (!this.isRunning) return;
|
|
|
|
|
requestAnimationFrame(this.animate);
|
|
|
|
|
|
2025-12-20 04:58:16 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-19 23:08:54 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-20 00:02:42 +00:00
|
|
|
const time = Date.now() * 0.002;
|
|
|
|
|
this.unitMeshes.forEach((mesh) => {
|
|
|
|
|
mesh.position.y += Math.sin(time) * 0.002;
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-19 23:07:36 +00:00
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Stops the game loop and cleans up resources.
|
|
|
|
|
*/
|
2025-12-19 23:07:36 +00:00
|
|
|
stop() {
|
|
|
|
|
this.isRunning = false;
|
2025-12-24 00:22:32 +00:00
|
|
|
if (this.inputManager && typeof this.inputManager.detach === "function") {
|
|
|
|
|
this.inputManager.detach();
|
|
|
|
|
}
|
2025-12-19 23:08:54 +00:00
|
|
|
if (this.controls) this.controls.dispose();
|
2025-12-19 23:07:36 +00:00
|
|
|
}
|
2025-12-22 22:34:43 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates the combat state in GameStateManager.
|
|
|
|
|
* Called when combat starts or when combat state changes (turn changes, etc.)
|
2025-12-24 00:22:32 +00:00
|
|
|
* Uses TurnSystem to get the spec-compliant CombatState, then enriches it for UI.
|
2025-12-22 22:34:43 +00:00
|
|
|
*/
|
|
|
|
|
updateCombatState() {
|
2025-12-24 00:22:32 +00:00
|
|
|
if (!this.gameStateManager || !this.turnSystem) {
|
2025-12-22 22:34:43 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Get spec-compliant combat state from TurnSystem
|
|
|
|
|
const turnSystemState = this.turnSystem.getCombatState();
|
2025-12-22 22:34:43 +00:00
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
if (!turnSystemState.isActive) {
|
|
|
|
|
// Combat not active, clear state
|
2025-12-22 22:34:43 +00:00
|
|
|
this.gameStateManager.setCombatState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Get active unit for UI enrichment
|
|
|
|
|
const activeUnit = this.turnSystem.getActiveUnit();
|
2025-12-22 22:34:43 +00:00
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// Build active unit status if we have an active unit (for UI)
|
2025-12-22 22:34:43 +00:00
|
|
|
let unitStatus = null;
|
|
|
|
|
if (activeUnit) {
|
2025-12-24 00:22:32 +00:00
|
|
|
// Calculate max AP using formula: 3 + floor(speed/5)
|
|
|
|
|
const speed = activeUnit.baseStats?.speed || 10;
|
|
|
|
|
const maxAP = 3 + Math.floor(speed / 5);
|
2025-12-22 22:34:43 +00:00
|
|
|
|
|
|
|
|
// 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 (placeholder for now - will be populated from unit's actions/skill tree)
|
|
|
|
|
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,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// If no skills from actions, provide a default attack skill
|
|
|
|
|
if (skills.length === 0) {
|
|
|
|
|
skills.push({
|
|
|
|
|
id: "attack",
|
|
|
|
|
name: "Attack",
|
|
|
|
|
icon: "⚔",
|
|
|
|
|
costAP: 3,
|
|
|
|
|
cooldown: 0,
|
|
|
|
|
isAvailable: activeUnit.currentAP >= 3,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unitStatus = {
|
|
|
|
|
id: activeUnit.id,
|
|
|
|
|
name: activeUnit.name,
|
|
|
|
|
portrait:
|
|
|
|
|
activeUnit.team === "PLAYER"
|
|
|
|
|
? "/assets/images/portraits/default.png"
|
|
|
|
|
: "/assets/images/portraits/enemy.png",
|
|
|
|
|
hp: {
|
|
|
|
|
current: activeUnit.currentHealth,
|
|
|
|
|
max: activeUnit.maxHealth,
|
|
|
|
|
},
|
|
|
|
|
ap: {
|
|
|
|
|
current: activeUnit.currentAP,
|
|
|
|
|
max: maxAP,
|
|
|
|
|
},
|
|
|
|
|
charge: activeUnit.chargeMeter || 0,
|
|
|
|
|
statuses: statuses,
|
|
|
|
|
skills: skills,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 00:22:32 +00:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
const portrait =
|
|
|
|
|
unit.team === "PLAYER"
|
|
|
|
|
? "/assets/images/portraits/default.png"
|
|
|
|
|
: "/assets/images/portraits/enemy.png";
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
unitId: unit.id,
|
|
|
|
|
portrait: unit.portrait || portrait,
|
|
|
|
|
team: unit.team || "ENEMY",
|
|
|
|
|
initiative: unit.chargeMeter || 0,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((entry) => entry !== null);
|
|
|
|
|
|
|
|
|
|
// Build combat state (enriched for UI, but includes spec fields)
|
2025-12-22 22:34:43 +00:00
|
|
|
const combatState = {
|
2025-12-24 00:22:32 +00:00
|
|
|
// 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
|
2025-12-22 22:34:43 +00:00
|
|
|
targetingMode: false, // Will be set when player selects a skill
|
2025-12-24 00:22:32 +00:00
|
|
|
roundNumber: turnSystemState.round, // Alias for UI
|
2025-12-22 22:34:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update GameStateManager
|
|
|
|
|
this.gameStateManager.setCombatState(combatState);
|
|
|
|
|
}
|
2025-12-24 00:22:32 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DELEGATE to TurnSystem
|
|
|
|
|
this.turnSystem.endTurn(activeUnit);
|
|
|
|
|
|
|
|
|
|
// Update combat state (TurnSystem will have advanced to next unit)
|
|
|
|
|
this.updateCombatState();
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Event handler for combat-start event from TurnSystem.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
_onCombatStart() {
|
|
|
|
|
// Combat has started
|
|
|
|
|
console.log("TurnSystem: Combat started");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Event handler for combat-end event from TurnSystem.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
|
|
|
|
_onCombatEnd() {
|
|
|
|
|
// Combat has ended
|
|
|
|
|
console.log("TurnSystem: Combat ended");
|
|
|
|
|
this.clearMovementHighlights();
|
|
|
|
|
}
|
2025-12-19 23:07:36 +00:00
|
|
|
}
|