Skill: jhauga/game-engine (version 1.0.0)

--- name: game-engine description: 'Expert skill for building web-based game engines and games using HTML5, Canvas, WebGL, and JavaScript. Use when asked to create games, build game engines, implement game physics, handle collision detection, set up game loops, manage sprites, add game controls, or work with 2D/3D rendering. Covers techniques for platformers, breakout-style games, maze games, tilemaps, audio, multiplayer via WebRTC, and publishing games.' --- # Game Engine Skill Build web-based games and game engines using HTML5 Canvas, WebGL, and JavaScript. This skill includes starter templates, reference documentation, and step-by-step workflows for 2D and 3D game development with frameworks such as Phaser, Three.js, Babylon.js, and A-Frame. ## When to Use This Skill - Building a game engine or game from scratch using web technologies - Implementing game loops, physics, collision detection, or rendering - Working with HTML5 Canvas, WebGL, or SVG for game graphics - Adding game controls (keyboard, mouse, touch, gamepad) - Creating 2D platformers, breakout-style games, maze games, or 3D experiences - Working with tilemaps, sprites, or animations - Adding audio to web games - Implementing multiplayer features with WebRTC or WebSockets - Optimizing game performance - Publishing and distributing web games ## Prerequisites - Basic knowledge of HTML, CSS, and JavaScript - A modern web browser with Canvas/WebGL support - A text editor or IDE - Optional: Node.js for build tooling and local development servers ## Core Concepts The following concepts form the foundation of every web-based game engine. ### Game Loop Every game engine revolves around the game loop -- a continuous cycle of: 1. **Process Input** - Read keyboard, mouse, touch, or gamepad input 2. **Update State** - Update game object positions, physics, AI, and logic 3. **Render** - Draw the current game state to the screen Use `requestAnimationFrame` for smooth, browser-optimized rendering. ### Rendering - **Canvas 2D** - Best for 2D games, sprite-based rendering, and tilemaps - **WebGL** - Hardware-accelerated 3D and advanced 2D rendering - **SVG** - Vector-based graphics, good for UI elements - **CSS** - Useful for DOM-based game elements and transitions ### Physics and Collision Detection - **2D Collision Detection** - AABB, circle, and SAT-based collision - **3D Collision Detection** - Bounding box, bounding sphere, and raycasting - **Velocity and Acceleration** - Basic Newtonian physics for movement - **Gravity** - Constant downward acceleration for platformers ### Controls - **Keyboard** - Arrow keys, WASD, and custom key bindings - **Mouse** - Click, move, and pointer lock for FPS-style controls - **Touch** - Mobile touch events and virtual joysticks - **Gamepad** - Gamepad API for controller support ### Audio - **Web Audio API** - Programmatic sound generation and spatial audio - **HTML5 Audio** - Simple audio playback for music and sound effects ## Step-by-Step Workflows ### Creating a Basic 2D Game 1. Set up an HTML file with a `<canvas>` element 2. Get the 2D rendering context 3. Implement the game loop using `requestAnimationFrame` 4. Create game objects with position, velocity, and size properties 5. Handle keyboard/mouse input for player control 6. Implement collision detection between game objects 7. Add scoring, lives, and win/lose conditions 8. Add sound effects and music ### Building a 3D Game 1. Choose a framework (Three.js, Babylon.js, A-Frame, or PlayCanvas) 2. Set up the scene, camera, and renderer 3. Load or create 3D models and textures 4. Implement lighting and shaders 5. Add physics and collision detection 6. Implement player controls and camera movement 7. Add audio and visual effects ### Publishing a Game 1. Optimize assets (compress images, minify code) 2. Test across browsers and devices 3. Choose distribution platform (web, app stores, game portals) 4. Implement monetization if needed 5. Promote through game communities and social media ## Game Templates Starter templates are available in the `assets/` folder. Each template provides a complete, working example that can be used as a starting point for a new project. | Template | Description | |----------|-------------| | `paddle-game-template.md` | 2D Breakout-style game with pure JavaScript | | `2d-maze-game.md` | Maze game with device orientation controls | | `2d-platform-game.md` | Platformer game using Phaser framework | | `gameBase-template-reop.md` | Game base template repository structure | | `simple-2d-engine.md` | Simple 2D platformer engine with collisions | ## Reference Documentation Detailed reference material is available in the `references/` folder. Consult these files for in-depth coverage of specific topics. | Reference | Topics Covered | |-----------|---------------| | `basics.md` | Game development introduction and anatomy | | `web-apis.md` | Canvas, WebGL, Web Audio, Gamepad, and other web APIs | | `techniques.md` | Collision detection, tilemaps, async scripts, audio | | `3d-web-games.md` | 3D theory, frameworks, shaders, WebXR | | `game-control-mechanisms.md` | Touch, keyboard, mouse, and gamepad controls | | `game-publishing.md` | Distribution, promotion, and monetization | | `algorithms.md` | Raycasting, collision, physics, vector math | | `terminology.md` | Game development glossary | | `game-engine-core-principals.md` | Core design principles for game engines | ## Troubleshooting | Issue | Solution | |-------|----------| | Canvas is blank | Check that you are calling drawing methods after getting the context and inside the game loop | | Game runs at different speeds | Use delta time in update calculations instead of fixed values | | Collision detection is inconsistent | Use continuous collision detection or reduce time steps for fast-moving objects | | Audio does not play | Browsers require user interaction before playing audio; trigger playback from a click handler | | Performance is poor | Profile with browser dev tools, reduce draw calls, use object pooling, and optimize asset sizes | | Touch controls are unresponsive | Prevent default touch behavior and handle touch events separately from mouse events | | WebGL context lost | Handle the `webglcontextlost` event and restore state on `webglcontextrestored` |

path: assets/2d-maze-game.md
content: # 2D Maze Game Template

A mobile-optimized 2D maze game where players guide a ball through a labyrinth of obstacles to reach a target hole. The game uses the **Device Orientation API** for tilt-based motion controls on mobile devices and keyboard arrow keys on desktop. Built with the **Phaser** framework (v2.x with Arcade Physics), it features multi-level progression, collision detection, audio feedback, vibration haptics, and a timer system.

