diff --git a/client/src/scenes/GameScene.ts b/client/src/scenes/GameScene.ts index e52b0a6..038f581 100644 --- a/client/src/scenes/GameScene.ts +++ b/client/src/scenes/GameScene.ts @@ -8,7 +8,7 @@ import { SuperlativesPanel } from '../ui/SuperlativesPanel.js'; import { LeftPanel } from '../ui/LeftPanel.js'; import { EventsFeed } from '../ui/EventsFeed.js'; import { InteractionEmojiManager } from '../ui/InteractionEmoji.js'; -import type { WorldState, StateUpdate, EntityState, Appearance } from '@dflike/shared'; +import type { WorldState, StateUpdate, EntityState, Appearance, NarrationEvent } from '@dflike/shared'; import { TILE_SIZE, SPRITE_FRAME_WIDTH, SPRITE_FRAME_HEIGHT, SPRITE_COLS, Direction, Terrain, TILESET_TILE_SIZE, TILESET_SCALE, DAY_HOURS, TOTAL_HOURS, SUNSET_DURATION_HOURS, SUNRISE_DURATION_HOURS, NIGHT_DARKNESS } from '@dflike/shared'; interface EntitySprite { @@ -40,6 +40,7 @@ export class GameScene extends Phaser.Scene { private highlightEnabled = false; private highlightedSpriteId: number | null = null; private dayNightOverlay!: Phaser.GameObjects.Graphics; + private narrationEvents: NarrationEvent[] = []; private currentGameTime = 0; private targetGameTime = 0; private lastPhaseKey = ''; @@ -173,9 +174,27 @@ export class GameScene extends Phaser.Scene { this.superlativesPanel.update(data); }; - this.client.onNarrationEvent = (event) => this.eventsFeed.addEvent(event); - this.client.onNarrationUpdate = (data) => this.eventsFeed.updateEvent(data.id, data.narration); - this.client.onNarrationHistory = (events) => this.eventsFeed.loadHistory(events); + this.client.onNarrationEvent = (event) => { + this.eventsFeed.addEvent(event); + this.narrationEvents.push(event); + if (this.narrationEvents.length > 50) { + this.narrationEvents = this.narrationEvents.slice(-50); + } + this.refreshRecentEvents(); + }; + this.client.onNarrationUpdate = (data) => { + this.eventsFeed.updateEvent(data.id, data.narration); + const existing = this.narrationEvents.find(e => e.id === data.id); + if (existing) { + existing.narration = data.narration; + } + this.refreshRecentEvents(); + }; + this.client.onNarrationHistory = (events) => { + this.eventsFeed.loadHistory(events); + this.narrationEvents = events.slice(-50); + this.refreshRecentEvents(); + }; document.addEventListener('narration-follow', ((e: CustomEvent) => { const { entityId } = e.detail; @@ -590,6 +609,7 @@ export class GameScene extends Phaser.Scene { const followed = this.entitySprites.get(followedId); if (followed) { this.npcInfoPanel.updateInfo(followed.lastState); + this.npcInfoPanel.updateRecentEvents(this.getEventsForEntity(followedId)); } } } @@ -703,6 +723,7 @@ export class GameScene extends Phaser.Scene { const portraitUrl = await this.portraitCompositor.compositePortrait(es.lastState.appearance); this.npcInfoPanel.show(es.lastState, portraitUrl); + this.npcInfoPanel.updateRecentEvents(this.getEventsForEntity(targetId)); } private async updateFollowPanel(): Promise { @@ -715,6 +736,7 @@ export class GameScene extends Phaser.Scene { const portraitUrl = await this.portraitCompositor.compositePortrait(es.lastState.appearance); this.npcInfoPanel.updatePortrait(portraitUrl); this.npcInfoPanel.updateInfo(es.lastState); + this.npcInfoPanel.updateRecentEvents(this.getEventsForEntity(targetId)); } private getNpcIds(): number[] { @@ -754,6 +776,22 @@ export class GameScene extends Phaser.Scene { } } + private getEventsForEntity(entityId: number): NarrationEvent[] { + return this.narrationEvents + .filter(e => e.entityIds[0] === entityId || e.entityIds[1] === entityId) + .slice(-3); + } + + private refreshRecentEvents(): void { + if (this.mode === 'follow' && this.npcInfoPanel.isVisible()) { + const npcIds = this.getNpcIds(); + const followedId = npcIds[this.followTargetIndex]; + if (followedId != null) { + this.npcInfoPanel.updateRecentEvents(this.getEventsForEntity(followedId)); + } + } + } + private getDayNightPhase(gameTime: number): { phase: 'day' | 'sunset' | 'night' | 'sunrise'; progress: number } { const hour = gameTime * TOTAL_HOURS; diff --git a/client/src/ui/NpcInfoPanel.ts b/client/src/ui/NpcInfoPanel.ts index 7bd0817..82c059e 100644 --- a/client/src/ui/NpcInfoPanel.ts +++ b/client/src/ui/NpcInfoPanel.ts @@ -1,4 +1,4 @@ -import type { EntityState } from '@dflike/shared'; +import type { EntityState, NarrationEvent } from '@dflike/shared'; const ACTIVITY_LABELS: Record = { wander: 'Wandering', @@ -32,6 +32,7 @@ export class NpcInfoPanel { private portraitImg: HTMLImageElement; private nameEl: HTMLDivElement; private activityEl: HTMLDivElement; + private recentEventsEl: HTMLDivElement; private separatorEl: HTMLDivElement; private needsBarsContainer: HTMLDivElement; private needsBars: Map = new Map(); @@ -216,6 +217,14 @@ export class NpcInfoPanel { `; this.statusContent.appendChild(this.activityEl); + // Recent events section (hidden by default) + this.recentEventsEl = document.createElement('div'); + this.recentEventsEl.style.cssText = ` + display: none; + padding: 2px 0; + `; + this.statusContent.appendChild(this.recentEventsEl); + // Decorative separator (EarthBound diamond pattern) this.separatorEl = document.createElement('div'); this.separatorEl.style.cssText = ` @@ -319,6 +328,43 @@ export class NpcInfoPanel { this.setNeedBar('Energy', needs.energy); } + updateRecentEvents(events: NarrationEvent[]): void { + if (events.length === 0) { + this.recentEventsEl.style.display = 'none'; + return; + } + + this.recentEventsEl.style.display = ''; + this.recentEventsEl.innerHTML = ''; + + const label = document.createElement('div'); + label.style.cssText = ` + font-family: 'Press Start 2P', monospace; + font-size: 8px; + color: ${EB.textMuted}; + margin-bottom: 3px; + `; + label.textContent = 'Recent:'; + this.recentEventsEl.appendChild(label); + + const recent = events.slice(-3); + for (const event of recent) { + const line = document.createElement('div'); + line.style.cssText = ` + font-family: 'Press Start 2P', monospace; + font-size: 10px; + color: ${EB.textSecondary}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.6; + `; + line.textContent = event.narration; + line.title = event.narration; + this.recentEventsEl.appendChild(line); + } + } + updatePortrait(portraitDataUrl: string): void { this.portraitImg.style.opacity = '0'; setTimeout(() => {