2025-12-22 04:40:48 +00:00
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
export class MissionManager {
|
|
|
|
|
constructor() {
|
|
|
|
|
// Campaign State
|
|
|
|
|
this.activeMissionId = null;
|
|
|
|
|
this.completedMissions = new Set();
|
|
|
|
|
this.missionRegistry = new Map();
|
|
|
|
|
|
|
|
|
|
// Active Run State
|
|
|
|
|
this.currentMissionDef = null;
|
|
|
|
|
this.currentObjectives = [];
|
|
|
|
|
|
|
|
|
|
// Register default missions
|
|
|
|
|
this.registerMission(tutorialMission);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
registerMission(missionDef) {
|
|
|
|
|
this.missionRegistry.set(missionDef.id, missionDef);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- PERSISTENCE (Campaign) ---
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
save() {
|
|
|
|
|
return {
|
|
|
|
|
completedMissions: Array.from(this.completedMissions)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- MISSION SETUP & NARRATIVE ---
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the configuration for the currently selected mission.
|
|
|
|
|
*/
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
return new Promise(async (resolve) => {
|
2025-12-22 04:40:48 +00:00
|
|
|
const introId = this.currentMissionDef.narrative.intro_sequence;
|
|
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
// Map narrative ID to filename
|
|
|
|
|
// NARRATIVE_TUTORIAL_INTRO -> tutorial_intro.json
|
|
|
|
|
const narrativeFileName = this._mapNarrativeIdToFileName(introId);
|
2025-12-22 04:40:48 +00:00
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
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
|
|
|
|
|
}
|
2025-12-22 04:40:48 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 05:20:33 +00:00
|
|
|
/**
|
|
|
|
|
* 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_', '');
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 04:40:48 +00:00
|
|
|
// --- GAMEPLAY LOGIC (Objectives) ---
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Called by GameLoop whenever a relevant event occurs.
|
|
|
|
|
* @param {string} type - 'ENEMY_DEATH', 'TURN_END', etc.
|
|
|
|
|
* @param {Object} 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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|