Add flour bonus item type with custom firework variant

- Extend bonus items spritesheet from 9 to 12 frames (add flour + 2 firework objects)
- Add `flour` type to BONUS_ITEM config with warm off-white/tan color palette and firework variant (2-3x scale, half opacity for dust cloud effect)
- Implement per-type firework variants in BonusItemManager._explode() - particles can now have varied sizes and alpha while maintaining standard physics body size
- Add flour SFX asset (bonus-flour.mp3) and preload it
- Update editor to support placing flour bonus items with appropriate icon, color, and sprite frame
- Update all documentation (README, sprites.md) to reflect the new bonus item type

Additionally:
- Add campaign 2 bus physics customization: grippier tires (higher friction coefficients) and stiffer/less-damped suspension for bouncier ride
- Thread per-campaign wheel friction and suspension parameters through Bus entity from themes.js
- Redesign level11 with shorter course, fewer kids (4 vs 3), tighter time limit (42s vs 119s), and flour bonus items replacing most cash/CTC/guitar
This commit is contained in:
Brian Fertig 2026-08-23 19:27:10 -06:00
parent 8cba629232
commit 1eea919c8b
14 changed files with 345 additions and 882 deletions

View File

@ -54,7 +54,7 @@ no code changes needed.**
| `kid_idle` | `assets/sprites/kid_idle.png` | 28x28 | passenger while aboard | | `kid_idle` | `assets/sprites/kid_idle.png` | 28x28 | passenger while aboard |
| `kid_ejected` | `assets/sprites/kid_ejected.png` | 28x28 | passenger after being thrown off | | `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) | | `smash_items` | `assets/sprites/smash-items.png` | 64x64 x 6 frames | 1x6 spritesheet: intact/smashed TV, chair, cone (see Smashables below) |
| `bonus_items` | `assets/sprites/bonus-items.png` | 64x64 x 9 frames | 1x9 spritesheet: CTC box + 2 firework objects, guitar + 2, cash + 2 (see Bonus Items below) | | `bonus_items` | `assets/sprites/bonus-items.png` | 64x64 x 12 frames | 1x12 spritesheet: CTC box + 2 firework objects, guitar + 2, cash + 2, flour + 2 (see Bonus Items below) |
| `bg_far` | `assets/backgrounds/bg_far.png` | 256x540 | tiled, slowest parallax layer | | `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_mid` | `assets/backgrounds/bg_mid.png` | 256x540 | tiled, mid parallax layer |
| `bg_near` | `assets/backgrounds/bg_near.png` | 256x540 | tiled, fastest parallax layer | | `bg_near` | `assets/backgrounds/bg_near.png` | 256x540 | tiled, fastest parallax layer |
@ -108,7 +108,7 @@ end-of-level score screen.
The **level editor** (`editor.html`) has a "Placeables" section with a kind The **level editor** (`editor.html`) has a "Placeables" section with a kind
toggle: **Smashables** (TV / chair / cone, float or gravity mode) and toggle: **Smashables** (TV / chair / cone, float or gravity mode) and
**Bonus items** (CTC box / guitar / cash, float-only). Pick the kind + type, **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 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 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. both the `items` and `bonusItems` arrays into the level file automatically.
@ -123,11 +123,11 @@ 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 them and re-export. "Start a new level" clears the import and unlocks the
section controls again. section controls again.
## Bonus Items (CTC box / guitar / cash) ## Bonus Items (CTC box / guitar / cash / flour)
`assets/sprites/bonus-items.png` is a 1x9 spritesheet (CTC box, 2 firework `assets/sprites/bonus-items.png` is a 1x12 spritesheet (CTC box, 2 firework
frames, guitar, 2 firework frames, cash, 2 firework frames - 64x64 per frames, guitar, 2 firework frames, cash, 2 firework frames, flour, 2 firework
frame), loaded as the `bonus_items` texture. Any level can carry an optional 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` array (see the editor's export for the exact shape):
```js ```js
@ -155,12 +155,17 @@ 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 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 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 color scheme (`BONUS_ITEM.colors`): CTC = warm toast-brown/cinnamon, guitar
= bright/deep green, cash = muted sage/olive-grey. = bright/deep green, cash = muted sage/olive-grey, flour = warm off-white/tan.
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).
Collected bonus items tally as `SCORE.perBonusItem` (100) each on the Collected bonus items tally as `SCORE.perBonusItem` (100) each on the
end-of-level score screen, revealed right after the smashed-props line. end-of-level score screen, revealed right after the smashed-props line.
`BONUS_ITEM` in `config.js` holds all the tuning knobs (particle count, `BONUS_ITEM` in `config.js` holds all the tuning knobs (particle count,
speed range, spin, lifetime, tint palette, pop scale/duration). speed range, spin, lifetime, tint palette, firework variants, pop
scale/duration).
## Phaser 4 loading ## Phaser 4 loading

