diff --git a/src/core/GameLoop.js b/src/core/GameLoop.js index c1859ac..0790ae6 100644 --- a/src/core/GameLoop.js +++ b/src/core/GameLoop.js @@ -1,3 +1,9 @@ +/** + * @typedef {import("./types.js").RunData} RunData + * @typedef {import("../grid/types.js").Position} Position + * @typedef {import("../units/Unit.js").Unit} Unit + */ + import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { VoxelGrid } from "../grid/VoxelGrid.js"; @@ -8,39 +14,68 @@ import { RuinGenerator } from "../generation/RuinGenerator.js"; import { InputManager } from "./InputManager.js"; import { MissionManager } from "../managers/MissionManager.js"; +/** + * Main game loop managing rendering, input, and game state. + * @class + */ export class GameLoop { constructor() { + /** @type {boolean} */ this.isRunning = 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; + /** @type {Map} */ this.unitMeshes = new Map(); + /** @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 }} */ this.deploymentState = { selectedUnitIndex: -1, deployedUnits: new Map(), // Map }; + + /** @type {import("./GameStateManager.js").GameStateManagerClass | null} */ + this.gameStateManager = 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( @@ -96,6 +131,10 @@ export class GameLoop { /** * 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 @@ -120,6 +159,10 @@ export class GameLoop { /** * 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; @@ -135,6 +178,13 @@ export class GameLoop { 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; @@ -142,11 +192,22 @@ export class GameLoop { 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 @@ -154,6 +215,10 @@ export class GameLoop { } } + /** + * Handles keyboard input. + * @param {string} code - Key code + */ handleKeyInput(code) { if (code === "Space" || code === "Enter") { this.triggerSelection(); @@ -179,6 +244,9 @@ export class GameLoop { console.log(`Deployment: Selected Unit Index ${index}`); } + /** + * Triggers selection action at cursor position. + */ triggerSelection() { const cursor = this.inputManager.getCursorPosition(); console.log("Action at:", cursor); @@ -217,6 +285,11 @@ export class GameLoop { } } + /** + * Starts a mission by ID. + * @param {string} missionId - Mission identifier + * @returns {Promise} + */ async startMission(missionId) { const mission = await fetch( `assets/data/missions/${missionId.toLowerCase()}.json` @@ -225,6 +298,11 @@ export class GameLoop { this.missionManager.startMission(missionData); } + /** + * Starts a level with the given run data. + * @param {RunData} runData - Run data containing mission and squad info + * @returns {Promise} + */ async startLevel(runData) { console.log("GameLoop: Generating Level..."); this.runData = runData; @@ -299,6 +377,13 @@ export class GameLoop { 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 || @@ -362,6 +447,9 @@ export class GameLoop { } } + /** + * Finalizes deployment phase and starts combat. + */ finalizeDeployment() { if ( !this.gameStateManager || @@ -391,11 +479,19 @@ export class GameLoop { console.log("Combat Started!"); } + /** + * Clears all unit meshes from the scene. + */ clearUnitMeshes() { this.unitMeshes.forEach((mesh) => this.scene.remove(mesh)); this.unitMeshes.clear(); } + /** + * 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); let color = 0xcccccc; @@ -408,6 +504,9 @@ export class GameLoop { this.unitMeshes.set(unit.id, mesh); } + /** + * Highlights spawn zones with visual indicators. + */ highlightZones() { const highlightMatPlayer = new THREE.MeshBasicMaterial({ color: 0x00ff00, @@ -433,6 +532,9 @@ export class GameLoop { }); } + /** + * Main animation loop. + */ animate() { if (!this.isRunning) return; requestAnimationFrame(this.animate); @@ -484,6 +586,9 @@ export class GameLoop { this.renderer.render(this.scene, this.camera); } + /** + * Stops the game loop and cleans up resources. + */ stop() { this.isRunning = false; if (this.inputManager) this.inputManager.detach(); diff --git a/src/core/GameStateManager.js b/src/core/GameStateManager.js index 678eab2..a748328 100644 --- a/src/core/GameStateManager.js +++ b/src/core/GameStateManager.js @@ -1,9 +1,21 @@ +/** + * @typedef {import("./types.js").GameState} GameState + * @typedef {import("./types.js").RunData} RunData + * @typedef {import("./types.js").EmbarkEventDetail} EmbarkEventDetail + * @typedef {import("./types.js").SquadMember} SquadMember + */ + import { Persistence } from "./Persistence.js"; import { RosterManager } from "../managers/RosterManager.js"; import { MissionManager } from "../managers/MissionManager.js"; import { narrativeManager } from "../managers/NarrativeManager.js"; +/** + * Manages the overall game state and transitions between different game modes. + * @class + */ class GameStateManagerClass { + /** @type {Record} */ static STATES = { INIT: "STATE_INIT", MAIN_MENU: "STATE_MAIN_MENU", @@ -12,37 +24,68 @@ class GameStateManagerClass { COMBAT: "STATE_COMBAT", }; + /** + * @param {GameState} currentState - Current game state + * @param {import("./GameLoop.js").GameLoop | null} gameLoop - Reference to game loop + * @param {Persistence} persistence - Persistence manager + * @param {RunData | null} activeRunData - Current active run data + * @param {RosterManager} rosterManager - Roster manager instance + * @param {MissionManager} missionManager - Mission manager instance + * @param {import("../managers/NarrativeManager.js").NarrativeManager} narrativeManager - Narrative manager instance + */ constructor() { + /** @type {GameState} */ this.currentState = GameStateManagerClass.STATES.INIT; + /** @type {import("./GameLoop.js").GameLoop | null} */ this.gameLoop = null; + /** @type {Persistence} */ this.persistence = new Persistence(); + /** @type {RunData | null} */ this.activeRunData = null; // Integrate Core Managers + /** @type {RosterManager} */ this.rosterManager = new RosterManager(); + /** @type {MissionManager} */ this.missionManager = new MissionManager(); + /** @type {import("../managers/NarrativeManager.js").NarrativeManager} */ this.narrativeManager = narrativeManager; // Track the singleton instance this.handleEmbark = this.handleEmbark.bind(this); } + /** @type {PromiseWithResolvers} */ #gameLoopInitialized = Promise.withResolvers(); + /** + * @returns {Promise} + */ get gameLoopInitialized() { return this.#gameLoopInitialized.promise; } + /** @type {PromiseWithResolvers} */ #rosterLoaded = Promise.withResolvers(); + /** + * @returns {Promise} + */ get rosterLoaded() { return this.#rosterLoaded.promise; } + /** + * Sets the game loop reference. + * @param {import("./GameLoop.js").GameLoop} loop - The game loop instance + */ setGameLoop(loop) { this.gameLoop = loop; this.#gameLoopInitialized.resolve(); } + /** + * Resets the state manager to initial state. + */ reset() { // Reset singleton state for testing this.currentState = GameStateManagerClass.STATES.INIT; @@ -55,6 +98,10 @@ class GameStateManagerClass { this.#rosterLoaded = Promise.withResolvers(); } + /** + * Initializes the game state manager. + * @returns {Promise} + */ async init() { console.log("System: Initializing State Manager..."); await this.persistence.init(); @@ -72,6 +119,12 @@ class GameStateManagerClass { this.transitionTo(GameStateManagerClass.STATES.MAIN_MENU); } + /** + * Transitions to a new game state. + * @param {GameState} newState - The new state to transition to + * @param {unknown} [payload] - Optional payload data for the transition + * @returns {Promise} + */ async transitionTo(newState, payload = null) { const oldState = this.currentState; const stateChanged = oldState !== newState; @@ -104,6 +157,9 @@ class GameStateManagerClass { } } + /** + * Starts a new game session. + */ startNewGame() { // Clear roster for a fresh start this.rosterManager.clear(); @@ -114,6 +170,10 @@ class GameStateManagerClass { this.transitionTo(GameStateManagerClass.STATES.TEAM_BUILDER); } + /** + * Continues a previously saved game. + * @returns {Promise} + */ async continueGame() { const save = await this.persistence.loadRun(); if (save) { @@ -123,6 +183,11 @@ class GameStateManagerClass { } } + /** + * Handles the embark event from the team builder. + * @param {CustomEvent} e - The embark event + * @returns {Promise} + */ async handleEmbark(e) { // Handle Draft Mode (New Recruits) if (e.detail.mode === "DRAFT") { @@ -141,6 +206,12 @@ class GameStateManagerClass { // --- INTERNAL HELPERS --- + /** + * Initializes a new run with the given squad. + * @param {SquadMember[]} squadManifest - The squad members to deploy + * @returns {Promise} + * @private + */ async _initializeRun(squadManifest) { await this.gameLoopInitialized; @@ -182,6 +253,11 @@ class GameStateManagerClass { await this.gameLoop.startLevel(this.activeRunData); } + /** + * Resumes a previously saved run. + * @returns {Promise} + * @private + */ async _resumeRun() { await this.gameLoopInitialized; if (this.activeRunData) { @@ -197,6 +273,11 @@ class GameStateManagerClass { } } + /** + * Checks if a save game exists and dispatches an event. + * @returns {Promise} + * @private + */ async _checkSaveGame() { const save = await this.persistence.loadRun(); window.dispatchEvent( @@ -204,6 +285,11 @@ class GameStateManagerClass { ); } + /** + * Saves the current roster. + * @returns {Promise} + * @private + */ async _saveRoster() { const data = this.rosterManager.save(); await this.persistence.saveRoster(data); @@ -211,5 +297,6 @@ class GameStateManagerClass { } // Export the Singleton Instance +/** @type {GameStateManagerClass} */ export const gameStateManager = new GameStateManagerClass(); export const GameStateManager = GameStateManagerClass; diff --git a/src/core/InputManager.js b/src/core/InputManager.js index f82ed78..708d6c3 100644 --- a/src/core/InputManager.js +++ b/src/core/InputManager.js @@ -1,27 +1,47 @@ +/** + * @typedef {import("../grid/types.js").Position} Position + */ + import * as THREE from "three"; /** * InputManager.js * Handles mouse interaction, raycasting, grid selection, keyboard, and Gamepad input. * Extends EventTarget for standard event handling. + * @class */ export class InputManager extends EventTarget { + /** + * @param {THREE.Camera} camera - Three.js camera + * @param {THREE.Scene} scene - Three.js scene + * @param {HTMLElement} domElement - DOM element for input + */ constructor(camera, scene, domElement) { super(); + /** @type {THREE.Camera} */ this.camera = camera; + /** @type {THREE.Scene} */ this.scene = scene; + /** @type {HTMLElement} */ this.domElement = domElement; + /** @type {THREE.Raycaster} */ this.raycaster = new THREE.Raycaster(); + /** @type {THREE.Vector2} */ this.mouse = new THREE.Vector2(); // Input State + /** @type {Set} */ this.keys = new Set(); + /** @type {Map} */ this.gamepads = new Map(); + /** @type {number} */ this.axisDeadzone = 0.2; // Increased slightly for stability // Cursor State + /** @type {THREE.Vector3} */ this.cursorPos = new THREE.Vector3(0, 0, 0); + /** @type {((x: number, y: number, z: number) => false | Position) | null} */ this.cursorValidator = null; // Function to check if a position is valid // Visual Cursor (Wireframe Box) @@ -78,7 +98,7 @@ export class InputManager extends EventTarget { /** * Set a validation function to restrict cursor movement. - * @param {Function} validatorFn - Takes (x, y, z). Returns true/false OR a modified {x,y,z} object. + * @param {(x: number, y: number, z: number) => false | Position} validatorFn - Takes (x, y, z). Returns false if invalid, or a modified position object. */ setValidator(validatorFn) { this.cursorValidator = validatorFn; @@ -87,6 +107,9 @@ export class InputManager extends EventTarget { /** * Programmatically move the cursor (e.g. via Gamepad or Keyboard). * This now simulates a raycast-like resolution to ensure consistent behavior. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate */ setCursor(x, y, z) { // Instead of raw rounding, we try to "snap" using the same logic as raycast @@ -125,6 +148,10 @@ export class InputManager extends EventTarget { ); } + /** + * Gets the current cursor position. + * @returns {THREE.Vector3} - Current cursor position + */ getCursorPosition() { return this.cursorPos; } @@ -145,6 +172,9 @@ export class InputManager extends EventTarget { ); } + /** + * Updates gamepad state. Should be called every frame. + */ update() { const activeGamepads = navigator.getGamepads ? navigator.getGamepads() : []; @@ -206,6 +236,11 @@ export class InputManager extends EventTarget { this.dispatchEvent(new CustomEvent("keyup", { detail: event.code })); } + /** + * Checks if a key is currently pressed. + * @param {string} code - Key code + * @returns {boolean} - True if key is pressed + */ isKeyPressed(code) { return this.keys.has(code); } @@ -244,6 +279,9 @@ export class InputManager extends EventTarget { /** * Resolves a world-space point and normal into voxel coordinates. * Shared logic for Raycasting and potentially other inputs. + * @param {THREE.Vector3} point - World space point + * @param {THREE.Vector3} normal - Surface normal + * @returns {{ hitPosition: Position; voxelPosition: Position; normal: THREE.Vector3 }} - Resolved cursor data */ resolveCursorFromWorldPosition(point, normal) { const p = point.clone(); @@ -268,6 +306,10 @@ export class InputManager extends EventTarget { }; } + /** + * Performs raycast from camera through mouse position. + * @returns {null | { hitPosition: Position; voxelPosition: Position; normal: THREE.Vector3 }} - Hit data or null + */ raycast() { this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObjects( diff --git a/src/core/Persistence.js b/src/core/Persistence.js index 902062b..0195a5d 100644 --- a/src/core/Persistence.js +++ b/src/core/Persistence.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("./types.js").RunData} RunData + * @typedef {import("../units/types.js").RosterSaveData} RosterSaveData + */ + /** * Persistence.js * Handles asynchronous saving and loading using IndexedDB. @@ -8,11 +13,20 @@ const RUN_STORE = "Runs"; const ROSTER_STORE = "Roster"; const VERSION = 2; // Bumped version to add Roster store +/** + * Handles game data persistence using IndexedDB. + * @class + */ export class Persistence { constructor() { + /** @type {IDBDatabase | null} */ this.db = null; } + /** + * Initializes the IndexedDB database. + * @returns {Promise} + */ async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, VERSION); @@ -42,16 +56,29 @@ export class Persistence { // --- RUN DATA --- + /** + * Saves run data. + * @param {RunData} runData - Run data to save + * @returns {Promise} + */ async saveRun(runData) { if (!this.db) await this.init(); return this._put(RUN_STORE, { ...runData, id: "active_run" }); } + /** + * Loads run data. + * @returns {Promise} + */ async loadRun() { if (!this.db) await this.init(); return this._get(RUN_STORE, "active_run"); } + /** + * Clears saved run data. + * @returns {Promise} + */ async clearRun() { if (!this.db) await this.init(); return this._delete(RUN_STORE, "active_run"); @@ -59,12 +86,21 @@ export class Persistence { // --- ROSTER DATA --- + /** + * Saves roster data. + * @param {RosterSaveData} rosterData - Roster data to save + * @returns {Promise} + */ async saveRoster(rosterData) { if (!this.db) await this.init(); // Wrap the raw data object in an ID for storage return this._put(ROSTER_STORE, { id: "player_roster", data: rosterData }); } + /** + * Loads roster data. + * @returns {Promise} + */ async loadRoster() { if (!this.db) await this.init(); const result = await this._get(ROSTER_STORE, "player_roster"); @@ -73,6 +109,13 @@ export class Persistence { // --- INTERNAL HELPERS --- + /** + * Internal helper to put data into a store. + * @param {string} storeName - Store name + * @param {unknown} item - Item to store + * @returns {Promise} + * @private + */ _put(storeName, item) { return new Promise((resolve, reject) => { const tx = this.db.transaction([storeName], "readwrite"); @@ -83,6 +126,13 @@ export class Persistence { }); } + /** + * Internal helper to get data from a store. + * @param {string} storeName - Store name + * @param {string} key - Key to retrieve + * @returns {Promise} + * @private + */ _get(storeName, key) { return new Promise((resolve, reject) => { const tx = this.db.transaction([storeName], "readonly"); @@ -93,6 +143,13 @@ export class Persistence { }); } + /** + * Internal helper to delete data from a store. + * @param {string} storeName - Store name + * @param {string} key - Key to delete + * @returns {Promise} + * @private + */ _delete(storeName, key) { return new Promise((resolve, reject) => { const tx = this.db.transaction([storeName], "readwrite"); diff --git a/src/core/types.d.ts b/src/core/types.d.ts new file mode 100644 index 0000000..7757fb8 --- /dev/null +++ b/src/core/types.d.ts @@ -0,0 +1,103 @@ +/** + * Type definitions for core game systems + */ + +import type { GameLoop } from "./GameLoop.js"; +import type { RosterManager } from "../managers/RosterManager.js"; +import type { MissionManager } from "../managers/MissionManager.js"; +import type { NarrativeManager } from "../managers/NarrativeManager.js"; +import type { Persistence } from "./Persistence.js"; + +/** + * Game state constants + */ +export type GameState = "STATE_INIT" | "STATE_MAIN_MENU" | "STATE_TEAM_BUILDER" | "STATE_DEPLOYMENT" | "STATE_COMBAT"; + +/** + * Run data structure for active game sessions + */ +export interface RunData { + id: string; + missionId: string; + seed: number; + depth: number; + biome: BiomeConfig; + squad: SquadMember[]; + objectives: Objective[]; + world_state: Record; +} + +/** + * Squad member definition + */ +export interface SquadMember { + id?: string; + classId?: string; + name?: string; + isNew?: boolean; + [key: string]: unknown; +} + +/** + * Biome configuration + */ +export interface BiomeConfig { + generator_config: { + seed_type: "FIXED" | "RANDOM"; + seed?: number; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +/** + * Objective definition + */ +export interface Objective { + type: string; + target_count?: number; + target_def_id?: string; + current?: number; + complete?: boolean; + [key: string]: unknown; +} + +/** + * Game state change event detail + */ +export interface GameStateChangeDetail { + oldState: GameState; + newState: GameState; + payload: unknown; +} + +/** + * Embark event detail + */ +export interface EmbarkEventDetail { + mode: "DRAFT" | "DEPLOY"; + squad: SquadMember[]; +} + +/** + * GameStateManager class interface + */ +export interface GameStateManagerInterface { + currentState: GameState; + gameLoop: GameLoop | null; + persistence: Persistence; + activeRunData: RunData | null; + rosterManager: RosterManager; + missionManager: MissionManager; + narrativeManager: NarrativeManager; + gameLoopInitialized: Promise; + rosterLoaded: Promise; + setGameLoop(loop: GameLoop): void; + reset(): void; + init(): Promise; + transitionTo(newState: GameState, payload?: unknown): Promise; + startNewGame(): void; + continueGame(): Promise; + handleEmbark(e: CustomEvent): Promise; +} + diff --git a/src/generation/BaseGenerator.js b/src/generation/BaseGenerator.js index fb254a5..3b96d0b 100644 --- a/src/generation/BaseGenerator.js +++ b/src/generation/BaseGenerator.js @@ -1,14 +1,39 @@ +/** + * @typedef {import("../grid/VoxelGrid.js").VoxelGrid} VoxelGrid + * @typedef {import("../grid/types.js").VoxelId} VoxelId + */ + import { SeededRandom } from "../utils/SeededRandom.js"; +/** + * Base class for world generators. + * @class + */ export class BaseGenerator { + /** + * @param {VoxelGrid} grid - Voxel grid to generate into + * @param {number} seed - Random seed + */ constructor(grid, seed) { + /** @type {VoxelGrid} */ this.grid = grid; + /** @type {SeededRandom} */ this.rng = new SeededRandom(seed); + /** @type {number} */ this.width = grid.size.x; + /** @type {number} */ this.height = grid.size.y; + /** @type {number} */ this.depth = grid.size.z; } + /** + * Counts solid neighbors around a position. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate + * @returns {number} - Number of solid neighbors + */ getSolidNeighbors(x, y, z) { let count = 0; for (let i = -1; i <= 1; i++) { diff --git a/src/generation/RuinGenerator.js b/src/generation/RuinGenerator.js index 04802e5..3c7b4dd 100644 --- a/src/generation/RuinGenerator.js +++ b/src/generation/RuinGenerator.js @@ -1,3 +1,7 @@ +/** + * @typedef {import("../grid/types.js").GeneratedAssets} GeneratedAssets + */ + import { BaseGenerator } from "./BaseGenerator.js"; // We can reuse the texture generators or create specific Ruin ones. import { RustedWallTextureGenerator } from "./textures/RustedWallTextureGenerator.js"; @@ -7,15 +11,23 @@ import { RustedFloorTextureGenerator } from "./textures/RustedFloorTextureGenera * Generates structured rooms and corridors. * Uses an "Additive" approach (Building in Void) to ensure good visibility. * Integrated with Procedural Texture Palette. + * @class */ export class RuinGenerator extends BaseGenerator { + /** + * @param {import("../grid/VoxelGrid.js").VoxelGrid} grid - Voxel grid to generate into + * @param {number} seed - Random seed + */ constructor(grid, seed) { super(grid, seed); // Use Rusted Floor for the Industrial aesthetic + /** @type {RustedFloorTextureGenerator} */ this.floorGen = new RustedFloorTextureGenerator(seed); + /** @type {RustedWallTextureGenerator} */ this.wallGen = new RustedWallTextureGenerator(seed); + /** @type {GeneratedAssets} */ this.generatedAssets = { palette: {}, // New: Explicitly track valid spawn locations for teams @@ -28,6 +40,10 @@ export class RuinGenerator extends BaseGenerator { this.preloadTextures(); } + /** + * Preloads texture variations. + * @private + */ preloadTextures() { const VARIATIONS = 10; const TEXTURE_SIZE = 128; diff --git a/src/grid/VoxelGrid.js b/src/grid/VoxelGrid.js index 7e46c3b..9e4e6ae 100644 --- a/src/grid/VoxelGrid.js +++ b/src/grid/VoxelGrid.js @@ -1,33 +1,76 @@ +/** + * @typedef {import("./types.js").GridSize} GridSize + * @typedef {import("./types.js").Position} Position + * @typedef {import("./types.js").VoxelId} VoxelId + * @typedef {import("./types.js").Hazard} Hazard + * @typedef {import("./types.js").VoxelData} VoxelData + * @typedef {import("./types.js").MoveUnitOptions} MoveUnitOptions + * @typedef {import("../units/Unit.js").Unit} Unit + */ + /** * VoxelGrid.js * The spatial data structure for the game world. * Manages terrain IDs (Uint8Array) and spatial unit lookups (Map). + * @class */ export class VoxelGrid { + /** + * @param {number} width - Grid width + * @param {number} height - Grid height + * @param {number} depth - Grid depth + */ constructor(width, height, depth) { + /** @type {GridSize} */ this.size = { x: width, y: height, z: depth }; // Flat array for terrain IDs (0=Air, 1=Floor, 10=Cover, etc.) + /** @type {Uint8Array} */ this.cells = new Uint8Array(width * height * depth); // Spatial Hash for Units: "x,y,z" -> UnitObject + /** @type {Map} */ this.unitMap = new Map(); // Hazard Map: "x,y,z" -> { id, duration } + /** @type {Map} */ this.hazardMap = new Map(); } // --- COORDINATE HELPERS --- + /** + * Generates a key string from coordinates. + * @param {number | Position} x - X coordinate or position object + * @param {number} [y] - Y coordinate + * @param {number} [z] - Z coordinate + * @returns {string} - Key string + * @private + */ _key(x, y, z) { // Handle object input {x,y,z} or raw args if (typeof x === "object") return `${x.x},${x.y},${x.z}`; return `${x},${y},${z}`; } + /** + * Calculates array index from coordinates. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate + * @returns {number} - Array index + * @private + */ _index(x, y, z) { return y * this.size.x * this.size.z + z * this.size.x + x; } + /** + * Checks if coordinates are within bounds. + * @param {number | Position} x - X coordinate or position object + * @param {number} [y] - Y coordinate + * @param {number} [z] - Z coordinate + * @returns {boolean} - True if within bounds + */ isValidBounds(x, y, z) { // Handle object input if (typeof x === "object") { @@ -48,11 +91,25 @@ export class VoxelGrid { // --- CORE VOXEL MANIPULATION --- + /** + * Gets the voxel ID at the specified coordinates. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate + * @returns {VoxelId} - Voxel ID (0 for air) + */ getCell(x, y, z) { if (!this.isValidBounds(x, y, z)) return 0; // Out of bounds is Air return this.cells[this._index(x, y, z)]; } + /** + * Sets the voxel ID at the specified coordinates. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate + * @param {VoxelId} id - Voxel ID to set + */ setCell(x, y, z, id) { if (this.isValidBounds(x, y, z)) { this.cells[this._index(x, y, z)] = id; @@ -62,6 +119,7 @@ export class VoxelGrid { /** * Fills the entire grid with a specific ID. * Used by RuinGenerator to create a solid block before carving. + * @param {VoxelId} id - Voxel ID to fill with */ fill(id) { this.cells.fill(id); @@ -70,6 +128,7 @@ export class VoxelGrid { /** * Creates a copy of the grid data. * Used by Cellular Automata for smoothing passes. + * @returns {VoxelGrid} - Cloned grid */ clone() { const newGrid = new VoxelGrid(this.size.x, this.size.y, this.size.z); @@ -79,27 +138,49 @@ export class VoxelGrid { // --- QUERY & PHYSICS --- + /** + * Checks if a position is solid. + * @param {Position} pos - Position to check + * @returns {boolean} - True if solid + */ isSolid(pos) { const id = this.getCell(pos.x, pos.y, pos.z); return id !== 0; // 0 is Air } + /** + * Checks if a position is occupied by a unit. + * @param {Position} pos - Position to check + * @returns {boolean} - True if occupied + */ isOccupied(pos) { return this.unitMap.has(this._key(pos)); } + /** + * Gets the unit at a position. + * @param {Position} pos - Position to check + * @returns {Unit | undefined} - Unit at position or undefined + */ getUnitAt(pos) { return this.unitMap.get(this._key(pos)); } /** * Returns true if the voxel is destructible cover (IDs 10-20). + * @param {Position} pos - Position to check + * @returns {boolean} - True if destructible */ isDestructible(pos) { const id = this.getCell(pos.x, pos.y, pos.z); return id >= 10 && id <= 20; } + /** + * Destroys a destructible voxel. + * @param {Position} pos - Position to destroy + * @returns {boolean} - True if destroyed + */ destroyVoxel(pos) { if (this.isDestructible(pos)) { this.setCell(pos.x, pos.y, pos.z, 0); // Turn to Air @@ -112,6 +193,10 @@ export class VoxelGrid { /** * Helper for AI to find cover or hazards. * Returns list of {x,y,z,id} objects within radius. + * @param {Position} center - Center position + * @param {number} radius - Search radius + * @param {((id: VoxelId, x: number, y: number, z: number) => boolean) | null} [filterFn] - Optional filter function + * @returns {VoxelData[]} - Array of voxel data */ getVoxelsInRadius(center, radius, filterFn = null) { const results = []; @@ -135,6 +220,11 @@ export class VoxelGrid { // --- UNIT MOVEMENT --- + /** + * Places a unit at a position. + * @param {Unit} unit - Unit to place + * @param {Position} pos - Target position + */ placeUnit(unit, pos) { // Remove from old location if (unit.position) { @@ -151,6 +241,13 @@ export class VoxelGrid { this.unitMap.set(this._key(pos), unit); } + /** + * Moves a unit to a target position. + * @param {Unit} unit - Unit to move + * @param {Position} targetPos - Target position + * @param {MoveUnitOptions} [options] - Move options + * @returns {boolean} - True if moved successfully + */ moveUnit(unit, targetPos, options = {}) { if (!this.isValidBounds(targetPos)) return false; @@ -168,12 +265,23 @@ export class VoxelGrid { // --- HAZARDS --- + /** + * Adds a hazard at a position. + * @param {Position} pos - Position to add hazard + * @param {string} typeId - Hazard type ID + * @param {number} duration - Hazard duration + */ addHazard(pos, typeId, duration) { if (this.isValidBounds(pos)) { this.hazardMap.set(this._key(pos), { id: typeId, duration }); } } + /** + * Gets the hazard at a position. + * @param {Position} pos - Position to check + * @returns {Hazard | undefined} - Hazard data or undefined + */ getHazardAt(pos) { return this.hazardMap.get(this._key(pos)); } diff --git a/src/grid/VoxelManager.js b/src/grid/VoxelManager.js index fcbcf0d..5ed3aaa 100644 --- a/src/grid/VoxelManager.js +++ b/src/grid/VoxelManager.js @@ -1,20 +1,34 @@ +/** + * @typedef {import("./types.js").GeneratedAssets} GeneratedAssets + * @typedef {import("./VoxelGrid.js").VoxelGrid} VoxelGrid + */ + import * as THREE from "three"; /** * VoxelManager.js * Handles the Three.js rendering of the VoxelGrid data. * Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels. + * @class */ export class VoxelManager { + /** + * @param {VoxelGrid} grid - Voxel grid to render + * @param {THREE.Scene} scene - Three.js scene + */ constructor(grid, scene) { + /** @type {VoxelGrid} */ this.grid = grid; + /** @type {THREE.Scene} */ this.scene = scene; // Map of Voxel ID -> InstancedMesh + /** @type {Map} */ this.meshes = new Map(); // Default Material Definitions (Fallback) // Store actual Material instances, not just configs + /** @type {Record} */ this.materials = { 1: new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.8 }), // Stone 2: new THREE.MeshStandardMaterial({ color: 0x3d2817, roughness: 1.0 }), // Dirt/Floor Base @@ -26,9 +40,11 @@ export class VoxelManager { }; // Shared Geometry + /** @type {THREE.BoxGeometry} */ this.geometry = new THREE.BoxGeometry(1, 1, 1); // Camera Anchor: Invisible object to serve as OrbitControls target + /** @type {THREE.Object3D} */ this.focusTarget = new THREE.Object3D(); this.focusTarget.name = "CameraFocusTarget"; this.scene.add(this.focusTarget); @@ -38,6 +54,7 @@ export class VoxelManager { * Updates the material definitions with generated assets. * Supports both simple Canvas textures and complex {diffuse, emissive, normal, roughness, bump} objects. * NOW SUPPORTS: 'palette' for batch loading procedural variations. + * @param {GeneratedAssets} assets - Generated assets from world generator */ updateMaterials(assets) { if (!assets) return; @@ -278,7 +295,7 @@ export class VoxelManager { /** * Helper to center the camera view on the grid. - * @param {Object} controls - The OrbitControls instance to update + * @param {import("three/examples/jsm/controls/OrbitControls.js").OrbitControls} controls - The OrbitControls instance to update */ focusCamera(controls) { if (controls && this.focusTarget) { @@ -287,6 +304,12 @@ export class VoxelManager { } } + /** + * Updates a single voxel (triggers full update). + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate + */ updateVoxel(x, y, z) { this.update(); } diff --git a/src/index.js b/src/index.js index 36a5d61..6078921 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,18 @@ import { gameStateManager } from "./core/GameStateManager.js"; +/** @type {HTMLElement | null} */ const gameViewport = document.querySelector("game-viewport"); +/** @type {HTMLElement | null} */ const teamBuilder = document.querySelector("team-builder"); +/** @type {HTMLElement | null} */ const mainMenu = document.getElementById("main-menu"); +/** @type {HTMLElement | null} */ const btnNewRun = document.getElementById("btn-start"); +/** @type {HTMLElement | null} */ const btnContinue = document.getElementById("btn-load"); +/** @type {HTMLElement | null} */ const loadingOverlay = document.getElementById("loading-overlay"); +/** @type {HTMLElement | null} */ const loadingMessage = document.getElementById("loading-message"); // --- Event Listeners --- diff --git a/src/managers/MissionManager.js b/src/managers/MissionManager.js index a4a46ca..cfbd704 100644 --- a/src/managers/MissionManager.js +++ b/src/managers/MissionManager.js @@ -1,31 +1,52 @@ +/** + * @typedef {import("./types.js").MissionDefinition} MissionDefinition + * @typedef {import("./types.js").MissionSaveData} MissionSaveData + * @typedef {import("./types.js").Objective} Objective + * @typedef {import("./types.js").GameEventData} GameEventData + */ + import tutorialMission from '../assets/data/missions/mission_tutorial_01.json' with { type: 'json' }; import { narrativeManager } from './NarrativeManager.js'; /** * MissionManager.js * Manages campaign progression, mission selection, narrative triggers, and objective tracking. + * @class */ export class MissionManager { constructor() { // Campaign State + /** @type {string | null} */ this.activeMissionId = null; + /** @type {Set} */ this.completedMissions = new Set(); + /** @type {Map} */ this.missionRegistry = new Map(); // Active Run State + /** @type {MissionDefinition | null} */ this.currentMissionDef = null; + /** @type {Objective[]} */ this.currentObjectives = []; // Register default missions this.registerMission(tutorialMission); } + /** + * Registers a mission definition. + * @param {MissionDefinition} missionDef - Mission definition to register + */ registerMission(missionDef) { this.missionRegistry.set(missionDef.id, missionDef); } // --- PERSISTENCE (Campaign) --- + /** + * Loads campaign save data. + * @param {MissionSaveData} saveData - Save data to load + */ load(saveData) { this.completedMissions = new Set(saveData.completedMissions || []); // Default to Tutorial if history is empty @@ -34,6 +55,10 @@ export class MissionManager { } } + /** + * Saves campaign data. + * @returns {MissionSaveData} - Serialized campaign data + */ save() { return { completedMissions: Array.from(this.completedMissions) @@ -44,6 +69,7 @@ export class MissionManager { /** * Gets the configuration for the currently selected mission. + * @returns {MissionDefinition | undefined} - Active mission definition */ getActiveMission() { if (!this.activeMissionId) return this.missionRegistry.get('MISSION_TUTORIAL_01'); @@ -133,7 +159,7 @@ export class MissionManager { /** * Called by GameLoop whenever a relevant event occurs. * @param {string} type - 'ENEMY_DEATH', 'TURN_END', etc. - * @param {Object} data - Context data + * @param {GameEventData} data - Context data */ onGameEvent(type, data) { if (!this.currentObjectives.length) return; diff --git a/src/managers/NarrativeManager.js b/src/managers/NarrativeManager.js index 1781a13..7601599 100644 --- a/src/managers/NarrativeManager.js +++ b/src/managers/NarrativeManager.js @@ -1,19 +1,28 @@ +/** + * @typedef {import("./types.js").NarrativeSequence} NarrativeSequence + * @typedef {import("./types.js").NarrativeNode} NarrativeNode + */ + /** * NarrativeManager.js * Manages the flow of story events, dialogue, and tutorials. * Extends EventTarget to broadcast UI updates to the DialogueOverlay. + * @class */ export class NarrativeManager extends EventTarget { constructor() { super(); + /** @type {NarrativeSequence | null} */ this.currentSequence = null; + /** @type {NarrativeNode | null} */ this.currentNode = null; + /** @type {Set} */ this.history = new Set(); // Track IDs of played sequences } /** * Loads and starts a narrative sequence. - * @param {Object} sequenceData - The JSON object of the conversation (from assets/data/narrative/). + * @param {NarrativeSequence} sequenceData - The JSON object of the conversation (from assets/data/narrative/). */ startSequence(sequenceData) { if (!sequenceData || !sequenceData.nodes) { diff --git a/src/managers/RosterManager.js b/src/managers/RosterManager.js index b1a1be6..683293d 100644 --- a/src/managers/RosterManager.js +++ b/src/managers/RosterManager.js @@ -1,17 +1,27 @@ +/** + * @typedef {import("../units/types.js").ExplorerData} ExplorerData + * @typedef {import("../units/types.js").RosterSaveData} RosterSaveData + */ + /** * RosterManager.js * Manages the persistent pool of Explorer units (The Barracks). * Handles recruitment, death, and selection for missions. + * @class */ export class RosterManager { constructor() { + /** @type {ExplorerData[]} */ this.roster = []; // List of active Explorer objects (Data only) + /** @type {ExplorerData[]} */ this.graveyard = []; // List of dead units + /** @type {number} */ this.rosterLimit = 12; } /** * Initializes the roster from saved data. + * @param {RosterSaveData} saveData - Saved roster data */ load(saveData) { this.roster = saveData.roster || []; @@ -20,6 +30,7 @@ export class RosterManager { /** * Serializes for save file. + * @returns {RosterSaveData} - Serialized roster data */ save() { return { @@ -30,7 +41,8 @@ export class RosterManager { /** * Adds a new unit to the roster. - * @param {Object} unitData - The unit definition (Class, Name, Stats) + * @param {Partial} unitData - The unit definition (Class, Name, Stats) + * @returns {ExplorerData | false} - The recruited unit or false if roster is full */ recruitUnit(unitData) { if (this.roster.length >= this.rosterLimit) { @@ -51,6 +63,7 @@ export class RosterManager { /** * Marks a unit as dead and moves them to the graveyard. + * @param {string} unitId - Unit ID to mark as dead */ handleUnitDeath(unitId) { const index = this.roster.findIndex((u) => u.id === unitId); @@ -65,6 +78,7 @@ export class RosterManager { /** * Returns units eligible for a mission. * Filters out injured or dead units. + * @returns {ExplorerData[]} - Array of deployable units */ getDeployableUnits() { return this.roster.filter((u) => u.status === "READY"); diff --git a/src/managers/UnitManager.js b/src/managers/UnitManager.js index 83abc83..2ee5b28 100644 --- a/src/managers/UnitManager.js +++ b/src/managers/UnitManager.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("./types.js").UnitRegistry} UnitRegistry + * @typedef {import("../units/types.js").UnitDefinition} UnitDefinition + * @typedef {import("../units/types.js").Position} Position + * @typedef {import("../units/types.js").UnitTeam} UnitTeam + */ + import { Unit } from "../units/Unit.js"; import { Explorer } from "../units/Explorer.js"; import { Enemy } from "../units/Enemy.js"; @@ -6,14 +13,18 @@ import { Enemy } from "../units/Enemy.js"; * UnitManager.js * Manages the lifecycle (creation, tracking, death) of all active units. * Acts as the Source of Truth for "Who is alive?" and "Where are they relative to me?". + * @class */ export class UnitManager { /** - * @param {Object} registry - Map or Object containing Unit Definitions (Stats, Models). + * @param {UnitRegistry} registry - Map or Object containing Unit Definitions (Stats, Models). */ constructor(registry) { + /** @type {UnitRegistry} */ this.registry = registry; + /** @type {Map} */ this.activeUnits = new Map(); // ID -> Unit Instance + /** @type {number} */ this.nextInstanceId = 0; } @@ -22,7 +33,8 @@ export class UnitManager { /** * Factory method to spawn a unit from a template ID. * @param {string} defId - The definition ID (e.g. 'CLASS_VANGUARD', 'ENEMY_SENTINEL') - * @param {string} team - 'PLAYER', 'ENEMY', 'NEUTRAL' + * @param {UnitTeam} team - 'PLAYER', 'ENEMY', 'NEUTRAL' + * @returns {Unit | null} - Created unit or null if definition not found */ createUnit(defId, team) { // Support both Map interface and Object interface for registry @@ -63,6 +75,11 @@ export class UnitManager { return unit; } + /** + * Removes a unit from the active units map. + * @param {string} unitId - Unit ID to remove + * @returns {boolean} - True if unit was removed + */ removeUnit(unitId) { if (this.activeUnits.has(unitId)) { const unit = this.activeUnits.get(unitId); @@ -75,14 +92,28 @@ export class UnitManager { // --- QUERIES --- + /** + * Gets a unit by ID. + * @param {string} id - Unit ID + * @returns {Unit | undefined} - Unit instance or undefined + */ getUnitById(id) { return this.activeUnits.get(id); } + /** + * Gets all active units. + * @returns {Unit[]} - Array of all active units + */ getAllUnits() { return Array.from(this.activeUnits.values()); } + /** + * Gets all units on a specific team. + * @param {UnitTeam} team - Team to filter by + * @returns {Unit[]} - Array of units on the team + */ getUnitsByTeam(team) { return this.getAllUnits().filter((u) => u.team === team); } @@ -90,9 +121,10 @@ export class UnitManager { /** * Finds all units within 'range' of 'centerPos'. * Used for AoE spells, Auras, and AI scanning. - * @param {Object} centerPos - {x, y, z} + * @param {Position} centerPos - {x, y, z} * @param {number} range - Distance in tiles (Manhattan) - * @param {string} [filterTeam] - Optional: Only return units of this team + * @param {UnitTeam | null} [filterTeam] - Optional: Only return units of this team + * @returns {Unit[]} - Array of units in range */ getUnitsInRange(centerPos, range, filterTeam = null) { const result = []; diff --git a/src/units/Enemy.js b/src/units/Enemy.js index b69e3ba..b2f79a8 100644 --- a/src/units/Enemy.js +++ b/src/units/Enemy.js @@ -1,20 +1,34 @@ +/** + * @typedef {import("./types.js").UnitDefinition} UnitDefinition + */ + import { Unit } from "./Unit.js"; /** * Enemy.js * NPC Unit controlled by the AI Controller. + * @class */ export class Enemy extends Unit { + /** + * @param {string} id - Unique unit identifier + * @param {string} name - Enemy name + * @param {UnitDefinition} def - Enemy definition + */ constructor(id, name, def) { // Construct with ID, Name, Type='ENEMY', and ModelID from def super(id, name, "ENEMY", def.model || "MODEL_ENEMY_DEFAULT"); // AI Logic + /** @type {string} */ this.archetypeId = def.ai_archetype || "BRUISER"; // e.g., 'BRUISER', 'KITER' + /** @type {number} */ this.aggroRange = def.aggro_range || 8; // Rewards + /** @type {number} */ this.xpValue = def.xp_value || 10; + /** @type {string} */ this.lootTableId = def.loot_table || "LOOT_TIER_1_COMMON"; // Hydrate Stats diff --git a/src/units/Explorer.js b/src/units/Explorer.js index 2d70e51..aab2147 100644 --- a/src/units/Explorer.js +++ b/src/units/Explorer.js @@ -1,17 +1,31 @@ +/** + * @typedef {import("./types.js").ClassMastery} ClassMastery + * @typedef {import("./types.js").Equipment} Equipment + */ + import { Unit } from "./Unit.js"; /** * Explorer.js * Player character class supporting Multi-Class Mastery and Persistent Progression. + * @class */ export class Explorer extends Unit { + /** + * @param {string} id - Unique unit identifier + * @param {string} name - Explorer name + * @param {string} startingClassId - Starting class ID + * @param {Record} classDefinition - Class definition data + */ constructor(id, name, startingClassId, classDefinition) { super(id, name, "EXPLORER", `${startingClassId}_MODEL`); + /** @type {string} */ this.activeClassId = startingClassId; // Persistent Mastery: Tracks progress for EVERY class this character has played // Key: ClassID, Value: { level, xp, skillPoints, unlockedNodes[] } + /** @type {Record} */ this.classMastery = {}; // Initialize the starting class entry @@ -24,6 +38,7 @@ export class Explorer extends Unit { } // Inventory + /** @type {Equipment} */ this.equipment = { weapon: null, armor: null, @@ -32,10 +47,16 @@ export class Explorer extends Unit { }; // Active Skills (Populated by Skill Tree) + /** @type {unknown[]} */ this.actions = []; + /** @type {unknown[]} */ this.passives = []; } + /** + * Initializes mastery data for a class. + * @param {string} classId - Class ID to initialize + */ initializeMastery(classId) { if (!this.classMastery[classId]) { this.classMastery[classId] = { @@ -49,7 +70,7 @@ export class Explorer extends Unit { /** * Updates base stats based on the active class's base + growth rates * level. - * @param {Object} classDef - The JSON definition of the class stats. + * @param {Record} classDef - The JSON definition of the class stats. */ recalculateBaseStats(classDef) { if (classDef.id !== this.activeClassId) { @@ -81,6 +102,8 @@ export class Explorer extends Unit { /** * Swaps the active class logic. * NOTE: Does NOT check unlock requirements (handled by UI/MetaSystem). + * @param {string} newClassId - New class ID + * @param {Record} newClassDef - New class definition */ changeClass(newClassId, newClassDef) { // 1. Ensure mastery record exists @@ -101,6 +124,7 @@ export class Explorer extends Unit { /** * Adds XP to the *current* class. + * @param {number} amount - XP amount to add */ gainExperience(amount) { const mastery = this.classMastery[this.activeClassId]; @@ -108,6 +132,10 @@ export class Explorer extends Unit { // Level up logic would be handled by a system checking XP curves } + /** + * Gets the current level of the active class. + * @returns {number} - Current level + */ getLevel() { return this.classMastery[this.activeClassId].level; } diff --git a/src/units/Unit.js b/src/units/Unit.js index 560f3eb..cc117cb 100644 --- a/src/units/Unit.js +++ b/src/units/Unit.js @@ -1,28 +1,56 @@ +/** + * @typedef {import("./types.js").UnitStats} UnitStats + * @typedef {import("./types.js").Position} Position + * @typedef {import("./types.js").FacingDirection} FacingDirection + * @typedef {import("./types.js").UnitType} UnitType + * @typedef {import("./types.js").UnitTeam} UnitTeam + * @typedef {import("./types.js").StatusEffect} StatusEffect + */ + /** * Unit.js * Base class for all entities on the grid (Explorers, Enemies, Structures). + * @class */ export class Unit { + /** + * @param {string} id - Unique unit identifier + * @param {string} name - Unit name + * @param {UnitType} type - Unit type + * @param {string} voxelModelID - Voxel model identifier + */ constructor(id, name, type, voxelModelID) { + /** @type {string} */ this.id = id; + /** @type {string} */ this.name = name; + /** @type {UnitType} */ this.type = type; // 'EXPLORER', 'ENEMY', 'STRUCTURE' + /** @type {string} */ this.voxelModelID = voxelModelID; // Grid State + /** @type {Position} */ this.position = { x: 0, y: 0, z: 0 }; + /** @type {FacingDirection} */ this.facing = "NORTH"; // Combat State + /** @type {number} */ this.currentHealth = 100; + /** @type {number} */ this.maxHealth = 100; // Derived from effective stats later + /** @type {number} */ this.currentAP = 0; // Action Points for current turn + /** @type {number} */ this.chargeMeter = 0; // Dynamic Initiative (0-100) + /** @type {StatusEffect[]} */ this.statusEffects = []; // Active debuffs/buffs // Base Stats (Raw values before gear/buffs) + /** @type {UnitStats} */ this.baseStats = { health: 100, attack: 10, @@ -33,11 +61,17 @@ export class Unit { movement: 4, tech: 0, }; + + /** @type {UnitTeam | undefined} */ + this.team = undefined; } /** * Updates position. * Note: Validation happens in VoxelGrid/MovementSystem. + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @param {number} z - Z coordinate */ setPosition(x, y, z) { this.position = { x, y, z }; @@ -45,6 +79,8 @@ export class Unit { /** * Consumes AP. Returns true if successful. + * @param {number} amount - Amount of AP to spend + * @returns {boolean} - True if successful */ spendAP(amount) { if (this.currentAP >= amount) { @@ -54,6 +90,10 @@ export class Unit { return false; } + /** + * Checks if the unit is alive. + * @returns {boolean} - True if alive + */ isAlive() { return this.currentHealth > 0; } diff --git a/src/units/types.d.ts b/src/units/types.d.ts new file mode 100644 index 0000000..8b0f6c1 --- /dev/null +++ b/src/units/types.d.ts @@ -0,0 +1,115 @@ +/** + * Type definitions for unit-related types + */ + +/** + * Unit base stats + */ +export interface UnitStats { + health: number; + attack: number; + defense: number; + magic: number; + speed: number; + willpower: number; + movement: number; + tech: number; +} + +/** + * Unit position + */ +export interface Position { + x: number; + y: number; + z: number; +} + +/** + * Unit facing direction + */ +export type FacingDirection = "NORTH" | "SOUTH" | "EAST" | "WEST"; + +/** + * Unit type + */ +export type UnitType = "EXPLORER" | "ENEMY" | "STRUCTURE"; + +/** + * Unit team + */ +export type UnitTeam = "PLAYER" | "ENEMY" | "NEUTRAL"; + +/** + * Unit status + */ +export type UnitStatus = "READY" | "INJURED" | "DEPLOYED" | "DEAD"; + +/** + * Status effect + */ +export interface StatusEffect { + id: string; + type: string; + duration: number; + [key: string]: unknown; +} + +/** + * Class mastery data + */ +export interface ClassMastery { + level: number; + xp: number; + skillPoints: number; + unlockedNodes: string[]; +} + +/** + * Equipment slots + */ +export interface Equipment { + weapon: unknown | null; + armor: unknown | null; + utility: unknown | null; + relic: unknown | null; +} + +/** + * Unit definition from registry + */ +export interface UnitDefinition { + type?: UnitType; + name: string; + stats?: Partial; + model?: string; + ai_archetype?: string; + aggro_range?: number; + xp_value?: number; + loot_table?: string; + [key: string]: unknown; +} + +/** + * Explorer unit data (for roster) + */ +export interface ExplorerData { + id: string; + name: string; + classId: string; + status: UnitStatus; + history: { + missions: number; + kills: number; + }; + [key: string]: unknown; +} + +/** + * Roster save data + */ +export interface RosterSaveData { + roster: ExplorerData[]; + graveyard: ExplorerData[]; +} + diff --git a/src/utils/SeededRandom.js b/src/utils/SeededRandom.js index ce9d2cf..dd00572 100644 --- a/src/utils/SeededRandom.js +++ b/src/utils/SeededRandom.js @@ -2,17 +2,28 @@ * SeededRandom.js * A deterministic pseudo-random number generator using Mulberry32. * Essential for reproducible procedural generation. + * @class */ export class SeededRandom { + /** + * @param {number | string} seed - Random seed (number or string) + */ constructor(seed) { // Hash the string seed to a number if necessary if (typeof seed === "string") { this.state = this.hashString(seed); } else { + /** @type {number} */ this.state = seed || Math.floor(Math.random() * 2147483647); } } + /** + * Hashes a string to a number. + * @param {string} str - String to hash + * @returns {number} - Hashed value + * @private + */ hashString(str) { let hash = 1779033703 ^ str.length; for (let i = 0; i < str.length; i++) { @@ -26,7 +37,10 @@ export class SeededRandom { }; } - // Mulberry32 Algorithm + /** + * Mulberry32 Algorithm - generates next random number. + * @returns {number} - Random number between 0 and 1 + */ next() { let t = (this.state += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); @@ -34,17 +48,31 @@ export class SeededRandom { return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } - // Returns float between [min, max) + /** + * Returns float between [min, max). + * @param {number} min - Minimum value + * @param {number} max - Maximum value + * @returns {number} - Random float + */ range(min, max) { return min + this.next() * (max - min); } - // Returns integer between [min, max] (inclusive) + /** + * Returns integer between [min, max] (inclusive). + * @param {number} min - Minimum value + * @param {number} max - Maximum value + * @returns {number} - Random integer + */ rangeInt(min, max) { return Math.floor(this.range(min, max + 1)); } - // Returns true/false based on probability (0.0 - 1.0) + /** + * Returns true/false based on probability (0.0 - 1.0). + * @param {number} probability - Probability between 0 and 1 + * @returns {boolean} - True if random value is less than probability + */ chance(probability) { return this.next() < probability; } diff --git a/src/utils/SimplexNoise.js b/src/utils/SimplexNoise.js index 05bdeef..a21290b 100644 --- a/src/utils/SimplexNoise.js +++ b/src/utils/SimplexNoise.js @@ -2,10 +2,14 @@ * SimplexNoise.js * A dependency-free, seeded 2D Simplex Noise implementation. * Based on the standard Stefan Gustavson algorithm. + * @class */ import { SeededRandom } from "./SeededRandom.js"; export class SimplexNoise { + /** + * @param {number | string | SeededRandom} seedOrRng - Seed value or SeededRandom instance + */ constructor(seedOrRng) { // Allow passing a seed OR an existing RNG instance const rng = @@ -14,6 +18,7 @@ export class SimplexNoise { : new SeededRandom(seedOrRng); // 1. Build Permutation Table + /** @type {Uint8Array} */ this.p = new Uint8Array(256); for (let i = 0; i < 256; i++) { this.p[i] = i; @@ -28,7 +33,9 @@ export class SimplexNoise { } // Duplicate for overflow handling + /** @type {Uint8Array} */ this.perm = new Uint8Array(512); + /** @type {Uint8Array} */ this.permMod12 = new Uint8Array(512); for (let i = 0; i < 512; i++) { this.perm[i] = this.p[i & 255]; @@ -36,6 +43,7 @@ export class SimplexNoise { } // Gradient vectors + /** @type {number[]} */ this.grad3 = [ 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, @@ -45,6 +53,9 @@ export class SimplexNoise { /** * Samples 2D Noise at coordinates x, y. * Returns a value roughly between -1.0 and 1.0. + * @param {number} xin - X coordinate + * @param {number} yin - Y coordinate + * @returns {number} - Noise value */ noise2D(xin, yin) { let n0, n1, n2; // Noise contributions from the three corners @@ -113,6 +124,15 @@ export class SimplexNoise { return 70.0 * (n0 + n1 + n2); } + /** + * Dot product helper. + * @param {number[]} g - Gradient array + * @param {number} gi - Gradient index + * @param {number} x - X component + * @param {number} y - Y component + * @returns {number} - Dot product + * @private + */ dot(g, gi, x, y) { return g[gi * 3] * x + g[gi * 3 + 1] * y; }