Initial Commit

This commit is contained in:
Brian Fertig 2026-02-22 19:17:36 -07:00
commit b4b42b20d3
22 changed files with 1385 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python3:*)"
]
}
}

67
CLAUDE.md Normal file
View File

@ -0,0 +1,67 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Running the Game
Start a local web server (required for ES6 module loading):
```bash
# Linux/Mac
./start_web.sh
# or manually:
python3 -m http.server 8000
```
Then open `http://localhost:8000` in a browser.
No build step, bundler, or package manager is used. The game runs as plain ES6 modules in the browser.
## Tech Stack
- **Phaser 3** (HTML5 game framework, loaded via CDN or local file)
- **Vanilla JavaScript** with ES6 `import`/`export` — no transpilation
- **Tiled Map Editor** for level design (exports to `assets/tilesets/levels-a.json`)
## Project Architecture
This is a **pre-implementation project** — assets and specs exist, but no JavaScript or HTML source files have been created yet. The `software.md` file is the authoritative specification.
### Key Assets
- `assets/sprites/player.png` — 128×256px spritesheet; frame 0 = standing, frame 1 = walking
- `assets/sprites/platforms.png` — 1280×1280px spritesheet; 128×128 tiles in a 10×10 grid
- `assets/tilesets/levels-a.json` — Tiled JSON export for Level 1; 30×20 tiles at 128×128px each
- `assets/tilesets/platforms.tsx` — Tileset with tile property definitions (floor, solid, barrier, ladder, button, color)
### Game Mechanics (from software.md)
**Viewport:** 1600×900, scaled to user's viewport.
**Player:** Spawns at tile (3, 11) in Level 1. Three lives. Arrow keys for movement; up arrow jumps when not on a ladder; up/down arrows climb when on a ladder; player snaps to ladder center horizontally.
**Tile properties** (defined in `platforms.tsx`, embedded in map layers):
- `floor=true` — one-way collision; player can jump through from below but cannot fall through
- `solid=true` — impassable from all directions
- `ladder=true` — climbable; `top=true` marks the top rung (jumping is allowed from top)
- `button=true` + `color={red|yellow|blue|green}` + `pressed={true|false}` — player lands on top to press; only one button active at a time
- `barrier=true` + `color` — toggled solid/passable by matching button; only the color matching the pressed button is `solid=false`
**Button/Barrier color system:** Pressing a button sets all same-color barriers to passable and all other barriers to solid. Tile sprites update to reflect state.
### Intended Code Structure
Per `software.md`, the codebase should use modular ES6 classes:
- One HTML entry point that loads Phaser 3 and bootstraps the game
- Separate scene files for levels (framework must support multiple levels)
- Classes/modules for: Player, Level/TileMap, Button, Barrier, Ladder
- No web packager — all imports must be relative paths resolvable by the browser
### Sprite Tile ID Reference (platforms.png)
| Color | Button unpressed | Button pressed | Barrier solid | Barrier passable |
|--------|-----------------|----------------|---------------|------------------|
| Red | 8 | 18 | 20 | 21 |
| Blue | 9 | 19 | 30 | 31 |
| Yellow | 28 | 38 | 40 | 41 |
| Green | 29 | 39 | 50 | 51 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

BIN
assets/sprites/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
assets/sprites/player.psd Normal file

Binary file not shown.

View File

@ -0,0 +1,14 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"properties": [
],
"propertyTypes": [
]
}

View File

@ -0,0 +1,47 @@
{
"Map/SizeTest": {
"height": 4300,
"width": 2
},
"activeFile": "untitled.tmx",
"expandedProjectPaths": [
"."
],
"fileStates": {
"platforms.tsx": {
"scaleInDock": 0.33,
"scaleInEditor": 1
},
"untitled.tmx": {
"scale": 0.33,
"selectedLayer": 3,
"viewCenter": {
"x": 60.60606060606062,
"y": 589.3939393939394
}
}
},
"last.exportedFilePath": "/home/brianfertig/git/BreadBakingBonanza/assets/tilesets",
"last.imagePath": "/home/brianfertig/git/BreadBakingBonanza/assets/sprites",
"lastUsedTilesetExportFilter": "All Files (*)",
"map.fixedSize": false,
"map.lastUsedExportFilter": "JSON map files (*.tmj *.json)",
"map.lastUsedFormat": "tmx",
"map.tileHeight": 128,
"map.tileWidth": 128,
"openFiles": [
"untitled.tmx",
"platforms.tsx"
],
"project": "bread-levels.tiled-project",
"property.type": "bool",
"recentFiles": [
"platforms.tsx",
"untitled.tmx"
],
"tileset.lastUsedFormat": "tsx",
"tileset.tileSize": {
"height": 128,
"width": 128
}
}