BIN
assets/fx/bonus-flour.mp3 Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

BIN
assets/voice/level11.mp3 Normal file

Binary file not shown.

View File

@ -51,8 +51,14 @@ with it (same ratios, just re-derive from that one constant).
only the texture is swapped per campaign), and a "lift kit": the wheel only the texture is swapped per campaign), and a "lift kit": the wheel
mount point sits ~8 design units lower under the chassis (see mount point sits ~8 design units lower under the chassis (see
`wheelRestOffsetY2` in `src/config.js` and the `bus` block in `wheelRestOffsetY2` in `src/config.js` and the `bus` block in
`src/data/themes.js`), so it reads as a lifted bus. Both wheels still `src/data/themes.js`), so it reads as a lifted bus. And its tires grip
share one image per campaign. harder: the per-campaign `bus` block also sets the wheel friction
coefficients (`wheelFriction2` / `wheelFrictionStatic2` in
`src/config.js`) - that's a physics feel change, not an art one. Its
shocks are different too - stiffer springs with less damping
(`suspensionStiffness2` / `suspensionDamping2`), so the wheel snaps
back to its rest position faster and bounces more vertically. Both
wheels still share one image per campaign.
- If you want a bus with different proportions (longer, taller, etc.) than - If you want a bus with different proportions (longer, taller, etc.) than
340x128, tell me the size you want art at - that number is also the physics 340x128, tell me the size you want art at - that number is also the physics
body size in `src/config.js` (`BUS.chassisWidth`/`chassisHeight`), so it body size in `src/config.js` (`BUS.chassisWidth`/`chassisHeight`), so it

View File

