diff --git a/html/js/render/assetPacks.js b/html/js/render/assetPacks.js index 6c27c62..45f2d82 100644 --- a/html/js/render/assetPacks.js +++ b/html/js/render/assetPacks.js @@ -161,6 +161,31 @@ treasure: [42, 15], // 897 — item torch: [50, 10] // 620 — rubble / prop } + }, + // Second topdown pack — same sheet, a DUNGEON palette (stone floors, barrels/crates instead + // of the wood-interior furniture). A distinct look with zero new assets, and it's the + // "interchangeable dungeon" style (the tiled renderer's lighting/shadows are the built-in FX). + 'roguelike-dungeon': { + id: 'roguelike-dungeon', + label: 'Roguelike Dungeon', + tileset: { url: 'assets/kenney/roguelikeSheet.png', tile: 16, spacing: 1 }, + fx: true, + tiles: { + floor: [6, 2], // 120 — stone floor + floor2: [13, 17], // 982 — plank accent + wall: [13, 12], // 697 — masonry + window: [42, 2], // 156 — window + bar: [23, 0], // barrels (a dungeon has no bar) + keg: [23, 0], + shelf: [25, 0], // crate + table: [25, 0], // crate as a table + chair: [23, 0], // barrel as a stool + barrel: [23, 0], + crate: [25, 0], + rug: [6, 2], // stone (no rug) + door: [37, 9], + entrance: [37, 9], exit: [37, 9], treasure: [42, 15], torch: [50, 10] + } } }; RK.activeThemeId = 'roguelike-interior'; @@ -342,6 +367,7 @@ // assets + a catalog row. Backward-compatible: one pack per projection resolves to these. if (RK.registerPack) { RK.registerPack({ id: 'roguelike-interior', label: 'Roguelike Interior', projection: 'topdown', kind: 'tiles', assets: RK.THEMES['roguelike-interior'] }); + RK.registerPack({ id: 'roguelike-dungeon', label: 'Roguelike Dungeon', projection: 'topdown', kind: 'tiles', assets: RK.THEMES['roguelike-dungeon'] }); 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 }); diff --git a/html/js/render/renderModes.js b/html/js/render/renderModes.js index fdc34c2..1bdff4c 100644 --- a/html/js/render/renderModes.js +++ b/html/js/render/renderModes.js @@ -6,13 +6,14 @@ // Render tiers. `premium` marks modes intended to be unlocked with credits — a cosmetic // entitlement gated by Operator Policy, the same pattern as avatar unlocks. NOTE: rendering // is client-side, so this is a soft/honour gate for cosmetics, not a hard security boundary. + // Modes are TECHNIQUES (how a scene is drawn). Packs are the SETS/styles within a projection + // (chosen via the pack picker) — so "Fancy" (tiles + FX) is a topdown PACK, not a mode. A + // premium mode unlocks once the user has ANY unlocked pack for its projection. RK.RENDER_MODES = [ - { id: 'tiles', label: 'Tiled', premium: false }, - { id: 'ascii', label: 'ASCII', premium: false }, - { id: 'fancy', label: 'Fancy', premium: true, pack: 'generated-skins' }, - { id: 'iso', label: 'Iso', premium: true, pack: 'iso-dungeon' }, - { id: '3d', label: '3D', premium: true, pack: 'kenney-3d-characters' } - // Planned: 'fancy-ascii' — shader-lit, animated glyphs. + { id: 'tiles', label: 'Tiled', premium: false, projection: 'topdown' }, + { id: 'ascii', label: 'ASCII', premium: false, projection: 'topdown' }, + { id: 'iso', label: 'Iso', premium: true, projection: 'iso' }, + { id: '3d', label: '3D', premium: true, projection: '3d' } ]; RK.entitlements = RK.entitlements || { premium: false, level: 'free', packs: {} }; @@ -39,8 +40,11 @@ var m = RK.modeMeta(id); if (!m) return false; if (RK.renderModeTestUnlocks && RK.renderModeTestUnlocks()) return true; + if (!m.premium) return true; + // A premium mode is available once the user has ANY unlocked pack for its projection. + if (m.projection && RK.unlockedPacks) return RK.unlockedPacks(m.projection).length > 0; if (m.pack && RK.canUsePack) return RK.canUsePack(m.pack); - return !m.premium || !!RK.entitlements.premium; + return !!RK.entitlements.premium; }; RK.createRenderer = function (mode, host, opts) { diff --git a/src/migrations/027_roguelike_dungeon_pack.sql b/src/migrations/027_roguelike_dungeon_pack.sql new file mode 100644 index 0000000..63b4bc9 --- /dev/null +++ b/src/migrations/027_roguelike_dungeon_pack.sql @@ -0,0 +1,8 @@ +-- 027: add the Roguelike Dungeon topdown tile pack as a catalog product. +-- Same sheet, a dungeon palette (a distinct 2D tileset). Cheap topdown-variety tier: 1 credit / +-- supporter (tier 1). Idempotent. +INSERT INTO cosmetic_catalog (pack_id, label, kind, projection, tier, unlock_min_credits, grant_only, active, sort_order, metadata) +VALUES ('roguelike-dungeon', 'Roguelike Dungeon (Tiles)', 'render-pack', 'topdown', 1, 1, false, true, 15, '{}'::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 6e01897..656afff 100644 --- a/src/multiplayer/entitlements.js +++ b/src/multiplayer/entitlements.js @@ -21,6 +21,10 @@ const DEFAULT_CATALOG = Object.freeze({ id: 'generated-skins', label: 'Premium Generated Skins', kind: 'render-pack', projection: 'topdown', tier: 1, unlockMinCredits: 1, grantOnly: false, premium: true }), + 'roguelike-dungeon': Object.freeze({ + id: 'roguelike-dungeon', label: 'Roguelike Dungeon (Tiles)', kind: 'render-pack', + projection: 'topdown', tier: 1, unlockMinCredits: 1, grantOnly: false, premium: true + }), 'iso-dungeon': Object.freeze({ id: 'iso-dungeon', label: 'Kenney Isometric Dungeon', kind: 'render-pack', projection: 'iso', tier: 2, unlockMinCredits: 10, grantOnly: false, premium: true diff --git a/test/renderPackResolver.test.js b/test/renderPackResolver.test.js index ad16209..002123e 100644 --- a/test/renderPackResolver.test.js +++ b/test/renderPackResolver.test.js @@ -105,10 +105,11 @@ describe('render pack visual resolver', () => { expect(RK.isoAssets.characters['char-merchant']).toBeUndefined(); expect(RK.isoAssets.characters['char-wizard']).toBeUndefined(); expect(RK.isoAssets.characters['char-goblin']).toBeUndefined(); - expect(RK.isoAssets.directions.down.idle).toContain('Male_3_Idle0.png'); - expect(RK.isoAssets.directions.up.idle).toContain('Male_7_Idle0.png'); + // 45° renders: 0=NW,2=SW,4=SE,6=NE. Facing follows visual movement (up->NE, down->SW, etc.). + expect(RK.isoAssets.directions.down.idle).toContain('Male_2_Idle0.png'); + expect(RK.isoAssets.directions.up.idle).toContain('Male_6_Idle0.png'); expect(RK.isoAssets.directions.left.idle).toContain('Male_0_Idle0.png'); - expect(RK.isoAssets.directions.right.idle).toContain('Male_5_Idle0.png'); + expect(RK.isoAssets.directions.right.idle).toContain('Male_4_Idle0.png'); }); test('projection visuals expose the saved base tint as a render color', () => { @@ -142,8 +143,7 @@ describe('render pack visual resolver', () => { // With test unlocks on, ALL premium modes and packs are available for QA RK.RENDER_MODE_TEST_UNLOCKS = true; - expect(RK.canUseMode('fancy')).toBe(true); - expect(RK.canUseMode('iso')).toBe(true); + expect(RK.canUseMode('iso')).toBe(true); // 'fancy' is no longer a mode — it's a topdown pack expect(RK.canUseMode('3d')).toBe(true); expect(RK.canUsePack('iso-dungeon')).toBe(true); expect(RK.canUsePack('kenney-3d-characters')).toBe(true);