feat: add floating emoji overlay for NPC social interactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-07 13:07:40 +00:00
parent 8daecfaa3f
commit cabb129fdf
2 changed files with 113 additions and 0 deletions
+16
View File
@@ -3,6 +3,7 @@ import type { SocketClient } from '../network/SocketClient.js';
import { CharacterCompositor } from '../sprites/CharacterCompositor.js';
import { PortraitCompositor } from '../sprites/PortraitCompositor.js';
import { NpcInfoPanel } from '../ui/NpcInfoPanel.js';
import { InteractionEmojiManager } from '../ui/InteractionEmoji.js';
import type { WorldState, StateUpdate, EntityState, Appearance } from '@dflike/shared';
import { TILE_SIZE, SPRITE_FRAME_WIDTH, SPRITE_FRAME_HEIGHT, SPRITE_COLS, Direction } from '@dflike/shared';
@@ -25,6 +26,7 @@ export class GameScene extends Phaser.Scene {
private followThrottle = 0;
private portraitCompositor!: PortraitCompositor;
private npcInfoPanel!: NpcInfoPanel;
private emojiManager!: InteractionEmojiManager;
constructor() {
super({ key: 'GameScene' });
@@ -39,6 +41,7 @@ export class GameScene extends Phaser.Scene {
this.compositor = new CharacterCompositor(this);
this.portraitCompositor = new PortraitCompositor();
this.npcInfoPanel = new NpcInfoPanel();
this.emojiManager = new InteractionEmojiManager();
// Draw tile grid
this.drawWorld();
@@ -236,6 +239,19 @@ export class GameScene extends Phaser.Scene {
}
}
// Update interaction emojis
const entityPositions = new Map<number, { screenX: number; screenY: number }>();
const cam = this.cameras.main;
for (const entity of update.entities) {
const es = this.entitySprites.get(entity.id);
if (es) {
const screenX = (es.sprite.x - cam.scrollX) * cam.zoom;
const screenY = (es.sprite.y - cam.scrollY) * cam.zoom;
entityPositions.set(entity.id, { screenX, screenY });
}
}
this.emojiManager.update(update.entities, cam, entityPositions);
// Live-update the follow panel if visible
if (this.mode === 'follow' && this.npcInfoPanel.isVisible()) {
const npcIds = this.getNpcIds();
+97
View File
@@ -0,0 +1,97 @@
import type { EntityState } from '@dflike/shared';
const EMOJI_POSITIVE = '\u{1F60A}';
const EMOJI_NEGATIVE = '\u{1F620}';
export class InteractionEmojiManager {
private activeEmojis: Map<number, HTMLDivElement> = new Map();
constructor() {
this.ensureStylesheet();
}
update(
entities: EntityState[],
_camera: Phaser.Cameras.Scene2D.Camera,
entityPositions: Map<number, { screenX: number; screenY: number }>,
): void {
const emoting = new Set<number>();
for (const entity of entities) {
if (entity.socialState?.phase === 'emoting' && entity.socialState.outcome) {
emoting.add(entity.id);
if (!this.activeEmojis.has(entity.id)) {
this.createEmoji(entity.id, entity.socialState.outcome);
}
}
}
// Remove emojis for entities no longer emoting
for (const [entityId, el] of this.activeEmojis) {
if (!emoting.has(entityId)) {
el.remove();
this.activeEmojis.delete(entityId);
}
}
// Update positions
for (const [entityId, el] of this.activeEmojis) {
const pos = entityPositions.get(entityId);
if (pos) {
el.style.left = `${pos.screenX}px`;
el.style.top = `${pos.screenY}px`;
}
}
}
private createEmoji(entityId: number, outcome: string): void {
const div = document.createElement('div');
div.style.cssText = `
position: fixed;
font-size: 24px;
pointer-events: none;
z-index: 999;
transform: translate(-50%, -100%);
`;
div.style.animation = 'emoji-float 2s ease-out forwards';
div.textContent = outcome === 'positive' ? EMOJI_POSITIVE : EMOJI_NEGATIVE;
document.body.appendChild(div);
this.activeEmojis.set(entityId, div);
}
private ensureStylesheet(): void {
if (document.getElementById('emoji-float-style')) return;
const style = document.createElement('style');
style.id = 'emoji-float-style';
style.textContent = `
@keyframes emoji-float {
0% {
transform: translate(-50%, -100%) translateY(0);
opacity: 1;
}
75% {
opacity: 1;
}
100% {
transform: translate(-50%, -100%) translateY(-20px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
destroy(): void {
for (const [, el] of this.activeEmojis) {
el.remove();
}
this.activeEmojis.clear();
const style = document.getElementById('emoji-float-style');
if (style) {
style.remove();
}
}
}