diff --git a/html/js/render/assetPacks.js b/html/js/render/assetPacks.js
index e8927d4..65e8ec5 100644
--- a/html/js/render/assetPacks.js
+++ b/html/js/render/assetPacks.js
@@ -96,6 +96,7 @@
var p = RK.pack(id);
if (!p) return false;
if (!p.premium) return true;
+ if (RK.renderModeTestUnlocks && RK.renderModeTestUnlocks() && (p.projection === 'iso' || p.projection === '3d')) return true;
return !!(RK.entitlements && RK.entitlements.packs && RK.entitlements.packs[id]);
};
RK.canUseAppearance = function (a) {
diff --git a/html/js/render/charCustomize.js b/html/js/render/charCustomize.js
index 4edcd13..26445c3 100644
--- a/html/js/render/charCustomize.js
+++ b/html/js/render/charCustomize.js
@@ -177,8 +177,9 @@
ctx.beginPath(); ctx.moveTo(17, 26); ctx.lineTo(35, 26); ctx.stroke();
return;
}
- var tint = item.tint === false ? 'none' : ((draft.colors && draft.colors[slot]) || draft.tint);
- var colors = item.tint === false ? null : draft.colors;
+ var colorable = item.colorable !== false;
+ var tint = colorable ? ((draft.colors && draft.colors[slot]) || draft.tint) : 'none';
+ var colors = colorable ? draft.colors : null;
if (!RK.drawCharTileCanvas || !RK.drawCharTileCanvas(ctx, item.frame, tint, 2, 2, 48, 48, colors, slot)) {
RK.loadCharAtlas(draw);
ctx.fillStyle = '#666'; ctx.font = '11px monospace'; ctx.textAlign = 'center'; ctx.fillText('...', 26, 30);
@@ -416,6 +417,7 @@
function modeStatus(mode, unlocked) {
if (!mode || !mode.premium) return 'Included';
+ if (unlocked && RK.renderModeTestUnlocks && RK.renderModeTestUnlocks()) return 'Test';
return unlocked ? 'Unlocked' : 'Locked';
}
@@ -479,6 +481,10 @@
}
}
+ function equipmentColorable(item) {
+ return !!(item && item.frame != null && item.colorable !== false);
+ }
+
function renderEditor() {
editSection.innerHTML = '';
if (!isCharDraft()) return;
@@ -493,6 +499,7 @@
(RK.CHAR_EQUIPMENT_SLOTS || []).forEach(function (slot) {
var catalog = RK.CHAR_EQUIPMENT[slot] || {};
var opts = row(slot.charAt(0).toUpperCase() + slot.slice(1), editSection);
+ var selectedItem = catalog[draft.equipment[slot]];
for (var id in catalog) {
(function (item) {
opts.appendChild(optionButton(item.label, draft.equipment[slot] === item.id, function () {
@@ -502,7 +509,9 @@
}, optionTileCanvas(slot, item, draft, null)));
})(catalog[id]);
}
- renderColorRow(slot.charAt(0).toUpperCase() + slot.slice(1) + ' color', slot, 'tint', editSection);
+ if (equipmentColorable(selectedItem)) {
+ renderColorRow(slot.charAt(0).toUpperCase() + slot.slice(1) + ' color', slot, 'tint', editSection);
+ }
});
}
diff --git a/html/js/render/charSprites.js b/html/js/render/charSprites.js
index 6729276..c3e695c 100644
--- a/html/js/render/charSprites.js
+++ b/html/js/render/charSprites.js
@@ -33,6 +33,21 @@
'192,117,28': 2,
'198,101,39': 3
};
+ var GEAR_SOURCE = {
+ '114,88,57': 0,
+ '149,92,24': 1,
+ '163,84,34': 1,
+ '176,128,82': 1,
+ '177,92,35': 2,
+ '192,117,28': 2,
+ '198,101,39': 3,
+ '200,164,128': 2,
+ '135,135,135': 0,
+ '202,202,202': 2,
+ '204,204,204': 2,
+ '232,232,232': 3,
+ '247,247,247': 3
+ };
RK.CHAR_TINTS = {
none: { id: 'none', label: 'Natural', color: null, ramp: null },
@@ -80,9 +95,9 @@
},
head: {
none: { id: 'none', label: 'None', frame: null },
- hood: { id: 'hood', label: 'Hood', frame: 24, tint: false },
- helm: { id: 'helm', label: 'Helm', frame: 454, tint: false },
- horns: { id: 'horns', label: 'Horns', frame: 354, tint: false },
+ hood: { id: 'hood', label: 'Hood', frame: 24 },
+ helm: { id: 'helm', label: 'Helm', frame: 454 },
+ horns: { id: 'horns', label: 'Horns', frame: 354 },
cap: { id: 'cap', label: 'Cap', frame: 462 }
},
shield: {
@@ -90,14 +105,14 @@
round: { id: 'round', label: 'Round', frame: 254 },
kite: { id: 'kite', label: 'Kite', frame: 304 },
tower: { id: 'tower', label: 'Tower', frame: 202 },
- buckler:{ id: 'buckler', label: 'Buckler', frame: 411, tint: false }
+ buckler:{ id: 'buckler', label: 'Buckler', frame: 411 }
},
weapon: {
none: { id: 'none', label: 'None', frame: null },
staff: { id: 'staff', label: 'Staff', frame: 154 },
- sword: { id: 'sword', label: 'Sword', frame: 369, tint: false },
- axe: { id: 'axe', label: 'Axe', frame: 375, tint: false },
- bow: { id: 'bow', label: 'Bow', frame: 107, tint: false }
+ sword: { id: 'sword', label: 'Sword', frame: 369 },
+ axe: { id: 'axe', label: 'Axe', frame: 375 },
+ bow: { id: 'bow', label: 'Bow', frame: 107 }
}
};
RK.CHAR_EQUIPMENT_SLOTS = EQUIP_SLOTS.slice();
@@ -234,7 +249,8 @@
var skin = RK.CHAR_SKIN_TONES[colors.skin];
var hair = RK.CHAR_HAIR_COLORS[colors.hair];
var slotTint = RK.CHAR_TINTS[slot === 'base' ? colors.base : (colors[slot] || tintId)];
- var needsSlotTint = slotTint && slotTint.ramp && (slot !== 'base' || colors.base !== 'none');
+ var slotRamp = slotTint && slotTint.ramp ? slotTint.ramp.map(hexToRgb) : null;
+ var needsSlotTint = !!slotRamp && (slot !== 'base' || colors.base !== 'none');
var needsBodyPalette = slot === 'base' && ((skin && colors.skin !== 'natural') || (hair && colors.hair !== 'copper') || needsSlotTint);
if (!a || (!needsBodyPalette && !needsSlotTint)) return null;
var key = idx + '|' + slot + '|' + JSON.stringify(colors);
@@ -250,8 +266,10 @@
if (slot === 'base') {
applyRamp(img, SKIN_SOURCE, skin && colors.skin !== 'natural' ? skin.ramp.map(hexToRgb) : null);
applyRamp(img, HAIR_SOURCE, hair && colors.hair !== 'copper' ? hair.ramp.map(hexToRgb) : null);
+ } else {
+ applyRamp(img, GEAR_SOURCE, slotRamp);
}
- applyRamp(img, TINT_SOURCE, slotTint && slotTint.ramp ? slotTint.ramp.map(hexToRgb) : null);
+ applyRamp(img, TINT_SOURCE, slotRamp);
ctx.putImageData(img, 0, 0);
tintedTiles[key] = cv;
return cv;
@@ -295,7 +313,8 @@
EQUIP_SLOTS.forEach(function (slot) {
var item = RK.CHAR_EQUIPMENT[slot][eq[slot]];
if (!item || item.frame == null) return;
- out.push({ slot: slot, id: item.id, frame: item.frame, tint: item.tint === false ? 'none' : tint, colorable: item.tint !== false });
+ var colorable = item.colorable !== false;
+ out.push({ slot: slot, id: item.id, frame: item.frame, tint: colorable ? tint : 'none', colorable: colorable });
});
return out;
};
diff --git a/html/js/render/renderModes.js b/html/js/render/renderModes.js
index a1f5470..384f2a1 100644
--- a/html/js/render/renderModes.js
+++ b/html/js/render/renderModes.js
@@ -16,6 +16,10 @@
];
RK.entitlements = RK.entitlements || { premium: false, level: 'free', packs: {} };
+ // Temporary production QA switch while Iso/3D/Fancy are being proven out. Flip this back to
+ // false when render modes return behind credits/pack entitlements.
+ RK.RENDER_MODE_TEST_UNLOCKS = true;
+ RK.renderModeTestUnlocks = function () { return RK.RENDER_MODE_TEST_UNLOCKS === true; };
RK.modeMeta = function (id) {
for (var i = 0; i < RK.RENDER_MODES.length; i++) {
@@ -27,6 +31,7 @@
RK.canUseMode = function (id) {
var m = RK.modeMeta(id);
if (!m) return false;
+ if (RK.renderModeTestUnlocks && RK.renderModeTestUnlocks()) return true;
if (m.pack && RK.canUsePack) return RK.canUsePack(m.pack);
return !m.premium || !!RK.entitlements.premium;
};
diff --git a/test/renderPackResolver.test.js b/test/renderPackResolver.test.js
index 1645354..f9b8947 100644
--- a/test/renderPackResolver.test.js
+++ b/test/renderPackResolver.test.js
@@ -53,6 +53,7 @@ function loadRenderKit() {
fs.readFileSync(path.join(__dirname, '../html/js/render/renderModes.js'), 'utf8'),
context
);
+ RK.RENDER_MODE_TEST_UNLOCKS = false;
return RK;
}
@@ -98,6 +99,23 @@ describe('render pack visual resolver', () => {
expect(RK.createRenderer('iso', {}, {}).name).toBe('iso');
});
+ test('temporary render test unlocks bypass mode gates only for render packs', () => {
+ const RK = loadRenderKit();
+ RK.setEntitlementSnapshot({ totalCreditsPurchased: 0, premium: false, packs: {} });
+
+ expect(RK.canUseMode('iso')).toBe(false);
+ expect(RK.canUsePack('iso-dungeon')).toBe(false);
+
+ RK.RENDER_MODE_TEST_UNLOCKS = true;
+ expect(RK.canUseMode('ascii')).toBe(true);
+ expect(RK.canUseMode('fancy')).toBe(true);
+ expect(RK.canUseMode('iso')).toBe(true);
+ expect(RK.canUseMode('3d')).toBe(true);
+ expect(RK.canUsePack('iso-dungeon')).toBe(true);
+ expect(RK.canUsePack('kenney-3d-characters')).toBe(true);
+ expect(RK.canUsePack('generated-skins')).toBe(false);
+ });
+
test('render pack code stays out of payment and account APIs', () => {
const files = [
'../html/js/render/assetPacks.js',