Give him a reason to ask you to stay
The rain is getting worse. You duck into a secondhand bookshop just before closing. Lin Che makes room for a chair and goes back to repairing a book.
You move a stack of books away from the leaking window. Later, you listen as he tells you about the first rainy night he spent here. When he turns the sign on the door, he glances back.
“I’ve still got a box of old letters to sort. If you’re not in a hurry… would you keep me company?”
We want that invitation to appear because of the time you’ve spent together. It isn’t an option waiting at the front door, or a vague request for the model to “increase affection.”
What the reader experiences
Section titled “What the reader experiences”Teaching demonstration: the actual StoryOrbit reader UI, with authored prose and scripted observation answers. State is computed by the real runtime from the download’s initial values. No model was called; these images do not establish model interpretation or generation quality. Select an image to enlarge it.
01 · Just waiting out the rain
Lin Che · affinity 0
02 · A little more trust
Lin Che · affinity 2
03 · The lamp after closing
The letters scene is unlocked
Opening the letters doesn’t force a romance or end the story. Lin Che has simply let the reader a little closer. What happens next is still theirs to decide.
Begin with something worth responding to
Section titled “Begin with something worth responding to”Unzip the download and open pack.json. Start with entries[0].opening; you don’t need to read every field yet.
Three paragraphs do the work: the reader arrives, Lin Che moves a chair, and rain starts reaching a stack of books. A cloth is within reach, but his hands are full. The reader has a reason to act.
Make the last paragraph yours. Is the top book a poetry collection with a train ticket tucked inside? A worn atlas? Keep the wet books and the opportunity to help, and the rules below will still fit.
Let him remember two small things
Section titled “Let him remember two small things”For this short story, affinity only needs three levels: 0 → 1 → 2. runtime.state declares the values the story can remember. defaults makes every new visit start with strangers:
{
"affinity": {
"type": "integer",
"minimum": 0,
"maximum": 2
},
"defaults": {
"affinity": 0,
"helped": false,
"listened": false,
"invited": false,
"letters_open": false
}
}
This excerpt shows the affinity definition and initial values. The complete file also declares four boolean fields:
helped: the reader has saved the wet books.listened: the reader has heard and responded to Lin Che’s memory.invited: Lin Che has actually offered the invitation.letters_open: the reader has accepted; the letters scene is available.
“I’ll help” doesn’t mean the books are dry. Questions in observations identify what happened during the turn; rules then update state from those answers. The saved_books question asks about a completed action, excluding plans, quotations and memories.
Here is the rule that rewards helping:
{
"kind": "update",
"id": "remember_helped",
"when": {
"op": "all",
"conditions": [
{
"op": "eq",
"path": "/helped",
"value": false
},
{
"op": "observed",
"id": "saved_books",
"value": true
}
]
},
"effects": [
{
"op": "set",
"path": "/helped",
"value": true
},
{
"op": "increment",
"path": "/affinity",
"by": 1,
"limit": 2
}
]
}
Read it as a sentence: if this hasn’t been counted before, and the reader really saved the books this turn, remember helped and add one point. Saving the same books again won’t add another. Listening to the memory follows the same pattern.
A model still interprets the prose, so try several ways of expressing an action. The rule prevents repeat rewards; it cannot guarantee that every ambiguous sentence will be understood as you intended.
Enough affinity earns an invitation
Section titled “Enough affinity earns an invitation”Affinity 2 doesn’t answer on the reader’s behalf. offer_invitation asks Lin Che to offer, then wait:
{
"kind": "guidance",
"id": "offer_invitation",
"when": {
"op": "all",
"conditions": [
{
"op": "gte",
"path": "/affinity",
"value": 2
},
{
"op": "eq",
"path": "/invited",
"value": false
}
]
},
"instruction": "Lin Che now trusts the reader. Have him invite them to stay after closing and sort a box of old letters with him. Make the invitation explicit in dialogue, then wait for their answer. Do not open the box or assume acceptance.",
"completion": {
"op": "observed",
"id": "offered_invitation",
"value": true
}
}
Its completion checks whether an invitation actually appeared. remember_invitation records that fact. Only after an invitation has been recorded and the reader explicitly accepts can unlock_letters set letters_open to true.
Conditions read the state at the beginning of a turn. If listening raises affinity to 2 this turn, the invitation can appear on a following turn. Likewise, the letter’s content becomes available on a turn after acceptance. Those small pauses give both characters room to respond.
Put the secret behind the door
Section titled “Put the secret behind the door”Don’t put the first letter’s contents in the global instructions. The model would already know them while the scene was locked.
The download keeps that scene in content, behind a condition:
{
"id": "after_hours_letters",
"selection": "always",
"when": {
"op": "eq",
"path": "/letters_open",
"value": true
}
}
The same block’s text describes the tin, the first envelope and the words Lin Che never sent. selection: "always" means this material is always supplied after when passes. It doesn’t bypass the gate or paste fixed dialogue onto the page.
This gate controls the writer’s context. In a playthrough, also watch for the model inventing similar details early, or making the invitation feel abrupt.
Make it your story
Section titled “Make it your story”Change the people and place first. Keep two specific acts of care, an invitation and a hidden scene. Perhaps the reader helps repair an instrument, hears about a disappointing tour, and earns an invitation to hear an unreleased song after rehearsal.
Then try three routes:
| What you do | What should happen |
|---|---|
| Ask to read the letters as soon as you arrive | The private letter block stays out of the writer’s context |
| Keep saving books without hearing the memory | Affinity stays at 1; the same action cannot be farmed |
| Save the books, hear the memory, then accept the invitation | Affinity reaches 2; after acceptance, the letters scene becomes available |
If you raise the threshold to 3, add a third way to earn affinity and update both the state’s maximum and each rule’s limit. Changing the threshold alone would leave a door that can never open.
Which pack version do you need?
Section titled “Which pack version do you need?”This complete example works in 0.3. Affinity, one-time events and conditional story content don’t require a visual-novel protocol.
| What you want to add | Version and next step |
|---|---|
| A keepsake for the first invitation | 0.4 achievements and badges. Try the achievements and endings lab. An achievement need not end the story. |
| The shop after dark, with Lin Che’s sprite on stage | 0.5 visual-novel presentation. Try the stage lab. The same state rules still govern affinity. |
| An existing 0.2 work | Start with the migration lab, then map its character variables and branches onto state and conditional content. |
Consult the current 0.5 reference for performance asset fields.
Check your edited file
Section titled “Check your edited file”The download includes a structural checker for Python 3.11+. Run these commands in the unzipped folder:
python3 -m venv .venv.venv/bin/python -m pip install -r requirements.txt.venv/bin/python check.py pack.jsonWindows PowerShell commands are in the included README.md. After Structural check passed, try the three routes in a compatible reader. A valid file and a convincing story are both worth checking.
To look up a field, expand runtime or content in the JSON tree and follow the structure of your example. When you’re ready to share your work, continue to private trials and publication.