Connect Suk Graduate casualty rescues and saved continuation
Implement both rescue bands with legal physical casualty routing, owner controls, all four bot profiles and durable battle receipts. Preserve paid support, elite identity, killed/returned leader custody and replay rejection. New Advanced Atreides skill choices exclude Suk while the KH loss-count ruling is pending; legacy assigned combinations retain their preflight guard. Public gates remain. Independent review fixed stale dead trainer assignments and detached rescue receipts. Final focused Suk/Leader Skills union: 46 passed. Typecheck, lint, 4,452 offline tests, production build and 43 HTTP tests passed on unchanged source tree 3b76239a46d0ba874299495e64295a5eaaa8f2415c3e9a041bee944bbce50c5a. Actual browser rescue and refresh retained counters, private hand and pending winner card cleanup. Two four-profile Basic/Advanced smoke games completed with JSON restoration and no rejected actions; neither exercised a Suk rescue. After the outage report the server was healthy. All 360 pre-check rooms and original seat/recovery records are unchanged; checks added 33 rooms. SQLite integrity and foreign-key checks pass. No server restart or database reset.
This commit is contained in:
1 parent
5082c81353
commit
af360c836b
23 files changed
+987
-26
No files matched your search
@@ -48,6 +48,7 @@ import { RicheseGift } from './richese-gift';
|
||||
import { Distrans } from './distrans';
|
||||
import { JuiceOfSapho } from './juice-of-sapho';
|
||||
import { LeaderSkillsPanel } from './leader-skills';
|
||||
import { SukGraduatePanel } from './suk-graduate';
|
||||
import { NullentropyBox, NullentropySearch } from './nullentropy-box';
|
||||
import { OrnithopterMovement } from './ornithopter-movement';
|
||||
import { DiscoveryOrnithopterMovement } from './discovery-flight-movement';
|
||||
@@ -2080,6 +2081,8 @@ export function GameTable({
|
||||
? 'Position your skilled leader'
|
||||
: g.decision.kind === 'leaderSkillRevival'
|
||||
? 'Choose a skill for the revived leader'
|
||||
: g.decision.kind === 'sukRescue'
|
||||
? 'Save your battle casualties'
|
||||
: g.decision.kind === 'homeworldRevivalDeployment'
|
||||
? 'Revival deployment'
|
||||
: g.decision.kind === 'ecazSpice'
|
||||
@@ -2878,6 +2881,8 @@ export function GameTable({
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : g.decision.kind === 'sukRescue' ? (
|
||||
<SukGraduatePanel key={g.decision.event} decision={g.decision} act={act} busy={transportBusy || !!me.autopilot} />
|
||||
) : g.decision.kind === 'battleLosses' ? (
|
||||
<>
|
||||
<p className="muted">
|
||||
|
||||
@@ -85,9 +85,11 @@ export function LeaderSkillsPanel({
|
||||
if (!skills) return null;
|
||||
|
||||
const offer = skills.offer;
|
||||
const availableSkills =
|
||||
offer?.cards.filter((skill) => !skills.unavailableSkills?.[skill]) ?? [];
|
||||
const activeSkill =
|
||||
offer?.cards.find((skill) => skill === selectedSkill) ??
|
||||
offer?.cards[0] ??
|
||||
availableSkills.find((skill) => skill === selectedSkill) ??
|
||||
availableSkills[0] ??
|
||||
null;
|
||||
const activeLeader =
|
||||
offer?.leader ??
|
||||
@@ -110,7 +112,7 @@ export function LeaderSkillsPanel({
|
||||
This development-only module currently connects the card-role strength
|
||||
modifiers for Warmaster, Master of Assassins, Swordmaster of Ginaz,
|
||||
Killer Medic and Prana-Bindu Adept, plus Planetologist movement and
|
||||
Special-card battle use. Other Leader Skill effects are
|
||||
Special-card battle use and Suk Graduate force rescue. Other Leader Skill effects are
|
||||
unfinished.
|
||||
</p>
|
||||
</div>
|
||||
@@ -197,6 +199,7 @@ export function LeaderSkillsPanel({
|
||||
<div className="grid gap-3">
|
||||
{offer.cards.map((skill) => {
|
||||
const card = leaderSkillCard(skill);
|
||||
const unavailable = skills.unavailableSkills?.[skill];
|
||||
return (
|
||||
<div key={skill} className="space-y-2">
|
||||
<label className="flex min-h-11 items-center gap-2">
|
||||
@@ -205,14 +208,15 @@ export function LeaderSkillsPanel({
|
||||
name="leader-skill-card"
|
||||
value={skill}
|
||||
checked={activeSkill === skill}
|
||||
disabled={busy}
|
||||
disabled={busy || !!unavailable}
|
||||
onChange={() => setSelectedSkill(skill)}
|
||||
/>
|
||||
Keep {card.name}
|
||||
</label>
|
||||
{unavailable && <p className="fine">{unavailable}</p>}
|
||||
<LeaderSkillCard
|
||||
skill={skill}
|
||||
context="Your private offer"
|
||||
context={unavailable ? 'Your private offer · Unavailable' : 'Your private offer'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { Action, Decision } from '@/game/engine';
|
||||
import type { SukRescueOption } from '@/game/suk-graduate';
|
||||
import { splitLocation } from '@/game/board';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function sukRescueLabel(option: SukRescueOption): string {
|
||||
const total = option.normal + option.elite;
|
||||
if (!total) return 'Do not rescue any forces';
|
||||
const kept = option.kept
|
||||
? `keep 1 ${option.kept.kind === 'normal' ? 'ordinary' : 'elite'} in sector ${splitLocation(option.kept.key).sector}; `
|
||||
: '';
|
||||
return `Save ${option.normal} ordinary + ${option.elite} elite: ${kept}return ${total - (option.kept ? 1 : 0)} to reserves`;
|
||||
}
|
||||
|
||||
export function SukGraduatePanel({ decision, act, busy }: {
|
||||
decision: Extract<Decision, { kind: 'sukRescue' }>;
|
||||
act: (action: Action) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
const [choice, setChoice] = useState<number | null>(null);
|
||||
return <section aria-label="Suk Graduate rescue" className="space-y-3">
|
||||
<h3>Suk Graduate rescue</h3>
|
||||
<p>{decision.mode === 'normal'
|
||||
? 'Your skilled leader remained face up. Return one of your selected casualties to reserves instead of the Tanks.'
|
||||
: 'Your skilled leader survived. You may save up to three selected casualties. One stays in its original sector; the rest return to reserves.'}</p>
|
||||
<p className="muted">Ordinary and elite counters keep their identity. Your paid battle support is unchanged.</p>
|
||||
<label className="block">Forces to save
|
||||
<select aria-label="Forces to save with Suk Graduate" className="w-full" disabled={busy}
|
||||
value={choice ?? ''} onChange={(event) => setChoice(event.target.value === '' ? null : Number(event.target.value))}>
|
||||
<option value="">Choose your rescue</option>
|
||||
{decision.options.map((option, index) => <option key={index} value={index}>{sukRescueLabel(option)}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{choice !== null && <p>{sukRescueLabel(decision.options[choice])}</p>}
|
||||
<Button disabled={busy || choice === null} onClick={() => {
|
||||
if (choice !== null && !busy) act({ type: 'decision', event: decision.event, choice });
|
||||
}}>Resolve rescue</Button>
|
||||
</section>;
|
||||
}
|
||||
Reference in new issue
Block a user