Files
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

118 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Character skins for the render kit. A skin is an animated sprite sheet: rows are facing
// directions, columns are animation frames. Sheets are large and premium, so they are:
// - lazy-loaded (only fetched when an entity actually uses the skin, in a sprite-capable mode),
// - gated by entitlement (RK.entitlements.premium),
// - delivered as WebP.
(function (root) {
'use strict';
var RK = root.RK = root.RK || {};
// Skin registry. `sheet` is hosted under assets/generated (gitignored / CDN-delivered).
RK.SKINS = {
'monero-knight': {
id: 'monero-knight', label: 'Monero Knight', premium: true, pack: 'generated-skins',
sheet: 'assets/generated/monero_knight_walk.webp',
frameW: 190, frameH: 233, cols: 8, rows: 4, fps: 10,
dirRows: { down: 0, left: 1, right: 2, up: 3 },
scale: 2.0, // sprite height ≈ scale × tile cell
moveWindowMs: 350 // consider "moving" (animate) if position changed this recently
},
'wownero-rogue': {
id: 'wownero-rogue', label: 'Wownero Rogue', premium: true, pack: 'generated-skins',
sheet: 'assets/generated/wownero_walk.webp',
frameW: 169, frameH: 223, cols: 8, rows: 4, fps: 10,
dirRows: { down: 0, left: 1, right: 2, up: 3 },
scale: 2.0, moveWindowMs: 350
},
'cypher-operative': {
id: 'cypher-operative', label: 'Cypher Operative', premium: true, pack: 'generated-skins',
sheet: 'assets/generated/operative_walk.webp',
frameW: 160, frameH: 225, cols: 8, rows: 4, fps: 10,
dirRows: { down: 0, left: 1, right: 2, up: 3 },
scale: 2.0, moveWindowMs: 350
}
};
RK.isSkin = function (id) { return !!(id && RK.SKINS[id]); };
// --- Lazy sheet loading ------------------------------------------------------------------
var sheets = {}; // id -> { ready, img, base, texCache, cbs }
RK.loadSkin = function (id) {
var s = RK.SKINS[id];
if (!s) return null;
if (sheets[id]) return sheets[id];
var rec = sheets[id] = { ready: false, cbs: [] };
var img = new Image();
img.onload = function () {
rec.ready = true; rec.img = img;
rec.cbs.forEach(function (cb) { try { cb(rec); } catch (_) {} });
rec.cbs = [];
};
img.onerror = function () { if (root.console) console.warn('skin sheet failed:', s.sheet); };
img.src = s.sheet;
return rec;
};
RK.skinSheet = function (id) { return (sheets[id] && sheets[id].ready) ? sheets[id] : null; };
// --- Movement-aware frame selection ------------------------------------------------------
var last = {}; // entity id -> { x, y, t }
RK.skinFrame = function (skin, e, nowMs) {
var st = last[e.id] || (last[e.id] = { x: e.x, y: e.y, t: 0 });
if (e.x !== st.x || e.y !== st.y) { st.x = e.x; st.y = e.y; st.t = nowMs; }
var moving = (nowMs - st.t) < (skin.moveWindowMs || 350);
var row = (skin.dirRows && skin.dirRows[e.facing] != null) ? skin.dirRows[e.facing] : 0;
var col = moving ? Math.floor(nowMs / 1000 * skin.fps) % skin.cols : 0; // idle -> frame 0
return { row: row, col: col };
};
// --- Canvas draw (TileRenderer). Returns false (and kicks a lazy load) if not ready. ------
RK.drawSkinCanvas = function (ctx, e, cell, nowMs) {
var rec = RK.skinSheet(e.avatar);
if (!rec) { RK.loadSkin(e.avatar); return false; }
var s = RK.SKINS[e.avatar];
var fr = RK.skinFrame(s, e, nowMs);
var dh = cell * (s.scale || 1.7);
var dw = dh * (s.frameW / s.frameH);
var cx = e.x * cell + cell / 2;
var dx = cx - dw / 2;
var dy = (e.y * cell + cell) - dh; // feet at tile bottom
ctx.imageSmoothingEnabled = true;
// Warm backlight so a dark character separates from the dark background.
var midY = dy + dh * 0.45;
var halo = ctx.createRadialGradient(cx, midY, 0, cx, midY, dw * 0.8);
halo.addColorStop(0, 'rgba(255,226,180,0.30)');
halo.addColorStop(1, 'rgba(255,226,180,0)');
ctx.fillStyle = halo;
ctx.beginPath(); ctx.arc(cx, midY, dw * 0.8, 0, Math.PI * 2); ctx.fill();
if (e.you) {
ctx.strokeStyle = 'rgba(255,255,255,0.85)'; ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(cx, e.y * cell + cell - 2, dw * 0.3, cell * 0.16, 0, 0, Math.PI * 2);
ctx.stroke();
}
ctx.drawImage(rec.img, fr.col * s.frameW, fr.row * s.frameH, s.frameW, s.frameH, dx, dy, dw, dh);
if (e.label) {
ctx.fillStyle = '#d7dbe0'; ctx.font = '11px monospace'; ctx.textAlign = 'center';
ctx.fillText(e.label, cx, dy - 2);
}
return true;
};
// --- Pixi texture for a skin frame (FancyRenderer), cached. ------------------------------
RK.skinTexture = function (id, row, col) {
var rec = RK.skinSheet(id);
if (!rec || typeof PIXI === 'undefined') return null;
var s = RK.SKINS[id];
if (!rec.base) {
rec.base = PIXI.BaseTexture.from(rec.img);
rec.base.scaleMode = PIXI.SCALE_MODES.LINEAR;
rec.texCache = {};
}
var key = row + ',' + col;
if (!rec.texCache[key]) {
rec.texCache[key] = new PIXI.Texture(rec.base, new PIXI.Rectangle(col * s.frameW, row * s.frameH, s.frameW, s.frameH));
}
return rec.texCache[key];
};
})(window);