mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Update interactions.js to reference new version of constants Improve NPCBarkSystem to dynamically import phone-chat minigame and handle fallback UI more gracefully Modify constants.js to conditionally export GAME_CONFIG based on Phaser availability Update implementation log for Phone Chat Minigame, detailing completed modules and next steps Create detailed implementation plan for Phone Chat Minigame, outlining structure, features, and integration points Add test HTML page for Phone Chat Minigame, including setup, chat tests, and history management functionalities
808 lines
31 KiB
JavaScript
808 lines
31 KiB
JavaScript
// Object interaction system
|
|
import { INTERACTION_RANGE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL } from '../utils/constants.js?v=8';
|
|
import { rooms } from '../core/rooms.js?v=16';
|
|
import { handleUnlock } from './unlock-system.js';
|
|
import { handleDoorInteraction } from './doors.js';
|
|
import { collectFingerprint, handleBiometricScan } from './biometrics.js';
|
|
import { addToInventory, removeFromInventory, createItemIdentifier } from './inventory.js';
|
|
|
|
let gameRef = null;
|
|
|
|
export function setGameInstance(gameInstance) {
|
|
gameRef = gameInstance;
|
|
}
|
|
|
|
// Helper function to calculate interaction distance with direction-based offset
|
|
// Extends reach from the edge of the player sprite in the direction the player is facing
|
|
function getInteractionDistance(playerSprite, targetX, targetY) {
|
|
const playerDirection = playerSprite.direction || 'down';
|
|
const SPRITE_HALF_WIDTH = 32; // 64px sprite / 2
|
|
const SPRITE_HALF_HEIGHT = 32; // 64px sprite / 2
|
|
const SPRITE_QUARTER_WIDTH = 16; // 64px sprite / 4 (for right/left)
|
|
const SPRITE_QUARTER_HEIGHT = 16; // 64px sprite / 4 (for down)
|
|
|
|
// Calculate offset point based on player direction
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
|
|
switch(playerDirection) {
|
|
case 'up':
|
|
offsetY = -SPRITE_HALF_HEIGHT;
|
|
break;
|
|
case 'down':
|
|
offsetY = SPRITE_QUARTER_HEIGHT;
|
|
break;
|
|
case 'left':
|
|
offsetX = -SPRITE_QUARTER_WIDTH;
|
|
break;
|
|
case 'right':
|
|
offsetX = SPRITE_QUARTER_WIDTH;
|
|
break;
|
|
case 'up-left':
|
|
offsetX = -SPRITE_HALF_WIDTH;
|
|
offsetY = -SPRITE_HALF_HEIGHT;
|
|
break;
|
|
case 'up-right':
|
|
offsetX = SPRITE_HALF_WIDTH;
|
|
offsetY = -SPRITE_HALF_HEIGHT;
|
|
break;
|
|
case 'down-left':
|
|
offsetX = -SPRITE_QUARTER_WIDTH;
|
|
offsetY = SPRITE_QUARTER_HEIGHT;
|
|
break;
|
|
case 'down-right':
|
|
offsetX = SPRITE_QUARTER_WIDTH;
|
|
offsetY = SPRITE_QUARTER_HEIGHT;
|
|
break;
|
|
}
|
|
|
|
// Measure from the offset point (edge of player sprite in facing direction)
|
|
const measureX = playerSprite.x + offsetX;
|
|
const measureY = playerSprite.y + offsetY;
|
|
|
|
const dx = targetX - measureX;
|
|
const dy = targetY - measureY;
|
|
return dx * dx + dy * dy; // Return squared distance for performance
|
|
}
|
|
|
|
export function checkObjectInteractions() {
|
|
// Skip if not enough time has passed since last check
|
|
const currentTime = performance.now();
|
|
if (this.lastInteractionCheck &&
|
|
currentTime - this.lastInteractionCheck < INTERACTION_CHECK_INTERVAL) {
|
|
return;
|
|
}
|
|
this.lastInteractionCheck = currentTime;
|
|
|
|
const player = window.player;
|
|
if (!player) {
|
|
return; // Player not created yet
|
|
}
|
|
|
|
// We'll measure distance from the closest edge of the player sprite
|
|
const px = player.x;
|
|
const py = player.y;
|
|
|
|
// Get viewport bounds for performance optimization
|
|
const camera = gameRef ? gameRef.cameras.main : null;
|
|
const margin = INTERACTION_RANGE * 2; // Larger margin to catch more objects
|
|
const viewBounds = camera ? {
|
|
left: camera.scrollX - margin,
|
|
right: camera.scrollX + camera.width + margin,
|
|
top: camera.scrollY - margin,
|
|
bottom: camera.scrollY + camera.height + margin
|
|
} : null;
|
|
|
|
// Check ALL objects in ALL rooms, not just current room
|
|
Object.entries(rooms).forEach(([roomId, room]) => {
|
|
if (!room.objects) return;
|
|
|
|
Object.values(room.objects).forEach(obj => {
|
|
// Skip inactive objects
|
|
if (!obj.active) {
|
|
return;
|
|
}
|
|
|
|
// Skip non-interactable objects (only highlight scenario items)
|
|
if (!obj.interactable) {
|
|
// Clear highlight if object was previously highlighted
|
|
if (obj.isHighlighted) {
|
|
obj.isHighlighted = false;
|
|
obj.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (obj.interactionIndicator) {
|
|
obj.interactionIndicator.destroy();
|
|
delete obj.interactionIndicator;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Skip highlighting for objects marked with noInteractionHighlight (like swivel chairs)
|
|
if (obj.noInteractionHighlight) {
|
|
return;
|
|
}
|
|
|
|
// Skip objects outside viewport for performance (if viewport bounds available)
|
|
if (viewBounds && (
|
|
obj.x < viewBounds.left ||
|
|
obj.x > viewBounds.right ||
|
|
obj.y < viewBounds.top ||
|
|
obj.y > viewBounds.bottom)) {
|
|
// Clear highlight if object is outside viewport
|
|
if (obj.isHighlighted) {
|
|
obj.isHighlighted = false;
|
|
obj.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (obj.interactionIndicator) {
|
|
obj.interactionIndicator.destroy();
|
|
delete obj.interactionIndicator;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Use squared distance for performance
|
|
const distanceSq = getInteractionDistance(player, obj.x, obj.y);
|
|
|
|
if (distanceSq <= INTERACTION_RANGE_SQ) {
|
|
if (!obj.isHighlighted) {
|
|
obj.isHighlighted = true;
|
|
obj.setTint(0x4da6ff); // Blue tint for interactable objects
|
|
// Add interaction indicator sprite
|
|
addInteractionIndicator(obj);
|
|
}
|
|
} else if (obj.isHighlighted) {
|
|
obj.isHighlighted = false;
|
|
obj.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (obj.interactionIndicator) {
|
|
obj.interactionIndicator.destroy();
|
|
delete obj.interactionIndicator;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Also check door sprites
|
|
if (room.doorSprites) {
|
|
Object.values(room.doorSprites).forEach(door => {
|
|
// Skip inactive or non-locked doors
|
|
if (!door.active || !door.doorProperties || !door.doorProperties.locked) {
|
|
// Clear highlight if door was previously highlighted
|
|
if (door.isHighlighted) {
|
|
door.isHighlighted = false;
|
|
door.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (door.interactionIndicator) {
|
|
door.interactionIndicator.destroy();
|
|
delete door.interactionIndicator;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Skip doors outside viewport for performance (if viewport bounds available)
|
|
if (viewBounds && (
|
|
door.x < viewBounds.left ||
|
|
door.x > viewBounds.right ||
|
|
door.y < viewBounds.top ||
|
|
door.y > viewBounds.bottom)) {
|
|
// Clear highlight if door is outside viewport
|
|
if (door.isHighlighted) {
|
|
door.isHighlighted = false;
|
|
door.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (door.interactionIndicator) {
|
|
door.interactionIndicator.destroy();
|
|
delete door.interactionIndicator;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Use squared distance for performance
|
|
const distanceSq = getInteractionDistance(player, door.x, door.y);
|
|
|
|
if (distanceSq <= INTERACTION_RANGE_SQ) {
|
|
if (!door.isHighlighted) {
|
|
door.isHighlighted = true;
|
|
door.setTint(0x4da6ff); // Blue tint for locked doors
|
|
// Add interaction indicator sprite for doors
|
|
addInteractionIndicator(door);
|
|
}
|
|
} else if (door.isHighlighted) {
|
|
door.isHighlighted = false;
|
|
door.clearTint();
|
|
// Clean up interaction sprite if exists
|
|
if (door.interactionIndicator) {
|
|
door.interactionIndicator.destroy();
|
|
delete door.interactionIndicator;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function getInteractionSpriteKey(obj) {
|
|
// Determine which sprite to show based on the object's interaction type
|
|
|
|
// Check for doors first (they may not have scenarioData)
|
|
if (obj.doorProperties) {
|
|
if (obj.doorProperties.locked) {
|
|
// Check door lock type
|
|
const lockType = obj.doorProperties.lockType;
|
|
if (lockType === 'password') return 'password';
|
|
if (lockType === 'pin') return 'pin';
|
|
return 'keyway'; // Default to keyway for key locks or unknown types
|
|
}
|
|
return null; // Unlocked doors don't need overlay
|
|
}
|
|
|
|
if (!obj || !obj.scenarioData) {
|
|
return null;
|
|
}
|
|
|
|
const data = obj.scenarioData;
|
|
|
|
// Check for locked containers and items
|
|
if (data.locked === true) {
|
|
// Check specific lock type
|
|
const lockType = data.lockType;
|
|
if (lockType === 'password') return 'password';
|
|
if (lockType === 'pin') return 'pin';
|
|
if (lockType === 'biometric') return 'fingerprint';
|
|
// Default to keyway for key locks or unknown types
|
|
return 'keyway';
|
|
}
|
|
|
|
// Unlocked containers don't need an overlay
|
|
// (they'll be opened via the container minigame when interacted with)
|
|
if (data.contents) {
|
|
return null; // No overlay for unlocked containers
|
|
}
|
|
|
|
// Check for fingerprint collection
|
|
if (data.hasFingerprint === true) {
|
|
return 'fingerprint';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function addInteractionIndicator(obj) {
|
|
// Only add indicator if we have a game instance and the object has a scene
|
|
if (!gameRef || !obj.scene || !obj.scene.add) {
|
|
return;
|
|
}
|
|
|
|
const spriteKey = getInteractionSpriteKey(obj);
|
|
if (!spriteKey) return;
|
|
|
|
// Create indicator sprite centered over the object
|
|
try {
|
|
// Get the center of the parent sprite, accounting for its origin
|
|
const center = obj.getCenter();
|
|
|
|
// Position indicator above the object (accounting for parent's display height)
|
|
const indicatorX = center.x;
|
|
const indicatorY = center.y; // Position above with 10px offset
|
|
|
|
const indicator = obj.scene.add.image(indicatorX, indicatorY, spriteKey);
|
|
indicator.setDepth(999); // High depth to appear on top
|
|
indicator.setOrigin(0.5, 0.5); // Center the sprite
|
|
// indicator.setScale(0.5); // Scale down to be less intrusive
|
|
|
|
// Add pulsing animation
|
|
obj.scene.tweens.add({
|
|
targets: indicator,
|
|
alpha: { from: 1, to: 0.5 },
|
|
duration: 800,
|
|
yoyo: true,
|
|
repeat: -1
|
|
});
|
|
|
|
// Store reference for cleanup
|
|
obj.interactionIndicator = indicator;
|
|
} catch (error) {
|
|
console.warn('Failed to add interaction indicator:', error);
|
|
}
|
|
}
|
|
|
|
export function handleObjectInteraction(sprite) {
|
|
console.log('OBJECT INTERACTION', {
|
|
name: sprite.name,
|
|
id: sprite.objectId,
|
|
scenarioData: sprite.scenarioData
|
|
});
|
|
|
|
if (!sprite) {
|
|
console.warn('Invalid sprite');
|
|
return;
|
|
}
|
|
|
|
// Handle swivel chair interaction - send it flying!
|
|
if (sprite.isSwivelChair && sprite.body) {
|
|
const player = window.player;
|
|
if (player) {
|
|
// Calculate direction from player to chair
|
|
const dx = sprite.x - player.x;
|
|
const dy = sprite.y - player.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
// Normalize the direction vector
|
|
const dirX = dx / distance;
|
|
const dirY = dy / distance;
|
|
|
|
// Apply a strong kick velocity
|
|
const kickForce = 600; // Pixels per second
|
|
sprite.body.setVelocity(dirX * kickForce, dirY * kickForce);
|
|
|
|
// Trigger spin direction calculation for visual rotation
|
|
if (window.calculateChairSpinDirection) {
|
|
window.calculateChairSpinDirection(player, sprite);
|
|
}
|
|
|
|
// Show feedback message
|
|
console.log('SWIVEL CHAIR KICKED', {
|
|
chairName: sprite.name,
|
|
velocity: { x: dirX * kickForce, y: dirY * kickForce }
|
|
});
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!sprite.scenarioData) {
|
|
console.warn('Invalid sprite or missing scenario data');
|
|
return;
|
|
}
|
|
|
|
// Handle the Crypto Workstation - pick it up if takeable, or use it if in inventory
|
|
if (sprite.scenarioData.type === "workstation") {
|
|
// If it's in inventory (marked as non-takeable), open it
|
|
if (!sprite.scenarioData.takeable) {
|
|
console.log('OPENING WORKSTATION FROM INVENTORY');
|
|
if (window.openCryptoWorkstation) {
|
|
window.openCryptoWorkstation();
|
|
} else {
|
|
window.gameAlert('Crypto workstation not available', 'error', 'Error', 3000);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Otherwise, try to pick it up and add to inventory
|
|
console.log('WORKSTATION ADDED TO INVENTORY');
|
|
addToInventory(sprite);
|
|
window.gameAlert(`${sprite.scenarioData.name} added to inventory. You can now use it for cryptographic analysis.`, 'success', 'Item Acquired', 5000);
|
|
return;
|
|
}
|
|
|
|
// Handle the Notepad - open notes minigame
|
|
if (sprite.scenarioData.type === "notepad") {
|
|
if (window.startNotesMinigame) {
|
|
// Check if notes minigame is specifically already running
|
|
if (window.MinigameFramework && window.MinigameFramework.currentMinigame &&
|
|
window.MinigameFramework.currentMinigame.navigateToNoteIndex) {
|
|
console.log('Notes minigame already running, navigating to notepad note instead');
|
|
// If notes minigame is already running, just navigate to the notepad note
|
|
if (window.MinigameFramework.currentMinigame.navigateToNoteIndex) {
|
|
window.MinigameFramework.currentMinigame.navigateToNoteIndex(0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Navigate to the notepad note (index 0) when clicking the notepad
|
|
// Create a minimal item just for navigation - no auto-add needed
|
|
const notepadItem = {
|
|
scenarioData: {
|
|
type: 'notepad',
|
|
name: 'Notepad'
|
|
}
|
|
};
|
|
window.startNotesMinigame(notepadItem, '', '', 0, false, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle the Bluetooth Scanner - only open minigame if it's already in inventory
|
|
if (sprite.scenarioData.type === "bluetooth_scanner") {
|
|
// Check if this is an inventory item (clicked from inventory)
|
|
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
|
|
|
|
if (isInventoryItem && window.startBluetoothScannerMinigame) {
|
|
console.log('Starting bluetooth scanner minigame from inventory');
|
|
window.startBluetoothScannerMinigame(sprite);
|
|
return;
|
|
}
|
|
// If it's not in inventory, let it fall through to the takeable logic below
|
|
}
|
|
|
|
// Handle the Fingerprint Kit - only open minigame if it's already in inventory
|
|
if (sprite.scenarioData.type === "fingerprint_kit") {
|
|
// Check if this is an inventory item (clicked from inventory)
|
|
const isInventoryItem = sprite.objectId && sprite.objectId.startsWith('inventory_');
|
|
|
|
if (isInventoryItem && window.startBiometricsMinigame) {
|
|
console.log('Starting biometrics minigame from inventory');
|
|
window.startBiometricsMinigame(sprite);
|
|
return;
|
|
}
|
|
// If it's not in inventory, let it fall through to the takeable logic below
|
|
}
|
|
|
|
// Handle the Lockpick Set - pick it up if takeable, or use it if in inventory
|
|
if (sprite.scenarioData.type === "lockpick" || sprite.scenarioData.type === "lockpickset") {
|
|
// If it's in inventory (marked as non-takeable), just acknowledge it
|
|
if (!sprite.scenarioData.takeable) {
|
|
console.log('LOCKPICK ALREADY IN INVENTORY');
|
|
window.gameAlert(`Used to pick pin tumbler locks.`, 'info', `${sprite.scenarioData.name}.`, 3000);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, try to pick it up and add to inventory
|
|
console.log('LOCKPICK SET ADDED TO INVENTORY');
|
|
addToInventory(sprite);
|
|
window.gameAlert(`${sprite.scenarioData.name} added to inventory. You can now use it to pick locks.`, 'success', 'Item Acquired', 5000);
|
|
return;
|
|
}
|
|
|
|
// Handle biometric scanner interaction
|
|
if (sprite.scenarioData.biometricType === 'fingerprint') {
|
|
handleBiometricScan(sprite);
|
|
return;
|
|
}
|
|
|
|
// Check for fingerprint collection possibility
|
|
if (sprite.scenarioData.hasFingerprint) {
|
|
// Check if player has fingerprint kit
|
|
const hasKit = window.inventory.items.some(item =>
|
|
item && item.scenarioData &&
|
|
item.scenarioData.type === 'fingerprint_kit'
|
|
);
|
|
|
|
if (hasKit) {
|
|
const sample = collectFingerprint(sprite);
|
|
if (sample) {
|
|
return; // Exit after collecting fingerprint
|
|
}
|
|
} else {
|
|
window.gameAlert("You need a fingerprint kit to collect samples from this surface!", 'warning', 'Missing Equipment', 4000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Skip range check for inventory items
|
|
const isInventoryItem = window.inventory && window.inventory.items.includes(sprite);
|
|
if (!isInventoryItem) {
|
|
// Check if player is in range
|
|
const player = window.player;
|
|
if (!player) return;
|
|
|
|
// Measure distance with direction-based offset
|
|
const distanceSq = getInteractionDistance(player, sprite.x, sprite.y);
|
|
|
|
if (distanceSq > INTERACTION_RANGE_SQ) {
|
|
console.log('INTERACTION_OUT_OF_RANGE', {
|
|
objectName: sprite.name,
|
|
objectId: sprite.objectId,
|
|
distance: Math.sqrt(distanceSq),
|
|
maxRange: Math.sqrt(INTERACTION_RANGE_SQ)
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
const data = sprite.scenarioData;
|
|
|
|
// Handle container items (suitcase, briefcase, bags, bins, etc.) - check BEFORE lock check
|
|
if (data.type === 'suitcase' || data.type === 'briefcase' || data.type === 'bag1' || data.type === 'bin1' || data.contents) {
|
|
console.log('CONTAINER ITEM INTERACTION', data);
|
|
|
|
// Check if container is locked
|
|
if (data.locked === true) {
|
|
console.log('CONTAINER LOCKED - UNLOCK SYSTEM WILL HANDLE', data);
|
|
handleUnlock(sprite, 'item');
|
|
return;
|
|
}
|
|
|
|
// Container is unlocked (or has no lock) - launch the container minigame
|
|
console.log('CONTAINER UNLOCKED/OPEN - LAUNCHING MINIGAME', data);
|
|
handleContainerInteraction(sprite);
|
|
return;
|
|
}
|
|
|
|
// Check if item is locked (non-container items)
|
|
if (data.locked === true) {
|
|
console.log('ITEM LOCKED', data);
|
|
handleUnlock(sprite, 'item');
|
|
return;
|
|
}
|
|
|
|
let message = `${data.name} `;
|
|
if (data.observations) {
|
|
message += `Observations: ${data.observations}\n`;
|
|
}
|
|
|
|
// For phone type objects, use the phone messages minigame
|
|
if (data.type === 'phone' && (data.text || data.voice)) {
|
|
console.log('Phone object detected:', { type: data.type, text: data.text, voice: data.voice });
|
|
// Start the phone messages minigame
|
|
if (window.MinigameFramework) {
|
|
// Initialize the framework if not already done
|
|
if (!window.MinigameFramework.mainGameScene && window.game) {
|
|
window.MinigameFramework.init(window.game);
|
|
}
|
|
|
|
const messages = [];
|
|
|
|
// Add text message if available
|
|
if (data.text) {
|
|
messages.push({
|
|
type: 'text',
|
|
sender: data.sender || 'Unknown',
|
|
text: data.text,
|
|
timestamp: data.timestamp || 'Unknown time',
|
|
read: false
|
|
});
|
|
}
|
|
|
|
// Add voice message if available
|
|
if (data.voice) {
|
|
messages.push({
|
|
type: 'voice',
|
|
sender: data.sender || 'Unknown',
|
|
text: data.text || null, // text is optional for voice messages
|
|
voice: data.voice,
|
|
timestamp: data.timestamp || 'Unknown time',
|
|
read: false
|
|
});
|
|
}
|
|
|
|
const minigameParams = {
|
|
title: data.name || 'Phone Messages',
|
|
messages: messages,
|
|
observations: data.observations,
|
|
lockable: sprite,
|
|
onComplete: (success, result) => {
|
|
console.log('Phone messages minigame completed:', success, result);
|
|
}
|
|
};
|
|
|
|
window.MinigameFramework.startMinigame('phone-messages', null, minigameParams);
|
|
return; // Exit early since minigame handles the interaction
|
|
}
|
|
}
|
|
|
|
// For text_file type objects, use the text file minigame
|
|
if (data.type === 'text_file' && data.text) {
|
|
console.log('Text file object detected:', { type: data.type, name: data.name, text: data.text });
|
|
// Start the text file minigame
|
|
if (window.MinigameFramework) {
|
|
// Initialize the framework if not already done
|
|
if (!window.MinigameFramework.mainGameScene && window.game) {
|
|
window.MinigameFramework.init(window.game);
|
|
}
|
|
|
|
const minigameParams = {
|
|
title: `Text File - ${data.name || 'Unknown File'}`,
|
|
fileName: data.name || 'Unknown File',
|
|
fileContent: data.text,
|
|
fileType: data.fileType || 'text',
|
|
observations: data.observations,
|
|
lockable: sprite,
|
|
source: data.source || 'Unknown Source',
|
|
onComplete: (success, result) => {
|
|
console.log('Text file minigame completed:', success, result);
|
|
}
|
|
};
|
|
|
|
window.MinigameFramework.startMinigame('text-file', null, minigameParams);
|
|
return; // Exit early since minigame handles the interaction
|
|
}
|
|
}
|
|
|
|
if (data.readable && data.text) {
|
|
message += `Text: ${data.text}\n`;
|
|
|
|
// For notes type objects, use the notes minigame
|
|
if (data.type === 'notes' && data.text) {
|
|
// Start the notes minigame
|
|
if (window.startNotesMinigame) {
|
|
window.startNotesMinigame(sprite, data.text, data.observations);
|
|
return; // Exit early since minigame handles the interaction
|
|
}
|
|
}
|
|
|
|
// Add readable text as a note (fallback for other readable objects)
|
|
// Skip notepad items since they're handled specially
|
|
if (data.text.trim().length > 0 && data.type !== 'notepad') {
|
|
const addedNote = window.addNote(data.name, data.text, data.important || false);
|
|
|
|
if (addedNote) {
|
|
window.gameAlert(`Added "${data.name}" to your notes.`, 'info', 'Note Added', 3000);
|
|
|
|
// If this is a note in the inventory, remove it after adding to notes list
|
|
if (isInventoryItem && data.type === 'notes') {
|
|
setTimeout(() => {
|
|
if (removeFromInventory(sprite)) {
|
|
window.gameAlert(`Removed "${data.name}" from inventory after recording in notes.`, 'success', 'Inventory Updated', 3000);
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.takeable) {
|
|
// Check if it's already in inventory
|
|
const itemIdentifier = createItemIdentifier(sprite.scenarioData);
|
|
const isInInventory = window.inventory.items.some(item =>
|
|
item && createItemIdentifier(item.scenarioData) === itemIdentifier
|
|
);
|
|
|
|
if (!isInInventory) {
|
|
console.log('INVENTORY ITEM ADDED', { item: itemIdentifier });
|
|
addToInventory(sprite);
|
|
}
|
|
}
|
|
|
|
// Show notification
|
|
window.gameAlert(message, 'info', data.name, 5000);
|
|
}
|
|
|
|
// Handle container item interactions
|
|
function handleContainerInteraction(sprite) {
|
|
const data = sprite.scenarioData;
|
|
console.log('Handling container interaction:', data);
|
|
|
|
// Check if container has contents
|
|
if (!data.contents || data.contents.length === 0) {
|
|
window.gameAlert(`${data.name} is empty.`, 'info', 'Empty Container', 3000);
|
|
return;
|
|
}
|
|
|
|
// Start the container minigame
|
|
if (window.startContainerMinigame) {
|
|
window.startContainerMinigame(sprite, data.contents, data.takeable);
|
|
} else {
|
|
console.error('Container minigame not available');
|
|
window.gameAlert('Container minigame not available', 'error', 'Error', 3000);
|
|
}
|
|
}
|
|
|
|
// Try to interact with the nearest interactable object within range
|
|
export function tryInteractWithNearest() {
|
|
const player = window.player;
|
|
if (!player) {
|
|
return;
|
|
}
|
|
|
|
const px = player.x;
|
|
const py = player.y;
|
|
|
|
let nearestObject = null;
|
|
let nearestDistance = INTERACTION_RANGE; // Only consider objects within interaction range
|
|
|
|
// Get player's facing direction and convert to angle for direction filtering
|
|
const playerDirection = player.direction || 'down';
|
|
let facingAngle = 0;
|
|
let angleTolerance = 70; // degrees - how wide the cone in front of player is
|
|
|
|
// Determine facing angle based on direction
|
|
// In canvas/Phaser: right=0°, down=90°, left=180°, up=270°
|
|
switch(playerDirection) {
|
|
case 'right':
|
|
facingAngle = 0;
|
|
break;
|
|
case 'down-right':
|
|
facingAngle = 45;
|
|
break;
|
|
case 'down':
|
|
facingAngle = 90;
|
|
break;
|
|
case 'down-left':
|
|
facingAngle = 135;
|
|
break;
|
|
case 'left':
|
|
facingAngle = 180;
|
|
break;
|
|
case 'up-left':
|
|
facingAngle = 225;
|
|
break;
|
|
case 'up':
|
|
facingAngle = 270; // Use 270 instead of -90
|
|
break;
|
|
case 'up-right':
|
|
facingAngle = 315; // Use 315 instead of -45
|
|
break;
|
|
default: // Fallback for any unknown directions
|
|
facingAngle = 90; // Default to down
|
|
}
|
|
|
|
// Helper function to check if an object is in front of the player
|
|
function isInFrontOfPlayer(objX, objY) {
|
|
const dx = objX - px;
|
|
const dy = objY - py;
|
|
|
|
// Calculate angle to object (in canvas coordinates where Y increases downward)
|
|
let angleToObject = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
|
|
// Normalize to 0-360
|
|
angleToObject = (angleToObject + 360) % 360;
|
|
|
|
// Calculate angular difference
|
|
let angleDiff = Math.abs(facingAngle - angleToObject);
|
|
if (angleDiff > 180) {
|
|
angleDiff = 360 - angleDiff;
|
|
}
|
|
|
|
return angleDiff <= angleTolerance;
|
|
}
|
|
|
|
// Check all objects in all rooms
|
|
Object.entries(rooms).forEach(([roomId, room]) => {
|
|
if (!room.objects) return;
|
|
|
|
Object.values(room.objects).forEach(obj => {
|
|
// Only consider interactable, active, and visible objects
|
|
if (!obj.active || !obj.interactable || !obj.visible) {
|
|
return;
|
|
}
|
|
|
|
// Calculate distance with direction-based offset
|
|
const distanceSq = getInteractionDistance(player, obj.x, obj.y);
|
|
const distance = Math.sqrt(distanceSq);
|
|
|
|
// Check if within range and in front of player
|
|
if (distance <= INTERACTION_RANGE && isInFrontOfPlayer(obj.x, obj.y)) {
|
|
if (distance < nearestDistance) {
|
|
nearestDistance = distance;
|
|
nearestObject = obj;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Also check door sprites (including all doors, not just locked ones)
|
|
if (room.doorSprites) {
|
|
Object.values(room.doorSprites).forEach(door => {
|
|
// Only consider active doors (check all doors, not just locked)
|
|
if (!door.active || !door.doorProperties) {
|
|
return;
|
|
}
|
|
|
|
// Calculate distance with direction-based offset
|
|
const distanceSq = getInteractionDistance(player, door.x, door.y);
|
|
const distance = Math.sqrt(distanceSq);
|
|
|
|
// Check if within range and in front of player
|
|
if (distance <= INTERACTION_RANGE && isInFrontOfPlayer(door.x, door.y)) {
|
|
if (distance < nearestDistance) {
|
|
nearestDistance = distance;
|
|
nearestObject = door;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Interact with the nearest object if one was found
|
|
if (nearestObject) {
|
|
// Check if this is a door (doors have doorProperties instead of scenarioData)
|
|
if (nearestObject.doorProperties) {
|
|
// Handle door interaction - triggers unlock/open sequence based on lock state
|
|
handleDoorInteraction(nearestObject);
|
|
} else {
|
|
// Handle regular object interaction
|
|
handleObjectInteraction(nearestObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export for global access
|
|
window.checkObjectInteractions = checkObjectInteractions;
|
|
window.handleObjectInteraction = handleObjectInteraction;
|
|
window.handleContainerInteraction = handleContainerInteraction;
|
|
window.tryInteractWithNearest = tryInteractWithNearest;
|