Enhance game state management by adding detailed type definitions and documentation for GameStateManager, GameLoop, Persistence, and other core components. Improve input handling in InputManager and refine unit management in RosterManager and UnitManager. Update VoxelGrid and VoxelManager for better spatial data handling and rendering. Integrate new features for mission management and narrative flow, ensuring comprehensive coverage of game mechanics and state transitions.

This commit is contained in:
Matthew Mone 2025-12-22 12:55:41 -08:00
parent 33a64c460c
commit 8d037bcd4d
20 changed files with 913 additions and 14 deletions

View file

@ -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<string, THREE.Mesh>} */
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<number, Unit> }} */
this.deploymentState = {
selectedUnitIndex: -1,
deployedUnits: new Map(), // Map<Index, UnitInstance>
};
/** @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<void>}
*/
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<void>}
*/
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();

View file

@ -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<string, GameState>} */
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<void>} */
#gameLoopInitialized = Promise.withResolvers();
/**
* @returns {Promise<void>}
*/
get gameLoopInitialized() {
return this.#gameLoopInitialized.promise;
}
/** @type {PromiseWithResolvers<unknown>} */
#rosterLoaded = Promise.withResolvers();
/**
* @returns {Promise<unknown>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<EmbarkEventDetail>} e - The embark event
* @returns {Promise<void>}
*/
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<void>}
* @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<void>}
* @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<void>}
* @private
*/
async _checkSaveGame() {
const save = await this.persistence.loadRun();
window.dispatchEvent(
@ -204,6 +285,11 @@ class GameStateManagerClass {
);
}
/**
* Saves the current roster.
* @returns {Promise<void>}
* @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;

View file

@ -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<string>} */
this.keys = new Set();
/** @type {Map<number, { buttons: boolean[]; axes: number[] }>} */
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(

View file

@ -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<void>}
*/
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<void>}
*/
async saveRun(runData) {
if (!this.db) await this.init();
return this._put(RUN_STORE, { ...runData, id: "active_run" });
}
/**
* Loads run data.
* @returns {Promise<RunData | undefined>}
*/
async loadRun() {
if (!this.db) await this.init();
return this._get(RUN_STORE, "active_run");
}
/**
* Clears saved run data.
* @returns {Promise<void>}
*/
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<void>}
*/
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<RosterSaveData | null>}
*/
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<void>}
* @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<unknown>}
* @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<void>}
* @private
*/
_delete(storeName, key) {
return new Promise((resolve, reject) => {
const tx = this.db.transaction([storeName], "readwrite");

103
src/core/types.d.ts vendored Normal file
View file

@ -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<string, unknown>;
}
/**
* 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<void>;
rosterLoaded: Promise<unknown>;
setGameLoop(loop: GameLoop): void;
reset(): void;
init(): Promise<void>;
transitionTo(newState: GameState, payload?: unknown): Promise<void>;
startNewGame(): void;
continueGame(): Promise<void>;
handleEmbark(e: CustomEvent<EmbarkEventDetail>): Promise<void>;
}

View file

@ -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++) {

View file

@ -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;

View file

@ -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<string, Unit>} */
this.unitMap = new Map();
// Hazard Map: "x,y,z" -> { id, duration }
/** @type {Map<string, Hazard>} */
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));
}

View file

@ -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<number, THREE.InstancedMesh>} */
this.meshes = new Map();
// Default Material Definitions (Fallback)
// Store actual Material instances, not just configs
/** @type {Record<number, THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[]>} */
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();
}

View file

@ -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 ---

View file

@ -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<string>} */
this.completedMissions = new Set();
/** @type {Map<string, MissionDefinition>} */
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;

View file

@ -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<string>} */
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) {

View file

@ -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<ExplorerData>} 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");

View file

@ -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<string, Unit>} */
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 = [];

View file

@ -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

View file

@ -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<string, unknown>} 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<string, ClassMastery>} */
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<string, unknown>} 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<string, unknown>} 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;
}

View file

@ -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;
}

115
src/units/types.d.ts vendored Normal file
View file

@ -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<UnitStats>;
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[];
}

View file

@ -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;
}

View file

@ -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;
}