From 65772870c6104b51c9b43c18dd2af32b3a67163a Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 09:08:26 +0200 Subject: [PATCH] Add NPC dialogues panel component --- src/app/NpcDialoguesPanel.tsx | 302 ++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 src/app/NpcDialoguesPanel.tsx diff --git a/src/app/NpcDialoguesPanel.tsx b/src/app/NpcDialoguesPanel.tsx new file mode 100644 index 00000000..3ed05e2f --- /dev/null +++ b/src/app/NpcDialoguesPanel.tsx @@ -0,0 +1,302 @@ +import { useEffect, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; + +import { + type ProjectDialogue, + createProjectDialogue, + createProjectDialogueLine +} from "../dialogues/project-dialogues"; + +interface NpcDialoguesPanelProps { + dialogues: ProjectDialogue[]; + defaultDialogueId: string | null; + selectedDialogueId: string | null; + onSelectDialogue(dialogueId: string | null): void; + onSetDefaultDialogueId(dialogueId: string | null): void; + onAddDialogue(): void; + onDeleteDialogue(dialogueId: string): void; + onSetDialogueTitle(dialogueId: string, title: string): void; + onAddDialogueLine(dialogueId: string): void; + onDeleteDialogueLine(dialogueId: string, lineId: string): void; + onSetDialogueLineSpeaker( + dialogueId: string, + lineId: string, + speakerName: string | null + ): void; + onSetDialogueLineText(dialogueId: string, lineId: string, text: string): void; +} + +function commitOnEnter( + event: ReactKeyboardEvent, + commit: () => void +) { + if (event.key !== "Enter") { + return; + } + + event.currentTarget.blur(); + commit(); +} + +export function NpcDialoguesPanel({ + dialogues, + defaultDialogueId, + selectedDialogueId, + onSelectDialogue, + onSetDefaultDialogueId, + onAddDialogue, + onDeleteDialogue, + onSetDialogueTitle, + onAddDialogueLine, + onDeleteDialogueLine, + onSetDialogueLineSpeaker, + onSetDialogueLineText +}: NpcDialoguesPanelProps) { + const selectedDialogue = + selectedDialogueId === null + ? null + : dialogues.find((dialogue) => dialogue.id === selectedDialogueId) ?? null; + const resolvedDefaultDialogueId = + defaultDialogueId !== null && + dialogues.some((dialogue) => dialogue.id === defaultDialogueId) + ? defaultDialogueId + : null; + const defaultDialogue = + resolvedDefaultDialogueId === null + ? null + : dialogues.find((dialogue) => dialogue.id === resolvedDefaultDialogueId) ?? null; + const [titleDraft, setTitleDraft] = useState(selectedDialogue?.title ?? ""); + const [lineDrafts, setLineDrafts] = useState< + Record + >({}); + + useEffect(() => { + setTitleDraft(selectedDialogue?.title ?? ""); + setLineDrafts( + selectedDialogue === null + ? {} + : Object.fromEntries( + selectedDialogue.lines.map((line) => [ + line.id, + { + speakerName: line.speakerName ?? "", + text: line.text + } + ]) + ) + ); + }, [selectedDialogueId, selectedDialogue]); + + const commitTitle = () => { + if (selectedDialogue === null) { + return; + } + + onSetDialogueTitle(selectedDialogue.id, titleDraft); + }; + + const getLineDraft = (dialogue: ProjectDialogue, lineId: string) => + lineDrafts[lineId] ?? + (() => { + const line = dialogue.lines.find((candidate) => candidate.id === lineId); + return { + speakerName: line?.speakerName ?? "", + text: line?.text ?? "" + }; + })(); + + return ( +
+
+
+ {defaultDialogue?.title ?? + (dialogues.length === 0 ? "No Dialogues" : "No Default Dialogue")} +
+
+ {dialogues.length === 0 + ? "Author one or more dialogues for this NPC. Sequences can later make this NPC talk, and direct NPC clicks use the default dialogue." + : defaultDialogue === null + ? "Pick a default dialogue for direct NPC click/talk behavior." + : `${defaultDialogue.lines.length} line${defaultDialogue.lines.length === 1 ? "" : "s"} in the default NPC dialogue.`} +
+
+ + + +
Dialogues
+ {dialogues.length === 0 ? ( +
No NPC dialogues authored yet.
+ ) : ( +
+ {dialogues.map((dialogue) => ( +
+
+ + +
+
+ ))} +
+ )} + +
+ +
+ + {selectedDialogue === null ? ( +
+ Select a dialogue to edit its title and lines. +
+ ) : ( +
+ + +
Lines
+
+ {selectedDialogue.lines.map((line, index) => { + const draft = getLineDraft(selectedDialogue, line.id); + + return ( +
+
+
{`Line ${index + 1}`}
+ +
+ +