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("../grid/types.js").Position} Position
|
||||
* @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";
|
||||
|
|
@ -476,6 +479,9 @@ export class GameLoop {
|
|||
this.gameStateManager.transitionTo("STATE_COMBAT");
|
||||
}
|
||||
|
||||
// Initialize combat state
|
||||
this.updateCombatState();
|
||||
|
||||
console.log("Combat Started!");
|
||||
}
|
||||
|
||||
|
|
@ -594,4 +600,117 @@ export class GameLoop {
|
|||
if (this.inputManager) this.inputManager.detach();
|
||||
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").EmbarkEventDetail} EmbarkEventDetail
|
||||
* @typedef {import("./types.js").SquadMember} SquadMember
|
||||
* @typedef {import("../ui/combat-hud.d.ts").CombatState} CombatState
|
||||
*/
|
||||
|
||||
import { Persistence } from "./Persistence.js";
|
||||
|
|
@ -42,6 +43,8 @@ class GameStateManagerClass {
|
|||
this.persistence = new Persistence();
|
||||
/** @type {RunData | null} */
|
||||
this.activeRunData = null;
|
||||
/** @type {CombatState | null} */
|
||||
this.combatState = null;
|
||||
|
||||
// Integrate Core Managers
|
||||
/** @type {RosterManager} */
|
||||
|
|
@ -91,6 +94,7 @@ class GameStateManagerClass {
|
|||
this.currentState = GameStateManagerClass.STATES.INIT;
|
||||
this.gameLoop = null;
|
||||
this.activeRunData = null;
|
||||
this.combatState = null;
|
||||
this.rosterManager = new RosterManager();
|
||||
this.missionManager = new MissionManager();
|
||||
// Reset promise resolvers
|
||||
|
|
@ -150,8 +154,12 @@ class GameStateManagerClass {
|
|||
break;
|
||||
|
||||
case GameStateManagerClass.STATES.DEPLOYMENT:
|
||||
// Clear combat state when leaving combat
|
||||
this.combatState = null;
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -294,6 +302,29 @@ class GameStateManagerClass {
|
|||
const data = this.rosterManager.save();
|
||||
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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class GameViewport extends LitElement {
|
|||
return {
|
||||
squad: { type: Array },
|
||||
deployedIds: { type: Array },
|
||||
combatState: { type: Object },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ export class GameViewport extends LitElement {
|
|||
super();
|
||||
this.squad = [];
|
||||
this.deployedIds = [];
|
||||
this.combatState = null;
|
||||
}
|
||||
|
||||
#handleUnitSelected(event) {
|
||||
|
|
@ -51,6 +53,29 @@ export class GameViewport extends LitElement {
|
|||
loop.init(container);
|
||||
gameStateManager.setGameLoop(loop);
|
||||
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() {
|
||||
|
|
@ -61,7 +86,7 @@ export class GameViewport extends LitElement {
|
|||
@unit-selected=${this.#handleUnitSelected}
|
||||
@start-battle=${this.#handleStartBattle}
|
||||
></deployment-hud>
|
||||
<combat-hud></combat-hud>
|
||||
<combat-hud .combatState=${this.combatState}></combat-hud>
|
||||
<dialogue-overlay></dialogue-overlay>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@ describe("Core: Persistence", () => {
|
|||
};
|
||||
|
||||
// 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 = () => {
|
||||
|
|
@ -46,7 +51,7 @@ describe("Core: Persistence", () => {
|
|||
};
|
||||
|
||||
// Mock indexedDB using defineProperty since it's read-only
|
||||
Object.defineProperty(globalObj, 'indexedDB', {
|
||||
Object.defineProperty(globalObj, "indexedDB", {
|
||||
value: {
|
||||
open: sinon.stub().returns(mockRequest),
|
||||
},
|
||||
|
|
@ -76,8 +81,10 @@ describe("Core: Persistence", () => {
|
|||
await initPromise;
|
||||
|
||||
expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 2)).to.be.true;
|
||||
expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be.true;
|
||||
expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to.be.true;
|
||||
expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be
|
||||
.true;
|
||||
expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to
|
||||
.be.true;
|
||||
expect(persistence.db).to.equal(mockDB);
|
||||
});
|
||||
|
||||
|
|
@ -166,7 +173,10 @@ describe("Core: Persistence", () => {
|
|||
|
||||
it("CoA 6: loadRoster should extract data from stored object", async () => {
|
||||
persistence.db = mockDB;
|
||||
const storedData = { id: "player_roster", data: { roster: [], graveyard: [] } };
|
||||
const storedData = {
|
||||
id: "player_roster",
|
||||
data: { roster: [], graveyard: [] },
|
||||
};
|
||||
|
||||
const mockGetRequest = {
|
||||
onsuccess: null,
|
||||
|
|
@ -225,7 +235,7 @@ describe("Core: Persistence", () => {
|
|||
}
|
||||
|
||||
// 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();
|
||||
|
||||
await savePromise;
|
||||
|
|
@ -234,4 +244,3 @@ describe("Core: Persistence", () => {
|
|||
expect(mockStore.put.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -155,4 +155,3 @@ describe("Generation: PostProcessor", () => {
|
|||
expect(airCountAfter).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue