The circle hitbox was using a radius of 22 * WORLD_SCALE, which caused gravity items to hover above the ground and other bodies due to a gap between their visual bottom edge and physical collision boundary. Reduced the radius to 16 * WORLD_SCALE (displaySize/4) so that the Matter.js body's on-screen radius of 64 world pixels matches the item's actual visual extent, ensuring items land flush with terrain and other objects instead of floating above them. |
||
|---|---|---|
| assets | ||
| src | ||
| vendor | ||
| README.md | ||
| editor.html | ||
| index.html | ||
| sprites.md | ||
| start_web.sh | ||
README.md
Monsterplex
A Trials-style physics driving game starring a school bus, crossed with Snuggle Truck: land badly enough and the g-force throws your passengers off. Built with Phaser 4, plain ES6 modules, and no package manager.
Running it
Browsers block ES module imports over file://, so serve the folder with any
static file server. No install needed - this ships with Python 3:
python3 -m http.server 8000
Then open http://localhost:8000/.
Controls
- Arrow Up / W - throttle
- Arrow Down / S - brake / reverse
- Arrow Left/Right / A/D - lean the bus left/right
Land smoothly or the g-force throws a kid off the bus. Lose all of them and the level fails.
Project layout
index.html entry point, import map -> vendor/phaser.esm.js
vendor/phaser.esm.js vendored local copy of Phaser 4 (see below)
src/main.js Phaser.Game config + scene list
src/config.js every tunable constant (physics, g-force threshold, camera, ...)
src/scenes/ Boot -> Preload -> Intro -> MainMenu -> LevelSelect (map) -> Play -> LevelScore -> LevelSelect / LevelFailed
src/entities/ Bus, Kid, Terrain
src/systems/ GForceMonitor, KidManager, InputController, CameraRig
src/data/levels/ the hand-authored levels (10, split across the two campaigns)
src/util/ asset manifest, placeholder-texture generator, localStorage progress, small UI helper
assets/ where real art goes (see manifest below) - currently empty
Adding real art
No art assets exist yet - src/util/assetManifest.js lists every image the
game expects, and PreloadScene falls back to a synthesized colored
placeholder (via src/util/placeholderTextures.js) for anything missing.
Drop a real PNG at the exact path below and it's picked up automatically -
no code changes needed.
| key | path | size | notes |
|---|---|---|---|
bus_chassis |
assets/sprites/bus_chassis.png |
170x64 | scaled to this size regardless of source resolution |
bus_wheel |
assets/sprites/bus_wheel.png |
52x52 | used for both wheels |
kid_idle |
assets/sprites/kid_idle.png |
28x28 | passenger while aboard |
kid_ejected |
assets/sprites/kid_ejected.png |
28x28 | passenger after being thrown off |
smash_items |
assets/sprites/smash-items.png |
64x64 x 6 frames | 1x6 spritesheet: intact/smashed TV, chair, cone (see Smashables below) |
bg_far |
assets/backgrounds/bg_far.png |
256x540 | tiled, slowest parallax layer |
bg_mid |
assets/backgrounds/bg_mid.png |
256x540 | tiled, mid parallax layer |
bg_near |
assets/backgrounds/bg_near.png |
256x540 | tiled, fastest parallax layer |
icon_kid |
assets/ui/icon_kid.png |
24x24 | HUD "kids aboard" icon |
favicon |
assets/favicon.png |
32x32 | browser tab icon |
Tuning the feel
Everything worth tweaking lives in src/config.js, notably:
GFORCE.ejectThresholdG- how hard a landing has to be before a kid is thrown off. Not physically derived - tune by playtesting.GFORCE.gracePeriodMs/ejectCooldownMs- ignore-window after level start/restart, and minimum time between two ejections so one bad landing doesn't empty the whole bus at once.BUS.*- suspension stiffness/damping, wheel friction, throttle/lean torque and their clamps.SMASH_ITEM.*- smashable prop physics (launch speed/angle/spin, density, etc. - see the block in config.js for the full list).SMASH_ITEM.launchSpeedandSMASH_ITEM.launchAngleare the two knobs that matter most for "how far and how high the item flies when the bus hits it."
Set DEBUG = true in config.js to show a live g-force readout and Matter's
physics debug overlay in PlayScene.
Smashables (TV / chair / cone)
assets/sprites/smash-items.png is a 1x6 spritesheet (TV, smashed TV, chair,
smashed chair, cone, smashed cone), loaded as the smash_items texture
(64x64 per frame). Any level can carry an optional items array
(see the editor's export for the exact shape):
items: [
{ type: 'tv', mode: 'float', x: 2500, y: 660 },
{ type: 'chair', mode: 'gravity', x: 3500, y: 760 },
]
mode: 'float'- the item rests exactly at(x, y)(a backdrop prop, static body, never moves until smashed).mode: 'gravity'- the item spawns at(x, y)but is a dynamic body, so it drops and settles onto the ground under that X at level start.
The first time the bus (chassis or either wheel) touches an intact item it
swaps to its smashed frame and launches up-and-forward relative to the bus's
travel direction, tumbling, on the world's normal gravity. It lands wherever
it lands and stays. SMASH_ITEM in config.js holds the launch physics.
Smashed items also tally as +SCORE.perSmashItem (50) each on the
end-of-level score screen.
The level editor (editor.html) has a "Smashables" section: pick a type
- mode, then click anywhere on the preview map to drop one there; click an
existing item (on the map or in the list) to remove it. Exporting the level
writes the
itemsarray into the level file automatically.
The editor can also load an existing level (the "Load an existing level"
section) - either pick a bundled level from the dropdown (all levels from
src/data/levels/index.js) or load a levelNN.js file from disk. Its
terrain is preserved exactly: since it wasn't built from sections, the
sections palette / sequence / size controls lock (dimmed and inert) and the
preview + export render the loaded level's own terrain. Details (id / name /
description / kids) and smashables stay editable, so you can tweak them and
re-export. "Start a new level" clears the import and unlocks the section
controls again.
Phaser 4 loading
Phaser is vendored locally at vendor/phaser.esm.js (downloaded once from
jsDelivr's CDN build) and wired up via an import map in index.html, so
every source file just does import Phaser from 'phaser' like a normal npm
project would - the only place that knows about the vendored file is that one
import map line. This keeps the project working fully offline with no
runtime dependency on a third-party CDN staying up.
To upgrade Phaser later: download a newer phaser.esm.js build over
vendor/phaser.esm.js and re-verify Bus.js against the Matter physics
Factory API in the new file (grep for class Factory under the Matter
section).