**Source reference:** [MDN - HTML5 Gamedev Phaser Device Orientation](https://developer.mozilla.org/en-US/docs/Games/Tutorials/HTML5_Gamedev_Phaser_Device_Orientation)
**Live demo:** [Cyber Orb](https://orb.enclavegames.com/)
**Source code:** [GitHub - EnclaveGames/Cyber-Orb](https://github.com/EnclaveGames/Cyber-Orb)

---

## Game Concept

The player controls a ball (the "orb") by tilting their mobile device or pressing arrow keys. The ball rolls through a maze of horizontal and vertical wall segments. The objective on each level is to navigate the ball to a hole at the top of the screen while avoiding walls. Collisions with walls trigger a bounce, a sound effect, and optional vibration. A timer tracks how long the player takes per level and across the entire game.

---

## Project Structure

```
project/
  index.html
  src/
    phaser-arcade-physics.2.2.2.min.js
    Boot.js
    Preloader.js
    MainMenu.js
    Howto.js
    Game.js
  img/
    ball.png
    hole.png
    element-horizontal.png
    element-vertical.png
    button-start.png
    loading-bg.png
    loading-bar.png
  audio/
    bounce.ogg
    bounce.mp3
    bounce.m4a
```

---

## Phaser Setup and Initialization

### HTML Entry Point

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Cyber Orb</title>
  <style>
    body { margin: 0; background: #333; }
  </style>
  <script src="src/phaser-arcade-physics.2.2.2.min.js"></script>
  <script src="src/Boot.js"></script>
  <script src="src/Preloader.js"></script>
  <script src="src/MainMenu.js"></script>
  <script src="src/Howto.js"></script>
  <script src="src/Game.js"></script>
</head>
<body>
  <script>
    (() => {
      const game = new Phaser.Game(320, 480, Phaser.CANVAS, "game");
      game.state.add("Boot", Ball.Boot);
      game.state.add("Preloader", Ball.Preloader);
      game.state.add("MainMenu", Ball.MainMenu);
      game.state.add("Howto", Ball.Howto);
      game.state.add("Game", Ball.Game);
      game.state.start("Boot");
    })();
  </script>
</body>
</html>
```

- Canvas size: `320 x 480`
- Renderer: `Phaser.CANVAS` (alternatives: `Phaser.WEBGL`, `Phaser.AUTO`)

---

## Game State Architecture

The game follows a linear state flow:

```
Boot --> Preloader --> MainMenu --> Howto --> Game
```

### Boot State

Loads minimal assets for the loading screen and configures scaling.

```javascript
const Ball = {
  _WIDTH: 320,
  _HEIGHT: 480,
};

Ball.Boot = function (game) {};
Ball.Boot.prototype = {
  preload() {
    this.load.image("preloaderBg", "img/loading-bg.png");
    this.load.image("preloaderBar", "img/loading-bar.png");
  },
  create() {
    this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.game.scale.pageAlignHorizontally = true;
    this.game.scale.pageAlignVertically = true;
    this.game.state.start("Preloader");
  },
};
```

### Preloader State

Displays a visual loading bar while loading all game assets. Audio is loaded in multiple formats for cross-browser compatibility.

```javascript
Ball.Preloader = function (game) {};
Ball.Preloader.prototype = {
  preload() {
    this.preloadBg = this.add.sprite(
      (Ball._WIDTH - 297) * 0.5,
      (Ball._HEIGHT - 145) * 0.5,
      "preloaderBg"
    );
    this.preloadBar = this.add.sprite(
      (Ball._WIDTH - 158) * 0.5,
      (Ball._HEIGHT - 50) * 0.5,
      "preloaderBar"
    );
    this.load.setPreloadSprite(this.preloadBar);

    this.load.image("ball", "img/ball.png");
    this.load.image("hole", "img/hole.png");
    this.load.image("element-w", "img/element-horizontal.png");
    this.load.image("element-h", "img/element-vertical.png");
    this.load.spritesheet("button-start", "img/button-start.png", 146, 51);
    this.load.audio("audio-bounce", [
      "audio/bounce.ogg",
      "audio/bounce.mp3",
      "audio/bounce.m4a",
    ]);
  },
  create() {
    this.game.state.start("MainMenu");
  },
};
```

### MainMenu State

Displays the title screen with a start button.

```javascript
Ball.MainMenu = function (game) {};
Ball.MainMenu.prototype = {
  create() {
    this.add.sprite(0, 0, "screen-mainmenu");
    this.gameTitle = this.add.sprite(Ball._WIDTH * 0.5, 40, "title");
    this.gameTitle.anchor.set(0.5, 0);

    this.startButton = this.add.button(
      Ball._WIDTH * 0.5, 200, "button-start",
      this.startGame, this,
      2, 0, 1  // hover, out, down frames
    );
    this.startButton.anchor.set(0.5, 0);
    this.startButton.input.useHandCursor = true;
  },
  startGame() {
    this.game.state.start("Howto");
  },
};
```

### Howto State

A single-click instruction screen before gameplay begins.

```javascript
Ball.Howto = function (game) {};
Ball.Howto.prototype = {
  create() {
    this.buttonContinue = this.add.button(
      0, 0, "screen-howtoplay",
      this.startGame, this
    );
  },
  startGame() {
    this.game.state.start("Game");
  },
};
```

---

## Device Orientation API Usage

The Device Orientation API provides real-time data about the physical tilt of a device. Two axes are used:

| Property | Axis | Range | Effect |
|----------|------|-------|--------|
| `event.gamma` | Left/right tilt | -90 to 90 degrees | Horizontal ball velocity |
| `event.beta` | Front/back tilt | -180 to 180 degrees | Vertical ball velocity |

### Registering the Listener

```javascript
// In the Game state's create() method
window.addEventListener("deviceorientation", this.handleOrientation);
```

### Handling Orientation Events

```javascript
handleOrientation(e) {
  const x = e.gamma; // left-right tilt
  const y = e.beta;  // front-back tilt
  Ball._player.body.velocity.x += x;
  Ball._player.body.velocity.y += y;
}
```

### Tilt Behavior

- Tilt device left: negative gamma, ball rolls left
- Tilt device right: positive gamma, ball rolls right
- Tilt device forward: positive beta, ball rolls down
- Tilt device backward: negative beta, ball rolls up

The tilt angle directly maps to velocity increments -- the steeper the tilt, the greater the force applied to the ball each frame.

---

## Core Game Mechanics

### Game State Structure

```javascript
Ball.Game = function (game) {};
Ball.Game.prototype = {
  create() {},
  initLevels() {},
  showLevel(level) {},
  updateCounter() {},
  managePause() {},
  manageAudio() {},
  update() {},
  wallCollision() {},
  handleOrientation(e) {},
  finishLevel() {},
};
```

### Ball Creation and Physics

```javascript
// In create()
this.ball = this.add.sprite(this.ballStartPos.x, this.ballStartPos.y, "ball");
this.ball.anchor.set(0.5);
this.physics.enable(this.ball, Phaser.Physics.ARCADE);
this.ball.body.setSize(18, 18);
this.ball.body.bounce.set(0.3, 0.3);
```

- Anchor at center `(0.5, 0.5)` for rotation around midpoint
- Physics body: 18x18 pixels
- Bounce coefficient: 0.3 (retains 30% velocity after wall collision)

### Keyboard Controls (Desktop Fallback)

```javascript
// In create()
this.keys = this.game.input.keyboard.createCursorKeys();

// In update()
if (this.keys.left.isDown) {
  this.ball.body.velocity.x -= this.movementForce;
} else if (this.keys.right.isDown) {
  this.ball.body.velocity.x += this.movementForce;
}
if (this.keys.up.isDown) {
  this.ball.body.velocity.y -= this.movementForce;
} else if (this.keys.down.isDown) {
  this.ball.body.velocity.y += this.movementForce;
}
```

### Hole (Goal) Setup

```javascript
this.hole = this.add.sprite(Ball._WIDTH * 0.5, 90, "hole");
this.physics.enable(this.hole, Phaser.Physics.ARCADE);
this.hole.anchor.set(0.5);
this.hole.body.setSize(2, 2);
```

The hole has a tiny 2x2 collision body for precise overlap detection.

---

## Level System

### Level Data Format

Each level is an array of wall segment objects with position and type:

```javascript
this.levelData = [
  [{ x: 96, y: 224, t: "w" }],                           // Level 1
  [
    { x: 72, y: 320, t: "w" },
    { x: 200, y: 320, t: "h" },
    { x: 72, y: 150, t: "w" },
  ],                                                       // Level 2
  // ... more levels
];
```

- `x, y`: Position in pixels
- `t`: Type -- `"w"` for horizontal wall, `"h"` for vertical wall

### Building Levels

```javascript
initLevels() {
  for (let i = 0; i < this.maxLevels; i++) {
    const newLevel = this.add.group();
    newLevel.enableBody = true;
    newLevel.physicsBodyType = Phaser.Physics.ARCADE;

    for (const item of this.levelData[i]) {
      newLevel.create(item.x, item.y, `element-${item.t}`);
    }

    newLevel.setAll("body.immovable", true);
    newLevel.visible = false;
    this.levels.push(newLevel);
  }
}
```

### Showing a Level

```javascript
showLevel(level) {
  const lvl = level || this.level;
  if (this.levels[lvl - 2]) {
    this.levels[lvl - 2].visible = false;
  }
  this.levels[lvl - 1].visible = true;
}
```

---

## Collision Detection

### Wall Collisions (Bounce)

```javascript
// In update()
this.physics.arcade.collide(
  this.ball, this.borderGroup,
  this.wallCollision, null, this
);
this.physics.arcade.collide(
  this.ball, this.levels[this.level - 1],
  this.wallCollision, null, this
);
```

`collide` causes the ball to bounce off walls and triggers the callback.

### Hole Overlap (Pass-Through Detection)

```javascript
this.physics.arcade.overlap(
  this.ball, this.hole,
  this.finishLevel, null, this
);
```

`overlap` detects intersection without physical collision response.

### Wall Collision Callback

```javascript
wallCollision() {
  if (this.audioStatus) {
    this.bounceSound.play();
  }
  if ("vibrate" in window.navigator) {
    window.navigator.vibrate(100);
  }
}
```

---

## Audio System

```javascript
// In create()
this.bounceSound = this.game.add.audio("audio-bounce");

// Toggle
manageAudio() {
  this.audioStatus = !this.audioStatus;
}
```

---

## Vibration API

```javascript
if ("vibrate" in window.navigator) {
  window.navigator.vibrate(100); // 100ms vibration pulse
}
```

Feature-detect before calling. Provides tactile feedback on supported mobile devices.

---

## Timer System

```javascript
// In create()
this.timer = 0;
this.totalTimer = 0;
this.timerText = this.game.add.text(15, 15, "Time: 0", this.fontBig);
this.totalTimeText = this.game.add.text(120, 30, "Total time: 0", this.fontSmall);
this.time.events.loop(Phaser.Timer.SECOND, this.updateCounter, this);

// Counter callback
updateCounter() {
  this.timer++;
  this.timerText.setText(`Time: ${this.timer}`);
  this.totalTimeText.setText(`Total time: ${this.totalTimer + this.timer}`);
}
```

---

## Level Completion

```javascript
finishLevel() {
  if (this.level >= this.maxLevels) {
    this.totalTimer += this.timer;
    alert(`Congratulations, game completed!\nTotal time: ${this.totalTimer}s`);
    this.game.state.start("MainMenu");
  } else {
    alert(`Level ${this.level} completed!`);
    this.totalTimer += this.timer;
    this.timer = 0;
    this.level++;
    this.timerText.setText(`Time: ${this.timer}`);
    this.totalTimeText.setText(`Total time: ${this.totalTimer}`);
    this.levelText.setText(`Level: ${this.level} / ${this.maxLevels}`);
    this.ball.body.x = this.ballStartPos.x;
    this.ball.body.y = this.ballStartPos.y;
    this.ball.body.velocity.x = 0;
    this.ball.body.velocity.y = 0;
    this.showLevel();
  }
}
```

---

## Complete Update Loop

```javascript
update() {
  // Keyboard input
  if (this.keys.left.isDown) {
    this.ball.body.velocity.x -= this.movementForce;
  } else if (this.keys.right.isDown) {
    this.ball.body.velocity.x += this.movementForce;
  }
  if (this.keys.up.isDown) {
    this.ball.body.velocity.y -= this.movementForce;
  } else if (this.keys.down.isDown) {
    this.ball.body.velocity.y += this.movementForce;
  }

  // Wall collisions
  this.physics.arcade.collide(
    this.ball, this.borderGroup, this.wallCollision, null, this
  );
  this.physics.arcade.collide(
    this.ball, this.levels[this.level - 1], this.wallCollision, null, this
  );

  // Hole overlap
  this.physics.arcade.overlap(
    this.ball, this.hole, this.finishLevel, null, this
  );
}
```

---

## Phaser API Quick Reference

| Function | Purpose |
|----------|---------|
| `this.add.sprite(x, y, key)` | Create a game object |
| `this.add.group()` | Create a container for objects |
| `this.add.button(x, y, key, cb, ctx, over, out, down)` | Create interactive button |
| `this.add.text(x, y, text, style)` | Create text display |
| `this.physics.enable(obj, system)` | Enable physics on object |
| `this.physics.arcade.collide(a, b, cb)` | Detect collision with bounce |
| `this.physics.arcade.overlap(a, b, cb)` | Detect overlap without bounce |
| `this.load.image(key, path)` | Load image asset |
| `this.load.spritesheet(key, path, w, h)` | Load sprite animation sheet |
| `this.load.audio(key, paths[])` | Load audio with format fallbacks |
| `this.game.add.audio(key)` | Instantiate audio object |
| `this.time.events.loop(interval, cb, ctx)` | Create repeating timer |


path: assets/2d-platform-game.md
content: # 2D Platform Game Template

A complete step-by-step guide for building a 2D platformer game using Phaser (v2.x / Phaser CE) with Arcade Physics. This template walks through every stage of development: setting up the project, creating platforms from JSON level data, adding a hero with physics-based movement and jumping, collectible coins, walking enemies, death and stomp mechanics, a scoreboard, sprite animations, win conditions with a door/key system, and multi-level progression.

**What you will build:** A classic side-scrolling platformer where a hero navigates platforms, collects coins, avoids or stomps on spider enemies, finds a key to unlock a door, and progresses through multiple levels -- with score tracking, animations, and physics.

**Prerequisites:** Basic to intermediate JavaScript knowledge, familiarity with HTML, and a local web server for development (e.g., browser-sync, live-server, or Python's SimpleHTTPServer).

**Source:** Based on the [Mozilla HTML5 Games Workshop - Platformer](https://mozdevs.github.io/html5-games-workshop/en/guides/platformer/start-here/). Project starter files available at the workshop repository.

---

## Start Here

This tutorial builds a 2D platformer using the **Phaser** framework. Phaser handles rendering, physics, input, audio, and asset loading so you can focus on game logic.

### What You Will Build

The finished game features:

- A hero character the player controls with the keyboard
- Platforms the hero can walk and jump on
- Collectible coins that increase the score
- Walking spider enemies that kill the hero on contact (but can be stomped from above)
- A key and door system: the hero must pick up a key to unlock the door and complete the level
- Multiple levels loaded from JSON data files
- A scoreboard showing collected coins
- Sprite animations for the hero (idle, running, jumping, falling)

### Project Structure

```
project/
  index.html
  js/
    phaser.min.js        (Phaser 2.6.2 or Phaser CE)
    main.js              (all game code goes here)
  audio/
    sfx/
      jump.wav
      coin.wav
      stomp.wav
      key.wav
      door.wav
  images/
    background.png
    ground.png
    grass:8x1.png        (platform tile images in various sizes)
    grass:6x1.png
    grass:4x1.png
    grass:2x1.png
    grass:1x1.png
    hero.png             (hero spritesheet: 36x42 per frame)
    hero_stopped.png     (single frame for initial steps)
    coin_animated.png    (coin spritesheet)
    spider.png           (spider spritesheet)
    invisible_wall.png   (invisible boundary for enemy AI)
    key.png              (key spritesheet)
    door.png             (door spritesheet)
    key_icon.png         (HUD icon for key)
    font:numbers.png     (bitmap font for score)
  data/
    level00.json
    level01.json
```

### Level Data Format

Each level is defined in a JSON file. The JSON structure describes positions of every entity:

```json
{
    "hero": { "x": 21, "y": 525 },
    "door": { "x": 169, "y": 546 },
    "key": { "x": 750, "y": 524 },
    "platforms": [
        { "image": "ground", "x": 0, "y": 546 },
        { "image": "grass:8x1", "x": 208, "y": 420 },
        { "image": "grass:4x1", "x": 420, "y": 336 },
        { "image": "grass:2x1", "x": 680, "y": 252 }
    ],
    "coins": [
        { "x": 147, "y": 525 },
        { "x": 189, "y": 525 },
        { "x": 399, "y": 399 },
        { "x": 441, "y": 336 }
    ],
    "spiders": [
        { "x": 121, "y": 399 }
    ],
    "decoration": {
        "grass": [
            { "x": 84, "y": 504, "frame": 0 },
            { "x": 420, "y": 504, "frame": 1 }
        ]
    }
}
```

Each entity type (hero, door, key, platforms, coins, spiders) has `x` and `y` coordinates. Platforms also specify which `image` asset to use for that platform tile.

---

## Initialise Phaser

The first step is setting up the HTML file and creating the Phaser game instance.

### HTML Entry Point

Create an `index.html` file that loads Phaser and your game script:

```html
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Platformer Game</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            background: #000;
        }
    </style>
    <script src="js/phaser.min.js"></script>
    <script src="js/main.js"></script>
</head>
<body>
    <div id="game"></div>
</body>
</html>
```

- The `<div id="game">` is the container where Phaser will insert the game canvas.
- Phaser is loaded first, then your game script.

### Creating the Game Instance

In `js/main.js`, create the Phaser game object and register a game state:

```javascript
// Create a Phaser game instance
// Parameters: width, height, renderer, DOM element ID
window.onload = function () {
    let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');

    // Add and start the play state
    game.state.add('play', PlayState);
    game.state.start('play');
};
```

- `960, 600` sets the game canvas dimensions in pixels.
- `Phaser.AUTO` lets Phaser choose between WebGL and Canvas rendering automatically.
- `'game'` is the ID of the DOM element that will contain the canvas.

### The PlayState Object

Define the game state as an object with lifecycle methods:

```javascript
PlayState = {};

PlayState.init = function () {
    // Called first when the state starts
};

PlayState.preload = function () {
    // Load all assets here
};

PlayState.create = function () {
    // Create game entities and set up the world
};

PlayState.update = function () {
    // Called every frame (~60 times per second)
    // Handle game logic, input, collisions here
};
```

- `init` -- runs first; used for configuration and receiving parameters.
- `preload` -- used to load all assets (images, audio, JSON) before the game starts.
- `create` -- called once after assets are loaded; used to create sprites, groups, and game objects.
- `update` -- called every frame at ~60fps; used for input handling, physics checks, and game logic.

At this point you should see an empty black canvas rendered on the page.

---

## The Game Loop

Phaser uses a game loop architecture. Every frame, Phaser calls `update()`, which is where you handle input, move sprites, and check collisions. Before the loop starts, `preload()` loads assets and `create()` sets up the initial game state.

### Loading and Displaying the Background

Start by loading and displaying a background image to verify the game loop is working:

```javascript
PlayState.preload = function () {
    this.game.load.image('background', 'images/background.png');
};

PlayState.create = function () {
    // Add the background image at position (0, 0)
    this.game.add.image(0, 0, 'background');
};
```

- `this.game.load.image(key, path)` loads an image and assigns it a key for later reference.
- `this.game.add.image(x, y, key)` creates a static image at the given position.

You should now see the background image rendered in the game canvas.

### Understanding the Frame Cycle

```
preload() -> [assets loaded] -> create() -> update() -> update() -> update() -> ...
```

Each call to `update()` represents one frame. The game targets 60 frames per second. All movement, input reading, and collision detection happen inside `update()`.

---

## Creating Platforms

Platforms are the surfaces the hero walks and jumps on. They are loaded from the level JSON data and created as physics-enabled sprites arranged in a group.

### Loading Platform Assets

Load the level JSON data and all platform tile images in `preload`:

```javascript
PlayState.preload = function () {
    this.game.load.image('background', 'images/background.png');

    // Load level data
    this.game.load.json('level:1', 'data/level01.json');

    // Load platform images
    this.game.load.image('ground', 'images/ground.png');
    this.game.load.image('grass:8x1', 'images/grass_8x1.png');
    this.game.load.image('grass:6x1', 'images/grass_6x1.png');
    this.game.load.image('grass:4x1', 'images/grass_4x1.png');
    this.game.load.image('grass:2x1', 'images/grass_2x1.png');
    this.game.load.image('grass:1x1', 'images/grass_1x1.png');
};
```

### Spawning Platforms from Level Data

Create a method to load the level and spawn each platform as a sprite inside a physics group:

```javascript
PlayState.create = function () {
    // Add the background
    this.game.add.image(0, 0, 'background');

    // Load level data and spawn entities
    this._loadLevel(this.game.cache.getJSON('level:1'));
};

PlayState._loadLevel = function (data) {
    // Create a group for platforms
    this.platforms = this.game.add.group();

    // Spawn each platform from the level data
    data.platforms.forEach(this._spawnPlatform, this);
};

PlayState._spawnPlatform = function (platform) {
    // Add a sprite at the platform's position using the specified image
    let sprite = this.platforms.create(platform.x, platform.y, platform.image);

    // Enable physics on this platform
    this.game.physics.enable(sprite);

    // Make platform immovable so it doesn't get pushed by the hero
    sprite.body.allowGravity = false;
    sprite.body.immovable = true;
};
```

- `this.game.add.group()` creates a Phaser group -- a container for related sprites that enables batch operations and collision detection.
- `this.platforms.create(x, y, key)` creates a sprite inside the group.
- `sprite.body.immovable = true` prevents the platform from being pushed by other physics bodies.
- `sprite.body.allowGravity = false` prevents platforms from falling due to gravity.

You should now see the ground and grass platform tiles rendered on the screen.

---

## The Main Character Sprite

Now add the hero character that the player will control.

### Loading the Hero Image

Add the hero image to `preload`. Initially we use a single static image; we will switch to a spritesheet later for animations:

```javascript
// In PlayState.preload:
this.game.load.image('hero', 'images/hero_stopped.png');
```

### Spawning the Hero

Add the hero to `_loadLevel` and create a spawn method:

```javascript
PlayState._loadLevel = function (data) {
    this.platforms = this.game.add.group();
    data.platforms.forEach(this._spawnPlatform, this);

    // Spawn the hero at the position defined in level data
    this._spawnCharacters({ hero: data.hero });
};

PlayState._spawnCharacters = function (data) {
    // Create the hero sprite
    this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');

    // Set the anchor to the bottom-center for easier positioning
    this.hero.anchor.set(0.5, 1);
};
```

- `anchor.set(0.5, 1)` sets the sprite's origin point to the horizontal center and vertical bottom. This makes it easier to position the hero on top of platforms, since the `y` position refers to the hero's feet rather than the top-left corner.

---

## Keyboard Controls

Capture keyboard input so the player can move the hero left, right, and jump.

### Setting Up Input Keys

In `init`, configure the keyboard controls:

```javascript
PlayState.init = function () {
    // Force integer rendering for pixel-art crispness
    this.game.renderer.renderSession.roundPixels = true;

    // Capture arrow keys
    this.keys = this.game.input.keyboard.addKeys({
        left: Phaser.KeyCode.LEFT,
        right: Phaser.KeyCode.RIGHT,
        up: Phaser.KeyCode.UP
    });
};
```

- `addKeys()` captures the specified keys and returns an object with key state references.
- `Phaser.KeyCode.LEFT`, `RIGHT`, `UP` correspond to the arrow keys.
- `renderSession.roundPixels = true` prevents pixel-art sprites from appearing blurry due to sub-pixel rendering.

### Reading Input in Update

Handle the key states in `update`. For now, just log the direction; the next step adds physics-based movement:

```javascript
PlayState.update = function () {
    this._handleInput();
};

PlayState._handleInput = function () {
    if (this.keys.left.isDown) {
        // Move hero left
    } else if (this.keys.right.isDown) {
        // Move hero right
    } else {
        // Stop (no key held)
    }
};
```

- `this.keys.left.isDown` returns `true` while the left arrow key is held down.
- The `else` clause handles the case where neither left nor right is pressed (the hero should stop).

---

## Moving Sprites with Physics

Enable Arcade Physics so the hero can move with velocity and interact with platforms through collisions.

### Enabling the Physics Engine

Enable Arcade Physics in `init`:

```javascript
PlayState.init = function () {
    this.game.renderer.renderSession.roundPixels = true;

    this.keys = this.game.input.keyboard.addKeys({
        left: Phaser.KeyCode.LEFT,
        right: Phaser.KeyCode.RIGHT,
        up: Phaser.KeyCode.UP
    });

    // Enable Arcade Physics
    this.game.physics.startSystem(Phaser.Physics.ARCADE);
};
```

### Adding a Physics Body to the Hero

Enable physics on the hero sprite in `_spawnCharacters`:

```javascript
PlayState._spawnCharacters = function (data) {
    this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
    this.hero.anchor.set(0.5, 1);

    // Enable physics body on the hero
    this.game.physics.enable(this.hero);
};
```

### Moving with Velocity

Now update `_handleInput` to set the hero's velocity based on key presses:

```javascript
const SPEED = 200; // pixels per second

PlayState._handleInput = function () {
    if (this.keys.left.isDown) {
        this.hero.body.velocity.x = -SPEED;
    } else if (this.keys.right.isDown) {
        this.hero.body.velocity.x = SPEED;
    } else {
        this.hero.body.velocity.x = 0;
    }
};
```

- `body.velocity.x` sets the horizontal speed in pixels per second.
- A negative value moves the sprite left; positive moves it right.
- Setting velocity to `0` when no keys are pressed makes the hero stop immediately.

The hero can now move left and right, but will fall through platforms and off the screen because there is no gravity or collision handling yet.

---

## Gravity

Add gravity so the hero falls downward and collides with platforms.

### Setting Global Gravity

Enable gravity for the entire physics world in `init`:

```javascript
PlayState.init = function () {
    this.game.renderer.renderSession.roundPixels = true;

    this.keys = this.game.input.keyboard.addKeys({
        left: Phaser.KeyCode.LEFT,
        right: Phaser.KeyCode.RIGHT,
        up: Phaser.KeyCode.UP
    });

    this.game.physics.startSystem(Phaser.Physics.ARCADE);

    // Set global gravity
    this.game.physics.arcade.gravity.y = 1200;
};
```

- `gravity.y = 1200` applies a downward acceleration of 1200 pixels per second squared to all physics-enabled sprites (unless they opt out with `allowGravity = false`).

### Collision Detection Between Hero and Platforms

Add collision detection in `update` so the hero lands on platforms instead of falling through:

```javascript
PlayState.update = function () {
    this._handleCollisions();
    this._handleInput();
};

PlayState._handleCollisions = function () {
    // Make the hero collide with the platform group
    this.game.physics.arcade.collide(this.hero, this.platforms);
};
```

- `arcade.collide(spriteA, groupB)` checks for physics collisions between the hero and every sprite in the platforms group. When the hero lands on a platform, the physics engine prevents it from passing through and resolves the overlap.
- It is important to call `_handleCollisions()` before `_handleInput()` so collision data (like whether the hero is touching the ground) is up to date when we process input.

The hero now falls due to gravity and lands on the platforms. You can walk left and right on the platforms.

---

## Jumps

Allow the hero to jump when the up arrow key is pressed -- but only when standing on a platform (no mid-air jumps).

### Implementing the Jump Mechanic

Add a jump constant and update `_handleInput`:

```javascript
const SPEED = 200;
const JUMP_SPEED = 600;

PlayState._handleInput = function () {
    if (this.keys.left.isDown) {
        this.hero.body.velocity.x = -SPEED;
    } else if (this.keys.right.isDown) {
        this.hero.body.velocity.x = SPEED;
    } else {
        this.hero.body.velocity.x = 0;
    }

    // Handle jumping
    if (this.keys.up.isDown) {
        this._jump();
    }
};

PlayState._jump = function () {
    let canJump = this.hero.body.touching.down;

    if (canJump) {
        this.hero.body.velocity.y = -JUMP_SPEED;
    }

    return canJump;
};
```

- `this.hero.body.touching.down` is `true` when the hero's physics body is touching another body on its underside -- meaning the hero is standing on something.
- Setting `velocity.y` to a negative value launches the hero upward (the y-axis points downward in screen coordinates).
- The `canJump` check prevents the hero from jumping while already in the air, enforcing single-jump behavior.
- The method returns whether the jump was performed, which is useful later for playing sound effects.

### Adding a Jump Sound Effect

Load a jump sound and play it on successful jumps:

```javascript
// In PlayState.preload:
this.game.load.audio('sfx:jump', 'audio/sfx/jump.wav');

// In PlayState.create:
this.sfx = {
    jump: this.game.add.audio('sfx:jump')
};

// In PlayState._jump, after setting velocity:
PlayState._jump = function () {
    let canJump = this.hero.body.touching.down;

    if (canJump) {
        this.hero.body.velocity.y = -JUMP_SPEED;
        this.sfx.jump.play();
    }

    return canJump;
};
```

---

## Pickable Coins

Add collectible coins that the player can pick up to increase their score.

### Loading Coin Assets

Load the coin spritesheet and coin sound effect in `preload`:

```javascript
// In PlayState.preload:
this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
this.game.load.audio('sfx:coin', 'audio/sfx/coin.wav');
```

- `load.spritesheet(key, path, frameWidth, frameHeight)` loads a spritesheet and slices it into individual frames of 22x22 pixels for animation.

### Spawning Coins from Level Data

Update `_loadLevel` to create a coins group and spawn each coin:

```javascript
PlayState._loadLevel = function (data) {
    this.platforms = this.game.add.group();
    this.coins = this.game.add.group();

    data.platforms.forEach(this._spawnPlatform, this);
    data.coins.forEach(this._spawnCoin, this);

    this._spawnCharacters({ hero: data.hero });
};

PlayState._spawnCoin = function (coin) {
    let sprite = this.coins.create(coin.x, coin.y, 'coin');
    sprite.anchor.set(0.5, 0.5);

    // Add a tween animation to make the coin bob up and down
    this.game.physics.enable(sprite);
    sprite.body.allowGravity = false;

    // Coin bobbing animation with a tween
    sprite.animations.add('rotate', [0, 1, 2, 1], 6, true); // 6fps, looping
    sprite.animations.play('rotate');
};
```

- Each coin is created inside the `coins` group for easy collision detection.
- `allowGravity = false` prevents coins from falling.
- The `animations.add` creates a frame animation using the spritesheet frames 0, 1, 2, 1 at 6fps, looping continuously.

### Collecting Coins

Add the coin sound to the sfx object and detect overlap between the hero and coins:

```javascript
// In PlayState.create, add to the sfx object:
this.sfx = {
    jump: this.game.add.audio('sfx:jump'),
    coin: this.game.add.audio('sfx:coin')
};

// In PlayState._handleCollisions:
PlayState._handleCollisions = function () {
    this.game.physics.arcade.collide(this.hero, this.platforms);

    // Detect overlap between hero and coins (no physical collision, just overlap)
    this.game.physics.arcade.overlap(
        this.hero, this.coins, this._onHeroVsCoin, null, this
    );
};

PlayState._onHeroVsCoin = function (hero, coin) {
    this.sfx.coin.play();
    coin.kill();  // Remove the coin from the game
    this.coinPickupCount++;
};
```

- `arcade.overlap()` checks if two sprites/groups overlap without resolving collisions physically. When an overlap is detected, it calls the callback function (`_onHeroVsCoin`).
- `coin.kill()` removes the coin sprite from the game world.
- `this.coinPickupCount` tracks the number of coins collected (initialize it in `_loadLevel`).

### Initializing the Coin Counter

```javascript
PlayState._loadLevel = function (data) {
    this.platforms = this.game.add.group();
    this.coins = this.game.add.group();

    data.platforms.forEach(this._spawnPlatform, this);
    data.coins.forEach(this._spawnCoin, this);

    this._spawnCharacters({ hero: data.hero });

    // Initialize coin counter
    this.coinPickupCount = 0;
};
```

---

## Walking Enemies

Add spider enemies that walk back and forth on platforms. The hero can stomp on them from above but dies if touching them from the side.

### Loading Enemy Assets

```javascript
// In PlayState.preload:
this.game.load.spritesheet('spider', 'images/spider.png', 42, 32);
this.game.load.image('invisible-wall', 'images/invisible_wall.png');
this.game.load.audio('sfx:stomp', 'audio/sfx/stomp.wav');
```

- The spider spritsheet has frames for a crawling animation.
- Invisible walls are placed at platform edges to keep spiders from walking off -- they are not rendered visually but have physics bodies.

### Spawning Enemies

Update `_loadLevel` and add a spawn method for spiders:

```javascript
PlayState._loadLevel = function (data) {
    this.platforms = this.game.add.group();
    this.coins = this.game.add.group();
    this.spiders = this.game.add.group();
    this.enemyWalls = this.game.add.group();

    data.platforms.forEach(this._spawnPlatform, this);
    data.coins.forEach(this._spawnCoin, this);
    data.spiders.forEach(this._spawnSpider, this);

    this._spawnCharacters({ hero: data.hero });

    // Make enemy walls invisible
    this.enemyWalls.visible = false;

    this.coinPickupCount = 0;
};
```

### Creating Invisible Walls on Platforms

Modify `_spawnPlatform` to add invisible walls at both edges of each platform:

```javascript
PlayState._spawnPlatform = function (platform) {
    let sprite = this.platforms.create(platform.x, platform.y, platform.image);
    this.game.physics.enable(sprite);
    sprite.body.allowGravity = false;
    sprite.body.immovable = true;

    // Spawn invisible walls at the left and right edges of this platform
    this._spawnEnemyWall(platform.x, platform.y, 'left');
    this._spawnEnemyWall(platform.x + sprite.width, platform.y, 'right');
};

PlayState._spawnEnemyWall = function (x, y, side) {
    let sprite = this.enemyWalls.create(x, y, 'invisible-wall');

    // Anchor to the bottom of the wall and adjust position based on side
    sprite.anchor.set(side === 'left' ? 1 : 0, 1);

    this.game.physics.enable(sprite);
    sprite.body.immovable = true;
    sprite.body.allowGravity = false;
};
```

- Each platform gets two invisible walls, one at each edge.
- The walls act as barriers that prevent spiders from walking off the edge.
- The anchor is set so the wall aligns to the correct side of the platform.

### Spawning and Animating Spiders

```javascript
PlayState._spawnSpider = function (spider) {
    let sprite = this.spiders.create(spider.x, spider.y, 'spider');
    sprite.anchor.set(0.5, 1);

    // Add the crawl animation
    sprite.animations.add('crawl', [0, 1, 2], 8, true);
    sprite.animations.add('die', [0, 4, 0, 4, 0, 4, 3, 3, 3, 3, 3, 3], 12);
    sprite.animations.play('crawl');

    // Enable physics
    this.game.physics.enable(sprite);

    // Set initial movement speed
    sprite.body.velocity.x = Spider.SPEED;
};

// Spider speed constant
const Spider = { SPEED: 100 };
```

- Spiders have two animations: `crawl` (looping) and `die` (played once on death).
- `velocity.x = 100` starts the spider moving to the right at 100 pixels per second.

### Making Spiders Bounce Off Walls

Add collision handling so spiders reverse direction when hitting invisible walls or platform edges:

```javascript
// In PlayState._handleCollisions:
PlayState._handleCollisions = function () {
    this.game.physics.arcade.collide(this.hero, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.enemyWalls);

    this.game.physics.arcade.overlap(
        this.hero, this.coins, this._onHeroVsCoin, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.spiders, this._onHeroVsEnemy, null, this
    );
};
```

To make spiders reverse direction when colliding with walls, check their velocity each frame and flip them:

```javascript
// In PlayState.update, after collision handling, update spider directions:
PlayState.update = function () {
    this._handleCollisions();
    this._handleInput();

    // Update spider facing direction based on velocity
    this.spiders.forEach(function (spider) {
        if (spider.body.touching.right || spider.body.blocked.right) {
            spider.body.velocity.x = -Spider.SPEED; // Turn left
        } else if (spider.body.touching.left || spider.body.blocked.left) {
            spider.body.velocity.x = Spider.SPEED; // Turn right
        }
    }, this);
};
```

- When a spider touches a wall on its right side, it reverses to move left, and vice versa.
- `body.touching` is set by Phaser after collision resolution.

---

## Death

Implement hero death when touching enemies and the stomp mechanic for killing enemies.

### Hero vs Enemy: Stomp or Die

When the hero overlaps with a spider, check if the hero is falling (stomping) or not:

```javascript
PlayState._onHeroVsEnemy = function (hero, enemy) {
    if (hero.body.velocity.y > 0) {
        // Hero is falling -> stomp the enemy
        enemy.body.velocity.x = 0; // Stop enemy movement
        enemy.body.enable = false; // Disable enemy physics

        // Play die animation then remove the enemy
        enemy.animations.play('die');
        enemy.events.onAnimationComplete.addOnce(function () {
            enemy.kill();
        });

        // Bounce the hero up after stomping
        hero.body.velocity.y = -JUMP_SPEED / 2;

        this.sfx.stomp.play();
    } else {
        // Hero touched enemy from side or below -> die
        this._killHero();
    }
};

PlayState._killHero = function () {
    this.hero.kill();
    // Restart the level after a short delay
    this.game.time.events.add(500, function () {
        this.game.state.restart(true, false, { level: this.level });
    }, this);
};
```

- If `hero.body.velocity.y > 0`, the hero is moving downward (falling), indicating a stomp.
- On stomp: the enemy stops, plays its death animation, and is removed. The hero bounces up.
- If the hero is not falling, the hero dies. `this.hero.kill()` removes the hero from the game.
- After 500ms, the entire state is restarted, effectively reloading the level.

### Add Stomp Sound

```javascript
// In PlayState.create, add to sfx:
this.sfx = {
    jump: this.game.add.audio('sfx:jump'),
    coin: this.game.add.audio('sfx:coin'),
    stomp: this.game.add.audio('sfx:stomp')
};
```

### Adding a Death Animation for the Hero

Make the hero flash and fall off the screen when dying:

```javascript
PlayState._killHero = function () {
    this.hero.alive = false;

    // Play a "dying" visual: the hero jumps up and falls off screen
    this.hero.body.velocity.y = -JUMP_SPEED / 2;
    this.hero.body.velocity.x = 0;
    this.hero.body.allowGravity = true;

    // Disable collisions so the hero falls through platforms
    this.hero.body.collideWorldBounds = false;

    // Restart after a delay
    this.game.time.events.add(1000, function () {
        this.game.state.restart(true, false, { level: this.level });
    }, this);
};
```

### Guarding Input When Dead

Prevent input from controlling the hero after death:

```javascript
PlayState._handleInput = function () {
    if (!this.hero.alive) { return; }

    if (this.keys.left.isDown) {
        this.hero.body.velocity.x = -SPEED;
    } else if (this.keys.right.isDown) {
        this.hero.body.velocity.x = SPEED;
    } else {
        this.hero.body.velocity.x = 0;
    }

    if (this.keys.up.isDown) {
        this._jump();
    }
};
```

- `this.hero.alive` is set to `false` in `_killHero`, so input is ignored after death and the hero falls off screen naturally.

---

## Scoreboard

Display the number of collected coins on screen using a bitmap font.

### Loading the Bitmap Font

```javascript
// In PlayState.preload:
this.game.load.image('font:numbers', 'images/numbers.png');
this.game.load.image('icon:coin', 'images/coin_icon.png');
```

### Creating the HUD

Create a fixed HUD (heads-up display) that shows the coin icon and count:

```javascript
PlayState._createHud = function () {
    let coinIcon = this.game.make.image(0, 0, 'icon:coin');

    // Create a dynamic text label for the coin count
    this.hud = this.game.add.group();

    // Use a retroFont or a regular text object for the score
    let scoreStyle = {
        font: '30px monospace',
        fill: '#fff'
    };
    this.coinFont = this.game.add.text(
        coinIcon.width + 7, 0, 'x0', scoreStyle
    );

    this.hud.add(coinIcon);
    this.hud.add(this.coinFont);

    this.hud.position.set(10, 10);
    this.hud.fixedToCamera = true;
};
```

Alternatively, using Phaser's `RetroFont` for pixel-art number rendering:

```javascript
PlayState._createHud = function () {
    // Bitmap-based number rendering using RetroFont
    this.coinFont = this.game.add.retroFont(
        'font:numbers', 20, 26,
        '0123456789X ', 6
    );

    let coinIcon = this.game.make.image(0, 0, 'icon:coin');

    let coinScoreImg = this.game.make.image(
        coinIcon.x + coinIcon.width + 7, 0, this.coinFont
    );

    this.hud = this.game.add.group();
    this.hud.add(coinIcon);
    this.hud.add(coinScoreImg);
    this.hud.position.set(10, 10);
    this.hud.fixedToCamera = true;
};
```

- `retroFont` creates a bitmap font from a spritesheet containing character glyphs.
- Parameters: image key, character width, character height, character set string, number of characters per row.

### Calling createHud in create

```javascript
PlayState.create = function () {
    this.game.add.image(0, 0, 'background');

    this._loadLevel(this.game.cache.getJSON('level:1'));

    // Create the HUD
    this._createHud();
};
```

### Updating the Score Display

Update the score text whenever a coin is collected:

```javascript
PlayState._onHeroVsCoin = function (hero, coin) {
    this.sfx.coin.play();
    coin.kill();
    this.coinPickupCount++;

    // Update the HUD
    this.coinFont.text = 'x' + this.coinPickupCount;
};
```

---

## Animations for the Main Character

Replace the static hero image with a spritesheet and add animations for different states: idle (stopped), running, jumping, and falling.

### Loading the Hero Spritesheet

Replace the single image load with a spritesheet in `preload`:

```javascript
// Replace: this.game.load.image('hero', 'images/hero_stopped.png');
// With:
this.game.load.spritesheet('hero', 'images/hero.png', 36, 42);
```

- The hero spritesheet is 36 pixels wide and 42 pixels tall per frame.
- Frames include idle, walk cycle, jump, and fall poses.

### Defining Animations

In `_spawnCharacters`, add animation definitions after creating the hero sprite:

```javascript
PlayState._spawnCharacters = function (data) {
    this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
    this.hero.anchor.set(0.5, 1);
    this.game.physics.enable(this.hero);

    // Define animations
    this.hero.animations.add('stop', [0]);               // Single frame: idle
    this.hero.animations.add('run', [1, 2], 8, true);    // 2 frames at 8fps, looping
    this.hero.animations.add('jump', [3]);                // Single frame: jumping up
    this.hero.animations.add('fall', [4]);                // Single frame: falling down
};
```

- `animations.add(name, frames, fps, loop)` registers an animation with the given name.
- Single-frame animations like `stop`, `jump`, and `fall` effectively set a static pose.
- The `run` animation alternates between frames 1 and 2 at 8fps.

### Playing the Correct Animation

Add a method to determine and play the right animation based on the hero's current state:

```javascript
PlayState._getAnimationName = function () {
    let name = 'stop'; // Default: standing still

    if (!this.hero.alive) {
        name = 'stop'; // Use idle frame when dead
    } else if (this.hero.body.velocity.y < 0) {
        name = 'jump'; // Moving upward
    } else if (this.hero.body.velocity.y > 0 && !this.hero.body.touching.down) {
        name = 'fall'; // Moving downward and not on ground
    } else if (this.hero.body.velocity.x !== 0 && this.hero.body.touching.down) {
        name = 'run';  // Moving horizontally on the ground
    }

    return name;
};
```

### Flipping the Sprite Based on Direction

Update the hero's facing direction and play the animation in `update`:

```javascript
PlayState.update = function () {
    this._handleCollisions();
    this._handleInput();

    // Flip sprite based on movement direction
    if (this.hero.body.velocity.x < 0) {
        this.hero.scale.x = -1; // Face left
    } else if (this.hero.body.velocity.x > 0) {
        this.hero.scale.x = 1;  // Face right
    }

    // Play the appropriate animation
    this.hero.animations.play(this._getAnimationName());

    // Update spider directions
    this.spiders.forEach(function (spider) {
        if (spider.body.touching.right || spider.body.blocked.right) {
            spider.body.velocity.x = -Spider.SPEED;
        } else if (spider.body.touching.left || spider.body.blocked.left) {
            spider.body.velocity.x = Spider.SPEED;
        }
    }, this);
};
```

- `this.hero.scale.x = -1` flips the sprite horizontally to face left. Setting it to `1` faces right. Because the anchor is at `(0.5, 1)`, the flip looks natural.
- `animations.play()` only restarts the animation if the name changes, so calling it every frame is safe and efficient.

---

## Win Condition

Add a door and key mechanic: the hero must collect a key, then reach the door to complete the level.

### Loading Door and Key Assets

```javascript
// In PlayState.preload:
this.game.load.spritesheet('door', 'images/door.png', 42, 66);
this.game.load.spritesheet('key', 'images/key.png', 20, 22);  // Key bobbing animation
this.game.load.image('icon:key', 'images/key_icon.png');

this.game.load.audio('sfx:key', 'audio/sfx/key.wav');
this.game.load.audio('sfx:door', 'audio/sfx/door.wav');
```

### Spawning the Door and Key

Update `_loadLevel` and `_spawnCharacters`:

```javascript
PlayState._loadLevel = function (data) {
    this.platforms = this.game.add.group();
    this.coins = this.game.add.group();
    this.spiders = this.game.add.group();
    this.enemyWalls = this.game.add.group();
    this.bgDecoration = this.game.add.group();

    // Must spawn decorations first (background layer)
    // Spawn door before hero so it renders behind the hero
    data.platforms.forEach(this._spawnPlatform, this);
    data.coins.forEach(this._spawnCoin, this);
    data.spiders.forEach(this._spawnSpider, this);

    this._spawnDoor(data.door.x, data.door.y);
    this._spawnKey(data.key.x, data.key.y);
    this._spawnCharacters({ hero: data.hero });

    this.enemyWalls.visible = false;

    this.coinPickupCount = 0;
    this.hasKey = false;
};

PlayState._spawnDoor = function (x, y) {
    this.door = this.bgDecoration.create(x, y, 'door');
    this.door.anchor.setTo(0.5, 1);

    this.game.physics.enable(this.door);
    this.door.body.allowGravity = false;
};

PlayState._spawnKey = function (x, y) {
    this.key = this.bgDecoration.create(x, y, 'key');
    this.key.anchor.set(0.5, 0.5);

    this.game.physics.enable(this.key);
    this.key.body.allowGravity = false;

    // Add a bobbing up-and-down tween to the key
    this.key.y -= 3;
    this.game.add.tween(this.key)
        .to({ y: this.key.y + 6 }, 800, Phaser.Easing.Sinusoidal.InOut)
        .yoyo(true)
        .loop()
        .start();
};
```

- The door is placed in a background decoration group so it renders behind the hero.
- The key has a sinusoidal bobbing tween that moves it 6 pixels up and down over 800ms, looping forever.

### Collecting the Key and Opening the Door

Add key and door sound effects to the sfx object:

```javascript
// In PlayState.create sfx:
this.sfx = {
    jump: this.game.add.audio('sfx:jump'),
    coin: this.game.add.audio('sfx:coin'),
    stomp: this.game.add.audio('sfx:stomp'),
    key: this.game.add.audio('sfx:key'),
    door: this.game.add.audio('sfx:door')
};
```

Add overlap detection for the key and door in `_handleCollisions`:

```javascript
PlayState._handleCollisions = function () {
    this.game.physics.arcade.collide(this.hero, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.enemyWalls);

    this.game.physics.arcade.overlap(
        this.hero, this.coins, this._onHeroVsCoin, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.spiders, this._onHeroVsEnemy, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.key, this._onHeroVsKey, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.door, this._onHeroVsDoor,
        // Only trigger if the hero has the key
        function (hero, door) {
            return this.hasKey && hero.body.touching.down;
        }, this
    );
};
```

- The door overlap has a **process callback** (the fourth argument) that only triggers the overlap callback when `this.hasKey` is true and the hero is standing on something. This prevents the hero from entering the door while falling or without the key.

### Key and Door Callbacks

```javascript
PlayState._onHeroVsKey = function (hero, key) {
    this.sfx.key.play();
    key.kill();
    this.hasKey = true;
};

PlayState._onHeroVsDoor = function (hero, door) {
    this.sfx.door.play();

    // Freeze the hero and play the door opening animation
    hero.body.velocity.x = 0;
    hero.body.velocity.y = 0;
    hero.body.enable = false;

    // Play door open animation (transition from closed to open frame)
    door.frame = 1; // Switch to "open" frame

    // Advance to the next level after a short delay
    this.game.time.events.add(500, this._goToNextLevel, this);
};

PlayState._goToNextLevel = function () {
    this.camera.fade('#000');
    this.camera.onFadeComplete.addOnce(function () {
        this.game.state.restart(true, false, {
            level: this.level + 1
        });
    }, this);
};
```

- When the hero touches the key, the key is removed and `hasKey` is set to `true`.
- When the hero reaches the door (with the key), the hero freezes, the door opens, and after a delay the game transitions to the next level.
- `camera.fade()` creates a fade-to-black transition for a polished level switch.

### Showing the Key Icon in the HUD

Update `_createHud` to show whether the hero has collected the key:

```javascript
PlayState._createHud = function () {
    this.keyIcon = this.game.make.image(0, 19, 'icon:key');
    this.keyIcon.anchor.set(0, 0.5);

    // ... existing coin HUD code ...

    this.hud.add(this.keyIcon);
    this.hud.add(coinIcon);
    this.hud.add(coinScoreImg);
    this.hud.position.set(10, 10);
    this.hud.fixedToCamera = true;
};
```

Update the key icon appearance each frame in `update`:

```javascript
// In PlayState.update, add:
this.keyIcon.frame = this.hasKey ? 1 : 0;
```

- Frame 0 shows a grayed-out key icon; frame 1 shows the collected key icon.

---

## Switching Levels

Support multiple levels by loading different JSON files based on a level index.

### Passing Level Number Through init

Modify `init` to accept a level parameter:

```javascript
PlayState.init = function (data) {
    this.game.renderer.renderSession.roundPixels = true;

    this.keys = this.game.input.keyboard.addKeys({
        left: Phaser.KeyCode.LEFT,
        right: Phaser.KeyCode.RIGHT,
        up: Phaser.KeyCode.UP
    });

    this.game.physics.startSystem(Phaser.Physics.ARCADE);
    this.game.physics.arcade.gravity.y = 1200;

    // Store the current level number (default to 0)
    this.level = (data.level || 0) % LEVEL_COUNT;
};

const LEVEL_COUNT = 2; // Total number of levels
```

- `data` is an object passed from `game.state.start()` or `game.state.restart()`.
- The modulo operation (`% LEVEL_COUNT`) wraps around to level 0 after the last level, creating an infinite loop of levels.

### Loading Level Data Dynamically

Update `preload` to load the correct level based on `this.level`:

```javascript
PlayState.preload = function () {
    this.game.load.image('background', 'images/background.png');

    // Load the current level's JSON data
    this.game.load.json('level:0', 'data/level00.json');
    this.game.load.json('level:1', 'data/level01.json');

    // ... load all other assets ...
};
```

Update `create` to use the correct level data:

```javascript
PlayState.create = function () {
    this.sfx = {
        jump: this.game.add.audio('sfx:jump'),
        coin: this.game.add.audio('sfx:coin'),
        stomp: this.game.add.audio('sfx:stomp'),
        key: this.game.add.audio('sfx:key'),
        door: this.game.add.audio('sfx:door')
    };

    this.game.add.image(0, 0, 'background');

    // Load level data based on current level number
    this._loadLevel(this.game.cache.getJSON('level:' + this.level));

    this._createHud();
};
```

### Starting the Game at Level 0

Update the initial state start to pass level 0:

```javascript
window.onload = function () {
    let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');
    game.state.add('play', PlayState);
    game.state.start('play', true, false, { level: 0 });
};
```

- The third and fourth `start` arguments control world/cache clearing. `true, false` keeps the cache between restarts (so assets do not need to be reloaded) but clears the world.
- `{ level: 0 }` is passed to `init` as the `data` parameter.

### Level Transition Flow

The complete level flow is:

1. Hero collects key -> `hasKey = true`
2. Hero reaches door -> `_onHeroVsDoor` fires
3. Camera fades to black -> `_goToNextLevel` fires
4. State restarts with `{ level: this.level + 1 }`
5. `init` receives the new level number
6. The correct level JSON is loaded and the game continues

---

## Moving Forward

Congratulations -- you have built a complete 2D platformer. Here are ideas for extending the game further:

### Suggested Improvements

- **Mobile / touch controls:** Add on-screen buttons or swipe gestures using `game.input.onDown` for touch-enabled devices.
- **More levels:** Create additional JSON level files with new platform layouts, coin placements, and enemy configurations.
- **Menu screen:** Add a `MenuState` with a title screen and start button before entering `PlayState`.
- **Game over screen:** Instead of instantly restarting, show a "Game Over" screen with the score.
- **Lives system:** Give the hero multiple lives instead of instant restart.
- **Power-ups:** Add items like speed boosts, double jump, or invincibility.
- **Moving platforms:** Create platforms that travel along a path using tweens.
- **Different enemy types:** Add flying enemies, enemies that shoot projectiles, or enemies with different movement patterns.
- **Parallax scrolling:** Add multiple background layers that scroll at different speeds for depth.
- **Camera scrolling:** For levels wider than the screen, use `game.camera.follow(this.hero)` to scroll with the hero.
- **Sound and music:** Add background music and additional sound effects for a more polished experience.
- **Particle effects:** Use Phaser's particle emitter for coin collection sparkles, enemy death effects, or dust when landing.

### Full Game Source Reference

Below is the complete `main.js` file combining all steps for reference. This represents the final state of the game with all features:

```javascript
// =============================================================================
// Constants
// =============================================================================

const SPEED = 200;
const JUMP_SPEED = 600;
const LEVEL_COUNT = 2;
const Spider = { SPEED: 100 };

// =============================================================================
// Game State: PlayState
// =============================================================================

PlayState = {};

// -----------------------------------------------------------------------------
// init
// -----------------------------------------------------------------------------

PlayState.init = function (data) {
    this.game.renderer.renderSession.roundPixels = true;

    this.keys = this.game.input.keyboard.addKeys({
        left: Phaser.KeyCode.LEFT,
        right: Phaser.KeyCode.RIGHT,
        up: Phaser.KeyCode.UP
    });

    this.game.physics.startSystem(Phaser.Physics.ARCADE);
    this.game.physics.arcade.gravity.y = 1200;

    this.level = (data.level || 0) % LEVEL_COUNT;
};

// -----------------------------------------------------------------------------
// preload
// -----------------------------------------------------------------------------

PlayState.preload = function () {
    // Background
    this.game.load.image('background', 'images/background.png');

    // Level data
    this.game.load.json('level:0', 'data/level00.json');
    this.game.load.json('level:1', 'data/level01.json');

    // Platform tiles
    this.game.load.image('ground', 'images/ground.png');
    this.game.load.image('grass:8x1', 'images/grass_8x1.png');
    this.game.load.image('grass:6x1', 'images/grass_6x1.png');
    this.game.load.image('grass:4x1', 'images/grass_4x1.png');
    this.game.load.image('grass:2x1', 'images/grass_2x1.png');
    this.game.load.image('grass:1x1', 'images/grass_1x1.png');

    // Characters
    this.game.load.spritesheet('hero', 'images/hero.png', 36, 42);
    this.game.load.spritesheet('spider', 'images/spider.png', 42, 32);
    this.game.load.image('invisible-wall', 'images/invisible_wall.png');

    // Collectibles
    this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
    this.game.load.spritesheet('key', 'images/key.png', 20, 22);
    this.game.load.spritesheet('door', 'images/door.png', 42, 66);

    // HUD
    this.game.load.image('icon:coin', 'images/coin_icon.png');
    this.game.load.image('icon:key', 'images/key_icon.png');
    this.game.load.image('font:numbers', 'images/numbers.png');

    // Audio
    this.game.load.audio('sfx:jump', 'audio/sfx/jump.wav');
    this.game.load.audio('sfx:coin', 'audio/sfx/coin.wav');
    this.game.load.audio('sfx:stomp', 'audio/sfx/stomp.wav');
    this.game.load.audio('sfx:key', 'audio/sfx/key.wav');
    this.game.load.audio('sfx:door', 'audio/sfx/door.wav');
};

// -----------------------------------------------------------------------------
// create
// -----------------------------------------------------------------------------

PlayState.create = function () {
    // Sound effects
    this.sfx = {
        jump: this.game.add.audio('sfx:jump'),
        coin: this.game.add.audio('sfx:coin'),
        stomp: this.game.add.audio('sfx:stomp'),
        key: this.game.add.audio('sfx:key'),
        door: this.game.add.audio('sfx:door')
    };

    // Background
    this.game.add.image(0, 0, 'background');

    // Load level
    this._loadLevel(this.game.cache.getJSON('level:' + this.level));

    // HUD
    this._createHud();
};

// -----------------------------------------------------------------------------
// update
// -----------------------------------------------------------------------------

PlayState.update = function () {
    this._handleCollisions();
    this._handleInput();

    // Update hero sprite direction and animation
    if (this.hero.body.velocity.x < 0) {
        this.hero.scale.x = -1;
    } else if (this.hero.body.velocity.x > 0) {
        this.hero.scale.x = 1;
    }
    this.hero.animations.play(this._getAnimationName());

    // Update spider directions when hitting walls
    this.spiders.forEach(function (spider) {
        if (spider.body.touching.right || spider.body.blocked.right) {
            spider.body.velocity.x = -Spider.SPEED;
        } else if (spider.body.touching.left || spider.body.blocked.left) {
            spider.body.velocity.x = Spider.SPEED;
        }
    }, this);

    // Update key icon in HUD
    this.keyIcon.frame = this.hasKey ? 1 : 0;
};

// -----------------------------------------------------------------------------
// Level Loading
// -----------------------------------------------------------------------------

PlayState._loadLevel = function (data) {
    // Create groups (order matters for rendering layers)
    this.bgDecoration = this.game.add.group();
    this.platforms = this.game.add.group();
    this.coins = this.game.add.group();
    this.spiders = this.game.add.group();
    this.enemyWalls = this.game.add.group();

    // Spawn entities from level data
    data.platforms.forEach(this._spawnPlatform, this);
    data.coins.forEach(this._spawnCoin, this);
    data.spiders.forEach(this._spawnSpider, this);

    this._spawnDoor(data.door.x, data.door.y);
    this._spawnKey(data.key.x, data.key.y);
    this._spawnCharacters({ hero: data.hero });

    // Hide invisible walls
    this.enemyWalls.visible = false;

    // Initialize game state
    this.coinPickupCount = 0;
    this.hasKey = false;
};

// -----------------------------------------------------------------------------
// Spawn Methods
// -----------------------------------------------------------------------------

PlayState._spawnPlatform = function (platform) {
    let sprite = this.platforms.create(platform.x, platform.y, platform.image);
    this.game.physics.enable(sprite);
    sprite.body.allowGravity = false;
    sprite.body.immovable = true;

    // Add invisible walls at both edges for enemy AI
    this._spawnEnemyWall(platform.x, platform.y, 'left');
    this._spawnEnemyWall(platform.x + sprite.width, platform.y, 'right');
};

PlayState._spawnEnemyWall = function (x, y, side) {
    let sprite = this.enemyWalls.create(x, y, 'invisible-wall');
    sprite.anchor.set(side === 'left' ? 1 : 0, 1);
    this.game.physics.enable(sprite);
    sprite.body.immovable = true;
    sprite.body.allowGravity = false;
};

PlayState._spawnCharacters = function (data) {
    this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
    this.hero.anchor.set(0.5, 1);
    this.game.physics.enable(this.hero);
    this.hero.body.collideWorldBounds = true;

    // Hero animations
    this.hero.animations.add('stop', [0]);
    this.hero.animations.add('run', [1, 2], 8, true);
    this.hero.animations.add('jump', [3]);
    this.hero.animations.add('fall', [4]);
};

PlayState._spawnCoin = function (coin) {
    let sprite = this.coins.create(coin.x, coin.y, 'coin');
    sprite.anchor.set(0.5, 0.5);
    this.game.physics.enable(sprite);
    sprite.body.allowGravity = false;

    sprite.animations.add('rotate', [0, 1, 2, 1], 6, true);
    sprite.animations.play('rotate');
};

PlayState._spawnSpider = function (spider) {
    let sprite = this.spiders.create(spider.x, spider.y, 'spider');
    sprite.anchor.set(0.5, 1);
    this.game.physics.enable(sprite);

    sprite.animations.add('crawl', [0, 1, 2], 8, true);
    sprite.animations.add('die', [0, 4, 0, 4, 0, 4, 3, 3, 3, 3, 3, 3], 12);
    sprite.animations.play('crawl');

    sprite.body.velocity.x = Spider.SPEED;
};

PlayState._spawnDoor = function (x, y) {
    this.door = this.bgDecoration.create(x, y, 'door');
    this.door.anchor.setTo(0.5, 1);
    this.game.physics.enable(this.door);
    this.door.body.allowGravity = false;
};

PlayState._spawnKey = function (x, y) {
    this.key = this.bgDecoration.create(x, y, 'key');
    this.key.anchor.set(0.5, 0.5);
    this.game.physics.enable(this.key);
    this.key.body.allowGravity = false;

    // Bobbing tween
    this.key.y -= 3;
    this.game.add.tween(this.key)
        .to({ y: this.key.y + 6 }, 800, Phaser.Easing.Sinusoidal.InOut)
        .yoyo(true)
        .loop()
        .start();
};

// -----------------------------------------------------------------------------
// Input
// -----------------------------------------------------------------------------

PlayState._handleInput = function () {
    if (!this.hero.alive) { return; }

    if (this.keys.left.isDown) {
        this.hero.body.velocity.x = -SPEED;
    } else if (this.keys.right.isDown) {
        this.hero.body.velocity.x = SPEED;
    } else {
        this.hero.body.velocity.x = 0;
    }

    if (this.keys.up.isDown) {
        this._jump();
    }
};

PlayState._jump = function () {
    let canJump = this.hero.body.touching.down;
    if (canJump) {
        this.hero.body.velocity.y = -JUMP_SPEED;
        this.sfx.jump.play();
    }
    return canJump;
};

// -----------------------------------------------------------------------------
// Collisions
// -----------------------------------------------------------------------------

PlayState._handleCollisions = function () {
    // Physical collisions
    this.game.physics.arcade.collide(this.hero, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.platforms);
    this.game.physics.arcade.collide(this.spiders, this.enemyWalls);

    // Overlap detection (no physical push)
    this.game.physics.arcade.overlap(
        this.hero, this.coins, this._onHeroVsCoin, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.spiders, this._onHeroVsEnemy, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.key, this._onHeroVsKey, null, this
    );
    this.game.physics.arcade.overlap(
        this.hero, this.door, this._onHeroVsDoor,
        function (hero, door) {
            return this.hasKey && hero.body.touching.down;
        }, this
    );
};

// -----------------------------------------------------------------------------
// Collision Callbacks
// -----------------------------------------------------------------------------

PlayState._onHeroVsCoin = function (hero, coin) {
    this.sfx.coin.play();
    coin.kill();
    this.coinPickupCount++;
    this.coinFont.text = 'x' + this.coinPickupCount;
};

PlayState._onHeroVsEnemy = function (hero, enemy) {
    if (hero.body.velocity.y > 0) {
        // Stomp: hero is falling onto the enemy
        enemy.body.velocity.x = 0;
        enemy.body.enable = false;
        enemy.animations.play('die');
        enemy.events.onAnimationComplete.addOnce(function () {
            enemy.kill();
        });
        hero.body.velocity.y = -JUMP_SPEED / 2;
        this.sfx.stomp.play();
    } else {
        // Hero dies
        this._killHero();
    }
};

PlayState._onHeroVsKey = function (hero, key) {
    this.sfx.key.play();
    key.kill();
    this.hasKey = true;
};

PlayState._onHeroVsDoor = function (hero, door) {
    this.sfx.door.play();
    hero.body.velocity.x = 0;
    hero.body.velocity.y = 0;
    hero.body.enable = false;

    door.frame = 1; // Open door

    this.game.time.events.add(500, this._goToNextLevel, this);
};

// -----------------------------------------------------------------------------
// Death and Level Transitions
// -----------------------------------------------------------------------------

PlayState._killHero = function () {
    this.hero.alive = false;
    this.hero.body.velocity.y = -JUMP_SPEED / 2;
    this.hero.body.velocity.x = 0;
    this.hero.body.allowGravity = true;
    this.hero.body.collideWorldBounds = false;

    this.game.time.events.add(1000, function () {
        this.game.state.restart(true, false, { level: this.level });
    }, this);
};

PlayState._goToNextLevel = function () {
    this.camera.fade('#000');
    this.camera.onFadeComplete.addOnce(function () {
        this.game.state.restart(true, false, {
            level: this.level + 1
        });
    }, this);
};

// -----------------------------------------------------------------------------
// Animations
// -----------------------------------------------------------------------------

PlayState._getAnimationName = function () {
    let name = 'stop';

    if (!this.hero.alive) {
        name = 'stop';
    } else if (this.hero.body.velocity.y < 0) {
        name = 'jump';
    } else if (this.hero.body.velocity.y > 0 && !this.hero.body.touching.down) {
        name = 'fall';
    } else if (this.hero.body.velocity.x !== 0 && this.hero.body.touching.down) {
        name = 'run';
    }

    return name;
};

// -----------------------------------------------------------------------------
// HUD
// -----------------------------------------------------------------------------

PlayState._createHud = function () {
    this.keyIcon = this.game.make.image(0, 19, 'icon:key');
    this.keyIcon.anchor.set(0, 0.5);

    let coinIcon = this.game.make.image(
        this.keyIcon.width + 7, 0, 'icon:coin'
    );

    let scoreStyle = { font: '24px monospace', fill: '#fff' };
    this.coinFont = this.game.add.text(
        coinIcon.x + coinIcon.width + 7, 0, 'x0', scoreStyle
    );

    this.hud = this.game.add.group();
    this.hud.add(this.keyIcon);
    this.hud.add(coinIcon);
    this.hud.add(this.coinFont);
    this.hud.position.set(10, 10);
    this.hud.fixedToCamera = true;
};

// =============================================================================
// Entry Point
// =============================================================================

window.onload = function () {
    let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');
    game.state.add('play', PlayState);
    game.state.start('play', true, false, { level: 0 });
};
```

### Key Concepts Summary

| Concept | Phaser API | Purpose |
|---------|-----------|---------|
| Game instance | `new Phaser.Game(w, h, renderer, container)` | Creates the game canvas and engine |
| Game states | `game.state.add()` / `game.state.start()` | Organizes code into init/preload/create/update lifecycle |
| Loading images | `game.load.image(key, path)` | Loads a static image asset |
| Loading spritesheets | `game.load.spritesheet(key, path, fw, fh)` | Loads an animated spritesheet |
| Loading JSON | `game.load.json(key, path)` | Loads JSON data (level definitions) |
| Loading audio | `game.load.audio(key, path)` | Loads a sound effect |
| Sprite groups | `game.add.group()` | Container for related sprites; enables batch collision detection |
| Physics bodies | `game.physics.enable(sprite)` | Adds an Arcade Physics body to a sprite |
| Gravity | `game.physics.arcade.gravity.y` | Global downward acceleration |
| Collision | `arcade.collide(a, b)` | Physical collision resolution (sprites push each other) |
| Overlap | `arcade.overlap(a, b, callback)` | Detection without physical push (for pickups) |
| Velocity | `sprite.body.velocity.x/y` | Movement speed in pixels per second |
| Immovable | `sprite.body.immovable = true` | Prevents sprite from being pushed by collisions |
| Animations | `sprite.animations.add(name, frames, fps, loop)` | Defines a frame animation |
| Tweens | `game.add.tween(target).to(props, duration, easing)` | Smooth property animation |
| Keyboard input | `game.input.keyboard.addKeys({...})` | Captures specific keyboard keys |
| Camera | `this.camera.fade()` | Screen transition effects |
| Anchor | `sprite.anchor.set(x, y)` | Sets the origin point for positioning and rotation |
| Sprite flipping | `sprite.scale.x = -1` | Horizontally mirrors the sprite |


path: assets/gameBase-template-reop.md
content: # GameBase Template Repository

A feature-rich, opinionated starter template for 2D game projects built with **Haxe** and the **Heaps** game engine. Created and maintained by **Sebastien Benard** (deepnight), the lead developer behind *Dead Cells*. GameBase provides a production-tested foundation with entity management, level integration via LDtk, rendering pipeline, and a game loop architecture -- all designed to let developers skip boilerplate and jump straight into game-specific logic.

**Repository:** [github.com/deepnight/gameBase](https://github.com/deepnight/gameBase)
**Author:** [Sebastien Benard / deepnight](https://deepnight.net)
**Technology:** Haxe + Heaps (HashLink or JS targets)
**Level editor integration:** [LDtk](https://ldtk.io)

---

## Purpose

GameBase exists to solve the "blank project" problem. Instead of setting up rendering, entity systems, camera controls, debug overlays, and level loading from scratch, developers clone this repository and begin implementing game-specific mechanics immediately. It reflects patterns refined through commercial game development, particularly from the development of *Dead Cells*.

Key benefits:
- Pre-built entity system with grid-based positioning and sub-pixel precision
- LDtk level editor integration for visual level design
- Built-in debug tools and overlays
- Frame-rate independent game loop with fixed-step updates
- Camera system with follow, shake, zoom, and clamp
- Configurable Controller/input management
- Scalable rendering pipeline with Heaps

---

## Repository Structure

```
gameBase/
  src/
    game/
      App.hx              -- Application entry point and initialization
      Game.hx             -- Main game process, holds level and entities
      Entity.hx           -- Base entity class with grid coords, velocity, animation
      Level.hx            -- Level loading and collision map from LDtk
      Camera.hx           -- Camera follow, shake, zoom, clamping
      Fx.hx               -- Visual effects (particles, flashes, etc.)
      Types.hx            -- Enums, typedefs, and constants
      en/
        Hero.hx            -- Player entity (example implementation)
        Mob.hx             -- Enemy entity (example implementation)
    import.hx             -- Global imports (available everywhere)
  res/
    atlas/                 -- Sprite sheets and texture atlases
    levels/                -- LDtk level project files
    fonts/                 -- Bitmap fonts
  .ldtk                   -- LDtk project file (root)
  build.hxml              -- Haxe compiler configuration
  Makefile                -- Build/run shortcuts
  README.md
```

---

## Key Files and Their Roles

### `src/game/App.hx` -- Application Entry Point

The main application class that extends `dn.Process`. Handles:
- Window/display initialization
- Scene management (root scene graph)
- Global input controller setup
- Debug toggle and console

```haxe
class App extends dn.Process {
  public static var ME : App;

  override function init() {
    ME = this;
    // Initialize rendering, controller, assets
    new Game();
  }
}
```

### `src/game/Game.hx` -- Game Process

Manages the active game session:
- Holds reference to the current `Level`
- Manages all active `Entity` instances (via a global linked list)
- Handles pause, game-over, and restart logic
- Coordinates camera and effects

```haxe
class Game extends dn.Process {
  public var level : Level;
  public var hero : en.Hero;
  public var fx : Fx;
  public var camera : Camera;

  public function new() {
    super(App.ME);
    level = new Level();
    fx = new Fx();
    camera = new Camera();
    hero = new en.Hero();
  }
}
```

### `src/game/Entity.hx` -- Base Entity

The core entity class featuring:
- **Grid-based positioning:** `cx`, `cy` (integer cell coordinates) plus `xr`, `yr` (sub-cell ratio 0.0 to 1.0) for smooth sub-pixel movement
- **Velocity and friction:** `dx`, `dy` (velocity) with configurable `frictX`, `frictY`
- **Gravity:** Optional per-entity gravity
- **Sprite management:** Animated sprite via Heaps `h2d.Anim` or `dn.heaps.HSprite`
- **Lifecycle:** `update()`, `fixedUpdate()`, `postUpdate()`, `dispose()`
- **Collision helpers:** `hasCollision(cx, cy)` check against the level collision map

```haxe
class Entity {
  // Grid position
  public var cx : Int = 0;   // Cell X
  public var cy : Int = 0;   // Cell Y
  public var xr : Float = 0.5; // X ratio within cell (0..1)
  public var yr : Float = 1.0; // Y ratio within cell (0..1)

  // Velocity
  public var dx : Float = 0;
  public var dy : Float = 0;

  // Pixel position (computed)
  public var attachX(get,never) : Float;
  inline function get_attachX() return (cx + xr) * Const.GRID;
  public var attachY(get,never) : Float;
  inline function get_attachY() return (cy + yr) * Const.GRID;

  // Physics step
  public function fixedUpdate() {
    xr += dx;
    dx *= frictX;

    // X collision
    if (xr > 1) { cx++; xr--; }
    if (xr < 0) { cx--; xr++; }

    yr += dy;
    dy *= frictY;

    // Y collision
    if (yr > 1) { cy++; yr--; }
    if (yr < 0) { cy--; yr++; }
  }
}
```

### `src/game/Level.hx` -- Level Management

Loads and manages level data from LDtk project files:
- Parses tile layers, entity layers, and int grid layers
- Builds a collision grid (`hasCollision(cx, cy)`)
- Provides helper methods to query the level structure

```haxe
class Level {
  var data : ldtk.Level;
  var collisions : Map<Int, Bool>;

  public function new(ldtkLevel) {
    data = ldtkLevel;
    // Parse IntGrid layer for collision marks
    for (cy in 0...data.l_Collisions.cHei)
      for (cx in 0...data.l_Collisions.cWid)
        if (data.l_Collisions.getInt(cx, cy) == 1)
          collisions.set(coordId(cx, cy), true);
  }

  public inline function hasCollision(cx:Int, cy:Int) : Bool {
    return collisions.exists(coordId(cx, cy));
  }
}
```

### `src/game/Camera.hx` -- Camera System

Provides:
- **Target tracking:** Follow an entity smoothly with configurable dead zones
- **Shake:** Screen shake with decay
- **Zoom:** Dynamic zoom in/out
- **Clamping:** Keep the camera within level bounds

### `src/game/Fx.hx` -- Effects System

Particle and visual effect management:
- Particle pools
- Screen flash
- Slow-motion helpers
- Color overlay effects

---

## Technology Stack

### Haxe

A cross-platform, high-level programming language that compiles to multiple targets:
- **HashLink (HL):** Native bytecode VM for desktop (primary dev target)
- **JavaScript (JS):** Browser/web target
- **C/C++:** Via HXCPP for native builds

### Heaps (Heaps.io)

A high-performance, cross-platform 2D/3D game engine:
- GPU-accelerated rendering via OpenGL/DirectX/WebGL
- Scene graph architecture with `h2d.Object` hierarchy
- Sprite batching and texture atlases
- Bitmap font rendering
- Input abstraction

### LDtk

A modern, open-source 2D level editor created by Sebastien Benard:
- Visual, tile-based level design
- IntGrid layers for collision and metadata
- Entity layers for game object placement
- Auto-tiling rules
- Haxe API auto-generated from the project file

---

## Setup Instructions

### Prerequisites

1. **Install Haxe** (4.0+): [haxe.org](https://haxe.org/download/)
2. **Install HashLink** (for desktop target): [hashlink.haxe.org](https://hashlink.haxe.org/)
3. **Install LDtk** (for level editing): [ldtk.io](https://ldtk.io/)

### Getting Started

```bash
# Clone the repository
git clone https://github.com/deepnight/gameBase.git my-game
cd my-game

# Install Haxe dependencies
haxelib install heaps
haxelib install deepnightLibs
haxelib install ldtk-haxe-api

# Build and run (HashLink target)
haxe build.hxml
hl bin/client.hl

# Or use the Makefile (if available)
make run
```

### Using as a Starting Point

1. **Clone or use the template** -- Do not fork; clone into a new directory with your game's name.
2. **Rename the package** -- Update `src/game/` package declarations and project references to match your game.
3. **Edit `build.hxml`** -- Adjust the main class, output path, and target as needed.
4. **Design levels in LDtk** -- Open the `.ldtk` file, define your layers and entities, and export.
5. **Implement entities** -- Create new entity classes in `src/game/en/` extending `Entity`.
6. **Iterate** -- Use the debug console (toggle in-game) for live inspection and tuning.

---

## Build Targets

| Target | Command | Output | Use Case |
|--------|---------|--------|----------|
| HashLink | `haxe build.hxml` | `bin/client.hl` | Development, desktop release |
| JavaScript | `haxe build.js.hxml` | `bin/client.js` | Web/browser builds |
| DirectX/OpenGL | Via HL native | Native executable | Production desktop release |

---

## Debug Features

GameBase includes built-in debug tooling:
- **Debug overlay:** Toggle with a key to show entity bounds, grid, velocities, collision map
- **Console:** In-game command console for toggling flags, teleporting, spawning entities
- **FPS counter:** Visible frame-rate and update-rate monitor
- **Process inspector:** View active processes and their hierarchy

---

## Game Loop Architecture

GameBase uses a fixed-timestep game loop pattern:

```
Each frame:
  1. preUpdate()    -- Input polling, pre-frame logic
  2. fixedUpdate()  -- Physics, movement, collisions (fixed timestep)
     - May run 0-N times per frame to catch up
  3. update()       -- General per-frame logic
  4. postUpdate()   -- Sprite position sync, camera update, rendering prep
```

This ensures physics behavior is consistent regardless of frame rate, while rendering and visual updates remain smooth.

---

## Entity Lifecycle

```
Constructor  -->  init()  -->  [game loop: fixedUpdate/update/postUpdate]  -->  dispose()
```

- **Constructor:** Set initial position, create sprite, register in global entity list
- **fixedUpdate():** Physics step (velocity, friction, gravity, collision)
- **update():** AI, state machine, animation triggers
- **postUpdate():** Sync sprite position to grid coordinates, apply visual effects
- **dispose():** Remove from entity list, destroy sprite, clean up references


path: assets/paddle-game-template.md
content: # Paddle Game Template (2D Breakout)

A complete step-by-step guide for building a 2D Breakout game with pure JavaScript and the HTML5 Canvas API. This template walks through every stage of development, from setting up the canvas to implementing a lives system and polished game loop.

**What you will build:** A classic breakout/paddle game where the player controls a paddle to bounce a ball and destroy a field of bricks, with score tracking, win/lose conditions, keyboard and mouse controls, and a lives system.

**Prerequisites:** Basic to intermediate JavaScript knowledge and familiarity with HTML.

**Source:** Based on the [MDN 2D Breakout Game Tutorial](https://developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript).

---

## Step 1: Create the Canvas and Draw on It

The first step is setting up the HTML document with a `<canvas>` element and learning to draw basic shapes using the 2D rendering context.

### HTML Structure

Create your base HTML file with an embedded canvas element:

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Gamedev Canvas Workshop</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      canvas {
        background: #eeeeee;
        display: block;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="480" height="320"></canvas>

    <script>
      // JavaScript code goes here
    </script>
  </body>
</html>
```

### Getting the Canvas Reference and 2D Context

The canvas element provides a drawing surface. You access it through a 2D rendering context:

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
```

- `canvas` is a reference to the HTML `<canvas>` element.
- `ctx` is the 2D rendering context object, which provides all drawing methods.

### Drawing a Filled Rectangle

Use `rect()` to define a rectangle and `fill()` to render it:

```javascript
ctx.beginPath();
ctx.rect(20, 40, 50, 50);
ctx.fillStyle = "red";
ctx.fill();
ctx.closePath();
```

- The first two parameters (`20, 40`) set the top-left corner coordinates.
- The second two parameters (`50, 50`) set the width and height.
- `fillStyle` sets the fill color.
- `fill()` renders the shape as a solid fill.

### Drawing a Circle

Use `arc()` to define a circle:

```javascript
ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();
```

- `240, 160` -- center x, y coordinates.
- `20` -- radius.
- `0` -- start angle (radians).
- `Math.PI * 2` -- end angle (full circle).
- `false` -- draw clockwise.

### Drawing a Stroked Rectangle (Outline Only)

Use `stroke()` instead of `fill()` for outlines, and `strokeStyle` for outline color:

```javascript
ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgb(0 0 255 / 50%)";
ctx.stroke();
ctx.closePath();
```

- Uses an RGB color with 50% alpha transparency.
- `stroke()` draws only the outline, not a solid fill.

### Key Methods Reference

| Method | Purpose |
|--------|---------|
| `beginPath()` | Start a new drawing path |
| `closePath()` | Close the current path |
| `rect(x, y, width, height)` | Define a rectangle |
| `arc(x, y, radius, startAngle, endAngle, counterclockwise)` | Define a circle or arc |
| `fillStyle` | Set the fill color |
| `fill()` | Fill the shape with the fill color |
| `strokeStyle` | Set the stroke (outline) color |
| `stroke()` | Draw an outline of the shape |

### Complete Code for Step 1

```html
<canvas id="myCanvas" width="480" height="320"></canvas>

<style>
  * { padding: 0; margin: 0; }
  canvas { background: #eeeeee; display: block; margin: 0 auto; }
</style>

<script>
  const canvas = document.getElementById("myCanvas");
  const ctx = canvas.getContext("2d");

  // Filled red square
  ctx.beginPath();
  ctx.rect(20, 40, 50, 50);
  ctx.fillStyle = "red";
  ctx.fill();
  ctx.closePath();

  // Filled green circle
  ctx.beginPath();
  ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
  ctx.fillStyle = "green";
  ctx.fill();
  ctx.closePath();

  // Stroked blue rectangle (semi-transparent)
  ctx.beginPath();
  ctx.rect(160, 10, 100, 40);
  ctx.strokeStyle = "rgb(0 0 255 / 50%)";
  ctx.stroke();
  ctx.closePath();
</script>
```

---

## Step 2: Move the Ball

Now we animate the ball by creating a game loop that redraws the canvas on each frame and updates the ball position using velocity variables.

### Creating the Draw Loop

Define a `draw()` function that executes repeatedly using `setInterval`:

```javascript
function draw() {
  // drawing code
}
setInterval(draw, 10);
```

`setInterval(draw, 10)` calls the `draw` function every 10 milliseconds, creating approximately 100 frames per second.

### Drawing the Ball

Inside the `draw()` function, draw a ball (circle) at a fixed position:

```javascript
ctx.beginPath();
ctx.arc(50, 50, 10, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
```

### Adding Position Variables

Instead of hardcoded positions, use variables so we can update them each frame. Place these above the `draw()` function:

```javascript
let x = canvas.width / 2;
let y = canvas.height - 30;
```

This starts the ball at the horizontal center, near the bottom of the canvas.

### Adding Velocity Variables

Define speed and direction for horizontal (`dx`) and vertical (`dy`) movement:

```javascript
let dx = 2;
let dy = -2;
```

- `dx = 2` moves the ball 2 pixels right per frame.
- `dy = -2` moves the ball 2 pixels up per frame (negative y is upward on canvas).

### Updating Position Each Frame

Add position updates at the end of the `draw()` function:

```javascript
x += dx;
y += dy;
```

### Clearing the Canvas

Without clearing, the ball leaves a trail. Add `clearRect()` at the start of each frame:

```javascript
ctx.clearRect(0, 0, canvas.width, canvas.height);
```

### Refactoring Into a Separate drawBall() Function

For clean, maintainable code, separate the ball-drawing logic:

```javascript
function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
```

### Complete Code for Step 2

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, 10, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  x += dx;
  y += dy;
}

setInterval(draw, 10);
```

**Key concepts:**
- **Animation loop**: `setInterval(draw, 10)` continuously redraws the scene.
- **Position variables**: `x` and `y` track the ball's current location.
- **Velocity variables**: `dx` and `dy` determine movement per frame.
- **Canvas clearing**: `clearRect()` removes the previous frame before drawing the new one.

---

## Step 3: Bounce Off the Walls

We add collision detection so the ball bounces off the canvas edges instead of disappearing.

### Defining the Ball Radius

Extract the ball radius into a named constant for reuse in collision calculations:

```javascript
const ballRadius = 10;
```

Update `drawBall()` to use this variable:

```javascript
function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
```

### Basic Wall Collision (Without Radius Adjustment)

The simplest approach checks if the next ball position goes beyond the canvas boundaries:

```javascript
// Left and right walls
if (x + dx > canvas.width || x + dx < 0) {
  dx = -dx;
}

// Top and bottom walls
if (y + dy > canvas.height || y + dy < 0) {
  dy = -dy;
}
```

Reversing `dx` or `dy` (multiplying by -1) changes the ball's direction.

### Improved Collision (Accounting for Ball Radius)

The basic version lets the ball sink halfway into the wall before bouncing. To fix this, account for the ball's radius:

```javascript
// Left and right walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
  dx = -dx;
}

// Top and bottom walls
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
  dy = -dy;
}
```

### Collision Detection Conditions

| Wall | Condition | Action |
|------|-----------|--------|
| **Left** | `x + dx < ballRadius` | `dx = -dx` |
| **Right** | `x + dx > canvas.width - ballRadius` | `dx = -dx` |
| **Top** | `y + dy < ballRadius` | `dy = -dy` |
| **Bottom** | `y + dy > canvas.height - ballRadius` | `dy = -dy` |

### Complete Code for Step 3

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();

  // Collision detection - left and right walls
  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }

  // Collision detection - top and bottom walls
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  x += dx;
  y += dy;
}

setInterval(draw, 10);
```

---

## Step 4: Paddle and Keyboard Controls

Now we add a player-controlled paddle at the bottom of the screen and wire up keyboard input (left/right arrow keys).

### Defining Paddle Variables

```javascript
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
```

- `paddleHeight` and `paddleWidth` define the paddle dimensions.
- `paddleX` starts the paddle centered horizontally. It is a `let` because it will change as the player moves it.

### Drawing the Paddle

Create a `drawPaddle()` function. The paddle sits at the very bottom of the canvas:

```javascript
function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}
```

- The y-position is `canvas.height - paddleHeight`, placing it flush with the bottom edge.

### Keyboard State Variables

Track whether arrow keys are currently pressed:

```javascript
let rightPressed = false;
let leftPressed = false;
```

### Event Listeners for Key Presses

Register handlers for `keydown` (key pressed) and `keyup` (key released):

```javascript
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
```

### Key Handler Functions

Set the boolean flags based on which key is pressed or released:

```javascript
function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}
```

Both `"ArrowRight"` (modern browsers) and `"Right"` (legacy IE/Edge) are checked for compatibility.

### Paddle Movement Logic (With Boundary Checking)

Add this inside the `draw()` function to move the paddle based on key state, while keeping it within canvas bounds:

```javascript
if (rightPressed) {
  paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
  paddleX = Math.max(paddleX - 7, 0);
}
```

- The paddle moves 7 pixels per frame.
- `Math.min` prevents the paddle from going past the right edge.
- `Math.max` prevents it from going past the left edge.

### Complete Code for Step 4

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  drawPaddle();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
    dy = -dy;
  }

  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

setInterval(draw, 10);
```

---

## Step 5: Game Over

We replace the bottom-wall bounce with actual game logic: the ball should bounce off the paddle, but if it misses, it is game over.

### Storing the Interval Reference

To stop the game loop on game over, store the interval ID:

```javascript
let interval = 0;
```

Then assign the return value of `setInterval`:

```javascript
interval = setInterval(draw, 10);
```

### Implementing Game Over and Paddle Collision

Replace the bottom-wall collision check. Instead of bouncing off the bottom edge, we now check whether the ball hits the paddle or misses it:

```javascript
if (y + dy < ballRadius) {
  // Ball hits top wall -- bounce
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  // Ball reaches bottom edge
  if (x > paddleX && x < paddleX + paddleWidth) {
    // Ball hits paddle -- bounce
    dy = -dy;
  } else {
    // Ball missed the paddle -- game over
    alert("GAME OVER");
    document.location.reload();
    clearInterval(interval);
  }
}
```

**How paddle collision works:**
- `x > paddleX` -- the ball is past the paddle's left edge.
- `x < paddleX + paddleWidth` -- the ball is before the paddle's right edge.
- If both are true, the ball is above the paddle, so it bounces.
- If the ball reaches the bottom without hitting the paddle, the game ends.

### Complete Code for Step 5

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;
let interval = 0;

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBall();
  drawPaddle();

  // Left and right wall collision
  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }

  // Top wall collision
  if (y + dy < ballRadius) {
    dy = -dy;
  } else if (y + dy > canvas.height - ballRadius) {
    // Bottom edge: paddle collision or game over
    if (x > paddleX && x < paddleX + paddleWidth) {
      dy = -dy;
    } else {
      alert("GAME OVER");
      document.location.reload();
      clearInterval(interval);
    }
  }

  // Paddle movement
  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

