Add a local faction-only prototype profile for all seven nonempty expansion selections, genuine Basic/Advanced initialization and existing private setup, controls, AI and saved continuation. Ecaz allocates six forces across Imperial Basin sectors and retains fourteen reserves before later setup; preserve Fremen, Advanced BG and Moritani ordering with printed and inferred boundaries explicit. Keep optional Ecaz Treachery cards separate, ordinary deck counts 33/35/47, and Richese cache separate. Public start and publication gates remain closed. Replace the Advanced Ixian/Richese pre-offer stop with an explicit per-lot Ixian decline. Bind the saved original offer to its round, turn, owner, independent event, exact card custody and terms before view/action/normalization. Decline resumes that unchanged lot once without using Technology; later normal lots still offer the exchange. Protect reserved cards across transfers, spending and random Harkonnen exchange, and preserve legitimate Gift/Truthtrance interruptions. The actual special-lot exchange still awaits replacement-custody interpretation. Validation: focused setup, rules, UI/bot, private JSON/SQLite reload and concurrent CAS tests and independent custody/persistence review passed. Typecheck, lint, 4573 offline tests, production build and 43 HTTP tests passed. Browser Ecaz setup rejected an incomplete total, saved a 2/2/2 split, and refreshed to fourteen reserves and unchanged private cards. A separate genuine Advanced QA game restored the Ixian choice; explicit decline opened the unchanged cache lot with legal bidding and retained private hand, spice, pool count and unused Technology after refresh. Five of six genuine four-profile samples finish: CHOAM Basic 1054 actions/28 JSON restores, CHOAM Advanced 225/6, Ecaz Basic 91/2, Ecaz Advanced 523/14, all-three Basic 476/12. Combined Advanced passes three former auction stops and reaches turn three/action614 with 16 restores, then hits an existing CHOAM movement/Ambassador arrival guard. Ecaz samples also include guarded arrival candidates with legal alternatives. Full combined play is not certified; arrival integration is the next gap. Retain initial failed logs, including a corrected private harness sold-card receipt alias. After the outage, all 597 opening rooms and original seat/recovery records were preserved (632 after QA/HTTP). A stale development module cache required restart after a fresh 598-room backup; all 598 matched afterward and the QA seat restored. Final integrity and foreign keys passed. No database reset, recurring automation or publication. Verified source tree: 5e2ff244a1167f2834bb46d609b8de914a34f7049ff7b06046243068c0935131
86 lines
2.3 KiB
TypeScript
86 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import type { Action, GameView } from '@/game/engine';
|
|
import {
|
|
ECAZ_START_FORCES,
|
|
ECAZ_START_LOCATIONS,
|
|
quoteEcazStartingForces,
|
|
} from '@/game/ecaz-setup';
|
|
import { splitLocation } from '@/game/board';
|
|
import { Button } from './ui/button';
|
|
import { Input } from './ui/input';
|
|
|
|
export function EcazSetup({
|
|
game,
|
|
act,
|
|
busy,
|
|
}: {
|
|
game: GameView;
|
|
act: (action: Action) => void;
|
|
busy: boolean;
|
|
}) {
|
|
const [placements, setPlacements] = useState<Record<string, number>>({
|
|
[ECAZ_START_LOCATIONS[0]]: ECAZ_START_FORCES,
|
|
});
|
|
const me = game.players.find((player) => player.id === game.me);
|
|
if (
|
|
game.status !== 'setup' ||
|
|
game.setupStage !== 'forces' ||
|
|
me?.faction !== 'ecaz' ||
|
|
me.reserves !== 20 ||
|
|
!game.setupPending.includes(game.me)
|
|
)
|
|
return null;
|
|
let quoted: Record<string, number> | null = null;
|
|
try {
|
|
quoted = quoteEcazStartingForces(placements);
|
|
} catch {
|
|
/* Show the allocation until its total is legal. */
|
|
}
|
|
const total = Object.values(placements).reduce(
|
|
(sum, count) => sum + count,
|
|
0,
|
|
);
|
|
return (
|
|
<section aria-label="Ecaz starting forces" className="flex flex-col gap-3">
|
|
<p>
|
|
Place six forces in Imperial Basin. Choose how many go in each sector;
|
|
fourteen forces remain in reserves.
|
|
</p>
|
|
{ECAZ_START_LOCATIONS.map((location) => (
|
|
<label key={location} htmlFor={`ecaz-start-${location}`}>
|
|
Imperial Basin · Sector {splitLocation(location).sector}
|
|
<Input
|
|
id={`ecaz-start-${location}`}
|
|
type="number"
|
|
min={0}
|
|
max={ECAZ_START_FORCES}
|
|
step={1}
|
|
value={placements[location] ?? 0}
|
|
disabled={busy}
|
|
onChange={(event) =>
|
|
setPlacements({
|
|
...placements,
|
|
[location]: Number(event.target.value),
|
|
})
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
<p className="fine" aria-live="polite">
|
|
{total} / {ECAZ_START_FORCES} forces assigned
|
|
</p>
|
|
<Button
|
|
className="game-action min-h-11"
|
|
disabled={busy || !quoted}
|
|
onClick={() => {
|
|
if (!busy && quoted) act({ type: 'ecazSetup', placements: quoted });
|
|
}}
|
|
>
|
|
Place Ecaz starting forces
|
|
</Button>
|
|
</section>
|
|
);
|
|
}
|