Files
BreakEscape/js/systems/npc-talk-icons.js

213 lines
6.4 KiB
JavaScript

/**
* NPC Talk Icon System
*
* Displays a "talk" icon above NPC heads when the player is within interaction range.
* Manages icon creation, positioning, and visibility based on player proximity.
*
* @module npc-talk-icons
*/
export class NPCTalkIconSystem {
constructor(scene) {
this.scene = scene;
this.npcIcons = new Map(); // { npcId: { npc, icon, sprite } }
// Offset from NPC position - use whole pixels to avoid sub-pixel rendering
this.ICON_OFFSET = { x: 0, y: -33 };
this.INTERACTION_RANGE = 64; // Pixels
this.UPDATE_INTERVAL = 200; // ms between updates
this.lastUpdate = 0;
this.ICON_WIDTH = 21; // Talk icon width in pixels
}
/**
* Initialize talk icons for all NPCs in the current room
* @param {Array} npcs - Array of NPC objects
* @param {Array} sprites - Array of NPC sprite objects
*/
init(npcs, sprites) {
this.npcs = npcs || [];
this.sprites = sprites || [];
// Create icons for each NPC sprite
if (this.sprites && Array.isArray(this.sprites)) {
this.sprites.forEach(spriteObj => {
this.createIconForNPC(spriteObj);
});
}
console.log(`💬 Initialized ${this.npcIcons.size} talk icons`);
}
/**
* Create a talk icon for an NPC sprite
* @param {Object} spriteObj - NPC sprite object
*/
createIconForNPC(spriteObj) {
if (!spriteObj || !spriteObj.npcId) return;
// Don't create duplicate icons
if (this.npcIcons.has(spriteObj.npcId)) return;
try {
// Calculate the offset to align icon's right edge with sprite's right edge
// Get sprite's actual width in pixels (accounting for scale)
const spriteWidth = spriteObj.width;
// Offset from sprite center to align right edges
const offsetX = 0; //(spriteWidth / 2) - (scaledIconWidth / 2);
// Calculate pixel-perfect position (round to whole pixels)
const iconX = Math.round(spriteObj.x + offsetX);
const iconY = Math.round(spriteObj.y + this.ICON_OFFSET.y);
// Create the icon image
const icon = this.scene.add.image(iconX, iconY, 'talk');
// Hide by default
icon.setVisible(false);
icon.setDepth(spriteObj.depth + 1);
// icon.setOrigin(0.5, 0.5);
// Store reference with calculated offset for consistent positioning
this.npcIcons.set(spriteObj.npcId, {
npc: spriteObj,
icon: icon,
visible: false,
offsetX: offsetX // Store for consistent updates
});
console.log(`💬 Created talk icon for NPC: ${spriteObj.npcId} at offset x=${offsetX}`);
} catch (error) {
console.error(`❌ Error creating talk icon for ${spriteObj.npcId}:`, error);
}
}
/**
* Update icon visibility based on player proximity
* @param {Object} player - Player sprite object
*/
update(player) {
if (!player) return;
// Throttle updates
const now = Date.now();
if (now - this.lastUpdate < this.UPDATE_INTERVAL) {
return;
}
this.lastUpdate = now;
// Check distance to each NPC
this.npcIcons.forEach((iconData, npcId) => {
const distance = Phaser.Math.Distance.Between(
player.x,
player.y,
iconData.npc.x,
iconData.npc.y
);
const shouldShow = distance <= this.INTERACTION_RANGE;
// Update icon visibility and position
if (shouldShow !== iconData.visible) {
iconData.icon.setVisible(shouldShow);
iconData.visible = shouldShow;
}
// Update position to follow NPC with pixel-perfect alignment
// Use stored offset to ensure consistent positioning without recalculating bounds
const newX = Math.round(iconData.npc.x + iconData.offsetX);
const newY = Math.round(iconData.npc.y + this.ICON_OFFSET.y);
iconData.icon.setPosition(newX, newY);
// Update depth if needed
const expectedDepth = iconData.npc.depth + 1;
if (iconData.icon.depth !== expectedDepth) {
iconData.icon.setDepth(expectedDepth);
}
});
}
/**
* Show icon for a specific NPC
* @param {string} npcId - NPC ID
*/
showIcon(npcId) {
const iconData = this.npcIcons.get(npcId);
if (iconData && !iconData.visible) {
iconData.icon.setVisible(true);
iconData.visible = true;
}
}
/**
* Hide icon for a specific NPC
* @param {string} npcId - NPC ID
*/
hideIcon(npcId) {
const iconData = this.npcIcons.get(npcId);
if (iconData && iconData.visible) {
iconData.icon.setVisible(false);
iconData.visible = false;
}
}
/**
* Hide all talk icons
*/
hideAll() {
this.npcIcons.forEach(iconData => {
iconData.icon.setVisible(false);
iconData.visible = false;
});
}
/**
* Show all talk icons
*/
showAll() {
this.npcIcons.forEach(iconData => {
iconData.icon.setVisible(true);
iconData.visible = true;
});
}
/**
* Remove talk icon for a specific NPC
* @param {string} npcId - NPC ID
*/
removeIcon(npcId) {
const iconData = this.npcIcons.get(npcId);
if (iconData) {
iconData.icon.destroy();
this.npcIcons.delete(npcId);
}
}
/**
* Cleanup all icons
*/
destroy() {
this.npcIcons.forEach(iconData => {
iconData.icon.destroy();
});
this.npcIcons.clear();
}
/**
* Set interaction range for showing icons
* @param {number} range - Range in pixels
*/
setInteractionRange(range) {
this.INTERACTION_RANGE = range;
}
/**
* Set icon offset from NPC position
* @param {Object} offset - {x, y} offset
*/
setIconOffset(offset) {
this.ICON_OFFSET = offset;
}
}
export default NPCTalkIconSystem;