interval = setInterval(draw, 10);
```

---

## Step 6: Build the Brick Field

Now we create the grid of bricks that the ball will destroy. The bricks are stored in a 2D array and drawn in rows and columns.

### Brick Configuration Variables

Define constants that control the layout of the brick field:

```javascript
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
```

- `brickRowCount` / `brickColumnCount` -- how many rows and columns of bricks.
- `brickWidth` / `brickHeight` -- dimensions of each individual brick.
- `brickPadding` -- space between bricks.
- `brickOffsetTop` / `brickOffsetLeft` -- distance from the top and left canvas edges to the first brick.

### Creating the Bricks 2D Array

Use nested loops to create a 2D array. Each brick stores its `x` and `y` position (initially `0`, calculated during drawing):

```javascript
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0 };
  }
}
```

### The drawBricks() Function

Loop through every brick, calculate its position, store it, and draw it:

```javascript
function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
      const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
      bricks[c][r].x = brickX;
      bricks[c][r].y = brickY;
      ctx.beginPath();
      ctx.rect(brickX, brickY, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}
```

**Position calculation formula:**
- `brickX = column * (brickWidth + brickPadding) + brickOffsetLeft`
- `brickY = row * (brickHeight + brickPadding) + brickOffsetTop`

This creates an evenly-spaced grid with consistent padding and margins.

### Calling drawBricks() in the Game Loop

Add the call at the beginning of your `draw()` function, after clearing the canvas:

```javascript
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  // ... rest of draw function
}
```

### Complete Code for Step 6

```javascript
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;