@ -91,6 +91,12 @@ export const BUS = {
wheelDensity: 0.004, wheelDensity: 0.004,
wheelFriction: 0.9, wheelFriction: 0.9,
wheelFrictionStatic: 1.4, wheelFrictionStatic: 1.4,
// Campaign 2's grippier tires (see `bus` in data/themes.js): higher
// friction coefficients for the bus_wheel_2 rubber - less spin/slip
// under throttle and brake, and the wheel grips the terrain harder.
// Dimensionless like the values above, not WORLD_SCALE-scaled.
wheelFriction2: 1.3,
wheelFrictionStatic2: 2.0,
// Lowered a lot from the original 0.02 - that was quietly eating most of // Lowered a lot from the original 0.02 - that was quietly eating most of
// the wheel's spin-up each second (it's a per-tick multiplicative decay, // the wheel's spin-up each second (it's a per-tick multiplicative decay,
// so 0.02 was burning off roughly a third of angular velocity per second) // so 0.02 was burning off roughly a third of angular velocity per second)
@ -103,6 +109,13 @@ export const BUS = {
// oscillate (bounce) instead of settling immediately - "more shocks." // oscillate (bounce) instead of settling immediately - "more shocks."
suspensionStiffness: 0.35, suspensionStiffness: 0.35,
suspensionDamping: 0.035, suspensionDamping: 0.035,
// Campaign 2's shocks (see `bus` in data/themes.js): stiffer springs that
// snap the wheel back to its rest position faster, paired with LESS
// damping so the extra spring energy survives as visible vertical bounce
// instead of being absorbed - a quicker, bouncier ride than campaign 1's
// soft settle.
suspensionStiffness2: 0.4,
suspensionDamping2: 0.012,
// How far the wheel can move up/down from wheelRestOffsetY. This will // How far the wheel can move up/down from wheelRestOffsetY. This will
// visibly pop the wheel outside its wheel well on a hard landing - that's // visibly pop the wheel outside its wheel well on a hard landing - that's
// the tradeoff for real suspension travel/bounce instead of a stiff ride. // the tradeoff for real suspension travel/bounce instead of a stiff ride.
@ -260,7 +273,7 @@ export const SMASH_ITEM = {
floatHeight: 90 * WORLD_SCALE, floatHeight: 90 * WORLD_SCALE,
}; };
// Bonus items (assets/sprites/bonus-items.png - 9 frames of 64x64: a bonus // Bonus items (assets/sprites/bonus-items.png - 12 frames of 64x64: a bonus
// item followed by its two firework-object frames - see // item followed by its two firework-object frames - see
// src/entities/BonusItemManager.js for the type/frame/color table). Placed // src/entities/BonusItemManager.js for the type/frame/color table). Placed
// per-level via the editor's "Placeable kind" switch ("Bonus items") and // per-level via the editor's "Placeable kind" switch ("Bonus items") and
@ -303,6 +316,18 @@ export const BONUS_ITEM = {
lifeMs: 1300, // how long a particle stays visible; the fade-out is the last 40% of this lifeMs: 1300, // how long a particle stays visible; the fade-out is the last 40% of this
particleDisplaySize: 40 * WORLD_SCALE, // each firework particle is shown square at this size (80 world px, half the bonus item's own 128) particleDisplaySize: 40 * WORLD_SCALE, // each firework particle is shown square at this size (80 world px, half the bonus item's own 128)
// Optional per-type firework overrides (defaults: plain particleDisplaySize,
// full opacity). Flour's dust puffs are the only type that uses one: each
// firework sprite is shown 2-3x the normal particle size at ~half opacity,
// so a hit reads as a soft cloud of dust billowing over the bus instead of
// a confetti pop. Read per item in BonusItemManager._explode (variant =
// BONUS_ITEM.fireworkVariants[item.spec.type] || {}). NOTE: this is purely
// visual - the particles' physics bodies stay at the standard size (below),
// so the bigger sprites don't change how they settle on the terrain.
fireworkVariants: {
flour: { scaleMin: 2, scaleMax: 3, alpha: 0.5 },
},
// Tint per bonus type, matched to each item's art: the firework sprites // Tint per bonus type, matched to each item's art: the firework sprites
// (which are drawn in their own colors) get a PHASER.MULTIPLY tint of // (which are drawn in their own colors) get a PHASER.MULTIPLY tint of
// these so the burst reads as an echo of the item's own color scheme // these so the burst reads as an echo of the item's own color scheme
@ -313,6 +338,7 @@ export const BONUS_ITEM = {
ctc: { main: 0xe0a040, accent: 0x703818 }, // warm toast-brown / cinnamon ctc: { main: 0xe0a040, accent: 0x703818 }, // warm toast-brown / cinnamon
guitar: { main: 0x50c050, accent: 0x105018 }, // bright green / deep forest guitar: { main: 0x50c050, accent: 0x105018 }, // bright green / deep forest
cash: { main: 0xaab888, accent: 0x404030 }, // muted sage / olive-grey (cash's own scheme is near-white grey-green) cash: { main: 0xaab888, accent: 0x404030 }, // muted sage / olive-grey (cash's own scheme is near-white grey-green)
flour: { main: 0xf4f1e8, accent: 0xcbb58e }, // warm off-white / tan (flour's own scheme is pale beige)
}, },
}; };

File diff suppressed because it is too large Load Diff

View File

