Seal exact native Homeworld Reinforcements costs and plan sources
This commit is contained in:
1 parent
05c4c8c7fb
commit
c64c08af89
32 files changed
+1484
-119
No files matched your search
@@ -3,8 +3,9 @@ import { useId } from 'react';
|
||||
import { HelpTip } from './help-tip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { TRUTH_CARD_NAMES } from '@/game/truthtrance';
|
||||
import { TRUTH_CARD_NAMES } from '@/game/truthtrance-card-names';
|
||||
import { planClaimText, type PlanClaim } from '@/game/battle-promises';
|
||||
import { reinforcementsSourceLabel } from './reinforcements-sources';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import type { Action, GameView, Plan } from '@/game/engine';
|
||||
|
||||
@@ -229,6 +230,9 @@ export function BattlePromises({
|
||||
<br />
|
||||
Kwisatz: {plan.kwisatz ? 'Included' : 'Not included'}
|
||||
</p>
|
||||
{plan.reinforcementsSources && (
|
||||
<p className="fine">Native reserve fee: {reinforcementsSourceLabel(g, plan.reinforcementsSources)}</p>
|
||||
)}
|
||||
<p className="fine">
|
||||
These choices satisfy your current commitments. Review and change
|
||||
the other elements before sealing your plan.
|
||||
|
||||
@@ -61,6 +61,8 @@ import { nativeShipmentSources } from '@/game/homeworld-options';
|
||||
import { botBattleChoices } from '@/game/bot-battle-choices';
|
||||
import { specialForceName, maxCombatDial, maxCombatSupport } from '@/game/combat';
|
||||
import type { NativeReserveSelections } from '@/game/homeworld-native-reserves';
|
||||
import { reinforcementsBattleForces } from '@/game/reinforcements-homeworld';
|
||||
import { ReinforcementsSources, reinforcementsSourceLabel } from './reinforcements-sources';
|
||||
import {
|
||||
StrongholdCardGallery,
|
||||
StrongholdCopyChoice,
|
||||
@@ -436,7 +438,26 @@ export function GameTable({
|
||||
Number.isFinite(stormDial) ? Math.trunc(stormDial) : 0,
|
||||
),
|
||||
);
|
||||
const ownBattleForces = g.battle?.ownForces;
|
||||
const selectedWeaponId = battlePlanCardValue(g, 'weapon', weapon);
|
||||
const selectedDefenseId = battlePlanCardValue(g, 'defense', defense);
|
||||
const selectedBattleWeapon = me.hand?.find(c => c.id === selectedWeaponId);
|
||||
const selectedBattleDefense = me.hand?.find(c => c.id === selectedDefenseId);
|
||||
const reinforcementsSelected = selectedWeaponId === 'ecaz-reinforcements' ||
|
||||
selectedDefenseId === 'ecaz-reinforcements';
|
||||
const [reinforcementsSourceDraft, setReinforcementsSourceDraft] = useState<{
|
||||
key: string; index: number;
|
||||
} | null>(null);
|
||||
const reinforcementsSourceChoices = g.battle?.reinforcements?.homeworldSources;
|
||||
const reinforcementsSourceKey = reinforcementsSourceChoices
|
||||
? `${g.code}:${me.id}:${g.battle?.event ?? ''}:${JSON.stringify(reinforcementsSourceChoices)}` : '';
|
||||
const reinforcementsSourceIndex = reinforcementsSourceDraft?.key === reinforcementsSourceKey
|
||||
? Math.min(reinforcementsSourceDraft.index, Math.max(0, (reinforcementsSourceChoices?.length ?? 1) - 1)) : 0;
|
||||
const selectedReserveSource = reinforcementsSourceChoices?.[reinforcementsSourceIndex];
|
||||
const rawOwnBattleForces = g.battle?.ownForces;
|
||||
const ownBattleForces = rawOwnBattleForces && reinforcementsSelected &&
|
||||
selectedReserveSource && g.battle?.native === me.id
|
||||
? reinforcementsBattleForces(rawOwnBattleForces, selectedReserveSource[g.battle!.territory])
|
||||
: rawOwnBattleForces;
|
||||
const occupyProfile = ecazOccupyOwnProfile(g);
|
||||
const battleDialMinimum = occupyProfile?.fixedEcazDial ?? 0;
|
||||
const battleFreeSupport = ownBattleForces?.freeSupport ??
|
||||
@@ -542,15 +563,10 @@ export function GameTable({
|
||||
selectedLeader: committed.leader ? String(committed.leader.value ?? '') : leader,
|
||||
card,
|
||||
});
|
||||
const selectedWeaponId = battlePlanCardValue(g, 'weapon', weapon);
|
||||
const selectedDefenseId = battlePlanCardValue(g, 'defense', defense);
|
||||
const selectedBattleWeapon = me.hand?.find(c => c.id === selectedWeaponId);
|
||||
const selectedBattleDefense = me.hand?.find(c => c.id === selectedDefenseId);
|
||||
const reinforcementsSelected = selectedWeaponId === 'ecaz-reinforcements' ||
|
||||
selectedDefenseId === 'ecaz-reinforcements';
|
||||
const reinforcementsPlanReason = !reinforcementsSelected ? null :
|
||||
!g.battle?.reinforcements ? 'Reinforcements is unavailable in this battle.' :
|
||||
g.battle.reinforcements.blocked;
|
||||
g.battle.reinforcements.blocked ??
|
||||
(g.homeworlds && !selectedReserveSource ? 'Choose the exact native Reinforcements reserve sources.' : null);
|
||||
const selectedBattleKwisatz = kwisatz && !!me.kwisatz?.active &&
|
||||
!me.kwisatz.dead && !g.battle?.kwisatzBlocked &&
|
||||
(!me.kwisatz.usedAt || me.kwisatz.usedAt === g.battle?.territory);
|
||||
@@ -5105,6 +5121,12 @@ export function GameTable({
|
||||
setWeapon(plan.weapon ?? '');
|
||||
setDefense(plan.defense ?? '');
|
||||
setKwisatz(!!plan.kwisatz);
|
||||
if (plan.reinforcementsSources) {
|
||||
const chosen = JSON.stringify(plan.reinforcementsSources);
|
||||
const index = reinforcementsSourceChoices?.findIndex(choice => JSON.stringify(choice) === chosen);
|
||||
if (index !== undefined && index >= 0)
|
||||
setReinforcementsSourceDraft({ key: reinforcementsSourceKey, index });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -5394,6 +5416,11 @@ export function GameTable({
|
||||
{g.battle.reinforcements.blocked && ` ${g.battle.reinforcements.blocked}`}
|
||||
</p>
|
||||
)}
|
||||
{reinforcementsSelected && reinforcementsSourceChoices && (
|
||||
<ReinforcementsSources game={g} choices={reinforcementsSourceChoices}
|
||||
selected={reinforcementsSourceIndex} busy={busy}
|
||||
onChange={index => setReinforcementsSourceDraft({ key: reinforcementsSourceKey, index })} />
|
||||
)}
|
||||
{g.battle.reinforcements?.noField && <p className="fine">
|
||||
Provisional native order: your No-Field first materializes{' '}
|
||||
{g.battle.reinforcements.noField.materializes} forces, leaving{' '}
|
||||
@@ -5505,6 +5532,8 @@ export function GameTable({
|
||||
leader,
|
||||
weapon,
|
||||
defense,
|
||||
...(reinforcementsSelected && selectedReserveSource
|
||||
? { reinforcementsSources: selectedReserveSource } : {}),
|
||||
}),
|
||||
!!occupyControl?.blocked || !!stonePlanReason || !!harassControl.blocked || !!smugglerPlanReason || !!reinforcementsPlanReason || !battlePairValid,
|
||||
)}
|
||||
@@ -5520,6 +5549,9 @@ export function GameTable({
|
||||
spice committed.
|
||||
</p>
|
||||
)}
|
||||
{g.battle.plans[me.id]?.reinforcementsSources && (
|
||||
<p className="fine">Sealed reserve fee: {reinforcementsSourceLabel(g, g.battle.plans[me.id].reinforcementsSources!)}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useId, type ReactElement } from 'react';
|
||||
import type { GameView } from '@/game/engine';
|
||||
import type { MixedShipmentExpression } from '@/game/mixed-shipment-question';
|
||||
import { TRUTH_CARD_NAMES, type TruthFact } from '@/game/truthtrance';
|
||||
import type { TruthFact } from '@/game/truthtrance';
|
||||
import { TRUTH_CARD_NAMES } from '@/game/truthtrance-card-names';
|
||||
import { SHIPMENT_EXPRESSION_MAX_DEPTH, SHIPMENT_EXPRESSION_MAX_LEAVES } from '@/game/shipment-promises';
|
||||
import { isKnowledgeFact, knowledgeFactInputError } from '@/game/truthtrance-knowledge';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HelpTip } from './help-tip';
|
||||
import { faction } from '@/game/catalog';
|
||||
import { leaderStrengthLabel } from '@/game/cards';
|
||||
import { BattleComponentInspection } from './battle-component-inspection';
|
||||
import { reinforcementsSourceLabel } from './reinforcements-sources';
|
||||
|
||||
export function PrivateBattlePlan({ game }: { game: GameView }) {
|
||||
const insight = game.battle?.fullPlanInsight;
|
||||
@@ -74,6 +75,12 @@ export function PrivateBattlePlan({ game }: { game: GameView }) {
|
||||
<dt>Kwisatz Haderach</dt>
|
||||
<dd>{insight.plan.kwisatz ? 'Included' : 'Not included'}</dd>
|
||||
</div>
|
||||
{insight.plan.reinforcementsSources && (
|
||||
<div>
|
||||
<dt>Native Reinforcements fee</dt>
|
||||
<dd>{reinforcementsSourceLabel(game, insight.plan.reinforcementsSources)}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
<BattleComponentInspection
|
||||
playerName={
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import type { GameView } from '@/game/engine';
|
||||
import type { NativeReserveSelections } from '@/game/homeworld-native-reserves';
|
||||
import { homeworldName } from '@/game/homeworld-shipment-options';
|
||||
|
||||
export function reinforcementsSourceLabel(game: GameView, sources: NativeReserveSelections): string {
|
||||
return Object.entries(sources).map(([world, forces]) =>
|
||||
`${homeworldName(game, world)}: ${forces.normal} normal + ${forces.elite} special`).join('; ');
|
||||
}
|
||||
|
||||
export function ReinforcementsSources({ game, choices, selected, onChange, busy }: {
|
||||
game: GameView;
|
||||
choices: readonly NativeReserveSelections[];
|
||||
selected: number;
|
||||
onChange: (index: number) => void;
|
||||
busy: boolean;
|
||||
}) {
|
||||
if (!choices.length) return null;
|
||||
return <fieldset disabled={busy} className="space-y-3 rounded-lg border border-[#a88b60]/50 p-3">
|
||||
<legend>Reinforcements reserve sources</legend>
|
||||
<p className="fine">Three original native reserves go to the Tanks, normal first. These are not the invading army or future Harass returns.</p>
|
||||
{choices.length > 1 ? <label className="block space-y-1">
|
||||
<span>Native reserve fee allocation</span>
|
||||
<select value={selected} onChange={event => onChange(Number(event.target.value))}>
|
||||
{choices.map((sources, index) => <option key={index} value={index}>
|
||||
{reinforcementsSourceLabel(game, sources)}
|
||||
</option>)}
|
||||
</select>
|
||||
</label> : <p className="fine">{reinforcementsSourceLabel(game, choices[0])}</p>}
|
||||
{choices.length > 1 && <p className="fine" aria-live="polite">
|
||||
Selected fee: {reinforcementsSourceLabel(game, choices[selected])}.
|
||||
</p>}
|
||||
<p className="notice">Provisional own-World composition: counters paying the fee cannot also fund your physical dial. The original printed face and support traits stay fixed; the bonus adds no physical counters.</p>
|
||||
</fieldset>;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ export function ShipmentPromises({
|
||||
id => id === CHEAP_HERO_TRAITOR
|
||||
? 'Cheap Hero / Heroine'
|
||||
: g.allLeaders.find(leader => leader.id === id)?.name ?? id,
|
||||
id => g.combatLocations.find(place => place.id === id)!.name,
|
||||
)
|
||||
: `Will you ${shipmentClaimText(p)} this turn?`}
|
||||
</li>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useId } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { TRUTH_CARD_NAMES } from '@/game/truthtrance';
|
||||
import { TRUTH_CARD_NAMES } from '@/game/truthtrance-card-names';
|
||||
import type { CardCountFact } from '@/game/truthtrance-card-count';
|
||||
import {
|
||||
HAND_INVENTORY_LABELS,
|
||||
|
||||
@@ -95,6 +95,7 @@ export function Truthtrance({
|
||||
? 'Cheap Hero / Heroine'
|
||||
: (g.allLeaders.find((l) => l.id === id)?.name ?? id);
|
||||
const name = (id: string) => g.players.find((p) => p.id === id)!.name;
|
||||
const locationName = (id: string) => g.combatLocations.find(place => place.id === id)!.name;
|
||||
const question: TruthQuestion =
|
||||
kind === 'fact'
|
||||
? {
|
||||
@@ -183,7 +184,7 @@ export function Truthtrance({
|
||||
</p>
|
||||
{w.question && (
|
||||
<blockquote>
|
||||
<p>{truthQuestionText(w.question, leaderName)}</p>
|
||||
<p>{truthQuestionText(w.question, leaderName, locationName)}</p>
|
||||
<p className="fine">Asked of {name(w.question.target)}</p>
|
||||
</blockquote>
|
||||
)}
|
||||
@@ -366,10 +367,10 @@ export function Truthtrance({
|
||||
)}
|
||||
<p className="notice" aria-label="Public grouped question preview">
|
||||
{kind === 'mixedShipment'
|
||||
? invalidMixed ?? truthQuestionText(question, leaderName)
|
||||
? invalidMixed ?? truthQuestionText(question, leaderName, locationName)
|
||||
: invalidShipment
|
||||
? 'Enter a whole minimum of one to twenty forces.'
|
||||
: truthQuestionText(question, leaderName)}
|
||||
: truthQuestionText(question, leaderName, locationName)}
|
||||
</p>
|
||||
</>
|
||||
) : kind === 'battlePlan' ? (
|
||||
@@ -404,7 +405,7 @@ export function Truthtrance({
|
||||
</fieldset>
|
||||
))}
|
||||
<p className="notice">
|
||||
{truthQuestionText(question, leaderName)}
|
||||
{truthQuestionText(question, leaderName, locationName)}
|
||||
</p>
|
||||
{g.battle &&
|
||||
![g.battle.attacker, g.battle.defender].includes(
|
||||
@@ -436,7 +437,7 @@ export function Truthtrance({
|
||||
<p className="notice">
|
||||
{invalidFact
|
||||
? 'Resolve the marked fields before asking this question.'
|
||||
: truthQuestionText(question, leaderName)}
|
||||
: truthQuestionText(question, leaderName, locationName)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
@@ -514,6 +515,7 @@ export function TruthHistory({ game: g }: { game: GameView }) {
|
||||
id === CHEAP_HERO_TRAITOR
|
||||
? 'Cheap Hero / Heroine'
|
||||
: (g.allLeaders.find((l) => l.id === id)?.name ?? id),
|
||||
id => g.combatLocations.find(place => place.id === id)!.name,
|
||||
)}
|
||||
</p>
|
||||
<strong>
|
||||
|
||||
Reference in new issue
Block a user