let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;

const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;

let rightPressed = false;
let leftPressed = false;
let interval = 0;

const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;

const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0 };
  }
}

document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);

function keyDownHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = true;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = true;
  }
}

function keyUpHandler(e) {
  if (e.key === "Right" || e.key === "ArrowRight") {
    rightPressed = false;
  } else if (e.key === "Left" || e.key === "ArrowLeft") {
    leftPressed = false;
  }
}

function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawPaddle() {
  ctx.beginPath();
  ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
  ctx.fillStyle = "#0095DD";
  ctx.fill();
  ctx.closePath();
}

function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
      const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
      bricks[c][r].x = brickX;
      bricks[c][r].y = brickY;
      ctx.beginPath();
      ctx.rect(brickX, brickY, brickWidth, brickHeight);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    }
  }
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();

  if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
    dx = -dx;
  }
  if (y + dy < ballRadius) {
    dy = -dy;
  } else if (y + dy > canvas.height - ballRadius) {
    if (x > paddleX && x < paddleX + paddleWidth) {
      dy = -dy;
    } else {
      alert("GAME OVER");
      document.location.reload();
      clearInterval(interval);
    }
  }

  if (rightPressed) {
    paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
  } else if (leftPressed) {
    paddleX = Math.max(paddleX - 7, 0);
  }

  x += dx;
  y += dy;
}

