aether-shards/src/managers/MissionManager.js

210 lines
7.2 KiB
JavaScript
Raw Normal View History

/**
* @typedef {import("./types.js").MissionDefinition} MissionDefinition
* @typedef {import("./types.js").MissionSaveData} MissionSaveData
* @typedef {import("./types.js").Objective} Objective
* @typedef {import("./types.js").GameEventData} GameEventData
*/
import tutorialMission from '../assets/data/missions/mission_tutorial_01.json' with { type: 'json' };
import { narrativeManager } from './NarrativeManager.js';
/**
* MissionManager.js
* Manages campaign progression, mission selection, narrative triggers, and objective tracking.
* @class
*/
export class MissionManager {
constructor() {
// Campaign State
/** @type {string | null} */
this.activeMissionId = null;
/** @type {Set<string>} */
this.completedMissions = new Set();
/** @type {Map<string, MissionDefinition>} */
this.missionRegistry = new Map();
// Active Run State
/** @type {MissionDefinition | null} */
this.currentMissionDef = null;
/** @type {Objective[]} */
this.currentObjectives = [];
// Register default missions
this.registerMission(tutorialMission);
}
/**
* Registers a mission definition.
* @param {MissionDefinition} missionDef - Mission definition to register
*/
registerMission(missionDef) {
this.missionRegistry.set(missionDef.id, missionDef);
}
// --- PERSISTENCE (Campaign) ---
/**
* Loads campaign save data.
* @param {MissionSaveData} saveData - Save data to load
*/
load(saveData) {
this.completedMissions = new Set(saveData.completedMissions || []);
// Default to Tutorial if history is empty
if (this.completedMissions.size === 0) {
this.activeMissionId = 'MISSION_TUTORIAL_01';
}
}
/**
* Saves campaign data.
* @returns {MissionSaveData} - Serialized campaign data
*/
save() {
return {
completedMissions: Array.from(this.completedMissions)
};
}
// --- MISSION SETUP & NARRATIVE ---
/**
* Gets the configuration for the currently selected mission.
* @returns {MissionDefinition | undefined} - Active mission definition
*/
getActiveMission() {
if (!this.activeMissionId) return this.missionRegistry.get('MISSION_TUTORIAL_01');
return this.missionRegistry.get(this.activeMissionId);
}
/**
* Prepares the manager for a new run.
* Resets objectives and prepares narrative hooks.
*/
setupActiveMission() {
const mission = this.getActiveMission();
this.currentMissionDef = mission;
// Hydrate objectives state
this.currentObjectives = mission.objectives.primary.map(obj => ({
...obj,
current: 0,
complete: false
}));
console.log(`Mission Setup: ${mission.config.title} - Objectives:`, this.currentObjectives);
}
/**
* Plays the intro narrative if one exists.
* Returns a Promise that resolves when the game should start.
*/
async playIntro() {
if (!this.currentMissionDef || !this.currentMissionDef.narrative || !this.currentMissionDef.narrative.intro_sequence) {
return Promise.resolve();
}
return new Promise(async (resolve) => {
const introId = this.currentMissionDef.narrative.intro_sequence;
// Map narrative ID to filename
// NARRATIVE_TUTORIAL_INTRO -> tutorial_intro.json
const narrativeFileName = this._mapNarrativeIdToFileName(introId);
try {
// Load the narrative JSON file
const response = await fetch(`assets/data/narrative/${narrativeFileName}.json`);
if (!response.ok) {
console.error(`Failed to load narrative: ${narrativeFileName}`);
resolve();
return;
}
const narrativeData = await response.json();
// Set up listener for narrative end
const onEnd = () => {
narrativeManager.removeEventListener('narrative-end', onEnd);
resolve();
};
narrativeManager.addEventListener('narrative-end', onEnd);
// Start the narrative sequence
console.log(`Playing Narrative Intro: ${introId}`);
narrativeManager.startSequence(narrativeData);
} catch (error) {
console.error(`Error loading narrative ${narrativeFileName}:`, error);
resolve(); // Resolve anyway to not block game start
}
});
}
/**
* Maps narrative sequence ID to filename.
* @param {string} narrativeId - The narrative ID from mission config
* @returns {string} The filename (without .json extension)
*/
_mapNarrativeIdToFileName(narrativeId) {
// Convert NARRATIVE_TUTORIAL_INTRO -> tutorial_intro
// Remove NARRATIVE_ prefix and convert to lowercase with underscores
const mapping = {
'NARRATIVE_TUTORIAL_INTRO': 'tutorial_intro',
'NARRATIVE_TUTORIAL_SUCCESS': 'tutorial_success'
};
return mapping[narrativeId] || narrativeId.toLowerCase().replace('NARRATIVE_', '');
}
// --- GAMEPLAY LOGIC (Objectives) ---
/**
* Called by GameLoop whenever a relevant event occurs.
* @param {string} type - 'ENEMY_DEATH', 'TURN_END', etc.
* @param {GameEventData} data - Context data
*/
onGameEvent(type, data) {
if (!this.currentObjectives.length) return;
let statusChanged = false;
this.currentObjectives.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++;
if (obj.target_count && obj.current >= obj.target_count) {
obj.complete = true;
statusChanged = true;
}
}
}
});
if (statusChanged) {
this.checkVictory();
}
}
checkVictory() {
const allPrimaryComplete = 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 }}));
}
}
completeActiveMission() {
if (this.activeMissionId) {
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';
}
}
}
}