fertig-classic-games/tools/lib/taStubScene.js

135 lines
6.5 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; 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;
}