mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat(npc): Add talk icon system and update NPC sprite configurations
This commit is contained in:
BIN
assets/characters/hacker-red-talk.png
Normal file
BIN
assets/characters/hacker-red-talk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/characters/hacker-red.png
Normal file
BIN
assets/characters/hacker-red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -67,6 +67,7 @@ export function preload() {
|
||||
this.load.image('keyway', 'assets/icons/keyway.png');
|
||||
this.load.image('password', 'assets/icons/password.png');
|
||||
this.load.image('pin', 'assets/icons/pin.png');
|
||||
this.load.image('talk', 'assets/icons/talk.png');
|
||||
|
||||
// Load new object sprites from Tiled map tileset
|
||||
// These are the key objects that appear in the new room_reception2.json
|
||||
@@ -357,41 +358,47 @@ export function preload() {
|
||||
this.load.image('torch-1', 'assets/objects/torch-1.png');
|
||||
|
||||
// Load character sprite sheet instead of single image
|
||||
this.load.spritesheet('hacker', 'assets/characters/hacker.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
|
||||
// Animated plant textures are loaded above
|
||||
|
||||
// Load swivel chair rotation images
|
||||
this.load.image('chair-exec-rotate1', 'assets/objects/chair-exec-rotate1.png');
|
||||
this.load.image('chair-exec-rotate2', 'assets/objects/chair-exec-rotate2.png');
|
||||
this.load.image('chair-exec-rotate3', 'assets/objects/chair-exec-rotate3.png');
|
||||
this.load.image('chair-exec-rotate4', 'assets/objects/chair-exec-rotate4.png');
|
||||
this.load.image('chair-exec-rotate5', 'assets/objects/chair-exec-rotate5.png');
|
||||
this.load.image('chair-exec-rotate6', 'assets/objects/chair-exec-rotate6.png');
|
||||
this.load.image('chair-exec-rotate7', 'assets/objects/chair-exec-rotate7.png');
|
||||
this.load.image('chair-exec-rotate8', 'assets/objects/chair-exec-rotate8.png');
|
||||
|
||||
// Load white chair rotation images
|
||||
this.load.image('chair-white-1-rotate1', 'assets/objects/chair-white-1-rotate1.png');
|
||||
this.load.image('chair-white-1-rotate2', 'assets/objects/chair-white-1-rotate2.png');
|
||||
this.load.image('chair-white-1-rotate3', 'assets/objects/chair-white-1-rotate3.png');
|
||||
this.load.image('chair-white-1-rotate4', 'assets/objects/chair-white-1-rotate4.png');
|
||||
this.load.image('chair-white-1-rotate5', 'assets/objects/chair-white-1-rotate5.png');
|
||||
this.load.image('chair-white-1-rotate6', 'assets/objects/chair-white-1-rotate6.png');
|
||||
this.load.image('chair-white-1-rotate7', 'assets/objects/chair-white-1-rotate7.png');
|
||||
this.load.image('chair-white-1-rotate8', 'assets/objects/chair-white-1-rotate8.png');
|
||||
|
||||
this.load.image('chair-white-2-rotate1', 'assets/objects/chair-white-2-rotate1.png');
|
||||
this.load.image('chair-white-2-rotate2', 'assets/objects/chair-white-2-rotate2.png');
|
||||
this.load.image('chair-white-2-rotate3', 'assets/objects/chair-white-2-rotate3.png');
|
||||
this.load.image('chair-white-2-rotate4', 'assets/objects/chair-white-2-rotate4.png');
|
||||
this.load.image('chair-white-2-rotate5', 'assets/objects/chair-white-2-rotate5.png');
|
||||
this.load.image('chair-white-2-rotate6', 'assets/objects/chair-white-2-rotate6.png');
|
||||
this.load.image('chair-white-2-rotate7', 'assets/objects/chair-white-2-rotate7.png');
|
||||
this.load.image('chair-white-2-rotate8', 'assets/objects/chair-white-2-rotate8.png');
|
||||
this.load.spritesheet('hacker', 'assets/characters/hacker.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
|
||||
// Load character sprite sheet instead of single image
|
||||
this.load.spritesheet('hacker-red', 'assets/characters/hacker-red.png', {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64
|
||||
});
|
||||
|
||||
// Animated plant textures are loaded above
|
||||
|
||||
// Load swivel chair rotation images
|
||||
this.load.image('chair-exec-rotate1', 'assets/objects/chair-exec-rotate1.png');
|
||||
this.load.image('chair-exec-rotate2', 'assets/objects/chair-exec-rotate2.png');
|
||||
this.load.image('chair-exec-rotate3', 'assets/objects/chair-exec-rotate3.png');
|
||||
this.load.image('chair-exec-rotate4', 'assets/objects/chair-exec-rotate4.png');
|
||||
this.load.image('chair-exec-rotate5', 'assets/objects/chair-exec-rotate5.png');
|
||||
this.load.image('chair-exec-rotate6', 'assets/objects/chair-exec-rotate6.png');
|
||||
this.load.image('chair-exec-rotate7', 'assets/objects/chair-exec-rotate7.png');
|
||||
this.load.image('chair-exec-rotate8', 'assets/objects/chair-exec-rotate8.png');
|
||||
|
||||
// Load white chair rotation images
|
||||
this.load.image('chair-white-1-rotate1', 'assets/objects/chair-white-1-rotate1.png');
|
||||
this.load.image('chair-white-1-rotate2', 'assets/objects/chair-white-1-rotate2.png');
|
||||
this.load.image('chair-white-1-rotate3', 'assets/objects/chair-white-1-rotate3.png');
|
||||
this.load.image('chair-white-1-rotate4', 'assets/objects/chair-white-1-rotate4.png');
|
||||
this.load.image('chair-white-1-rotate5', 'assets/objects/chair-white-1-rotate5.png');
|
||||
this.load.image('chair-white-1-rotate6', 'assets/objects/chair-white-1-rotate6.png');
|
||||
this.load.image('chair-white-1-rotate7', 'assets/objects/chair-white-1-rotate7.png');
|
||||
this.load.image('chair-white-1-rotate8', 'assets/objects/chair-white-1-rotate8.png');
|
||||
|
||||
this.load.image('chair-white-2-rotate1', 'assets/objects/chair-white-2-rotate1.png');
|
||||
this.load.image('chair-white-2-rotate2', 'assets/objects/chair-white-2-rotate2.png');
|
||||
this.load.image('chair-white-2-rotate3', 'assets/objects/chair-white-2-rotate3.png');
|
||||
this.load.image('chair-white-2-rotate4', 'assets/objects/chair-white-2-rotate4.png');
|
||||
this.load.image('chair-white-2-rotate5', 'assets/objects/chair-white-2-rotate5.png');
|
||||
this.load.image('chair-white-2-rotate6', 'assets/objects/chair-white-2-rotate6.png');
|
||||
this.load.image('chair-white-2-rotate7', 'assets/objects/chair-white-2-rotate7.png');
|
||||
this.load.image('chair-white-2-rotate8', 'assets/objects/chair-white-2-rotate8.png');
|
||||
|
||||
// Load audio files
|
||||
// NPC system sounds
|
||||
|
||||
@@ -1742,6 +1742,11 @@ export function updatePlayerRoom() {
|
||||
discoveredRooms.add(doorTransitionRoom);
|
||||
window.discoveredRooms = discoveredRooms;
|
||||
console.log(`✅ Marked room ${doorTransitionRoom} as discovered`);
|
||||
|
||||
// Update NPC talk icons for the new room
|
||||
if (window.npcTalkIcons && rooms[doorTransitionRoom].npcSprites) {
|
||||
window.npcTalkIcons.init([], rooms[doorTransitionRoom].npcSprites);
|
||||
}
|
||||
}
|
||||
|
||||
if (previousRoom) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
/**
|
||||
* Process game action tags from Ink story
|
||||
* Tags format: # unlock_door:ceo, # give_item:keycard|CEO Keycard, etc.
|
||||
* Filters out speaker tags (player, npc, speaker:player, speaker:npc)
|
||||
*
|
||||
* @param {Array<string>} tags - Array of tag strings from Ink story
|
||||
* @param {Object} ui - UI controller with showNotification method
|
||||
@@ -26,11 +27,25 @@ export function processGameActionTags(tags, ui) {
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('🏷️ Processing game action tags:', tags);
|
||||
// Filter out speaker tags - only process action tags
|
||||
const actionTags = tags.filter(tag => {
|
||||
const action = tag.split(':')[0].trim().toLowerCase();
|
||||
return action !== 'player' &&
|
||||
action !== 'npc' &&
|
||||
action !== 'speaker' &&
|
||||
!tag.includes('speaker:');
|
||||
});
|
||||
|
||||
if (actionTags.length === 0) {
|
||||
// No action tags to process (all were speaker tags)
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('🏷️ Processing game action tags:', actionTags);
|
||||
|
||||
const results = [];
|
||||
|
||||
tags.forEach(tag => {
|
||||
actionTags.forEach(tag => {
|
||||
const trimmedTag = tag.trim();
|
||||
|
||||
// Skip empty tags
|
||||
@@ -177,15 +192,18 @@ export function getActionTags(tags) {
|
||||
|
||||
/**
|
||||
* Determine speaker from tags
|
||||
* Finds the LAST speaker tag (most recent/current speaker)
|
||||
*
|
||||
* @param {Array<string>} tags - Tags from story
|
||||
* @param {string} defaultSpeaker - Default speaker if not found in tags
|
||||
* @returns {string} Speaker ('npc' or 'player')
|
||||
*/
|
||||
export function determineSpeaker(tags, defaultSpeaker = 'npc') {
|
||||
if (!tags) return defaultSpeaker;
|
||||
if (!tags || tags.length === 0) return defaultSpeaker;
|
||||
|
||||
for (const tag of tags) {
|
||||
const trimmed = tag.trim().toLowerCase();
|
||||
// Check tags in REVERSE order to find the last speaker tag (current speaker)
|
||||
for (let i = tags.length - 1; i >= 0; i--) {
|
||||
const trimmed = tags[i].trim().toLowerCase();
|
||||
if (trimmed === 'player' || trimmed === 'speaker:player') {
|
||||
return 'player';
|
||||
}
|
||||
|
||||
@@ -229,20 +229,8 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
* @returns {string} Speaker ('npc' or 'player')
|
||||
*/
|
||||
determineSpeaker(result) {
|
||||
// Check for speaker tag in result
|
||||
if (result.tags) {
|
||||
for (const tag of result.tags) {
|
||||
if (tag === 'player' || tag === 'speaker:player') {
|
||||
return 'player';
|
||||
}
|
||||
if (tag === 'npc' || tag === 'speaker:npc') {
|
||||
return 'npc';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: alternate speakers, or start with NPC
|
||||
return this.currentSpeaker === 'player' ? 'npc' : 'npc';
|
||||
// Use the shared helper function from chat-helpers
|
||||
return determineSpeakerFromTags(result.tags, 'npc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +256,40 @@ export class PersonChatMinigame extends MinigameScene {
|
||||
|
||||
// Then display the result (NPC response) after a small delay
|
||||
setTimeout(() => {
|
||||
this.displayDialogueResult(result);
|
||||
// Extract NPC-only content from the accumulated result
|
||||
// Split by speaker tag to separate player and NPC dialogue
|
||||
let npcText = '';
|
||||
let npcTags = [];
|
||||
|
||||
// Find the section with speaker:npc tag
|
||||
if (result.tags && result.tags.includes('speaker:npc')) {
|
||||
// Split the text by lines and reconstruct based on tags
|
||||
const lines = result.text.split('\n').filter(line => line.trim());
|
||||
const tagIndex = result.tags.indexOf('speaker:npc');
|
||||
|
||||
// Get number of player lines before NPC (based on speaker:player tag position)
|
||||
const playerTagIndex = result.tags.indexOf('speaker:player');
|
||||
let playerLineCount = playerTagIndex >= 0 ? 1 : 0;
|
||||
|
||||
// Skip player lines and collect NPC lines
|
||||
npcText = lines.slice(playerLineCount).join('\n').trim();
|
||||
npcTags = result.tags.filter((tag, idx) => idx >= tagIndex);
|
||||
|
||||
console.log(`📄 Extracted NPC text: "${npcText.substring(0, 50)}..."`);
|
||||
} else {
|
||||
// Fallback: use full result if no speaker tag
|
||||
npcText = result.text;
|
||||
npcTags = result.tags;
|
||||
}
|
||||
|
||||
// Create a new result with only the NPC's dialogue
|
||||
const npcOnlyResult = {
|
||||
...result,
|
||||
text: npcText,
|
||||
tags: npcTags
|
||||
};
|
||||
|
||||
this.displayDialogueResult(npcOnlyResult);
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('❌ Error handling choice:', error);
|
||||
|
||||
@@ -266,18 +266,36 @@ export function checkObjectInteractions() {
|
||||
if (distanceSq <= INTERACTION_RANGE_SQ) {
|
||||
if (!sprite.isHighlighted) {
|
||||
sprite.isHighlighted = true;
|
||||
sprite.setTint(0x4da6ff); // Blue tint for interactable NPCs
|
||||
// Add interaction indicator sprite
|
||||
addInteractionIndicator(sprite);
|
||||
// Add talk icon indicator for NPC (created on first highlight)
|
||||
if (!sprite.interactionIndicator) {
|
||||
addInteractionIndicator(sprite);
|
||||
}
|
||||
// Show talk icon and don't apply tint - icon provides visual feedback
|
||||
if (sprite.interactionIndicator) {
|
||||
sprite.interactionIndicator.setVisible(true);
|
||||
sprite.talkIconVisible = true;
|
||||
}
|
||||
} else if (sprite.interactionIndicator && !sprite.talkIconVisible) {
|
||||
// Update position of talk icon to stay pixel-perfect on NPC
|
||||
const iconX = Math.round(sprite.x + 0);
|
||||
const iconY = Math.round(sprite.y - 48);
|
||||
sprite.interactionIndicator.setPosition(iconX, iconY);
|
||||
sprite.interactionIndicator.setVisible(true);
|
||||
sprite.talkIconVisible = true;
|
||||
}
|
||||
} else if (sprite.isHighlighted) {
|
||||
sprite.isHighlighted = false;
|
||||
sprite.clearTint();
|
||||
// Clean up interaction sprite if exists
|
||||
// Hide talk icon when out of range
|
||||
if (sprite.interactionIndicator) {
|
||||
sprite.interactionIndicator.destroy();
|
||||
delete sprite.interactionIndicator;
|
||||
sprite.interactionIndicator.setVisible(false);
|
||||
sprite.talkIconVisible = false;
|
||||
}
|
||||
} else if (sprite.interactionIndicator && sprite.talkIconVisible) {
|
||||
// Update position even when not highlighted (for smooth following)
|
||||
const iconX = Math.round(sprite.x + 0);
|
||||
const iconY = Math.round(sprite.y - 48);
|
||||
sprite.interactionIndicator.setPosition(iconX, iconY);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -341,6 +359,29 @@ function addInteractionIndicator(obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
// NPCs get the talk icon above their heads with pixel-perfect positioning
|
||||
if (obj._isNPC) {
|
||||
try {
|
||||
// Talk icon positioned above NPC with pixel-perfect coordinates
|
||||
const talkIconX = Math.round(obj.x + 0); // Centered above
|
||||
const talkIconY = Math.round(obj.y - 48); // 48 pixels above
|
||||
|
||||
const indicator = obj.scene.add.image(talkIconX, talkIconY, 'talk');
|
||||
indicator.setDepth(obj.depth + 1);
|
||||
indicator.setOrigin(0.5, 0.5);
|
||||
indicator.setScale(0.75); // Slightly smaller than full size
|
||||
indicator.setVisible(false); // Hidden until player is in range
|
||||
|
||||
// Store reference for cleanup and visibility management
|
||||
obj.interactionIndicator = indicator;
|
||||
obj.talkIconVisible = false;
|
||||
} catch (error) {
|
||||
console.warn('Failed to add talk icon for NPC:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-NPC objects use the standard interaction indicator sprite
|
||||
const spriteKey = getInteractionSpriteKey(obj);
|
||||
if (!spriteKey) return;
|
||||
|
||||
|
||||
206
js/systems/npc-talk-icons.js
Normal file
206
js/systems/npc-talk-icons.js
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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: -48 };
|
||||
this.INTERACTION_RANGE = 64; // Pixels
|
||||
this.UPDATE_INTERVAL = 200; // ms between updates
|
||||
this.lastUpdate = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 pixel-perfect position (round to avoid sub-pixel rendering)
|
||||
const iconX = Math.round(spriteObj.x + this.ICON_OFFSET.x);
|
||||
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.setScale(0.75); // Slightly smaller than full size
|
||||
// Disable antialiasing to keep pixels sharp
|
||||
icon.setOrigin(0.5, 0.5);
|
||||
|
||||
// Store reference
|
||||
this.npcIcons.set(spriteObj.npcId, {
|
||||
npc: spriteObj,
|
||||
icon: icon,
|
||||
visible: false
|
||||
});
|
||||
|
||||
console.log(`💬 Created talk icon for NPC: ${spriteObj.npcId}`);
|
||||
} 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
|
||||
// Round to whole pixels to avoid sub-pixel rendering
|
||||
const newX = Math.round(iconData.npc.x + this.ICON_OFFSET.x);
|
||||
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;
|
||||
@@ -22,8 +22,8 @@
|
||||
"npcType": "person",
|
||||
"roomId": "test_room",
|
||||
"position": { "x": 5, "y": 3 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteTalk": "assets/characters/hacker-talk.png",
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteTalk": "assets/characters/hacker-red-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
|
||||
Reference in New Issue
Block a user