adventure: let a player leave town from the web, not only read about it

W5a gave the web two verbs that cost nothing. These are the three that take
arguments and spend coins: set out for a zone with a supply loadout, walk back
into the run you extracted from, hire the pet sitter for a week or a month.
Between them they cover the most common thing anybody does in the game, which
until now could only be typed into Matrix.

Arguments are the new surface, so they are the thing to be careful with. Nothing
in a request is trusted: every zone, loadout and duration is looked up in the
offer list gogobee pushed onto that owner's own private row, and the order stores
what was found there rather than what was sent. A forged zone resolves to nothing
and never becomes an order. gogobee then re-resolves all of it anyway, because an
offer is a quote and a quote is not a permission.

The money confirm is the equip panel's, lifted: cost, balance, and the balance it
leaves. When it does not cover, it says so instead of printing a negative.

Verified in a browser rather than only in tests, which is where both real defects
came from: the confirm box was appending to the whole panel and so appeared at
the bottom of the section instead of under the button that raised it, and button
prices printed as EUR45000 above a dialog reading EUR45,000.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
prosolis
2026-07-24 19:47:26 -07:00
parent 6b0aae9f4a
commit 868a29e992
11 changed files with 685 additions and 57 deletions
+5
View File
@@ -117,6 +117,11 @@ func runMigrations(d *sql.DB) error {
addColumnIfMissing(d, "adventure_run_beat", "prose", "TEXT NOT NULL DEFAULT ''")
// Ask 7: upgrade orders carry a target tier for the 5 standard equipment slots.
addColumnIfMissing(d, "equip_orders", "tier", "INTEGER NOT NULL DEFAULT 0")
// W5b: the three verbs that take arguments (which zone, which loadout, how
// many days of sitting) carry them as one small JSON object. W5a's two verbs
// take none, so an existing row gets '' and reads back as no params — which is
// exactly what extract and siege_join mean.
addColumnIfMissing(d, "adventure_orders", "params", "TEXT NOT NULL DEFAULT ''")
// Adventure alerts. A subscription made before they existed knows only the OIDC
// subject, and the adventure ownership join needs the Matrix localpart — so an
// existing row gets "" here and is skipped for owner-scoped alerts until the