Add campaign 2 bus wheel variant (bigger wheels via theme)

- Introduce `bus_wheel_2` asset and register it in the asset manifest
- Add `BUS.wheelRadius2` config for the larger wheel radius
- Extend themes with a `bus` block to select wheel texture and radius per campaign; campaign02 uses the new bigger wheels while keeping the same axle center
- Make `Bus` accept a bus theme parameter and use provided wheel texture/radius when creating wheels
- Update `PlayScene` to pass the active theme's `bus` settings into the Bus constructor
- Document the new per-campaign bus art/sizing behavior in sprites.md
This commit is contained in:
Brian Fertig 2026-08-23 18:33:48 -06:00
parent 3033c1d62d
commit eaeebf1980
7 changed files with 75 additions and 13 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -44,6 +44,12 @@ with it (same ratios, just re-derive from that one constant).
no separate left/right variant needed. It's a real physics body, so it
visually spins as the bus drives; a plain hubcap/circle reads better at
speed than fine detail.
- Campaign 2 uses its own wheel image, `bus_wheel_2.png` (bigger - it's
displayed at a ~1.2x radius around the same axle center, selected via the
`bus` block in `src/data/themes.js`), and its own chassis image,
`bus_chassis_2.png` (same 340x128 design box, physics body unchanged -
only the texture is swapped per campaign).
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

