Merge branch 'worktree-relationship-system'

# Conflicts:
#	client/src/ui/CommandPanel.ts
This commit is contained in:
root
2026-03-07 16:47:43 +00:00
4 changed files with 375 additions and 9 deletions
+51 -1
View File
@@ -30,6 +30,9 @@ export class GameScene extends Phaser.Scene {
private npcInfoPanel!: NpcInfoPanel;
private emojiManager!: InteractionEmojiManager;
private commandPanel!: CommandPanel;
private followCommandPanel!: CommandPanel;
private highlightEnabled = false;
private highlightedSpriteId: number | null = null;
constructor() {
super({ key: 'GameScene' });
@@ -45,8 +48,13 @@ export class GameScene extends Phaser.Scene {
this.portraitCompositor = new PortraitCompositor();
this.npcInfoPanel = new NpcInfoPanel();
this.emojiManager = new InteractionEmojiManager();
this.commandPanel = new CommandPanel();
this.commandPanel = new CommandPanel('command-panel', 'COMMANDS', [{ key: '1', label: 'Spawn NPC' }]);
this.commandPanel.show(); // Start in camera mode, so show immediately
this.followCommandPanel = new CommandPanel('follow-command-panel', 'FOLLOW', [
{ key: '\u2190 \u2192', label: 'Cycle NPC' },
{ key: '1', label: 'Highlight' },
{ key: 'TAB', label: 'Camera Mode' },
]);
// Draw tile grid
this.drawWorld();
@@ -79,8 +87,12 @@ export class GameScene extends Phaser.Scene {
if (this.mode === 'follow') {
this.showFollowPanel();
this.commandPanel.hide();
this.followCommandPanel.show();
} else if (prevMode === 'follow') {
this.npcInfoPanel.hide();
this.followCommandPanel.hide();
this.clearHighlight();
this.highlightEnabled = false;
}
// Command panel visibility
@@ -93,6 +105,17 @@ export class GameScene extends Phaser.Scene {
// Spawn NPC command (camera mode only)
this.input.keyboard!.addKey('ONE').on('down', () => {
if (this.mode === 'follow') {
this.highlightEnabled = !this.highlightEnabled;
if (this.highlightEnabled) {
const npcIds = this.getNpcIds();
const targetId = npcIds[this.followTargetIndex];
if (targetId != null) this.applyHighlight(targetId);
} else {
this.clearHighlight();
}
return;
}
if (this.mode !== 'camera') return;
const cam = this.cameras.main;
const centerTileX = Math.floor((cam.scrollX + cam.width / 2) / TILE_SIZE);
@@ -257,6 +280,9 @@ export class GameScene extends Phaser.Scene {
for (const [id, es] of this.entitySprites) {
if (!activeIds.has(id)) {
es.sprite.destroy();
if (id === this.highlightedSpriteId) {
this.highlightedSpriteId = null;
}
this.entitySprites.delete(id);
}
}
@@ -346,11 +372,17 @@ export class GameScene extends Phaser.Scene {
if (left) {
this.followTargetIndex = (this.followTargetIndex - 1 + npcIds.length) % npcIds.length;
this.followThrottle = 150;
if (this.highlightEnabled) {
this.applyHighlight(npcIds[this.followTargetIndex]);
}
this.updateModeText();
this.updateFollowPanel();
} else if (right) {
this.followTargetIndex = (this.followTargetIndex + 1) % npcIds.length;
this.followThrottle = 150;
if (this.highlightEnabled) {
this.applyHighlight(npcIds[this.followTargetIndex]);
}
this.updateModeText();
this.updateFollowPanel();
}
@@ -406,4 +438,22 @@ export class GameScene extends Phaser.Scene {
this.modeText.setText(`Mode: ${this.mode.toUpperCase()} [TAB to toggle]`);
}
}
private applyHighlight(entityId: number): void {
const es = this.entitySprites.get(entityId);
if (!es) return;
this.clearHighlight();
es.sprite.postFX.addGlow(0xff0000, 2, 0, false, 0.1, 4);
this.highlightedSpriteId = entityId;
}
private clearHighlight(): void {
if (this.highlightedSpriteId != null) {
const es = this.entitySprites.get(this.highlightedSpriteId);
if (es) {
es.sprite.postFX.clear();
}
this.highlightedSpriteId = null;
}
}
}
+10 -8
View File
@@ -21,10 +21,10 @@ export class CommandPanel {
private listContainer: HTMLDivElement;
private visible = false;
constructor() {
constructor(id = 'command-panel', title = 'COMMANDS', commands: Command[] = [{ key: '1', label: 'Spawn NPC' }]) {
// Outer frame (doubled border, same as NpcInfoPanel)
this.outerFrame = document.createElement('div');
this.outerFrame.id = 'command-panel';
this.outerFrame.id = id;
this.outerFrame.style.cssText = `
position: fixed;
bottom: 16px;
@@ -76,8 +76,8 @@ export class CommandPanel {
container.appendChild(shine);
// Title
const title = document.createElement('div');
title.style.cssText = `
const titleEl = document.createElement('div');
titleEl.style.cssText = `
font-size: 9px;
color: ${EB.textMuted};
text-align: center;
@@ -87,8 +87,8 @@ export class CommandPanel {
position: relative;
z-index: 2;
`;
title.textContent = '\u25C6 COMMANDS \u25C6';
container.appendChild(title);
titleEl.textContent = `\u25C6 ${title} \u25C6`;
container.appendChild(titleEl);
// Commands list
this.listContainer = document.createElement('div');
@@ -104,8 +104,10 @@ export class CommandPanel {
this.outerFrame.appendChild(container);
document.body.appendChild(this.outerFrame);
// Add default commands
this.addCommand({ key: '1', label: 'Spawn NPC' });
// Add commands
for (const cmd of commands) {
this.addCommand(cmd);
}
}
private addCommand(cmd: Command): void {
@@ -0,0 +1,48 @@
# Follow Mode Highlight & Command Legend
## Problem
With many NPCs on screen, it's hard to identify which NPC you're following. Follow mode also lacks a command legend (camera mode has one).
## Design
### Highlight Effect
- Phaser `postFX.addGlow(0xff0000, outlineSize)` on the followed NPC's sprite
- Toggle on/off with `1` key (only in follow mode)
- Default: OFF
- When cycling NPCs (left/right), move glow from old sprite to new sprite (if enabled)
- Clear glow when switching back to camera mode
### Mode-Specific Key Bindings
The `1` key already gates on camera mode (`if (this.mode !== 'camera') return`). Add a parallel follow-mode branch:
- Camera mode `1`: Spawn NPC (existing)
- Follow mode `1`: Toggle highlight
### Follow Mode Command Legend
Reuse existing `CommandPanel` class with a second instance:
| Key | Label |
|-----|-------|
| ← → | Cycle NPC |
| 1 | Highlight |
| TAB | Camera Mode |
- Title: "FOLLOW" (matching camera panel's "COMMANDS" style)
- Show when entering follow mode, hide when leaving
- Same position/styling as camera mode command panel
### State
- `highlightEnabled: boolean` on GameScene (default `false`)
- `followCommandPanel: CommandPanel` on GameScene (separate instance)
### Data Flow
1. Enter follow mode → show follow command panel, hide camera command panel
2. Press `1` in follow mode → toggle `highlightEnabled`, apply/remove glow on current sprite
3. Cycle NPC → if highlight enabled, remove glow from old sprite, add to new sprite
4. Exit follow mode → clear glow, reset `highlightEnabled`, hide follow command panel
@@ -0,0 +1,266 @@
# Follow Mode Highlight & Command Legend — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a toggleable red glow highlight on the followed NPC and a follow-mode command legend panel.
**Architecture:** Parameterize `CommandPanel` to accept title and commands. Add highlight state and glow management to `GameScene`. Bind `1` key to toggle highlight in follow mode.
**Tech Stack:** Phaser 3 postFX (glow), HTML/CSS command panel (existing)
---
### Task 1: Parameterize CommandPanel constructor
**Files:**
- Modify: `client/src/ui/CommandPanel.ts`
**Step 1: Update constructor to accept title and commands**
Change the constructor signature and replace hardcoded values:
```typescript
export class CommandPanel {
private outerFrame: HTMLDivElement;
private listContainer: HTMLDivElement;
private visible = false;
constructor(title = 'COMMANDS', commands: Command[] = [{ key: '1', label: 'Spawn NPC' }]) {
```
Replace line 90:
```typescript
title.textContent = `\u25C6 ${title} \u25C6`;
```
Replace line 108 (the hardcoded `addCommand` call) with a loop:
```typescript
for (const cmd of commands) {
this.addCommand(cmd);
}
```
**Step 2: Update camera panel instantiation in GameScene**
In `GameScene.ts` line 48, pass explicit args to preserve current behavior:
```typescript
this.commandPanel = new CommandPanel('COMMANDS', [{ key: '1', label: 'Spawn NPC' }]);
```
**Step 3: Verify client builds**
Run: `npm -w client run build`
Expected: Build succeeds with no errors.
**Step 4: Commit**
```bash
git add client/src/ui/CommandPanel.ts client/src/scenes/GameScene.ts
git commit -m "refactor: parameterize CommandPanel title and commands"
```
---
### Task 2: Add follow mode command panel
**Files:**
- Modify: `client/src/scenes/GameScene.ts`
**Step 1: Add followCommandPanel property**
After line 32, add:
```typescript
private followCommandPanel!: CommandPanel;
```
**Step 2: Create follow panel in create()**
After line 49 (`this.commandPanel.show()`), add:
```typescript
this.followCommandPanel = new CommandPanel('FOLLOW', [
{ key: '\u2190 \u2192', label: 'Cycle NPC' },
{ key: '1', label: 'Highlight' },
{ key: 'TAB', label: 'Camera Mode' },
]);
```
**Step 3: Wire follow panel visibility into TAB handler**
In the TAB key handler (lines 68-92), update panel visibility logic.
When entering follow mode (line 79-81), replace:
```typescript
if (this.mode === 'follow') {
this.showFollowPanel();
this.commandPanel.hide();
}
```
with:
```typescript
if (this.mode === 'follow') {
this.showFollowPanel();
this.commandPanel.hide();
this.followCommandPanel.show();
}
```
When entering camera mode (lines 82-84), add follow panel hide:
```typescript
} else if (prevMode === 'follow') {
this.npcInfoPanel.hide();
this.followCommandPanel.hide();
}
```
The existing camera command panel show/hide block (lines 86-91) remains unchanged.
**Step 4: Verify client builds**
Run: `npm -w client run build`
Expected: Build succeeds.
**Step 5: Commit**
```bash
git add client/src/scenes/GameScene.ts
git commit -m "feat: add follow mode command legend panel"
```
---
### Task 3: Add highlight toggle and glow effect
**Files:**
- Modify: `client/src/scenes/GameScene.ts`
**Step 1: Add highlight state property**
After the `followCommandPanel` property, add:
```typescript
private highlightEnabled = false;
private highlightedSpriteId: number | null = null;
```
**Step 2: Add glow helper methods**
After `updateModeText()` method, add:
```typescript
private applyHighlight(entityId: number): void {
const es = this.entitySprites.get(entityId);
if (!es) return;
this.clearHighlight();
es.sprite.postFX.addGlow(0xff0000, 2, 0, false, 0.1, 4);
this.highlightedSpriteId = entityId;
}
private clearHighlight(): void {
if (this.highlightedSpriteId != null) {
const es = this.entitySprites.get(this.highlightedSpriteId);
if (es) {
es.sprite.postFX.clear();
}
this.highlightedSpriteId = null;
}
}
```
Note: `addGlow(color, outerStrength, innerStrength, knockout, quality, distance)` — we use a moderate outer strength of 2 with distance 4 for a visible but not overwhelming red outline. Adjust if needed after visual testing.
**Step 3: Add follow-mode branch to the ONE key handler**
In the `ONE` key handler (lines 95-101), change:
```typescript
this.input.keyboard!.addKey('ONE').on('down', () => {
if (this.mode !== 'camera') return;
```
to:
```typescript
this.input.keyboard!.addKey('ONE').on('down', () => {
if (this.mode === 'follow') {
this.highlightEnabled = !this.highlightEnabled;
if (this.highlightEnabled) {
const npcIds = this.getNpcIds();
const targetId = npcIds[this.followTargetIndex];
if (targetId != null) this.applyHighlight(targetId);
} else {
this.clearHighlight();
}
return;
}
if (this.mode !== 'camera') return;
```
**Step 4: Move glow when cycling NPCs**
In the follow mode NPC cycling section (lines 344-356), after each index change and before `this.updateModeText()`, add glow transfer:
For the `left` branch, after `this.followTargetIndex = ...`:
```typescript
if (this.highlightEnabled) {
this.applyHighlight(npcIds[this.followTargetIndex]);
}
```
Same for the `right` branch.
**Step 5: Clear highlight when exiting follow mode**
In the TAB handler, in the `else if (prevMode === 'follow')` block, add:
```typescript
this.clearHighlight();
this.highlightEnabled = false;
```
**Step 6: Handle entity removal edge case**
In `handleStateUpdate`, in the entity removal loop (lines 257-262), after `es.sprite.destroy()`, clear highlight if the removed entity was highlighted:
```typescript
if (id === this.highlightedSpriteId) {
this.highlightedSpriteId = null;
}
```
**Step 7: Verify client builds**
Run: `npm -w client run build`
Expected: Build succeeds.
**Step 8: Commit**
```bash
git add client/src/scenes/GameScene.ts
git commit -m "feat: add toggleable red glow highlight on followed NPC"
```
---
### Task 4: Manual visual verification
**Step 1: Start server and client**
Run in two terminals:
- `npm -w server run dev`
- `npm -w client run dev`
**Step 2: Verify camera mode**
- Confirm command legend shows "COMMANDS" with "1 Spawn NPC"
- Press `1` to spawn NPCs
- Confirm no follow command panel visible
**Step 3: Verify follow mode**
- Press TAB to enter follow mode
- Confirm command legend switches to "FOLLOW" with three commands
- Press left/right to cycle NPCs
- Press `1` to toggle highlight — confirm red glow appears around followed NPC
- Cycle NPCs — confirm glow follows to new NPC
- Press `1` again — confirm glow disappears
- Press TAB to return to camera mode — confirm follow panel hides, camera panel shows
**Step 4: Commit any adjustments**
If glow parameters need tuning (color, strength, distance), adjust in `applyHighlight()` and commit.