The Beach Mystery treats its story as a directed graph of node objects defined entirely inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/apursley2012/the-beach-mystery/llms.txt
Use this file to discover all available pages before exploring further.
data/story.js. Rather than encoding narrative branches as nested if/else chains inside components, every scene, dialogue beat, and decision point is expressed as a plain JavaScript object with a predictable shape. The rendering engine reads the current node, switches on its type, and delegates to the appropriate UI — it never needs to know the story’s content. This clean separation means adding a new branch is as simple as adding a new object to STORY_NODES and pointing an existing next or options[n].next field at its id.
Why a Node Graph?
Content is decoupled from logic
All story text, choices, and state mutations live in
data/story.js. The StoryPlayer component reads the current node ID from gameState.currentNode and renders whatever it finds — it never hardcodes a scene name or dialogue line.Branches are just pointers
Every narration node has a
next field that names the following node ID. Every choice node has an options array where each option names its own next ID. Adding a branch is adding an object and updating a pointer.State mutations are declarative
A node can set inventory flags by including a
setState object (e.g. { hasJewel: true }). The engine merges these flags when the node is entered. No imperative mutation code is needed in components.Endings are declared, not detected
A narration node that concludes a storyline carries an
ending field with the ending ID. The engine calls reachEnding(endingId) automatically when it renders that node — no special casing required.Node Types
There are three node types. Thetype field controls which UI the player sees and which hook function advances the story.
narration — text lines with auto-advance
narration — text lines with auto-advance
A
narration node presents one or more lines of story text. Lines are revealed one at a time; the player clicks Continue → after each line. When the last line is shown, clicking Continue calls advanceNode(), which transitions currentNode to the value of next.Narration nodes may also carry:setState— an object of inventory flags to merge intogameState.stateon entry.ending— an ending ID string. When rendered, the engine callsreachEnding(endingId)and records the achievement.
choice — branching decision buttons
choice — branching decision buttons
riddle — free-text answer with hint
riddle — free-text answer with hint
A
riddle node shows riddleText and a free-text input. The player types an answer and submits. If the answer (case-insensitive) matches the answer field, solveRiddle() is called, which records { nodeId, choice: 'echo' } in history and advances to node.next.If the player submits two wrong answers, the hint string appears beneath the input. The single riddle in the game is:I speak without a mouth and hear without ears. I have no body, but I come alive with the wind. What am I? Answer: echo
Node Interface
Concrete Examples
chestChoice — a choice node
chestOpened. Choosing No navigates to chestSkipped. Neither option has a setState on the choice node itself — state is applied by the destination node on arrival.
chestOpened — a narration node with setState
chestOpened is entered (via advanceNode() after the chestChoice “Yes” path), the engine merges { hasOpenedChest: true, hasJewel: true, hasMap: true } into gameState.state. This inventory state is then used further down the graph to determine which version of cave nodes to serve.
Text Interpolation
Story lines use{placeholder} tokens that are replaced at render time with values from gameState.variables. Variables are set when the player completes the five-question personalisation form and passed into startGame(variables). A formatted_time variable is also injected automatically — it captures the current HH:MM time at game start.
The full set of interpolation tokens:
| Token | Source | Example |
|---|---|---|
{firstName} | Personalisation Q1 | "Hello, Alex!" |
{cartoonChar} | Personalisation Q4 | "You glance at your Goofy watch." |
{formatted_time} | Auto-injected at game start | "It is 14:32." |
{favoriteColor} | Personalisation Q2 | "a brilliant blue jewel" |
{favoriteJewel} | Personalisation Q5 | "you pocket the sapphire" |
{favoriteAnimal} | Personalisation Q3 | "a beautiful fox sleeps" |
Tokens are replaced using a simple string
replace chain at render time, not during data loading. The raw {placeholder} strings are stored in story.js exactly as shown above.Complete Node Graph
The full decision tree, showing every node and the transitions between them:Endings reachable from each root decision
| First Choice | Second Choice | Third Choice | Ending |
|---|---|---|---|
| Open Chest → Yes | Follow Map → Yes | Solve Riddle | 🏝️ The Island’s Secret |
| Open Chest → Yes | Follow Map → No | Enter Cave → Yes, Left | 💎 The Hidden Treasure Chamber |
| Open Chest → Yes | Follow Map → No | Enter Cave → Yes, Right | 🐾 The Guardian’s Den |
| Open Chest → Yes | Follow Map → No | Enter Cave → No | 🌅 The Peaceful Shore |
| Open Chest → No | Enter Cave → Yes, Left | — | 💎 The Hidden Treasure Chamber |
| Open Chest → No | Enter Cave → Yes, Right | — | 🐾 The Guardian’s Den |
| Open Chest → No | Enter Cave → No | — | 🌅 The Peaceful Shore |
To add a new story branch, add a new object to
STORY_NODES in data/story.js and point an existing next or options[n].next field at its id. No changes to any component or hook are required. See Adding Story Nodes for a step-by-step walkthrough.