# Spritesheets Reference — Phaser Gamedev

Frame dimension measurement, loading, animations, texture atlases, and nine-slice UI for Phaser 3.60+.

---

## MANDATORY: Measure Frame Dimensions Before Loading

**The #1 Phaser bug is wrong `frameWidth` / `frameHeight`.** Wrong values cause:
- Animations playing the wrong frames
- Tiles from adjacent rows appearing inside frames
- Invisible sprites (frame larger than texture)

**How to measure correctly:**

1. Open the spritesheet PNG in any image editor (Preview, GIMP, Photoshop, Paint.NET)
2. Note the total pixel dimensions (e.g., 384x192)
3. Count how many columns and rows of frames the sheet has (e.g., 8 columns x 4 rows)
4. Divide: `frameWidth = 384 / 8 = 48`, `frameHeight = 192 / 4 = 48`
5. Check for margin (border around the whole sheet) and spacing (gap between frames)

If the sheet has **spacing** (1-2px transparent gaps between frames), include it:

```
|<-- frameWidth -->|<spacing>|<-- frameWidth -->|
```

Never estimate frame sizes from visual inspection — pixel-count or use the source asset's documentation.

---

## Loading Spritesheets

```typescript
// In BootScene.preload():

// Simple uniform grid — most common
this.load.spritesheet('player', 'assets/player.png', {
  frameWidth: 48,    // MEASURE — width of one frame in pixels
  frameHeight: 48,   // MEASURE — height of one frame in pixels
});

// With spacing between frames
this.load.spritesheet('enemy', 'assets/enemy-sheet.png', {
  frameWidth: 64,
  frameHeight: 64,
  spacing: 2,   // pixel gap between adjacent frames
  margin: 1,    // pixel border around the entire sheet
});

// Limit how many frames to load (stops early — useful for partial sheets)
this.load.spritesheet('explosion', 'assets/explosion.png', {
  frameWidth: 128,
  frameHeight: 128,
  endFrame: 11,  // load frames 0-11 (zero-indexed), ignore the rest
});
```

---

## Creating Animations

All animations are defined in `create()`, after assets are loaded:

```typescript
// In GameScene.create():

// Looping walk cycle
this.anims.create({
  key: 'player-walk',
  frames: this.anims.generateFrameNumbers('player', { start: 0, end: 7 }),
  frameRate: 12,
  repeat: -1,   // -1 = loop forever
});

// One-shot attack animation
this.anims.create({
  key: 'player-attack',
  frames: this.anims.generateFrameNumbers('player', { start: 8, end: 13 }),
  frameRate: 16,
  repeat: 0,   // play once
});

// Idle — slow looping
this.anims.create({
  key: 'player-idle',
  frames: this.anims.generateFrameNumbers('player', { start: 14, end: 17 }),
  frameRate: 6,
  repeat: -1,
});

// Non-contiguous frames — pick specific indices
this.anims.create({
  key: 'player-hurt',
  frames: this.anims.generateFrameNumbers('player', { frames: [18, 19, 18, 19, 20] }),
  frameRate: 10,
  repeat: 0,
});
```

**Playing animations on a sprite:**

```typescript
// Play (restarts if already playing)
sprite.play('player-walk');

// Play only if not already playing this animation
sprite.play('player-idle', true); // second arg = ignoreIfPlaying

// Listen for one-shot animation completion
sprite.once('animationcomplete', (anim: Phaser.Animations.Animation) => {
  if (anim.key === 'player-attack') {
    sprite.play('player-idle');
  }
});

// Flip horizontally for left-facing movement
sprite.setFlipX(true);
```

---

## Texture Atlases (JSON)

For mixed-size frames (different dimensions per frame), use a **texture atlas** instead of a spritesheet. Atlas files are generated by tools like TexturePacker.

```typescript
// In preload():
this.load.atlas('ui', 'assets/ui-atlas.png', 'assets/ui-atlas.json');

// In create() — reference by frame name from the JSON
const panel = this.add.image(400, 300, 'ui', 'panel-background.png');
const btn = this.add.image(400, 400, 'ui', 'button-normal.png');

// Atlas-based animation — use generateFrameNames, not generateFrameNumbers
this.anims.create({
  key: 'coin-spin',
  frames: this.anims.generateFrameNames('ui', {
    prefix: 'coin-',
    start: 1,
    end: 8,
    zeroPad: 2,  // "coin-01", "coin-02", ...
    suffix: '.png',
  }),
  frameRate: 10,
  repeat: -1,
});
```

**Why use atlases over individual spritesheets:**
- One draw call for all atlas frames vs N draw calls for N images
- Works with mixed-size frames (buttons, icons, characters all in one atlas)
- TexturePacker optimizes packing and generates the JSON automatically

---

## Nine-Slice UI (Phaser 3.60+)

Nine-slice stretches UI panels and buttons without distorting the corners.

```typescript
// In preload():
this.load.image('panel', 'assets/panel.png'); // e.g., 48x48 image with 8px corners

// In create():
// NineSlice(scene, x, y, texture, frame, width, height, leftWidth, rightWidth, topHeight, bottomHeight)
const panel = this.add.nineslice(
  400, 300,     // position
  'panel',      // texture key
  undefined,    // frame (undefined for whole texture)
  200, 100,     // target width x height
  8, 8,         // left and right border widths in pixels
  8, 8          // top and bottom border heights in pixels
);

// Resize dynamically — corners stay intact
panel.setSize(300, 150);
```

**Corner layout:**

```
+------+--------------+------+
|  TL  |  Top (tiles) |  TR  |
+------+--------------+------+
| Left |   Center     | Right|
|(tile)|  (stretches) |(tile)|
+------+--------------+------+
|  BL  | Bot (tiles)  |  BR  |
+------+--------------+------+
```

Corners (TL, TR, BL, BR) are never scaled. Edges tile. Center stretches.

---

## Common Pitfalls

| Pitfall | Symptom | Fix |
|---------|---------|-----|
| Wrong `frameWidth` | Each animation frame shows parts of adjacent frames | Open PNG, measure pixels, divide by column/row count |
| `anims.create()` in `preload()` | Exception: texture not loaded | Move all `anims.create()` calls to `create()` |
| Animation key typo | `sprite.play()` silently does nothing | Log `this.anims.anims.entries` to see registered keys |
| Duplicate key name | Second `anims.create()` with same key ignored | Each animation key must be unique per scene |
| `generateFrameNumbers` vs `generateFrameNames` | Wrong frame type for atlas | Spritesheets use `generateFrameNumbers`; atlases use `generateFrameNames` |
| Missing `repeat: -1` | Animation plays once and stops | Add `repeat: -1` for looping animations |
