Skip to content

S6: Salvage, and selling it - #373

Merged
jamescrosswell merged 8 commits into
mainfrom
task/353-salvage
Oct 22, 2026
Merged

jamescrosswell merged 8 commits into
mainfrom
task/353-salvage

Conversation

@jamescrosswell

@jamescrosswell jamescrosswell commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Salvage to pick up in the metro with E, within the carry limit, and selling it at the scrap trader's stall and the clinic, at the economy's prices, through E3's ledger. Built on the contract in #372 and S3's metro (#370).

Closes #353

What it does

  • Salvage lies at the maps' salvage pickups. TownMap now reads each map's pickups layer. A pickup can name its item in the map, as a secret does, so S9's generated sewers can say what lies where. The metro's six (S3's metro_salvage_1 … _6) don't, so PlayerConfig.salvage fills them in, from copper wire (1.5 kg) to a servo motor on the high ledge (3 kg): 12.5 kg in all, against the 9 kg the player can carry beside the lamp.
  • Salvage comes back; secrets don't. A salvage pickup has another lying there PlayerConfig.respawn (a sim day) after it was last taken, by Player.taken's time, unless PlayerConfig.salvage gives it its own. The three secrets, 25 and 40 in cash and a ration pack, stay taken for good.
  • Picking up (Pickups, simile.player): E takes from the nearest pile in reach, theirs or the world's: its money, and each item that still fits. Whatever of a pickup didn't fit becomes a pile of theirs in left.
  • Selling (Trade): anywhere in the scrap trader's stall or the clinic, player_state's buyer names it with the economy's price for each kind of salvage they carry, and E sells everything it buys.
  • Prices are sim facts, in EconomyConfig.salvage (copper wire 6, scrap steel 5, circuit board 8, brass valve 7, signal relay 6, servo motor 18), paid by the buyers. Whoever later voices them only says them.
  • The client: the corner hint says what E picks up or sells and for how much ("Press E to pick up 25 in cash.", "Press E to sell your copper wire and circuit board at the scrap trader's stall for 14."), what won't fit, and E does it. A small pulsing glint, gold for cash, marks each pile on the street (side/piles.ts).

The ledger balances, with the player in it

The player's money now goes through E3's ledger as PLAYER: meals (S5 paid them as from OUTSIDE), salvage sold (salvage) and money found (found), both from OUTSIDE. Economy.pay moves Player.money for PLAYER, once the player module joins the player to the economy. It raises ValueError and records nothing when the player can't pay, so a caller that forgets to check (S7's fitting, say) can't let net(PLAYER) drift. The player isn't a resident, so to the residents' books they're outside the village: came_in, went_out, held and gap are as before for residents, and ledger.net(PLAYER) is always what the player has gained. Tests check both after picking up and selling.

Please look at

  • The glint is drawn in code, about 0.8 m across, outlined, over the pile's cell. It's visible on the train's roof and the catwalk in the harness, but small at the street's zoom, and S4's darkness will want a say in whether it shows in the dark.
  • The prices and weights are a first guess for S15's loop: one round of the metro's salvage is worth 50, the secrets 65, and salvage comes back daily. S7 sets the bionics' prices against them.
  • The stall has nobody behind it. It buys whenever the player's in it, as does the clinic until S7's doctor. No NPC was needed.

For S7

  • E at a buyer wins over talking. The buyer is in E's reach at 0 tiles while the player carries something it buys, so once the doctor stands in the clinic, E there sells rather than talks. S7 decides which E should do.

Notes

  • Tests against the real metro: every salvage pickup on the maps has something set to lie there, the metro's nine piles are where S3 put them, the train roof's wire and the catwalk's 40 are picked up, and the stall and the clinic buy. The pick-ups and the sale at the stall go over the WebSocket too.
  • Residents' days are unchanged. test_simulate's 24-hour day (from 08:00, seed 0, talk 3) on main (3bd04b0, with the metro) and on this branch: 75,577 EntityMoved events, identical, and the same summary.
  • Saves still load. On a copy of simile.db in a temp dir: all 11 residents back on their saved tiles and the player as saved; then the stairwell's 25 and the copper wire picked up in the metro, the wire sold at the stall, and the save written and reloaded with taken and both ledger entries.
  • Outside the Owns, by the lead's extension: server/simile/world/map.py and server/tests/world/test_map.py, for the pickups layer. In the client, beside ui/ and world/reach.ts, the glint is a new client/src/side/piles.ts with three lines in side-scene.ts and one in side/README.md.
  • Hunger.pick_up moved to Pickups, with its tests.

Follow-ups

  • D1's report (simile/tools/report.py) counts any entry from OUTSIDE as money into the village, so the player's salvage and found money show there as "in", and their meals as "between" residents. It wants a row for the player.

jamescrosswell and others added 4 commits October 23, 2026 04:23
Salvage lies at the maps' pickups and sells at the scrap trader's stall
and the clinic, at the economy's prices, through the ledger.

- TownMap reads each map's pickups layer.
- The player's money goes through E3's ledger as PLAYER: meals, salvage
  sales and money found. They're outside the residents' books, so the
  residents' gap is as before, and net(PLAYER) is what they've gained.
- Pickups: what lies at the maps' salvage and secret pickups until taken,
  and picking up from those and what they left. Taken pickups are kept
  in Player.taken; what doesn't fit of one goes into left.
- Trade: the buyer where they stand, and selling to it.
- player_state carries the buyer and the world's piles.
- Client: E picks up and sells, the corner says what and for how much,
  and a glint marks each pile on the street.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each salvage pickup has its own respawn in PlayerConfig.salvage, a sim
day for now, counted from when Player.taken says it was last taken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metro's six salvage pickups each have something set to lie there,
its secrets hold 25, 40 and a ration pack, and the scrap trader's stall
and the clinic buy. The synthetic pickups and stalls now live only where
the real ones aren't.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Changes requested: one item, the comment rule. The behaviour, the ledger, the scope and the tests are right.

Change

  1. Cut every docstring this PR adds or lengthens to one line. The READMEs already say the rest.
    • economy/economy.py: Economy (now 4 lines), pay and offer (2 each)
    • economy/ledger.py: Ledger (was 1 line, now 2)
    • player/pickups.py: Pickups and pick_up (2 each)
    • player/trade.py: sell (2)
    • world/map.py: Pickup (2) and load_map (3 lines before, now 4). load_map isn't yours, so putting it back to 3 lines is enough.

Checked and fine

  • Scope. Changes stay inside Owns and the lead's pickups extension (map.py, test_map.py). The client changes are side/piles.ts, its hook-up in side-scene.ts and README lines in side/ and world/. No schema or protocol change. git merge-tree is clean against S10: The player, visibly bionic #374 and S11: The village reacts #376.
  • Done-when. test_pickups.py, test_trade.py, test_economy.py and test_ledger.py cover picking up within the limit and selling at EconomyConfig.salvage's prices. They also check gap == 0, with ledger.net(PLAYER) equal to the player's change.
  • Behaviour.
    • E takes from the nearest pile, theirs first on a tie, as the client does.
    • It takes the money whatever they carry. The rest of a pickup goes into left.
    • When nothing fits, nothing is taken and nothing is recorded. A second E takes nothing.
    • Salvage is back after its own respawn, to the second. PlayerSync sends the new pile on the next tick, so it shows even if they're standing on it. Secrets never come back.
    • sell ignores: no buyer, something they don't carry, something the buyer doesn't buy, and the lamp.
    • Zero money is fine: selling and finding need none, and meals still check first.
  • Ledger.
    • PLAYER is left out of held.
    • The player's meals still count in came_in, as they did from OUTSIDE.
    • OUTSIDE → PLAYER touches neither side, so the residents' books are unchanged.
    • S5's meals now go through buy(PLAYER, …). The café's drink stays free, because on_served only charges residents.
  • Residents' days. I re-ran the 24-hour day (08:00, seed 0, talk 3) on main and on this branch. Each gives 75,577 EntityMoveds, with the same hash and the same summary.
  • Saves. taken and the found and salvage entries round-trip through WorldStore. Old saves load.
  • Client.
    • The hint uses fitting, as the server picks up.
    • E sends pick_up, or sell with null.
    • The glint is gold for cash-only piles.
    • salvage.test.ts, piles.test.ts and reach.test.ts cover these.
  • Code. No Claude API, SDK or key. No heavy imports at module level. Nothing personal.
  • Tests.
    • pytest tests/player tests/economy tests/world tests/persistence: 331 passed.
    • vitest run src/side src/ui src/net src/world: 759 passed.

Not blocking

  • Economy.pay floors the player's money at 0 but records the full amount. A caller that forgets to check, such as S7's fitting, would let net(PLAYER) drift silently. Raising instead would be safer.

  • Generated salvage pickups. PlayerConfig.salvage is keyed by pickup id, so S9's generated sewer pickups would be logged and left empty. The map could name the item, as it does for secrets.

  • E at a buyer always wins (tiles: 0). Once S7's doctor is in the clinic, E there will sell rather than talk while the player carries salvage.

  • Duplicates read oddly. Two of one kind come out as "your copper wire and copper wire" and "The servo motor and the servo motor".

  • Two README lines run past 100 columns: world/README.md's usedByE line and ui/README.md's E line.

  • The D1 follow-up is right. It's worth filing. The report counts salvage and found money as "in", and the player's meals now as "between".

  • S4. S4 will touch the same hunks in:

    • player/lifespan.py (stops)
    • the intro of player/README.md
    • world/map.py: the layer constants, TownMap's fields, and load_map's call and docstring
    • the imports in test_map.py
    • side-scene.ts: imports, fields, create, cleanups and update
    • side/README.md's list
    • ui/help.ts, for the lamp key

    S4 has no branch yet, so branching it after this merges avoids all of that. Whether the glints (at streetFurniture + 0.5) show in the dark is S4's call.

Taste, for you

  • The glint. It's 0.8 m across and small at the street's zoom.
  • Prices and respawn. One round of salvage is worth 50 and the secrets 65. Salvage comes back daily, so it's an endless 50 a day against meals at 5 to 10. S7 prices the bionics against that.
  • The stall. It buys with nobody behind it.

- Docstrings this PR adds or lengthens are one line; load_map's is back
  to three.
- Economy.pay raises, recording nothing, when the player can't pay, so
  net(PLAYER) can't drift.
- A salvage pickup can name its item in the map, as a secret does, with
  PlayerConfig.salvage the fallback for the metro's six, and
  PlayerConfig.respawn the delay unless a pickup has its own.
- The hint and the sale log say each kind once: "copper wire ×2".
- Wrap the two long README lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 22, 2026
@jamescrosswell

Copy link
Copy Markdown
Contributor Author

Approved. I re-reviewed at 876458a, and everything from the last review is fixed.

Since fdeec1b

  • Docstrings. Every docstring the PR adds or touches is one line, except load_map, which is back to its original 3 lines. The new counted docstrings are one line too.
  • pay raising can't break S5's meals.
    • The only call that makes the player pay is buy(PLAYER, …) in Hunger._pay.
    • _eat_meal returns before it when meal.price > money. pay raises only when amount > money + 1e-9, so it is never stricter than that check.
    • Selling and finding pay to the player, so they never reach the check.
    • test_no_meal_they_cannot_pay_for still passes, and the new test shows a refused payment records nothing.
  • Map-named salvage suits S9.
    • S3's planner (street/plan.py's _pickup) already writes item, item_kind, item_name and item_weight for any kind of pickup.
    • I ran a planned salvage pickup with an item through it and then load_map. It loads as map-named salvage, so S9 only has to set item.
    • The map's item comes before the config's, and PlayerConfig.respawn is the default.
  • Duplicates are said once with a count, in both the hint and the log.
  • READMEs. No added line is over 100 columns now.
  • Tests and the day.
    • pytest tests/player tests/economy tests/world tests/persistence: 335 passed.
    • vitest run src/side src/ui src/net src/world: 760 passed.
    • The 24-hour day is still 75,577 EntityMoveds, identical to main.