@ -29,6 +29,10 @@ export const BUS = {
// the axles are NOT symmetric around the chassis center). Offsets are from
// the chassis center; re-measure these if bus_chassis.png is replaced.
wheelRadius: 17 * WORLD_SCALE,
// Campaign 2's bus uses the bigger bus_wheel_2.png (125px vs bus_wheel.png's
// 104px, ~1.2x) around the SAME wheel center - 17 * 1.2 ≈ 20. See
// `bus` in data/themes.js, which selects it for campaign02 only.
wheelRadius2: 20 * WORLD_SCALE,
rearWheelOffsetX: -35 * WORLD_SCALE,
frontWheelOffsetX: 64 * WORLD_SCALE,
// Where the wheel's CENTER rests at equilibrium. Raised from the

View File

@ -1,5 +1,8 @@
import { BUS } from '../config.js';
// Per-campaign themes for the IN-LEVEL (play) presentation - the parallax
// background stack, base camera color, and terrain look shown while actually
// background stack, base camera color, terrain look, and the bus's own
// art/sizing (e.g. campaign 2's bigger wheels) shown while actually
// driving a level. This is distinct from a campaign's *map* art (bgKey) and
// menu music (musicKey), which live on the campaign object in
// data/levels/index.js.
@ -75,6 +78,18 @@ export const DEFAULT_THEME = {
// visible through gaps. A hex string, the same format the scenes pass to
// Phaser's setBackgroundColor().
bgColor: '#8fc7e8',
// The bus's own look (see Bus.js's constructor and BUS in config.js).
// `chassisKey` is the chassis texture (purely visual - the physics body
// stays the same BUS.chassisWidth/Height rectangle either way);
// `wheelTextureKey` is the texture both wheels render with; `wheelRadius`
// is the physics/display radius in world pixels. Changing the radius
// keeps the wheel centered on the same spot (the wishbone anchors and
// wheelRestOffsetY are radius-agnostic) - only the size changes.
bus: {
chassisKey: 'bus_chassis',
wheelTextureKey: 'bus_wheel',
wheelRadius: BUS.wheelRadius,
},
// The three parallax layers, back to front. `far` is the full-viewport
// sky; `mid`/`near` are bottom-anchored ground tiles (see PlayScene's
// _buildParallax). Keys are texture keys, not file paths.
@ -92,8 +107,9 @@ export const THEMES = {
campaign01: DEFAULT_THEME,
// Campaign 2 ("Underprivileged Community") - the urban _2 background set,
// a dusk base color matched to bg_far_2's sky, and the asphalt-terrain
// look above.
// a dusk base color matched to bg_far_2's sky, the asphalt-terrain
// look above, and the bigger bus_wheel_2.png wheels (same center, ~1.2x
// radius - see BUS.wheelRadius2 in config.js).
campaign02: {
bgColor: '#92656d',
parallax: {
@ -102,12 +118,19 @@ export const THEMES = {
near: 'bg_near_2',
},
terrain: URBAN_TERRAIN,
bus: {
chassisKey: 'bus_chassis_2',
wheelTextureKey: 'bus_wheel_2',
wheelRadius: BUS.wheelRadius2,
},
},
// Add campaign03: { ... } here when it's ready.
};
// Resolves the theme for a campaign id, always returning a complete theme
// (falls back to the default for unknown/absent campaigns).
// (falls back to the default for unknown/absent campaigns). Nested objects
// like `bus` are whole-value overrides, so a campaign entry that sets them
// must set every field it wants (they're small and stable - that's fine).
export function getTheme(campaignId) {
const theme = THEMES[campaignId];
return theme ? { ...DEFAULT_THEME, ...theme } : DEFAULT_THEME;

View File

@ -8,9 +8,23 @@ import { BUS } from '../config.js';
// with no vertical give, which is too harsh for a bus that needs real
// suspension travel to absorb bumps.
export default class Bus {
constructor(scene, x, y, angle = 0) {
constructor(scene, x, y, angle = 0, busTheme = null) {
this.scene = scene;
const matter = scene.matter;
// Per-campaign bus look (see `bus` in data/themes.js): which chassis
// texture and wheel texture to render and the physics/display wheel
// radius. 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. NOTE: only the wheel
// size varies - the wheel's CENTER is anchored to the chassis via the
// wishbone below (anchorY/wheelRestOffsetY are radius-agnostic), so a
// bigger radius grows the wheel around the same center, exactly as
// intended.
const { chassisKey = 'bus_chassis', wheelTextureKey = 'bus_wheel', wheelRadius = BUS.wheelRadius } = busTheme || {};
this.chassisKey = chassisKey;
this.wheelTextureKey = wheelTextureKey;
this.wheelRadius = wheelRadius;
this.group = matter.world.nextGroup(true);
// Dedicated category so the compartment fixtures below can target kids
@ -44,7 +58,7 @@ export default class Bus {
// of hanging level, snapping into place on the first physics step.
const constraintLength = Math.hypot(BUS.axleSpread, BUS.suspensionTravel);
this.chassis = matter.add.image(x, y, 'bus_chassis', null, {
this.chassis = matter.add.image(x, y, this.chassisKey, null, {
shape: { type: 'rectangle', width: BUS.chassisWidth, height: BUS.chassisHeight },
chamfer: { radius: BUS.chassisHeight * 0.35 },
density: BUS.chassisDensity,
@ -61,8 +75,8 @@ export default class Bus {
this.chassis.setDepth(2);
const wheelY = y + BUS.wheelRestOffsetY;
this.wheelRear = this._createWheel(x + BUS.rearWheelOffsetX, wheelY);
this.wheelFront = this._createWheel(x + BUS.frontWheelOffsetX, wheelY);
this.wheelRear = this._createWheel(x + BUS.rearWheelOffsetX, wheelY, this.wheelTextureKey, this.wheelRadius);
this.wheelFront = this._createWheel(x + BUS.frontWheelOffsetX, wheelY, this.wheelTextureKey, this.wheelRadius);
this.wheelRearLinks = this._attachWishbone(this.wheelRear, BUS.rearWheelOffsetX, anchorY, constraintLength);
this.wheelFrontLinks = this._attachWishbone(this.wheelFront, BUS.frontWheelOffsetX, anchorY, constraintLength);
@ -225,9 +239,9 @@ export default class Bus {
];
}
_createWheel(x, y) {
const wheel = this.scene.matter.add.image(x, y, 'bus_wheel', null, {
shape: { type: 'circle', radius: BUS.wheelRadius },
_createWheel(x, y, textureKey, radius) {
const wheel = this.scene.matter.add.image(x, y, textureKey, null, {
shape: { type: 'circle', radius },
density: BUS.wheelDensity,
friction: BUS.wheelFriction,
frictionStatic: BUS.wheelFrictionStatic,
@ -235,7 +249,7 @@ export default class Bus {
restitution: BUS.wheelRestitution,
collisionFilter: { group: this.group, category: this.busCategory },
});
wheel.setDisplaySize(BUS.wheelRadius * 2, BUS.wheelRadius * 2);
wheel.setDisplaySize(radius * 2, radius * 2);
wheel.setDepth(2); // matches the chassis - see its setDepth comment
return wheel;
}

View File

@ -50,7 +50,15 @@ export default class PlayScene extends Phaser.Scene {
this.terrain = new Terrain(this, this.level, this.theme.terrain);
this.finishLine = new FinishLine(this, this.level);
this.bus = new Bus(this, this.level.startPosition.x, this.level.startPosition.y, this.level.startAngle || 0);
this.bus = new Bus(
this,
this.level.startPosition.x,
this.level.startPosition.y,
this.level.startAngle || 0,
// Campaign 2 gets its own wheel art + size from here (see the `bus`
// block in data/themes.js); campaign 1 uses the defaults in Bus.js.
this.theme.bus
);
this.kidManager = new KidManager(this, this.bus, this.level.kidsAboard);

View File

@ -5,7 +5,14 @@ import { WORLD_SCALE } from '../config.js';
// nothing else in the codebase needs to change (see placeholderTextures.js).
export const ASSET_MANIFEST = [
{ key: 'bus_chassis', path: 'assets/sprites/bus_chassis.png', width: 170 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0x2255aa },
// Campaign 2's chassis art - same 340x128 design box, different look.
// Picked per campaign via the `bus` theme entry in data/themes.js; the
// physics body is unchanged (see Bus.js's constructor).
{ key: 'bus_chassis_2', path: 'assets/sprites/bus_chassis_2.png', width: 170 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0x2255aa },
{ key: 'bus_wheel', path: 'assets/sprites/bus_wheel.png', width: 34 * WORLD_SCALE, height: 34 * WORLD_SCALE, kind: 'circle', color: 0x222222 },
// Campaign 2's bigger wheels (125px source vs bus_wheel.png's 104px) -
// picked per campaign via the `bus` theme entry in data/themes.js.
{ key: 'bus_wheel_2', path: 'assets/sprites/bus_wheel_2.png', width: 34 * WORLD_SCALE, height: 34 * WORLD_SCALE, kind: 'circle', color: 0x222222 },
// Real art is a 5-frame sheet (one distinct kid per frame, 56x56px each,
// matching BUS.maxSeats) - frameWidth/frameHeight (raw source pixels, NOT
// WORLD_SCALE-relative) make PreloadScene load it as a spritesheet