Files
BreakEscape/docs/FRAME_NUMBER_FIX.md
Z. Cliffe Schreuders fb6e9b603c Enhance character sprite loading and animation handling
- Updated the game to support new character sprite atlases for both male and female characters, allowing for a wider variety of NPC designs.
- Improved player sprite initialization to dynamically select between atlas-based and legacy sprites, enhancing flexibility in character representation.
- Refined collision box settings based on sprite type, ensuring accurate physics interactions for both atlas (80x80) and legacy (64x64) sprites.
- Enhanced NPC behavior to utilize atlas animations, allowing for more fluid and diverse animations based on available frames.

Files modified:
- game.js: Added new character atlases and updated sprite loading logic.
- player.js: Improved player sprite handling and collision box adjustments.
- npc-behavior.js: Updated animation handling for NPCs to support atlas-based animations.
- npc-sprites.js: Enhanced NPC sprite creation to accommodate atlas detection and initial frame selection.
- scenario.json.erb: Updated player and NPC configurations to utilize new sprite sheets and animation settings.
- m01_npc_sarah.ink: Revised dialogue options to include new interactions related to NPCs.
2026-02-11 00:18:21 +00:00

5.0 KiB

Frame Number Fix - Atlas vs Legacy Sprites

Problem

Error when creating NPCs with atlas sprites:

Texture "male_spy" has no frame "20"

Root Cause

The NPC sprite creation was using a hardcoded frame number (20) which works for legacy 64x64 sprites but doesn't exist in atlas sprites.

Legacy Sprites (64x64)

  • Use numbered frames: 0, 1, 2, 3, ..., 20, 21, etc.
  • Frame 20 is the idle down-right frame
  • Frames are generated from a regular grid layout

Atlas Sprites (80x80)

  • Use named frames: "breathing-idle_south_frame_000", "walk_east_frame_001", etc.
  • Frame numbers don't exist - only frame names
  • Frames are defined in the JSON atlas

Solution

Detect sprite type and use appropriate initial frame:

Implementation

// Check if this is an atlas sprite
const isAtlas = scene.cache.json.exists(spriteSheet);

// Determine initial frame
let initialFrame;
if (isAtlas) {
    // Atlas sprite - use first frame from breathing-idle_south animation
    const atlasData = scene.cache.json.get(spriteSheet);
    if (atlasData?.animations?.['breathing-idle_south']) {
        initialFrame = atlasData.animations['breathing-idle_south'][0];
    } else {
        // Fallback to first frame in atlas
        initialFrame = 0;
    }
} else {
    // Legacy sprite - use configured frame or default to 20
    initialFrame = config.idleFrame || 20;
}

// Create sprite with correct frame
const sprite = scene.add.sprite(worldPos.x, worldPos.y, spriteSheet, initialFrame);

Frame Selection Logic

For Atlas Sprites

  1. First choice: First frame of breathing-idle_south animation (facing down)

    • Example: "breathing-idle_south_frame_000"
    • This ensures the character starts in a natural idle pose facing downward
  2. Fallback: Frame 0 (first frame in the atlas)

    • Used if breathing-idle animation doesn't exist

For Legacy Sprites

  1. First choice: config.idleFrame (if specified in scenario)
  2. Fallback: Frame 20 (down-right idle frame)

Why Frame 20 for Legacy?

Legacy sprites use this frame layout:

Row 0 (frames 0-4):   Right walk
Row 1 (frames 5-9):   Down walk
Row 2 (frames 10-14): Up walk
Row 3 (frames 15-19): Up-right walk
Row 4 (frames 20-24): Down-right walk  ← Frame 20 is first frame of this row

Frame 20 is the idle down-right pose, which is a good default starting position.

Why breathing-idle_south for Atlas?

Atlas sprites have structured animation names:

breathing-idle_south  → Idle breathing facing down
breathing-idle_east   → Idle breathing facing right
walk_north           → Walking upward

breathing-idle_south (down) is the most natural default direction for a character to face when first appearing.

Files Modified

File: public/break_escape/js/systems/npc-sprites.js
Function: createNPCSprite()
Lines: 25-60

Testing

Verified with:

  • Atlas sprites (female_hacker_hood, male_spy, etc.) - No frame errors
  • Legacy sprites (hacker, hacker-red) - Still works as before
  • NPCs spawn with correct initial pose
  • Animations play correctly after spawn
  • Console logging shows correct frame selection

Console Output

🎭 NPC briefing_cutscene created with atlas sprite (male_spy), initial frame: breathing-idle_south_frame_000
🎭 NPC sarah_martinez created with atlas sprite (female_office_worker), initial frame: breathing-idle_south_frame_000
🎭 NPC old_npc created with legacy sprite (hacker), initial frame: 20

Error Prevention

Before Fix

const idleFrame = config.idleFrame || 20;  // ❌ Always uses number
const sprite = scene.add.sprite(x, y, spriteSheet, idleFrame);
// ERROR: Texture "male_spy" has no frame "20"

After Fix

const isAtlas = scene.cache.json.exists(spriteSheet);
let initialFrame;
if (isAtlas) {
    // Use named frame from atlas
    initialFrame = atlasData.animations['breathing-idle_south'][0];
} else {
    // Use numbered frame
    initialFrame = config.idleFrame || 20;
}
const sprite = scene.add.sprite(x, y, spriteSheet, initialFrame);
// ✅ Works for both atlas and legacy sprites

This is part of a series of fixes for atlas sprite support:

  1. 8-directional animation support
  2. Collision box adjustment for 80x80 sprites
  3. NPC animation stuck on single frame
  4. Initial frame selection (this fix)

Future Improvements

  • Allow specifying initial direction in scenario config
  • Support custom initial frames per NPC
  • Add visual indicator of sprite type in debug mode
  • Validate frame exists before creating sprite

Backward Compatibility

Fully backward compatible

  • Legacy sprites continue to use frame 20 (or configured frame)
  • Atlas sprites use appropriate named frames
  • No changes needed to existing scenarios
  • Automatic detection ensures correct behavior

Performance

  • No performance impact: Frame selection happens once at sprite creation
  • Minimal overhead: Single JSON cache check to determine sprite type
  • Efficient: Uses first frame from animation data without searching