diff --git a/docs/LANES/LANE_C_PUB.md b/docs/LANES/LANE_C_PUB.md index b6b5333..b6bfc35 100644 --- a/docs/LANES/LANE_C_PUB.md +++ b/docs/LANES/LANE_C_PUB.md @@ -29,6 +29,9 @@ > **v5.0-beta amendment (2026-07-17, R26 wave 4):** **§7.2a's id form is now SOURCING-SCOPED** — > `sku_` for real, `mint_` for mint. **Prefixes are namespace fences**, not decoration: > my R25 line was written before mint existed and read as one space where there are two. Doc-only. +> **v7.0-alpha amendment (2026-07-18, R30 ledger #2):** **§9 added — the sell-counter contract** (the +> no-pump law made structural: `offer(p) = min(p−1, max(1, floor(p·0.5)))` — strictly below the buy side +> for every p ≥ 1, by construction). §0–§8 unchanged. *The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off @@ -462,3 +465,85 @@ never persisted. a *different* seeded pick than the dig's offers — a fan, not the crate's contents. A sold record's cover may linger in the fan until the room rebuilds. If we ever want the fan to honour `gone`, **that** is when I'd publish a post-build hook; it is not needed for the epoch's claim, because the crate you *riffle* is true. + +--- + +## 9. The sell counter (v7.0-alpha, R30 ledger #2) → Lanes F/B + +**The mirror of the buy card.** Walk to the keeper's counter holding items the shop trades in → a sell +card (the same manila price-sticker treatment as the dig's pull panel): item, OFFER, SELL. Module: +`web/js/interiors/sell.js` — DOM-only (zero draws, zero GPU resources), same consumer pattern as +`dig.js` (C ships the interaction; F owns the in-game input/mode hook). + +### 9.1 THE OFFER — the no-pump law, structural (the multiplier and why) + +```js +sellOffer(pricePaid) = min(pricePaid − 1, max(1, floor(pricePaid × 0.5))) // and never < 0 +``` + +- **The multiplier is 0.5** — the classic secondhand-dealer margin (a keeper pays about half of what + he'll sticker it at; he has to eat too). It keeps selling *meaningful* (half your money back beats a + full crate of duds) without being an income source. +- **The `min(pricePaid − 1, …)` clamp is the law made structural, not tuned:** the offer is **strictly + below what you paid for every `pricePaid ≥ 1`** (at `p = 1` the offer is $0 and the SELL button + disables — the keeper won't buy what he can't sticker; at `p = 2` the naive `max(1, floor(p/2)) = 1` + would meet the clamp anyway — the clamp exists so no future retune of the multiplier can ever push an + offer to break even). **Buy-then-immediately-sell therefore loses ≥ $1 per round trip, always** — + F's no-pump gate (`R30 ledger #5`, monotonic cash loss) holds **by construction**, and a change to + this formula that breaks that property is a contract violation, not a balance pass. +- **Alpha basis = `pricePaid`.** The alpha carries no guide-band data (the BIBLE bands are beta), and + in the alpha every buy happens at the shop's asking price — so `pricePaid` IS the buy-side price the + player faced. **Beta swaps only the basis, not the shape:** when E's guide bands land, the offer + becomes `min(bandLow − 1, max(1, floor(bandLow × 0.5)))` — below the band's *low* edge by + construction, so a gem bought under band still nets its finder margin only against the band, never + against the keeper. The formula's clamp survives the swap untouched. + +### 9.2 What the keeper buys (alpha: type = type, strict) + +- `sellableIn(shopType, item)` ⇔ `item.type === shopType`. A record shop buys records, a book shop + books — **any** shop of the type (per-keeper taste, haggling, pawn-buys-everything: all beta, not + built). +- **An item with no `type` is NOT sellable — fail-closed** (vacuous-gate law: a matcher that passes an + untyped item would "match" everywhere). **→ Lane F ASK:** stamp `type` (the registry stock type: + `record`/`book`/`toy`/…) on every collection item at buy time — the R30 published shape + (`{townKey, godverseShopId|shopId, sku|slotId, pricePaid, dayFound}`) doesn't carry it, and without + it nothing is sellable anywhere. Carrying `title`/`artist` too is strongly asked (B's collection UI + needs them; my card falls back to the sku/slotId label when absent — ugly but honest). + +### 9.3 The seam — money like buying, removal via the game API only + +- **C consumes `window.PROCITY.game.collection` READ-ONLY.** The card never splices, reorders, or + mutates it (same species of law as §8.3's `items[]`). +- **`onSell(item, offer)`** is the single side-effect callback — the exact mirror of the dig's + `onBuy(offer)` (the proven wallet seam). The **consumer** (F) does both halves: credit the wallet, + remove the item from the collection **through the game API**. `onSell` returning `false` = keeper + veto: card unchanged, nothing moves. +- `wallet.sell(item, offer)` is provided (C-owned `wallet.js`, mirrors `buy()`): credits `offer`, + drops the matching v0-inventory entry, notifies. F binds it exactly like it binds `wallet.buy`. +- Till ring: the sale rings `'till'` from the counter emitter (the R28 one-shot pattern, same + distance-gain math as the dig's) — silent-and-happy with no engine / `?mute` / `?noassets`. + +### 9.4 The trigger + the card + +- **Where:** within **`SELL_DIST = 2.0 m`** of `room.counter.pose` (bench centre, room-local — the + frame the interior camera walks in). Helper exported: `nearCounter(room, pos, dist?)`. +- **Suggested E routing for F** (matches my harness): aimed bin → dig (unchanged) · else near counter + with ≥ 1 sellable → sell card · else shelf-buy. Pointer unlocks while the card is up (dig precedent). +- **The card:** one item at a time — artist/title (or the sku/slotId fallback), the round OFFER + sticker, SELL, `‹ ›` cycling when more than one sellable, ✕/Esc closes. `open()` returns `false` + (no card, no DOM) when nothing is sellable. + +### 9.5 Fail-soft (the classic-pure law, C's half) + +No game layer (`?classic=1` / `?game=0` ⇒ `window.PROCITY.game` absent) → the consumer never opens +the sell UI, **and** `open()` itself returns `false` on a missing/empty item list — zero errors, zero +DOM, zero localStorage. `createSell()` allocates DOM only; `dispose()` removes every node + listener. + +### 9.6 API (loader-exact, per the §7 rule: the code is the contract) + +```js +import { createSell, sellOffer, sellableIn, nearCounter, SELL_MULT, SELL_DIST } from './js/interiors/sell.js'; +const sell = createSell(); +sell.open({ shopType, shopName, items, getCash, onSell, onClose, emitters }) // → bool (false = nothing sellable) +sell.close(); sell.dispose(); sell.active // mirrors dig.js +```