docs: add inventions panel implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,869 @@
|
||||
# Inventions Panel Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add an "I" (Inventions) tab to the left-side panel showing a scrollable log of all inventions with name, category, materials, requirements, inventor, and day.
|
||||
|
||||
**Architecture:** Extend shared types with `InventionSummary` and two new socket events. Extend `InventionTimeline` to store full recipe data. Add `InventionsPanel` UI class and wire it through `SocketClient`, `SocketServer`, and `GameScene`. Add notification dot to the tab.
|
||||
|
||||
**Tech Stack:** TypeScript, Socket.io, Phaser 3, HTML/CSS DOM overlays
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add InventionSummary type and socket events to shared types
|
||||
|
||||
**Files:**
|
||||
- Modify: `shared/src/types.ts`
|
||||
|
||||
**Step 1: Add the InventionSummary interface and socket events**
|
||||
|
||||
In `shared/src/types.ts`, add after the `SuperlativesData` interface (around line 217):
|
||||
|
||||
```typescript
|
||||
export interface InventionSummary {
|
||||
itemId: string;
|
||||
name: string;
|
||||
category: 'resource' | 'tool' | 'material' | 'structure';
|
||||
inputs: { itemId: string; quantity: number }[];
|
||||
workshopType: string | null;
|
||||
toolRequired: string | null;
|
||||
inventorName: string;
|
||||
day: number;
|
||||
}
|
||||
```
|
||||
|
||||
In the `ServerEvents` interface, add two new events after the `'memory-history'` line:
|
||||
|
||||
```typescript
|
||||
'invention-event': (data: InventionSummary) => void;
|
||||
'invention-history': (data: InventionSummary[]) => void;
|
||||
```
|
||||
|
||||
**Step 2: Rebuild shared types**
|
||||
|
||||
Run: `npx -w shared tsc`
|
||||
Expected: Clean compile, no errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add shared/src/types.ts
|
||||
git commit -m "feat(shared): add InventionSummary type and socket events"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Extend InventionTimeline to store full invention data
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/industry/inventionTimeline.ts`
|
||||
- Test: `server/src/industry/__tests__/inventionTimeline.test.ts`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Add to `inventionTimeline.test.ts`:
|
||||
|
||||
```typescript
|
||||
it('stores and returns full invention summary data', () => {
|
||||
const timeline = createInventionTimeline();
|
||||
timeline.record({
|
||||
itemId: 'rope',
|
||||
inventorEntityId: 1,
|
||||
inventorName: 'Gwen',
|
||||
tick: 500,
|
||||
day: 2,
|
||||
name: 'Rope',
|
||||
category: 'material',
|
||||
inputs: [{ itemId: 'plant_fiber', quantity: 3 }],
|
||||
workshopType: null,
|
||||
toolRequired: null,
|
||||
});
|
||||
const summaries = timeline.getSummaries();
|
||||
expect(summaries).toHaveLength(1);
|
||||
expect(summaries[0]).toEqual({
|
||||
itemId: 'rope',
|
||||
name: 'Rope',
|
||||
category: 'material',
|
||||
inputs: [{ itemId: 'plant_fiber', quantity: 3 }],
|
||||
workshopType: null,
|
||||
toolRequired: null,
|
||||
inventorName: 'Gwen',
|
||||
day: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('getSummaries returns entries in insertion order', () => {
|
||||
const timeline = createInventionTimeline();
|
||||
timeline.record({
|
||||
itemId: 'a', inventorEntityId: 1, inventorName: 'X', tick: 300, day: 1,
|
||||
name: 'A Item', category: 'tool', inputs: [], workshopType: null, toolRequired: null,
|
||||
});
|
||||
timeline.record({
|
||||
itemId: 'b', inventorEntityId: 2, inventorName: 'Y', tick: 100, day: 1,
|
||||
name: 'B Item', category: 'resource', inputs: [], workshopType: null, toolRequired: null,
|
||||
});
|
||||
const summaries = timeline.getSummaries();
|
||||
expect(summaries[0].itemId).toBe('a');
|
||||
expect(summaries[1].itemId).toBe('b');
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose src/industry/__tests__/inventionTimeline.test.ts`
|
||||
Expected: FAIL — `name`, `category`, `inputs`, `workshopType`, `toolRequired` not part of `InventionEntry`; `getSummaries` does not exist
|
||||
|
||||
**Step 3: Update InventionEntry and add getSummaries**
|
||||
|
||||
Replace the contents of `server/src/industry/inventionTimeline.ts`:
|
||||
|
||||
```typescript
|
||||
import type { EntityId, InventionSummary } from '@dflike/shared';
|
||||
|
||||
export interface InventionEntry {
|
||||
itemId: string;
|
||||
inventorEntityId: EntityId;
|
||||
inventorName: string;
|
||||
tick: number;
|
||||
day: number;
|
||||
name: string;
|
||||
category: 'resource' | 'tool' | 'material' | 'structure';
|
||||
inputs: { itemId: string; quantity: number }[];
|
||||
workshopType: string | null;
|
||||
toolRequired: string | null;
|
||||
}
|
||||
|
||||
export interface InventionTimeline {
|
||||
record(entry: InventionEntry): void;
|
||||
getAll(): InventionEntry[];
|
||||
getSummaries(): InventionSummary[];
|
||||
countByEntity(entityId: EntityId): number;
|
||||
}
|
||||
|
||||
export function createInventionTimeline(): InventionTimeline {
|
||||
const entries: InventionEntry[] = [];
|
||||
|
||||
return {
|
||||
record(entry: InventionEntry): void {
|
||||
entries.push(entry);
|
||||
},
|
||||
|
||||
getAll(): InventionEntry[] {
|
||||
return [...entries];
|
||||
},
|
||||
|
||||
getSummaries(): InventionSummary[] {
|
||||
return entries.map(e => ({
|
||||
itemId: e.itemId,
|
||||
name: e.name,
|
||||
category: e.category,
|
||||
inputs: e.inputs,
|
||||
workshopType: e.workshopType,
|
||||
toolRequired: e.toolRequired,
|
||||
inventorName: e.inventorName,
|
||||
day: e.day,
|
||||
}));
|
||||
},
|
||||
|
||||
countByEntity(entityId: EntityId): number {
|
||||
return entries.filter(e => e.inventorEntityId === entityId).length;
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update existing tests to include new required fields**
|
||||
|
||||
The existing tests pass minimal `InventionEntry` objects. Update each `timeline.record(...)` call in `inventionTimeline.test.ts` to include the new fields with defaults. Add these fields to every existing record call:
|
||||
|
||||
```typescript
|
||||
name: 'Item', category: 'resource' as const, inputs: [], workshopType: null, toolRequired: null,
|
||||
```
|
||||
|
||||
For example, the first test becomes:
|
||||
```typescript
|
||||
timeline.record({
|
||||
itemId: 'rope',
|
||||
inventorEntityId: 1,
|
||||
inventorName: 'Gwen',
|
||||
tick: 500,
|
||||
day: 2,
|
||||
name: 'Rope',
|
||||
category: 'material',
|
||||
inputs: [],
|
||||
workshopType: null,
|
||||
toolRequired: null,
|
||||
});
|
||||
```
|
||||
|
||||
**Step 5: Run tests to verify they pass**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose src/industry/__tests__/inventionTimeline.test.ts`
|
||||
Expected: All tests PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/industry/inventionTimeline.ts server/src/industry/__tests__/inventionTimeline.test.ts
|
||||
git commit -m "feat(server): extend InventionTimeline with full invention data and getSummaries"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Update inventionRegistrar to pass full data to timeline
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/industry/inventionRegistrar.ts`
|
||||
- Test: `server/src/industry/__tests__/inventionRegistrar.test.ts`
|
||||
|
||||
**Step 1: Update the timeline.record call in registerInvention**
|
||||
|
||||
In `server/src/industry/inventionRegistrar.ts`, change line 45 from:
|
||||
|
||||
```typescript
|
||||
timeline.record({ itemId, inventorEntityId, inventorName, tick, day });
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
timeline.record({
|
||||
itemId,
|
||||
inventorEntityId,
|
||||
inventorName,
|
||||
tick,
|
||||
day,
|
||||
name: raw.name,
|
||||
category: raw.category as 'resource' | 'tool' | 'material' | 'structure',
|
||||
inputs: raw.inputs,
|
||||
workshopType: raw.workshopType ?? null,
|
||||
toolRequired: raw.toolRequired ?? null,
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run existing registrar tests to verify nothing broke**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose src/industry/__tests__/inventionRegistrar.test.ts`
|
||||
Expected: All tests PASS (the test mocks the timeline, so the extra fields are just passed through)
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/industry/inventionRegistrar.ts
|
||||
git commit -m "feat(server): pass full invention data to timeline on registration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add invention callback and socket events to SocketServer
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/systems/inventionSystem.ts`
|
||||
- Modify: `server/src/network/SocketServer.ts`
|
||||
|
||||
**Step 1: Add onInventionCreated callback to inventionSystem**
|
||||
|
||||
In `server/src/systems/inventionSystem.ts`, after the `registerInvention(...)` call (around line 107), add:
|
||||
|
||||
```typescript
|
||||
// Emit invention summary for UI
|
||||
const summary: InventionSummary = {
|
||||
itemId: validation.itemId!,
|
||||
name: parsed.name,
|
||||
category: parsed.category as 'resource' | 'tool' | 'material' | 'structure',
|
||||
inputs: parsed.inputs,
|
||||
workshopType: parsed.workshopType ?? null,
|
||||
toolRequired: parsed.toolRequired ?? null,
|
||||
inventorName: name,
|
||||
day,
|
||||
};
|
||||
onInventionCreated?.(summary);
|
||||
```
|
||||
|
||||
Add `InventionSummary` to the import from `@dflike/shared`. Add `onInventionCreated` as a parameter to `createInventionSystem`:
|
||||
|
||||
```typescript
|
||||
export function createInventionSystem(
|
||||
llmService: LlmService,
|
||||
narrationService: NarrationService,
|
||||
eventMemoryService: EventMemoryService,
|
||||
onInventionCreated?: (summary: InventionSummary) => void,
|
||||
): InventionSystem {
|
||||
```
|
||||
|
||||
**Step 2: Wire up in SocketServer**
|
||||
|
||||
In `server/src/network/SocketServer.ts`, in the `setupBroadcast` method, after the existing narration/thought/memory hooks, add:
|
||||
|
||||
```typescript
|
||||
this.gameLoop.onInventionCreated = (summary) => {
|
||||
this.io.emit('invention-event', summary);
|
||||
};
|
||||
```
|
||||
|
||||
In the `setupConnections` method, after `socket.emit('narration-history', ...)` (line 85), add:
|
||||
|
||||
```typescript
|
||||
// Send invention history
|
||||
const inventionTimeline = this.gameLoop.world.getSingleton<InventionTimeline>('inventionTimeline');
|
||||
if (inventionTimeline) {
|
||||
socket.emit('invention-history', inventionTimeline.getSummaries());
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Wire the callback in GameLoop**
|
||||
|
||||
The `onInventionCreated` callback needs to be exposed on `GameLoop`. Check how `GameLoop` creates the invention system and add the callback parameter. In `GameLoop`, add a public property:
|
||||
|
||||
```typescript
|
||||
public onInventionCreated: ((summary: InventionSummary) => void) | null = null;
|
||||
```
|
||||
|
||||
Pass it when creating the invention system (or pass a closure that delegates to it).
|
||||
|
||||
**Step 4: Run server tests to verify nothing broke**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose`
|
||||
Expected: All 338 tests PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/systems/inventionSystem.ts server/src/network/SocketServer.ts server/src/game/GameLoop.ts
|
||||
git commit -m "feat(server): emit invention-event and invention-history socket events"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add invention callbacks to SocketClient
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/network/SocketClient.ts`
|
||||
|
||||
**Step 1: Add the callbacks and listeners**
|
||||
|
||||
In `client/src/network/SocketClient.ts`, add to the imports:
|
||||
|
||||
```typescript
|
||||
import type { ..., InventionSummary } from '@dflike/shared';
|
||||
```
|
||||
|
||||
Add two new callback properties after `onMemoryHistory`:
|
||||
|
||||
```typescript
|
||||
onInventionEvent: ((data: InventionSummary) => void) | null = null;
|
||||
onInventionHistory: ((data: InventionSummary[]) => void) | null = null;
|
||||
```
|
||||
|
||||
Add two new socket listeners in the constructor, after the `memory-history` listener:
|
||||
|
||||
```typescript
|
||||
this.socket.on('invention-event', (data) => this.onInventionEvent?.(data));
|
||||
this.socket.on('invention-history', (data) => this.onInventionHistory?.(data));
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/network/SocketClient.ts
|
||||
git commit -m "feat(client): add invention event/history callbacks to SocketClient"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Create InventionsPanel UI component
|
||||
|
||||
**Files:**
|
||||
- Create: `client/src/ui/InventionsPanel.ts`
|
||||
|
||||
**Step 1: Create the panel**
|
||||
|
||||
Create `client/src/ui/InventionsPanel.ts`:
|
||||
|
||||
```typescript
|
||||
import type { InventionSummary } from '@dflike/shared';
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
resource: '#6bc46b',
|
||||
tool: '#d0a050',
|
||||
material: '#8090d0',
|
||||
structure: '#c07070',
|
||||
};
|
||||
|
||||
export class InventionsPanel {
|
||||
private container: HTMLDivElement;
|
||||
private listEl: HTMLDivElement;
|
||||
private emptyEl: HTMLDivElement;
|
||||
private inventions: InventionSummary[] = [];
|
||||
private _hasUnseen = false;
|
||||
onUnseenChanged: ((hasUnseen: boolean) => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
this.emptyEl = document.createElement('div');
|
||||
this.emptyEl.style.cssText = `
|
||||
color: #8878a8;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
this.emptyEl.textContent = 'No inventions yet...';
|
||||
|
||||
this.listEl = document.createElement('div');
|
||||
this.listEl.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
this.container.appendChild(this.emptyEl);
|
||||
this.container.appendChild(this.listEl);
|
||||
}
|
||||
|
||||
getElement(): HTMLDivElement {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
get hasUnseen(): boolean {
|
||||
return this._hasUnseen;
|
||||
}
|
||||
|
||||
clearUnseen(): void {
|
||||
this._hasUnseen = false;
|
||||
this.onUnseenChanged?.(false);
|
||||
}
|
||||
|
||||
addInvention(data: InventionSummary): void {
|
||||
this.inventions.push(data);
|
||||
this.emptyEl.style.display = 'none';
|
||||
// Prepend (newest first)
|
||||
const card = this.createCard(data);
|
||||
this.listEl.insertBefore(card, this.listEl.firstChild);
|
||||
this._hasUnseen = true;
|
||||
this.onUnseenChanged?.(true);
|
||||
}
|
||||
|
||||
loadHistory(data: InventionSummary[]): void {
|
||||
this.inventions = [...data];
|
||||
this.listEl.innerHTML = '';
|
||||
if (data.length === 0) {
|
||||
this.emptyEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
this.emptyEl.style.display = 'none';
|
||||
// Show newest first
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
this.listEl.appendChild(this.createCard(data[i]));
|
||||
}
|
||||
}
|
||||
|
||||
private createCard(data: InventionSummary): HTMLDivElement {
|
||||
const card = document.createElement('div');
|
||||
card.style.cssText = `
|
||||
background: #22223a;
|
||||
border: 1px solid #8878a8;
|
||||
padding: 6px 8px;
|
||||
font-size: 9px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
// Name + category tag
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 3px;
|
||||
`;
|
||||
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.style.cssText = `color: #e0d0b0; font-size: 10px;`;
|
||||
nameEl.textContent = data.name;
|
||||
|
||||
const tagEl = document.createElement('span');
|
||||
const tagColor = CATEGORY_COLORS[data.category] ?? '#888';
|
||||
tagEl.style.cssText = `
|
||||
color: ${tagColor};
|
||||
font-size: 8px;
|
||||
border: 1px solid ${tagColor};
|
||||
padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
`;
|
||||
tagEl.textContent = data.category;
|
||||
|
||||
header.appendChild(nameEl);
|
||||
header.appendChild(tagEl);
|
||||
card.appendChild(header);
|
||||
|
||||
// Materials
|
||||
if (data.inputs.length > 0) {
|
||||
const materialsEl = document.createElement('div');
|
||||
materialsEl.style.cssText = `color: #a0a0c0; font-size: 8px;`;
|
||||
materialsEl.textContent = data.inputs
|
||||
.map(i => `${i.quantity}x ${i.itemId.replace(/_/g, ' ')}`)
|
||||
.join(', ');
|
||||
card.appendChild(materialsEl);
|
||||
}
|
||||
|
||||
// Requirements
|
||||
const reqs: string[] = [];
|
||||
if (data.workshopType) reqs.push(data.workshopType.replace(/_/g, ' '));
|
||||
if (data.toolRequired) reqs.push(data.toolRequired.replace(/_/g, ' '));
|
||||
if (reqs.length > 0) {
|
||||
const reqEl = document.createElement('div');
|
||||
reqEl.style.cssText = `color: #c09060; font-size: 8px;`;
|
||||
reqEl.textContent = `Requires: ${reqs.join(', ')}`;
|
||||
card.appendChild(reqEl);
|
||||
}
|
||||
|
||||
// Footer: inventor + day
|
||||
const footer = document.createElement('div');
|
||||
footer.style.cssText = `
|
||||
color: #8878a8;
|
||||
font-size: 8px;
|
||||
margin-top: 3px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const inventorEl = document.createElement('span');
|
||||
inventorEl.textContent = data.inventorName;
|
||||
|
||||
const dayEl = document.createElement('span');
|
||||
dayEl.textContent = `Day ${data.day}`;
|
||||
|
||||
footer.appendChild(inventorEl);
|
||||
footer.appendChild(dayEl);
|
||||
card.appendChild(footer);
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/ui/InventionsPanel.ts
|
||||
git commit -m "feat(client): add InventionsPanel UI component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Add inventions tab to LeftPanel with notification dot
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/ui/LeftPanel.ts`
|
||||
|
||||
**Step 1: Extend LeftPanel constructor to accept inventions content**
|
||||
|
||||
Update the constructor signature to accept a third parameter:
|
||||
|
||||
```typescript
|
||||
constructor(
|
||||
superlativesContent: HTMLDivElement,
|
||||
eventsFeedContent: HTMLDivElement,
|
||||
inventionsContent: HTMLDivElement,
|
||||
)
|
||||
```
|
||||
|
||||
Add a new field:
|
||||
```typescript
|
||||
private inventionsContent: HTMLDivElement;
|
||||
private inventionsTab: HTMLDivElement;
|
||||
private notificationDot: HTMLDivElement;
|
||||
```
|
||||
|
||||
Store `inventionsContent` in constructor. Update `activeTab` type:
|
||||
```typescript
|
||||
private activeTab: 'stats' | 'events' | 'inventions' | null = null;
|
||||
```
|
||||
|
||||
**Step 2: Add the inventions tab with notification dot**
|
||||
|
||||
Create the "I" tab after the "E" tab in the constructor:
|
||||
|
||||
```typescript
|
||||
this.inventionsTab = this.createTab('I');
|
||||
this.inventionsTab.style.position = 'relative';
|
||||
this.notificationDot = document.createElement('div');
|
||||
this.notificationDot.style.cssText = `
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #f0d060;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
`;
|
||||
this.inventionsTab.appendChild(this.notificationDot);
|
||||
this.inventionsTab.addEventListener('click', () => {
|
||||
if (this.activeTab === 'inventions' && this.expanded) {
|
||||
this.collapse();
|
||||
} else {
|
||||
this.expand('inventions');
|
||||
}
|
||||
});
|
||||
|
||||
tabContainer.appendChild(this.inventionsTab);
|
||||
```
|
||||
|
||||
**Step 3: Update showContent to handle 'inventions'**
|
||||
|
||||
Add the inventions case to `showContent`:
|
||||
|
||||
```typescript
|
||||
} else if (tab === 'inventions') {
|
||||
this.headerEl.textContent = 'INVENTIONS';
|
||||
this.contentArea.appendChild(this.inventionsContent);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update updateTabs to handle inventions tab**
|
||||
|
||||
Add to `updateTabs`:
|
||||
|
||||
```typescript
|
||||
this.inventionsTab.style.background =
|
||||
this.activeTab === 'inventions' && this.expanded ? '#2a2a4e' : '#1a1a2e';
|
||||
this.inventionsTab.dataset.active =
|
||||
this.activeTab === 'inventions' && this.expanded ? 'true' : 'false';
|
||||
```
|
||||
|
||||
**Step 5: Update expand to accept 'inventions' tab type**
|
||||
|
||||
Update the `expand` method signature:
|
||||
```typescript
|
||||
expand(tab: 'stats' | 'events' | 'inventions'): void {
|
||||
```
|
||||
|
||||
Also update `showContent` signature:
|
||||
```typescript
|
||||
private showContent(tab: 'stats' | 'events' | 'inventions'): void {
|
||||
```
|
||||
|
||||
**Step 6: Add notification dot methods**
|
||||
|
||||
```typescript
|
||||
showNotificationDot(): void {
|
||||
this.notificationDot.style.display = '';
|
||||
}
|
||||
|
||||
hideNotificationDot(): void {
|
||||
this.notificationDot.style.display = 'none';
|
||||
}
|
||||
```
|
||||
|
||||
Also update `expand` — when opening the inventions tab, hide the dot:
|
||||
|
||||
```typescript
|
||||
expand(tab: 'stats' | 'events' | 'inventions'): void {
|
||||
this.activeTab = tab;
|
||||
this.expanded = true;
|
||||
this.showContent(tab);
|
||||
this.updateTabs();
|
||||
this.container.style.transform = 'translateY(-50%) translateX(0)';
|
||||
if (tab === 'inventions') {
|
||||
this.hideNotificationDot();
|
||||
}
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('left-panel-opened', { detail: { tab } }),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/ui/LeftPanel.ts
|
||||
git commit -m "feat(client): add inventions tab with notification dot to LeftPanel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Wire everything together in GameScene
|
||||
|
||||
**Files:**
|
||||
- Modify: `client/src/scenes/GameScene.ts`
|
||||
|
||||
**Step 1: Import InventionsPanel and wire it up**
|
||||
|
||||
Add import:
|
||||
```typescript
|
||||
import { InventionsPanel } from '../ui/InventionsPanel.js';
|
||||
```
|
||||
|
||||
Add field:
|
||||
```typescript
|
||||
private inventionsPanel!: InventionsPanel;
|
||||
```
|
||||
|
||||
In `create()`, replace the LeftPanel construction block (around lines 73-78):
|
||||
|
||||
```typescript
|
||||
this.superlativesPanel = new SuperlativesPanel();
|
||||
this.eventsFeed = new EventsFeed();
|
||||
this.inventionsPanel = new InventionsPanel();
|
||||
this.leftPanel = new LeftPanel(
|
||||
this.superlativesPanel.getElement(),
|
||||
this.eventsFeed.getElement(),
|
||||
this.inventionsPanel.getElement(),
|
||||
);
|
||||
|
||||
this.inventionsPanel.onUnseenChanged = (hasUnseen) => {
|
||||
if (hasUnseen && this.leftPanel.getActiveTab() !== 'inventions') {
|
||||
this.leftPanel.showNotificationDot();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Update the `left-panel-opened` event listener to handle the inventions tab clearing unseen state:
|
||||
|
||||
```typescript
|
||||
document.addEventListener('left-panel-opened', ((e: CustomEvent) => {
|
||||
if (e.detail.tab === 'stats') {
|
||||
this.client.subscribeSuperlatives();
|
||||
}
|
||||
if (e.detail.tab === 'inventions') {
|
||||
this.inventionsPanel.clearUnseen();
|
||||
}
|
||||
}) as EventListener);
|
||||
```
|
||||
|
||||
Wire the invention socket callbacks after the memory callbacks:
|
||||
|
||||
```typescript
|
||||
this.client.onInventionEvent = (data) => {
|
||||
this.inventionsPanel.addInvention(data);
|
||||
};
|
||||
|
||||
this.client.onInventionHistory = (data) => {
|
||||
this.inventionsPanel.loadHistory(data);
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Build client to verify compilation**
|
||||
|
||||
Run: `npm -w client run build`
|
||||
Expected: Clean build, no errors
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add client/src/scenes/GameScene.ts
|
||||
git commit -m "feat(client): wire InventionsPanel into GameScene"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Update GameLoop to pass invention callback
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/game/GameLoop.ts`
|
||||
|
||||
**Step 1: Check how GameLoop creates the invention system**
|
||||
|
||||
Read `server/src/game/GameLoop.ts` to find where `createInventionSystem` is called. Add the `onInventionCreated` callback as the fourth parameter.
|
||||
|
||||
Add a public property to GameLoop:
|
||||
```typescript
|
||||
public onInventionCreated: ((summary: InventionSummary) => void) | null = null;
|
||||
```
|
||||
|
||||
When creating the invention system, pass a closure:
|
||||
```typescript
|
||||
const inventionSystem = createInventionSystem(
|
||||
this.llmService,
|
||||
this.narrationService,
|
||||
this.eventMemoryService,
|
||||
(summary) => this.onInventionCreated?.(summary),
|
||||
);
|
||||
```
|
||||
|
||||
Import `InventionSummary` from `@dflike/shared`.
|
||||
|
||||
**Step 2: Run all server tests**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose`
|
||||
Expected: All tests PASS
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/game/GameLoop.ts
|
||||
git commit -m "feat(server): expose onInventionCreated callback from GameLoop"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Fix any remaining test failures from InventionEntry changes
|
||||
|
||||
**Files:**
|
||||
- Modify: Any test files that create `InventionEntry` objects directly
|
||||
|
||||
**Step 1: Search for all test files that use `InventionEntry` or `timeline.record`**
|
||||
|
||||
Search for `timeline.record` and `InventionEntry` across all test files. Files likely affected:
|
||||
- `server/src/industry/__tests__/inventionRegistrar.test.ts`
|
||||
- `server/src/systems/__tests__/inventionSystem.test.ts`
|
||||
- `server/src/systems/__tests__/inventionIntegration.test.ts`
|
||||
|
||||
**Step 2: Update each test to include the new required fields**
|
||||
|
||||
Add the new fields (`name`, `category`, `inputs`, `workshopType`, `toolRequired`) wherever `InventionEntry` objects are constructed. Use sensible defaults like:
|
||||
|
||||
```typescript
|
||||
name: 'Test Item',
|
||||
category: 'resource' as const,
|
||||
inputs: [],
|
||||
workshopType: null,
|
||||
toolRequired: null,
|
||||
```
|
||||
|
||||
**Step 3: Run all server tests**
|
||||
|
||||
Run: `npm -w server run test -- --reporter=verbose`
|
||||
Expected: All tests PASS
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "test(server): update tests for extended InventionEntry fields"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: End-to-end verification
|
||||
|
||||
**Step 1: Build shared types**
|
||||
|
||||
Run: `npx -w shared tsc`
|
||||
Expected: Clean compile
|
||||
|
||||
**Step 2: Run all server tests**
|
||||
|
||||
Run: `npm -w server run test`
|
||||
Expected: All tests PASS
|
||||
|
||||
**Step 3: Build client**
|
||||
|
||||
Run: `npm -w client run build`
|
||||
Expected: Clean build
|
||||
|
||||
**Step 4: Manual smoke test (optional)**
|
||||
|
||||
Run server and client, verify:
|
||||
- "I" tab appears on the left panel
|
||||
- Tab shows notification dot when inventions occur
|
||||
- Clicking "I" shows the inventions list
|
||||
- Each entry shows name, category tag, materials, requirements, inventor, day
|
||||
Reference in New Issue
Block a user