mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-19 07:08:33 +02:00
feat(render): pack registry — multi-pack, interchangeable render environments
The engine behind 'add more 2D tilesets / iso packs / 3D skins as products'. A pack is a self-contained render environment for a projection (topdown/iso/3d); because scenes are kind-based, the SAME pack renders both town and dungeon (interchangeable), and each maps to a catalog id (monetizable, entitlement-gated). - packRegistry.js: registerPack / packsForProjection / unlockedPacks (canUsePack) / activePackId (saved-if-unlocked else first unlocked) / setActivePack / activePackAssets. Per-projection active-pack selection, persisted. In-memory store fallback so it's testable under Node. - assetPacks: registers the 3 built-ins (roguelike-interior topdown, iso-dungeon, kenney-3d-characters) + activeIsoAssets()/activeThreeAssets() resolvers; RK.theme() now defaults to the active topdown pack. - isoRenderer + avatarVisuals (iso/3d character lookup) read the ACTIVE pack. - packRegistry loaded before assetPacks in tavern + game. Backward-compatible: one pack per projection resolves to today's defaults — nothing changes visually. Adding a pack now = registerPack(...) + assets + a catalog row. Tests: +5 (register/list, unlock filter, active/saved/fallback, refuse locked). Suite 58 suites / 419 tests green.
This commit is contained in:
@@ -79,6 +79,7 @@
|
||||
<script src="js/render/atlas.js"></script>
|
||||
<script src="js/render/skins.js"></script>
|
||||
<script src="js/render/charSprites.js"></script>
|
||||
<script src="js/render/packRegistry.js"></script>
|
||||
<script src="js/render/assetPacks.js"></script>
|
||||
<script src="js/render/renderModes.js"></script>
|
||||
<script src="js/render/charCustomize.js"></script>
|
||||
|
||||
@@ -165,7 +165,14 @@
|
||||
};
|
||||
RK.activeThemeId = 'roguelike-interior';
|
||||
|
||||
RK.theme = function (id) { return RK.THEMES[id || RK.activeThemeId] || null; };
|
||||
RK.theme = function (id) {
|
||||
// With no explicit id, use the active topdown pack (registry-selected + entitlement-gated).
|
||||
if (!id && RK.activePackId) {
|
||||
var ap = RK.activePackId('topdown');
|
||||
if (ap && RK.THEMES[ap]) return RK.THEMES[ap];
|
||||
}
|
||||
return RK.THEMES[id || RK.activeThemeId] || null;
|
||||
};
|
||||
|
||||
// Effective tile map = theme defaults merged with localStorage overrides from the picker.
|
||||
RK.tileMap = function (id) {
|
||||
@@ -305,6 +312,19 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Register the built-in RENDER packs in the pack registry (packRegistry.js) so iso/3D become
|
||||
// multi-pack like topdown themes already are. Each maps to its catalog id; the active pack per
|
||||
// projection is entitlement-gated + user-selectable. Adding a pack later = registerPack(...) +
|
||||
// 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: 'iso-dungeon', label: 'Isometric Dungeon', projection: 'iso', kind: 'tiles', assets: RK.isoAssets });
|
||||
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).
|
||||
RK.activeIsoAssets = function () { return (RK.activePackAssets && RK.activePackAssets('iso')) || RK.isoAssets; };
|
||||
RK.activeThreeAssets = function () { return (RK.activePackAssets && RK.activePackAssets('3d')) || RK.threeAssets; };
|
||||
|
||||
RK.packForProjection = function (projection) {
|
||||
for (var id in RK.PACKS) {
|
||||
if (RK.PACKS[id].projection === projection) return RK.PACKS[id];
|
||||
|
||||
@@ -73,13 +73,13 @@
|
||||
}
|
||||
|
||||
function isoCharacterFor(appearance) {
|
||||
var assets = RK.isoAssets || {};
|
||||
var assets = (RK.activeIsoAssets && RK.activeIsoAssets()) || RK.isoAssets || {};
|
||||
var chars = assets.characters || {};
|
||||
return chars[appearance.avatar] || chars.fallback || assets.character || null;
|
||||
}
|
||||
|
||||
function modelFor(appearance) {
|
||||
var assets = RK.threeAssets || {};
|
||||
var assets = (RK.activeThreeAssets && RK.activeThreeAssets()) || RK.threeAssets || {};
|
||||
var models = assets.models || {};
|
||||
return models[appearance.avatar] || models.fallback || null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
opts = opts || {};
|
||||
this.name = 'iso';
|
||||
this.host = host;
|
||||
this.assets = (root.RK && RK.isoAssets) || {};
|
||||
this.assets = (root.RK && ((RK.activeIsoAssets && RK.activeIsoAssets()) || RK.isoAssets)) || {};
|
||||
this.tileW = opts.tileW || (this.assets.tile && this.assets.tile.w) || 84;
|
||||
this.tileH = opts.tileH || (this.assets.tile && this.assets.tile.h) || 42;
|
||||
this.imageW = opts.imageW || (this.assets.tile && this.assets.tile.imageW) || 92;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Pack registry — the engine behind "add more 2D tilesets / iso packs / 3D skins as products".
|
||||
//
|
||||
// A PACK is a rich, self-contained render environment for a projection (topdown | iso | 3d). Because
|
||||
// every scene (tavern OR dungeon) is kind-based (floor/wall/table/torch/monster…), the SAME pack
|
||||
// renders both — packs are interchangeable across town and dungeon. Each pack maps 1:1 to a catalog
|
||||
// entry (id/tier/threshold), so it's a monetizable product gated by entitlements.
|
||||
//
|
||||
// This module makes packs MULTI (like RK.THEMES already is for topdown) and adds active-pack
|
||||
// selection per projection. Renderers ask for the active pack's assets; a picker lets an unlocked
|
||||
// user switch. Adding a pack then = register(def) + drop assets + a catalog row.
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var RK = root.RK = root.RK || {};
|
||||
|
||||
// id -> { id, label, projection: 'topdown'|'iso'|'3d', kind, assets }
|
||||
// `assets` is the render-kit payload for that projection (theme tiles / iso set / 3d models).
|
||||
RK.PACK_REGISTRY = RK.PACK_REGISTRY || {};
|
||||
|
||||
// Storage: localStorage in the browser, an in-memory map under Node/tests.
|
||||
var mem = {};
|
||||
function store() {
|
||||
try { if (root.localStorage) return root.localStorage; } catch (_) {}
|
||||
return { getItem: function (k) { return k in mem ? mem[k] : null; }, setItem: function (k, v) { mem[k] = String(v); } };
|
||||
}
|
||||
function key(projection) { return 'rk_pack_' + projection; }
|
||||
|
||||
RK.registerPack = function (def) {
|
||||
if (!def || !def.id || !def.projection) return null;
|
||||
RK.PACK_REGISTRY[def.id] = def;
|
||||
return def;
|
||||
};
|
||||
RK.getPackDef = function (id) { return (id && RK.PACK_REGISTRY[id]) || null; };
|
||||
|
||||
RK.packsForProjection = function (projection) {
|
||||
var out = [];
|
||||
for (var id in RK.PACK_REGISTRY) {
|
||||
if (RK.PACK_REGISTRY[id].projection === projection) out.push(RK.PACK_REGISTRY[id]);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Packs the user can actually use for a projection (free ones + entitlement-unlocked ones).
|
||||
RK.unlockedPacks = function (projection) {
|
||||
return RK.packsForProjection(projection).filter(function (p) {
|
||||
return !RK.canUsePack || RK.canUsePack(p.id);
|
||||
});
|
||||
};
|
||||
|
||||
// The user's chosen pack if it's registered for this projection AND unlocked; otherwise the
|
||||
// first unlocked pack; otherwise null.
|
||||
RK.activePackId = function (projection) {
|
||||
var saved = store().getItem(key(projection));
|
||||
var def = saved && RK.PACK_REGISTRY[saved];
|
||||
if (def && def.projection === projection && (!RK.canUsePack || RK.canUsePack(saved))) return saved;
|
||||
var unlocked = RK.unlockedPacks(projection);
|
||||
return unlocked.length ? unlocked[0].id : null;
|
||||
};
|
||||
|
||||
RK.activePack = function (projection) { return RK.getPackDef(RK.activePackId(projection)); };
|
||||
RK.activePackAssets = function (projection) {
|
||||
var def = RK.activePack(projection);
|
||||
return def ? def.assets : null;
|
||||
};
|
||||
|
||||
// Select a pack (persisted per projection). Refused if unregistered or locked. Returns success.
|
||||
RK.setActivePack = function (id) {
|
||||
var def = RK.getPackDef(id);
|
||||
if (!def) return false;
|
||||
if (RK.canUsePack && !RK.canUsePack(id)) return false;
|
||||
store().setItem(key(def.projection), id);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = RK;
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
@@ -15,6 +15,7 @@
|
||||
<script src="js/render/charSprites.js"></script>
|
||||
<script src="js/render/catSprites.js"></script>
|
||||
<script src="js/render/zoomControl.js"></script>
|
||||
<script src="js/render/packRegistry.js"></script>
|
||||
<script src="js/render/assetPacks.js"></script>
|
||||
<script src="js/render/avatarVisuals.js"></script>
|
||||
<script src="js/render/designedRoom.js"></script>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Pack registry — the engine for multi-pack, interchangeable render environments (products).
|
||||
*/
|
||||
describe('pack registry', () => {
|
||||
let RK;
|
||||
beforeEach(() => { jest.resetModules(); RK = require('../html/js/render/packRegistry'); });
|
||||
|
||||
function seed() {
|
||||
RK.registerPack({ id: 'topA', label: 'A', projection: 'topdown', assets: { a: 1 } });
|
||||
RK.registerPack({ id: 'isoFree', label: 'IsoFree', projection: 'iso', assets: { i: 1 } });
|
||||
RK.registerPack({ id: 'isoPrem', label: 'IsoPrem', projection: 'iso', assets: { i: 2 } });
|
||||
}
|
||||
|
||||
test('registers packs and lists them by projection', () => {
|
||||
seed();
|
||||
expect(RK.packsForProjection('iso').map(p => p.id).sort()).toEqual(['isoFree', 'isoPrem']);
|
||||
expect(RK.getPackDef('topA').assets).toEqual({ a: 1 });
|
||||
expect(RK.registerPack({ id: 'x' })).toBeNull(); // no projection -> rejected
|
||||
});
|
||||
|
||||
test('unlockedPacks filters by canUsePack', () => {
|
||||
seed();
|
||||
RK.canUsePack = (id) => id !== 'isoPrem';
|
||||
expect(RK.unlockedPacks('iso').map(p => p.id)).toEqual(['isoFree']);
|
||||
});
|
||||
|
||||
test('active pack = saved-if-unlocked, else first unlocked; assets resolve', () => {
|
||||
seed();
|
||||
RK.canUsePack = () => true;
|
||||
expect(RK.activePackId('iso')).toBe('isoFree'); // default = first unlocked
|
||||
expect(RK.setActivePack('isoPrem')).toBe(true);
|
||||
expect(RK.activePackId('iso')).toBe('isoPrem'); // saved choice sticks
|
||||
expect(RK.activePackAssets('iso')).toEqual({ i: 2 });
|
||||
});
|
||||
|
||||
test('a saved pack that later locks falls back; setActivePack refuses a locked pack', () => {
|
||||
seed();
|
||||
RK.canUsePack = () => true;
|
||||
RK.setActivePack('isoPrem');
|
||||
RK.canUsePack = (id) => id !== 'isoPrem'; // access lost
|
||||
expect(RK.activePackId('iso')).toBe('isoFree'); // graceful fallback
|
||||
expect(RK.setActivePack('isoPrem')).toBe(false); // refused
|
||||
});
|
||||
|
||||
test('a projection with no packs resolves to null', () => {
|
||||
seed();
|
||||
expect(RK.activePackId('3d')).toBeNull();
|
||||
expect(RK.activePackAssets('3d')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user