feat(thought): wire client socket, GameScene thought handling, emoji animation, and info panel display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 21:12:00 +00:00
parent b515f7195f
commit daddf2583d
3 changed files with 100 additions and 3 deletions
+2
View File
@@ -17,6 +17,7 @@ export class SocketClient {
onNarrationEvent: ((data: NarrationEvent) => void) | null = null;
onNarrationUpdate: ((data: { id: number; narration: string }) => void) | null = null;
onNarrationHistory: ((data: NarrationEvent[]) => void) | null = null;
onNpcThought: ((data: { entityId: number; text: string; emoji: string }) => void) | null = null;
constructor(url: string) {
this.socket = io(url);
@@ -48,6 +49,7 @@ export class SocketClient {
this.socket.on('narration-event', (data) => this.onNarrationEvent?.(data));
this.socket.on('narration-update', (data) => this.onNarrationUpdate?.(data));
this.socket.on('narration-history', (data) => this.onNarrationHistory?.(data));
this.socket.on('npc-thought', (data) => this.onNpcThought?.(data));
}
get playerId(): string | null { return this._playerId; }
+57
View File
@@ -41,6 +41,7 @@ export class GameScene extends Phaser.Scene {
private highlightedSpriteId: number | null = null;
private dayNightOverlay!: Phaser.GameObjects.Graphics;
private narrationEvents: NarrationEvent[] = [];
private npcThoughts: Map<number, { text: string; emoji: string }> = new Map();
private currentGameTime = 0;
private targetGameTime = 0;
private lastPhaseKey = '';
@@ -201,6 +202,12 @@ export class GameScene extends Phaser.Scene {
this.refreshRecentEvents();
};
this.client.onNpcThought = (data) => {
this.npcThoughts.set(data.entityId, { text: data.text, emoji: data.emoji });
this.refreshThought();
this.showThoughtEmoji(data.entityId, data.emoji);
};
document.addEventListener('narration-follow', ((e: CustomEvent) => {
const { entityId } = e.detail;
const npcIds = this.getNpcIds();
@@ -733,6 +740,8 @@ 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));
const thought = this.npcThoughts.get(targetId);
this.npcInfoPanel.updateThought(thought?.text ?? null);
}
private async updateFollowPanel(): Promise<void> {
@@ -746,6 +755,8 @@ export class GameScene extends Phaser.Scene {
this.npcInfoPanel.updatePortrait(portraitUrl);
this.npcInfoPanel.updateInfo(es.lastState);
this.npcInfoPanel.updateRecentEvents(this.getEventsForEntity(targetId));
const thought = this.npcThoughts.get(targetId);
this.npcInfoPanel.updateThought(thought?.text ?? null);
}
private getNpcIds(): number[] {
@@ -801,6 +812,52 @@ export class GameScene extends Phaser.Scene {
}
}
private refreshThought(): void {
if (this.mode === 'follow' && this.npcInfoPanel.isVisible()) {
const npcIds = this.getNpcIds();
const followedId = npcIds[this.followTargetIndex];
if (followedId != null) {
const thought = this.npcThoughts.get(followedId);
this.npcInfoPanel.updateThought(thought?.text ?? null);
}
}
}
private showThoughtEmoji(entityId: number, emoji: string): void {
const es = this.entitySprites.get(entityId);
if (!es) return;
const x = es.sprite.x;
const y = es.sprite.y - 30;
const text = this.add.text(x, y, emoji, { fontSize: '16px' })
.setOrigin(0.5)
.setDepth(1000)
.setAlpha(0);
// Fade in
this.tweens.add({
targets: text,
alpha: 1,
y: y - 8,
duration: 500,
ease: 'Power2',
onComplete: () => {
// Hold, then fade out
this.time.delayedCall(4000, () => {
this.tweens.add({
targets: text,
alpha: 0,
y: y - 16,
duration: 500,
ease: 'Power2',
onComplete: () => text.destroy(),
});
});
},
});
}
private getDayNightPhase(gameTime: number): { phase: 'day' | 'sunset' | 'night' | 'sunrise'; progress: number } {
const hour = gameTime * TOTAL_HOURS;
+41 -3
View File
@@ -33,6 +33,7 @@ export class NpcInfoPanel {
private nameEl: HTMLDivElement;
private activityEl: HTMLDivElement;
private recentEventsEl: HTMLDivElement;
private thoughtEl: HTMLDivElement;
private separatorEl: HTMLDivElement;
private needsBarsContainer: HTMLDivElement;
private needsBars: Map<string, { wrapper: HTMLDivElement; fill: HTMLDivElement; valueEl: HTMLDivElement }> = new Map();
@@ -60,6 +61,9 @@ export class NpcInfoPanel {
border: 3px solid ${EB.borderOuter};
background: ${EB.borderGap};
z-index: 1000;
max-height: calc(100vh - 32px);
display: flex;
flex-direction: column;
transform: translateX(410px);
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow:
@@ -78,6 +82,9 @@ export class NpcInfoPanel {
background: ${EB.bgPanel};
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
`;
// Subtle diagonal shine overlay
@@ -106,6 +113,7 @@ export class NpcInfoPanel {
display: flex;
justify-content: center;
position: relative;
flex-shrink: 0;
`;
// Inner portrait border (another doubled frame, just for the portrait)
@@ -148,6 +156,7 @@ export class NpcInfoPanel {
display: flex;
border-bottom: 2px solid ${EB.borderInner};
font-family: 'Press Start 2P', monospace;
flex-shrink: 0;
`;
this.statusTab = document.createElement('div');
@@ -181,8 +190,15 @@ export class NpcInfoPanel {
tabBar.appendChild(this.relationshipsTab);
this.container.appendChild(tabBar);
// Content wrapper
// Content wrapper (scrollable area below portrait + tabs)
const contentWrapper = document.createElement('div');
contentWrapper.style.cssText = `
overflow-y: auto;
min-height: 0;
flex: 1;
scrollbar-width: thin;
scrollbar-color: ${EB.borderInner} ${EB.bgDeep};
`;
// Status content (existing info section)
this.statusContent = document.createElement('div');
@@ -234,6 +250,20 @@ export class NpcInfoPanel {
`;
this.statusContent.appendChild(this.backstoryEl);
// Inner thought (italic first-person, shown above recent events)
this.thoughtEl = document.createElement('div');
this.thoughtEl.style.cssText = `
display: none;
font-family: 'Press Start 2P', monospace;
font-size: 10px;
color: ${EB.textSecondary};
text-align: center;
padding: 4px 8px;
font-style: italic;
line-height: 1.6;
`;
this.statusContent.appendChild(this.thoughtEl);
// Recent events section (hidden by default)
this.recentEventsEl = document.createElement('div');
this.recentEventsEl.style.cssText = `
@@ -294,8 +324,6 @@ export class NpcInfoPanel {
display: none;
padding: 8px 12px;
font-family: 'Press Start 2P', monospace;
max-height: 300px;
overflow-y: auto;
`;
contentWrapper.appendChild(this.relationshipsContent);
@@ -394,6 +422,16 @@ export class NpcInfoPanel {
}
}
updateThought(text: string | null): void {
if (!text) {
this.thoughtEl.style.display = 'none';
return;
}
this.thoughtEl.style.display = '';
this.thoughtEl.textContent = `"${text}"`;
this.thoughtEl.title = text;
}
updatePortrait(portraitDataUrl: string): void {
this.portraitImg.style.opacity = '0';
setTimeout(() => {