From 4e55ab9e86a91d07626739b524b9a79a8d603f45 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:30:48 +1100 Subject: [PATCH] Allow selective mobile stronghold spice collection --- components/mobile-stronghold.tsx | 44 ++++-- docs/CURRENT_STATUS.md | 8 ++ docs/IMPLEMENTATION_STATUS.md | 10 ++ docs/IX_PROTOTYPE.md | 23 ++++ docs/RULE_DECISIONS.md | 10 ++ game/engine.ts | 40 +++++- game/reference.ts | 2 +- ...le-stronghold-collection-controls.test.tsx | 122 +++++++++++++++++ ...bile-stronghold-collection-runtime.test.ts | 126 ++++++++++++++++++ tests/mobile-stronghold.test.ts | 18 +++ tests/semuta-ixian-movement-engine.test.ts | 7 +- 11 files changed, 394 insertions(+), 16 deletions(-) create mode 100644 tests/mobile-stronghold-collection-controls.test.tsx create mode 100644 tests/mobile-stronghold-collection-runtime.test.ts diff --git a/components/mobile-stronghold.tsx b/components/mobile-stronghold.tsx index 8d38cf65..22ab6110 100644 --- a/components/mobile-stronghold.tsx +++ b/components/mobile-stronghold.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { HelpTip } from './help-tip'; import { GRAPH, @@ -33,6 +34,7 @@ export function MobileStronghold({ const [destination, setDestination] = useState('polar_sink:0'); const [route, setRoute] = useState(pointer ? [pointer] : []); const [collect, setCollect] = useState(true); + const [collectionEdits, setCollectionEdits] = useState>({}); const [skillSelections, setSkillSelections] = useState>({}); const max = card ? 2 : 3; const current = route.at(-1) ?? ''; @@ -53,17 +55,23 @@ export function MobileStronghold({ const chosen = skillSelections[id]; return chosen === false ? [] : [typeof chosen === 'string' && keys.includes(chosen) ? chosen : keys[0]]; }) : []; - let preview = 0; const remaining: Record = {}; - for (const key of route) { + const collections = route.map((key, index) => { const before = remaining[key] ?? Math.max(0, (g.spice[key] ?? 0) - Number(skillPiles.includes(key))); - const amount = Math.min(before, (me.forces[MOBILE_LOCATION] ?? 0) * 2); + const maximum = Math.min(before, (me.forces[MOBILE_LOCATION] ?? 0) * 2); + const editKey = `${index}:${key}`; + const raw = collectionEdits[editKey]; + const selected = raw === undefined ? maximum : Number(raw); + const legal = !collect || (raw !== '' && Number.isSafeInteger(selected) && selected >= 0 && selected <= maximum); + const amount = collect && legal ? selected : 0; remaining[key] = before - amount; - preview += amount; - } + return { location: key, editKey, amount, maximum, raw, legal }; + }); + const preview = collections.reduce((total, choice) => total + choice.amount, 0); const valid = placement || (!blocked && route.length > 1 && + mobileRouteDistance(route) <= max && route.every((key) => splitLocation(key).sector !== g.storm) && territory(splitLocation(current).territory).type !== 'stronghold'); return ( @@ -84,10 +92,24 @@ export function MobileStronghold({ {!placement && ( <>
    - {route.map((key) => ( -
  1. - {label(key)} - {g.spice[key] ? ` · ${g.spice[key]} spice` : ''} + {collections.map((choice, index) => ( +
  2. + + {!choice.legal &&

    Choose a whole amount from 0 to {choice.maximum}.

    }
  3. ))}
@@ -160,15 +182,17 @@ export function MobileStronghold({ )}