midnight_mystery / app /static /RoofFadeSystem.js
lcpizani's picture
Add full app: pages, assets, backend, config
811d960
Raw
History Blame Contribute Delete
3.49 kB
/**
* RoofFadeSystem β€” Phaser 3, per-building
*
* One instance per physical building. Each instance:
* - Creates its own Image of the shared roof texture
* - Clips it to this building's footprint via a geometry mask
* so only THIS building's roof section fades β€” all others are unaffected
* - Fades when the player walks under the building footprint (incl. halls)
* - Stays closed on optional exclude rects (balconies, courtyards)
* - Fades back in on exit
*/
const ROOF_TILE = 48;
class RoofFadeSystem {
/**
* @param {Phaser.Scene} scene
* @param {string} roofKey - preloaded texture key for the roof image
* @param {number} worldW - world width in pixels (roof display size)
* @param {number} worldH - world height in pixels
* @param {number[]} visualRect - [tx, ty, tw, th] tile coords β€” roof mask footprint
* @param {number[][]} triggerRects - [[tx,ty,tw,th], ...] areas that peel the roof
* @param {object} [opts]
* @param {number[][]} [opts.excludeRects] - outdoor slices that keep the roof visible
* @param {number} [opts.tileSize=48]
* @param {number} [opts.roofDepth=8]
* @param {number} [opts.fadeSpeed=4.8] alpha/sec (~0.08/frame @ 60fps)
*/
constructor(scene, roofKey, worldW, worldH, visualRect, triggerRects, opts = {}) {
const T = opts.tileSize ?? ROOF_TILE;
this._fadeSpeed = opts.fadeSpeed ?? 4.8;
this._alpha = 1.0;
const toPx = ([zx, zy, zw, zh]) => ({
x: zx * T, y: zy * T,
right: (zx + zw) * T,
bottom: (zy + zh) * T,
});
// Trigger rects in pixel coords for fast per-frame hit-testing.
this._triggers = triggerRects.map(toPx);
this._excludes = (opts.excludeRects || []).map(toPx);
// One Image per building β€” same texture, independent alpha.
this.roof = scene.add.image(0, 0, roofKey)
.setOrigin(0, 0)
.setDisplaySize(worldW, worldH)
.setDepth(opts.roofDepth ?? 8);
// Geometry mask clips the image to this building's footprint only.
const [vx, vy, vw, vh] = visualRect;
const clipG = scene.make.graphics({ add: false });
clipG.fillStyle(0xffffff);
clipG.fillRect(vx * T, vy * T, vw * T, vh * T);
this.roof.setMask(clipG.createGeometryMask());
}
_inRect(px, py, r) {
return px >= r.x && px < r.right && py >= r.y && py < r.bottom;
}
_isInside(px, py) {
let underRoof = false;
for (const t of this._triggers) {
if (this._inRect(px, py, t)) { underRoof = true; break; }
}
if (!underRoof) return false;
for (const e of this._excludes) {
if (this._inRect(px, py, e)) return false;
}
return true;
}
/**
* @param {number} px - player world-X pixels
* @param {number} py - player world-Y pixels
* @param {number} dt - delta time ms
*/
update(px, py, dt) {
const target = this._isInside(px, py) ? 0.0 : 1.0;
const step = (dt / 1000) * this._fadeSpeed;
if (this._alpha < target) {
this._alpha = Math.min(this._alpha + step, target);
} else if (this._alpha > target) {
this._alpha = Math.max(this._alpha - step, target);
}
if (this._alpha < 0.004) this._alpha = 0;
if (this._alpha > 0.996) this._alpha = 1;
this.roof.setAlpha(this._alpha);
}
get isFullyOpen() { return this._alpha === 0; }
get isFullyClosed() { return this._alpha === 1; }
destroy() { this.roof?.destroy(); }
}