feat(packs): Medieval Town iso pack (2nd iso environment) + pack picker

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.
This commit is contained in:
jwinterm
2026-07-11 13:57:47 -04:00
parent 11b7fb1272
commit 3d9b5420ee
10 changed files with 75 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1004 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+23
View File
@@ -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).
+40
View File
@@ -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 @@
</div>
<div id="modebar" class="hidden"></div>
<div id="packbar" class="hidden"></div>
<div id="stage" class="hidden"></div>
<p id="status" class="muted"></p>
<p class="muted">Move with arrow keys or WASD. ★ modes are premium (credit unlock).</p>
@@ -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) {
+8
View File
@@ -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;
+4
View File
@@ -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