interval = setInterval(draw, 10);
```

---

## Step 7: Collision Detection

With bricks on screen, we need to detect when the ball hits one and make it disappear. Each brick gets a `status` property: `1` means visible, `0` means destroyed.

### Adding the Status Property to Bricks

Update the brick initialization to include a `status` flag:

```javascript
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
  bricks[c] = [];
  for (let r = 0; r < brickRowCount; r++) {
    bricks[c][r] = { x: 0, y: 0, status: 1 };
  }
}
```

### The collisionDetection() Function

Loop through every brick and check if the ball's center is within the brick's bounding box:

```javascript
function collisionDetection() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const b = bricks[c][r];
      if (b.status === 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
        }
      }
    }
  }
}
```

**Collision conditions (all four must be true simultaneously):**
- `x > b.x` -- ball center is to the right of the brick's left edge.
- `x < b.x + brickWidth` -- ball center is to the left of the brick's right edge.
- `y > b.y` -- ball center is below the brick's top edge.
- `y < b.y + brickHeight` -- ball center is above the brick's bottom edge.

When a collision is detected:
- `dy = -dy` reverses the ball's vertical direction (bounce).
- `b.status = 0` marks the brick as destroyed.

### Updating drawBricks() to Respect Status

Only draw bricks that are still active (`status === 1`):

```javascript
function drawBricks() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      if (bricks[c][r].status === 1) {
        const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
        const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
        bricks[c][r].x = brickX;
        bricks[c][r].y = brickY;
        ctx.beginPath();
        ctx.rect(brickX, brickY, brickWidth, brickHeight);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }
    }
  }
}
```

### Calling collisionDetection() in the Game Loop

Add the call in your `draw()` function, after drawing all elements:

```javascript
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  collisionDetection();
  // ... rest of draw function
}
```

---

## Step 8: Track the Score and Win

We add a score counter that increments each time a brick is destroyed, and a win condition that triggers when all bricks are gone.

### Initializing the Score

```javascript
let score = 0;
```

### The drawScore() Function

Display the current score on the canvas using text rendering:

```javascript
function drawScore() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText(`Score: ${score}`, 8, 20);
}
```

- `ctx.font` sets the font size and family (like CSS).
- `ctx.fillText(text, x, y)` renders text at the given coordinates.
- Position `(8, 20)` places the score in the top-left corner.

### Incrementing the Score

In the `collisionDetection()` function, increment the score when a brick is hit:

```javascript
dy = -dy;
b.status = 0;
score++;
```

### Adding the Win Condition

After incrementing the score, check if the player has destroyed all bricks:

```javascript
score++;
if (score === brickRowCount * brickColumnCount) {
  alert("YOU WIN, CONGRATULATIONS!");
  document.location.reload();
  clearInterval(interval);
}
```

The total number of bricks is `brickRowCount * brickColumnCount`. When the score reaches that number, every brick has been destroyed.

### Complete collisionDetection() with Score and Win

```javascript
function collisionDetection() {
  for (let c = 0; c < brickColumnCount; c++) {
    for (let r = 0; r < brickRowCount; r++) {
      const b = bricks[c][r];
      if (b.status === 1) {
        if (
          x > b.x &&
          x < b.x + brickWidth &&
          y > b.y &&
          y < b.y + brickHeight
        ) {
          dy = -dy;
          b.status = 0;
          score++;
          if (score === brickRowCount * brickColumnCount) {
            alert("YOU WIN, CONGRATULATIONS!");
            document.location.reload();
            clearInterval(interval);
          }
        }
      }
    }
  }
}
```

### Calling drawScore() in the Game Loop

Add the call in your `draw()` function:

```javascript
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  drawScore();
  collisionDetection();
  // ... rest of draw function
}
```

### Canvas Text Methods Reference

| Method/Property | Purpose |
|-----------------|---------|
| `ctx.font` | Set font size and family |
| `ctx.fillStyle` | Set text color |
| `ctx.fillText(text, x, y)` | Draw filled text at coordinates |

---

## Step 9: Mouse Controls

In addition to keyboard controls, we add mouse support so the player can move the paddle by moving the mouse.

### Adding the mousemove Event Listener

Register the handler alongside the existing keyboard listeners:

```javascript
document.addEventListener("mousemove", mouseMoveHandler);
```

### The mouseMoveHandler Function

Calculate the mouse's horizontal position relative to the canvas and update the paddle position:

```javascript
function mouseMoveHandler(e) {
  const relativeX = e.clientX - canvas.offsetLeft;
  if (relativeX > 0 && relativeX < canvas.width) {
    paddleX = relativeX - paddleWidth / 2;
  }
}
```

**How it works:**
- `e.clientX` -- the mouse's horizontal position in the browser viewport.
- `canvas.offsetLeft` -- the distance from the canvas's left edge to the viewport's left edge.
- `relativeX` -- the mouse position relative to the canvas (not the viewport).
- The boundary check (`relativeX > 0 && relativeX < canvas.width`) ensures the paddle only moves when the mouse is over the canvas.
- `paddleX = relativeX - paddleWidth / 2` centers the paddle under the mouse cursor by subtracting half the paddle width.

### Complete Event Listener Setup (Keyboard + Mouse)

```javascript
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("mousemove", mouseMoveHandler);
```

Both control methods work simultaneously. The player can use arrow keys or mouse -- or switch between them at any time.

---

## Step 10: Finishing Up

The final step adds a lives system (so the player gets multiple chances) and upgrades the game loop from `setInterval` to `requestAnimationFrame` for smoother rendering.

### Adding the Lives Variable

```javascript
let lives = 3;
```

### The drawLives() Function

Display the remaining lives in the top-right corner:

```javascript
function drawLives() {
  ctx.font = "16px Arial";
  ctx.fillStyle = "#0095DD";
  ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}
