diff --git a/html/index.html b/html/index.html index f0bc3bf..a5d77d4 100644 --- a/html/index.html +++ b/html/index.html @@ -88,6 +88,9 @@ + diff --git a/html/js/render/renderModes.js b/html/js/render/renderModes.js index 94f08b7..1ccdac5 100644 --- a/html/js/render/renderModes.js +++ b/html/js/render/renderModes.js @@ -2,13 +2,18 @@ (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.externalRenderersEnabled(); + return !!(RK.THREE && RK.THREE.THREE) + || RK.localRenderersEnabled() + || RK.externalRenderersEnabled(); } return true; }; @@ -116,20 +121,46 @@ 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; } - if (!RK.externalRenderersEnabled()) { cb(false); return; } + var sources = RK.threeModuleSources(); + if (!sources) { cb(false); return; } threeCbs.push(cb); if (threeLoading) return; threeLoading = true; Promise.all([ - import('https://cdn.jsdelivr.net/npm/three@0.160.0/+esm'), - import('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/loaders/GLTFLoader.js/+esm') + import(sources.library), + import(sources.loader), + import(sources.skeleton) ]).then(function (mods) { - RK.THREE = { THREE: mods[0], GLTFLoader: mods[1].GLTFLoader }; + 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); }); diff --git a/html/js/render/spGameRenderer.js b/html/js/render/spGameRenderer.js index 7ea0066..7f25358 100644 --- a/html/js/render/spGameRenderer.js +++ b/html/js/render/spGameRenderer.js @@ -16,6 +16,19 @@ function doc() { return root.document; } function gameDisplay() { return doc() && doc().getElementById('game-display'); } + // Keyboard movement is intentionally scoped to #game-display so typing in chat and controls + // can never move the player. UI controls that affect the live renderer call this immediately + // after activation to restore that explicit gameplay focus. + SP.focusGameplay = function () { + var gd = gameDisplay(); + if (!gd || !gd.focus) return false; + gd.setAttribute('tabindex', '-1'); + try { gd.focus({ preventScroll: true }); } + catch (_) { gd.focus(); } + if (root.UI && UI.updateFocusIndicator) UI.updateFocusIndicator(); + return doc().activeElement === gd; + }; + // A dedicated, SELF-SIZING child host so the RK canvas doesn't collide with the ROT canvas and // doesn't depend on the ROT canvas for its dimensions (it's an aspect-ratio viewport the camera // transform is clipped to). @@ -93,7 +106,11 @@ b.type = 'button'; b.textContent = label; b.title = title; b.setAttribute('aria-label', title); b.style.cssText = 'min-width:30px;height:28px;padding:0 7px;background:#151b22;color:#e5e7eb;' + 'border:1px solid #3b4552;border-radius:4px;cursor:pointer;font:inherit;'; - b.addEventListener('click', function (e) { e.stopPropagation(); action(); }); + b.addEventListener('click', function (e) { + e.stopPropagation(); + action(); + SP.focusGameplay(); + }); controls.appendChild(b); } addButton('−', 'Zoom out', function () { SP._setZoom(((SP._zoom != null) ? SP._zoom : SP._defaultZoom()) / 1.18); }); @@ -113,9 +130,9 @@ var host = rkHost(); if (!host || !RK.createRenderer) return null; var mode = SP.mode(); - // 3D needs THREE, lazy-loaded from a CDN. The sync createRenderer() silently returns a Tiled - // renderer when THREE isn't loaded yet ("picked 3D, got original"). Kick off the load and - // re-mount as real 3D once it's ready; meanwhile we render Tiled so the screen isn't blank. + // 3D needs THREE, lazy-loaded from the pinned same-origin vendor route (or the explicitly + // enabled CDN fallback). The sync createRenderer() returns Tiled until it is ready; remount + // as real 3D after loading so the screen never goes blank. if (mode === '3d' && RK.ensureThree && RK.threeReady && !RK.threeReady()) { if (!SP._threePending) { SP._threePending = true; diff --git a/html/js/render/threeRenderer.js b/html/js/render/threeRenderer.js index db62b3d..953bff5 100644 --- a/html/js/render/threeRenderer.js +++ b/html/js/render/threeRenderer.js @@ -18,6 +18,7 @@ this.host = host; this.THREE = RK.THREE.THREE; this.GLTFLoader = RK.THREE.GLTFLoader; + this.SkeletonUtils = RK.THREE.SkeletonUtils; this.models = {}; this.mixers = []; this.entities = {}; @@ -26,6 +27,7 @@ this.panX = 0; this.panY = 0; this._sizeKey = null; + this._destroyed = false; this._init(); } @@ -66,7 +68,11 @@ this.scene.add(this.world); this.clock = new T.Clock(); var self = this; - this._raf = requestAnimationFrame(function tick() { self._animate(); self._raf = requestAnimationFrame(tick); }); + this._raf = requestAnimationFrame(function tick() { + if (self._destroyed || !self.renderer) { self._raf = null; return; } + if (self._animate() === false) { self._raf = null; return; } + self._raf = requestAnimationFrame(tick); + }); }; // Fill the host and frame a fixed "screenful" around the player. The camera FOLLOWS the player @@ -110,12 +116,31 @@ return new T.MeshStandardMaterial({ color: colorNum(hex), roughness: 0.82, metalness: 0.02 }); }; + ThreeRenderer.prototype._disposeObject = function (object) { + if (!object || !object.traverse) return; + object.traverse(function (obj) { + if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose(); + if (!obj.material) return; + var materials = Array.isArray(obj.material) ? obj.material : [obj.material]; + materials.forEach(function (mat) { if (mat && mat.dispose) mat.dispose(); }); + }); + }; + ThreeRenderer.prototype._buildTiles = function (scene) { var T = this.THREE; - this.world.clear(); + // Visibility changes rebuild the floor/walls, but live entities must stay attached to their + // stable layer. Clearing the whole world here made every avatar disappear after the first + // fog-of-war update because this.entities still pointed at detached objects. + if (this.tileLayer) { + this.world.remove(this.tileLayer); + this._disposeObject(this.tileLayer); + } this.tileLayer = new T.Group(); - this.entityLayer = new T.Group(); - this.world.add(this.tileLayer, this.entityLayer); + this.world.add(this.tileLayer); + if (!this.entityLayer) { + this.entityLayer = new T.Group(); + this.world.add(this.entityLayer); + } var cx = (scene.cols - 1) / 2, cz = (scene.rows - 1) / 2; for (var y = 0; y < scene.rows; y++) { for (var x = 0; x < scene.cols; x++) { @@ -146,16 +171,16 @@ ThreeRenderer.prototype._applyModelTint = function (model, visual) { var tint = tintForVisual(visual, null); - if (!tint || !model) return; + if (!model) return; var T = this.THREE; - var color = new T.Color(colorNum(tint)); + var color = tint ? new T.Color(colorNum(tint)) : null; model.traverse(function (obj) { if (!obj.isMesh || !obj.material) return; var materials = Array.isArray(obj.material) ? obj.material : [obj.material]; var next = materials.map(function (mat) { - if (!mat || !mat.color) return mat; + if (!mat) return mat; var copy = mat.clone(); - copy.color.copy(color); + if (color && copy.color) copy.color.copy(color); return copy; }); obj.material = Array.isArray(obj.material) ? next : next[0]; @@ -182,7 +207,7 @@ box.getSize(size); box.getCenter(center); var maxDim = Math.max(size.x, size.y, size.z) || 1; - var scale = 5.0 / maxDim; + var scale = 1.8 / maxDim; model.position.set(-center.x, -box.min.y, -center.z); wrapper.scale.setScalar(scale); wrapper.add(model); @@ -240,10 +265,17 @@ if (visual && visual.allowed !== false && visual.model) { this._loadModel(visual, function (rec) { if (!rec || !rec.gltf || !shell.parent) return; - var model = rec.gltf.scene.clone(true); + var model = self.SkeletonUtils && self.SkeletonUtils.clone + ? self.SkeletonUtils.clone(rec.gltf.scene) + : rec.gltf.scene.clone(true); self._applyModelTint(model, visual); model.rotation.y = Math.PI; var fitted = self._fitModel(model); + if (shell._body) { + shell.remove(shell._body); + self._disposeObject(shell._body); + shell._body = null; + } shell.add(fitted); shell._model = fitted; if (rec.gltf.animations && rec.gltf.animations.length) { @@ -275,7 +307,9 @@ seen[id] = true; var ent = this.entities[id]; if (!ent) { - ent = this.entities[id] = { obj: this._makeEntity(e), x: e.x, y: e.y }; + var obj = this._makeEntity(e); + obj.position.set(e.x - cx, 0, e.y - cz); + ent = this.entities[id] = { obj: obj, x: e.x, y: e.y }; this.entityLayer.add(ent.obj); } ent.e = e; @@ -284,7 +318,14 @@ } for (id in this.entities) { if (!seen[id]) { - this.entityLayer.remove(this.entities[id].obj); + var removed = this.entities[id].obj; + this.entityLayer.remove(removed); + if (removed._mixer) { + removed._mixer.stopAllAction(); + var mixerIndex = this.mixers.indexOf(removed._mixer); + if (mixerIndex >= 0) this.mixers.splice(mixerIndex, 1); + } + this._disposeObject(removed); delete this.entities[id]; } } @@ -348,16 +389,21 @@ this.renderer.render(this.scene, this.camera); } catch (err) { if (root.console) console.warn('3D animate error; stopping 3D loop:', err && err.message); - if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; } - return; + return false; } + return true; }; ThreeRenderer.prototype.destroy = function () { + this._destroyed = true; if (this._raf) cancelAnimationFrame(this._raf); + this._raf = null; + for (var i = 0; i < this.mixers.length; i++) this.mixers[i].stopAllAction(); + this._disposeObject(this.world); if (this.renderer) { if (this.renderer.domElement && this.renderer.domElement.parentNode) this.renderer.domElement.parentNode.removeChild(this.renderer.domElement); this.renderer.dispose(); + if (this.renderer.forceContextLoss) this.renderer.forceContextLoss(); } this.renderer = null; this.entities = {}; diff --git a/html/js/ui/spRenderPicker.js b/html/js/ui/spRenderPicker.js index afc2e8a..b93cd7d 100644 --- a/html/js/ui/spRenderPicker.js +++ b/html/js/ui/spRenderPicker.js @@ -59,7 +59,10 @@ b.setAttribute('aria-disabled', usable ? 'false' : 'true'); b.onclick = function () { if (!usable) return; - if (RK.SPGame.setMode(m.id)) build(); + if (RK.SPGame.setMode(m.id)) { + build(); + if (RK.SPGame.focusGameplay) RK.SPGame.focusGameplay(); + } }; modeRow.appendChild(b); }); @@ -80,6 +83,7 @@ if (RK.setActivePack && RK.setActivePack(p.id)) { if (RK.SPGame.refreshPack) RK.SPGame.refreshPack(); build(); + if (RK.SPGame.focusGameplay) RK.SPGame.focusGameplay(); } }; packRow.appendChild(b); diff --git a/html/match.html b/html/match.html index f29a163..46c2128 100644 --- a/html/match.html +++ b/html/match.html @@ -106,6 +106,9 @@ + diff --git a/html/tavern.html b/html/tavern.html index a019f8a..c3445c2 100644 --- a/html/tavern.html +++ b/html/tavern.html @@ -28,6 +28,9 @@ + @@ -98,13 +109,20 @@ Responsible Play ← Lobby -
- - - +
+
+ Walk in anonymously + No account required. Pick an optional display name and character. +
+
+ + + + +
- -
+

Connecting to the tavern…

+ @@ -116,7 +134,7 @@

-

Move with arrow keys or WASD · drag/pinch to explore · ★ may require credits · ⛔ is unavailable on this server.

+