diff --git a/client/src/scenes/GameScene.ts b/client/src/scenes/GameScene.ts index f4ca8ad..4b9a0c6 100644 --- a/client/src/scenes/GameScene.ts +++ b/client/src/scenes/GameScene.ts @@ -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; + } + } } diff --git a/client/src/ui/CommandPanel.ts b/client/src/ui/CommandPanel.ts index 14e639f..a23b220 100644 --- a/client/src/ui/CommandPanel.ts +++ b/client/src/ui/CommandPanel.ts @@ -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 { diff --git a/docs/plans/2026-03-07-follow-highlight-design.md b/docs/plans/2026-03-07-follow-highlight-design.md new file mode 100644 index 0000000..f2ad0f4 --- /dev/null +++ b/docs/plans/2026-03-07-follow-highlight-design.md @@ -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 diff --git a/docs/plans/2026-03-07-follow-highlight-plan.md b/docs/plans/2026-03-07-follow-highlight-plan.md new file mode 100644 index 0000000..884eb83 --- /dev/null +++ b/docs/plans/2026-03-07-follow-highlight-plan.md @@ -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.