Enhance GameLoop and MissionManager integration for unit death handling and mission events
Refactor the GameLoop to implement unit death handling through the new handleUnitDeath method, which removes units from the grid and dispatches relevant events to the MissionManager. Set up MissionManager references and event listeners for mission victory and failure. Update the MissionManager to manage secondary objectives and failure conditions, ensuring proper tracking of game events like unit deaths and turn ends. This integration improves gameplay dynamics and mission management.
This commit is contained in:
parent
f04905044d
commit
06389903c5
2 changed files with 522 additions and 25 deletions
|
|
@ -808,7 +808,8 @@ export class GameLoop {
|
|||
target: target,
|
||||
killedUnit: target,
|
||||
});
|
||||
// TODO: Handle unit death (remove from grid, trigger death effects, etc.)
|
||||
// Handle unit death
|
||||
this.handleUnitDeath(target);
|
||||
}
|
||||
// Process passive item effects for ON_DAMAGED trigger (on target)
|
||||
this.processPassiveItemEffects(target, "ON_DAMAGED", {
|
||||
|
|
@ -1337,6 +1338,16 @@ export class GameLoop {
|
|||
const allUnits = this.unitManager.getAllUnits();
|
||||
this.turnSystem.startCombat(allUnits);
|
||||
|
||||
// WIRING: Set up MissionManager references
|
||||
if (this.missionManager) {
|
||||
this.missionManager.setUnitManager(this.unitManager);
|
||||
this.missionManager.setTurnSystem(this.turnSystem);
|
||||
this.missionManager.setupActiveMission();
|
||||
}
|
||||
|
||||
// WIRING: Listen for mission events
|
||||
this._setupMissionEventListeners();
|
||||
|
||||
// Update combat state immediately so UI shows combat HUD
|
||||
this.updateCombatState().catch(console.error);
|
||||
|
||||
|
|
@ -2234,6 +2245,13 @@ export class GameLoop {
|
|||
_onTurnEnd(detail) {
|
||||
// Clear movement highlights when turn ends
|
||||
this.clearMovementHighlights();
|
||||
|
||||
// Dispatch TURN_END event to MissionManager
|
||||
if (this.missionManager && this.turnSystem) {
|
||||
const currentTurn = this.turnSystem.round || 0;
|
||||
this.missionManager.updateTurn(currentTurn);
|
||||
this.missionManager.onGameEvent('TURN_END', { turn: currentTurn });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2455,6 +2473,128 @@ export class GameLoop {
|
|||
if (context.didAttack === true) return false;
|
||||
}
|
||||
|
||||
return true; // All conditions passed
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unit death: removes from grid, dispatches events, and updates MissionManager.
|
||||
* @param {Unit} unit - The unit that died
|
||||
*/
|
||||
handleUnitDeath(unit) {
|
||||
if (!unit || !this.grid || !this.unitManager) return;
|
||||
|
||||
// Remove unit from grid
|
||||
if (unit.position) {
|
||||
this.grid.removeUnit(unit.position);
|
||||
}
|
||||
|
||||
// Remove unit from UnitManager
|
||||
this.unitManager.removeUnit(unit.id);
|
||||
|
||||
// Remove unit mesh from scene
|
||||
const mesh = this.unitMeshes.get(unit.id);
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh);
|
||||
this.unitMeshes.delete(unit.id);
|
||||
// Dispose geometry and material
|
||||
if (mesh.geometry) mesh.geometry.dispose();
|
||||
if (mesh.material) {
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material.forEach(mat => {
|
||||
if (mat.map) mat.map.dispose();
|
||||
mat.dispose();
|
||||
});
|
||||
} else {
|
||||
if (mesh.material.map) mesh.material.map.dispose();
|
||||
mesh.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch death event to MissionManager
|
||||
if (this.missionManager) {
|
||||
const eventType = unit.team === 'ENEMY' ? 'ENEMY_DEATH' : 'PLAYER_DEATH';
|
||||
const unitDefId = unit.defId || unit.id;
|
||||
this.missionManager.onGameEvent(eventType, {
|
||||
unitId: unit.id,
|
||||
defId: unitDefId,
|
||||
team: unit.team
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`${unit.name} (${unit.team}) has been removed from combat.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for mission victory and failure.
|
||||
* @private
|
||||
*/
|
||||
_setupMissionEventListeners() {
|
||||
// Listen for mission victory
|
||||
window.addEventListener('mission-victory', (event) => {
|
||||
this._handleMissionVictory(event.detail);
|
||||
});
|
||||
|
||||
// Listen for mission failure
|
||||
window.addEventListener('mission-failure', (event) => {
|
||||
this._handleMissionFailure(event.detail);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mission victory.
|
||||
* @param {Object} detail - Victory event detail
|
||||
* @private
|
||||
*/
|
||||
_handleMissionVictory(detail) {
|
||||
console.log('Mission Victory!', detail);
|
||||
|
||||
// Pause the game
|
||||
this.isPaused = true;
|
||||
|
||||
// Stop the game loop
|
||||
this.stop();
|
||||
|
||||
// TODO: Show victory screen UI
|
||||
// For now, just log and transition back to main menu after a delay
|
||||
setTimeout(() => {
|
||||
if (this.gameStateManager) {
|
||||
this.gameStateManager.transitionTo('STATE_MAIN_MENU');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mission failure.
|
||||
* @param {Object} detail - Failure event detail
|
||||
* @private
|
||||
*/
|
||||
_handleMissionFailure(detail) {
|
||||
console.log('Mission Failed!', detail);
|
||||
|
||||
// Pause the game
|
||||
this.isPaused = true;
|
||||
|
||||
// Stop the game loop
|
||||
this.stop();
|
||||
|
||||
// TODO: Show failure screen UI
|
||||
// For now, just log and transition back to main menu after a delay
|
||||
setTimeout(() => {
|
||||
if (this.gameStateManager) {
|
||||
this.gameStateManager.transitionTo('STATE_MAIN_MENU');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks DID_NOT_ATTACK condition (for ON_TURN_END)
|
||||
if (condition.type === "DID_NOT_ATTACK") {
|
||||
// This would need to track if the unit attacked this turn
|
||||
// For now, we'll assume it's tracked in context
|
||||
if (context.didAttack === true) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ export class MissionManager {
|
|||
this.currentMissionDef = null;
|
||||
/** @type {Objective[]} */
|
||||
this.currentObjectives = [];
|
||||
/** @type {Objective[]} */
|
||||
this.secondaryObjectives = [];
|
||||
/** @type {Array<{type: string; [key: string]: unknown}>} */
|
||||
this.failureConditions = [];
|
||||
/** @type {UnitManager | null} */
|
||||
this.unitManager = null;
|
||||
/** @type {TurnSystem | null} */
|
||||
this.turnSystem = null;
|
||||
/** @type {number} */
|
||||
this.currentTurn = 0;
|
||||
|
||||
// Register default missions
|
||||
this.registerMission(tutorialMission);
|
||||
|
|
@ -76,6 +86,22 @@ export class MissionManager {
|
|||
return this.missionRegistry.get(this.activeMissionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unit manager reference for objective checking.
|
||||
* @param {UnitManager} unitManager - Unit manager instance
|
||||
*/
|
||||
setUnitManager(unitManager) {
|
||||
this.unitManager = unitManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the turn system reference for turn-based objectives.
|
||||
* @param {TurnSystem} turnSystem - Turn system instance
|
||||
*/
|
||||
setTurnSystem(turnSystem) {
|
||||
this.turnSystem = turnSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the manager for a new run.
|
||||
* Resets objectives and prepares narrative hooks.
|
||||
|
|
@ -83,15 +109,32 @@ export class MissionManager {
|
|||
setupActiveMission() {
|
||||
const mission = this.getActiveMission();
|
||||
this.currentMissionDef = mission;
|
||||
this.currentTurn = 0;
|
||||
|
||||
// Hydrate objectives state
|
||||
this.currentObjectives = mission.objectives.primary.map(obj => ({
|
||||
// Hydrate primary objectives state
|
||||
this.currentObjectives = (mission.objectives.primary || []).map(obj => ({
|
||||
...obj,
|
||||
current: 0,
|
||||
complete: false
|
||||
}));
|
||||
|
||||
console.log(`Mission Setup: ${mission.config.title} - Objectives:`, this.currentObjectives);
|
||||
// Hydrate secondary objectives state
|
||||
this.secondaryObjectives = (mission.objectives.secondary || []).map(obj => ({
|
||||
...obj,
|
||||
current: 0,
|
||||
complete: false
|
||||
}));
|
||||
|
||||
// Store failure conditions
|
||||
this.failureConditions = mission.objectives.failure_conditions || [];
|
||||
|
||||
console.log(`Mission Setup: ${mission.config.title} - Primary Objectives:`, this.currentObjectives);
|
||||
if (this.secondaryObjectives.length > 0) {
|
||||
console.log('Secondary Objectives:', this.secondaryObjectives);
|
||||
}
|
||||
if (this.failureConditions.length > 0) {
|
||||
console.log('Failure Conditions:', this.failureConditions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -148,7 +191,9 @@ export class MissionManager {
|
|||
// Remove NARRATIVE_ prefix and convert to lowercase with underscores
|
||||
const mapping = {
|
||||
'NARRATIVE_TUTORIAL_INTRO': 'tutorial_intro',
|
||||
'NARRATIVE_TUTORIAL_SUCCESS': 'tutorial_success'
|
||||
'NARRATIVE_TUTORIAL_SUCCESS': 'tutorial_success',
|
||||
'NARRATIVE_ACT1_FINAL_WIN': 'act1_final_win',
|
||||
'NARRATIVE_ACT1_FINAL_LOSE': 'act1_final_lose'
|
||||
};
|
||||
|
||||
return mapping[narrativeId] || narrativeId.toLowerCase().replace('NARRATIVE_', '');
|
||||
|
|
@ -158,53 +203,365 @@ export class MissionManager {
|
|||
|
||||
/**
|
||||
* Called by GameLoop whenever a relevant event occurs.
|
||||
* @param {string} type - 'ENEMY_DEATH', 'TURN_END', etc.
|
||||
* @param {string} type - 'ENEMY_DEATH', 'TURN_END', 'PLAYER_DEATH', etc.
|
||||
* @param {GameEventData} data - Context data
|
||||
*/
|
||||
onGameEvent(type, data) {
|
||||
if (!this.currentObjectives.length) return;
|
||||
if (!this.currentMissionDef) return;
|
||||
|
||||
// Check failure conditions first
|
||||
this.checkFailureConditions(type, data);
|
||||
|
||||
// Update objectives
|
||||
let statusChanged = false;
|
||||
|
||||
this.currentObjectives.forEach(obj => {
|
||||
// Process primary objectives
|
||||
statusChanged = this.updateObjectives(this.currentObjectives, type, data) || statusChanged;
|
||||
|
||||
// Process secondary objectives
|
||||
this.updateObjectives(this.secondaryObjectives, type, data);
|
||||
|
||||
// Check for ELIMINATE_ALL objective completion (needs active check)
|
||||
// Check after enemy death or at turn end
|
||||
if (type === 'ENEMY_DEATH') {
|
||||
statusChanged = this.checkEliminateAllObjective() || statusChanged;
|
||||
} else if (type === 'TURN_END') {
|
||||
// Also check on turn end in case all enemies died from status effects
|
||||
statusChanged = this.checkEliminateAllObjective() || statusChanged;
|
||||
}
|
||||
|
||||
if (statusChanged) {
|
||||
this.checkVictory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates objectives based on game events.
|
||||
* @param {Objective[]} objectives - Objectives to update
|
||||
* @param {string} eventType - Event type
|
||||
* @param {GameEventData} data - Event data
|
||||
* @returns {boolean} True if any objective status changed
|
||||
*/
|
||||
updateObjectives(objectives, eventType, data) {
|
||||
let statusChanged = false;
|
||||
|
||||
objectives.forEach(obj => {
|
||||
if (obj.complete) return;
|
||||
|
||||
// Logic for 'ELIMINATE_ALL' or 'ELIMINATE_UNIT'
|
||||
if (type === 'ENEMY_DEATH') {
|
||||
if (obj.type === 'ELIMINATE_ALL' ||
|
||||
(obj.type === 'ELIMINATE_UNIT' && data.unitId === obj.target_def_id)) {
|
||||
|
||||
obj.current++;
|
||||
// ELIMINATE_UNIT: Track specific enemy deaths
|
||||
if (eventType === 'ENEMY_DEATH' && obj.type === 'ELIMINATE_UNIT') {
|
||||
if (data.unitId === obj.target_def_id || data.defId === obj.target_def_id) {
|
||||
obj.current = (obj.current || 0) + 1;
|
||||
if (obj.target_count && obj.current >= obj.target_count) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SURVIVE: Check turn count
|
||||
if (eventType === 'TURN_END' && obj.type === 'SURVIVE') {
|
||||
if (obj.turn_count && this.currentTurn >= obj.turn_count) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// REACH_ZONE: Check if unit reached target zone
|
||||
if (eventType === 'UNIT_MOVE' && obj.type === 'REACH_ZONE') {
|
||||
if (data.position && obj.zone_coords) {
|
||||
const reached = obj.zone_coords.some(coord =>
|
||||
coord.x === data.position.x &&
|
||||
coord.y === data.position.y &&
|
||||
coord.z === data.position.z
|
||||
);
|
||||
if (reached) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// INTERACT: Check if unit interacted with target object
|
||||
if (eventType === 'INTERACT' && obj.type === 'INTERACT') {
|
||||
if (data.objectId === obj.target_object_id) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// SQUAD_SURVIVAL: Check if minimum units are alive
|
||||
if (eventType === 'PLAYER_DEATH' && obj.type === 'SQUAD_SURVIVAL') {
|
||||
if (this.unitManager) {
|
||||
const playerUnits = Array.from(this.unitManager.activeUnits.values())
|
||||
.filter(u => u.team === 'PLAYER' && u.currentHealth > 0);
|
||||
if (obj.min_alive && playerUnits.length >= obj.min_alive) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (statusChanged) {
|
||||
this.checkVictory();
|
||||
return statusChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if ELIMINATE_ALL objective is complete.
|
||||
* @returns {boolean} True if status changed
|
||||
*/
|
||||
checkEliminateAllObjective() {
|
||||
let statusChanged = false;
|
||||
|
||||
[...this.currentObjectives, ...this.secondaryObjectives].forEach(obj => {
|
||||
if (obj.complete || obj.type !== 'ELIMINATE_ALL') return;
|
||||
|
||||
if (this.unitManager) {
|
||||
const enemies = Array.from(this.unitManager.activeUnits.values())
|
||||
.filter(u => u.team === 'ENEMY' && u.currentHealth > 0);
|
||||
|
||||
if (enemies.length === 0) {
|
||||
obj.complete = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return statusChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks failure conditions and triggers failure if met.
|
||||
* @param {string} eventType - Event type
|
||||
* @param {GameEventData} data - Event data
|
||||
*/
|
||||
checkFailureConditions(eventType, data) {
|
||||
if (!this.failureConditions.length) return;
|
||||
|
||||
for (const condition of this.failureConditions) {
|
||||
// SQUAD_WIPE: All player units are dead
|
||||
if (condition.type === 'SQUAD_WIPE') {
|
||||
if (this.unitManager) {
|
||||
const playerUnits = Array.from(this.unitManager.activeUnits.values())
|
||||
.filter(u => u.team === 'PLAYER' && u.currentHealth > 0);
|
||||
if (playerUnits.length === 0) {
|
||||
this.triggerFailure('SQUAD_WIPE');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VIP_DEATH: VIP unit died
|
||||
if (condition.type === 'VIP_DEATH' && eventType === 'PLAYER_DEATH') {
|
||||
if (data.unitId && condition.target_tag) {
|
||||
const unit = this.unitManager?.getUnitById(data.unitId);
|
||||
if (unit && unit.tags && unit.tags.includes(condition.target_tag)) {
|
||||
this.triggerFailure('VIP_DEATH', { unitId: data.unitId });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TURN_LIMIT_EXCEEDED: Mission took too long
|
||||
if (condition.type === 'TURN_LIMIT_EXCEEDED' && eventType === 'TURN_END') {
|
||||
if (condition.turn_limit && this.currentTurn > condition.turn_limit) {
|
||||
this.triggerFailure('TURN_LIMIT_EXCEEDED', { turn: this.currentTurn });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers mission failure.
|
||||
* @param {string} reason - Failure reason
|
||||
* @param {GameEventData} [data] - Additional failure data
|
||||
*/
|
||||
triggerFailure(reason, data = {}) {
|
||||
console.log(`MISSION FAILED: ${reason}`, data);
|
||||
window.dispatchEvent(new CustomEvent('mission-failure', {
|
||||
detail: {
|
||||
missionId: this.activeMissionId,
|
||||
reason: reason,
|
||||
...data
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
checkVictory() {
|
||||
const allPrimaryComplete = this.currentObjectives.every(o => o.complete);
|
||||
const allPrimaryComplete = this.currentObjectives.length > 0 &&
|
||||
this.currentObjectives.every(o => o.complete);
|
||||
if (allPrimaryComplete) {
|
||||
console.log("VICTORY! Mission Objectives Complete.");
|
||||
this.completeActiveMission();
|
||||
// Dispatch event for GameLoop to handle Victory Screen
|
||||
window.dispatchEvent(new CustomEvent('mission-victory', { detail: { missionId: this.activeMissionId }}));
|
||||
window.dispatchEvent(new CustomEvent('mission-victory', {
|
||||
detail: {
|
||||
missionId: this.activeMissionId,
|
||||
primaryObjectives: this.currentObjectives,
|
||||
secondaryObjectives: this.secondaryObjectives
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
completeActiveMission() {
|
||||
if (this.activeMissionId) {
|
||||
/**
|
||||
* Completes the active mission and distributes rewards.
|
||||
*/
|
||||
async completeActiveMission() {
|
||||
if (!this.activeMissionId || !this.currentMissionDef) return;
|
||||
|
||||
// Mark mission as completed
|
||||
this.completedMissions.add(this.activeMissionId);
|
||||
// Simple campaign logic: If Tutorial done, unlock next (Placeholder)
|
||||
if (this.activeMissionId === 'MISSION_TUTORIAL_01') {
|
||||
// this.activeMissionId = 'MISSION_ACT1_01';
|
||||
|
||||
// Distribute rewards
|
||||
this.distributeRewards();
|
||||
|
||||
// Play outro narrative if available
|
||||
if (this.currentMissionDef.narrative?.outro_success) {
|
||||
await this.playOutro(this.currentMissionDef.narrative.outro_success);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributes mission rewards (XP, currency, items, unlocks).
|
||||
*/
|
||||
distributeRewards() {
|
||||
if (!this.currentMissionDef || !this.currentMissionDef.rewards) return;
|
||||
|
||||
const rewards = this.currentMissionDef.rewards;
|
||||
const rewardData = {
|
||||
xp: 0,
|
||||
currency: {},
|
||||
items: [],
|
||||
unlocks: [],
|
||||
factionReputation: {}
|
||||
};
|
||||
|
||||
// Guaranteed rewards
|
||||
if (rewards.guaranteed) {
|
||||
if (rewards.guaranteed.xp) {
|
||||
rewardData.xp += rewards.guaranteed.xp;
|
||||
}
|
||||
if (rewards.guaranteed.currency) {
|
||||
Object.assign(rewardData.currency, rewards.guaranteed.currency);
|
||||
}
|
||||
if (rewards.guaranteed.items) {
|
||||
rewardData.items.push(...rewards.guaranteed.items);
|
||||
}
|
||||
if (rewards.guaranteed.unlocks) {
|
||||
rewardData.unlocks.push(...rewards.guaranteed.unlocks);
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional rewards (based on secondary objectives)
|
||||
if (rewards.conditional) {
|
||||
rewards.conditional.forEach(conditional => {
|
||||
const objective = this.secondaryObjectives.find(obj => obj.id === conditional.objective_id);
|
||||
if (objective && objective.complete && conditional.reward) {
|
||||
if (conditional.reward.xp) {
|
||||
rewardData.xp += conditional.reward.xp;
|
||||
}
|
||||
if (conditional.reward.currency) {
|
||||
Object.assign(rewardData.currency, conditional.reward.currency);
|
||||
}
|
||||
if (conditional.reward.items) {
|
||||
rewardData.items.push(...conditional.reward.items);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Faction reputation
|
||||
if (rewards.faction_reputation) {
|
||||
Object.assign(rewardData.factionReputation, rewards.faction_reputation);
|
||||
}
|
||||
|
||||
// Dispatch reward event
|
||||
window.dispatchEvent(new CustomEvent('mission-rewards', {
|
||||
detail: rewardData
|
||||
}));
|
||||
|
||||
// Handle unlocks (store in localStorage)
|
||||
if (rewardData.unlocks.length > 0) {
|
||||
this.unlockClasses(rewardData.unlocks);
|
||||
}
|
||||
|
||||
console.log('Mission Rewards Distributed:', rewardData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks classes and stores them in localStorage.
|
||||
* @param {string[]} classIds - Array of class IDs to unlock
|
||||
*/
|
||||
unlockClasses(classIds) {
|
||||
const storageKey = 'aether_shards_unlocks';
|
||||
let unlocks = [];
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
unlocks = JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load unlocks from storage:', e);
|
||||
}
|
||||
|
||||
// Add new unlocks
|
||||
classIds.forEach(classId => {
|
||||
if (!unlocks.includes(classId)) {
|
||||
unlocks.push(classId);
|
||||
}
|
||||
});
|
||||
|
||||
// Save back to storage
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(unlocks));
|
||||
console.log('Unlocked classes:', classIds);
|
||||
} catch (e) {
|
||||
console.error('Failed to save unlocks to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the outro narrative if one exists.
|
||||
* @param {string} outroId - Narrative sequence ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async playOutro(outroId) {
|
||||
return new Promise(async (resolve) => {
|
||||
const narrativeFileName = this._mapNarrativeIdToFileName(outroId);
|
||||
|
||||
try {
|
||||
const response = await fetch(`assets/data/narrative/${narrativeFileName}.json`);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load outro narrative: ${narrativeFileName}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const narrativeData = await response.json();
|
||||
|
||||
const onEnd = () => {
|
||||
narrativeManager.removeEventListener('narrative-end', onEnd);
|
||||
resolve();
|
||||
};
|
||||
narrativeManager.addEventListener('narrative-end', onEnd);
|
||||
|
||||
console.log(`Playing Narrative Outro: ${outroId}`);
|
||||
narrativeManager.startSequence(narrativeData);
|
||||
} catch (error) {
|
||||
console.error(`Error loading outro narrative ${narrativeFileName}:`, error);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current turn count.
|
||||
* @param {number} turn - Current turn number
|
||||
*/
|
||||
updateTurn(turn) {
|
||||
this.currentTurn = turn;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue