Compose Harass withdrawal with provisional Stone Burner timing

This commit is contained in:
Quintine committed 2026-10-16 06:35:04 +11:00
1 parent 5d13374f7e
commit feb2b03251
23 files changed
+1362 -124

No files matched your search

+12 -2
View File
@@ -163,7 +163,7 @@ import {
isMirrorWeapon,
} from '@/game/battle-cards';
import { validBattleSlotPair } from '@/game/battle-card-slots';
import { stoneBurnerPlanBlock } from '@/game/stone-burner';
import { stoneBurnerPlanBlock, stoneBurnerWithdrawalContext } from '@/game/stone-burner';
import { mirrorWeaponModeBlock } from '@/game/mirror-weapon-mode';
import { TECH_TOKENS, ownedTech } from '@/game/tech-tokens';
import {
@@ -577,6 +577,11 @@ export function GameTable({
);
const mirrorSelected = isMirrorWeapon(selectedSpecialWeapon);
const stoneContext = g.battle?.stoneBurnerContext;
const stoneCandidateContext = selectedSpecialWeapon &&
(selectedWeaponId === 'ecaz-harass-withdraw' || selectedDefenseId === 'ecaz-harass-withdraw')
? stoneBurnerWithdrawalContext(stoneContext?.occupy,
g.battle!.attacker === me.id ? 'attacker' : 'defender', g.battle?.harassWithdraw?.occupy)
: stoneContext?.occupy;
const stonePlanReason = !selectedSpecialWeapon
? null
: ((mirrorSelected ? mirrorWeaponModeBlock(g) : null) ?? stoneContext?.blocked ??
@@ -595,7 +600,7 @@ export function GameTable({
g.battle!.tieWinner === g.battle!.attacker
? 'attacker'
: 'defender',
stoneContext.occupy,
stoneCandidateContext,
),
)
.find((reason) => reason !== null) ?? null)));
@@ -3083,6 +3088,11 @@ export function GameTable({
still apply.
</p>
{g.decision.copiedFrom && <p className="fine">Your physical Mirror Weapon copied the opposing Stone Burner. Your choice is independent of its owner’s; winning retains Mirror as Mirror.</p>}
<p className="fine">
Provisional Harass &amp; Withdraw timing: compare undialed
forces remaining after a successful withdrawal. An opposing
successful Traitor cancels that withdrawal.
</p>
<a href="/rules?topic=stone-burner">
Read Stone Burner rules
</a>
+5
View File
@@ -81,6 +81,11 @@ export function HarassWithdrawGuide({
Your leader can still die. An opponent’s successful traitor call cancels
the withdrawal. Discard this card after use.
</p>
<p className="fine">
Provisional Stone Burner timing: compare undialed forces remaining after
a successful withdrawal, not the forces already returned to reserves.
In Occupy, the other ally’s undialed forces still count.
</p>
</section>
);
}
+22 -1
View File
@@ -2,7 +2,7 @@ import type { GameView } from '@/game/engine';
import { faction } from '@/game/catalog';
import { leaderStrengthLabel } from '@/game/cards';
import { isStoneBurner } from '@/game/battle-cards';
import { stoneBurnerComparison } from '@/game/stone-burner';
import { stoneBurnerComparison, stoneBurnerWithdrawalContext } from '@/game/stone-burner';
import { RevealedPlanPieces } from './revealed-plan-pieces';
import { KwisatzInspector } from './kwisatz-inspector';
export function RevealedBattle({ game }: { game: GameView }) {
@@ -10,6 +10,18 @@ export function RevealedBattle({ game }: { game: GameView }) {
if (!battle?.revealed) return null;
const attackerPlan = battle.plans[battle.attacker];
const defenderPlan = battle.plans[battle.defender];
let stoneContext = battle.stoneBurnerContext?.occupy;
const withdrawing = [battle.attacker, battle.defender].filter(id => {
const plan = battle.plans[id];
return plan && [plan.weapon, plan.defense].includes('ecaz-harass-withdraw');
});
for (const id of withdrawing) {
const occupy = battle.ecazOccupy?.lead === id ? {
cardUser: id, ecaz: battle.ecazOccupy.ecaz, canceled: battle.ecazOccupy.canceled,
} : undefined;
stoneContext = stoneBurnerWithdrawalContext(stoneContext,
id === battle.attacker ? 'attacker' : 'defender', occupy);
}
const comparisons =
battle.cards.some(isStoneBurner) &&
battle.ownForces &&
@@ -23,6 +35,8 @@ export function RevealedBattle({ game }: { game: GameView }) {
game.me === battle.defender ? battle.ownForces! : opponent,
defenderPlan.dial,
defenderPlan.support,
battle.tieWinner === battle.attacker ? 'attacker' : 'defender',
stoneContext,
),
)
: [];
@@ -37,6 +51,13 @@ export function RevealedBattle({ game }: { game: GameView }) {
<h3>Revealed battle plans</h3>
<a className="battle-display-link" href="#table-decisions">Return to battle decisions</a>
<p className="revealed-plans-help">Both plans are public. Inspect any played component; use the decision controls to resolve the battle. <a href="/rules?topic=revealed-battle-components#revealed-battle-components">Component guide</a> · <a href="/rules?topic=battle#battle">Battle rules</a></p>
{comparisons.length > 0 && withdrawing.length > 0 && (
<p className="fine">
Provisional Stone Burner counts below assume Harass &amp; Withdraw
succeeds. An opposing successful Traitor cancels withdrawal and decides
the battle instead.
</p>
)}
<div className="revealed-plan-pair">
{[battle.attacker, battle.defender].map((id) => {
const plan = battle.plans[id];