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