Applied Module 12 · The Lora Playbook

Level Editor Lite

What you'll learn

~60 min
  • Build a browser-based tile placement editor with React and a canvas grid
  • Implement a tile palette with ground, wall, platform, hazard, spawn, and exit types
  • Export level layouts as JSON that Phaser can load directly
  • Add layer toggling for background and foreground elements

What you’re building

Up to this point, every enemy position, platform edge, and pickup location in your prototypes has been hardcoded as pixel coordinates typed by hand or generated by the AI. That works for a single test room. It does not work for designing an actual game level where you want to place 50 tiles, playtest, move 12 of them, add a hazard pit, and re-export — all before lunch.

Professional studios use level editors. Tiled, LDtk, Unity’s built-in scene editor — they all do the same fundamental thing: let you paint content onto a grid and export it as data. You are going to build a lightweight version of this. Not because you need every feature of Tiled, but because building your own editor means the output format matches your Phaser scene loader exactly, with zero conversion steps.

BloodRayne’s levels were a mix of platforming corridors, combat arenas, and vertical climbing sections. Your editor will let you design all of these by painting tiles: ground for walkable surfaces, walls for boundaries, platforms for vertical sections, hazards for environmental danger, spawn points for enemies, and exit triggers for level transitions.

Forty-five minutes from now you will have a browser-based tool where you click to place tiles on a grid, pick from a color-coded palette, switch between background and foreground layers, resize the grid, erase mistakes, and export the whole layout as JSON that your Phaser game can import directly.

Software pattern: Visual data editor with structured export

Canvas grid + palette selection + click-to-place + JSON serialization. This pattern shows up everywhere: floor plan editors, dashboard layout builders, form designers, pixel art tools. Any application where humans need to visually arrange elements on a grid and export the arrangement as structured data.


The showcase

