// Render mode registry, factory, and entitlements for the render kit (RK). (function (root) { 'use strict'; var RK = root.RK = root.RK || {}; RK.localRenderersEnabled = function () { return !!(root.WOWNGEON_RUNTIME && root.WOWNGEON_RUNTIME.rendererLocalEnabled === true); }; RK.externalRenderersEnabled = function () { return !!(root.WOWNGEON_RUNTIME && root.WOWNGEON_RUNTIME.rendererCdnEnabled === true); }; RK.isModeRuntimeAvailable = function (id) { if (!RK.modeMeta || !RK.modeMeta(id)) return false; if (id === '3d') { return !!(RK.THREE && RK.THREE.THREE) || RK.localRenderersEnabled() || RK.externalRenderersEnabled(); } return true; }; // 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, 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: {} }; // Per-session QA bypass for premium render-mode gating (Fancy/Iso/3D). Default OFF. A tester // opts IN for their own browser only via ?unlock=1 (persisted); nobody else is affected: // ?unlock=1 -> on (sticky) ?unlock=0 -> off RK.RENDER_MODE_TEST_UNLOCKS = false; try { var q = (root.location && root.location.search) || ''; var m = /[?&]unlock=([01])/.exec(q); if (m) { try { root.localStorage.setItem('rk_unlock_all', m[1]); } catch (_) {} } RK.RENDER_MODE_TEST_UNLOCKS = (root.localStorage && root.localStorage.getItem('rk_unlock_all') === '1'); } catch (_) { /* no location/localStorage */ } RK.renderModeTestUnlocks = function () { return RK.RENDER_MODE_TEST_UNLOCKS === true; }; RK.modeMeta = function (id) { for (var i = 0; i < RK.RENDER_MODES.length; i++) { if (RK.RENDER_MODES[i].id === id) return RK.RENDER_MODES[i]; } return null; }; RK.canUseMode = function (id) { var m = RK.modeMeta(id); if (!m) return false; if (!RK.isModeRuntimeAvailable(id)) 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 !!RK.entitlements.premium; }; RK.modeAvailability = function (id) { var runtimeAvailable = RK.isModeRuntimeAvailable(id); var usable = runtimeAvailable && RK.canUseMode(id); return { usable: usable, runtimeAvailable: runtimeAvailable, reason: !runtimeAvailable ? 'runtime_unavailable' : (usable ? null : 'entitlement_locked') }; }; RK.createRenderer = function (mode, host, opts) { if (!RK.canUseMode(mode)) return new RK.TileRenderer(host, opts); if (mode === 'ascii' && RK.AsciiRenderer) return new RK.AsciiRenderer(host, opts); if (mode === 'iso' && RK.IsoRenderer) return new RK.IsoRenderer(host, opts); if (mode === '3d' && RK.ThreeRenderer && RK.THREE) { try { return new RK.ThreeRenderer(host, opts); } catch (e) { if (root.console) console.warn('3D renderer failed; using tiled:', e && e.message); return new RK.TileRenderer(host, opts); } } if (mode === 'fancy') { try { if (!RK.FancyRenderer) throw new Error('fancy renderer not loaded'); return new RK.FancyRenderer(host, opts); } catch (e) { if (root.console) console.warn('Fancy renderer unavailable; using tiled:', e.message); return new RK.TileRenderer(host, opts); } } return new RK.TileRenderer(host, opts); // default / fallback }; // Lazy-load PixiJS (+ @pixi/unsafe-eval for the strict CSP) only when Fancy is first needed, // so ASCII/Tiled users never download the WebGL library. var pixiLoading = false, pixiCbs = []; RK.pixiReady = function () { return typeof PIXI !== 'undefined'; }; RK.ensurePixi = function (cb) { if (RK.pixiReady()) { cb(true); return; } if (!RK.externalRenderersEnabled()) { cb(false); return; } pixiCbs.push(cb); if (pixiLoading) return; pixiLoading = true; var inject = function (src, onload, onerror) { var el = document.createElement('script'); el.src = src; el.onload = onload; el.onerror = onerror; document.head.appendChild(el); }; var done = function (ok) { var cbs = pixiCbs; pixiCbs = []; cbs.forEach(function (f) { f(ok); }); }; inject('https://cdn.jsdelivr.net/npm/pixi.js@7.4.2/dist/pixi.min.js', function () { // @pixi/unsafe-eval patches the shader compiler on load (needed under the CSP). inject('https://cdn.jsdelivr.net/npm/@pixi/unsafe-eval@7.4.2/dist/unsafe-eval.min.js', function () { done(true); }, function () { done(false); }); }, function () { done(false); }); }; var threeLoading = false, threeCbs = []; RK.threeReady = function () { return !!(RK.THREE && RK.THREE.THREE); }; RK.threeModuleSources = function () { if (RK.localRenderersEnabled()) { var version = String(root.WOWNGEON_RUNTIME.rendererLocalVersion || ''); if (!/^\d+\.\d+\.\d+$/.test(version)) return null; var base = '/vendor/three/' + version; return { library: base + '/three.module.min.js', loader: base + '/addons/loaders/GLTFLoader.js', skeleton: base + '/addons/utils/SkeletonUtils.js' }; } if (!RK.externalRenderersEnabled()) return null; return { library: 'https://cdn.jsdelivr.net/npm/three@0.160.0/+esm', loader: 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js/+esm', skeleton: 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/utils/SkeletonUtils.js/+esm' }; }; RK.ensureThree = function (cb) { if (RK.threeReady()) { cb(true); return; } var sources = RK.threeModuleSources(); if (!sources) { cb(false); return; } threeCbs.push(cb); if (threeLoading) return; threeLoading = true; Promise.all([ import(sources.library), import(sources.loader), import(sources.skeleton) ]).then(function (mods) { threeLoading = false; RK.THREE = { THREE: mods[0], GLTFLoader: mods[1].GLTFLoader, SkeletonUtils: mods[2] }; var cbs = threeCbs; threeCbs = []; cbs.forEach(function (f) { f(true); }); }).catch(function (e) { threeLoading = false; if (root.console) console.warn('Three renderer unavailable:', e && e.message); var cbs = threeCbs; threeCbs = []; cbs.forEach(function (f) { f(false); }); }); }; // Create a renderer, loading Pixi on demand for Fancy; falls back to Tiled if Pixi fails. RK.createRendererAsync = function (mode, host, opts, cb) { if (!RK.canUseMode(mode)) { cb(new RK.TileRenderer(host, opts)); return; } if (mode === 'fancy') { RK.ensurePixi(function (ok) { if (!ok) { cb(new RK.TileRenderer(host, opts)); return; } try { cb(new RK.FancyRenderer(host, opts)); } catch (e) { if (root.console) console.warn('Fancy renderer failed; using tiled:', e.message); cb(new RK.TileRenderer(host, opts)); } }); return; } if (mode === '3d') { RK.ensureThree(function (ok) { if (!ok) { cb(new RK.TileRenderer(host, opts)); return; } try { cb(new RK.ThreeRenderer(host, opts)); } catch (e) { if (root.console) console.warn('3D renderer failed; using tiled:', e.message); cb(new RK.TileRenderer(host, opts)); } }); return; } cb(RK.createRenderer(mode, host, opts)); }; RK.loadMode = function (def) { try { var m = localStorage.getItem('rk_mode'); if (RK.modeMeta(m) && RK.canUseMode(m)) return m; } catch (_) { /* ignore */ } return def || 'tiles'; }; RK.saveMode = function (mode) { try { localStorage.setItem('rk_mode', mode); } catch (_) { /* ignore */ } }; })(window);