Files
ark-stronghold-protocol/test/render/fakepixi.js
T

149 lines
8.8 KiB
JavaScript

// test/render/fakepixi.js — a minimal headless stand-in for PIXI 7 + pixi-spine + a 2D canvas document, enough to
// construct and update render/units.js views and the procedural textures of render/textures.js in Node.
// Records every canvas made (`canvases`) and every BaseTexture created / destroyed, so tests can assert what a
// view allocated. Install before the first render call; `restore()` puts the previous globals back.
const point = (x = 0, y = 0) => ({ x, y, set(a = 0, b = a) { this.x = a; this.y = b; } });
// every CanvasRenderingContext2D method is a no-op; gradients / measurements return plausible objects
function context2d(canvas) {
const store = {};
return new Proxy(store, {
get(t, k) {
if (k in t) return t[k];
if (k === 'canvas') return canvas;
if (k === 'measureText') return (s) => ({ width: String(s).length * 10, actualBoundingBoxAscent: 8, actualBoundingBoxDescent: 2 });
if (k === 'createLinearGradient' || k === 'createRadialGradient' || k === 'createConicGradient' || k === 'createPattern') return () => ({ addColorStop() {} });
if (k === 'getImageData' || k === 'createImageData') return (x, y, w, h) => { const W = w ?? x?.width ?? 1, H = h ?? x?.height ?? 1; return { width: W, height: H, data: new Uint8ClampedArray(W * H * 4) }; };
if (k === 'getTransform') return () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
if (typeof k === 'symbol') return undefined;
return () => {};
},
set(t, k, v) { t[k] = v; return true; },
});
}
export function installFakePixi() {
const canvases = [];
const baseTextures = [];
const prev = { PIXI: globalThis.PIXI, document: globalThis.document, had: { PIXI: 'PIXI' in globalThis, document: 'document' in globalThis } };
class DisplayObject {
constructor() {
this.children = []; this.parent = null; this.visible = true; this.renderable = true; this.alpha = 1; this.tint = 0xffffff;
this.position = point(); this.scale = point(1, 1); this.anchor = point(); this.pivot = point(); this.skew = point();
this.rotation = 0; this.zIndex = 0; this.destroyed = false; this.blendMode = 0; this.sortableChildren = false;
this._w = null; this._h = null;
}
get x() { return this.position.x; } set x(v) { this.position.x = v; }
get y() { return this.position.y; } set y(v) { this.position.y = v; }
get width() { return this._w ?? (this.texture ? this.texture.width * Math.abs(this.scale.x) : 0); }
set width(v) { this._w = v; }
get height() { return this._h ?? (this.texture ? this.texture.height * Math.abs(this.scale.y) : 0); }
set height(v) { this._h = v; }
addChild(...cs) { for (const c of cs) { if (c.parent && c.parent !== this) c.parent.removeChild(c); if (!this.children.includes(c)) this.children.push(c); c.parent = this; } return cs[0]; }
addChildAt(c) { return this.addChild(c); }
removeChild(...cs) { for (const c of cs) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); c.parent = null; } return cs[0]; }
removeChildren() { const cs = this.children.slice(); for (const c of cs) this.removeChild(c); return cs; }
getLocalBounds() { return { x: -50, y: -100, width: 100, height: 100 }; }
getBounds() { return { x: 0, y: 0, width: 10, height: 10 }; }
toGlobal(p) { return { x: p.x, y: p.y }; }
on() { return this; } off() { return this; } once() { return this; }
destroy() { this.destroyed = true; this.parent?.removeChild(this); }
}
class Container extends DisplayObject {}
class BaseTexture {
constructor(resource = null) {
this.resource = resource; this.valid = true; this.destroyed = false; this.width = resource?.width || 1; this.height = resource?.height || 1;
this._glTextures = {}; baseTextures.push(this);
}
static from(src) { return new BaseTexture(src); }
update() {} setStyle() {} dispose() {} on() { return this; } once() { return this; } off() { return this; }
destroy() { this.destroyed = true; }
}
class Rectangle { constructor(x = 0, y = 0, width = 0, height = 0) { Object.assign(this, { x, y, width, height }); } }
class Texture {
constructor(base, frame) {
this.baseTexture = base || new BaseTexture();
this.frame = frame || new Rectangle(0, 0, this.baseTexture.width, this.baseTexture.height);
this.width = this.frame.width; this.height = this.frame.height; this.destroyed = false; this.orig = this.frame;
}
static from(src) { return new Texture(BaseTexture.from(src)); }
get valid() { return this.baseTexture.valid; }
update() {} on() { return this; } once() { return this; }
destroy(base) { this.destroyed = true; if (base) this.baseTexture.destroy(); }
}
Texture.EMPTY = new Texture(new BaseTexture({ width: 1, height: 1 }));
Texture.WHITE = new Texture(new BaseTexture({ width: 16, height: 16 }));
class Sprite extends Container { constructor(tex) { super(); this.texture = tex || Texture.EMPTY; } }
class TilingSprite extends Sprite {}
class Graphics extends Container {
constructor() { super(); this.geometry = { graphicsData: [] }; }
clear() { return this; } beginFill() { return this; } endFill() { return this; } lineStyle() { return this; } moveTo() { return this; } lineTo() { return this; }
drawRect() { return this; } drawCircle() { return this; } drawEllipse() { return this; } drawPolygon() { return this; } drawRoundedRect() { return this; } closePath() { return this; }
quadraticCurveTo() { return this; } bezierCurveTo() { return this; } arc() { return this; }
}
class Text extends Sprite {
constructor(text = '', style = {}) { super(); this.text = text; this.style = style; this.resolution = 1; this.rasters = 0; this.texture = new Texture(new BaseTexture({ width: 1, height: 1 })); }
updateText() { this.rasters++; }
}
class BitmapText extends Text {}
class Matrix { constructor() { this.a = 1; this.b = 0; this.c = 0; this.d = 1; this.tx = 0; this.ty = 0; } set() { return this; } translate() { return this; } scale() { return this; } identity() { return this; } }
class RenderTexture extends Texture { static create({ width = 1, height = 1 } = {}) { return new RenderTexture(new BaseTexture({ width, height })); } resize() {} }
const track = () => ({ timeScale: 1, trackTime: 0, animationEnd: 1, animationStart: 0, loop: false, mixDuration: 0, animation: { duration: 1, name: '' }, listener: null });
class Spine extends Container {
constructor(data) {
super();
this.spineData = { findAnimation: () => ({ duration: 1 }), ...data };
this.autoUpdate = true;
this.stateData = { defaultMix: 0, setMix() {} };
this.skeleton = { slots: [], setToSetupPose() {}, setSlotsToSetupPose() {}, updateWorldTransform() {}, findSlot: () => null };
const tracks = [];
this.state = {
tracks, timeScale: 1,
setAnimation: (i, name, loop) => { const e = { ...track(), loop, animation: { duration: 1, name } }; tracks[i] = e; return e; },
addAnimation: (i, name, loop) => { const e = { ...track(), loop, animation: { duration: 1, name } }; return e; },
setEmptyAnimation: () => track(), addEmptyAnimation: () => track(), clearTrack() {}, clearTracks() {}, addListener() {}, update() {}, apply() {},
};
}
update() {}
}
const modes = new Proxy({}, { get: () => 0 });
const P = {
Container, Sprite, TilingSprite, Graphics, Text, BitmapText, Texture, BaseTexture, Rectangle, Matrix, RenderTexture, DisplayObject,
ParticleContainer: Container,
BLEND_MODES: { NORMAL: 0, ADD: 1, MULTIPLY: 2, SCREEN: 3 }, MIPMAP_MODES: modes, SCALE_MODES: modes, WRAP_MODES: modes, ALPHA_MODES: modes,
BitmapFont: { from() {} },
utils: { TextureCache: {}, BaseTextureCache: {} },
spine: { Spine },
};
globalThis.PIXI = P;
globalThis.document = {
createElement(tag) {
const c = { tag, width: 300, height: 150, style: {} };
if (tag === 'canvas') { c.getContext = () => (c._ctx || (c._ctx = context2d(c))); canvases.push(c); }
return c;
},
fonts: undefined,
};
return {
P, canvases, baseTextures,
restore() {
if (prev.had.PIXI) globalThis.PIXI = prev.PIXI; else delete globalThis.PIXI;
if (prev.had.document) globalThis.document = prev.document; else delete globalThis.document;
},
};
}
/** Layers + context for a UnitView (render/units.js ViewCtx). */
export function fakeViewCtx(P, extra = {}) {
const layers = {};
for (const k of ['ground', 'anim', 'overlay', 'shadow', 'groundFx', 'fxNormal', 'units', 'fxAdd', 'bars', 'text', 'screen']) layers[k] = new P.Container();
return {
P, layers, settings: { damageNumbers: true, quality: 'high' }, fx: null,
heightAt: () => 0, animRate: () => 1, timeScale: () => 1, lookupDef: () => null, crowded: () => false,
frameNo: () => 0, impostorInterval: () => 0, clipAllowed: () => true, surfaceLayer: () => null,
...extra,
};
}