Files
BreakEscape/planning_notes/rails-engine-migration/SERVER_CLIENT_MODEL_ASSESSMENT.md

10 KiB

Server-Client Model Compatibility Assessment

Executive Summary

The current refactoring is HIGHLY POSITIVE for server-client migration. The new architecture requires only minimal changes to transition to lazy-loaded server-side room data. The separation of Tiled visual structure from scenario game logic is perfectly aligned with a remote data model.


Current Architecture (Post-Refactoring)

Data Flow

1. Preload Phase:
   - Load ALL Tiled map JSONs (room_reception2.json, room_office2.json, etc.)
   - Load ALL image assets
   - Load entire scenario JSON (ceo_exfil.json with all rooms)

2. Create Phase:
   - Initialize rooms system
   - Calculate room positions for lazy loading

3. Runtime - When room is unlocked:
   loadRoom(roomId)
   → Fetch roomData from window.gameScenario.rooms[roomId]
   → Call createRoom(roomId, roomData, position)

Key Components

Visual Layer (Tiled):

  • Tiled JSON files (room_reception2.json)
  • Static sprite positions and layers
  • Loaded at game start
  • Independent of room unlock state

Game Logic Layer (Scenario):

  • Scenario JSON (ceo_exfil.json)
  • Object properties (locked, contents, takeable, etc.)
  • Currently loaded entirely at game start
  • Used during createRoom() to populate loaded rooms

Analysis: Server-Client Compatibility

POSITIVE FACTORS

1. Clean Separation of Concerns

The refactoring cleanly separates:

  • Visual Structure (Tiled): Room layouts, sprite positions, layers
  • Game Logic (Scenario): Object properties, interactions, contents

This is IDEAL for server-client because:

  • Client loads Tiled visuals once at startup
  • Server sends game logic data on-demand
  • No coupling between visual structure and dynamic data

2. TiledItemPool Architecture

Current approach:

const itemPool = new TiledItemPool(objectsByLayer, map);
usedItem = itemPool.findMatchFor(scenarioObj);
sprite = createSpriteFromMatch(usedItem, scenarioObj, position, roomId, index, map);

Why this is perfect for server-client:

  • Item pool operates entirely on LOCAL Tiled data (already loaded)
  • Matching logic doesn't depend on ALL scenario objects being present
  • Can match objects incrementally as server sends scenario data
  • Matching is deterministic: same Tiled item + same scenario object = same result

3. Lazy Loading Already Exists

function loadRoom(roomId) {
    const roomData = gameScenario.rooms[roomId];
    createRoom(roomId, roomData, position);
}

Current implementation:

  • Rooms are lazy-loaded when approached
  • Only createRoom() is called at runtime
  • Perfect hook point for fetching server data

4. Modular Helper Functions

All room loading logic is cleanly separated:

  • applyTiledProperties() - works with Tiled data only
  • applyScenarioProperties() - works with scenario data only
  • createSpriteFromMatch() - combines both data sources
  • setDepthAndStore() - finalizes sprite in game world

These can be called incrementally as data arrives.

5. No Hard Coupling to Preload

// Current: gameScenario loaded at preload
this.load.json('gameScenarioJSON', scenarioFile);

// But: Only accessed when loadRoom() is called
const roomData = gameScenario.rooms[roomId];

This single point of access (loadRoom) is easily replaceable with a server call.


Migration Path: Current → Server-Client Model

Phase 1: Minimal Changes (1-2 hours)

Modify loadRoom() to fetch from server:

async function loadRoom(roomId) {
    const position = window.roomPositions[roomId];
    
    if (!position) {
        console.error(`Cannot load room ${roomId}: missing position`);
        return;
    }
    
    // CHANGE: Fetch room data from server instead of window.gameScenario
    let roomData;
    try {
        const response = await fetch(`/api/rooms/${roomId}`, {
            headers: {
                'Authorization': `Bearer ${window.playerToken}`
            }
        });
        roomData = await response.json();
    } catch (error) {
        console.error(`Failed to fetch room ${roomId}:`, error);
        return;
    }
    
    console.log(`Lazy loading room: ${roomId}`);
    createRoom(roomId, roomData, position);
    revealRoom(roomId);
}

Remove preloading of scenario JSON:

// OLD (in game.js preload):
this.load.json('gameScenarioJSON', scenarioFile);

// NEW: Not needed - will fetch on demand
// This reduces initial load time significantly

Keep preloading Tiled files:

// KEEP these - they're visual structure, not gameplay logic
this.load.tilemapTiledJSON('room_reception', 'assets/rooms/room_reception2.json');
this.load.tilemapTiledJSON('room_office', 'assets/rooms/room_office2.json');
// ... etc

Phase 2: Initial Inventory Items (1 hour)

Update processInitialInventoryItems():

// OLD: Loop through window.gameScenario.rooms
// NEW: Need to fetch initial room data before processing

export async function processInitialInventoryItems() {
    console.log('Processing initial inventory items');
    
    // Option A: Fetch only the starting room
    const startRoomId = window.gameScenario.startRoom;
    const response = await fetch(`/api/rooms/${startRoomId}`);
    const roomData = await response.json();
    
    if (roomData.objects && Array.isArray(roomData.objects)) {
        roomData.objects.forEach(obj => {
            if (obj.inInventory === true) {
                const inventoryItem = createInventorySprite(obj);
                if (inventoryItem) {
                    addToInventory(inventoryItem);
                }
            }
        });
    }
}

Keep scenario bootstrap data:

// In create() or early initialization, fetch minimal data
const response = await fetch('/api/scenario/metadata');
window.gameScenario = await response.json();
// This should contain: { startRoom, scenarioName, briefDescription, ... }
// But NOT the full room data

What DOESN'T Need to Change

Tiled Map Loading (Already Perfect)

  • Tiled files stay local
  • No server interaction needed
  • Massive visual content loaded once at startup
  • No change required

Room Positioning System

const position = window.roomPositions[roomId];
  • This is calculated from Tiled file dimensions
  • Works identically with server-side scenario data
  • Zero changes needed

Sprite Creation Logic

  • TiledItemPool.findMatchFor() - works identically
  • createSpriteFromMatch() - works identically
  • applyScenarioProperties() - works identically
  • Only data source changes (server instead of preloaded JSON)

All Interaction Systems

  • Inventory system
  • Lock/key system
  • Container system
  • Minigames
  • All operate on sprite properties that come from scenario data
  • Work identically once sprite is populated

Key Benefits of Current Architecture for Server-Client

1. Bandwidth Optimization

  • Tiled files (~500KB) loaded once
  • Room scenario data loaded on-demand
  • Average room: ~10-20KB
  • vs. Loading entire scenario upfront: ~100-200KB

2. Security Improvement

  • Server doesn't send room data until unlocked
  • Can't explore entire scenario from network tab
  • Dynamic content validation server-side

3. Scalability

  • Can add rooms without affecting startup time
  • Scenario data can be regenerated/updated server-side
  • Client remains thin, server-driven

4. Flexibility

  • Server can send different room data based on:
    • Player progression
    • Difficulty level
    • Custom rules
    • Time limits
  • Client doesn't need to know about these rules

Implementation Checklist

Required Changes:

  • Modify loadRoom() to fetch from /api/rooms/{roomId}
  • Remove this.load.json('gameScenarioJSON', scenarioFile) from preload
  • Create minimal scenario bootstrap endpoint
  • Update processInitialInventoryItems() to fetch start room
  • Add authorization headers to server calls
  • Handle network errors gracefully

No Changes Needed:

  • TiledItemPool matching logic
  • Sprite creation (createSpriteFromMatch, etc.)
  • Interaction systems
  • Inventory system
  • All minigames
  • Tiled file loading
  • Room positioning calculations

Testing Points:

  • Rooms load when approached
  • Properties match scenario data
  • Interactions work correctly
  • Initial inventory populates correctly
  • No race conditions with concurrent room loads
  • Network errors handled gracefully

Potential Challenges & Solutions

Challenge 1: Race Conditions

Problem: What if player approaches multiple rooms before first request completes?

Solution:

const loadingRooms = new Set();

async function loadRoom(roomId) {
    if (loadingRooms.has(roomId)) {
        return; // Already loading
    }
    
    loadingRooms.add(roomId);
    try {
        // ... load from server
    } finally {
        loadingRooms.delete(roomId);
    }
}

Challenge 2: Network Failures

Problem: Server unreachable when player enters room?

Solution:

  • Show loading indicator
  • Implement retry logic with exponential backoff
  • Fallback to cached data if available
  • Show error modal if all retries fail

Challenge 3: Latency

Problem: Noticeable delay loading room?

Solution:

  • Prefetch adjacent room data in background
  • Show smooth transition/animation during load
  • Cache room data client-side (IndexedDB)

Conclusion

This refactoring is EXCELLENT preparation for server-client migration.

The clean separation of visual structure (Tiled) from game logic (Scenario) means:

  • Can migrate with minimal code changes
  • Existing TiledItemPool architecture works perfectly
  • All existing interaction systems continue to work
  • Only data source changes (preloaded JSON → server API)

Recommended approach:

  1. Keep current architecture as-is
  2. When ready for server migration:
    • Change loadRoom() to fetch from API
    • Update initial inventory loading
    • Add authentication/authorization
    • That's it - everything else works

Estimated migration time: 4-6 hours of development