diff --git a/src/config.js b/src/config.js index 7305e5f..9477555 100644 --- a/src/config.js +++ b/src/config.js @@ -9,6 +9,12 @@ export const GAME_HEIGHT = 540 * WORLD_SCALE; // Set true to show live g-force / debug readouts during PlayScene. export const DEBUG = false; +// Set true to render the invisible kid compartment (floor + left/right +// walls) as simple colored rectangles that track the chassis, plus a live +// per-kid local-position/state readout in the PlayScene HUD. Troubleshooting +// aid for ejection behavior - off during normal play. +export const COMPARTMENT_DEBUG = true; + export const STORAGE_KEY = 'monsterplex.progress.v1'; // Matter runs Phaser's default fixed 60Hz step. diff --git a/src/scenes/PlayScene.js b/src/scenes/PlayScene.js index c9fd76b..be86fa1 100644 --- a/src/scenes/PlayScene.js +++ b/src/scenes/PlayScene.js @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import { GAME_WIDTH, GAME_HEIGHT, DEBUG, WORLD_SCALE } from '../config.js'; +import { GAME_WIDTH, GAME_HEIGHT, DEBUG, WORLD_SCALE, COMPARTMENT_DEBUG } from '../config.js'; import { getLevelById } from '../data/levels/index.js'; import Bus from '../entities/Bus.js'; import Terrain from '../entities/Terrain.js'; @@ -13,6 +13,7 @@ import { stopMenuMusic } from '../util/music.js'; import { playLevelVoiceLine } from '../util/voiceLine.js'; import { formatTime } from '../util/formatTime.js'; import { createPill } from '../util/pill.js'; +import buildCompartmentDebug from '../util/compartmentDebug.js'; export default class PlayScene extends Phaser.Scene { constructor() { @@ -41,6 +42,11 @@ export default class PlayScene extends Phaser.Scene { this.kidManager = new KidManager(this, this.bus, this.level.kidsAboard); + // Troubleshooting overlay: renders the invisible compartment fixtures + // (floor + walls) as simple rectangles tracking the bus, plus per-kid + // state readout. Off by default - toggle COMPARTMENT_DEBUG in config.js. + this.compartmentDebug = COMPARTMENT_DEBUG ? buildCompartmentDebug(this, this.bus, this.kidManager) : null; + this.inputController = new InputController(this); this.engineSound = new EngineSound(this, this.bus); @@ -320,6 +326,7 @@ export default class PlayScene extends Phaser.Scene { // Matter.World#shutdown() already calls removeAllListeners() on itself // internally (verified against the vendored source), so this was always // redundant, not just now-unsafe. + if (this.compartmentDebug) this.compartmentDebug.destroy(); if (this.gForceMonitor) this.gForceMonitor.destroy(); if (this.kidManager) this.kidManager.destroy(); if (this.engineSound) this.engineSound.destroy(); diff --git a/src/util/compartmentDebug.js b/src/util/compartmentDebug.js new file mode 100644 index 0000000..7d6501a --- /dev/null +++ b/src/util/compartmentDebug.js @@ -0,0 +1,95 @@ +import Phaser from 'phaser'; +import { BUS, KID, WORLD_SCALE } from '../config.js'; + +// Debug-only visualisation of the invisible kid compartment (see +// Bus._buildCompartment): one simple colored rectangle per fixture +// (floor + left wall + right wall) tracking the chassis every frame, plus +// the open-top line and one marker per kid showing its state. Used for +// troubleshooting ejection behavior (does the kid visually leave through +// the open top before KidManager latches it? where does it sit relative to +// the floor/walls while aboard?). +// +// Rectangles are plain Phaser.GameObjects.Rectangle (rendering only - they +// are NOT physics bodies, deliberately, so they can't interfere with the +// very collision behavior being observed). +export default function buildCompartmentDebug(scene, bus, kidManager) { + const depth = 5; + + const mkRect = (w, h, color, alpha) => + scene.add.rectangle(0, 0, w, h, color, alpha).setOrigin(0.5).setDepth(depth); + + const floor = mkRect(BUS.compartmentHalfWidth * 2, BUS.compartmentFloorThickness, 0x00e676, 0.75); + const leftWall = mkRect(BUS.compartmentWallThickness, (BUS.compartmentFloorY - BUS.compartmentTopY) + BUS.compartmentFloorThickness, 0x40c4ff, 0.75); + const rightWall = mkRect(BUS.compartmentWallThickness, (BUS.compartmentFloorY - BUS.compartmentTopY) + BUS.compartmentFloorThickness, 0xff5252, 0.75); + + // The open-top boundary (BUS.compartmentTopY) is where the aboard/ejected + // transition happens - drawn as a thin dashed-style line so "crossing + // this line" is visible in gameplay. + const topLine = mkRect(BUS.compartmentHalfWidth * 2 + BUS.compartmentWallThickness * 2, 2 * WORLD_SCALE, 0xffeb3b, 0.9); + + // One marker per kid: small rectangle at the kid's position, color-coded + // by state (green = aboard, red = ejected). + const kidMarkers = kidManager.kids.map((kid) => mkRect(KID.radius * 2 + 4 * WORLD_SCALE, KID.radius * 2 + 4 * WORLD_SCALE, 0xffffff, 0.0)); + + // HUD readout, same style as the other pills. + const label = scene.add.text(24 * WORLD_SCALE, 110 * WORLD_SCALE, '', { + fontFamily: 'monospace', + fontSize: `${13 * WORLD_SCALE}px`, + color: '#1a1f29', + }).setScrollFactor(0).setDepth(10); + + const shapes = [floor, leftWall, rightWall, topLine]; + + function update() { + const chassis = bus.chassis; + const cos = Math.cos(chassis.rotation); + const sin = Math.sin(chassis.rotation); + const toWorld = (localX, localY) => ({ + x: chassis.x + localX * cos - localY * sin, + y: chassis.y + localX * sin + localY * cos, + }); + + const f = toWorld(0, BUS.compartmentFloorY); + floor.setPosition(f.x, f.y); + floor.setAngle(chassis.rotation); + + const wallHeight = (BUS.compartmentFloorY - BUS.compartmentTopY) + BUS.compartmentFloorThickness; + const wallCenterY = (BUS.compartmentFloorY + BUS.compartmentTopY) / 2; + const l = toWorld(-BUS.compartmentHalfWidth, wallCenterY); + leftWall.setPosition(l.x, l.y); + leftWall.setAngle(chassis.rotation); + const r = toWorld(BUS.compartmentHalfWidth, wallCenterY); + rightWall.setPosition(r.x, r.y); + rightWall.setAngle(chassis.rotation); + + const t = toWorld(0, BUS.compartmentTopY); + topLine.setPosition(t.x, t.y); + topLine.setAngle(chassis.rotation); + + let lines = [`floor: x${-BUS.compartmentHalfWidth}..${BUS.compartmentHalfWidth} y=${BUS.compartmentFloorY}`]; + lines.push(`walls: x=${-BUS.compartmentHalfWidth} / x=${BUS.compartmentHalfWidth} top(open): y=${BUS.compartmentTopY}`); + for (let i = 0; i < kidManager.kids.length; i++) { + const kid = kidManager.kids[i]; + const dx = kid.image.x - chassis.x; + const dy = kid.image.y - chassis.y; + const localX = dx * cos + dy * sin; + const localY = -dx * sin + dy * cos; + const marker = kidMarkers[i]; + marker.setPosition(kid.image.x, kid.image.y); + marker.setFillStyle(kid.state === 'aboard' ? 0x00e676 : 0xff5252, 0.9); + marker.setSize(KID.radius * 2 + 4 * WORLD_SCALE, KID.radius * 2 + 4 * WORLD_SCALE); + lines.push(`kid${i}: ${kid.state} local=(${localX.toFixed(0)}, ${localY.toFixed(0)})${kid.state === 'ejected' && kid.ejectedAt !== null ? ` outFor=${((scene.time.now - kid.ejectedAt) / 1000).toFixed(1)}s` : ''}`); + } + label.setText(lines.join('\n'), { align: 'left' }); + } + + scene.events.on('update', update); + scene.events.once('shutdown', () => { + scene.events.off('update', update); + for (const s of shapes) s.destroy(); + for (const m of kidMarkers) m.destroy(); + label.destroy(); + }); + + return { shapes, kidMarkers, label, destroy: () => { shapes.concat(kidMarkers).concat([label]).forEach((s) => s.destroy()); } }; +}