diff --git a/DECISIONS.md b/DECISIONS.md index b91fd3f..de1271c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1948,3 +1948,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **The default is labelled "(if you don't answer)"**, and the countdown line says what the pilot will do when it runs out. - **Decide later** puts a call off for the session; the Bridge lists every waiting call with Answer, and its tab badge counts them alongside goals to claim. - **`PlayerState.nextRadioCallAt`** tells the app when to refresh for the next call (a call appears just by time passing, which no save change marks). It only gives the time, not which event. + +## Step 26.7 + +- **"Ship detail" is the Upgrades sheet**: it's the one per-ship sheet, and repairs use the same shop, so the hull and its Repair button sit at its top. Its subtitle now says repairs are done there too. +- **Which events are highlights**: good events (whatever was chosen) and any that hurt a hull, ranked just below level-ups. A detour or a fee without damage is in the Logbook and the short list, not a highlight. +- **The short list shows a trip's event next to the trip**, without it counting as one of the 3 events: a delivery with a detour still reads as a quick check-in. +- **The hull bar colours** follow the rules: amber below 50% (no Dangerous cargo), red below 25% (Needs repair). +- **Developer tools: Damage hull** sets a ship to 20%, to try the warning and repairs (the CR's manual check). diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx index de08506..644c819 100644 --- a/apps/web/src/components/AwaySummary.tsx +++ b/apps/web/src/components/AwaySummary.tsx @@ -15,6 +15,7 @@ import { useClock } from "../clock/clock"; import { fill, formatCredits, locationName } from "../format"; import { useGameAction, useLoadedPlayer } from "../state/actions"; import { Button, Icon, Sheet } from "../ui"; +import { eventResultText } from "./EventResults"; import { SleepBanner } from "./FleetSleep"; const words = content.awayWording; @@ -106,6 +107,8 @@ export function highlightText(highlight: AwayHighlight, state: PlayerState): str }); case "problem": return problemText(highlight.problem, name); + case "event": + return eventResultText(highlight.entry, state); } } @@ -143,6 +146,8 @@ export function directText(entry: LogEntry, state: PlayerState): string { level: entry.level, points: plural(entry.skillPoints, "skill point"), }); + case "event": + return eventResultText(entry, state); default: return ""; } diff --git a/apps/web/src/components/EventResults.tsx b/apps/web/src/components/EventResults.tsx new file mode 100644 index 0000000..c8ccd97 --- /dev/null +++ b/apps/web/src/components/EventResults.tsx @@ -0,0 +1,45 @@ +import { content } from "@deadhead/content"; +import type { LogEntry, PlayerState } from "@deadhead/schema"; +import { MS_PER_HOUR } from "@deadhead/sim"; +import { formatCredits } from "../format"; + +type EventEntry = Extract; + +/** Game hours for a real-time delay, to one decimal ("3 h", "1.5 h"). */ +function hours(delayMs: number): string { + const h = Math.abs(delayMs * content.config.timeCompression) / MS_PER_HOUR; + return `${Math.round(h * 10) / 10} h`; +} + +/** What an event actually changed, in a few words: "+3 h", "hull −14%", "+1,200 cr". */ +export function eventEffects(entry: EventEntry): string[] { + const parts: string[] = []; + if (entry.delayMs > 0) parts.push(`+${hours(entry.delayMs)}`); + if (entry.delayMs < 0) parts.push(`${hours(entry.delayMs)} sooner`); + if (entry.hullLoss > 0) parts.push(`hull −${entry.hullLoss}%`); + if (entry.payLoss > 0) parts.push(`−${formatCredits(entry.payLoss)} pay`); + if (entry.fee > 0) parts.push(`${formatCredits(entry.fee)} fee`); + if (entry.bonus > 0) parts.push(`+${formatCredits(entry.bonus)}`); + if (entry.priorityLost > 0) { + parts.push(`missed the Priority window (−${formatCredits(entry.priorityLost)})`); + } + if (entry.xpBonusPct > 0) parts.push(`+${entry.xpBonusPct}% XP`); + if (entry.extraFuelFu > 0) parts.push(`+${Math.round(entry.extraFuelFu)} fu fuel`); + return parts; +} + +/** + * An event's result in a line (CR-026 §5): "Debris field ahead — Rosa: Go + * around (no answer) · +3 h". Automatic events read "Solar flare — Rosa: +3 h". + */ +export function eventResultText(entry: EventEntry, state: PlayerState): string { + const def = content.tripEvent(entry.eventId); + const pilot = state.pilots.find((p) => p.id === entry.pilotId)?.name ?? "A pilot"; + const effects = eventEffects(entry); + const choice = + def?.kind === "radio" + ? `${def.options.find((o) => o.id === entry.optionId)?.label ?? entry.optionId}${entry.answered ? "" : " (no answer)"}` + : null; + const what = [choice, ...effects].filter((p): p is string => p !== null); + return `${def?.title ?? entry.eventId} — ${pilot}: ${what.length > 0 ? what.join(" · ") : "no harm done"}`; +} diff --git a/apps/web/src/components/PilotLogbook.tsx b/apps/web/src/components/PilotLogbook.tsx index 98ca398..d551143 100644 --- a/apps/web/src/components/PilotLogbook.tsx +++ b/apps/web/src/components/PilotLogbook.tsx @@ -7,6 +7,7 @@ import { formatCredits, locationName } from "../format"; import { formatLocalTime } from "../screens/DispatchSheet"; import { Button, Icon } from "../ui"; import { pilotNow } from "./AwaySummary"; +import { eventResultText } from "./EventResults"; import { formatSleepTime } from "./FleetSleep"; /** One pilot's part of the logbook (decision 0.5.141), or the company's own entries. */ @@ -157,6 +158,19 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) { {entry.skillPoints === 1 ? "skill point" : "skill points"} ); + case "event": + return ( + + {eventResultText(entry, state)} + + ); + case "repair": + return ( + + {shipName(state, entry.shipId)}: hull repaired from {entry.fromPct}% to 100%,{" "} + {formatCredits(entry.cost)} + + ); case "goal-ready": return ( diff --git a/apps/web/src/game.ts b/apps/web/src/game.ts index 3419455..c412985 100644 --- a/apps/web/src/game.ts +++ b/apps/web/src/game.ts @@ -62,6 +62,7 @@ export function simShip(ship: Ship): ShipState { upgrades: ship.upgrades, inShopUntil: ship.inShopUntil, tank: { fuelFu: ship.fuelFu, avgPrice: ship.fuelAvgPrice }, + hullConditionPct: ship.hullConditionPct, }; } diff --git a/apps/web/src/screens/DevTools.tsx b/apps/web/src/screens/DevTools.tsx index 1ee3e65..cb8af6b 100644 --- a/apps/web/src/screens/DevTools.tsx +++ b/apps/web/src/screens/DevTools.tsx @@ -187,6 +187,14 @@ export function DevTools() { > Finish shop work +