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 __dirname = dirname(__filename);
|
||||
|
||||
// Image file extensions to copy
|
||||
// File extensions to copy
|
||||
const IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
|
||||
const DATA_EXTENSIONS = [".json"];
|
||||
|
||||
// Recursively copy image files from src to dist
|
||||
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
|
||||
mkdirSync("dist", { recursive: true });
|
||||
|
||||
|
|
@ -53,4 +78,7 @@ copyFileSync("src/index.html", "dist/index.html");
|
|||
// Copy images
|
||||
copyImages("src", "dist");
|
||||
|
||||
// Copy assets (JSON, markdown, TypeScript definitions)
|
||||
copyAssets("src/assets", "dist/assets");
|
||||
|
||||
console.log("Build complete!");
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { MissionManager } from "../managers/MissionManager.js";
|
|||
export class GameLoop {
|
||||
constructor() {
|
||||
this.isRunning = false;
|
||||
this.phase = "INIT";
|
||||
|
||||
// 1. Core Systems
|
||||
this.scene = new THREE.Scene();
|
||||
|
|
@ -184,7 +183,10 @@ export class GameLoop {
|
|||
const cursor = this.inputManager.getCursorPosition();
|
||||
console.log("Action at:", cursor);
|
||||
|
||||
if (this.phase === "DEPLOYMENT") {
|
||||
if (
|
||||
this.gameStateManager &&
|
||||
this.gameStateManager.currentState === "STATE_DEPLOYMENT"
|
||||
) {
|
||||
const selIndex = this.deploymentState.selectedUnitIndex;
|
||||
|
||||
if (selIndex !== -1) {
|
||||
|
|
@ -227,7 +229,6 @@ export class GameLoop {
|
|||
console.log("GameLoop: Generating Level...");
|
||||
this.runData = runData;
|
||||
this.isRunning = true;
|
||||
this.phase = "DEPLOYMENT";
|
||||
this.clearUnitMeshes();
|
||||
|
||||
// Reset Deployment State
|
||||
|
|
@ -299,7 +300,11 @@ export class GameLoop {
|
|||
}
|
||||
|
||||
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(
|
||||
targetTile.x,
|
||||
|
|
@ -358,7 +363,11 @@ export class GameLoop {
|
|||
}
|
||||
|
||||
finalizeDeployment() {
|
||||
if (this.phase !== "DEPLOYMENT") return;
|
||||
if (
|
||||
!this.gameStateManager ||
|
||||
this.gameStateManager.currentState !== "STATE_DEPLOYMENT"
|
||||
)
|
||||
return;
|
||||
const enemyCount = 2;
|
||||
for (let i = 0; i < enemyCount; i++) {
|
||||
const spotIndex = Math.floor(Math.random() * this.enemySpawnZone.length);
|
||||
|
|
@ -370,11 +379,15 @@ export class GameLoop {
|
|||
this.enemySpawnZone.splice(spotIndex, 1);
|
||||
}
|
||||
}
|
||||
this.phase = "ACTIVE";
|
||||
|
||||
// Switch to standard movement validator for the game
|
||||
this.inputManager.setValidator(this.validateCursorMove.bind(this));
|
||||
|
||||
// Notify GameStateManager about state change
|
||||
if (this.gameStateManager) {
|
||||
this.gameStateManager.transitionTo("STATE_COMBAT");
|
||||
}
|
||||
|
||||
console.log("Combat Started!");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ class GameStateManagerClass {
|
|||
INIT: "STATE_INIT",
|
||||
MAIN_MENU: "STATE_MAIN_MENU",
|
||||
TEAM_BUILDER: "STATE_TEAM_BUILDER",
|
||||
GAME_RUN: "STATE_GAME_RUN",
|
||||
DEPLOYMENT: "STATE_DEPLOYMENT",
|
||||
COMBAT: "STATE_COMBAT",
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
|
@ -40,6 +41,18 @@ class GameStateManagerClass {
|
|||
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() {
|
||||
console.log("System: Initializing State Manager...");
|
||||
await this.persistence.init();
|
||||
|
|
@ -58,9 +71,14 @@ class GameStateManagerClass {
|
|||
}
|
||||
|
||||
async transitionTo(newState, payload = null) {
|
||||
console.log(`State Transition: ${this.currentState} -> ${newState}`);
|
||||
const oldState = this.currentState;
|
||||
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(
|
||||
new CustomEvent("gamestate-changed", {
|
||||
|
|
@ -68,21 +86,21 @@ class GameStateManagerClass {
|
|||
})
|
||||
);
|
||||
|
||||
// Only run state-specific logic if state actually changed
|
||||
if (stateChanged) {
|
||||
switch (newState) {
|
||||
case GameStateManagerClass.STATES.MAIN_MENU:
|
||||
if (this.gameLoop) this.gameLoop.stop();
|
||||
await this._checkSaveGame();
|
||||
break;
|
||||
|
||||
case GameStateManagerClass.STATES.GAME_RUN:
|
||||
if (!this.activeRunData && payload) {
|
||||
await this._initializeRun(payload);
|
||||
} else {
|
||||
await this._resumeRun();
|
||||
}
|
||||
case GameStateManagerClass.STATES.DEPLOYMENT:
|
||||
case GameStateManagerClass.STATES.COMBAT:
|
||||
// These states are handled by GameLoop, no special logic needed here
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startNewGame() {
|
||||
this.transitionTo(GameStateManagerClass.STATES.TEAM_BUILDER);
|
||||
|
|
@ -92,11 +110,12 @@ class GameStateManagerClass {
|
|||
const save = await this.persistence.loadRun();
|
||||
if (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)
|
||||
if (e.detail.mode === "DRAFT") {
|
||||
e.detail.squad.forEach((unit) => {
|
||||
|
|
@ -106,7 +125,8 @@ class GameStateManagerClass {
|
|||
});
|
||||
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 ---
|
||||
|
|
@ -147,7 +167,11 @@ class GameStateManagerClass {
|
|||
|
||||
// Pass the Mission Manager to the Game Loop so it can report events (Deaths, etc)
|
||||
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() {
|
||||
|
|
@ -155,8 +179,13 @@ class GameStateManagerClass {
|
|||
if (this.activeRunData) {
|
||||
// Re-hook the mission manager
|
||||
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
|
||||
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":
|
||||
loadingMessage.textContent = "INITIALIZING TEAM BUILDER...";
|
||||
break;
|
||||
case "STATE_GAME_RUN":
|
||||
case "STATE_DEPLOYMENT":
|
||||
case "STATE_COMBAT":
|
||||
loadingMessage.textContent = "INITIALIZING GAME ENGINE...";
|
||||
break;
|
||||
}
|
||||
|
|
@ -37,7 +38,8 @@ window.addEventListener("gamestate-changed", async (e) => {
|
|||
await import("./ui/team-builder.js");
|
||||
teamBuilder.toggleAttribute("hidden", false);
|
||||
break;
|
||||
case "STATE_GAME_RUN":
|
||||
case "STATE_DEPLOYMENT":
|
||||
case "STATE_COMBAT":
|
||||
await import("./ui/game-viewport.js");
|
||||
gameViewport.toggleAttribute("hidden", false);
|
||||
break;
|
||||
|
|
@ -53,11 +55,13 @@ window.addEventListener("save-check-complete", (e) => {
|
|||
}
|
||||
});
|
||||
|
||||
btnNewRun.addEventListener("click", async () => {
|
||||
teamBuilder.addEventListener("embark", async (e) => {
|
||||
gameStateManager.handleEmbark(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 () => {
|
||||
gameStateManager.startNewGame();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -77,31 +77,57 @@ export class MissionManager {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(async (resolve) => {
|
||||
const introId = this.currentMissionDef.narrative.intro_sequence;
|
||||
|
||||
// Mock loader: In real app, fetch the JSON from assets/data/narrative/
|
||||
// For prototype, we'll assume narrativeManager can handle the ID or we pass a mock.
|
||||
// const narrativeData = await fetch(`assets/data/narrative/${introId}.json`).then(r => r.json());
|
||||
// Map narrative ID to filename
|
||||
// NARRATIVE_TUTORIAL_INTRO -> tutorial_intro.json
|
||||
const narrativeFileName = this._mapNarrativeIdToFileName(introId);
|
||||
|
||||
// We'll simulate the event listener logic
|
||||
try {
|
||||
// Load the narrative JSON file
|
||||
const response = await fetch(`assets/data/narrative/${narrativeFileName}.json`);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load narrative: ${narrativeFileName}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const narrativeData = await response.json();
|
||||
|
||||
// Set up listener for narrative end
|
||||
const onEnd = () => {
|
||||
narrativeManager.removeEventListener('narrative-end', onEnd);
|
||||
resolve();
|
||||
};
|
||||
narrativeManager.addEventListener('narrative-end', onEnd);
|
||||
|
||||
// Trigger the manager (Assuming it has a loader, or we modify it to accept ID)
|
||||
// For this snippet, we assume startSequence accepts data.
|
||||
// In a full implementation, you'd load the JSON here.
|
||||
// Start the narrative sequence
|
||||
console.log(`Playing Narrative Intro: ${introId}`);
|
||||
// narrativeManager.startSequence(loadedJson);
|
||||
|
||||
// Fallback for prototype if no JSON loader:
|
||||
setTimeout(onEnd, 100); // Instant resolve for now to prevent hanging
|
||||
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) ---
|
||||
|
||||
/**
|
||||
|
|
|
|||
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
|
||||
selectedId: { type: String }, // ID of unit currently being placed
|
||||
maxUnits: { type: Number },
|
||||
currentState: { type: String }, // Current game state
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -140,12 +141,25 @@ export class DeploymentHUD extends LitElement {
|
|||
this.deployedIds = [];
|
||||
this.selectedId = null;
|
||||
this.maxUnits = 4;
|
||||
this.currentState = null;
|
||||
window.addEventListener("deployment-update", (e) => {
|
||||
this.deployedIds = e.detail.deployedIndices;
|
||||
});
|
||||
window.addEventListener("gamestate-changed", (e) => {
|
||||
this.currentState = e.detail.newState;
|
||||
});
|
||||
}
|
||||
|
||||
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 canStart = deployedCount > 0; // At least 1 unit required
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LitElement, html, css } from "lit";
|
||||
import { narrativeManager } from "../../systems/NarrativeManager.js";
|
||||
import { narrativeManager } from "../managers/NarrativeManager.js";
|
||||
|
||||
export class DialogueOverlay extends LitElement {
|
||||
static get styles() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { gameStateManager } from "../core/GameStateManager.js";
|
|||
import { GameLoop } from "../core/GameLoop.js";
|
||||
|
||||
import "./deployment-hud.js";
|
||||
import "./dialogue-overlay.js";
|
||||
import "./combat-hud.js";
|
||||
|
||||
export class GameViewport extends LitElement {
|
||||
static styles = css`
|
||||
|
|
@ -37,6 +39,12 @@ export class GameViewport extends LitElement {
|
|||
gameStateManager.gameLoop.selectDeploymentUnit(index);
|
||||
}
|
||||
|
||||
#handleStartBattle() {
|
||||
if (gameStateManager.gameLoop) {
|
||||
gameStateManager.gameLoop.finalizeDeployment();
|
||||
}
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
const container = this.shadowRoot.getElementById("canvas-container");
|
||||
const loop = new GameLoop();
|
||||
|
|
@ -51,7 +59,10 @@ export class GameViewport extends LitElement {
|
|||
.squad=${this.squad}
|
||||
.deployedIds=${this.deployedIds}
|
||||
@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" }],
|
||||
};
|
||||
|
||||
// Mock gameStateManager for deployment phase
|
||||
gameLoop.gameStateManager = {
|
||||
currentState: "STATE_DEPLOYMENT",
|
||||
transitionTo: sinon.stub(),
|
||||
};
|
||||
|
||||
// startLevel should now prepare the map but NOT spawn units immediately
|
||||
await gameLoop.startLevel(runData);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ describe("Core: GameStateManager (Singleton)", () => {
|
|||
init: sinon.stub().resolves(),
|
||||
saveRun: sinon.stub().resolves(),
|
||||
loadRun: sinon.stub().resolves(null),
|
||||
loadRoster: sinon.stub().resolves(null),
|
||||
saveRoster: sinon.stub().resolves(),
|
||||
};
|
||||
// Inject Mock (replacing the real Persistence instance)
|
||||
gameStateManager.persistence = mockPersistence;
|
||||
|
|
@ -29,6 +31,23 @@ describe("Core: GameStateManager (Singleton)", () => {
|
|||
startLevel: 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 () => {
|
||||
|
|
@ -60,20 +79,14 @@ describe("Core: GameStateManager (Singleton)", () => {
|
|||
|
||||
const mockSquad = [{ id: "u1" }];
|
||||
|
||||
// Handle Async Chain
|
||||
let resolveEngineStart;
|
||||
const engineStartPromise = new Promise((r) => {
|
||||
resolveEngineStart = r;
|
||||
});
|
||||
mockGameLoop.startLevel = sinon.stub().callsFake(() => {
|
||||
resolveEngineStart();
|
||||
});
|
||||
// Mock startLevel to resolve immediately
|
||||
mockGameLoop.startLevel = sinon.stub().resolves();
|
||||
|
||||
gameStateManager.handleEmbark({ detail: { squad: mockSquad } });
|
||||
await engineStartPromise;
|
||||
// Await the full async chain
|
||||
await gameStateManager.handleEmbark({ detail: { squad: mockSquad } });
|
||||
|
||||
expect(gameStateManager.currentState).to.equal(
|
||||
GameStateManager.STATES.GAME_RUN
|
||||
GameStateManager.STATES.DEPLOYMENT
|
||||
);
|
||||
expect(mockPersistence.saveRun.calledWith(gameStateManager.activeRunData))
|
||||
.to.be.true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue