Story
The twelve chapters of **Hidden Love**, playable. The game itself.
game.story.Story
The twelve chapters of Hidden Love, playable. The game itself.
This component owns the phase machine, walks the chapter scripts in
game.story.Chapters, moves the three relationship meters on what the player
says, runs the phone, and saves and loads. Everything else in the game reads
what it publishes on game.Game.bus.
Mounted by the scene entrypoint onto the Story entity. It drives nothing on
screen and spawns nothing in the world — game.screens.Screens draws it and
game.cast.Cast poses for it — so calling a method is the whole interface,
which is why it can be driven from a UI callback, from a key, or from execute
while testing.
Driving it
local s = entity.find("Story").component.get("game.story.Story")
s:begin() -- New Game
s:advance() -- read the next line, taking the next chapter when this runs out
s:choose(1) -- take the first choice on a line that offers one
s:phoneOpen() -- read the phone; clears the unread dot
s:phoneReply(1) -- reply to it
s:save() / s:load() / s:hasSave()
s:report() -- the meters, warmth 0..1, the tier, how many choices were taken
From a screen, the same things are reachable by callback id, so the UI never
reaches for the component directly: story_new, story_continue,
story_advance, story_choice (data = index), story_phone, phone_reply
(data = index), story_save, story_subtitles.
What it publishes
"chapter" (n, chapter) · "place" (key) · "meter" (key, value) ·
"say" (speakerKey, bn, en) · "choice" (index, en) ·
"phone" (who, en) · "weather" ("dry" | "rain") · "beat" (key)
say carries both languages and they are not the same string twice: bn is
spoken (it goes to the sound slice, which makes a voice clip from it) and
en is shown. No font the engine ships has a Bengali glyph, so Bangla
reaches the player through the voice — never through the text on screen.
The rules it enforces
- The story is
Game.CHAPTERS, in order. Twelve chapters, always all twelve.advance()runs off the end of a chapter into the next one. - It cannot be lost. Whatever the meters read, the last chapter closes and
endingis set fromGame.tier(...). There is no fail state. - The meters move on what was said, never on repetition — the only writers
are choice
effectsand phonereplies, and every one of them is attached to a decision the player actually made. - Progress saves itself at every chapter boundary (
nextChaptercallssave()), with manual save and load beside it.
The save
Game.SAVE_PATH — /zero/source/local/hidden_love_save.json, nine key=value
lines. Hand-written rather than JSON so it has no dependency that could go
missing, and a save that fails to parse can never stop the game from loading.
Where it goes wrong
Three bugs were found here by testing rather than by reading, and each is worth knowing about because each was silent:
string.gmatch(payload, "([%a]+)=(.-)\n?")restores nothing..-is non-greedy and\n?is optional, so the capture matches the empty string every time and every key parses as no value at all — withhasSave()still reporting true. The value must be([^\n]*).- Replies read off
thread[#thread]do nothing in a thread whose closing line is hers and unanswered. It has to be the last message that offers replies. - A second
local M = {}in the chapters module silently shadows the first, soreturn Mhands back the phone threads and the twelve chapters are gone.
Scoped to this part · feeds back into the world's score.