- Create `src/data/themes.js` with theme definitions (background color, parallax layers) and a `getTheme()` resolver that falls back to campaign 1's default look
- Update `PlayScene` to load the active campaign's theme instead of hardcoding colors/texture keys
- Register campaign 2's `_2` background assets in the asset manifest with placeholder fallbacks
- Add updated background art for campaign 2 (`bg_far_2.png`)
- Register `halfpipe` and `reverseHalfpipe` in SECTION_TYPES and GENERATORS
- Implement both as single cubic Bezier curves with vertical tangents at entrance/exit:
- halfpipe: near-vertical drop into a rounded bottom, then a shorter climb; exit ends below entrance (net decline)
- reverseHalfpipe: vertical mirror of halfpipe — short rise arcing up and over, then a longer drop; exit also ends below entrance
- Use monotonic x(t) = W * t^2 * (3 - 2t) to keep terrain valid as y(x)
- Sample each profile with 24 points and snap endpoints exactly for seamless section joins
- Scale vertical magnitudes by vScale; chosen defaults yield ~350 base-unit drop and ~210 net decline at default width
Introduces a subtle fade-in/out name pill for completed level nodes on hover,
improving discoverability of level names without permanently cluttering the UI.
Also adds voice assets for levels 2–10.
- Add per-item sound keys to `SmashItemManager` and `BonusItemManager`, playing a dedicated clip on smash/collection (load-tolerant via `cache.audio.exists`)
- Preload new FX clips: `smash-{tv,chair,cone}` and `bonus-{cash,ctc,guitar}`
- Rebuild level07 terrain/layout with updated items, bonus placements, goal, and camera bounds
- Move level01 item positions to match new layout
- Update smash-items sprite sheet (PNG/PSD)
Introduces a new "bonus item" system alongside the existing smashables:
- **Gameplay**: Floating collectible prizes (CTC box, guitar, cash) that the bus passes through with zero physical response. On first contact, the item pops (scale-up + fade) and spawns a radial burst of tinted firework particles that arc down under gravity, settle on terrain, and fade out. Each collected item scores 100 points on the end-of-level tally.
- **Config**: `BONUS_ITEM` in `config.js` holds all tuning knobs (particle count, speed range, spin, lifetime, per-type tint palette, pop scale/duration). `SCORE.perBonusItem` added.
- **Entity**: New `BonusItemManager` class owns static sensor bodies for each bonus item, detects bus collisions, and orchestrates the pop + firework burst animation. Particles are real Matter bodies exempted from bus/kid collision so they can land on terrain without affecting gameplay.
- **Editor**: "Smashables" section renamed to "Placeables" with a kind toggle (Smashables vs Bonus items). Bonus items are float-only with no gravity option. Preview map, click-to-place, item list, and export all support both kinds. `splitPlaceables()` in `levelBuilder.js` splits the unified editor list into the two level arrays (`items` / `bonusItems`).
- **Levels**: Levels 2–6 updated with bonus item placements (and some additional smashable items).
- **Assets**: New `bonus-items.png` spritesheet (9 frames: 3 bonus items × [item + 2 firework objects]) registered in the asset manifest.
- **Score screen**: `_revealBonus()` method in `LevelScoreScene` displays collected bonus count and points, skipped when zero.
The previous fix only exempted the bus's `busCategory` from smashed item collisions, leaving items still colliding with and ejecting kids riding at the open top of the compartment. This commit:
- Adds `~this.bus.kidCategory` to the initial collision filter in `_buildItem`, so new smash items are ghost-to-kids from creation
- Updates the post-smash mask update to also clear `kidCategory` (previously only `busCategory` was cleared), preventing a regression where item↔kid collisions would silently re-enable after the smash animation
Items remain solid against terrain and other props, preserving normal landing/physics behavior while ensuring their impact can no longer contribute to ejecting kids.
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.
- New SmashItemManager entity that spawns items from level data, detects bus contact, swaps to smashed frame, and applies up-and-forward impulse with tumble
- Add `SMASH_ITEM` config block for launch speed/angle/spin, density, friction, restitution, and float height
- Extend asset manifest and spritesheet (6 frames: intact/smashed TV, chair, cone)
- Level editor gains "Smashables" section to place/delete items via click on preview map; exports `items` array in level data
- Level editor can now import bundled or file-based levels, preserving exact terrain while locking section editing; metadata and items remain editable for re-export
- Score scene reveals smashed item count as a bonus line (+50 each) between kids and time tally
- PlayScene wires SmashItemManager lifecycle and passes smashedCount to score scene
- README documents smashables feature, editor workflow, and import behavior
- New SmashItemManager entity that spawns items from level data, detects bus contact, swaps to smashed frame, and applies up-and-forward impulse with tumble
- Add `SMASH_ITEM` config block for launch speed/angle/spin, density, friction, restitution, and float height
- Extend asset manifest and spritesheet (6 frames: intact/smashed TV, chair, cone)
- Level editor gains "Smashables" section to place/delete items via click on preview map; exports `items` array in level data
- Level editor can now import bundled or file-based levels, preserving exact terrain while locking section editing; metadata and items remain editable for re-export
- Score scene reveals smashed item count as a bonus line (+50 each) between kids and time tally
- PlayScene wires SmashItemManager lifecycle and passes smashedCount to score scene
- README documents smashables feature, editor workflow, and import behavior
The previous clamp logic only bounded localY (vertical) after a radius check, which couldn't distinguish between:
1. A kid that tunneled through the floor/wall (should be clamped back)
2. A kid resting on nearby ground below the bus chassis (should NOT be clamped)
Since the bus floats ~chassis-height above ground, an outside kid sitting on the ground would have localY below the floor line and get yanked up into the bus every tick until it exited compartmentClampRadius.
Added a horizontal span check that only clamps kids whose |localX| is within the actual compartment extent (half-width + wall thickness + kid radius). This ensures:
- Kids that tunneled through floor/walls are still corrected (they're inside the span when they cross it)
- Kids outside the bus's horizontal footprint are left alone, preventing the "ejected kid teleported to bus" bug
Also disabled COMPARTMENT_DEBUG mode in config.js.
Introduce `COMPARTMENT_DEBUG` config flag and a new `compartmentDebug.js` utility that renders the invisible bus compartment (floor, left/right walls, and open-top boundary) as colored rectangles tracking the chassis each frame. Includes per-kid position markers color-coded by state (aboard/ejected) and a live HUD readout showing local coordinates and ejection timing to diagnose why kids eject unexpectedly.
The old flat `Rectangle` background couldn't have rounded corners, so buttons visually clashed with the pill UI elements. Switching to a `Graphics` object lets us draw a rounded rect (using the shared `PILL_RADIUS`) while preserving the same fill/stroke colors and hover behavior.
Key details:
- Added `drawBg()` helper that clears and redraws the rounded rect + outline, reused for both normal and hover states.
- Replaced `setFillStyle` hover handlers with re-draw calls since Graphics has no fill style API.
- Defined an explicit hit area matching the drawn rounded rect; without it, `setInteractive()` on a Graphics uses the full 512x512 texture frame, causing clicks near other buttons to land on the wrong one.
This change introduces a new `src/util/pill.js` module that provides reusable helpers for creating "pill"-style UI labels: a semi-transparent white rounded rectangle backing (with black stroke) sized to the text, plus optional interactivity. It replaces the previous pattern of using Phaser's plain rectangular `backgroundColor` on Text objects, which couldn't produce rounded corners or consistent padding/stroke styling.
Key changes:
- Add `createPill(scene, x, y, text, style, originX, originY)` returning a Container with graphics backing + text, supporting corner origins and automatic re-fitting when text updates.
- Add `pillInteractive(container, config)` to make pills clickable with a properly sized hit area (Containers lack texture frames by default).
- Update UI scenes (`MainMenuScene`, `PlayScene`, `LevelSelectScene`, `LevelScoreScene`) to use `createPill` for HUD elements, labels, score/time displays, and menu/back buttons.
- Remove per-call `backgroundColor` usage in favor of the shared pill styling constants in `pill.js`.
This centralizes pill styling and behavior, improves visual consistency (rounded corners, padding, stroke), and simplifies interaction setup across scenes.
Cleared nodes on the level select map now display the player's best score in a gold pill that rolls up from 0 on entrance, replacing the previous "kids saved" label. The tally scene records each run's final score alongside kids saved/total via `markLevelComplete`, and new `getBestScore` helper exposes it for display.
- Add levels 7-10 and move all 10 into campaign01 (campaign02 left empty
as scaffolding)
- Replace the old S-curve auto-layout with a 2-lane circuit: top lane
runs right→left, bottom lane left→right, finish flag in the
bottom-right corner. Comfortable up to ~12 levels per campaign.
- Re-export node/flag sprites as tight 512x512 badges (badge IS the
file) instead of centered-on-960x540 frames; update mapArt.js tables
(BADGE_FILE_FRAC, BADGE_HIT_FRAC, NODE_ART_FRAMES) and asset manifest
accordingly
- Size the finish-flag nudge to stay on-screen when a custom `positions`
layout ends at an edge (flips direction if out of bounds)
- Guard against empty campaigns: skip path/flag rendering when there are
no points, and bail early in _playFirst()
- Shrink node pill font slightly so the longest tag fits under tightly
spaced nodes
- Update README.md and sprites.md to reflect the new sprite convention,
lane geometry, and level count
- Refactor level select scene to handle variable node texture dimensions
instead of assuming all node art is centered on a 960×540 canvas
- Change BADGE_FILE_FRAC and nodeDisplaySize to accept per-state keys,
allowing map_node_current (512×512 badge) to differ from the other
nodes (960×540 frames)
- Update map_finish to use its new 512×512 tight export; reduce its
display scale accordingly
- Rename campaigns: "Sunny Suburbs" → "Evergreen Lake",
"Dusk Junction" → "Underprivileged Community"
- Update asset manifest dimensions and sprites.md documentation
Replace the flat level-grid level select screen with a campaign-based
map view. Levels are grouped into themed campaigns (e.g. "Sunny Suburbs",
"Dusk Junction") each with its own background art and node positions
rendered along a Catmull-Rom spline ribbon.
Key changes:
- Introduce CAMPAIGNS array in src/data/levels/index.js with helpers for
campaign/level lookup.
- Add src/data/levels/positioning.js for auto-layout S-curve generation
and Catmull-Rom spline sampling.
- Rewrite LevelSelectScene.js to render per-campaign maps with animated
node entrance, idle pulse on the current level, hover states,
tool-tips for locked levels, and ← → / P keyboard navigation.
- Remove the now-unused LevelCompleteScene; scores now record progress
and return directly to the map via LevelScoreScene.
- Add map assets (campaign backgrounds, node/flag/tag sprites) and
manifest entries with a documented 960x540 centered-canvas convention.
- Add src/util/mapArt.js for shared map geometry constants and scaling.
Replace the flat level-grid level select screen with a campaign-based
map view. Levels are grouped into themed campaigns (e.g. "Sunny Suburbs",
"Dusk Junction") each with its own background art and node positions
rendered along a Catmull-Rom spline ribbon.
Key changes:
- Introduce CAMPAIGNS array in src/data/levels/index.js with helpers for
campaign/level lookup.
- Add src/data/levels/positioning.js for auto-layout S-curve generation
and Catmull-Rom spline sampling.
- Rewrite LevelSelectScene.js to render per-campaign maps with animated
node entrance, idle pulse on the current level, hover states,
tool-tips for locked levels, and ← → / P keyboard navigation.
- Remove the now-unused LevelCompleteScene; scores now record progress
and return directly to the map via LevelScoreScene.
- Add map assets (campaign backgrounds, node/flag/tag sprites) and
manifest entries with a documented 960x540 centered-canvas convention.
- Add src/util/mapArt.js for shared map geometry constants and scaling.
- Remove physical ceiling fixture; compartment top is now intentionally open
- Kids eject when passing above the top and re-board when falling back down
- Add KID.ceilingReboardDelayMs to prevent accidental catches from residual
downward drift immediately after ejection
- Introduce a one-shot `ejected` latch to prevent kids who landed outside
the bus from being pulled back in later
- Replace `isSettledRelativeToChassis` with `_isMovingDownwardIntoCompartment`
to verify downward chassis-relative velocity
- Add "kid_reload" sound effect on successful re-boarding
- Update comments across config, Bus, Kid, and KidManager to document the
new position/velocity-based architecture
Add four new section types to the level editor:
- dropOff: instant vertical drop with gentle downward tail
- gap: flat lead-in, x-gap, flat landing at same height
- lip: flat run into a steepening (t²) kicker ramp
- dropGap: like gap but landing is well below takeoff
Fix Terrain physics body generation to extrude straight down from
segment endpoints instead of using rotated rectangles. A previous
perpendicular-offset approach caused near-vertical segments (dropOff
cliffs) to balloon physics bodies sideways under adjacent terrain,
making the bus ride above the drawn ground.
Update level03 ("Mind the Gap") to use editor-generated terrain data.
Generalize levelBuilder smoothing to detect gap-shaped sections
(structurally via takeoffPoints/landingPoints) rather than by type
string, so future gap types work automatically.
- IntroScene: use bg_main.png poster art as full background, layer
logo.png with a breathing tween aligned to the baked wordmark via
fitted similarity transform constants, and add a double-stroke
"Press any key" prompt (replaced auto-advance with input-only trigger).
- MainMenuScene: replace text title with bg_menu.png background and
centered logo.png overlay; add semi-transparent background pill to
instructions text for readability over the treeline.
- LevelSelectScene: add bg_menu.png background and background pill to
back button text for contrast.
- assetManifest: register bg_main, logo, and bg_menu textures.
- New assets: bg_menu.png, ctc.png, paid.png, bg_main.png, logo.png.
Correct the rectangle center offset calculation in Terrain to account
for both X and Y components perpendicular to each segment. Previously,
only the Y offset was applied, causing physics bodies to shift sideways
on sloped terrain. This fix resolves the bus visibly riding above or
through the ground on non-flat segments.
Also adds background and logo assets.
- Fix terrain preview aspect ratio to match in-game rendering by using
consistent X/Y scaling, preventing slopes from appearing distorted
- Calculate timeLimit automatically based on level length and pace
(TIME_LIMIT_PACE) to ensure consistent difficulty scaling across levels
- Add max-height and vertical scrolling to the level preview panel
- Generate level02 (The Big Jump) from the editor with manual tweak
Introduce LevelScoreScene to display a choreographed score breakdown (base score, per-kid results, time bonus) using a frozen snapshot of the win condition.
Replace generic icon_kid in the HUD with individual kid portraits that update based on each kid's specific status (aboard vs. ejected).
Extract _formatTime to a shared utility used by both PlayScene and LevelScoreScene.
Add audio assets for the scoring sequence.
- Add `isSettledRelativeToChassis()` to measure settling speed relative to the bus instead of absolute world velocity, allowing kids resting inside a moving bus to re-board properly
- Gate re-boarding on being within the compartment's horizontal width to prevent false positives from kids resting on the ground at the bus's height
- Use a separate settling timer (`_slowSinceRelative`) to prevent interference between absolute and relative settle checks
- Increase `compartmentWallThickness` to 16 for better physical collision response against fast-moving kids
- Add `timeLimit` configuration to levels 1-4
- Implement countdown timer that starts when throttle is pressed
- Display remaining time in HUD, turning red when under 10s
- Add 'time-up' failure condition and message in LevelFailedScene
- Add checkered finish line entity with rippling flag pole (purely visual)
- Add three kid fall voice clips with random playback on kid ejection
- Add load tolerance for optional voice assets matching voiceLine.js pattern
- Preload kid fall sounds in PreloadScene and wire into KidManager
Replace flat green ground fill with layered subsoil/road/foliage bands
that match the parallax background palette. Add pebble texture on the
road surface and sparse grass tufts along the edge for visual depth.
Use a hash-based PRNG instead of Math.random() so decorative elements
remain consistent across level rebuilds.
- Implement EngineSound system that pitches engine-heavy.mp3 based on rear
wheel angular velocity, with rate smoothing to avoid stair-stepping
- Add continuous menu music across Intro → MainMenu → LevelSelect scenes
- Add per-level voice-over support (level01: "Get the Money!")
- Redesign level01 with hand-crafted terrain sections (Flat, Incline,
Jump, Roller, Descent, Bumps, Flat, Descent, Bumps, Flat)
- Editor: per-section width and vertical scale (vScale) controls
- Editor: smooth entry blending between sections using angle-based
interpolation (avoids slope overshoot from naive height blending)
- Editor: new size-panel UI with range/number inputs and smooth checkbox
- Fix KidManager re-boarding: ejected kids must be settled before
re-boarding to prevent mid-air teleportation
- Load audio assets in PreloadScene; handle missing voice files gracefully
The level now only fails on "all kids lost" once every ejected kid has
settled outside the bus (restSpeed/settleDuration hysteresis) or fallen
past the camera bounds, giving a rescue window after a jump. Also fails
when the bus itself drops below the level, and guards the compartment
clamp with a distance radius so far-away kids aren't yanked toward the
bus.
Ejection used to be g-force-triggered, but the suspension isolates the
chassis from shocks so the old threshold was effectively unreachable -
kids were never actually being ejected. Instead the bus now carries an
invisible three-sided box (floor + left/right walls, deliberately no
ceiling) pinned to the chassis with rigid constraints. The fixtures are
standalone bodies in a dedicated kid collision category, not compounded
into the chassis, because Matter only consults the root body's filter
on compounds, so only standalone bodies can be made to collide with kids
while the chassis part stays exempt.
KidManager now checks each kid's rotation-corrected chassis-local
position every physics tick and flips state in both directions: crossing
above the open top marks the kid ejected (light custom gravity,
kid_ejected texture), and drifting back down through it re-marks the kid
aboard. A per-tick hard clamp keeps kids from tunneling through the
floor/walls on a fast hit. Seat constraints and the settle timer are
gone - containment is purely physical collision. GForceMonitor is
demoted to a debug-only readout for landing diagnosis.
Kid art is now 5-frame spritesheets (one distinct kid per seat), with
the frame picked by seat index in Kid.js, and render order is fixed with
explicit depths (kids 1, bus chassis/wheels 2).
- Ejection: drop GFORCE.ejectThresholdG from 6 to 0.5 - the bouncier
suspension isolates the chassis from shock, so the old threshold was
effectively unreachable and kids were never actually being ejected.
Ejected kids now pop straight up through the roof (ignoring chassis
velocity/tilt) and fly under a lighter gravity (KID.ejectedGravityScale
0.35, applied via KidManager's beforeupdate handler) for a floaty,
Snuggle-Truck-style arc.
- Cut maxLeanAngularVelocity and leanTorqueStep in half for a slower,
more gradual lean that's easier to fine-tune mid-air.
- Parallax: far layer is now screen-fixed, scaled uniformly (no stretch),
and pans exactly once from start to goal by level progress; mid/near
layers are bottom-anchored and tiled at their own uniform scales. Kids
render at depth -1 so the bus always draws over them.
- Level builder: size camera bounds to the level's actual terrain min/max
Y (with padding, 1000px floor) instead of a fixed 1000 height, so long
runs of declines no longer push the bus below the camera's reach.
- Add level 04 "Mega Drop" (5 kids, extended terrain ending in the mega
drop sequence).
- New browser level editor (editor.html + src/editor/): assemble levels
from snap-together section generators (flat/inclines/hills/ditch/jump)
with a live canvas preview, metadata form, and export to ready-to-drop
levelNN.js source; example export registered as level04.js.
- Add parallax background sprites (bg_far/mid/near).
- Bus retune: softer, bouncier suspension (stiffness 0.45->0.35, damping
0.08->0.035, travel 12->30, restitution 0.3->0.45) with wheel rest
point raised 22->34 for a bouncier stance; drive force cut ~60%
(0.011->0.0045) for a much longer runway and lower top speed.
- Gravity 1 -> 0.675 for ~1.5x hang time off jumps; constraintIterations
2 -> 10 so the soft wishbone springs converge within one step.
- Bus: clamp each wheel to the underside of its wishbone anchor line on
every matter afterupdate, fixing wheels punching through to the
mirrored solution and sticking high on the chassis after hard impacts;
add no-op destroy() for the world listener.
- CameraRig: speed-driven horizontal lookahead (bus framed ~10% from the
left edge at rest, out to 1/3 of the screen at speed) via followOffset;
PlayScene scales the g-force grace period to 5.5s for the new gravity
and calls the new update()/destroy() hooks.
- Extend level01's terrain runway by 30x.
Throttle/brake now apply a direct, ground-only force to the chassis on
top of spinning the wheel, since friction-mediated propulsion alone
couldn't reach a satisfying top speed. isGrounded() checks the engine's
live collision pairs instead of tallying start/end events, which left a
counter permanently stuck above zero when crossing segment seams and
turned airborne throttle into a rocket thruster.
Kid.destroy() and GForceMonitor.destroy() no longer call
matter.world.removeConstraint()/off(): by the time a scene's 'shutdown'
handler runs, the Matter World plugin has already torn down and nulled
this.matter.world, throwing 'Cannot read properties of null'. Matter's
World#shutdown() clears all bodies/constraints/listeners itself anyway,
so both removals were redundant, not just unsafe.
"