fix: enhance interaction mechanics by adding player facing direction for NPC and object interactions

This commit is contained in:
Z. Cliffe Schreuders
2026-02-19 14:46:55 +00:00
parent f50fa24018
commit 8fa5b15412
5 changed files with 79 additions and 35 deletions

View File

@@ -940,6 +940,18 @@ module BreakEscape
end
def find_accessible_item(item_type, item_id, item_name)
# Priority 0: Dynamically-added items (e.g., dropped by defeated NPCs).
# These were already security-validated by add_item_to_room! so they are always allowed.
@game.player_state['room_states']&.each do |room_id, room_state|
room_state['objects_added']&.each do |added_obj|
next unless added_obj['type'] == item_type
next unless added_obj['key_id'] == item_id || added_obj['id'] == item_id ||
added_obj['name'] == item_name || added_obj['name'] == item_id ||
item_id.nil?
return { item: added_obj, location: { type: 'room', room_id: room_id } }
end
end
# Priority 1: Items in unlocked rooms (most accessible)
@game.scenario_data['rooms'].each do |room_id, room_data|
if room_data['locked'] == false || @game.player_state['unlockedRooms'].include?(room_id)

View File

@@ -1,5 +1,5 @@
import { initializeRooms, calculateWorldBounds, calculateRoomPositions, createRoom, revealRoom, updatePlayerRoom, rooms } from './rooms.js?v=16';
import { createPlayer, updatePlayerMovement, movePlayerToPoint, player } from './player.js?v=7';
import { createPlayer, updatePlayerMovement, movePlayerToPoint, facePlayerToward, player } from './player.js?v=8';
import { initializePathfinder } from './pathfinding.js?v=7';
import { initializeInventory, processInitialInventoryItems } from '../systems/inventory.js?v=8';
import { checkObjectInteractions, setGameInstance, isObjectInInteractionRange } from '../systems/interactions.js?v=28';
@@ -877,38 +877,29 @@ export async function create() {
// Check for NPC sprites at the clicked position first
const npcAtPosition = findNPCAtPosition(worldX, worldY);
if (npcAtPosition) {
// Try to interact with the NPC
if (window.tryInteractWithNPC) {
const interactionSuccessful = window.tryInteractWithNPC(npcAtPosition);
if (interactionSuccessful) {
// Interaction was successful (NPC was in range)
return; // Exit early after handling the interaction
} else {
// NPC was out of range - treat click as a movement request
// Calculate floor-level destination along direct line from player to NPC
// Account for sprite padding (16px for atlas sprites)
const spriteCenterToBottom = npcAtPosition.height * (1 - (npcAtPosition.originY || 0.5));
const paddingOffset = npcAtPosition.isAtlas ? SPRITE_PADDING_BOTTOM_ATLAS : SPRITE_PADDING_BOTTOM_LEGACY;
const npcBottomY = npcAtPosition.y + spriteCenterToBottom - paddingOffset;
// Calculate direction from player to NPC
const dx = npcAtPosition.x - player.x;
const dy = npcBottomY - player.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and stop short by offset
const stopShortOffset = TILE_SIZE * 0.75; // Stop 24 pixels short (3/4 tile)
const normalizedDx = dx / distance;
const normalizedDy = dy / distance;
const targetX = npcAtPosition.x - normalizedDx * stopShortOffset;
const targetY = npcBottomY - normalizedDy * stopShortOffset;
movePlayerToPoint(targetX, targetY);
}
return;
if (isObjectInInteractionRange(npcAtPosition)) {
// NPC is in range - face toward them then interact.
facePlayerToward(npcAtPosition.x, npcAtPosition.y);
if (window.tryInteractWithNPC) {
window.tryInteractWithNPC(npcAtPosition);
}
} else {
// NPC is out of range - move toward them, stopping just short.
const spriteCenterToBottom = npcAtPosition.height * (1 - (npcAtPosition.originY || 0.5));
const paddingOffset = npcAtPosition.isAtlas ? SPRITE_PADDING_BOTTOM_ATLAS : SPRITE_PADDING_BOTTOM_LEGACY;
const npcBottomY = npcAtPosition.y + spriteCenterToBottom - paddingOffset;
const dx = npcAtPosition.x - player.x;
const dy = npcBottomY - player.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
const stopShortOffset = TILE_SIZE * 0.75;
const normalizedDx = dx / distance;
const normalizedDy = dy / distance;
movePlayerToPoint(npcAtPosition.x - normalizedDx * stopShortOffset,
npcBottomY - normalizedDy * stopShortOffset);
}
}
return;
}
// Check for objects at the clicked position
@@ -920,9 +911,13 @@ export async function create() {
for (const obj of objectsAtPosition) {
if (obj.interactable && window.handleObjectInteraction) {
if (isObjectInInteractionRange(obj)) {
// Object is in range - interact directly.
// Object is in range - face toward it then interact directly.
// Click always targets the clicked object; no direction-based selection.
facePlayerToward(obj.x, obj.y);
window.handleObjectInteraction(obj);
} else if (obj.isSwivelChair) {
// Chairs: move onto the clicked position (player sits/stands at the chair).
movePlayerToPoint(worldX, worldY);
} else {
// Object is out of range - move toward it, stopping just short.
const objBottomY = obj.y + obj.height * (1 - (obj.originY || 0));

View File

@@ -747,6 +747,41 @@ export function movePlayerToPoint(x, y) {
}
}
/**
* Turn the player to face a world position, updating direction and idle animation.
* Call this before triggering a click-based interaction so the player visually
* faces the object/NPC they are acting on.
*/
export function facePlayerToward(targetX, targetY) {
if (!player) return;
const dx = targetX - player.x;
const dy = targetY - player.y;
const absX = Math.abs(dx);
const absY = Math.abs(dy);
let direction;
if (absX > absY * 2) {
direction = dx > 0 ? 'right' : 'left';
} else if (absY > absX * 2) {
direction = dy > 0 ? 'down' : 'up';
} else {
direction = dy > 0 ? (dx > 0 ? 'down-right' : 'down-left')
: (dx > 0 ? 'up-right' : 'up-left');
}
player.direction = direction;
player.lastDirection = direction;
// Play idle animation for the new direction (handles atlas vs legacy sprite mapping)
const animDir = getAnimationKey(direction);
const currentAnim = player.anims.currentAnim?.key || '';
if (!currentAnim.includes('punch') && !currentAnim.includes('jab') &&
!currentAnim.includes('death') && !currentAnim.includes('taking-punch')) {
player.anims.play(`idle-${animDir}`, true);
}
}
function updatePlayerDepth(x, y) {
// Get the bottom of the player sprite, accounting for padding
// Atlas sprites (80x80) have 16px padding at bottom, legacy sprites (64x64) have minimal padding

View File

@@ -611,8 +611,10 @@ export function handleObjectInteraction(sprite) {
}
// Handle keycard cloning (when clicked from inventory)
if (sprite.scenarioData.type === 'keycard') {
console.log('KEYCARD INTERACTION - checking for cloner');
// Only intercept if the card is already in inventory (takeable === false).
// If takeable is true the card is still in the world and should be picked up normally.
if (sprite.scenarioData.type === 'keycard' && sprite.scenarioData.takeable === false) {
console.log('KEYCARD INTERACTION (inventory) - checking for cloner');
// Check if player has RFID cloner
const hasCloner = window.inventory.items.some(item =>

View File

@@ -877,7 +877,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A
"type": "text_file",
"id": "contingency_files",
"name": "CONTINGENCY - IT Audit Response",
"takeable": true,
"takeable": false,
"readable": true,
"text": "═══════════════════════════════════════════════════════════\n CONTINGENCY PLAN: IT AUDIT RESPONSE\n [ACTIVATE IF COMPROMISED]\n═══════════════════════════════════════════════════════════\n\nIf audit discovers anomalies, activate CONTINGENCY.\n\nIT Manager Kevin Park becomes the fall guy.\nHis access patterns can be retroactively modified.\nHis termination provides closure for the company and\nends investigation.\n\nPREPARED EVIDENCE:\n- Fake security logs showing Kevin accessing servers at odd hours\n- Forged email from Kevin to 'unknown external party'\n- Timeline framing Kevin as source of data breach\n\nKevin helped you. Gave you access. Trusted you.\nAnd Derek is going to destroy his career—maybe his life—\nto cover ENTROPY's tracks.\n\n[This file triggers a moral choice when collected]",
"observations": "Derek's plan to frame Kevin for everything"