From 3d9b5420eeca2cf2f806185a63a70ab3638f7253 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 11 Jul 2026 13:57:47 -0400 Subject: [PATCH] feat(packs): Medieval Town iso pack (2nd iso environment) + pack picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proves the packs-as-products loop end to end: - Copied Kenney Isometric Medieval Town wood tiles (floor/floor2/wall/window/ door/banner) into assets/kenney/iso-medieval/. - Registered 'iso-medieval' as a 2nd iso RENDER pack (its own tile geometry — Medieval tiles are shorter than the dungeon set; reuses the dungeon characters). Strong on architecture; no medieval furniture at this scale, so furniture kinds fall back to floor for now (shines most in the dungeon). - Catalog product: DEFAULT_CATALOG + migration 026, iso tier 2 / 15 credits. - PACK PICKER in the tavern: when a projection has >1 unlocked pack, a selector appears to switch live (setActivePack -> re-render). Uses ?unlock=1 to preview. So: switch Iso ★, and the pack picker lets you flip iso-dungeon <-> Medieval Town live. Adding the next pack = assets + registerPack + a catalog row. Suite 58/419 green. --- html/assets/kenney/iso-medieval/banner.png | Bin 0 -> 765 bytes html/assets/kenney/iso-medieval/door.png | Bin 0 -> 1004 bytes html/assets/kenney/iso-medieval/floor.png | Bin 0 -> 3904 bytes html/assets/kenney/iso-medieval/floor2.png | Bin 0 -> 3222 bytes html/assets/kenney/iso-medieval/wall.png | Bin 0 -> 2613 bytes html/assets/kenney/iso-medieval/window.png | Bin 0 -> 3168 bytes html/js/render/assetPacks.js | 23 ++++++++++++ html/tavern.html | 40 +++++++++++++++++++++ src/migrations/026_iso_medieval_pack.sql | 8 +++++ src/multiplayer/entitlements.js | 4 +++ 10 files changed, 75 insertions(+) create mode 100644 html/assets/kenney/iso-medieval/banner.png create mode 100644 html/assets/kenney/iso-medieval/door.png create mode 100644 html/assets/kenney/iso-medieval/floor.png create mode 100644 html/assets/kenney/iso-medieval/floor2.png create mode 100644 html/assets/kenney/iso-medieval/wall.png create mode 100644 html/assets/kenney/iso-medieval/window.png create mode 100644 src/migrations/026_iso_medieval_pack.sql diff --git a/html/assets/kenney/iso-medieval/banner.png b/html/assets/kenney/iso-medieval/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..594919c29a6e44b2cb8ccd42ae4ab87645fa7d7e GIT binary patch literal 765 zcmeAS@N?(olHy`uVBq!ia0vp^mw@;S2Q!eoxv3)>NU@|l`Z_W&Z0zU$lgJ9>=Lh(N zxSn2H-_}rFmzq}O7nB|sxN}MCnd*XFP1P&9T9?o6I=!wQsPN8`lFD?SyDd3)rxxB> zR&sku$-S1GS&h-V=VZq_+1y=LdS_|LovoF(w^rVrT5zv5=gzjuJKL(leQa(68QZJU zENt(Uq~9w^yH}i+qPTS%&_te+Aiv=K4)y!@*YB@)aM;hViT!W|0|S$yr;B4q#jUq@ zP6stR2sm6^@z8RgDJQ22v+d9S`BPX}l9%4$IJD*awKoxSU!~pHXz(h^p^1f4NX27< zf+KDQZ}6mLomxSYrj@Q#3JTc9>gCa?d-~9=M=4R+3RkVuZ=WjFEye!SYgB)nqo#bUjGlV^4Bo_X=~VxZKPz1x4!{XFMNZD$wvg#AmNobE8&`NKcU zu~oP0y@J1||qAV{zcUrFb`b7;}ZkXq8k1P{?BKxxT{=_mR zEsyqgwyAf!GZba{^0#Kp?N$rwtr7}!v*J+ksP~R~r95-?QmaXZ+OCO(x7)?LYs%O1bWQ&vTl}mdiVz)bU{W{$=@+$LZ%}w@$R0X?01J`%;bPULWa| zd>HCb!foH=Loeni1yy`B=~MdexpC&IjUJafk14sjNo$(i@|fIpO!4R|kIP-h6nu50 z8bzdXmz;DxJh{gzXic99^Th*8E@UX5buy88xoy=mMX68UXO?fYx)g&uM({)y9;v_a am)Jc;_DCGRU7-(5L=2v;elF{r5}E)=g+15+ literal 0 HcmV?d00001 diff --git a/html/assets/kenney/iso-medieval/door.png b/html/assets/kenney/iso-medieval/door.png new file mode 100644 index 0000000000000000000000000000000000000000..90fb775da3cd78d4d3357c6aa8fe2537db38f99f GIT binary patch literal 1004 zcmeAS@N?(olHy`uVBq!ia0vp^mw@;S2Q!eoxv3)>NU@|l`Z_W&Z0zU$lgJ9>Zwc@T zaeZ`R(y`6`yXO?FUD&^FX4TFGwTq^9K)r4b?yc`YIxS-I#cBW9^@tlP+(nd$6zN(#G1$+iNdwsjkZjSUD;E_=2>< zOGt=`o|b%f@Hl z>-D2Dww(Iprt4k%^SqmNXl?2Fx1r(D#f2Lc9Gh4;g;YEykjnT{KY3k<+9Z$gZF}Qx zzn=TnLiz1BI}c&jTT2T=?&bekd2`9h6C2B2bJ8EL_VHEgy%Hnx^mlXDueyrozi0WX ztDTKg5A}A`k~goLw{qnYi?c}^ckHaJh;AyG<>y@``u^#)_lHux8ZNP^|K_3Zt8%sN z%EXsTmb{*&(Y4E9?j)1d{41p=Uwu45D2QEc^?5~AmA`7Mj~`Z4z3MZ~bIFdz5JTHl zHiqpb2_B}wOur;qo=td^7WBoGdGAj_4@KAQoG%O+Sr0C~x#Z9n&(e4Q=4(iBE4t2q zZyfmg8E@N{r7E#pimt6EN`f+f#ZF+-T$1ZqrPdz4taj|F)|`<^zLBdi~l zm36P7Y~?1OWK9*{v(iGDE1yfq3wd5Pv|Av&zN+WGr=sZTEx*)SrVn;4 z&#$ZhuG4PVvOQ>>K+w*hbuvM_g7V}9m!vF>*`6ijFzbB$3FWi;^TK_MJVd9+1YO+M z^ySN)ilDYrp6P4rOlO2!PWrZV$>wSGQWu<+#ZO(?RP3z0@>}J@%HR7x$ncPnP>4ug dQcnMv9v@p`FLU=&12DNWc)I$ztaD0e0sw~bxOM;l literal 0 HcmV?d00001 diff --git a/html/assets/kenney/iso-medieval/floor.png b/html/assets/kenney/iso-medieval/floor.png new file mode 100644 index 0000000000000000000000000000000000000000..485359493de1c496f377cda4fd3e2c5538127f3b GIT binary patch literal 3904 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 literal 0 HcmV?d00001 diff --git a/html/assets/kenney/iso-medieval/floor2.png b/html/assets/kenney/iso-medieval/floor2.png new file mode 100644 index 0000000000000000000000000000000000000000..9980a0e608644f91e7f792b7e4f83e810650a658 GIT binary patch literal 3222 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 literal 0 HcmV?d00001 diff --git a/html/js/render/assetPacks.js b/html/js/render/assetPacks.js index 1b75a4b..635155d 100644 --- a/html/js/render/assetPacks.js +++ b/html/js/render/assetPacks.js @@ -294,6 +294,28 @@ } }; + // Second iso pack — Kenney Isometric Medieval Town (a coherent, DIFFERENT environment). Strong on + // architecture (floor/walls/door/window/banner); no medieval furniture at this scale, so furniture + // kinds fall back to floor for now. Reuses the dungeon-pack characters. Its tiles are shorter + // (210x244) than the dungeon set, so its own tile geometry. + RK.isoMedievalAssets = { + pack: 'iso-medieval', + avatar: 'char-villager', + tile: { w: 84, h: 42, imageW: 84, imageH: 98 }, + tiles: { + floor: 'assets/kenney/iso-medieval/floor.png', + floor2: 'assets/kenney/iso-medieval/floor2.png', + wall: 'assets/kenney/iso-medieval/wall.png', + window: 'assets/kenney/iso-medieval/window.png', + door: 'assets/kenney/iso-medieval/door.png', + banner: 'assets/kenney/iso-medieval/banner.png', + fallback: 'assets/kenney/iso-medieval/floor.png' + }, + character: RK.isoAssets.character, + directions: RK.isoAssets.directions, + characters: RK.isoAssets.characters + }; + RK.threeAssets = { pack: 'kenney-3d-characters', models: { @@ -319,6 +341,7 @@ if (RK.registerPack) { RK.registerPack({ id: 'roguelike-interior', label: 'Roguelike Interior', projection: 'topdown', kind: 'tiles', assets: RK.THEMES['roguelike-interior'] }); RK.registerPack({ id: 'iso-dungeon', label: 'Isometric Dungeon', projection: 'iso', kind: 'tiles', assets: RK.isoAssets }); + RK.registerPack({ id: 'iso-medieval', label: 'Medieval Town', projection: 'iso', kind: 'tiles', assets: RK.isoMedievalAssets }); RK.registerPack({ id: 'kenney-3d-characters', label: 'Animated 3D', projection: '3d', kind: 'skin', assets: RK.threeAssets }); } // Active-pack resolvers the renderers read (fall back to the single default if the registry is absent). diff --git a/html/tavern.html b/html/tavern.html index 80acccb..0fa4765 100644 --- a/html/tavern.html +++ b/html/tavern.html @@ -49,6 +49,10 @@ .modebtn { background: #0a0c0f; color: var(--text); border: 1px solid var(--line); font-weight: 500; } .modebtn.active { background: var(--accent); color: #08130a; border-color: var(--accent); } .modebtn.locked { opacity: 0.55; } + #packbar { display: flex; gap: 6px; margin-bottom: 8px; align-items: center; } + #packbar .packlabel { color: var(--muted); font-size: 12px; margin-right: 2px; } + .packbtn { background: #0a0c0f; color: var(--text); border: 1px solid var(--line); font-size: 12px; padding: 3px 10px; border-radius: 5px; cursor: pointer; } + .packbtn.active { background: var(--accent); color: #08130a; border-color: var(--accent); } #chatPanel { margin-top: 12px; } #chatLog { height: 160px; overflow-y: auto; border: 1px solid var(--line); border-radius: 4px; padding: 8px; background: #0a0c0f; font-size: 13px; line-height: 1.45; } @@ -72,6 +76,7 @@ +

Move with arrow keys or WASD. ★ modes are premium (credit unlock).

@@ -131,6 +136,7 @@ var joinBtn = document.getElementById('joinBtn'); var stageEl = document.getElementById('stage'); var modebar = document.getElementById('modebar'); + var packbar = document.getElementById('packbar'); var chatLog = document.getElementById('chatLog'); var chatForm = document.getElementById('chatForm'); var chatInput = document.getElementById('chatInput'); @@ -219,6 +225,7 @@ currentRenderer = r; draw(); if (RK.attachZoom) { RK.attachZoom(stageEl); RK.applyZoom(stageEl); } // wheel/pinch zoom, all modes + buildPackbar(); // show pack choices for this mode's projection var active = r.name || mode; setStatus(active === mode ? 'Render mode: ' + active @@ -237,6 +244,39 @@ modebar.appendChild(b); }); updateModeButtons(); + buildPackbar(); + } + + // The render projection a mode belongs to (topdown themes / iso packs / 3d skins). + function projForMode(mode) { + if (mode === 'iso') return 'iso'; + if (mode === '3d') return '3d'; + return 'topdown'; + } + + // Pack picker: when the current projection has more than one UNLOCKED pack, show a selector + // to switch between them. This is the "packs are products" engine made visible. + function buildPackbar() { + if (!packbar || !RK.unlockedPacks) return; + packbar.innerHTML = ''; + var proj = projForMode(currentMode || 'tiles'); + var packs = RK.unlockedPacks(proj); + if (packs.length < 2) { packbar.classList.add('hidden'); return; } + packbar.classList.remove('hidden'); + var activeId = RK.activePackId(proj); + var lbl = document.createElement('span'); + lbl.className = 'packlabel'; + lbl.textContent = 'Pack:'; + packbar.appendChild(lbl); + packs.forEach(function (p) { + var b = document.createElement('button'); + b.className = 'packbtn' + (p.id === activeId ? ' active' : ''); + b.textContent = p.label; + b.addEventListener('click', function () { + if (RK.setActivePack(p.id)) { buildPackbar(); setMode(currentMode); } + }); + packbar.appendChild(b); + }); } function updateModeButtons() { Array.prototype.forEach.call(modebar.children, function (b) { diff --git a/src/migrations/026_iso_medieval_pack.sql b/src/migrations/026_iso_medieval_pack.sql new file mode 100644 index 0000000..7175925 --- /dev/null +++ b/src/migrations/026_iso_medieval_pack.sql @@ -0,0 +1,8 @@ +-- 026: add the Medieval Town isometric pack as a catalog product. +-- A second iso environment (interchangeable across tavern + dungeon), sold at iso-tier: unlock by +-- 15 lifetime credits OR premium (tier 2). Idempotent. +INSERT INTO cosmetic_catalog (pack_id, label, kind, projection, tier, unlock_min_credits, grant_only, active, sort_order, metadata) +VALUES ('iso-medieval', 'Medieval Town (Isometric)', 'render-pack', 'iso', 2, 15, false, true, 40, '{}'::jsonb) +ON CONFLICT (pack_id) DO UPDATE + SET label = EXCLUDED.label, projection = EXCLUDED.projection, tier = EXCLUDED.tier, + unlock_min_credits = EXCLUDED.unlock_min_credits, active = true; diff --git a/src/multiplayer/entitlements.js b/src/multiplayer/entitlements.js index f356f73..6e01897 100644 --- a/src/multiplayer/entitlements.js +++ b/src/multiplayer/entitlements.js @@ -25,6 +25,10 @@ const DEFAULT_CATALOG = Object.freeze({ id: 'iso-dungeon', label: 'Kenney Isometric Dungeon', kind: 'render-pack', projection: 'iso', tier: 2, unlockMinCredits: 10, grantOnly: false, premium: true }), + 'iso-medieval': Object.freeze({ + id: 'iso-medieval', label: 'Medieval Town (Isometric)', kind: 'render-pack', + projection: 'iso', tier: 2, unlockMinCredits: 15, grantOnly: false, premium: true + }), 'kenney-3d-characters': Object.freeze({ id: 'kenney-3d-characters', label: 'Kenney Animated 3D Characters', kind: 'render-pack', projection: '3d', tier: 3, unlockMinCredits: 25, grantOnly: false, premium: true