190 lines
9.4 KiB
Markdown
190 lines
9.4 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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):
|
||
|
||
```js
|
||
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):
|
||
|
||
```js
|
||
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.minSpeed`–`maxSpeed` (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).
|