diff --git a/DECISIONS.md b/DECISIONS.md index 395e336..9f486f5 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1644,3 +1644,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **When every ship leaves a location, its board falls back to the whole fleet** (decision 0.5.115), so its jobs show again for deciding where to go. That is why the hiding test keeps the Courier at Earth. - **Dispatch doesn't consult the filter**: a contract hidden from the board fails `validateDispatch` for every pair anyway, with its reason. - **Discarding boards mid-period can re-use contract ids**: a regenerated board numbers its jobs from 0 again, so a job accepted before the update hides the regenerated job with its id for the rest of that period. That lasts at most one 10-minute period after the update. + +## Step 19.4 + +- **"Needs Heavy Cargo 2" stays on cards**: it describes the job, as distance and mass do, and every job shown can be taken by someone. Only the rose problem pills and the red mass went. +- **One empty-board message for both cases**: a board emptied by filtering and one whose jobs were all taken read the same, "No jobs here your fleet can take right now", with the countdown. The client can't tell the two apart, and the advice is the same. +- **The "No idle ships here" hint above the board is unchanged**; on an empty board it still points to the Fleet screen. diff --git a/apps/web/src/components/Goods.tsx b/apps/web/src/components/Goods.tsx index 9ced4e9..65536b5 100644 --- a/apps/web/src/components/Goods.tsx +++ b/apps/web/src/components/Goods.tsx @@ -1,9 +1,8 @@ import { content } from "@deadhead/content"; -import type { Contract, PlayerState } from "@deadhead/schema"; -import { contractTripGameHours, MS_PER_HOUR } from "@deadhead/sim"; +import type { Contract } from "@deadhead/schema"; +import { MS_PER_HOUR } from "@deadhead/sim"; import { formatGameHoursSpan } from "../clock/timeFormat"; import { usePreferences } from "../state/preferences"; -import { shipStatsOf } from "../game"; import { cargoName, formatDuration } from "../format"; import { Pill, type Tone } from "../ui"; @@ -58,19 +57,3 @@ export function PriorityChip({ windowHours }: { windowHours: number }) { ); } - -/** - * True when a Priority contract has idle ships at its origin and none of them - * can make the window (CR-007). Display only; the sim decides at dispatch. - */ -export function tooSlowForAll(state: PlayerState, contract: Contract): boolean { - if (!contract.priority || contract.deliveryWindowGameHours === null) return false; - const idle = state.ships.filter((s) => s.status === "idle" && s.locationId === contract.originId); - if (idle.length === 0) return false; - // Effective stats: an engines upgrade can make the window (CR-009). - return idle.every( - (ship) => - contractTripGameHours(shipStatsOf(ship), contract, content) > - contract.deliveryWindowGameHours!, - ); -} diff --git a/apps/web/src/screens/LocationBoards.tsx b/apps/web/src/screens/LocationBoards.tsx index 52a81ee..b26ed6e 100644 --- a/apps/web/src/screens/LocationBoards.tsx +++ b/apps/web/src/screens/LocationBoards.tsx @@ -6,14 +6,14 @@ import { useApi } from "../api/context"; import { serverNow, useClock } from "../clock/clock"; import { Countdown } from "../components/Countdown"; import { formatClock, formatCredits, formatMillionKm, locationName, skillName } from "../format"; -import { distanceAt, shipStatsOf } from "../game"; +import { distanceAt } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { pairOptions } from "./DispatchSheet"; import { DeadheadPicker } from "./Fleet"; import { content } from "@deadhead/content"; import { RouteStrip } from "../components/RouteStrip"; import { Art, Button, Icon, Panel, Pill } from "../ui"; -import { ClassChip, goodName, PriorityChip, tooSlowForAll } from "../components/Goods"; +import { ClassChip, goodName, PriorityChip } from "../components/Goods"; export const locationBoardKey = (locationId: string) => ["board", "location", locationId] as const; @@ -70,19 +70,6 @@ export function requirementsText(contract: Contract): string | null { return parts.length > 0 ? parts.join(", ") : null; } -/** - * The most a player's ship can carry for a board at this location: the largest - * ship here, or in the whole fleet when none are here; null with no ships - * (decision 0.5.53). - */ -export function carryLimit(state: PlayerState, locationId: string): number | null { - const here = state.ships.filter((s) => s.locationId === locationId); - const ships = here.length > 0 ? here : state.ships; - if (ships.length === 0) return null; - // Effective capacity: an expanded cargo bay carries more (CR-009). - return Math.max(...ships.map((s) => shipStatsOf(s).cargoCapacityTonnes)); -} - /** * The location the Contracts screen opens on: where the most recently arrived * idle ship is, otherwise Earth (CR-002). @@ -156,9 +143,7 @@ function LocationBoard({ onSelect?: (contract: Contract) => void; }) { const api = useApi(); - const state = useLoadedPlayer(); const now = useClock((s) => s.now); - const limit = carryLimit(state, locationId); const board = useQuery({ queryKey: locationBoardKey(locationId), queryFn: () => api.call("getContractBoard", { locationId }), @@ -202,14 +187,18 @@ function LocationBoard({

); const stuck = ; + // Boards show only jobs the fleet can complete (CR-019), so an empty one says so. if (board.data.contracts.length === 0) { return (
- {refresh} - {stuck} - - No contracts left this period. + + No jobs here your fleet can take right now. New contracts in{" "} + + + + . + {stuck}
); } @@ -221,7 +210,6 @@ function LocationBoard({