Go to file
Brian Fertig e6a37c1b3a Add trash can/tent/cat smashables, spray paint/backpack bonus items, and
- Expand smash-items spritesheet from 6 to 12 frames (8x2 grid) with new trash can, tent, and cat props plus their smash sound effects
- Expand bonus-items spritesheet from 12 to 18 frames (9x2 grid) with new spray paint and backpack collectibles plus their firework sounds
- Add level12 ("Curb Check") as a designed level and levels 13-20 as placeholder stubs to fill campaign 2's 10-level map
- Give campaign 2 a custom 3-row serpentine layout on the level select screen to differentiate it from campaign 1's two-lane design
- Update editor preview rendering to handle multi-row spritesheet grids and expose the new item types in the placeables panel
2026-08-26 19:40:26 -06:00
assets Add trash can/tent/cat smashables, spray paint/backpack bonus items, and 2026-08-26 19:40:26 -06:00
src Add trash can/tent/cat smashables, spray paint/backpack bonus items, and 2026-08-26 19:40:26 -06:00
vendor
README.md Add trash can/tent/cat smashables, spray paint/backpack bonus items, and 2026-08-26 19:40:26 -06:00
editor.html feat: add bonus items (CTC box / guitar / cash) with firework burst on collection 2026-08-22 19:00:55 -06:00
index.html
sprites.md Add flour bonus item type with custom firework variant 2026-08-23 19:27:10 -06:00
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 12 frames 8x2 spritesheet: intact/smashed TV, chair, cone, trash can, tent, cat (see Smashables below)
bonus_items assets/sprites/bonus-items.png 64x64 x 18 frames 9x2 spritesheet: CTC box + 2 firework objects, guitar + 2, cash + 2, flour + 2, spray paint + 2, backpack + 2 (see Bonus Items 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.launchSpeed and SMASH_ITEM.launchAngle are 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 / trash can / tent / cat)

assets/sprites/smash-items.png is an 8x2 spritesheet (TV, smashed TV, chair, smashed chair, cone, smashed cone, trash can, smashed trash can, tent, smashed tent, cat, smashed cat), 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 },
  { type: 'trashcan', mode: 'float',   x: 4500, y: 640 },
  { type: 'tent',     mode: 'float',   x: 5500, y: 620 },
  { type: 'cat',      mode: 'gravity', x: 6500, y: 780 },
]
  • 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 "Placeables" section with a kind toggle: Smashables (TV / chair / cone / trash can / tent / cat, float or gravity mode) and Bonus items (CTC box / guitar / cash / flour, float-only). Pick the kind + type, 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 both the items and bonusItems arrays 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 both placeable types stay editable, so you can tweak them and re-export. "Start a new level" clears the import and unlocks the section controls again.

Bonus Items (CTC box / guitar / cash / flour / spray paint / backpack)

assets/sprites/bonus-items.png is a 9x2 spritesheet (CTC box, 2 firework frames, guitar, 2 firework frames, cash, 2 firework frames, flour, 2 firework frames, spray paint, 2 firework frames, backpack, 2 firework frames - 64x64 per frame), loaded as the bonus_items texture. Any level can carry an optional bonusItems array (see the editor's export for the exact shape):

bonusItems: [
  { type: 'ctc',    x: 3000, y: 500 },
  { type: 'cash',   x: 5000, y: 450 },
]

Bonus items are always float - they're static sensors resting exactly at (x, y), never dynamic, never on the ground. The editor enforces this: the bonus-item form has no gravity option, only a type selector.

The first time the bus (chassis or either wheel) touches an uncollected bonus item it pops: the item sprite scales up and fades out over ~200 ms, and a radial burst of the item's two firework-object frames (14 particles, alternating frames and tints) launches outward from the pop point at BONUS_ITEM.minSpeedmaxSpeed (Matter px/step, same convention as SMASH_ITEM.launchSpeed), arcing down under the world's real gravity, then settling on the terrain and fading out after BONUS_ITEM.lifeMs (1.3 s).

Each firework particle is a real Matter body (so gravity and terrain response apply), but its collision mask explicitly excludes the bus and the kids (same trick as SmashItemManager._smash) so it can never bump the bus or flick a kid out of the compartment. It does still collide with terrain so it lands where it lands. Particles are tinted per the bonus type's own color scheme (BONUS_ITEM.colors): CTC = warm toast-brown/cinnamon, guitar = bright/deep green, cash = muted sage/olive-grey, flour = warm off-white/tan, spray paint = pale red/blue, backpack = tan/brown. Some types override the burst's look entirely via BONUS_ITEM.fireworkVariants: flour's puffs launch 2-3x the standard particle size at half opacity (a soft cloud of dust, still with the standard-sized physics bodies underneath), and spray paint's puffs keep the standard size but run at half opacity (a semi-transparent spray mist).

Collected bonus items tally as SCORE.perBonusItem (100) each on the end-of-level score screen, revealed right after the smashed-props line. BONUS_ITEM in config.js holds all the tuning knobs (particle count, speed range, spin, lifetime, tint palette, firework variants, pop scale/duration).

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).