```

### Implementing the Lives System

Replace the immediate game-over logic with a lives-based system. When the ball misses the paddle:

```javascript
if (y + dy < ballRadius) {
  dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
  if (x > paddleX && x < paddleX + paddleWidth) {
    dy = -dy;
  } else {
    lives--;
    if (!lives) {
      alert("GAME OVER");
      document.location.reload();
    } else {
      // Reset ball and paddle positions
      x = canvas.width / 2;
      y = canvas.height - 30;
      dx = 2;
      dy = -2;
      paddleX = (canvas.width - paddleWidth) / 2;
    }
  }
}
```

**What happens when a life is lost:**
- `lives--` decrements the lives counter.
- If `lives` reaches `0`, the game ends with an alert and page reload.
- Otherwise, the ball resets to center-bottom, velocity resets, and the paddle resets to center.

### Upgrading to requestAnimationFrame

Replace `setInterval` with `requestAnimationFrame` for a smoother, browser-optimized game loop:

**Old approach (remove):**
```javascript
interval = setInterval(draw, 10);
```

**New approach:**
Add `requestAnimationFrame(draw)` at the end of the `draw()` function:

```javascript
function draw() {
  // ... all drawing and logic ...
  requestAnimationFrame(draw);
}

// Start the game by calling draw() once:
draw();
```

`requestAnimationFrame` lets the browser schedule rendering at the optimal frame rate (typically 60fps), which is more efficient than a fixed 10ms interval.

### Calling drawLives() in the Game Loop

```javascript
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBricks();
  drawBall();
  drawPaddle();
  drawScore();
  drawLives();
  collisionDetection();
  // ... rest of logic ...
  requestAnimationFrame(draw);
}
```

---

## Complete Final Game Code

Below is the entire game in a single, self-contained HTML file. This is the final product of all 10 steps combined.

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>2D Breakout Game</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      canvas {
        background: #eeeeee;
        display: block;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="480" height="320"></canvas>

    <script>
      const canvas = document.getElementById("myCanvas");
      const ctx = canvas.getContext("2d");

      // --- Ball ---
      const ballRadius = 10;
      let x = canvas.width / 2;
      let y = canvas.height - 30;
      let dx = 2;
      let dy = -2;

      // --- Paddle ---
      const paddleHeight = 10;
      const paddleWidth = 75;
      let paddleX = (canvas.width - paddleWidth) / 2;

      // --- Controls ---
      let rightPressed = false;
      let leftPressed = false;

      // --- Bricks ---
      const brickRowCount = 3;
      const brickColumnCount = 5;
      const brickWidth = 75;
      const brickHeight = 20;
      const brickPadding = 10;
      const brickOffsetTop = 30;
      const brickOffsetLeft = 30;

      const bricks = [];
      for (let c = 0; c < brickColumnCount; c++) {
        bricks[c] = [];
        for (let r = 0; r < brickRowCount; r++) {
          bricks[c][r] = { x: 0, y: 0, status: 1 };
        }
      }

      // --- Score and Lives ---
      let score = 0;
      let lives = 3;

      // =====================
      // Event Listeners
      // =====================
      document.addEventListener("keydown", keyDownHandler);
      document.addEventListener("keyup", keyUpHandler);
      document.addEventListener("mousemove", mouseMoveHandler);

      function keyDownHandler(e) {
        if (e.key === "Right" || e.key === "ArrowRight") {
          rightPressed = true;
        } else if (e.key === "Left" || e.key === "ArrowLeft") {
          leftPressed = true;
        }
      }

      function keyUpHandler(e) {
        if (e.key === "Right" || e.key === "ArrowRight") {
          rightPressed = false;
        } else if (e.key === "Left" || e.key === "ArrowLeft") {
          leftPressed = false;
        }
      }

      function mouseMoveHandler(e) {
        const relativeX = e.clientX - canvas.offsetLeft;
        if (relativeX > 0 && relativeX < canvas.width) {
          paddleX = relativeX - paddleWidth / 2;
        }
      }

      // =====================
      // Collision Detection
      // =====================
      function collisionDetection() {
        for (let c = 0; c < brickColumnCount; c++) {
          for (let r = 0; r < brickRowCount; r++) {
            const b = bricks[c][r];
            if (b.status === 1) {
              if (
                x > b.x &&
                x < b.x + brickWidth &&
                y > b.y &&
                y < b.y + brickHeight
              ) {
                dy = -dy;
                b.status = 0;
                score++;
                if (score === brickRowCount * brickColumnCount) {
                  alert("YOU WIN, CONGRATULATIONS!");
                  document.location.reload();
                }
              }
            }
          }
        }
      }

      // =====================
      // Drawing Functions
      // =====================
      function drawBall() {
        ctx.beginPath();
        ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }

      function drawPaddle() {
        ctx.beginPath();
        ctx.rect(
          paddleX,
          canvas.height - paddleHeight,
          paddleWidth,
          paddleHeight
        );
        ctx.fillStyle = "#0095DD";
        ctx.fill();
        ctx.closePath();
      }

      function drawBricks() {
        for (let c = 0; c < brickColumnCount; c++) {
          for (let r = 0; r < brickRowCount; r++) {
            if (bricks[c][r].status === 1) {
              const brickX =
                c * (brickWidth + brickPadding) + brickOffsetLeft;
              const brickY =
                r * (brickHeight + brickPadding) + brickOffsetTop;
              bricks[c][r].x = brickX;
              bricks[c][r].y = brickY;
              ctx.beginPath();
              ctx.rect(brickX, brickY, brickWidth, brickHeight);
              ctx.fillStyle = "#0095DD";
              ctx.fill();
              ctx.closePath();
            }
          }
        }
      }

      function drawScore() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText(`Score: ${score}`, 8, 20);
      }

      function drawLives() {
        ctx.font = "16px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
      }

      // =====================
      // Main Game Loop
      // =====================
      function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawBricks();
        drawBall();
        drawPaddle();
        drawScore();
        drawLives();
        collisionDetection();

        // Left and right wall collision
        if (
          x + dx > canvas.width - ballRadius ||
          x + dx < ballRadius
        ) {
          dx = -dx;
        }

        // Top wall collision
        if (y + dy < ballRadius) {
          dy = -dy;
        } else if (y + dy > canvas.height - ballRadius) {
          // Bottom edge: paddle collision or lose a life
          if (x > paddleX && x < paddleX + paddleWidth) {
            dy = -dy;
          } else {
            lives--;
            if (!lives) {
              alert("GAME OVER");
              document.location.reload();
            } else {
              x = canvas.width / 2;
              y = canvas.height - 30;
              dx = 2;
              dy = -2;
              paddleX = (canvas.width - paddleWidth) / 2;
            }
          }
        }

        // Paddle movement (keyboard)
        if (rightPressed) {
          paddleX = Math.min(
            paddleX + 7,
            canvas.width - paddleWidth
          );
        } else if (leftPressed) {
          paddleX = Math.max(paddleX - 7, 0);
        }

        x += dx;
        y += dy;
        requestAnimationFrame(draw);
      }

      draw();
    </script>
  </body>
</html>
```