@ -84,14 +84,21 @@ export const DEFAULT_THEME = {
// `wheelTextureKey` is the texture both wheels render with; `wheelRadius` // `wheelTextureKey` is the texture both wheels render with; `wheelRadius`
// is the physics/display radius in world pixels; `wheelRestOffsetY` is // is the physics/display radius in world pixels; `wheelRestOffsetY` is
// the wheel mount point, local to the chassis (y down) - raising it is // the wheel mount point, local to the chassis (y down) - raising it is
// the "lift kit" knob (campaign 2). A bigger radius keeps the wheel // the "lift kit" knob (campaign 2); `wheelFriction`/`wheelFrictionStatic`
// centered on its axle (the wishbone anchors derive from these // are the tire grip coefficients (campaign 2's tires grip harder);
// offsets, so they stay put) - only the size changes. // `suspensionStiffness`/`suspensionDamping` are the shock settings
// (campaign 2's shocks snap back faster and bounce more). A bigger radius
// keeps the wheel centered on its axle (the wishbone anchors derive from
// these offsets, so they stay put) - only the size changes.
bus: { bus: {
chassisKey: 'bus_chassis', chassisKey: 'bus_chassis',
wheelTextureKey: 'bus_wheel', wheelTextureKey: 'bus_wheel',
wheelRadius: BUS.wheelRadius, wheelRadius: BUS.wheelRadius,
wheelRestOffsetY: BUS.wheelRestOffsetY, wheelRestOffsetY: BUS.wheelRestOffsetY,
wheelFriction: BUS.wheelFriction,
wheelFrictionStatic: BUS.wheelFrictionStatic,
suspensionStiffness: BUS.suspensionStiffness,
suspensionDamping: BUS.suspensionDamping,
}, },
// The three parallax layers, back to front. `far` is the full-viewport // The three parallax layers, back to front. `far` is the full-viewport
// sky; `mid`/`near` are bottom-anchored ground tiles (see PlayScene's // sky; `mid`/`near` are bottom-anchored ground tiles (see PlayScene's
@ -129,6 +136,16 @@ export const THEMES = {
// campaign 1's (see BUS.wheelRestOffsetY2) - the bus sits taller and // campaign 1's (see BUS.wheelRestOffsetY2) - the bus sits taller and
// the bigger wheels hang lower under the body. // the bigger wheels hang lower under the body.
wheelRestOffsetY: BUS.wheelRestOffsetY2, wheelRestOffsetY: BUS.wheelRestOffsetY2,
// Grippier tires (see BUS.wheelFriction2 / wheelFrictionStatic2 in
// config.js) - less slip under throttle/brake, harder grip on the
// road.
wheelFriction: BUS.wheelFriction2,
wheelFrictionStatic: BUS.wheelFrictionStatic2,
// Stiffer, less-damped shocks (see BUS.suspensionStiffness2 /
// suspensionDamping2) - the wheel returns to its rest position
// faster and the extra spring energy shows up as vertical bounce.
suspensionStiffness: BUS.suspensionStiffness2,
suspensionDamping: BUS.suspensionDamping2,
}, },
}, },
// Add campaign03: { ... } here when it's ready. // Add campaign03: { ... } here when it's ready.

View File

