Commit Graph

8 Commits

Author SHA1 Message Date
prosolis
7e59697754 adventure: tell Pete whether a backpack item beats what's worn
For each backpack magic item, compute the per-stat diff against whatever
occupies the slot it would equip into (mi.Slot — the same slot the web
Equip button targets, so the card describes the trade that actually
happens). The verdict is strict dominance: all-gain is an upgrade, all-loss
a downgrade, mixed a sidegrade with no winner claimed (the case the two
opaque effect strings could never show). Empty slot reads 'new'; an
attunement item with no free bond reads 'inert', which overrides the stat
verdict because wearing it does nothing.

The diff is over tempered effects on both sides and reuses the engine's
own magicItemEffectFor, so nothing here re-derives power math that could
drift from the game. Rides an additive Compare object on the private
backpack ItemView — no migration, no endpoint, no public surface.

Rings collapse to the same path: every ring equips to ring_1 (ring_2 is
declared but never assigned by any live code), so a backpack ring simply
compares against the ring_1 occupant.
2026-07-17 10:04:18 -07:00
prosolis
7960838b3f adventure: drain Pete's equip queue and apply it for real
Poll Pete for equip/unequip orders an owner placed on the web, run them through
the real magic-item equip path so bond caps and slot eviction still hold, and
file a verdict. Same reverse pipe as mischief, with one difference that matters:
the equip action is not idempotent, so a re-offered order after a lost ack would
double-move the item. An equip_applied_orders ledger keyed on the order guid is
the guard: applied once, re-offers only re-file the stored verdict.

The delicate remove-before-equip ordering that prevents item duplication is now
one shared applyMagicEquip/applyMagicUnequip core, called by both the DM
resolver and this poller, so the anti-dup ordering can't drift between two
copies. Items now carry their inventory row id to Pete as the equip handle.
2026-07-17 08:44:44 -07:00
prosolis
32520eb7ec adventure: send Pete the fog-of-war zone map beside the room count 2026-07-17 08:18:11 -07:00
prosolis
b6d4e4ccec adventure: send Pete what an item actually is, and what's worn
The self-view listed a name, a tier and a price — everything except what a
player decides on. The facts were all there, just not on the wire.

Three things the contract spec got wrong, found by reading both sides:

Equipping *moves* the row out of adventure_inventory into
magic_item_equipped, so the two sets are disjoint. The spec's `attuned` on a
backpack item can never be true — bond state isn't false there, it's
undefined. The real gap was that worn items weren't sent at all: the panel
showed the backpack and hid the sword. Hence Equipped, where Attuned means
something and an inert item can be seen.

Stat modifiers ARE modeled. The spec said they weren't, and that shipping
them meant either an engine change or a display-only approximation that lies
the first time it disagrees with the engine. But magicItemEffectSummary is
the engine's own summary — the same function the game speaks with. Sending it
can't drift, because there's nothing to drift from.

SkillSource is two different things: "mining" on masterwork gear, and the
internal "magic_item:<id>" registry pointer on magic-item rows. Sending it
raw would put gogobee's IDs on a page, and Pete couldn't tell them apart to
filter them. Only the skill name goes out.

Desc and Effect resolve at the push site because an inventory row carries
neither — descriptions live on MagicItem/EquipmentDef, and the combat delta
is computed, never stored. Shop gear resolves by (slot, tier); Name is
decorative there.

All additive and omitempty on the private /api/ingest/detail push, so neither
side has to deploy first.
2026-07-17 06:44:45 -07:00
prosolis
11bfce780c adventure: gogobee pushes each adventurer's sheet to Pete's detail page
The board on Pete shows flat rows; this hands it two more channels so a name
can become a page. Public stats + equipped gear ride the roster snapshot
(RosterDetail on each entry, keyed by the anonymous token, no handle). The
private self-view — inventory, vault, house, pets — rides its own push keyed
by localpart, so Pete only ever serves it back to the one signed-in owner it
belongs to; the board token rides along so the ownership check is a join, never
a reversal of the one-way token. The private set skips no one for opt-out (that
governs the public board only) and skips the dead (no live page to own).
2026-07-14 22:22:39 -07:00
prosolis
ba306f0b59 mischief: gogobee learns to fill orders placed on Pete's web board
The game-side half of Mischief Makers M3. gogobee polls Pete for storefront
orders and opens the contract itself — the money, the eligibility, the fight
are all its own, exactly as a Matrix !mischief buy.

- roster push now carries each buyer's advisory euro balance (keyed by localpart,
  a separate keyspace from the anonymous board token) and the live tier catalog,
  so the storefront renders gogobee's current prices and never hardcodes one
- placeWebMischief: the fulfilment path, debit-first-then-refund so the money
  state is a pure function of the order guid. DebitIdem + an order_guid stamp on
  the contract make a retried claim neither double-charge nor double-open
- resolveRosterToken recomputes the one-way board token per live player to name
  a mark; the buyer is @<username>:<homeserver> from Authentik's preferred_username
- a 30s poll loop drives it; a lost verdict just leaves the order pending to
  re-run, so the loop is its own retry and needs no durable queue
- euro.HasExternalTx lets the affordability gate tell a first attempt (no debt
  for a mischief buy, like Matrix) from a retry of one already paid

order_guid column added to mischief_contracts (schema + idempotent ALTER).
2026-07-14 21:12:37 -07:00
prosolis
2ab6e7843a roster: a quietly-succeeding ticker looks exactly like a dead one
The push logged nothing on success, so during the first deploy an empty board
and a silent log were indistinguishable from a ticker that never started — and
the debug went looking for a bug that wasn't there. (The snapshot was simply
2 minutes out; the first tick hadn't fired yet.)

Logging every push at INFO would be noise forever. Log the transitions instead:
the first time it works, the moment it breaks, and when it recovers.
2026-07-13 18:19:16 -07:00
prosolis
ced75786b9 news: tell Pete who's out there right now, not just who died
We only ever told Pete about outcomes. Nothing emitted when an expedition
*started*, which is why the two bored adventurers walked into dungeons and the
news feed said nothing at all — it wasn't broken, it had nothing to say.

Two halves:

A roster snapshot, pushed every 2 minutes. Deliberately NOT on the durable fact
queue: a fact is history and losing it loses it forever, so it retries. A
snapshot is a photograph of the present, and a retried one is a lie — by the
time it lands, she's moved. The next tick carries the truth. That's also what
lets Pete's staleness timer work: if we stay down, nothing arrives, and the
board stops claiming to be live instead of insisting forever that Josie is
still in holymachina.

And a "departure" bulletin when a bored adventurer lets itself out.

The snapshot omits opted-out players rather than anonymizing them, and carries a
board token distinct from every event token, so a standing row can't become the
key that links a player's dispatches back together.

The player_meta scan folds last_player_action_at/created_at in Go instead of
COALESCE()ing in SQL — modernc rebuilds time.Time from the declared column type
and COALESCE erases it. A failed scan here would publish an empty board and
every adventurer would vanish from the page.
2026-07-13 18:05:49 -07:00