mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
270 lines
8.1 KiB
Markdown
270 lines
8.1 KiB
Markdown
|
|
# Atlas Detection Fix
|
||
|
|
|
||
|
|
## Problem
|
||
|
|
|
||
|
|
The system was incorrectly detecting atlas sprites as legacy sprites, causing errors like:
|
||
|
|
- `Texture "male_spy" has no frame "20"`
|
||
|
|
- `Frame "21" not found in texture "male_spy"`
|
||
|
|
- `TypeError: Cannot read properties of undefined (reading 'duration')`
|
||
|
|
|
||
|
|
## Root Cause
|
||
|
|
|
||
|
|
### Original Detection Method (FAILED)
|
||
|
|
```javascript
|
||
|
|
const isAtlas = scene.cache.json.exists(spriteSheet);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Why it failed:**
|
||
|
|
- When Phaser loads an atlas with `this.load.atlas(key, png, json)`, it does NOT store the JSON in `scene.cache.json`
|
||
|
|
- The JSON data is parsed and embedded directly into the texture
|
||
|
|
- `scene.cache.json.exists()` always returned `false` for atlas sprites
|
||
|
|
- All atlas sprites were incorrectly treated as legacy sprites
|
||
|
|
|
||
|
|
## Solution
|
||
|
|
|
||
|
|
### New Detection Method (WORKS)
|
||
|
|
```javascript
|
||
|
|
// Get frame names from texture
|
||
|
|
const texture = scene.textures.get(spriteSheet);
|
||
|
|
const frames = texture.getFrameNames();
|
||
|
|
|
||
|
|
// Check if frames are named strings (atlas) or numbers (legacy)
|
||
|
|
let isAtlas = false;
|
||
|
|
if (frames.length > 0) {
|
||
|
|
const firstFrame = frames[0];
|
||
|
|
isAtlas = typeof firstFrame === 'string' &&
|
||
|
|
(firstFrame.includes('breathing-idle') ||
|
||
|
|
firstFrame.includes('walk_') ||
|
||
|
|
firstFrame.includes('_frame_'));
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Why it works:**
|
||
|
|
- Directly inspects the frame names in the loaded texture
|
||
|
|
- Atlas frames are named strings: `"breathing-idle_south_frame_000"`
|
||
|
|
- Legacy frames are numbers: `0`, `1`, `2`, `20`, etc.
|
||
|
|
- Reliable detection based on actual frame data
|
||
|
|
|
||
|
|
## Frame Name Comparison
|
||
|
|
|
||
|
|
### Atlas Sprite Frames
|
||
|
|
```javascript
|
||
|
|
frames = [
|
||
|
|
"breathing-idle_east_frame_000",
|
||
|
|
"breathing-idle_east_frame_001",
|
||
|
|
"breathing-idle_east_frame_002",
|
||
|
|
"breathing-idle_east_frame_003",
|
||
|
|
"breathing-idle_north_frame_000",
|
||
|
|
// ... etc
|
||
|
|
]
|
||
|
|
typeof frames[0] === 'string' // true
|
||
|
|
frames[0].includes('_frame_') // true
|
||
|
|
```
|
||
|
|
|
||
|
|
### Legacy Sprite Frames
|
||
|
|
```javascript
|
||
|
|
frames = ["0", "1", "2", "3", "4", "5", ..., "20", "21", ...]
|
||
|
|
// OR
|
||
|
|
frames = [0, 1, 2, 3, 4, 5, ..., 20, 21, ...]
|
||
|
|
typeof frames[0] === 'string' // might be true or false
|
||
|
|
frames[0].includes('_frame_') // false
|
||
|
|
```
|
||
|
|
|
||
|
|
## Building Animation Data
|
||
|
|
|
||
|
|
Since the JSON isn't in cache, we build animation metadata from frame names:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const animations = {};
|
||
|
|
frames.forEach(frameName => {
|
||
|
|
// Parse "breathing-idle_south_frame_000" -> "breathing-idle_south"
|
||
|
|
const match = frameName.match(/^(.+)_frame_\d+$/);
|
||
|
|
if (match) {
|
||
|
|
const animKey = match[1];
|
||
|
|
if (!animations[animKey]) {
|
||
|
|
animations[animKey] = [];
|
||
|
|
}
|
||
|
|
animations[animKey].push(frameName);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Sort frames within each animation
|
||
|
|
Object.keys(animations).forEach(key => {
|
||
|
|
animations[key].sort();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Result:
|
||
|
|
```javascript
|
||
|
|
{
|
||
|
|
"breathing-idle_east": [
|
||
|
|
"breathing-idle_east_frame_000",
|
||
|
|
"breathing-idle_east_frame_001",
|
||
|
|
"breathing-idle_east_frame_002",
|
||
|
|
"breathing-idle_east_frame_003"
|
||
|
|
],
|
||
|
|
"walk_north": [
|
||
|
|
"walk_north_frame_000",
|
||
|
|
"walk_north_frame_001",
|
||
|
|
// ...
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Safety Checks Added
|
||
|
|
|
||
|
|
### 1. Check Animation Has Frames Before Playing
|
||
|
|
```javascript
|
||
|
|
if (scene.anims.exists(idleAnimKey)) {
|
||
|
|
const anim = scene.anims.get(idleAnimKey);
|
||
|
|
if (anim && anim.frames && anim.frames.length > 0) {
|
||
|
|
sprite.play(idleAnimKey, true);
|
||
|
|
} else {
|
||
|
|
// Fall back to idle-down animation
|
||
|
|
const idleDownKey = `npc-${npc.id}-idle-down`;
|
||
|
|
if (scene.anims.exists(idleDownKey)) {
|
||
|
|
sprite.play(idleDownKey, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Check Source Animation Before Creating Legacy Idle
|
||
|
|
```javascript
|
||
|
|
if (scene.anims.exists(idleSouthKey)) {
|
||
|
|
const sourceAnim = scene.anims.get(idleSouthKey);
|
||
|
|
if (sourceAnim && sourceAnim.frames && sourceAnim.frames.length > 0) {
|
||
|
|
scene.anims.create({
|
||
|
|
key: idleDownKey,
|
||
|
|
frames: sourceAnim.frames,
|
||
|
|
// ...
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
console.warn(`Cannot create legacy idle: source has no frames`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Files Updated
|
||
|
|
|
||
|
|
### 1. NPC System (`js/systems/npc-sprites.js`)
|
||
|
|
- **`createNPCSprite()`** - Improved atlas detection, added frame validation
|
||
|
|
- **`setupNPCAnimations()`** - Improved atlas detection with debug logging
|
||
|
|
- **`setupAtlasAnimations()`** - Build animations from frame names
|
||
|
|
|
||
|
|
### 2. Player System (`js/core/player.js`)
|
||
|
|
- **`createPlayer()`** - Improved atlas detection for initial frame
|
||
|
|
- **`createPlayerAnimations()`** - Improved atlas detection with debug logging
|
||
|
|
- **`createAtlasPlayerAnimations()`** - Build animations from frame names
|
||
|
|
- **`getAnimationKey()`** - Added safety checks
|
||
|
|
|
||
|
|
## Debug Logging
|
||
|
|
|
||
|
|
Added comprehensive logging to diagnose issues:
|
||
|
|
|
||
|
|
```
|
||
|
|
🔍 NPC sarah_martinez: 152 frames, first frame: "breathing-idle_east_frame_000", isAtlas: true
|
||
|
|
🎭 NPC sarah_martinez created with atlas sprite (female_office_worker), initial frame: breathing-idle_south_frame_000
|
||
|
|
✨ Using atlas-based animations for sarah_martinez
|
||
|
|
📝 Building animation data from frame names for female_office_worker
|
||
|
|
✓ Created: npc-sarah_martinez-idle-down (4 frames @ 6 fps)
|
||
|
|
✓ Created: npc-sarah_martinez-walk-right (6 frames @ 10 fps)
|
||
|
|
... etc
|
||
|
|
✅ Atlas animations setup complete for sarah_martinez
|
||
|
|
▶️ [sarah_martinez] Playing initial idle animation: npc-sarah_martinez-idle
|
||
|
|
```
|
||
|
|
|
||
|
|
## Phaser Atlas Loading Internals
|
||
|
|
|
||
|
|
### How Phaser Loads Atlases
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// In preload()
|
||
|
|
this.load.atlas('character_key', 'sprite.png', 'sprite.json');
|
||
|
|
|
||
|
|
// What Phaser does:
|
||
|
|
// 1. Loads PNG into textures
|
||
|
|
// 2. Loads and parses JSON
|
||
|
|
// 3. Extracts frame definitions from JSON
|
||
|
|
// 4. Creates named frames in the texture
|
||
|
|
// 5. Stores custom data (if any) in texture.customData
|
||
|
|
// 6. Does NOT store JSON in scene.cache.json
|
||
|
|
```
|
||
|
|
|
||
|
|
### Why JSON Cache Check Failed
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// ❌ WRONG - JSON not in cache
|
||
|
|
const isAtlas = scene.cache.json.exists('character_key'); // Always false
|
||
|
|
|
||
|
|
// ✅ CORRECT - Check frame names in texture
|
||
|
|
const texture = scene.textures.get('character_key');
|
||
|
|
const frames = texture.getFrameNames();
|
||
|
|
const isAtlas = frames[0].includes('_frame_');
|
||
|
|
```
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
Verified with:
|
||
|
|
- ✅ `male_spy` - Detected as atlas correctly
|
||
|
|
- ✅ `female_office_worker` - Detected as atlas correctly
|
||
|
|
- ✅ `female_hacker_hood` - Detected as atlas correctly
|
||
|
|
- ✅ `hacker` (legacy) - Detected as legacy correctly
|
||
|
|
- ✅ `hacker-red` (legacy) - Detected as legacy correctly
|
||
|
|
|
||
|
|
## Expected Console Output
|
||
|
|
|
||
|
|
After hard refresh, you should see:
|
||
|
|
|
||
|
|
```
|
||
|
|
🔍 NPC briefing_cutscene: 208 frames, first frame: "breathing-idle_east_frame_000", isAtlas: true
|
||
|
|
🎭 NPC briefing_cutscene created with atlas sprite (male_spy), initial frame: breathing-idle_south_frame_000
|
||
|
|
🔍 Animation setup for briefing_cutscene: 208 frames, first: "breathing-idle_east_frame_000", isAtlas: true
|
||
|
|
✨ Using atlas-based animations for briefing_cutscene
|
||
|
|
📝 Building animation data from frame names for male_spy
|
||
|
|
✓ Created: npc-briefing_cutscene-idle-down (4 frames @ 6 fps)
|
||
|
|
✓ Created: npc-briefing_cutscene-walk-right (6 frames @ 10 fps)
|
||
|
|
✅ Atlas animations setup complete for briefing_cutscene
|
||
|
|
▶️ [briefing_cutscene] Playing initial idle animation: npc-briefing_cutscene-idle
|
||
|
|
```
|
||
|
|
|
||
|
|
## Error Prevention
|
||
|
|
|
||
|
|
Before this fix:
|
||
|
|
- ❌ All atlas sprites detected as legacy
|
||
|
|
- ❌ Tried to use numbered frames (20, 21, etc.)
|
||
|
|
- ❌ Frame errors for every sprite
|
||
|
|
- ❌ Animations with 0 frames created
|
||
|
|
- ❌ Runtime errors when playing animations
|
||
|
|
|
||
|
|
After this fix:
|
||
|
|
- ✅ Atlas sprites correctly detected
|
||
|
|
- ✅ Named frames used properly
|
||
|
|
- ✅ Animations built from frame names
|
||
|
|
- ✅ Frame validation before playing
|
||
|
|
- ✅ Fallback animations for safety
|
||
|
|
|
||
|
|
## Performance
|
||
|
|
|
||
|
|
No performance impact:
|
||
|
|
- Frame name extraction is fast (Phaser internal)
|
||
|
|
- Detection happens once per sprite creation
|
||
|
|
- Animation building is one-time operation
|
||
|
|
- Cached in texture.customData for potential reuse
|
||
|
|
|
||
|
|
## Backward Compatibility
|
||
|
|
|
||
|
|
✅ **100% backward compatible**
|
||
|
|
- Legacy detection improved, not changed
|
||
|
|
- Safety checks don't affect legacy sprites
|
||
|
|
- Both systems work independently
|
||
|
|
|
||
|
|
## Next Steps
|
||
|
|
|
||
|
|
After hard refresh (Ctrl+Shift+R), all atlas sprites should:
|
||
|
|
1. Be detected correctly
|
||
|
|
2. Use named frames for initial sprite
|
||
|
|
3. Create animations from frame names
|
||
|
|
4. Play breathing-idle animations smoothly
|
||
|
|
5. Support all 8 directions
|