/** * @file Story panel component displaying the main questline narrative. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable complexity -- Complex component with many conditional render paths */ import { STORY_CHAPTERS } from "@elysium/types"; import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { cdnImage } from "../../utils/cdn.js"; /** * Substitutes the character name placeholder in story text. * @param text - The story text with placeholders. * @param characterName - The player's character name. * @returns The text with placeholders replaced. */ const substituteCharacterName = ( text: string, characterName: string, ): string => { const fallback = characterName === "" ? "the guild leader" : characterName; return text.replaceAll("{characterName}", fallback); }; /** * Renders the story panel with chapter navigation and content. * @returns The JSX element. */ const StoryPanel = (): JSX.Element => { const { state, completeChapter } = useGame(); const [ activeChapterIndex, setActiveChapterIndex ] = useState(0); if (state === null) { return (

{"Loading…"}

); } const unlockedIds = state.story?.unlockedChapterIds ?? []; const completedChapters = state.story?.completedChapters ?? []; const { characterName } = state.player; const activeChapter = STORY_CHAPTERS[activeChapterIndex]; const isUnlocked = unlockedIds.includes(activeChapter?.id ?? ""); const completion = activeChapter === undefined ? null : completedChapters.find((completedChapter) => { return completedChapter.chapterId === activeChapter.id; }) ?? null; const isUnread = isUnlocked && completion === null; return (
{STORY_CHAPTERS.map((chapter, index) => { const unlocked = unlockedIds.includes(chapter.id); const completed = completedChapters.some((completedChapter) => { return completedChapter.chapterId === chapter.id; }); const unread = unlocked && !completed; function handleChapterSelect(): void { setActiveChapterIndex(index); } return ( ); })}
{activeChapter === undefined ? null :
{isUnlocked ? <> {activeChapter.title}

{"Chapter "} {activeChapterIndex + 1} {": "} {activeChapter.title}

{substituteCharacterName(activeChapter.content, characterName). split("\n\n"). map((paragraph, paraIndex) => { // eslint-disable-next-line react/no-array-index-key -- Static content paragraphs have no stable id return

{paragraph}

; })}
{completion === null && isUnread ?

{"What do you do?"}

{activeChapter.choices.map((storyChoice) => { const chapterForClosure = activeChapter; function handleChoice(): void { completeChapter(chapterForClosure.id, storyChoice.id); } return ( ); })}
: null} {completion === null ? null :

{"Your choice:"}{" "} { activeChapter.choices.find((storyChoice) => { return storyChoice.id === completion.choiceId; })?.label }

{substituteCharacterName( activeChapter.choices.find((storyChoice) => { return storyChoice.id === completion.choiceId; })?.outcome ?? "", characterName, )}

} :

{"Chapter "} {activeChapterIndex + 1}

{"đź”’ This chapter has not yet been unlocked."}

}
}
); }; export { StoryPanel };