diff --git a/README_ROOM_LOADING.md b/README_ROOM_LOADING.md new file mode 100644 index 0000000..8f2d9bd --- /dev/null +++ b/README_ROOM_LOADING.md @@ -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` diff --git a/README_ROOM_LOADING_IMPROVEMENTS.md b/README_ROOM_LOADING_IMPROVEMENTS.md new file mode 100644 index 0000000..e6f0b14 --- /dev/null +++ b/README_ROOM_LOADING_IMPROVEMENTS.md @@ -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. diff --git a/ROOM_LOADING_SUMMARY.md b/ROOM_LOADING_SUMMARY.md new file mode 100644 index 0000000..5f5e152 --- /dev/null +++ b/ROOM_LOADING_SUMMARY.md @@ -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.