Skip to content

Commit 5735d79

Browse files
authored
Client task source of truth & xml metadata helper (#419)
* remove client current_task_id * client bump
1 parent a0dc3fc commit 5735d79

14 files changed

Lines changed: 423 additions & 79 deletions

File tree

client/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@gradient-bang/app",
33
"description": "Gradient Bang web client",
44
"private": true,
5-
"version": "0.10.2",
5+
"version": "0.10.3",
66
"type": "module",
77
"scripts": {
88
"dev": "vite --host",

client/app/src/GameContext.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,7 @@ export function GameProvider({ children }: GameProviderProps) {
850850
fighters: ship.fighters,
851851
max_fighters: ship.max_fighters,
852852
current_task_id: ship.current_task_id,
853+
current_task_actor: ship.current_task_actor,
853854
})
854855
}
855856
}
@@ -908,6 +909,7 @@ export function GameProvider({ children }: GameProviderProps) {
908909
fighters: ship.fighters,
909910
max_fighters: ship.max_fighters,
910911
current_task_id: ship.current_task_id,
912+
current_task_actor: ship.current_task_actor,
911913
})
912914
}
913915

@@ -1541,9 +1543,9 @@ export function GameProvider({ children }: GameProviderProps) {
15411543
const myId = useGameStore.getState().character_id
15421544
const isMyTask = !data.actor_character_id || data.actor_character_id === myId
15431545

1544-
// Only track tasks initiated by this player in the task engine.
1545-
// Other corp members' tasks show ship "busy" status via the
1546-
// ship's current_task_id but don't fill our task engine badges.
1546+
// Only tasks initiated by this player get full task-engine
1547+
// metadata. Ship busy state is tracked separately so other
1548+
// corp members' tasks can still mark a ship occupied.
15471549
if (taskId && isMyTask) {
15481550
useGameStore.getState().addActiveTask({
15491551
task_id: taskId,
@@ -1559,11 +1561,12 @@ export function GameProvider({ children }: GameProviderProps) {
15591561
})
15601562
}
15611563

1562-
// Update corp ship's task state so ShipCard shows busy status
15631564
if (taskId && data.ship_id && isKnownFleetShip(data.ship_id)) {
1564-
upsertCorporationShip(data.ship_id, {
1565-
current_task_id: taskId,
1566-
current_task_actor_name: data.actor_character_name ?? null,
1565+
useGameStore.getState().setShipTaskOccupancy(data.ship_id, {
1566+
task_id: taskId,
1567+
actor_name: data.actor_character_name ?? null,
1568+
task_status: data.task_status === "waking" ? "waking" : "active",
1569+
source: "live",
15671570
})
15681571
}
15691572
break
@@ -1574,28 +1577,35 @@ export function GameProvider({ children }: GameProviderProps) {
15741577
const data = e.payload as Msg.TaskFinishMessage
15751578

15761579
const taskId = normalizeTaskId(data.task_id)
1580+
if (!taskId) break
15771581

1578-
// Clear corp ship's task state regardless of who owns the task
1579-
if (data.ship_id && isKnownFleetShip(data.ship_id)) {
1580-
upsertCorporationShip(data.ship_id, {
1581-
current_task_id: null,
1582-
current_task_actor_name: null,
1583-
})
1582+
const activeTasks = useGameStore.getState().activeTasks
1583+
const selectedTaskId =
1584+
activeTasks[taskId] ? taskId : (
1585+
Object.keys(activeTasks).find((activeTaskId) => activeTaskId.startsWith(taskId))
1586+
)
1587+
const activeTask = selectedTaskId ? activeTasks[selectedTaskId] : undefined
1588+
const shipId = data.ship_id ?? activeTask?.ship_id
1589+
1590+
if (shipId && isKnownFleetShip(shipId)) {
1591+
useGameStore.getState().clearShipTaskOccupancyByShipId(shipId)
1592+
} else {
1593+
useGameStore.getState().clearShipTaskOccupancyByTaskId(taskId)
15841594
}
15851595

15861596
// Only process task engine cleanup if this task is in our
15871597
// active tasks (i.e. we tracked it on task.start because it
15881598
// belongs to us).
1589-
if (taskId && useGameStore.getState().activeTasks[taskId]) {
1590-
useGameStore.getState().removeActiveTask(taskId)
1599+
if (selectedTaskId) {
1600+
useGameStore.getState().removeActiveTask(selectedTaskId)
15911601

15921602
// Backward compatibility while old short IDs may still exist in local state.
1593-
if (taskId.length > 6) {
1594-
useGameStore.getState().removeActiveTask(taskId.slice(0, 6))
1603+
if (selectedTaskId.length > 6) {
1604+
useGameStore.getState().removeActiveTask(selectedTaskId.slice(0, 6))
15951605
} else {
15961606
const activeTaskIds = Object.keys(useGameStore.getState().activeTasks)
15971607
for (const activeTaskId of activeTaskIds) {
1598-
if (activeTaskId.startsWith(taskId)) {
1608+
if (activeTaskId.startsWith(selectedTaskId)) {
15991609
useGameStore.getState().removeActiveTask(activeTaskId)
16001610
}
16011611
}
@@ -1628,10 +1638,9 @@ export function GameProvider({ children }: GameProviderProps) {
16281638
const shipId = data.ship_id ?? activeTask?.ship_id
16291639

16301640
if (shipId && isKnownFleetShip(shipId)) {
1631-
upsertCorporationShip(shipId, {
1632-
current_task_id: null,
1633-
current_task_actor_name: null,
1634-
})
1641+
useGameStore.getState().clearShipTaskOccupancyByShipId(shipId)
1642+
} else {
1643+
useGameStore.getState().clearShipTaskOccupancyByTaskId(taskId)
16351644
}
16361645

16371646
if (selectedTaskId) {

client/app/src/components/ShipStatusPopover.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
PopoverTitle,
1919
PopoverTrigger,
2020
} from "@/components/primitives/Popover"
21+
import type { ShipTaskOccupancy } from "@/stores/taskSlice"
2122
import { cn } from "@/utils/tailwind"
2223

2324
import { Badge } from "./primitives/Badge"
@@ -40,7 +41,15 @@ function StatusRow({
4041
)
4142
}
4243

43-
export const ShipStatusPopover = ({ ship, isActive }: { ship: Ship; isActive?: boolean }) => {
44+
export const ShipStatusPopover = ({
45+
ship,
46+
isActive,
47+
taskOccupancy,
48+
}: {
49+
ship: Ship
50+
isActive?: boolean
51+
taskOccupancy?: ShipTaskOccupancy
52+
}) => {
4453
const byoa = ship.byoa ?? null
4554
const isOnline = byoa?.presence?.online === true
4655
const actor = ship.current_task_actor ?? null
@@ -85,16 +94,17 @@ export const ShipStatusPopover = ({ ship, isActive }: { ship: Ship; isActive?: b
8594
<StatusRow
8695
label="Active actor"
8796
value={
97+
taskOccupancy?.actor_name ??
8898
actor?.character_name ??
8999
actor?.character_id_prefix ?? <span className="text-subtle-foreground"></span>
90100
}
91101
/>
92102
<StatusRow
93103
label="Task"
94104
value={
95-
ship.current_task_id ?
105+
taskOccupancy?.task_id ?
96106
<span className="font-mono">
97-
{ship.current_task_id.replace(/-/g, "").slice(0, 12)}
107+
{taskOccupancy.task_id.replace(/-/g, "").slice(0, 12)}
98108
</span>
99109
: <span className="text-subtle-foreground"></span>
100110
}

client/app/src/components/panels/PlayerShipPanel.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ShipCardStat } from "@/components/ShipCardStat"
88
import { CreditsIcon, CurrentSectorIcon, FighterIcon, FuelIcon } from "@/icons"
99
import useAudioStore from "@/stores/audio"
1010
import useGameStore from "@/stores/game"
11+
import { selectShipTaskOccupancy } from "@/stores/taskSlice"
1112
import { isShipInCombat } from "@/utils/combat"
1213
import { formatCurrency } from "@/utils/formatting"
1314
import { shipTypeVerbose } from "@/utils/game"
@@ -53,9 +54,14 @@ const ShipCard = ({ ship }: { ship: ShipSelf }) => {
5354
const activeTask = useGameStore((state) =>
5455
Object.values(state.activeTasks).find((task) => task.ship_id === ship.ship_id)
5556
)
56-
const isBusy = !!(activeTask || ship.current_task_id)
57-
const isWaking = activeTask?.task_status === "waking"
58-
const actorName = activeTask?.actor_character_name ?? ship.current_task_actor_name
57+
const taskOccupancy = useGameStore((state) => selectShipTaskOccupancy(state, ship.ship_id))
58+
const isBusy = !!taskOccupancy
59+
const isWaking = taskOccupancy?.task_status === "waking"
60+
const actorName =
61+
activeTask?.actor_character_name ??
62+
taskOccupancy?.actor_name ??
63+
ship.current_task_actor?.character_name ??
64+
ship.current_task_actor_name
5965
const isInCombat = useGameStore((state) => isShipInCombat(state, ship.ship_id))
6066

6167
useEffect(() => {
@@ -76,7 +82,9 @@ const ShipCard = ({ ship }: { ship: ShipSelf }) => {
7682
</AnimatePresence>
7783
<div className="relative z-10 flex flex-col gap-2 flex-1 min-w-0">
7884
<div className="flex flex-row gap-1 items-center">
79-
{ship.byoa && <ShipStatusPopover ship={ship} isActive={isBusy} />}
85+
{ship.byoa && (
86+
<ShipStatusPopover ship={ship} isActive={isBusy} taskOccupancy={taskOccupancy} />
87+
)}
8088
<div className="flex flex-row gap-2 items-center">
8189
<div className="text-sm uppercase text-white font-semibold">{ship.ship_name}</div>
8290
<div className="text-xxs text-subtle-foreground">{shipTypeVerbose(ship.ship_type)}</div>

client/app/src/stores/game.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ const createGameSlice: StateCreator<GameStoreState, [], [], GameSlice> = (set, g
272272
})
273273
),
274274

275-
setShips: (ships: ShipSelf[]) =>
275+
setShips: (ships: ShipSelf[]) => {
276276
set(
277277
produce((state) => {
278278
const now = new Date().toISOString()
@@ -294,7 +294,9 @@ const createGameSlice: StateCreator<GameStoreState, [], [], GameSlice> = (set, g
294294
}
295295
state.destroyedShips = ships.filter((s: ShipSelf) => !!s.destroyed_at)
296296
})
297-
),
297+
)
298+
get().hydrateShipTaskOccupancyFromSnapshot(ships)
299+
},
298300

299301
addShip: (ship: Partial<ShipSelf>) =>
300302
set(
@@ -442,6 +444,10 @@ const createGameSlice: StateCreator<GameStoreState, [], [], GameSlice> = (set, g
442444

443445
setCorporation: (corporation: Corporation | undefined) => {
444446
const hadCorp = !!get().corporation
447+
const previousCorpShipIds =
448+
get()
449+
.ships.data?.filter((ship) => ship.owner_type === "corporation")
450+
.map((ship) => ship.ship_id) ?? []
445451
// Detect corp transition and invalidate map coverage so the next
446452
// map.local/map.region replaces stale data instead of merging.
447453
if (!hadCorp && corporation) {
@@ -489,6 +495,7 @@ const createGameSlice: StateCreator<GameStoreState, [], [], GameSlice> = (set, g
489495
fighters: ship.fighters,
490496
max_fighters: ship.max_fighters,
491497
current_task_id: ship.current_task_id,
498+
current_task_actor: ship.current_task_actor,
492499
byoa:
493500
ship.byoa === undefined ? existingShip?.byoa
494501
: ship.byoa ?
@@ -514,6 +521,13 @@ const createGameSlice: StateCreator<GameStoreState, [], [], GameSlice> = (set, g
514521
}
515522
})
516523
)
524+
if (corporation?.ships) {
525+
get().hydrateShipTaskOccupancyFromSnapshot(corporation.ships)
526+
} else if (hadCorp) {
527+
for (const shipId of previousCorpShipIds) {
528+
get().clearShipTaskOccupancyByShipId(shipId)
529+
}
530+
}
517531
},
518532

519533
setStarfieldReady: (starfieldReady: boolean) => set({ starfieldReady }),

0 commit comments

Comments
 (0)