mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
fix: enhance interaction mechanics by adding player facing direction for NPC and object interactions
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,39 +877,30 @@ 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 (isObjectInInteractionRange(npcAtPosition)) {
|
||||
// NPC is in range - face toward them then interact.
|
||||
facePlayerToward(npcAtPosition.x, npcAtPosition.y);
|
||||
if (window.tryInteractWithNPC) {
|
||||
const interactionSuccessful = window.tryInteractWithNPC(npcAtPosition);
|
||||
|
||||
if (interactionSuccessful) {
|
||||
// Interaction was successful (NPC was in range)
|
||||
return; // Exit early after handling the interaction
|
||||
window.tryInteractWithNPC(npcAtPosition);
|
||||
}
|
||||
} 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)
|
||||
// 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;
|
||||
|
||||
// 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 stopShortOffset = TILE_SIZE * 0.75;
|
||||
const normalizedDx = dx / distance;
|
||||
const normalizedDy = dy / distance;
|
||||
const targetX = npcAtPosition.x - normalizedDx * stopShortOffset;
|
||||
const targetY = npcBottomY - normalizedDy * stopShortOffset;
|
||||
movePlayerToPoint(targetX, targetY);
|
||||
movePlayerToPoint(npcAtPosition.x - normalizedDx * stopShortOffset,
|
||||
npcBottomY - normalizedDy * stopShortOffset);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for objects at the clicked position
|
||||
const objectsAtPosition = findObjectsAtPosition(worldX, worldY);
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user