mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
213 lines
6.4 KiB
JavaScript
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;
|