// Minimal Phaser stand-in for tools/verifyTotalAnnihilation.js. // // TAWorldView, TAFx and TAArt deliberately import no Phaser, so the real render path can be // exercised in Node against this. It models the behaviour that matters and NOT the behaviour // that would hide bugs — in particular a Container here draws in insertion order and only // reorders when sort() is called, exactly like the real thing. export function makeStubScene(w = 1920, h = 1080) { const created = []; const track = (o) => { created.push(o); return o; }; const base = (type, extra = {}) => { const o = { type, x: 0, y: 0, depth: 0, visible: true, alpha: 1, rotation: 0, scaleX: 1, scaleY: 1, displayWidth: 0, displayHeight: 0, tint: null, frame: null, texture: null, parent: null, setPosition(x, y) { this.x = x; this.y = y; return this; }, setOrigin() { return this; }, setDepth(d) { this.depth = d; return this; }, setVisible(v) { this.visible = v; return this; }, setAlpha(a) { this.alpha = a; return this; }, setRotation(r) { this.rotation = r; return this; }, setScale(s) { this.scaleX = this.scaleY = s; return this; }, setDisplaySize(dw, dh) { this.displayWidth = dw; this.displayHeight = dh; return this; }, setTint(t) { this.tint = t; return this; }, setTexture(k) { this.texture = k; return this; }, setFrame(f) { this.frame = f; return this; }, setInteractive() { return this; }, setSize() { return this; }, setStrokeStyle() { return this; }, setFillStyle() { return this; }, on() { return this; }, destroy() { this.destroyed = true; }, ...extra, }; return track(o); }; const gfx = () => base('graphics', { ops: 0, clear() { this.ops = 0; return this; }, lineStyle() { return this; }, fillStyle() { return this; }, beginPath() { return this; }, closePath() { return this; }, moveTo() { return this; }, lineTo() { return this; }, fillPath() { return this; }, strokePath() { return this; }, fillRect() { this.ops++; return this; }, strokeRect() { this.ops++; return this; }, fillCircle() { this.ops++; return this; }, strokeCircle() { this.ops++; return this; }, fillEllipse() { this.ops++; return this; }, strokeEllipse() { this.ops++; return this; }, lineBetween() { this.ops++; return this; }, fillTriangle() { this.ops++; return this; }, fillRoundedRect() { this.ops++; return this; }, strokeRoundedRect() { this.ops++; return this; }, }); const container = () => base('container', { list: [], add(o) { for (const c of [].concat(o)) { if (c) { c.parent = this; this.list.push(c); } } return this; }, remove() { return this; }, // Phaser Containers draw in INSERTION order and only reorder when sort() is called, // so the stub must not quietly sort for us or it hides exactly the bug we're hunting. sort(prop) { this.list = this.list.slice().sort((a, b) => (a[prop] ?? 0) - (b[prop] ?? 0)); return this; }, }); const ctxStub = new Proxy({}, { get: (_t, p) => { if (p === 'createImageData') return (w2, h2) => ({ data: new Uint8ClampedArray(w2 * h2 * 4) }); if (p === 'getImageData') return (x, y, w2, h2) => ({ data: new Uint8ClampedArray(w2 * h2 * 4) }); return () => {}; }, set: () => true, }); const textures = new Map(); // Camera maths copied from Phaser's Camera.preRender so the stub agrees with the engine: // midX = scrollX + width/2; worldView.x = midX - (width/zoom)/2 // Anything looser would let a broken zoom anchor pass. const cam = (x = 0, y = 0, cw = w, ch = h) => ({ x, y, width: cw, height: ch, scrollX: 0, scrollY: 0, zoom: 1, ignored: [], worldView: { x: 0, y: 0, width: cw, height: ch, right: cw, bottom: ch }, setBounds() { return this; }, setBackgroundColor() { return this; }, setZoom(z) { this.zoom = z; this._sync(); return this; }, setScroll(sx, sy) { this.scrollX = sx; this.scrollY = sy; this._sync(); return this; }, centerOn(px, py) { this.scrollX = px - cw / 2; this.scrollY = py - ch / 2; this._sync(); return this; }, getWorldPoint(px, py) { return { x: this.worldView.x + px / this.zoom, y: this.worldView.y + py / this.zoom }; }, ignore(o) { this.ignored.push(o); return this; }, shake() { return this; }, _sync() { this.worldView.width = cw / this.zoom; this.worldView.height = ch / this.zoom; this.worldView.x = this.scrollX + cw / 2 - this.worldView.width / 2; this.worldView.y = this.scrollY + ch / 2 - this.worldView.height / 2; this.worldView.right = this.worldView.x + this.worldView.width; this.worldView.bottom = this.worldView.y + this.worldView.height; }, }); const main = cam(); const scene = { created, scale: { width: w, height: h }, time: { now: 1000, delayedCall: () => ({ remove() {} }) }, tweens: { add: () => ({}) }, input: { activePointer: { x: 0, y: 0 }, on() {}, off() {}, keyboard: { addKeys: () => ({}), on() {} }, mouse: { disableContextMenu() {} } }, events: { once() {}, on() {} }, cameras: { main, cameras: [main], add: () => cam(), remove() {} }, add: { container: () => container(), graphics: () => gfx(), image: (x, y, key, frame) => base('image', { x, y, texture: key, frame }), rectangle: (x, y, rw, rh) => base('rectangle', { x, y, width: rw, height: rh }), text: (x, y, t) => base('text', { x, y, text: t, width: 10, height: 10, setText(v) { this.text = v; return this; }, setColor() { return this; } }), renderTexture: (x, y, rw, rh) => base('renderTexture', { x, y, width: rw, height: rh, draws: 0, beginDraw() { return this; }, batchDraw() { this.draws++; return this; }, endDraw() { return this; }, draw() { this.draws++; return this; }, }), circle: (x, y) => base('circle', { x, y }), }, make: { image: (cfg) => base('image', { texture: cfg.key }), graphics: () => gfx() }, textures: { exists: (k) => textures.has(k), remove: (k) => textures.delete(k), get: (k) => textures.get(k), createCanvas(key, cw2, ch2) { if (!Number.isFinite(cw2) || !Number.isFinite(ch2) || cw2 <= 0 || ch2 <= 0) { throw new Error(`createCanvas(${key}) got ${cw2}x${ch2}`); } const frames = []; const t = { key, width: cw2, height: ch2, frames, context: ctxStub, getContext: () => ctxStub, refresh() {}, setFilter() { return this; }, add(f) { frames.push(f); return this; }, }; textures.set(key, t); return t; }, }, }; return scene; }