From fa18e99a94a08e1711e6388678762bec43806565 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 6 Jul 2026 07:26:12 -0400 Subject: [PATCH] correct iso avatar customization --- html/js/render/assetPacks.js | 22 +++++++++++---------- html/js/render/charCustomize.js | 35 ++++++++++++++++++++++++--------- html/js/render/isoRenderer.js | 28 ++++++++++++++++++++------ test/renderPackResolver.test.js | 13 ++++++++---- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/html/js/render/assetPacks.js b/html/js/render/assetPacks.js index a1a2118..ef429bf 100644 --- a/html/js/render/assetPacks.js +++ b/html/js/render/assetPacks.js @@ -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) } }; diff --git a/html/js/render/charCustomize.js b/html/js/render/charCustomize.js index e282bb3..1834d7c 100644 --- a/html/js/render/charCustomize.js +++ b/html/js/render/charCustomize.js @@ -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')); diff --git a/html/js/render/isoRenderer.js b/html/js/render/isoRenderer.js index 2ac375e..9948196 100644 --- a/html/js/render/isoRenderer.js +++ b/html/js/render/isoRenderer.js @@ -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(); diff --git a/test/renderPackResolver.test.js b/test/renderPackResolver.test.js index 4c22709..cecf8ea 100644 --- a/test/renderPackResolver.test.js +++ b/test/renderPackResolver.test.js @@ -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', () => {