Add combat state management to GameLoop and GameStateManager. Implement updateCombatState method to manage turn queue and active unit status during combat. Integrate combat state updates in GameViewport for UI responsiveness. Enhance type definitions for combat-related structures.
This commit is contained in:
parent
1b8775657f
commit
17590cdab0
5 changed files with 200 additions and 17 deletions
|
|
@ -2,6 +2,9 @@
|
||||||
* @typedef {import("./types.js").RunData} RunData
|
* @typedef {import("./types.js").RunData} RunData
|
||||||
* @typedef {import("../grid/types.js").Position} Position
|
* @typedef {import("../grid/types.js").Position} Position
|
||||||
* @typedef {import("../units/Unit.js").Unit} Unit
|
* @typedef {import("../units/Unit.js").Unit} Unit
|
||||||
|
* @typedef {import("../ui/combat-hud.d.ts").CombatState} CombatState
|
||||||
|
* @typedef {import("../ui/combat-hud.d.ts").UnitStatus} UnitStatus
|
||||||
|
* @typedef {import("../ui/combat-hud.d.ts").QueueEntry} QueueEntry
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
@ -476,6 +479,9 @@ export class GameLoop {
|
||||||
this.gameStateManager.transitionTo("STATE_COMBAT");
|
this.gameStateManager.transitionTo("STATE_COMBAT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize combat state
|
||||||
|
this.updateCombatState();
|
||||||
|
|
||||||
console.log("Combat Started!");
|
console.log("Combat Started!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -594,4 +600,117 @@ export class GameLoop {
|
||||||
if (this.inputManager) this.inputManager.detach();
|
if (this.inputManager) this.inputManager.detach();
|
||||||
if (this.controls) this.controls.dispose();
|
if (this.controls) this.controls.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the combat state in GameStateManager.
|
||||||
|
* Called when combat starts or when combat state changes (turn changes, etc.)
|
||||||
|
*/
|
||||||
|
updateCombatState() {
|
||||||
|
if (!this.gameStateManager || !this.grid || !this.unitManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all units from the grid
|
||||||
|
const allUnits = Array.from(this.grid.unitMap.values()).filter(
|
||||||
|
(unit) => unit.isAlive && unit.isAlive()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allUnits.length === 0) {
|
||||||
|
// No units, clear combat state
|
||||||
|
this.gameStateManager.setCombatState(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build turn queue sorted by initiative (chargeMeter)
|
||||||
|
const turnQueue = allUnits
|
||||||
|
.map((unit) => {
|
||||||
|
// Get portrait path (placeholder for now)
|
||||||
|
const portrait =
|
||||||
|
unit.team === "PLAYER"
|
||||||
|
? "/assets/images/portraits/default.png"
|
||||||
|
: "/assets/images/portraits/enemy.png";
|
||||||
|
|
||||||
|
return {
|
||||||
|
unitId: unit.id,
|
||||||
|
portrait: portrait,
|
||||||
|
team: unit.team || "ENEMY",
|
||||||
|
initiative: unit.chargeMeter || 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.initiative - a.initiative); // Sort by initiative descending
|
||||||
|
|
||||||
|
// Get active unit (first in queue)
|
||||||
|
const activeUnitId = turnQueue.length > 0 ? turnQueue[0].unitId : null;
|
||||||
|
const activeUnit = allUnits.find((u) => u.id === activeUnitId);
|
||||||
|
|
||||||
|
// Build active unit status if we have an active unit
|
||||||
|
let unitStatus = null;
|
||||||
|
if (activeUnit) {
|
||||||
|
// Get max AP (default to 10 for now, can be derived from stats later)
|
||||||
|
const maxAP = 10;
|
||||||
|
|
||||||
|
// Convert status effects to status icons
|
||||||
|
const statuses = (activeUnit.statusEffects || []).map((effect) => ({
|
||||||
|
id: effect.id || "unknown",
|
||||||
|
icon: effect.icon || "❓",
|
||||||
|
turnsRemaining: effect.duration || 0,
|
||||||
|
description: effect.description || effect.name || "Status Effect",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build skills (placeholder for now - will be populated from unit's actions/skill tree)
|
||||||
|
const skills = (activeUnit.actions || []).map((action, index) => ({
|
||||||
|
id: action.id || `skill_${index}`,
|
||||||
|
name: action.name || "Unknown Skill",
|
||||||
|
icon: action.icon || "⚔",
|
||||||
|
costAP: action.costAP || 0,
|
||||||
|
cooldown: action.cooldown || 0,
|
||||||
|
isAvailable:
|
||||||
|
activeUnit.currentAP >= (action.costAP || 0) &&
|
||||||
|
(action.cooldown || 0) === 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If no skills from actions, provide a default attack skill
|
||||||
|
if (skills.length === 0) {
|
||||||
|
skills.push({
|
||||||
|
id: "attack",
|
||||||
|
name: "Attack",
|
||||||
|
icon: "⚔",
|
||||||
|
costAP: 3,
|
||||||
|
cooldown: 0,
|
||||||
|
isAvailable: activeUnit.currentAP >= 3,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unitStatus = {
|
||||||
|
id: activeUnit.id,
|
||||||
|
name: activeUnit.name,
|
||||||
|
portrait:
|
||||||
|
activeUnit.team === "PLAYER"
|
||||||
|
? "/assets/images/portraits/default.png"
|
||||||
|
: "/assets/images/portraits/enemy.png",
|
||||||
|
hp: {
|
||||||
|
current: activeUnit.currentHealth,
|
||||||
|
max: activeUnit.maxHealth,
|
||||||
|
},
|
||||||
|
ap: {
|
||||||
|
current: activeUnit.currentAP,
|
||||||
|
max: maxAP,
|
||||||
|
},
|
||||||
|
charge: activeUnit.chargeMeter || 0,
|
||||||
|
statuses: statuses,
|
||||||
|
skills: skills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build combat state
|
||||||
|
const combatState = {
|
||||||
|
activeUnit: unitStatus,
|
||||||
|
turnQueue: turnQueue,
|
||||||
|
targetingMode: false, // Will be set when player selects a skill
|
||||||
|
roundNumber: 1, // TODO: Track actual round number
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update GameStateManager
|
||||||
|
this.gameStateManager.setCombatState(combatState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* @typedef {import("./types.js").RunData} RunData
|
* @typedef {import("./types.js").RunData} RunData
|
||||||
* @typedef {import("./types.js").EmbarkEventDetail} EmbarkEventDetail
|
* @typedef {import("./types.js").EmbarkEventDetail} EmbarkEventDetail
|
||||||
* @typedef {import("./types.js").SquadMember} SquadMember
|
* @typedef {import("./types.js").SquadMember} SquadMember
|
||||||
|
* @typedef {import("../ui/combat-hud.d.ts").CombatState} CombatState
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Persistence } from "./Persistence.js";
|
import { Persistence } from "./Persistence.js";
|
||||||
|
|
@ -42,6 +43,8 @@ class GameStateManagerClass {
|
||||||
this.persistence = new Persistence();
|
this.persistence = new Persistence();
|
||||||
/** @type {RunData | null} */
|
/** @type {RunData | null} */
|
||||||
this.activeRunData = null;
|
this.activeRunData = null;
|
||||||
|
/** @type {CombatState | null} */
|
||||||
|
this.combatState = null;
|
||||||
|
|
||||||
// Integrate Core Managers
|
// Integrate Core Managers
|
||||||
/** @type {RosterManager} */
|
/** @type {RosterManager} */
|
||||||
|
|
@ -91,6 +94,7 @@ class GameStateManagerClass {
|
||||||
this.currentState = GameStateManagerClass.STATES.INIT;
|
this.currentState = GameStateManagerClass.STATES.INIT;
|
||||||
this.gameLoop = null;
|
this.gameLoop = null;
|
||||||
this.activeRunData = null;
|
this.activeRunData = null;
|
||||||
|
this.combatState = null;
|
||||||
this.rosterManager = new RosterManager();
|
this.rosterManager = new RosterManager();
|
||||||
this.missionManager = new MissionManager();
|
this.missionManager = new MissionManager();
|
||||||
// Reset promise resolvers
|
// Reset promise resolvers
|
||||||
|
|
@ -150,8 +154,12 @@ class GameStateManagerClass {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GameStateManagerClass.STATES.DEPLOYMENT:
|
case GameStateManagerClass.STATES.DEPLOYMENT:
|
||||||
|
// Clear combat state when leaving combat
|
||||||
|
this.combatState = null;
|
||||||
|
break;
|
||||||
|
|
||||||
case GameStateManagerClass.STATES.COMBAT:
|
case GameStateManagerClass.STATES.COMBAT:
|
||||||
// These states are handled by GameLoop, no special logic needed here
|
// Combat state will be managed by GameLoop via setCombatState()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,6 +302,29 @@ class GameStateManagerClass {
|
||||||
const data = this.rosterManager.save();
|
const data = this.rosterManager.save();
|
||||||
await this.persistence.saveRoster(data);
|
await this.persistence.saveRoster(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current combat state.
|
||||||
|
* Called by GameLoop when combat state changes.
|
||||||
|
* @param {CombatState | null} combatState - The new combat state
|
||||||
|
*/
|
||||||
|
setCombatState(combatState) {
|
||||||
|
this.combatState = combatState;
|
||||||
|
// Dispatch event so UI components can react to combat state changes
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("combat-state-changed", {
|
||||||
|
detail: { combatState },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current combat state.
|
||||||
|
* @returns {CombatState | null} The current combat state
|
||||||
|
*/
|
||||||
|
getCombatState() {
|
||||||
|
return this.combatState;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export the Singleton Instance
|
// Export the Singleton Instance
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export class GameViewport extends LitElement {
|
||||||
return {
|
return {
|
||||||
squad: { type: Array },
|
squad: { type: Array },
|
||||||
deployedIds: { type: Array },
|
deployedIds: { type: Array },
|
||||||
|
combatState: { type: Object },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ export class GameViewport extends LitElement {
|
||||||
super();
|
super();
|
||||||
this.squad = [];
|
this.squad = [];
|
||||||
this.deployedIds = [];
|
this.deployedIds = [];
|
||||||
|
this.combatState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleUnitSelected(event) {
|
#handleUnitSelected(event) {
|
||||||
|
|
@ -51,6 +53,29 @@ export class GameViewport extends LitElement {
|
||||||
loop.init(container);
|
loop.init(container);
|
||||||
gameStateManager.setGameLoop(loop);
|
gameStateManager.setGameLoop(loop);
|
||||||
this.squad = await gameStateManager.rosterLoaded;
|
this.squad = await gameStateManager.rosterLoaded;
|
||||||
|
|
||||||
|
// Set up combat state updates
|
||||||
|
this.#setupCombatStateUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupCombatStateUpdates() {
|
||||||
|
// Listen for combat state changes
|
||||||
|
window.addEventListener("combat-state-changed", (e) => {
|
||||||
|
this.combatState = e.detail.combatState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for game state changes to clear combat state when leaving combat
|
||||||
|
window.addEventListener("gamestate-changed", () => {
|
||||||
|
this.#updateCombatState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial update
|
||||||
|
this.#updateCombatState();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateCombatState() {
|
||||||
|
// Get combat state from GameStateManager
|
||||||
|
this.combatState = gameStateManager.getCombatState();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -61,7 +86,7 @@ export class GameViewport extends LitElement {
|
||||||
@unit-selected=${this.#handleUnitSelected}
|
@unit-selected=${this.#handleUnitSelected}
|
||||||
@start-battle=${this.#handleStartBattle}
|
@start-battle=${this.#handleStartBattle}
|
||||||
></deployment-hud>
|
></deployment-hud>
|
||||||
<combat-hud></combat-hud>
|
<combat-hud .combatState=${this.combatState}></combat-hud>
|
||||||
<dialogue-overlay></dialogue-overlay>`;
|
<dialogue-overlay></dialogue-overlay>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,12 @@ describe("Core: Persistence", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use window or self for browser environment
|
// Use window or self for browser environment
|
||||||
globalObj = typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : globalThis);
|
globalObj =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window
|
||||||
|
: typeof self !== "undefined"
|
||||||
|
? self
|
||||||
|
: globalThis;
|
||||||
});
|
});
|
||||||
|
|
||||||
const createMockRequest = () => {
|
const createMockRequest = () => {
|
||||||
|
|
@ -46,7 +51,7 @@ describe("Core: Persistence", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock indexedDB using defineProperty since it's read-only
|
// Mock indexedDB using defineProperty since it's read-only
|
||||||
Object.defineProperty(globalObj, 'indexedDB', {
|
Object.defineProperty(globalObj, "indexedDB", {
|
||||||
value: {
|
value: {
|
||||||
open: sinon.stub().returns(mockRequest),
|
open: sinon.stub().returns(mockRequest),
|
||||||
},
|
},
|
||||||
|
|
@ -59,15 +64,15 @@ describe("Core: Persistence", () => {
|
||||||
|
|
||||||
it("CoA 1: init should create database and object stores", async () => {
|
it("CoA 1: init should create database and object stores", async () => {
|
||||||
const request = createMockRequest();
|
const request = createMockRequest();
|
||||||
|
|
||||||
// Start init, which will call indexedDB.open
|
// Start init, which will call indexedDB.open
|
||||||
const initPromise = persistence.init();
|
const initPromise = persistence.init();
|
||||||
|
|
||||||
// Trigger upgrade first (happens synchronously during open)
|
// Trigger upgrade first (happens synchronously during open)
|
||||||
if (request.onupgradeneeded) {
|
if (request.onupgradeneeded) {
|
||||||
request.onupgradeneeded({ target: { result: mockDB } });
|
request.onupgradeneeded({ target: { result: mockDB } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then trigger success
|
// Then trigger success
|
||||||
if (request.onsuccess) {
|
if (request.onsuccess) {
|
||||||
request.onsuccess({ target: { result: mockDB } });
|
request.onsuccess({ target: { result: mockDB } });
|
||||||
|
|
@ -76,8 +81,10 @@ describe("Core: Persistence", () => {
|
||||||
await initPromise;
|
await initPromise;
|
||||||
|
|
||||||
expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 2)).to.be.true;
|
expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 2)).to.be.true;
|
||||||
expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be.true;
|
expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be
|
||||||
expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to.be.true;
|
.true;
|
||||||
|
expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to
|
||||||
|
.be.true;
|
||||||
expect(persistence.db).to.equal(mockDB);
|
expect(persistence.db).to.equal(mockDB);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -166,7 +173,10 @@ describe("Core: Persistence", () => {
|
||||||
|
|
||||||
it("CoA 6: loadRoster should extract data from stored object", async () => {
|
it("CoA 6: loadRoster should extract data from stored object", async () => {
|
||||||
persistence.db = mockDB;
|
persistence.db = mockDB;
|
||||||
const storedData = { id: "player_roster", data: { roster: [], graveyard: [] } };
|
const storedData = {
|
||||||
|
id: "player_roster",
|
||||||
|
data: { roster: [], graveyard: [] },
|
||||||
|
};
|
||||||
|
|
||||||
const mockGetRequest = {
|
const mockGetRequest = {
|
||||||
onsuccess: null,
|
onsuccess: null,
|
||||||
|
|
@ -205,7 +215,7 @@ describe("Core: Persistence", () => {
|
||||||
|
|
||||||
it("CoA 8: saveRun should auto-init if db not initialized", async () => {
|
it("CoA 8: saveRun should auto-init if db not initialized", async () => {
|
||||||
const request = createMockRequest();
|
const request = createMockRequest();
|
||||||
|
|
||||||
const runData = { seed: 12345 };
|
const runData = { seed: 12345 };
|
||||||
const mockPutRequest = {
|
const mockPutRequest = {
|
||||||
onsuccess: sinon.stub(),
|
onsuccess: sinon.stub(),
|
||||||
|
|
@ -215,7 +225,7 @@ describe("Core: Persistence", () => {
|
||||||
|
|
||||||
// Start saveRun, which will trigger init
|
// Start saveRun, which will trigger init
|
||||||
const savePromise = persistence.saveRun(runData);
|
const savePromise = persistence.saveRun(runData);
|
||||||
|
|
||||||
// Trigger upgrade and success for init
|
// Trigger upgrade and success for init
|
||||||
if (request.onupgradeneeded) {
|
if (request.onupgradeneeded) {
|
||||||
request.onupgradeneeded({ target: { result: mockDB } });
|
request.onupgradeneeded({ target: { result: mockDB } });
|
||||||
|
|
@ -223,9 +233,9 @@ describe("Core: Persistence", () => {
|
||||||
if (request.onsuccess) {
|
if (request.onsuccess) {
|
||||||
request.onsuccess({ target: { result: mockDB } });
|
request.onsuccess({ target: { result: mockDB } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit for init to complete, then trigger put success
|
// Wait a bit for init to complete, then trigger put success
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
mockPutRequest.onsuccess();
|
mockPutRequest.onsuccess();
|
||||||
|
|
||||||
await savePromise;
|
await savePromise;
|
||||||
|
|
@ -234,4 +244,3 @@ describe("Core: Persistence", () => {
|
||||||
expect(mockStore.put.calledOnce).to.be.true;
|
expect(mockStore.put.calledOnce).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ describe("Generation: PostProcessor", () => {
|
||||||
it("CoA 4: floodFill should not include disconnected tiles", () => {
|
it("CoA 4: floodFill should not include disconnected tiles", () => {
|
||||||
// Fill entire grid with solid first to isolate regions
|
// Fill entire grid with solid first to isolate regions
|
||||||
grid.fill(1);
|
grid.fill(1);
|
||||||
|
|
||||||
// Create two separate regions with proper floor setup
|
// Create two separate regions with proper floor setup
|
||||||
// Region 1: connected tiles
|
// Region 1: connected tiles
|
||||||
grid.setCell(1, 0, 1, 1); // Floor
|
grid.setCell(1, 0, 1, 1); // Floor
|
||||||
|
|
@ -155,4 +155,3 @@ describe("Generation: PostProcessor", () => {
|
||||||
expect(airCountAfter).to.be.greaterThan(0);
|
expect(airCountAfter).to.be.greaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue