Files
wownerogue/html/js/render/avatarVisuals.js
T

198 lines
7.9 KiB
JavaScript

// AvatarVisuals is the render-kit resolver for turning a saved appearance identity into a
// drawable visual for a specific projection/context. Legacy ROT screens can use fallback tiles;
// canvas-capable renderers can draw the same identity as a composed character or premium skin.
(function (root) {
'use strict';
var RK = root.RK = root.RK || {};
var DEFAULT_EQUIPMENT = { body: 'none', head: 'none', shield: 'none', weapon: 'none' };
var DEFAULT_APPEARANCE = { avatar: 'default', tint: 'none', equipment: DEFAULT_EQUIPMENT };
var loading = {};
function normalize(appearance) {
if (RK.normalizeAppearance) return RK.normalizeAppearance(appearance || DEFAULT_APPEARANCE);
return DEFAULT_APPEARANCE;
}
function kindFor(appearance) {
if (!appearance) return 'legacy';
if (RK.isChar && RK.isChar(appearance.avatar)) return 'char';
if (RK.isSkin && RK.isSkin(appearance.avatar)) return 'skin';
if (appearance.avatar && /^kenney-/.test(appearance.avatar)) return 'model3d';
return 'legacy';
}
function fallbackTile(opts) {
opts = opts || {};
if (opts.fallbackTile) return opts.fallbackTile;
if (root.GameTiles && root.GameTiles.getPlayerTile) return root.GameTiles.getPlayerTile();
return '@';
}
function labelFor(appearance) {
if (RK.appearance) {
var info = RK.appearance(appearance);
if (info && info.label) return info.label;
}
return 'Character';
}
function resolve(appearance, opts) {
opts = opts || {};
var ap = normalize(appearance);
var kind = kindFor(ap);
return {
appearance: ap,
kind: kind,
projection: opts.projection || 'legacy-rot',
context: opts.context || 'player',
label: labelFor(ap),
fallbackTile: fallbackTile(opts),
canvas: kind === 'char' || kind === 'skin'
};
}
function rememberLoad(key, start, onReady) {
if (loading[key]) {
if (onReady) loading[key].push(onReady);
return;
}
loading[key] = onReady ? [onReady] : [];
start(function () {
var cbs = loading[key] || [];
delete loading[key];
cbs.forEach(function (cb) { try { cb(); } catch (_) { /* ignore */ } });
});
}
function ensureCanvasReady(visual, onReady) {
visual = visual && visual.appearance ? visual : resolve(visual);
if (visual.kind === 'char') {
if (RK.charAtlas && RK.charAtlas()) return true;
if (!RK.loadCharAtlas) return false;
rememberLoad('char-atlas', function (done) { RK.loadCharAtlas(done); }, onReady);
return false;
}
if (visual.kind === 'skin') {
if (RK.skinSheet && RK.skinSheet(visual.appearance.avatar)) return true;
if (!RK.loadSkin) return false;
rememberLoad('skin:' + visual.appearance.avatar, function (done) {
var rec = RK.loadSkin(visual.appearance.avatar);
if (rec && rec.cbs) rec.cbs.push(done);
}, onReady);
return false;
}
return false;
}
function drawCharIcon(ctx, visual, x, y, size) {
if (!RK.drawCharCompositeCanvas) return false;
return RK.drawCharCompositeCanvas(ctx, visual.appearance, x, y, size);
}
function drawSkinIcon(ctx, visual, x, y, size) {
var rec = RK.skinSheet && RK.skinSheet(visual.appearance.avatar);
var skin = RK.SKINS && RK.SKINS[visual.appearance.avatar];
if (!rec || !skin) return false;
var row = (skin.dirRows && skin.dirRows.down) || 0;
var dh = size;
var dw = dh * (skin.frameW / skin.frameH);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(rec.img, 0, row * skin.frameH, skin.frameW, skin.frameH, x + (size - dw) / 2, y, dw, dh);
return true;
}
function drawIcon(ctx, visual, rect, opts) {
opts = opts || {};
visual = visual && visual.appearance ? visual : resolve(visual, opts);
if (!ctx || !rect || !ensureCanvasReady(visual, opts.onReady)) return false;
var size = Math.min(rect.w || rect.size || 32, rect.h || rect.size || 32);
var x = rect.x + Math.max(0, ((rect.w || size) - size) / 2);
var y = rect.y + Math.max(0, ((rect.h || size) - size) / 2);
ctx.clearRect(rect.x, rect.y, rect.w || size, rect.h || size);
if (visual.kind === 'char') return drawCharIcon(ctx, visual, x, y, size);
if (visual.kind === 'skin') return drawSkinIcon(ctx, visual, x, y, size);
return false;
}
function drawTopdownChar(ctx, visual, entity, viewport, opts) {
opts = opts || {};
var ch = RK.CHARS && RK.CHARS[visual.appearance.avatar];
if (!ch || !RK.charFrame || !RK.drawCharTileCanvas) return false;
var cell = viewport.cell || 32;
var e = {
id: entity.id || 'avatar',
x: entity.x || 0,
y: entity.y || 0,
facing: entity.facing || 'down'
};
var frame = RK.charFrame(ch, e, opts.now || Date.now());
var dw = cell * (opts.scale || 1.8);
var dh = dw * frame.squash;
var cx = viewport.screenX * cell + cell / 2;
var dy = (viewport.screenY * cell + cell) - dh + frame.bob;
var ap = RK.charAppearance({ avatar: visual.appearance.avatar, appearance: visual.appearance });
ctx.imageSmoothingEnabled = false;
function drawComposite(dx, dy2) {
RK.drawCharTileCanvas(ctx, ch.frame, ap.tint, dx, dy2, dw, dh, ap.colors, 'base');
RK.charOverlayParts(ap).forEach(function (part) {
RK.drawCharTileCanvas(ctx, part.frame, part.tint, dx, dy2, dw, dh, part.colorable ? ap.colors : null, part.slot);
});
}
if (frame.flip) {
ctx.save();
ctx.translate(cx + dw / 2, dy);
ctx.scale(-1, 1);
drawComposite(0, 0);
ctx.restore();
} else {
drawComposite(cx - dw / 2, dy);
}
return true;
}
function drawTopdownSkin(ctx, visual, entity, viewport, opts) {
opts = opts || {};
var rec = RK.skinSheet && RK.skinSheet(visual.appearance.avatar);
var skin = RK.SKINS && RK.SKINS[visual.appearance.avatar];
if (!rec || !skin || !RK.skinFrame) return false;
var cell = viewport.cell || 32;
var e = {
id: entity.id || 'avatar-skin',
x: entity.x || 0,
y: entity.y || 0,
facing: entity.facing || 'down'
};
var frame = RK.skinFrame(skin, e, opts.now || Date.now());
var dh = cell * (skin.scale || 1.7);
var dw = dh * (skin.frameW / skin.frameH);
var cx = viewport.screenX * cell + cell / 2;
var dx = cx - dw / 2;
var dy = (viewport.screenY * cell + cell) - dh;
ctx.imageSmoothingEnabled = true;
ctx.drawImage(rec.img, frame.col * skin.frameW, frame.row * skin.frameH, skin.frameW, skin.frameH, dx, dy, dw, dh);
return true;
}
function drawTopdownWorld(ctx, visual, entity, viewport, opts) {
opts = opts || {};
visual = visual && visual.appearance ? visual : resolve(visual, opts);
if (!ctx || !entity || !viewport || !ensureCanvasReady(visual, opts.onReady)) return false;
if (visual.kind === 'char') return drawTopdownChar(ctx, visual, entity, viewport, opts);
if (visual.kind === 'skin') return drawTopdownSkin(ctx, visual, entity, viewport, opts);
return false;
}
RK.avatarVisuals = {
DEFAULT_APPEARANCE: DEFAULT_APPEARANCE,
normalize: normalize,
kindFor: kindFor,
resolve: resolve,
ensureCanvasReady: ensureCanvasReady,
drawIcon: drawIcon,
drawTopdownWorld: drawTopdownWorld
};
RK.resolveAvatarVisual = resolve;
})(window);