Show each job's length in game hours on the board
CI / check (push) Failing after 6m28s
Details
CI / check (push) Failing after 6m28s
Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
96336ea26d
commit
936fb9e506
|
|
@ -1978,3 +1978,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
|
||||
- **CR-018's completability rule is CR-019's Step 18.1** in this repo: the hull reason is recorded there as a temporary one.
|
||||
- **The game design document is left to the designer** (CR-026 §9).
|
||||
|
||||
## Fix: job length on the board
|
||||
|
||||
- **A job's length depends on the ship**, so the board shows it for the player's fastest crewed ship that could take the job (the permanent checks: hold, range, landing, skills), preferring ships already at the job's origin, since those are the ones that would fly it. Its label says it's game time, and a tooltip says which ship it's for.
|
||||
- **It's the whole trip in game hours** (travel plus loading and takeoff/landing), the figure the Priority window and duty use.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { content, type Location, type ShipModel } from "@deadhead/content";
|
||||
import type { Pilot, PlayerState, Ship } from "@deadhead/schema";
|
||||
import type { Contract, Pilot, PlayerState, Ship } from "@deadhead/schema";
|
||||
import {
|
||||
checkRoute,
|
||||
contractTripGameHours,
|
||||
permanentFailure,
|
||||
effectiveStats,
|
||||
isInShop,
|
||||
MS_PER_HOUR,
|
||||
|
|
@ -190,3 +192,24 @@ export function readyShips(
|
|||
notReady(state, s, now) === null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* How long a job takes in game hours (the trip's travel and handling), for the
|
||||
* player's fastest ship that could take it: one at the job's origin if any,
|
||||
* else anywhere in the fleet. Null with no ship that could.
|
||||
*/
|
||||
export function jobGameHours(
|
||||
state: Pick<PlayerState, "ships" | "pilots">,
|
||||
contract: Contract,
|
||||
): number | null {
|
||||
const able = state.ships.flatMap((ship) => {
|
||||
const pilot = state.pilots.find((p) => p.assignedShipId === ship.id);
|
||||
if (!pilot) return [];
|
||||
const member = { modelId: ship.modelId, skills: pilot.skills, upgrades: ship.upgrades };
|
||||
if (permanentFailure(contract, member, undefined, content) !== null) return [];
|
||||
return [{ ship, hours: contractTripGameHours(shipStatsOf(ship), contract, content) }];
|
||||
});
|
||||
const here = able.filter((a) => a.ship.locationId === contract.originId);
|
||||
const pool = here.length > 0 ? here : able;
|
||||
return pool.length > 0 ? Math.min(...pool.map((a) => a.hours)) : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,15 @@ import { useSearchParams } from "react-router";
|
|||
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, notReady, readyShips } from "../game";
|
||||
import {
|
||||
formatClock,
|
||||
formatCredits,
|
||||
formatGameHours,
|
||||
formatMillionKm,
|
||||
locationName,
|
||||
skillName,
|
||||
} from "../format";
|
||||
import { distanceAt, jobGameHours, notReady, readyShips } from "../game";
|
||||
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
|
||||
import { pairOptions } from "./DispatchSheet";
|
||||
import { DeadheadPicker } from "./Fleet";
|
||||
|
|
@ -271,6 +278,7 @@ function LocationBoard({
|
|||
<span>
|
||||
{contract.shipper} · <span>{contract.massTonnes} t</span> ·{" "}
|
||||
{formatMillionKm(distanceAt(contract.originId, contract.destinationId))}
|
||||
<TripHours contract={contract} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-auto flex items-end justify-between gap-2 border-t border-space-700/70 pt-2 text-sm">
|
||||
|
|
@ -294,6 +302,22 @@ function LocationBoard({
|
|||
);
|
||||
}
|
||||
|
||||
/** " · 5.4 h": the job's length in game hours for the fastest ship that could take it. */
|
||||
function TripHours({ contract }: { contract: Contract }) {
|
||||
const state = useLoadedPlayer();
|
||||
const hours = jobGameHours(state, contract);
|
||||
if (hours === null) return null;
|
||||
return (
|
||||
<span
|
||||
aria-label={`Takes ${formatGameHours(hours)} of game time`}
|
||||
title="Game hours, for your fastest ship that can take it"
|
||||
>
|
||||
{" "}
|
||||
· {formatGameHours(hours)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Why no ship here can take a job now (decision 0.5.134): none docked, or the
|
||||
* pilots here resting (with who wakes first), or none crewed for the player.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
import { content } from "@deadhead/content";
|
||||
import type { Contract, Pilot, Ship } from "@deadhead/schema";
|
||||
import { contractTripGameHours, effectiveStats } from "@deadhead/sim";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useClock } from "../src/clock/clock";
|
||||
import { formatGameHours } from "../src/format";
|
||||
import { jobGameHours } from "../src/game";
|
||||
import { pilotExtras } from "./fixtures";
|
||||
import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render";
|
||||
|
||||
const NOW = Date.parse("2100-01-01T00:00:00Z");
|
||||
|
||||
const job = (over: Partial<Contract> = {}): Contract => ({
|
||||
id: "j1",
|
||||
originId: "earth",
|
||||
destinationId: "moon",
|
||||
cargoTypeId: "standard",
|
||||
goodId: "consumer-goods",
|
||||
shipper: "Pacific Orbital Logistics",
|
||||
priority: false,
|
||||
deliveryWindowGameHours: null,
|
||||
tier: 0,
|
||||
massTonnes: 12,
|
||||
payout: 1_500,
|
||||
requiredSkill: null,
|
||||
requiredLongHaulLevel: 0,
|
||||
offeredAt: NOW,
|
||||
expiresAt: NOW + 30 * 60_000,
|
||||
firstContact: false,
|
||||
...over,
|
||||
});
|
||||
|
||||
const ship = (id: string, modelId: string, locationId: string, pilotId: string | null): Ship => ({
|
||||
id,
|
||||
modelId,
|
||||
name: id,
|
||||
status: "idle",
|
||||
locationId,
|
||||
dispatchId: null,
|
||||
pilotId,
|
||||
lastArrivalAt: 0,
|
||||
...shipExtras(modelId),
|
||||
});
|
||||
const pilot = (id: string, shipId: string): Pilot => ({
|
||||
...samplePlayerState().pilots[0]!,
|
||||
id,
|
||||
name: id,
|
||||
isAvatar: id === "avatar",
|
||||
assignedShipId: shipId,
|
||||
locationId: null,
|
||||
...pilotExtras,
|
||||
});
|
||||
|
||||
const hoursFor = (modelId: string, contract: Contract) =>
|
||||
contractTripGameHours(
|
||||
effectiveStats(content.shipModel(modelId)!, {}, content),
|
||||
contract,
|
||||
content,
|
||||
);
|
||||
|
||||
describe("job length on the board", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] });
|
||||
vi.setSystemTime(NOW);
|
||||
useClock.setState({ offsetMs: 0, synced: true, now: NOW });
|
||||
});
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("shows the game hours beside the weight and distance", async () => {
|
||||
const contract = job();
|
||||
const state = samplePlayerState({
|
||||
tutorial: null,
|
||||
bays: [{ id: "b", locationId: "earth" }],
|
||||
ships: [ship("s1", "courier", "earth", "avatar")],
|
||||
pilots: [pilot("avatar", "s1")],
|
||||
});
|
||||
renderApp(
|
||||
fakeApi({
|
||||
getPlayerState: () => state,
|
||||
getContractBoard: () => ({ contracts: [contract], periodEndsAt: NOW + 30 * 60_000 }),
|
||||
}),
|
||||
"/contracts?location=earth",
|
||||
);
|
||||
const hours = formatGameHours(hoursFor("courier", contract));
|
||||
const time = await screen.findByLabelText(`Takes ${hours} of game time`);
|
||||
const line = time.parentElement!;
|
||||
expect(line).toHaveTextContent(`Pacific Orbital Logistics · 12 t · 0.38M km · ${hours}`);
|
||||
expect(within(line).getByText(/12 t/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("times the job for the fastest ship that could take it, preferring one at the origin", () => {
|
||||
const contract = job();
|
||||
const ships = [
|
||||
ship("slow-here", "freighter", "earth", "p1"),
|
||||
ship("fast-away", "clipper", "moon", "p2"),
|
||||
];
|
||||
const pilots = [pilot("p1", "slow-here"), pilot("p2", "fast-away")];
|
||||
expect(jobGameHours({ ships, pilots }, contract)).toBeCloseTo(hoursFor("freighter", contract));
|
||||
// Nobody at Earth: the fastest anywhere.
|
||||
const away = ships.map((s) => ({ ...s, locationId: "moon" }));
|
||||
expect(jobGameHours({ ships: away, pilots }, contract)).toBeCloseTo(
|
||||
Math.min(hoursFor("freighter", contract), hoursFor("clipper", contract)),
|
||||
);
|
||||
// No crewed ship that could take it: no time shown.
|
||||
expect(jobGameHours({ ships, pilots: [] }, contract)).toBeNull();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 710 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
|
|
@ -559,3 +559,7 @@ Spec: `memory-bank/change-requests/CR-026-random-events-hull.md` (received as "C
|
|||
- [x] **Step 26.10 — Plan amendments and regression.** CR-026 §9 applied: plan 0.3 (random events no longer deferred), 0.5 (0.5.143–0.5.152, recorded with the CR and in Step 26.9), Steps 3.13 and 3.17; CR-009 Step 10F.1 (Hull Plating); CR-014 Steps 10K.6 (events in the loop and catch-up) and 10K.9 (radio-call notifications); CR-019 Step 18.1 (the hull reason isn't a hidden-job rule); CR-021 Step 19.4 (Independent pilots get events); CR-024 Step 22.2 (answer and repair are manual decisions); CR-025 Step 23.2 (event highlights). `game-design-document.md` §13 is the designer's to update. Full suite: typecheck, lint, 1564 unit tests and 11 e2e tests pass.
|
||||
|
||||
**Phase 26 is complete; stopped for review.** Manual check for the user: on a fresh company, fly past the deal and three more trips, send a ship on a long contract (6+ game hours), and check the first event is the Derelict cargo pod radio call; answer it; later let a radio call run out and check the default applied (its result in the summary and the Logbook); use Developer tools → Damage hull on a ship, then repair it at an HQ from its Upgrades sheet.
|
||||
|
||||
## Fixes after Phase 26
|
||||
|
||||
- [x] **Job length on the board.** Location board cards show how long the job takes in game hours beside the weight and distance ("Atlas Heavy Industries · 6 t · 0.38M km · 2.2 h"), for the fastest of the player's crewed ships that could take it, preferring one at the job's origin (`jobGameHours` in `apps/web/src/game.ts`); none is shown if no ship could. The tutorial board already showed its trip time. Tests: `apps/web/test/job-hours.test.tsx`. Screenshots: `docs/screenshots/fix-job-hours-{phone,desktop}.png`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue