Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 3a9e3328-0…

Story

The twelve chapters of **Hidden Love**, playable. The game itself.

by◐lauramarka·posted 14d ago
What it does

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 ending is set from Game.tier(...). There is no fail state.
  • The meters move on what was said, never on repetition — the only writers are choice effects and phone replies, and every one of them is attached to a decision the player actually made.
  • Progress saves itself at every chapter boundary (nextChapter calls save()), 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 — with hasSave() 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, so return M hands back the phone threads and the twelve chapters are gone.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

game.story.Story — the twelve chapters, 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 reads what this publishes on `game.Game.bus`. IT DRIVES NOTHING ON SCREEN AND SPAWNS NOTHING IN THE WORLD. It publishes state and answers calls; `game.screens.Screens` draws it and `game.cast.Cast` poses for it. 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. local s = entity.find("Story").component.get("game.story.Story") s:begin() -- New Game s:advance() -- next line s:choose(1) -- take the first choice on the line s:phoneOpen() / s:phoneReply(1) s:save() / s:load() The bus events it emits are the shared vocabulary in `design/spec.md`: "chapter" (n, chapter) · "place" (key) · "meter" (key, value) "say" (speakerKey, bn, en) · "choice" (id, en) · "phone" (thread, message) "weather" ("dry" | "rain") · "beat" (key)

currentScript( ) → void

emitSay( ) → void

emitChoice( ) → void

announce( ) → void

present( ) → void

Load the line `public.line` points at and put it on the bus.

clampMeter(v: ?) → void

argtypedescription
v?

toMeters(effects: ?) → void

Move the meters a choice declares, clamping each, and announce every one that moved. `Game.addMeter` is the rule for a single meter on a state table; this is the same rule applied to this component's live fields, so a choice cannot push a meter past its bounds.

argtypedescription
effects?

warmthState( ) → void

awake( ) → void

start( ) → void

onDestroy( ) → void

onCallback(callbackId: ?, data: ?) → void

UI callbacks. The screens slice drives the story through these ids, so it never has to reach for the component directly.

argtypedescription
callbackId?
data?

begin( ) → number

Start a new game at chapter 1 with every meter at its starting value.

examples

entity.find("Story").component.get("game.story.Story"):begin()

advance( ) → number

Read the next line, taking the next chapter when this one runs out.

examples

story:advance()

choose(index: number) → number

Take the choice at `index` on the current line, moving the meters.

argtypedescription
indexnumberWhich choice, 1-based, as emitted on the bus.

examples

story:choose(1)

nextChapter( ) → number

Close this chapter and open the next, or end the game on chapter 12.

examples

story:nextChapter()

report( ) →

The three meters as they stand, and the warmth they add up to.

examples

story:report()

phoneOpen( ) → number

Open the phone. Reading it clears the unread dot.

examples

story:phoneOpen()

phoneReply(index: number) → number

Reply to the message the phone is showing. A reply changes the next line, which is the point of the phone existing at all.

argtypedescription
indexnumberWhich reply, 1-based.

examples

story:phoneReply(1)

currentLine( ) →

The line being read right now: its speaker, its English line, and the choices it offers. A screen calls this to repaint at any time, rather than depending on having been subscribed at the exact moment the line was presented — which is what a reload takes away.

examples

story:currentLine()

chapterKey( ) → string

The chapter key the walker is in, e.g. "rain" for chapter 6.

examples

story:chapterKey()

currentChapterKey( ) → string

save( ) → boolean

Write the run to Game.SAVE_PATH. Called automatically at every chapter.

examples

story:save()

load( ) → boolean

Restore the run from Game.SAVE_PATH and resume at the saved chapter.

examples

story:load()

hasSave( ) → boolean

Whether a save exists to continue from.

examples

story:hasSave()

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

11items
▣
module · born here
❒asset
# Chapters <!-- zero:scaffolded-readme --> <!-- Replace the line below with what this module provides and who consumes it, then delete both comment lines. --> TODO: describe this module.
▲ 0↑ born
▣
module · born here
❒asset
# game.Game The spine of **Hidden Love — A Bangladeshi Romance Story**. One shared module holding the constants and the rules, so no two slices have to agree by convention. ```lua local Game = require("game.Game") Game.bus.on("chapter", function(n) print(n) end) ``` Nothing here touches the engine at require time — it is data and pure functions, safe to require from a scene entrypoint, a component, a module or an `execute` chunk. ## What it holds | Field | What it is | |---|---| | `ART` | The art-direction sentence, verbatim from the reference pass. Every generated prompt reuses it word for word. | | `TITLE` / `SUBTITLE` / `ENDING_TITLE` | The title card and the closing card. | | `CAST` | The seven named people: who they are, what they wear, and the reference image for the two leads. | | `CHAPTERS` | The twelve chapters in order — `place`, `who`, `beat`, `objective`. The storyline is this table. | | `PLACES` | The seven sets the world slice builds, each with the reference image it answers to. | | `METERS` / `TIERS` | friendship, trust, closeness — and the three ways the same ending can land. | | `MOUNTS` | The names of the scene's mount entities a slice attaches to. | | `SLICES` | The five slice modules the entrypoint requires and mounts. | | `bus` | `on` / `off` / `emit`. How slices talk without requiring each other. | ## The rule the whole game runs on Every chapter reaches the end. The meters decide how *warm* the telling is, not whether it finishes — `warmth(state)` is the mean of the three meters as 0..1, and `tier(state)` names what that earns at the finale. ## State and saving `Game.newState()` is the whole save: chapter, the three meters, the choices taken, flags, the phone log, and time in chapter. `Game.SAVE_PATH` is where it is written, under `/zero/source/local/`. `Game.addMeter(state, key, amount)` clamps and announces on the bus; move the relationship through it and never by assignment, so everything watching stays in step. ## Mounting The scene lays down one entity per key in `Game.MOUNTS` and the entrypoint hands them to each slice's `mount(refs)`. A slice that is not present yet is not an error — the scene loads without it.
▲ 0↑ born
backing path · game/story/Story.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.