Skip to content

S7: The clinic - #382

Merged
jamescrosswell merged 10 commits into
mainfrom
task/354-clinic
Oct 22, 2026
Merged

jamescrosswell merged 10 commits into
mainfrom
task/354-clinic

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

The clinic in the metro's depot, run by a tunnel doctor who buys a kidney for the player's first money and fits the three bionics, each for money and the part it replaces. Built on the contract in #381, which this branch includes until that merges.

Closes #354

What it does

  • The doctor, Annelie Joubert, a struck-off Apex surgeon, is content/npcs/tunnels/annelie.json: an Npc with values, a persona and a voice. load_npcs reads only content/npcs/*.json, so she's no resident. She isn't in Behaviour, the economy's residents, gossip, reflection or the incident. simile.clinic puts her in every snapshot at the clinic's clinic_doctor spot, by wrapping hub.snapshot, so the client draws and rings her like anyone else.

  • What she trades (Clinic) goes on the clinic's player_state buyer as parts and fittings, through a small keepers_of(app) hook in simile.player's Trade.

    • A kidney: she buys one for 100, never the second, as the lead decided. It's paid from OUTSIDE through E3's ledger, and the part is sold.
    • A fitting: it takes the price to OUTSIDE and puts the bionic in place of the part: the legs in the legs, the datajack in the vertebra, the eye in either eye.
    • She won't fit what they can't afford, so Economy.pay never raises.
    • Both only work in the clinic.
  • Prices and stock are sim facts in ClinicConfig. She has one of each bionic, so what's in stock is the stock less what's fitted, and nothing more is saved.

  • Talking to her (DoctorTalk) goes through L1, at the player's priority, like a resident's reply. It runs outside B2's engine, which only knows residents, so nothing in conversation/ changed.

    • She greets the player as they walk in, at most every 2 sim hours, so the client plays her greetings.
    • Her system prompt is who she is and the fixed price list, with no haggling. Each turn adds what she can see of them (their bionics, a sold kidney) and what she'd buy or fit now.
    • It ends when they walk out, or after 60 sim seconds of silence.
  • The clinic panel (client/src/ui/clinic-panel.ts). While she'd buy or fit something, the corner says "Press E to see what the Clinic buys and fits", and E opens the panel. It lists:

    • talking to her, while she's ringed, which closes the panel and opens the chat to her;
    • the salvage the clinic buys;
    • a kidney;
    • each fitting in each part it can take, dimmed when they can't afford it, saying what it costs against what they have.

    ↑ ↓ and Enter or a click, and Esc or E closes it. Selling a part or a fitting takes a second Enter, since neither can be undone.

  • How E chooses: E at the clinic opens the panel, and the panel's first row talks. Enter still talks to the ringed resident, as everywhere. Once she'd buy and fit nothing more, E there sells salvage as at the stall, and with nothing to sell it talks to her. The stall is unchanged. As S13 has it, one press does one thing: the clinic is in reach where the buyer would be, at no distance, and E takes the nearest thing in reach, a pile first on a tie.

Pricing for S15's loop

S6's economy: about 50 a round of the metro's salvage (32 without the legs, since the servo motor is on the high ledge), back every sim day, and 65 in secrets once. Meals are 5 at the café and 10 at the noodle bar, and free at home; the sums below count two café meals a day.

Step In Out Left
Sell a kidney 100 100
Fit the legs 80 20
Day 1: the secrets (65) and a round with the legs (50), two meals 115 10 125
Day 2: a round, two meals 50 10 165
Fit the datajack 150 15
Days 3–7: a round a day, less two meals 40 a day 215 by day 7
Fit the eye 200 15

So the kidney buys the legs with a little over, the datajack takes two days of rounds, and the eye takes about four and a half days of the metro alone. S9's sewers, behind the datajack, should bring that down to two or three. test_the_prices_carry_s15s_loop checks these sums against the configs.

Please look at

  • The doctor's figure, whole-FLUX as Side view: residents as whole FLUX characters, clean style (approach 1) #337 does it: one painting from text, every pose an edit. It passed every check first time and took 422 s of FLUX at 8-bit, about 8 minutes in all. Appearance has no lab coat or scrubs, so she's in a pale blue t-shirt with a leather apron and glasses. She reads more as a kindly grandmother than a back-alley surgeon, and only a new accessory or a free-text look would change that.
    the painting
    every frame
  • The clinic in the real client, against the real server on a copy of the save:
    the clinic up close
    the panel
    selling a kidney asks twice
    after the legs
  • Her voice. VoiceDesign (voice-lab's design_npcs.py, about 2 minutes) made it, but every one of its six attempts sounds American to the accent classifier, not South African. That's the same limit that led to every resident but Mirela getting a recorded voice. Her four greetings (simile greetings annelie, 35 s) are in that voice. A recording through import_voices.py, then simile greetings annelie --force, would fix it.
  • What she says, checked with three Haiku calls through L1:

    (greeting) You're whole. What needs fixing?
    I need those legs, but I've only got fifty. Do me a deal? — No deals. Hydraulic legs are eighty. You've got fifty, so you come back when you've got thirty more.
    What does the eye do, and what would it cost me? — Infrared eye is two hundred. Shows the heat in a face when someone lies to you.

Notes

  • Residents' days are unchanged. I ran test_simulate's 24-hour day (from 08:00, seed 0, talk 3) on main (a0918e2, with S4) and on this branch. Both give 75,577 EntityMoved events with the same digest, and the same summary.
  • Saves still load. I loaded a copy of simile.db in a temp dir with the whole server and SIMILE_LLM=fake. The save predates the player, so it loaded the starting player, and the 11 residents plus the doctor were in the snapshot. In the clinic, buyer had the kidney and the three fittings. I sold the kidney (100) and fitted the legs (−80). The save was written with the body, 20 money and both ledger entries, and reloaded with them.
  • Ledger kinds. A kidney is booked as salvage from OUTSIDE and a fitting as a purchase to OUTSIDE, both at clinic. Their own kinds (organ, fitting) would be two lines in economy/ledger.py, outside this task's Owns.
  • B2 logs "Ignoring the player saying …" for each line to her, since its player_say handler doesn't know her. Her own handler answers.
  • Outside the Owns, as the lead extended them:
    • server/simile/player/ (keepers_of and a keepers argument to Trade), and the client's reach and hint code (world/reach.ts, a new world/buyers.ts, ui/salvage.ts, ui/dialogue-scene.ts, ui/help.ts).
    • Registering her where figures and greetings are loaded: dolls/build.py's people() and personas(), which also mark her manifest "resident": false, voicing/cli.py, tests/voicing/test_greetings.py, voice-lab/npcs.py (which now also reads content/npcs/tunnels/), and content/voices/greetings/lines.json and made.json.
    • Her voice's own files, content/voices/annelie.wav and .txt, and her seed in content/voices/seeds.json.
    • A line each in the player, world, UI and dolls READMEs.
    • The client's dollFor lent anyone without a figure one of the residents', and that list would have taken in hers. It now skips any manifest with "resident": false, so a stranger never wears the doctor (dolls/manifest.ts, world/assets.ts, and their tests).
    • tests/npc/test_cli.py, which read every entry in content/npcs/ as a file. It now reads every file under it, the tunnels' folder included.
  • simile voice measure doesn't know her either, so her lines are paced at the default rate.

Follow-ups

  • Ledger kinds of their own for organs and fittings, so D1's report doesn't count a kidney as salvage.
  • A recorded voice for her, in a South African accent.
  • The top-level README's controls still predate S6's selling and this panel.

🤖 Generated with Claude Code

jamescrosswell and others added 10 commits October 23, 2026 07:19
Adds what S7 needs for the clinic's doctor to buy a kidney and fit the
bionics: `sell_part` sells a part of the player's body by its `Body` field,
`fit` asks for a bionic in place of a part, and `player_state`'s `buyer`
gains `parts`, what it pays for parts of the body, and `fittings`, the
bionics it fits with their prices and the parts each can take. `BodyPart`
names the six `Body` fields. The new fields are left out of dumps while
empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/src/net/protocol.ts
#	docs/protocol.md
#	server/simile/protocol/messages.py
#	server/simile/protocol/protocol.schema.json
#	server/tests/protocol/test_messages.py
…s by BodyPart

A Fitting rejects a part its bionic can't replace or a part named twice,
a Buyer offers each bionic once, and Buyer.parts is keyed by BodyPart, so
the schema gains propertyNames. The clinic's doctor buys one kidney, never
the second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tunnel doctor, Annelie Joubert, runs the clinic in the metro's depot:
an Npc with values, a whole-FLUX figure and a voice for her greetings,
in content/npcs/tunnels/ so she's never a resident. She buys one kidney
for 100 and fits the hydraulic legs (80), the datajack (150) and the
infrared eye (200), each for money and the part it replaces, through E3's
ledger. Prices and stock are in ClinicConfig, and she only voices them:
her talk goes through L1 with the price list and what she'd do now.

E at the clinic opens a panel of what she buys and fits, with talking to
her first; selling a part or a fitting asks twice. The clinic adds its
trade to the player's buyer through simile.player's keepers_of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/src/ui/dialogue-scene.ts
Her manifest says "resident": false, which the dolls build writes for
anyone outside the village, and the client's dollFor lends only residents'
figures to anyone without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	client/src/ui/README.md
#	client/src/ui/dialogue-scene.ts
#	client/src/ui/help.ts
#	client/src/world/README.md
#	client/src/world/reach.ts
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Review: approved.

  • Scope. The contract files match S7a: Contract for the clinic #381's head (54e2445) byte for byte, and nothing in server/simile/conversation/ changed. Each path outside Owns is minimal and needed: keepers_of plus a Trade argument, the reach and hint code, registering her figure and greetings, voice-lab/npcs.py, her voice files, dollFor skipping "resident": false, and the rglob fix in test_cli.py, which the new tunnels/ folder would otherwise break.
  • Done-when. It's covered by test_selling_a_kidney_pays_the_doctors_price_through_the_ledger, test_each_fitting_takes_the_money_and_the_part and test_you_cant_buy_what_you_cant_afford, plus the end-to-end lifespan test over the websocket, which checks the ledger entries and that the ledger balances.
  • Trying to break it. Each case is refused: selling a sold kidney or the second one, fitting the same bionic twice, a second eye, a part a bionic doesn't replace, fitting without the money, and trading outside the clinic. A trade is one synchronous handler, so a restart can't land halfway through one. Stock is worked out from the body, and S1's persistence tests already round-trip a sold kidney and fitted bionics.
  • She isn't a resident. load_npcs only globs *.json. simulate (24 h from 08:00, seed 0, talk 3) gives 75,577 EntityMoved on this branch, which matches main. Reflection skips her talks: no resident takes part, so it makes no LLM call. Ambient and works ignore talks with the player.
  • E. Opening the clinic counts as reach at distance 0, and a pile still wins a tie. S13's reach tests pass.
  • Code. No anthropic or API key, and no heavy module-level imports in cli.py, lifespan.py or __init__.py. Comments are one line each; the multi-line JSDoc in protocol.ts is generated from S7a: Contract for the clinic #381's schema. annelie.wav is 288 KB, a 6 s VoiceDesign clip (24 kHz mono, with her seeds.json entry).
  • Runs. pytest tests/clinic tests/player tests/economy tests/npc tests/story tests/persistence gives 1641 passed. vitest run src/dolls src/ui src/world src/side gives 871 passed.

Notes, not blockers:

  • Her look reads as a kindly grandmother in an apron, not a back-alley surgeon. Her generated voice sounds American. Both are your call.
  • After the datajack, the eye takes about 4.6 days of metro rounds (185 at 40 a day), until S9's sewers add income.
  • The ledger books a kidney as salvage and a fitting as a purchase, so D1's report would count the kidney as salvage. That's worth the follow-up issue the PR proposes.
  • Only her greetings are voiced. Voicing adds voices only for behaviour's residents, so her replies are text only.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 20:04
@jamescrosswell
jamescrosswell merged commit f541205 into main Oct 22, 2026
8 checks passed
@jamescrosswell
jamescrosswell deleted the task/354-clinic branch October 22, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S7: The clinic

1 participant