Compare commits
2 commits
095bd778fd
...
17590cdab0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17590cdab0 | ||
|
|
1b8775657f |
5 changed files with 238 additions and 31 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>`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,17 @@ describe("Core: Persistence", () => {
|
|||
transaction: sinon.stub().returns(mockTransaction),
|
||||
};
|
||||
|
||||
// Mock indexedDB.open
|
||||
// Use window or self for browser environment
|
||||
globalObj =
|
||||
typeof window !== "undefined"
|
||||
? window
|
||||
: typeof self !== "undefined"
|
||||
? self
|
||||
: globalThis;
|
||||
});
|
||||
|
||||
const createMockRequest = () => {
|
||||
// Mock indexedDB.open - create a new request each time
|
||||
mockRequest = {
|
||||
onerror: null,
|
||||
onsuccess: null,
|
||||
|
|
@ -40,34 +50,41 @@ describe("Core: Persistence", () => {
|
|||
result: mockDB,
|
||||
};
|
||||
|
||||
// Use window or self for browser environment
|
||||
globalObj = typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : globalThis);
|
||||
globalObj.indexedDB = {
|
||||
open: sinon.stub().returns(mockRequest),
|
||||
};
|
||||
});
|
||||
// Mock indexedDB using defineProperty since it's read-only
|
||||
Object.defineProperty(globalObj, "indexedDB", {
|
||||
value: {
|
||||
open: sinon.stub().returns(mockRequest),
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const triggerSuccess = () => {
|
||||
if (mockRequest.onsuccess) {
|
||||
mockRequest.onsuccess({ target: { result: mockDB } });
|
||||
}
|
||||
};
|
||||
|
||||
const triggerUpgrade = () => {
|
||||
if (mockRequest.onupgradeneeded) {
|
||||
mockRequest.onupgradeneeded({ target: { result: mockDB } });
|
||||
}
|
||||
return mockRequest;
|
||||
};
|
||||
|
||||
it("CoA 1: init should create database and object stores", async () => {
|
||||
triggerUpgrade();
|
||||
triggerSuccess();
|
||||
const request = createMockRequest();
|
||||
|
||||
await persistence.init();
|
||||
// Start init, which will call indexedDB.open
|
||||
const initPromise = persistence.init();
|
||||
|
||||
// Trigger upgrade first (happens synchronously during open)
|
||||
if (request.onupgradeneeded) {
|
||||
request.onupgradeneeded({ target: { result: mockDB } });
|
||||
}
|
||||
|
||||
// Then trigger success
|
||||
if (request.onsuccess) {
|
||||
request.onsuccess({ target: { result: mockDB } });
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
@ -156,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,
|
||||
|
|
@ -194,17 +214,28 @@ describe("Core: Persistence", () => {
|
|||
});
|
||||
|
||||
it("CoA 8: saveRun should auto-init if db not initialized", async () => {
|
||||
triggerUpgrade();
|
||||
triggerSuccess();
|
||||
const request = createMockRequest();
|
||||
|
||||
const runData = { seed: 12345 };
|
||||
const mockPutRequest = {
|
||||
onsuccess: null,
|
||||
onsuccess: sinon.stub(),
|
||||
onerror: null,
|
||||
};
|
||||
mockStore.put.returns(mockPutRequest);
|
||||
|
||||
// Start saveRun, which will trigger init
|
||||
const savePromise = persistence.saveRun(runData);
|
||||
|
||||
// Trigger upgrade and success for init
|
||||
if (request.onupgradeneeded) {
|
||||
request.onupgradeneeded({ target: { result: mockDB } });
|
||||
}
|
||||
if (request.onsuccess) {
|
||||
request.onsuccess({ target: { result: mockDB } });
|
||||
}
|
||||
|
||||
// Wait a bit for init to complete, then trigger put success
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
mockPutRequest.onsuccess();
|
||||
|
||||
await savePromise;
|
||||
|
|
@ -213,4 +244,3 @@ describe("Core: Persistence", () => {
|
|||
expect(mockStore.put.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ describe("Generation: PostProcessor", () => {
|
|||
});
|
||||
|
||||
it("CoA 4: floodFill should not include disconnected tiles", () => {
|
||||
// Fill entire grid with solid first to isolate regions
|
||||
grid.fill(1);
|
||||
|
||||
// Create two separate regions with proper floor setup
|
||||
// Region 1: connected tiles
|
||||
grid.setCell(1, 0, 1, 1); // Floor
|
||||
|
|
@ -89,7 +92,7 @@ describe("Generation: PostProcessor", () => {
|
|||
grid.setCell(2, 0, 1, 1); // Floor
|
||||
grid.setCell(2, 1, 1, 0); // Air (connected)
|
||||
|
||||
// Region 2: disconnected (no floor connection)
|
||||
// Region 2: disconnected (surrounded by solid, no path to region 1)
|
||||
grid.setCell(10, 0, 10, 1); // Floor
|
||||
grid.setCell(10, 1, 10, 0); // Air (disconnected)
|
||||
|
||||
|
|
@ -152,4 +155,3 @@ describe("Generation: PostProcessor", () => {
|
|||
expect(airCountAfter).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue