diff --git a/html/assets/kenney/iso-medieval/banner.png b/html/assets/kenney/iso-medieval/banner.png new file mode 100644 index 0000000..594919c Binary files /dev/null and b/html/assets/kenney/iso-medieval/banner.png differ diff --git a/html/assets/kenney/iso-medieval/door.png b/html/assets/kenney/iso-medieval/door.png new file mode 100644 index 0000000..90fb775 Binary files /dev/null and b/html/assets/kenney/iso-medieval/door.png differ diff --git a/html/assets/kenney/iso-medieval/floor.png b/html/assets/kenney/iso-medieval/floor.png new file mode 100644 index 0000000..4853594 Binary files /dev/null and b/html/assets/kenney/iso-medieval/floor.png differ diff --git a/html/assets/kenney/iso-medieval/floor2.png b/html/assets/kenney/iso-medieval/floor2.png new file mode 100644 index 0000000..9980a0e Binary files /dev/null and b/html/assets/kenney/iso-medieval/floor2.png differ diff --git a/html/assets/kenney/iso-medieval/wall.png b/html/assets/kenney/iso-medieval/wall.png new file mode 100644 index 0000000..5c79918 Binary files /dev/null and b/html/assets/kenney/iso-medieval/wall.png differ diff --git a/html/assets/kenney/iso-medieval/window.png b/html/assets/kenney/iso-medieval/window.png new file mode 100644 index 0000000..b3a4fc1 Binary files /dev/null and b/html/assets/kenney/iso-medieval/window.png differ 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