144 lines
7.0 KiB
JavaScript
144 lines
7.0 KiB
JavaScript
// 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; this.slices.length = 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; },
|
|
// Pie slices record their geometry: the production overlays are only meaningful if the
|
|
// ARC they sweep matches the progress fraction, which a bare call counter cannot show.
|
|
slices: [],
|
|
slice(x, y, radius, startAngle, endAngle) {
|
|
this.ops++;
|
|
this.slices.push({ x, y, radius, startAngle, endAngle });
|
|
return this;
|
|
},
|
|
arc() { 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;
|
|
}
|