From c89c01e5f12d55ac8d3f5ffcba0e7f6dfd800fef Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 6 Jul 2026 04:38:28 -0400 Subject: [PATCH] improve gear tinting and unlock render tests --- html/js/render/assetPacks.js | 1 + html/js/render/charCustomize.js | 15 ++++++++++--- html/js/render/charSprites.js | 39 ++++++++++++++++++++++++--------- html/js/render/renderModes.js | 5 +++++ test/renderPackResolver.test.js | 18 +++++++++++++++ 5 files changed, 65 insertions(+), 13 deletions(-) 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',