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_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) |
| `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_mid` | `assets/backgrounds/bg_mid.png` | 256x540 | tiled, mid 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
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
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.
@ -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
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
frames, guitar, 2 firework frames, cash, 2 firework frames - 64x64 per
frame), loaded as the `bonus_items` texture. Any level can carry an optional
`assets/sprites/bonus-items.png` is a 1x12 spritesheet (CTC box, 2 firework
frames, guitar, 2 firework frames, cash, 2 firework frames, flour, 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
@ -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
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.
= 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
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, pop scale/duration).
speed range, spin, lifetime, tint palette, firework variants, pop
scale/duration).
## 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
mount point sits ~8 design units lower under the chassis (see
`wheelRestOffsetY2` in `src/config.js` and the `bus` block in
`src/data/themes.js`), so it reads as a lifted bus. Both wheels still
share one image per campaign.
`src/data/themes.js`), so it reads as a lifted bus. And its tires grip
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
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

View File

@ -91,6 +91,12 @@ export const BUS = {
wheelDensity: 0.004,
wheelFriction: 0.9,
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
// 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)
@ -103,6 +109,13 @@ export const BUS = {
// oscillate (bounce) instead of settling immediately - "more shocks."
suspensionStiffness: 0.35,
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
// 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.
@ -260,7 +273,7 @@ export const SMASH_ITEM = {
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
// src/entities/BonusItemManager.js for the type/frame/color table). Placed
// 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
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
// (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
@ -313,6 +338,7 @@ export const BONUS_ITEM = {
ctc: { main: 0xe0a040, accent: 0x703818 }, // warm toast-brown / cinnamon
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)
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`
// is the physics/display radius in world pixels; `wheelRestOffsetY` 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
// centered on its axle (the wishbone anchors derive from these
// offsets, so they stay put) - only the size changes.
// the "lift kit" knob (campaign 2); `wheelFriction`/`wheelFrictionStatic`
// are the tire grip coefficients (campaign 2's tires grip harder);
// `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: {
chassisKey: 'bus_chassis',
wheelTextureKey: 'bus_wheel',
wheelRadius: BUS.wheelRadius,
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
// 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
// the bigger wheels hang lower under the body.
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.

View File

@ -29,7 +29,7 @@ const state = {
imported: null,
// 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)
// 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
// the exported level uses); refIndex is the track section it was added
// under (rebased on reordering).
@ -204,19 +204,20 @@ const ITEM_SPRITE_FRAMES = { tv: 0, chair: 2, cone: 4 };
const ITEM_SPRITE_CELL = 64;
// Bonus items ("Placeables" toggle -> "Bonus items"): float-only collectible
// prizes - CTC box / guitar / cash. See BonusItemManager.js in the game for
// the type/frame/color table; frames here are the BONUS frame of each pair
// (0 CTC, 3 guitar, 6 cash).
// prizes - CTC box / guitar / cash / flour. See BonusItemManager.js in the
// game for the type/frame/color table; frames here are the BONUS frame of
// each pair (0 CTC, 3 guitar, 6 cash, 9 flour).
const BONUS_TYPES = [
{ id: 'ctc', label: 'CTC Box' },
{ id: 'guitar', label: 'Guitar' },
{ id: 'cash', label: 'Cash' },
{ id: 'flour', label: 'Flour' },
];
const BONUS_COLORS = { ctc: '#e0a040', guitar: '#4b9a45', cash: '#b3ad9c' };
const BONUS_ICONS = { ctc: 'CT', guitar: 'GT', cash: '$' };
const BONUS_COLORS = { ctc: '#e0a040', guitar: '#4b9a45', cash: '#b3ad9c', flour: '#cbb58e' };
const BONUS_ICONS = { ctc: 'CT', guitar: 'GT', cash: '$', flour: 'FL' };
const BONUS_SPRITE = new Image();
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
// to draw the "falls to here" drop line for gravity-mode items in the
@ -249,7 +250,7 @@ function buildItemsForm() {
itemsFormEl.appendChild(fieldset);
// --- 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.
const kindRadios = [];
for (const kind of ['smashable', 'bonus']) {
@ -265,7 +266,7 @@ function buildItemsForm() {
});
const label = document.createElement('label');
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);
}
const kindWrap = document.createElement('div');

View File

@ -1,15 +1,17 @@
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,
// 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
// the sprites the explosion burst is made of (see _explode below).
const BONUS_TYPES = {
ctc: { frame: 0, fireworks: [1, 2], label: 'CTC Box', soundKey: 'bonus_ctc' },
guitar: { frame: 3, fireworks: [4, 5], label: 'Guitar', soundKey: 'bonus_guitar' },
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 {
@ -121,7 +123,7 @@ export default class BonusItemManager {
const { scene } = this;
// 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 /
// kidFallSound.js, so a missing clip degrades to silent instead of
// 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 [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;
for (let i = 0; i < count; i++) {
// 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, {
// Circle hitbox, same displaySize/4 convention as the item's own
// 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 },
density: 0.001,
friction: 0.4,
@ -175,9 +186,22 @@ export default class BonusItemManager {
restitution: 0.25, // small bounce when they settle on the ground
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.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
// 60Hz physics step, matching every other velocity in this codebase -

View File

@ -12,25 +12,37 @@ export default class Bus {
this.scene = scene;
const matter = scene.matter;
// Per-campaign bus look (see `bus` in data/themes.js): chassis and
// wheel texture, the physics/display wheel radius, and the wheel
// mount point (wheelRestOffsetY - campaign 2's "lift kit", which drops
// the wheels further under the chassis). Defaults to campaign 1's
// values, so existing callers (and any bus built outside a themed
// scene) are unchanged. 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.)
// wheel texture, the physics/display wheel radius, the wheel mount
// point (wheelRestOffsetY - campaign 2's "lift kit", which drops the
// wheels further under the chassis), the tire grip (the
// wheelFriction/wheelFrictionStatic coefficients - campaign 2's tires
// grip the road harder), and the shocks (suspensionStiffness /
// suspensionDamping - campaign 2's return faster and bounce more).
// Defaults to campaign 1's values, so existing callers (and any bus
// built outside a themed scene) are unchanged.
// 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 {
chassisKey = 'bus_chassis',
wheelTextureKey = 'bus_wheel',
wheelRadius = BUS.wheelRadius,
wheelRestOffsetY = BUS.wheelRestOffsetY,
wheelFriction = BUS.wheelFriction,
wheelFrictionStatic = BUS.wheelFrictionStatic,
suspensionStiffness = BUS.suspensionStiffness,
suspensionDamping = BUS.suspensionDamping,
} = busTheme || {};
this.chassisKey = chassisKey;
this.wheelTextureKey = wheelTextureKey;
this.wheelRadius = wheelRadius;
this._wheelRestOffsetY = wheelRestOffsetY;
this._wheelFriction = wheelFriction;
this._wheelFrictionStatic = wheelFrictionStatic;
this._suspensionStiffness = suspensionStiffness;
this._suspensionDamping = suspensionDamping;
this.group = matter.world.nextGroup(true);
// Dedicated category so the compartment fixtures below can target kids
@ -239,12 +251,13 @@ export default class Bus {
const options = (anchorX) => ({
pointA: { x: anchorX, y: anchorY },
pointB: { x: 0, y: 0 },
damping: BUS.suspensionDamping,
// Per-campaign shocks (see the constructor's busTheme comment).
damping: this._suspensionDamping,
});
return [
matter.add.constraint(this.chassis, wheel, length, BUS.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)),
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, {
shape: { type: 'circle', radius },
density: BUS.wheelDensity,
friction: BUS.wheelFriction,
frictionStatic: BUS.wheelFrictionStatic,
// Per-campaign tire grip (campaign 2's bus_wheel_2 rubber is
// stickier - see the busTheme block in the constructor and
// BUS.wheelFriction2 in config.js).
friction: this._wheelFriction,
frictionStatic: this._wheelFrictionStatic,
frictionAir: BUS.wheelFrictionAir,
restitution: BUS.wheelRestitution,
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_ctc', 'assets/fx/bonus-ctc.mp3');
this.load.audio('bonus_guitar', 'assets/fx/bonus-guitar.mp3');
this.load.audio('bonus_flour', 'assets/fx/bonus-flour.mp3');
loadKidFallSounds(this.load);
// 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),
// 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 },
// 9 frames of 64x64 bonus items (CTC box + 2 firework frames, guitar + 2,
// cash + 2 - see BONUS_ITEM in config.js and BonusItemManager.js).
// 12 frames of 64x64 bonus items (CTC box + 2 firework frames, guitar + 2,
// 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 },
// Campaign 1 ("Evergreen Lake") parallax set - see DEFAULT_THEME in
// data/themes.js. The _2 set is campaign 2's; both are 3136x672 natively