Here is the complete tool:

  • Canvas grid: Configurable grid (default 25x19 tiles at 32px each = 800x608 viewport). Grid lines visible. Click any cell to place the selected tile type.
  • Tile palette: Sidebar with 6 tile types, each a colored button with label:
    • Ground (#4a5568, gray) — walkable floor
    • Wall (#1e293b, dark blue-gray) — impassable boundary
    • Platform (#8B5CF6, purple) — one-way platform the player can jump through from below
    • Hazard (#ef4444, red) — kills or damages on contact (spikes, fire pits)
    • Spawn (#f59e0b, yellow) — enemy spawn point (one per enemy)
    • Exit (#22c55e, green) — level transition trigger
  • Eraser mode: Click the eraser tool or press E to switch to eraser. Click a tile to remove it.
  • Layer toggle: Two layers — Background (floor, walls) and Foreground (platforms, hazards, spawns, exits). Toggle visibility of each layer to work on one at a time.
  • Grid size selector: Dropdown to change grid dimensions (20x15, 25x19, 30x23, 40x30). Changing size clears the grid with a confirmation prompt.
  • JSON export: Button that generates a JSON object with grid dimensions, tile size, and a 2D array of tile types per layer. Copy to clipboard or download as .json file.
  • JSON import: Button to load a previously exported JSON file and render it on the grid for editing.
  • Undo: Ctrl+Z undoes the last tile placement (keeps a history stack of 50 actions).

The prompt

Open your terminal, navigate to a project folder, start your AI CLI tool, and paste this prompt:

Build a browser-based tile level editor using React and HTML5 Canvas.
The editor outputs JSON that a Phaser 3 game can load as level data.
This is a tool for designing levels for a BloodRayne-inspired action
platformer.
PROJECT STRUCTURE:
level-editor-lite/
package.json
index.html
src/
main.jsx (React app entry point)
App.jsx (main editor layout)
Canvas.jsx (HTML5 Canvas grid with click-to-place)
Palette.jsx (tile type selector sidebar)
Toolbar.jsx (grid size, layers, export/import, undo)
tileTypes.js (tile type definitions with colors and labels)
exportLevel.js (JSON serialization and Phaser format output)
importLevel.js (JSON deserialization and grid population)
useEditorState.js (React hook for editor state management)
public/
(empty, Vite serves index.html from root)
REQUIREMENTS:
1. TILE TYPES (src/tileTypes.js)
Export an array of tile definitions:
[
{ id: 'ground', label: 'Ground', color: '#4a5568', layer: 'background', shortcut: '1' },
{ id: 'wall', label: 'Wall', color: '#1e293b', layer: 'background', shortcut: '2' },
{ id: 'platform', label: 'Platform', color: '#8B5CF6', layer: 'foreground', shortcut: '3' },
{ id: 'hazard', label: 'Hazard', color: '#ef4444', layer: 'foreground', shortcut: '4' },
{ id: 'spawn', label: 'Spawn', color: '#f59e0b', layer: 'foreground', shortcut: '5' },
{ id: 'exit', label: 'Exit', color: '#22c55e', layer: 'foreground', shortcut: '6' }
]
2. EDITOR STATE (src/useEditorState.js)
Custom React hook managing:
- gridWidth, gridHeight (default 25, 19)
- tileSize: 32
- selectedTile: 'ground' (current palette selection)
- eraserActive: false
- activeLayer: 'all' | 'background' | 'foreground'
- backgroundLayer: 2D array [height][width] of tile IDs or null
- foregroundLayer: 2D array [height][width] of tile IDs or null
- undoStack: array of { layer, x, y, previousTile } (max 50)
- Methods: placeTile(x, y), eraseTile(x, y), undo(), setGridSize(w, h),
clearGrid(), exportJSON(), importJSON(data)
3. CANVAS COMPONENT (src/Canvas.jsx)
HTML5 Canvas rendering the grid:
a. RENDERING
- Canvas size: gridWidth * tileSize x gridHeight * tileSize
- Draw grid lines: thin (#333) lines between cells
- Draw background layer tiles first, then foreground on top
- Each tile: filled rectangle in the tile type's color
- Spawn tiles: add a small "S" letter centered in the cell
- Exit tiles: add a small "E" letter centered in the cell
- Hidden layer tiles: draw at 20% opacity (not completely invisible)
b. INTERACTION
- Click: place selected tile at grid coordinates (snap to grid)
- Click + drag: paint tiles continuously while mouse is held
- Right-click: erase tile at that position
- Mouse hover: show a semi-transparent preview of the selected tile
- Calculate grid coordinates from mouse position: x = floor(mouseX / tileSize),
y = floor(mouseY / tileSize)
c. LAYER FILTERING
- If activeLayer is 'background': only background tiles are editable,
foreground shown at 20% opacity
- If activeLayer is 'foreground': only foreground tiles are editable,
background shown at 20% opacity
- If activeLayer is 'all': both layers editable, both at full opacity
- Placing a tile respects the tile's defined layer, not the active view
4. PALETTE COMPONENT (src/Palette.jsx)
Sidebar tile selector:
- Vertical list of tile type buttons
- Each button: colored square (32x32) + label text + keyboard shortcut hint
- Selected tile: highlighted border (#fff)
- Eraser button at the bottom with "E" shortcut
- Keyboard shortcuts: 1-6 for tile types, E for eraser
- Group by layer: "Background" header, then ground + wall;
"Foreground" header, then platform + hazard + spawn + exit
5. TOOLBAR COMPONENT (src/Toolbar.jsx)
Top bar with editor controls:
a. GRID SIZE
- Dropdown with preset sizes: 20x15, 25x19, 30x23, 40x30
- Changing size shows a confirm dialog ("This will clear the grid. Continue?")
- After confirm: reset both layers to empty arrays of new dimensions
b. LAYER TOGGLE
- Three buttons: "All" | "Background" | "Foreground"
- Active button highlighted with #8B5CF6 underline
- Affects which layer is editable and opacity of the other layer
c. UNDO
- Button: "Undo (Ctrl+Z)"
- Global keyboard listener for Ctrl+Z
- Reverts the last tile placement or erasure
d. EXPORT
- "Export JSON" button: generates JSON and copies to clipboard
- Also triggers a file download of the JSON as "level-export.json"
- Toast notification: "Level exported! Copied to clipboard."
e. IMPORT
- "Import JSON" button: opens file picker for .json files
- Validates the file format, populates both layers on the grid
- Toast notification: "Level imported!" or error message
f. CLEAR
- "Clear Grid" button with confirmation dialog
- Resets both layers to empty
6. EXPORT FORMAT (src/exportLevel.js)
The exported JSON matches what Phaser can consume:
{
"meta": {
"name": "untitled",
"gridWidth": 25,
"gridHeight": 19,
"tileSize": 32,
"exportedAt": "2026-03-30T12:00:00Z"
},
"layers": {
"background": [
[null, null, "ground", "ground", ...],
["wall", null, "ground", "ground", ...],
...
],
"foreground": [
[null, null, null, null, ...],
[null, "platform", null, null, ...],
...
]
},
"spawns": [
{ "x": 5, "y": 12, "type": "enemy" }
],
"exits": [
{ "x": 24, "y": 9, "target": "next_level" }
]
}
"spawns" and "exits" arrays are extracted from the foreground layer
for convenience — Phaser can iterate them directly to place entities.
7. STYLING
- Dark theme: page background #09090b, editor panel #111118
- Palette sidebar: width 180px, left side, background #18181b
- Toolbar: top bar, height 48px, background #18181b, border-bottom #333
- Canvas: centered in remaining space
- Buttons: #2a2a3e background, #e5e5e5 text, #8B5CF6 border on active
- Toast notifications: bottom-right, auto-dismiss after 3 seconds
- Font: system-ui
DEPENDENCIES: react, react-dom, vite, @vitejs/plugin-react
Add "dev" script: "vite"
💡This is a tool, not a game feature

The level editor is not part of the game the player sees. It is your content creation pipeline. You use it to design levels, export JSON, and feed that JSON into the Phaser game. Separating the editor from the game means you can iterate on level design without touching game code.


What you get

After generation:

level-editor-lite/
package.json
index.html
src/
main.jsx
App.jsx
Canvas.jsx
Palette.jsx
Toolbar.jsx
tileTypes.js
exportLevel.js
importLevel.js
useEditorState.js

Fire it up

Terminal window
cd level-editor-lite
npm install
npm run dev

Open the URL. You should see a dark-themed editor with a grid in the center, a tile palette on the left, and a toolbar at the top. The grid is empty. Ground is selected by default.

Click anywhere on the grid to place a gray ground tile. Click and drag to paint a floor. Press 4 to switch to hazard, paint some red danger zones. Press 5 for spawn points, place a few yellow enemy markers. Press 6 for an exit, place a green tile at the right edge. Click “Export JSON” and check your clipboard — that is a complete level definition your Phaser game can load.

If something is off

ProblemFollow-up prompt
Canvas does not respond to clicksClicking the canvas does nothing. Make sure the canvas element has onClick and onMouseMove handlers that calculate grid coordinates from the event's offsetX and offsetY divided by tileSize, floored to integers. Also ensure the canvas ref is properly set and the component re-renders after state changes.
Click-and-drag paints only the first tileI can place one tile per click, but dragging does not paint continuously. Add onMouseDown to set a "painting" state, onMouseMove to place tiles while painting is true, and onMouseUp to stop painting. Track the painting state in a useRef so it does not trigger re-renders.
Exported JSON has wrong dimensionsThe exported JSON shows gridWidth: 0 and empty arrays. Make sure the export function reads from the current editor state (gridWidth, gridHeight, backgroundLayer, foregroundLayer) and that these values are passed correctly from the hook to the export utility.
Layer toggle does not change opacitySwitching between Background and Foreground view modes does not change tile opacity. In the Canvas render loop, check the activeLayer state. When drawing a tile, if the tile's layer does not match activeLayer (and activeLayer is not 'all'), set ctx.globalAlpha = 0.2 before drawing that tile and reset to 1.0 after.

Deep dive

This editor is a content creation tool, and understanding its data flow clarifies why it is structured this way.

The grid is two 2D arrays. Background layer holds ground and wall tiles. Foreground layer holds platforms, hazards, spawns, and exits. This separation matters because in Phaser, background tiles are static collision geometry and foreground objects are interactive entities with behaviors. Keeping them in separate arrays means the game loader can process them independently.

Click-to-place with grid snapping is the core interaction. The mouse position is divided by tile size and floored to get grid coordinates. This means every tile lands perfectly aligned, even with sloppy clicks. Drag-painting uses the same math on mouse-move events, giving you the “paint” feel that makes filling large areas fast.

The export format is designed for Phaser consumption. The JSON includes raw 2D arrays (for tilemap rendering) plus extracted spawns and exits arrays (for entity creation). A Phaser scene loader reads the background array to create static collision tiles, reads the foreground array to create platforms and hazards with physics, and iterates the spawns array to instantiate enemies at those positions. No format conversion. No intermediate processing.

Undo uses a simple stack pattern. Every tile placement pushes the previous state of that cell onto the stack. Undo pops the most recent entry and restores it. This is the same pattern used in text editors, drawing tools, and form builders. Fifty entries is enough for practical editing without consuming significant memory.

🔍Loading exported levels in Phaser

Once you export a level JSON from this editor, loading it in Phaser looks like this (the AI will generate this code when you integrate in L19):

// In your Phaser scene's create() method
const levelData = await fetch('levels/chapter1.json').then(r => r.json());
const { gridWidth, gridHeight, tileSize } = levelData.meta;
// Create background tiles as static physics bodies
levelData.layers.background.forEach((row, y) => {
row.forEach((tile, x) => {
if (tile === 'ground' || tile === 'wall') {
const block = this.physics.add.staticImage(
x * tileSize + tileSize/2, y * tileSize + tileSize/2, tile
);
}
});
});
// Spawn enemies at marked positions
levelData.spawns.forEach(spawn => {
this.createEnemy(spawn.x * tileSize, spawn.y * tileSize);
});

The editor creates the data. The game consumes it. Two separate tools, one shared format.


Customize it

Add a tile name editor

Add a text input at the top of the editor for the level name. The name
is saved in the exported JSON under meta.name. When importing, show
the level name. Default to "untitled" if no name is set. This helps
organize multiple exported levels.

Add entity properties

When a spawn or exit tile is placed, show a small properties panel.
For spawns: dropdown to select enemy type (basic, fast, heavy, boss).
For exits: text input for the target level name. Store these properties
in the exported JSON's spawns and exits arrays. This lets the Phaser
game know what kind of enemy to create at each spawn and where each
exit leads.

Add a playtest preview

Add a "Preview" button that launches a minimal Phaser instance in a
modal. The preview loads the current grid as a level with a controllable
player character (WASD movement, no combat). The player can walk on
ground tiles, collide with walls, land on platforms, take damage from
hazards (flash red), and trigger exits (show "Level Complete" text).
This lets you playtest basic traversal without exporting.

Try it yourself

  1. Paste the main prompt and generate the project.
  2. Run npm install && npm run dev and open the editor.
  3. Design a simple combat arena: ground floor across the bottom, walls on the sides, a few platforms at different heights, hazards below the platforms, 3-4 enemy spawns, and an exit at the top-right.
  4. Toggle between Background and Foreground views to see how the layers separate.
  5. Export the JSON. Open it in a text editor and verify it contains your level layout.
  6. Clear the grid, import the JSON you just exported. The level should reconstruct exactly as you designed it.
  7. Now design a second level — a vertical climbing section with narrow platforms and hazards. Export that too. You now have two levels ready for integration.

Key takeaways

  • Visual editors eliminate coordinate guessing. Placing tiles by clicking is orders of magnitude faster and more accurate than typing {x: 384, y: 256} in code.
  • JSON export is the bridge to Phaser. The editor’s output format is designed so the game loader can consume it directly with no conversion step.
  • Two layers match Phaser’s architecture. Background for static collision, foreground for interactive entities. Separate arrays, separate processing.
  • The editor is a content pipeline tool. It does not ship with the game. It runs separately, produces data files, and those files drive the game. Content and engine are independent.
  • Undo makes experimentation safe. Try a layout, hate it, Ctrl+Z. The cost of trying something is zero.
💡Design thinking, not coding

The level editor shifts your work from writing code to making design decisions. Where should the enemies be? How high should the platforms be? Where does the player enter and exit? These are game design questions, not programming questions. The tool lets you answer them visually.


What’s next

You can design levels, but how do you know they are actually playable? An enemy might be placed inside a wall. A jump might be too far for the player’s physics. A room might be trivially easy or impossibly hard. In the next lesson you will build a game test harness — an automated test runner that scripts collision checks, spawn verification, and balance scenarios, then generates an HTML report with pass/fail results. QA without a QA team.