144 lines
3.9 KiB
JavaScript
144 lines
3.9 KiB
JavaScript
|
|
/**
|
||
|
|
* NarrativeManager.js
|
||
|
|
* Manages the flow of story events, dialogue, and tutorials.
|
||
|
|
* Extends EventTarget to broadcast UI updates to the DialogueOverlay.
|
||
|
|
*/
|
||
|
|
export class NarrativeManager extends EventTarget {
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
this.currentSequence = null;
|
||
|
|
this.currentNode = null;
|
||
|
|
this.history = new Set(); // Track IDs of played sequences
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Loads and starts a narrative sequence.
|
||
|
|
* @param {Object} 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;
|
||
|
|
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");
|
||
|
|
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();
|