diff --git a/assets/sprites/bus_chassis_2.png b/assets/sprites/bus_chassis_2.png new file mode 100644 index 0000000..8aec06f Binary files /dev/null and b/assets/sprites/bus_chassis_2.png differ diff --git a/sprites.md b/sprites.md index 15c5e1e..dd7fe56 100644 --- a/sprites.md +++ b/sprites.md @@ -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 diff --git a/src/config.js b/src/config.js index 4f78967..dcfa485 100644 --- a/src/config.js +++ b/src/config.js @@ -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 diff --git a/src/data/themes.js b/src/data/themes.js index ba3733c..4a5f324 100644 --- a/src/data/themes.js +++ b/src/data/themes.js @@ -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; diff --git a/src/entities/Bus.js b/src/entities/Bus.js index b9ea168..076ddaa 100644 --- a/src/entities/Bus.js +++ b/src/entities/Bus.js @@ -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; } diff --git a/src/scenes/PlayScene.js b/src/scenes/PlayScene.js index 0fc30f2..165c172 100644 --- a/src/scenes/PlayScene.js +++ b/src/scenes/PlayScene.js @@ -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); diff --git a/src/util/assetManifest.js b/src/util/assetManifest.js index d4cf77b..6fcc53c 100644 --- a/src/util/assetManifest.js +++ b/src/util/assetManifest.js @@ -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