feat: introduce core game systems including input management with raycasting, voxel and mission managers, game loop, combat HUD, and unit assets.
This commit is contained in:
parent
964a12fa47
commit
2898701b46
7 changed files with 333 additions and 46 deletions
19
src/assets/data/units/mule_bot.json
Normal file
19
src/assets/data/units/mule_bot.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"id": "UNIT_MULE_BOT",
|
||||||
|
"name": "Mule Bot",
|
||||||
|
"type": "ALLY",
|
||||||
|
"base_stats": {
|
||||||
|
"health": 250,
|
||||||
|
"attack": 0,
|
||||||
|
"defense": 10,
|
||||||
|
"magic": 0,
|
||||||
|
"speed": 8,
|
||||||
|
"willpower": 5,
|
||||||
|
"movement": 4
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"mesh": "MULE_BOT_MESH",
|
||||||
|
"color": "0xAAAAAA"
|
||||||
|
},
|
||||||
|
"actions": []
|
||||||
|
}
|
||||||
|
|
@ -1130,6 +1130,14 @@ 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;
|
||||||
|
|
||||||
|
// Replay controls initialization if needed (e.g. after stop())
|
||||||
|
if (!this.controls && this.camera && this.renderer) {
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enableDamping = true;
|
||||||
|
this.controls.dampingFactor = 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearUnitMeshes();
|
this.clearUnitMeshes();
|
||||||
this.clearMovementHighlights();
|
this.clearMovementHighlights();
|
||||||
this.clearSpawnZoneHighlights();
|
this.clearSpawnZoneHighlights();
|
||||||
|
|
@ -1137,12 +1145,29 @@ export class GameLoop {
|
||||||
this.clearZoneMarkers();
|
this.clearZoneMarkers();
|
||||||
this.clearRangeHighlights();
|
this.clearRangeHighlights();
|
||||||
|
|
||||||
|
// Re-attach input listeners if they were detached
|
||||||
|
if (this.inputManager && typeof this.inputManager.attach === "function") {
|
||||||
|
this.inputManager.attach();
|
||||||
|
}
|
||||||
|
|
||||||
// Reset Deployment State
|
// Reset Deployment State
|
||||||
this.deploymentState = {
|
this.deploymentState = {
|
||||||
selectedUnitIndex: -1,
|
selectedUnitIndex: -1,
|
||||||
deployedUnits: new Map(), // Map<Index, UnitInstance>
|
deployedUnits: new Map(), // Map<Index, UnitInstance>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Notify UI to clear deployment state
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("deployment-update", {
|
||||||
|
detail: {
|
||||||
|
deployedIndices: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restart the animation loop if it was stopped
|
||||||
|
this.animate();
|
||||||
|
|
||||||
this.grid = new VoxelGrid(20, 10, 20);
|
this.grid = new VoxelGrid(20, 10, 20);
|
||||||
const generator = new RuinGenerator(this.grid, runData.seed);
|
const generator = new RuinGenerator(this.grid, runData.seed);
|
||||||
generator.generate();
|
generator.generate();
|
||||||
|
|
@ -1159,19 +1184,7 @@ export class GameLoop {
|
||||||
|
|
||||||
// Dispose of old VoxelManager if it exists
|
// Dispose of old VoxelManager if it exists
|
||||||
if (this.voxelManager) {
|
if (this.voxelManager) {
|
||||||
// Clear all meshes from the old VoxelManager
|
this.voxelManager.clear();
|
||||||
this.voxelManager.meshes.forEach((mesh) => {
|
|
||||||
this.scene.remove(mesh);
|
|
||||||
if (mesh.geometry) mesh.geometry.dispose();
|
|
||||||
if (mesh.material) {
|
|
||||||
if (Array.isArray(mesh.material)) {
|
|
||||||
mesh.material.forEach((mat) => mat.dispose());
|
|
||||||
} else {
|
|
||||||
mesh.material.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.voxelManager.meshes.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.voxelManager = new VoxelManager(this.grid, this.scene);
|
this.voxelManager = new VoxelManager(this.grid, this.scene);
|
||||||
|
|
@ -1187,8 +1200,14 @@ export class GameLoop {
|
||||||
const classRegistry = new Map();
|
const classRegistry = new Map();
|
||||||
|
|
||||||
// Lazy-load class definitions
|
// Lazy-load class definitions
|
||||||
const [vanguardDef, weaverDef, scavengerDef, tinkerDef, custodianDef] =
|
const [
|
||||||
await Promise.all([
|
vanguardDef,
|
||||||
|
weaverDef,
|
||||||
|
scavengerDef,
|
||||||
|
tinkerDef,
|
||||||
|
custodianDef,
|
||||||
|
muleBotDef,
|
||||||
|
] = await Promise.all([
|
||||||
import("../assets/data/classes/vanguard.json", {
|
import("../assets/data/classes/vanguard.json", {
|
||||||
with: { type: "json" },
|
with: { type: "json" },
|
||||||
}).then((m) => m.default),
|
}).then((m) => m.default),
|
||||||
|
|
@ -1204,6 +1223,9 @@ export class GameLoop {
|
||||||
import("../assets/data/classes/custodian.json", {
|
import("../assets/data/classes/custodian.json", {
|
||||||
with: { type: "json" },
|
with: { type: "json" },
|
||||||
}).then((m) => m.default),
|
}).then((m) => m.default),
|
||||||
|
import("../assets/data/units/mule_bot.json", {
|
||||||
|
with: { type: "json" },
|
||||||
|
}).then((m) => m.default),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Register all class definitions
|
// Register all class definitions
|
||||||
|
|
@ -1213,6 +1235,7 @@ export class GameLoop {
|
||||||
scavengerDef,
|
scavengerDef,
|
||||||
tinkerDef,
|
tinkerDef,
|
||||||
custodianDef,
|
custodianDef,
|
||||||
|
muleBotDef,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const classDef of classDefs) {
|
for (const classDef of classDefs) {
|
||||||
|
|
@ -1604,6 +1627,82 @@ export class GameLoop {
|
||||||
console.log(`Spawned ${totalSpawned} enemies from mission definition`);
|
console.log(`Spawned ${totalSpawned} enemies from mission definition`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn Escort/VIP Units defined in objectives
|
||||||
|
// Check primary and secondary objectives for ESCORT type
|
||||||
|
const allObjectives = [
|
||||||
|
...(missionDef.objectives?.primary || []),
|
||||||
|
...(missionDef.objectives?.secondary || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const escortObjectives = allObjectives.filter(
|
||||||
|
(obj) => obj.type === "ESCORT"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const obj of escortObjectives) {
|
||||||
|
if (obj.target_def_id) {
|
||||||
|
// Create the VIP unit
|
||||||
|
// We assign to PLAYER team so the user can control them (or we could use NEUTRAL/ALLY if AI controlled)
|
||||||
|
// For now, assuming player control is desired for "Supply Run"
|
||||||
|
const vipUnit = this.unitManager.createUnit(
|
||||||
|
obj.target_def_id,
|
||||||
|
"PLAYER"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (vipUnit) {
|
||||||
|
// Place near player start
|
||||||
|
let spawnPos = { x: 2, y: 1, z: 2 }; // Fallback
|
||||||
|
|
||||||
|
if (this.playerSpawnZone.length > 0) {
|
||||||
|
// Use the last spot in the spawn zone or a dedicated spot
|
||||||
|
// Ideally we find a free spot near the spawn zone center
|
||||||
|
const spot = this.playerSpawnZone[0]; // Just use first spot for now, or find free one
|
||||||
|
|
||||||
|
// Find a free spot near the spawn zone
|
||||||
|
const startX = spot.x;
|
||||||
|
const startZ = spot.z;
|
||||||
|
|
||||||
|
// Spiral search for free spot
|
||||||
|
let found = false;
|
||||||
|
for (let r = 0; r < 5; r++) {
|
||||||
|
for (let dx = -r; dx <= r; dx++) {
|
||||||
|
for (let dz = -r; dz <= r; dz++) {
|
||||||
|
const tx = startX + dx;
|
||||||
|
const tz = startZ + dz;
|
||||||
|
if (
|
||||||
|
this.grid.isValidBounds({ x: tx, y: spot.y, z: tz }) &&
|
||||||
|
!this.grid.isOccupied({ x: tx, y: spot.y, z: tz })
|
||||||
|
) {
|
||||||
|
const walkableY = this.movementSystem?.findWalkableY(
|
||||||
|
tx,
|
||||||
|
tz,
|
||||||
|
spot.y
|
||||||
|
);
|
||||||
|
if (walkableY !== null) {
|
||||||
|
spawnPos = { x: tx, y: walkableY, z: tz };
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
if (found) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.grid.placeUnit(vipUnit, spawnPos);
|
||||||
|
this.createUnitMesh(vipUnit, spawnPos);
|
||||||
|
console.log(
|
||||||
|
`Spawned Escort VIP: ${vipUnit.name} at ${spawnPos.x},${spawnPos.y},${spawnPos.z}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`Failed to create Escort VIP unit: ${obj.target_def_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn mission objects
|
// Spawn mission objects
|
||||||
const missionObjects = missionDef?.mission_objects || [];
|
const missionObjects = missionDef?.mission_objects || [];
|
||||||
for (const objDef of missionObjects) {
|
for (const objDef of missionObjects) {
|
||||||
|
|
@ -2166,6 +2265,17 @@ export class GameLoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Override from Unit Definition (e.g. Mule Bot)
|
||||||
|
if (unit.voxelModelID && unit.voxelModelID.color) {
|
||||||
|
// Handle both string hex "0xAAAAAA" and number
|
||||||
|
if (typeof unit.voxelModelID.color === "string") {
|
||||||
|
color = parseInt(unit.voxelModelID.color.replace("0x", ""), 16);
|
||||||
|
} else {
|
||||||
|
color = unit.voxelModelID.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const material = new THREE.MeshStandardMaterial({ color: color });
|
const material = new THREE.MeshStandardMaterial({ color: color });
|
||||||
const mesh = new THREE.Mesh(geometry, material);
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
|
@ -2720,6 +2830,11 @@ export class GameLoop {
|
||||||
this.clearMissionObjects();
|
this.clearMissionObjects();
|
||||||
this.clearRangeHighlights();
|
this.clearRangeHighlights();
|
||||||
|
|
||||||
|
// Force a final render to clear the screen
|
||||||
|
if (this.renderer && this.scene && this.camera) {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear unit manager
|
// Clear unit manager
|
||||||
if (this.unitManager) {
|
if (this.unitManager) {
|
||||||
// UnitManager doesn't have a clear method, but we can reset it by clearing units
|
// UnitManager doesn't have a clear method, but we can reset it by clearing units
|
||||||
|
|
@ -2729,6 +2844,11 @@ export class GameLoop {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear VoxelManager (map meshes and materials)
|
||||||
|
if (this.voxelManager) {
|
||||||
|
this.voxelManager.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// Reset deployment state
|
// Reset deployment state
|
||||||
this.deploymentState = {
|
this.deploymentState = {
|
||||||
selectedUnitIndex: -1,
|
selectedUnitIndex: -1,
|
||||||
|
|
@ -2738,7 +2858,10 @@ export class GameLoop {
|
||||||
if (this.inputManager && typeof this.inputManager.detach === "function") {
|
if (this.inputManager && typeof this.inputManager.detach === "function") {
|
||||||
this.inputManager.detach();
|
this.inputManager.detach();
|
||||||
}
|
}
|
||||||
if (this.controls) this.controls.dispose();
|
if (this.controls) {
|
||||||
|
this.controls.dispose();
|
||||||
|
this.controls = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,11 @@ export class InputManager extends EventTarget {
|
||||||
window.addEventListener("keyup", this.onKeyUp);
|
window.addEventListener("keyup", this.onKeyUp);
|
||||||
window.addEventListener("gamepadconnected", this.onGamepadConnected);
|
window.addEventListener("gamepadconnected", this.onGamepadConnected);
|
||||||
window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected);
|
window.addEventListener("gamepaddisconnected", this.onGamepadDisconnected);
|
||||||
|
|
||||||
|
// Re-add cursor to scene if missing and scene is available
|
||||||
|
if (this.cursor && this.scene && !this.cursor.parent) {
|
||||||
|
this.scene.add(this.cursor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
detach() {
|
||||||
|
|
|
||||||
|
|
@ -753,4 +753,44 @@ export class VoxelManager {
|
||||||
this.clearRangeHighlights();
|
this.clearRangeHighlights();
|
||||||
this.clearReticle();
|
this.clearReticle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all meshes and disposes of geometry/materials.
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
// Clear instanced meshes
|
||||||
|
this.meshes.forEach((mesh) => {
|
||||||
|
this.scene.remove(mesh);
|
||||||
|
if (mesh.geometry) mesh.geometry.dispose();
|
||||||
|
// Materials are shared, handled below
|
||||||
|
});
|
||||||
|
this.meshes.clear();
|
||||||
|
|
||||||
|
// Clear highlights
|
||||||
|
this.clearHighlights();
|
||||||
|
|
||||||
|
// Dispose of materials
|
||||||
|
Object.values(this.materials).forEach((material) => {
|
||||||
|
if (Array.isArray(material)) {
|
||||||
|
material.forEach((m) => {
|
||||||
|
if (m.map) m.map.dispose();
|
||||||
|
if (m.emissiveMap) m.emissiveMap.dispose();
|
||||||
|
if (m.normalMap) m.normalMap.dispose();
|
||||||
|
if (m.roughnessMap) m.roughnessMap.dispose();
|
||||||
|
if (m.bumpMap) m.bumpMap.dispose();
|
||||||
|
m.dispose();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (material.map) material.map.dispose();
|
||||||
|
if (material.emissiveMap) material.emissiveMap.dispose();
|
||||||
|
if (material.normalMap) material.normalMap.dispose();
|
||||||
|
if (material.roughnessMap) material.roughnessMap.dispose();
|
||||||
|
if (material.bumpMap) material.bumpMap.dispose();
|
||||||
|
material.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear references
|
||||||
|
this.materials = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export class MissionManager {
|
||||||
/** @type {Set<string>} */
|
/** @type {Set<string>} */
|
||||||
this.unlockedMissions = new Set(); // Track unlocked missions
|
this.unlockedMissions = new Set(); // Track unlocked missions
|
||||||
this.unlockedMissions.add("MISSION_ACT1_01"); // Default unlock
|
this.unlockedMissions.add("MISSION_ACT1_01"); // Default unlock
|
||||||
|
this.proceduralMissionsUnlocked = false; // Flag for procedural generation
|
||||||
/** @type {Map<string, MissionDefinition>} */
|
/** @type {Map<string, MissionDefinition>} */
|
||||||
this.missionRegistry = new Map();
|
this.missionRegistry = new Map();
|
||||||
/** @type {Map<string, MissionDefinition>} */
|
/** @type {Map<string, MissionDefinition>} */
|
||||||
|
|
@ -171,11 +172,10 @@ export class MissionManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if procedural missions are unlocked.
|
* Checks if procedural missions are unlocked.
|
||||||
* Procedural missions unlock after completing mission 3 (end of tutorial phase).
|
* @returns {boolean}
|
||||||
* @returns {boolean} True if procedural missions are unlocked
|
|
||||||
*/
|
*/
|
||||||
areProceduralMissionsUnlocked() {
|
areProceduralMissionsUnlocked() {
|
||||||
return this.completedMissions.has("MISSION_STORY_03");
|
return this.proceduralMissionsUnlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -742,11 +742,16 @@ export class MissionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SURVIVE: Check turn count
|
||||||
// SURVIVE: Check turn count
|
// SURVIVE: Check turn count
|
||||||
if (eventType === "TURN_END" && obj.type === "SURVIVE") {
|
if (eventType === "TURN_END" && obj.type === "SURVIVE") {
|
||||||
if (obj.turn_count && this.currentTurn >= obj.turn_count) {
|
const limit = obj.turn_limit || obj.turn_count;
|
||||||
|
if (limit && this.currentTurn >= limit) {
|
||||||
obj.complete = true;
|
obj.complete = true;
|
||||||
statusChanged = true;
|
statusChanged = true;
|
||||||
|
console.log(
|
||||||
|
`[MissionManager] SURVIVE objective complete (Turn ${this.currentTurn} >= ${limit})`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1141,6 +1146,11 @@ export class MissionManager {
|
||||||
// Procedural missions are now unlocked
|
// Procedural missions are now unlocked
|
||||||
// They will be generated when the mission board is accessed
|
// They will be generated when the mission board is accessed
|
||||||
console.log("Procedural missions unlocked!");
|
console.log("Procedural missions unlocked!");
|
||||||
|
this.proceduralMissionsUnlocked = true;
|
||||||
|
// Persist the unlock
|
||||||
|
this.unlockClasses(["UNLOCK_PROCEDURAL_MISSIONS"]);
|
||||||
|
// Refresh board immediately
|
||||||
|
this.refreshProceduralMissions();
|
||||||
} else if (unlock.startsWith("MISSION_")) {
|
} else if (unlock.startsWith("MISSION_")) {
|
||||||
this.unlockMission(unlock);
|
this.unlockMission(unlock);
|
||||||
}
|
}
|
||||||
|
|
@ -1295,6 +1305,8 @@ export class MissionManager {
|
||||||
for (const u of unlocks) {
|
for (const u of unlocks) {
|
||||||
if (u.startsWith("MISSION_")) {
|
if (u.startsWith("MISSION_")) {
|
||||||
this.unlockedMissions.add(u);
|
this.unlockedMissions.add(u);
|
||||||
|
} else if (u === "UNLOCK_PROCEDURAL_MISSIONS") {
|
||||||
|
this.proceduralMissionsUnlocked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,8 @@ export class CombatHUD extends LitElement {
|
||||||
margin: 0 0 var(--spacing-sm) 0;
|
margin: 0 0 var(--spacing-sm) 0;
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
color: var(--color-accent-gold);
|
color: var(--color-accent-gold);
|
||||||
border-bottom: var(--border-width-thin) solid var(--color-border-default);
|
border-bottom: var(--border-width-thin) solid
|
||||||
|
var(--color-border-default);
|
||||||
padding-bottom: var(--spacing-xs);
|
padding-bottom: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +414,8 @@ export class CombatHUD extends LitElement {
|
||||||
margin-bottom: var(--spacing-sm);
|
margin-bottom: var(--spacing-sm);
|
||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xs);
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
border-left: var(--border-width-thin) solid var(--color-border-default);
|
border-left: var(--border-width-thin) solid
|
||||||
|
var(--color-border-default);
|
||||||
padding-left: var(--spacing-sm);
|
padding-left: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,6 +522,7 @@ export class CombatHUD extends LitElement {
|
||||||
this.combatState = null;
|
this.combatState = null;
|
||||||
this.hidden = false;
|
this.hidden = false;
|
||||||
this._missionVictoryHandler = this._handleMissionVictory.bind(this);
|
this._missionVictoryHandler = this._handleMissionVictory.bind(this);
|
||||||
|
this._gameStateChangeHandler = this._handleGameStateChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
@ -527,12 +530,27 @@ export class CombatHUD extends LitElement {
|
||||||
// Listen for mission victory to hide the combat HUD
|
// Listen for mission victory to hide the combat HUD
|
||||||
window.addEventListener("mission-victory", this._missionVictoryHandler);
|
window.addEventListener("mission-victory", this._missionVictoryHandler);
|
||||||
window.addEventListener("mission-failure", this._missionVictoryHandler);
|
window.addEventListener("mission-failure", this._missionVictoryHandler);
|
||||||
|
// Listen for game state changes to reset visibility
|
||||||
|
window.addEventListener("gamestate-changed", this._gameStateChangeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
window.removeEventListener("mission-victory", this._missionVictoryHandler);
|
window.removeEventListener("mission-victory", this._missionVictoryHandler);
|
||||||
window.removeEventListener("mission-failure", this._missionVictoryHandler);
|
window.removeEventListener("mission-failure", this._missionVictoryHandler);
|
||||||
|
window.removeEventListener(
|
||||||
|
"gamestate-changed",
|
||||||
|
this._gameStateChangeHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleGameStateChange(e) {
|
||||||
|
// Only show Combat HUD when in actual combat state
|
||||||
|
if (e.detail.newState === "STATE_COMBAT") {
|
||||||
|
this.hidden = false;
|
||||||
|
} else {
|
||||||
|
this.hidden = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMissionVictory() {
|
_handleMissionVictory() {
|
||||||
|
|
@ -701,9 +719,7 @@ export class CombatHUD extends LitElement {
|
||||||
<h3>OBJECTIVES</h3>
|
<h3>OBJECTIVES</h3>
|
||||||
${missionObjectives.primary?.map(
|
${missionObjectives.primary?.map(
|
||||||
(obj) => html`
|
(obj) => html`
|
||||||
<div
|
<div class="objective-item ${obj.complete ? "complete" : ""}">
|
||||||
class="objective-item ${obj.complete ? "complete" : ""}"
|
|
||||||
>
|
|
||||||
<div class="objective-description">
|
<div class="objective-description">
|
||||||
${obj.complete ? "✓ " : ""}${obj.description}
|
${obj.complete ? "✓ " : ""}${obj.description}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -717,7 +733,8 @@ export class CombatHUD extends LitElement {
|
||||||
${obj.type === "REACH_ZONE" && obj.zone_coords
|
${obj.type === "REACH_ZONE" && obj.zone_coords
|
||||||
? html`
|
? html`
|
||||||
<div class="objective-progress">
|
<div class="objective-progress">
|
||||||
Zones: ${obj.zone_coords.length} target${obj.zone_coords.length !== 1 ? "s" : ""}
|
Zones: ${obj.zone_coords.length}
|
||||||
|
target${obj.zone_coords.length !== 1 ? "s" : ""}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
@ -726,9 +743,7 @@ export class CombatHUD extends LitElement {
|
||||||
)}
|
)}
|
||||||
${missionObjectives.secondary?.length > 0
|
${missionObjectives.secondary?.length > 0
|
||||||
? html`
|
? html`
|
||||||
<h3 style="margin-top: var(--spacing-md);">
|
<h3 style="margin-top: var(--spacing-md);">SECONDARY</h3>
|
||||||
SECONDARY
|
|
||||||
</h3>
|
|
||||||
${missionObjectives.secondary.map(
|
${missionObjectives.secondary.map(
|
||||||
(obj) => html`
|
(obj) => html`
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
73
test/managers/MissionManager_objectives.test.js
Normal file
73
test/managers/MissionManager_objectives.test.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { expect } from "@esm-bundle/chai";
|
||||||
|
import sinon from "sinon";
|
||||||
|
import { MissionManager } from "../../src/managers/MissionManager.js";
|
||||||
|
|
||||||
|
describe("Manager: MissionManager - SURVIVE Objective Bug", () => {
|
||||||
|
let manager;
|
||||||
|
let mockPersistence;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPersistence = {
|
||||||
|
loadUnlocks: sinon.stub().resolves([]),
|
||||||
|
saveUnlocks: sinon.stub().resolves(),
|
||||||
|
};
|
||||||
|
manager = new MissionManager(mockPersistence);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should complete SURVIVE objective when turn limit is reached (using turn_limit property)", async () => {
|
||||||
|
// Mock a mission with SURVIVE objective using turn_limit (like in the JSON files)
|
||||||
|
manager.currentMissionDef = {
|
||||||
|
id: "MISSION_TEST",
|
||||||
|
objectives: {
|
||||||
|
primary: [
|
||||||
|
{
|
||||||
|
type: "SURVIVE",
|
||||||
|
turn_limit: 5, // Using turn_limit as found in JSON
|
||||||
|
current: 0,
|
||||||
|
complete: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rewards: { guaranteed: {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.activeMissionId = "MISSION_TEST";
|
||||||
|
manager.currentObjectives = manager.currentMissionDef.objectives.primary;
|
||||||
|
manager.currentTurn = 0;
|
||||||
|
|
||||||
|
// Simulate turns passing
|
||||||
|
manager.updateTurn(5);
|
||||||
|
manager.onGameEvent("TURN_END", { turn: 5 });
|
||||||
|
|
||||||
|
// Should be complete
|
||||||
|
expect(
|
||||||
|
manager.currentObjectives[0].complete,
|
||||||
|
"Objective should be complete after 5 turns"
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should unlock procedural missions when reward is granted", async () => {
|
||||||
|
// Setup
|
||||||
|
const unlockStr = "UNLOCK_PROCEDURAL_MISSIONS";
|
||||||
|
manager.currentMissionDef = {
|
||||||
|
rewards: {
|
||||||
|
guaranteed: {
|
||||||
|
unlocks: [unlockStr],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stub unlockClasses to verify it's called
|
||||||
|
manager.unlockClasses = sinon.stub();
|
||||||
|
// Stub refreshProceduralMissions
|
||||||
|
manager.refreshProceduralMissions = sinon.stub();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
manager.distributeRewards();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(manager.proceduralMissionsUnlocked).to.be.true;
|
||||||
|
expect(manager.unlockClasses.calledWith([unlockStr])).to.be.true;
|
||||||
|
expect(manager.refreshProceduralMissions.called).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue