Add Room Loading Documentation: Introduce comprehensive documentation for the room loading system, detailing the architecture, data flow, and matching algorithm. Include proposed improvements for refactoring, implementation plans, and examples for better maintainability and testability. Create a summary file to provide an overview of the documentation structure and key concepts.

This commit is contained in:
Z. Cliffe Schreuders
2025-10-22 00:32:00 +01:00
parent 48b1ad3bbf
commit 90da3cdb5e
3 changed files with 1441 additions and 0 deletions

574
README_ROOM_LOADING.md Normal file
View File

@@ -0,0 +1,574 @@
# Room Loading System Design
## Overview
The room loading system in BreakEscape coordinates two distinct data sources to create a complete room experience:
1. **Scenario JSON Files** (e.g., `ceo_exfil.json`) - Define game logic, item properties, and game state
2. **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:
1. **Scenario Loading**: `window.gameScenario` is populated with scenario data from the selected scenario JSON file
2. **Tilemap Preloading**: Tiled map files are preloaded in the Phaser scene's `preload()` function
3. **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
```javascript
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**:
1. **Collect Available Sprites**: Extract all objects from Tiled layers, organized by type
2. **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:
1. Regular items layer (items)
2. Conditional items layer (conditional_items)
3. 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
3. **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:
```javascript
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:
1. **Regular Items Layer** - First choice (most commonly used items)
2. **Conditional Items Layer** - For items that might not always be present
3. **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.)
```json
{
"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
```json
{
"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
```json
{
"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.)
```json
{
"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:
```javascript
// 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:
1. Create sprite at a **random valid position** in the room
2. Use the object type as the sprite name
3. Apply all scenario properties normally
4. 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
1. **Hidden Initially**: All room elements are created but hidden (`setVisible(false)`)
2. **Revealed on Load**: When `revealRoom()` is called, elements become visible
3. **Controlled Updates**: Visibility changes based on player proximity and game state
### Room Reveal Logic
```javascript
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:
```javascript
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
```json
{
"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
```json
{
"name": "items",
"objects": [
{
"gid": 243,
"height": 21,
"width": 12,
"x": 100,
"y": 150
}
]
}
```
### Processing Steps
1. **Extract Type**: `scenarioObj.type = "key"`
2. **Extract Image**: `getImageNameFromObject(tiledObj)``"key"`
3. **Match**: Find tiled object with base type "key"
4. **Create Sprite**: At position (100, 150) with image "key.png"
5. **Merge Data**:
- Position: (100, 150) ← from Tiled
- Visual: "key.png" ← from Tiled
- Name: "Office Key" ← from Scenario
- Properties: takeable, key_id, observations ← from Scenario
6. **Set Depth**: Based on Y position and room layout
7. **Store**: In `rooms[roomId].objects[objectId]`
### Output: Interactive Game Object
```javascript
{
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`)
```javascript
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`)
```javascript
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
1. **Layer Caching**: Tile layers are only created once per room
2. **Sprite Pooling**: Reuse sprites when possible (future optimization)
3. **Depth Sorting**: Calculated once at load time, updated when needed
4. **Visibility Culling**: Rooms far from player are not rendered
---
## Debugging and Logging
The system provides comprehensive console logging:
```javascript
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
1. **Remote Room Loading**: Replace local scenario with API calls
2. **Dynamic Item Placement**: Algorithm-based positioning instead of Tiled layer placement
3. **Item Pooling**: Reuse sprite objects for better performance
4. **Streaming LOD**: Load distant rooms at reduced detail
5. **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`

View File

@@ -0,0 +1,525 @@
# Room Loading System - Proposed Improvements
## Executive Summary
The current room loading system successfully coordinates Scenario JSON and Tiled Map data. However, there is an opportunity to **refactor the matching algorithm** to be more explicit and maintainable by:
1. **Centralize item matching logic** into a dedicated matching function
2. **Unify the approach** for all item types (regular items, conditional items, table items)
3. **Improve separation of concerns** between visual (Tiled) and logical (Scenario) data
4. **Make the system more testable** with clear input/output contracts
---
## Current Approach Analysis
### Strengths
**Type-based matching works well**: Objects are matched by type (key, phone, notes, etc.)
**De-duplication system prevents duplicate visuals**: Used items are tracked effectively
**Fallback handling**: Missing matches get random placement
**Property merging**: Scenario data is applied to matched sprites
### Current Flow
```
Scenario Objects → Type Lookup → Find Tiled Item → Create Sprite → Apply Properties
(scattered) (in function) (inline) (inline)
```
### Challenges
**Matching logic is embedded** in `processScenarioObjectsWithConditionalMatching()`
**Three separate item maps** (regular, conditional, conditional_table) managed manually
**Hard to test** matching logic in isolation
**Order-dependent**: Items are removed from arrays with `.shift()`
**Limited filtering**: Only supports type matching, no additional criteria
---
## Proposed Improved Approach
### New Architecture
```
┌──────────────────────────────┐
│ Scenario Objects │
│ (what should be present) │
└────────────────┬─────────────┘
┌────────────────────────────┐
│ Item Matching Engine │
│ (Centralized Logic) │
│ │
│ 1. Search all layers │
│ 2. Match by criteria │
│ 3. Reserve item │
│ 4. Return match │
└────────────┬───────────────┘
┌───────┴────────┐
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Tiled Items │ │ Match Result │
│ (Position & │ │ (Item + Type)│
│ Sprite) │ └──────────────┘
└─────────────┘ │
┌──────────────────┐
│ Create Sprite │
│ (Position from │
│ Tiled) │
└────────┬─────────┘
┌──────────────────┐
│ Apply Properties │
│ (Data from │
│ Scenario) │
└──────────────────┘
```
### Key Improvements
1. **Centralized Matching Function**
- Single source of truth for matching logic
- Clear responsibility: find best matching Tiled item for scenario object
- Easier to debug and modify
2. **Item Pool Management**
- Unified data structure for all items (regular + conditional)
- Track availability explicitly
- Reserve items instead of consuming with `.shift()`
3. **Clear Separation**
- Matching: Find the visual representation
- Merging: Combine visual (Tiled) + logical (Scenario) data
- Rendering: Display the result
---
## Implementation Plan
### Step 1: Create Item Matching Module
Create a new function `matchScenarioObjectToTiledItem()`:
```javascript
/**
* Matches a scenario object to an available Tiled item sprite
*
* @param {Object} scenarioObj - The scenario object definition
* @param {Object} availableItems - Collections of available Tiled items
* @param {Set} reservedItems - Items already matched/reserved
* @returns {Object|null} Matched Tiled item or null if no match found
*
* @example
* const match = matchScenarioObjectToTiledItem(
* { type: 'key', name: 'Office Key' },
* { items: [...], conditionalItems: [...], tableItems: [...] },
* reservedItemsSet
* );
* // Returns: { gid: 243, x: 100, y: 150, imageName: 'key' }
*/
function matchScenarioObjectToTiledItem(
scenarioObj,
availableItems,
reservedItems
) {
const searchType = scenarioObj.type;
// Search priority: regular items → conditional items → table items
const searchLayers = [
{ name: 'items', items: availableItems.items || [] },
{ name: 'conditionalItems', items: availableItems.conditionalItems || [] },
{ name: 'conditionalTableItems', items: availableItems.conditionalTableItems || [] }
];
for (const layer of searchLayers) {
for (const tiledItem of layer.items) {
// Skip if already reserved
const itemId = getItemIdentifier(tiledItem);
if (reservedItems.has(itemId)) {
continue;
}
// Extract type from image name
const imageName = getImageNameFromObject(tiledItem);
const baseType = extractBaseTypeFromImageName(imageName);
// Match by type
if (baseType === searchType) {
return {
tiledItem,
imageName,
baseType,
layer: layer.name
};
}
}
}
return null;
}
```
### Step 2: Create Item Pool Manager
```javascript
/**
* Manages the collection of available Tiled items
*/
class TiledItemPool {
constructor(objectsByLayer, map) {
this.items = {};
this.conditionalItems = {};
this.conditionalTableItems = {};
this.reserved = new Set();
this.populateFromLayers(objectsByLayer);
}
populateFromLayers(objectsByLayer) {
this.items = this.indexByType(objectsByLayer.items || []);
this.conditionalItems = this.indexByType(objectsByLayer.conditional_items || []);
this.conditionalTableItems = this.indexByType(objectsByLayer.conditional_table_items || []);
}
indexByType(items) {
const indexed = {};
items.forEach(item => {
const imageName = getImageNameFromObject(item);
const baseType = extractBaseTypeFromImageName(imageName);
if (!indexed[baseType]) {
indexed[baseType] = [];
}
indexed[baseType].push(item);
});
return indexed;
}
findMatchFor(scenarioObj) {
const searchType = scenarioObj.type;
// Try each layer in priority order
for (const indexedItems of [this.items, this.conditionalItems, this.conditionalTableItems]) {
const candidates = indexedItems[searchType] || [];
for (const item of candidates) {
const itemId = getItemIdentifier(item);
if (!this.reserved.has(itemId)) {
return item;
}
}
}
return null;
}
reserve(tiledItem) {
const itemId = getItemIdentifier(tiledItem);
this.reserved.add(itemId);
}
isReserved(tiledItem) {
const itemId = getItemIdentifier(tiledItem);
return this.reserved.has(itemId);
}
getUnreservedItems() {
// Return all non-reserved items for processing
const unreserved = [];
const collectUnreserved = (indexed) => {
Object.values(indexed).forEach(items => {
items.forEach(item => {
if (!this.isReserved(item)) {
unreserved.push(item);
}
});
});
};
collectUnreserved(this.items);
collectUnreserved(this.conditionalItems);
collectUnreserved(this.conditionalTableItems);
return unreserved;
}
}
```
### Step 3: Refactor processScenarioObjectsWithConditionalMatching
**Old approach**: Loop through scenarios, search for items, consume with `.shift()`
**New approach**: Use centralized matching, then process all scenarios uniformly
```javascript
function processScenarioObjectsWithConditionalMatching(roomId, position, objectsByLayer) {
const gameScenario = window.gameScenario;
if (!gameScenario.rooms[roomId].objects) {
return new Set();
}
// 1. Initialize item pool
const itemPool = new TiledItemPool(objectsByLayer);
const usedItems = new Set();
console.log(`Processing ${gameScenario.rooms[roomId].objects.length} scenario objects for room ${roomId}`);
// 2. Process each scenario object
gameScenario.rooms[roomId].objects.forEach((scenarioObj, index) => {
// Skip inventory items
if (scenarioObj.inInventory) {
return;
}
// Find matching Tiled item
const match = itemPool.findMatchFor(scenarioObj);
if (match) {
// Item found - use it
const sprite = createSpriteFromMatch(match, scenarioObj, position, roomId, index);
itemPool.reserve(match);
usedItems.add(getImageNameFromObject(match));
usedItems.add(extractBaseTypeFromImageName(getImageNameFromObject(match)));
console.log(`✓ Matched ${scenarioObj.type} to visual item`);
} else {
// No item found - create at random position
const sprite = createSpriteAtRandomPosition(scenarioObj, position, roomId, index);
console.log(`✗ No visual match for ${scenarioObj.type} - created at random position`);
}
});
// 3. Process unreserved Tiled items
const unreservedItems = itemPool.getUnreservedItems();
unreservedItems.forEach(tiledItem => {
const imageName = getImageNameFromObject(tiledItem);
const baseType = extractBaseTypeFromImageName(imageName);
if (!usedItems.has(imageName) && !usedItems.has(baseType)) {
createSpriteFromTiledItem(tiledItem, position, roomId, 'item');
}
});
return usedItems;
}
```
### Step 4: Helper Functions
```javascript
/**
* Create a unique identifier for a Tiled item
*/
function getItemIdentifier(tiledItem) {
return `gid_${tiledItem.gid}_x${tiledItem.x}_y${tiledItem.y}`;
}
/**
* Create sprite from a scenario object matched to a Tiled item
*/
function createSpriteFromMatch(tiledItem, scenarioObj, position, roomId, index) {
const imageName = getImageNameFromObject(tiledItem);
// Create sprite at Tiled position
const sprite = gameRef.add.sprite(
Math.round(position.x + tiledItem.x),
Math.round(position.y + tiledItem.y - tiledItem.height),
imageName
);
// Apply Tiled visual properties
applyTiledProperties(sprite, tiledItem);
// Apply scenario properties (override/enhance Tiled data)
applyScenarioProperties(sprite, scenarioObj, roomId, index);
// Set depth and store
setDepthAndStore(sprite, position, roomId);
return sprite;
}
/**
* Apply visual/transform properties from Tiled item
*/
function applyTiledProperties(sprite, tiledItem) {
sprite.setOrigin(0, 0);
if (tiledItem.rotation) {
sprite.setRotation(Phaser.Math.DegToRad(tiledItem.rotation));
}
if (tiledItem.flipX) {
sprite.setFlipX(true);
}
if (tiledItem.flipY) {
sprite.setFlipY(true);
}
}
/**
* Apply game logic properties from scenario
*/
function applyScenarioProperties(sprite, scenarioObj, roomId, index) {
sprite.scenarioData = scenarioObj;
sprite.interactable = true;
sprite.name = scenarioObj.name;
sprite.objectId = `${roomId}_${scenarioObj.type}_${index}`;
sprite.setInteractive({ useHandCursor: true });
// Store all scenario properties for interaction system
Object.keys(scenarioObj).forEach(key => {
sprite[key] = scenarioObj[key];
});
}
/**
* Set depth based on room position and elevation
*/
function setDepthAndStore(sprite, position, roomId) {
const roomTopY = position.y;
const backWallThreshold = roomTopY + (2 * TILE_SIZE);
const itemBottomY = sprite.y + sprite.height;
const elevation = itemBottomY < backWallThreshold ? (backWallThreshold - itemBottomY) : 0;
const objectDepth = itemBottomY + 0.5 + elevation;
sprite.setDepth(objectDepth);
sprite.elevation = elevation;
// Initially hide
sprite.setVisible(false);
// Store
rooms[roomId].objects[sprite.objectId] = sprite;
}
```
---
## Benefits of This Approach
### 1. **Testability**
```javascript
// Easy to test the matching function in isolation
const match = matchScenarioObjectToTiledItem(
{ type: 'key', name: 'Test Key' },
{ items: [mockKey1, mockKey2], ... },
new Set()
);
expect(match).toBeDefined();
expect(match.baseType).toBe('key');
```
### 2. **Maintainability**
- Clear separation of concerns
- Matching logic not mixed with rendering
- Easier to add new matching criteria
### 3. **Debuggability**
```javascript
// Can log exactly what happened to each object
console.log(`Scenario: ${scenarioObj.type} → Matched: ${match ? 'YES' : 'NO'}`);
console.log(`Visual from: ${match.layer}`);
```
### 4. **Extensibility**
Can easily add new matching criteria:
```javascript
function findBestMatchFor(scenarioObj, itemPool, criteria = {}) {
// Could support:
// - Proximity matching (closest to expected position)
// - Appearance matching (specific style/color)
// - Priority matching (preferred item types)
// - Constraint matching (must be table/floor item, etc.)
}
```
### 5. **Reusability**
The `TiledItemPool` class could be used elsewhere:
- For dynamic item placement
- For inventory system
- For content validation
---
## Migration Path
### Phase 1: Minimal (Current Implementation Kept)
- ✅ Document current system thoroughly
- ✅ Add helper functions for matching
- Keep existing `processScenarioObjectsWithConditionalMatching` working
### Phase 2: Refactor (Gradual Improvement)
- Create `TiledItemPool` class
- Create `matchScenarioObjectToTiledItem()` function
- Update `processScenarioObjectsWithConditionalMatching` to use new functions
- Test and debug
### Phase 3: Optimize (Full Implementation)
- Replace item `.shift()` calls with pool reservation
- Add full test coverage
- Performance optimize if needed
---
## Example: Before and After
### Current Code Flow
```javascript
// Current: Scattered logic
if (regularItemsByType[objType] && regularItemsByType[objType].length > 0) {
usedItem = regularItemsByType[objType].shift(); // Consume with shift
console.log(`Using regular item for ${objType}`);
}
else if (conditionalItemsByType[objType] && conditionalItemsByType[objType].length > 0) {
usedItem = conditionalItemsByType[objType].shift(); // Another shift
console.log(`Using conditional item for ${objType}`);
}
// ... more matching logic spread throughout function
```
### Improved Code Flow
```javascript
// Improved: Centralized matching
const match = itemPool.findMatchFor(scenarioObj);
if (match) {
const sprite = createSpriteFromMatch(match, scenarioObj, position, roomId, index);
itemPool.reserve(match);
console.log(`✓ Matched ${scenarioObj.type} to visual`);
} else {
const sprite = createSpriteAtRandomPosition(scenarioObj, position, roomId, index);
console.log(`✗ No match for ${scenarioObj.type}`);
}
```
---
## Related Documentation
- **Current Implementation**: See `README_ROOM_LOADING.md`
- **Scenario Format**: See `README_scenario_design.md`
- **Architecture**: Room Loading System Design
---
## Questions for Review
1. Should the item pool maintain order (FIFO, closest proximity)?
2. Should there be a "preferred" item type for each scenario object?
3. Should matching support position-based proximity criteria?
4. Should the pool be cached/reused for multiple rooms?
---
## Conclusion
The proposed improvements maintain the strong foundations of the current system while making it more maintainable, testable, and extensible. The changes are backward-compatible and can be implemented gradually without disrupting current functionality.

342
ROOM_LOADING_SUMMARY.md Normal file
View File

@@ -0,0 +1,342 @@
# Room Loading System - Documentation Summary
## Overview
This directory now contains comprehensive documentation on the BreakEscape room loading system, which coordinates **Scenario JSON files** (game logic) with **Tiled Map JSON files** (visual layout).
## Documentation Files
### 1. **README_ROOM_LOADING.md** (574 lines)
**Complete guide to the current room loading architecture**
Contains:
- Overview of architecture and data flow
- Detailed room loading process (5 phases)
- Matching algorithm explanation
- Object layer details and processing
- Depth layering philosophy
- Property application flow
- Item tracking and de-duplication
- Complete end-to-end example
- Collision and physics systems
- Performance considerations
- Debugging guide
- API reference for main functions
**Use this to understand:** How the system currently works
### 2. **README_ROOM_LOADING_IMPROVEMENTS.md** (525 lines)
**Proposed improvements and refactoring strategies**
Contains:
- Current approach analysis (strengths & challenges)
- Proposed improved architecture
- Implementation plan with code examples:
- `TiledItemPool` class
- `matchScenarioObjectToTiledItem()` function
- Refactored `processScenarioObjectsWithConditionalMatching()`
- Helper functions for sprite creation
- Benefits of improvements
- Gradual migration path (3 phases)
- Before/after code comparison
**Use this to understand:** How to improve the system
---
## Key Concepts
### Two-Source Architecture
```
Scenario JSON (Logic) Tiled Map JSON (Visual)
├─ type: "key" ├─ gid: 243
├─ name: "Office Key" ├─ x: 100
├─ takeable: true ├─ y: 150
├─ observations: "..." └─ imageName: "key"
└─ ...
↓ ↓
└─────────────────┬─────────────┘
MATCHING & MERGING
Final Game Object (Position + Properties)
```
### Three Processing Phases
1. **Collection** - Gather all Tiled items from layers
2. **Matching** - Match scenario objects to Tiled items by type
3. **Fallback** - Create sprites for unmatched items (random position)
### Layer Priority
When matching scenario objects:
1. Regular items layer (most common)
2. Conditional items layer (scenario-specific)
3. Conditional table items layer (on tables)
---
## Quick Reference
### For Understanding Current System
1. Read "Overview" section of README_ROOM_LOADING.md
2. Review "Room Loading Process" (3 phases)
3. Study "Matching Algorithm" section
4. Trace through "Example: Complete Scenario Object Processing"
### For Understanding Proposed Improvements
1. Read "Current Approach Analysis" in README_ROOM_LOADING_IMPROVEMENTS.md
2. Review "Proposed Improved Approach"
3. Study "Implementation Plan" with code examples
4. Review "Benefits of This Approach"
### For Implementation
1. Follow "Migration Path" (Phase 1, 2, 3)
2. Implement helper functions from "Step 4"
3. Create TiledItemPool class from "Step 2"
4. Refactor processScenarioObjectsWithConditionalMatching from "Step 3"
---
## Main Source File
The implementation is in: **`js/core/rooms.js`**
Key function: `processScenarioObjectsWithConditionalMatching()` (lines 612-842)
---
## Data Sources
### Scenario Format
Example: `scenarios/ceo_exfil.json`
```json
{
"rooms": {
"reception": {
"objects": [
{
"type": "key",
"name": "Office Key",
"takeable": true,
"key_id": "office1_key:40,35,38,32,10",
"observations": "A key to access the office areas"
}
]
}
}
}
```
### Tiled Map Format
Example: `assets/rooms/room_reception2.json`
- Contains layers: tables, table_items, conditional_items, items, conditional_table_items
- Each object has: gid (sprite ID), x, y, width, height, rotation, etc.
---
## System Flow Diagram
```
┌─────────────────────────────────────────────────────┐
│ Game Initialization │
│ • Load scenario JSON │
│ • Preload Tiled map files │
│ • Calculate room positions │
└────────────────────┬────────────────────────────────┘
Player Moves Near Door
┌────────────────────────────┐
│ Load Room (Lazy) │
│ │
│ 1. Load Tilemap │
│ 2. Create tile layers │
│ 3. Create door sprites │
│ 4. Process object layers │
│ 5. Match & merge objects │
└────────────────┬───────────┘
┌──────────┴──────────┐
▼ ▼
Scenario Matching Tiled Items
Objects matched → visual layer
to visual items positions
│ │
└──────────┬────────┘
┌────────────────────────┐
│ Create Sprites │
│ • Position from Tiled │
│ • Properties from │
│ Scenario │
└────────────┬───────────┘
┌────────────────────────┐
│ Reveal Room │
│ Show to Player │
└────────────────────────┘
```
---
## Key Functions (Reference)
| Function | Purpose | File |
|----------|---------|------|
| `loadRoom(roomId)` | Trigger room loading | `js/core/rooms.js:103` |
| `createRoom(roomId, roomData, position)` | Create all room elements | `js/core/rooms.js:351` |
| `processScenarioObjectsWithConditionalMatching()` | Match & merge objects | `js/core/rooms.js:612` |
| `getImageNameFromObject(obj)` | Extract image name from Tiled | `js/core/rooms.js:844` |
| `extractBaseTypeFromImageName(imageName)` | Get base type (key, phone, etc.) | `js/core/rooms.js:897` |
| `revealRoom(roomId)` | Make room visible | `js/core/rooms.js:1413` |
---
## Constants
```javascript
const TILE_SIZE = 32; // pixels
const DOOR_ALIGN_OVERLAP = 64; // pixels
const INTERACTION_RANGE_SQ = 5000; // pixels²
```
---
## Depth Calculation
```
Object Depth = ObjectBottomY + LayerOffset + Elevation
Where:
- ObjectBottomY = Y position + height
- LayerOffset = 0.5 (for objects)
- Elevation = Height above back wall (0 for most items)
```
---
## Testing the System
### Check Console Output
Browser console shows detailed logging:
```
Creating room reception of type room_reception
Collected items layer with 27 objects
Collected conditional_items layer with 9 objects
Processing 11 scenario objects for room reception
Looking for scenario object type: key
✓ Created key using key
Applied scenario data to key: { name: "Office Key", ... }
```
### Verify Items Appear
- Open developer tools (F12)
- Check "room_reception" in `window.rooms`
- Verify objects in `rooms[roomId].objects` have correct properties
---
## Performance Impact
### Lazy Loading
- ✅ Reduces initial load time
- ✅ Lower memory footprint
- ✅ Smoother transitions between rooms
### Asset Reuse
- Tiled items with same imageName reuse sprite asset
- No duplication of image data
### Depth Sorting
- Calculated once at load time
- Updated as needed during gameplay
---
## Related Systems
1. **Inventory System** - `js/systems/inventory.js`
- Items marked `inInventory: true` go here instead of room
2. **Interaction System** - `js/systems/interactions.js`
- Handles clicks on loaded objects
- Triggers minigames, dialogs, etc.
3. **Door System** - `js/systems/doors.js`
- Sprite-based door transitions
- Triggers `loadRoom()` on proximity
4. **Physics System** - `js/systems/object-physics.js`
- Collision detection
- Player movement constraints
---
## Common Issues & Solutions
### Issue: Item appears at wrong position
**Cause**: Tiled Y coordinate is top-left; game uses bottom-left
**Solution**: Subtract height when creating sprite (`y - height`)
### Issue: Scenario object not appearing
**Cause**: No matching Tiled item (checked by type)
**Cure**: Add item to Tiled map with matching type image, or item is in inventory
### Issue: Duplicate visuals
**Cause**: Same Tiled item used twice
**Solution**: Tracked by `usedItems` Set - shouldn't happen
### Issue: Wrong depth/layering
**Cause**: Depth not set based on Y position
**Solution**: Verify `setDepth()` called with `objectBottomY + 0.5`
---
## Glossary
| Term | Definition |
|------|-----------|
| **GID** | Global ID in Tiled - identifies which sprite/image |
| **Tileset** | Collection of sprites/images |
| **Object Layer** | Layer in Tiled map containing interactive objects |
| **Scenario Object** | Item defined in scenario JSON (has properties) |
| **Tiled Item** | Sprite placed in Tiled map (has position) |
| **Matching** | Process of linking scenario object to Tiled item |
| **Merging** | Combining Tiled position + scenario properties |
| **Depth** | Z-order for rendering (higher = on top) |
---
## Next Steps
### To Understand the System
→ Start with README_ROOM_LOADING.md
### To Improve the System
→ Review README_ROOM_LOADING_IMPROVEMENTS.md
→ Implement Phase 1-3 improvements
### To Debug Issues
→ Check console logs
→ Verify scenario vs Tiled data matches
→ Inspect `window.rooms` in developer tools
---
## Document History
- **Created**: October 21, 2025
- **Status**: Documentation complete, improvements documented
- **Files**:
- README_ROOM_LOADING.md (current system)
- README_ROOM_LOADING_IMPROVEMENTS.md (proposed improvements)
- ROOM_LOADING_SUMMARY.md (this file)
---
**For questions or clarifications**, refer to the detailed sections in the main documentation files.