---

## Quick Reference: All Game Variables

| Variable | Type | Purpose |
|----------|------|---------|
| `canvas` | const | Reference to the HTML canvas element |
| `ctx` | const | 2D rendering context |
| `ballRadius` | const | Radius of the ball (10) |
| `x`, `y` | let | Current ball position |
| `dx`, `dy` | let | Ball velocity (pixels per frame) |
| `paddleHeight` | const | Height of the paddle (10) |
| `paddleWidth` | const | Width of the paddle (75) |
| `paddleX` | let | Current horizontal position of the paddle |
| `rightPressed` | let | Whether the right arrow key is held down |
| `leftPressed` | let | Whether the left arrow key is held down |
| `brickRowCount` | const | Number of brick rows (3) |
| `brickColumnCount` | const | Number of brick columns (5) |
| `brickWidth` | const | Width of each brick (75) |
| `brickHeight` | const | Height of each brick (20) |
| `brickPadding` | const | Space between bricks (10) |
| `brickOffsetTop` | const | Distance from top of canvas to first brick row (30) |
| `brickOffsetLeft` | const | Distance from left of canvas to first brick column (30) |
| `bricks` | const | 2D array holding all brick objects |
| `score` | let | Current player score |
| `lives` | let | Remaining lives (starts at 3) |

## Quick Reference: All Functions

| Function | Purpose |
|----------|---------|
| `keyDownHandler(e)` | Sets `rightPressed` or `leftPressed` to `true` on key press |
| `keyUpHandler(e)` | Sets `rightPressed` or `leftPressed` to `false` on key release |
| `mouseMoveHandler(e)` | Moves paddle to follow mouse horizontal position |
| `collisionDetection()` | Checks ball against all active bricks; destroys hit bricks, increments score, checks win |
| `drawBall()` | Renders the ball at current `(x, y)` position |
| `drawPaddle()` | Renders the paddle at current `paddleX` position |
| `drawBricks()` | Renders all bricks with `status === 1` |
| `drawScore()` | Renders the score text in the top-left corner |
| `drawLives()` | Renders the lives text in the top-right corner |
| `draw()` | Main game loop: clears canvas, draws everything, handles collisions, updates positions |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)