16 KiB
Room Loading System Design
Overview
The room loading system in BreakEscape coordinates two distinct data sources to create a complete room experience:
- Scenario JSON Files (e.g.,
ceo_exfil.json) - Define game logic, item properties, and game state - Tiled Map JSON Files (e.g.,
room_reception2.json) - Define visual layout, sprite positions, and room structure
This document explains how these systems work together to load and render rooms.
Architecture Overview
Data Flow Diagram
┌─────────────────────────────────────────────────────────────────┐
│ Scenario JSON │
│ (rooms → room data → objects with properties) │
└──────────────┬──────────────────────────────────────────────────┘
│ Contains: name, type, takeable, readable, etc.
│
▼
┌──────────────────────┐
│ Matching Algorithm │
│ (Type-based lookup) │
└──────────┬───────────┘
│
┌──────────┴──────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ Tiled Map Items │ │ Scene Objects │
│ (Position & Sprite)│ │ (Properties) │
└─────────────────────┘ └──────────────────┘
│ │
│ Merge Properties │
└─────────────┬───────┘
▼
┌──────────────────────┐
│ Final Game Object │
│ (Position + Props) │
└──────────────────────┘
Room Loading Process
1. Initialization Phase
When the game starts, the following steps occur:
- Scenario Loading:
window.gameScenariois populated with scenario data from the selected scenario JSON file - Tilemap Preloading: Tiled map files are preloaded in the Phaser scene's
preload()function - Room Position Calculation: Room positions are calculated based on connections and layout
2. Lazy Loading
Rooms are loaded on-demand when:
- The player moves close to an adjacent room (detected via door sprite proximity)
- Eventually, this will be determined by a remote API request
function loadRoom(roomId) {
const gameScenario = window.gameScenario;
const roomData = gameScenario.rooms[roomId];
const position = window.roomPositions[roomId];
createRoom(roomId, roomData, position);
revealRoom(roomId);
}
3. Room Creation Phase
The createRoom() function orchestrates the complete room setup:
Step 3a: Load Tilemap
- Create a Phaser tilemap from the preloaded Tiled JSON data
- Add tileset images to the map
- Initialize room data structure:
rooms[roomId]
Step 3b: Create Tile Layers
- Iterate through all layers in the Tiled map (floor, walls, collision, etc.)
- Create sprite layers for each, positioned at the room's world coordinates
- Set depth values based on the Depth Layering Philosophy
- Skip the "doors" layer (handled by sprite-based doors system)
Step 3c: Create Door Sprites
- Parse scenario room connections
- Create interactive door sprites at appropriate positions
- Doors serve as transition triggers to adjacent rooms
Step 3d: Process Tiled Object Layers
The system processes five object layers from the Tiled map:
- tables - Static table/furniture objects that don't move
- table_items - Items placed on tables (phones, keyboards, etc.)
- conditional_items - Items in the main space that may be scenario-specific
- conditional_table_items - Table items that may be scenario-specific
- items - Regular items in the room (plants, chairs, etc.)
Step 3e: Match and Merge Objects
This is the critical matching phase:
- Collect Available Sprites: Extract all objects from Tiled layers, organized by type
- Process Scenario Objects: For each object defined in the scenario:
- Extract the object type (e.g., "key", "notes", "phone")
- Search for matching visual representation in this priority:
- Regular items layer (items)
- Conditional items layer (conditional_items)
- Conditional table items layer (conditional_table_items)
- Merge Properties: Apply scenario properties to the matched sprite
- Mark as Used: Track which Tiled items have been consumed
- Process Remaining Sprites: Create sprites for unused Tiled items with default properties
Matching Algorithm
Type-Based Matching
The system uses a type-based matching approach where each scenario object is matched to a Tiled sprite by type:
Scenario Object: { type: "key", name: "Office Key", takeable: true, ... }
▼
Search for matching type
▼
Tiled Item: { gid: 243, imageName: "key", x: 100, y: 150 }
▼
Match Found! Merge:
▼
Final Object: {
imageName: "key",
x: 100, y: 150, // Position from Tiled
name: "Office Key", // Name from Scenario
takeable: true, // Properties from Scenario
observations: "..."
}
Image Name Extraction
The system extracts the base type from Tiled object image names:
function extractBaseTypeFromImageName(imageName) {
// Examples:
// "key.png" → "key"
// "phone5.png" → "phone"
// "notes3.png" → "notes"
// "plant-large1.png" → "plant"
}
Matching Priority
When looking for a Tiled sprite to match a scenario object:
- Regular Items Layer - First choice (most commonly used items)
- Conditional Items Layer - For items that might not always be present
- Conditional Table Items Layer - For table-specific scenario items
This priority allows flexibility in where visual assets are placed while maintaining predictable matching behavior.
Object Layer Details
Table Structure (From Tiled)
Purpose: Define base furniture objects (desks, tables, etc.)
{
"gid": 118,
"height": 47,
"name": "",
"rotation": 0,
"type": "",
"visible": true,
"width": 174,
"x": 75.67,
"y": 89.67
}
Processing:
- Tables are processed first to establish base positions
- Groups are created for table + table_items organization
- Tables act as anchor points for table items
Table Items Structure (From Tiled)
Purpose: Items that should visually appear on or near tables
{
"gid": 358,
"height": 23,
"name": "",
"x": 86,
"y": 64.5
}
Processing:
- Grouped with their closest table
- Set to same depth as table + slight offset for proper ordering
- Sorted north-to-south (lower Y values first)
Conditional Items Structure (From Tiled)
Purpose: Items that appear conditionally based on scenario
{
"gid": 227,
"name": "",
"x": 13.5,
"y": 51
}
Processing:
- Available for scenario matching
- Only rendered if a scenario object matches them
- Otherwise ignored (not rendered in the room)
Items Structure (From Tiled)
Purpose: Always-present background objects (plants, chairs, etc.)
{
"gid": 176,
"height": 21,
"name": "",
"x": 197.67,
"y": 45.67
}
Processing:
- Most numerous layer
- Rendered unless consumed by scenario matching
- Provide visual richness to the room
Depth Layering Philosophy
All depth calculations use: World Y Position + Layer Offset
Room Layers
Depth Priority (lowest to highest):
1. Floor: roomWorldY + 0.1
2. Collision: roomWorldY + 0.15
3. Walls: roomWorldY + 0.2
4. Props: roomWorldY + 0.3
5. Other: roomWorldY + 0.4
Interactive Elements
Depth Priority (lowest to highest):
1. Doors: doorY + 0.45
2. Door Tops: doorY + 0.55
3. Animated Doors: doorBottomY + 0.45
4. Animated Door Tops: doorBottomY + 0.55
5. Player: playerBottomY + 0.5
6. Objects: objectBottomY + 0.5
Key Principle: The deeper (higher Y position) an object is in the room, the higher its depth value, ensuring natural layering.
Property Application Flow
When a scenario object is matched to a Tiled sprite:
// 1. Find matching Tiled sprite
const usedItem = regularItemsByType[scenarioObj.type].shift();
// 2. Create sprite at Tiled position
const sprite = gameRef.add.sprite(
Math.round(position.x + usedItem.x),
Math.round(position.y + usedItem.y - usedItem.height),
imageName
);
// 3. Apply scenario properties
sprite.scenarioData = scenarioObj;
sprite.interactable = true;
sprite.name = scenarioObj.name;
sprite.objectId = `${roomId}_${scenarioObj.type}_${index}`;
// 4. Apply visual properties from Tiled
if (usedItem.rotation) {
sprite.setRotation(Phaser.Math.DegToRad(usedItem.rotation));
}
// 5. Set depth and elevation
const objectBottomY = sprite.y + sprite.height;
const objectDepth = objectBottomY + 0.5 + elevation;
sprite.setDepth(objectDepth);
Handling Missing Matches
If a scenario object has no matching Tiled sprite:
- Create sprite at a random valid position in the room
- Use the object type as the sprite name
- Apply all scenario properties normally
- Log a warning for debugging
Fallback Position Logic:
- Generate random coordinates within the room bounds
- Exclude padding areas (edge of room)
- Verify no overlap with existing objects
- Maximum 50 attempts before placement
Room Visibility and Rendering
Visibility State
- Hidden Initially: All room elements are created but hidden (
setVisible(false)) - Revealed on Load: When
revealRoom()is called, elements become visible - Controlled Updates: Visibility changes based on player proximity and game state
Room Reveal Logic
function revealRoom(roomId) {
const room = rooms[roomId];
// Show all layers
Object.values(room.layers).forEach(layer => {
layer.setVisible(true);
layer.setAlpha(1);
});
// Show all objects
Object.values(room.objects).forEach(obj => {
obj.setVisible(true);
});
// Show door sprites
room.doorSprites.forEach(door => {
door.setVisible(true);
});
}
Item Tracking and De-duplication
The system prevents the same visual sprite from being used twice through the usedItems Set:
const usedItems = new Set();
// After using a sprite:
usedItems.add(imageName); // Full image name
usedItems.add(baseType); // Base type (key, phone, etc.)
// Before processing a Tiled sprite:
if (usedItems.has(imageName) || usedItems.has(baseType)) {
// Skip this sprite - already used
continue;
}
Example: Complete Scenario Object Processing
Input: Scenario Definition
{
"type": "key",
"name": "Office Key",
"takeable": true,
"key_id": "office1_key:40,35,38,32,10",
"observations": "A key to access the office areas"
}
Input: Tiled Map Layer
{
"name": "items",
"objects": [
{
"gid": 243,
"height": 21,
"width": 12,
"x": 100,
"y": 150
}
]
}
Processing Steps
- Extract Type:
scenarioObj.type = "key" - Extract Image:
getImageNameFromObject(tiledObj)→"key" - Match: Find tiled object with base type "key"
- Create Sprite: At position (100, 150) with image "key.png"
- Merge Data:
- Position: (100, 150) ← from Tiled
- Visual: "key.png" ← from Tiled
- Name: "Office Key" ← from Scenario
- Properties: takeable, key_id, observations ← from Scenario
- Set Depth: Based on Y position and room layout
- Store: In
rooms[roomId].objects[objectId]
Output: Interactive Game Object
{
x: 100,
y: 150,
sprite: "key.png",
name: "Office Key",
type: "key",
takeable: true,
key_id: "office1_key:40,35,38,32,10",
observations: "A key to access the office areas",
interactive: true,
scenarioData: {...}
}
Collision and Physics
Wall Collision
- Walls layer defines immovable boundaries
- Thin collision boxes created for each wall tile
- Player cannot pass through walls
Door Transitions
- Door sprites detect player proximity
- When player is close enough,
loadRoom()is triggered - Adjacent room is loaded and revealed
Object Interactions
- Interactive objects are clickable
- Interaction radius is defined by
INTERACTION_RANGE_SQ - Objects trigger appropriate minigames or dialogs
Constants and Configuration
Key Constants (from js/utils/constants.js)
const TILE_SIZE = 32; // Base tile size in pixels
const DOOR_ALIGN_OVERLAP = 64; // Door alignment overlap
const GRID_SIZE = 32; // Grid size for pathfinding
const INTERACTION_RANGE_SQ = 5000; // Squared interaction range
const INTERACTION_CHECK_INTERVAL = 100; // Check interval in ms
Object Scales (from js/core/rooms.js)
const OBJECT_SCALES = {
'notes': 0.75,
'key': 0.75,
'phone': 1,
'tablet': 0.75,
'bluetooth_scanner': 0.7
};
Performance Considerations
Lazy Loading Benefits
- Only rooms near the player are loaded
- Reduces memory usage and draw calls
- Faster initial game load time
Optimization Strategies
- Layer Caching: Tile layers are only created once per room
- Sprite Pooling: Reuse sprites when possible (future optimization)
- Depth Sorting: Calculated once at load time, updated when needed
- Visibility Culling: Rooms far from player are not rendered
Debugging and Logging
The system provides comprehensive console logging:
console.log(`Creating room ${roomId} of type ${roomData.type}`);
console.log(`Collected ${layerName} layer with ${objects.length} objects`);
console.log(`Created ${objType} using ${imageName}`);
console.log(`Applied scenario data to ${objType}:`, scenarioObj);
Enable the browser console to see detailed room loading information.
API Reference
Main Functions
loadRoom(roomId)
- Loads a room from the scenario and Tiled map
- Called by door transition system
- Parameters:
roomId(string)
createRoom(roomId, roomData, position)
- Creates all room elements (layers, objects, doors)
- Coordinates the complete room setup
- Parameters:
roomId,roomData(scenario),position{x, y}
revealRoom(roomId)
- Makes room elements visible to the player
- Called after room creation completes
- Parameters:
roomId(string)
processScenarioObjectsWithConditionalMatching(roomId, position, objectsByLayer)
- Internal: Matches scenario objects to Tiled sprites
- Returns: Set of used item identifiers
Future Improvements
- Remote Room Loading: Replace local scenario with API calls
- Dynamic Item Placement: Algorithm-based positioning instead of Tiled layer placement
- Item Pooling: Reuse sprite objects for better performance
- Streaming LOD: Load distant rooms at reduced detail
- Narrative-based Visibility: Show/hide items based on story state
Related Files
- Scenario Format: See
README_scenario_design.md - Tiled Map Format: Tiled Editor documentation
- Game State:
js/systems/inventory.js,js/systems/interactions.js - Visual Rendering:
js/systems/object-physics.js,js/systems/player-effects.js