/** * @typedef {import("./types.js").NarrativeSequence} NarrativeSequence * @typedef {import("./types.js").NarrativeNode} NarrativeNode */ /** * NarrativeManager.js * Manages the flow of story events, dialogue, and tutorials. * Extends EventTarget to broadcast UI updates to the DialogueOverlay. * @class */ export class NarrativeManager extends EventTarget { constructor() { super(); /** @type {NarrativeSequence | null} */ this.currentSequence = null; /** @type {NarrativeNode | null} */ this.currentNode = null; /** @type {Set} */ this.history = new Set(); // Track IDs of played sequences } /** * Loads and starts a narrative sequence. * @param {NarrativeSequence} sequenceData - The JSON object of the conversation (from assets/data/narrative/). */ startSequence(sequenceData) { if (!sequenceData || !sequenceData.nodes) { console.error("NarrativeManager: Invalid sequence data", sequenceData); return; } console.log(`NarrativeManager: Starting Sequence '${sequenceData.id}'`); this.currentSequence = sequenceData; this.history.add(sequenceData.id); // Find first node (usually index 0 or has explicit start flag, here we use index 0) this.currentNode = sequenceData.nodes[0]; // Process entry triggers for the first node this._processNodeTriggers(this.currentNode); this.broadcastUpdate(); } /** * Advances to the next node in the linear sequence. * If the current node has choices, this method should strictly be blocked by the UI, * but we include a check here for safety. */ next() { if (!this.currentNode) return; if (this.currentNode.type === "CHOICE") { console.warn( "NarrativeManager: Cannot call next() on a CHOICE node. User must select option." ); return; } const nextId = this.currentNode.next; // If the next node is "END", we still need to wait for user to click again // to actually close the dialogue. This gives them time to read the last message. if (!nextId || nextId === "END") { // User clicked on the last message - now close it this.endSequence(); return; } this._advanceToNode(nextId); } /** * Handles player choice selection from a branching node. * @param {number} choiceIndex - The index of the chosen option in the `choices` array. */ makeChoice(choiceIndex) { if (!this.currentNode || !this.currentNode.choices) return; const choice = this.currentNode.choices[choiceIndex]; if (!choice) return; // Process Choice-specific triggers (e.g., immediate reputation gain) if (choice.trigger) { this.dispatchEvent( new CustomEvent("narrative-trigger", { detail: { action: choice.trigger }, }) ); } this._advanceToNode(choice.next); } /** * Internal helper to handle transition logic. */ _advanceToNode(nextId) { if (!nextId || nextId === "END") { this.endSequence(); return; } const nextNode = this.currentSequence.nodes.find((n) => n.id === nextId); if (!nextNode) { console.error( `NarrativeManager: Node '${nextId}' not found in sequence.` ); this.endSequence(); return; } this.currentNode = nextNode; this._processNodeTriggers(this.currentNode); // If it's an ACTION node (invisible), execute trigger and auto-advance if (this.currentNode.type === "ACTION") { // Triggers are already processed above, just move to next // Use setTimeout to allow event loop to breathe if needed, or sync recursion this._advanceToNode(this.currentNode.next); } else { this.broadcastUpdate(); } } _processNodeTriggers(node) { if (node && node.trigger) { console.log("NarrativeManager: Dispatching Trigger", node.trigger); this.dispatchEvent( new CustomEvent("narrative-trigger", { detail: { action: node.trigger }, }) ); } } endSequence() { console.log("NarrativeManager: Sequence Ended"); // Only end if we actually have a sequence active if (!this.currentSequence) { console.warn("NarrativeManager: endSequence called but no active sequence"); return; } this.currentSequence = null; this.currentNode = null; this.dispatchEvent(new CustomEvent("narrative-end")); } /** * Sends the current node data to the UI (DialogueOverlay). */ broadcastUpdate() { this.dispatchEvent( new CustomEvent("narrative-update", { detail: { node: this.currentNode, active: !!this.currentNode, }, }) ); } } // Export singleton for global access export const narrativeManager = new NarrativeManager();