From 2b071a44a274aa66ed206a13d6b77f4f675e5faa Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:47:06 +1000 Subject: [PATCH] Connect live Nexus offer withdrawal and named alliance history An unallied sender can withdraw its unanswered offer during an ordinary Nexus. The table names the outgoing recipient and keeps this distinct from breaking a formed alliance. The chronicle now records named proposals, withdrawals and breaks. Existing authoritative quote, timing locks, readiness, physical custody, Nexus-card forfeiture and request/version handling remain unchanged. Full-table rendering checks cover owner/recipient/observer controls, replacement, withdrawal, incoming offers, break, blocked timing and JSON continuation. A production SQLite race permits exactly one acceptance or withdrawal at a version; a stale withdrawal cannot break an alliance formed by the accepted request. Private restored views and seat credentials remain intact. All four current AI profiles retain legal participation; no strategy or calibration changes. Independent review found no material issue. Types, lint, all 4973 offline tests, production build and 49 HTTP tests passed on stable source. Browser QA used a fresh three-human Basic lobby, genuine setup and a controlled later-Storm boundary with existing Spice Cards reordered; actual Storm/Worm resolution opened the Nexus. This is a focused position, not a full-game sample. Browser proposal, refresh, keyboard withdrawal, retained incoming invitation, acceptance, break and named chronicle entries passed. At390px the page375px and withdraw339px by46px fit, with16px text. Final refresh restored no offers or alliances. No screenshot was captured; full visual/assistive and all-configuration acceptance remain open. Browser lost-response injection was not performed. All 1459 original rooms, 3081 seats and auxiliary rows are unchanged. SQLite integrity and foreign keys pass. The healthy server was reused without restart, migration, reset or recurring maintenance. Full rules and expansion completion remain gated. The goal retains deferred full AI development, three adjacent approximate75% targets, no usage cutoff, selective subagents and verified Git pushes. Private evidence: 20260921-nexus-offer. Verified source tree: e44e61b651e66cad23326bcca3ba2eecd6bb5a779a16e0273db8d0039f453511 --- README.md | 4 ++ app/globals.css | 3 + components/game-table.tsx | 11 ++++ docs/CURRENT_STATUS.md | 31 +++++----- docs/IMPLEMENTATION_STATUS.md | 11 ++++ docs/NEXUS_OFFERS.md | 40 ++++++++++++ game/engine.ts | 11 +++- game/reference.ts | 24 ++++++++ tests/fixture-nexus-offer.ts | 29 +++++++++ tests/nexus-offer-controls.test.tsx | 94 +++++++++++++++++++++++++++++ tests/nexus-offer-recovery.test.ts | 60 ++++++++++++++++++ 11 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 docs/NEXUS_OFFERS.md create mode 100644 tests/fixture-nexus-offer.ts create mode 100644 tests/nexus-offer-controls.test.tsx create mode 100644 tests/nexus-offer-recovery.test.ts diff --git a/README.md b/README.md index 4a7c6a6d..73f2ddee 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ Use **Bribes** beneath the table to pay a non-allied faction. The recipient's share becomes spendable at the next Mentat Pause; the panel shows your own incoming bribes and the current payment limit. See [bribe controls](docs/BRIBE_CONTROLS.md). +At a Nexus, an unanswered alliance offer names its recipient and offers +**Withdraw your alliance offer**. Formed alliances keep the separate **Break +alliance** action. See [Nexus offers](docs/NEXUS_OFFERS.md). + The table header offers optional **Sound effects**, with mute, volume and a test sample. Short cues mark phase changes and automatic action notices. Sound starts off; preferences stay in your browser. See [table sounds](docs/TABLE_SOUNDS.md) diff --git a/app/globals.css b/app/globals.css index dda7e4db..39f9b56e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1949,3 +1949,6 @@ footer { .bribe-fields label { display: grid; gap: .35rem; flex: 1 1 12rem; min-width: 0; } .bribe-fields select, .bribe-fields input { min-height: 44px; width: 100%; min-width: 0; font-size: 1rem; } .bribe-fields button { font-size: 1rem; min-height: 44px; max-width: 100%; height: auto; white-space: normal; overflow-wrap: anywhere; } + +.nexus-offer .notice, .nexus-offer .game-action { font-size: 1rem; } +.nexus-offer .game-action { min-height: 44px; height: auto; max-width: 100%; white-space: normal; } diff --git a/components/game-table.tsx b/components/game-table.tsx index f5de4015..29e00c7e 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -210,6 +210,8 @@ export function GameTable({ exitDisabled?: boolean; }) { const me = g.players.find((p) => p.id === g.me)!; + const outgoingAlliance = g.allianceOffers[me.id]; + const outgoingAllianceName = g.players.find((p) => p.id === outgoingAlliance)?.name; const shipmentAvailable = nexusGuildShipmentAvailable(g); const movementAvailable = nexusGuildMovementAvailable(g); const guildCunning = nexusGuildCunningActive(g); @@ -3392,6 +3394,15 @@ export function GameTable({

A sandworm has opened a Nexus. Alliances may change now.

+ {outgoingAlliance && !me.ally && ( +
+

+ Your alliance offer is to {outgoingAllianceName ?? 'a player no longer seated'}. + You remain unallied until both players agree. +

+ {actionButton('Withdraw your alliance offer', { type: 'alliance' })} +
+ )}