correct iso avatar customization

This commit is contained in:
jwinterm
2026-07-06 07:26:12 -04:00
parent 49ea01606e
commit fa18e99a94
4 changed files with 69 additions and 29 deletions
+12 -10
View File
@@ -203,6 +203,7 @@
RK.isoAssets = {
pack: 'iso-dungeon',
avatar: 'char-villager',
tile: { w: 84, h: 42, imageW: 92, imageH: 184 },
tiles: {
floor: 'assets/kenney/iso-dungeon/tiles/planks_S.png',
@@ -211,17 +212,18 @@
table: 'assets/kenney/iso-dungeon/tiles/chestClosed_S.png',
fallback: 'assets/kenney/iso-dungeon/tiles/planks_S.png'
},
character: isoMale(0),
// The Kenney files named Male_0..Male_7 are directional renders of the same body,
// not different character classes. The customizer exposes one honest Iso body for now.
character: isoMale(3),
directions: {
down: isoMale(3),
up: isoMale(7),
left: isoMale(0),
right: isoMale(5)
},
characters: {
fallback: isoMale(0),
'char-villager': isoMale(0),
'char-elder': isoMale(1),
'char-barbarian': isoMale(2),
'char-monk': isoMale(3),
'char-ranger': isoMale(4),
'char-bard': isoMale(5),
'char-rogue': isoMale(6),
'char-merchant': isoMale(7)
fallback: isoMale(3),
'char-villager': isoMale(3)
}
};
+26 -9
View File
@@ -117,17 +117,21 @@
return 'topdown';
}
function isoAvatarId() {
return (RK.isoAssets && RK.isoAssets.avatar) || 'char-villager';
}
function hasIsoCharacter(a) {
return !!(a && a.kind === 'char' && RK.isoAssets && RK.isoAssets.characters && RK.isoAssets.characters[a.id]);
return !!(a && a.kind === 'char' && a.id === isoAvatarId());
}
function appearanceVisibleInProjection(a, projection) {
if (!a) return false;
if (a.kind === 'color') return true;
if (projection === 'topdown') return a.kind === 'char' || a.kind === 'skin';
if (projection === 'topdown') return a.kind === 'color' || a.kind === 'char' || a.kind === 'skin';
if (projection === 'iso') return hasIsoCharacter(a);
if (projection === '3d') return a.kind === 'model3d';
return false;
if (projection === 'ascii') return a.kind === 'color';
return a.kind === 'color';
}
function loadThumbImage(url, onReady) {
@@ -140,7 +144,8 @@
return rec;
}
function tintColorForThumb(a, draft) {
function tintColorForThumb(a, draft, projection) {
if (projection === 'iso') return null;
var source = {};
var d = draft || {};
for (var dk in d) source[dk] = d[dk];
@@ -163,6 +168,13 @@
return (chars && (chars[a.id] || chars.fallback)) || (RK.isoAssets && RK.isoAssets.character) || null;
}
function isoSkinTintForDraft(draft) {
var colors = cloneColors(draft && draft.colors, draft && draft.tint);
if (!colors.skin || colors.skin === 'natural') return null;
var tone = RK.CHAR_SKIN_TONES && RK.CHAR_SKIN_TONES[colors.skin];
return (tone && tone.color) || null;
}
function imageBounds(img) {
var key = img && img.src;
if (!img || !key) return null;
@@ -210,7 +222,7 @@
}
function drawProjectionThumb(ctx, a, draft, projection, onReady) {
var color = tintColorForThumb(a, draft);
var color = tintColorForThumb(a, draft, projection);
ctx.clearRect(0, 0, 72, 80);
if (projection === 'ascii') {
ctx.fillStyle = color || '#9aa4b2';
@@ -238,7 +250,7 @@
var b = imageBounds(rec.img);
var dh = 52;
var dw = dh * (b.w / b.h);
drawTintedImage(ctx, rec.img, b, 36 - dw / 2, 74 - dh, dw, dh, color);
drawTintedImage(ctx, rec.img, b, 36 - dw / 2, 74 - dh, dw, dh, isoSkinTintForDraft(draft));
} else {
ctx.fillStyle = color || '#9aa4b2';
ctx.beginPath();
@@ -683,6 +695,11 @@
else ensureColorDraft();
var projection = activeProjection();
if (projection === 'iso' && isCharDraft()) {
sectionTitle('Color', editSection);
renderColorRow('Skin', 'skin', 'skin', editSection);
return;
}
if (projection !== 'topdown' || !isCharDraft()) {
sectionTitle('Color', editSection);
renderColorRow('Color', 'base', 'tint', editSection);
@@ -782,7 +799,7 @@
}, redrawers));
var lab = document.createElement('div');
lab.className = 'rk-option-label';
lab.textContent = a.label + (a.premium ? ' *' : '');
lab.textContent = (activeProjection() === 'iso' ? 'Adventurer' : a.label) + (a.premium ? ' *' : '');
card.appendChild(lab);
card.onclick = function () { selectAvatar(a); };
grid.appendChild(card);
@@ -828,7 +845,7 @@
} else {
drawBaseThumb(pctx, base, draft, 'topdown', redrawAll);
}
previewName.textContent = base.label || base.id;
previewName.textContent = projection === 'iso' ? 'Adventurer' : (base.label || base.id);
previewMeta.textContent = projection !== 'topdown'
? (projection.toUpperCase() + ' identity')
: (base.premium ? 'Premium pack' : (base.kind === 'char' ? 'Customizable' : 'Free'));
+22 -6
View File
@@ -50,9 +50,16 @@
IsoRenderer.prototype._preload = function () {
var tiles = this.assets.tiles || {};
for (var k in tiles) this._load(tiles[k]);
var self = this;
function loadCharacter(ch) {
if (!ch) return;
self._load(ch.idle);
(ch.run || []).forEach(self._load.bind(self));
}
var ch = this.assets.character || {};
this._load(ch.idle);
(ch.run || []).forEach(this._load.bind(this));
loadCharacter(ch);
var dirs = this.assets.directions || {};
for (var d in dirs) loadCharacter(dirs[d]);
};
IsoRenderer.prototype._project = function (x, y, originX, originY) {
@@ -120,6 +127,15 @@
return cv;
};
IsoRenderer.prototype._skinTintFor = function (visual) {
var ap = visual && visual.appearance;
var skin = ap && ap.colors && ap.colors.skin;
if (!skin || skin === 'natural') return null;
var tones = root.RK && RK.CHAR_SKIN_TONES;
var tone = tones && tones[skin];
return (tone && tone.color) || null;
};
IsoRenderer.prototype._tileUrl = function (kind) {
var tiles = this.assets.tiles || {};
return tiles[kind] || tiles.fallback || tiles.floor;
@@ -137,11 +153,13 @@
IsoRenderer.prototype._charFrame = function (e, now) {
var visual = this._visualFor(e);
if (visual && visual.allowed === false) return null;
var dirs = this.assets.directions || {};
var facing = (e && e.facing) || 'down';
var key = e.id || 'anon';
var st = this.last[key] || (this.last[key] = { x: e.x, y: e.y, t: 0 });
if (st.x !== e.x || st.y !== e.y) { st.x = e.x; st.y = e.y; st.t = now; }
var moving = (now - st.t) < 360;
var ch = (visual && visual.character) || (this.assets.character || {});
var ch = dirs[facing] || (visual && visual.character) || (this.assets.character || {});
if (!moving || !ch.run || ch.run.length === 0) return { url: ch.idle, character: ch, visual: visual };
this._animating = true;
return { url: ch.run[Math.floor(now / 80) % ch.run.length], character: ch, visual: visual };
@@ -196,9 +214,7 @@
rec = this._load(frame && frame.url);
if (rec && rec.ready) {
var ch = frame.character || this.assets.character || {};
var tint = root.RK && RK.avatarVisuals && RK.avatarVisuals.tintColorFor
? RK.avatarVisuals.tintColorFor(frame.visual && frame.visual.appearance)
: null;
var tint = this._skinTintFor(frame.visual);
this._drawSpriteImage(this._tintedImage(rec.img, tint), it.sx, it.sy + this.tileH * 0.95, ch.imageH || 92);
} else {
ctx.beginPath();
+9 -4
View File
@@ -82,7 +82,7 @@ describe('render pack visual resolver', () => {
RK.setEntitlementSnapshot({ totalCreditsPurchased: 1 });
const iso = RK.avatarVisuals.resolve({ avatar: 'char-ranger' }, { projection: 'iso' });
expect(iso).toMatchObject({ projection: 'iso', kind: 'iso', pack: 'iso-dungeon', allowed: true });
expect(iso.character.idle).toContain('Male_4_Idle0.png');
expect(iso.character.idle).toContain('Male_3_Idle0.png');
const compat = RK.resolveAppearance(
{ appearance: { avatar: 'kenney-survivor-female' } },
@@ -98,12 +98,17 @@ describe('render pack visual resolver', () => {
expect(RK.isoAssets.tiles.wall).toContain('stoneWallHalf_S.png');
});
test('iso pack exposes only real imported character variants', () => {
test('iso pack treats imported Male files as directions, not class variants', () => {
const RK = loadRenderKit();
expect(RK.isoAssets.characters['char-villager'].idle).toContain('Male_0_Idle0.png');
expect(RK.isoAssets.characters['char-merchant'].idle).toContain('Male_7_Idle0.png');
expect(RK.isoAssets.avatar).toBe('char-villager');
expect(RK.isoAssets.characters['char-villager'].idle).toContain('Male_3_Idle0.png');
expect(RK.isoAssets.characters['char-merchant']).toBeUndefined();
expect(RK.isoAssets.characters['char-wizard']).toBeUndefined();
expect(RK.isoAssets.characters['char-goblin']).toBeUndefined();
expect(RK.isoAssets.directions.down.idle).toContain('Male_3_Idle0.png');
expect(RK.isoAssets.directions.up.idle).toContain('Male_7_Idle0.png');
expect(RK.isoAssets.directions.left.idle).toContain('Male_0_Idle0.png');
expect(RK.isoAssets.directions.right.idle).toContain('Male_5_Idle0.png');
});
test('projection visuals expose the saved base tint as a render color', () => {