// screens_combat.jsx — Combat & resolution screens // Event, Intercept, Battle, Plunder — the screens where the world acts on you // and you must resolve the current situation before navigating freely. // // Depends on: window.D, window.L, window.E, window.UI, window.S // Exposes: EventScreen, InterceptScreen, BattleScreen, PlunderScreen on window.S // // Loaded after screens_voyage.jsx so it can pick up MapScreen/SailingScreen // dependencies if needed (currently none — fully self-contained). window.S = window.S || {}; (() => { const { useState, useRef, useEffect, useMemo } = React; const { PORTS, SHIPS, FACTIONS } = window.D; const L = window.L; const A = window.E.A; const { T, panelStyle, Bar, Pill, Btn, SectionTitle, EmptyState, TutorialPopup, BackButton, Panel, IconSailboat, IconAnchor, IconSwords, IconCannon, IconTarget, IconGrapple, IconWind, IconSkull, getGoodIcon, useFlashOnChange, TransferLayout, ShipSideSprite, FactionPill, ShipSprite, } = window.UI; const { shouldShowTutorial, markTutorialSeen } = window.L; // Import distance-based action lookup const { LEGAL_ACTIONS_BY_DISTANCE } = window.D; // ── Action preview helper ────────────────────────────────────────────── function getActionPreview(state, action, distance, enemy, battle = null) { const shipStats = L.getShipStats(state); const cannons = shipStats.cannons; const mult = window.D.DISTANCE_DAMAGE_MULTIPLIERS[action]?.[distance] || 1.0; const hullDmgPct = L.getEquipmentEffect(state, "hullDmgPct") || 0; const crewDmgPct = L.getEquipmentEffect(state, "crewDmgPct") || 0; // Broadside: cannons * (0.8–1.2) * mult if (action === "broadside") { const baseMin = cannons * 0.8 * mult; const baseMax = cannons * 1.2 * mult; const hullMin = Math.max(1, Math.floor(baseMin * 0.6 * (1 + hullDmgPct))); const hullMax = Math.max(1, Math.floor(baseMax * 0.6 * (1 + hullDmgPct))); const crewMin = Math.floor(baseMin * 0.4 / 3 * (1 + crewDmgPct)); const crewMax = Math.floor(baseMax * 0.4 / 3 * (1 + crewDmgPct)); return { description: "Full cannon volley. Reliable damage.", hullRange: [hullMin, hullMax], crewRange: [crewMin, crewMax], hitChance: 1.0, }; } // Precision: cannons * (1.2–1.8) * mult (fixed) if (action === "precision") { const hitChance = Math.min(1, 0.7 + (L.getEquipmentEffect(state, "precisionHitPct") || 0)); const baseMin = cannons * 1.2 * mult; const baseMax = cannons * 1.8 * mult; const hullMin = Math.max(1, Math.floor(baseMin * 0.9 * (1 + hullDmgPct))); const hullMax = Math.max(1, Math.floor(baseMax * 0.9 * (1 + hullDmgPct))); const crewMin = Math.floor(baseMin * 0.1 / 3 * (1 + crewDmgPct)); const crewMax = Math.floor(baseMax * 0.1 / 3 * (1 + crewDmgPct)); return { description: "Aimed shot. High damage if it hits.", hullRange: [hullMin, hullMax], crewRange: [crewMin, crewMax], hitChance, }; } // Continue Fighting – deterministic (fixed) if (action === "continue_fighting" && battle) { const ratio = L.getBoardingRatio(state, battle, enemy); const crew = battle.playerCrew; const enemyCrew = battle.enemyCrew; const playerLoss = Math.ceil(crew * 0.15 * (1 - ratio)); const enemyLoss = Math.ceil(enemyCrew * 0.15 * ratio); return { description: "Press the attack in boarding.", crewLossPlayer: playerLoss, crewLossEnemy: enemyLoss, advantage: Math.round(ratio * 100), hitChance: null, }; } // All other actions (static descriptions) const staticDescriptions = { grapple: "Board the enemy ship. Requires Close range.", evade: "Attempt to flee. Speed check.", close_distance: "Move closer to the enemy.", open_distance: "Move further away.", fall_back: "Return to naval combat. Costs crew.", demand_surrender: "Force them to yield (requires advantage).", surrender: "Yield to the enemy.", }; return { description: staticDescriptions[action] || "", hullRange: null, crewRange: null, hitChance: null, }; } // ── Detect if the player's action missed or failed ────────────────── const MISS_PHRASES = [ "splashes harmlessly", "goes wide", "overcorrect and miss", "flies past the enemy", "Your grapple fails", "repels your boarders", "thrown back", ]; const isPlayerMissOrFail = (text) => { if (!text) return false; return MISS_PHRASES.some(phrase => text.includes(phrase)); }; // Equipment that has visual representation on the ship sprite const VISUAL_EQUIPMENT = ["war_pennants", "extra_sails", "lateen_rig"]; const getVisualEquipment = (state) => { const allEquipped = [ ...(state.ship.equipment?.hull || []), ...(state.ship.equipment?.armament || []), ...(state.ship.equipment?.rigging || []), ...(state.ship.equipment?.special || []), ]; return allEquipped.filter(key => VISUAL_EQUIPMENT.includes(key)); }; // ── EVENT SCREEN ───────────────────────────────────────────────────── function EventScreen({ state, dispatch }) { const ev = state.activeEvent; if (!ev) return null; const typeColor = { hazard: T.redBr, choice: T.gold, reward: T.greenBr, crew: T.blueBr, faction: T.purpleBr, }; return (
{/* ── SVG Illustration ── */} {ev.svg && (
{ev.title} { // Hide the container if the SVG fails to load e.currentTarget.parentElement.style.display = "none"; }} />
)} {/* ── Existing Event UI ── */}
Day {state.day}
{ev.title}

{ev.desc}

{ev.choices.map((c, i) => ( dispatch({ type: A.RESOLVE_EVENT, choiceIndex: i })} onMouseEnter={e => e.currentTarget.style.borderColor = T.borderBr} onMouseLeave={e => e.currentTarget.style.borderColor = T.border} >
{c.label}
{c.outcome.log}
))}
); } // ── INTERCEPT SCREEN ────────────────────────────────────────────────── const InterceptScreen = ({ state, dispatch }) => { const session = state.encounterSession; if (!session || session.phase !== "intercept") return null; const { enemy, intercept } = session; const enemyShip = SHIPS[enemy.shipType || L.guessShipType(enemy)] || {}; // Memoize combat flavour lines to avoid regeneration on re-renders const combatFlavourLines = React.useMemo(() => { const disposition = session.aiDisposition ?? L.computeAIDisposition(state, enemy, session.type); return window.G.generateCombatFlavour(disposition); }, [session, state, enemy]); return (
⚠ ENCOUNTER
{/* Merged Flavour Panel: existing + AI combat flavour */}

