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', () => {