@ -29,7 +29,7 @@ const state = {
imported: null, imported: null,
// Smashable props + bonus items placed on the course. Each entry has a // Smashable props + bonus items placed on the course. Each entry has a
// `kind`: 'smashable' (TV/chair/cone - can float or fall, smash off on hit) // `kind`: 'smashable' (TV/chair/cone - can float or fall, smash off on hit)
// or 'bonus' (CTC/guitar/cash - always float, pop into fireworks on hit). // or 'bonus' (CTC/guitar/cash/flour - always float, pop into fireworks on hit).
// x/y are in the SAME scaled world units the preview canvas plots (and that // x/y are in the SAME scaled world units the preview canvas plots (and that
// the exported level uses); refIndex is the track section it was added // the exported level uses); refIndex is the track section it was added
// under (rebased on reordering). // under (rebased on reordering).
@ -204,19 +204,20 @@ const ITEM_SPRITE_FRAMES = { tv: 0, chair: 2, cone: 4 };
const ITEM_SPRITE_CELL = 64; const ITEM_SPRITE_CELL = 64;
// Bonus items ("Placeables" toggle -> "Bonus items"): float-only collectible // Bonus items ("Placeables" toggle -> "Bonus items"): float-only collectible
// prizes - CTC box / guitar / cash. See BonusItemManager.js in the game for // prizes - CTC box / guitar / cash / flour. See BonusItemManager.js in the
// the type/frame/color table; frames here are the BONUS frame of each pair // game for the type/frame/color table; frames here are the BONUS frame of
// (0 CTC, 3 guitar, 6 cash). // each pair (0 CTC, 3 guitar, 6 cash, 9 flour).
const BONUS_TYPES = [ const BONUS_TYPES = [
{ id: 'ctc', label: 'CTC Box' }, { id: 'ctc', label: 'CTC Box' },
{ id: 'guitar', label: 'Guitar' }, { id: 'guitar', label: 'Guitar' },
{ id: 'cash', label: 'Cash' }, { id: 'cash', label: 'Cash' },
{ id: 'flour', label: 'Flour' },
]; ];
const BONUS_COLORS = { ctc: '#e0a040', guitar: '#4b9a45', cash: '#b3ad9c' }; const BONUS_COLORS = { ctc: '#e0a040', guitar: '#4b9a45', cash: '#b3ad9c', flour: '#cbb58e' };
const BONUS_ICONS = { ctc: 'CT', guitar: 'GT', cash: '$' }; const BONUS_ICONS = { ctc: 'CT', guitar: 'GT', cash: '$', flour: 'FL' };
const BONUS_SPRITE = new Image(); const BONUS_SPRITE = new Image();
BONUS_SPRITE.src = 'assets/sprites/bonus-items.png'; BONUS_SPRITE.src = 'assets/sprites/bonus-items.png';
const BONUS_SPRITE_FRAMES = { ctc: 0, guitar: 3, cash: 6 }; const BONUS_SPRITE_FRAMES = { ctc: 0, guitar: 3, cash: 6, flour: 9 };
// Samples the terrain surface Y (highest ground) at a given world X - used // Samples the terrain surface Y (highest ground) at a given world X - used
// to draw the "falls to here" drop line for gravity-mode items in the // to draw the "falls to here" drop line for gravity-mode items in the
@ -249,7 +250,7 @@ function buildItemsForm() {
itemsFormEl.appendChild(fieldset); itemsFormEl.appendChild(fieldset);
// --- Placeable kind toggle: smashables (TV/chair/cone) vs bonus items // --- Placeable kind toggle: smashables (TV/chair/cone) vs bonus items
// (CTC/guitar/cash). Only the relevant type+mode controls show below, // (CTC/guitar/cash/flour). Only the relevant type+mode controls show below,
// and clicks on the preview place whichever kind is selected. // and clicks on the preview place whichever kind is selected.
const kindRadios = []; const kindRadios = [];
for (const kind of ['smashable', 'bonus']) { for (const kind of ['smashable', 'bonus']) {
@ -265,7 +266,7 @@ function buildItemsForm() {
}); });
const label = document.createElement('label'); const label = document.createElement('label');
label.className = 'checkbox-row'; label.className = 'checkbox-row';
label.append(radio, kind === 'smashable' ? 'Smashables (TV / Chair / Cone)' : 'Bonus items (CTC / Guitar / Cash)'); label.append(radio, kind === 'smashable' ? 'Smashables (TV / Chair / Cone)' : 'Bonus items (CTC / Guitar / Cash / Flour)');
kindRadios.push(label); kindRadios.push(label);
} }
const kindWrap = document.createElement('div'); const kindWrap = document.createElement('div');

View File

