Integrate Smuggler companion with owned Richese No-Field
Connect the GF9 FAQ combination: one free physical reserve force accompanies an owned concealed No-Field at its unchanged one-marker price. Preserve zero/three/five token values, actual supply at later reveal, optional human choice and all four legal AI paths. Genuine CHOAM & Richese Leader Skills development setup adds the two sourced battle cards without duplicating their Ix identities. Public mode and publication gates stay closed; allied and remaining combined routes are unfinished. Bind the private declaration across initial No-Field cancellation and later Advanced Guild interception, including saved interruptions. Validate before projection/actions/normalization, hide the internal proof, reject altered declarations immutably, and withdraw the companion once only on successful commitment. Independent rules/privacy/persistence review found no blocker. Preserve other audit setup gates despite expanded deck assembly. Repair the existing randomized recovery test with a genuine seeded Rihani offer and explicit human choice, replacing an inappropriate AI-policy call on a human seat. Initial Karama and later Guild stop retain their distinct existing opportunity outcomes. Validation: 72 focused regressions plus 10 deck tests; typecheck, lint, 4520 offline tests, production build and 43 HTTP tests passed. SQLite restart, concurrent allowance/cancellation and exact physical custody passed. Browser genuine assignment, private token selection, opt-in, one-spice shipment, refresh, five-force reveal and final refresh preserved six physical Arrakeen forces, 14 reserves, zero Tanks, four spice, Crysknife and Soo-Soo Sook. Genuine four-profile Basic/Advanced games completed in 77/116 actions with 3/4 JSON restores and no rejected candidates; Advanced exercised one companion shipment and later reveal. After the outage the existing server was healthy. Preserved all 529 opening rooms and original seat/recovery records (563 after QA/HTTP). SQLite integrity and foreign keys passed. No restart, database reset or automation. Verified source tree: c2aee4138bcb46a05814a340d62eadace3470247ca620844eb0375e78ea4d6c4
This commit is contained in:
1 parent
bf8036dfc1
commit
788453e07e
20 files changed
+1213
-51
No files matched your search
@@ -7,9 +7,42 @@ import { territory, MOBILE_STRONGHOLD } from '@/game/board';
|
||||
import { fighterCount } from '@/game/advisors';
|
||||
import { reserveShipmentCost } from '@/game/shipment-price';
|
||||
import type { RicheseNoFieldView } from '@/game/richese-no-field';
|
||||
import { quoteSmugglerNoField } from '@/game/smuggler-no-field';
|
||||
import { ShipmentQuote } from './shipment-quote';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export function SmugglerNoFieldChoice({
|
||||
quote,
|
||||
use,
|
||||
busy,
|
||||
onChange,
|
||||
}: {
|
||||
quote: ReturnType<typeof quoteSmugglerNoField>;
|
||||
use: boolean;
|
||||
busy: boolean;
|
||||
onChange: (use: boolean) => void;
|
||||
}) {
|
||||
if (!quote) return null;
|
||||
return (
|
||||
<div className="rounded border border-[#8e8159] p-3">
|
||||
<label className="flex min-h-11 items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={use}
|
||||
disabled={busy}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
/>
|
||||
Use Smuggler to add 1 real reserve force at no added cost
|
||||
</label>
|
||||
<p className="fine mt-2">
|
||||
The companion force enters the empty territory immediately. The No-Field
|
||||
token still uses the normal one-force shipment price, and its hidden
|
||||
forces remain in reserves until revelation.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** This renderer receives public player data only, never the private token model. */
|
||||
export function NoFieldBoardMarkers({
|
||||
players,
|
||||
@@ -76,6 +109,7 @@ export function RicheseNoFieldControls({
|
||||
}) {
|
||||
const id = useId();
|
||||
const [selection, setSelection] = useState('');
|
||||
const [useSmuggler, setUseSmuggler] = useState(false);
|
||||
const info = game.richeseNoField;
|
||||
const me = game.players.find((player) => player.id === game.me);
|
||||
if (!info || info.owner !== game.me || !info.private || !me) return null;
|
||||
@@ -97,6 +131,9 @@ export function RicheseNoFieldControls({
|
||||
game.karamaShipping?.player === me.id,
|
||||
};
|
||||
const cost = reserveShipmentCost(rate, selectedTerritory.type, 1);
|
||||
const smuggler = info.canShip
|
||||
? quoteSmugglerNoField(game, me.id, destination)
|
||||
: null;
|
||||
const share =
|
||||
!me.ally || allyPayment === ''
|
||||
? Math.max(0, cost - (me.spice ?? 0))
|
||||
@@ -137,8 +174,9 @@ export function RicheseNoFieldControls({
|
||||
<div className="flex flex-col gap-3 py-3">
|
||||
<p className="fine">
|
||||
Your token values are private until revealed. A concealed marker
|
||||
counts as one force, even when its value is zero. Physical forces
|
||||
remain in reserves until revelation.
|
||||
counts as one force, even when its value is zero. Its hidden forces
|
||||
remain in reserves until revelation; a Smuggler companion is a
|
||||
separate real force that leaves reserves immediately.
|
||||
</p>
|
||||
<ul
|
||||
className="m-0 flex flex-wrap gap-3 p-0"
|
||||
@@ -240,6 +278,12 @@ export function RicheseNoFieldControls({
|
||||
}}
|
||||
unavailableReasons={problems}
|
||||
/>
|
||||
<SmugglerNoFieldChoice
|
||||
quote={smuggler}
|
||||
use={useSmuggler}
|
||||
busy={busy}
|
||||
onChange={setUseSmuggler}
|
||||
/>
|
||||
<Button
|
||||
className="min-h-11 whitespace-normal"
|
||||
disabled={busy || problems.length > 0}
|
||||
@@ -253,10 +297,12 @@ export function RicheseNoFieldControls({
|
||||
territory: destination,
|
||||
sector,
|
||||
allyPayment: share,
|
||||
...(useSmuggler && smuggler ? { smuggler: true } : {}),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Ship concealed No-Field {selected?.value ?? ''} · {cost} spice
|
||||
{useSmuggler && smuggler ? ' · + 1 real force' : ''}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user