mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
Add Room Management and Collision Systems: Introduce a new room management system with simplified depth layering for object rendering. Implement collision management and door systems for improved player interactions. Refactor existing room logic to enhance modularity and maintainability, separating concerns into dedicated modules for doors, collisions, and object physics. Update relevant files to ensure seamless integration with the game environment.
This commit is contained in:
1761
js/core/rooms.js
1761
js/core/rooms.js
File diff suppressed because it is too large
Load Diff
1519
js/core/rooms_new.js
Normal file
1519
js/core/rooms_new.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,22 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the lock's pin configuration for key generation
|
||||
getLockPinConfiguration() {
|
||||
if (!this.pins || this.pins.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
pinCount: this.pinCount,
|
||||
pinHeights: this.pins.map(pin => pin.originalHeight),
|
||||
pinLengths: this.pins.map(pin => ({
|
||||
keyPinLength: pin.keyPinLength,
|
||||
driverPinLength: pin.driverPinLength
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
loadLockConfiguration() {
|
||||
// Load lock configuration from global storage
|
||||
const config = window.lockConfigurations[this.lockId];
|
||||
@@ -1691,7 +1707,15 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
this.updateKeyPosition(0);
|
||||
// Show key selection again
|
||||
if (this.keySelectionMode) {
|
||||
this.createKeysForChallenge('correct_key');
|
||||
// For main game, go back to original key selection interface
|
||||
// For challenge mode (locksmith-forge.html), use the training interface
|
||||
if (this.params?.lockable?.id === 'progressive-challenge') {
|
||||
// This is the locksmith-forge.html challenge mode
|
||||
this.createKeysForChallenge('correct_key');
|
||||
} else {
|
||||
// This is the main game - go back to key selection
|
||||
this.startWithKeySelection();
|
||||
}
|
||||
}
|
||||
}, 2000); // Longer delay to show the red flash
|
||||
}
|
||||
@@ -1704,11 +1728,32 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
|
||||
console.log('Snapping pins to exact positions based on key cuts for shear line alignment');
|
||||
|
||||
// Ensure key data matches lock pin count
|
||||
if (keyDataToUse.cuts.length !== this.pinCount) {
|
||||
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock has ${this.pinCount} pins. Adjusting key data.`);
|
||||
// Truncate or pad cuts to match pin count
|
||||
if (keyDataToUse.cuts.length > this.pinCount) {
|
||||
keyDataToUse.cuts = keyDataToUse.cuts.slice(0, this.pinCount);
|
||||
} else {
|
||||
// Pad with default cuts if key has fewer cuts than lock has pins
|
||||
while (keyDataToUse.cuts.length < this.pinCount) {
|
||||
keyDataToUse.cuts.push(40); // Default cut depth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set each pin to the exact final position based on key cut dimensions
|
||||
keyDataToUse.cuts.forEach((cutDepth, index) => {
|
||||
if (index >= this.pinCount) return;
|
||||
if (index >= this.pinCount) {
|
||||
console.warn(`Key has ${keyDataToUse.cuts.length} cuts but lock only has ${this.pinCount} pins. Skipping cut ${index}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pin = this.pins[index];
|
||||
if (!pin) {
|
||||
console.error(`Pin at index ${index} is undefined. Available pins: ${this.pins.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the exact position where the pin should rest on the key cut
|
||||
// The cut depth represents how deep the cut is from the blade top
|
||||
@@ -1722,6 +1767,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
const cutSurfaceY = keyBladeBaseY + cutDepth;
|
||||
|
||||
// Calculate where the pin bottom should be to rest on the cut surface
|
||||
// Add safety check for undefined properties
|
||||
if (!pin.driverPinLength || !pin.keyPinLength) {
|
||||
console.warn(`Pin ${pin.index} missing length properties:`, pin);
|
||||
return; // Skip this pin if properties are missing
|
||||
}
|
||||
const pinRestY = 200 - 50 + pin.driverPinLength + pin.keyPinLength; // Pin rest position
|
||||
const targetKeyPinBottom = cutSurfaceY;
|
||||
|
||||
@@ -2632,6 +2682,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
const pinWorldY = 200;
|
||||
|
||||
// Calculate pin's current position (including any existing movement)
|
||||
// Add safety check for undefined properties
|
||||
if (!pin.driverPinLength || !pin.keyPinLength) {
|
||||
console.warn(`Pin ${pinIndex} missing length properties in checkHookCollisions:`, pin);
|
||||
return; // Skip this pin if properties are missing
|
||||
}
|
||||
const pinCurrentY = pinWorldY - 50 + pin.driverPinLength + pin.keyPinLength - pin.currentHeight;
|
||||
const keyPinTop = pinCurrentY - pin.keyPinLength;
|
||||
const keyPinBottom = pinCurrentY;
|
||||
@@ -2714,6 +2769,11 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
pin.keyPin.fillStyle(0xdd3333);
|
||||
|
||||
// Calculate new position based on currentHeight
|
||||
// Add safety check for undefined properties
|
||||
if (!pin.driverPinLength || !pin.keyPinLength) {
|
||||
console.warn(`Pin ${pin.index} missing length properties in updatePinVisuals:`, pin);
|
||||
return; // Skip this pin if properties are missing
|
||||
}
|
||||
const newKeyPinY = -50 + pin.driverPinLength - pin.currentHeight;
|
||||
const keyPinTopY = newKeyPinY;
|
||||
const keyPinBottomY = newKeyPinY + pin.keyPinLength;
|
||||
@@ -2773,13 +2833,21 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
// Try to load saved pin heights for this lock
|
||||
const savedPinHeights = this.loadLockConfiguration();
|
||||
|
||||
// Check if predefined pin heights were passed
|
||||
const predefinedPinHeights = this.params?.predefinedPinHeights;
|
||||
|
||||
for (let i = 0; i < this.pinCount; i++) {
|
||||
const pinX = 100 + margin + i * pinSpacing;
|
||||
const pinY = 200;
|
||||
|
||||
// Use saved pin heights if available, otherwise generate random ones
|
||||
// Use predefined pin heights if available, otherwise use saved or generate random ones
|
||||
let keyPinLength, driverPinLength;
|
||||
if (savedPinHeights && savedPinHeights[i] !== undefined) {
|
||||
if (predefinedPinHeights && predefinedPinHeights[i] !== undefined) {
|
||||
// Use predefined configuration
|
||||
keyPinLength = predefinedPinHeights[i];
|
||||
driverPinLength = 75 - keyPinLength; // Total height is 75
|
||||
console.log(`Using predefined pin height for pin ${i}: ${keyPinLength}`);
|
||||
} else if (savedPinHeights && savedPinHeights[i] !== undefined) {
|
||||
// Use saved configuration
|
||||
keyPinLength = savedPinHeights[i];
|
||||
driverPinLength = 75 - keyPinLength; // Total height is 75
|
||||
@@ -2807,6 +2875,13 @@ export class LockpickingMinigamePhaser extends MinigameScene {
|
||||
spring: null
|
||||
};
|
||||
|
||||
// Ensure pin properties are valid
|
||||
if (!pin.keyPinLength || !pin.driverPinLength) {
|
||||
console.error(`Pin ${i} created with invalid lengths:`, pin);
|
||||
pin.keyPinLength = pin.keyPinLength || 30; // Default fallback
|
||||
pin.driverPinLength = pin.driverPinLength || 45; // Default fallback
|
||||
}
|
||||
|
||||
// Create pin container
|
||||
pin.container = this.scene.add.container(pinX, pinY);
|
||||
|
||||
|
||||
601
js/systems/collision.js
Normal file
601
js/systems/collision.js
Normal file
@@ -0,0 +1,601 @@
|
||||
/**
|
||||
* COLLISION MANAGEMENT SYSTEM
|
||||
* ===========================
|
||||
*
|
||||
* Handles static collision geometry, tile-based collision, and wall management.
|
||||
* Separated from rooms.js for better modularity and maintainability.
|
||||
*/
|
||||
|
||||
import { TILE_SIZE } from '../utils/constants.js';
|
||||
import { getOppositeDirection } from './doors.js';
|
||||
|
||||
let gameRef = null;
|
||||
let rooms = null;
|
||||
|
||||
// Initialize collision system
|
||||
export function initializeCollision(gameInstance, roomsRef) {
|
||||
gameRef = gameInstance;
|
||||
rooms = roomsRef;
|
||||
}
|
||||
|
||||
// Function to create thin collision boxes for wall tiles
|
||||
export function createWallCollisionBoxes(wallLayer, roomId, position) {
|
||||
console.log(`Creating wall collision boxes for room ${roomId}`);
|
||||
|
||||
// Get room dimensions from the map
|
||||
const map = rooms[roomId].map;
|
||||
const roomWidth = map.widthInPixels;
|
||||
const roomHeight = map.heightInPixels;
|
||||
|
||||
console.log(`Room ${roomId} dimensions: ${roomWidth}x${roomHeight} at position (${position.x}, ${position.y})`);
|
||||
|
||||
const collisionBoxes = [];
|
||||
|
||||
// Get all wall tiles from the layer
|
||||
const wallTiles = wallLayer.getTilesWithin(0, 0, map.width, map.height, { isNotEmpty: true });
|
||||
|
||||
wallTiles.forEach(tile => {
|
||||
const tileX = tile.x;
|
||||
const tileY = tile.y;
|
||||
const worldX = position.x + (tileX * TILE_SIZE);
|
||||
const worldY = position.y + (tileY * TILE_SIZE);
|
||||
|
||||
// Create collision boxes for all applicable edges (not just one)
|
||||
const tileCollisionBoxes = [];
|
||||
|
||||
// North wall (top 2 rows) - collision on south edge
|
||||
if (tileY < 2) {
|
||||
const collisionBox = gameRef.add.rectangle(
|
||||
worldX + TILE_SIZE / 2,
|
||||
worldY + TILE_SIZE - 4, // 4px from south edge
|
||||
TILE_SIZE,
|
||||
8, // Thicker collision box
|
||||
0x000000,
|
||||
0 // Invisible
|
||||
);
|
||||
tileCollisionBoxes.push(collisionBox);
|
||||
}
|
||||
|
||||
// South wall (bottom row) - collision on south edge
|
||||
if (tileY === map.height - 1) {
|
||||
const collisionBox = gameRef.add.rectangle(
|
||||
worldX + TILE_SIZE / 2,
|
||||
worldY + TILE_SIZE - 4, // 4px from south edge
|
||||
TILE_SIZE,
|
||||
8, // Thicker collision box
|
||||
0x000000,
|
||||
0 // Invisible
|
||||
);
|
||||
tileCollisionBoxes.push(collisionBox);
|
||||
}
|
||||
|
||||
// West wall (left column) - collision on east edge
|
||||
if (tileX === 0) {
|
||||
const collisionBox = gameRef.add.rectangle(
|
||||
worldX + TILE_SIZE - 4, // 4px from east edge
|
||||
worldY + TILE_SIZE / 2,
|
||||
8, // Thicker collision box
|
||||
TILE_SIZE,
|
||||
0x000000,
|
||||
0 // Invisible
|
||||
);
|
||||
tileCollisionBoxes.push(collisionBox);
|
||||
}
|
||||
|
||||
// East wall (right column) - collision on west edge
|
||||
if (tileX === map.width - 1) {
|
||||
const collisionBox = gameRef.add.rectangle(
|
||||
worldX + 4, // 4px from west edge
|
||||
worldY + TILE_SIZE / 2,
|
||||
8, // Thicker collision box
|
||||
TILE_SIZE,
|
||||
0x000000,
|
||||
0 // Invisible
|
||||
);
|
||||
tileCollisionBoxes.push(collisionBox);
|
||||
}
|
||||
|
||||
// Set up all collision boxes for this tile
|
||||
tileCollisionBoxes.forEach(collisionBox => {
|
||||
collisionBox.setVisible(false);
|
||||
gameRef.physics.add.existing(collisionBox, true);
|
||||
|
||||
// Wait for the next frame to ensure body is fully initialized
|
||||
gameRef.time.delayedCall(0, () => {
|
||||
if (collisionBox.body) {
|
||||
// Use direct property assignment (fallback method)
|
||||
collisionBox.body.immovable = true;
|
||||
}
|
||||
});
|
||||
|
||||
collisionBoxes.push(collisionBox);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Created ${collisionBoxes.length} wall collision boxes for room ${roomId}`);
|
||||
|
||||
// Add collision with player for all collision boxes
|
||||
const player = window.player;
|
||||
if (player && player.body) {
|
||||
collisionBoxes.forEach(collisionBox => {
|
||||
gameRef.physics.add.collider(player, collisionBox);
|
||||
});
|
||||
console.log(`Added ${collisionBoxes.length} wall collision boxes for room ${roomId}`);
|
||||
} else {
|
||||
console.warn(`Player not ready for room ${roomId}, storing ${collisionBoxes.length} collision boxes for later`);
|
||||
if (!rooms[roomId].pendingWallCollisionBoxes) {
|
||||
rooms[roomId].pendingWallCollisionBoxes = [];
|
||||
}
|
||||
rooms[roomId].pendingWallCollisionBoxes.push(...collisionBoxes);
|
||||
}
|
||||
|
||||
// Store collision boxes in room for cleanup
|
||||
if (!rooms[roomId].wallCollisionBoxes) {
|
||||
rooms[roomId].wallCollisionBoxes = [];
|
||||
}
|
||||
rooms[roomId].wallCollisionBoxes.push(...collisionBoxes);
|
||||
}
|
||||
|
||||
// Function to remove wall tiles under doors
|
||||
export function removeTilesUnderDoor(wallLayer, roomId, position) {
|
||||
console.log(`Removing wall tiles under doors in room ${roomId}`);
|
||||
|
||||
// Remove wall tiles under doors using the same positioning logic as door sprites
|
||||
const gameScenario = window.gameScenario;
|
||||
const roomData = gameScenario.rooms[roomId];
|
||||
if (!roomData || !roomData.connections) {
|
||||
console.log(`No connections found for room ${roomId}, skipping wall tile removal`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get room dimensions for door positioning (same as door sprite creation)
|
||||
const map = gameRef.cache.tilemap.get(roomData.type);
|
||||
let roomWidth = 800, roomHeight = 600; // fallback
|
||||
|
||||
if (map) {
|
||||
if (map.json) {
|
||||
roomWidth = map.json.width * TILE_SIZE;
|
||||
roomHeight = map.json.height * TILE_SIZE;
|
||||
} else if (map.data) {
|
||||
roomWidth = map.data.width * TILE_SIZE;
|
||||
roomHeight = map.data.height * TILE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
const connections = roomData.connections;
|
||||
|
||||
// Process each connection direction
|
||||
Object.entries(connections).forEach(([direction, connectedRooms]) => {
|
||||
const roomList = Array.isArray(connectedRooms) ? connectedRooms : [connectedRooms];
|
||||
|
||||
roomList.forEach((connectedRoom, index) => {
|
||||
// Calculate door position using the same logic as door sprite creation
|
||||
let doorX, doorY;
|
||||
let doorWidth = TILE_SIZE, doorHeight = TILE_SIZE * 2;
|
||||
|
||||
switch (direction) {
|
||||
case 'north':
|
||||
if (roomList.length === 1) {
|
||||
// Single connection - check the connecting room's connections to determine position
|
||||
const connectingRoom = roomList[0];
|
||||
const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.south;
|
||||
|
||||
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
|
||||
// The connecting room has multiple south doors, find which one connects to this room
|
||||
const doorIndex = connectingRoomConnections.indexOf(roomId);
|
||||
if (doorIndex >= 0) {
|
||||
// When the connecting room has multiple doors, position this door to match
|
||||
// If this room is at index 0 (left), position door on the right (southeast)
|
||||
// If this room is at index 1 (right), position door on the left (southwest)
|
||||
if (doorIndex === 0) {
|
||||
// This room is on the left, so door should be on the right
|
||||
doorX = position.x + roomWidth - TILE_SIZE * 1.5;
|
||||
} else {
|
||||
// This room is on the right, so door should be on the left
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Fallback to left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Single door - use left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Multiple connections - use 1.5 tile spacing from edges
|
||||
const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing
|
||||
const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors
|
||||
doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge
|
||||
}
|
||||
doorY = position.y + TILE_SIZE;
|
||||
break;
|
||||
case 'south':
|
||||
if (roomList.length === 1) {
|
||||
// Single connection - check if the connecting room has multiple doors
|
||||
const connectingRoom = roomList[0];
|
||||
const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.north;
|
||||
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
|
||||
// The connecting room has multiple north doors, find which one connects to this room
|
||||
const doorIndex = connectingRoomConnections.indexOf(roomId);
|
||||
if (doorIndex >= 0) {
|
||||
// When the connecting room has multiple doors, position this door to match
|
||||
// If this room is at index 0 (left), position door on the right (southeast)
|
||||
// If this room is at index 1 (right), position door on the left (southwest)
|
||||
if (doorIndex === 0) {
|
||||
// This room is on the left, so door should be on the right
|
||||
doorX = position.x + roomWidth - TILE_SIZE * 1.5;
|
||||
} else {
|
||||
// This room is on the right, so door should be on the left
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Fallback to left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Single door - use left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
}
|
||||
} else {
|
||||
// Multiple connections - use 1.5 tile spacing from edges
|
||||
const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing
|
||||
const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors
|
||||
doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge
|
||||
}
|
||||
doorY = position.y + roomHeight - TILE_SIZE;
|
||||
break;
|
||||
case 'east':
|
||||
doorX = position.x + roomWidth - TILE_SIZE;
|
||||
doorY = position.y + roomHeight / 2;
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
break;
|
||||
case 'west':
|
||||
doorX = position.x + TILE_SIZE;
|
||||
doorY = position.y + roomHeight / 2;
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Phaser's getTilesWithin to get tiles that overlap with the door area
|
||||
const doorBounds = {
|
||||
x: doorX - (doorWidth / 2), // Door sprite origin is center, so adjust bounds
|
||||
y: doorY - (doorHeight / 2),
|
||||
width: doorWidth,
|
||||
height: doorHeight
|
||||
};
|
||||
|
||||
// Convert door bounds to tilemap coordinates (relative to the layer)
|
||||
const doorBoundsInTilemap = {
|
||||
x: doorBounds.x - wallLayer.x,
|
||||
y: doorBounds.y - wallLayer.y,
|
||||
width: doorBounds.width,
|
||||
height: doorBounds.height
|
||||
};
|
||||
|
||||
console.log(`Removing wall tiles for ${roomId} -> ${connectedRoom} (${direction}): door at (${doorX}, ${doorY}), world bounds:`, doorBounds, `tilemap bounds:`, doorBoundsInTilemap);
|
||||
console.log(`Wall layer info: x=${wallLayer.x}, y=${wallLayer.y}, width=${wallLayer.width}, height=${wallLayer.height}`);
|
||||
|
||||
// Try a different approach - convert to tile coordinates first
|
||||
const doorTileX = Math.floor(doorBoundsInTilemap.x / TILE_SIZE);
|
||||
const doorTileY = Math.floor(doorBoundsInTilemap.y / TILE_SIZE);
|
||||
const doorTilesWide = Math.ceil(doorBoundsInTilemap.width / TILE_SIZE);
|
||||
const doorTilesHigh = Math.ceil(doorBoundsInTilemap.height / TILE_SIZE);
|
||||
|
||||
console.log(`Door tile coordinates: (${doorTileX}, ${doorTileY}) covering ${doorTilesWide}x${doorTilesHigh} tiles`);
|
||||
|
||||
// Check what tiles exist in the door area manually
|
||||
let foundTiles = [];
|
||||
for (let x = 0; x < doorTilesWide; x++) {
|
||||
for (let y = 0; y < doorTilesHigh; y++) {
|
||||
const tileX = doorTileX + x;
|
||||
const tileY = doorTileY + y;
|
||||
const tile = wallLayer.getTileAt(tileX, tileY);
|
||||
if (tile && tile.index !== -1) {
|
||||
foundTiles.push({x: tileX, y: tileY, tile: tile});
|
||||
console.log(`Found wall tile at (${tileX}, ${tileY}) with index ${tile.index}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Manually found ${foundTiles.length} wall tiles in door area`);
|
||||
|
||||
// Get all tiles within the door bounds (using tilemap coordinates)
|
||||
const overlappingTiles = wallLayer.getTilesWithin(
|
||||
doorBoundsInTilemap.x,
|
||||
doorBoundsInTilemap.y,
|
||||
doorBoundsInTilemap.width,
|
||||
doorBoundsInTilemap.height
|
||||
);
|
||||
|
||||
console.log(`getTilesWithin found ${overlappingTiles.length} tiles overlapping with door area`);
|
||||
|
||||
// Use the manually found tiles if getTilesWithin didn't work
|
||||
const tilesToRemove = foundTiles.length > 0 ? foundTiles : overlappingTiles;
|
||||
|
||||
// Remove wall tiles that overlap with the door
|
||||
tilesToRemove.forEach(tileData => {
|
||||
const tileX = tileData.x;
|
||||
const tileY = tileData.y;
|
||||
|
||||
// Remove the wall tile
|
||||
const removedTile = wallLayer.tilemap.removeTileAt(
|
||||
tileX,
|
||||
tileY,
|
||||
true, // replaceWithNull
|
||||
true, // recalculateFaces
|
||||
wallLayer // layer
|
||||
);
|
||||
|
||||
if (removedTile) {
|
||||
console.log(`Removed wall tile at (${tileX}, ${tileY}) under door ${roomId} -> ${connectedRoom}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate collision after removing tiles
|
||||
if (tilesToRemove.length > 0) {
|
||||
console.log(`Recalculating collision for wall layer in ${roomId} after removing ${tilesToRemove.length} tiles`);
|
||||
wallLayer.setCollisionByExclusion([-1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to remove wall tiles from a specific room for a door connection
|
||||
export function removeWallTilesForDoorInRoom(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
|
||||
console.log(`Removing wall tiles in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
|
||||
|
||||
const room = rooms[roomId];
|
||||
if (!room || !room.wallsLayers || room.wallsLayers.length === 0) {
|
||||
console.log(`No wall layers found for room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the door position in the connected room
|
||||
// The door should be on the opposite side of the connection
|
||||
const oppositeDirection = getOppositeDirection(direction);
|
||||
const roomPosition = window.roomPositions[roomId];
|
||||
const roomData = window.gameScenario.rooms[roomId];
|
||||
|
||||
if (!roomPosition || !roomData) {
|
||||
console.log(`Missing position or data for room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get room dimensions
|
||||
const roomWidth = roomData.width || 320;
|
||||
const roomHeight = roomData.height || 288;
|
||||
|
||||
// Calculate door position in the connected room based on the opposite direction
|
||||
let doorX, doorY, doorWidth, doorHeight;
|
||||
|
||||
// Calculate door position based on the room's door configuration
|
||||
if (direction === 'north' || direction === 'south') {
|
||||
// For north/south connections, calculate X position based on room configuration
|
||||
const oppositeDirection = getOppositeDirection(direction);
|
||||
const connections = roomData.connections?.[oppositeDirection];
|
||||
|
||||
if (Array.isArray(connections)) {
|
||||
// Multiple doors - find the one that connects to fromRoomId
|
||||
const doorIndex = connections.indexOf(fromRoomId);
|
||||
if (doorIndex >= 0) {
|
||||
const totalDoors = connections.length;
|
||||
const availableWidth = roomWidth - (TILE_SIZE * 3); // 1.5 tiles from each edge
|
||||
const doorSpacing = totalDoors > 1 ? availableWidth / (totalDoors - 1) : 0;
|
||||
doorX = roomPosition.x + TILE_SIZE * 1.5 + (doorIndex * doorSpacing);
|
||||
} else {
|
||||
doorX = roomPosition.x + roomWidth / 2; // Default to center
|
||||
}
|
||||
} else {
|
||||
// Single door - check if the connecting room has multiple doors
|
||||
const connectingRoomConnections = window.gameScenario.rooms[fromRoomId]?.connections?.[direction];
|
||||
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
|
||||
// The connecting room has multiple doors, find which one connects to this room
|
||||
const doorIndex = connectingRoomConnections.indexOf(roomId);
|
||||
if (doorIndex >= 0) {
|
||||
// When the connecting room has multiple doors, position this door to match
|
||||
// If this room is at index 0 (left), position door on the right (southeast)
|
||||
// If this room is at index 1 (right), position door on the left (southwest)
|
||||
if (doorIndex === 0) {
|
||||
// This room is on the left, so door should be on the right
|
||||
doorX = roomPosition.x + roomWidth - TILE_SIZE * 1.5;
|
||||
console.log(`Wall tile removal door positioning for ${roomId}: left room (index 0), door on right (southeast), calculated doorX=${doorX}`);
|
||||
} else {
|
||||
// This room is on the right, so door should be on the left
|
||||
doorX = roomPosition.x + TILE_SIZE * 1.5;
|
||||
console.log(`Wall tile removal door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), calculated doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Fallback to left positioning
|
||||
doorX = roomPosition.x + TILE_SIZE * 1.5;
|
||||
console.log(`Wall tile removal door positioning for ${roomId}: fallback to left, calculated doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Single door - use left positioning
|
||||
doorX = roomPosition.x + TILE_SIZE * 1.5;
|
||||
console.log(`Wall tile removal door positioning for ${roomId}: single connection to ${fromRoomId}, calculated doorX=${doorX}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (direction === 'north') {
|
||||
// Original door is north, so new door should be south
|
||||
doorY = roomPosition.y + roomHeight - TILE_SIZE;
|
||||
} else {
|
||||
// Original door is south, so new door should be north
|
||||
doorY = roomPosition.y + TILE_SIZE;
|
||||
}
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
} else if (direction === 'east' || direction === 'west') {
|
||||
// For east/west connections, calculate Y position based on room configuration
|
||||
doorY = roomPosition.y + roomHeight / 2; // Center of room
|
||||
if (direction === 'east') {
|
||||
// Original door is east, so new door should be west
|
||||
doorX = roomPosition.x + TILE_SIZE;
|
||||
} else {
|
||||
// Original door is west, so new door should be east
|
||||
doorX = roomPosition.x + roomWidth - TILE_SIZE;
|
||||
}
|
||||
doorWidth = TILE_SIZE;
|
||||
doorHeight = TILE_SIZE * 2;
|
||||
} else {
|
||||
console.log(`Unknown direction: ${direction}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// For debugging: Calculate what the door position should be based on room dimensions
|
||||
const expectedSouthDoorY = roomPosition.y + roomHeight - TILE_SIZE;
|
||||
const expectedNorthDoorY = roomPosition.y + TILE_SIZE;
|
||||
console.log(`Expected door positions for ${roomId}: north=${expectedNorthDoorY}, south=${expectedSouthDoorY}`);
|
||||
|
||||
// Debug: Log the room position and calculated door position
|
||||
console.log(`Room ${roomId} position: (${roomPosition.x}, ${roomPosition.y}), dimensions: ${roomWidth}x${roomHeight}`);
|
||||
console.log(`Original door at (${doorWorldX}, ${doorWorldY}), calculated door at (${doorX}, ${doorY})`);
|
||||
console.log(`Direction: ${direction}, oppositeDirection: ${getOppositeDirection(direction)}`);
|
||||
console.log(`Room connections:`, roomData.connections);
|
||||
|
||||
|
||||
|
||||
console.log(`Calculated door position in ${roomId}: (${doorX}, ${doorY}) for ${oppositeDirection} connection`);
|
||||
|
||||
// Remove wall tiles from all wall layers in this room
|
||||
room.wallsLayers.forEach(wallLayer => {
|
||||
// Calculate door bounds
|
||||
// For north/south doors, the door sprite origin is at the center, but we need to adjust for the actual door position
|
||||
let doorBounds;
|
||||
if (oppositeDirection === 'north' || oppositeDirection === 'south') {
|
||||
// For north/south doors, the door should cover the full width and be positioned at the edge
|
||||
doorBounds = {
|
||||
x: doorX - (doorWidth / 2),
|
||||
y: doorY, // Don't subtract half height - the door is positioned at the edge
|
||||
width: doorWidth,
|
||||
height: doorHeight
|
||||
};
|
||||
} else {
|
||||
// For east/west doors, use center positioning
|
||||
doorBounds = {
|
||||
x: doorX - (doorWidth / 2),
|
||||
y: doorY - (doorHeight / 2),
|
||||
width: doorWidth,
|
||||
height: doorHeight
|
||||
};
|
||||
}
|
||||
|
||||
// For debugging: Show the door sprite dimensions and bounds
|
||||
console.log(`Door sprite at (${doorX}, ${doorY}) with dimensions ${doorWidth}x${doorHeight}`);
|
||||
console.log(`Door bounds: x=${doorBounds.x}, y=${doorBounds.y}, width=${doorBounds.width}, height=${doorBounds.height}`);
|
||||
|
||||
// Convert door bounds to tilemap coordinates
|
||||
const doorBoundsInTilemap = {
|
||||
x: doorBounds.x - wallLayer.x,
|
||||
y: doorBounds.y - wallLayer.y,
|
||||
width: doorBounds.width,
|
||||
height: doorBounds.height
|
||||
};
|
||||
|
||||
console.log(`Removing wall tiles in ${roomId} for ${oppositeDirection} door: world bounds:`, doorBounds, `tilemap bounds:`, doorBoundsInTilemap);
|
||||
console.log(`Wall layer position: (${wallLayer.x}, ${wallLayer.y}), size: ${wallLayer.width}x${wallLayer.height}`);
|
||||
console.log(`Room position: (${roomPosition.x}, ${roomPosition.y}), door position: (${doorX}, ${doorY})`);
|
||||
|
||||
// Convert to tile coordinates
|
||||
const doorTileX = Math.floor(doorBoundsInTilemap.x / TILE_SIZE);
|
||||
const doorTileY = Math.floor(doorBoundsInTilemap.y / TILE_SIZE);
|
||||
const doorTilesWide = Math.ceil(doorBoundsInTilemap.width / TILE_SIZE);
|
||||
const doorTilesHigh = Math.ceil(doorBoundsInTilemap.height / TILE_SIZE);
|
||||
|
||||
console.log(`Expected tile Y: ${Math.floor((doorY - roomPosition.y) / TILE_SIZE)}, actual tile Y: ${doorTileY}`);
|
||||
|
||||
console.log(`Door tile coordinates in ${roomId}: (${doorTileX}, ${doorTileY}) covering ${doorTilesWide}x${doorTilesHigh} tiles`);
|
||||
|
||||
// Check what tiles exist in the door area manually
|
||||
let foundTiles = [];
|
||||
for (let x = 0; x < doorTilesWide; x++) {
|
||||
for (let y = 0; y < doorTilesHigh; y++) {
|
||||
const tileX = doorTileX + x;
|
||||
const tileY = doorTileY + y;
|
||||
const tile = wallLayer.getTileAt(tileX, tileY);
|
||||
if (tile && tile.index !== -1) {
|
||||
foundTiles.push({x: tileX, y: tileY, tile: tile});
|
||||
console.log(`Found wall tile at (${tileX}, ${tileY}) with index ${tile.index} in ${roomId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Manually found ${foundTiles.length} wall tiles in door area in ${roomId}`);
|
||||
|
||||
// Remove wall tiles that overlap with the door
|
||||
foundTiles.forEach(tileData => {
|
||||
const tileX = tileData.x;
|
||||
const tileY = tileData.y;
|
||||
|
||||
// Remove the wall tile
|
||||
const removedTile = wallLayer.tilemap.removeTileAt(
|
||||
tileX,
|
||||
tileY,
|
||||
true, // replaceWithNull
|
||||
true, // recalculateFaces
|
||||
wallLayer // layer
|
||||
);
|
||||
|
||||
if (removedTile) {
|
||||
console.log(`Removed wall tile at (${tileX}, ${tileY}) under door in ${roomId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate collision after removing tiles
|
||||
if (foundTiles.length > 0) {
|
||||
console.log(`Recalculating collision for wall layer in ${roomId} after removing ${foundTiles.length} tiles`);
|
||||
wallLayer.setCollisionByExclusion([-1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to remove wall tiles from all overlapping room layers at a world position
|
||||
export function removeWallTilesAtWorldPosition(worldX, worldY, debugInfo = '') {
|
||||
console.log(`Removing wall tiles at world position (${worldX}, ${worldY}) - ${debugInfo}`);
|
||||
|
||||
// Find all rooms and their wall layers that could contain this world position
|
||||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||||
if (!room.wallsLayers || room.wallsLayers.length === 0) return;
|
||||
|
||||
room.wallsLayers.forEach(wallLayer => {
|
||||
try {
|
||||
// Convert world coordinates to tile coordinates for this layer
|
||||
const tileX = Math.floor((worldX - room.position.x) / TILE_SIZE);
|
||||
const tileY = Math.floor((worldY - room.position.y) / TILE_SIZE);
|
||||
|
||||
// Check if the tile coordinates are within the layer bounds
|
||||
const wallTile = wallLayer.getTileAt(tileX, tileY);
|
||||
if (wallTile && wallTile.index !== -1) {
|
||||
// Remove the wall tile using the map's removeTileAt method
|
||||
const removedTile = room.map.removeTileAt(
|
||||
tileX,
|
||||
tileY,
|
||||
true, // replaceWithNull
|
||||
true, // recalculateFaces
|
||||
wallLayer // layer
|
||||
);
|
||||
|
||||
if (removedTile) {
|
||||
console.log(` Removed wall tile at (${tileX},${tileY}) from room ${roomId} layer ${wallLayer.name}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` No wall tile found at (${tileX},${tileY}) in room ${roomId} layer ${wallLayer.name || 'unnamed'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error removing wall tile from room ${roomId}:`, error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Export for global access
|
||||
window.createWallCollisionBoxes = createWallCollisionBoxes;
|
||||
window.removeTilesUnderDoor = removeTilesUnderDoor;
|
||||
window.removeWallTilesForDoorInRoom = removeWallTilesForDoorInRoom;
|
||||
window.removeWallTilesAtWorldPosition = removeWallTilesAtWorldPosition;
|
||||
664
js/systems/doors.js
Normal file
664
js/systems/doors.js
Normal file
@@ -0,0 +1,664 @@
|
||||
/**
|
||||
* DOOR SYSTEM
|
||||
* ===========
|
||||
*
|
||||
* Handles door sprites, interactions, transitions, and visibility management.
|
||||
* Separated from rooms.js for better modularity and maintainability.
|
||||
*/
|
||||
|
||||
import { TILE_SIZE } from '../utils/constants.js';
|
||||
import { handleUnlock, getLockRequirementsForDoor } from './interactions.js';
|
||||
|
||||
let gameRef = null;
|
||||
let rooms = null;
|
||||
|
||||
// Global toggle for disabling locks during testing
|
||||
window.DISABLE_LOCKS = false; // Set to true in console to bypass all lock checks
|
||||
|
||||
// Console helper functions for testing
|
||||
window.toggleLocks = function() {
|
||||
window.DISABLE_LOCKS = !window.DISABLE_LOCKS;
|
||||
console.log(`Locks ${window.DISABLE_LOCKS ? 'DISABLED' : 'ENABLED'} for testing`);
|
||||
return window.DISABLE_LOCKS;
|
||||
};
|
||||
|
||||
window.disableLocks = function() {
|
||||
window.DISABLE_LOCKS = true;
|
||||
console.log('Locks DISABLED for testing - all doors will open without minigames');
|
||||
};
|
||||
|
||||
window.enableLocks = function() {
|
||||
window.DISABLE_LOCKS = false;
|
||||
console.log('Locks ENABLED - doors will require proper unlocking');
|
||||
};
|
||||
|
||||
// Door transition cooldown system
|
||||
let lastDoorTransitionTime = 0;
|
||||
const DOOR_TRANSITION_COOLDOWN = 1000; // 1 second cooldown between transitions
|
||||
let lastDoorTransition = null; // Track the last door transition to prevent repeats
|
||||
|
||||
// Initialize door system
|
||||
export function initializeDoors(gameInstance, roomsRef) {
|
||||
gameRef = gameInstance;
|
||||
rooms = roomsRef;
|
||||
}
|
||||
|
||||
// Function to create door sprites based on gameScenario connections
|
||||
export function createDoorSpritesForRoom(roomId, position) {
|
||||
const gameScenario = window.gameScenario;
|
||||
const roomData = gameScenario.rooms[roomId];
|
||||
if (!roomData || !roomData.connections) {
|
||||
console.log(`No connections found for room ${roomId}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Creating door sprites for room ${roomId}:`, roomData.connections);
|
||||
|
||||
const doorSprites = [];
|
||||
const connections = roomData.connections;
|
||||
|
||||
// Get room dimensions for door positioning
|
||||
const map = gameRef.cache.tilemap.get(roomData.type);
|
||||
let roomWidth = 800, roomHeight = 600; // fallback
|
||||
|
||||
if (map) {
|
||||
if (map.json) {
|
||||
roomWidth = map.json.width * TILE_SIZE;
|
||||
roomHeight = map.json.height * TILE_SIZE;
|
||||
} else if (map.data) {
|
||||
roomWidth = map.data.width * TILE_SIZE;
|
||||
roomHeight = map.data.height * TILE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Room ${roomId} dimensions: ${roomWidth}x${roomHeight}, position: (${position.x}, ${position.y})`);
|
||||
|
||||
// Create door sprites for each connection direction
|
||||
Object.entries(connections).forEach(([direction, connectedRooms]) => {
|
||||
const roomList = Array.isArray(connectedRooms) ? connectedRooms : [connectedRooms];
|
||||
|
||||
roomList.forEach((connectedRoom, index) => {
|
||||
// Calculate door position based on direction
|
||||
let doorX, doorY;
|
||||
let doorWidth = TILE_SIZE, doorHeight = TILE_SIZE * 2;
|
||||
|
||||
switch (direction) {
|
||||
case 'north':
|
||||
// Door at top of room, 1.5 tiles in from sides
|
||||
if (roomList.length === 1) {
|
||||
// Single connection - check the connecting room's connections to determine position
|
||||
const connectingRoom = roomList[0];
|
||||
const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.south;
|
||||
|
||||
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
|
||||
// The connecting room has multiple south doors, find which one connects to this room
|
||||
const doorIndex = connectingRoomConnections.indexOf(roomId);
|
||||
if (doorIndex >= 0) {
|
||||
// When the connecting room has multiple doors, position this door to match
|
||||
// If this room is at index 0 (left), position door on the right (southeast)
|
||||
// If this room is at index 1 (right), position door on the left (southwest)
|
||||
if (doorIndex === 0) {
|
||||
// This room is on the left, so door should be on the right
|
||||
doorX = position.x + roomWidth - TILE_SIZE * 1.5;
|
||||
console.log(`North door positioning for ${roomId}: left room (index 0), door on right (southeast), doorX=${doorX}`);
|
||||
} else {
|
||||
// This room is on the right, so door should be on the left
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`North door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Fallback to left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`North door positioning for ${roomId}: fallback to left, doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Single door - use left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`North door positioning for ${roomId}: single connection to ${connectingRoom}, doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Multiple connections - use 1.5 tile spacing from edges
|
||||
const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing
|
||||
const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors
|
||||
doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge
|
||||
}
|
||||
doorY = position.y + TILE_SIZE; // 1 tile from top
|
||||
console.log(`North door Y position: ${doorY} (position.y=${position.y}, TILE_SIZE=${TILE_SIZE})`);
|
||||
break;
|
||||
case 'south':
|
||||
// Door at bottom of room, 1.5 tiles in from sides
|
||||
if (roomList.length === 1) {
|
||||
// Single connection - check if the connecting room has multiple doors
|
||||
const connectingRoom = roomList[0];
|
||||
const connectingRoomConnections = window.gameScenario.rooms[connectingRoom]?.connections?.north;
|
||||
if (Array.isArray(connectingRoomConnections) && connectingRoomConnections.length > 1) {
|
||||
// The connecting room has multiple north doors, find which one connects to this room
|
||||
const doorIndex = connectingRoomConnections.indexOf(roomId);
|
||||
if (doorIndex >= 0) {
|
||||
// When the connecting room has multiple doors, position this door to match
|
||||
// If this room is at index 0 (left), position door on the right (southeast)
|
||||
// If this room is at index 1 (right), position door on the left (southwest)
|
||||
if (doorIndex === 0) {
|
||||
// This room is on the left, so door should be on the right
|
||||
doorX = position.x + roomWidth - TILE_SIZE * 1.5;
|
||||
console.log(`South door positioning for ${roomId}: left room (index 0), door on right (southeast), doorX=${doorX}`);
|
||||
} else {
|
||||
// This room is on the right, so door should be on the left
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`South door positioning for ${roomId}: right room (index ${doorIndex}), door on left (southwest), doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Fallback to left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`South door positioning for ${roomId}: fallback to left, doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Single door - use left positioning
|
||||
doorX = position.x + TILE_SIZE * 1.5;
|
||||
console.log(`South door positioning for ${roomId}: single connection to ${connectingRoom}, doorX=${doorX}`);
|
||||
}
|
||||
} else {
|
||||
// Multiple connections - use 1.5 tile spacing from edges
|
||||
const availableWidth = roomWidth - (TILE_SIZE * 1.5 * 2); // Subtract edge spacing
|
||||
const doorSpacing = availableWidth / (roomList.length - 1); // Space between doors
|
||||
doorX = position.x + TILE_SIZE * 1.5 + (doorSpacing * index); // Start at 1.5 tiles from edge
|
||||
}
|
||||
doorY = position.y + roomHeight - TILE_SIZE; // 1 tile from bottom
|
||||
// replace the bottom most tile with a copy of the tile above
|
||||
|
||||
|
||||
break;
|
||||
case 'east':
|
||||
// Door at right side of room, 1 tile in from top/bottom
|
||||
doorX = position.x + roomWidth - TILE_SIZE; // 1 tile from right
|
||||
doorY = position.y + roomHeight / 2; // Center of room
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
break;
|
||||
case 'west':
|
||||
// Door at left side of room, 1 tile in from top/bottom
|
||||
doorX = position.x + TILE_SIZE; // 1 tile from left
|
||||
doorY = position.y + roomHeight / 2; // Center of room
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
break;
|
||||
default:
|
||||
return; // Skip unknown directions
|
||||
}
|
||||
|
||||
// Create door sprite
|
||||
console.log(`Creating door sprite at (${doorX}, ${doorY}) for ${roomId} -> ${connectedRoom}`);
|
||||
|
||||
// Create a colored rectangle as a fallback if door texture fails
|
||||
let doorSprite;
|
||||
try {
|
||||
doorSprite = gameRef.add.sprite(doorX, doorY, 'door_32');
|
||||
} catch (error) {
|
||||
console.warn(`Failed to create door sprite with 'door_32' texture, creating colored rectangle instead:`, error);
|
||||
// Create a colored rectangle as fallback
|
||||
const graphics = gameRef.add.graphics();
|
||||
graphics.fillStyle(0xff0000, 1); // Red color
|
||||
graphics.fillRect(-TILE_SIZE/2, -TILE_SIZE, TILE_SIZE, TILE_SIZE * 2);
|
||||
graphics.setPosition(doorX, doorY);
|
||||
doorSprite = graphics;
|
||||
}
|
||||
doorSprite.setOrigin(0.5, 0.5);
|
||||
doorSprite.setDepth(doorY + 0.45); // World Y + door layer offset
|
||||
doorSprite.setAlpha(1); // Visible by default
|
||||
doorSprite.setVisible(true); // Ensure visibility
|
||||
|
||||
console.log(`Door sprite created:`, {
|
||||
x: doorSprite.x,
|
||||
y: doorSprite.y,
|
||||
visible: doorSprite.visible,
|
||||
alpha: doorSprite.alpha,
|
||||
depth: doorSprite.depth,
|
||||
texture: doorSprite.texture?.key,
|
||||
width: doorSprite.width,
|
||||
height: doorSprite.height,
|
||||
displayWidth: doorSprite.displayWidth,
|
||||
displayHeight: doorSprite.displayHeight
|
||||
});
|
||||
console.log(`Door depth: ${doorSprite.depth} (roomDepth: ${doorY}, between tiles and sprites)`);
|
||||
|
||||
// Set up door properties
|
||||
doorSprite.doorProperties = {
|
||||
roomId: roomId,
|
||||
connectedRoom: connectedRoom,
|
||||
direction: direction,
|
||||
worldX: doorX,
|
||||
worldY: doorY,
|
||||
open: false,
|
||||
locked: roomData.locked || false,
|
||||
lockType: roomData.lockType || null,
|
||||
requires: roomData.requires || null
|
||||
};
|
||||
|
||||
// Set up door info for transition detection
|
||||
doorSprite.doorInfo = {
|
||||
roomId: roomId,
|
||||
connectedRoom: connectedRoom,
|
||||
direction: direction
|
||||
};
|
||||
|
||||
// Set up collision
|
||||
gameRef.physics.add.existing(doorSprite);
|
||||
doorSprite.body.setSize(doorWidth, doorHeight);
|
||||
doorSprite.body.setImmovable(true);
|
||||
|
||||
// Add collision with player
|
||||
if (window.player && window.player.body) {
|
||||
gameRef.physics.add.collider(window.player, doorSprite);
|
||||
}
|
||||
|
||||
// Set up interaction zone
|
||||
const zone = gameRef.add.zone(doorX, doorY, doorWidth, doorHeight);
|
||||
zone.setInteractive({ useHandCursor: true });
|
||||
zone.on('pointerdown', () => handleDoorInteraction(doorSprite));
|
||||
|
||||
doorSprite.interactionZone = zone;
|
||||
doorSprites.push(doorSprite);
|
||||
|
||||
console.log(`Created door sprite for ${roomId} -> ${connectedRoom} (${direction}) at (${doorX}, ${doorY})`);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Created ${doorSprites.length} door sprites for room ${roomId}`);
|
||||
|
||||
// Log camera position for debugging
|
||||
if (gameRef.cameras && gameRef.cameras.main) {
|
||||
console.log(`Camera position:`, {
|
||||
x: gameRef.cameras.main.scrollX,
|
||||
y: gameRef.cameras.main.scrollY,
|
||||
width: gameRef.cameras.main.width,
|
||||
height: gameRef.cameras.main.height
|
||||
});
|
||||
}
|
||||
|
||||
return doorSprites;
|
||||
}
|
||||
|
||||
// Function to handle door interactions
|
||||
function handleDoorInteraction(doorSprite) {
|
||||
const player = window.player;
|
||||
if (!player) return;
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
player.x, player.y,
|
||||
doorSprite.x, doorSprite.y
|
||||
);
|
||||
|
||||
const DOOR_INTERACTION_RANGE = 2 * TILE_SIZE;
|
||||
|
||||
if (distance > DOOR_INTERACTION_RANGE) {
|
||||
console.log('Door too far to interact');
|
||||
return;
|
||||
}
|
||||
|
||||
const props = doorSprite.doorProperties;
|
||||
console.log(`Interacting with door: ${props.roomId} -> ${props.connectedRoom}`);
|
||||
|
||||
// Check if locks are disabled for testing
|
||||
if (window.DISABLE_LOCKS) {
|
||||
console.log('LOCKS DISABLED FOR TESTING - Opening door directly');
|
||||
openDoor(doorSprite);
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.locked) {
|
||||
console.log(`Door is locked. Type: ${props.lockType}, Requires: ${props.requires}`);
|
||||
// Use the proper lock system from interactions.js
|
||||
handleUnlock(doorSprite, 'door');
|
||||
} else {
|
||||
openDoor(doorSprite);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to unlock a door (called by interactions.js after successful unlock)
|
||||
function unlockDoor(doorSprite) {
|
||||
const props = doorSprite.doorProperties;
|
||||
console.log(`Unlocking door: ${props.roomId} -> ${props.connectedRoom}`);
|
||||
|
||||
// Mark door as unlocked
|
||||
props.locked = false;
|
||||
|
||||
// TODO: Implement unlock animation/effect
|
||||
|
||||
// Open the door
|
||||
openDoor(doorSprite);
|
||||
}
|
||||
|
||||
// Function to open a door
|
||||
function openDoor(doorSprite) {
|
||||
const props = doorSprite.doorProperties;
|
||||
console.log(`Opening door: ${props.roomId} -> ${props.connectedRoom}`);
|
||||
|
||||
// Load the connected room if it doesn't exist
|
||||
if (!rooms[props.connectedRoom]) {
|
||||
console.log(`Loading room: ${props.connectedRoom}`);
|
||||
// Import the loadRoom function from rooms.js
|
||||
if (window.loadRoom) {
|
||||
window.loadRoom(props.connectedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove wall tiles from the connected room under the door position
|
||||
if (window.removeWallTilesForDoorInRoom) {
|
||||
window.removeWallTilesForDoorInRoom(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
|
||||
}
|
||||
|
||||
// Remove the matching door sprite from the connected room
|
||||
removeMatchingDoorSprite(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
|
||||
|
||||
// Create animated door sprite on the opposite side
|
||||
createAnimatedDoorOnOppositeSide(props.connectedRoom, props.roomId, props.direction, doorSprite.x, doorSprite.y);
|
||||
|
||||
// Remove the door sprite
|
||||
doorSprite.destroy();
|
||||
if (doorSprite.interactionZone) {
|
||||
doorSprite.interactionZone.destroy();
|
||||
}
|
||||
|
||||
props.open = true;
|
||||
}
|
||||
|
||||
// Function to remove the matching door sprite from the connected room
|
||||
function removeMatchingDoorSprite(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
|
||||
console.log(`Removing matching door sprite in room ${roomId} for door from ${fromRoomId} (${direction})`);
|
||||
|
||||
const room = rooms[roomId];
|
||||
if (!room || !room.doorSprites) {
|
||||
console.log(`No door sprites found for room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the door sprite that connects to the fromRoomId
|
||||
const matchingDoorSprite = room.doorSprites.find(doorSprite => {
|
||||
const props = doorSprite.doorProperties;
|
||||
return props && props.connectedRoom === fromRoomId;
|
||||
});
|
||||
|
||||
if (matchingDoorSprite) {
|
||||
console.log(`Found matching door sprite in room ${roomId}, removing it`);
|
||||
matchingDoorSprite.destroy();
|
||||
if (matchingDoorSprite.interactionZone) {
|
||||
matchingDoorSprite.interactionZone.destroy();
|
||||
}
|
||||
|
||||
// Remove from the doorSprites array
|
||||
const index = room.doorSprites.indexOf(matchingDoorSprite);
|
||||
if (index > -1) {
|
||||
room.doorSprites.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
console.log(`No matching door sprite found in room ${roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create animated door sprite on the opposite side
|
||||
function createAnimatedDoorOnOppositeSide(roomId, fromRoomId, direction, doorWorldX, doorWorldY) {
|
||||
console.log(`Creating animated door on opposite side in room ${roomId} for door from ${fromRoomId} (${direction}) at world position (${doorWorldX}, ${doorWorldY})`);
|
||||
|
||||
const room = rooms[roomId];
|
||||
if (!room) {
|
||||
console.log(`Room ${roomId} not found, cannot create animated door`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the door position in the connected room
|
||||
const oppositeDirection = getOppositeDirection(direction);
|
||||
const roomPosition = window.roomPositions[roomId];
|
||||
const roomData = window.gameScenario.rooms[roomId];
|
||||
|
||||
if (!roomPosition || !roomData) {
|
||||
console.log(`Missing position or data for room ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get room dimensions from tilemap (same as door sprite creation)
|
||||
const map = gameRef.cache.tilemap.get(roomData.type);
|
||||
let roomWidth = 320, roomHeight = 288; // fallback (10x9 tiles at 32px)
|
||||
|
||||
if (map) {
|
||||
if (map.json) {
|
||||
roomWidth = map.json.width * TILE_SIZE;
|
||||
roomHeight = map.json.height * TILE_SIZE;
|
||||
} else if (map.data) {
|
||||
roomWidth = map.data.width * TILE_SIZE;
|
||||
roomHeight = map.data.height * TILE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the same world coordinates as the original door
|
||||
let doorX = doorWorldX, doorY = doorWorldY, doorWidth, doorHeight;
|
||||
|
||||
// Set door dimensions based on direction
|
||||
if (direction === 'north' || direction === 'south') {
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
} else if (direction === 'east' || direction === 'west') {
|
||||
doorWidth = TILE_SIZE * 2;
|
||||
doorHeight = TILE_SIZE;
|
||||
} else {
|
||||
console.log(`Unknown direction: ${direction}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the animated door sprite
|
||||
let animatedDoorSprite;
|
||||
let doorTopSprite;
|
||||
try {
|
||||
// Create main door sprite
|
||||
animatedDoorSprite = gameRef.add.sprite(doorX, doorY, 'door_sheet');
|
||||
|
||||
// Calculate the bottom of the door (where it meets the ground)
|
||||
const doorBottomY = doorY + (TILE_SIZE * 2) / 2; // doorY is center, so add half height to get bottom
|
||||
|
||||
// Set sprite properties
|
||||
animatedDoorSprite.setOrigin(0.5, 0.5);
|
||||
animatedDoorSprite.setDepth(doorBottomY + 0.45); // Bottom Y + door layer offset
|
||||
animatedDoorSprite.setVisible(true);
|
||||
|
||||
// Play the opening animation
|
||||
animatedDoorSprite.play('door_open');
|
||||
|
||||
// Create door top sprite (6th frame) at high z-index
|
||||
doorTopSprite = gameRef.add.sprite(doorX, doorY, 'door_sheet');
|
||||
doorTopSprite.setOrigin(0.5, 0.5);
|
||||
doorTopSprite.setDepth(doorBottomY + 0.55); // Bottom Y + door top layer offset
|
||||
doorTopSprite.setVisible(true);
|
||||
doorTopSprite.play('door_top');
|
||||
|
||||
// Store references to the animated doors in the room
|
||||
if (!room.animatedDoors) {
|
||||
room.animatedDoors = [];
|
||||
}
|
||||
room.animatedDoors.push(animatedDoorSprite);
|
||||
room.animatedDoors.push(doorTopSprite);
|
||||
|
||||
console.log(`Created animated door sprite at (${doorX}, ${doorY}) in room ${roomId} with door top`);
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Failed to create animated door sprite:`, error);
|
||||
// Fallback to a simple colored rectangle
|
||||
const graphics = gameRef.add.graphics();
|
||||
graphics.fillStyle(0x00ff00, 1); // Green color for open door
|
||||
graphics.fillRect(-doorWidth/2, -doorHeight/2, doorWidth, doorHeight);
|
||||
graphics.setPosition(doorX, doorY);
|
||||
|
||||
// Calculate the bottom of the door (where it meets the ground)
|
||||
const doorBottomY = doorY + (TILE_SIZE * 2) / 2; // doorY is center, so add half height to get bottom
|
||||
graphics.setDepth(doorBottomY + 0.45); // Bottom Y + door layer offset
|
||||
|
||||
if (!room.animatedDoors) {
|
||||
room.animatedDoors = [];
|
||||
}
|
||||
room.animatedDoors.push(graphics);
|
||||
|
||||
console.log(`Created fallback animated door at (${doorX}, ${doorY}) in room ${roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get the opposite direction
|
||||
export function getOppositeDirection(direction) {
|
||||
switch (direction) {
|
||||
case 'north': return 'south';
|
||||
case 'south': return 'north';
|
||||
case 'east': return 'west';
|
||||
case 'west': return 'east';
|
||||
default: return direction;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if player has crossed a door threshold
|
||||
export function checkDoorTransitions(player) {
|
||||
// Check cooldown first
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastDoorTransitionTime < DOOR_TRANSITION_COOLDOWN) {
|
||||
return null; // Still in cooldown
|
||||
}
|
||||
|
||||
const playerBottomY = player.y + (player.height * player.scaleY) / 2;
|
||||
let closestTransition = null;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
// Check all rooms for door transitions
|
||||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||||
if (!room.doorSprites) return;
|
||||
|
||||
room.doorSprites.forEach(doorSprite => {
|
||||
// Get door information from the sprite's custom properties
|
||||
const doorInfo = doorSprite.doorInfo;
|
||||
if (!doorInfo) return;
|
||||
|
||||
const { direction, connectedRoom } = doorInfo;
|
||||
|
||||
// Skip if this would transition to the current room
|
||||
if (connectedRoom === window.currentPlayerRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if this is the same transition we just made
|
||||
if (lastDoorTransition === `${window.currentPlayerRoom}->${connectedRoom}`) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate door threshold based on direction
|
||||
let doorThreshold = null;
|
||||
const roomPosition = room.position;
|
||||
const roomHeight = room.map.heightInPixels;
|
||||
|
||||
if (direction === 'north') {
|
||||
// North door: threshold is 2 tiles down from top (bottom of door)
|
||||
doorThreshold = roomPosition.y + TILE_SIZE * 2; // 1 tile from top + 1 more tile for door height
|
||||
} else if (direction === 'south') {
|
||||
// South door: threshold is 2 tiles up from bottom (top of door)
|
||||
doorThreshold = roomPosition.y + roomHeight - TILE_SIZE * 2; // 1 tile from bottom + 1 more tile for door height
|
||||
}
|
||||
|
||||
if (doorThreshold !== null) {
|
||||
// Check if player has crossed the threshold
|
||||
let shouldTransition = false;
|
||||
if (direction === 'north' && playerBottomY <= doorThreshold) {
|
||||
shouldTransition = true;
|
||||
} else if (direction === 'south' && playerBottomY >= doorThreshold) {
|
||||
shouldTransition = true;
|
||||
}
|
||||
|
||||
if (shouldTransition) {
|
||||
// Calculate distance to this door threshold
|
||||
const distanceToThreshold = Math.abs(playerBottomY - doorThreshold);
|
||||
|
||||
// Only consider this transition if it's closer than any previous one
|
||||
if (distanceToThreshold < closestDistance) {
|
||||
closestDistance = distanceToThreshold;
|
||||
closestTransition = connectedRoom;
|
||||
console.log(`Player crossed ${direction} door threshold in ${roomId} -> ${connectedRoom} (current: ${window.currentPlayerRoom}, distance: ${distanceToThreshold.toFixed(2)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// If a transition was detected, set the cooldown and track the transition
|
||||
if (closestTransition) {
|
||||
lastDoorTransitionTime = currentTime;
|
||||
lastDoorTransition = `${window.currentPlayerRoom}->${closestTransition}`;
|
||||
}
|
||||
|
||||
return closestTransition;
|
||||
}
|
||||
|
||||
// Update door sprites visibility based on which rooms are revealed
|
||||
export function updateDoorSpritesVisibility() {
|
||||
const discoveredRooms = window.discoveredRooms || new Set();
|
||||
console.log(`updateDoorSpritesVisibility called. Discovered rooms:`, Array.from(discoveredRooms));
|
||||
|
||||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||||
if (!room.doorSprites) return;
|
||||
|
||||
room.doorSprites.forEach(doorSprite => {
|
||||
// Get the door sprite's bounds (it covers 2 tiles vertically)
|
||||
const doorSpriteBounds = {
|
||||
x: doorSprite.x - TILE_SIZE/2, // Left edge of door sprite (center origin)
|
||||
y: doorSprite.y - TILE_SIZE, // Top edge of door sprite (center origin)
|
||||
width: TILE_SIZE, // Door sprite width
|
||||
height: TILE_SIZE * 2 // Door sprite height (2 tiles)
|
||||
};
|
||||
|
||||
// Check if this room is revealed (doors should be visible if their room is visible)
|
||||
const thisRoomRevealed = discoveredRooms.has(roomId);
|
||||
|
||||
// Check how many other revealed rooms this door overlaps with
|
||||
let overlappingRevealedRooms = 0;
|
||||
|
||||
Object.entries(rooms).forEach(([otherRoomId, otherRoom]) => {
|
||||
if (!discoveredRooms.has(otherRoomId)) return; // Skip unrevealed rooms
|
||||
|
||||
const otherRoomBounds = {
|
||||
x: otherRoom.position.x,
|
||||
y: otherRoom.position.y,
|
||||
width: otherRoom.map.widthInPixels,
|
||||
height: otherRoom.map.heightInPixels
|
||||
};
|
||||
|
||||
// Check if door sprite bounds overlap with this revealed room
|
||||
if (boundsOverlap(doorSpriteBounds, otherRoomBounds)) {
|
||||
overlappingRevealedRooms++;
|
||||
}
|
||||
});
|
||||
|
||||
// Door should be visible if its room is revealed OR if it overlaps with any revealed room
|
||||
const shouldBeVisible = thisRoomRevealed || overlappingRevealedRooms > 0;
|
||||
|
||||
console.log(`Door sprite at (${doorSprite.x}, ${doorSprite.y}) in room ${roomId}:`);
|
||||
console.log(` This room revealed: ${thisRoomRevealed}`);
|
||||
console.log(` Overlapping revealed rooms: ${overlappingRevealedRooms}`);
|
||||
console.log(` Should be visible: ${shouldBeVisible}`);
|
||||
|
||||
if (shouldBeVisible) {
|
||||
doorSprite.setVisible(true);
|
||||
doorSprite.setAlpha(1);
|
||||
} else {
|
||||
doorSprite.setVisible(false);
|
||||
doorSprite.setAlpha(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to check if two rectangles overlap
|
||||
function boundsOverlap(rect1, rect2) {
|
||||
return rect1.x < rect2.x + rect2.width &&
|
||||
rect1.x + rect1.width > rect2.x &&
|
||||
rect1.y < rect2.y + rect2.height &&
|
||||
rect1.y + rect1.height > rect2.y;
|
||||
}
|
||||
|
||||
|
||||
// Export for global access
|
||||
window.updateDoorSpritesVisibility = updateDoorSpritesVisibility;
|
||||
window.checkDoorTransitions = checkDoorTransitions;
|
||||
|
||||
// Export functions for use by other modules
|
||||
export { unlockDoor };
|
||||
@@ -1,6 +1,7 @@
|
||||
// Object interaction system
|
||||
import { INTERACTION_RANGE, INTERACTION_RANGE_SQ, INTERACTION_CHECK_INTERVAL, TILE_SIZE, DOOR_ALIGN_OVERLAP } from '../utils/constants.js?v=7';
|
||||
import { rooms } from '../core/rooms.js?v=16';
|
||||
import { unlockDoor } from './doors.js';
|
||||
|
||||
// Helper function to check if two rectangles overlap
|
||||
function boundsOverlap(rect1, rect2) {
|
||||
@@ -12,6 +13,190 @@ function boundsOverlap(rect1, rect2) {
|
||||
|
||||
let gameRef = null;
|
||||
|
||||
// Global key-lock mapping system
|
||||
// This ensures each key matches exactly one lock in the game
|
||||
window.keyLockMappings = window.keyLockMappings || {};
|
||||
|
||||
// Predefined lock configurations for the game
|
||||
// Each lock has a unique ID and pin configuration
|
||||
const PREDEFINED_LOCK_CONFIGS = {
|
||||
'ceo_briefcase_lock': {
|
||||
id: 'ceo_briefcase_lock',
|
||||
pinCount: 4,
|
||||
pinHeights: [32, 28, 35, 30], // Specific pin heights for CEO briefcase
|
||||
difficulty: 'medium'
|
||||
},
|
||||
'office_drawer_lock': {
|
||||
id: 'office_drawer_lock',
|
||||
pinCount: 3,
|
||||
pinHeights: [25, 30, 28],
|
||||
difficulty: 'easy'
|
||||
},
|
||||
'server_room_lock': {
|
||||
id: 'server_room_lock',
|
||||
pinCount: 5,
|
||||
pinHeights: [40, 35, 38, 32, 36],
|
||||
difficulty: 'hard'
|
||||
},
|
||||
'storage_cabinet_lock': {
|
||||
id: 'storage_cabinet_lock',
|
||||
pinCount: 4,
|
||||
pinHeights: [29, 33, 27, 31],
|
||||
difficulty: 'medium'
|
||||
}
|
||||
};
|
||||
|
||||
// Function to assign keys to locks based on scenario definitions
|
||||
function assignKeysToLocks() {
|
||||
console.log('Assigning keys to locks based on scenario definitions...');
|
||||
|
||||
// Get all keys from inventory
|
||||
const playerKeys = window.inventory?.items?.filter(item =>
|
||||
item && item.scenarioData &&
|
||||
item.scenarioData.type === 'key'
|
||||
) || [];
|
||||
|
||||
console.log(`Found ${playerKeys.length} keys in inventory`);
|
||||
|
||||
// Get all rooms from the current scenario
|
||||
const rooms = window.gameState?.scenario?.rooms || {};
|
||||
console.log(`Found ${Object.keys(rooms).length} rooms in scenario`);
|
||||
|
||||
// Find all locks that require keys
|
||||
const keyLocks = [];
|
||||
Object.entries(rooms).forEach(([roomId, roomData]) => {
|
||||
if (roomData.locked && roomData.lockType === 'key' && roomData.requires) {
|
||||
keyLocks.push({
|
||||
roomId: roomId,
|
||||
requiredKeyId: roomData.requires,
|
||||
roomName: roomData.type || roomId
|
||||
});
|
||||
}
|
||||
|
||||
// Also check objects within rooms for key locks
|
||||
if (roomData.objects) {
|
||||
roomData.objects.forEach((obj, objIndex) => {
|
||||
if (obj.locked && obj.lockType === 'key' && obj.requires) {
|
||||
keyLocks.push({
|
||||
roomId: roomId,
|
||||
objectIndex: objIndex,
|
||||
requiredKeyId: obj.requires,
|
||||
objectName: obj.name || obj.type
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Found ${keyLocks.length} key locks in scenario:`, keyLocks);
|
||||
|
||||
// Create mappings based on scenario definitions
|
||||
keyLocks.forEach(lock => {
|
||||
const keyId = lock.requiredKeyId;
|
||||
|
||||
// Find the key in player inventory
|
||||
const key = playerKeys.find(k => k.scenarioData.key_id === keyId);
|
||||
|
||||
if (key) {
|
||||
// Create a lock configuration for this specific lock
|
||||
const lockConfig = {
|
||||
id: `${lock.roomId}_${lock.objectIndex !== undefined ? `obj_${lock.objectIndex}` : 'room'}`,
|
||||
pinCount: 4, // Default pin count
|
||||
pinHeights: generatePinHeightsForLock(lock.roomId, keyId), // Generate consistent pin heights
|
||||
difficulty: 'medium'
|
||||
};
|
||||
|
||||
// Store the mapping
|
||||
window.keyLockMappings[keyId] = {
|
||||
lockId: lockConfig.id,
|
||||
lockConfig: lockConfig,
|
||||
keyName: key.scenarioData.name,
|
||||
roomId: lock.roomId,
|
||||
objectIndex: lock.objectIndex,
|
||||
lockName: lock.objectName || lock.roomName
|
||||
};
|
||||
|
||||
console.log(`Assigned key "${key.scenarioData.name}" (${keyId}) to lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''}`);
|
||||
} else {
|
||||
console.warn(`Key "${keyId}" required by lock in ${lock.roomName}${lock.objectName ? ` - ${lock.objectName}` : ''} not found in inventory`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Key-lock mappings based on scenario:', window.keyLockMappings);
|
||||
}
|
||||
|
||||
// Function to generate consistent pin heights for a lock based on room and key
|
||||
function generatePinHeightsForLock(roomId, keyId) {
|
||||
// Use a deterministic seed based on room and key IDs
|
||||
const seed = (roomId + keyId).split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const random = (min, max) => {
|
||||
const x = Math.sin(seed++) * 10000;
|
||||
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
const pinHeights = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
pinHeights.push(25 + random(0, 37)); // 25-62 range
|
||||
}
|
||||
|
||||
return pinHeights;
|
||||
}
|
||||
|
||||
// Function to check if a key matches a specific lock
|
||||
function doesKeyMatchLock(keyId, lockId) {
|
||||
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mapping = window.keyLockMappings[keyId];
|
||||
return mapping.lockId === lockId;
|
||||
}
|
||||
|
||||
// Function to get the lock ID that a key is assigned to
|
||||
function getKeyAssignedLock(keyId) {
|
||||
if (!window.keyLockMappings || !window.keyLockMappings[keyId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return window.keyLockMappings[keyId].lockId;
|
||||
}
|
||||
|
||||
// Console helper functions for testing
|
||||
window.reassignKeysToLocks = function() {
|
||||
// Clear existing mappings
|
||||
window.keyLockMappings = {};
|
||||
assignKeysToLocks();
|
||||
console.log('Key-lock mappings reassigned based on current scenario');
|
||||
};
|
||||
|
||||
window.showKeyLockMappings = function() {
|
||||
console.log('Current key-lock mappings:', window.keyLockMappings);
|
||||
console.log('Available lock configurations:', PREDEFINED_LOCK_CONFIGS);
|
||||
|
||||
// Show scenario-based mappings
|
||||
if (window.gameState?.scenario?.rooms) {
|
||||
console.log('Current scenario rooms:', Object.keys(window.gameState.scenario.rooms));
|
||||
}
|
||||
};
|
||||
|
||||
window.testKeyLockMatch = function(keyId, lockId) {
|
||||
const matches = doesKeyMatchLock(keyId, lockId);
|
||||
console.log(`Key "${keyId}" ${matches ? 'MATCHES' : 'DOES NOT MATCH'} lock "${lockId}"`);
|
||||
return matches;
|
||||
};
|
||||
|
||||
// Function to reinitialize mappings when scenario changes
|
||||
window.initializeKeyLockMappings = function() {
|
||||
console.log('Initializing key-lock mappings for current scenario...');
|
||||
window.keyLockMappings = {};
|
||||
assignKeysToLocks();
|
||||
};
|
||||
|
||||
// Initialize key-lock mappings when the game starts
|
||||
if (window.inventory && window.inventory.items) {
|
||||
assignKeysToLocks();
|
||||
}
|
||||
|
||||
export function setGameInstance(gameInstance) {
|
||||
gameRef = gameInstance;
|
||||
}
|
||||
@@ -462,7 +647,7 @@ function removeFromInventory(item) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnlock(lockable, type) {
|
||||
export function handleUnlock(lockable, type) {
|
||||
console.log('UNLOCK ATTEMPT');
|
||||
|
||||
// Get lock requirements based on type
|
||||
@@ -676,7 +861,7 @@ function handleUnlock(lockable, type) {
|
||||
}
|
||||
}
|
||||
|
||||
function getLockRequirementsForDoor(doorSprite) {
|
||||
export function getLockRequirementsForDoor(doorSprite) {
|
||||
const doorWorldX = doorSprite.x;
|
||||
const doorWorldY = doorSprite.y;
|
||||
|
||||
@@ -745,14 +930,8 @@ function getLockRequirementsForItem(item) {
|
||||
|
||||
function unlockTarget(lockable, type, layer) {
|
||||
if (type === 'door') {
|
||||
// After unlocking, open the door
|
||||
// Find the room that contains this door sprite
|
||||
const room = Object.values(rooms).find(r =>
|
||||
r.doorSprites && r.doorSprites.includes(lockable)
|
||||
);
|
||||
if (room) {
|
||||
openDoor(lockable, room);
|
||||
}
|
||||
// After unlocking, use the proper door unlock function
|
||||
unlockDoor(lockable);
|
||||
} else {
|
||||
// Handle item unlocking
|
||||
if (lockable.scenarioData) {
|
||||
@@ -773,13 +952,7 @@ function unlockTarget(lockable, type, layer) {
|
||||
console.log(`${type} unlocked successfully`);
|
||||
}
|
||||
|
||||
// This function is no longer needed - door unlocking is handled by the minigame system
|
||||
// and door opening is handled by openDoor()
|
||||
function unlockDoor(doorTile, doorsLayer) {
|
||||
console.log('unlockDoor called - this should not happen');
|
||||
// The unlock process is handled by the minigame system
|
||||
// When unlock is successful, unlockTarget() calls openDoor()
|
||||
}
|
||||
// Legacy unlockDoor function removed - door unlocking is now handled by doors.js
|
||||
|
||||
// Store door zones globally so we can manage them
|
||||
window.doorZones = window.doorZones || new Map();
|
||||
@@ -982,6 +1155,104 @@ function startLockpickingMinigame(lockable, scene, difficulty = 'medium', callba
|
||||
});
|
||||
}
|
||||
|
||||
// Function to generate key cuts that match a specific lock's pin configuration
|
||||
function generateKeyCutsForLock(key, lockable) {
|
||||
const keyId = key.scenarioData.key_id;
|
||||
|
||||
// Check if this key has a predefined lock assignment
|
||||
if (window.keyLockMappings && window.keyLockMappings[keyId]) {
|
||||
const mapping = window.keyLockMappings[keyId];
|
||||
const lockConfig = mapping.lockConfig;
|
||||
|
||||
console.log(`Generating cuts for key "${key.scenarioData.name}" assigned to lock "${mapping.lockId}"`);
|
||||
|
||||
// Generate cuts based on the assigned lock's pin configuration
|
||||
const cuts = [];
|
||||
const pinHeights = lockConfig.pinHeights || [];
|
||||
|
||||
for (let i = 0; i < lockConfig.pinCount; i++) {
|
||||
const keyPinLength = pinHeights[i] || 30; // Use predefined pin height
|
||||
|
||||
// Calculate cut depth with INVERSE relationship to key pin length
|
||||
// Longer key pins need shallower cuts (less lift required)
|
||||
// Shorter key pins need deeper cuts (more lift required)
|
||||
|
||||
// Based on the lockpicking minigame formula:
|
||||
// Cut depth = key pin length - gap from key blade top to shear line
|
||||
const keyBladeTop_world = 175; // Key blade top position
|
||||
const shearLine_world = 155; // Shear line position
|
||||
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
|
||||
|
||||
// Calculate the required cut depth
|
||||
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
|
||||
|
||||
// Clamp to valid range (0 to 110, which is key blade height)
|
||||
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
|
||||
|
||||
cuts.push(Math.round(clampedCutDepth));
|
||||
|
||||
console.log(`Pin ${i}: keyPinLength=${keyPinLength}, cutDepth=${clampedCutDepth} (gap=${gapFromKeyBladeTopToShearLine})`);
|
||||
}
|
||||
|
||||
console.log(`Generated cuts for key ${keyId} (assigned to ${mapping.lockId}):`, cuts);
|
||||
return cuts;
|
||||
}
|
||||
|
||||
// Fallback: Try to get the lock's pin configuration from the minigame framework
|
||||
let lockConfig = null;
|
||||
const lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
|
||||
if (window.lockConfigurations && window.lockConfigurations[lockId]) {
|
||||
lockConfig = window.lockConfigurations[lockId];
|
||||
}
|
||||
|
||||
// If no saved config, generate a default configuration
|
||||
if (!lockConfig) {
|
||||
console.log(`No predefined mapping for key ${keyId} and no saved lock configuration for ${lockId}, generating default cuts`);
|
||||
// Generate random cuts based on the key_id for consistency
|
||||
let seed = key.scenarioData.key_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const random = (min, max) => {
|
||||
const x = Math.sin(seed++) * 10000;
|
||||
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
const cuts = [];
|
||||
const numCuts = key.scenarioData.pinCount || 4;
|
||||
for (let i = 0; i < numCuts; i++) {
|
||||
cuts.push(random(20, 80)); // Random cuts between 20-80
|
||||
}
|
||||
return cuts;
|
||||
}
|
||||
|
||||
// Generate cuts based on the lock's actual pin configuration
|
||||
console.log(`Generating key cuts for lock ${lockId} with config:`, lockConfig);
|
||||
|
||||
const cuts = [];
|
||||
const pinHeights = lockConfig.pinHeights || [];
|
||||
|
||||
// Generate cuts that will work with the lock's pin heights
|
||||
for (let i = 0; i < lockConfig.pinCount; i++) {
|
||||
const keyPinLength = pinHeights[i] || (25 + Math.random() * 37.5); // Default if missing
|
||||
|
||||
// Calculate cut depth with INVERSE relationship to key pin length
|
||||
// Based on the lockpicking minigame formula:
|
||||
// Cut depth = key pin length - gap from key blade top to shear line
|
||||
const keyBladeTop_world = 175; // Key blade top position
|
||||
const shearLine_world = 155; // Shear line position
|
||||
const gapFromKeyBladeTopToShearLine = keyBladeTop_world - shearLine_world; // 20
|
||||
|
||||
// Calculate the required cut depth
|
||||
const cutDepth_needed = keyPinLength - gapFromKeyBladeTopToShearLine;
|
||||
|
||||
// Clamp to valid range (0 to 110, which is key blade height)
|
||||
const clampedCutDepth = Math.max(0, Math.min(110, cutDepth_needed));
|
||||
|
||||
cuts.push(Math.round(clampedCutDepth));
|
||||
}
|
||||
|
||||
console.log(`Generated cuts for key ${key.scenarioData.key_id}:`, cuts);
|
||||
return cuts;
|
||||
}
|
||||
|
||||
function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
|
||||
console.log('Starting key selection minigame', { playerKeys, requiredKeyId });
|
||||
|
||||
@@ -1004,37 +1275,95 @@ function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
|
||||
window.MinigameFramework.init(window.game);
|
||||
}
|
||||
|
||||
// Determine the lock ID for this lockable based on scenario data
|
||||
let lockId = null;
|
||||
|
||||
// Try to find the lock ID from the scenario data
|
||||
if (lockable.scenarioData?.requires) {
|
||||
// This is a key lock, find which key it requires
|
||||
const requiredKeyId = lockable.scenarioData.requires;
|
||||
|
||||
// Find the mapping for this key to get the lock ID
|
||||
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
|
||||
lockId = window.keyLockMappings[requiredKeyId].lockId;
|
||||
console.log(`Found lock ID "${lockId}" for key "${requiredKeyId}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default lock ID
|
||||
if (!lockId) {
|
||||
lockId = lockable.scenarioData?.lockId || lockable.id || 'default_lock';
|
||||
console.log(`Using fallback lock ID "${lockId}"`);
|
||||
}
|
||||
|
||||
// Find the key that matches this lock
|
||||
const matchingKey = playerKeys.find(key => doesKeyMatchLock(key.scenarioData.key_id, lockId));
|
||||
|
||||
let keysToShow = playerKeys;
|
||||
if (matchingKey) {
|
||||
console.log(`Found matching key "${matchingKey.scenarioData.name}" for lock "${lockId}"`);
|
||||
// For now, show all keys so player has to figure out which one works
|
||||
// In the future, you could show only the matching key or give hints
|
||||
} else {
|
||||
console.log(`No matching key found for lock "${lockId}", showing all keys`);
|
||||
}
|
||||
|
||||
// Convert inventory keys to the format expected by the minigame
|
||||
const inventoryKeys = playerKeys.map(key => {
|
||||
const inventoryKeys = keysToShow.map(key => {
|
||||
// Generate cuts data if not present
|
||||
let cuts = key.scenarioData.cuts;
|
||||
if (!cuts) {
|
||||
// Generate random cuts based on the key_id for consistency
|
||||
const seed = key.scenarioData.key_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const random = (min, max) => {
|
||||
const x = Math.sin(seed++) * 10000;
|
||||
return Math.floor((x - Math.floor(x)) * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
cuts = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
cuts.push(random(20, 80)); // Random cuts between 20-80
|
||||
}
|
||||
// Generate cuts that match the lock's pin configuration
|
||||
cuts = generateKeyCutsForLock(key, lockable);
|
||||
}
|
||||
|
||||
return {
|
||||
id: key.scenarioData.key_id,
|
||||
name: key.scenarioData.name,
|
||||
cuts: cuts,
|
||||
pinCount: key.scenarioData.pinCount || 5
|
||||
pinCount: key.scenarioData.pinCount || 4, // Default to 4 pins to match most locks
|
||||
matchesLock: doesKeyMatchLock(key.scenarioData.key_id, lockId) // Add flag for matching
|
||||
};
|
||||
});
|
||||
|
||||
// Determine which lock configuration to use for this lockable
|
||||
let lockConfig = null;
|
||||
|
||||
// First, try to find the lock configuration from scenario-based mappings
|
||||
if (lockable.scenarioData?.requires) {
|
||||
const requiredKeyId = lockable.scenarioData.requires;
|
||||
if (window.keyLockMappings && window.keyLockMappings[requiredKeyId]) {
|
||||
lockConfig = window.keyLockMappings[requiredKeyId].lockConfig;
|
||||
console.log(`Using scenario-based lock configuration for key "${requiredKeyId}":`, lockConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to predefined configurations
|
||||
if (!lockConfig && PREDEFINED_LOCK_CONFIGS[lockId]) {
|
||||
lockConfig = PREDEFINED_LOCK_CONFIGS[lockId];
|
||||
console.log(`Using predefined lock configuration for ${lockId}:`, lockConfig);
|
||||
}
|
||||
|
||||
// Final fallback to default configuration
|
||||
if (!lockConfig) {
|
||||
lockConfig = {
|
||||
id: lockId,
|
||||
pinCount: 4,
|
||||
pinHeights: [30, 28, 32, 29],
|
||||
difficulty: 'medium'
|
||||
};
|
||||
console.log(`Using default lock configuration for ${lockId}:`, lockConfig);
|
||||
}
|
||||
|
||||
// Start the key selection minigame
|
||||
window.MinigameFramework.startMinigame('lockpicking', null, {
|
||||
keyMode: true,
|
||||
skipStartingKey: true,
|
||||
lockable: lockable,
|
||||
lockId: lockId,
|
||||
pinCount: lockConfig.pinCount,
|
||||
predefinedPinHeights: lockConfig.pinHeights, // Pass the predefined pin heights
|
||||
difficulty: lockConfig.difficulty,
|
||||
cancelText: 'Close',
|
||||
onComplete: (success, result) => {
|
||||
if (success) {
|
||||
@@ -1049,9 +1378,25 @@ function startKeySelectionMinigame(lockable, type, playerKeys, requiredKeyId) {
|
||||
});
|
||||
|
||||
// Start with key selection using inventory keys
|
||||
// Wait for the minigame to be fully initialized and lock configuration to be saved
|
||||
setTimeout(() => {
|
||||
if (window.MinigameFramework.currentMinigame && window.MinigameFramework.currentMinigame.startWithKeySelection) {
|
||||
window.MinigameFramework.currentMinigame.startWithKeySelection(inventoryKeys, requiredKeyId);
|
||||
// Regenerate keys with the actual lock configuration now that it's been created
|
||||
const updatedInventoryKeys = playerKeys.map(key => {
|
||||
let cuts = key.scenarioData.cuts;
|
||||
if (!cuts) {
|
||||
cuts = generateKeyCutsForLock(key, lockable);
|
||||
}
|
||||
|
||||
return {
|
||||
id: key.scenarioData.key_id,
|
||||
name: key.scenarioData.name,
|
||||
cuts: cuts,
|
||||
pinCount: key.scenarioData.pinCount || 4
|
||||
};
|
||||
});
|
||||
|
||||
window.MinigameFramework.currentMinigame.startWithKeySelection(updatedInventoryKeys, requiredKeyId);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
@@ -1152,7 +1497,7 @@ function startDustingMinigame(item) {
|
||||
item.scene = window.game;
|
||||
|
||||
// Start the dusting minigame
|
||||
window.MinigameFramework.startMinigame('dusting', {
|
||||
window.MinigameFramework.startMinigame('dusting', null, {
|
||||
item: item,
|
||||
scene: item.scene,
|
||||
onComplete: (success, result) => {
|
||||
|
||||
280
js/systems/object-physics.js
Normal file
280
js/systems/object-physics.js
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* OBJECT PHYSICS SYSTEM
|
||||
* =====================
|
||||
*
|
||||
* Handles physics bodies, collision setup, and object behavior for chairs and other objects.
|
||||
* Separated from rooms.js for better modularity and maintainability.
|
||||
*/
|
||||
|
||||
import { TILE_SIZE } from '../utils/constants.js';
|
||||
|
||||
let gameRef = null;
|
||||
let rooms = null;
|
||||
|
||||
// Initialize object physics system
|
||||
export function initializeObjectPhysics(gameInstance, roomsRef) {
|
||||
gameRef = gameInstance;
|
||||
rooms = roomsRef;
|
||||
}
|
||||
|
||||
// Set up collision detection between chairs and other objects
|
||||
export function setupChairCollisions(chair) {
|
||||
if (!chair || !chair.body) return;
|
||||
|
||||
// Collision with other chairs
|
||||
if (window.chairs) {
|
||||
window.chairs.forEach(otherChair => {
|
||||
if (otherChair !== chair && otherChair.body) {
|
||||
gameRef.physics.add.collider(chair, otherChair);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collision with tables and other static objects
|
||||
Object.values(rooms).forEach(room => {
|
||||
if (room.objects) {
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (obj !== chair && obj.body && obj.body.immovable) {
|
||||
gameRef.physics.add.collider(chair, obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collision with wall collision boxes
|
||||
Object.values(rooms).forEach(room => {
|
||||
if (room.wallCollisionBoxes) {
|
||||
room.wallCollisionBoxes.forEach(wallBox => {
|
||||
if (wallBox.body) {
|
||||
gameRef.physics.add.collider(chair, wallBox);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collision with closed door sprites
|
||||
Object.values(rooms).forEach(room => {
|
||||
if (room.doorSprites) {
|
||||
room.doorSprites.forEach(doorSprite => {
|
||||
// Only collide with closed doors (doors that haven't been opened)
|
||||
if (doorSprite.body && doorSprite.body.immovable) {
|
||||
gameRef.physics.add.collider(chair, doorSprite);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up collisions between existing chairs and new room objects
|
||||
export function setupExistingChairsWithNewRoom(roomId) {
|
||||
if (!window.chairs) return;
|
||||
|
||||
const room = rooms[roomId];
|
||||
if (!room) return;
|
||||
|
||||
// Collision with new room's tables and static objects
|
||||
if (room.objects) {
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (obj.body && obj.body.immovable) {
|
||||
window.chairs.forEach(chair => {
|
||||
if (chair.body) {
|
||||
gameRef.physics.add.collider(chair, obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collision with new room's wall collision boxes
|
||||
if (room.wallCollisionBoxes) {
|
||||
room.wallCollisionBoxes.forEach(wallBox => {
|
||||
if (wallBox.body) {
|
||||
window.chairs.forEach(chair => {
|
||||
if (chair.body) {
|
||||
gameRef.physics.add.collider(chair, wallBox);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collision with new room's door sprites
|
||||
if (room.doorSprites) {
|
||||
room.doorSprites.forEach(doorSprite => {
|
||||
// Only collide with closed doors (doors that haven't been opened)
|
||||
if (doorSprite.body && doorSprite.body.immovable) {
|
||||
window.chairs.forEach(chair => {
|
||||
if (chair.body) {
|
||||
gameRef.physics.add.collider(chair, doorSprite);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate chair spin direction based on contact point
|
||||
export function calculateChairSpinDirection(player, chair) {
|
||||
if (!chair.isSwivelChair) return;
|
||||
|
||||
// Get relative position of player to chair SPRITE center (not collision box)
|
||||
const chairSpriteCenterX = chair.x + chair.width / 2;
|
||||
const chairSpriteCenterY = chair.y + chair.height / 2;
|
||||
const playerX = player.x + player.width / 2;
|
||||
const playerY = player.y + player.height / 2;
|
||||
|
||||
// Calculate offset from chair sprite center
|
||||
const offsetX = playerX - chairSpriteCenterX;
|
||||
const offsetY = playerY - chairSpriteCenterY;
|
||||
|
||||
// Calculate distance from center using sprite dimensions (not collision box)
|
||||
const distanceFromCenter = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
|
||||
// Use the larger sprite dimension for maxDistance to make center area larger
|
||||
const maxDistance = Math.max(chair.width, chair.height) / 2;
|
||||
const centerRatio = distanceFromCenter / maxDistance;
|
||||
|
||||
|
||||
// Determine spin based on distance from center (EXTREMELY large center area)
|
||||
if (centerRatio > 1.2) { // 120% from center - edge hit (strong spin) - ONLY VERY EDGES
|
||||
// Determine spin direction based on which side of chair player is on
|
||||
if (Math.abs(offsetX) > Math.abs(offsetY)) {
|
||||
// Horizontal contact - spin based on X offset
|
||||
chair.spinDirection = offsetX > 0 ? 1 : -1; // Right side = clockwise, left side = counter-clockwise
|
||||
} else {
|
||||
// Vertical contact - spin based on Y offset and player movement
|
||||
const playerVelocityX = player.body.velocity.x;
|
||||
if (Math.abs(playerVelocityX) > 10) {
|
||||
// Player is moving horizontally - use that for spin direction
|
||||
chair.spinDirection = playerVelocityX > 0 ? 1 : -1;
|
||||
} else {
|
||||
// Use Y offset for spin direction
|
||||
chair.spinDirection = offsetY > 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Strong spin for edge hits
|
||||
const spinIntensity = Math.min(centerRatio, 1.0);
|
||||
chair.maxRotationSpeed = 0.15 * spinIntensity;
|
||||
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.05); // Strong rotation start
|
||||
|
||||
|
||||
} else if (centerRatio > 0.8) { // 80-120% from center - moderate hit
|
||||
// Moderate spin
|
||||
if (Math.abs(offsetX) > Math.abs(offsetY)) {
|
||||
chair.spinDirection = offsetX > 0 ? 1 : -1;
|
||||
} else {
|
||||
const playerVelocityX = player.body.velocity.x;
|
||||
chair.spinDirection = Math.abs(playerVelocityX) > 10 ? (playerVelocityX > 0 ? 1 : -1) : (offsetY > 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
const spinIntensity = centerRatio * 0.3; // Reduced intensity
|
||||
chair.maxRotationSpeed = 0.06 * spinIntensity;
|
||||
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.015); // Moderate rotation start
|
||||
|
||||
|
||||
} else { // 0-80% from center - center hit (minimal spin) - MASSIVE CENTER AREA
|
||||
// Very minimal or no spin for center hits
|
||||
chair.spinDirection = 0;
|
||||
chair.maxRotationSpeed = 0.01; // Very slow spin
|
||||
chair.rotationSpeed = Math.max(chair.rotationSpeed, 0.002); // Minimal rotation start
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Update swivel chair rotation based on movement
|
||||
export function updateSwivelChairRotation() {
|
||||
if (!window.chairs) return;
|
||||
|
||||
window.chairs.forEach(chair => {
|
||||
if (!chair.hasWheels || !chair.body) return;
|
||||
|
||||
// Update chair depth based on current position (for all chairs with wheels)
|
||||
updateSpriteDepth(chair, chair.elevation || 0);
|
||||
|
||||
// Only process rotation for swivel chairs
|
||||
if (!chair.isSwivelChair) return;
|
||||
|
||||
// Calculate movement speed
|
||||
const velocity = Math.sqrt(
|
||||
chair.body.velocity.x * chair.body.velocity.x +
|
||||
chair.body.velocity.y * chair.body.velocity.y
|
||||
);
|
||||
|
||||
// Update rotation speed based on movement
|
||||
if (velocity > 10) {
|
||||
// Chair is moving - increase rotation speed (slower acceleration)
|
||||
chair.rotationSpeed = Math.min(chair.rotationSpeed + 0.01, chair.maxRotationSpeed);
|
||||
|
||||
// If no spin direction set, set a default one for testing
|
||||
if (chair.spinDirection === 0) {
|
||||
chair.spinDirection = 1; // Default to clockwise
|
||||
}
|
||||
} else {
|
||||
// Chair is slowing down - decrease rotation speed (slower deceleration)
|
||||
chair.rotationSpeed = Math.max(chair.rotationSpeed - 0.005, 0);
|
||||
|
||||
// Reset spin direction when chair stops moving
|
||||
if (chair.rotationSpeed < 0.01) {
|
||||
chair.spinDirection = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update frame based on rotation speed and direction
|
||||
if (chair.rotationSpeed > 0.01) {
|
||||
// Apply spin direction to rotation
|
||||
const rotationDelta = chair.rotationSpeed * chair.spinDirection;
|
||||
chair.currentFrame += rotationDelta;
|
||||
|
||||
// Handle frame wrapping (8 frames total: 0-7)
|
||||
if (chair.currentFrame >= 8) {
|
||||
chair.currentFrame = 0; // Loop back to first frame
|
||||
} else if (chair.currentFrame < 0) {
|
||||
chair.currentFrame = 7; // Loop back to last frame (for counter-clockwise)
|
||||
}
|
||||
|
||||
// Set the texture based on current frame and chair type
|
||||
const frameIndex = Math.floor(chair.currentFrame) + 1; // Convert to 1-based index
|
||||
let newTexture;
|
||||
|
||||
// Determine texture prefix based on original texture
|
||||
if (chair.originalTexture && chair.originalTexture.startsWith('chair-exec-rotate')) {
|
||||
newTexture = `chair-exec-rotate${frameIndex}`;
|
||||
} else if (chair.originalTexture && chair.originalTexture.startsWith('chair-white-1-rotate')) {
|
||||
newTexture = `chair-white-1-rotate${frameIndex}`;
|
||||
} else if (chair.originalTexture && chair.originalTexture.startsWith('chair-white-2-rotate')) {
|
||||
newTexture = `chair-white-2-rotate${frameIndex}`;
|
||||
} else {
|
||||
// Fallback to exec chair if original texture is unknown
|
||||
newTexture = `chair-exec-rotate${frameIndex}`;
|
||||
}
|
||||
|
||||
// Check if texture exists before setting
|
||||
if (gameRef.textures.exists(newTexture)) {
|
||||
chair.setTexture(newTexture);
|
||||
} else {
|
||||
console.warn(`Texture not found: ${newTexture}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reusable function to update sprite depth based on Y position and elevation
|
||||
export function updateSpriteDepth(sprite, elevation = 0) {
|
||||
if (!sprite || !sprite.active) return;
|
||||
|
||||
// Get the bottom of the sprite (feet position)
|
||||
const spriteBottomY = sprite.y + (sprite.height * sprite.scaleY);
|
||||
|
||||
// Calculate depth: world Y position + layer offset + elevation
|
||||
const spriteDepth = spriteBottomY + 0.5 + elevation;
|
||||
|
||||
// Set the sprite depth
|
||||
sprite.setDepth(spriteDepth);
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.setupChairCollisions = setupChairCollisions;
|
||||
window.setupExistingChairsWithNewRoom = setupExistingChairsWithNewRoom;
|
||||
window.calculateChairSpinDirection = calculateChairSpinDirection;
|
||||
window.updateSwivelChairRotation = updateSwivelChairRotation;
|
||||
window.updateSpriteDepth = updateSpriteDepth;
|
||||
268
js/systems/player-effects.js
Normal file
268
js/systems/player-effects.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* PLAYER EFFECTS SYSTEM
|
||||
* =====================
|
||||
*
|
||||
* Handles visual effects and animations triggered by player interactions.
|
||||
* Separated from rooms.js for better modularity and maintainability.
|
||||
*/
|
||||
|
||||
import { TILE_SIZE } from '../utils/constants.js';
|
||||
|
||||
let gameRef = null;
|
||||
let rooms = null;
|
||||
|
||||
// Player bump effect variables
|
||||
let playerBumpTween = null;
|
||||
let isPlayerBumping = false;
|
||||
let lastPlayerPosition = { x: 0, y: 0 };
|
||||
let steppedOverItems = new Set(); // Track items we've already stepped over
|
||||
let playerVisualOverlay = null; // Visual overlay for hop effect
|
||||
let lastHopTime = 0; // Track when last hop occurred
|
||||
const HOP_COOLDOWN = 300; // 300ms cooldown between hops
|
||||
|
||||
// Initialize player effects system
|
||||
export function initializePlayerEffects(gameInstance, roomsRef) {
|
||||
gameRef = gameInstance;
|
||||
rooms = roomsRef;
|
||||
}
|
||||
|
||||
// Function to create player bump effect when walking over items
|
||||
export function createPlayerBumpEffect() {
|
||||
if (!window.player || isPlayerBumping) return;
|
||||
|
||||
// Check cooldown to prevent double hopping
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastHopTime < HOP_COOLDOWN) {
|
||||
return; // Still in cooldown, skip this frame
|
||||
}
|
||||
|
||||
const player = window.player;
|
||||
const currentX = player.x;
|
||||
const currentY = player.y;
|
||||
|
||||
// Check if player has moved significantly (to detect stepping over items)
|
||||
const hasMoved = Math.abs(currentX - lastPlayerPosition.x) > 5 ||
|
||||
Math.abs(currentY - lastPlayerPosition.y) > 5;
|
||||
|
||||
if (!hasMoved) return;
|
||||
|
||||
// Update last position
|
||||
lastPlayerPosition = { x: currentX, y: currentY };
|
||||
|
||||
// Check all rooms for floor items
|
||||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||||
if (!room.objects) return;
|
||||
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (!obj.visible || !obj.scenarioData) return;
|
||||
|
||||
// Create unique identifier for this item
|
||||
const itemId = `${roomId}_${obj.objectId || obj.name}_${obj.x}_${obj.y}`;
|
||||
|
||||
// Skip if we've already stepped over this item recently
|
||||
if (steppedOverItems.has(itemId)) return;
|
||||
|
||||
// Check if this is a floor item (not furniture)
|
||||
const isFloorItem = obj.scenarioData.type &&
|
||||
!obj.scenarioData.type.includes('table') &&
|
||||
!obj.scenarioData.type.includes('chair') &&
|
||||
!obj.scenarioData.type.includes('desk') &&
|
||||
!obj.scenarioData.type.includes('safe') &&
|
||||
!obj.scenarioData.type.includes('workstation');
|
||||
|
||||
if (!isFloorItem) return;
|
||||
|
||||
// Check if player collision box intersects with bottom portion of item
|
||||
const playerCollisionLeft = currentX - (player.body.width / 2);
|
||||
const playerCollisionRight = currentX + (player.body.width / 2);
|
||||
const playerCollisionTop = currentY - (player.body.height / 2);
|
||||
const playerCollisionBottom = currentY + (player.body.height / 2);
|
||||
|
||||
// Focus on bottom 1/3 of the item sprite
|
||||
const itemBottomStart = obj.y + (obj.height * 2/3); // Start of bottom third
|
||||
const itemBottomEnd = obj.y + obj.height; // Bottom of item
|
||||
|
||||
const itemLeft = obj.x;
|
||||
const itemRight = obj.x + obj.width;
|
||||
|
||||
// Check if player collision box intersects with bottom third of item
|
||||
if (playerCollisionRight >= itemLeft &&
|
||||
playerCollisionLeft <= itemRight &&
|
||||
playerCollisionBottom >= itemBottomStart &&
|
||||
playerCollisionTop <= itemBottomEnd) {
|
||||
|
||||
// Player stepped over a floor item - create one-time hop effect
|
||||
steppedOverItems.add(itemId);
|
||||
lastHopTime = currentTime; // Update hop time
|
||||
|
||||
// Remove from set after 2 seconds to allow re-triggering
|
||||
setTimeout(() => {
|
||||
steppedOverItems.delete(itemId);
|
||||
}, 2000);
|
||||
|
||||
// Create one-time hop effect
|
||||
if (playerBumpTween) {
|
||||
playerBumpTween.destroy();
|
||||
}
|
||||
|
||||
isPlayerBumping = true;
|
||||
|
||||
// Create hop effect using visual overlay
|
||||
if (playerBumpTween) {
|
||||
playerBumpTween.destroy();
|
||||
}
|
||||
|
||||
// Create a visual overlay sprite that follows the player
|
||||
if (playerVisualOverlay) {
|
||||
playerVisualOverlay.destroy();
|
||||
}
|
||||
|
||||
playerVisualOverlay = gameRef.add.sprite(player.x, player.y, player.texture.key);
|
||||
playerVisualOverlay.setFrame(player.frame.name);
|
||||
playerVisualOverlay.setScale(player.scaleX, player.scaleY);
|
||||
playerVisualOverlay.setFlipX(player.flipX); // Copy horizontal flip state
|
||||
playerVisualOverlay.setFlipY(player.flipY); // Copy vertical flip state
|
||||
playerVisualOverlay.setDepth(player.depth + 1);
|
||||
playerVisualOverlay.setAlpha(0.8);
|
||||
|
||||
// Hide the original player temporarily
|
||||
player.setAlpha(0);
|
||||
|
||||
// Always hop upward - negative Y values move sprite up on screen
|
||||
const hopHeight = -15; // Consistent upward hop
|
||||
|
||||
// Debug: Log the hop details
|
||||
console.log(`Hop triggered - Player Y: ${player.y}, Overlay Y: ${playerVisualOverlay.y}, Hop Height: ${hopHeight}, Target Y: ${playerVisualOverlay.y + hopHeight}`);
|
||||
console.log(`Player movement - DeltaX: ${currentX - lastPlayerPosition.x}, DeltaY: ${currentY - lastPlayerPosition.y}`);
|
||||
|
||||
// Start the hop animation with a simple up-down motion
|
||||
playerBumpTween = gameRef.tweens.add({
|
||||
targets: { hopOffset: 0 },
|
||||
hopOffset: hopHeight,
|
||||
duration: 120,
|
||||
ease: 'Power2',
|
||||
yoyo: true,
|
||||
onUpdate: (tween) => {
|
||||
if (playerVisualOverlay && playerVisualOverlay.active) {
|
||||
// Apply the hop offset to the current player position
|
||||
playerVisualOverlay.setY(player.y + tween.getValue());
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
// Clean up overlay and restore player
|
||||
if (playerVisualOverlay) {
|
||||
playerVisualOverlay.destroy();
|
||||
playerVisualOverlay = null;
|
||||
}
|
||||
player.setAlpha(1); // Restore player visibility
|
||||
isPlayerBumping = false;
|
||||
playerBumpTween = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Make overlay follow player movement during hop
|
||||
const followPlayer = () => {
|
||||
if (playerVisualOverlay && playerVisualOverlay.active) {
|
||||
// Update X position and flip states, Y is handled by the tween
|
||||
playerVisualOverlay.setX(player.x);
|
||||
playerVisualOverlay.setFlipX(player.flipX); // Update flip state
|
||||
playerVisualOverlay.setFlipY(player.flipY); // Update flip state
|
||||
}
|
||||
};
|
||||
|
||||
// Update overlay position every frame during hop
|
||||
const followInterval = setInterval(() => {
|
||||
if (!playerVisualOverlay || !playerVisualOverlay.active) {
|
||||
clearInterval(followInterval);
|
||||
return;
|
||||
}
|
||||
followPlayer();
|
||||
}, 16); // ~60fps
|
||||
|
||||
// Clean up interval when hop completes
|
||||
setTimeout(() => {
|
||||
clearInterval(followInterval);
|
||||
}, 240); // Slightly longer than animation duration
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create plant sway effect when player walks through
|
||||
export function createPlantSwayEffect() {
|
||||
if (!window.player) return;
|
||||
|
||||
const player = window.player;
|
||||
const currentX = player.x;
|
||||
const currentY = player.y;
|
||||
|
||||
// Check if player is moving (has velocity)
|
||||
const isMoving = Math.abs(player.body.velocity.x) > 10 || Math.abs(player.body.velocity.y) > 10;
|
||||
if (!isMoving) return;
|
||||
|
||||
// Check all rooms for plants
|
||||
Object.entries(rooms).forEach(([roomId, room]) => {
|
||||
if (!room.objects) return;
|
||||
|
||||
Object.values(room.objects).forEach(obj => {
|
||||
if (!obj.visible || !obj.canSway) return;
|
||||
|
||||
// Check if player is near the plant (within 40 pixels)
|
||||
const distance = Phaser.Math.Distance.Between(currentX, currentY, obj.x + obj.width/2, obj.y + obj.height/2);
|
||||
|
||||
if (distance < 40 && !obj.isSwaying) {
|
||||
obj.isSwaying = true;
|
||||
|
||||
// Create sway effect using displacement FX
|
||||
// This creates a realistic distortion effect while keeping the base stationary
|
||||
const swayIntensity = 0.05; // Increased intensity for more dramatic motion
|
||||
const swayDuration = Phaser.Math.Between(400, 600); // Half the time - much faster animation
|
||||
|
||||
// Calculate sway direction based on player position relative to plant
|
||||
const playerDirection = currentX > obj.x + obj.width/2 ? 1 : -1;
|
||||
const displacementX = playerDirection * swayIntensity;
|
||||
const displacementY = (Math.random() - 0.5) * swayIntensity * 0.8; // More vertical movement
|
||||
|
||||
// Create a complex sway animation using displacement
|
||||
const swayTween = gameRef.tweens.add({
|
||||
targets: obj.displacementFX,
|
||||
x: displacementX,
|
||||
y: displacementY,
|
||||
duration: swayDuration / 3,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
onComplete: () => {
|
||||
// Second sway phase with opposite direction
|
||||
gameRef.tweens.add({
|
||||
targets: obj.displacementFX,
|
||||
x: -displacementX * 0.8, // More dramatic opposite movement
|
||||
y: -displacementY * 0.8,
|
||||
duration: swayDuration / 3,
|
||||
ease: 'Sine.easeInOut',
|
||||
yoyo: true,
|
||||
onComplete: () => {
|
||||
// Final settle phase - return to original state
|
||||
gameRef.tweens.add({
|
||||
targets: obj.displacementFX,
|
||||
x: 0.01, // Slightly higher default displacement
|
||||
y: 0.01, // Slightly higher default displacement
|
||||
duration: swayDuration / 3,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => {
|
||||
obj.isSwaying = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Plant ${obj.name} swaying with intensity ${swayIntensity}, direction ${playerDirection}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.createPlayerBumpEffect = createPlayerBumpEffect;
|
||||
window.createPlantSwayEffect = createPlantSwayEffect;
|
||||
Reference in New Issue
Block a user