feat(ui): team builder — lead + helpers with live duration preview, wired into the action panels (#44)
This commit is contained in:
@@ -581,8 +581,8 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
const jumpId = await insertTaskRow(db, { type: 'JUMP', payload: { systemId: f.sys1, hops: 1 }, leadCrewId: c1, helpers: [], duration: 2, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const exploreId = await insertTaskRow(db, { type: 'EXPLORE', payload: { planetId: f.p1 }, leadCrewId: c2, helpers: [c3], duration: 4, remaining: 4, status: 'PENDING', queuedAtTurn: 3 })
|
||||
const expected = [
|
||||
{ id: jumpId, type: 'JUMP', targetLabel: 'Outpost', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
{ id: exploreId, type: 'EXPLORE', targetLabel: 'Basalt', leadName: 'CREW 2', helperNames: ['CREW 3'], remaining: 4 },
|
||||
{ id: jumpId, type: 'JUMP', targetLabel: 'Outpost', leadName: 'CREW 1', helperNames: [], remaining: 1, status: 'ACTIVE', leadCrewId: c1, helperCrewIds: [] },
|
||||
{ id: exploreId, type: 'EXPLORE', targetLabel: 'Basalt', leadName: 'CREW 2', helperNames: ['CREW 3'], remaining: 4, status: 'PENDING', leadCrewId: c2, helperCrewIds: [c3] },
|
||||
]
|
||||
expect((await getWorldState(db)).tasks).toEqual(expected)
|
||||
expect((await getSystemState(db)).tasks).toEqual(expected)
|
||||
@@ -608,8 +608,8 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
const exploreId = await insertTaskRow(db, { type: 'EXPLORE', payload: { planetId: f.p0 }, leadCrewId: c2, helpers: [], duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db) // must not throw out of the read
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'EXPLORE', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
{ id: exploreId, type: 'EXPLORE', targetLabel: 'Alder', leadName: 'CREW 2', helperNames: [], remaining: 1 }, // the well-formed sibling still resolves
|
||||
{ id: badId, type: 'EXPLORE', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1, status: 'ACTIVE', leadCrewId: c1, helperCrewIds: [] },
|
||||
{ id: exploreId, type: 'EXPLORE', targetLabel: 'Alder', leadName: 'CREW 2', helperNames: [], remaining: 1, status: 'ACTIVE', leadCrewId: c2, helperCrewIds: [] }, // the well-formed sibling still resolves
|
||||
])
|
||||
})
|
||||
|
||||
@@ -620,7 +620,7 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
const badId = await insertRawTaskRow(db, { type: 'JUMP', payload: JSON.stringify({ systemId: f.sys1, hops: 1 }), helpers: '[not-json', leadCrewId: c1, duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db)
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'JUMP', targetLabel: 'Outpost', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
{ id: badId, type: 'JUMP', targetLabel: 'Outpost', leadName: 'CREW 1', helperNames: [], remaining: 1, status: 'ACTIVE', leadCrewId: c1, helperCrewIds: [] },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -631,7 +631,7 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
const badId = await insertRawTaskRow(db, { type: 'TELEPORT', payload: JSON.stringify({ systemId: f.sys1, hops: 1 }), helpers: '[]', leadCrewId: c1, duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db)
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'TELEPORT', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
{ id: badId, type: 'TELEPORT', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1, status: 'ACTIVE', leadCrewId: c1, helperCrewIds: [] },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
+11
-3
@@ -26,7 +26,10 @@ export interface WorldSystemDto {
|
||||
|
||||
// One row of the snapshot task queue — what the GET builders expose of the tasks table
|
||||
// (distinct from the tick's TickResult, which reports every row including DONE): the display
|
||||
// fields the UI needs, names resolved, no raw payload.
|
||||
// fields the UI needs, names resolved, no raw payload — plus the raw row state (JC-11):
|
||||
// `status` (the array's ACTIVE-block-first order isn't readable back from it) and the crew
|
||||
// ids, which the team builder needs for exact member↔row matching (name matching breaks on
|
||||
// the `unknown` fallback).
|
||||
export interface QueueDto {
|
||||
id: number
|
||||
type: TaskType
|
||||
@@ -34,6 +37,9 @@ export interface QueueDto {
|
||||
leadName: string
|
||||
helperNames: string[]
|
||||
remaining: number
|
||||
status: TaskStatus // ACTIVE block first in the array, then PENDING
|
||||
leadCrewId: number // crew.id of the team lead
|
||||
helperCrewIds: number[] // crew.id of the supporting crew (degrades to [] on a corrupted blob, like helperNames)
|
||||
}
|
||||
|
||||
export interface WorldState {
|
||||
@@ -251,12 +257,14 @@ async function taskQueue(db: GameDb): Promise<QueueDto[]> {
|
||||
helperNames: parseHelpers(t.helpers).map(crewName),
|
||||
remaining: t.remaining,
|
||||
status: t.status as TaskStatus,
|
||||
leadCrewId: t.leadCrewId,
|
||||
helperCrewIds: parseHelpers(t.helpers),
|
||||
queuedAtTurn: t.queuedAtTurn,
|
||||
}))
|
||||
const byQueueOrder = (rows: typeof queue) => rows.sort((a, b) => a.queuedAtTurn - b.queuedAtTurn || a.id - b.id)
|
||||
const ordered = [...byQueueOrder(queue.filter((t) => t.status === 'ACTIVE')), ...byQueueOrder(queue.filter((t) => t.status === 'PENDING'))]
|
||||
// The sort keys are only for ordering — the exposed shape is exactly QueueDto.
|
||||
return ordered.map(({ status, queuedAtTurn, ...dto }) => dto)
|
||||
// queuedAtTurn is only for ordering — the exposed shape is exactly QueueDto (status stays: JC-11).
|
||||
return ordered.map(({ queuedAtTurn, ...dto }) => dto)
|
||||
}
|
||||
|
||||
export async function getWorldState(db: GameDb): Promise<WorldState> {
|
||||
|
||||
+4
-2
@@ -131,9 +131,11 @@ describe('OpenAPI spec', () => {
|
||||
expect((crewState.properties.tasks as { items: { $ref: string } }).items.$ref).toBe('#/components/schemas/TaskDto');
|
||||
|
||||
const taskDto = openApiSpec.components.schemas.TaskDto as { properties: Record<string, unknown>; required: string[] };
|
||||
expect(Object.keys(taskDto.properties)).toEqual(['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining']);
|
||||
expect(taskDto.required).toEqual(['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining']);
|
||||
expect(Object.keys(taskDto.properties)).toEqual(['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining', 'status', 'leadCrewId', 'helperCrewIds']);
|
||||
expect(taskDto.required).toEqual(['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining', 'status', 'leadCrewId', 'helperCrewIds']);
|
||||
expect((taskDto.properties.type as { enum: string[] }).enum).toEqual(['JUMP', 'EXPLORE', 'SHIP_UPGRADE', 'SHIP_EXPAND', 'SHIP_BUILD', 'RESEARCH']);
|
||||
expect((taskDto.properties.status as { enum: string[] }).enum).toEqual(['ACTIVE', 'PENDING']);
|
||||
expect((taskDto.properties.helperCrewIds as { items: { type: string } }).items.type).toBe('integer');
|
||||
|
||||
const member = openApiSpec.components.schemas.CrewMember.properties;
|
||||
expect(Object.keys(member)).toEqual(['id', 'name', 'role', 'trait_good', 'trait_bad', 'origin']);
|
||||
|
||||
+12
-1
@@ -913,8 +913,19 @@ export const openApiSpec = {
|
||||
leadName: { type: 'string', description: 'The team lead by name; `unknown` when the crew row no longer exists' },
|
||||
helperNames: { type: 'array', description: 'The supporting crew by name (empty when none)', items: { type: 'string' } },
|
||||
remaining: { type: 'integer', description: 'Turns left — the stored row value (a pure read, not recomputed)' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['ACTIVE', 'PENDING'],
|
||||
description: 'The row state — the ACTIVE block precedes the PENDING block in the array',
|
||||
},
|
||||
leadCrewId: { type: 'integer', description: 'crew.id of the team lead' },
|
||||
helperCrewIds: {
|
||||
type: 'array',
|
||||
description: 'crew.id of the supporting crew (empty when none)',
|
||||
items: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
required: ['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining'],
|
||||
required: ['id', 'type', 'targetLabel', 'leadName', 'helperNames', 'remaining', 'status', 'leadCrewId', 'helperCrewIds'],
|
||||
},
|
||||
JumpRequest: {
|
||||
type: 'object',
|
||||
|
||||
Reference in New Issue
Block a user