View File

@ -0,0 +1,186 @@
{ "compressionlevel":-1,
"height":20,
"infinite":true,
"layers":[
{
"chunks":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3, 4, 5, 6, 4, 5, 6, 4, 5, 6, 4, 5, 4, 5, 6, 7,
13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 17],
"height":16,
"width":16,
"x":0,
"y":0
},
{
"data":[23, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 27,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":16,
"width":16,
"x":0,
"y":16
}],
"height":48,
"id":1,
"name":"floor",
"opacity":1,
"startx":-16,
"starty":-16,
"type":"tilelayer",
"visible":true,
"width":32,
"x":0,
"y":0
},
{
"chunks":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 2147483650, 1, 1, 1, 1, 1, 2,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2147483650, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":16,
"width":16,
"x":0,
"y":0
}],
"height":16,
"id":2,
"name":"platforms",
"opacity":1,
"startx":0,
"starty":0,
"type":"tilelayer",
"visible":true,
"width":16,
"x":0,
"y":0
},
{
"chunks":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0,
0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 18, 8, 0, 0, 0, 0,
0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
0, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":16,
"width":16,
"x":0,
"y":0
}],
"height":16,
"id":3,
"name":"ladders",
"opacity":1,
"startx":0,
"starty":0,
"type":"tilelayer",
"visible":true,
"width":16,
"x":0,
"y":0
},
{
"chunks":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 9, 0, 32, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 21, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":16,
"width":16,
"x":0,
"y":0
}],
"height":16,
"id":4,
"name":"barriers",
"opacity":1,
"startx":0,
"starty":0,
"type":"tilelayer",
"visible":true,
"width":16,
"x":0,
"y":0
}],
"nextlayerid":5,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.1-99-gec89c545",
"tileheight":128,
"tilesets":[
{
"firstgid":1,
"source":"platforms.tsx"
}],
"tilewidth":128,
"type":"map",
"version":"1.10",
"width":30
}

View File

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.1-99-gec89c545" name="platforms" tilewidth="128" tileheight="128" tilecount="100" columns="10">
<image source="../sprites/platforms.png" width="1280" height="1280"/>
<tile id="0">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="2">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="3">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="4">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="5">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="6">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="7">
<properties>
<property name="ladder" type="bool" value="true"/>
<property name="top" type="bool" value="true"/>
</properties>
</tile>
<tile id="8">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="red"/>
<property name="pressed" type="bool" value="false"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="9">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="blue"/>
<property name="pressed" type="bool" value="false"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="10">
<properties>
<property name="floor" type="bool" value="true"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="12">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="13">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="14">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="15">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="16">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="17">
<properties>
<property name="ladder" type="bool" value="true"/>
</properties>
</tile>
<tile id="18">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="red"/>
<property name="pressed" type="bool" value="true"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="19">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="blue"/>
<property name="pressed" type="bool" value="true"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="20">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="red"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="21">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="red"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="22">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="23">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="24">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="25">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="26">
<properties>
<property name="floor" type="bool" value="true"/>
</properties>
</tile>
<tile id="28">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="yellow"/>
<property name="pressed" type="bool" value="false"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="29">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="green"/>
<property name="pressed" type="bool" value="false"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="30">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="blue"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="31">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="blue"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="38">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="yellow"/>
<property name="pressed" type="bool" value="true"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="39">
<properties>
<property name="button" type="bool" value="true"/>
<property name="color" value="green"/>
<property name="pressed" type="bool" value="true"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="40">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="yellow"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="41">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="yellow"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
<tile id="50">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="green"/>
<property name="solid" type="bool" value="true"/>
</properties>
</tile>
<tile id="51">
<properties>
<property name="barrier" type="bool" value="true"/>
<property name="color" value="green"/>
<property name="solid" type="bool" value="false"/>
</properties>
</tile>
</tileset>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.1-99-gec89c545" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="128" tileheight="128" infinite="1" nextlayerid="5" nextobjectid="1">
<editorsettings>
<export target="levels-a.json" format="json"/>
</editorsettings>
<tileset firstgid="1" source="platforms.tsx"/>
<layer id="1" name="floor" width="30" height="20">
<data encoding="csv">
<chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
3,4,5,6,4,5,6,4,5,6,4,5,4,5,6,7,
13,14,15,15,15,15,15,15,15,15,15,15,15,15,16,17
</chunk>
<chunk x="0" y="16" width="16" height="16">
23,24,25,25,25,25,25,25,25,25,25,25,25,25,26,27,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
<layer id="2" name="platforms" width="30" height="20">
<data encoding="csv">
<chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,2147483650,1,1,1,1,1,2,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
2147483650,1,1,1,1,1,1,1,1,1,1,1,2,0,0,0,
0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
<layer id="3" name="ladders" width="30" height="20">
<data encoding="csv">
<chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,
0,8,0,0,0,0,0,0,0,0,18,8,0,0,0,0,
0,18,0,0,0,0,0,0,0,0,0,18,0,0,0,0,
0,18,0,0,0,0,0,0,0,0,0,18,0,0,0,0,
0,18,0,0,0,0,0,0,0,0,0,18,0,0,0,0,
0,18,0,0,0,0,0,0,0,0,0,18,0,0,0,0,
0,18,0,0,0,0,0,0,0,0,0,18,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
<layer id="4" name="barriers" width="30" height="20">
<data encoding="csv">
<chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,9,0,32,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,21,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,21,0,20,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</chunk>
</data>
</layer>
</map>

27
index.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bread Baking Bonanza</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
overflow: hidden;
}
canvas { display: block; }
</style>
</head>
<body>
<!-- Phaser 3 as a classic script so window.Phaser is set before main.js runs -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.87.0/dist/phaser.min.js"></script>
<!-- Game bootstrap as ES6 module (deferred by default, runs after scripts above) -->
<script type="module" src="src/main.js"></script>
</body>
</html>

BIN
raw_assets/jeremiah.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 KiB

68
software.md Normal file
View File

@ -0,0 +1,68 @@
# Build Guidelines
Create an HTML Phaser 3 video game. The goal of this game will be to collect "ingredients" and reach the "oven" in order to cook flavored bread. The elements of ingredients and the oven will be introduced at a later time. The game begins on level 1, and will have more levels later so please make a modular framework to support this. Level one will take place on levels-a.json tileset which leverages the platforms.png spritesheet. The player should begin level 1 at coodinates 3, 11 on the tilesheet, and leverages the player.png spritesheet. The game will function as a standard platformer, but with the mechanics outlined below.
## Tools and Organization
- Phaser version 3 HTML game
- Use JavaScript
- Have JavaScript objects reference each other directly via IMPORT and EXPORT using ES6 standards
- Do **NOT** require a web packager.
- Create files and classes in a manner that allows future modifications and scaling at a modular level
## Basic Framework
- 1600 x 900 view
- Scale view to user's viewport.
- Use basic termporary vector graphics that can later be replaced by sprites
## Spritesheet Details
- ./assets/sprites/platforms.png
- 128px X 128px
- Red colored sprites:
- 8, 18, 20, 21
- Yellow colored sprites:
- 28, 38, 40, 41
- Blue colored sprites:
- 9, 19, 30, 31
- Green colored sprites:
- 29, 39, 50, 51
- ./assets/sprites/player.png
- 128px X 256px
- Player standing sprite: 0
- Player walking: animate between 0 and 1 
## Tileset Details
- ./assets/tilesets/levels-a.json
| Tile Property | Value | Description |
| --- | --- | --- |
| floor | true | If player lands on top of this tile, or is on top of the tile, they should not fall through it. The player can jump through a floor but cannot fall through it. |
| solid | true | The player cannot move through this tile from any direction. It should be completely solid and impassable |
| barrier | true | Each {color} barrier has two types of tiles, {solid=true} and {solid=false}. Only one color barrier can be {solid=false} at any time. The solid color is based on the {button=true} {color} that is {pressed=true}. Whenever a {color} {button} becomes pressed all barriers with the same {color} as the {button} become {solid=false} and change to the sprite of the same {color} that is {barrier=true} and {solid=false}. All other barriers change, or remain at, {solid=true} with their corresponding {color} |
| ladder | true | These tiles are ladders, the player should be able to climb up and down these. When climbing up or down the ladder, the player should snap to the center of the ladder. They should also be able to move left or right and fall off the ladder after leaving it's bounds. |
| button | true | Each {color} button has two types of tiles, {pressed=true} and {pressed=false}. Only one button can be pressed at a time. A button is pressed by the player jumping on top of the button. Once the player jumps on the button it becomes {pressed=true} and switches it's tile to the correct tile in the tileset. All other buttons become {pressed=false} and change their tile to the corresponding tile for their {color}. Then all barrier rules are applied. |
| color | { red \| yellow \| blue \| green } | There are four colored {buttons} and {barriers} that are possible to exist on a level. All {buttons} and {barriers} are connected to each other by {color} |
## Gameplay
- Things I will add later.
- Enemies
- ingredients
- oven
- keys and locks
- background images for levels
- background music
- sound effects
## Controls
- Left and right arrow keys to walk
- Up and Down when over a ladder to climb up and down
- Up when not on a ladder, or when on a {top=true} ladder, to jump
## Mechanics
\- The player should have three total lives to start with. Later in game design we'll add ways to get more lives.

146
src/entities/Player.js Normal file
View File

@ -0,0 +1,146 @@
import TilePropertyHelper from '../systems/TilePropertyHelper.js';
export default class Player {
/**
* @param {Phaser.Scene} scene
* @param {number} x World pixel x (center)
* @param {number} y World pixel y (center)
* @param {Phaser.Tilemaps.TilemapLayer} laddersLayer
*/
constructor(scene, x, y, laddersLayer) {
this._scene = scene;
this._laddersLayer = laddersLayer;
this.lives = 3;
this.onLadder = false;
this.atLadderTop = false;
// Create physics sprite
this._sprite = scene.physics.add.sprite(x, y, 'player', 0);
this._sprite.setCollideWorldBounds(false);
// Body is narrower/shorter than the 128x256 frame to fit the character artwork
this._sprite.body.setSize(80, 240).setOffset(24, 8);
// Animations
scene.anims.create({
key: 'walk',
frames: scene.anims.generateFrameNumbers('player', { start: 0, end: 1 }),
frameRate: 8,
repeat: -1,
});
scene.anims.create({
key: 'stand',
frames: [{ key: 'player', frame: 0 }],
frameRate: 1,
repeat: 0,
});
this._cursors = scene.input.keyboard.createCursorKeys();
this._jumpKey = this._cursors.up;
}
get sprite() { return this._sprite; }
get body() { return this._sprite.body; }
update(time, delta) {
this._handleLadder();
this._handleMovement();
this._updateAnimation();
}
_handleLadder() {
const sprite = this._sprite;
const cursors = this._cursors;
// Tile at player center (for body-of-ladder detection)
const centerTile = this._laddersLayer.getTileAtWorldXY(sprite.x, sprite.y);
// Tile at player feet (for ladder-top detection)
const feetTile = this._laddersLayer.getTileAtWorldXY(sprite.x, sprite.y + 116);
const centerIsLadder = centerTile && TilePropertyHelper.isLadder(centerTile.index);
const feetIsLadderTop = feetTile && TilePropertyHelper.isLadderTop(feetTile.index);
// Update atLadderTop: feet are resting on a top rung and not actively climbing
this.atLadderTop = !!(feetIsLadderTop && !this.onLadder);
if (this.onLadder) {
// Move up or down
if (cursors.up.isDown) {
sprite.body.setVelocityY(-200);
} else if (cursors.down.isDown) {
sprite.body.setVelocityY(200);
} else {
sprite.body.setVelocityY(0);
}
// Exit ladder when pressing left or right
if (cursors.left.isDown || cursors.right.isDown) {
this._exitLadder();
return;
}
// Exit ladder if center is no longer over a ladder tile
if (!centerIsLadder) {
this._exitLadder();
}
} else {
// Enter ladder when player center overlaps a ladder tile and pressing up/down
if (centerIsLadder && (cursors.up.isDown || cursors.down.isDown)) {
this.onLadder = true;
sprite.body.setGravityY(-800); // Cancels world gravity (800 + -800 = 0)
sprite.body.setVelocityY(0);
sprite.body.setVelocityX(0);
// Snap horizontally to ladder center
sprite.x = centerTile.pixelX + 64;
}
}
}
_exitLadder() {
this.onLadder = false;
this._sprite.body.setGravityY(0); // Remove local gravity override; world gravity resumes
}
_handleMovement() {
if (this.onLadder) return;
const sprite = this._sprite;
const cursors = this._cursors;
const body = sprite.body;
// Horizontal movement
if (cursors.left.isDown) {
body.setVelocityX(-250);
sprite.setFlipX(true);
} else if (cursors.right.isDown) {
body.setVelocityX(250);
sprite.setFlipX(false);
} else {
body.setVelocityX(0);
}
// Jump: only on key press (JustDown prevents hold-to-jump repeat)
// Allowed when: grounded AND (not on ladder OR standing at ladder top)
if (Phaser.Input.Keyboard.JustDown(this._jumpKey)) {
if (body.blocked.down) {
body.setVelocityY(-550);
}
}
}
_updateAnimation() {
const sprite = this._sprite;
if (this.onLadder) {
sprite.anims.stop();
sprite.setFrame(0);
return;
}
if (sprite.body.velocity.x !== 0) {
sprite.anims.play('walk', true);
} else {
sprite.anims.play('stand', true);
}
}
}

23
src/main.js Normal file
View File

@ -0,0 +1,23 @@
import BootScene from './scenes/BootScene.js';
import Level1Scene from './scenes/Level1Scene.js';
const config = {
type: Phaser.AUTO,
width: 1600,
height: 900,
backgroundColor: '#1a1a2e',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
physics: {
default: 'arcade',
arcade: {
gravity: { y: 800 },
debug: false,
},
},
scene: [BootScene, Level1Scene],
};
new Phaser.Game(config);

24
src/scenes/BootScene.js Normal file
View File

@ -0,0 +1,24 @@
export default class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
preload() {
// Tiled JSON map (infinite chunked format)
this.load.tilemapTiledJSON('level1', '../../assets/tilesets/levels-a.json');
// Tileset spritesheet (1280x1280, 128x128 tiles)
// Key must match the name Phaser derives from the TSX source: 'platforms'
this.load.image('platforms', '../../assets/sprites/platforms.png');
// Player spritesheet (128x256 per frame)
this.load.spritesheet('player', '../../assets/sprites/player.png', {
frameWidth: 128,
frameHeight: 256,
});
}
create() {
this.scene.start('Level1Scene');
}
}

219
src/scenes/Level1Scene.js Normal file
View File

@ -0,0 +1,219 @@
import Player from '../entities/Player.js';
import ButtonBarrierSystem from '../systems/ButtonBarrierSystem.js';
import TilePropertyHelper, {
FLOOR_GIDS,
SOLID_FLOOR_GID,
LADDER_TOP_GID,
} from '../systems/TilePropertyHelper.js';
export default class Level1Scene extends Phaser.Scene {
constructor() {
super({ key: 'Level1Scene' });
}
create() {
// -------------------------
// Tilemap — patch external tileset reference
// -------------------------
// levels-a.json references platforms.tsx as an external source.
// Phaser cannot fetch .tsx files, so the tileset object in the JSON cache
// has no `tiles` array, causing a crash when the parser tries tileset.tiles[n].
// We replace the external reference with the required metadata before parsing.
const cachedMap = this.cache.tilemap.get('level1');
if (cachedMap) {
// Fix 1: Replace external tileset source reference with embedded metadata.
// Phaser cannot fetch .tsx files, so the JSON tileset object has no `tiles`
// array, causing tileset.tiles[n] to throw during parsing.
cachedMap.data.tilesets = cachedMap.data.tilesets.map(ts => {
if (!ts.source) return ts;
const name = ts.source.split('/').pop().replace(/\.\w+$/, '');
return {
firstgid: ts.firstgid,
name,
columns: 10,
tilecount: 100,
tileheight: 128,
tilewidth: 128,
image: '../sprites/platforms.png',
imageheight: 1280,
imagewidth: 1280,
margin: 0,
spacing: 0,
tiles: [],
};
});
// Fix 2: Normalize startx/starty on every infinite-map layer.
// Phaser places chunk data into the layer data array at index
// [chunk.y - starty][chunk.x - startx]
// then renders data[row][col] at world pixel (col*tw, row*th).
// The floor layer has startx=-16, starty=-16 even though all chunks
// sit at x>=0, y>=0 — this pushes floor tiles to data rows 30+ which
// maps to world y ~3840px, far off-screen. Setting startx/starty to the
// actual minimum chunk coordinates fixes the placement.
for (const layer of cachedMap.data.layers) {
if (layer.type !== 'tilelayer' || !layer.chunks || !layer.chunks.length) continue;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const chunk of layer.chunks) {
minX = Math.min(minX, chunk.x);
minY = Math.min(minY, chunk.y);
maxX = Math.max(maxX, chunk.x + chunk.width);
maxY = Math.max(maxY, chunk.y + chunk.height);
}
layer.startx = minX;
layer.starty = minY;
layer.width = maxX - minX;
layer.height = maxY - minY;
}
}
const map = this.make.tilemap({ key: 'level1' });
// 'platforms' = name Phaser derives from the external tileset source 'platforms.tsx'
// If tiles render blank, verify with: console.log(map.tilesets[0].name)
const tileset = map.addTilesetImage('platforms', 'platforms', 128, 128, 0, 0);
this._floorLayer = map.createLayer('floor', tileset, 0, 0);
this._platformsLayer = map.createLayer('platforms', tileset, 0, 0);
this._laddersLayer = map.createLayer('ladders', tileset, 0, 0);
this._barriersLayer = map.createLayer('barriers', tileset, 0, 0);
// -------------------------
// Tile collision setup
// -------------------------
// Floor layer: one-way — only collide when player falls down
this._floorLayer.setCollision(FLOOR_GIDS);
// Platforms layer: floor tiles (one-way) + the one solid wall tile
this._platformsLayer.setCollision([...FLOOR_GIDS, SOLID_FLOOR_GID]);
// Ladder layer: only the top rung acts as a one-way surface
this._laddersLayer.setCollision([LADDER_TOP_GID]);
// Barrier layer: initial collision set by ButtonBarrierSystem.init()
// -------------------------
// Player
// -------------------------
// Spawn at tile (3, 11) — center of tile in world pixels
const spawnX = 3 * 128 + 64; // 448
const spawnY = 11 * 128 + 64; // 1472
this._player = new Player(this, spawnX, spawnY, this._laddersLayer);
const playerSprite = this._player.sprite;
// -------------------------
// Button / Barrier system
// -------------------------
this._bbSystem = new ButtonBarrierSystem(this, this._barriersLayer);
this._bbSystem.init();
// -------------------------
// Physics colliders
// -------------------------
// Floor layer: one-way (pass through from below, land from above)
this.physics.add.collider(
playerSprite,
this._floorLayer,
null,
(player, tile) => player.body.velocity.y >= 0,
this
);
// Platforms layer: one-way for floor tiles; full collision for solid wall tile
this.physics.add.collider(
playerSprite,
this._platformsLayer,
null,
(player, tile) => {
if (tile.index === SOLID_FLOOR_GID) return true;
return player.body.velocity.y >= 0;
},
this
);
// Ladder layer: top rung as one-way floor; disabled while actively climbing
this.physics.add.collider(
playerSprite,
this._laddersLayer,
null,
(player, tile) => !this._player.onLadder && player.body.velocity.y >= 0,
this
);
// Barriers layer: dynamic — processCallback checks current solid state per tile
this.physics.add.collider(
playerSprite,
this._barriersLayer,
null,
(player, tile) => {
const props = TilePropertyHelper.getPropsByGID(tile.index);
return !!(props && props.solid);
},
this
);
// -------------------------
// Camera
// -------------------------
const worldW = map.width * map.tileWidth; // 30 * 128 = 3840
const worldH = map.height * map.tileHeight; // 20 * 128 = 2560
this.cameras.main
.setBounds(0, 0, worldW, worldH)
.startFollow(playerSprite, true, 0.1, 0.1);
this.physics.world.setBounds(0, 0, worldW, worldH);
// -------------------------
// HUD
// -------------------------
this._livesText = this.add
.text(16, 16, 'Lives: 3', {
fontFamily: 'monospace',
fontSize: '28px',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 4,
})
.setScrollFactor(0)
.setDepth(100);
// Track velocity from the previous frame for button-landing detection
this._prevVelocityY = 0;
}
update(time, delta) {
// Store velocity BEFORE player.update() so we know if player was falling
this._prevVelocityY = this._player.body.velocity.y;
this._player.update(time, delta);
this._checkButtonPress();
this._livesText.setText('Lives: ' + this._player.lives);
}
/**
* Detect when the player lands on top of an unpressed button tile and press it.
* Uses prevVelocityY to distinguish a real landing from walking across the top.
*/
_checkButtonPress() {
const body = this._player.body;
// Player must be touching the ground this frame
if (!body.blocked.down) return;
// Must have been falling (not just standing; threshold filters walking-on-top)
if (this._prevVelocityY < 50) return;
const sprite = this._player.sprite;
// Sample a point just below the player's feet center
const feetX = sprite.x;
const feetY = sprite.y + 120; // approx bottom of physics body
const tile = this._barriersLayer.getTileAtWorldXY(feetX, feetY + 4);
if (!tile || tile.index <= 0) return;
if (this._bbSystem.isButtonTile(tile.index)) {
this._bbSystem.pressButton(tile.x, tile.y);
}
}
}

View File

@ -0,0 +1,113 @@
import TilePropertyHelper, { COLOR_TILES } from './TilePropertyHelper.js';
export default class ButtonBarrierSystem {
/**
* @param {Phaser.Scene} scene
* @param {Phaser.Tilemaps.TilemapLayer} barriersLayer
*/
constructor(scene, barriersLayer) {
this._scene = scene;
this._barriersLayer = barriersLayer;
this._buttons = []; // [{ col, row, color }]
this._barriers = []; // [{ col, row, color }]
this._activeColor = null;
}
/**
* Scan the barriers layer to build button/barrier registries and set initial
* collision state. Call once after the layer is created.
*/
init() {
const layer = this._barriersLayer;
layer.forEachTile((tile) => {
if (tile.index <= 0) return;
const props = TilePropertyHelper.getPropsByGID(tile.index);
if (props.button) {
this._buttons.push({ col: tile.x, row: tile.y, color: props.color });
if (props.pressed) {
this._activeColor = props.color;
}
} else if (props.barrier) {
this._barriers.push({ col: tile.x, row: tile.y, color: props.color });
}
});
this._rebuildCollision();
}
/**
* Press the button at the given tile coordinates.
* Updates button/barrier tile indices and rebuilds collision.
* @param {number} col Tile column
* @param {number} row Tile row
*/
pressButton(col, row) {
const entry = this._buttons.find(b => b.col === col && b.row === row);
if (!entry) return;
if (entry.color === this._activeColor) return; // Already active
this._activeColor = entry.color;
// Update all button tiles
for (const btn of this._buttons) {
const gids = COLOR_TILES[btn.color];
const newGID = btn.color === this._activeColor ? gids.btn_on : gids.btn_off;
this._barriersLayer.putTileAt(newGID, btn.col, btn.row);
}
// Update all barrier tiles
for (const bar of this._barriers) {
const gids = COLOR_TILES[bar.color];
const newGID = bar.color === this._activeColor
? gids.barrier_open
: gids.barrier_solid;
this._barriersLayer.putTileAt(newGID, bar.col, bar.row);
}
this._rebuildCollision();
}
/**
* Returns true if the given GID is an unpressed button (a valid press target).
* @param {number} gid
*/
isButtonTile(gid) {
const props = TilePropertyHelper.getPropsByGID(gid);
return !!(props.button && !props.pressed);
}
/**
* Rebuild which tiles in the barriers layer have active collision.
* Solid: all unpressed buttons + all non-active-color solid barriers.
* Passable: active-color barriers + pressed button.
*/
_rebuildCollision() {
const solidGIDs = [];
for (const [color, gids] of Object.entries(COLOR_TILES)) {
if (color !== this._activeColor) {
solidGIDs.push(gids.btn_off);
solidGIDs.push(gids.barrier_solid);
}
// Active color's btn_on and barrier_open remain passable (not added)
}
// setCollision(indices, collides, recalculate)
// Passing false to a second call clears existing, then we set only solidGIDs
this._barriersLayer.setCollision(solidGIDs, true, true);
// Explicitly remove collision from active-color tiles (in case of leftovers)
if (this._activeColor) {
const gids = COLOR_TILES[this._activeColor];
this._barriersLayer.setCollision(
[gids.btn_on, gids.barrier_open],
false,
true
);
}
}
}

View File

@ -0,0 +1,105 @@
/**
* TilePropertyHelper.js
*
* Static lookup table for all tile properties, transcribed from platforms.tsx.
* Phaser cannot load external TSX files, so properties are defined here.
*
* IMPORTANT: Phaser's tile.index is a 1-based GID.
* - Use getPropsByGID(tile.index) when working with Phaser tile objects.
* - Use getProps(tileId) with 0-based IDs from the TSX.
* - All setCollision() / putTileAt() calls use 1-based GIDs.
*/
// 0-based tile ID -> property object (source: assets/tilesets/platforms.tsx)
const TILE_PROPS = {
0: { floor: true },
2: { floor: true },
3: { floor: true },
4: { floor: true },
5: { floor: true },
6: { floor: true },
7: { ladder: true, top: true },
8: { button: true, color: 'red', pressed: false, solid: true },
9: { button: true, color: 'blue', pressed: false, solid: true },
10: { floor: true, solid: true },
12: { floor: true },
13: { floor: true },
14: { floor: true },
15: { floor: true },
16: { floor: true },
17: { ladder: true },
18: { button: true, color: 'red', pressed: true, solid: false },
19: { button: true, color: 'blue', pressed: true, solid: false },
20: { barrier: true, color: 'red', solid: true },
21: { barrier: true, color: 'red', solid: false },
22: { floor: true },
23: { floor: true },
24: { floor: true },
25: { floor: true },
26: { floor: true },
28: { button: true, color: 'yellow', pressed: false, solid: true },
29: { button: true, color: 'green', pressed: false, solid: true },
30: { barrier: true, color: 'blue', solid: true },
31: { barrier: true, color: 'blue', solid: false },
38: { button: true, color: 'yellow', pressed: true, solid: false },
39: { button: true, color: 'green', pressed: true, solid: false },
40: { barrier: true, color: 'yellow', solid: true },
41: { barrier: true, color: 'yellow', solid: false },
50: { barrier: true, color: 'green', solid: true },
51: { barrier: true, color: 'green', solid: false },
};
/**
* GIDs (1-based, matching Phaser tile.index) for each color's button/barrier tiles.
* btn_off = unpressed button GID
* btn_on = pressed button GID
* barrier_solid = solid barrier GID
* barrier_open = passable barrier GID
*/
export const COLOR_TILES = {
red: { btn_off: 9, btn_on: 19, barrier_solid: 21, barrier_open: 22 },
blue: { btn_off: 10, btn_on: 20, barrier_solid: 31, barrier_open: 32 },
yellow: { btn_off: 29, btn_on: 39, barrier_solid: 41, barrier_open: 42 },
green: { btn_off: 30, btn_on: 40, barrier_solid: 51, barrier_open: 52 },
};
// GIDs of floor tiles for one-way collision (does not include SOLID_FLOOR_GID)
export const FLOOR_GIDS = [
1, // tile_id 0
3, 4, 5, 6, 7, // tile_id 26
13, 14, 15, 16, 17, // tile_id 1216
23, 24, 25, 26, 27, // tile_id 2226
];
// GID of the tile that is both floor AND solid (impassable from all sides)
export const SOLID_FLOOR_GID = 11; // tile_id 10
// Ladder GIDs
export const LADDER_GID = 18; // tile_id 17 — standard rung
export const LADDER_TOP_GID = 8; // tile_id 7 — top rung (allows jumping)
// All GIDs that start as solid in the barriers layer (unpressed buttons + solid barriers)
export const SOLID_BARRIER_GIDS = [
9, 10, 29, 30, // unpressed buttons: red, blue, yellow, green
21, 31, 41, 51, // solid barriers: red, blue, yellow, green
];
export default class TilePropertyHelper {
/** Returns property object for a 0-based tile ID (TSX convention). */
static getProps(tileId) {
return TILE_PROPS[tileId] || {};
}
/** Returns property object for a 1-based GID (Phaser tile.index convention). */
static getPropsByGID(gid) {
return TILE_PROPS[gid - 1] || {};
}
static isFloor(gid) { return !!(this.getPropsByGID(gid).floor); }
static isSolid(gid) { return !!(this.getPropsByGID(gid).solid); }
static isLadder(gid) { return !!(this.getPropsByGID(gid).ladder); }
static isLadderTop(gid) { const p = this.getPropsByGID(gid); return !!(p.ladder && p.top); }
static isButton(gid) { return !!(this.getPropsByGID(gid).button); }
static isBarrier(gid) { return !!(this.getPropsByGID(gid).barrier); }
static getColor(gid) { return this.getPropsByGID(gid).color || null; }
}

1
start_web.bat Normal file
View File

@ -0,0 +1 @@
python -m http.server 8000

4
start_web.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Start a simple HTTP server on port 8000
python3 -m http.server 8000