aether-shards/src/managers/NarrativeManager.js

167 lines
4.7 KiB
JavaScript
Raw Normal View History

/**
* @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<string>} */
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();