232 lines
7.8 KiB
TypeScript
232 lines
7.8 KiB
TypeScript
import type { PlayerState, RouteRequest } from "@deadhead/schema";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useApi } from "../api/context";
|
|
import { serverNow, syncClock } from "../clock/clock";
|
|
import { locationName } from "../format";
|
|
import { errorMessage, useLoadedPlayer } from "../state/actions";
|
|
import { PLAYER_STATE_KEY } from "../state/player";
|
|
import { Button } from "../ui";
|
|
|
|
const MINUTE = 60_000;
|
|
const selectClass =
|
|
"min-w-0 flex-1 rounded-lg border border-glass-line bg-space-900 px-2 py-1.5 text-sm text-ink-100";
|
|
|
|
/**
|
|
* Settings → Developer (non-production builds only, CR-011): fast-forward the
|
|
* server clock, add credits, land every flight now, and adjust pilots, ships
|
|
* and discoveries for testing.
|
|
*/
|
|
export function DevTools() {
|
|
const api = useApi();
|
|
const queryClient = useQueryClient();
|
|
const state = useLoadedPlayer();
|
|
const refresh = async () => {
|
|
await syncClock(api);
|
|
await queryClient.invalidateQueries({ queryKey: PLAYER_STATE_KEY });
|
|
await queryClient.invalidateQueries({ queryKey: ["board"] });
|
|
};
|
|
/** The pilot, ship and discovery tools answer with the new state. */
|
|
const applyState = async (next: PlayerState) => {
|
|
queryClient.setQueryData(PLAYER_STATE_KEY, next);
|
|
await queryClient.invalidateQueries({ queryKey: ["board"] });
|
|
};
|
|
const advance = useMutation({
|
|
mutationFn: (advanceMs: number) => api.call("devAdvanceClock", { advanceMs }),
|
|
onSuccess: refresh,
|
|
});
|
|
const credits = useMutation({
|
|
mutationFn: () => api.call("devAddCredits", { amount: 10_000 }),
|
|
onSuccess: refresh,
|
|
});
|
|
const pilotTool = useMutation({
|
|
mutationFn: (input: RouteRequest<"devPilot">) => api.call("devPilot", input),
|
|
onSuccess: applyState,
|
|
});
|
|
const shipTool = useMutation({
|
|
mutationFn: (input: RouteRequest<"devShip">) => api.call("devShip", input),
|
|
onSuccess: applyState,
|
|
});
|
|
const discoverAll = useMutation({
|
|
mutationFn: () => api.call("devDiscoverAll", {}),
|
|
onSuccess: applyState,
|
|
});
|
|
|
|
const [pilotId, setPilotId] = useState(state.pilots[0]?.id ?? "");
|
|
const [shipId, setShipId] = useState(state.ships[0]?.id ?? "");
|
|
const discovered = state.locations.filter((l) => l.discovered).map((l) => l.id);
|
|
const [destination, setDestination] = useState(discovered[0] ?? "earth");
|
|
const pilot = state.pilots.find((p) => p.id === pilotId);
|
|
const ship = state.ships.find((s) => s.id === shipId);
|
|
|
|
const lastArrival = Math.max(0, ...state.activeDispatches.map((d) => d.arrivalAt));
|
|
const busy =
|
|
advance.isPending ||
|
|
credits.isPending ||
|
|
pilotTool.isPending ||
|
|
shipTool.isPending ||
|
|
discoverAll.isPending;
|
|
const error =
|
|
advance.error ?? credits.error ?? pilotTool.error ?? shipTool.error ?? discoverAll.error;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy}
|
|
onClick={() => advance.mutate(10 * MINUTE)}
|
|
>
|
|
Advance 10 minutes
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy}
|
|
onClick={() => advance.mutate(60 * MINUTE)}
|
|
>
|
|
Advance 1 hour
|
|
</Button>
|
|
<Button variant="secondary" size="sm" disabled={busy} onClick={() => credits.mutate()}>
|
|
Add 10,000 credits
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || lastArrival <= serverNow()}
|
|
onClick={() =>
|
|
advance.mutate(Math.max(1000, Math.ceil(lastArrival - serverNow()) + 1000))
|
|
}
|
|
>
|
|
Finish all flights
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || discovered.length === state.locations.length}
|
|
onClick={() => discoverAll.mutate()}
|
|
>
|
|
Discover everywhere
|
|
</Button>
|
|
</div>
|
|
|
|
{state.pilots.length > 0 && (
|
|
<fieldset className="space-y-2">
|
|
<legend className="text-xs text-ink-400">Pilot</legend>
|
|
<select
|
|
aria-label="Pilot"
|
|
value={pilotId}
|
|
onChange={(e) => setPilotId(e.target.value)}
|
|
className={selectClass}
|
|
>
|
|
{state.pilots.map((p) => (
|
|
<option key={p.id} value={p.id}>
|
|
{p.name} · level {p.level} · {p.xp} XP
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !pilot}
|
|
onClick={() => pilotTool.mutate({ pilotId, action: "level-up" })}
|
|
>
|
|
Level up
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !pilot || (pilot.dutyHours === 0 && !pilot.resting)}
|
|
onClick={() => pilotTool.mutate({ pilotId, action: "clear-rest" })}
|
|
>
|
|
Clear duty and rest
|
|
</Button>
|
|
</div>
|
|
</fieldset>
|
|
)}
|
|
|
|
{state.ships.length > 0 && (
|
|
<fieldset className="space-y-2">
|
|
<legend className="text-xs text-ink-400">Ship</legend>
|
|
<select
|
|
aria-label="Ship"
|
|
value={shipId}
|
|
onChange={(e) => setShipId(e.target.value)}
|
|
className={selectClass}
|
|
>
|
|
{state.ships.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.name} · {s.modelId} · {s.locationId ? locationName(s.locationId) : "in flight"}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !ship}
|
|
onClick={() => shipTool.mutate({ shipId, action: "max-upgrades" })}
|
|
>
|
|
Max upgrades
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !ship || Object.keys(ship.upgrades).length === 0}
|
|
onClick={() => shipTool.mutate({ shipId, action: "reset-upgrades" })}
|
|
>
|
|
Reset upgrades
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !ship || ship.inShopUntil === null}
|
|
onClick={() => shipTool.mutate({ shipId, action: "finish-shop" })}
|
|
>
|
|
Finish shop work
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !ship}
|
|
onClick={() => shipTool.mutate({ shipId, action: "damage-hull" })}
|
|
>
|
|
Damage hull
|
|
</Button>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<select
|
|
aria-label="Move to"
|
|
value={destination}
|
|
onChange={(e) => setDestination(e.target.value)}
|
|
className={selectClass}
|
|
>
|
|
{discovered.map((id) => (
|
|
<option key={id} value={id}>
|
|
{locationName(id)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
disabled={busy || !ship || ship.status !== "idle" || ship.locationId === destination}
|
|
onClick={() => shipTool.mutate({ shipId, action: "move", locationId: destination })}
|
|
>
|
|
Move here
|
|
</Button>
|
|
</div>
|
|
</fieldset>
|
|
)}
|
|
|
|
{error && (
|
|
<p role="alert" className="text-sm text-rose-300">
|
|
{errorMessage(error)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|