mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 22:58:32 +02:00
394 lines
17 KiB
JavaScript
394 lines
17 KiB
JavaScript
// FancyRenderer — WebGL tier via PixiJS (v7). Beyond flat shapes: a real blurred bloom on
|
|
// entities, warm flickering lights along the bar, drifting dust motes, and a vignette — all
|
|
// programmatic (no art assets). Requires a global PIXI (loaded from CDN, with @pixi/unsafe-eval
|
|
// so shaders compile under a strict CSP); the factory falls back to tiled if PIXI is missing.
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
function colorNum(hex) { return parseInt(String(hex).replace('#', ''), 16) || 0; }
|
|
function facingDelta(f) {
|
|
if (f === 'up') return { x: 0, y: -1 };
|
|
if (f === 'left') return { x: -1, y: 0 };
|
|
if (f === 'right') return { x: 1, y: 0 };
|
|
return { x: 0, y: 1 };
|
|
}
|
|
// A soft radial-gradient texture (used for lights, motes, and the vignette).
|
|
function radialTexture(size, stops) {
|
|
var c = document.createElement('canvas');
|
|
c.width = c.height = size;
|
|
var ctx = c.getContext('2d');
|
|
var g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
stops.forEach(function (s) { g.addColorStop(s[0], s[1]); });
|
|
ctx.fillStyle = g;
|
|
ctx.fillRect(0, 0, size, size);
|
|
return PIXI.Texture.from(c);
|
|
}
|
|
|
|
function FancyRenderer(host, opts) {
|
|
if (typeof PIXI === 'undefined') throw new Error('PIXI not loaded');
|
|
opts = opts || {};
|
|
this.name = 'fancy';
|
|
this.host = host;
|
|
this.cell = opts.cell || 26;
|
|
this.app = new PIXI.Application({ backgroundColor: 0x0a0c0f, antialias: true });
|
|
this.app.view.className = 'rk-canvas';
|
|
this.canvas = this.app.view;
|
|
host.appendChild(this.app.view);
|
|
|
|
this.tileLayer = new PIXI.Container();
|
|
this.lightLayer = new PIXI.Container();
|
|
this.entityLayer = new PIXI.Container();
|
|
this.particleLayer = new PIXI.Container();
|
|
this.app.stage.addChild(this.tileLayer, this.lightLayer, this.entityLayer, this.particleLayer);
|
|
this.vignette = null;
|
|
|
|
this.texLight = radialTexture(128, [[0, 'rgba(255,190,110,0.95)'], [1, 'rgba(255,190,110,0)']]);
|
|
this.texMote = radialTexture(64, [[0, 'rgba(255,240,210,0.9)'], [1, 'rgba(255,240,210,0)']]);
|
|
this.texVig = radialTexture(256, [[0, 'rgba(0,0,0,0)'], [0.6, 'rgba(0,0,0,0)'], [1, 'rgba(0,0,0,0.62)']]);
|
|
|
|
this.sprites = {};
|
|
this.lights = [];
|
|
this.particles = [];
|
|
this._sizeKey = null;
|
|
this._t = 0;
|
|
|
|
var self = this;
|
|
this._tick = function (dt) { self._animate(dt); };
|
|
this.app.ticker.add(this._tick);
|
|
}
|
|
|
|
FancyRenderer.prototype.render = function (scene) {
|
|
if (!scene || !this.app) return;
|
|
var cell = this.cell, w = scene.cols * cell, h = scene.rows * cell;
|
|
this.app.renderer.resize(w, h);
|
|
var key = scene.cols + 'x' + scene.rows;
|
|
var hasRoom = !!(window.RK && RK.roomReady && RK.roomReady());
|
|
if (key !== this._sizeKey || hasRoom !== this._hadRoom) {
|
|
this._buildTiles(scene);
|
|
this._buildLights(scene);
|
|
this._buildVignette(w, h);
|
|
this._seedParticles(w, h);
|
|
this._sizeKey = key;
|
|
this._hadRoom = hasRoom;
|
|
}
|
|
this._syncEntities(scene);
|
|
};
|
|
|
|
FancyRenderer.prototype._buildTiles = function (scene) {
|
|
this.tileLayer.removeChildren();
|
|
var cell = this.cell;
|
|
// A designed room is loaded — draw its layered tiles as sprites from the room atlas.
|
|
if (window.RK && RK.roomReady && RK.roomReady()) {
|
|
var d = RK.activeRoom.desc, a = RK.activeRoom.atlas;
|
|
for (var li = 0; li < d.layerOrder.length; li++) {
|
|
var grid = d.layers[d.layerOrder[li]];
|
|
for (var ry = 0; ry < d.rows; ry++) {
|
|
for (var rx = 0; rx < d.cols; rx++) {
|
|
var idx = grid[ry][rx];
|
|
if (idx < 0) continue;
|
|
var tex = a.texture(idx % a.cols, Math.floor(idx / a.cols));
|
|
if (!tex) continue;
|
|
var sp = new PIXI.Sprite(tex);
|
|
sp.x = rx * cell; sp.y = ry * cell; sp.width = cell; sp.height = cell;
|
|
this.tileLayer.addChild(sp);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
for (var y = 0; y < scene.rows; y++) {
|
|
for (var x = 0; x < scene.cols; x++) {
|
|
var def = scene.legend[scene.grid[y][x]] || { color: '#333' };
|
|
var g = new PIXI.Graphics();
|
|
g.beginFill(colorNum(def.color));
|
|
g.drawRoundedRect(0, 0, cell - 1, cell - 1, 4);
|
|
g.endFill();
|
|
if (def.solid) {
|
|
g.beginFill(0xffffff, 0.09);
|
|
g.drawRoundedRect(0, 0, cell - 1, Math.max(2, cell * 0.2), 4);
|
|
g.endFill();
|
|
g.beginFill(0x000000, 0.22);
|
|
g.drawRect(0, cell - 3, cell - 1, 3);
|
|
g.endFill();
|
|
}
|
|
g.x = x * cell;
|
|
g.y = y * cell;
|
|
this.tileLayer.addChild(g);
|
|
}
|
|
}
|
|
};
|
|
|
|
FancyRenderer.prototype._buildLights = function (scene) {
|
|
this.lightLayer.removeChildren();
|
|
this.lights = [];
|
|
var cell = this.cell;
|
|
for (var y = 0; y < scene.rows; y++) {
|
|
for (var x = 0; x < scene.cols; x++) {
|
|
if (scene.grid[y][x] !== 'bar' || (x % 3) !== 0) continue; // sample the bar
|
|
var s = new PIXI.Sprite(this.texLight);
|
|
s.anchor.set(0.5);
|
|
s.blendMode = PIXI.BLEND_MODES.ADD;
|
|
s.x = x * cell + cell / 2;
|
|
s.y = y * cell + cell / 2;
|
|
s.scale.set(cell * 2.4 / 128);
|
|
s.alpha = 0.5;
|
|
this.lightLayer.addChild(s);
|
|
this.lights.push({ s: s, base: 0.5, phase: Math.random() * 6.28 });
|
|
}
|
|
}
|
|
};
|
|
|
|
FancyRenderer.prototype._buildVignette = function (w, h) {
|
|
if (this.vignette) { this.app.stage.removeChild(this.vignette); this.vignette.destroy(); }
|
|
this.vignette = new PIXI.Sprite(this.texVig);
|
|
this.vignette.anchor.set(0.5);
|
|
this.vignette.x = w / 2;
|
|
this.vignette.y = h / 2;
|
|
this.vignette.width = w * 1.25;
|
|
this.vignette.height = h * 1.35;
|
|
this.app.stage.addChild(this.vignette); // drawn on top of everything
|
|
};
|
|
|
|
FancyRenderer.prototype._seedParticles = function (w, h) {
|
|
this.particleLayer.removeChildren();
|
|
this.particles = [];
|
|
var n = Math.min(60, Math.max(12, Math.round(w * h / 9000)));
|
|
for (var i = 0; i < n; i++) {
|
|
var p = new PIXI.Sprite(this.texMote);
|
|
p.anchor.set(0.5);
|
|
p.blendMode = PIXI.BLEND_MODES.ADD;
|
|
p.x = Math.random() * w;
|
|
p.y = Math.random() * h;
|
|
p.scale.set(0.12 + Math.random() * 0.18);
|
|
p.alpha = 0.15 + Math.random() * 0.2;
|
|
this.particleLayer.addChild(p);
|
|
this.particles.push({
|
|
s: p, w: w, h: h, ph: Math.random() * 6.28,
|
|
vx: (Math.random() - 0.5) * 0.25, vy: -(0.05 + Math.random() * 0.15)
|
|
});
|
|
}
|
|
};
|
|
|
|
FancyRenderer.prototype._makeSprite = function (e) {
|
|
var cell = this.cell, r = cell * 0.34, col = colorNum(e.color);
|
|
var c = new PIXI.Container();
|
|
|
|
var glow = new PIXI.Graphics();
|
|
glow.beginFill(col, 0.6);
|
|
glow.drawCircle(0, 0, r * 1.4);
|
|
glow.endFill();
|
|
try {
|
|
if (!PIXI.BlurFilter) throw new Error('no blur');
|
|
glow.filters = [new PIXI.BlurFilter(8)]; // real soft bloom
|
|
} catch (_) {
|
|
// No blur available: fake it with layered alpha rings.
|
|
glow.clear();
|
|
[[1.9, 0.05], [1.5, 0.10], [1.15, 0.18]].forEach(function (ring) {
|
|
glow.beginFill(col, ring[1]);
|
|
glow.drawCircle(0, 0, r * ring[0]);
|
|
glow.endFill();
|
|
});
|
|
}
|
|
glow.blendMode = PIXI.BLEND_MODES.ADD;
|
|
c.addChild(glow);
|
|
c._glow = glow;
|
|
|
|
var body = new PIXI.Graphics();
|
|
body.beginFill(col);
|
|
body.drawCircle(0, 0, r);
|
|
body.endFill();
|
|
var d = facingDelta(e.facing);
|
|
body.beginFill(0x0a0c0f);
|
|
body.drawCircle(d.x * r * 0.55, d.y * r * 0.55, cell * 0.08);
|
|
body.endFill();
|
|
c.addChild(body);
|
|
|
|
if (e.you) {
|
|
var ring = new PIXI.Graphics();
|
|
ring.lineStyle(2, 0xffffff, 0.9);
|
|
ring.drawCircle(0, 0, r + 2);
|
|
c.addChild(ring);
|
|
}
|
|
if (e.label) {
|
|
var label = new PIXI.Text(e.label, { fontFamily: 'monospace', fontSize: 11, fill: 0xd7dbe0 });
|
|
label.anchor.set(0.5, 1);
|
|
label.y = -cell * 0.5;
|
|
c.addChild(label);
|
|
}
|
|
return c;
|
|
};
|
|
|
|
// A premium animated-skin sprite (Pixi). Frames are swapped each tick in _animate.
|
|
FancyRenderer.prototype._makeSkinSprite = function (e) {
|
|
var s = window.RK.SKINS[e.avatar];
|
|
var c = new PIXI.Container();
|
|
var h = this.cell * (s.scale || 1.8);
|
|
// Warm backlight so a dark character reads against the dark room.
|
|
var glow = new PIXI.Graphics();
|
|
[[h * 0.55, 0.05], [h * 0.4, 0.10], [h * 0.28, 0.16]].forEach(function (rr) {
|
|
glow.beginFill(0xffe2b4, rr[1]); glow.drawCircle(0, 0, rr[0]); glow.endFill();
|
|
});
|
|
glow.blendMode = PIXI.BLEND_MODES.ADD;
|
|
glow.y = this.cell * 0.5 - h * 0.42;
|
|
c.addChild(glow);
|
|
var spr = new PIXI.Sprite(window.RK.skinTexture(e.avatar, 0, 0));
|
|
spr.anchor.set(0.5, 0.9);
|
|
spr.height = h; spr.width = h * (s.frameW / s.frameH);
|
|
spr.y = this.cell * 0.5; // feet at the tile's bottom
|
|
c.addChild(spr);
|
|
c._skinSpr = spr; c._skinId = e.avatar;
|
|
if (e.you) {
|
|
var ring = new PIXI.Graphics();
|
|
ring.lineStyle(2, 0xffffff, 0.85);
|
|
ring.drawEllipse(0, this.cell * 0.5, this.cell * 0.3, this.cell * 0.16);
|
|
c.addChild(ring);
|
|
}
|
|
if (e.label) {
|
|
var label = new PIXI.Text(e.label, { fontFamily: 'monospace', fontSize: 11, fill: 0xd7dbe0 });
|
|
label.anchor.set(0.5, 1); label.y = this.cell * 0.5 - h - 2;
|
|
c.addChild(label);
|
|
}
|
|
return c;
|
|
};
|
|
|
|
function configureCharPart(spr, h) {
|
|
spr.anchor.set(0.5, 0.9);
|
|
spr.height = h;
|
|
spr.width = h;
|
|
return spr;
|
|
}
|
|
|
|
// A roguelike character sprite (procedural walk + composited equipment), animated in _animate.
|
|
FancyRenderer.prototype._makeCharSprite = function (e) {
|
|
var appearance = window.RK.charAppearance(e);
|
|
var ch = window.RK.CHARS[appearance.avatar];
|
|
var c = new PIXI.Container();
|
|
var h = this.cell * 1.8;
|
|
var body = new PIXI.Container();
|
|
body.y = this.cell * 0.5;
|
|
var baseTex = window.RK.charTileTexture(ch.frame, appearance.tint, appearance.colors, 'base');
|
|
if (baseTex) body.addChild(configureCharPart(new PIXI.Sprite(baseTex), h));
|
|
window.RK.charOverlayParts(appearance).forEach(function (part) {
|
|
var tex = window.RK.charTileTexture(part.frame, part.tint, part.colorable ? appearance.colors : null, part.slot);
|
|
if (tex) body.addChild(configureCharPart(new PIXI.Sprite(tex), h));
|
|
});
|
|
c.addChild(body);
|
|
c._charBody = body; c._charId = appearance.avatar; c._charKey = window.RK.charRenderKey(e);
|
|
if (e.you) {
|
|
var ring = new PIXI.Graphics();
|
|
ring.lineStyle(2, 0xffffff, 0.85);
|
|
ring.drawEllipse(0, this.cell * 0.5, this.cell * 0.3, this.cell * 0.16);
|
|
c.addChild(ring);
|
|
}
|
|
if (e.label) {
|
|
var label = new PIXI.Text(e.label, { fontFamily: 'monospace', fontSize: 11, fill: 0xd7dbe0 });
|
|
label.anchor.set(0.5, 1); label.y = this.cell * 0.5 - h - 2;
|
|
c.addChild(label);
|
|
}
|
|
return c;
|
|
};
|
|
|
|
FancyRenderer.prototype._syncEntities = function (scene) {
|
|
var cell = this.cell, seen = {}, RKr = window.RK, focusFallback = null;
|
|
for (var i = 0; i < scene.entities.length; i++) {
|
|
var e = scene.entities[i];
|
|
seen[e.id] = true;
|
|
var wantSkin = !!(RKr && RKr.isSkin && RKr.isSkin(e.avatar));
|
|
var wantChar = !!(RKr && RKr.isChar && RKr.isChar(e.avatar));
|
|
if (wantSkin && !RKr.skinSheet(e.avatar)) RKr.loadSkin(e.avatar); // lazy fetch
|
|
if (wantChar && !RKr.charAtlas()) RKr.loadCharAtlas();
|
|
var kind = (wantSkin && RKr.skinSheet(e.avatar)) ? 'skin'
|
|
: (wantChar && RKr.charAtlas()) ? 'char'
|
|
: 'circle';
|
|
var renderKey = (kind === 'char' && RKr.charRenderKey) ? RKr.charRenderKey(e) : e.avatar;
|
|
var s = this.sprites[e.id];
|
|
if (s && (s.kind !== kind || s.renderKey !== renderKey)) { // sheet loaded or appearance changed -> rebuild
|
|
this.entityLayer.removeChild(s.c); s.c.destroy({ children: true });
|
|
s = null; delete this.sprites[e.id];
|
|
}
|
|
if (!s) {
|
|
var c = (kind === 'skin') ? this._makeSkinSprite(e)
|
|
: (kind === 'char') ? this._makeCharSprite(e)
|
|
: this._makeSprite(e);
|
|
c.x = e.x * cell + cell / 2;
|
|
c.y = e.y * cell + cell / 2;
|
|
this.entityLayer.addChild(c);
|
|
s = this.sprites[e.id] = { c: c, kind: kind, renderKey: renderKey };
|
|
}
|
|
s.tx = e.x; s.ty = e.y; s.e = e;
|
|
if (e.kind === 'player' && !focusFallback) focusFallback = e;
|
|
if (e.you || e.cameraTarget) {
|
|
this.focusPoint = { x: e.x * cell + cell / 2, y: e.y * cell + cell / 2 };
|
|
focusFallback = null;
|
|
}
|
|
}
|
|
if (!this.focusPoint && focusFallback) {
|
|
this.focusPoint = { x: focusFallback.x * cell + cell / 2, y: focusFallback.y * cell + cell / 2 };
|
|
}
|
|
for (var id in this.sprites) {
|
|
if (!seen[id]) {
|
|
this.entityLayer.removeChild(this.sprites[id].c);
|
|
this.sprites[id].c.destroy({ children: true });
|
|
delete this.sprites[id];
|
|
}
|
|
}
|
|
};
|
|
|
|
FancyRenderer.prototype._animate = function (dt) {
|
|
this._t += dt;
|
|
var t = this._t, cell = this.cell, k = Math.min(1, 0.22 * dt), i, nowMs = Date.now();
|
|
|
|
for (var id in this.sprites) {
|
|
var s = this.sprites[id];
|
|
var tx = s.tx * cell + cell / 2, ty = s.ty * cell + cell / 2;
|
|
s.c.x += (tx - s.c.x) * k;
|
|
s.c.y += (ty - s.c.y) * k;
|
|
if (s.c._charBody && s.e) {
|
|
var cf = window.RK.charFrame(window.RK.CHARS[s.c._charId], s.e, nowMs);
|
|
s.c._charBody.scale.x = cf.flip ? -1 : 1;
|
|
s.c._charBody.scale.y = cf.squash;
|
|
s.c._charBody.y = this.cell * 0.5 + cf.bob;
|
|
} else if (s.c._skinSpr && s.e) {
|
|
var sk = window.RK.SKINS[s.c._skinId];
|
|
var fr = window.RK.skinFrame(sk, s.e, nowMs);
|
|
var tex = window.RK.skinTexture(s.c._skinId, fr.row, fr.col);
|
|
if (tex) s.c._skinSpr.texture = tex;
|
|
} else if (s.c._glow) {
|
|
s.c._glow.scale.set(1 + Math.sin(t * 0.09 + s.tx) * 0.16);
|
|
s.c._glow.alpha = 0.65 + Math.sin(t * 0.11) * 0.25;
|
|
}
|
|
}
|
|
for (i = 0; i < this.lights.length; i++) {
|
|
var L = this.lights[i];
|
|
L.s.alpha = L.base + Math.sin(t * 0.25 + L.phase) * 0.14 + Math.sin(t * 0.9 + L.phase * 1.7) * 0.06;
|
|
}
|
|
for (i = 0; i < this.particles.length; i++) {
|
|
var p = this.particles[i];
|
|
p.s.x += p.vx * dt;
|
|
p.s.y += p.vy * dt;
|
|
if (p.s.y < -8) { p.s.y = p.h + 8; p.s.x = Math.random() * p.w; }
|
|
if (p.s.x < -8) p.s.x = p.w + 8; else if (p.s.x > p.w + 8) p.s.x = -8;
|
|
p.s.alpha = 0.12 + (Math.sin(t * 0.15 + p.ph) + 1) * 0.12;
|
|
}
|
|
};
|
|
|
|
FancyRenderer.prototype.destroy = function () {
|
|
try {
|
|
if (this.app) {
|
|
this.app.ticker.remove(this._tick);
|
|
this.app.destroy(true, { children: true });
|
|
}
|
|
} catch (_) { /* ignore */ }
|
|
[this.texLight, this.texMote, this.texVig].forEach(function (tx) {
|
|
try { if (tx) tx.destroy(true); } catch (_) { /* ignore */ }
|
|
});
|
|
this.app = null;
|
|
this.sprites = {};
|
|
this.lights = [];
|
|
this.particles = [];
|
|
};
|
|
|
|
root.RK = root.RK || {};
|
|
root.RK.FancyRenderer = FancyRenderer;
|
|
})(window);
|