Notes for S9, not blocking

  • Prices. A map-named item needs a price in EconomyConfig.salvage. Without one, the buyers silently won't take it. A test over each seed's salvage would catch this.
  • Loading. Pickups reads the maps once, at start. A level generated mid-session would need it rebuilt.

Merging with the other open branches

  • Clean: S10: The player, visibly bionic #374, S11: The village reacts #376 and S4a: Contract: the lamp switch #377.
  • S4 (task/351-darkness-lamp, no PR yet) conflicts in player/lifespan.py, side-scene.ts (cleanups) and dialogue-scene.ts (the hunger.update line). In each, keep both sides.
  • S13 (S13: The journal #378) conflicts in reach.ts, dialogue-scene.ts, help.ts and the ui/ and world/ READMEs.
    • Mostly keep both sides. But S13's reachables.piles is still player.left only, so the merge has to keep this PR's version.
    • S13 also handles pick_up for clues (story/recording.py). One E press near both a clue and a pile would then do both on the server.

@jamescrosswell
jamescrosswell marked this pull request as ready for review October 22, 2026 17:46
@jamescrosswell
jamescrosswell merged commit af6faa9 into main Oct 22, 2026
7 checks passed
@jamescrosswell
jamescrosswell deleted the task/353-salvage branch October 22, 2026 17:46
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.

S6: Salvage, and selling it

1 participant