Skip to content

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.”

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
StoryOrbit reader teaching demonstration: 01 · Just waiting out the rain, Lin Che · affinity 0
Rain is reaching the books by the window. You can help, or carry on browsing.

02 · A little more trust

Lin Che · affinity 2
StoryOrbit reader teaching demonstration: 02 · A little more trust, Lin Che · affinity 2
The books are safe; a memory has been heard. Lin Che offers an invitation. Whether to stay is your choice.

03 · The lamp after closing

The letters scene is unlocked
StoryOrbit reader teaching demonstration: 03 · The lamp after closing, The letters scene is unlocked
After accepting, you open the tin together. Only now can the writer receive the first letter’s story.

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.

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.

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.

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.

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.

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.

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.

The download includes a structural checker for Python 3.11+. Run these commands in the unzipped folder:

Terminal window
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python check.py pack.json

Windows 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.