feat: Implement core game loop, state management, mission generation, and add initial mission data with debug commands.
This commit is contained in:
parent
2898701b46
commit
930b1c7438
32 changed files with 1022 additions and 315 deletions
5
build.js
5
build.js
|
|
@ -1,5 +1,7 @@
|
|||
import { build } from "esbuild";
|
||||
import { copyFileSync, mkdirSync, readdirSync } from "fs";
|
||||
import { generateManifest } from "./src/utils/generate_manifest.js";
|
||||
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
|
|
@ -78,6 +80,9 @@ copyFileSync("src/index.html", "dist/index.html");
|
|||
// Copy images
|
||||
copyImages("src", "dist");
|
||||
|
||||
// Generate manifest
|
||||
generateManifest();
|
||||
|
||||
// Copy assets (JSON, markdown, TypeScript definitions)
|
||||
copyAssets("src/assets", "dist/assets");
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Missions use a context-aware "Operation: [Adjective] [Noun] [Numeral]" format.
|
|||
|
||||
- **Skirmish Nouns:** _Thunder, Storm, Iron, Fury, Shield, Hammer._
|
||||
- **Salvage Nouns:** _Cache, Vault, Echo, Spark, Harvest, Trove._
|
||||
- **Assassination Nouns:** _Viper, Dagger, Fang, Night, Razor, Sting._
|
||||
- **Eliminate Unit Nouns:** _Viper, Dagger, Fang, Night, Razor, Sting._
|
||||
- **Recon Nouns:** _Eye, Watch, Path, Horizon, Whisper, Scope._
|
||||
|
||||
### **B. Narrative Synthesis (Dynamic Context)**
|
||||
|
|
@ -53,7 +53,7 @@ _The Generator creates these JSON blobs and registers them with the NarrativeMan
|
|||
- **Config:** High Cover Density.
|
||||
- **Reward Bonus:** Items/Materials.
|
||||
|
||||
#### **3. Assassination (Boss Focus)**
|
||||
#### **3. Eliminate Unit (Boss Focus)**
|
||||
|
||||
- **Objective:** ELIMINATE_UNIT (Named Elite).
|
||||
- **Icon:** assets/icons/mission_skull.png.
|
||||
|
|
|
|||
18
src/assets/data/manifest.json
Normal file
18
src/assets/data/manifest.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"classes": [
|
||||
"classes/aether_sentinel.json",
|
||||
"classes/aether_weaver.json",
|
||||
"classes/arcane_scourge.json",
|
||||
"classes/battle_mage.json",
|
||||
"classes/custodian.json",
|
||||
"classes/field_engineer.json",
|
||||
"classes/sapper.json",
|
||||
"classes/scavenger.json",
|
||||
"classes/tinker.json",
|
||||
"classes/vanguard.json"
|
||||
],
|
||||
"units": [
|
||||
"units/enemy_commander.json",
|
||||
"units/mule_bot.json"
|
||||
]
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ This example utilizes every capability of the system.
|
|||
|
||||
### **Procedural & Dynamic Fields**
|
||||
|
||||
- **boss_config** (in `config`): Used for Assassination missions to define the target.
|
||||
- **boss_config** (in `config`): Used for Eliminate Unit missions to define the target.
|
||||
- **target_def_id**: Enemy Definition ID for the boss.
|
||||
- **name**: Specific name override for the boss.
|
||||
- **stats**: Object containing multipliers for hp and attack.
|
||||
|
|
|
|||
2
src/assets/data/missions/mission.d.ts
vendored
2
src/assets/data/missions/mission.d.ts
vendored
|
|
@ -51,7 +51,7 @@ export interface MissionConfig {
|
|||
* - "locked": Mission shows as locked with requirements visible (default for SIDE_QUEST, PROCEDURAL)
|
||||
*/
|
||||
visibility_when_locked?: "hidden" | "locked";
|
||||
/** Boss configuration for Assassination missions (Procedural only) */
|
||||
/** Boss configuration for Eliminate Unit missions (Procedural only) */
|
||||
boss_config?: BossConfig;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_RUSTING_WASTES",
|
||||
"hazards": ["HAZARD_NEST_SPORES"]
|
||||
"hazards": [
|
||||
"HAZARD_NEST_SPORES"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "HIGH"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_11_INTRO",
|
||||
|
|
@ -33,7 +39,10 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"unlocks": ["BLUEPRINT_HEAVY_PLATE_MK2", "MISSION_STORY_12"]
|
||||
"unlocks": [
|
||||
"BLUEPRINT_HEAVY_PLATE_MK2",
|
||||
"MISSION_STORY_12"
|
||||
]
|
||||
},
|
||||
"faction_reputation": {
|
||||
"IRON_LEGION": 50
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_FUNGAL_CAVES",
|
||||
"hazards": ["HAZARD_REGROWING_VINES"]
|
||||
"hazards": [
|
||||
"HAZARD_REGROWING_VINES"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "MEDIUM"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_16_INTRO",
|
||||
|
|
@ -29,7 +35,10 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"unlocks": ["BLUEPRINT_REGEN_RING", "MISSION_STORY_17"]
|
||||
"unlocks": [
|
||||
"BLUEPRINT_REGEN_RING",
|
||||
"MISSION_STORY_17"
|
||||
]
|
||||
},
|
||||
"faction_reputation": {
|
||||
"SILENT_SANCTUARY": 40
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_CRYSTAL_SPIRES",
|
||||
"hazards": ["HAZARD_FOG"]
|
||||
"hazards": [
|
||||
"HAZARD_FOG"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "LOW"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_17_INTRO",
|
||||
|
|
@ -29,8 +35,12 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"items": ["ITEM_SPIRIT_LANTERN"],
|
||||
"unlocks": ["MISSION_STORY_18"]
|
||||
"items": [
|
||||
"ITEM_SPIRIT_LANTERN"
|
||||
],
|
||||
"unlocks": [
|
||||
"MISSION_STORY_18"
|
||||
]
|
||||
},
|
||||
"faction_reputation": {
|
||||
"SILENT_SANCTUARY": 50
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_VOID_SEEP",
|
||||
"hazards": ["HAZARD_SPORE_VENTS"]
|
||||
"hazards": [
|
||||
"HAZARD_SPORE_VENTS"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "ARENA"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_18_INTRO",
|
||||
|
|
@ -28,7 +34,10 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"unlocks": ["CLASS_CUSTODIAN_MASTERY", "MISSION_STORY_19"]
|
||||
"unlocks": [
|
||||
"CLASS_CUSTODIAN_MASTERY",
|
||||
"MISSION_STORY_19"
|
||||
]
|
||||
},
|
||||
"faction_reputation": {
|
||||
"SILENT_SANCTUARY": 75
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_RUSTING_WASTES",
|
||||
"hazards": ["HAZARD_LIVE_WIRES"]
|
||||
"hazards": [
|
||||
"HAZARD_LIVE_WIRES"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "HIGH"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_20_INTRO",
|
||||
|
|
@ -31,7 +37,9 @@
|
|||
"currency": {
|
||||
"aether_shards": 800
|
||||
},
|
||||
"unlocks": ["MISSION_STORY_21"]
|
||||
"unlocks": [
|
||||
"MISSION_STORY_21"
|
||||
]
|
||||
}
|
||||
},
|
||||
"mission_objects": [
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_CRYSTAL_SPIRES",
|
||||
"hazards": ["HAZARD_GRAVITY_FLUX"]
|
||||
"hazards": [
|
||||
"HAZARD_GRAVITY_FLUX"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "LOW"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_21_INTRO",
|
||||
|
|
@ -28,8 +34,12 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"items": ["ITEM_AETHER_LENS"],
|
||||
"unlocks": ["MISSION_STORY_22"]
|
||||
"items": [
|
||||
"ITEM_AETHER_LENS"
|
||||
],
|
||||
"unlocks": [
|
||||
"MISSION_STORY_22"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,11 @@
|
|||
"icon": "assets/icons/mission_skull.png"
|
||||
},
|
||||
"biome": {
|
||||
"type": "BIOME_FUNGAL_CAVES"
|
||||
"type": "BIOME_FUNGAL_CAVES",
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "MEDIUM"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_22_INTRO",
|
||||
|
|
@ -27,8 +31,12 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"items": ["ITEM_CORRUPTED_IDOL"],
|
||||
"unlocks": ["MISSION_STORY_23"]
|
||||
"items": [
|
||||
"ITEM_CORRUPTED_IDOL"
|
||||
],
|
||||
"unlocks": [
|
||||
"MISSION_STORY_23"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_CONTESTED_FRONTIER",
|
||||
"hazards": ["HAZARD_FALLING_DEBRIS"]
|
||||
"hazards": [
|
||||
"HAZARD_FALLING_DEBRIS"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "EXTREME"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_23_INTRO",
|
||||
|
|
@ -40,7 +46,9 @@
|
|||
"currency": {
|
||||
"aether_shards": 900
|
||||
},
|
||||
"unlocks": ["MISSION_STORY_24"]
|
||||
"unlocks": [
|
||||
"MISSION_STORY_24"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,11 @@
|
|||
"icon": "assets/icons/mission_elevator.png"
|
||||
},
|
||||
"biome": {
|
||||
"type": "BIOME_VOID_SEEP"
|
||||
"type": "BIOME_VOID_SEEP",
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "ARENA"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_25_INTRO",
|
||||
|
|
@ -31,7 +35,10 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"unlocks": ["ACCESS_ACT_4", "MISSION_STORY_26"]
|
||||
"unlocks": [
|
||||
"ACCESS_ACT_4",
|
||||
"MISSION_STORY_26"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_CRYSTAL_SPIRES",
|
||||
"hazards": ["HAZARD_GRAVITY_FLUX_HARD"]
|
||||
"hazards": [
|
||||
"HAZARD_GRAVITY_FLUX_HARD"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "LOW"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_27_INTRO",
|
||||
|
|
@ -35,7 +41,9 @@
|
|||
"currency": {
|
||||
"ancient_cores": 2
|
||||
},
|
||||
"unlocks": ["MISSION_STORY_28"]
|
||||
"unlocks": [
|
||||
"MISSION_STORY_28"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,13 @@
|
|||
},
|
||||
"biome": {
|
||||
"type": "BIOME_FUNGAL_CAVES",
|
||||
"hazards": ["HAZARD_TOXIC_ATMOSPHERE"]
|
||||
"hazards": [
|
||||
"HAZARD_TOXIC_ATMOSPHERE"
|
||||
],
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "MEDIUM"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_28_INTRO",
|
||||
|
|
@ -31,7 +37,9 @@
|
|||
"currency": {
|
||||
"ancient_cores": 2
|
||||
},
|
||||
"unlocks": ["MISSION_STORY_29"]
|
||||
"unlocks": [
|
||||
"MISSION_STORY_29"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,11 @@
|
|||
"icon": "assets/icons/mission_shadow.png"
|
||||
},
|
||||
"biome": {
|
||||
"type": "BIOME_VOID_SEEP"
|
||||
"type": "BIOME_VOID_SEEP",
|
||||
"generator_config": {
|
||||
"seed_type": "RANDOM",
|
||||
"density": "ARENA"
|
||||
}
|
||||
},
|
||||
"narrative": {
|
||||
"intro_sequence": "NARRATIVE_29_INTRO",
|
||||
|
|
@ -26,8 +30,12 @@
|
|||
},
|
||||
"rewards": {
|
||||
"guaranteed": {
|
||||
"items": ["ITEM_VOID_ESSENCE"],
|
||||
"unlocks": ["MISSION_STORY_30"]
|
||||
"items": [
|
||||
"ITEM_VOID_ESSENCE"
|
||||
],
|
||||
"unlocks": [
|
||||
"MISSION_STORY_30"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/assets/data/units/enemy_commander.json
Normal file
14
src/assets/data/units/enemy_commander.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "DYNAMIC_COMMANDER",
|
||||
"type": "ENEMY",
|
||||
"name": "Opposing Commander",
|
||||
"model": "MODEL_ENEMY_COMMANDER",
|
||||
"stats": {
|
||||
"health": 200,
|
||||
"attack": 15,
|
||||
"defense": 5,
|
||||
"speed": 6
|
||||
},
|
||||
"ai_archetype": "TACTICIAN",
|
||||
"xp_value": 100
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ export class DebugCommands {
|
|||
|
||||
// Check for level up (simple XP curve: 100 * level^2)
|
||||
const mastery = unit.classMastery[unit.activeClassId];
|
||||
const xpForNextLevel = 100 * (mastery.level ** 2);
|
||||
const xpForNextLevel = 100 * mastery.level ** 2;
|
||||
|
||||
while (mastery.xp >= xpForNextLevel && mastery.level < 30) {
|
||||
mastery.level += 1;
|
||||
|
|
@ -87,9 +87,9 @@ export class DebugCommands {
|
|||
const leveledUp = newLevel > oldLevel;
|
||||
results.push(
|
||||
`${unit.name} (${unit.id}): +${amount} XP. Level: ${oldLevel} → ${newLevel}. ` +
|
||||
`XP: ${mastery.xp}/${100 * (newLevel ** 2)}. ` +
|
||||
`Skill Points: ${mastery.skillPoints}. ` +
|
||||
(leveledUp ? "✨ LEVELED UP!" : "")
|
||||
`XP: ${mastery.xp}/${100 * newLevel ** 2}. ` +
|
||||
`Skill Points: ${mastery.skillPoints}. ` +
|
||||
(leveledUp ? "✨ LEVELED UP!" : "")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ export class DebugCommands {
|
|||
// Calculate XP for this level (simple curve: sum of 100 * level^2 for all previous levels)
|
||||
let totalXP = 0;
|
||||
for (let i = 1; i < level; i++) {
|
||||
totalXP += 100 * (i ** 2);
|
||||
totalXP += 100 * i ** 2;
|
||||
}
|
||||
mastery.xp = 0; // Reset XP to 0 for current level
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ export class DebugCommands {
|
|||
|
||||
results.push(
|
||||
`${unit.name} (${unit.id}): Level set to ${level} (was ${oldLevel}). ` +
|
||||
`Skill Points: ${mastery.skillPoints}`
|
||||
`Skill Points: ${mastery.skillPoints}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +200,9 @@ export class DebugCommands {
|
|||
}
|
||||
|
||||
if (mastery.unlockedNodes.includes(nodeId)) {
|
||||
results.push(`${unit.name} (${unit.id}): Node ${nodeId} already unlocked`);
|
||||
results.push(
|
||||
`${unit.name} (${unit.id}): Node ${nodeId} already unlocked`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -248,7 +250,9 @@ export class DebugCommands {
|
|||
const results = [];
|
||||
for (const unit of units) {
|
||||
if (!this.gameLoop?.classRegistry) {
|
||||
results.push(`${unit.name}: Cannot unlock all skills - class registry not available`);
|
||||
results.push(
|
||||
`${unit.name}: Cannot unlock all skills - class registry not available`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -262,8 +266,8 @@ export class DebugCommands {
|
|||
// For now, return a helpful message
|
||||
results.push(
|
||||
`${unit.name} (${unit.id}): Note - Skill trees are generated dynamically. ` +
|
||||
`Use unlockSkill(unitId, nodeId) with specific node IDs. ` +
|
||||
`To see available nodes, open the character sheet for this unit.`
|
||||
`Use unlockSkill(unitId, nodeId) with specific node IDs. ` +
|
||||
`To see available nodes, open the character sheet for this unit.`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -336,7 +340,9 @@ export class DebugCommands {
|
|||
this.gameStateManager._saveHubStash();
|
||||
}
|
||||
|
||||
return `Added ${shards} Aether Shards${cores > 0 ? ` and ${cores} Ancient Cores` : ""} to hub stash`;
|
||||
return `Added ${shards} Aether Shards${
|
||||
cores > 0 ? ` and ${cores} Ancient Cores` : ""
|
||||
} to hub stash`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -355,7 +361,9 @@ export class DebugCommands {
|
|||
|
||||
const enemies = this._getEnemies(enemyId);
|
||||
if (enemies.length === 0) {
|
||||
return `No enemies found${enemyId !== "all" ? ` with ID: ${enemyId}` : ""}`;
|
||||
return `No enemies found${
|
||||
enemyId !== "all" ? ` with ID: ${enemyId}` : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
|
@ -385,9 +393,10 @@ export class DebugCommands {
|
|||
return "Error: Game loop or unit manager not available";
|
||||
}
|
||||
|
||||
const units = unitId === "all"
|
||||
? this.gameLoop.unitManager.getUnitsByTeam("PLAYER")
|
||||
: [this.gameLoop.unitManager.getUnitById(unitId)].filter(Boolean);
|
||||
const units =
|
||||
unitId === "all"
|
||||
? this.gameLoop.unitManager.getUnitsByTeam("PLAYER")
|
||||
: [this.gameLoop.unitManager.getUnitById(unitId)].filter(Boolean);
|
||||
|
||||
if (units.length === 0) {
|
||||
return `No units found${unitId !== "all" ? ` with ID: ${unitId}` : ""}`;
|
||||
|
|
@ -397,7 +406,11 @@ export class DebugCommands {
|
|||
for (const unit of units) {
|
||||
const oldHP = unit.currentHealth;
|
||||
unit.currentHealth = unit.maxHealth;
|
||||
results.push(`${unit.name} (${unit.id}): Healed ${unit.maxHealth - oldHP} HP (${oldHP} → ${unit.maxHealth})`);
|
||||
results.push(
|
||||
`${unit.name} (${unit.id}): Healed ${
|
||||
unit.maxHealth - oldHP
|
||||
} HP (${oldHP} → ${unit.maxHealth})`
|
||||
);
|
||||
}
|
||||
|
||||
return results.join("\n");
|
||||
|
|
@ -440,7 +453,8 @@ export class DebugCommands {
|
|||
return "Error: Mission manager not available";
|
||||
}
|
||||
|
||||
const objectives = this.gameStateManager.missionManager.currentObjectives || [];
|
||||
const objectives =
|
||||
this.gameStateManager.missionManager.currentObjectives || [];
|
||||
let objective = null;
|
||||
|
||||
if (typeof objectiveId === "number") {
|
||||
|
|
@ -459,7 +473,9 @@ export class DebugCommands {
|
|||
// Check victory
|
||||
this.gameStateManager.missionManager.checkVictory();
|
||||
|
||||
return `Objective completed: ${objective.type} (${objective.id || "unnamed"})`;
|
||||
return `Objective completed: ${objective.type} (${
|
||||
objective.id || "unnamed"
|
||||
})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -485,7 +501,8 @@ export class DebugCommands {
|
|||
return `Triggered narrative: ${narrativeId}`;
|
||||
} else {
|
||||
// Try to trigger current mission's intro/outro
|
||||
const missionDef = this.gameStateManager.missionManager?.currentMissionDef;
|
||||
const missionDef =
|
||||
this.gameStateManager.missionManager?.currentMissionDef;
|
||||
if (missionDef?.narrative) {
|
||||
const narrativeToUse =
|
||||
missionDef.narrative.intro_success ||
|
||||
|
|
@ -495,7 +512,9 @@ export class DebugCommands {
|
|||
this.gameStateManager.narrativeManager
|
||||
.loadSequence(narrativeToUse)
|
||||
.then(() => {
|
||||
this.gameStateManager.narrativeManager.startSequence(narrativeToUse);
|
||||
this.gameStateManager.narrativeManager.startSequence(
|
||||
narrativeToUse
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error loading narrative:", error);
|
||||
|
|
@ -523,16 +542,18 @@ export class DebugCommands {
|
|||
const missionManager = this.gameStateManager.missionManager;
|
||||
missionManager.refreshProceduralMissions(false);
|
||||
|
||||
const proceduralCount = Array.from(missionManager.missionRegistry.values())
|
||||
.filter((m) => m.type === "SIDE_QUEST" && m.id?.startsWith("SIDE_OP_"))
|
||||
.length;
|
||||
const proceduralCount = Array.from(
|
||||
missionManager.missionRegistry.values()
|
||||
).filter(
|
||||
(m) => m.type === "SIDE_QUEST" && m.id?.startsWith("SIDE_OP_")
|
||||
).length;
|
||||
|
||||
return `Regenerated procedural missions. ${proceduralCount} missions available on board.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a specific mission type and adds it to the mission board.
|
||||
* @param {string} archetype - Mission archetype: "SKIRMISH", "SALVAGE", "ASSASSINATION", or "RECON"
|
||||
* @param {string} archetype - Mission archetype: "SKIRMISH", "SALVAGE", "ELIMINATE_UNIT", or "RECON"
|
||||
* @param {number} [tier=2] - Campaign tier (1-5), defaults to 2
|
||||
* @param {string} [biomeType] - Biome type ID (optional, will pick random if not specified)
|
||||
* @returns {string} Result message
|
||||
|
|
@ -542,11 +563,13 @@ export class DebugCommands {
|
|||
return "Error: MissionManager not available";
|
||||
}
|
||||
|
||||
const validArchetypes = ["SKIRMISH", "SALVAGE", "ASSASSINATION", "RECON"];
|
||||
const validArchetypes = ["SKIRMISH", "SALVAGE", "ELIMINATE_UNIT", "RECON"];
|
||||
const upperArchetype = archetype.toUpperCase();
|
||||
|
||||
if (!validArchetypes.includes(upperArchetype)) {
|
||||
return `Error: Invalid archetype "${archetype}". Valid types: ${validArchetypes.join(", ")}`;
|
||||
return `Error: Invalid archetype "${archetype}". Valid types: ${validArchetypes.join(
|
||||
", "
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (tier < 1 || tier > 5) {
|
||||
|
|
@ -566,7 +589,8 @@ export class DebugCommands {
|
|||
|
||||
// Pick a biome if not specified
|
||||
if (!biomeType) {
|
||||
biomeType = unlockedRegions[Math.floor(Math.random() * unlockedRegions.length)];
|
||||
biomeType =
|
||||
unlockedRegions[Math.floor(Math.random() * unlockedRegions.length)];
|
||||
}
|
||||
|
||||
// Generate mission with specific archetype
|
||||
|
|
@ -577,14 +601,18 @@ export class DebugCommands {
|
|||
const maxAttempts = 50;
|
||||
|
||||
while (!mission && attempts < maxAttempts) {
|
||||
const candidate = MissionGenerator.generateSideOp(tier, unlockedRegions, history);
|
||||
const candidate = MissionGenerator.generateSideOp(
|
||||
tier,
|
||||
unlockedRegions,
|
||||
history
|
||||
);
|
||||
// Check if this mission matches our desired archetype
|
||||
const primaryObj = candidate.objectives?.primary?.[0];
|
||||
if (primaryObj) {
|
||||
const archetypeMap = {
|
||||
SKIRMISH: "ELIMINATE_ALL",
|
||||
SALVAGE: "INTERACT",
|
||||
ASSASSINATION: "ELIMINATE_UNIT",
|
||||
ELIMINATE_UNIT: "ELIMINATE_UNIT",
|
||||
RECON: "REACH_ZONE",
|
||||
};
|
||||
|
||||
|
|
@ -610,7 +638,11 @@ export class DebugCommands {
|
|||
// Register the mission
|
||||
missionManager.registerMission(mission);
|
||||
|
||||
return `Generated ${upperArchetype} mission: "${mission.config.title}" (Tier ${tier}, ${mission.biome?.type || biomeType}). Mission ID: ${mission.id}`;
|
||||
return `Generated ${upperArchetype} mission: "${
|
||||
mission.config.title
|
||||
}" (Tier ${tier}, ${mission.biome?.type || biomeType}). Mission ID: ${
|
||||
mission.id
|
||||
}`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -627,85 +659,187 @@ export class DebugCommands {
|
|||
"font-weight: bold; font-size: 16px; color: #4CAF50;"
|
||||
);
|
||||
|
||||
console.log("%cEXPLORER & LEVELING:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cEXPLORER & LEVELING:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %caddXP(unitId, amount)%c - Add XP to explorer(s)",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
' unitId: "first", "all", or specific ID'
|
||||
);
|
||||
console.log(
|
||||
" %csetLevel(unitId, level)%c - Set explorer level directly (1-30)",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %caddSkillPoints(unitId, amount)%c - Add skill points to explorer",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(" %caddXP(unitId, amount)%c - Add XP to explorer(s)", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" unitId: \"first\", \"all\", or specific ID");
|
||||
console.log(" %csetLevel(unitId, level)%c - Set explorer level directly (1-30)", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %caddSkillPoints(unitId, amount)%c - Add skill points to explorer", "color: #FF9800;", "color: inherit;");
|
||||
console.log("");
|
||||
|
||||
console.log("%cSKILL TREE:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cSKILL TREE:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %cunlockSkill(unitId, nodeId)%c - Unlock a specific skill node",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %cunlockAllSkills(unitId)%c - Unlock all skill nodes for explorer",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(" %cunlockSkill(unitId, nodeId)%c - Unlock a specific skill node", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %cunlockAllSkills(unitId)%c - Unlock all skill nodes for explorer", "color: #FF9800;", "color: inherit;");
|
||||
console.log("");
|
||||
|
||||
console.log("%cINVENTORY:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cINVENTORY:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %caddItem(itemDefId, quantity, target)%c - Add item",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(' target: "hub" or "run"');
|
||||
console.log(
|
||||
" %caddCurrency(shards, cores)%c - Add currency to hub stash",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(" %caddItem(itemDefId, quantity, target)%c - Add item", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" target: \"hub\" or \"run\"");
|
||||
console.log(" %caddCurrency(shards, cores)%c - Add currency to hub stash", "color: #FF9800;", "color: inherit;");
|
||||
console.log("");
|
||||
|
||||
console.log("%cCOMBAT:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cCOMBAT:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %ckillEnemy(enemyId)%c - Kill enemy",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(" %ckillEnemy(enemyId)%c - Kill enemy", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" enemyId: \"all\" or specific ID");
|
||||
console.log(" %chealUnit(unitId)%c - Heal unit to full", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" unitId: \"all\" or specific ID");
|
||||
console.log(' enemyId: "all" or specific ID');
|
||||
console.log(
|
||||
" %chealUnit(unitId)%c - Heal unit to full",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(' unitId: "all" or specific ID');
|
||||
console.log("");
|
||||
|
||||
console.log("%cMISSION & NARRATIVE:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cMISSION & NARRATIVE:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %cregenerateMissions()%c - Regenerate all procedural missions on board",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %cgenerateMission(type, tier, biome)%c - Generate specific mission type",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
' type: "SKIRMISH", "SALVAGE", "ELIMINATE_UNIT", "RECON"'
|
||||
);
|
||||
console.log(" %cregenerateMissions()%c - Regenerate all procedural missions on board", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %cgenerateMission(type, tier, biome)%c - Generate specific mission type", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" type: \"SKIRMISH\", \"SALVAGE\", \"ASSASSINATION\", \"RECON\"");
|
||||
console.log(" tier: 1-5 (default: 2)");
|
||||
console.log(" biome: optional biome ID (e.g., \"BIOME_RUSTING_WASTES\")");
|
||||
console.log(
|
||||
' biome: optional biome ID (e.g., "BIOME_RUSTING_WASTES")'
|
||||
);
|
||||
console.log(" Examples:");
|
||||
console.log(" %cgenerateMission(\"RECON\")%c - Generate RECON mission (for testing zone objectives)", "color: #4CAF50; font-family: monospace;", "color: inherit;");
|
||||
console.log(" %cgenerateMission(\"SKIRMISH\", 3)%c - Generate tier 3 SKIRMISH mission", "color: #4CAF50; font-family: monospace;", "color: inherit;");
|
||||
console.log(" %ctriggerVictory()%c - Trigger mission victory", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %ccompleteObjective(objectiveId)%c - Complete a specific objective", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %ctriggerNarrative(narrativeId)%c - Trigger narrative sequence", "color: #FF9800;", "color: inherit;");
|
||||
console.log(
|
||||
' %cgenerateMission("RECON")%c - Generate RECON mission (for testing zone objectives)',
|
||||
"color: #4CAF50; font-family: monospace;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
' %cgenerateMission("SKIRMISH", 3)%c - Generate tier 3 SKIRMISH mission',
|
||||
"color: #4CAF50; font-family: monospace;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %ctriggerVictory()%c - Trigger mission victory",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %ccompleteObjective(objectiveId)%c - Complete a specific objective",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %ctriggerNarrative(narrativeId)%c - Trigger narrative sequence",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log("");
|
||||
|
||||
console.log("%cUTILITY:", "font-weight: bold; color: #2196F3;");
|
||||
console.log(
|
||||
"%cUTILITY:",
|
||||
"font-weight: bold; color: #2196F3;"
|
||||
" %chelp()%c - Show this help",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistIds()%c - List all available IDs",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistPlayerIds()%c - List player unit IDs only",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistEnemyIds()%c - List enemy unit IDs only",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistItemIds(limit)%c - List item IDs only (default limit: 100)",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistUnits()%c - List all units with details",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %clistItems(limit)%c - List available items with details (default limit: 50)",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(
|
||||
" %cgetState()%c - Get current game state info",
|
||||
"color: #FF9800;",
|
||||
"color: inherit;"
|
||||
);
|
||||
console.log(" %chelp()%c - Show this help", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistIds()%c - List all available IDs", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistPlayerIds()%c - List player unit IDs only", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistEnemyIds()%c - List enemy unit IDs only", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistItemIds(limit)%c - List item IDs only (default limit: 100)", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistUnits()%c - List all units with details", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %clistItems(limit)%c - List available items with details (default limit: 50)", "color: #FF9800;", "color: inherit;");
|
||||
console.log(" %cgetState()%c - Get current game state info", "color: #FF9800;", "color: inherit;");
|
||||
console.log("");
|
||||
|
||||
console.log("%cQUICK EXAMPLES:", "font-weight: bold; color: #9C27B0;");
|
||||
console.log(
|
||||
"%cQUICK EXAMPLES:",
|
||||
"font-weight: bold; color: #9C27B0;"
|
||||
' %cdebugCommands.addXP("first", 500)',
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
' %cdebugCommands.setLevel("all", 10)',
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
' %cdebugCommands.addItem("ITEM_SWORD_T1", 1, "hub")',
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
' %cdebugCommands.generateMission("RECON")',
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
" %cdebugCommands.regenerateMissions()",
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
' %cdebugCommands.killEnemy("all")',
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(
|
||||
" %cdebugCommands.triggerVictory()",
|
||||
"color: #4CAF50; font-family: monospace;"
|
||||
);
|
||||
console.log(" %cdebugCommands.addXP(\"first\", 500)", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.setLevel(\"all\", 10)", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.addItem(\"ITEM_SWORD_T1\", 1, \"hub\")", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.generateMission(\"RECON\")", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.regenerateMissions()", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.killEnemy(\"all\")", "color: #4CAF50; font-family: monospace;");
|
||||
console.log(" %cdebugCommands.triggerVictory()", "color: #4CAF50; font-family: monospace;");
|
||||
console.log("");
|
||||
|
||||
console.log(
|
||||
|
|
@ -811,7 +945,10 @@ export class DebugCommands {
|
|||
return "No items found in registry";
|
||||
}
|
||||
|
||||
let result = `Available Items (showing ${Math.min(limit, items.length)} of ${items.length}):\n\n`;
|
||||
let result = `Available Items (showing ${Math.min(
|
||||
limit,
|
||||
items.length
|
||||
)} of ${items.length}):\n\n`;
|
||||
items.slice(0, limit).forEach((item) => {
|
||||
result += ` - ${item.id} (${item.type || "UNKNOWN"})`;
|
||||
if (item.name) {
|
||||
|
|
@ -851,7 +988,9 @@ export class DebugCommands {
|
|||
});
|
||||
|
||||
if (items.length > limit) {
|
||||
result += `\n... and ${items.length - limit} more items (use listItems() to see all)`;
|
||||
result += `\n... and ${
|
||||
items.length - limit
|
||||
} more items (use listItems() to see all)`;
|
||||
}
|
||||
|
||||
result += `\nUse with: addItem("${items[0]?.id}", 1, "hub")`;
|
||||
|
|
@ -902,7 +1041,9 @@ export class DebugCommands {
|
|||
result += "\n";
|
||||
});
|
||||
if (items.length > 20) {
|
||||
result += ` ... and ${items.length - 20} more (use listItemIds() to see all)\n`;
|
||||
result += ` ... and ${
|
||||
items.length - 20
|
||||
} more (use listItemIds() to see all)\n`;
|
||||
}
|
||||
result += "\n";
|
||||
}
|
||||
|
|
@ -913,7 +1054,9 @@ export class DebugCommands {
|
|||
if (objectives.length > 0) {
|
||||
result += "OBJECTIVE IDs:\n";
|
||||
objectives.forEach((obj, idx) => {
|
||||
result += ` ${idx} or "${obj.id || `objective_${idx}`}" - ${obj.type}\n`;
|
||||
result += ` ${idx} or "${obj.id || `objective_${idx}`}" - ${
|
||||
obj.type
|
||||
}\n`;
|
||||
});
|
||||
result += "\n";
|
||||
}
|
||||
|
|
@ -926,16 +1069,21 @@ export class DebugCommands {
|
|||
|
||||
// Narrative IDs (from current mission)
|
||||
if (this.gameStateManager?.missionManager?.currentMissionDef?.narrative) {
|
||||
const narrative = this.gameStateManager.missionManager.currentMissionDef.narrative;
|
||||
const narrative =
|
||||
this.gameStateManager.missionManager.currentMissionDef.narrative;
|
||||
result += "NARRATIVE IDs (from current mission):\n";
|
||||
if (narrative.intro) result += ` "${narrative.intro}" - Intro\n`;
|
||||
if (narrative.intro_success) result += ` "${narrative.intro_success}" - Intro Success\n`;
|
||||
if (narrative.outro_success) result += ` "${narrative.outro_success}" - Outro Success\n`;
|
||||
if (narrative.outro_failure) result += ` "${narrative.outro_failure}" - Outro Failure\n`;
|
||||
if (narrative.intro_success)
|
||||
result += ` "${narrative.intro_success}" - Intro Success\n`;
|
||||
if (narrative.outro_success)
|
||||
result += ` "${narrative.outro_success}" - Outro Success\n`;
|
||||
if (narrative.outro_failure)
|
||||
result += ` "${narrative.outro_failure}" - Outro Failure\n`;
|
||||
result += "\n";
|
||||
}
|
||||
|
||||
result += "Use listPlayerIds(), listEnemyIds(), or listItemIds() for detailed lists.";
|
||||
result +=
|
||||
"Use listPlayerIds(), listEnemyIds(), or listItemIds() for detailed lists.";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -947,7 +1095,9 @@ export class DebugCommands {
|
|||
getState() {
|
||||
let result = "Game State:\n\n";
|
||||
|
||||
result += `Current State: ${this.gameStateManager?.currentState || "UNKNOWN"}\n`;
|
||||
result += `Current State: ${
|
||||
this.gameStateManager?.currentState || "UNKNOWN"
|
||||
}\n`;
|
||||
result += `Game Running: ${this.gameLoop?.isRunning || false}\n`;
|
||||
result += `Game Paused: ${this.gameLoop?.isPaused || false}\n`;
|
||||
|
||||
|
|
@ -957,7 +1107,9 @@ export class DebugCommands {
|
|||
if (mm.currentObjectives) {
|
||||
result += `Objectives: ${mm.currentObjectives.length}\n`;
|
||||
mm.currentObjectives.forEach((obj, idx) => {
|
||||
result += ` ${idx + 1}. ${obj.type} - ${obj.complete ? "✓" : "✗"} (${obj.current || 0}/${obj.target_count || 0})\n`;
|
||||
result += ` ${idx + 1}. ${obj.type} - ${obj.complete ? "✓" : "✗"} (${
|
||||
obj.current || 0
|
||||
}/${obj.target_count || 0})\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1025,4 +1177,3 @@ export const debugCommands = new DebugCommands();
|
|||
if (typeof window !== "undefined") {
|
||||
window.debugCommands = debugCommands;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1198,83 +1198,76 @@ export class GameLoop {
|
|||
|
||||
// Create a proper registry with actual class definitions
|
||||
const classRegistry = new Map();
|
||||
// Dynamic Asset Loading
|
||||
try {
|
||||
const manifestRes = await fetch("./assets/data/manifest.json");
|
||||
const manifest = await manifestRes.json();
|
||||
|
||||
// Lazy-load class definitions
|
||||
const [
|
||||
vanguardDef,
|
||||
weaverDef,
|
||||
scavengerDef,
|
||||
tinkerDef,
|
||||
custodianDef,
|
||||
muleBotDef,
|
||||
] = await Promise.all([
|
||||
import("../assets/data/classes/vanguard.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
import("../assets/data/classes/aether_weaver.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
import("../assets/data/classes/scavenger.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
import("../assets/data/classes/tinker.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
import("../assets/data/classes/custodian.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
import("../assets/data/units/mule_bot.json", {
|
||||
with: { type: "json" },
|
||||
}).then((m) => m.default),
|
||||
]);
|
||||
const loadAssets = async (files) => {
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
const res = await fetch(`./assets/data/${file}`);
|
||||
return res.json();
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Register all class definitions
|
||||
const classDefs = [
|
||||
vanguardDef,
|
||||
weaverDef,
|
||||
scavengerDef,
|
||||
tinkerDef,
|
||||
custodianDef,
|
||||
muleBotDef,
|
||||
];
|
||||
const [classDefs, unitDefs] = await Promise.all([
|
||||
loadAssets(manifest.classes || []),
|
||||
loadAssets(manifest.units || []),
|
||||
]);
|
||||
|
||||
for (const classDef of classDefs) {
|
||||
if (classDef && classDef.id) {
|
||||
// Add type field for compatibility
|
||||
classRegistry.set(classDef.id, {
|
||||
...classDef,
|
||||
type: "EXPLORER",
|
||||
});
|
||||
// Register Classes
|
||||
for (const classDef of classDefs) {
|
||||
if (classDef && classDef.id) {
|
||||
classRegistry.set(classDef.id, {
|
||||
...classDef,
|
||||
type: classDef.type || "EXPLORER",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create registry object with get method for UnitManager
|
||||
const unitRegistry = {
|
||||
get: (id) => {
|
||||
// 1. Check Class Registry (Explorers)
|
||||
if (classRegistry.has(id)) {
|
||||
return classRegistry.get(id);
|
||||
}
|
||||
|
||||
// 2. Check Loaded Unit Definitions
|
||||
const foundUnit = unitDefs.find((u) => u.id === id);
|
||||
if (foundUnit) {
|
||||
return foundUnit;
|
||||
}
|
||||
|
||||
// 3. Fallback for generic enemies (legacy support)
|
||||
if (id.startsWith("ENEMY_")) {
|
||||
return {
|
||||
type: "ENEMY",
|
||||
name: "Enemy",
|
||||
stats: { health: 50, attack: 8, defense: 3, speed: 8 },
|
||||
ai_archetype: "BRUISER",
|
||||
};
|
||||
}
|
||||
|
||||
console.warn(`Unit definition not found: ${id}`);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
this.unitManager = new UnitManager(unitRegistry);
|
||||
this.classRegistry = classRegistry;
|
||||
|
||||
console.log(
|
||||
`Loaded ${classDefs.length} classes and ${unitDefs.length} units.`
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to load game assets:", err);
|
||||
// Fallback or critical error handling
|
||||
this.unitManager = new UnitManager(classRegistry); // fallback to empty/partial
|
||||
}
|
||||
|
||||
// Create registry object with get method for UnitManager
|
||||
const unitRegistry = {
|
||||
get: (id) => {
|
||||
// Try to get from class registry first
|
||||
if (classRegistry.has(id)) {
|
||||
return classRegistry.get(id);
|
||||
}
|
||||
|
||||
// Fallback for enemy units
|
||||
if (id.startsWith("ENEMY_")) {
|
||||
return {
|
||||
type: "ENEMY",
|
||||
name: "Enemy",
|
||||
stats: { health: 50, attack: 8, defense: 3, speed: 8 },
|
||||
ai_archetype: "BRUISER",
|
||||
};
|
||||
}
|
||||
|
||||
console.warn(`Unit definition not found: ${id}`);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
this.unitManager = new UnitManager(unitRegistry);
|
||||
// Store classRegistry reference for accessing class definitions later
|
||||
this.classRegistry = classRegistry;
|
||||
|
||||
// WIRING: Connect Systems to Data
|
||||
this.movementSystem.setContext(this.grid, this.unitManager);
|
||||
this.turnSystem.setContext(this.unitManager);
|
||||
|
|
@ -1553,7 +1546,32 @@ export class GameLoop {
|
|||
|
||||
// Get enemy spawns from mission definition
|
||||
const missionDef = await this.missionManager?.getActiveMission();
|
||||
const enemySpawns = missionDef?.enemy_spawns || [];
|
||||
const enemySpawns = [...(missionDef?.enemy_spawns || [])];
|
||||
|
||||
// Auto-populate spawn for ELIMINATE_UNIT objectives if missing
|
||||
// This unifies the logic so Story missions don't need explicit enemy_spawns for bosses
|
||||
const allObjectives = [
|
||||
...(missionDef.objectives?.primary || []),
|
||||
...(missionDef.objectives?.secondary || []),
|
||||
];
|
||||
|
||||
allObjectives.forEach((obj) => {
|
||||
if (obj.type === "ELIMINATE_UNIT" && obj.target_def_id) {
|
||||
// Check if already spawning
|
||||
const alreadySpawning = enemySpawns.some(
|
||||
(s) => s.enemy_def_id === obj.target_def_id
|
||||
);
|
||||
if (!alreadySpawning) {
|
||||
console.log(
|
||||
`Auto-adding spawn for ELIMINATE_UNIT target: ${obj.target_def_id}`
|
||||
);
|
||||
enemySpawns.push({
|
||||
enemy_def_id: obj.target_def_id,
|
||||
count: obj.target_count || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If no enemy_spawns defined, fall back to default behavior
|
||||
if (enemySpawns.length === 0) {
|
||||
|
|
@ -1629,10 +1647,6 @@ export class GameLoop {
|
|||
|
||||
// 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"
|
||||
|
|
@ -2265,17 +2279,16 @@ 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;
|
||||
// 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 mesh = new THREE.Mesh(geometry, material);
|
||||
|
|
@ -2864,6 +2877,54 @@ export class GameLoop {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely disposes of the GameLoop and all its resources.
|
||||
* This is used when destroying the game instance to return to the main menu.
|
||||
*/
|
||||
dispose() {
|
||||
this.stop();
|
||||
|
||||
// Dispose Renderer
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose();
|
||||
if (this.renderer.domElement && this.renderer.domElement.parentNode) {
|
||||
this.renderer.domElement.parentNode.removeChild(
|
||||
this.renderer.domElement
|
||||
);
|
||||
}
|
||||
this.renderer = null;
|
||||
}
|
||||
|
||||
// Dispose Scene
|
||||
if (this.scene) {
|
||||
this.scene.clear();
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
// Detach Input Manager
|
||||
if (this.inputManager) {
|
||||
this.inputManager.detach();
|
||||
this.inputManager = null;
|
||||
}
|
||||
|
||||
// Clear references
|
||||
this.camera = null;
|
||||
this.controls = null;
|
||||
this.grid = null;
|
||||
this.voxelManager = null;
|
||||
this.unitManager = null;
|
||||
this.turnSystem = null;
|
||||
this.movementSystem = null;
|
||||
this.skillTargetingSystem = null;
|
||||
this.effectProcessor = null;
|
||||
this.inventoryManager = null;
|
||||
this.missionManager = null;
|
||||
this.gameStateManager = null;
|
||||
this.runData = null;
|
||||
|
||||
console.log("GameLoop: Disposed completely.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the combat state in GameStateManager.
|
||||
* Called when combat starts or when combat state changes (turn changes, etc.)
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ class GameStateManagerClass {
|
|||
return this.#gameLoopInitialized.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current game loop.
|
||||
* Called when the game viewport is removed or the game is reset.
|
||||
*/
|
||||
disconnectGameLoop() {
|
||||
this.gameLoop = null;
|
||||
this.#gameLoopInitialized = Promise.withResolvers();
|
||||
}
|
||||
|
||||
/** @type {PromiseWithResolvers<unknown>} */
|
||||
#rosterLoaded = Promise.withResolvers();
|
||||
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@
|
|||
<!-- HUB SCREEN -->
|
||||
<hub-screen hidden aria-label="Hub Screen"></hub-screen>
|
||||
<!-- GAME VIEWPORT CONTAINER -->
|
||||
<game-viewport hidden aria-label="Game World"></game-viewport>
|
||||
<!-- game-viewport is now dynamically created/removed by index.js -->
|
||||
|
||||
<!-- UI LAYER for modals and overlays -->
|
||||
<div id="ui-layer" aria-live="polite"></div>
|
||||
|
|
|
|||
19
src/index.js
19
src/index.js
|
|
@ -1,7 +1,8 @@
|
|||
import { gameStateManager } from "./core/GameStateManager.js";
|
||||
|
||||
// GameViewport is dynamic
|
||||
/** @type {HTMLElement | null} */
|
||||
const gameViewport = document.querySelector("game-viewport");
|
||||
let gameViewport = null;
|
||||
/** @type {HTMLElement | null} */
|
||||
const teamBuilder = document.querySelector("team-builder");
|
||||
/** @type {HTMLElement | null} */
|
||||
|
|
@ -235,7 +236,10 @@ window.addEventListener("gamestate-changed", async (e) => {
|
|||
|
||||
loadingOverlay.toggleAttribute("hidden", false);
|
||||
mainMenu.toggleAttribute("hidden", true);
|
||||
gameViewport.toggleAttribute("hidden", true);
|
||||
mainMenu.toggleAttribute("hidden", true);
|
||||
if (gameViewport) {
|
||||
gameViewport.toggleAttribute("hidden", true);
|
||||
}
|
||||
teamBuilder.toggleAttribute("hidden", true);
|
||||
if (hubScreen) {
|
||||
hubScreen.toggleAttribute("hidden", true);
|
||||
|
|
@ -266,6 +270,12 @@ window.addEventListener("gamestate-changed", async (e) => {
|
|||
// Show main menu for new players
|
||||
mainMenu.toggleAttribute("hidden", false);
|
||||
}
|
||||
// Clean up game viewport if it exists
|
||||
if (gameViewport) {
|
||||
console.log("Main Menu: Removing GameViewport to clean up resources");
|
||||
gameViewport.remove();
|
||||
gameViewport = null;
|
||||
}
|
||||
break;
|
||||
case "STATE_TEAM_BUILDER":
|
||||
await import("./ui/team-builder.js");
|
||||
|
|
@ -299,6 +309,11 @@ window.addEventListener("gamestate-changed", async (e) => {
|
|||
case "STATE_DEPLOYMENT":
|
||||
case "STATE_COMBAT":
|
||||
await import("./ui/game-viewport.js");
|
||||
// Create new viewport if it doesn't exist
|
||||
if (!gameViewport) {
|
||||
gameViewport = document.createElement("game-viewport");
|
||||
document.body.appendChild(gameViewport);
|
||||
}
|
||||
gameViewport.toggleAttribute("hidden", false);
|
||||
// Squad will be updated by game-viewport's #updateSquad() method
|
||||
// which listens to gamestate-changed events
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ export class MissionGenerator {
|
|||
];
|
||||
|
||||
/**
|
||||
* Nouns for Assassination (Kill) missions
|
||||
* Nouns for Eliminate Unit missions
|
||||
*/
|
||||
static NOUNS_ASSASSINATION = [
|
||||
static NOUNS_ELIMINATE_UNIT = [
|
||||
"Viper",
|
||||
"Dagger",
|
||||
"Fang",
|
||||
|
|
@ -307,7 +307,7 @@ export class MissionGenerator {
|
|||
const tierConfig = this.TIER_CONFIG[validTier];
|
||||
|
||||
// Select archetype
|
||||
const archetypes = ["SKIRMISH", "SALVAGE", "ASSASSINATION", "RECON"];
|
||||
const archetypes = ["SKIRMISH", "SALVAGE", "ELIMINATE_UNIT", "RECON"];
|
||||
const archetype = this.randomChoice(archetypes);
|
||||
|
||||
// Select noun based on archetype
|
||||
|
|
@ -319,8 +319,8 @@ export class MissionGenerator {
|
|||
case "SALVAGE":
|
||||
noun = this.randomChoice(this.NOUNS_SALVAGE);
|
||||
break;
|
||||
case "ASSASSINATION":
|
||||
noun = this.randomChoice(this.NOUNS_ASSASSINATION);
|
||||
case "ELIMINATE_UNIT":
|
||||
noun = this.randomChoice(this.NOUNS_ELIMINATE_UNIT);
|
||||
break;
|
||||
case "RECON":
|
||||
noun = this.randomChoice(this.NOUNS_RECON);
|
||||
|
|
@ -398,8 +398,8 @@ export class MissionGenerator {
|
|||
expiresIn: 3, // Expires in 3 campaign days
|
||||
};
|
||||
|
||||
// Add boss config for Assassination missions
|
||||
if (archetype === "ASSASSINATION") {
|
||||
// Add boss config for Eliminate Unit missions
|
||||
if (archetype === "ELIMINATE_UNIT") {
|
||||
// Find target ID from objectives
|
||||
const targetObj = objectives.primary.find(
|
||||
(o) => o.type === "ELIMINATE_UNIT"
|
||||
|
|
@ -454,7 +454,7 @@ export class MissionGenerator {
|
|||
case "RECON":
|
||||
verb = "scout";
|
||||
break;
|
||||
case "ASSASSINATION":
|
||||
case "ELIMINATE_UNIT":
|
||||
verb = "assassinate";
|
||||
break;
|
||||
}
|
||||
|
|
@ -533,7 +533,7 @@ export class MissionGenerator {
|
|||
failure_conditions: [{ type: "SQUAD_WIPE" }],
|
||||
};
|
||||
|
||||
case "ASSASSINATION":
|
||||
case "ELIMINATE_UNIT":
|
||||
// Generate a random elite enemy ID
|
||||
const eliteEnemies = [
|
||||
"ENEMY_ELITE_ECHO",
|
||||
|
|
@ -600,8 +600,8 @@ export class MissionGenerator {
|
|||
const spawns = [];
|
||||
|
||||
switch (archetype) {
|
||||
case "ASSASSINATION":
|
||||
// For ASSASSINATION, spawn the target enemy from the ELIMINATE_UNIT objective
|
||||
case "ELIMINATE_UNIT":
|
||||
// For ELIMINATE_UNIT, spawn the target enemy from the ELIMINATE_UNIT objective
|
||||
const eliminateUnitObj = objectives.primary?.find(
|
||||
(obj) => obj.type === "ELIMINATE_UNIT"
|
||||
);
|
||||
|
|
@ -699,7 +699,7 @@ export class MissionGenerator {
|
|||
density = "HIGH"; // High density for obstacles/cover
|
||||
break;
|
||||
|
||||
case "ASSASSINATION":
|
||||
case "ELIMINATE_UNIT":
|
||||
size = { x: 22, y: 12, z: 22 };
|
||||
roomCount = 7;
|
||||
density = "MEDIUM";
|
||||
|
|
@ -757,7 +757,7 @@ export class MissionGenerator {
|
|||
return `Clear the sector of hostile forces in ${biomeName}.`;
|
||||
case "SALVAGE":
|
||||
return `Recover lost supplies before the enemy secures them in ${biomeName}.`;
|
||||
case "ASSASSINATION":
|
||||
case "ELIMINATE_UNIT":
|
||||
return `A High-Value Target has been spotted in ${biomeName}. Eliminate them.`;
|
||||
case "RECON":
|
||||
return `Survey the designated coordinates in ${biomeName}.`;
|
||||
|
|
@ -788,9 +788,9 @@ export class MissionGenerator {
|
|||
baseXP * multiplier * this.randomFloat(0.9, 1.1)
|
||||
);
|
||||
|
||||
// Assassination missions get bonus currency
|
||||
// Eliminate Unit missions get bonus currency
|
||||
let finalCurrency = currencyAmount;
|
||||
if (archetype === "ASSASSINATION") {
|
||||
if (archetype === "ELIMINATE_UNIT") {
|
||||
finalCurrency = Math.round(finalCurrency * 1.5);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,18 @@ export class GameViewport extends LitElement {
|
|||
}
|
||||
|
||||
async firstUpdated() {
|
||||
// Only init if connected
|
||||
if (!this.isConnected) return;
|
||||
|
||||
if (this.loop) {
|
||||
console.warn("GameViewport: Loop already exists, skipping init");
|
||||
return;
|
||||
}
|
||||
|
||||
const container = this.shadowRoot.getElementById("canvas-container");
|
||||
const loop = new GameLoop();
|
||||
loop.init(container);
|
||||
gameStateManager.setGameLoop(loop);
|
||||
this.loop = new GameLoop();
|
||||
this.loop.init(container);
|
||||
gameStateManager.setGameLoop(this.loop);
|
||||
|
||||
// Don't set squad from rosterLoaded - that's the full roster, not the current mission squad
|
||||
// Squad will be set from activeRunData when transitioning to deployment state
|
||||
|
|
@ -98,6 +106,17 @@ export class GameViewport extends LitElement {
|
|||
this.#updateSquad();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
console.log("GameViewport: disconnectedCallback called");
|
||||
if (this.loop) {
|
||||
console.log("GameViewport: Disposing GameLoop...");
|
||||
this.loop.dispose();
|
||||
this.loop = null;
|
||||
}
|
||||
gameStateManager.disconnectGameLoop();
|
||||
}
|
||||
|
||||
#setupCombatStateUpdates() {
|
||||
// Listen for combat state changes
|
||||
window.addEventListener("combat-state-changed", (e) => {
|
||||
|
|
@ -208,7 +227,10 @@ export class GameViewport extends LitElement {
|
|||
.result=${this.debriefResult}
|
||||
@return-to-hub=${() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("debrief-closed", { bubbles: true, composed: true })
|
||||
new CustomEvent("debrief-closed", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}
|
||||
></mission-debrief>`
|
||||
|
|
|
|||
37
src/utils/generate_manifest.js
Normal file
37
src/utils/generate_manifest.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const ASSETS_DIR = path.resolve(__dirname, "../assets/data");
|
||||
const OUTPUT_FILE = path.resolve(ASSETS_DIR, "manifest.json");
|
||||
|
||||
function getJsonFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => path.join(path.basename(dir), f).replace(/\\/g, "/"));
|
||||
}
|
||||
|
||||
export function generateManifest() {
|
||||
console.log("Generating asset manifest...");
|
||||
|
||||
const classes = getJsonFiles(path.join(ASSETS_DIR, "classes"));
|
||||
const units = getJsonFiles(path.join(ASSETS_DIR, "units"));
|
||||
|
||||
const manifest = {
|
||||
classes,
|
||||
units,
|
||||
};
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(manifest, null, 2));
|
||||
console.log(`Manifest written to ${OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
// Allow running directly
|
||||
if (process.argv[1] === __filename) {
|
||||
generateManifest();
|
||||
}
|
||||
66
src/utils/verify_missions.js
Normal file
66
src/utils/verify_missions.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const MISSIONS_DIR = path.resolve(__dirname, "../assets/data/missions");
|
||||
|
||||
function verifyMissions() {
|
||||
console.log("Verifying missions in:", MISSIONS_DIR);
|
||||
const files = fs.readdirSync(MISSIONS_DIR).filter((f) => f.endsWith(".json"));
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const filePath = path.join(MISSIONS_DIR, file);
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const mission = JSON.parse(content);
|
||||
|
||||
const missing = [];
|
||||
// Required root fields based on Mission.ts
|
||||
if (!mission.id) missing.push("id");
|
||||
if (!mission.type) missing.push("type");
|
||||
if (!mission.config) missing.push("config");
|
||||
if (!mission.biome) missing.push("biome");
|
||||
if (!mission.objectives) missing.push("objectives");
|
||||
if (!mission.rewards) missing.push("rewards");
|
||||
|
||||
// Required children
|
||||
if (mission.config && !mission.config.title) missing.push("config.title");
|
||||
if (mission.config && !mission.config.description)
|
||||
missing.push("config.description");
|
||||
if (mission.config && mission.config.difficulty_tier === undefined)
|
||||
missing.push("config.difficulty_tier");
|
||||
|
||||
if (mission.biome && !mission.biome.type) missing.push("biome.type");
|
||||
// generator_config is technically required in the interface `MissionBiome`
|
||||
// although older files might be missing it.
|
||||
if (mission.biome && !mission.biome.generator_config)
|
||||
missing.push("biome.generator_config");
|
||||
|
||||
if (mission.objectives && !mission.objectives.primary)
|
||||
missing.push("objectives.primary");
|
||||
if (mission.rewards && !mission.rewards.guaranteed)
|
||||
missing.push("rewards.guaranteed");
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log(
|
||||
`Mission ${file} is missing required fields: ${missing.join(", ")}`
|
||||
);
|
||||
hasError = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error parsing ${file}:`, err.message);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasError) {
|
||||
console.log("All missions pass structure verification.");
|
||||
}
|
||||
}
|
||||
|
||||
verifyMissions();
|
||||
|
|
@ -15,6 +15,47 @@ describe("Combat State Specification - CoA Tests", function () {
|
|||
let mockGameStateManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Stub fetch explicitly for this test suite
|
||||
if (!window.fetch.restore) {
|
||||
sinon.stub(window, "fetch");
|
||||
}
|
||||
|
||||
const mockResponse = (body) =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(body),
|
||||
});
|
||||
|
||||
window.fetch.callsFake((url) => {
|
||||
if (typeof url === "string" && url.includes("manifest.json")) {
|
||||
return mockResponse({
|
||||
classes: ["classes/vanguard.json"],
|
||||
units: [],
|
||||
});
|
||||
}
|
||||
if (typeof url === "string" && url.includes("vanguard.json")) {
|
||||
return mockResponse({
|
||||
id: "CLASS_VANGUARD",
|
||||
name: "Vanguard",
|
||||
type: "EXPLORER",
|
||||
base_stats: {
|
||||
health: 120,
|
||||
attack: 12,
|
||||
defense: 8,
|
||||
magic: 0,
|
||||
speed: 8,
|
||||
willpower: 5,
|
||||
movement: 3,
|
||||
},
|
||||
growth_rates: { health: 10, attack: 1, defense: 1 },
|
||||
starting_equipment: ["ITEM_RUSTY_BLADE", "ITEM_SCRAP_PLATE"],
|
||||
skillTreeData: { active_skills: [], passive_skills: [] },
|
||||
model: "vanguard.glb",
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(`Not Found: ${url}`));
|
||||
});
|
||||
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
|
|
@ -48,6 +89,9 @@ describe("Combat State Specification - CoA Tests", function () {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (window.fetch.restore) {
|
||||
window.fetch.restore();
|
||||
}
|
||||
gameLoop.stop();
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,127 @@
|
|||
import sinon from "sinon";
|
||||
import { GameLoop } from "../../../src/core/GameLoop.js";
|
||||
|
||||
/**
|
||||
* Mock Data for Assets
|
||||
*/
|
||||
const MOCK_MANIFEST = {
|
||||
classes: ["classes/vanguard.json"],
|
||||
units: ["enemies/default.json"],
|
||||
};
|
||||
|
||||
const MOCK_VANGUARD = {
|
||||
id: "CLASS_VANGUARD",
|
||||
name: "Vanguard",
|
||||
baseStats: {
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
ap: 3,
|
||||
maxAp: 3,
|
||||
movement: 5,
|
||||
defense: 10,
|
||||
resistance: 5,
|
||||
speed: 10,
|
||||
accuracy: 85,
|
||||
evasion: 5,
|
||||
critRate: 5,
|
||||
critDamage: 1.5,
|
||||
},
|
||||
growths: {
|
||||
hp: 1.0,
|
||||
ap: 0,
|
||||
movement: 0,
|
||||
defense: 0.5,
|
||||
resistance: 0.3,
|
||||
speed: 0.4,
|
||||
accuracy: 0.4,
|
||||
evasion: 0.2,
|
||||
critRate: 0.2,
|
||||
},
|
||||
equipment: {
|
||||
mainHand: "SWORD_BASIC",
|
||||
armor: "ARMOR_BASIC",
|
||||
},
|
||||
skills: ["STRIKE", "GUARD"],
|
||||
};
|
||||
|
||||
const MOCK_ENEMY = {
|
||||
id: "ENEMY_DEFAULT",
|
||||
name: "Drone",
|
||||
baseStats: {
|
||||
hp: 50,
|
||||
maxHp: 50,
|
||||
ap: 3,
|
||||
maxAp: 3,
|
||||
movement: 4,
|
||||
defense: 2,
|
||||
resistance: 0,
|
||||
speed: 8,
|
||||
accuracy: 80,
|
||||
evasion: 10,
|
||||
critRate: 5,
|
||||
critDamage: 1.5,
|
||||
},
|
||||
equipment: {
|
||||
mainHand: "BEAM_RIFLE",
|
||||
},
|
||||
};
|
||||
|
||||
// Keep track of the stub to restore it
|
||||
let fetchStub = null;
|
||||
|
||||
/**
|
||||
* Mocks window.fetch to return game assets.
|
||||
*/
|
||||
export function mockFetchAssets() {
|
||||
if (fetchStub) return; // Already mocked
|
||||
|
||||
fetchStub = sinon.stub(window, "fetch");
|
||||
|
||||
fetchStub.callsFake(async (url) => {
|
||||
if (url.includes("manifest.json")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => MOCK_MANIFEST,
|
||||
};
|
||||
}
|
||||
if (url.includes("classes/vanguard.json")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => MOCK_VANGUARD,
|
||||
};
|
||||
}
|
||||
if (url.includes("enemies/default.json")) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => MOCK_ENEMY,
|
||||
};
|
||||
}
|
||||
// Return empty/default for others to prevent crashes
|
||||
return {
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: async () => ({}),
|
||||
text: async () => "Not Found",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores window.fetch.
|
||||
*/
|
||||
export function restoreFetchAssets() {
|
||||
if (fetchStub) {
|
||||
fetchStub.restore();
|
||||
fetchStub = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a basic GameLoop setup for tests.
|
||||
* @returns {{ gameLoop: GameLoop; container: HTMLElement }}
|
||||
*/
|
||||
export function createGameLoopSetup() {
|
||||
mockFetchAssets(); // Auto-mock assets
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const gameLoop = new GameLoop();
|
||||
|
|
@ -28,6 +144,8 @@ export function cleanupGameLoop(gameLoop, container) {
|
|||
gameLoop.renderer.dispose();
|
||||
gameLoop.renderer.forceContextLoss();
|
||||
}
|
||||
|
||||
restoreFetchAssets(); // Restore fetch
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ describe("Core: GameStateManager - Hub Integration", () => {
|
|||
load: sinon.stub(),
|
||||
save: sinon.stub().returns({ completedMissions: [] }),
|
||||
completedMissions: new Set(),
|
||||
_ensureMissionsLoaded: sinon.stub().resolves(),
|
||||
areProceduralMissionsUnlocked: sinon.stub().returns(false),
|
||||
refreshProceduralMissions: sinon.stub(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -70,7 +73,8 @@ describe("Core: GameStateManager - Hub Integration", () => {
|
|||
await gameStateManager.continueGame();
|
||||
|
||||
expect(mockPersistence.loadRun.called).to.be.true;
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be
|
||||
.true;
|
||||
// Hub should show because roster exists
|
||||
transitionSpy.restore();
|
||||
});
|
||||
|
|
@ -96,7 +100,9 @@ describe("Core: GameStateManager - Hub Integration", () => {
|
|||
});
|
||||
|
||||
it("should go to Hub when completed missions exist but no active run", async () => {
|
||||
gameStateManager.missionManager.completedMissions.add("MISSION_TUTORIAL_01");
|
||||
gameStateManager.missionManager.completedMissions.add(
|
||||
"MISSION_TUTORIAL_01"
|
||||
);
|
||||
mockPersistence.loadRun.resolves(null);
|
||||
|
||||
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
|
||||
|
|
@ -104,7 +110,8 @@ describe("Core: GameStateManager - Hub Integration", () => {
|
|||
|
||||
await gameStateManager.continueGame();
|
||||
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be
|
||||
.true;
|
||||
// Hub should show because completed missions exist
|
||||
transitionSpy.restore();
|
||||
});
|
||||
|
|
@ -164,10 +171,10 @@ describe("Core: GameStateManager - Hub Integration", () => {
|
|||
await gameStateManager.transitionTo(GameStateManager.STATES.MAIN_MENU);
|
||||
|
||||
// Check that transitionTo was called with MAIN_MENU (could be from init or our call)
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
||||
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be
|
||||
.true;
|
||||
// Hub should be shown because roster exists
|
||||
transitionSpy.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ describe("Systems: MissionGenerator", function () {
|
|||
it("should have type-specific noun arrays", () => {
|
||||
expect(MissionGenerator.NOUNS_SKIRMISH).to.be.an("array");
|
||||
expect(MissionGenerator.NOUNS_SALVAGE).to.be.an("array");
|
||||
expect(MissionGenerator.NOUNS_ASSASSINATION).to.be.an("array");
|
||||
expect(MissionGenerator.NOUNS_ELIMINATE_UNIT).to.be.an("array");
|
||||
expect(MissionGenerator.NOUNS_RECON).to.be.an("array");
|
||||
|
||||
expect(MissionGenerator.NOUNS_SKIRMISH).to.include("Thunder");
|
||||
expect(MissionGenerator.NOUNS_SALVAGE).to.include("Cache");
|
||||
expect(MissionGenerator.NOUNS_ASSASSINATION).to.include("Viper");
|
||||
expect(MissionGenerator.NOUNS_ELIMINATE_UNIT).to.include("Viper");
|
||||
expect(MissionGenerator.NOUNS_RECON).to.include("Eye");
|
||||
});
|
||||
});
|
||||
|
|
@ -151,7 +151,7 @@ describe("Systems: MissionGenerator", function () {
|
|||
} else if (primaryObj.type === "INTERACT") {
|
||||
archetypes.add("SALVAGE");
|
||||
} else if (primaryObj.type === "ELIMINATE_UNIT") {
|
||||
archetypes.add("ASSASSINATION");
|
||||
archetypes.add("ELIMINATE_UNIT");
|
||||
} else if (primaryObj.type === "REACH_ZONE") {
|
||||
archetypes.add("RECON");
|
||||
}
|
||||
|
|
@ -276,16 +276,16 @@ describe("Systems: MissionGenerator", function () {
|
|||
}
|
||||
expect(foundSalvage).to.be.true;
|
||||
|
||||
// Test Assassination (ELIMINATE_UNIT)
|
||||
let foundAssassination = false;
|
||||
for (let i = 0; i < 30 && !foundAssassination; i++) {
|
||||
// Test Eliminate Unit
|
||||
let foundEliminateUnit = false;
|
||||
for (let i = 0; i < 30 && !foundEliminateUnit; i++) {
|
||||
const mission = MissionGenerator.generateSideOp(
|
||||
2,
|
||||
unlockedRegions,
|
||||
emptyHistory
|
||||
);
|
||||
if (mission.objectives.primary[0].type === "ELIMINATE_UNIT") {
|
||||
foundAssassination = true;
|
||||
foundEliminateUnit = true;
|
||||
expect(mission.objectives.primary[0]).to.have.property(
|
||||
"target_def_id"
|
||||
);
|
||||
|
|
@ -294,7 +294,7 @@ describe("Systems: MissionGenerator", function () {
|
|||
);
|
||||
}
|
||||
}
|
||||
expect(foundAssassination).to.be.true;
|
||||
expect(foundEliminateUnit).to.be.true;
|
||||
|
||||
// Test Recon (REACH_ZONE)
|
||||
let foundRecon = false;
|
||||
|
|
@ -520,17 +520,17 @@ describe("Systems: MissionGenerator", function () {
|
|||
|
||||
it("CoA 19: Should calculate currency with tier multiplier and random factor", () => {
|
||||
// Generate multiple missions to account for different archetypes
|
||||
// Non-assassination missions: Base 50 * 2.5 (tier 3) * random(0.8, 1.2) = 100-150 range
|
||||
// Assassination missions get 1.5x bonus: 150-225 range
|
||||
let foundNonAssassination = false;
|
||||
for (let i = 0; i < 20 && !foundNonAssassination; i++) {
|
||||
// Non-Eliminate Unit missions: Base 50 * 2.5 (tier 3) * random(0.8, 1.2) = 100-150 range
|
||||
// Eliminate Unit missions get 1.5x bonus: 150-225 range
|
||||
let foundNonEliminateUnit = false;
|
||||
for (let i = 0; i < 20 && !foundNonEliminateUnit; i++) {
|
||||
const mission = MissionGenerator.generateSideOp(3, unlockedRegions, []);
|
||||
const currency = mission.rewards.guaranteed.currency.aether_shards;
|
||||
const isAssassination =
|
||||
const isEliminateUnit =
|
||||
mission.objectives.primary[0].type === "ELIMINATE_UNIT";
|
||||
|
||||
if (!isAssassination) {
|
||||
foundNonAssassination = true;
|
||||
if (!isEliminateUnit) {
|
||||
foundNonEliminateUnit = true;
|
||||
expect(currency).to.be.at.least(100); // 50 * 2.5 * 0.8 = 100
|
||||
expect(currency).to.be.at.most(150); // 50 * 2.5 * 1.2 = 150
|
||||
} else {
|
||||
|
|
@ -539,28 +539,28 @@ describe("Systems: MissionGenerator", function () {
|
|||
expect(currency).to.be.at.most(225); // 50 * 2.5 * 1.2 * 1.5 = 225
|
||||
}
|
||||
}
|
||||
expect(foundNonAssassination).to.be.true;
|
||||
expect(foundNonEliminateUnit).to.be.true;
|
||||
});
|
||||
|
||||
it("CoA 20: Should give bonus currency for Assassination missions", () => {
|
||||
let foundAssassination = false;
|
||||
let assassinationCurrency = 0;
|
||||
it("CoA 20: Should give bonus currency for Eliminate Unit missions", () => {
|
||||
let foundEliminateUnit = false;
|
||||
let eliminateUnitCurrency = 0;
|
||||
let otherCurrency = 0;
|
||||
|
||||
for (let i = 0; i < 50 && !foundAssassination; i++) {
|
||||
for (let i = 0; i < 50 && !foundEliminateUnit; i++) {
|
||||
const mission = MissionGenerator.generateSideOp(2, unlockedRegions, []);
|
||||
if (mission.objectives.primary[0].type === "ELIMINATE_UNIT") {
|
||||
foundAssassination = true;
|
||||
assassinationCurrency =
|
||||
foundEliminateUnit = true;
|
||||
eliminateUnitCurrency =
|
||||
mission.rewards.guaranteed.currency.aether_shards;
|
||||
} else {
|
||||
otherCurrency = mission.rewards.guaranteed.currency.aether_shards;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundAssassination) {
|
||||
// Assassination should have higher currency (1.5x bonus)
|
||||
expect(assassinationCurrency).to.be.greaterThan(otherCurrency * 0.9);
|
||||
if (foundEliminateUnit) {
|
||||
// Eliminate Unit should have higher currency (1.5x bonus)
|
||||
expect(eliminateUnitCurrency).to.be.greaterThan(otherCurrency * 0.9);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -632,18 +632,18 @@ describe("Systems: MissionGenerator", function () {
|
|||
.to.be.true;
|
||||
});
|
||||
|
||||
it("CoA 24: Should generate boss config for Assassination missions", () => {
|
||||
let foundAssassination = false;
|
||||
it("CoA 24: Should generate boss config for Eliminate Unit missions", () => {
|
||||
let foundEliminateUnit = false;
|
||||
let attempts = 0;
|
||||
|
||||
while (!foundAssassination && attempts < 50) {
|
||||
while (!foundEliminateUnit && attempts < 50) {
|
||||
const mission = MissionGenerator.generateSideOp(
|
||||
2,
|
||||
unlockedRegions,
|
||||
emptyHistory
|
||||
);
|
||||
if (mission.config.boss_config) {
|
||||
foundAssassination = true;
|
||||
foundEliminateUnit = true;
|
||||
const config = mission.config.boss_config;
|
||||
|
||||
expect(config).to.have.property("target_def_id");
|
||||
|
|
@ -655,8 +655,8 @@ describe("Systems: MissionGenerator", function () {
|
|||
attempts++;
|
||||
}
|
||||
expect(
|
||||
foundAssassination,
|
||||
"Should have generated an Assassination mission with boss config"
|
||||
foundEliminateUnit,
|
||||
"Should have generated an Eliminate Unit mission with boss config"
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ describe("UI: MissionBoard", () => {
|
|||
let container;
|
||||
let mockMissionManager;
|
||||
|
||||
let originalMissionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
originalMissionManager = gameStateManager.missionManager;
|
||||
|
||||
// Mock MissionManager - set up BEFORE creating element
|
||||
mockMissionManager = {
|
||||
missionRegistry: new Map(),
|
||||
|
|
@ -27,6 +31,7 @@ describe("UI: MissionBoard", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
gameStateManager.missionManager = originalMissionManager;
|
||||
if (container && container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
}
|
||||
|
|
@ -92,9 +97,9 @@ describe("UI: MissionBoard", () => {
|
|||
mockMissionManager.missionRegistry.set(mission2.id, mission2);
|
||||
|
||||
// Wait for initial load to complete, then trigger a reload
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
// Dispatch event to trigger reload
|
||||
window.dispatchEvent(new CustomEvent('missions-updated'));
|
||||
window.dispatchEvent(new CustomEvent("missions-updated"));
|
||||
await waitForUpdate();
|
||||
|
||||
const missionCards = queryShadowAll(".mission-card");
|
||||
|
|
@ -150,7 +155,9 @@ describe("UI: MissionBoard", () => {
|
|||
const missionCard = queryShadow(".mission-card");
|
||||
const description = missionCard.querySelector(".mission-description");
|
||||
expect(description).to.exist;
|
||||
expect(description.textContent).to.include("This is a test mission description.");
|
||||
expect(description.textContent).to.include(
|
||||
"This is a test mission description."
|
||||
);
|
||||
});
|
||||
|
||||
it("should display difficulty information", async () => {
|
||||
|
|
@ -273,12 +280,12 @@ describe("UI: MissionBoard", () => {
|
|||
};
|
||||
mockMissionManager.missionRegistry.set(mission.id, mission);
|
||||
mockMissionManager.completedMissions.add(mission.id);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent('missions-updated'));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent("missions-updated"));
|
||||
await waitForUpdate();
|
||||
|
||||
// Switch to completed tab
|
||||
const completedTab = queryShadow('.tab-button:last-child');
|
||||
const completedTab = queryShadow(".tab-button:last-child");
|
||||
if (completedTab) {
|
||||
completedTab.click();
|
||||
await waitForUpdate();
|
||||
|
|
@ -299,12 +306,12 @@ describe("UI: MissionBoard", () => {
|
|||
};
|
||||
mockMissionManager.missionRegistry.set(mission.id, mission);
|
||||
mockMissionManager.completedMissions.add(mission.id);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent('missions-updated'));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent("missions-updated"));
|
||||
await waitForUpdate();
|
||||
|
||||
// Switch to completed tab
|
||||
const completedTab = queryShadow('.tab-button:last-child');
|
||||
const completedTab = queryShadow(".tab-button:last-child");
|
||||
if (completedTab) {
|
||||
completedTab.click();
|
||||
await waitForUpdate();
|
||||
|
|
@ -403,10 +410,30 @@ describe("UI: MissionBoard", () => {
|
|||
describe("Mission Type Styling", () => {
|
||||
it("should apply correct styling for different mission types", async () => {
|
||||
const missions = [
|
||||
{ id: "M1", type: "STORY", config: { title: "Story", description: "Test" }, rewards: {} },
|
||||
{ id: "M2", type: "SIDE_QUEST", config: { title: "Side", description: "Test" }, rewards: {} },
|
||||
{ id: "M3", type: "TUTORIAL", config: { title: "Tutorial", description: "Test" }, rewards: {} },
|
||||
{ id: "M4", type: "PROCEDURAL", config: { title: "Proc", description: "Test" }, rewards: {} },
|
||||
{
|
||||
id: "M1",
|
||||
type: "STORY",
|
||||
config: { title: "Story", description: "Test" },
|
||||
rewards: {},
|
||||
},
|
||||
{
|
||||
id: "M2",
|
||||
type: "SIDE_QUEST",
|
||||
config: { title: "Side", description: "Test" },
|
||||
rewards: {},
|
||||
},
|
||||
{
|
||||
id: "M3",
|
||||
type: "TUTORIAL",
|
||||
config: { title: "Tutorial", description: "Test" },
|
||||
rewards: {},
|
||||
},
|
||||
{
|
||||
id: "M4",
|
||||
type: "PROCEDURAL",
|
||||
config: { title: "Proc", description: "Test" },
|
||||
rewards: {},
|
||||
},
|
||||
];
|
||||
|
||||
missions.forEach((m) => mockMissionManager.missionRegistry.set(m.id, m));
|
||||
|
|
@ -468,7 +495,9 @@ describe("UI: MissionBoard", () => {
|
|||
expect(missionCards.length).to.equal(2);
|
||||
|
||||
const mission2Card = Array.from(missionCards).find((card) =>
|
||||
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
|
||||
card
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.includes("Second Mission")
|
||||
);
|
||||
expect(mission2Card).to.exist;
|
||||
expect(mission2Card.classList.contains("locked")).to.be.true;
|
||||
|
|
@ -499,7 +528,9 @@ describe("UI: MissionBoard", () => {
|
|||
|
||||
const missionCards = queryShadowAll(".mission-card");
|
||||
const mission2Card = Array.from(missionCards).find((card) =>
|
||||
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
|
||||
card
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.includes("Second Mission")
|
||||
);
|
||||
expect(mission2Card).to.exist;
|
||||
expect(mission2Card.classList.contains("locked")).to.be.false;
|
||||
|
|
@ -529,7 +560,9 @@ describe("UI: MissionBoard", () => {
|
|||
|
||||
const missionCards = queryShadowAll(".mission-card");
|
||||
const mission2Card = Array.from(missionCards).find((card) =>
|
||||
card.querySelector(".mission-title")?.textContent.includes("Second Mission")
|
||||
card
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.includes("Second Mission")
|
||||
);
|
||||
expect(mission2Card).to.exist;
|
||||
expect(mission2Card.textContent).to.include("Requires");
|
||||
|
|
@ -600,7 +633,9 @@ describe("UI: MissionBoard", () => {
|
|||
expect(titles).to.include("Second Quest");
|
||||
|
||||
const mission2Card = Array.from(missionCards).find((card) =>
|
||||
card.querySelector(".mission-title")?.textContent.includes("Second Quest")
|
||||
card
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.includes("Second Quest")
|
||||
);
|
||||
expect(mission2Card.classList.contains("locked")).to.be.true;
|
||||
});
|
||||
|
|
@ -626,27 +661,31 @@ describe("UI: MissionBoard", () => {
|
|||
mockMissionManager.missionRegistry.set(mission1.id, mission1);
|
||||
mockMissionManager.missionRegistry.set(mission2.id, mission2);
|
||||
mockMissionManager.completedMissions.add("MISSION_01");
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent('missions-updated'));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
window.dispatchEvent(new CustomEvent("missions-updated"));
|
||||
await waitForUpdate();
|
||||
// Wait a bit more for the component to process the update
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await waitForUpdate();
|
||||
|
||||
// Check active tab - should show MISSION_02 (prerequisites met)
|
||||
const activeCards = queryShadowAll(".missions-grid .mission-card");
|
||||
expect(activeCards.length).to.equal(1);
|
||||
const activeTitle = activeCards[0].querySelector(".mission-title")?.textContent.trim();
|
||||
const activeTitle = activeCards[0]
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.trim();
|
||||
expect(activeTitle).to.equal("Second Story");
|
||||
|
||||
// Check completed tab - should show MISSION_01 (completed)
|
||||
const completedTab = queryShadow('.tab-button:last-child');
|
||||
const completedTab = queryShadow(".tab-button:last-child");
|
||||
if (completedTab) {
|
||||
completedTab.click();
|
||||
await waitForUpdate();
|
||||
const completedCards = queryShadowAll(".missions-grid .mission-card");
|
||||
expect(completedCards.length).to.equal(1);
|
||||
const completedTitle = completedCards[0].querySelector(".mission-title")?.textContent.trim();
|
||||
const completedTitle = completedCards[0]
|
||||
.querySelector(".mission-title")
|
||||
?.textContent.trim();
|
||||
expect(completedTitle).to.equal("First Story");
|
||||
}
|
||||
});
|
||||
|
|
@ -683,4 +722,3 @@ describe("UI: MissionBoard", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue