Merge branch 'worktree-relationship-system'
# Conflicts: # client/src/ui/CommandPanel.ts
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user