Enhance game state management and UI integration for combat and deployment phases. Introduce CombatHUD and update DeploymentHUD to reflect current game state. Refactor GameLoop and GameStateManager to manage state transitions more effectively. Implement asset copying for JSON files in the build process. Add tests for new functionalities and ensure proper state handling during gameplay.
This commit is contained in:
parent
5797d9ac68
commit
da55cafc8f
11 changed files with 310 additions and 65 deletions
30
build.js
30
build.js
|
|
@ -6,8 +6,9 @@ import { fileURLToPath } from "url";
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
// Image file extensions to copy
|
// File extensions to copy
|
||||||
const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
||||||
|
const DATA_EXTENSIONS = [".json"];
|
||||||
|
|
||||||
// Recursively copy image files from src to dist
|
// Recursively copy image files from src to dist
|
||||||
function copyImages(srcDir, distDir) {
|
function copyImages(srcDir, distDir) {
|
||||||
|
|
@ -33,6 +34,30 @@ function copyImages(srcDir, distDir) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively copy data files (JSON, markdown, TypeScript definitions) from assets to dist
|
||||||
|
function copyAssets(srcDir, distDir) {
|
||||||
|
const entries = readdirSync(srcDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const srcPath = join(srcDir, entry.name);
|
||||||
|
const distPath = join(distDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
mkdirSync(distPath, { recursive: true });
|
||||||
|
copyAssets(srcPath, distPath);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
const lastDot = entry.name.lastIndexOf(".");
|
||||||
|
if (lastDot !== -1) {
|
||||||
|
const ext = entry.name.toLowerCase().substring(lastDot);
|
||||||
|
if (DATA_EXTENSIONS.includes(ext)) {
|
||||||
|
mkdirSync(distDir, { recursive: true });
|
||||||
|
copyFileSync(srcPath, distPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure dist directory exists
|
// Ensure dist directory exists
|
||||||
mkdirSync("dist", { recursive: true });
|
mkdirSync("dist", { recursive: true });
|
||||||
|
|
||||||
|
|
@ -53,4 +78,7 @@ copyFileSync("src/index.html", "dist/index.html");
|
||||||
// Copy images
|
// Copy images
|
||||||
copyImages("src", "dist");
|
copyImages("src", "dist");
|
||||||
|
|
||||||
|
// Copy assets (JSON, markdown, TypeScript definitions)
|
||||||
|
copyAssets("src/assets", "dist/assets");
|
||||||
|
|
||||||
console.log("Build complete!");
|
console.log("Build complete!");
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { MissionManager } from "../managers/MissionManager.js";
|
||||||
export class GameLoop {
|
export class GameLoop {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.isRunning = false;
|
this.isRunning = false;
|
||||||
this.phase = "INIT";
|
|
||||||
|
|
||||||
// 1. Core Systems
|
// 1. Core Systems
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
|
|
@ -184,7 +183,10 @@ export class GameLoop {
|
||||||
const cursor = this.inputManager.getCursorPosition();
|
const cursor = this.inputManager.getCursorPosition();
|
||||||
console.log("Action at:", cursor);
|
console.log("Action at:", cursor);
|
||||||
|
|
||||||
if (this.phase === "DEPLOYMENT") {
|
if (
|
||||||
|
this.gameStateManager &&
|
||||||
|
this.gameStateManager.currentState === "STATE_DEPLOYMENT"
|
||||||
|
) {
|
||||||
const selIndex = this.deploymentState.selectedUnitIndex;
|
const selIndex = this.deploymentState.selectedUnitIndex;
|
||||||
|
|
||||||
if (selIndex !== -1) {
|
if (selIndex !== -1) {
|
||||||
|
|
@ -227,7 +229,6 @@ export class GameLoop {
|
||||||
console.log("GameLoop: Generating Level...");
|
console.log("GameLoop: Generating Level...");
|
||||||
this.runData = runData;
|
this.runData = runData;
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
this.phase = "DEPLOYMENT";
|
|
||||||
this.clearUnitMeshes();
|
this.clearUnitMeshes();
|
||||||
|
|
||||||
// Reset Deployment State
|
// Reset Deployment State
|
||||||
|
|
@ -299,7 +300,11 @@ export class GameLoop {
|
||||||
}
|
}
|
||||||
|
|
||||||
deployUnit(unitDef, targetTile, existingUnit = null) {
|
deployUnit(unitDef, targetTile, existingUnit = null) {
|
||||||
if (this.phase !== "DEPLOYMENT") return null;
|
if (
|
||||||
|
!this.gameStateManager ||
|
||||||
|
this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
const isValid = this.validateDeploymentCursor(
|
const isValid = this.validateDeploymentCursor(
|
||||||
targetTile.x,
|
targetTile.x,
|
||||||
|
|
@ -358,7 +363,11 @@ export class GameLoop {
|
||||||
}
|
}
|
||||||
|
|
||||||
finalizeDeployment() {
|
finalizeDeployment() {
|
||||||
if (this.phase !== "DEPLOYMENT") return;
|
if (
|
||||||
|
!this.gameStateManager ||
|
||||||
|
this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
|
||||||
|
)
|
||||||
|
return;
|
||||||
const enemyCount = 2;
|
const enemyCount = 2;
|
||||||
for (let i = 0; i < enemyCount; i++) {
|
for (let i = 0; i < enemyCount; i++) {
|
||||||
const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length);
|
const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length);
|
||||||
|
|
@ -370,11 +379,15 @@ export class GameLoop {
|
||||||
this.enemySpawnZone.splice(spotIndex, 1);
|
this.enemySpawnZone.splice(spotIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.phase = "ACTIVE";
|
|
||||||
|
|
||||||
// Switch to standard movement validator for the game
|
// Switch to standard movement validator for the game
|
||||||
this.inputManager.setValidator(this.validateCursorMove.bind(this));
|
this.inputManager.setValidator(this.validateCursorMove.bind(this));
|
||||||
|
|
||||||
|
// Notify GameStateManager about state change
|
||||||
|
if (this.gameStateManager) {
|
||||||
|
this.gameStateManager.transitionTo("STATE_COMBAT");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Combat Started!");
|
console.log("Combat Started!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ class GameStateManagerClass {
|
||||||
INIT: "STATE_INIT",
|
INIT: "STATE_INIT",
|
||||||
MAIN_MENU: "STATE_MAIN_MENU",
|
MAIN_MENU: "STATE_MAIN_MENU",
|
||||||
TEAM_BUILDER: "STATE_TEAM_BUILDER",
|
TEAM_BUILDER: "STATE_TEAM_BUILDER",
|
||||||
GAME_RUN: "STATE_GAME_RUN",
|
DEPLOYMENT: "STATE_DEPLOYMENT",
|
||||||
|
COMBAT: "STATE_COMBAT",
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -40,6 +41,18 @@ class GameStateManagerClass {
|
||||||
this.#gameLoopInitialized.resolve();
|
this.#gameLoopInitialized.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
// Reset singleton state for testing
|
||||||
|
this.currentState = GameStateManagerClass.STATES.INIT;
|
||||||
|
this.gameLoop = null;
|
||||||
|
this.activeRunData = null;
|
||||||
|
this.rosterManager = new RosterManager();
|
||||||
|
this.missionManager = new MissionManager();
|
||||||
|
// Reset promise resolvers
|
||||||
|
this.#gameLoopInitialized = Promise.withResolvers();
|
||||||
|
this.#rosterLoaded = Promise.withResolvers();
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
console.log("System: Initializing State Manager...");
|
console.log("System: Initializing State Manager...");
|
||||||
await this.persistence.init();
|
await this.persistence.init();
|
||||||
|
|
@ -58,9 +71,14 @@ class GameStateManagerClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
async transitionTo(newState, payload = null) {
|
async transitionTo(newState, payload = null) {
|
||||||
console.log(`State Transition: ${this.currentState} -> ${newState}`);
|
|
||||||
const oldState = this.currentState;
|
const oldState = this.currentState;
|
||||||
this.currentState = newState;
|
const stateChanged = oldState !== newState;
|
||||||
|
|
||||||
|
// Only update state and run switch logic if state actually changed
|
||||||
|
if (stateChanged) {
|
||||||
|
console.log(`State Transition: ${oldState} -> ${newState}`);
|
||||||
|
this.currentState = newState;
|
||||||
|
}
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("gamestate-changed", {
|
new CustomEvent("gamestate-changed", {
|
||||||
|
|
@ -68,19 +86,19 @@ class GameStateManagerClass {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (newState) {
|
// Only run state-specific logic if state actually changed
|
||||||
case GameStateManagerClass.STATES.MAIN_MENU:
|
if (stateChanged) {
|
||||||
if (this.gameLoop) this.gameLoop.stop();
|
switch (newState) {
|
||||||
await this._checkSaveGame();
|
case GameStateManagerClass.STATES.MAIN_MENU:
|
||||||
break;
|
if (this.gameLoop) this.gameLoop.stop();
|
||||||
|
await this._checkSaveGame();
|
||||||
|
break;
|
||||||
|
|
||||||
case GameStateManagerClass.STATES.GAME_RUN:
|
case GameStateManagerClass.STATES.DEPLOYMENT:
|
||||||
if (!this.activeRunData && payload) {
|
case GameStateManagerClass.STATES.COMBAT:
|
||||||
await this._initializeRun(payload);
|
// These states are handled by GameLoop, no special logic needed here
|
||||||
} else {
|
break;
|
||||||
await this._resumeRun();
|
}
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,11 +110,12 @@ class GameStateManagerClass {
|
||||||
const save = await this.persistence.loadRun();
|
const save = await this.persistence.loadRun();
|
||||||
if (save) {
|
if (save) {
|
||||||
this.activeRunData = save;
|
this.activeRunData = save;
|
||||||
this.transitionTo(GameStateManagerClass.STATES.GAME_RUN);
|
// Will transition to DEPLOYMENT after run is initialized
|
||||||
|
await this._resumeRun();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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") {
|
||||||
e.detail.squad.forEach((unit) => {
|
e.detail.squad.forEach((unit) => {
|
||||||
|
|
@ -106,7 +125,8 @@ class GameStateManagerClass {
|
||||||
});
|
});
|
||||||
this._saveRoster();
|
this._saveRoster();
|
||||||
}
|
}
|
||||||
this.transitionTo(GameStateManagerClass.STATES.GAME_RUN, e.detail.squad);
|
// Will transition to DEPLOYMENT after run is initialized
|
||||||
|
await this._initializeRun(e.detail.squad);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INTERNAL HELPERS ---
|
// --- INTERNAL HELPERS ---
|
||||||
|
|
@ -147,7 +167,11 @@ class GameStateManagerClass {
|
||||||
|
|
||||||
// Pass the Mission Manager to the Game Loop so it can report events (Deaths, etc)
|
// Pass the Mission Manager to the Game Loop so it can report events (Deaths, etc)
|
||||||
this.gameLoop.missionManager = this.missionManager;
|
this.gameLoop.missionManager = this.missionManager;
|
||||||
this.gameLoop.startLevel(this.activeRunData);
|
// Give GameLoop a reference to GameStateManager so it can notify about state changes
|
||||||
|
this.gameLoop.gameStateManager = this;
|
||||||
|
await this.gameLoop.startLevel(this.activeRunData);
|
||||||
|
// Transition to deployment state after level is initialized
|
||||||
|
this.transitionTo(GameStateManagerClass.STATES.DEPLOYMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resumeRun() {
|
async _resumeRun() {
|
||||||
|
|
@ -155,8 +179,13 @@ class GameStateManagerClass {
|
||||||
if (this.activeRunData) {
|
if (this.activeRunData) {
|
||||||
// Re-hook the mission manager
|
// Re-hook the mission manager
|
||||||
this.gameLoop.missionManager = this.missionManager;
|
this.gameLoop.missionManager = this.missionManager;
|
||||||
|
// Give GameLoop a reference to GameStateManager so it can notify about state changes
|
||||||
|
this.gameLoop.gameStateManager = this;
|
||||||
// TODO: Ideally we reload the mission state from the save file here
|
// TODO: Ideally we reload the mission state from the save file here
|
||||||
this.gameLoop.startLevel(this.activeRunData);
|
await this.gameLoop.startLevel(this.activeRunData);
|
||||||
|
// Transition to appropriate state based on save data
|
||||||
|
// For now, always go to deployment
|
||||||
|
this.transitionTo(GameStateManagerClass.STATES.DEPLOYMENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
src/index.js
16
src/index.js
|
|
@ -20,7 +20,8 @@ window.addEventListener("gamestate-changed", async (e) => {
|
||||||
case "STATE_TEAM_BUILDER":
|
case "STATE_TEAM_BUILDER":
|
||||||
loadingMessage.textContent = "INITIALIZING TEAM BUILDER...";
|
loadingMessage.textContent = "INITIALIZING TEAM BUILDER...";
|
||||||
break;
|
break;
|
||||||
case "STATE_GAME_RUN":
|
case "STATE_DEPLOYMENT":
|
||||||
|
case "STATE_COMBAT":
|
||||||
loadingMessage.textContent = "INITIALIZING GAME ENGINE...";
|
loadingMessage.textContent = "INITIALIZING GAME ENGINE...";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +38,8 @@ window.addEventListener("gamestate-changed", async (e) => {
|
||||||
await import("./ui/team-builder.js");
|
await import("./ui/team-builder.js");
|
||||||
teamBuilder.toggleAttribute("hidden", false);
|
teamBuilder.toggleAttribute("hidden", false);
|
||||||
break;
|
break;
|
||||||
case "STATE_GAME_RUN":
|
case "STATE_DEPLOYMENT":
|
||||||
|
case "STATE_COMBAT":
|
||||||
await import("./ui/game-viewport.js");
|
await import("./ui/game-viewport.js");
|
||||||
gameViewport.toggleAttribute("hidden", false);
|
gameViewport.toggleAttribute("hidden", false);
|
||||||
break;
|
break;
|
||||||
|
|
@ -53,11 +55,13 @@ window.addEventListener("save-check-complete", (e) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up embark listener once (not inside button click)
|
||||||
|
teamBuilder.addEventListener("embark", async (e) => {
|
||||||
|
await gameStateManager.handleEmbark(e);
|
||||||
|
gameViewport.squad = teamBuilder.squad;
|
||||||
|
});
|
||||||
|
|
||||||
btnNewRun.addEventListener("click", async () => {
|
btnNewRun.addEventListener("click", async () => {
|
||||||
teamBuilder.addEventListener("embark", async (e) => {
|
|
||||||
gameStateManager.handleEmbark(e);
|
|
||||||
gameViewport.squad = teamBuilder.squad;
|
|
||||||
});
|
|
||||||
gameStateManager.startNewGame();
|
gameStateManager.startNewGame();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,31 +77,57 @@ export class MissionManager {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const introId = this.currentMissionDef.narrative.intro_sequence;
|
const introId = this.currentMissionDef.narrative.intro_sequence;
|
||||||
|
|
||||||
// Mock loader: In real app, fetch the JSON from assets/data/narrative/
|
// Map narrative ID to filename
|
||||||
// For prototype, we'll assume narrativeManager can handle the ID or we pass a mock.
|
// NARRATIVE_TUTORIAL_INTRO -> tutorial_intro.json
|
||||||
// const narrativeData = await fetch(`assets/data/narrative/${introId}.json`).then(r => r.json());
|
const narrativeFileName = this._mapNarrativeIdToFileName(introId);
|
||||||
|
|
||||||
// We'll simulate the event listener logic
|
try {
|
||||||
const onEnd = () => {
|
// Load the narrative JSON file
|
||||||
narrativeManager.removeEventListener('narrative-end', onEnd);
|
const response = await fetch(`assets/data/narrative/${narrativeFileName}.json`);
|
||||||
resolve();
|
if (!response.ok) {
|
||||||
};
|
console.error(`Failed to load narrative: ${narrativeFileName}`);
|
||||||
narrativeManager.addEventListener('narrative-end', onEnd);
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger the manager (Assuming it has a loader, or we modify it to accept ID)
|
const narrativeData = await response.json();
|
||||||
// For this snippet, we assume startSequence accepts data.
|
|
||||||
// In a full implementation, you'd load the JSON here.
|
|
||||||
console.log(`Playing Narrative Intro: ${introId}`);
|
|
||||||
// narrativeManager.startSequence(loadedJson);
|
|
||||||
|
|
||||||
// Fallback for prototype if no JSON loader:
|
// Set up listener for narrative end
|
||||||
setTimeout(onEnd, 100); // Instant resolve for now to prevent hanging
|
const onEnd = () => {
|
||||||
|
narrativeManager.removeEventListener('narrative-end', onEnd);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
narrativeManager.addEventListener('narrative-end', onEnd);
|
||||||
|
|
||||||
|
// Start the narrative sequence
|
||||||
|
console.log(`Playing Narrative Intro: ${introId}`);
|
||||||
|
narrativeManager.startSequence(narrativeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading narrative ${narrativeFileName}:`, error);
|
||||||
|
resolve(); // Resolve anyway to not block game start
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps narrative sequence ID to filename.
|
||||||
|
* @param {string} narrativeId - The narrative ID from mission config
|
||||||
|
* @returns {string} The filename (without .json extension)
|
||||||
|
*/
|
||||||
|
_mapNarrativeIdToFileName(narrativeId) {
|
||||||
|
// Convert NARRATIVE_TUTORIAL_INTRO -> tutorial_intro
|
||||||
|
// Remove NARRATIVE_ prefix and convert to lowercase with underscores
|
||||||
|
const mapping = {
|
||||||
|
'NARRATIVE_TUTORIAL_INTRO': 'tutorial_intro',
|
||||||
|
'NARRATIVE_TUTORIAL_SUCCESS': 'tutorial_success'
|
||||||
|
};
|
||||||
|
|
||||||
|
return mapping[narrativeId] || narrativeId.toLowerCase().replace('NARRATIVE_', '');
|
||||||
|
}
|
||||||
|
|
||||||
// --- GAMEPLAY LOGIC (Objectives) ---
|
// --- GAMEPLAY LOGIC (Objectives) ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
101
src/ui/combat-hud.js
Normal file
101
src/ui/combat-hud.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { LitElement, html, css } from "lit";
|
||||||
|
|
||||||
|
export class CombatHUD extends LitElement {
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
border: 2px solid #ff0000;
|
||||||
|
padding: 15px 30px;
|
||||||
|
text-align: center;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #ff6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
left: 30px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
border: 2px solid #ff0000;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
currentState: { type: String },
|
||||||
|
currentTurn: { type: String },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.currentState = null;
|
||||||
|
this.currentTurn = "PLAYER";
|
||||||
|
window.addEventListener("gamestate-changed", (e) => {
|
||||||
|
this.currentState = e.detail.newState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Only show during COMBAT state
|
||||||
|
if (this.currentState !== "STATE_COMBAT") {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="header">
|
||||||
|
<h2>COMBAT ACTIVE</h2>
|
||||||
|
<div class="status-bar">Turn: ${this.currentTurn}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="turn-indicator">
|
||||||
|
<div>State: ${this.currentState}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
Use WASD or Arrow Keys to move cursor | SPACE/ENTER to select
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("combat-hud", CombatHUD);
|
||||||
|
|
||||||
|
|
@ -131,6 +131,7 @@ export class DeploymentHUD extends LitElement {
|
||||||
deployedIds: { type: Array }, // List of IDs currently on the board
|
deployedIds: { type: Array }, // List of IDs currently on the board
|
||||||
selectedId: { type: String }, // ID of unit currently being placed
|
selectedId: { type: String }, // ID of unit currently being placed
|
||||||
maxUnits: { type: Number },
|
maxUnits: { type: Number },
|
||||||
|
currentState: { type: String }, // Current game state
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,12 +141,25 @@ export class DeploymentHUD extends LitElement {
|
||||||
this.deployedIds = [];
|
this.deployedIds = [];
|
||||||
this.selectedId = null;
|
this.selectedId = null;
|
||||||
this.maxUnits = 4;
|
this.maxUnits = 4;
|
||||||
|
this.currentState = null;
|
||||||
window.addEventListener("deployment-update", (e) => {
|
window.addEventListener("deployment-update", (e) => {
|
||||||
this.deployedIds = e.detail.deployedIndices;
|
this.deployedIds = e.detail.deployedIndices;
|
||||||
});
|
});
|
||||||
|
window.addEventListener("gamestate-changed", (e) => {
|
||||||
|
this.currentState = e.detail.newState;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// Hide the deployment HUD when not in deployment state
|
||||||
|
// Show by default (when currentState is null) since we start in deployment
|
||||||
|
if (
|
||||||
|
this.currentState !== null &&
|
||||||
|
this.currentState !== "STATE_DEPLOYMENT"
|
||||||
|
) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
const deployedCount = this.deployedIds.length;
|
const deployedCount = this.deployedIds.length;
|
||||||
const canStart = deployedCount > 0; // At least 1 unit required
|
const canStart = deployedCount > 0; // At least 1 unit required
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, html, css } from "lit";
|
||||||
import { narrativeManager } from "../../systems/NarrativeManager.js";
|
import { narrativeManager } from "../managers/NarrativeManager.js";
|
||||||
|
|
||||||
export class DialogueOverlay extends LitElement {
|
export class DialogueOverlay extends LitElement {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { gameStateManager } from "../core/GameStateManager.js";
|
||||||
import { GameLoop } from "../core/GameLoop.js";
|
import { GameLoop } from "../core/GameLoop.js";
|
||||||
|
|
||||||
import "./deployment-hud.js";
|
import "./deployment-hud.js";
|
||||||
|
import "./dialogue-overlay.js";
|
||||||
|
import "./combat-hud.js";
|
||||||
|
|
||||||
export class GameViewport extends LitElement {
|
export class GameViewport extends LitElement {
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
|
@ -37,6 +39,12 @@ export class GameViewport extends LitElement {
|
||||||
gameStateManager.gameLoop.selectDeploymentUnit(index);
|
gameStateManager.gameLoop.selectDeploymentUnit(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#handleStartBattle() {
|
||||||
|
if (gameStateManager.gameLoop) {
|
||||||
|
gameStateManager.gameLoop.finalizeDeployment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async firstUpdated() {
|
async firstUpdated() {
|
||||||
const container = this.shadowRoot.getElementById("canvas-container");
|
const container = this.shadowRoot.getElementById("canvas-container");
|
||||||
const loop = new GameLoop();
|
const loop = new GameLoop();
|
||||||
|
|
@ -51,7 +59,10 @@ export class GameViewport extends LitElement {
|
||||||
.squad=${this.squad}
|
.squad=${this.squad}
|
||||||
.deployedIds=${this.deployedIds}
|
.deployedIds=${this.deployedIds}
|
||||||
@unit-selected=${this.#handleUnitSelected}
|
@unit-selected=${this.#handleUnitSelected}
|
||||||
></deployment-hud>`;
|
@start-battle=${this.#handleStartBattle}
|
||||||
|
></deployment-hud>
|
||||||
|
<combat-hud></combat-hud>
|
||||||
|
<dialogue-overlay></dialogue-overlay>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,12 @@ describe("Core: GameLoop (Integration)", function () {
|
||||||
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock gameStateManager for deployment phase
|
||||||
|
gameLoop.gameStateManager = {
|
||||||
|
currentState: "STATE_DEPLOYMENT",
|
||||||
|
transitionTo: sinon.stub(),
|
||||||
|
};
|
||||||
|
|
||||||
// startLevel should now prepare the map but NOT spawn units immediately
|
// startLevel should now prepare the map but NOT spawn units immediately
|
||||||
await gameLoop.startLevel(runData);
|
await gameLoop.startLevel(runData);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
init: sinon.stub().resolves(),
|
init: sinon.stub().resolves(),
|
||||||
saveRun: sinon.stub().resolves(),
|
saveRun: sinon.stub().resolves(),
|
||||||
loadRun: sinon.stub().resolves(null),
|
loadRun: sinon.stub().resolves(null),
|
||||||
|
loadRoster: sinon.stub().resolves(null),
|
||||||
|
saveRoster: sinon.stub().resolves(),
|
||||||
};
|
};
|
||||||
// Inject Mock (replacing the real Persistence instance)
|
// Inject Mock (replacing the real Persistence instance)
|
||||||
gameStateManager.persistence = mockPersistence;
|
gameStateManager.persistence = mockPersistence;
|
||||||
|
|
@ -29,6 +31,23 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
startLevel: sinon.spy(),
|
startLevel: sinon.spy(),
|
||||||
stop: sinon.spy(),
|
stop: sinon.spy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 4. Mock MissionManager
|
||||||
|
gameStateManager.missionManager = {
|
||||||
|
setupActiveMission: sinon.stub(),
|
||||||
|
getActiveMission: sinon.stub().returns({
|
||||||
|
id: "MISSION_TUTORIAL_01",
|
||||||
|
config: { title: "Test Mission" },
|
||||||
|
biome: {
|
||||||
|
generator_config: {
|
||||||
|
seed_type: "RANDOM",
|
||||||
|
seed: 12345,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objectives: [],
|
||||||
|
}),
|
||||||
|
playIntro: sinon.stub().resolves(),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("CoA 1: Should initialize and transition to MAIN_MENU", async () => {
|
it("CoA 1: Should initialize and transition to MAIN_MENU", async () => {
|
||||||
|
|
@ -60,20 +79,14 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
|
|
||||||
const mockSquad = [{ id: "u1" }];
|
const mockSquad = [{ id: "u1" }];
|
||||||
|
|
||||||
// Handle Async Chain
|
// Mock startLevel to resolve immediately
|
||||||
let resolveEngineStart;
|
mockGameLoop.startLevel = sinon.stub().resolves();
|
||||||
const engineStartPromise = new Promise((r) => {
|
|
||||||
resolveEngineStart = r;
|
|
||||||
});
|
|
||||||
mockGameLoop.startLevel = sinon.stub().callsFake(() => {
|
|
||||||
resolveEngineStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
gameStateManager.handleEmbark({ detail: { squad: mockSquad } });
|
// Await the full async chain
|
||||||
await engineStartPromise;
|
await gameStateManager.handleEmbark({ detail: { squad: mockSquad } });
|
||||||
|
|
||||||
expect(gameStateManager.currentState).to.equal(
|
expect(gameStateManager.currentState).to.equal(
|
||||||
GameStateManager.STATES.GAME_RUN
|
GameStateManager.STATES.DEPLOYMENT
|
||||||
);
|
);
|
||||||
expect(mockPersistence.saveRun.calledWith(gameStateManager.activeRunData))
|
expect(mockPersistence.saveRun.calledWith(gameStateManager.activeRunData))
|
||||||
.to.be.true;
|
.to.be.true;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue