From 83020f6c088e8f9f35fb0a586cb39d18cc7fd9d7 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 6 Jul 2026 04:26:00 -0400 Subject: [PATCH] polish character customizer render choices --- html/index.html | 1 + html/js/core/singlePlayerAvatar.js | 7 +- html/js/render/avatarVisuals.js | 15 +++- html/js/render/charCustomize.js | 119 ++++++++++++++++++++++++----- html/tavern.html | 13 +++- test/singlePlayerAvatar.test.js | 9 +++ 6 files changed, 137 insertions(+), 27 deletions(-) diff --git a/html/index.html b/html/index.html index fd9047d..b2fe43d 100644 --- a/html/index.html +++ b/html/index.html @@ -75,6 +75,7 @@ + diff --git a/html/js/core/singlePlayerAvatar.js b/html/js/core/singlePlayerAvatar.js index 855a128..ee7d4e8 100644 --- a/html/js/core/singlePlayerAvatar.js +++ b/html/js/core/singlePlayerAvatar.js @@ -198,7 +198,12 @@ y: tileY * cell, w: cols * cell, h: cell - }, { onReady: requestRedraw }); + }, { + context: 'legend', + scale: 0.78, + offsetY: Math.max(1, Math.round(cell * 0.08)), + onReady: requestRedraw + }); } }; diff --git a/html/js/render/avatarVisuals.js b/html/js/render/avatarVisuals.js index 2f958b1..e14cd85 100644 --- a/html/js/render/avatarVisuals.js +++ b/html/js/render/avatarVisuals.js @@ -156,10 +156,17 @@ opts = opts || {}; visual = visual && visual.appearance ? visual : resolve(visual, opts); if (!ctx || !rect || !ensureCanvasReady(visual, opts.onReady)) return false; - var size = Math.min(rect.w || rect.size || 32, rect.h || rect.size || 32); - var x = rect.x + Math.max(0, ((rect.w || size) - size) / 2); - var y = rect.y + Math.max(0, ((rect.h || size) - size) / 2); - ctx.clearRect(rect.x, rect.y, rect.w || size, rect.h || size); + var boxW = rect.w || rect.size || 32; + var boxH = rect.h || rect.size || 32; + var natural = Math.min(boxW, boxH); + var legend = visual.context === 'legend' || opts.context === 'legend'; + var scale = opts.scale || (legend ? 0.78 : 1); + var size = opts.size || Math.max(1, Math.round(natural * scale)); + var offsetX = opts.offsetX || 0; + var offsetY = opts.offsetY != null ? opts.offsetY : (legend ? Math.round(natural * 0.08) : 0); + var x = rect.x + Math.max(0, (boxW - size) / 2) + offsetX; + var y = rect.y + Math.max(0, (boxH - size) / 2) + offsetY; + ctx.clearRect(rect.x, rect.y, boxW, boxH); if (visual.kind === 'char') return drawCharIcon(ctx, visual, x, y, size); if (visual.kind === 'skin') return drawSkinIcon(ctx, visual, x, y, size); return false; diff --git a/html/js/render/charCustomize.js b/html/js/render/charCustomize.js index 1995f0c..4edcd13 100644 --- a/html/js/render/charCustomize.js +++ b/html/js/render/charCustomize.js @@ -196,39 +196,46 @@ style.textContent = [ '.rk-customize{position:fixed;inset:0;z-index:9999;background:rgba(4,6,8,.94);color:#d7dbe0;font-family:ui-monospace,monospace;overflow:hidden;}', '.rk-customize *{box-sizing:border-box;letter-spacing:0;}', - '.rk-customize-shell{height:100%;max-width:1180px;margin:0 auto;padding:16px;display:grid;grid-template-rows:auto minmax(0,1fr);gap:14px;}', - '.rk-customize-top{display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid #202832;padding-bottom:12px;}', - '.rk-customize-title{font-size:16px;font-weight:700;color:#f0f3f6;overflow-wrap:anywhere;}', + '.rk-customize-shell{height:100%;max-width:1080px;margin:0 auto;padding:12px;display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;}', + '.rk-customize-top{display:flex;align-items:center;justify-content:space-between;gap:10px;border-bottom:1px solid #202832;padding-bottom:10px;}', + '.rk-customize-title{font-size:15px;font-weight:700;color:#f0f3f6;overflow-wrap:anywhere;}', '.rk-customize-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}', - '.rk-customize-body{min-height:0;display:grid;grid-template-columns:190px minmax(0,1fr);gap:16px;}', - '.rk-customize-preview{align-self:start;position:sticky;top:0;border:1px solid #26303b;background:#0b0f14;border-radius:8px;padding:12px;}', + '.rk-customize-body{min-height:0;display:grid;grid-template-columns:168px minmax(0,1fr);gap:12px;}', + '.rk-customize-preview{align-self:start;position:sticky;top:0;border:1px solid #26303b;background:#0b0f14;border-radius:6px;padding:10px;}', '.rk-customize-preview canvas{display:block;width:100%;height:auto;image-rendering:pixelated;background:#090c10;border:1px solid #2a313a;border-radius:6px;}', - '.rk-customize-name{font-size:13px;font-weight:700;color:#f0f3f6;margin-top:10px;overflow-wrap:anywhere;}', + '.rk-customize-name{font-size:12px;font-weight:700;color:#f0f3f6;margin-top:8px;overflow-wrap:anywhere;}', '.rk-customize-meta{font-size:11px;color:#8b949e;margin-top:4px;overflow-wrap:anywhere;}', - '.rk-customize-content{min-width:0;overflow:auto;padding-right:4px;}', - '.rk-customize-section{margin-bottom:18px;}', + '.rk-customize-content{min-width:0;overflow:auto;padding-right:4px;scrollbar-gutter:stable;}', + '.rk-customize-section{margin-bottom:12px;}', '.rk-customize-section h3{margin:0 0 8px;font-size:12px;line-height:1.2;color:#9aa4b2;font-weight:700;text-transform:uppercase;}', - '.rk-avatar-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:8px;}', - '.rk-option-card{min-width:0;min-height:112px;border:2px solid #2a313a;border-radius:8px;background:#11161c;color:#c7ccd4;cursor:pointer;padding:7px;font:inherit;font-size:11px;text-align:center;}', + '.rk-mode-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:6px;}', + '.rk-mode-card{min-width:0;border:2px solid #2a313a;border-radius:6px;background:#11161c;color:#c7ccd4;cursor:pointer;padding:7px 8px;font:inherit;text-align:left;display:grid;gap:2px;}', + '.rk-mode-card:hover{border-color:#566274;background:#151b22;}', + '.rk-mode-card.is-selected{border-color:#3fb950;box-shadow:0 0 0 1px rgba(63,185,80,.3) inset;}', + '.rk-mode-card.is-locked{opacity:.5;cursor:not-allowed;}', + '.rk-mode-label{font-size:12px;font-weight:700;color:#dce2ea;overflow-wrap:anywhere;}', + '.rk-mode-status{font-size:10px;color:#8b949e;overflow-wrap:anywhere;}', + '.rk-avatar-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(78px,1fr));gap:6px;}', + '.rk-option-card{min-width:0;min-height:92px;border:2px solid #2a313a;border-radius:6px;background:#11161c;color:#c7ccd4;cursor:pointer;padding:6px;font:inherit;font-size:10px;text-align:center;}', '.rk-option-card:hover{border-color:#566274;background:#151b22;}', '.rk-option-card.is-selected{border-color:#3fb950;box-shadow:0 0 0 1px rgba(63,185,80,.3) inset;}', '.rk-option-card.is-locked{opacity:.48;cursor:not-allowed;}', - '.rk-option-card canvas{max-width:72px;width:100%;height:auto;}', + '.rk-option-card canvas{max-width:60px;width:100%;height:auto;}', '.rk-option-label{margin-top:5px;line-height:1.2;overflow-wrap:anywhere;color:#d0d6de;}', - '.rk-customize-row{display:grid;grid-template-columns:86px minmax(0,1fr);gap:10px;align-items:start;margin-bottom:10px;}', - '.rk-row-label{font-size:12px;color:#9aa4b2;padding-top:9px;overflow-wrap:anywhere;}', - '.rk-row-options{display:flex;gap:7px;flex-wrap:wrap;min-width:0;}', - '.rk-swatch,.rk-equip-option{border:2px solid #2a313a;border-radius:8px;background:#11161c;color:#c7ccd4;cursor:pointer;font:inherit;font-size:11px;}', - '.rk-swatch{width:70px;min-height:58px;padding:6px;}', - '.rk-equip-option{width:78px;min-height:76px;padding:5px;}', + '.rk-customize-row{display:grid;grid-template-columns:72px minmax(0,1fr);gap:8px;align-items:start;margin-bottom:8px;}', + '.rk-row-label{font-size:11px;color:#9aa4b2;padding-top:8px;overflow-wrap:anywhere;}', + '.rk-row-options{display:flex;gap:6px;flex-wrap:wrap;min-width:0;}', + '.rk-swatch,.rk-equip-option{border:2px solid #2a313a;border-radius:6px;background:#11161c;color:#c7ccd4;cursor:pointer;font:inherit;font-size:10px;}', + '.rk-swatch{width:58px;min-height:50px;padding:5px;}', + '.rk-equip-option{width:64px;min-height:68px;padding:4px;}', '.rk-swatch:hover,.rk-equip-option:hover{border-color:#566274;background:#151b22;}', '.rk-swatch.is-selected,.rk-equip-option.is-selected{border-color:#3fb950;box-shadow:0 0 0 1px rgba(63,185,80,.3) inset;}', - '.rk-swatch-dot{display:block;width:24px;height:24px;margin:0 auto 5px;border-radius:50%;border:1px solid rgba(255,255,255,.35);}', + '.rk-swatch-dot{display:block;width:20px;height:20px;margin:0 auto 4px;border-radius:50%;border:1px solid rgba(255,255,255,.35);}', '.rk-none-dot{background:linear-gradient(135deg,#171c22 0,#171c22 45%,#9aa4b2 47%,#9aa4b2 53%,#171c22 55%,#171c22 100%);}', - '.rk-button{font:inherit;border-radius:8px;padding:8px 14px;cursor:pointer;font-weight:700;}', + '.rk-button{font:inherit;border-radius:6px;padding:7px 12px;cursor:pointer;font-weight:700;}', '.rk-save{border:0;background:#3fb950;color:#071109;}', '.rk-cancel{border:1px solid #2a313a;background:#10151b;color:#d7dbe0;}', - '@media (max-width:760px){.rk-customize-shell{padding:10px;}.rk-customize-top{align-items:flex-start;}.rk-customize-body{grid-template-columns:1fr;overflow:auto;}.rk-customize-content{overflow:visible;padding-right:0;}.rk-customize-preview{position:relative;display:grid;grid-template-columns:96px minmax(0,1fr);gap:10px;align-items:center;}.rk-customize-preview canvas{width:96px;}.rk-customize-row{grid-template-columns:1fr;gap:6px;}.rk-row-label{padding-top:0;}.rk-avatar-grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr));}.rk-option-card{min-height:104px;}.rk-customize-actions{width:100%;justify-content:flex-start;}}' + '@media (max-width:760px){.rk-customize-shell{padding:10px;}.rk-customize-top{align-items:flex-start;}.rk-customize-body{grid-template-columns:1fr;overflow:auto;}.rk-customize-content{overflow:visible;padding-right:0;}.rk-customize-preview{position:relative;display:grid;grid-template-columns:88px minmax(0,1fr);gap:10px;align-items:center;}.rk-customize-preview canvas{width:88px;}.rk-customize-row{grid-template-columns:1fr;gap:5px;}.rk-row-label{padding-top:0;}.rk-avatar-grid{grid-template-columns:repeat(auto-fill,minmax(76px,1fr));}.rk-option-card{min-height:90px;}.rk-customize-actions{width:100%;justify-content:flex-start;}}' ].join(''); document.head.appendChild(style); } @@ -311,6 +318,21 @@ content.className = 'rk-customize-content'; body.appendChild(content); + var selectedMode = RK.loadMode ? RK.loadMode('tiles') : 'tiles'; + var modeCards = {}; + var renderModeGrid = null; + if (RK.RENDER_MODES && RK.RENDER_MODES.length) { + var renderSection = document.createElement('section'); + renderSection.className = 'rk-customize-section'; + content.appendChild(renderSection); + var renderTitle = document.createElement('h3'); + renderTitle.textContent = 'Render'; + renderSection.appendChild(renderTitle); + renderModeGrid = document.createElement('div'); + renderModeGrid.className = 'rk-mode-grid'; + renderSection.appendChild(renderModeGrid); + } + var avatarSection = document.createElement('section'); avatarSection.className = 'rk-customize-section'; content.appendChild(avatarSection); @@ -381,6 +403,62 @@ return b; } + function packLabel(id) { + var pack = RK.pack && RK.pack(id); + return (pack && pack.label) || 'premium pack'; + } + + function modeLockedTitle(mode) { + var label = mode && mode.label ? mode.label : 'Mode'; + if (mode && mode.pack) return label + ' is locked. Buy any credits or unlock ' + packLabel(mode.pack) + '.'; + return label + ' is locked. Buy any credits to unlock premium modes.'; + } + + function modeStatus(mode, unlocked) { + if (!mode || !mode.premium) return 'Included'; + return unlocked ? 'Unlocked' : 'Locked'; + } + + function markRenderMode() { + for (var id in modeCards) { + if (id === selectedMode) modeCards[id].classList.add('is-selected'); + else modeCards[id].classList.remove('is-selected'); + } + } + + function selectRenderMode(mode) { + if (RK.canUseMode && !RK.canUseMode(mode.id)) return; + selectedMode = mode.id; + if (RK.saveMode) RK.saveMode(mode.id); + markRenderMode(); + } + + function buildRenderModes() { + if (!renderModeGrid || !RK.RENDER_MODES) return; + renderModeGrid.innerHTML = ''; + modeCards = {}; + RK.RENDER_MODES.forEach(function (mode) { + var unlocked = !RK.canUseMode || RK.canUseMode(mode.id); + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'rk-mode-card' + (unlocked ? '' : ' is-locked'); + b.setAttribute('aria-disabled', unlocked ? 'false' : 'true'); + b.title = unlocked ? ('Use ' + mode.label) : modeLockedTitle(mode); + var label = document.createElement('span'); + label.className = 'rk-mode-label'; + label.textContent = mode.label + (mode.premium ? ' *' : ''); + b.appendChild(label); + var status = document.createElement('span'); + status.className = 'rk-mode-status'; + status.textContent = modeStatus(mode, unlocked); + b.appendChild(status); + b.onclick = function () { selectRenderMode(mode); }; + renderModeGrid.appendChild(b); + modeCards[mode.id] = b; + }); + markRenderMode(); + } + function setColor(slot, id) { ensureCharDraft(); draft.colors[slot] = id; @@ -513,6 +591,7 @@ cancel.onclick = function () { document.body.removeChild(wrap); }; document.body.appendChild(wrap); + buildRenderModes(); mark(); renderEditor(); redrawAll(); diff --git a/html/tavern.html b/html/tavern.html index 3aa5c14..62b50ad 100644 --- a/html/tavern.html +++ b/html/tavern.html @@ -139,8 +139,17 @@ function draw() { if (currentRenderer && currentScene) currentRenderer.render(currentScene); } + function lockedModeMessage(mode) { + var meta = RK.modeMeta ? RK.modeMeta(mode) : null; + var label = (meta && meta.label) || mode; + if (meta && meta.pack && RK.pack) { + var pack = RK.pack(meta.pack); + return label + ' is locked. Buy any credits or unlock ' + ((pack && pack.label) || 'the pack') + '.'; + } + return label + ' is locked. Buy any credits to unlock premium modes.'; + } function setMode(mode) { - if (!RK.canUseMode(mode)) { setStatus(mode + ' is a premium mode (credit unlock).'); return; } + if (!RK.canUseMode(mode)) { setStatus(lockedModeMessage(mode)); return; } if (currentRenderer) { currentRenderer.destroy(); currentRenderer = null; } currentMode = mode; RK.saveMode(mode); @@ -164,7 +173,7 @@ b.className = 'modebtn'; b.dataset.mode = m.id; b.textContent = m.label + (m.premium ? ' ★' : ''); - if (!RK.canUseMode(m.id)) { b.classList.add('locked'); b.title = 'Premium pack — buy credits to unlock'; } + if (!RK.canUseMode(m.id)) { b.classList.add('locked'); b.title = lockedModeMessage(m.id); } b.addEventListener('click', function () { setMode(m.id); }); modebar.appendChild(b); }); diff --git a/test/singlePlayerAvatar.test.js b/test/singlePlayerAvatar.test.js index d9877a4..d205e59 100644 --- a/test/singlePlayerAvatar.test.js +++ b/test/singlePlayerAvatar.test.js @@ -6,6 +6,8 @@ describe('single-player character identity wiring', () => { const socketHandlers = fs.readFileSync(path.join(__dirname, '../html/js/network/socketHandlers.js'), 'utf8'); const renderEngine = fs.readFileSync(path.join(__dirname, '../html/js/display/renderEngine.js'), 'utf8'); const screenManager = fs.readFileSync(path.join(__dirname, '../html/js/display/screenManager.js'), 'utf8'); + const avatarVisuals = fs.readFileSync(path.join(__dirname, '../html/js/render/avatarVisuals.js'), 'utf8'); + const singlePlayerAvatar = fs.readFileSync(path.join(__dirname, '../html/js/core/singlePlayerAvatar.js'), 'utf8'); test('main page loads the avatar visual resolver before the overlay bridge', () => { const order = [ @@ -13,6 +15,7 @@ describe('single-player character identity wiring', () => { 'js/render/skins.js', 'js/render/charSprites.js', 'js/render/assetPacks.js', + 'js/render/renderModes.js', 'js/render/charCustomize.js', 'js/render/avatarVisuals.js', 'js/core/singlePlayerAvatar.js' @@ -40,4 +43,10 @@ describe('single-player character identity wiring', () => { expect(screenManager).toContain('SinglePlayerAvatar.clearOverlay'); expect(screenManager).toContain('SinglePlayerAvatar.drawLegendIcon'); }); + + test('welcome legend uses compact one-row avatar metrics', () => { + expect(singlePlayerAvatar).toContain("context: 'legend'"); + expect(singlePlayerAvatar).toContain('scale: 0.78'); + expect(avatarVisuals).toContain("visual.context === 'legend'"); + }); });