@ -1,15 +1,17 @@
import { BONUS_ITEM } from '../config.js'; import { BONUS_ITEM } from '../config.js';
// bonus-items.png is 9 frames of 64x64, in order: // bonus-items.png is 12 frames of 64x64, in order:
// 0 CTC box, 1 CTC firework object 1, 2 CTC firework object 2, // 0 CTC box, 1 CTC firework object 1, 2 CTC firework object 2,
// 3 Guitar, 4 Guitar firework object 1, 5 Guitar firework object 2, // 3 Guitar, 4 Guitar firework object 1, 5 Guitar firework object 2,
// 6 Cash, 7 Cash firework object 1, 8 Cash firework object 2. // 6 Cash, 7 Cash firework object 1, 8 Cash firework object 2,
// 9 Flour, 10 Flour firework object 1, 11 Flour firework object 2.
// Each bonus item is followed by its two firework-object frames, which are // Each bonus item is followed by its two firework-object frames, which are
// the sprites the explosion burst is made of (see _explode below). // the sprites the explosion burst is made of (see _explode below).
const BONUS_TYPES = { const BONUS_TYPES = {
ctc: { frame: 0, fireworks: [1, 2], label: 'CTC Box', soundKey: 'bonus_ctc' }, ctc: { frame: 0, fireworks: [1, 2], label: 'CTC Box', soundKey: 'bonus_ctc' },
guitar: { frame: 3, fireworks: [4, 5], label: 'Guitar', soundKey: 'bonus_guitar' }, guitar: { frame: 3, fireworks: [4, 5], label: 'Guitar', soundKey: 'bonus_guitar' },
cash: { frame: 6, fireworks: [7, 8], label: 'Cash', soundKey: 'bonus_cash' }, cash: { frame: 6, fireworks: [7, 8], label: 'Cash', soundKey: 'bonus_cash' },
flour: { frame: 9, fireworks: [10, 11], label: 'Flour', soundKey: 'bonus_flour' },
}; };
export default class BonusItemManager { export default class BonusItemManager {
@ -121,7 +123,7 @@ export default class BonusItemManager {
const { scene } = this; const { scene } = this;
// SFX: the item's own bonus clip (bonus-ctc.mp3 / bonus-guitar.mp3 / // SFX: the item's own bonus clip (bonus-ctc.mp3 / bonus-guitar.mp3 /
// bonus-cash.mp3, loaded in PreloadScene). cache.audio.exists guards // bonus-cash.mp3 / bonus-flour.mp3, loaded in PreloadScene). cache.audio.exists guards
// against a failed load - same load-tolerant pattern as voiceLine.js / // against a failed load - same load-tolerant pattern as voiceLine.js /
// kidFallSound.js, so a missing clip degrades to silent instead of // kidFallSound.js, so a missing clip degrades to silent instead of
// throwing. Each item collects at most once, so no overlap concern. // throwing. Each item collects at most once, so no overlap concern.
@ -145,6 +147,12 @@ export default class BonusItemManager {
const palette = BONUS_ITEM.colors[item.spec.type] || BONUS_ITEM.colors.ctc; const palette = BONUS_ITEM.colors[item.spec.type] || BONUS_ITEM.colors.ctc;
const [mainTint, accentTint] = [palette.main, palette.accent]; const [mainTint, accentTint] = [palette.main, palette.accent];
// Optional per-type firework override (BONUS_ITEM.fireworkVariants):
// flour's dust puffs launch 2-3x bigger and semi-transparent (the soft
// "cloud of dust" read) - every other type falls back to the standard
// size/full-opacity show. scaleRange is applied per particle below.
const variant = BONUS_ITEM.fireworkVariants[item.spec.type] || {};
const count = BONUS_ITEM.particleCount; const count = BONUS_ITEM.particleCount;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
// Alternate the two firework-object frames around the ring (and the // Alternate the two firework-object frames around the ring (and the
@ -167,7 +175,10 @@ export default class BonusItemManager {
const particle = scene.matter.add.image(x, y, 'bonus_items', frame, { const particle = scene.matter.add.image(x, y, 'bonus_items', frame, {
// Circle hitbox, same displaySize/4 convention as the item's own // Circle hitbox, same displaySize/4 convention as the item's own
// hitbox above (and SMASH_ITEM.radius) - keeps every circle body in // hitbox above (and SMASH_ITEM.radius) - keeps every circle body in
// the game on one consistent sizing rule. // the game on one consistent sizing rule. NOTE: this always uses the
// STANDARD particleDisplaySize, even when the sprite below is shown
// bigger (flour's dust variant) - the physics footprint stays the
// same size for every item type, only the visible sprite grows.
shape: { type: 'circle', radius: BONUS_ITEM.particleDisplaySize / 4 }, shape: { type: 'circle', radius: BONUS_ITEM.particleDisplaySize / 4 },
density: 0.001, density: 0.001,
friction: 0.4, friction: 0.4,
@ -175,9 +186,22 @@ export default class BonusItemManager {
restitution: 0.25, // small bounce when they settle on the ground restitution: 0.25, // small bounce when they settle on the ground
collisionFilter: { mask: 0xffffffff & ~this.bus.busCategory & ~this.bus.kidCategory }, collisionFilter: { mask: 0xffffffff & ~this.bus.busCategory & ~this.bus.kidCategory },
}); });
particle.setDisplaySize(BONUS_ITEM.particleDisplaySize, BONUS_ITEM.particleDisplaySize); // Per-particle size: every type defaults to the standard particle size;
// a type with a firework variant (flour) varies each particle's scale
// between scaleMin and scaleMax of it, so the burst reads as soft,
// differently-sized puffs rather than one uniform ring.
const scaleRange = variant.scaleMax - variant.scaleMin;
// 1 (no variant) or scaleMin..scaleMax when a variant defines one.
const particleScale = scaleRange > 0
? variant.scaleMin + Math.random() * scaleRange
: 1;
particle.setDisplaySize(
BONUS_ITEM.particleDisplaySize * particleScale,
BONUS_ITEM.particleDisplaySize * particleScale,
);
particle.setDepth(5); // above the bus, under the HUD - this is the show, let it read on top particle.setDepth(5); // above the bus, under the HUD - this is the show, let it read on top
particle.setTint(tint); particle.setTint(tint);
if (variant.alpha !== undefined) particle.setAlpha(variant.alpha); // e.g. flour's semi-transparent dust
// Launch outward. setVelocity is the Phaser Matter component (px per // Launch outward. setVelocity is the Phaser Matter component (px per
// 60Hz physics step, matching every other velocity in this codebase - // 60Hz physics step, matching every other velocity in this codebase -

View File

@ -12,25 +12,37 @@ export default class Bus {
this.scene = scene; this.scene = scene;
const matter = scene.matter; const matter = scene.matter;
// Per-campaign bus look (see `bus` in data/themes.js): chassis and // Per-campaign bus look (see `bus` in data/themes.js): chassis and
// wheel texture, the physics/display wheel radius, and the wheel // wheel texture, the physics/display wheel radius, the wheel mount
// mount point (wheelRestOffsetY - campaign 2's "lift kit", which drops // point (wheelRestOffsetY - campaign 2's "lift kit", which drops the
// the wheels further under the chassis). Defaults to campaign 1's // wheels further under the chassis), the tire grip (the
// values, so existing callers (and any bus built outside a themed // wheelFriction/wheelFrictionStatic coefficients - campaign 2's tires
// scene) are unchanged. The chassis texture is visual only - the // grip the road harder), and the shocks (suspensionStiffness /
// physics body stays the same BUS.chassisWidth/Height rectangle // suspensionDamping - campaign 2's return faster and bounce more).
// either way. (The wishbone anchors below are DERIVED from these // Defaults to campaign 1's values, so existing callers (and any bus
// values, so a bigger radius and/or a lower mount point both stay // built outside a themed scene) are unchanged.
// correctly centered on the same axle line.) // The chassis texture is visual only - the physics body stays the same
// BUS.chassisWidth/Height rectangle either way. (The wishbone anchors
// below are DERIVED from these values, so a bigger radius and/or a
// lower mount point both stay correctly centered on the same axle
// line.)
const { const {
chassisKey = 'bus_chassis', chassisKey = 'bus_chassis',
wheelTextureKey = 'bus_wheel', wheelTextureKey = 'bus_wheel',
wheelRadius = BUS.wheelRadius, wheelRadius = BUS.wheelRadius,
wheelRestOffsetY = BUS.wheelRestOffsetY, wheelRestOffsetY = BUS.wheelRestOffsetY,
wheelFriction = BUS.wheelFriction,
wheelFrictionStatic = BUS.wheelFrictionStatic,
suspensionStiffness = BUS.suspensionStiffness,
suspensionDamping = BUS.suspensionDamping,
} = busTheme || {}; } = busTheme || {};
this.chassisKey = chassisKey; this.chassisKey = chassisKey;
this.wheelTextureKey = wheelTextureKey; this.wheelTextureKey = wheelTextureKey;
this.wheelRadius = wheelRadius; this.wheelRadius = wheelRadius;
this._wheelRestOffsetY = wheelRestOffsetY; this._wheelRestOffsetY = wheelRestOffsetY;
this._wheelFriction = wheelFriction;
this._wheelFrictionStatic = wheelFrictionStatic;
this._suspensionStiffness = suspensionStiffness;
this._suspensionDamping = suspensionDamping;
this.group = matter.world.nextGroup(true); this.group = matter.world.nextGroup(true);
// Dedicated category so the compartment fixtures below can target kids // Dedicated category so the compartment fixtures below can target kids
@ -239,12 +251,13 @@ export default class Bus {
const options = (anchorX) => ({ const options = (anchorX) => ({
pointA: { x: anchorX, y: anchorY }, pointA: { x: anchorX, y: anchorY },
pointB: { x: 0, y: 0 }, pointB: { x: 0, y: 0 },
damping: BUS.suspensionDamping, // Per-campaign shocks (see the constructor's busTheme comment).
damping: this._suspensionDamping,
}); });
return [ return [
matter.add.constraint(this.chassis, wheel, length, BUS.suspensionStiffness, options(offsetX - BUS.axleSpread)), matter.add.constraint(this.chassis, wheel, length, this._suspensionStiffness, options(offsetX - BUS.axleSpread)),
matter.add.constraint(this.chassis, wheel, length, BUS.suspensionStiffness, options(offsetX + BUS.axleSpread)), matter.add.constraint(this.chassis, wheel, length, this._suspensionStiffness, options(offsetX + BUS.axleSpread)),
]; ];
} }
@ -252,8 +265,11 @@ export default class Bus {
const wheel = this.scene.matter.add.image(x, y, textureKey, null, { const wheel = this.scene.matter.add.image(x, y, textureKey, null, {
shape: { type: 'circle', radius }, shape: { type: 'circle', radius },
density: BUS.wheelDensity, density: BUS.wheelDensity,
friction: BUS.wheelFriction, // Per-campaign tire grip (campaign 2's bus_wheel_2 rubber is
frictionStatic: BUS.wheelFrictionStatic, // stickier - see the busTheme block in the constructor and
// BUS.wheelFriction2 in config.js).
friction: this._wheelFriction,
frictionStatic: this._wheelFrictionStatic,
frictionAir: BUS.wheelFrictionAir, frictionAir: BUS.wheelFrictionAir,
restitution: BUS.wheelRestitution, restitution: BUS.wheelRestitution,
collisionFilter: { group: this.group, category: this.busCategory }, collisionFilter: { group: this.group, category: this.busCategory },

View File

@ -42,6 +42,7 @@ export default class PreloadScene extends Phaser.Scene {
this.load.audio('bonus_cash', 'assets/fx/bonus-cash.mp3'); this.load.audio('bonus_cash', 'assets/fx/bonus-cash.mp3');
this.load.audio('bonus_ctc', 'assets/fx/bonus-ctc.mp3'); this.load.audio('bonus_ctc', 'assets/fx/bonus-ctc.mp3');
this.load.audio('bonus_guitar', 'assets/fx/bonus-guitar.mp3'); this.load.audio('bonus_guitar', 'assets/fx/bonus-guitar.mp3');
this.load.audio('bonus_flour', 'assets/fx/bonus-flour.mp3');
loadKidFallSounds(this.load); loadKidFallSounds(this.load);
// Not every level has a voice-over - this just attempts one per level // Not every level has a voice-over - this just attempts one per level

View File

@ -26,8 +26,8 @@ export const ASSET_MANIFEST = [
// frameWidth/frameHeight are raw SOURCE pixels (one 64x64 cell each), // frameWidth/frameHeight are raw SOURCE pixels (one 64x64 cell each),
// same convention as the kid sheets above. // same convention as the kid sheets above.
{ key: 'smash_items', path: 'assets/sprites/smash-items.png', width: 64 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0x4c8a5c, frameWidth: 64, frameHeight: 64 }, { key: 'smash_items', path: 'assets/sprites/smash-items.png', width: 64 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0x4c8a5c, frameWidth: 64, frameHeight: 64 },
// 9 frames of 64x64 bonus items (CTC box + 2 firework frames, guitar + 2, // 12 frames of 64x64 bonus items (CTC box + 2 firework frames, guitar + 2,
// cash + 2 - see BONUS_ITEM in config.js and BonusItemManager.js). // cash + 2, flour + 2 - see BONUS_ITEM in config.js and BonusItemManager.js).
{ key: 'bonus_items', path: 'assets/sprites/bonus-items.png', width: 64 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0xe0a040, frameWidth: 64, frameHeight: 64 }, { key: 'bonus_items', path: 'assets/sprites/bonus-items.png', width: 64 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0xe0a040, frameWidth: 64, frameHeight: 64 },
// Campaign 1 ("Evergreen Lake") parallax set - see DEFAULT_THEME in // Campaign 1 ("Evergreen Lake") parallax set - see DEFAULT_THEME in
// data/themes.js. The _2 set is campaign 2's; both are 3136x672 natively // data/themes.js. The _2 set is campaign 2's; both are 3136x672 natively