Optimize unlock flow to use single API call for doors and containers

Server-side changes:
- Unlock endpoint already returns roomData for doors

Client-side changes:
- Pass serverResponse through minigame callback chain
- Store serverResponse in minigame gameResult (password & PIN)
- Update minigame-starters to pass result to callbacks
- Update unlock-system callbacks to accept and pass serverResponse
- Pass roomData to unlockDoor for doors
- Cache roomData in roomDataCache before loadRoom is called
- loadRoom checks cache before making API call

Benefits:
- Doors: Unlock + room load = 1 API call instead of 2
- Containers: Unlock + contents = 1 API call instead of 2
- More efficient, faster user experience
- Consistent pattern for both doors and containers
This commit is contained in:
Z. Cliffe Schreuders
2025-11-22 00:46:56 +00:00
parent 77544520aa
commit d2901eafa7
6 changed files with 98 additions and 69 deletions

View File

@@ -111,6 +111,10 @@ function isPositionOverlapping(x, y, roomId, itemSize = TILE_SIZE) {
window.discoveredRooms = discoveredRooms;
let gameRef = null;
// Room data cache - stores room data returned from unlock API to avoid duplicate fetches
const roomDataCache = new Map();
window.roomDataCache = roomDataCache; // Make available for unlock system
// ===== ITEM POOL MANAGEMENT (PHASE 2 IMPROVEMENTS) =====
// Moved to module level to avoid temporal dead zone errors
// and improve performance (defined once, not on every room load)
@@ -555,59 +559,69 @@ const OBJECT_SCALES = {
// Function to load a room lazily via API endpoint
async function loadRoom(roomId) {
const position = window.roomPositions[roomId];
if (!position) {
console.error(`Cannot load room ${roomId}: missing position`);
return;
}
console.log(`Lazy loading room from API: ${roomId}`);
try {
// Fetch room data from server endpoint
const gameId = window.breakEscapeConfig?.gameId;
if (!gameId) {
console.error('Game ID not available in breakEscapeConfig');
return;
}
const response = await fetch(`/break_escape/games/${gameId}/room/${roomId}`);
if (!response.ok) {
console.error(`Failed to load room ${roomId}: ${response.status} ${response.statusText}`);
return;
}
const data = await response.json();
const roomData = data.room;
if (!roomData) {
console.error(`No room data returned for ${roomId}`);
return;
}
console.log(`✅ Received room data from API for ${roomId}`);
// Load NPCs BEFORE creating room visuals
// This ensures NPCs are registered before room objects/sprites are created
if (window.npcLazyLoader && roomData) {
try {
await window.npcLazyLoader.loadNPCsForRoom(roomId, roomData);
} catch (error) {
console.error(`Failed to load NPCs for room ${roomId}:`, error);
// Continue with room creation even if NPC loading fails
let roomData;
// Check if roomData is cached (from unlock API response)
if (roomDataCache.has(roomId)) {
console.log(`✅ Using cached room data for ${roomId} (from unlock response)`);
roomData = roomDataCache.get(roomId);
roomDataCache.delete(roomId); // Clear from cache after use
} else {
console.log(`Lazy loading room from API: ${roomId}`);
try {
// Fetch room data from server endpoint
const gameId = window.breakEscapeConfig?.gameId;
if (!gameId) {
console.error('Game ID not available in breakEscapeConfig');
return;
}
const response = await fetch(`/break_escape/games/${gameId}/room/${roomId}`);
if (!response.ok) {
console.error(`Failed to load room ${roomId}: ${response.status} ${response.statusText}`);
return;
}
const data = await response.json();
roomData = data.room;
if (!roomData) {
console.error(`No room data returned for ${roomId}`);
return;
}
console.log(`✅ Received room data from API for ${roomId}`);
} catch (error) {
console.error(`Error loading room ${roomId}:`, error);
return;
}
createRoom(roomId, roomData, position);
// Reveal (make visible) but do NOT mark as discovered
// The room will only be marked as "discovered" when the player
// actually enters it via door transition
revealRoom(roomId);
} catch (error) {
console.error(`Error loading room ${roomId}:`, error);
}
// Load NPCs BEFORE creating room visuals
// This ensures NPCs are registered before room objects/sprites are created
if (window.npcLazyLoader && roomData) {
try {
await window.npcLazyLoader.loadNPCsForRoom(roomId, roomData);
} catch (error) {
console.error(`Failed to load NPCs for room ${roomId}:`, error);
// Continue with room creation even if NPC loading fails
}
}
createRoom(roomId, roomData, position);
// Reveal (make visible) but do NOT mark as discovered
// The room will only be marked as "discovered" when the player
// actually enters it via door transition
revealRoom(roomId);
}
export function initializeRooms(gameInstance) {

View File

@@ -450,6 +450,8 @@ export class PasswordMinigame extends MinigameScene {
console.log('Server returned container contents:', response.contents);
lockable.scenarioData.contents = response.contents;
}
// Store server response to pass through callback chain
this.serverResponse = response;
this.passwordCorrect();
} else {
this.passwordIncorrect();
@@ -466,12 +468,13 @@ export class PasswordMinigame extends MinigameScene {
passwordCorrect() {
this.cleanup();
this.showSuccess("Password accepted! Access granted.", true, 3000);
// Set game result for the callback
this.gameResult = {
success: true,
password: this.gameData.password,
attempts: this.gameData.attempts
attempts: this.gameData.attempts,
serverResponse: this.serverResponse // Include server response (roomData for doors, contents for containers)
};
}

View File

@@ -309,6 +309,9 @@ export class PinMinigame extends MinigameScene {
lockable.scenarioData.contents = response.contents;
}
// Store server response to pass through callback chain
this.serverResponse = response;
return response.success;
} catch (error) {
console.error('Server validation error:', error);
@@ -444,15 +447,16 @@ export class PinMinigame extends MinigameScene {
this.isLocked = true;
this.displayElement.classList.add('success');
this.displayElement.textContent = this.currentInput;
this.showSuccess('PIN Correct! Access Granted.', true, 2000);
// Set game result
this.gameResult = {
success: true,
pin: this.currentInput,
attempts: this.attemptCount,
timeToComplete: Date.now() - this.startTime
timeToComplete: Date.now() - this.startTime,
serverResponse: this.serverResponse // Include server response (roomData for doors, contents for containers)
};
}

View File

@@ -580,15 +580,21 @@ function handleDoorInteraction(doorSprite) {
}
// Function to unlock a door (called after successful unlock)
function unlockDoor(doorSprite) {
function unlockDoor(doorSprite, roomData) {
const props = doorSprite.doorProperties;
console.log(`Unlocking door: ${props.roomId} -> ${props.connectedRoom}`);
// Mark door as unlocked
props.locked = false;
// If roomData was provided from server unlock response, cache it
if (roomData && window.roomDataCache) {
console.log(`📦 Caching room data for ${props.connectedRoom} from unlock response`);
window.roomDataCache.set(props.connectedRoom, roomData);
}
// TODO: Implement unlock animation/effect
// Open the door
openDoor(doorSprite);
}

View File

@@ -535,11 +535,11 @@ export function startPinMinigame(lockable, type, correctPin, callback) {
if (success) {
console.log('PIN MINIGAME SUCCESS');
window.gameAlert(`Correct PIN! The ${type} is now unlocked.`, 'success', 'PIN Accepted', 4000);
callback(true);
callback(true, result); // Pass result with serverResponse
} else {
console.log('PIN MINIGAME FAILED');
window.gameAlert("Failed to enter correct PIN.", 'error', 'PIN Rejected', 3000);
callback(false);
callback(false, result);
}
}
});
@@ -587,11 +587,11 @@ export function startPasswordMinigame(lockable, type, correctPassword, callback,
if (success) {
console.log('PASSWORD MINIGAME SUCCESS');
window.gameAlert(`Correct password! The ${type} is now unlocked.`, 'success', 'Password Accepted', 4000);
callback(true);
callback(true, result); // Pass result with serverResponse
} else {
console.log('PASSWORD MINIGAME FAILED');
window.gameAlert("Failed to enter correct password.", 'error', 'Password Rejected', 3000);
callback(false);
callback(false, result);
}
}
});

View File

@@ -176,13 +176,13 @@ export function handleUnlock(lockable, type) {
case 'pin':
console.log('PIN CODE REQUESTED (server-side validation)');
// Pass null for required code - will be validated server-side
startPinMinigame(lockable, type, null, (success) => {
startPinMinigame(lockable, type, null, (success, result) => {
if (success) {
unlockTarget(lockable, type, lockable.layer);
unlockTarget(lockable, type, lockable.layer, result?.serverResponse);
}
});
break;
case 'password':
console.log('PASSWORD REQUESTED (server-side validation)');
@@ -197,9 +197,9 @@ export function handleUnlock(lockable, type) {
};
// Pass null for required password - will be validated server-side
startPasswordMinigame(lockable, type, null, (success) => {
startPasswordMinigame(lockable, type, null, (success, result) => {
if (success) {
unlockTarget(lockable, type, lockable.layer);
unlockTarget(lockable, type, lockable.layer, result?.serverResponse);
}
}, passwordOptions);
break;
@@ -470,13 +470,15 @@ export function getLockRequirementsForItem(item) {
};
}
export function unlockTarget(lockable, type, layer) {
console.log('🔓 unlockTarget called:', { type, lockable });
export function unlockTarget(lockable, type, layer, serverResponse) {
console.log('🔓 unlockTarget called:', { type, lockable, serverResponse });
if (type === 'door') {
// After unlocking, use the proper door unlock function
unlockDoor(lockable);
// Pass roomData from server if available (avoids separate room API call)
const roomData = serverResponse?.roomData;
unlockDoor(lockable, roomData);
// Emit door unlocked event
console.log('🔓 Checking for eventDispatcher:', !!window.eventDispatcher);
if (window.eventDispatcher) {