{intercept.flavourText}

{combatFlavourLines.map((line, i) => (

{line}

))}
{/* Enemy details with ship sprite, faction & risk pills */}
{enemy.name}
{/* Ship Sprite */}
{[ ["Hull", `${enemy.hull}/${enemy.maxHull || enemy.hull}`], ["Cannons", enemy.cannons], ["Crew", enemy.crew], ["Speed", enemyShip.speed ?? "?"], ].map(([l, v]) => (
{l}
{v}
))}
{/* Options */}
CHOOSE YOUR RESPONSE:
{intercept.options.map(opt => (
opt.available && dispatch(opt.action)} style={{ width: "100%", textAlign: "left", opacity: opt.available ? 1 : 0.45 }} > {opt.label} {!opt.available && opt.reason && (
✗ {opt.reason}
)} {opt.id === "flee" && opt.available && opt.speedCheck && (
Speed check: your {opt.speedCheck.player} vs their {opt.speedCheck.enemy}
)}
))}
); }; // ── BATTLE SCREEN ───────────────────────────────────────────────────── function BattleScreen({ state, dispatch }) { const session = state.encounterSession; if (!session || session.phase !== "battle" || !session.battle) return null; const battle = session.battle; const enemy = session.enemy; const done = ["victory", "defeat", "fled"].includes(battle.phase); const isBoarding = battle.subPhase === "boarding"; const playerPct = battle.playerHull / SHIPS[state.ship.type].maxHull; const enemyPct = battle.enemyHull / enemy.hull; const [showTutorial, setShowTutorial] = React.useState( () => shouldShowTutorial(state, "battle") ); const [pulsedAction, setPulsedAction] = useState(null); // ── Boarding ratio (now using the same function as the resolver) ── const ratio = L.getBoardingRatio(state, battle, enemy); const playerRatioPct = Math.round(ratio * 100); const enemyRatioPct = 100 - playerRatioPct; // ── Boarding action availability ────────────────────────────── const canDemandSurrender = isBoarding && ratio >= 0.65; const demandSurrenderTooltip = !isBoarding ? "Not in boarding phase" : ratio < 0.65 ? `Need a clear advantage (${Math.round(ratio * 100)}% / 65% required)` : ""; // ── Action previews ────────────────────────────────────────────── const actionPreviews = useMemo(() => { const previews = {}; const actions = ["broadside", "precision", "grapple", "evade", "close_distance", "open_distance"]; if (isBoarding) { actions.push("continue_fighting", "fall_back", "demand_surrender", "surrender"); } actions.forEach(a => { if (isBoarding && ["broadside", "precision", "grapple", "evade", "close_distance", "open_distance"].includes(a)) { previews[a] = { description: "Not available in boarding.", hullRange: null, crewRange: null, hitChance: null }; } else { previews[a] = getActionPreview(state, a, battle.distance, enemy, isBoarding ? battle : null); } }); return previews; }, [state, battle.distance, enemy, isBoarding, battle]); const [missFlash, setMissFlash] = useState(false); const prevLogLen = useRef(battle.log?.length || 0); useEffect(() => { if (!battle) return; const newLen = battle.log.length; if (newLen > prevLogLen.current) { const latest = battle.log[newLen - 1] || ""; if (isPlayerMissOrFail(latest)) { setMissFlash(true); const timer = setTimeout(() => setMissFlash(false), 600); return () => clearTimeout(timer); } } prevLogLen.current = newLen; }, [battle?.log?.length]); const [isNarrowBattle, setIsNarrowBattle] = React.useState(window.innerWidth < 700); React.useEffect(() => { const handle = () => setIsNarrowBattle(window.innerWidth < 700); window.addEventListener("resize", handle); return () => window.removeEventListener("resize", handle); }, []); // ── Distance gating (naval only) ────────────────────────────── const legalActions = LEGAL_ACTIONS_BY_DISTANCE[battle.distance] || []; const isActionLegal = (action) => legalActions.includes(action); // ── Tooltip with crew check for Grapple ──────────────────────── const getActionTooltip = (action) => { if (action === "grapple") { const messages = []; const legal = isActionLegal(action); const isCrewZero = battle.playerCrew === 0; if (!legal) messages.push("Grapple requires Close distance."); if (isCrewZero) messages.push("No crew left to board with."); return messages.length > 0 ? messages.join(" ") : null; } if (isActionLegal(action)) return null; const map = { broadside: "Broadside is available at all distances", precision: "Precision is available at all distances", close_distance: "Close Distance requires Far or Medium distance", open_distance: "Open Distance requires Medium or Close distance", evade: "Evade requires Far distance", }; return map[action] || "Not available at this distance"; }; // ── Distance indicator component ──────────────────────────────── const DistanceIndicator = () => { const distances = ["far", "medium", "close"]; const labels = ["Far", "Medium", "Close"]; const currentIndex = distances.indexOf(battle.distance); return ( Distance: {battle.distance.toUpperCase()}
{distances.map((d, i) => { const isActive = i === currentIndex; const isPast = i < currentIndex; const dotColor = isActive ? T.gold : isPast ? T.goldDim : T.textFaint; return (
{i < distances.length - 1 && (
)}
); })}
{battle.distance === "far" && "Long range – cannons at full spread"} {battle.distance === "medium" && "Standard engagement range"} {battle.distance === "close" && "Point-blank – boarding range"} ); }; // ── Advantage Bar (boarding phase) ────────────────────────────── const AdvantageBar = () => { if (!isBoarding) return null; // Use the same ratio as the resolver const pPct = Math.round(ratio * 100); const ePct = 100 - pPct; // For display: effective crew counts (same as resolver) const playerEffective = Math.round(battle.playerCrew * (0.5 + state.crew.morale / 200)); const enemyMoraleStandin = { low: 50, medium: 65, high: 80, assault: 90 }[enemy.risk] ?? 60; const enemyEffective = Math.round(battle.enemyCrew * (0.5 + enemyMoraleStandin / 200)); return (
{pPct}%
{ePct}%
Your crew: {battle.playerCrew} Morale: {state.crew.morale}% Effective: {playerEffective}
Enemy crew: {battle.enemyCrew} Morale: {enemyMoraleStandin}% Effective: {enemyEffective}
); }; // ── Color maps for action buttons ────────────────────────────────── const navalColors = { broadside: T.redBr, precision: T.yellow, grapple: T.blueBr, evade: T.greenBr, open_distance: T.greenBr, close_distance: T.greenBr, }; const boardingColors = { continue_fighting: T.greenBr, fall_back: T.goldDim, demand_surrender: T.blueBr, surrender: T.redBr, }; return (
{showTutorial && ( { markTutorialSeen("battle", disableAll); setShowTutorial(false); }} > {isBoarding ? ( <>

You've grappled the enemy ship! Choose your action:

  • Continue Fighting — keep pressing the attack
  • Fall Back — retreat and return to naval combat
  • Demand Surrender — force them to yield (requires clear advantage)
  • Surrender — yield to the enemy

The advantage bar shows your relative boarding strength based on crew count and morale.

) : ( <>

Choose an action each round:

  • Broadside — reliable cannon volley
  • Precision — risky but devastating if it hits
  • Close Distance — move closer to the enemy
  • Open Distance — move further away
  • Grapple — board the enemy and move to Boarding phase of the combat.
  • Evade — attempt to flee the battle, depend on your ship speed.

Watch your hull and crew. If your hull reaches zero, you lose. Loosing all crew results in capture.

)}
)}
{isBoarding ? ( <> BOARDING ACTION — ROUND {battle.round} ) : ( <> NAVAL BATTLE — ROUND {battle.round} )}
{/* ── Ship panels (naval only) ────────────────────────────── */} {!isBoarding && ( (() => { const playerType = state.ship.type; const enemyType = L.guessShipType(enemy); const playerVisual = window.D.SHIP_VISUALS?.[playerType]; const enemyVisual = window.D.SHIP_VISUALS?.[enemyType]; const playerLen = playerVisual?.hullLength || 400; const enemyLen = enemyVisual?.hullLength || 400; const maxLen = Math.max(playerLen, enemyLen); const playerSize = playerLen / maxLen; const enemySize = enemyLen / maxLen; const baseW = isNarrowBattle ? 150 : 270; const baseH = isNarrowBattle ? 100 : 175; return (
{/* Player ship panel */}
{state.ship.name}
Hull: {battle.playerHull} / {SHIPS[state.ship.type].maxHull}
= 0.6 ? T.greenBr : playerPct >= 0.3 ? T.gold : T.redBr} h={10} /> {battle.convoyHull !== undefined && ( <>
Convoy Hull: {battle.convoyHull} / 50
= 0.6 ? T.greenBr : battle.convoyHull / 50 >= 0.3 ? T.gold : T.redBr} h={8} /> )}
{state.crew.roster.length} crew · {L.getShipStats(state).cannons} cannons
{/* Enemy ship panel */}
{enemy.name}
Hull: {battle.enemyHull} / {enemy.hull}
= 0.6 ? T.greenBr : enemyPct >= 0.3 ? T.gold : T.redBr} h={10} />
{battle.enemyCrew} crew · {enemy.cannons} cannons
); })() )} {/* ── Boarding: Advantage Bar ────────────────────────────── */} {isBoarding && } {/* ── Distance indicator (naval only) ────────────────────── */} {!isBoarding && } {isNarrowBattle && window.innerWidth < 400 && !isBoarding && (
Tip: rotate your phone to landscape for a better battle view
)} {/* ── Log panel ──────────────────────────────────────────── */}
{[...battle.log].reverse().map((e, i) => { const isLatest = i === 0; const isMissFlash = isLatest && missFlash && isPlayerMissOrFail(e); return (
{e}
); })}
{!done ? (
{isBoarding ? ( // ── Boarding Actions ──────────────────────────────────
BOARDING ACTIONS — {playerRatioPct}% advantage
{[ { a: "continue_fighting", label: "Continue Fighting", desc: actionPreviews.continue_fighting?.description || "Press the attack" }, { a: "fall_back", label: "Fall Back", desc: actionPreviews.fall_back?.description || "Return to naval combat" }, { a: "demand_surrender", label: "Demand Surrender", desc: actionPreviews.demand_surrender?.description || "Force them to yield", disabled: !canDemandSurrender, tooltip: demandSurrenderTooltip }, { a: "surrender", label: "Surrender", desc: actionPreviews.surrender?.description || "Yield to the enemy" }, ].map(({ a, label, desc, disabled = false, tooltip = "" }) => { const preview = actionPreviews[a] || {}; const playerLoss = preview.crewLossPlayer; const enemyLoss = preview.crewLossEnemy; const adv = preview.advantage !== undefined ? `Advantage: ${preview.advantage}%` : ""; const info = a === "continue_fighting" ? `You lose: ${playerLoss} crew · Enemy loses: ${enemyLoss} crew ${adv ? ` · ${adv}` : ''}` : a === "fall_back" ? `You lose: ${playerLoss} crew` : ""; return ( { if (disabled) return; dispatch({ type: A.BATTLE_ACTION, action: a }); setPulsedAction(a); setTimeout(() => setPulsedAction(null), 150); }} onMouseEnter={e => { if (disabled) return; const color = boardingColors[a]; e.currentTarget.style.borderColor = color; e.currentTarget.style.boxShadow = `0 0 14px ${color}55`; e.currentTarget.style.transform = "scale(1.03)"; }} onMouseLeave={e => { if (disabled) return; e.currentTarget.style.borderColor = ''; e.currentTarget.style.boxShadow = "none"; e.currentTarget.style.transform = "scale(1)"; }} title={tooltip || ""} >
{label}
{desc}
{info && (
{info}
)} {disabled && tooltip && (
✗ {tooltip}
)}
); })}
) : ( // ── Naval Actions ────────────────────────────────────
CHOOSE YOUR ACTION:
{[ { a: "broadside", label: , lbl: " Broadside", desc: actionPreviews.broadside?.description || "Full cannon volley" }, { a: "precision", label: , lbl: " Precision", desc: actionPreviews.precision?.description || "Aimed shot" }, { a: "grapple", label: , lbl: " Grapple", desc: actionPreviews.grapple?.description || "Board the enemy" }, ].map(({ a, label, lbl, desc }) => { const legal = isActionLegal(a); const isCrewZero = a === "grapple" && battle.playerCrew === 0; const disabled = !legal || isCrewZero; // Determine the tooltip and inline message let tooltip = null; if (disabled) { const messages = []; if (!legal) messages.push("Grapple requires Close distance."); if (isCrewZero) messages.push("No crew left to board with."); tooltip = messages.join(" "); } const preview = actionPreviews[a] || {}; const hull = preview.hullRange ? `${preview.hullRange[0]}–${preview.hullRange[1]}` : null; const crew = preview.crewRange ? `${preview.crewRange[0]}–${preview.crewRange[1]}` : null; const hit = preview.hitChance !== null && preview.hitChance < 1 ? `Hit: ${Math.round(preview.hitChance*100)}%` : null; const infoParts = [hull ? `Hull: ${hull}` : null, crew ? `Crew: ${crew}` : null, hit].filter(Boolean); const info = infoParts.join(' · '); return ( { if (disabled) return; dispatch({ type: A.BATTLE_ACTION, action: a }); setPulsedAction(a); setTimeout(() => setPulsedAction(null), 150); }} onMouseEnter={e => { if (disabled) return; const color = navalColors[a]; e.currentTarget.style.borderColor = color; e.currentTarget.style.boxShadow = `0 0 14px ${color}55`; e.currentTarget.style.transform = "scale(1.03)"; }} onMouseLeave={e => { if (disabled) return; e.currentTarget.style.borderColor = ''; e.currentTarget.style.boxShadow = "none"; e.currentTarget.style.transform = "scale(1)"; }} title={tooltip || ""} >
{label}{lbl}
{desc}
{info && (
{info}
)} {disabled && tooltip && (
✗ {tooltip}
)}
); })}
{[ { a: "evade", label: , lbl: " Evade", desc: actionPreviews.evade?.description || "Flee if faster" }, { a: "open_distance", label: , lbl: " Open Distance", desc: actionPreviews.open_distance?.description || "Move further away" }, { a: "close_distance", label: , lbl: " Close Distance", desc: actionPreviews.close_distance?.description || "Move closer" }, ].map(({ a, label, lbl, desc }) => { const legal = isActionLegal(a); const tooltip = !legal ? (() => { const map = { close_distance: "Close Distance requires Far or Medium distance", open_distance: "Open Distance requires Medium or Close distance", evade: "Evade requires Far distance", }; return map[a] || "Not available at this distance"; })() : null; const preview = actionPreviews[a] || {}; return ( { if (!legal) return; dispatch({ type: A.BATTLE_ACTION, action: a }); setPulsedAction(a); setTimeout(() => setPulsedAction(null), 150); }} onMouseEnter={e => { if (!legal) return; const color = navalColors[a]; e.currentTarget.style.borderColor = color; e.currentTarget.style.boxShadow = `0 0 14px ${color}55`; e.currentTarget.style.transform = "scale(1.03)"; }} onMouseLeave={e => { if (!legal) return; e.currentTarget.style.borderColor = ''; e.currentTarget.style.boxShadow = "none"; e.currentTarget.style.transform = "scale(1)"; }} title={tooltip || ""} >
{label}{lbl}
{desc}
{!legal && tooltip && (
✗ {tooltip}
)}
); })}
)}
) : ( // ── Victory / Defeat / Fled ──────────────────────────────
{battle.phase === "victory" && (<> VICTORY!)} {battle.phase === "fled" && (<> ESCAPED)} {battle.phase === "defeat" && (<> DEFEATED)}
{battle.phase === "victory" && battle.canPlunder ? (
dispatch({ type: A.NAVIGATE, screen: "plunder" })}> Plunder the Ship dispatch({ type: A.DISMISS_BATTLE })}> Sail Away
) : ( <> {battle.phase === "victory" && battle.goldReward > 0 && (
+{battle.goldReward} gold
)} dispatch({ type: A.DISMISS_BATTLE })}> {session.returnScreen === "sailing" && state.destination && state.sailingDaysLeft > 0 ? "Continue Voyage" : session.returnScreen === "arrive" && state.destination ? "Enter Port" : "Return to Port"} )}
)}
); } // ── PLUNDER SCREEN ──────────────────────────────────────────────────── function PlunderScreen({ state, dispatch }) { const session = state.encounterSession; if (!session || session.phase !== "plunder") return null; const battle = session.battle; if (!battle || !battle.canPlunder) return null; const enemyCargo = battle.enemyCargo || {}; const goldReward = battle.goldReward || 0; const holdCapacity = L.getHoldCapacity(state) || 200; const [playerItems, setPlayerItems] = React.useState({ ...(state.hold?.items || {}) }); const [enemyItems, setEnemyItems] = React.useState({ ...enemyCargo }); const used = Object.values(playerItems).reduce((s, q) => s + q, 0); const free = Math.max(0, holdCapacity - used); const goodsValue = Object.entries(enemyItems).reduce((sum, [good, qty]) => { const res = window.D.RESOURCES[good]; const price = res?.basePrice ?? 0; return sum + price * (qty || 0); }, 0); const totalValue = goldReward + goodsValue; const totalFlash = useFlashOnChange(totalValue, { direction: 'up' }); const hasIllegal = Object.keys(enemyItems).some( g => window.D.RESOURCES[g]?.illegal ); const enemyTotal = Object.values(enemyItems).reduce((s, q) => s + q, 0); const moveToPlayer = (good) => { const available = enemyItems[good] || 0; if (available <= 0 || free < 1) return; setEnemyItems(prev => ({ ...prev, [good]: prev[good] - 1 })); setPlayerItems(prev => ({ ...prev, [good]: (prev[good] || 0) + 1 })); }; const moveToEnemy = (good) => { const available = playerItems[good] || 0; if (available <= 0) return; setPlayerItems(prev => ({ ...prev, [good]: prev[good] - 1 })); setEnemyItems(prev => ({ ...prev, [good]: (prev[good] || 0) + 1 })); }; const takeAll = () => { const priority = Object.entries(enemyItems) .map(([good, qty]) => ({ good, qty, price: window.D.RESOURCES[good]?.basePrice ?? 0 })) .filter(g => g.qty > 0) .sort((a, b) => b.price - a.price); let remainingFree = free; const newPlayer = { ...playerItems }; for (const { good, qty } of priority) { const takeQty = Math.min(qty, remainingFree); if (takeQty > 0) { newPlayer[good] = (newPlayer[good] || 0) + takeQty; remainingFree -= takeQty; } } dispatch({ type: window.E.A.TAKE_PLUNDER, holdItems: newPlayer }); }; const handleConfirm = () => { dispatch({ type: window.E.A.TAKE_PLUNDER, holdItems: playerItems }); }; return (
Plunder the {session.enemy.name}
{/* ── Top summary panel ──────────────────────────────────── */}
Plunder gold
+{goldReward}g
Cargo value
{goodsValue}g
Total haul
{totalValue}g
Take All
{hasIllegal && (
⚠ Illegal goods detected — patrols may inspect
)}
{/* ── Two‑column transfer layout ─────────────────────────── */} holdCapacity * 0.8 ? T.redBr : T.greenBr} h={8} />
{Object.keys(playerItems).length === 0 ? ( ) : ( Object.entries(playerItems).map(([good, qty]) => (
{getGoodIcon(good)} {window.D.RESOURCES[good]?.name || good} ×{qty} moveToEnemy(good)}>Jettison
)) )}
} rightTitle="ENEMY CARGO" rightContent={ Object.keys(enemyItems).length === 0 ? ( ) : ( (() => { let illegalDividerShown = false; return Object.entries(enemyItems).map(([good, qty]) => { const isIllegal = window.D.RESOURCES[good]?.illegal; const showDivider = isIllegal && !illegalDividerShown; if (showDivider) illegalDividerShown = true; return ( {showDivider && (
)}
{getGoodIcon(good)} {window.D.RESOURCES[good]?.name || good} {isIllegal && } ×{qty} moveToPlayer(good)} disabled={free < 1}>+ Take
); }); })() ) } /> {/* ── Confirm ────────────────────────────────────────────── */}
Plunder gold: +{goldReward}g
Confirm Plunder
); } Object.assign(window.S, { EventScreen, InterceptScreen, BattleScreen, PlunderScreen }); })();