fix: prevent async race in sprite creation and emoji lifecycle leak
Two bugs fixed: 1. NPC sprites not fully rendering: createEntitySprite is async but was called without await from handleStateUpdate. Concurrent state updates triggered multiple simultaneous creation calls for the same entity. The second call composited before textures loaded, producing empty spritesheets. Fixed with a creatingEntities guard set. 2. Interaction emojis not appearing after first use: animationend listener was attached during removal (when animation had already completed), so it never fired. Ghost entries in activeEmojis map blocked future emoji creation. Fixed by self-cleaning via animationend listener attached at creation time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ export class GameScene extends Phaser.Scene {
|
||||
private compositor!: CharacterCompositor;
|
||||
private worldState!: WorldState;
|
||||
private entitySprites: Map<number, EntitySprite> = new Map();
|
||||
private creatingEntities: Set<number> = new Set();
|
||||
private cursors!: Phaser.Types.Input.Keyboard.CursorKeys;
|
||||
private wasd!: Record<string, Phaser.Input.Keyboard.Key>;
|
||||
private mode: 'avatar' | 'camera' | 'follow' = 'camera';
|
||||
@@ -226,8 +227,11 @@ export class GameScene extends Phaser.Scene {
|
||||
const textureKey = this.compositor.getKey(entity.appearance);
|
||||
this.playAnimation(existing.sprite, textureKey, entity.movement.direction, entity.movement.state);
|
||||
existing.lastState = entity;
|
||||
} else if (entity.appearance) {
|
||||
this.createEntitySprite(entity);
|
||||
} else if (entity.appearance && !this.creatingEntities.has(entity.id)) {
|
||||
this.creatingEntities.add(entity.id);
|
||||
this.createEntitySprite(entity).finally(() => {
|
||||
this.creatingEntities.delete(entity.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,21 +26,9 @@ export class InteractionEmojiManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove emojis for entities no longer emoting
|
||||
// Let the CSS animation finish naturally before removing the element
|
||||
// Update positions for active emojis
|
||||
// (animationend self-cleanup in createEmoji handles removal)
|
||||
for (const [entityId, el] of this.activeEmojis) {
|
||||
if (!emoting.has(entityId) && !el.dataset.removing) {
|
||||
el.dataset.removing = '1';
|
||||
el.addEventListener('animationend', () => {
|
||||
el.remove();
|
||||
this.activeEmojis.delete(entityId);
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Update positions (skip emojis that are fading out)
|
||||
for (const [entityId, el] of this.activeEmojis) {
|
||||
if (el.dataset.removing) continue;
|
||||
const pos = entityPositions.get(entityId);
|
||||
if (pos) {
|
||||
el.style.left = `${pos.screenX}px`;
|
||||
@@ -61,6 +49,12 @@ export class InteractionEmojiManager {
|
||||
div.style.animation = 'emoji-float 2s ease-out forwards';
|
||||
div.textContent = outcome === 'positive' ? EMOJI_POSITIVE : EMOJI_NEGATIVE;
|
||||
|
||||
// Self-cleanup when animation completes
|
||||
div.addEventListener('animationend', () => {
|
||||
div.remove();
|
||||
this.activeEmojis.delete(entityId);
|
||||
}, { once: true });
|
||||
|
||||
document.body.appendChild(div);
|
||||
this.activeEmojis.set(entityId, div);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user