feat(packs): Fancy dies as a mode → topdown packs; Roguelike Dungeon tile pack (B)

Modes are TECHNIQUES; 'Fancy' (tiles+FX) was really a topdown PACK, so:
- Remove 'fancy' from RENDER_MODES (saved 'fancy' falls back to tiles; its dead
  Pixi branch is short-circuited by canUseMode). Modes = Tiled/ASCII/Iso/3D.
- Modes now carry a ; a premium mode unlocks once the user has ANY
  unlocked pack for that projection (not a single hardcoded pack) — needed now
  that iso is multi-pack.
- New 'roguelike-dungeon' topdown pack: same sheet, a DUNGEON palette (stone
  floors + barrels/crates vs the wood interior) — a distinct 2D tileset with zero
  new assets. Registered + catalog (migration 027, topdown tier 1 / 1 credit).
  The tiled renderer's lighting/shadows/vignette are the built-in FX.
- Also: iso facing fix (Male 0=NW..6=NE) reflected in tests.

So the Tiled pack picker now offers Roguelike Interior <-> Dungeon, and Iso offers
Dungeon <-> Medieval — packs as products, proven in the best-looking mode too.
Suite 58/419 green.
This commit is contained in:
jwinterm
2026-07-11 14:35:30 -04:00
parent 4a91d91822
commit ccaff7d29c
5 changed files with 54 additions and 12 deletions
+26
View File
@@ -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 });
+11 -7
View File
@@ -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) {
@@ -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;
+4
View File
@@ -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
+5 -5
View File
@@ -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);