mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-10 05:54:52 +00:00
skin support, shape mounting unification
This commit is contained in:
parent
14e25beaf8
commit
bc7d30c5c6
98 changed files with 1952 additions and 1744 deletions
|
|
@ -95,6 +95,7 @@ to a React component tree.
|
|||
add regression tests.
|
||||
- To make changes visible to `t2-mapper`, make sure you run `npm run build` and
|
||||
`yalc push` in `t2-demo-parser`.
|
||||
- Example demos are in `t2-demo-parser/data`.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
import{s as e}from"./SettingsProvider-CeFD8Cx1.js";import{a as t,c as n,i as r,l as i,n as a,o,r as s,s as c,t as l}from"./AudioEmitter-CRRhpHuE.js";export{l as AudioEmitter,a as audioBufferCache,s as getCachedAudioBuffer,e as getEffectiveSoundRate,r as getSoundGeneration,t as playOneShotSound,o as resolveAudioProfile,c as stopAllTrackedSounds,n as trackSound,i as untrackSound};
|
||||
import{s as e}from"./SettingsProvider-BeB5OnG9.js";import{a as t,c as n,i as r,l as i,n as a,o,r as s,s as c,t as l}from"./AudioEmitter-Cdm9ofcT.js";export{l as AudioEmitter,a as audioBufferCache,s as getCachedAudioBuffer,e as getEffectiveSoundRate,r as getSoundGeneration,t as playOneShotSound,o as resolveAudioProfile,c as stopAllTrackedSounds,n as trackSound,i as untrackSound};
|
||||
File diff suppressed because one or more lines are too long
1
docs/assets/AudioEmitter-Cdm9ofcT.js
Normal file
1
docs/assets/AudioEmitter-Cdm9ofcT.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{U as i}from"./index-CcOasdr9.js";var a=e(n(),1),o=t(),s={InputForm:`_InputForm_18kom_1`,Input:`_Input_18kom_1`},c=r();function l(){let e=(0,o.c)(8),[t,n]=(0,a.useState)(``),r;e[0]===t?r=e[1]:(r=e=>{e.preventDefault();let r=t.trim();r&&(i.getState().sendCommand(`messageSent`,r),n(``))},e[0]=t,e[1]=r);let l=r,f;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(f=e=>n(e.target.value),e[2]=f):f=e[2];let p;e[3]===t?p=e[4]:(p=(0,c.jsx)(`input`,{className:s.Input,type:`text`,placeholder:`Say something…`,value:t,onChange:f,onKeyDown:d,onKeyUp:u,maxLength:255}),e[3]=t,e[4]=p);let m;return e[5]!==l||e[6]!==p?(m=(0,c.jsx)(`form`,{className:s.InputForm,onSubmit:l,children:p}),e[5]=l,e[6]=p,e[7]=m):m=e[7],m}function u(e){return e.stopPropagation()}function d(e){return e.stopPropagation()}export{l as ChatInput};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{U as i}from"./index-BEehCpzM.js";var a=e(n(),1),o=t(),s={InputForm:`_InputForm_18kom_1`,Input:`_Input_18kom_1`},c=r();function l(){let e=(0,o.c)(8),[t,n]=(0,a.useState)(``),r;e[0]===t?r=e[1]:(r=e=>{e.preventDefault();let r=t.trim();r&&(i.getState().sendCommand(`messageSent`,r),n(``))},e[0]=t,e[1]=r);let l=r,f;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(f=e=>n(e.target.value),e[2]=f):f=e[2];let p;e[3]===t?p=e[4]:(p=(0,c.jsx)(`input`,{className:s.Input,type:`text`,placeholder:`Say something…`,value:t,onChange:f,onKeyDown:d,onKeyUp:u,maxLength:255}),e[3]=t,e[4]=p);let m;return e[5]!==l||e[6]!==p?(m=(0,c.jsx)(`form`,{className:s.InputForm,onSubmit:l,children:p}),e[5]=l,e[6]=p,e[7]=m):m=e[7],m}function u(e){return e.stopPropagation()}function d(e){return e.stopPropagation()}export{l as ChatInput};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{a as n,s as r}from"./SettingsProvider-CeFD8Cx1.js";import{i}from"./engineStore-Mono9lyt.js";import{c as a}from"./three.module-C9W4LJrj.js";import{r as o}from"./index-CcOasdr9.js";import{c as s,i as c,l,p as u,r as d}from"./AudioEmitter-CRRhpHuE.js";var f=e(t(),1);function p(){let{audioLoader:e,audioListener:t}=u(),{audioEnabled:p}=n(),m=i(e=>e.playback.streamSnapshot?.chatMessages),h=i(e=>e.playback.streamSnapshot?.timeSec),g=(0,f.useRef)(new WeakSet),_=(0,f.useRef)(new Map);return(0,f.useEffect)(()=>{if(!p||!e||!t||!m?.length||h==null)return;let n=g.current,i=_.current;for(let u of m)if(!n.has(u)&&(n.add(u),u.soundPath&&!(Math.abs(h-u.timeSec)>2)))try{let n=o(u.soundPath),f=u.soundPitch??1,p=u.sender,m=c();d(n,e,e=>{if(m!==c())return;if(p){let e=i.get(p);if(e){try{e.stop()}catch{}l(e);try{e.disconnect()}catch{}i.delete(p)}}let n=new a(t);n.setBuffer(e),n.setPlaybackRate(r(f)),s(n,f),p&&i.set(p,n),n.play(),n.source.onended=()=>{l(n);try{n.disconnect()}catch{}p&&i.get(p)===n&&i.delete(p)}})}catch{}},[p,e,t,m,h]),null}export{p as ChatSoundPlayer};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{a as n,s as r}from"./SettingsProvider-BeB5OnG9.js";import{i}from"./engineStore-DXFfg1NG.js";import{c as a}from"./three.module-DeDv86YO.js";import{r as o}from"./index-BEehCpzM.js";import{c as s,i as c,l,p as u,r as d}from"./AudioEmitter-Cdm9ofcT.js";var f=e(t(),1);function p(){let{audioLoader:e,audioListener:t}=u(),{audioEnabled:p}=n(),m=i(e=>e.playback.streamSnapshot?.chatMessages),h=i(e=>e.playback.streamSnapshot?.timeSec),g=(0,f.useRef)(new WeakSet),_=(0,f.useRef)(new Map);return(0,f.useEffect)(()=>{if(!p||!e||!t||!m?.length||h==null)return;let n=g.current,i=_.current;for(let u of m)if(!n.has(u)&&(n.add(u),u.soundPath&&!(Math.abs(h-u.timeSec)>2)))try{let n=o(u.soundPath),f=u.soundPitch??1,p=u.sender,m=c();d(n,e,e=>{if(m!==c())return;if(p){let e=i.get(p);if(e){try{e.stop()}catch{}l(e);try{e.disconnect()}catch{}i.delete(p)}}let n=new a(t);n.setBuffer(e),n.setPlaybackRate(r(f)),s(n,f),p&&i.set(p,n),n.play(),n.source.onended=()=>{l(n);try{n.disconnect()}catch{}p&&i.get(p)===n&&i.delete(p)}})}catch{}},[p,e,t,m,h]),null}export{p as ChatSoundPlayer};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{E as i,Ft as a,p as o}from"./three.module-C9W4LJrj.js";var s=t(),c=e(n(),1),l=r(),u=(0,l.jsx)(`lineBasicMaterial`,{color:`#ff0000`,depthTest:!1,depthWrite:!1,fog:!1,transparent:!0});function d({size:e}){return(0,l.jsx)(`lineSegments`,{geometry:(0,c.useMemo)(()=>new i(new o(...e)),[e[0],e[1],e[2]]),renderOrder:9999,children:u})}function f(e){let t=(0,s.c)(4),{radius:n}=e,r=n===void 0?1:n,o;t[0]===r?o=t[1]:(o=new i(new a(r,8,6)),t[0]=r,t[1]=o);let c=o,d;return t[2]===c?d=t[3]:(d=(0,l.jsx)(`lineSegments`,{geometry:c,renderOrder:9999,children:u}),t[2]=c,t[3]=d),d}export{f as n,d as t};
|
||||
1
docs/assets/DebugBounds-DUxS5ppE.js
Normal file
1
docs/assets/DebugBounds-DUxS5ppE.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{n as e,t}from"./jsx-runtime-BpGWiA-R.js";import{E as n,Ft as r,p as i}from"./three.module-DeDv86YO.js";var a=e(),o=t(),s=(0,o.jsx)(`lineBasicMaterial`,{color:`#ff0000`,depthTest:!1,depthWrite:!1,fog:!1,transparent:!0});function c(e){let t=(0,a.c)(6),{size:r}=e,c;t[0]!==r[0]||t[1]!==r[1]||t[2]!==r[2]?(c=new n(new i(r[0],r[1],r[2])),t[0]=r[0],t[1]=r[1],t[2]=r[2],t[3]=c):c=t[3];let l=c,u;return t[4]===l?u=t[5]:(u=(0,o.jsx)(`lineSegments`,{geometry:l,renderOrder:9999,children:s}),t[4]=l,t[5]=u),u}function l(e){let t=(0,a.c)(4),{radius:i}=e,c=i===void 0?1:i,l;t[0]===c?l=t[1]:(l=new n(new r(c,8,6)),t[0]=c,t[1]=l);let u=l,d;return t[2]===u?d=t[3]:(d=(0,o.jsx)(`lineSegments`,{geometry:u,renderOrder:9999,children:s}),t[2]=u,t[3]=d),d}export{l as n,c as t};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as e,t}from"./chunk-DECur_0Z.js";import{n,r,t as i}from"./jsx-runtime-BpGWiA-R.js";import{n as a,r as o}from"./react-three-fiber.esm-CD18QK1u.js";import{t as s}from"./Html-2FQrdEMx.js";var c=t(((e,t)=>{(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):n.Stats=r()})(e,function(){var e=function(){function t(e){return i.appendChild(e.dom),e}function n(e){for(var t=0;t<i.children.length;t++)i.children[t].style.display=t===e?`block`:`none`;r=e}var r=0,i=document.createElement(`div`);i.style.cssText=`position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000`,i.addEventListener(`click`,function(e){e.preventDefault(),n(++r%i.children.length)},!1);var a=(performance||Date).now(),o=a,s=0,c=t(new e.Panel(`FPS`,`#0ff`,`#002`)),l=t(new e.Panel(`MS`,`#0f0`,`#020`));if(self.performance&&self.performance.memory)var u=t(new e.Panel(`MB`,`#f08`,`#201`));return n(0),{REVISION:16,dom:i,addPanel:t,showPanel:n,begin:function(){a=(performance||Date).now()},end:function(){s++;var e=(performance||Date).now();if(l.update(e-a,200),e>o+1e3&&(c.update(1e3*s/(e-o),100),o=e,s=0,u)){var t=performance.memory;u.update(t.usedJSHeapSize/1048576,t.jsHeapSizeLimit/1048576)}return e},update:function(){a=this.end()},domElement:i,setMode:n}};return e.Panel=function(e,t,n){var r=1/0,i=0,a=Math.round,o=a(window.devicePixelRatio||1),s=80*o,c=48*o,l=3*o,u=2*o,d=3*o,f=15*o,p=74*o,m=30*o,h=document.createElement(`canvas`);h.width=s,h.height=c,h.style.cssText=`width:80px;height:48px`;var g=h.getContext(`2d`);return g.font=`bold `+9*o+`px Helvetica,Arial,sans-serif`,g.textBaseline=`top`,g.fillStyle=n,g.fillRect(0,0,s,c),g.fillStyle=t,g.fillText(e,l,u),g.fillRect(d,f,p,m),g.fillStyle=n,g.globalAlpha=.9,g.fillRect(d,f,p,m),{dom:h,update:function(c,_){r=Math.min(r,c),i=Math.max(i,c),g.fillStyle=n,g.globalAlpha=1,g.fillRect(0,0,s,f),g.fillStyle=t,g.fillText(a(c)+` `+e+` (`+a(r)+`-`+a(i)+`)`,l,u),g.drawImage(h,d+o,f,p-o,m,d,f,p-o,m),g.fillRect(d+p-o,f,o,m),g.fillStyle=n,g.globalAlpha=.9,g.fillRect(d+p-o,f,o,a((1-c/_)*m))}}},e})})),l=e(r());function u(e,t){typeof e==`function`?e(t):e!=null&&(e.current=t)}function d(e,t=[],n){let[r,i]=l.useState();return l.useLayoutEffect(()=>{let t=e();return i(t),u(n,t),()=>u(n,null)},t),r}var f=e(c());function p({showPanel:e=0,className:t,parent:n}){let r=d(()=>new f.default,[]);return l.useEffect(()=>{if(r){let i=n&&n.current||document.body;r.showPanel(e),i?.appendChild(r.dom);let s=(t??``).split(` `).filter(e=>e);s.length&&r.dom.classList.add(...s);let c=o(()=>r.begin()),l=a(()=>r.end());return()=>{s.length&&r.dom.classList.remove(...s),i?.removeChild(r.dom),c(),l()}}},[n,r,t,e]),null}var m=n(),h={StatsPanel:`_StatsPanel_10m5i_1`,AxisLabel:`_AxisLabel_10m5i_8`},g=i();function _(){let e=(0,m.c)(10),t=(0,l.useRef)(null),n;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(n=()=>{let e=t.current;e&&e.setColors(`rgb(153, 255, 0)`,`rgb(0, 153, 255)`,`rgb(255, 153, 0)`)},e[0]=n):n=e[0],(0,l.useEffect)(n);let r;e[1]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,g.jsx)(p,{className:h.StatsPanel}),e[1]=r):r=e[1];let i;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(i=[70],e[2]=i):i=e[2];let a;e[3]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,g.jsx)(`axesHelper`,{ref:t,args:i,renderOrder:999,children:(0,g.jsx)(`lineBasicMaterial`,{depthTest:!1,depthWrite:!1,fog:!1,vertexColors:!0})}),e[3]=a):a=e[3];let o;e[4]===Symbol.for(`react.memo_cache_sentinel`)?(o=[80,0,0],e[4]=o):o=e[4];let c;e[5]===Symbol.for(`react.memo_cache_sentinel`)?(c=(0,g.jsx)(s,{position:o,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`y`,children:`Y`})}),e[5]=c):c=e[5];let u;e[6]===Symbol.for(`react.memo_cache_sentinel`)?(u=[0,80,0],e[6]=u):u=e[6];let d;e[7]===Symbol.for(`react.memo_cache_sentinel`)?(d=(0,g.jsx)(s,{position:u,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`z`,children:`Z`})}),e[7]=d):d=e[7];let f;e[8]===Symbol.for(`react.memo_cache_sentinel`)?(f=[0,0,80],e[8]=f):f=e[8];let _;return e[9]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,g.jsxs)(g.Fragment,{children:[r,a,c,d,(0,g.jsx)(s,{position:f,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`x`,children:`X`})})]}),e[9]=_):_=e[9],_}export{_ as DebugElements};
|
||||
import{r as e,t}from"./chunk-DECur_0Z.js";import{n,r,t as i}from"./jsx-runtime-BpGWiA-R.js";import{n as a,r as o}from"./react-three-fiber.esm-B4ybsNEe.js";import{t as s}from"./Html-BMPGAmIZ.js";var c=t(((e,t)=>{(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):n.Stats=r()})(e,function(){var e=function(){function t(e){return i.appendChild(e.dom),e}function n(e){for(var t=0;t<i.children.length;t++)i.children[t].style.display=t===e?`block`:`none`;r=e}var r=0,i=document.createElement(`div`);i.style.cssText=`position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000`,i.addEventListener(`click`,function(e){e.preventDefault(),n(++r%i.children.length)},!1);var a=(performance||Date).now(),o=a,s=0,c=t(new e.Panel(`FPS`,`#0ff`,`#002`)),l=t(new e.Panel(`MS`,`#0f0`,`#020`));if(self.performance&&self.performance.memory)var u=t(new e.Panel(`MB`,`#f08`,`#201`));return n(0),{REVISION:16,dom:i,addPanel:t,showPanel:n,begin:function(){a=(performance||Date).now()},end:function(){s++;var e=(performance||Date).now();if(l.update(e-a,200),e>o+1e3&&(c.update(1e3*s/(e-o),100),o=e,s=0,u)){var t=performance.memory;u.update(t.usedJSHeapSize/1048576,t.jsHeapSizeLimit/1048576)}return e},update:function(){a=this.end()},domElement:i,setMode:n}};return e.Panel=function(e,t,n){var r=1/0,i=0,a=Math.round,o=a(window.devicePixelRatio||1),s=80*o,c=48*o,l=3*o,u=2*o,d=3*o,f=15*o,p=74*o,m=30*o,h=document.createElement(`canvas`);h.width=s,h.height=c,h.style.cssText=`width:80px;height:48px`;var g=h.getContext(`2d`);return g.font=`bold `+9*o+`px Helvetica,Arial,sans-serif`,g.textBaseline=`top`,g.fillStyle=n,g.fillRect(0,0,s,c),g.fillStyle=t,g.fillText(e,l,u),g.fillRect(d,f,p,m),g.fillStyle=n,g.globalAlpha=.9,g.fillRect(d,f,p,m),{dom:h,update:function(c,_){r=Math.min(r,c),i=Math.max(i,c),g.fillStyle=n,g.globalAlpha=1,g.fillRect(0,0,s,f),g.fillStyle=t,g.fillText(a(c)+` `+e+` (`+a(r)+`-`+a(i)+`)`,l,u),g.drawImage(h,d+o,f,p-o,m,d,f,p-o,m),g.fillRect(d+p-o,f,o,m),g.fillStyle=n,g.globalAlpha=.9,g.fillRect(d+p-o,f,o,a((1-c/_)*m))}}},e})})),l=e(r());function u(e,t){typeof e==`function`?e(t):e!=null&&(e.current=t)}function d(e,t=[],n){let[r,i]=l.useState();return l.useLayoutEffect(()=>{let t=e();return i(t),u(n,t),()=>u(n,null)},t),r}var f=e(c());function p({showPanel:e=0,className:t,parent:n}){let r=d(()=>new f.default,[]);return l.useEffect(()=>{if(r){let i=n&&n.current||document.body;r.showPanel(e),i?.appendChild(r.dom);let s=(t??``).split(` `).filter(e=>e);s.length&&r.dom.classList.add(...s);let c=o(()=>r.begin()),l=a(()=>r.end());return()=>{s.length&&r.dom.classList.remove(...s),i?.removeChild(r.dom),c(),l()}}},[n,r,t,e]),null}var m=n(),h={StatsPanel:`_StatsPanel_10m5i_1`,AxisLabel:`_AxisLabel_10m5i_8`},g=i();function _(){let e=(0,m.c)(10),t=(0,l.useRef)(null),n;e[0]===Symbol.for(`react.memo_cache_sentinel`)?(n=()=>{let e=t.current;e&&e.setColors(`rgb(153, 255, 0)`,`rgb(0, 153, 255)`,`rgb(255, 153, 0)`)},e[0]=n):n=e[0],(0,l.useEffect)(n);let r;e[1]===Symbol.for(`react.memo_cache_sentinel`)?(r=(0,g.jsx)(p,{className:h.StatsPanel}),e[1]=r):r=e[1];let i;e[2]===Symbol.for(`react.memo_cache_sentinel`)?(i=[70],e[2]=i):i=e[2];let a;e[3]===Symbol.for(`react.memo_cache_sentinel`)?(a=(0,g.jsx)(`axesHelper`,{ref:t,args:i,renderOrder:999,children:(0,g.jsx)(`lineBasicMaterial`,{depthTest:!1,depthWrite:!1,fog:!1,vertexColors:!0})}),e[3]=a):a=e[3];let o;e[4]===Symbol.for(`react.memo_cache_sentinel`)?(o=[80,0,0],e[4]=o):o=e[4];let c;e[5]===Symbol.for(`react.memo_cache_sentinel`)?(c=(0,g.jsx)(s,{position:o,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`y`,children:`Y`})}),e[5]=c):c=e[5];let u;e[6]===Symbol.for(`react.memo_cache_sentinel`)?(u=[0,80,0],e[6]=u):u=e[6];let d;e[7]===Symbol.for(`react.memo_cache_sentinel`)?(d=(0,g.jsx)(s,{position:u,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`z`,children:`Z`})}),e[7]=d):d=e[7];let f;e[8]===Symbol.for(`react.memo_cache_sentinel`)?(f=[0,0,80],e[8]=f):f=e[8];let _;return e[9]===Symbol.for(`react.memo_cache_sentinel`)?(_=(0,g.jsxs)(g.Fragment,{children:[r,a,c,d,(0,g.jsx)(s,{position:f,center:!0,children:(0,g.jsx)(`span`,{className:h.AxisLabel,"data-axis":`x`,children:`X`})})]}),e[9]=_):_=e[9],_}export{_ as DebugElements};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{t as i}from"./logger-B058WGzf.js";var a=t(),o=e(n(),1),s=r(),c=i(`DebugSuspense`);function l(e){let t=(0,a.c)(10),{name:n,fallback:r,children:i}=e,c=r===void 0?null:r,l;t[0]!==c||t[1]!==n?(l=(0,s.jsx)(u,{name:n,children:c}),t[0]=c,t[1]=n,t[2]=l):l=t[2];let f;t[3]===n?f=t[4]:(f=(0,s.jsx)(d,{name:n}),t[3]=n,t[4]=f);let p;return t[5]!==i||t[6]!==n||t[7]!==l||t[8]!==f?(p=(0,s.jsxs)(o.Suspense,{name:n,fallback:l,children:[f,i]}),t[5]=i,t[6]=n,t[7]=l,t[8]=f,t[9]=p):p=t[9],p}function u(e){let t=(0,a.c)(3),{name:n,children:r}=e,i,s;return t[0]===n?(i=t[1],s=t[2]):(i=()=>{c.debug(`🛑 SUSPENDED: %s`,n)},s=[n],t[0]=n,t[1]=i,t[2]=s),(0,o.useEffect)(i,s),r}function d(e){let t=(0,a.c)(3),{name:n}=e,r,i;return t[0]===n?(r=t[1],i=t[2]):(r=()=>{c.debug(`✅ RESOLVED: %s`,n)},i=[n],t[0]=n,t[1]=r,t[2]=i),(0,o.useEffect)(r,i),null}export{l as t};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{t as i}from"./logger-CySD1nLn.js";var a=t(),o=e(n(),1),s=r(),c=i(`DebugSuspense`);function l(e){let t=(0,a.c)(10),{name:n,fallback:r,children:i}=e,c=r===void 0?null:r,l;t[0]!==c||t[1]!==n?(l=(0,s.jsx)(u,{name:n,children:c}),t[0]=c,t[1]=n,t[2]=l):l=t[2];let f;t[3]===n?f=t[4]:(f=(0,s.jsx)(d,{name:n}),t[3]=n,t[4]=f);let p;return t[5]!==i||t[6]!==n||t[7]!==l||t[8]!==f?(p=(0,s.jsxs)(o.Suspense,{name:n,fallback:l,children:[f,i]}),t[5]=i,t[6]=n,t[7]=l,t[8]=f,t[9]=p):p=t[9],p}function u(e){let t=(0,a.c)(3),{name:n,children:r}=e,i,s;return t[0]===n?(i=t[1],s=t[2]):(i=()=>{c.debug(`🛑 SUSPENDED: %s`,n)},s=[n],t[0]=n,t[1]=i,t[2]=s),(0,o.useEffect)(i,s),r}function d(e){let t=(0,a.c)(3),{name:n}=e,r,i;return t[0]===n?(r=t[1],i=t[2]):(r=()=>{c.debug(`✅ RESOLVED: %s`,n)},i=[n],t[0]=n,t[1]=r,t[2]=i),(0,o.useEffect)(r,i),null}export{l as t};
|
||||
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{B as i,H as a,I as o,L as s,R as c,V as l,w as u,z as d}from"./index-CcOasdr9.js";import{n as f,t as p}from"./gr-C6l4behh.js";var m=e(n(),1),h=t(),g={Root:`_Root_flggk_1`,PlayPause:`_PlayPause_flggk_16 _Button_p0g82_2`,Time:`_Time_flggk_36`,Seek:`_Seek_flggk_54`,Speed:`_Speed_flggk_61`,CameraMode:`_CameraMode_flggk_62`,Field:`_Field_flggk_80`},_=r();function v(e){let t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=Math.floor(e%60);return t>0?`${t}:${n.toString().padStart(2,`0`)}:${r.toString().padStart(2,`0`)}`:`${n}:${r.toString().padStart(2,`0`)}`}function y(){let e=(0,h.c)(42),t=l(),n=d(),r=s(),y=c(),x=a(),{play:S,pause:C,seek:w,setSpeed:T}=i(),E,D;e[0]!==n||e[1]!==C||e[2]!==S||e[3]!==t?(E=()=>{if(!t)return;let e=e=>{if(e.code!==`Space`)return;let t=e.target;t.tagName===`INPUT`||t.tagName===`TEXTAREA`||t.tagName===`SELECT`||t.tagName===`BUTTON`||t.isContentEditable||(e.preventDefault(),n?C():S())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},D=[t,n,S,C],e[0]=n,e[1]=C,e[2]=S,e[3]=t,e[4]=E,e[5]=D):(E=e[4],D=e[5]),(0,m.useEffect)(E,D);let O;e[6]!==T||e[7]!==x?(O=()=>{let e=o.indexOf(x);e>0&&T(o[e-1])},e[6]=T,e[7]=x,e[8]=O):O=e[8],u(`decreasePlaybackSpeed`,O);let k;e[9]!==T||e[10]!==x?(k=()=>{let e=o.indexOf(x);e<o.length-1&&T(o[e+1])},e[9]=T,e[10]=x,e[11]=k):k=e[11],u(`increasePlaybackSpeed`,k);let A;e[12]===w?A=e[13]:(A=e=>{w(parseFloat(e.target.value))},e[12]=w,e[13]=A);let j=A,M;e[14]===T?M=e[15]:(M=e=>{T(parseFloat(e.target.value))},e[14]=T,e[15]=M);let N=M;if(!t||!Number.isFinite(t.duration))return null;let P=n?C:S,F=n?`Pause`:`Play`,I;e[16]===n?I=e[17]:(I=n?(0,_.jsx)(p,{}):(0,_.jsx)(f,{}),e[16]=n,e[17]=I);let L;e[18]!==P||e[19]!==F||e[20]!==I?(L=(0,_.jsx)(`button`,{className:g.PlayPause,onClick:P,"aria-label":F,autoFocus:!0,children:I}),e[18]=P,e[19]=F,e[20]=I,e[21]=L):L=e[21];let R;e[22]===r?R=e[23]:(R=v(r),e[22]=r,e[23]=R);let z;e[24]===y?z=e[25]:(z=v(y),e[24]=y,e[25]=z);let B=`${R} / ${z}`,V;e[26]===B?V=e[27]:(V=(0,_.jsx)(`span`,{className:g.Time,children:B}),e[26]=B,e[27]=V);let H;e[28]!==r||e[29]!==y||e[30]!==j?(H=(0,_.jsx)(`input`,{className:g.Seek,type:`range`,min:0,max:y,step:.01,value:r,onChange:j}),e[28]=r,e[29]=y,e[30]=j,e[31]=H):H=e[31];let U;e[32]===Symbol.for(`react.memo_cache_sentinel`)?(U=(0,_.jsx)(`label`,{htmlFor:`playbackSpeed`,children:`Speed`}),e[32]=U):U=e[32];let W;e[33]===Symbol.for(`react.memo_cache_sentinel`)?(W=o.map(b),e[33]=W):W=e[33];let G;e[34]!==N||e[35]!==x?(G=(0,_.jsxs)(`div`,{className:g.Field,children:[U,(0,_.jsx)(`select`,{id:`playbackSpeed`,className:g.Speed,value:x,onChange:N,children:W})]}),e[34]=N,e[35]=x,e[36]=G):G=e[36];let K;return e[37]!==V||e[38]!==H||e[39]!==G||e[40]!==L?(K=(0,_.jsxs)(`div`,{className:g.Root,children:[L,V,H,G]}),e[37]=V,e[38]=H,e[39]=G,e[40]=L,e[41]=K):K=e[41],K}function b(e){return(0,_.jsxs)(`option`,{value:e,children:[e,`×`]},e)}export{y as DemoPlaybackControls};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{B as i,H as a,I as o,L as s,R as c,V as l,w as u,z as d}from"./index-BEehCpzM.js";import{n as f,t as p}from"./gr-CIZJuMcZ.js";var m=e(n(),1),h=t(),g={Root:`_Root_flggk_1`,PlayPause:`_PlayPause_flggk_16 _Button_p0g82_2`,Time:`_Time_flggk_36`,Seek:`_Seek_flggk_54`,Speed:`_Speed_flggk_61`,CameraMode:`_CameraMode_flggk_62`,Field:`_Field_flggk_80`},_=r();function v(e){let t=Math.floor(e/3600),n=Math.floor(e%3600/60),r=Math.floor(e%60);return t>0?`${t}:${n.toString().padStart(2,`0`)}:${r.toString().padStart(2,`0`)}`:`${n}:${r.toString().padStart(2,`0`)}`}function y(){let e=(0,h.c)(42),t=l(),n=d(),r=s(),y=c(),x=a(),{play:S,pause:C,seek:w,setSpeed:T}=i(),E,D;e[0]!==n||e[1]!==C||e[2]!==S||e[3]!==t?(E=()=>{if(!t)return;let e=e=>{if(e.code!==`Space`)return;let t=e.target;t.tagName===`INPUT`||t.tagName===`TEXTAREA`||t.tagName===`SELECT`||t.tagName===`BUTTON`||t.isContentEditable||(e.preventDefault(),n?C():S())};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},D=[t,n,S,C],e[0]=n,e[1]=C,e[2]=S,e[3]=t,e[4]=E,e[5]=D):(E=e[4],D=e[5]),(0,m.useEffect)(E,D);let O;e[6]!==T||e[7]!==x?(O=()=>{let e=o.indexOf(x);e>0&&T(o[e-1])},e[6]=T,e[7]=x,e[8]=O):O=e[8],u(`decreasePlaybackSpeed`,O);let k;e[9]!==T||e[10]!==x?(k=()=>{let e=o.indexOf(x);e<o.length-1&&T(o[e+1])},e[9]=T,e[10]=x,e[11]=k):k=e[11],u(`increasePlaybackSpeed`,k);let A;e[12]===w?A=e[13]:(A=e=>{w(parseFloat(e.target.value))},e[12]=w,e[13]=A);let j=A,M;e[14]===T?M=e[15]:(M=e=>{T(parseFloat(e.target.value))},e[14]=T,e[15]=M);let N=M;if(!t||!Number.isFinite(t.duration))return null;let P=n?C:S,F=n?`Pause`:`Play`,I;e[16]===n?I=e[17]:(I=n?(0,_.jsx)(p,{}):(0,_.jsx)(f,{}),e[16]=n,e[17]=I);let L;e[18]!==P||e[19]!==F||e[20]!==I?(L=(0,_.jsx)(`button`,{className:g.PlayPause,onClick:P,"aria-label":F,autoFocus:!0,children:I}),e[18]=P,e[19]=F,e[20]=I,e[21]=L):L=e[21];let R;e[22]===r?R=e[23]:(R=v(r),e[22]=r,e[23]=R);let z;e[24]===y?z=e[25]:(z=v(y),e[24]=y,e[25]=z);let B=`${R} / ${z}`,V;e[26]===B?V=e[27]:(V=(0,_.jsx)(`span`,{className:g.Time,children:B}),e[26]=B,e[27]=V);let H;e[28]!==r||e[29]!==y||e[30]!==j?(H=(0,_.jsx)(`input`,{className:g.Seek,type:`range`,min:0,max:y,step:.01,value:r,onChange:j}),e[28]=r,e[29]=y,e[30]=j,e[31]=H):H=e[31];let U;e[32]===Symbol.for(`react.memo_cache_sentinel`)?(U=(0,_.jsx)(`label`,{htmlFor:`playbackSpeed`,children:`Speed`}),e[32]=U):U=e[32];let W;e[33]===Symbol.for(`react.memo_cache_sentinel`)?(W=o.map(b),e[33]=W):W=e[33];let G;e[34]!==N||e[35]!==x?(G=(0,_.jsxs)(`div`,{className:g.Field,children:[U,(0,_.jsx)(`select`,{id:`playbackSpeed`,className:g.Speed,value:x,onChange:N,children:W})]}),e[34]=N,e[35]=x,e[36]=G):G=e[36];let K;return e[37]!==V||e[38]!==H||e[39]!==G||e[40]!==L?(K=(0,_.jsxs)(`div`,{className:g.Root,children:[L,V,H,G]}),e[37]=V,e[38]=H,e[39]=G,e[40]=L,e[41]=K):K=e[41],K}function b(e){return(0,_.jsxs)(`option`,{value:e,children:[e,`×`]},e)}export{y as DemoPlaybackControls};
|
||||
1
docs/assets/ExplosionShape-Cg4WaJEo.js
Normal file
1
docs/assets/ExplosionShape-Cg4WaJEo.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t,t as n}from"./jsx-runtime-BpGWiA-R.js";import{a as r}from"./react-three-fiber.esm-B4ybsNEe.js";import{n as i,r as a}from"./engineStore-DXFfg1NG.js";import{$ as o,o as s}from"./three.module-DeDv86YO.js";import{i as c,l}from"./GenericShape-Disamruh.js";import{_ as u,a as d,g as f,h as p,l as m}from"./playbackUtils-D5VkIMBR.js";import{t as h}from"./useAnisotropy-D9b509fd.js";import{n as g}from"./streamPlaybackStore-CGokDWAy.js";var _=e(t(),1),v=n();function y(e){let t=e.sizes,n=e.times;if(!Array.isArray(t)||t.length===0)return{times:[0,1],sizes:[[1,1,1],[1,1,1]]};let r=t.map(e=>[e.x/100,e.y/100,e.z/100]);return{times:Array.isArray(n)?n:r.map((e,t)=>t/Math.max(r.length-1,1)),sizes:r}}function b(e,t){let{times:n,sizes:r}=e;if(n.length===0)return[1,1,1];if(t<=n[0])return r[0];if(t>=n[n.length-1])return r[r.length-1];for(let e=0;e<n.length-1;e++)if(t>=n[e]&&t<=n[e+1]){let i=(t-n[e])/(n[e+1]-n[e]);return[r[e][0]+(r[e+1][0]-r[e][0])*i,r[e][1]+(r[e+1][1]-r[e][1])*i,r[e][2]+(r[e+1][2]-r[e][2])*i]}return r[r.length-1]}function x({entity:e}){let t=g.getState().playback,n=c(e.shapeName),x=h(),S=(0,_.useRef)(null),C=(0,_.useRef)(i()),w=(0,_.useRef)(Math.random()*Math.PI*2),T=(0,_.useRef)([]),E=(0,_.useMemo)(()=>{if(e.explosionDataBlockId)return t.getDataBlockData(e.explosionDataBlockId)},[e.explosionDataBlockId,t]),D=(0,_.useMemo)(()=>E?y(E):void 0,[E]),O=(0,_.useMemo)(()=>{let e=E?.explosionScale;return e?[e.x/100,e.y/100,e.z/100]:[1,1,1]},[E]),k=(E?.lifetimeMS??31)*32,A=e.faceViewer!==!1,{scene:j,mixer:M,visNodes:N,iflInfos:P,materials:F}=(0,_.useMemo)(()=>{let t=l(n.scene),r=[];t.traverse(e=>{if(!e.isMesh||!e.material)return;let t=Array.isArray(e.material)?e.material[0]:e.material;if(!t?.userData)return;let n=new Set(t.userData.flag_names??[]),i=t.userData.resource_path;if(n.has(`IflMaterial`)&&i){let t=e.userData;r.push({mesh:e,iflPath:`textures/${i}.ifl`,sequenceName:t?.ifl_sequence?String(t.ifl_sequence).toLowerCase():void 0,duration:t?.ifl_duration?Number(t.ifl_duration):void 0,cyclic:t?.ifl_sequence?!!t.ifl_cyclic:void 0,toolBegin:t?.ifl_tool_begin==null?void 0:Number(t.ifl_tool_begin)})}}),m(t,e.shapeName,{anisotropy:x});let i=[];t.traverse(e=>{if(!e.isMesh)return;let t=e.userData;if(!t)return;let n=t.vis_keyframes,r=t.vis_duration,a=(t.vis_sequence??``).toLowerCase();!a||!Array.isArray(n)||n.length<=1||!r||r<=0||a===`ambient`&&i.push({mesh:e,keyframes:n,duration:r,cyclic:!!t.vis_cyclic})});for(let e of i)e.mesh.visible=!0,e.mesh.material&&!Array.isArray(e.mesh.material)&&(e.mesh.material.transparent=!0,e.mesh.material.depthWrite=!1);for(let e of r)e.mesh.userData?.vis_sequence||(e.mesh.visible=!0);let a=new Map;for(let e of n.animations)a.set(e.name.toLowerCase(),e);let c=a.get(`ambient`),u=null;if(c){u=new s(t);let e=u.clipAction(c);e.setLoop(o,1),e.clampWhenFinished=!0,e.timeScale=(E?.playSpeed??20)/20,e.play()}let d=[];return t.traverse(e=>{e.isMesh&&(Array.isArray(e.material)?d.push(...e.material):e.material&&d.push(e.material))}),t.traverse(e=>{e.frustumCulled=!1}),{scene:t,mixer:u,visNodes:i,iflInfos:r,materials:d}},[n,E,x]);return(0,_.useEffect)(()=>()=>{d(j),M?.uncacheRoot(j)},[j,M]),(0,_.useEffect)(()=>{T.current=[];for(let e of P)f(e.iflPath).then(t=>{let n=Array.isArray(e.mesh.material)?e.mesh.material[0]:e.mesh.material;n&&(n.map=t.texture,n.needsUpdate=!0),T.current.push({atlas:t,info:e})}).catch(()=>{})},[P]),r((e,t)=>{let n=S.current;if(!n)return;let r=a.getState().playback,o=r.status===`playing`?t*r.rate:0,s=i()-C.current,c=Math.min(s/k,1),l=s/1e3;M&&M.update(o);let d=c>.8?1-(c-.8)/.2:1;for(let{mesh:e,keyframes:t,duration:n,cyclic:r}of N){let i=e.material;if(!i||Array.isArray(i))continue;let a=l/n,o=r?a%1:Math.min(a,1),s=t.length,c=o*s,u=Math.floor(c)%s,f=(u+1)%s,p=c-Math.floor(c);i.opacity=(t[u]+(t[f]-t[u])*p)*d}if(d<1)for(let e of F)`opacity`in e&&(e.transparent=!0,e.opacity*=d);for(let{atlas:e,info:t}of T.current){let n;n=t.sequenceName&&t.duration?(t.cyclic?l/t.duration%1:Math.min(l/t.duration,1))*t.duration+(t.toolBegin??0):l,u(e,p(e,n))}if(D){let e=b(D,c);n.scale.set(e[0]*O[0],e[1]*O[1],e[2]*O[2])}A&&(n.lookAt(e.camera.position),n.rotateZ(w.current))}),(0,v.jsx)(`group`,{ref:S,children:(0,v.jsx)(`group`,{rotation:[0,Math.PI,0],children:(0,v.jsx)(`primitive`,{object:j})})})}export{x as ExplosionShape};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{i}from"./react-three-fiber.esm-CD18QK1u.js";import{a}from"./SettingsProvider-CeFD8Cx1.js";import{r as o}from"./cameraTourStore-DQ989o2x.js";import{Ot as s,Ut as c,b as l,jt as u,p as d}from"./three.module-C9W4LJrj.js";import{t as f}from"./Texture-D2i1vM3o.js";import{p}from"./index-CcOasdr9.js";import{t as m}from"./DebugBounds-BnJbEQUF.js";import{t as h}from"./DebugSuspense-D7OBM5vj.js";var g=e(n(),1),_=t(),v=`
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{a as i}from"./react-three-fiber.esm-B4ybsNEe.js";import{a}from"./SettingsProvider-BeB5OnG9.js";import{r as o}from"./cameraTourStore-CfKPrs02.js";import{Ot as s,Ut as c,b as l,jt as u,p as d}from"./three.module-DeDv86YO.js";import{t as f}from"./Texture-DuIJU_zO.js";import{p}from"./index-BEehCpzM.js";import{t as m}from"./DebugBounds-DUxS5ppE.js";import{t as h}from"./DebugSuspense-DmIsfY-y.js";var g=e(n(),1),_=t(),v=`
|
||||
#include <fog_pars_vertex>
|
||||
|
||||
varying vec2 vUv;
|
||||
526
docs/assets/GameView-DL3qb_dC.js
Normal file
526
docs/assets/GameView-DL3qb_dC.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
docs/assets/GenericShape-Disamruh.js
Normal file
1
docs/assets/GenericShape-Disamruh.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{t as i}from"./useQuery-B1ja4RVJ.js";import{c as a}from"./manifest-MMwwguGo.js";import{i as o,it as s,l as c,n as l,rt as u}from"./index-CcOasdr9.js";var d=e(n(),1),f=t(),p={GuiMarkup:`_GuiMarkup_jg4va_1`,Bullet:`_Bullet_jg4va_12`},m=r(),h=new Set([`spop`,`spush`,`lmargin`,`font`,`color`,`bitmap`,`a`,`/a`]);function g(e){return e.split(/<([^><]+)>/g).map((e,t)=>{if(t%2==0)return e?{type:`text`,value:e}:null;{let[t,...n]=e.split(`:`);return h.has(t.toLowerCase())?{type:`tag`,name:t,args:n}:{type:`text`,value:`<${e}>`}}}).filter(e=>e!=null)}function _(e){let[t,n]=e;return{fontDescription:t,fontSize:n?Math.max(11,Math.min(parseInt(n.trim(),10),16)):void 0}}function v(e){let t=g(e),n={type:`span`,source:`root`,style:{},children:[]},r=n,i=[r],a=e=>e.children!=null&&e.children.some(e=>typeof e==`string`||a(e));for(let e of t)switch(e.type){case`text`:r.children.push(e.value);break;case`tag`:switch(e.name){case`spush`:{let e={type:`span`,source:`spush`,style:{},children:[]};r.children.push(e),r=e,i.push(r);break}case`spop`:if(r.source!==`root`){let e=i.pop();for(;e.source!==`spush`;)e=i.pop();r=i[i.length-1]}break;case`lmargin`:break;case`font`:{let t=_(e.args).fontSize;if(!a(r))r.style.fontSize=t;else{let e={type:`span`,source:`spush`,style:{fontSize:t},children:[]};r.children.push(e),r=e,i.push(r)}break}case`color`:if(!a(r))r.style.color=`#${e.args[0].trim()}`;else{let t={type:`span`,source:`spush`,style:{color:`#${e.args[0].trim()}`},children:[]};r.children.push(t),r=t,i.push(r)}break;case`bitmap`:{let t={type:`bitmap`,value:e.args[0]};r.children.push(t);break}case`a`:{let t=e.args[0].trim().split(` `),n={type:`a`,source:`a`,value:`http://${t.length===2&&t[0]===`wwwlink`?t[1]:t[0]}`,style:{},children:[]};r.children.push(n),r=n,i.push(r);break}case`/a`:{let e=i.pop();for(;e.source!==`a`;)e=i.pop();r=i[i.length-1];break}}}return y(n)}function y(e){switch(e.type){case`span`:return d.createElement(`span`,{style:Object.keys(e.style).length===0?void 0:e.style},...e.children.map(e=>typeof e==`string`?e:y(e)));case`a`:return d.createElement(`a`,{href:e.value,style:Object.keys(e.style).length===0?void 0:e.style,rel:`noopener noreferrer`,target:`_blank`},...e.children.map(e=>typeof e==`string`?e:y(e)));case`bitmap`:return(0,m.jsx)(S,{name:e.value})}}var b=new Map;function x(e){if(b.has(e))return b.get(e);let t;try{t=o(a(`textures/gui/${e}`))}catch{t=null}return b.set(e,t),t}function S(e){let t=(0,f.c)(5),{name:n}=e,r;t[0]===n?r=t[1]:(r=x(n),t[0]=n,t[1]=r);let i=r;if(i){let e;return t[2]===i?e=t[3]:(e=(0,m.jsx)(`img`,{src:i,alt:``,className:p.Bitmap}),t[2]=i,t[3]=e),e}if(/bullet/i.test(n)){let e;return t[4]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,m.jsx)(`span`,{className:p.Bullet,children:`•`}),t[4]=e):e=t[4],e}return null}var C=/<(?:font|color|bitmap|just|lmargin|a):/i;function w(e){return C.test(e)}function T(e,t){let n=t.toUpperCase();return e.split(`
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{t as i}from"./useQuery-C-bcqv6a.js";import{c as a}from"./manifest-DDCwpSLV.js";import{i as o,it as s,l as c,n as l,rt as u}from"./index-BEehCpzM.js";var d=e(n(),1),f=t(),p={GuiMarkup:`_GuiMarkup_jg4va_1`,Bullet:`_Bullet_jg4va_12`},m=r(),h=new Set([`spop`,`spush`,`lmargin`,`font`,`color`,`bitmap`,`a`,`/a`]);function g(e){return e.split(/<([^><]+)>/g).map((e,t)=>{if(t%2==0)return e?{type:`text`,value:e}:null;{let[t,...n]=e.split(`:`);return h.has(t.toLowerCase())?{type:`tag`,name:t,args:n}:{type:`text`,value:`<${e}>`}}}).filter(e=>e!=null)}function _(e){let[t,n]=e;return{fontDescription:t,fontSize:n?Math.max(11,Math.min(parseInt(n.trim(),10),16)):void 0}}function v(e){let t=g(e),n={type:`span`,source:`root`,style:{},children:[]},r=n,i=[r],a=e=>e.children!=null&&e.children.some(e=>typeof e==`string`||a(e));for(let e of t)switch(e.type){case`text`:r.children.push(e.value);break;case`tag`:switch(e.name){case`spush`:{let e={type:`span`,source:`spush`,style:{},children:[]};r.children.push(e),r=e,i.push(r);break}case`spop`:if(r.source!==`root`){let e=i.pop();for(;e.source!==`spush`;)e=i.pop();r=i[i.length-1]}break;case`lmargin`:break;case`font`:{let t=_(e.args).fontSize;if(!a(r))r.style.fontSize=t;else{let e={type:`span`,source:`spush`,style:{fontSize:t},children:[]};r.children.push(e),r=e,i.push(r)}break}case`color`:if(!a(r))r.style.color=`#${e.args[0].trim()}`;else{let t={type:`span`,source:`spush`,style:{color:`#${e.args[0].trim()}`},children:[]};r.children.push(t),r=t,i.push(r)}break;case`bitmap`:{let t={type:`bitmap`,value:e.args[0]};r.children.push(t);break}case`a`:{let t=e.args[0].trim().split(` `),n={type:`a`,source:`a`,value:`http://${t.length===2&&t[0]===`wwwlink`?t[1]:t[0]}`,style:{},children:[]};r.children.push(n),r=n,i.push(r);break}case`/a`:{let e=i.pop();for(;e.source!==`a`;)e=i.pop();r=i[i.length-1];break}}}return y(n)}function y(e){switch(e.type){case`span`:return d.createElement(`span`,{style:Object.keys(e.style).length===0?void 0:e.style},...e.children.map(e=>typeof e==`string`?e:y(e)));case`a`:return d.createElement(`a`,{href:e.value,style:Object.keys(e.style).length===0?void 0:e.style,rel:`noopener noreferrer`,target:`_blank`},...e.children.map(e=>typeof e==`string`?e:y(e)));case`bitmap`:return(0,m.jsx)(S,{name:e.value})}}var b=new Map;function x(e){if(b.has(e))return b.get(e);let t;try{t=o(a(`textures/gui/${e}`))}catch{t=null}return b.set(e,t),t}function S(e){let t=(0,f.c)(5),{name:n}=e,r;t[0]===n?r=t[1]:(r=x(n),t[0]=n,t[1]=r);let i=r;if(i){let e;return t[2]===i?e=t[3]:(e=(0,m.jsx)(`img`,{src:i,alt:``,className:p.Bitmap}),t[2]=i,t[3]=e),e}if(/bullet/i.test(n)){let e;return t[4]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,m.jsx)(`span`,{className:p.Bullet,children:`•`}),t[4]=e):e=t[4],e}return null}var C=/<(?:font|color|bitmap|just|lmargin|a):/i;function w(e){return C.test(e)}function T(e,t){let n=t.toUpperCase();return e.split(`
|
||||
`).flatMap(e=>{let t=e.match(/^\[([^\]]+)\]/);return t&&!t[1].toUpperCase().split(/\s+/).includes(n)?[]:[e.replace(/^\[[^\]]+\]/,``)]}).join(`
|
||||
`)}function E(e){let t=(0,f.c)(4),{markup:n}=e,r;t[0]===n?r=t[1]:(r=v(n),t[0]=n,t[1]=r);let i=r,a;return t[2]===i?a=t[3]:(a=(0,m.jsx)(`div`,{className:p.GuiMarkup,children:i}),t[2]=i,t[3]=a),a}var D={Dialog:`_Dialog_tbn5d_1 _Dialog_1t9wa_1`,Overlay:`_Overlay_tbn5d_10 _Overlay_1t9wa_22`,Body:`_Body_tbn5d_14`,Left:`_Left_tbn5d_22`,PreviewImage:`_PreviewImage_tbn5d_29`,PreviewImageFloating:`_PreviewImageFloating_tbn5d_35`,Title:`_Title_tbn5d_45`,MapMeta:`_MapMeta_tbn5d_53`,MapPlanet:`_MapPlanet_tbn5d_63`,MapQuote:`_MapQuote_tbn5d_67`,MapBlurb:`_MapBlurb_tbn5d_86`,Section:`_Section_tbn5d_91`,SectionTitle:`_SectionTitle_tbn5d_95`,MusicTrack:`_MusicTrack_tbn5d_105`,MusicButton:`_MusicButton_tbn5d_119`,Footer:`_Footer_tbn5d_146`,CloseButton:`_CloseButton_tbn5d_158 _DialogButton_1t9wa_33`,Hint:`_Hint_tbn5d_162`,MusicTrackName:`_MusicTrackName_tbn5d_168`};function ee(e){let t=(0,f.c)(2),n;return t[0]===e?n=t[1]:(n={queryKey:[`parsedMission`,e],queryFn:()=>c(e)},t[0]=e,t[1]=n),i(n)}function te(e){for(let t of e.body){if(t.type!==`ObjectDeclaration`)continue;let{instanceName:e,body:n}=t;if(e&&e.type===`Identifier`&&e.name.toLowerCase()===`missiongroup`){let e={};for(let t of n){if(t.type!==`Assignment`)continue;let{target:n,value:r}=t;n.type===`Identifier`&&r.type===`StringLiteral`&&(e[n.name.toLowerCase()]=r.value)}return e}}return{}}function ne(e,t){if(e)try{return o(a(`textures/gui/${e}`))}catch{}try{return o(a(`textures/gui/Load_${t}`))}catch{}return null}function re(e){let t=(0,f.c)(7),{src:n,alt:r,className:i}=e,a=i===void 0?D.PreviewImage:i,[o,s]=(0,d.useState)(null),c,l;if(t[0]===n?(c=t[1],l=t[2]):(c=()=>{let e=!1,t;return fetch(n).then(j).then(A).then(k).then(n=>{e||!n||(t=URL.createObjectURL(n),s(t))}).catch(O),()=>{e=!0,t&&URL.revokeObjectURL(t)}},l=[n],t[0]=n,t[1]=c,t[2]=l),(0,d.useEffect)(c,l),!o)return null;let u;return t[3]!==r||t[4]!==a||t[5]!==o?(u=(0,m.jsx)(`img`,{src:o,alt:r,className:a}),t[3]=r,t[4]=a,t[5]=o,t[6]=u):u=t[6],u}function O(){}function k(e){return new Promise(t=>{let n=document.createElement(`canvas`);n.width=e.width,n.height=e.height,n.getContext(`2d`)?.drawImage(e,0,0),e.close(),n.toBlob(t)})}function A(e){return createImageBitmap(e,{colorSpaceConversion:`none`})}function j(e){return e.blob()}function ie(e){let t=(0,f.c)(22),{track:n}=e,[r,i]=(0,d.useState)(!1),[a,o]=(0,d.useState)(!0),c=(0,d.useRef)(null),p;t[0]===n?p=t[1]:(p=n.toLowerCase(),t[0]=n,t[1]=p);let h=`${l}music/${p}.mp3`,g,_;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(g=()=>{let e=c.current;if(e)return()=>{e.pause()}},_=[],t[2]=g,t[3]=_):(g=t[2],_=t[3]),(0,d.useEffect)(g,_);let v;t[4]===r?v=t[5]:(v=()=>{let e=c.current;e&&(r?e.pause():e.play().catch(()=>o(!1)))},t[4]=r,t[5]=v);let y=v,b,x,S;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(b=()=>i(!0),x=()=>i(!1),S=()=>o(!1),t[6]=b,t[7]=x,t[8]=S):(b=t[6],x=t[7],S=t[8]);let C;t[9]===h?C=t[10]:(C=(0,m.jsx)(`audio`,{ref:c,src:h,loop:!0,onPlay:b,onPause:x,onError:S}),t[9]=h,t[10]=C);let w;t[11]===n?w=t[12]:(w=(0,m.jsx)(`span`,{className:D.MusicTrackName,children:n}),t[11]=n,t[12]=w);let T;t[13]!==a||t[14]!==r||t[15]!==y?(T=a&&(0,m.jsx)(`button`,{className:D.MusicButton,onClick:y,"aria-label":r?`Pause music`:`Play music`,children:r?(0,m.jsx)(s,{}):(0,m.jsx)(u,{})}),t[13]=a,t[14]=r,t[15]=y,t[16]=T):T=t[16];let E;return t[17]!==r||t[18]!==T||t[19]!==C||t[20]!==w?(E=(0,m.jsxs)(`div`,{className:D.MusicTrack,"data-playing":r,children:[C,w,T]}),t[17]=r,t[18]=T,t[19]=C,t[20]=w,t[21]=E):E=t[21],E}function M(e){let t=(0,f.c)(100),{onClose:n,missionName:r,missionType:i}=e,{data:a}=ee(r),o=(0,d.useRef)(null),s,c;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(s=()=>{o.current?.focus();try{document.exitPointerLock()}catch{}},c=[],t[0]=s,t[1]=c):(s=t[0],c=t[1]),(0,d.useEffect)(s,c);let l,u;t[2]===n?(l=t[3],u=t[4]):(l=()=>{let e=e=>{if(e.key===`Escape`)n();else if(e.key===`k`&&(e.metaKey||e.ctrlKey)){n();return}e.stopImmediatePropagation()},t=se;return window.addEventListener(`keydown`,e,{capture:!0}),window.addEventListener(`keyup`,t,{capture:!0}),()=>{window.removeEventListener(`keydown`,e,{capture:!0}),window.removeEventListener(`keyup`,t,{capture:!0})}},u=[n],t[2]=n,t[3]=l,t[4]=u),(0,d.useEffect)(l,u);let p;t[5]===a?p=t[6]:(p=a?te(a.ast):{},t[5]=a,t[6]=p);let h=p,g;t[7]!==r||t[8]!==a?(g=a?ne(a.bitmap,r):null,t[7]=r,t[8]=a,t[9]=g):g=t[9];let _=g,v=a?.displayName??r,y;t[10]===i?y=t[11]:(y=i.toLowerCase(),t[10]=i,t[11]=y);let b=y===`singleplayer`,x=h.musictrack,S,C,O,k,A,j,M,N,P,F,I,L,R,z,B,V,H,U;if(t[12]!==_||t[13]!==v||t[14]!==b||t[15]!==i||t[16]!==n||t[17]!==a){let e=a?.missionString?T(a.missionString,i):null,r,s,c,l;if(t[36]!==a?.missionQuote){if(l=a?.missionQuote?.trim()??``,s=w(l),c=``,r=``,!s)for(let e of l.split(`
|
||||
`)){let t=e.trim();t.match(/^--[^-]/)?r=t.replace(/^-+\s*/,``).trim():t&&(c+=(c?`
|
||||
1
docs/assets/Mission-B47ZUclM.js
Normal file
1
docs/assets/Mission-B47ZUclM.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
docs/assets/PlayerModel-CxZbg1mL.js
Normal file
1
docs/assets/PlayerModel-CxZbg1mL.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{i}from"./react-three-fiber.esm-CD18QK1u.js";import{g as a,m as o,p as s}from"./playbackUtils-bg3Dy5d2.js";import{Ct as c,Wt as l,b as u,kt as d}from"./three.module-C9W4LJrj.js";import{t as f}from"./Texture-D2i1vM3o.js";import{p}from"./index-CcOasdr9.js";var m=t(),h=e(n(),1),g=r(),_=new l,v=new l,y=new l,b=new l,x=new l,S=new l,C=new l(0,1,0);function w(e){let t=(0,m.c)(14),{entity:n}=e,{visual:r}=n,i;t[0]===r.texture?i=t[1]:(i=p(r.texture),t[0]=r.texture,t[1]=i);let a=f(i,T),o=Array.isArray(a)?a[0]:a,s;t[2]!==r.color.b||t[3]!==r.color.g||t[4]!==r.color.r?(s=new u().setRGB(r.color.r,r.color.g,r.color.b,d),t[2]=r.color.b,t[3]=r.color.g,t[4]=r.color.r,t[5]=s):s=t[5];let c=s,l;t[6]===r.size?l=t[7]:(l=[r.size,r.size,1],t[6]=r.size,t[7]=l);let h;t[8]!==c||t[9]!==o?(h=(0,g.jsx)(`spriteMaterial`,{map:o,color:c,transparent:!0,blending:2,depthWrite:!1,toneMapped:!1}),t[8]=c,t[9]=o,t[10]=h):h=t[10];let _;return t[11]!==l||t[12]!==h?(_=(0,g.jsx)(`sprite`,{scale:l,children:h}),t[11]=l,t[12]=h,t[13]=_):_=t[13],_}function T(e){o(Array.isArray(e)?e[0]:e)}function E(e){let t=(0,m.c)(29),{entity:n}=e,{visual:r}=n,o=(0,h.useRef)(null),l=(0,h.useRef)(null),u=(0,h.useRef)(null),d;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(d=new c,t[0]=d):d=t[0];let w=(0,h.useRef)(d),T;t[1]===r.texture?T=t[2]:(T=p(r.texture),t[1]=r.texture,t[2]=T);let E=r.crossTexture??r.texture,O;t[3]===E?O=t[4]:(O=p(E),t[3]=E,t[4]=O);let k;t[5]!==T||t[6]!==O?(k=[T,O],t[5]=T,t[6]=O,t[7]=k):k=t[7];let A=f(k,D),j;t[8]===A?j=t[9]:(j=Array.isArray(A)?A:[A,A],t[8]=A,t[9]=j);let[M,N]=j,P;t[10]!==n.direction||t[11]!==n.keyframes?.[0]||t[12]!==r.crossSize||t[13]!==r.crossViewAng||t[14]!==r.renderCross||t[15]!==r.tracerLength||t[16]!==r.tracerWidth?(P=e=>{let{camera:t}=e,i=o.current,c=l.current;if(!i||!c)return;let d=n.keyframes?.[0],f=d?.position,p=n.direction??d?.velocity;if(!f||!p){i.visible=!1,u.current&&(u.current.visible=!1);return}if(a(p,_),_.lengthSq()<1e-8){i.visible=!1,u.current&&(u.current.visible=!1);return}_.normalize(),i.visible=!0,a(f,S),v.copy(S).sub(t.position),y.crossVectors(v,_),y.lengthSq()<1e-8&&(y.crossVectors(C,_),y.lengthSq()<1e-8&&y.set(1,0,0)),y.normalize().multiplyScalar(r.tracerWidth);let m=r.tracerLength*.5;b.copy(_).multiplyScalar(-m),x.copy(_).multiplyScalar(m);let h=c.array;h[0]=b.x+y.x,h[1]=b.y+y.y,h[2]=b.z+y.z,h[3]=b.x-y.x,h[4]=b.y-y.y,h[5]=b.z-y.z,h[6]=x.x-y.x,h[7]=x.y-y.y,h[8]=x.z-y.z,h[9]=x.x+y.x,h[10]=x.y+y.y,h[11]=x.z+y.z,c.needsUpdate=!0;let g=u.current;if(!g)return;if(!r.renderCross){g.visible=!1;return}v.normalize();let T=_.dot(v);if(T>-r.crossViewAng&&T<r.crossViewAng){g.visible=!1;return}g.visible=!0,s(_,w.current),g.quaternion.copy(w.current),g.scale.setScalar(r.crossSize)},t[10]=n.direction,t[11]=n.keyframes?.[0],t[12]=r.crossSize,t[13]=r.crossViewAng,t[14]=r.renderCross,t[15]=r.tracerLength,t[16]=r.tracerWidth,t[17]=P):P=t[17],i(P);let F;t[18]===Symbol.for(`react.memo_cache_sentinel`)?(F=(0,g.jsx)(`bufferAttribute`,{ref:l,attach:`attributes-position`,args:[new Float32Array(12),3]}),t[18]=F):F=t[18];let I;t[19]===Symbol.for(`react.memo_cache_sentinel`)?(I=(0,g.jsx)(`bufferAttribute`,{attach:`attributes-uv`,args:[new Float32Array([0,0,0,1,1,1,1,0]),2]}),t[19]=I):I=t[19];let L;t[20]===Symbol.for(`react.memo_cache_sentinel`)?(L=(0,g.jsxs)(`bufferGeometry`,{children:[F,I,(0,g.jsx)(`bufferAttribute`,{attach:`index`,args:[new Uint16Array([0,1,2,0,2,3]),1]})]}),t[20]=L):L=t[20];let R;t[21]===M?R=t[22]:(R=(0,g.jsxs)(`mesh`,{ref:o,children:[L,(0,g.jsx)(`meshBasicMaterial`,{map:M,transparent:!0,blending:2,side:2,depthWrite:!1,toneMapped:!1})]}),t[21]=M,t[22]=R);let z;t[23]!==N||t[24]!==r.renderCross?(z=r.renderCross?(0,g.jsxs)(`mesh`,{ref:u,children:[(0,g.jsxs)(`bufferGeometry`,{children:[(0,g.jsx)(`bufferAttribute`,{attach:`attributes-position`,args:[new Float32Array([-.5,0,-.5,.5,0,-.5,.5,0,.5,-.5,0,.5]),3]}),(0,g.jsx)(`bufferAttribute`,{attach:`attributes-uv`,args:[new Float32Array([0,0,0,1,1,1,1,0]),2]}),(0,g.jsx)(`bufferAttribute`,{attach:`index`,args:[new Uint16Array([0,1,2,0,2,3]),1]})]}),(0,g.jsx)(`meshBasicMaterial`,{map:N,transparent:!0,blending:2,side:2,depthWrite:!1,toneMapped:!1})]}):null,t[23]=N,t[24]=r.renderCross,t[25]=z):z=t[25];let B;return t[26]!==R||t[27]!==z?(B=(0,g.jsxs)(g.Fragment,{children:[R,z]}),t[26]=R,t[27]=z,t[28]=B):B=t[28],B}function D(e){let t=Array.isArray(e)?e:[e];for(let e of t)o(e)}export{w as SpriteProjectile,E as TracerProjectile};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{n as t,r as n,t as r}from"./jsx-runtime-BpGWiA-R.js";import{a as i}from"./react-three-fiber.esm-B4ybsNEe.js";import{Ct as a,Wt as o,b as s,kt as c}from"./three.module-DeDv86YO.js";import{t as l}from"./Texture-DuIJU_zO.js";import{p as u}from"./index-BEehCpzM.js";import{d,f,m as p}from"./playbackUtils-D5VkIMBR.js";var m=t(),h=e(n(),1),g=r(),_=new o,v=new o,y=new o,b=new o,x=new o,S=new o,C=new o(0,1,0);function w(e){let t=(0,m.c)(14),{entity:n}=e,{visual:r}=n,i;t[0]===r.texture?i=t[1]:(i=u(r.texture),t[0]=r.texture,t[1]=i);let a=l(i,T),o=Array.isArray(a)?a[0]:a,d;t[2]!==r.color.b||t[3]!==r.color.g||t[4]!==r.color.r?(d=new s().setRGB(r.color.r,r.color.g,r.color.b,c),t[2]=r.color.b,t[3]=r.color.g,t[4]=r.color.r,t[5]=d):d=t[5];let f=d,p;t[6]===r.size?p=t[7]:(p=[r.size,r.size,1],t[6]=r.size,t[7]=p);let h;t[8]!==f||t[9]!==o?(h=(0,g.jsx)(`spriteMaterial`,{map:o,color:f,transparent:!0,blending:2,depthWrite:!1,toneMapped:!1}),t[8]=f,t[9]=o,t[10]=h):h=t[10];let _;return t[11]!==p||t[12]!==h?(_=(0,g.jsx)(`sprite`,{scale:p,children:h}),t[11]=p,t[12]=h,t[13]=_):_=t[13],_}function T(e){f(Array.isArray(e)?e[0]:e)}function E(e){let t=(0,m.c)(29),{entity:n}=e,{visual:r}=n,o=(0,h.useRef)(null),s=(0,h.useRef)(null),c=(0,h.useRef)(null),f;t[0]===Symbol.for(`react.memo_cache_sentinel`)?(f=new a,t[0]=f):f=t[0];let w=(0,h.useRef)(f),T;t[1]===r.texture?T=t[2]:(T=u(r.texture),t[1]=r.texture,t[2]=T);let E=r.crossTexture??r.texture,O;t[3]===E?O=t[4]:(O=u(E),t[3]=E,t[4]=O);let k;t[5]!==T||t[6]!==O?(k=[T,O],t[5]=T,t[6]=O,t[7]=k):k=t[7];let A=l(k,D),j;t[8]===A?j=t[9]:(j=Array.isArray(A)?A:[A,A],t[8]=A,t[9]=j);let[M,N]=j,P;t[10]!==n.direction||t[11]!==n.keyframes?.[0]||t[12]!==r.crossSize||t[13]!==r.crossViewAng||t[14]!==r.renderCross||t[15]!==r.tracerLength||t[16]!==r.tracerWidth?(P=e=>{let{camera:t}=e,i=o.current,a=s.current;if(!i||!a)return;let l=n.keyframes?.[0],u=l?.position,f=n.direction??l?.velocity;if(!u||!f){i.visible=!1,c.current&&(c.current.visible=!1);return}if(p(f,_),_.lengthSq()<1e-8){i.visible=!1,c.current&&(c.current.visible=!1);return}_.normalize(),i.visible=!0,p(u,S),v.copy(S).sub(t.position),y.crossVectors(v,_),y.lengthSq()<1e-8&&(y.crossVectors(C,_),y.lengthSq()<1e-8&&y.set(1,0,0)),y.normalize().multiplyScalar(r.tracerWidth);let m=r.tracerLength*.5;b.copy(_).multiplyScalar(-m),x.copy(_).multiplyScalar(m);let h=a.array;h[0]=b.x+y.x,h[1]=b.y+y.y,h[2]=b.z+y.z,h[3]=b.x-y.x,h[4]=b.y-y.y,h[5]=b.z-y.z,h[6]=x.x-y.x,h[7]=x.y-y.y,h[8]=x.z-y.z,h[9]=x.x+y.x,h[10]=x.y+y.y,h[11]=x.z+y.z,a.needsUpdate=!0;let g=c.current;if(!g)return;if(!r.renderCross){g.visible=!1;return}v.normalize();let T=_.dot(v);if(T>-r.crossViewAng&&T<r.crossViewAng){g.visible=!1;return}g.visible=!0,d(_,w.current),g.quaternion.copy(w.current),g.scale.setScalar(r.crossSize)},t[10]=n.direction,t[11]=n.keyframes?.[0],t[12]=r.crossSize,t[13]=r.crossViewAng,t[14]=r.renderCross,t[15]=r.tracerLength,t[16]=r.tracerWidth,t[17]=P):P=t[17],i(P);let F;t[18]===Symbol.for(`react.memo_cache_sentinel`)?(F=(0,g.jsx)(`bufferAttribute`,{ref:s,attach:`attributes-position`,args:[new Float32Array(12),3]}),t[18]=F):F=t[18];let I;t[19]===Symbol.for(`react.memo_cache_sentinel`)?(I=(0,g.jsx)(`bufferAttribute`,{attach:`attributes-uv`,args:[new Float32Array([0,0,0,1,1,1,1,0]),2]}),t[19]=I):I=t[19];let L;t[20]===Symbol.for(`react.memo_cache_sentinel`)?(L=(0,g.jsxs)(`bufferGeometry`,{children:[F,I,(0,g.jsx)(`bufferAttribute`,{attach:`index`,args:[new Uint16Array([0,1,2,0,2,3]),1]})]}),t[20]=L):L=t[20];let R;t[21]===M?R=t[22]:(R=(0,g.jsxs)(`mesh`,{ref:o,children:[L,(0,g.jsx)(`meshBasicMaterial`,{map:M,transparent:!0,blending:2,side:2,depthWrite:!1,toneMapped:!1})]}),t[21]=M,t[22]=R);let z;t[23]!==N||t[24]!==r.renderCross?(z=r.renderCross?(0,g.jsxs)(`mesh`,{ref:c,children:[(0,g.jsxs)(`bufferGeometry`,{children:[(0,g.jsx)(`bufferAttribute`,{attach:`attributes-position`,args:[new Float32Array([-.5,0,-.5,.5,0,-.5,.5,0,.5,-.5,0,.5]),3]}),(0,g.jsx)(`bufferAttribute`,{attach:`attributes-uv`,args:[new Float32Array([0,0,0,1,1,1,1,0]),2]}),(0,g.jsx)(`bufferAttribute`,{attach:`index`,args:[new Uint16Array([0,1,2,0,2,3]),1]})]}),(0,g.jsx)(`meshBasicMaterial`,{map:N,transparent:!0,blending:2,side:2,depthWrite:!1,toneMapped:!1})]}):null,t[23]=N,t[24]=r.renderCross,t[25]=z):z=t[25];let B;return t[26]!==R||t[27]!==z?(B=(0,g.jsxs)(g.Fragment,{children:[R,z]}),t[26]=R,t[27]=z,t[28]=B):B=t[28],B}function D(e){let t=Array.isArray(e)?e:[e];for(let e of t)f(e)}export{w as SpriteProjectile,E as TracerProjectile};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{t as n}from"./logger-B058WGzf.js";var r=e(t(),1),i=n(`ShapeErrorBoundary`),a=class extends r.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,t){i.error(`Shape load failed: %s %s`,e.message,t.componentStack)}render(){return this.state.hasError?this.props.fallback:this.props.children}};export{a as t};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{t as n}from"./logger-CySD1nLn.js";var r=e(t(),1),i=n(`ShapeErrorBoundary`),a=class extends r.Component{state={hasError:!1};static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(e,t){i.error(`Shape load failed: %s %s`,e.message,t.componentStack)}render(){return this.state.hasError?this.props.fallback:this.props.children}};export{a as t};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
89
docs/assets/StreamingController-DAyX4exD.js
Normal file
89
docs/assets/StreamingController-DAyX4exD.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{a as n,o as r}from"./react-three-fiber.esm-CD18QK1u.js";import{Bt as i,zt as a}from"./three.module-C9W4LJrj.js";var o=e(t()),s=e=>e===Object(e)&&!Array.isArray(e)&&typeof e!=`function`;function c(e,t){let c=r(e=>e.gl),l=n(i,s(e)?Object.values(e):e);return(0,o.useLayoutEffect)(()=>{t?.(l)},[t]),(0,o.useEffect)(()=>{if(`initTexture`in c){let e=[];Array.isArray(l)?e=l:l instanceof a?e=[l]:s(l)&&(e=Object.values(l)),e.forEach(e=>{e instanceof a&&c.initTexture(e)})}},[c,l]),(0,o.useMemo)(()=>{if(s(e)){let t={},n=0;for(let r in e)t[r]=l[n++];return t}else return l},[e,l])}c.preload=e=>n.preload(i,e),c.clear=e=>n.clear(i,e);export{c as t};
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t}from"./jsx-runtime-BpGWiA-R.js";import{o as n,s as r}from"./react-three-fiber.esm-B4ybsNEe.js";import{Bt as i,zt as a}from"./three.module-DeDv86YO.js";var o=e(t()),s=e=>e===Object(e)&&!Array.isArray(e)&&typeof e!=`function`;function c(e,t){let c=r(e=>e.gl),l=n(i,s(e)?Object.values(e):e);return(0,o.useLayoutEffect)(()=>{t?.(l)},[t]),(0,o.useEffect)(()=>{if(`initTexture`in c){let e=[];Array.isArray(l)?e=l:l instanceof a?e=[l]:s(l)&&(e=Object.values(l)),e.forEach(e=>{e instanceof a&&c.initTexture(e)})}},[c,l]),(0,o.useMemo)(()=>{if(s(e)){let t={},n=0;for(let r in e)t[r]=l[n++];return t}else return l},[e,l])}c.preload=e=>n.preload(i,e),c.clear=e=>n.clear(i,e);export{c as t};
|
||||
|
|
@ -1 +1 @@
|
|||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{i as t}from"./react-three-fiber.esm-CD18QK1u.js";import{r as n}from"./SettingsProvider-CeFD8Cx1.js";import{n as r}from"./JoystickContext-D1zeSnLk.js";import{E as i,k as a}from"./index-CcOasdr9.js";var o=e(),s=.004,c=2.5,l=.08,u=.15,d=.15;function f(){let e=(0,o.c)(9),{speedMultiplier:f,touchMode:p,invertDrag:m,invertJoystick:h}=n(),{moveState:g,lookState:_}=r(),v=a(),[,y]=i(),b;return e[0]!==y||e[1]!==m||e[2]!==h||e[3]!==_.current||e[4]!==g.current||e[5]!==v||e[6]!==f||e[7]!==p?(b=(e,t)=>{let{force:n,angle:r}=g.current,{force:i,angle:a}=_.current,o=y().touchLook,b=m?1:-1,x=0,S=0;p===`moveLookStick`&&o&&o.dragging&&(x=b*o.deltaX*s,S=b*o.deltaY*s);let C=0,w=0;if(p===`dualStick`){if(i>u){let e=(i-u)/(1-u),n=Math.cos(a),r=Math.sin(a),o=h?1:-1;x-=o*n*e*c*t,S+=o*r*e*c*t}if(n>l){let e=(n-l)/(1-l),t=Math.cos(r),i=Math.sin(r);C=Math.max(-1,Math.min(1,t*e*(.8*f+.05))),w=Math.max(-1,Math.min(1,i*e*(.8*f+.05)))}}else if(p===`moveLookStick`&&n>0&&(w=Math.min(1,.5*f+.05),n>=d)){let e=Math.cos(r),i=Math.sin(r),a=(n-d)/(1-d),o=h?1:-1;x-=o*e*a*c*.5*t,S+=o*i*a*c*.5*t}!(x!==0||S!==0)&&!(C!==0||w!==0)||v({deltaYaw:x,deltaPitch:S,x:C,y:w,z:0,triggers:[],delta:t})},e[0]=y,e[1]=m,e[2]=h,e[3]=_.current,e[4]=g.current,e[5]=v,e[6]=f,e[7]=p,e[8]=b):b=e[8],t(b),null}export{f as TouchHandler};
|
||||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{a as t}from"./react-three-fiber.esm-B4ybsNEe.js";import{r as n}from"./SettingsProvider-BeB5OnG9.js";import{n as r}from"./JoystickContext-BKqyiaNN.js";import{E as i,k as a}from"./index-BEehCpzM.js";var o=e(),s=.004,c=2.5,l=.08,u=.15,d=.15;function f(){let e=(0,o.c)(9),{speedMultiplier:f,touchMode:p,invertDrag:m,invertJoystick:h}=n(),{moveState:g,lookState:_}=r(),v=a(),[,y]=i(),b;return e[0]!==y||e[1]!==m||e[2]!==h||e[3]!==_.current||e[4]!==g.current||e[5]!==v||e[6]!==f||e[7]!==p?(b=(e,t)=>{let{force:n,angle:r}=g.current,{force:i,angle:a}=_.current,o=y().touchLook,b=m?1:-1,x=0,S=0;p===`moveLookStick`&&o&&o.dragging&&(x=b*o.deltaX*s,S=b*o.deltaY*s);let C=0,w=0;if(p===`dualStick`){if(i>u){let e=(i-u)/(1-u),n=Math.cos(a),r=Math.sin(a),o=h?1:-1;x-=o*n*e*c*t,S+=o*r*e*c*t}if(n>l){let e=(n-l)/(1-l),t=Math.cos(r),i=Math.sin(r);C=Math.max(-1,Math.min(1,t*e*(.8*f+.05))),w=Math.max(-1,Math.min(1,i*e*(.8*f+.05)))}}else if(p===`moveLookStick`&&n>0&&(w=Math.min(1,.5*f+.05),n>=d)){let e=Math.cos(r),i=Math.sin(r),a=(n-d)/(1-d),o=h?1:-1;x-=o*e*a*c*.5*t,S+=o*i*a*c*.5*t}!(x!==0||S!==0)&&!(C!==0||w!==0)||v({deltaYaw:x,deltaPitch:S,x:C,y:w,z:0,triggers:[],delta:t})},e[0]=y,e[1]=m,e[2]=h,e[3]=_.current,e[4]=g.current,e[5]=v,e[6]=f,e[7]=p,e[8]=b):b=e[8],t(b),null}export{f as TouchHandler};
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/nipplejs-cKsequWj.js","assets/chunk-DECur_0Z.js"])))=>i.map(i=>d[i]);
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t,t as n}from"./jsx-runtime-BpGWiA-R.js";import{r}from"./SettingsProvider-CeFD8Cx1.js";import{t as i}from"./preload-helper-BPkniflS.js";import{n as a}from"./JoystickContext-D1zeSnLk.js";var o=e(t(),1),s={Joystick:`_Joystick_155b9_1`,Left:`_Left_155b9_11 _Joystick_155b9_1`,Right:`_Right_155b9_17 _Joystick_155b9_1`},c=n();function l(e){let t=e.querySelector(`.back`);t&&(t.style.background=`rgba(3, 79, 76, 0.6)`,t.style.border=`1px solid rgba(0, 219, 223, 0.5)`,t.style.boxShadow=`inset 0 0 10px rgba(0, 0, 0, 0.7)`);let n=e.querySelector(`.front`);n&&(n.style.background=`radial-gradient(circle at 50% 50%, rgba(23, 247, 198, 0.9) 0%, rgba(9, 184, 170, 0.95) 100%)`,n.style.border=`2px solid rgba(255, 255, 255, 0.4)`,n.style.boxShadow=`0 2px 4px rgba(0, 0, 0, 0.5), 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 -1px 2px rgba(0, 0, 0, 0.3)`)}function u(){let{touchMode:t}=r(),[n,u]=(0,o.useState)(null),[d,f]=(0,o.useState)(null),{moveState:p,lookState:m,setMoveState:h,setLookState:g}=a();(0,o.useEffect)(()=>{if(!n)return;let t=null,r=!1;return i(()=>import(`./nipplejs-cKsequWj.js`).then(t=>e(t.default,1)).then(e=>{r||(t=e.default.create({zone:n,mode:`static`,position:{left:`70px`,bottom:`70px`},size:120,restOpacity:.9,dynamicPage:!0}),l(n),t.on(`move`,(e,t)=>{h({angle:t.angle.radian,force:Math.min(1,t.force)})}),t.on(`end`,()=>{h({force:0})}))}),__vite__mapDeps([0,1])),()=>{r=!0,t?.destroy()}},[p,n,h]),(0,o.useEffect)(()=>{if(!d)return;let t=null,n=!1;return i(()=>import(`./nipplejs-cKsequWj.js`).then(t=>e(t.default,1)).then(e=>{n||(t=e.default.create({zone:d,mode:`static`,position:{right:`70px`,bottom:`70px`},size:120,restOpacity:.9,dynamicPage:!0}),l(d),t.on(`move`,(e,t)=>{g({angle:t.angle.radian,force:Math.min(1,t.force)})}),t.on(`end`,()=>{g({force:0})}))}),__vite__mapDeps([0,1])),()=>{n=!0,t?.destroy()}},[m,d,g]);let _=()=>{document.activeElement instanceof HTMLElement&&document.activeElement.blur()};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(`div`,{ref:u,className:t===`dualStick`?s.Left:s.Joystick,onContextMenu:e=>e.preventDefault(),onTouchStart:_},t),t===`dualStick`?(0,c.jsx)(`div`,{ref:f,className:s.Right,onContextMenu:e=>e.preventDefault(),onTouchStart:_}):null]})}export{u as TouchJoystick};
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/nipplejs-DYp3oo-D.js","assets/chunk-DECur_0Z.js"])))=>i.map(i=>d[i]);
|
||||
import{r as e}from"./chunk-DECur_0Z.js";import{r as t,t as n}from"./jsx-runtime-BpGWiA-R.js";import{r}from"./SettingsProvider-BeB5OnG9.js";import{t as i}from"./preload-helper-CnJ98jGT.js";import{n as a}from"./JoystickContext-BKqyiaNN.js";var o=e(t(),1),s={Joystick:`_Joystick_155b9_1`,Left:`_Left_155b9_11 _Joystick_155b9_1`,Right:`_Right_155b9_17 _Joystick_155b9_1`},c=n();function l(e){let t=e.querySelector(`.back`);t&&(t.style.background=`rgba(3, 79, 76, 0.6)`,t.style.border=`1px solid rgba(0, 219, 223, 0.5)`,t.style.boxShadow=`inset 0 0 10px rgba(0, 0, 0, 0.7)`);let n=e.querySelector(`.front`);n&&(n.style.background=`radial-gradient(circle at 50% 50%, rgba(23, 247, 198, 0.9) 0%, rgba(9, 184, 170, 0.95) 100%)`,n.style.border=`2px solid rgba(255, 255, 255, 0.4)`,n.style.boxShadow=`0 2px 4px rgba(0, 0, 0, 0.5), 0 1px 1px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 -1px 2px rgba(0, 0, 0, 0.3)`)}function u(){let{touchMode:t}=r(),[n,u]=(0,o.useState)(null),[d,f]=(0,o.useState)(null),{moveState:p,lookState:m,setMoveState:h,setLookState:g}=a();(0,o.useEffect)(()=>{if(!n)return;let t=null,r=!1;return i(()=>import(`./nipplejs-DYp3oo-D.js`).then(t=>e(t.default,1)).then(e=>{r||(t=e.default.create({zone:n,mode:`static`,position:{left:`70px`,bottom:`70px`},size:120,restOpacity:.9,dynamicPage:!0}),l(n),t.on(`move`,(e,t)=>{h({angle:t.angle.radian,force:Math.min(1,t.force)})}),t.on(`end`,()=>{h({force:0})}))}),__vite__mapDeps([0,1])),()=>{r=!0,t?.destroy()}},[p,n,h]),(0,o.useEffect)(()=>{if(!d)return;let t=null,n=!1;return i(()=>import(`./nipplejs-DYp3oo-D.js`).then(t=>e(t.default,1)).then(e=>{n||(t=e.default.create({zone:d,mode:`static`,position:{right:`70px`,bottom:`70px`},size:120,restOpacity:.9,dynamicPage:!0}),l(d),t.on(`move`,(e,t)=>{g({angle:t.angle.radian,force:Math.min(1,t.force)})}),t.on(`end`,()=>{g({force:0})}))}),__vite__mapDeps([0,1])),()=>{n=!0,t?.destroy()}},[m,d,g]);let _=()=>{document.activeElement instanceof HTMLElement&&document.activeElement.blur()};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(`div`,{ref:u,className:t===`dualStick`?s.Left:s.Joystick,onContextMenu:e=>e.preventDefault(),onTouchStart:_},t),t===`dualStick`?(0,c.jsx)(`div`,{ref:f,className:s.Right,onContextMenu:e=>e.preventDefault(),onTouchStart:_}):null]})}export{u as TouchJoystick};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{i as t,n}from"./traditional-ec-lUAFC.js";var r=e();function i(e,t=null,n=`feature`){return{targets:e,categoryName:t,tourType:n,currentIndex:0,phase:`traveling`,elapsed:0,phaseDuration:0,curve:null,startPos:null,startQuat:null,orbitCenter:null,orbitRadius:null,orbitStartAngle:0}}var a=t(e=>({animation:null,flyTo(t,n=`feature`){e({animation:i([t],null,n)})},startTour(t,n,r=`feature`){t.length!==0&&e({animation:i(t,n,r)})},advanceTarget(){e(e=>e.animation?{animation:{...e.animation,currentIndex:e.animation.currentIndex+1,phase:`traveling`,elapsed:0,curve:null,startPos:null,startQuat:null,orbitCenter:null,orbitRadius:null,orbitStartAngle:0}}:e)},cancel(){e({animation:null})}}));function o(e,t){return n(a,e,t)}function s(e){let t=(0,r.c)(2),n;return t[0]===e?n=t[1]:(n=t=>{let n=t.animation;return!n||n.tourType!==`debug`?!1:n.targets[n.currentIndex]?.entityId===e},t[0]=e,t[1]=n),o(n)}export{o as n,s as r,a as t};
|
||||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{i as t,n}from"./traditional-DPdbI9gv.js";var r=e();function i(e,t=null,n=`feature`){return{targets:e,categoryName:t,tourType:n,currentIndex:0,phase:`traveling`,elapsed:0,phaseDuration:0,curve:null,startPos:null,startQuat:null,orbitCenter:null,orbitRadius:null,orbitStartAngle:0}}var a=t(e=>({animation:null,flyTo(t,n=`feature`){e({animation:i([t],null,n)})},startTour(t,n,r=`feature`){t.length!==0&&e({animation:i(t,n,r)})},advanceTarget(){e(e=>e.animation?{animation:{...e.animation,currentIndex:e.animation.currentIndex+1,phase:`traveling`,elapsed:0,curve:null,startPos:null,startQuat:null,orbitCenter:null,orbitRadius:null,orbitStartAngle:0}}:e)},cancel(){e({animation:null})}}));function o(e,t){return n(a,e,t)}function s(e){let t=(0,r.c)(2),n;return t[0]===e?n=t[1]:(n=t=>{let n=t.animation;return!n||n.tourType!==`debug`?!1:n.targets[n.currentIndex]?.entityId===e},t[0]=e,t[1]=n),o(n)}export{o as n,s as r,a as t};
|
||||
File diff suppressed because one or more lines are too long
1
docs/assets/demoStreaming-CnoaUA7O.js
Normal file
1
docs/assets/demoStreaming-CnoaUA7O.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{i as t,n}from"./traditional-ec-lUAFC.js";var r=e=>(t,n,r)=>{let i=r.subscribe;return r.subscribe=((e,t,n)=>{let a=e;if(t){let i=n?.equalityFn||Object.is,o=e(r.getState());a=n=>{let r=e(n);if(!i(o,r)){let e=o;t(o=r,e)}},n?.fireImmediately&&t(o,o)}return i(a)}),e(t,n,r)};function i(e){let t=new Map;for(let n of e.state.datablocks.values()){if(n._class!==`tsshapeconstructor`)continue;let e=n.baseshape;if(typeof e!=`string`)continue;let r=e.toLowerCase(),i=r.replace(/\.dts$/i,``)+`_`,a=new Map;for(let e=0;e<=127;e++){let t=n[`sequence${e}`];if(typeof t!=`string`)continue;let r=t.indexOf(` `);if(r===-1)continue;let o=t.slice(0,r).toLowerCase(),s=t.slice(r+1).trim().toLowerCase();if(!s||!o.startsWith(i)||!o.endsWith(`.dsq`))continue;let c=o.slice(i.length,-4);c&&a.set(s,c)}a.size>0&&t.set(r,a)}return t}function a(e,t,n){let r=new Map;for(let n of e){let e=t.clipAction(n);r.set(n.name.toLowerCase(),e)}if(n)for(let[e,t]of n){let n=r.get(t);n&&!r.has(e)&&r.set(e,n)}return r}e();function o(e){return e.toLowerCase()}function s(e){let t=o(e.trim());return t.startsWith(`$`)?t.slice(1):t}function c(e,t,n){return e<t?t:e>n?n:e}function l(e){let t={},n={},r={},i={};for(let n of e.state.objectsById.values())t[n._id]=0,n._name&&(r[o(n._name)]=n._id,n._isDatablock&&(i[o(n._name)]=n._id));for(let t of e.state.globals.keys())n[s(t)]=0;return{objectVersionById:t,globalVersionByName:n,objectIdsByName:r,datablockIdsByName:i}}var u={runtime:{runtime:null,sequenceAliases:new Map,objectVersionById:{},globalVersionByName:{},objectIdsByName:{},datablockIdsByName:{},lastRuntimeTick:0},playback:{recording:null,status:`stopped`,timeMs:0,rate:1,durationMs:0,streamSnapshot:null}},d=t()(r(e=>({...u,setRuntime(t){let n=l(t),r=i(t);e(e=>({...e,runtime:{runtime:t,sequenceAliases:r,objectVersionById:n.objectVersionById,globalVersionByName:n.globalVersionByName,objectIdsByName:n.objectIdsByName,datablockIdsByName:n.datablockIdsByName,lastRuntimeTick:0}}))},clearRuntime(){e(e=>({...e,runtime:{runtime:null,sequenceAliases:new Map,objectVersionById:{},globalVersionByName:{},objectIdsByName:{},datablockIdsByName:{},lastRuntimeTick:0}}))},applyRuntimeBatch(t,n){t.length!==0&&e(e=>{let r={...e.runtime.objectVersionById},i={...e.runtime.globalVersionByName},a={...e.runtime.objectIdsByName},c={...e.runtime.datablockIdsByName},l=e=>{e!=null&&(r[e]=(r[e]??0)+1)};for(let e of t){if(e.type===`object.created`){let t=e.object;if(l(e.objectId),t._name){let n=o(t._name);a[n]=e.objectId,t._isDatablock&&(c[n]=e.objectId)}l(t._parent?._id);continue}if(e.type===`object.deleted`){let t=e.object;if(delete r[e.objectId],t?._name){let e=o(t._name);delete a[e],t._isDatablock&&delete c[e]}l(t?._parent?._id);continue}if(e.type===`field.changed`){l(e.objectId);continue}if(e.type===`global.changed`){let t=s(e.name);i[t]=(i[t]??0)+1;continue}}let u=n?.tick??(e.runtime.lastRuntimeTick>0?e.runtime.lastRuntimeTick+1:1);return{...e,runtime:{...e.runtime,objectVersionById:r,globalVersionByName:i,objectIdsByName:a,datablockIdsByName:c,lastRuntimeTick:u}}})},setRecording(t){let n=Math.max(0,(t?.duration??0)*1e3);e(e=>({...e,playback:{recording:t,status:t?`stopped`:e.playback.status,timeMs:t?0:e.playback.timeMs,rate:t?1:e.playback.rate,durationMs:n,streamSnapshot:t?null:e.playback.streamSnapshot}}))},setPlaybackTime(t){e(e=>{let n=c(t,0,e.playback.durationMs);return{...e,playback:{...e.playback,timeMs:n}}})},setPlaybackStatus(t){e(e=>({...e,playback:{...e.playback,status:t}}))},setPlaybackRate(t){let n=Number.isFinite(t)?c(t,.01,16):1;e(e=>({...e,playback:{...e.playback,rate:n}}))},setPlaybackStreamSnapshot(t){e(e=>({...e,playback:{...e.playback,streamSnapshot:t}}))}}))),f=0;function p(){return f}function m(e,t){f+=e*t*1e3}function h(){f=0}d.subscribe(e=>e.playback.status,e=>{e===`stopped`&&h()});function g(){return d}function _(e,t){return n(d,e,t)}export{g as a,_ as i,p as n,a as o,d as r,r as s,m as t};
|
||||
import{n as e}from"./jsx-runtime-BpGWiA-R.js";import{i as t,n}from"./traditional-DPdbI9gv.js";var r=e=>(t,n,r)=>{let i=r.subscribe;return r.subscribe=((e,t,n)=>{let a=e;if(t){let i=n?.equalityFn||Object.is,o=e(r.getState());a=n=>{let r=e(n);if(!i(o,r)){let e=o;t(o=r,e)}},n?.fireImmediately&&t(o,o)}return i(a)}),e(t,n,r)};function i(e){let t=new Map;for(let n of e.state.datablocks.values()){if(n._class!==`tsshapeconstructor`)continue;let e=n.baseshape;if(typeof e!=`string`)continue;let r=e.toLowerCase(),i=r.replace(/\.dts$/i,``)+`_`,a=new Map;for(let e=0;e<=127;e++){let t=n[`sequence${e}`];if(typeof t!=`string`)continue;let r=t.indexOf(` `);if(r===-1)continue;let o=t.slice(0,r).toLowerCase(),s=t.slice(r+1).trim().toLowerCase();if(!s||!o.startsWith(i)||!o.endsWith(`.dsq`))continue;let c=o.slice(i.length,-4);c&&a.set(s,c)}a.size>0&&t.set(r,a)}return t}function a(e,t,n){let r=new Map;for(let n of e){let e=t.clipAction(n);r.set(n.name.toLowerCase(),e)}if(n)for(let[e,t]of n){let n=r.get(t);n&&!r.has(e)&&r.set(e,n)}return r}e();function o(e){return e.toLowerCase()}function s(e){let t=o(e.trim());return t.startsWith(`$`)?t.slice(1):t}function c(e,t,n){return e<t?t:e>n?n:e}function l(e){let t={},n={},r={},i={};for(let n of e.state.objectsById.values())t[n._id]=0,n._name&&(r[o(n._name)]=n._id,n._isDatablock&&(i[o(n._name)]=n._id));for(let t of e.state.globals.keys())n[s(t)]=0;return{objectVersionById:t,globalVersionByName:n,objectIdsByName:r,datablockIdsByName:i}}var u={runtime:{runtime:null,sequenceAliases:new Map,objectVersionById:{},globalVersionByName:{},objectIdsByName:{},datablockIdsByName:{},lastRuntimeTick:0},playback:{recording:null,status:`stopped`,timeMs:0,rate:1,durationMs:0,streamSnapshot:null}},d=t()(r(e=>({...u,setRuntime(t){let n=l(t),r=i(t);e(e=>({...e,runtime:{runtime:t,sequenceAliases:r,objectVersionById:n.objectVersionById,globalVersionByName:n.globalVersionByName,objectIdsByName:n.objectIdsByName,datablockIdsByName:n.datablockIdsByName,lastRuntimeTick:0}}))},clearRuntime(){e(e=>({...e,runtime:{runtime:null,sequenceAliases:new Map,objectVersionById:{},globalVersionByName:{},objectIdsByName:{},datablockIdsByName:{},lastRuntimeTick:0}}))},applyRuntimeBatch(t,n){t.length!==0&&e(e=>{let r={...e.runtime.objectVersionById},i={...e.runtime.globalVersionByName},a={...e.runtime.objectIdsByName},c={...e.runtime.datablockIdsByName},l=e=>{e!=null&&(r[e]=(r[e]??0)+1)};for(let e of t){if(e.type===`object.created`){let t=e.object;if(l(e.objectId),t._name){let n=o(t._name);a[n]=e.objectId,t._isDatablock&&(c[n]=e.objectId)}l(t._parent?._id);continue}if(e.type===`object.deleted`){let t=e.object;if(delete r[e.objectId],t?._name){let e=o(t._name);delete a[e],t._isDatablock&&delete c[e]}l(t?._parent?._id);continue}if(e.type===`field.changed`){l(e.objectId);continue}if(e.type===`global.changed`){let t=s(e.name);i[t]=(i[t]??0)+1;continue}}let u=n?.tick??(e.runtime.lastRuntimeTick>0?e.runtime.lastRuntimeTick+1:1);return{...e,runtime:{...e.runtime,objectVersionById:r,globalVersionByName:i,objectIdsByName:a,datablockIdsByName:c,lastRuntimeTick:u}}})},setRecording(t){let n=Math.max(0,(t?.duration??0)*1e3);e(e=>({...e,playback:{recording:t,status:t?`stopped`:e.playback.status,timeMs:t?0:e.playback.timeMs,rate:t?1:e.playback.rate,durationMs:n,streamSnapshot:t?null:e.playback.streamSnapshot}}))},setPlaybackTime(t){e(e=>{let n=c(t,0,e.playback.durationMs);return{...e,playback:{...e.playback,timeMs:n}}})},setPlaybackStatus(t){e(e=>({...e,playback:{...e.playback,status:t}}))},setPlaybackRate(t){let n=Number.isFinite(t)?c(t,.01,16):1;e(e=>({...e,playback:{...e.playback,rate:n}}))},setPlaybackStreamSnapshot(t){e(e=>({...e,playback:{...e.playback,streamSnapshot:t}}))}}))),f=0;function p(){return f}function m(e,t){f+=e*t*1e3}function h(){f=0}d.subscribe(e=>e.playback.status,e=>{e===`stopped`&&h()});function g(){return d}function _(e,t){return n(d,e,t)}export{g as a,_ as i,p as n,a as o,d as r,r as s,m as t};
|
||||
|
|
@ -1 +1 @@
|
|||
import{t as e}from"./iconBase-BCRUFbxq.js";function t(t){return e({tag:`svg`,attr:{viewBox:`0 0 24 24`},child:[{tag:`path`,attr:{fill:`none`,strokeWidth:`2`,d:`M3,22.0000002 L21,12 L3,2 L3,22.0000002 Z M5,19 L17.5999998,11.9999999 L5,5 L5,19 Z M7,16 L14.1999999,12 L7,8 L7,16 Z M9,13 L10.8,12 L9,11 L9,13 Z`},child:[]}]})(t)}function n(t){return e({tag:`svg`,attr:{viewBox:`0 0 24 24`},child:[{tag:`path`,attr:{fill:`none`,strokeWidth:`2`,d:`M3,21 L9,21 L9,3 L3,3 L3,21 Z M4,19 L8,19 L8,5 L4,5 L4,19 Z M5,17 L7,17 L7,7 L5,7 L5,17 Z M15,21 L21,21 L21,3 L15,3 L15,21 Z M16,19 L20,19 L20,5 L16,5 L16,19 Z M17,17 L19,17 L19,7 L17,7 L17,17 Z`},child:[]}]})(t)}export{t as n,n as t};
|
||||
import{t as e}from"./iconBase-DjT_EJem.js";function t(t){return e({tag:`svg`,attr:{viewBox:`0 0 24 24`},child:[{tag:`path`,attr:{fill:`none`,strokeWidth:`2`,d:`M3,22.0000002 L21,12 L3,2 L3,22.0000002 Z M5,19 L17.5999998,11.9999999 L5,5 L5,19 Z M7,16 L14.1999999,12 L7,8 L7,16 Z M9,13 L10.8,12 L9,11 L9,13 Z`},child:[]}]})(t)}function n(t){return e({tag:`svg`,attr:{viewBox:`0 0 24 24`},child:[{tag:`path`,attr:{fill:`none`,strokeWidth:`2`,d:`M3,21 L9,21 L9,3 L3,3 L3,21 Z M4,19 L8,19 L8,5 L4,5 L4,19 Z M5,17 L7,17 L7,7 L5,7 L5,17 Z M15,21 L21,21 L21,3 L15,3 L15,21 Z M16,19 L20,19 L20,5 L16,5 L16,19 Z M17,17 L19,17 L19,7 L17,7 L17,17 Z`},child:[]}]})(t)}export{t as n,n as t};
|
||||
13
docs/assets/index-BEehCpzM.js
Normal file
13
docs/assets/index-BEehCpzM.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
import{r as e,t}from"./chunk-DECur_0Z.js";import{t as n}from"./logger-B058WGzf.js";function r(e){let t=e.indexOf(`::`);return t===-1?null:{namespace:e.slice(0,t),method:e.slice(t+2)}}var i={"+":`$.add`,"-":`$.sub`,"*":`$.mul`,"/":`$.div`,"<":`$.lt`,"<=":`$.le`,">":`$.gt`,">=":`$.ge`,"==":`$.eq`,"!=":`$.ne`,"%":`$.mod`,"&":`$.bitand`,"|":`$.bitor`,"^":`$.bitxor`,"<<":`$.shl`,">>":`$.shr`},a=class{indent;runtime;functions;globals;locals;indentLevel=0;currentClass=null;currentFunction=null;constructor(e={}){this.indent=e.indent??` `,this.runtime=e.runtime??`$`,this.functions=e.functions??`$f`,this.globals=e.globals??`$g`,this.locals=e.locals??`$l`}getAccessInfo(e){if(e.type===`Variable`){let t=JSON.stringify(e.name),n=e.scope===`global`?this.globals:this.locals;return{getter:`${n}.get(${t})`,setter:e=>`${n}.set(${t}, ${e})`,postIncHelper:`${n}.postInc(${t})`,postDecHelper:`${n}.postDec(${t})`}}if(e.type===`MemberExpression`){let t=this.expression(e.object),n=e.property.type===`Identifier`?JSON.stringify(e.property.name):this.expression(e.property);return{getter:`${this.runtime}.prop(${t}, ${n})`,setter:e=>`${this.runtime}.setProp(${t}, ${n}, ${e})`,postIncHelper:`${this.runtime}.propPostInc(${t}, ${n})`,postDecHelper:`${this.runtime}.propPostDec(${t}, ${n})`}}if(e.type===`IndexExpression`){let t=Array.isArray(e.index)?e.index.map(e=>this.expression(e)):[this.expression(e.index)];if(e.object.type===`Variable`){let n=JSON.stringify(e.object.name),r=e.object.scope===`global`?this.globals:this.locals,i=t.join(`, `);return{getter:`${r}.get(${n}, ${i})`,setter:e=>`${r}.set(${n}, ${i}, ${e})`,postIncHelper:`${r}.postInc(${n}, ${i})`,postDecHelper:`${r}.postDec(${n}, ${i})`}}if(e.object.type===`MemberExpression`){let n=e.object,r=this.expression(n.object),i=n.property.type===`Identifier`?JSON.stringify(n.property.name):this.expression(n.property),a=`${this.runtime}.key(${i}, ${t.join(`, `)})`;return{getter:`${this.runtime}.prop(${r}, ${a})`,setter:e=>`${this.runtime}.setProp(${r}, ${a}, ${e})`,postIncHelper:`${this.runtime}.propPostInc(${r}, ${a})`,postDecHelper:`${this.runtime}.propPostDec(${r}, ${a})`}}let n=this.expression(e.object),r=t.length===1?t[0]:`${this.runtime}.key(${t.join(`, `)})`;return{getter:`${this.runtime}.getIndex(${n}, ${r})`,setter:e=>`${this.runtime}.setIndex(${n}, ${r}, ${e})`,postIncHelper:`${this.runtime}.indexPostInc(${n}, ${r})`,postDecHelper:`${this.runtime}.indexPostDec(${n}, ${r})`}}return null}generate(e){let t=[];for(let n of e.body){let e=this.statement(n);e&&t.push(e)}return t.join(`
|
||||
import{r as e,t}from"./chunk-DECur_0Z.js";import{t as n}from"./logger-CySD1nLn.js";function r(e){let t=e.indexOf(`::`);return t===-1?null:{namespace:e.slice(0,t),method:e.slice(t+2)}}var i={"+":`$.add`,"-":`$.sub`,"*":`$.mul`,"/":`$.div`,"<":`$.lt`,"<=":`$.le`,">":`$.gt`,">=":`$.ge`,"==":`$.eq`,"!=":`$.ne`,"%":`$.mod`,"&":`$.bitand`,"|":`$.bitor`,"^":`$.bitxor`,"<<":`$.shl`,">>":`$.shr`},a=class{indent;runtime;functions;globals;locals;indentLevel=0;currentClass=null;currentFunction=null;constructor(e={}){this.indent=e.indent??` `,this.runtime=e.runtime??`$`,this.functions=e.functions??`$f`,this.globals=e.globals??`$g`,this.locals=e.locals??`$l`}getAccessInfo(e){if(e.type===`Variable`){let t=JSON.stringify(e.name),n=e.scope===`global`?this.globals:this.locals;return{getter:`${n}.get(${t})`,setter:e=>`${n}.set(${t}, ${e})`,postIncHelper:`${n}.postInc(${t})`,postDecHelper:`${n}.postDec(${t})`}}if(e.type===`MemberExpression`){let t=this.expression(e.object),n=e.property.type===`Identifier`?JSON.stringify(e.property.name):this.expression(e.property);return{getter:`${this.runtime}.prop(${t}, ${n})`,setter:e=>`${this.runtime}.setProp(${t}, ${n}, ${e})`,postIncHelper:`${this.runtime}.propPostInc(${t}, ${n})`,postDecHelper:`${this.runtime}.propPostDec(${t}, ${n})`}}if(e.type===`IndexExpression`){let t=Array.isArray(e.index)?e.index.map(e=>this.expression(e)):[this.expression(e.index)];if(e.object.type===`Variable`){let n=JSON.stringify(e.object.name),r=e.object.scope===`global`?this.globals:this.locals,i=t.join(`, `);return{getter:`${r}.get(${n}, ${i})`,setter:e=>`${r}.set(${n}, ${i}, ${e})`,postIncHelper:`${r}.postInc(${n}, ${i})`,postDecHelper:`${r}.postDec(${n}, ${i})`}}if(e.object.type===`MemberExpression`){let n=e.object,r=this.expression(n.object),i=n.property.type===`Identifier`?JSON.stringify(n.property.name):this.expression(n.property),a=`${this.runtime}.key(${i}, ${t.join(`, `)})`;return{getter:`${this.runtime}.prop(${r}, ${a})`,setter:e=>`${this.runtime}.setProp(${r}, ${a}, ${e})`,postIncHelper:`${this.runtime}.propPostInc(${r}, ${a})`,postDecHelper:`${this.runtime}.propPostDec(${r}, ${a})`}}let n=this.expression(e.object),r=t.length===1?t[0]:`${this.runtime}.key(${t.join(`, `)})`;return{getter:`${this.runtime}.getIndex(${n}, ${r})`,setter:e=>`${this.runtime}.setIndex(${n}, ${r}, ${e})`,postIncHelper:`${this.runtime}.indexPostInc(${n}, ${r})`,postDecHelper:`${this.runtime}.indexPostDec(${n}, ${r})`}}return null}generate(e){let t=[];for(let n of e.body){let e=this.statement(n);e&&t.push(e)}return t.join(`
|
||||
|
||||
`)}statement(e){switch(e.type){case`Comment`:return``;case`ExpressionStatement`:return this.line(`${this.expression(e.expression)};`);case`FunctionDeclaration`:return this.functionDeclaration(e);case`PackageDeclaration`:return this.packageDeclaration(e);case`DatablockDeclaration`:return this.datablockDeclaration(e);case`ObjectDeclaration`:return this.line(`${this.objectDeclaration(e)};`);case`IfStatement`:return this.ifStatement(e);case`ForStatement`:return this.forStatement(e);case`WhileStatement`:return this.whileStatement(e);case`DoWhileStatement`:return this.doWhileStatement(e);case`SwitchStatement`:return this.switchStatement(e);case`ReturnStatement`:return this.returnStatement(e);case`BreakStatement`:return this.line(`break;`);case`ContinueStatement`:return this.line(`continue;`);case`BlockStatement`:return this.blockStatement(e);default:throw Error(`Unknown statement type: ${e.type}`)}}functionDeclaration(e){let t=r(e.name.name);if(t){let n=t.namespace,r=t.method;this.currentClass=n.toLowerCase(),this.currentFunction=r.toLowerCase();let i=this.functionBody(e.body,e.params);return this.currentClass=null,this.currentFunction=null,`${this.line(`${this.runtime}.registerMethod(${JSON.stringify(n)}, ${JSON.stringify(r)}, function() {`)}\n${i}\n${this.line(`});`)}`}else{let t=e.name.name;this.currentFunction=t.toLowerCase();let n=this.functionBody(e.body,e.params);return this.currentFunction=null,`${this.line(`${this.runtime}.registerFunction(${JSON.stringify(t)}, function() {`)}\n${n}\n${this.line(`});`)}`}}functionBody(e,t){this.indentLevel++;let n=[];n.push(this.line(`const ${this.locals} = ${this.runtime}.locals();`));for(let e=0;e<t.length;e++)n.push(this.line(`${this.locals}.set(${JSON.stringify(t[e].name)}, arguments[${e}]);`));for(let t of e.body)n.push(this.statement(t));return this.indentLevel--,n.join(`
|
||||
`)}packageDeclaration(e){let t=JSON.stringify(e.name.name);this.indentLevel++;let n=e.body.map(e=>this.statement(e)).join(`
|
||||
47
docs/assets/playbackUtils-D5VkIMBR.js
Normal file
47
docs/assets/playbackUtils-D5VkIMBR.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
docs/assets/react-three-fiber.esm-B4ybsNEe.js
Normal file
12
docs/assets/react-three-fiber.esm-B4ybsNEe.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{t as e}from"./logger-B058WGzf.js";import{Ct as t,rt as n}from"./three.module-C9W4LJrj.js";var r=e(`ghostToScene`);function i(e,t={x:0,y:0,z:0}){return e&&typeof e==`object`&&`x`in e?e:t}function a(e,t={r:0,g:0,b:0}){return e&&typeof e==`object`&&`r`in e?e:t}function o(e,t={r:.5,g:.5,b:.5,a:1}){return e&&typeof e==`object`&&`r`in e?e:t}function s(e){if(e&&typeof e==`object`&&`elements`in e&&Array.isArray(e.elements))return e;if(e&&typeof e==`object`&&`position`in e&&`rotation`in e){let{position:t,rotation:n}=e,r=n.x*n.x,i=n.y*n.y,a=n.z*n.z,o=n.x*n.y,s=n.x*n.z,c=n.y*n.z,l=n.w*n.x,u=n.w*n.y,d=n.w*n.z;return{elements:[1-2*(i+a),2*(o+d),2*(s-u),0,2*(o-d),1-2*(r+a),2*(c+l),0,2*(s+u),2*(c-l),1-2*(r+i),0,t.x,t.y,t.z,1],position:{x:t.x,y:t.y,z:t.z}}}return{elements:[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],position:{x:0,y:0,z:0}}}function c(e,t){return{className:`TerrainBlock`,ghostIndex:e,terrFileName:t.terrFileName??``,detailTextureName:t.detailTextureName??``,squareSize:t.squareSize??8,emptySquareRuns:t.emptySquareRuns}}function l(e,t){return{className:`InteriorInstance`,ghostIndex:e,interiorFile:t.interiorFile??``,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1}),showTerrainInside:t.showTerrainInside??!1,skinBase:t.skinBase??``,alarmState:t.alarmState??!1}}function u(e,t){return{className:`TSStatic`,ghostIndex:e,shapeName:t.shapeName??``,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1})}}function d(e,t){let n=Array.isArray(t.fogVolumes)?t.fogVolumes.map(e=>({visibleDistance:e.visibleDistance??0,minHeight:e.minHeight??0,maxHeight:e.maxHeight??0,color:a(e.color)})):[],r=Array.isArray(t.cloudLayers)?t.cloudLayers.map(e=>({texture:e.texture??``,heightPercent:e.heightPercent??0,speed:e.speed??0})):[];return{className:`Sky`,ghostIndex:e,materialList:t.materialList??``,fogColor:a(t.fogColor),visibleDistance:t.visibleDistance??1e3,fogDistance:t.fogDistance??0,skySolidColor:a(t.skySolidColor),useSkyTextures:t.useSkyTextures??!0,fogVolumes:n,cloudLayers:r,windVelocity:i(t.windVelocity)}}function f(e,t){return{className:`Sun`,ghostIndex:e,direction:i(t.direction,{x:.57735,y:.57735,z:-.57735}),color:o(t.color,{r:.7,g:.7,b:.7,a:1}),ambient:o(t.ambient,{r:.5,g:.5,b:.5,a:1}),textures:Array.isArray(t.textures)?t.textures:void 0}}function p(e,t){return{className:`MissionArea`,ghostIndex:e,area:t.area??{x:-512,y:-512,w:1024,h:1024},flightCeiling:t.flightCeiling??2e3,flightCeilingRange:t.flightCeilingRange??50}}function m(e,t){return{className:`WaterBlock`,ghostIndex:e,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1}),surfaceName:t.surfaceName??``,envMapName:t.envMapName??``,surfaceOpacity:t.surfaceOpacity??.75,waveMagnitude:t.waveMagnitude??1,envMapIntensity:t.envMapIntensity??1}}function h(e,t,n){let i;switch(e){case`TerrainBlock`:return i=c(t,n),r.debug(`TerrainBlock #%d: terrFileName=%s`,t,i.terrFileName),i;case`InteriorInstance`:return i=l(t,n),r.debug(`InteriorInstance #%d: interiorFile=%s`,t,i.interiorFile),i;case`TSStatic`:return u(t,n);case`Sky`:{i=d(t,n);let e=i;return r.debug(`Sky #%d: materialList=%s fogColor=(%s, %s, %s) visibleDist=%d fogDist=%d useSkyTextures=%s`,t,e.materialList,e.fogColor.r.toFixed(3),e.fogColor.g.toFixed(3),e.fogColor.b.toFixed(3),e.visibleDistance,e.fogDistance,e.useSkyTextures),i}case`Sun`:{i=f(t,n);let e=i;return r.debug(`Sun #%d: dir=(%s, %s, %s) color=(%s, %s, %s) ambient=(%s, %s, %s)`,t,e.direction.x.toFixed(3),e.direction.y.toFixed(3),e.direction.z.toFixed(3),e.color.r.toFixed(3),e.color.g.toFixed(3),e.color.b.toFixed(3),e.ambient.r.toFixed(3),e.ambient.g.toFixed(3),e.ambient.b.toFixed(3)),i}case`MissionArea`:return p(t,n);case`WaterBlock`:return m(t,n);default:return null}}function g(e){return[e.y,e.z,e.x]}function _(e){return[e.y,e.z,e.x]}function v(e){let r=e.elements,i=new n,a=i.elements;a[0]=r[5],a[1]=r[6],a[2]=r[4],a[3]=0,a[4]=r[9],a[5]=r[10],a[6]=r[8],a[7]=0,a[8]=r[1],a[9]=r[2],a[10]=r[0],a[11]=0,a[12]=0,a[13]=0,a[14]=0,a[15]=1;let o=new t;return o.setFromRotationMatrix(i),o.conjugate(),o}export{h as i,_ as n,g as r,v as t};
|
||||
import{t as e}from"./logger-CySD1nLn.js";import{Ct as t,rt as n}from"./three.module-DeDv86YO.js";var r=e(`ghostToScene`);function i(e,t={x:0,y:0,z:0}){return e&&typeof e==`object`&&`x`in e?e:t}function a(e,t={r:0,g:0,b:0}){return e&&typeof e==`object`&&`r`in e?e:t}function o(e,t={r:.5,g:.5,b:.5,a:1}){return e&&typeof e==`object`&&`r`in e?e:t}function s(e){if(e&&typeof e==`object`&&`elements`in e&&Array.isArray(e.elements))return e;if(e&&typeof e==`object`&&`position`in e&&`rotation`in e){let{position:t,rotation:n}=e,r=n.x*n.x,i=n.y*n.y,a=n.z*n.z,o=n.x*n.y,s=n.x*n.z,c=n.y*n.z,l=n.w*n.x,u=n.w*n.y,d=n.w*n.z;return{elements:[1-2*(i+a),2*(o+d),2*(s-u),0,2*(o-d),1-2*(r+a),2*(c+l),0,2*(s+u),2*(c-l),1-2*(r+i),0,t.x,t.y,t.z,1],position:{x:t.x,y:t.y,z:t.z}}}return{elements:[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],position:{x:0,y:0,z:0}}}function c(e,t){return{className:`TerrainBlock`,ghostIndex:e,terrFileName:t.terrFileName??``,detailTextureName:t.detailTextureName??``,squareSize:t.squareSize??8,emptySquareRuns:t.emptySquareRuns}}function l(e,t){return{className:`InteriorInstance`,ghostIndex:e,interiorFile:t.interiorFile??``,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1}),showTerrainInside:t.showTerrainInside??!1,skinBase:t.skinBase??``,alarmState:t.alarmState??!1}}function u(e,t){return{className:`TSStatic`,ghostIndex:e,shapeName:t.shapeName??``,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1})}}function d(e,t){let n=Array.isArray(t.fogVolumes)?t.fogVolumes.map(e=>({visibleDistance:e.visibleDistance??0,minHeight:e.minHeight??0,maxHeight:e.maxHeight??0,color:a(e.color)})):[],r=Array.isArray(t.cloudLayers)?t.cloudLayers.map(e=>({texture:e.texture??``,heightPercent:e.heightPercent??0,speed:e.speed??0})):[];return{className:`Sky`,ghostIndex:e,materialList:t.materialList??``,fogColor:a(t.fogColor),visibleDistance:t.visibleDistance??1e3,fogDistance:t.fogDistance??0,skySolidColor:a(t.skySolidColor),useSkyTextures:t.useSkyTextures??!0,fogVolumes:n,cloudLayers:r,windVelocity:i(t.windVelocity)}}function f(e,t){return{className:`Sun`,ghostIndex:e,direction:i(t.direction,{x:.57735,y:.57735,z:-.57735}),color:o(t.color,{r:.7,g:.7,b:.7,a:1}),ambient:o(t.ambient,{r:.5,g:.5,b:.5,a:1}),textures:Array.isArray(t.textures)?t.textures:void 0}}function p(e,t){return{className:`MissionArea`,ghostIndex:e,area:t.area??{x:-512,y:-512,w:1024,h:1024},flightCeiling:t.flightCeiling??2e3,flightCeilingRange:t.flightCeilingRange??50}}function m(e,t){return{className:`WaterBlock`,ghostIndex:e,transform:s(t.transform),scale:i(t.scale,{x:1,y:1,z:1}),surfaceName:t.surfaceName??``,envMapName:t.envMapName??``,surfaceOpacity:t.surfaceOpacity??.75,waveMagnitude:t.waveMagnitude??1,envMapIntensity:t.envMapIntensity??1}}function h(e,t,n){let i;switch(e){case`TerrainBlock`:return i=c(t,n),r.debug(`TerrainBlock #%d: terrFileName=%s`,t,i.terrFileName),i;case`InteriorInstance`:return i=l(t,n),r.debug(`InteriorInstance #%d: interiorFile=%s`,t,i.interiorFile),i;case`TSStatic`:return u(t,n);case`Sky`:{i=d(t,n);let e=i;return r.debug(`Sky #%d: materialList=%s fogColor=(%s, %s, %s) visibleDist=%d fogDist=%d useSkyTextures=%s`,t,e.materialList,e.fogColor.r.toFixed(3),e.fogColor.g.toFixed(3),e.fogColor.b.toFixed(3),e.visibleDistance,e.fogDistance,e.useSkyTextures),i}case`Sun`:{i=f(t,n);let e=i;return r.debug(`Sun #%d: dir=(%s, %s, %s) color=(%s, %s, %s) ambient=(%s, %s, %s)`,t,e.direction.x.toFixed(3),e.direction.y.toFixed(3),e.direction.z.toFixed(3),e.color.r.toFixed(3),e.color.g.toFixed(3),e.color.b.toFixed(3),e.ambient.r.toFixed(3),e.ambient.g.toFixed(3),e.ambient.b.toFixed(3)),i}case`MissionArea`:return p(t,n);case`WaterBlock`:return m(t,n);default:return null}}function g(e){return[e.y,e.z,e.x]}function _(e){return[e.y,e.z,e.x]}function v(e){let r=e.elements,i=new n,a=i.elements;a[0]=r[5],a[1]=r[6],a[2]=r[4],a[3]=0,a[4]=r[9],a[5]=r[10],a[6]=r[8],a[7]=0,a[8]=r[1],a[9]=r[2],a[10]=r[0],a[11]=0,a[12]=0,a[13]=0,a[14]=0,a[15]=1;let o=new t;return o.setFromRotationMatrix(i),o.conjugate(),o}export{h as i,_ as n,g as r,v as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{i as e}from"./traditional-ec-lUAFC.js";var t=e()(()=>({time:0,playback:null,root:null,cameraMode:`original`,orbitOverrideYaw:0,orbitOverridePitch:0}));function n(){t.setState({time:0,playback:null,cameraMode:`original`,orbitOverrideYaw:0,orbitOverridePitch:0})}export{t as n,n as t};
|
||||
import{i as e}from"./traditional-DPdbI9gv.js";var t=e()(()=>({time:0,playback:null,root:null,cameraMode:`original`,orbitOverrideYaw:0,orbitOverridePitch:0}));function n(){t.setState({time:0,playback:null,cameraMode:`original`,orbitOverrideYaw:0,orbitOverridePitch:0})}export{t as n,n as t};
|
||||
1
docs/assets/stringUtils-Bvtx11IK.js
Normal file
1
docs/assets/stringUtils-Bvtx11IK.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
function e(e){return e.replace(/\\/g,`/`).replace(/\/+/g,`/`)}function t(e){return`${e.toLocaleString()} ms`}var n={1:`Storm`,2:`Inferno`,3:`Starwolf`,4:`Diamond Sword`,5:`Blood Eagle`,6:`Phoenix`},r={1:`base`,2:`baseb`,3:`swolf`,4:`dsword`,5:`beagle`,6:`cotp`};function i(e,t){let n=``,r=e.datablock,i=r?t.get(String(r)):void 0;if(e.nametag!=null&&String(e.nametag)!==``)n=String(e.nametag);else if(i){let e=i.targetnametag==null?``:String(i.targetnametag);e!==``&&!e.startsWith(`_`)&&(n=e)}if(i){let e=i.targettypetag==null?``:String(i.targettypetag);if(e!==``&&!e.startsWith(`_`))return n!==``&&n.toLowerCase()!==e.toLowerCase()?a(`${n} ${e}`):a(e)}if(!n&&i){let e=i.pickupname==null?``:String(i.pickupname);if(e)return o(e)}return a(n)}function a(e){return e.replace(/\x01(\d+)/g,`<#$1>`)}function o(e){let t=e.replace(/^(an?\s+|some\s+)/i,``);return t?t.split(` `).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `):e}export{e as a,i,n,t as r,r as t};
|
||||
|
|
@ -1 +0,0 @@
|
|||
function e(e){return e.replace(/\\/g,`/`).replace(/\/+/g,`/`)}function t(e){return`${e.toLocaleString()} ms`}var n={1:`Storm`,2:`Inferno`,3:`Starwolf`,4:`Diamond Sword`,5:`Blood Eagle`,6:`Phoenix`};function r(e,t){let n=``,r=e.datablock,o=r?t.get(String(r)):void 0;if(e.nametag!=null&&String(e.nametag)!==``)n=String(e.nametag);else if(o){let e=o.targetnametag==null?``:String(o.targetnametag);e!==``&&!e.startsWith(`_`)&&(n=e)}if(o){let e=o.targettypetag==null?``:String(o.targettypetag);if(e!==``&&!e.startsWith(`_`))return n!==``&&n.toLowerCase()!==e.toLowerCase()?i(`${n} ${e}`):i(e)}if(!n&&o){let e=o.pickupname==null?``:String(o.pickupname);if(e)return a(e)}return i(n)}function i(e){return e.replace(/\x01(\d+)/g,`<#$1>`)}function a(e){let t=e.replace(/^(an?\s+|some\s+)/i,``);return t?t.split(` `).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join(` `):e}export{e as i,t as n,r,n as t};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as l}from"./three.module-C9W4LJrj.js";var u=new a;u.setOptions({premultiplyAlpha:`none`});var d=new Map;function f(e,t){let n=d.get(e);if(n)return t&&n.image&&t(n),n;let r=new l;return r.flipY=!1,d.set(e,r),u.load(e,e=>{r.image=e,r.needsUpdate=!0,t?.(r)}),r}function p(e){let t=d.get(e);return t?t.image?Promise.resolve(t):new Promise(e=>{let n=()=>{t.image?e(t):setTimeout(n,16)};n()}):new Promise((t,n)=>{let r=new l;r.flipY=!1,d.set(e,r),u.load(e,e=>{r.image=e,r.needsUpdate=!0,t(r)},void 0,n)})}function m(e,t={}){let{repeat:n=[1,1],disableMipmaps:a=!1,anisotropy:c,noColorSpace:l=!1}=t;return e.wrapS=e.wrapT=i,l||(e.colorSpace=o),e.repeat.set(...n),e.flipY=!1,e.anisotropy=c??1,a?(e.generateMipmaps=!1,e.minFilter=s):(e.generateMipmaps=!0,e.minFilter=r),e.magFilter=s,e.needsUpdate=!0,e}function h(r){let a=new e(r,256,256,t,n);return a.colorSpace=``,a.wrapS=a.wrapT=i,a.generateMipmaps=!1,a.minFilter=s,a.magFilter=s,a.needsUpdate=!0,a}var g=`
|
||||
import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as l}from"./three.module-DeDv86YO.js";var u=`
|
||||
#ifdef USE_FOG
|
||||
// Check fog enabled uniform - allows toggling without shader recompilation
|
||||
#ifdef USE_VOLUMETRIC_FOG
|
||||
|
|
@ -119,7 +119,7 @@ import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as
|
|||
} // end fogEnabled check
|
||||
#endif
|
||||
#endif
|
||||
`;function _(){c.fog_pars_fragment=`
|
||||
`;function d(){c.fog_pars_fragment=`
|
||||
#ifdef USE_FOG
|
||||
uniform vec3 fogColor;
|
||||
varying float vFogDepth;
|
||||
|
|
@ -150,7 +150,7 @@ import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as
|
|||
#endif
|
||||
|
||||
#endif
|
||||
`,c.fog_fragment=g,c.fog_pars_vertex=`
|
||||
`,c.fog_fragment=u,c.fog_pars_vertex=`
|
||||
#ifdef USE_FOG
|
||||
varying float vFogDepth;
|
||||
#ifdef USE_FOG_WORLD_POSITION
|
||||
|
|
@ -170,7 +170,7 @@ import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as
|
|||
vFogWorldPosition = (modelMatrix * _fogPos2).xyz;
|
||||
#endif
|
||||
#endif
|
||||
`}function v(e,t){e.uniforms.fogVolumeData=t.fogVolumeData,e.uniforms.cameraHeight=t.cameraHeight,e.uniforms.fogEnabled=t.fogEnabled,e.uniforms.fogDistanceScale=t.fogDistanceScale}function y(e,t){v(e,t),e.vertexShader=e.vertexShader.replace(`#include <fog_pars_vertex>`,`#include <fog_pars_vertex>
|
||||
`}function f(e,t){e.uniforms.fogVolumeData=t.fogVolumeData,e.uniforms.cameraHeight=t.cameraHeight,e.uniforms.fogEnabled=t.fogEnabled,e.uniforms.fogDistanceScale=t.fogDistanceScale}function p(e,t){f(e,t),e.vertexShader=e.vertexShader.replace(`#include <fog_pars_vertex>`,`#include <fog_pars_vertex>
|
||||
#ifdef USE_FOG
|
||||
#define USE_FOG_WORLD_POSITION
|
||||
#define USE_VOLUMETRIC_FOG
|
||||
|
|
@ -191,4 +191,4 @@ import{C as e,Dt as t,Ht as n,J as r,Ot as i,P as a,kt as o,q as s,t as c,zt as
|
|||
uniform bool fogEnabled;
|
||||
#define USE_FOG_WORLD_POSITION
|
||||
varying vec3 vFogWorldPosition;
|
||||
#endif`),e.fragmentShader=e.fragmentShader.replace(`#include <fog_fragment>`,g)}var b=3,x=4,S={fogVolumeData:{value:new Float32Array(b*x)},cameraHeight:{value:0},fogEnabled:{value:!0},fogDistanceScale:{value:1}};function C(e,t,n=!0){S.cameraHeight.value=e,S.fogVolumeData.value.set(t),S.fogEnabled.value=n}function w(){S.cameraHeight.value=0,S.fogVolumeData.value.fill(0),S.fogEnabled.value=!0,S.fogDistanceScale.value=1}function T(e){let t=new Float32Array(b*x);for(let n=0;n<b;n++){let r=n*x,i=e[n];i&&(t[r+0]=i.visibleDistance,t[r+1]=i.minHeight,t[r+2]=i.maxHeight,t[r+3]=i.percentage)}return t}export{g as a,f as c,m as d,C as i,p as l,T as n,y as o,w as r,_ as s,S as t,h as u};
|
||||
#endif`),e.fragmentShader=e.fragmentShader.replace(`#include <fog_fragment>`,u)}var m=3,h=4,g={fogVolumeData:{value:new Float32Array(m*h)},cameraHeight:{value:0},fogEnabled:{value:!0},fogDistanceScale:{value:1}};function _(e,t,n=!0){g.cameraHeight.value=e,g.fogVolumeData.value.set(t),g.fogEnabled.value=n}function v(){g.cameraHeight.value=0,g.fogVolumeData.value.fill(0),g.fogEnabled.value=!0,g.fogDistanceScale.value=1}function y(e){let t=new Float32Array(m*h);for(let n=0;n<m;n++){let r=n*h,i=e[n];i&&(t[r+0]=i.visibleDistance,t[r+1]=i.minHeight,t[r+2]=i.maxHeight,t[r+3]=i.percentage)}return t}var b=new a;b.setOptions({premultiplyAlpha:`none`});var x=new Map;function S(e,t,n){let r=x.get(e);if(r)return t&&r.image&&t(r),r;let i=new l;return i.flipY=!1,x.set(e,i),b.load(e,e=>{i.image=e,i.needsUpdate=!0,t?.(i)},void 0,()=>{x.delete(e),n?.(e)}),i}function C(e){let t=x.get(e);return t?t.image?Promise.resolve(t):new Promise(e=>{let n=()=>{t.image?e(t):setTimeout(n,16)};n()}):new Promise((t,n)=>{let r=new l;r.flipY=!1,x.set(e,r),b.load(e,e=>{r.image=e,r.needsUpdate=!0,t(r)},void 0,n)})}function w(e,t={}){let{repeat:n=[1,1],disableMipmaps:a=!1,anisotropy:c,noColorSpace:l=!1}=t;return e.wrapS=e.wrapT=i,l||(e.colorSpace=o),e.repeat.set(...n),e.flipY=!1,e.anisotropy=c??1,a?(e.generateMipmaps=!1,e.minFilter=s):(e.generateMipmaps=!0,e.minFilter=r),e.magFilter=s,e.needsUpdate=!0,e}function T(r){let a=new e(r,256,256,t,n);return a.colorSpace=``,a.wrapS=a.wrapT=i,a.generateMipmaps=!1,a.minFilter=s,a.magFilter=s,a.needsUpdate=!0,a}export{g as a,_ as c,d,w as i,u as l,C as n,y as o,T as r,v as s,S as t,p as u};
|
||||
|
|
@ -1 +1 @@
|
|||
import{o as e}from"./react-three-fiber.esm-CD18QK1u.js";function t(){return e(n)}function n(e){return e.gl.capabilities.getMaxAnisotropy()}export{t};
|
||||
import{s as e}from"./react-three-fiber.esm-B4ybsNEe.js";function t(){return e(n)}function n(e){return e.gl.capabilities.getMaxAnisotropy()}export{t};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{r as e,t}from"./useBaseQuery-Dx4-Q7Ip.js";function n(n,r){return t(n,e,r)}export{n as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -19,25 +19,25 @@
|
|||
<link rel="apple-touch-icon" href="/t2-mapper/icon-512.png" />
|
||||
<meta name="theme-color" content="#067272" />
|
||||
<link rel="preconnect" href="https://www.gstatic.com" crossorigin />
|
||||
<script type="module" crossorigin src="/t2-mapper/assets/index-CcOasdr9.js"></script>
|
||||
<script type="module" crossorigin src="/t2-mapper/assets/index-BEehCpzM.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/chunk-DECur_0Z.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/preload-helper-BPkniflS.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/three.module-C9W4LJrj.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/streamHelpers-DNksG7mZ.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/logger-B058WGzf.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/mission-S3koG-vu.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/preload-helper-CnJ98jGT.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/logger-CySD1nLn.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/mission-yBcX4xit.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/jsx-runtime-BpGWiA-R.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/traditional-ec-lUAFC.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/react-three-fiber.esm-CD18QK1u.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/engineStore-Mono9lyt.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/stringUtils-EmGsjr9D.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/manifest-MMwwguGo.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/SettingsProvider-CeFD8Cx1.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/iconBase-BCRUFbxq.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/JoystickContext-D1zeSnLk.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/scene-BpfzP6B-.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/cameraTourStore-DQ989o2x.js">
|
||||
<link rel="stylesheet" crossorigin href="/t2-mapper/assets/index-BD1vN1aq.css">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/three.module-DeDv86YO.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/traditional-DPdbI9gv.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/react-three-fiber.esm-B4ybsNEe.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/streamHelpers-DZp0O0LI.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/engineStore-DXFfg1NG.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/stringUtils-Bvtx11IK.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/manifest-DDCwpSLV.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/SettingsProvider-BeB5OnG9.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/iconBase-DjT_EJem.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/JoystickContext-BKqyiaNN.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/scene-KKwVL-xq.js">
|
||||
<link rel="modulepreload" crossorigin href="/t2-mapper/assets/cameraTourStore-CfKPrs02.js">
|
||||
<link rel="stylesheet" crossorigin href="/t2-mapper/assets/index-BONY_dmA.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export default defineConfig([
|
|||
{ args: "none", caughtErrors: "none", varsIgnorePattern: "^_" },
|
||||
],
|
||||
"react-hooks/set-state-in-effect": "off",
|
||||
"react-hooks/preserve-manual-memoization": "warn",
|
||||
"react-refresh/only-export-components": "warn",
|
||||
"react-hooks/immutability": "warn",
|
||||
"no-control-regex": "off",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ import { MapInspector } from "@/src/components/MapInspector";
|
|||
import { SettingsProvider } from "@/src/components/SettingsProvider";
|
||||
// Three.js has its own loaders for textures and models, but we need to load other
|
||||
// stuff too, e.g. missions, terrains, and more. This client is used for those.
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const debugMaterial = (
|
|||
/** Red wireframe bounding box for debug tour visualization. */
|
||||
export function DebugBounds({ size }: { size: [number, number, number] }) {
|
||||
const edges = useMemo(
|
||||
() => new EdgesGeometry(new BoxGeometry(...size)),
|
||||
() => new EdgesGeometry(new BoxGeometry(size[0], size[1], size[2])),
|
||||
[size[0], size[1], size[2]], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -128,8 +128,28 @@
|
|||
}
|
||||
|
||||
.Detail {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 4px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.Detail > div {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.Detail dt {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.Detail dt::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
.Detail dd {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,35 @@ function getEntityLabel(entity: GameEntity): string {
|
|||
return entity.className;
|
||||
}
|
||||
|
||||
function getEntityDetail(entity: GameEntity): string | undefined {
|
||||
if ("shapeName" in entity && entity.shapeName) return entity.shapeName;
|
||||
if (entity.renderType === "InteriorInstance" && "interiorData" in entity) {
|
||||
return entity.interiorData.interiorFile;
|
||||
function getEntityDetail(
|
||||
entity: GameEntity,
|
||||
): Record<string, string> | undefined {
|
||||
const fields: Record<string, string> = {};
|
||||
|
||||
if ("shapeName" in entity && entity.shapeName) {
|
||||
fields.shape = entity.shapeName;
|
||||
}
|
||||
if ("dataBlock" in entity && entity.dataBlock) return entity.dataBlock;
|
||||
if (entity.renderType === "TerrainBlock" && "terrainData" in entity)
|
||||
return entity.terrainData.terrFileName;
|
||||
if (entity.renderType === "WaterBlock" && "waterData" in entity)
|
||||
return entity.waterData.surfaceName;
|
||||
if ("audioFileName" in entity && entity.audioFileName)
|
||||
return entity.audioFileName;
|
||||
return undefined;
|
||||
if (entity.renderType === "Player" && "skinPrefName" in entity) {
|
||||
if (entity.skinPrefName) fields.skin = entity.skinPrefName;
|
||||
else if (entity.skinName) fields.skin = entity.skinName;
|
||||
}
|
||||
if (entity.renderType === "InteriorInstance" && "interiorData" in entity) {
|
||||
fields.interior = entity.interiorData.interiorFile;
|
||||
}
|
||||
if (!fields.shape && "dataBlock" in entity && entity.dataBlock) {
|
||||
fields.dataBlock = entity.dataBlock;
|
||||
}
|
||||
if (entity.renderType === "TerrainBlock" && "terrainData" in entity) {
|
||||
fields.terrain = entity.terrainData.terrFileName;
|
||||
}
|
||||
if (entity.renderType === "WaterBlock" && "waterData" in entity) {
|
||||
fields.surface = entity.waterData.surfaceName;
|
||||
}
|
||||
if ("audioFileName" in entity && entity.audioFileName) {
|
||||
fields.audio = entity.audioFileName;
|
||||
}
|
||||
|
||||
return Object.keys(fields).length > 0 ? fields : undefined;
|
||||
}
|
||||
|
||||
function getEntityPosition(
|
||||
|
|
@ -96,7 +112,16 @@ function EntityRow({ entity }: { entity: GameEntity }) {
|
|||
<span className={styles.Type}>{label}</span>{" "}
|
||||
<span className={styles.ID}>{entity.id}</span>
|
||||
</div>
|
||||
{detail && <span className={styles.Detail}>{detail}</span>}
|
||||
{detail && (
|
||||
<dl className={styles.Detail}>
|
||||
{Object.entries(detail).map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt>{key}</dt>
|
||||
<dd>{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
{pos && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { lazy, memo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { lazy, memo, useMemo, useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
import type {
|
||||
GameEntity,
|
||||
ShapeEntity as ShapeEntityType,
|
||||
} from "../state/gameEntityTypes";
|
||||
import { ShapeRenderer, ShapePlaceholder } from "./GenericShape";
|
||||
import { ShapeRenderer, MountedShapeContent } from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
import type { StaticShapeType } from "./ShapeInfoProvider";
|
||||
import { DebugSuspense } from "./DebugSuspense";
|
||||
import { ShapeErrorBoundary } from "./ShapeErrorBoundary";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { DEFAULT_TEAM_NAMES } from "../stringUtils";
|
||||
import { useDataSource } from "../state/gameEntityStore";
|
||||
import { resolveEmapFromDatablock } from "./resolveEmap";
|
||||
import { Camera } from "./Camera";
|
||||
import { WayPoint } from "./WayPoint";
|
||||
import { TerrainBlock } from "./TerrainBlock";
|
||||
|
|
@ -20,6 +19,7 @@ import { InteriorInstance } from "./InteriorInstance";
|
|||
import { Sky } from "./Sky";
|
||||
import { AudioEnabled } from "./AudioEnabled";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import { useRotation } from "./useRotation";
|
||||
|
||||
function createLazy(
|
||||
name: string,
|
||||
|
|
@ -48,7 +48,7 @@ function createLazy(
|
|||
const PlayerModel = createLazy("PlayerModel", () => import("./PlayerModel"));
|
||||
const ExplosionShape = createLazy(
|
||||
"ExplosionShape",
|
||||
() => import("./ShapeModel"),
|
||||
() => import("./ExplosionShape"),
|
||||
);
|
||||
const TracerProjectile = createLazy(
|
||||
"TracerProjectile",
|
||||
|
|
@ -64,7 +64,6 @@ const ForceFieldBare = createLazy(
|
|||
);
|
||||
const AudioEmitter = createLazy("AudioEmitter", () => import("./AudioEmitter"));
|
||||
const WaterBlock = createLazy("WaterBlock", () => import("./WaterBlock"));
|
||||
const WeaponModel = createLazy("WeaponModel", () => import("./ShapeModel"));
|
||||
|
||||
/**
|
||||
* Renders a GameEntity by dispatching to the appropriate renderer based
|
||||
|
|
@ -121,23 +120,24 @@ export const EntityRenderer = memo(function EntityRenderer({
|
|||
});
|
||||
|
||||
function ShapeEntity({ entity }: { entity: ShapeEntityType }) {
|
||||
const { animationEnabled } = useSettings();
|
||||
const dataSource = useDataSource();
|
||||
const isStreaming = dataSource === "demo" || dataSource === "live";
|
||||
const groupRef = useRef<Group>(null);
|
||||
|
||||
// Y-axis spinning for Items with rotate=true
|
||||
useFrame(() => {
|
||||
if (!groupRef.current || !entity.rotate || !animationEnabled) return;
|
||||
const t = performance.now() / 1000;
|
||||
groupRef.current.rotation.y = (t / 3.0) * Math.PI * 2;
|
||||
});
|
||||
useRotation(entity, groupRef);
|
||||
|
||||
if (!entity.shapeName) {
|
||||
throw new Error(`Shape entity missing shapeName: ${entity.id}`);
|
||||
}
|
||||
|
||||
const torqueObject = entity.runtimeObject as TorqueObject | undefined;
|
||||
const shapeType = (entity.shapeType ?? "StaticShape") as StaticShapeType;
|
||||
|
||||
const emap = useMemo(
|
||||
() => resolveEmapFromDatablock(entity.dataBlockId, entity.dataBlock),
|
||||
[entity.dataBlockId, entity.dataBlock],
|
||||
);
|
||||
|
||||
// Flag label for flag Items
|
||||
const isFlag = entity.dataBlock?.toLowerCase() === "flag";
|
||||
const teamName =
|
||||
|
|
@ -155,48 +155,35 @@ function ShapeEntity({ entity }: { entity: ShapeEntityType }) {
|
|||
|
||||
return (
|
||||
<ShapeInfoProvider
|
||||
object={torqueObject}
|
||||
object={entity.runtimeObject as TorqueObject | undefined}
|
||||
shapeName={entity.shapeName}
|
||||
type={shapeType}
|
||||
>
|
||||
<group ref={entity.rotate ? groupRef : undefined}>
|
||||
<ShapeRenderer
|
||||
loadingColor={loadingColor}
|
||||
streamEntity={torqueObject ? undefined : entity}
|
||||
emap={entity.emap}
|
||||
streamEntity={isStreaming ? entity : undefined}
|
||||
emap={emap}
|
||||
entityId={entity.id}
|
||||
skinName={entity.skinName}
|
||||
mounted={
|
||||
entity.weaponShape
|
||||
? {
|
||||
0: (
|
||||
<MountedShapeContent
|
||||
shapeName={entity.weaponShape}
|
||||
imageDataBlockId={entity.imageDataBlockIds?.[0]}
|
||||
entityId={entity.id}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{flagLabel ? (
|
||||
<FloatingLabel opacity={0.6}>{flagLabel}</FloatingLabel>
|
||||
) : null}
|
||||
</ShapeRenderer>
|
||||
{entity.barrelShapeName && (
|
||||
<ShapeInfoProvider
|
||||
object={torqueObject}
|
||||
shapeName={entity.barrelShapeName}
|
||||
type="Turret"
|
||||
>
|
||||
<group position={[0, 1.5, 0]}>
|
||||
<ShapeRenderer />
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
)}
|
||||
{entity.weaponShape && (
|
||||
<ShapeErrorBoundary
|
||||
fallback={
|
||||
<ShapePlaceholder color="red" label={entity.weaponShape} />
|
||||
}
|
||||
>
|
||||
<DebugSuspense
|
||||
name={`Weapon:${entity.id}/${entity.weaponShape}`}
|
||||
fallback={
|
||||
<ShapePlaceholder color="cyan" label={entity.weaponShape} />
|
||||
}
|
||||
>
|
||||
<WeaponModel entity={entity} />
|
||||
</DebugSuspense>
|
||||
</ShapeErrorBoundary>
|
||||
)}
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,120 +1,21 @@
|
|||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { AnimationMixer, LoopOnce, Quaternion, Vector3 } from "three";
|
||||
import { AnimationMixer, LoopOnce } from "three";
|
||||
import type { Group, Material } from "three";
|
||||
import { effectNow, engineStore } from "../state/engineStore";
|
||||
import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import {
|
||||
_r90,
|
||||
_r90inv,
|
||||
disposeClonedScene,
|
||||
getPosedNodeTransform,
|
||||
processShapeScene,
|
||||
} from "../stream/playbackUtils";
|
||||
import { disposeClonedScene, processShapeScene } from "../stream/playbackUtils";
|
||||
import {
|
||||
loadIflAtlas,
|
||||
getFrameIndexForTime,
|
||||
updateAtlasFrame,
|
||||
} from "./useIflTexture";
|
||||
import type { IflAtlas } from "./useIflTexture";
|
||||
import { ShapeRenderer, useStaticShape } from "./GenericShape";
|
||||
} from "./iflAtlas";
|
||||
import type { IflAtlas } from "./iflAtlas";
|
||||
import { useStaticShape } from "./GenericShape";
|
||||
import { useAnisotropy } from "./useAnisotropy";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import type { ExplosionEntity, ShapeEntity } from "../state/gameEntityTypes";
|
||||
import type { ExplosionEntity } from "../state/gameEntityTypes";
|
||||
import { streamPlaybackStore } from "../state/streamPlaybackStore";
|
||||
|
||||
/**
|
||||
* Renders a mounted weapon using the Torque engine's mount system.
|
||||
*
|
||||
* The weapon's `Mountpoint` node is aligned to the player's `Mount0` node
|
||||
* (right hand). Both nodes come from the GLB skeleton in its idle ("Root"
|
||||
* animation) pose, with the weapon-specific arm animation applied additively.
|
||||
* The mount transform is conjugated by ShapeRenderer's 90° Y rotation:
|
||||
* T_mount = R90 * M0 * MP^(-1) * R90^(-1).
|
||||
*/
|
||||
export function WeaponModel({ entity }: { entity: ShapeEntity }) {
|
||||
const shapeName = entity.weaponShape;
|
||||
const playerShapeName = entity.shapeName;
|
||||
const playerGltf = useStaticShape(playerShapeName!);
|
||||
const weaponGltf = useStaticShape(shapeName!);
|
||||
|
||||
const mountTransform = useMemo(() => {
|
||||
// Get Mount0 from the player's posed skeleton with arm animation applied.
|
||||
// TODO: resolve arm animation from armAction index once actionAnimMap
|
||||
// is available here. For now use default arm pose.
|
||||
const armThread = "lookde";
|
||||
const m0 = getPosedNodeTransform(
|
||||
playerGltf.scene,
|
||||
playerGltf.animations,
|
||||
"Mount0",
|
||||
[armThread],
|
||||
);
|
||||
if (!m0) return { position: undefined, quaternion: undefined };
|
||||
|
||||
// Get Mountpoint from weapon (may not be animated).
|
||||
const mp = getPosedNodeTransform(
|
||||
weaponGltf.scene,
|
||||
weaponGltf.animations,
|
||||
"Mountpoint",
|
||||
);
|
||||
|
||||
// Compute T_mount = R90 * M0 * MP^(-1) * R90^(-1)
|
||||
// This conjugates the GLB-space mount transform by ShapeRenderer's 90° Y
|
||||
// rotation so the weapon is correctly oriented in entity space.
|
||||
let combinedPos: Vector3;
|
||||
let combinedQuat: Quaternion;
|
||||
|
||||
if (mp) {
|
||||
// MP^(-1)
|
||||
const mpInvQuat = mp.quaternion.clone().invert();
|
||||
const mpInvPos = mp.position.clone().negate().applyQuaternion(mpInvQuat);
|
||||
|
||||
// M0 * MP^(-1)
|
||||
combinedQuat = m0.quaternion.clone().multiply(mpInvQuat);
|
||||
combinedPos = mpInvPos
|
||||
.clone()
|
||||
.applyQuaternion(m0.quaternion)
|
||||
.add(m0.position);
|
||||
} else {
|
||||
combinedPos = m0.position.clone();
|
||||
combinedQuat = m0.quaternion.clone();
|
||||
}
|
||||
|
||||
// R90 * combined * R90^(-1)
|
||||
const mountPos = combinedPos.applyQuaternion(_r90);
|
||||
const mountQuat = _r90.clone().multiply(combinedQuat).multiply(_r90inv);
|
||||
|
||||
return { position: mountPos, quaternion: mountQuat };
|
||||
}, [
|
||||
playerGltf.animations,
|
||||
playerGltf.scene,
|
||||
shapeName,
|
||||
weaponGltf.animations,
|
||||
weaponGltf.scene,
|
||||
]);
|
||||
|
||||
const torqueObject = useMemo<TorqueObject>(
|
||||
() => ({
|
||||
_class: "weapon",
|
||||
_className: "Weapon",
|
||||
_id: 0,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider object={torqueObject} shapeName={shapeName!} type="Item">
|
||||
<group
|
||||
position={mountTransform.position}
|
||||
quaternion={mountTransform.quaternion}
|
||||
>
|
||||
<ShapeRenderer loadingColor="#4488ff" />
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Explosion shape rendering ──
|
||||
//
|
||||
// Explosion DTS shapes are flat billboard planes with IFL-animated textures,
|
||||
|
|
@ -266,7 +167,6 @@ export function ExplosionShape({ entity }: { entity: ExplosionEntity }) {
|
|||
|
||||
processShapeScene(scene, entity.shapeName, {
|
||||
anisotropy,
|
||||
emap: "emap" in entity ? (entity as any).emap : undefined,
|
||||
});
|
||||
|
||||
// Collect vis-animated nodes keyed by sequence name.
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { memo, Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import { Fragment, memo, Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import type { AnimationAction } from "three";
|
||||
import { useGLTF, useTexture } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { createPortal, useFrame } from "@react-three/fiber";
|
||||
import { createLogger } from "../logger";
|
||||
import { FALLBACK_TEXTURE_URL, textureToUrl, shapeToUrl } from "../loaders";
|
||||
import { shapeToUrl } from "../loaders";
|
||||
import {
|
||||
MeshStandardMaterial,
|
||||
AdditiveAnimationBlendMode,
|
||||
|
|
@ -13,16 +14,15 @@ import {
|
|||
AnimationUtils,
|
||||
LoopOnce,
|
||||
LoopRepeat,
|
||||
BufferGeometry,
|
||||
Group,
|
||||
Box3,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import { setupTexture } from "../textureUtils";
|
||||
import { useAnisotropy } from "./useAnisotropy";
|
||||
import { useDebug, useSettings } from "./SettingsProvider";
|
||||
import { useShapeInfo, isOrganicShape } from "./ShapeInfoProvider";
|
||||
import { useShapeInfo, ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
import type { StaticShapeType } from "./ShapeInfoProvider";
|
||||
import {
|
||||
useEngineSelector,
|
||||
effectNow,
|
||||
|
|
@ -30,14 +30,11 @@ import {
|
|||
} from "../state/engineStore";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import {
|
||||
useIflTexture,
|
||||
loadIflAtlas,
|
||||
getFrameIndexForTime,
|
||||
updateAtlasFrame,
|
||||
} from "./useIflTexture";
|
||||
import type { IflAtlas } from "./useIflTexture";
|
||||
import { createMaterialFromFlags } from "../shapeMaterial";
|
||||
import type { MaterialResult } from "../shapeMaterial";
|
||||
} from "./iflAtlas";
|
||||
import type { IflAtlas } from "./iflAtlas";
|
||||
import { useIsDebugTourTarget } from "../state/cameraTourStore";
|
||||
import { DebugBounds } from "./DebugBounds";
|
||||
import { useEntitySoundSlots } from "./useEntitySoundSlots";
|
||||
|
|
@ -46,15 +43,22 @@ import {
|
|||
replaceWithShapeMaterial,
|
||||
disposeClonedScene,
|
||||
buildRestPoseClip,
|
||||
getPosedNodeTransform,
|
||||
} from "../stream/playbackUtils";
|
||||
import { resolveEmapFromImageSlot } from "./resolveEmap";
|
||||
import type { ThreadState as StreamThreadState } from "../stream/types";
|
||||
|
||||
const STANDARD_90_ROTATION: [x: number, y: number, z: number] = [
|
||||
0,
|
||||
Math.PI / 2,
|
||||
0,
|
||||
];
|
||||
|
||||
/** Shape entity data readable in useFrame for streaming mode. */
|
||||
interface StreamShapeEntity {
|
||||
id: string;
|
||||
threads?: StreamThreadState[];
|
||||
damageState?: number;
|
||||
emap?: boolean;
|
||||
wheels?: Array<{
|
||||
speed: number;
|
||||
lateralSlip: number;
|
||||
|
|
@ -69,6 +73,57 @@ interface StreamShapeEntity {
|
|||
|
||||
const log = createLogger("GenericShape");
|
||||
|
||||
/**
|
||||
* Content for a mounted shape. Computes the Mountpoint inverse offset from the
|
||||
* child shape's GLB so the child's grip point aligns to the parent's mount bone.
|
||||
* Rendered via createPortal into the parent's mount bone.
|
||||
*/
|
||||
export function MountedShapeContent({
|
||||
shapeName,
|
||||
imageDataBlockId,
|
||||
entityId,
|
||||
shapeType = "StaticShape",
|
||||
skinName,
|
||||
}: {
|
||||
shapeName: string;
|
||||
imageDataBlockId?: number;
|
||||
entityId?: string;
|
||||
shapeType?: StaticShapeType;
|
||||
skinName?: string;
|
||||
}) {
|
||||
const childGltf = useStaticShape(shapeName);
|
||||
const emap = useMemo(
|
||||
() => resolveEmapFromImageSlot(imageDataBlockId),
|
||||
[imageDataBlockId],
|
||||
);
|
||||
|
||||
// Compute Mountpoint inverse so the child's grip aligns to the bone origin.
|
||||
const offset = useMemo(() => {
|
||||
const mp = getPosedNodeTransform(
|
||||
childGltf.scene as Group,
|
||||
childGltf.animations,
|
||||
"Mountpoint",
|
||||
);
|
||||
if (!mp) return null;
|
||||
const invQuat = mp.quaternion.clone().invert();
|
||||
const invPos = mp.position.clone().negate().applyQuaternion(invQuat);
|
||||
return { position: invPos, quaternion: invQuat };
|
||||
}, [childGltf.scene, childGltf.animations]);
|
||||
|
||||
return (
|
||||
<ShapeInfoProvider shapeName={shapeName} type={shapeType}>
|
||||
<group position={offset?.position} quaternion={offset?.quaternion}>
|
||||
<ShapeRenderer
|
||||
emap={emap}
|
||||
entityId={entityId}
|
||||
skinName={skinName}
|
||||
noRotation
|
||||
/>
|
||||
</group>
|
||||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/** WheeledVehicle per-wheel animation state (position-controlled, not threaded). */
|
||||
interface WheelAnimState {
|
||||
wheelAction?: AnimationAction;
|
||||
|
|
@ -83,20 +138,6 @@ function shapeNowSec(): number {
|
|||
return recording != null ? effectNow() / 1000 : performance.now() / 1000;
|
||||
}
|
||||
|
||||
/** Shared props for texture rendering components */
|
||||
interface TextureProps {
|
||||
material: MeshStandardMaterial;
|
||||
shapeName?: string;
|
||||
geometry?: BufferGeometry;
|
||||
backGeometry?: BufferGeometry;
|
||||
castShadow?: boolean;
|
||||
receiveShadow?: boolean;
|
||||
/** DTS object visibility (0–1). Values < 1 enable alpha blending. */
|
||||
vis?: number;
|
||||
/** When true, material is created transparent for vis keyframe animation. */
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a .glb file that was converted from a .dts, used for static shapes.
|
||||
*/
|
||||
|
|
@ -105,232 +146,9 @@ export function useStaticShape(shapeName: string) {
|
|||
return useGLTF(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated IFL (Image File List) material component. Creates a sprite sheet
|
||||
* from all frames and animates via texture offset.
|
||||
*/
|
||||
const IflTexture = memo(function IflTexture({
|
||||
material,
|
||||
shapeName,
|
||||
geometry,
|
||||
backGeometry,
|
||||
castShadow = false,
|
||||
receiveShadow = false,
|
||||
vis = 1,
|
||||
animated = false,
|
||||
}: TextureProps) {
|
||||
const resourcePath = material.userData.resource_path;
|
||||
const flagNames = useMemo(
|
||||
() =>
|
||||
material.userData.flag_names
|
||||
? new Set<string>(material.userData.flag_names)
|
||||
: EMPTY_FLAG_NAMES,
|
||||
[material.userData.flag_names],
|
||||
);
|
||||
const iflPath = `textures/${resourcePath}.ifl`;
|
||||
|
||||
const texture = useIflTexture(iflPath);
|
||||
const isOrganic = !!(shapeName && isOrganicShape(shapeName));
|
||||
|
||||
const customMaterial = useMemo(
|
||||
() =>
|
||||
createMaterialFromFlags(
|
||||
material,
|
||||
texture,
|
||||
flagNames,
|
||||
isOrganic,
|
||||
vis,
|
||||
animated,
|
||||
),
|
||||
[material, texture, flagNames, isOrganic, vis, animated],
|
||||
);
|
||||
|
||||
useDisposeMaterial(customMaterial);
|
||||
|
||||
// Two-pass rendering for organic/translucent materials
|
||||
// Render BackSide first (with flipped normals), then FrontSide
|
||||
if (Array.isArray(customMaterial)) {
|
||||
return (
|
||||
<>
|
||||
<mesh
|
||||
geometry={backGeometry || geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial[0]} attach="material" />
|
||||
</mesh>
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial[1]} attach="material" />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
|
||||
function useDisposeMaterial(material: MaterialResult) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (Array.isArray(material)) {
|
||||
material.forEach((m) => m.dispose());
|
||||
} else {
|
||||
material.dispose();
|
||||
}
|
||||
};
|
||||
}, [material]);
|
||||
}
|
||||
|
||||
const EMPTY_FLAG_NAMES = new Set<string>();
|
||||
|
||||
const StaticTexture = memo(function StaticTexture({
|
||||
material,
|
||||
shapeName,
|
||||
geometry,
|
||||
backGeometry,
|
||||
castShadow = false,
|
||||
receiveShadow = false,
|
||||
vis = 1,
|
||||
animated = false,
|
||||
}: TextureProps) {
|
||||
const resourcePath = material.userData.resource_path;
|
||||
const flagNames = useMemo(
|
||||
() =>
|
||||
material.userData.flag_names
|
||||
? new Set<string>(material.userData.flag_names)
|
||||
: EMPTY_FLAG_NAMES,
|
||||
[material.userData.flag_names],
|
||||
);
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (!resourcePath) {
|
||||
log.warn(
|
||||
'No resource_path found on "%s" — rendering fallback',
|
||||
shapeName,
|
||||
);
|
||||
}
|
||||
return resourcePath ? textureToUrl(resourcePath) : FALLBACK_TEXTURE_URL;
|
||||
}, [resourcePath, shapeName]);
|
||||
|
||||
const isOrganic = !!(shapeName && isOrganicShape(shapeName));
|
||||
const isTranslucent = flagNames.has("Translucent");
|
||||
const anisotropy = useAnisotropy();
|
||||
|
||||
const texture = useTexture(url, (texture) => {
|
||||
// Organic/alpha-tested textures need special handling to avoid mipmap artifacts
|
||||
if (isOrganic || isTranslucent) {
|
||||
return setupTexture(texture, { disableMipmaps: true, anisotropy });
|
||||
}
|
||||
// Standard color texture setup for diffuse-only materials
|
||||
return setupTexture(texture, { anisotropy });
|
||||
});
|
||||
|
||||
const customMaterial = useMemo(
|
||||
() =>
|
||||
createMaterialFromFlags(
|
||||
material,
|
||||
texture,
|
||||
flagNames,
|
||||
isOrganic,
|
||||
vis,
|
||||
animated,
|
||||
),
|
||||
[material, texture, flagNames, isOrganic, vis, animated],
|
||||
);
|
||||
|
||||
useDisposeMaterial(customMaterial);
|
||||
|
||||
// Two-pass rendering for organic/translucent materials
|
||||
// Render BackSide first (with flipped normals), then FrontSide
|
||||
if (Array.isArray(customMaterial)) {
|
||||
return (
|
||||
<>
|
||||
<mesh
|
||||
geometry={backGeometry || geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial[0]} attach="material" />
|
||||
</mesh>
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial[1]} attach="material" />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<mesh
|
||||
geometry={geometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
>
|
||||
<primitive object={customMaterial} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
|
||||
export const ShapeTexture = memo(function ShapeTexture({
|
||||
material,
|
||||
shapeName,
|
||||
geometry,
|
||||
backGeometry,
|
||||
castShadow = false,
|
||||
receiveShadow = false,
|
||||
vis = 1,
|
||||
animated = false,
|
||||
}: TextureProps) {
|
||||
const flagNames = new Set(material.userData.flag_names ?? []);
|
||||
const isIflMaterial = flagNames.has("IflMaterial");
|
||||
const resourcePath = material.userData.resource_path;
|
||||
|
||||
// Use IflTexture for animated materials
|
||||
if (isIflMaterial && resourcePath) {
|
||||
return (
|
||||
<IflTexture
|
||||
material={material}
|
||||
shapeName={shapeName}
|
||||
geometry={geometry}
|
||||
backGeometry={backGeometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
vis={vis}
|
||||
animated={animated}
|
||||
/>
|
||||
);
|
||||
} else if (material.name) {
|
||||
return (
|
||||
<StaticTexture
|
||||
material={material}
|
||||
shapeName={shapeName}
|
||||
geometry={geometry}
|
||||
backGeometry={backGeometry}
|
||||
castShadow={castShadow}
|
||||
receiveShadow={receiveShadow}
|
||||
vis={vis}
|
||||
animated={animated}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
// Dead code removed: IflTexture, StaticTexture, ShapeTexture, useDisposeMaterial
|
||||
// were part of an unused React-based IFL rendering path. All IFL materials
|
||||
// are now handled imperatively via loadIflAtlas + processShapeScene.
|
||||
|
||||
export function ShapePlaceholder({
|
||||
color,
|
||||
|
|
@ -369,6 +187,9 @@ export const ShapeRenderer = memo(function ShapeRenderer({
|
|||
emap,
|
||||
entityId,
|
||||
children,
|
||||
mounted,
|
||||
noRotation,
|
||||
skinName,
|
||||
}: {
|
||||
loadingColor?: string;
|
||||
/** Stable entity reference whose fields are mutated in-place. */
|
||||
|
|
@ -377,19 +198,23 @@ export const ShapeRenderer = memo(function ShapeRenderer({
|
|||
emap?: boolean;
|
||||
entityId?: string;
|
||||
children?: React.ReactNode;
|
||||
/** Content to render at each mount point bone (Mount0, Mount1, etc.). */
|
||||
mounted?: Record<number, ReactNode>;
|
||||
/** Skip the 90° Y rotation (for shapes mounted inside a parent that already rotates). */
|
||||
noRotation?: boolean;
|
||||
/** Skin texture URL (Torque reSkin: replaces "base." textures with this URL). */
|
||||
skinName?: string;
|
||||
}) {
|
||||
const { object, shapeName } = useShapeInfo();
|
||||
const { shapeName } = useShapeInfo();
|
||||
|
||||
if (!shapeName) {
|
||||
return (
|
||||
<DebugPlaceholder color="orange" label={`${object?._id}: <missing>`} />
|
||||
);
|
||||
return <DebugPlaceholder color="orange" label={`${entityId}: <missing>`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<DebugPlaceholder color="red" label={`${object?._id}: ${shapeName}`} />
|
||||
<DebugPlaceholder color="red" label={`${entityId}: ${shapeName}`} />
|
||||
}
|
||||
onError={(error) => {
|
||||
log.error("Shape error: %s: %o", shapeName, error);
|
||||
|
|
@ -400,8 +225,12 @@ export const ShapeRenderer = memo(function ShapeRenderer({
|
|||
streamEntity={streamEntity}
|
||||
emap={emap}
|
||||
entityId={entityId}
|
||||
/>
|
||||
{children}
|
||||
mounted={mounted}
|
||||
noRotation={noRotation}
|
||||
skinName={skinName}
|
||||
>
|
||||
{children}
|
||||
</ShapeModelLoader>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
@ -434,6 +263,10 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
streamEntity,
|
||||
emap,
|
||||
entityId,
|
||||
children,
|
||||
mounted,
|
||||
noRotation,
|
||||
skinName,
|
||||
}: {
|
||||
gltf: ReturnType<typeof useStaticShape>;
|
||||
/** Stable entity reference whose fields are mutated in-place. */
|
||||
|
|
@ -441,6 +274,13 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
/** Datablock enables environment map reflections. */
|
||||
emap?: boolean;
|
||||
entityId?: string;
|
||||
children?: ReactNode;
|
||||
/** Content to render at each mount point bone (Mount0, Mount1, etc.). */
|
||||
mounted?: Record<number, ReactNode>;
|
||||
/** Skip the 90° Y rotation (for mounted shapes). */
|
||||
noRotation?: boolean;
|
||||
/** Skin texture URL (Torque reSkin: replaces "base." textures). */
|
||||
skinName?: string;
|
||||
}) {
|
||||
const { object, shapeName } = useShapeInfo();
|
||||
const { debugMode } = useDebug();
|
||||
|
|
@ -458,6 +298,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
mesh: any;
|
||||
iflPath: string;
|
||||
hasVisSequence: boolean;
|
||||
repeat: boolean;
|
||||
iflSequence?: string;
|
||||
iflDuration?: number;
|
||||
iflCyclic?: boolean;
|
||||
|
|
@ -487,6 +328,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
mesh: node,
|
||||
iflPath: `textures/${rp}.ifl`,
|
||||
hasVisSequence: !!ud?.vis_sequence,
|
||||
repeat: flags.has("SWrap") || flags.has("TWrap"),
|
||||
iflSequence: iflSeq,
|
||||
iflDuration: iflDur,
|
||||
iflCyclic,
|
||||
|
|
@ -497,7 +339,8 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
|
||||
processShapeScene(scene, shapeName ?? undefined, {
|
||||
anisotropy,
|
||||
emap: emap ?? streamEntity?.emap,
|
||||
emap,
|
||||
skinName,
|
||||
});
|
||||
|
||||
// Un-hide IFL meshes that don't have a vis sequence — they should always
|
||||
|
|
@ -582,7 +425,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
visNodesBySequence: visBySeq,
|
||||
iflMeshes: iflInfos,
|
||||
};
|
||||
}, [gltf, anisotropy, emap]);
|
||||
}, [gltf.scene, gltf.animations, shapeName, anisotropy, emap, skinName]);
|
||||
|
||||
// Dispose cloned geometries and materials when the scene is replaced or
|
||||
// the component unmounts, to prevent GPU memory from accumulating.
|
||||
|
|
@ -598,6 +441,8 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
|
||||
interface IflAnimInfo {
|
||||
atlas: IflAtlas;
|
||||
/** Material reference for swap-mode texture updates. */
|
||||
mat: any;
|
||||
sequenceName?: string;
|
||||
/** Controlling sequence duration in seconds. */
|
||||
sequenceDuration?: number;
|
||||
|
|
@ -629,7 +474,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
iflAnimInfosRef.current = [];
|
||||
iflMeshAtlasRef.current.clear();
|
||||
for (const info of iflMeshes) {
|
||||
loadIflAtlas(info.iflPath)
|
||||
loadIflAtlas(info.iflPath, { repeat: info.repeat })
|
||||
.then((atlas) => {
|
||||
const mat = Array.isArray(info.mesh.material)
|
||||
? info.mesh.material[0]
|
||||
|
|
@ -640,6 +485,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
}
|
||||
const iflInfo = {
|
||||
atlas,
|
||||
mat,
|
||||
sequenceName: info.iflSequence,
|
||||
sequenceDuration: info.iflDuration,
|
||||
cyclic: info.iflCyclic,
|
||||
|
|
@ -1102,40 +948,43 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
if (iflAnimInfos.length > 0) {
|
||||
iflTimeRef.current += effectDelta;
|
||||
for (const info of iflAnimInfos) {
|
||||
if (!animationEnabled) {
|
||||
updateAtlasFrame(info.atlas, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.sequenceName && info.sequenceDuration) {
|
||||
// Sequence-driven IFL: find the thread playing this sequence and
|
||||
// compute time = pos * duration + toolBegin (matching the engine).
|
||||
let iflTime = 0;
|
||||
for (const [, thread] of threads) {
|
||||
if (thread.sequence === info.sequenceName) {
|
||||
const elapsed = shapeNowSec() - thread.startTime;
|
||||
const dur = info.sequenceDuration;
|
||||
// Reproduce th->pos: cyclic wraps [0,1), non-cyclic clamps [0,1]
|
||||
const pos = info.cyclic
|
||||
? (elapsed / dur) % 1
|
||||
: Math.min(elapsed / dur, 1);
|
||||
iflTime = pos * dur + (info.toolBegin ?? 0);
|
||||
break;
|
||||
}
|
||||
const updateAtlasAndMaterial = (
|
||||
info: IflAnimInfo,
|
||||
frameIndex: number,
|
||||
) => {
|
||||
updateAtlasFrame(info.atlas, frameIndex);
|
||||
if (info.atlas.swapMode && info.mat.map !== info.atlas.texture) {
|
||||
info.mat.map = info.atlas.texture;
|
||||
info.mat.needsUpdate = true;
|
||||
}
|
||||
updateAtlasFrame(
|
||||
info.atlas,
|
||||
getFrameIndexForTime(info.atlas, iflTime),
|
||||
);
|
||||
} else {
|
||||
// No controlling sequence: use accumulated real time.
|
||||
// (In the engine, these would stay at frame 0, but cycling is more
|
||||
// useful for display purposes.)
|
||||
updateAtlasFrame(
|
||||
info.atlas,
|
||||
getFrameIndexForTime(info.atlas, iflTimeRef.current),
|
||||
);
|
||||
};
|
||||
let frameIndex = 0;
|
||||
if (animationEnabled) {
|
||||
let iflTime = 0;
|
||||
if (info.sequenceName && info.sequenceDuration) {
|
||||
// Sequence-driven IFL: find the thread playing this sequence and
|
||||
// compute time = pos * duration + toolBegin (matching the engine).
|
||||
for (const [, thread] of threads) {
|
||||
if (thread.sequence === info.sequenceName) {
|
||||
const elapsed = shapeNowSec() - thread.startTime;
|
||||
const dur = info.sequenceDuration;
|
||||
// Reproduce th->pos: cyclic wraps [0,1), non-cyclic clamps [0,1]
|
||||
const pos = info.cyclic
|
||||
? (elapsed / dur) % 1
|
||||
: Math.min(elapsed / dur, 1);
|
||||
iflTime = pos * dur + (info.toolBegin ?? 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No controlling sequence: use accumulated real time.
|
||||
// (In the engine, these would stay at frame 0, but cycling is more
|
||||
// useful for display purposes.)
|
||||
iflTime = iflTimeRef.current;
|
||||
}
|
||||
frameIndex = getFrameIndexForTime(info.atlas, iflTime);
|
||||
}
|
||||
updateAtlasAndMaterial(info, frameIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1157,12 +1006,26 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
};
|
||||
}, [isTarget, gltf.scene]);
|
||||
|
||||
// Find mount point bones in the cloned scene for portal rendering.
|
||||
const mountBones = useMemo(() => {
|
||||
if (!mounted) return null;
|
||||
const bones: Record<number, Group> = {};
|
||||
for (const slot of Object.keys(mounted)) {
|
||||
const index = Number(slot);
|
||||
const nodeName = `Mount${index}`;
|
||||
clonedScene.traverse((node: any) => {
|
||||
if (node.name === nodeName) bones[index] = node;
|
||||
});
|
||||
}
|
||||
return Object.keys(bones).length > 0 ? bones : null;
|
||||
}, [clonedScene, mounted]);
|
||||
|
||||
return (
|
||||
<group rotation={[0, Math.PI / 2, 0]}>
|
||||
<group rotation={noRotation ? undefined : STANDARD_90_ROTATION}>
|
||||
<primitive object={clonedScene} />
|
||||
{debugMode ? (
|
||||
<FloatingLabel>
|
||||
{object?._id}: {shapeName}
|
||||
{entityId}: {shapeName}
|
||||
</FloatingLabel>
|
||||
) : null}
|
||||
{shapeBounds && (
|
||||
|
|
@ -1170,6 +1033,17 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
<DebugBounds size={shapeBounds.size} />
|
||||
</group>
|
||||
)}
|
||||
{children}
|
||||
{mountBones &&
|
||||
mounted &&
|
||||
Object.entries(mounted).map(([slot, content]) => {
|
||||
const bone = mountBones[Number(slot)];
|
||||
return bone ? (
|
||||
<Fragment key={slot}>
|
||||
{createPortal(<group>{content}</group>, bone)}
|
||||
</Fragment>
|
||||
) : null;
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
});
|
||||
|
|
@ -1178,10 +1052,18 @@ function ShapeModelLoader({
|
|||
streamEntity,
|
||||
emap,
|
||||
entityId,
|
||||
children,
|
||||
mounted,
|
||||
noRotation,
|
||||
skinName,
|
||||
}: {
|
||||
streamEntity?: StreamShapeEntity;
|
||||
emap?: boolean;
|
||||
entityId?: string;
|
||||
children?: ReactNode;
|
||||
mounted?: Record<number, ReactNode>;
|
||||
noRotation?: boolean;
|
||||
skinName?: string;
|
||||
}) {
|
||||
const { shapeName } = useShapeInfo();
|
||||
const gltf = useStaticShape(shapeName);
|
||||
|
|
@ -1191,6 +1073,11 @@ function ShapeModelLoader({
|
|||
streamEntity={streamEntity}
|
||||
emap={emap}
|
||||
entityId={entityId}
|
||||
/>
|
||||
mounted={mounted}
|
||||
noRotation={noRotation}
|
||||
skinName={skinName}
|
||||
>
|
||||
{children}
|
||||
</ShapeModel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { createPortal, useFrame } from "@react-three/fiber";
|
||||
import {
|
||||
AdditiveAnimationBlendMode,
|
||||
AnimationMixer,
|
||||
|
|
@ -19,7 +19,6 @@ import { AnimationClip } from "three";
|
|||
import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import {
|
||||
ANIM_TRANSITION_TIME,
|
||||
DEFAULT_EYE_HEIGHT,
|
||||
buildRestPoseClip,
|
||||
disposeClonedScene,
|
||||
getKeyframeAtTime,
|
||||
|
|
@ -30,13 +29,23 @@ import { pickMoveAnimation } from "../stream/playerAnimation";
|
|||
import { WeaponImageStateMachine } from "../stream/weaponStateMachine";
|
||||
import type { WeaponAnimState } from "../stream/weaponStateMachine";
|
||||
import { getAliasedActions } from "../torqueScript/shapeConstructor";
|
||||
import { useStaticShape, ShapePlaceholder } from "./GenericShape";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
useStaticShape,
|
||||
ShapePlaceholder,
|
||||
MountedShapeContent,
|
||||
} from "./GenericShape";
|
||||
import { textureToUrl } from "../loaders";
|
||||
import { useAnisotropy } from "./useAnisotropy";
|
||||
import { ShapeErrorBoundary } from "./ShapeErrorBoundary";
|
||||
import { DebugSuspense } from "./DebugSuspense";
|
||||
import { useIsDebugTourTarget } from "../state/cameraTourStore";
|
||||
import { DebugBounds } from "./DebugBounds";
|
||||
import { useEntitySoundSlots } from "./useEntitySoundSlots";
|
||||
import {
|
||||
resolveEmapFromDatablock,
|
||||
resolveEmapFromImageSlot,
|
||||
} from "./resolveEmap";
|
||||
import { useAudio } from "./AudioContext";
|
||||
import {
|
||||
resolveAudioProfile,
|
||||
|
|
@ -55,6 +64,53 @@ import { PlayerNameplate } from "./PlayerNameplate";
|
|||
import { streamPlaybackStore } from "../state/streamPlaybackStore";
|
||||
import type { PlayerEntity } from "../state/gameEntityTypes";
|
||||
|
||||
/**
|
||||
* Per-entity animated eye bone position in model space (GLB coordinates).
|
||||
* Written by PlayerModel after mixer.update(), read by StreamingController
|
||||
* for first-person camera positioning.
|
||||
*/
|
||||
export const playerEyePositions = new Map<string, Vector3>();
|
||||
|
||||
const SKIN_BASE_URL = "https://exogen.github.io/t2-skins/skins/";
|
||||
const SKIN_MANIFEST_URL = "https://exogen.github.io/t2-skins/skins.json";
|
||||
|
||||
/** Map shape DTS name to skin texture suffix. */
|
||||
const SKIN_SUFFIXES: Record<string, string> = {
|
||||
"light_male.dts": "lmale",
|
||||
"light_female.dts": "lfemale",
|
||||
"medium_male.dts": "mmale",
|
||||
"medium_female.dts": "mfemale",
|
||||
"heavy_male.dts": "hmale",
|
||||
"bioderm_light.dts": "lbioderm",
|
||||
"bioderm_medium.dts": "mbioderm",
|
||||
"bioderm_heavy.dts": "hbioderm",
|
||||
};
|
||||
|
||||
/** Processed custom skin manifest: suffix → Set of available skin names. */
|
||||
type SkinLookup = Record<string, Set<string>>;
|
||||
|
||||
/** Fetch the remote custom skins manifest (once, cached forever).
|
||||
* Returns a lookup of suffix → Set<skinName> for O(1) has() checks. */
|
||||
function useCustomSkinManifest() {
|
||||
return useQuery<SkinLookup>({
|
||||
queryKey: ["customSkinManifest"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(SKIN_MANIFEST_URL);
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
const raw: { customSkins?: Record<string, string[]> } = await res.json();
|
||||
const lookup: SkinLookup = {};
|
||||
if (raw.customSkins) {
|
||||
for (const [suffix, names] of Object.entries(raw.customSkins)) {
|
||||
lookup[suffix] = new Set(names);
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
},
|
||||
staleTime: Infinity,
|
||||
retry: 1,
|
||||
});
|
||||
}
|
||||
|
||||
/** Number of table actions in the engine's ActionAnimationList (Tribes2.exe build 25034). */
|
||||
const NUM_TABLE_ACTION_ANIMS = 8;
|
||||
|
||||
|
|
@ -229,47 +285,90 @@ export function PlayerModel({ entity }: { entity: PlayerEntity }) {
|
|||
(state) => state.playback.streamSnapshot?.controlPlayerGhostId,
|
||||
);
|
||||
|
||||
// Resolve skin texture URL: local manifest first, then remote manifest.
|
||||
const { data: skinManifest } = useCustomSkinManifest();
|
||||
|
||||
const skinUrl = useMemo(() => {
|
||||
const skin = entity.skinPrefName ?? entity.skinName;
|
||||
if (!skin || skin === "base") return undefined;
|
||||
const suffix = SKIN_SUFFIXES[shapeName.toLowerCase()];
|
||||
if (!suffix) return undefined;
|
||||
|
||||
// 1. Check local manifest (built-in skins like beagle, swolf, baseb).
|
||||
try {
|
||||
return textureToUrl(`skins/${skin}.${suffix}`, null);
|
||||
} catch {
|
||||
// Not in local manifest.
|
||||
}
|
||||
|
||||
// 2. Check remote manifest (custom skins).
|
||||
if (skinManifest?.[suffix]?.has(skin)) {
|
||||
return `${SKIN_BASE_URL}${skin}.${suffix}.png`;
|
||||
}
|
||||
|
||||
// 3. Not found — no skin override.
|
||||
return undefined;
|
||||
}, [entity.skinPrefName, entity.skinName, shapeName, skinManifest]);
|
||||
|
||||
// Resolve emap per-datablock (emap is a datablock property, not entity).
|
||||
const emap = useMemo(
|
||||
() => resolveEmapFromDatablock(entity.dataBlockId, entity.dataBlock),
|
||||
[entity.dataBlockId, entity.dataBlock],
|
||||
);
|
||||
|
||||
// Clone scene preserving skeleton bindings, create mixer, find mount bones.
|
||||
const { clonedScene, mixer, mount0, mount1, mount2, iflInitializers } =
|
||||
useMemo(() => {
|
||||
const scene = SkeletonUtils.clone(gltf.scene) as Group;
|
||||
const iflInits = processShapeScene(scene, undefined, {
|
||||
anisotropy,
|
||||
emap: entity.emap,
|
||||
});
|
||||
const {
|
||||
clonedScene,
|
||||
mixer,
|
||||
mount0,
|
||||
mount1,
|
||||
mount2,
|
||||
eyeBone,
|
||||
iflInitializers,
|
||||
} = useMemo(() => {
|
||||
const scene = SkeletonUtils.clone(gltf.scene) as Group;
|
||||
const iflInits = processShapeScene(scene, undefined, {
|
||||
anisotropy,
|
||||
emap: emap,
|
||||
skinUrl,
|
||||
});
|
||||
|
||||
// Use front-face-only rendering so the camera can see out from inside the
|
||||
// model in first-person (backface culling hides interior faces).
|
||||
scene.traverse((n: any) => {
|
||||
if (n.isMesh && n.material) {
|
||||
const mats = Array.isArray(n.material) ? n.material : [n.material];
|
||||
for (const m of mats) m.side = FrontSide;
|
||||
}
|
||||
});
|
||||
// Use front-face-only rendering so the camera can see out from inside the
|
||||
// model in first-person (backface culling hides interior faces).
|
||||
scene.traverse((n: any) => {
|
||||
if (n.isMesh && n.material) {
|
||||
const mats = Array.isArray(n.material) ? n.material : [n.material];
|
||||
for (const m of mats) m.side = FrontSide;
|
||||
}
|
||||
});
|
||||
|
||||
const mix = new AnimationMixer(scene);
|
||||
const mix = new AnimationMixer(scene);
|
||||
|
||||
let m0: Object3D | null = null;
|
||||
let m1: Object3D | null = null;
|
||||
let m2: Object3D | null = null;
|
||||
scene.traverse((n) => {
|
||||
if (!m0 && n.name === "Mount0") m0 = n;
|
||||
if (!m1 && n.name === "Mount1") m1 = n;
|
||||
if (!m2 && n.name === "Mount2") m2 = n;
|
||||
});
|
||||
let m0: Object3D | null = null;
|
||||
let m1: Object3D | null = null;
|
||||
let m2: Object3D | null = null;
|
||||
let eye: Object3D | null = null;
|
||||
scene.traverse((n) => {
|
||||
if (!m0 && n.name === "Mount0") m0 = n;
|
||||
if (!m1 && n.name === "Mount1") m1 = n;
|
||||
if (!m2 && n.name === "Mount2") m2 = n;
|
||||
if (!eye && n.name === "Eye") eye = n;
|
||||
});
|
||||
|
||||
return {
|
||||
clonedScene: scene,
|
||||
mixer: mix,
|
||||
mount0: m0,
|
||||
mount1: m1,
|
||||
mount2: m2,
|
||||
iflInitializers: iflInits,
|
||||
};
|
||||
}, [gltf.scene, anisotropy, entity.emap]);
|
||||
return {
|
||||
clonedScene: scene,
|
||||
mixer: mix,
|
||||
mount0: m0,
|
||||
mount1: m1,
|
||||
mount2: m2,
|
||||
eyeBone: eye as Object3D | null,
|
||||
iflInitializers: iflInits,
|
||||
};
|
||||
}, [gltf.scene, anisotropy, emap, skinUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
playerEyePositions.delete(entity.id);
|
||||
disposeClonedScene(clonedScene);
|
||||
mixer.uncacheRoot(clonedScene);
|
||||
};
|
||||
|
|
@ -502,7 +601,7 @@ export function PlayerModel({ entity }: { entity: PlayerEntity }) {
|
|||
|
||||
// ShapeBase sound slots (weapon switch sounds, etc.) — managed by shared hook.
|
||||
const entityRef = useRef(entity);
|
||||
entityRef.current = entity;
|
||||
entityRef.current = entity; // eslint-disable-line react-hooks/refs
|
||||
useEntitySoundSlots(entityRef, clonedScene);
|
||||
|
||||
// Jet thrust sound. Played client-side by Player::updateJetEffects via
|
||||
|
|
@ -824,6 +923,28 @@ export function PlayerModel({ entity }: { entity: PlayerEntity }) {
|
|||
} else {
|
||||
mixer.update(0);
|
||||
}
|
||||
|
||||
// Write animated Eye bone position for first-person camera.
|
||||
// Torque's Player::getEyeTransform reads the eye node's POSITION
|
||||
// from the animated skeleton (rotation is discarded — head rotation
|
||||
// is constructed from mHead pitch/yaw instead).
|
||||
if (eyeBone) {
|
||||
clonedScene.updateWorldMatrix(false, true);
|
||||
let eyePos = playerEyePositions.get(entity.id);
|
||||
if (!eyePos) {
|
||||
eyePos = new Vector3();
|
||||
playerEyePositions.set(entity.id, eyePos);
|
||||
}
|
||||
// Get Eye bone position in GLB model-local space.
|
||||
eyeBone.getWorldPosition(eyePos);
|
||||
clonedScene.worldToLocal(eyePos);
|
||||
// Convert GLB (x,y,z) → entity-local Three.js space via R90:
|
||||
// same swizzle as PlayerEyeOffset's static extraction.
|
||||
const gx = eyePos.x;
|
||||
const gy = eyePos.y;
|
||||
const gz = eyePos.z;
|
||||
eyePos.set(gz, gy, -gx);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -854,44 +975,31 @@ export function PlayerModel({ entity }: { entity: PlayerEntity }) {
|
|||
</DebugSuspense>
|
||||
</ShapeErrorBoundary>
|
||||
)}
|
||||
{currentPackShape && mount1 && (
|
||||
<ShapeErrorBoundary
|
||||
key={currentPackShape}
|
||||
fallback={<ShapePlaceholder color="red" label={currentPackShape} />}
|
||||
>
|
||||
<DebugSuspense
|
||||
name={`Pack:${entity.id}/${currentPackShape}`}
|
||||
fallback={
|
||||
<ShapePlaceholder color="cyan" label={currentPackShape} />
|
||||
}
|
||||
>
|
||||
<PackModel
|
||||
packShape={currentPackShape}
|
||||
mountBone={mount1}
|
||||
emap={entity.emap}
|
||||
{currentPackShape &&
|
||||
mount1 &&
|
||||
createPortal(
|
||||
<Suspense>
|
||||
<MountedShapeContent
|
||||
shapeName={currentPackShape}
|
||||
imageDataBlockId={entity.imageDataBlockIds?.[2]}
|
||||
entityId={entity.id}
|
||||
/>
|
||||
</DebugSuspense>
|
||||
</ShapeErrorBoundary>
|
||||
)}
|
||||
{currentFlagShape && mount2 && (
|
||||
<ShapeErrorBoundary
|
||||
key={currentFlagShape}
|
||||
fallback={<ShapePlaceholder color="red" label={currentFlagShape} />}
|
||||
>
|
||||
<DebugSuspense
|
||||
name={`Flag:${entity.id}/${currentFlagShape}`}
|
||||
fallback={
|
||||
<ShapePlaceholder color="cyan" label={currentFlagShape} />
|
||||
}
|
||||
>
|
||||
<PackModel
|
||||
packShape={currentFlagShape}
|
||||
mountBone={mount2}
|
||||
emap={entity.emap}
|
||||
</Suspense>,
|
||||
mount1,
|
||||
)}
|
||||
{currentFlagShape &&
|
||||
mount2 &&
|
||||
createPortal(
|
||||
<Suspense>
|
||||
<MountedShapeContent
|
||||
shapeName={currentFlagShape}
|
||||
imageDataBlockId={entity.imageDataBlockIds?.[3]}
|
||||
entityId={entity.id}
|
||||
skinName={entity.imageSkinNames?.[3]}
|
||||
/>
|
||||
</DebugSuspense>
|
||||
</ShapeErrorBoundary>
|
||||
)}
|
||||
</Suspense>,
|
||||
mount2,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -966,6 +1074,10 @@ function WeaponModel({
|
|||
}) {
|
||||
const engineStore = useEngineStoreApi();
|
||||
const weaponGltf = useStaticShape(weaponShape);
|
||||
const emap = useMemo(
|
||||
() => resolveEmapFromImageSlot(entity.imageDataBlockIds?.[0]),
|
||||
[entity.imageDataBlockIds],
|
||||
);
|
||||
const anisotropy = useAnisotropy();
|
||||
|
||||
// Clone weapon with skeleton bindings, create dedicated mixer.
|
||||
|
|
@ -979,7 +1091,7 @@ function WeaponModel({
|
|||
const clone = SkeletonUtils.clone(weaponGltf.scene) as Group;
|
||||
const iflInits = processShapeScene(clone, undefined, {
|
||||
anisotropy,
|
||||
emap: entity.emap,
|
||||
emap,
|
||||
});
|
||||
|
||||
// Compute Mountpoint inverse offset so the weapon's grip aligns to Mount0.
|
||||
|
|
@ -1025,7 +1137,7 @@ function WeaponModel({
|
|||
visNodesBySequence: visBySeq,
|
||||
weaponIflInitializers: iflInits,
|
||||
};
|
||||
}, [weaponGltf, anisotropy]);
|
||||
}, [weaponGltf, anisotropy, emap]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -1317,96 +1429,3 @@ function applyWeaponAnim(
|
|||
currentAnimRef.current = targetName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a pack shape to the player's Mount1 bone. Packs are static
|
||||
* mounted images (no state machine or animation) — just positioned via
|
||||
* the pack shape's Mountpoint node inverse offset, same as weapons.
|
||||
*/
|
||||
function PackModel({
|
||||
packShape,
|
||||
mountBone,
|
||||
emap,
|
||||
}: {
|
||||
packShape: string;
|
||||
mountBone: Object3D;
|
||||
emap?: boolean;
|
||||
}) {
|
||||
const packGltf = useStaticShape(packShape);
|
||||
const anisotropy = useAnisotropy();
|
||||
|
||||
const { packClone, packIflInitializers } = useMemo(() => {
|
||||
const clone = SkeletonUtils.clone(packGltf.scene) as Group;
|
||||
const iflInits = processShapeScene(clone, undefined, {
|
||||
anisotropy,
|
||||
emap,
|
||||
});
|
||||
|
||||
// Compute Mountpoint inverse offset so the pack aligns to Mount1.
|
||||
const mp = getPosedNodeTransform(
|
||||
packGltf.scene,
|
||||
packGltf.animations,
|
||||
"Mountpoint",
|
||||
);
|
||||
if (mp) {
|
||||
const invQuat = mp.quaternion.clone().invert();
|
||||
const invPos = mp.position.clone().negate().applyQuaternion(invQuat);
|
||||
clone.position.copy(invPos);
|
||||
clone.quaternion.copy(invQuat);
|
||||
}
|
||||
|
||||
return { packClone: clone, packIflInitializers: iflInits };
|
||||
}, [packGltf, anisotropy]);
|
||||
|
||||
useEffect(() => {
|
||||
mountBone.add(packClone);
|
||||
return () => {
|
||||
mountBone.remove(packClone);
|
||||
disposeClonedScene(packClone);
|
||||
};
|
||||
}, [packClone, mountBone]);
|
||||
|
||||
// Initialize IFL materials (animated texture sequences).
|
||||
useEffect(() => {
|
||||
const cleanups: (() => void)[] = [];
|
||||
for (const { mesh, initialize } of packIflInitializers) {
|
||||
initialize(mesh, () => streamPlaybackStore.getState().time)
|
||||
.then((dispose) => cleanups.push(dispose))
|
||||
.catch(() => {});
|
||||
}
|
||||
return () => cleanups.forEach((fn) => fn());
|
||||
}, [packIflInitializers]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the eye offset from a player model's Eye bone in the idle ("Root"
|
||||
* animation) pose. The Eye node is a child of "Bip01 Head" in the skeleton
|
||||
* hierarchy. Its world Y in GLB Y-up space gives the height above the player's
|
||||
* feet, which we use as the first-person camera offset.
|
||||
*/
|
||||
export function PlayerEyeOffset({
|
||||
shapeName,
|
||||
eyeOffsetRef,
|
||||
}: {
|
||||
shapeName: string;
|
||||
eyeOffsetRef: MutableRefObject<Vector3>;
|
||||
}) {
|
||||
const gltf = useStaticShape(shapeName);
|
||||
|
||||
useEffect(() => {
|
||||
// Get Eye node position from the posed (Root animation) skeleton.
|
||||
const eye = getPosedNodeTransform(gltf.scene, gltf.animations, "Eye");
|
||||
if (eye) {
|
||||
// Convert from GLB space to entity space via ShapeRenderer's R90:
|
||||
// R90 maps GLB (x,y,z) -> entity (z, y, -x).
|
||||
// This gives ~(0.169, 2.122, 0.0) -- 17cm forward and 2.12m up.
|
||||
eyeOffsetRef.current.set(eye.position.z, eye.position.y, -eye.position.x);
|
||||
} else {
|
||||
eyeOffsetRef.current.set(0, DEFAULT_EYE_HEIGHT, 0);
|
||||
}
|
||||
}, [gltf, eyeOffsetRef]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { Quaternion, Vector3 } from "three";
|
||||
import {
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from "../stream/playbackUtils";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { ParticleEffects } from "./ParticleEffects";
|
||||
import { PlayerEyeOffset } from "./PlayerModel";
|
||||
import { playerEyePositions } from "./PlayerModel";
|
||||
import { stopAllTrackedSounds } from "./AudioEmitter";
|
||||
import { useEngineStoreApi, advanceEffectClock } from "../state/engineStore";
|
||||
import { gameEntityStore } from "../state/gameEntityStore";
|
||||
|
|
@ -66,6 +66,7 @@ function mutateRenderFields(
|
|||
const e = renderEntity as unknown as Record<string, unknown>;
|
||||
e.threads = stream.threads;
|
||||
e.damageState = stream.damageState;
|
||||
e.weaponShape = stream.weaponShape;
|
||||
e.targetRenderFlags = stream.targetRenderFlags;
|
||||
e.iffColor = stream.iffColor;
|
||||
e.soundSlots = stream.soundSlots;
|
||||
|
|
@ -88,10 +89,55 @@ function getEntityMap(snapshot: StreamSnapshot): EntityById {
|
|||
const _tmpVec = new Vector3();
|
||||
const _interpQuatA = new Quaternion();
|
||||
const _interpQuatB = new Quaternion();
|
||||
const _headQuat = new Quaternion();
|
||||
const _bodyQuat = new Quaternion();
|
||||
const _billboardFlip = new Quaternion(0, 1, 0, 0); // 180° around Y
|
||||
const _orbitDir = new Vector3();
|
||||
const _orbitTarget = new Vector3();
|
||||
const _orbitCandidate = new Vector3();
|
||||
|
||||
/** PlayerData::maxLookAngle — all Tribes 2 armor datablocks use 1.5 rad (~85.9°). */
|
||||
const DEFAULT_MAX_LOOK_ANGLE = 1.5;
|
||||
|
||||
// Axes for head rotation quaternion construction (Three.js Y-up space).
|
||||
const _axisX = new Vector3(1, 0, 0);
|
||||
const _axisY = new Vector3(0, 1, 0);
|
||||
|
||||
/**
|
||||
* Compute first-person camera transform from entity state, matching
|
||||
* Torque's Player::getEyeTransform (binary-verified at FUN_005eead0).
|
||||
*
|
||||
* Position = worldTransform * animatedEyeNodePosition
|
||||
* Rotation = bodyQuat * headQuat(yaw, pitch)
|
||||
*
|
||||
* The eye node's animated ROTATION is discarded — only its position is used.
|
||||
* Head rotation is constructed from mHead.x (pitch) and mHead.z (yaw).
|
||||
*/
|
||||
function computeFirstPersonCamera(
|
||||
camera: { position: Vector3; quaternion: Quaternion },
|
||||
playerGroup: { position: Vector3; quaternion: Quaternion },
|
||||
eyePos: Vector3,
|
||||
headPitch: number,
|
||||
headYaw: number,
|
||||
maxLookAngle = DEFAULT_MAX_LOOK_ANGLE,
|
||||
): void {
|
||||
// Position: body position + body rotation * eye offset.
|
||||
_tmpVec.copy(eyePos).applyQuaternion(playerGroup.quaternion);
|
||||
camera.position.copy(playerGroup.position).add(_tmpVec);
|
||||
|
||||
// Rotation: body * head, where head = yaw(Y) * pitch(X).
|
||||
// In Torque (Z-up): zmat(mHead.z) * xmat(mHead.x).
|
||||
// In Three.js body-local frame: Y rotation for yaw, X for pitch.
|
||||
// Pitch is negated: Torque positive pitch = look up, but positive
|
||||
// Three.js X rotation = look down.
|
||||
const pitchRad = -headPitch * maxLookAngle;
|
||||
const yawRad = headYaw * maxLookAngle;
|
||||
_headQuat.setFromAxisAngle(_axisY, yawRad);
|
||||
_bodyQuat.setFromAxisAngle(_axisX, pitchRad);
|
||||
_headQuat.multiply(_bodyQuat); // yaw * pitch
|
||||
camera.quaternion.copy(playerGroup.quaternion).multiply(_headQuat);
|
||||
}
|
||||
|
||||
export function StreamingController({
|
||||
recording,
|
||||
}: {
|
||||
|
|
@ -102,13 +148,11 @@ export function StreamingController({
|
|||
const playbackClockRef = useRef(0);
|
||||
const prevTickSnapshotRef = useRef<StreamSnapshot | null>(null);
|
||||
const currentTickSnapshotRef = useRef<StreamSnapshot | null>(null);
|
||||
const eyeOffsetRef = useRef(new Vector3(0, DEFAULT_EYE_HEIGHT, 0));
|
||||
const streamRef = useRef<StreamingPlayback | null>(
|
||||
recording.streamingPlayback ?? null,
|
||||
);
|
||||
const publishedSnapshotRef = useRef<StreamSnapshot | null>(null);
|
||||
const lastSyncedSnapshotRef = useRef<StreamSnapshot | null>(null);
|
||||
const [firstPersonShape, setFirstPersonShape] = useState<string | null>(null);
|
||||
|
||||
const syncRenderableEntities = useCallback((snapshot: StreamSnapshot) => {
|
||||
if (snapshot === lastSyncedSnapshotRef.current) return;
|
||||
|
|
@ -179,39 +223,30 @@ export function StreamingController({
|
|||
}
|
||||
|
||||
// Removal pass: delete entities no longer in the snapshot.
|
||||
// Retain explosion entities with DTS shapes for up to 5 seconds.
|
||||
for (const [id, entity] of map) {
|
||||
if (currentIds.has(id)) continue;
|
||||
if (
|
||||
entity.renderType === "Explosion" &&
|
||||
entity.shapeName &&
|
||||
entity.spawnTime != null
|
||||
) {
|
||||
const age = snapshot.timeSec - entity.spawnTime;
|
||||
if (age < 5) continue;
|
||||
// Skip removal when the snapshot is empty — this happens during mission
|
||||
// transitions (EndGhosting clears the engine's entities, but no new ones
|
||||
// have arrived yet). Keeping the old render entities visible avoids a
|
||||
// blank screen flash; they'll be replaced when the new mission loads.
|
||||
if (currentIds.size > 0) {
|
||||
for (const [id, entity] of map) {
|
||||
if (currentIds.has(id)) continue;
|
||||
// Retain explosion entities with DTS shapes for up to 5 seconds.
|
||||
if (
|
||||
entity.renderType === "Explosion" &&
|
||||
entity.shapeName &&
|
||||
entity.spawnTime != null
|
||||
) {
|
||||
const age = snapshot.timeSec - entity.spawnTime;
|
||||
if (age < 5) continue;
|
||||
}
|
||||
map.delete(id);
|
||||
structuralChange = true;
|
||||
}
|
||||
map.delete(id);
|
||||
structuralChange = true;
|
||||
}
|
||||
|
||||
if (structuralChange) {
|
||||
gameEntityStore.getState().bumpStreamVersion();
|
||||
}
|
||||
|
||||
let nextFirstPersonShape: string | null = null;
|
||||
if (
|
||||
snapshot.camera?.mode === "first-person" &&
|
||||
snapshot.camera.controlEntityId
|
||||
) {
|
||||
const entity = map.get(snapshot.camera.controlEntityId);
|
||||
const sn = entity ? getField(entity, "shapeName") : undefined;
|
||||
if (sn) {
|
||||
nextFirstPersonShape = sn;
|
||||
}
|
||||
}
|
||||
setFirstPersonShape((prev) =>
|
||||
prev === nextFirstPersonShape ? prev : nextFirstPersonShape,
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -487,7 +522,7 @@ export function StreamingController({
|
|||
continue;
|
||||
}
|
||||
}
|
||||
if (!entity?.position) {
|
||||
if (!entity?.position || entity.hidden) {
|
||||
child.visible = false;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -610,22 +645,38 @@ export function StreamingController({
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
cameraMode === "original" &&
|
||||
mode === "first-person" &&
|
||||
root &&
|
||||
currentCamera?.controlEntityId
|
||||
) {
|
||||
// First-person camera: either add the eye offset on top of the
|
||||
// stream camera position (original mode), or fully compute the
|
||||
// camera transform from entity state (non-authoritative, e.g.
|
||||
// observing a different player than the demo recorder).
|
||||
if (mode === "first-person" && root && currentCamera?.controlEntityId) {
|
||||
const eyePos = playerEyePositions.get(currentCamera.controlEntityId);
|
||||
const playerGroup = root.children.find(
|
||||
(child) => child.name === currentCamera.controlEntityId,
|
||||
);
|
||||
if (playerGroup) {
|
||||
_tmpVec
|
||||
.copy(eyeOffsetRef.current)
|
||||
.applyQuaternion(playerGroup.quaternion);
|
||||
state.camera.position.add(_tmpVec);
|
||||
} else {
|
||||
state.camera.position.y += eyeOffsetRef.current.y;
|
||||
|
||||
if (cameraMode === "original") {
|
||||
// Stream camera position = entity base position. Add animated
|
||||
// eye bone offset (rotated by body orientation).
|
||||
if (eyePos && playerGroup) {
|
||||
_tmpVec.copy(eyePos).applyQuaternion(playerGroup.quaternion);
|
||||
state.camera.position.add(_tmpVec);
|
||||
} else {
|
||||
state.camera.position.y += DEFAULT_EYE_HEIGHT;
|
||||
}
|
||||
} else if (playerGroup) {
|
||||
// Non-authoritative: compute full camera transform from entity
|
||||
// state, matching Torque's Player::getEyeTransform.
|
||||
const controlEntity = currentEntities.get(
|
||||
currentCamera.controlEntityId,
|
||||
);
|
||||
computeFirstPersonCamera(
|
||||
state.camera,
|
||||
playerGroup,
|
||||
eyePos ?? _tmpVec.set(0, DEFAULT_EYE_HEIGHT, 0),
|
||||
controlEntity?.headPitch ?? 0,
|
||||
controlEntity?.headYaw ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -645,14 +696,6 @@ export function StreamingController({
|
|||
playback={recording.streamingPlayback}
|
||||
snapshotRef={currentTickSnapshotRef}
|
||||
/>
|
||||
{firstPersonShape && (
|
||||
<Suspense>
|
||||
<PlayerEyeOffset
|
||||
shapeName={firstPersonShape}
|
||||
eyeOffsetRef={eyeOffsetRef}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
import { useMemo } from "react";
|
||||
import { useTexture } from "@react-three/drei";
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
CanvasTexture,
|
||||
ClampToEdgeWrapping,
|
||||
NearestFilter,
|
||||
RepeatWrapping,
|
||||
SRGBColorSpace,
|
||||
Texture,
|
||||
} from "three";
|
||||
import { iflTextureToUrl, loadImageFrameList } from "../loaders";
|
||||
import { loadTextureAsync } from "../textureUtils";
|
||||
import { useTick, TICK_RATE } from "./TickProvider";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
|
||||
/** One IFL tick in seconds (Torque converts at 1/30s per tick). */
|
||||
export const IFL_TICK_SECONDS = 1 / 30;
|
||||
|
||||
export interface IflAtlas {
|
||||
texture: CanvasTexture;
|
||||
texture: CanvasTexture | Texture;
|
||||
columns: number;
|
||||
rows: number;
|
||||
/** Number of unique image slots in the atlas grid. */
|
||||
|
|
@ -33,6 +29,10 @@ export interface IflAtlas {
|
|||
totalDurationSeconds: number;
|
||||
/** Last rendered atlas slot, to avoid redundant offset updates. */
|
||||
lastSlot: number;
|
||||
/** When true, swap individual textures instead of atlas offsets (for RepeatWrapping). */
|
||||
swapMode?: boolean;
|
||||
/** Individual textures per unique frame (only set when swapMode=true). */
|
||||
frameTextures?: Texture[];
|
||||
}
|
||||
|
||||
// Module-level cache for atlas textures, shared across all components.
|
||||
|
|
@ -117,6 +117,39 @@ function createAtlas(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a swap-mode "atlas" for textures that need RepeatWrapping.
|
||||
* Instead of packing into a grid, stores individual textures and swaps
|
||||
* the material's map on each frame change.
|
||||
*/
|
||||
function createSwapAtlas(
|
||||
uniqueTextures: Texture[],
|
||||
frameToSlot: number[],
|
||||
): IflAtlas {
|
||||
if (uniqueTextures.length === 0) {
|
||||
throw new Error("Cannot create IFL swap atlas with no textures");
|
||||
}
|
||||
// Set up each texture for repeat wrapping.
|
||||
for (const tex of uniqueTextures) {
|
||||
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||
tex.colorSpace = SRGBColorSpace;
|
||||
tex.flipY = false;
|
||||
tex.needsUpdate = true;
|
||||
}
|
||||
return {
|
||||
texture: uniqueTextures[0],
|
||||
columns: 1,
|
||||
rows: 1,
|
||||
slotCount: uniqueTextures.length,
|
||||
frameToSlot,
|
||||
frameOffsetSeconds: [],
|
||||
totalDurationSeconds: 0,
|
||||
lastSlot: -1,
|
||||
swapMode: true,
|
||||
frameTextures: uniqueTextures,
|
||||
};
|
||||
}
|
||||
|
||||
function computeTiming(
|
||||
atlas: IflAtlas,
|
||||
frames: { name: string; frameCount: number }[],
|
||||
|
|
@ -133,15 +166,28 @@ function computeTiming(
|
|||
* Set the atlas texture offset to show the image for the given IFL entry.
|
||||
* Uses `frameToSlot` to map the entry index to the atlas grid position.
|
||||
*/
|
||||
export function updateAtlasFrame(atlas: IflAtlas, frameIndex: number) {
|
||||
/**
|
||||
* Update the atlas to show the given frame. In atlas mode, adjusts texture
|
||||
* offset. In swap mode, updates `atlas.texture` to the frame's texture
|
||||
* (caller must apply it to the material's .map).
|
||||
* Returns true if the texture changed (swap mode only — caller needs to
|
||||
* update material.map).
|
||||
*/
|
||||
export function updateAtlasFrame(atlas: IflAtlas, frameIndex: number): boolean {
|
||||
const slot = atlas.frameToSlot[frameIndex] ?? 0;
|
||||
if (slot === atlas.lastSlot) return;
|
||||
if (slot === atlas.lastSlot) return false;
|
||||
atlas.lastSlot = slot;
|
||||
|
||||
if (atlas.swapMode && atlas.frameTextures) {
|
||||
atlas.texture = atlas.frameTextures[slot] ?? atlas.frameTextures[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
const col = slot % atlas.columns;
|
||||
// Flip row: canvas Y=0 is top, but texture V=0 is bottom.
|
||||
const row = atlas.rows - 1 - Math.floor(slot / atlas.columns);
|
||||
atlas.texture.offset.set(col / atlas.columns, row / atlas.rows);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,8 +210,12 @@ export function getFrameIndexForTime(atlas: IflAtlas, seconds: number): number {
|
|||
* if the same IFL has been loaded before. The returned atlas can be animated
|
||||
* per-frame with `updateAtlasFrame` + `getFrameIndexForTime`.
|
||||
*/
|
||||
export async function loadIflAtlas(iflPath: string): Promise<IflAtlas> {
|
||||
const cached = atlasCache.get(iflPath);
|
||||
export async function loadIflAtlas(
|
||||
iflPath: string,
|
||||
options?: { repeat?: boolean },
|
||||
): Promise<IflAtlas> {
|
||||
const cacheKey = options?.repeat ? `${iflPath}:repeat` : iflPath;
|
||||
const cached = atlasCache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const frames = await loadImageFrameList(iflPath);
|
||||
|
|
@ -173,52 +223,12 @@ export async function loadIflAtlas(iflPath: string): Promise<IflAtlas> {
|
|||
const urls = uniqueNames.map((name) => iflTextureToUrl(name, iflPath));
|
||||
const textures = await Promise.all(urls.map(loadTextureAsync));
|
||||
|
||||
const atlas = createAtlas(textures, frameToSlot);
|
||||
// Use swap mode for repeating textures (atlas ClampToEdge breaks tiling).
|
||||
const atlas = options?.repeat
|
||||
? createSwapAtlas(textures, frameToSlot)
|
||||
: createAtlas(textures, frameToSlot);
|
||||
computeTiming(atlas, frames);
|
||||
atlasCache.set(iflPath, atlas);
|
||||
atlasCache.set(cacheKey, atlas);
|
||||
|
||||
return atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an IFL (Image File List) and returns an animated texture.
|
||||
* The texture atlas is shared across all components using the same IFL path.
|
||||
*/
|
||||
export function useIflTexture(iflPath: string): Texture {
|
||||
const { animationEnabled } = useSettings();
|
||||
|
||||
const { data: frames } = useSuspenseQuery({
|
||||
queryKey: ["ifl", iflPath],
|
||||
queryFn: () => loadImageFrameList(iflPath),
|
||||
});
|
||||
|
||||
const { uniqueNames, frameToSlot } = useMemo(
|
||||
() => deduplicateFrames(frames),
|
||||
[frames],
|
||||
);
|
||||
|
||||
const textureUrls = useMemo(
|
||||
() => uniqueNames.map((name) => iflTextureToUrl(name, iflPath)),
|
||||
[uniqueNames, iflPath],
|
||||
);
|
||||
|
||||
const textures = useTexture(textureUrls);
|
||||
|
||||
const atlas = useMemo(() => {
|
||||
let cached = atlasCache.get(iflPath);
|
||||
if (!cached) {
|
||||
cached = createAtlas(textures, frameToSlot);
|
||||
atlasCache.set(iflPath, cached);
|
||||
}
|
||||
computeTiming(cached, frames);
|
||||
return cached;
|
||||
}, [iflPath, textures, frames, frameToSlot]);
|
||||
|
||||
useTick((tick) => {
|
||||
const time = tick / TICK_RATE;
|
||||
const frameIndex = animationEnabled ? getFrameIndexForTime(atlas, time) : 0;
|
||||
updateAtlasFrame(atlas, frameIndex);
|
||||
});
|
||||
|
||||
return atlas.texture;
|
||||
}
|
||||
40
src/components/resolveEmap.ts
Normal file
40
src/components/resolveEmap.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Resolve the `emap` flag from a datablock. Works for both streaming mode
|
||||
* (numeric datablock ID) and mission mode (name-based lookup).
|
||||
*/
|
||||
|
||||
import { engineStore } from "../state/engineStore";
|
||||
|
||||
export function resolveEmapFromDatablock(
|
||||
dataBlockId?: number,
|
||||
dataBlockName?: string,
|
||||
): boolean {
|
||||
// Streaming mode: look up by numeric ID.
|
||||
if (dataBlockId != null) {
|
||||
const sp = engineStore.getState().playback.recording?.streamingPlayback;
|
||||
if (sp) {
|
||||
const db = sp.getDataBlockData(dataBlockId);
|
||||
return !!db?.emap;
|
||||
}
|
||||
}
|
||||
// Mission mode: look up by name via TorqueScript runtime.
|
||||
if (dataBlockName) {
|
||||
const runtime = engineStore.getState().runtime.runtime;
|
||||
if (runtime) {
|
||||
const db = runtime.state.datablocks.get(dataBlockName);
|
||||
return !!db?.emap;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Resolve emap for a mounted image by its datablock ID. */
|
||||
export function resolveEmapFromImageSlot(imageDataBlockId?: number): boolean {
|
||||
if (imageDataBlockId == null) return false;
|
||||
const sp = engineStore.getState().playback.recording?.streamingPlayback;
|
||||
if (sp) {
|
||||
const db = sp.getDataBlockData(imageDataBlockId);
|
||||
return !!db?.emap;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -89,9 +89,16 @@ export function useEntitySoundSlots(
|
|||
const playback = engineStore.getState().playback;
|
||||
const isPlaying = playback.status === "playing";
|
||||
|
||||
// Build index for O(1) slot lookup (avoids find() per slot per frame).
|
||||
const slotByIndex: Array<
|
||||
{ index: number; playing: boolean; profileId?: number } | undefined
|
||||
> = [];
|
||||
if (soundSlots) {
|
||||
for (const s of soundSlots) slotByIndex[s.index] = s;
|
||||
}
|
||||
|
||||
for (let i = 0; i < MAX_SOUND_SLOTS; i++) {
|
||||
// Find this slot's data from the ghost update.
|
||||
const slotData = soundSlots?.find((s) => s.index === i);
|
||||
const slotData = slotByIndex[i];
|
||||
const shouldPlay = !!slotData?.playing && slotData.profileId != null;
|
||||
const profileId = slotData?.profileId ?? -1;
|
||||
const current = slots[i];
|
||||
|
|
|
|||
22
src/components/useRotation.ts
Normal file
22
src/components/useRotation.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { RefObject } from "react";
|
||||
import { Group } from "three";
|
||||
import { ShapeEntity } from "../state/gameEntityTypes";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export function useRotation(entity: ShapeEntity, ref: RefObject<Group | null>) {
|
||||
const { animationEnabled } = useSettings();
|
||||
|
||||
useFrame(
|
||||
entity.rotate && animationEnabled
|
||||
? () => {
|
||||
if (ref.current) {
|
||||
const t = performance.now() / 1000;
|
||||
ref.current.rotation.y = (t / 3.0) * Math.PI * 2;
|
||||
}
|
||||
}
|
||||
: noop,
|
||||
);
|
||||
}
|
||||
|
|
@ -65,9 +65,12 @@ export function iflTextureToUrl(name: string, iflPath: string) {
|
|||
return getUrlForPath(resourceKey, FALLBACK_TEXTURE_URL);
|
||||
}
|
||||
|
||||
export function textureToUrl(name: string) {
|
||||
export function textureToUrl(
|
||||
name: string,
|
||||
fallbackUrl: string | null = FALLBACK_TEXTURE_URL,
|
||||
): string {
|
||||
const resourceKey = getStandardTextureResourceKey(`textures/${name}`);
|
||||
return getUrlForPath(resourceKey, FALLBACK_TEXTURE_URL);
|
||||
return getUrlForPath(resourceKey, fallbackUrl ?? undefined);
|
||||
}
|
||||
|
||||
export function audioToUrl(fileName: string) {
|
||||
|
|
|
|||
|
|
@ -127,14 +127,15 @@ export interface ShapeEntity extends PositionedBase {
|
|||
shapeName?: string;
|
||||
shapeType?: string;
|
||||
dataBlock?: string;
|
||||
/** Datablock enables environment map reflections. */
|
||||
emap?: boolean;
|
||||
/** Datablock IDs for each mounted image slot (0-3). */
|
||||
imageDataBlockIds?: (number | undefined)[];
|
||||
/** Skin name from TargetManager (e.g. "beagle" for flags, team skins). */
|
||||
skinName?: string;
|
||||
threads?: ThreadState[];
|
||||
/** Torque DamageState: 0=Enabled, 1=Disabled, 2=Destroyed. */
|
||||
damageState?: number;
|
||||
rotate?: boolean;
|
||||
teamId?: number;
|
||||
barrelShapeName?: string;
|
||||
targetRenderFlags?: number;
|
||||
iffColor?: { r: number; g: number; b: number };
|
||||
weaponShape?: string;
|
||||
|
|
@ -160,8 +161,10 @@ export interface PlayerEntity extends PositionedBase {
|
|||
renderType: "Player";
|
||||
shapeName?: string;
|
||||
dataBlock?: string;
|
||||
/** Datablock enables environment map reflections. */
|
||||
emap?: boolean;
|
||||
/** Datablock IDs for each mounted image slot (0-3). */
|
||||
imageDataBlockIds?: (number | undefined)[];
|
||||
/** Skin names for each mounted image slot (e.g. flag team skin on slot 3). */
|
||||
imageSkinNames?: (string | undefined)[];
|
||||
weaponShape?: string;
|
||||
/** Arm blend animation action index from Player ghost (networked). */
|
||||
armAction?: number;
|
||||
|
|
@ -171,6 +174,10 @@ export interface PlayerEntity extends PositionedBase {
|
|||
falling?: boolean;
|
||||
jetting?: boolean;
|
||||
playerName?: string;
|
||||
/** Player skin (team skin like "base", "baseb"). */
|
||||
skinName?: string;
|
||||
/** Player preferred skin (chosen skin like "RandySavage"). */
|
||||
skinPrefName?: string;
|
||||
iffColor?: { r: number; g: number; b: number };
|
||||
threads?: ThreadState[];
|
||||
weaponImageState?: WeaponImageState;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
yawPitchToQuaternion,
|
||||
playerYawToQuaternion,
|
||||
torqueQuatToThreeJS,
|
||||
matrixFToThreeJSQuat,
|
||||
torqueQuatHeading,
|
||||
torqueQuatPitch,
|
||||
isValidPosition,
|
||||
|
|
@ -71,12 +72,16 @@ export interface MutableEntity {
|
|||
dataBlockId?: number;
|
||||
shapeHint?: string;
|
||||
dataBlock?: string;
|
||||
/** Datablock enables environment map reflections. */
|
||||
emap?: boolean;
|
||||
visual?: StreamVisual;
|
||||
direction?: [number, number, number];
|
||||
weaponShape?: string;
|
||||
/** Datablock IDs for each mounted image slot (0-3). */
|
||||
imageDataBlockIds?: (number | undefined)[];
|
||||
playerName?: string;
|
||||
/** Player skin (team skin like "base", "baseb"). */
|
||||
skinName?: string;
|
||||
/** Player preferred skin override (chosen skin like "RandySavage"). */
|
||||
skinPrefName?: string;
|
||||
position?: [number, number, number];
|
||||
rotation: [number, number, number, number];
|
||||
velocity?: [number, number, number];
|
||||
|
|
@ -106,18 +111,24 @@ export interface MutableEntity {
|
|||
weaponImageStatesDbId?: number;
|
||||
packShape?: string;
|
||||
flagShape?: string;
|
||||
/** Skin names for each mounted image slot (from ghost ImageMask skinTag). */
|
||||
imageSkinNames?: (string | undefined)[];
|
||||
falling?: boolean;
|
||||
jetting?: boolean;
|
||||
headPitch?: number;
|
||||
headYaw?: number;
|
||||
targetRenderFlags?: number;
|
||||
carryingFlag?: boolean;
|
||||
/** ShapeBase sound slots (4 max). Raw ghost SoundMask data — components
|
||||
* manage PositionalAudio objects directly, matching Tribes 2's approach. */
|
||||
soundSlots?: Array<{ index: number; playing: boolean; profileId?: number }>;
|
||||
/** ShapeBase hidden state (binaryCloak from CloakMask). True = invisible.
|
||||
* Flags are hidden when carried by a player; the flag Item ghost stays
|
||||
* in scope but shouldn't render. */
|
||||
hidden?: boolean;
|
||||
/** Item mStatic flag (from InitialUpdateMask). Static items (flags at
|
||||
* flagstand) skip all physics in Item::processTick. */
|
||||
isStaticItem?: boolean;
|
||||
|
||||
/** Item velocity interpolation state. The client simulates full physics
|
||||
* (gravity, collision, bounce) for non-static, non-at-rest items. */
|
||||
itemPhysics?: {
|
||||
|
|
@ -212,6 +223,8 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
|
||||
// ── Target system ──
|
||||
protected targetNames = new Map<number, string>();
|
||||
protected targetSkins = new Map<number, string>();
|
||||
protected targetSkinPrefs = new Map<number, string>();
|
||||
protected targetTeams = new Map<number, number>();
|
||||
protected targetRenderFlags = new Map<number, number>();
|
||||
/** Deferred nameTag→targetId for TargetInfoEvents that arrived before their NetStringEvent. */
|
||||
|
|
@ -375,6 +388,8 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
this.audioEvents = [];
|
||||
this.netStrings.clear();
|
||||
this.targetNames.clear();
|
||||
this.targetSkins.clear();
|
||||
this.targetSkinPrefs.clear();
|
||||
this.targetTeams.clear();
|
||||
this.targetRenderFlags.clear();
|
||||
this.sensorGroupColors.clear();
|
||||
|
|
@ -619,18 +634,31 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
if (targetId != null && renderFlags != null) {
|
||||
this.targetRenderFlags.set(targetId, renderFlags);
|
||||
}
|
||||
// Skin tags — resolve via net string table.
|
||||
const skinTag = data.skinTag as number | undefined;
|
||||
if (targetId != null && skinTag != null && skinTag !== 0x400) {
|
||||
const resolved = this.netStrings.get(skinTag);
|
||||
if (resolved) this.targetSkins.set(targetId, resolved);
|
||||
}
|
||||
const skinPrefTag = data.skinPrefTag as number | undefined;
|
||||
if (targetId != null && skinPrefTag != null && skinPrefTag !== 0x400) {
|
||||
const resolved = this.netStrings.get(skinPrefTag);
|
||||
if (resolved) this.targetSkinPrefs.set(targetId, resolved);
|
||||
}
|
||||
// Apply all known target info to existing entities.
|
||||
if (targetId != null) {
|
||||
const name = this.targetNames.get(targetId);
|
||||
const team = this.targetTeams.get(targetId);
|
||||
const rf = this.targetRenderFlags.get(targetId);
|
||||
const skin = this.targetSkins.get(targetId);
|
||||
const skinPref = this.targetSkinPrefs.get(targetId);
|
||||
for (const entity of this.entities.values()) {
|
||||
if (entity.targetId === targetId) {
|
||||
if (name) {
|
||||
entity.playerName = name;
|
||||
}
|
||||
if (name) entity.playerName = name;
|
||||
if (team != null) entity.sensorGroup = team;
|
||||
if (rf != null) entity.targetRenderFlags = rf;
|
||||
if (skin) entity.skinName = skin;
|
||||
if (skinPref) entity.skinPrefName = skinPref;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -922,11 +950,15 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.visual = undefined;
|
||||
entity.targetId = undefined;
|
||||
entity.targetRenderFlags = undefined;
|
||||
entity.carryingFlag = undefined;
|
||||
entity.sensorGroup = undefined;
|
||||
entity.playerName = undefined;
|
||||
entity.weaponShape = undefined;
|
||||
entity.imageDataBlockIds = undefined;
|
||||
entity.packShape = undefined;
|
||||
entity.flagShape = undefined;
|
||||
entity.imageSkinNames = undefined;
|
||||
entity.skinName = undefined;
|
||||
entity.skinPrefName = undefined;
|
||||
entity.falling = undefined;
|
||||
entity.jetting = undefined;
|
||||
entity.weaponImageState = undefined;
|
||||
|
|
@ -940,6 +972,7 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.energy = undefined;
|
||||
entity.maxEnergy = undefined;
|
||||
entity.damageState = undefined;
|
||||
entity.hidden = undefined;
|
||||
entity.actionAnim = undefined;
|
||||
entity.actionAtEnd = undefined;
|
||||
entity.armAction = undefined;
|
||||
|
|
@ -970,9 +1003,6 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.shapeHint = shapeName;
|
||||
entity.dataBlock = shapeName;
|
||||
}
|
||||
if (blockData && "emap" in blockData) {
|
||||
entity.emap = !!blockData.emap;
|
||||
}
|
||||
if (
|
||||
entity.type === "Player" &&
|
||||
typeof blockData?.maxEnergy === "number"
|
||||
|
|
@ -1075,12 +1105,15 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.frozen = data.frozen;
|
||||
}
|
||||
|
||||
// Weapon images (Player)
|
||||
if (entity.type === "Player") {
|
||||
// Mounted images (ShapeBase slot 0-3). All ShapeBase subclasses have
|
||||
// image slots: Player weapons, Turret barrels, Vehicle turrets, etc.
|
||||
{
|
||||
const images = data.images as
|
||||
| Array<{
|
||||
index?: number;
|
||||
dataBlockId?: number;
|
||||
skinTagIndex?: number;
|
||||
skinName?: string;
|
||||
triggerDown?: boolean;
|
||||
ammo?: boolean;
|
||||
loaded?: boolean;
|
||||
|
|
@ -1089,20 +1122,16 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
fireCount?: number;
|
||||
}>
|
||||
| undefined;
|
||||
if (Array.isArray(images) && images.length > 0) {
|
||||
if (images && images.length > 0) {
|
||||
const weaponImage = images.find((img) => img.index === 0);
|
||||
if (weaponImage?.dataBlockId && weaponImage.dataBlockId > 0) {
|
||||
const blockData = this.getDataBlockData(weaponImage.dataBlockId);
|
||||
const weaponShape = resolveShapeName("ShapeBaseImageData", blockData);
|
||||
if (weaponShape) {
|
||||
const mountPoint = blockData?.mountPoint as number | undefined;
|
||||
if (
|
||||
(mountPoint == null || mountPoint <= 0) &&
|
||||
!/pack_/i.test(weaponShape)
|
||||
) {
|
||||
entity.weaponShape = weaponShape;
|
||||
}
|
||||
entity.weaponShape = weaponShape;
|
||||
}
|
||||
if (!entity.imageDataBlockIds) entity.imageDataBlockIds = [];
|
||||
entity.imageDataBlockIds[0] = weaponImage.dataBlockId;
|
||||
|
||||
const prev = entity.weaponImageState;
|
||||
entity.weaponImageState = {
|
||||
|
|
@ -1126,47 +1155,74 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.weaponShape = undefined;
|
||||
entity.weaponImageState = undefined;
|
||||
entity.weaponImageStates = undefined;
|
||||
if (entity.imageDataBlockIds) entity.imageDataBlockIds[0] = undefined;
|
||||
}
|
||||
|
||||
// Pack image (slot 2 = $BackpackSlot, mountPoint 1 = Mount1)
|
||||
const packImage = images.find((img) => img.index === 2);
|
||||
if (packImage?.dataBlockId && packImage.dataBlockId > 0) {
|
||||
const blockData = this.getDataBlockData(packImage.dataBlockId);
|
||||
const shape = resolveShapeName("ShapeBaseImageData", blockData);
|
||||
if (shape) {
|
||||
entity.packShape = shape;
|
||||
// Pack and flag image slots are Player-only. Other ShapeBase types
|
||||
// (vehicles, turrets) can have weapons in slots 2-3 but those are NOT
|
||||
// packs/flags — only slot 0 is universal.
|
||||
if (entity.type === "Player") {
|
||||
// Pack image (slot 2 = $BackpackSlot, mountPoint 1 = Mount1)
|
||||
const packImage = images.find((img) => img.index === 2);
|
||||
if (packImage?.dataBlockId && packImage.dataBlockId > 0) {
|
||||
const blockData = this.getDataBlockData(packImage.dataBlockId);
|
||||
const shape = resolveShapeName("ShapeBaseImageData", blockData);
|
||||
if (shape) {
|
||||
entity.packShape = shape;
|
||||
}
|
||||
if (!entity.imageDataBlockIds) entity.imageDataBlockIds = [];
|
||||
entity.imageDataBlockIds[2] = packImage.dataBlockId;
|
||||
} else if (packImage && !packImage.dataBlockId) {
|
||||
entity.packShape = undefined;
|
||||
if (entity.imageDataBlockIds)
|
||||
entity.imageDataBlockIds[2] = undefined;
|
||||
}
|
||||
|
||||
// Flag image (slot 3 = $FlagSlot, mountPoint 2 = Mount2)
|
||||
const flagImage = images.find((img) => img.index === 3);
|
||||
if (flagImage?.dataBlockId && flagImage.dataBlockId > 0) {
|
||||
if (!entity.imageDataBlockIds) entity.imageDataBlockIds = [];
|
||||
entity.imageDataBlockIds[3] = flagImage.dataBlockId;
|
||||
const blockData = this.getDataBlockData(flagImage.dataBlockId);
|
||||
const shape = resolveShapeName("ShapeBaseImageData", blockData);
|
||||
if (shape) {
|
||||
entity.flagShape = shape;
|
||||
}
|
||||
if (entity.targetId != null && entity.targetId >= 0) {
|
||||
const prev = this.targetRenderFlags.get(entity.targetId) ?? 0;
|
||||
const updated = prev | 0x2;
|
||||
if (updated !== prev) {
|
||||
this.targetRenderFlags.set(entity.targetId, updated);
|
||||
entity.targetRenderFlags = updated;
|
||||
}
|
||||
}
|
||||
} else if (flagImage && !flagImage.dataBlockId) {
|
||||
entity.flagShape = undefined;
|
||||
if (entity.imageDataBlockIds)
|
||||
entity.imageDataBlockIds[3] = undefined;
|
||||
if (entity.targetId != null && entity.targetId >= 0) {
|
||||
const prev = this.targetRenderFlags.get(entity.targetId) ?? 0;
|
||||
const updated = prev & ~0x2;
|
||||
if (updated !== prev) {
|
||||
this.targetRenderFlags.set(entity.targetId, updated);
|
||||
entity.targetRenderFlags = updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (packImage && !packImage.dataBlockId) {
|
||||
entity.packShape = undefined;
|
||||
}
|
||||
|
||||
// Flag image (slot 3 = $FlagSlot, mountPoint 2 = Mount2)
|
||||
const flagImage = images.find((img) => img.index === 3);
|
||||
if (flagImage?.dataBlockId && flagImage.dataBlockId > 0) {
|
||||
entity.carryingFlag = true;
|
||||
const blockData = this.getDataBlockData(flagImage.dataBlockId);
|
||||
const shape = resolveShapeName("ShapeBaseImageData", blockData);
|
||||
if (shape) {
|
||||
entity.flagShape = shape;
|
||||
// Per-slot image skin tags (e.g. flag team skin on slot 3).
|
||||
for (const img of images) {
|
||||
if (img.index == null) continue;
|
||||
let skinName: string | undefined;
|
||||
if (img.skinTagIndex != null) {
|
||||
skinName = this.netStrings.get(img.skinTagIndex);
|
||||
} else if (img.skinName) {
|
||||
skinName = img.skinName;
|
||||
}
|
||||
if (entity.targetId != null && entity.targetId >= 0) {
|
||||
const prev = this.targetRenderFlags.get(entity.targetId) ?? 0;
|
||||
const updated = prev | 0x2;
|
||||
if (updated !== prev) {
|
||||
this.targetRenderFlags.set(entity.targetId, updated);
|
||||
entity.targetRenderFlags = updated;
|
||||
}
|
||||
}
|
||||
} else if (flagImage && !flagImage.dataBlockId) {
|
||||
entity.carryingFlag = false;
|
||||
entity.flagShape = undefined;
|
||||
if (entity.targetId != null && entity.targetId >= 0) {
|
||||
const prev = this.targetRenderFlags.get(entity.targetId) ?? 0;
|
||||
const updated = prev & ~0x2;
|
||||
if (updated !== prev) {
|
||||
this.targetRenderFlags.set(entity.targetId, updated);
|
||||
entity.targetRenderFlags = updated;
|
||||
}
|
||||
if (skinName) {
|
||||
if (!entity.imageSkinNames) entity.imageSkinNames = [];
|
||||
entity.imageSkinNames[img.index] = skinName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1223,6 +1279,16 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
).rotation,
|
||||
);
|
||||
if (converted) entity.rotation = converted;
|
||||
} else if (
|
||||
Array.isArray(
|
||||
(data.transform as { elements?: unknown } | undefined)?.elements,
|
||||
)
|
||||
) {
|
||||
// MatrixF (16 floats) — decompose rotation from the 3x3 submatrix.
|
||||
const converted = matrixFToThreeJSQuat(
|
||||
(data.transform as { elements: number[] }).elements,
|
||||
);
|
||||
if (converted) entity.rotation = converted;
|
||||
} else if (
|
||||
entity.type === "Item" &&
|
||||
typeof (data.rotation as { angle?: unknown } | undefined)?.angle ===
|
||||
|
|
@ -1271,7 +1337,11 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
}
|
||||
}
|
||||
const atRest = data.atRest as boolean | undefined;
|
||||
if (atRest === false && !entity.isStaticItem && isVec3Like(data.velocity)) {
|
||||
if (
|
||||
atRest === false &&
|
||||
!entity.isStaticItem &&
|
||||
isVec3Like(data.velocity)
|
||||
) {
|
||||
const vel = data.velocity as Vec3;
|
||||
entity.itemPhysics = {
|
||||
velocity: [vel.x, vel.y, vel.z],
|
||||
|
|
@ -1379,6 +1449,11 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
if (typeof data.damageState === "number") {
|
||||
entity.damageState = data.damageState;
|
||||
}
|
||||
// CloakMask: binaryCloak is mFadeVal == 1.0 (binary-verified at
|
||||
// FUN_005eead0). true = fully visible, false = invisible (faded out).
|
||||
if (typeof data.binaryCloak === "boolean") {
|
||||
entity.hidden = !data.binaryCloak;
|
||||
}
|
||||
if (typeof data.action === "number") {
|
||||
entity.actionAnim = data.action;
|
||||
entity.actionAtEnd = !!data.actionAtEnd;
|
||||
|
|
@ -1387,6 +1462,18 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
entity.armAction = data.armAction;
|
||||
}
|
||||
|
||||
// MountedMask: when a player unmounts from a vehicle, the server resets
|
||||
// to RootAnim (table action 0) via Player::onUnmount→setActionThread.
|
||||
// Table actions (0-6) are never sent over the wire, so actionAnim would
|
||||
// remain at the stale Sitting index. Clear it on unmount.
|
||||
if (typeof data.mountObject === "number") {
|
||||
if (data.mountObject === -1) {
|
||||
// Unmounting — reset action to table anim (not sent by server).
|
||||
entity.actionAnim = undefined;
|
||||
entity.actionAtEnd = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Threads
|
||||
if (Array.isArray(data.threads)) {
|
||||
const incoming = data.threads as ThreadState[];
|
||||
|
|
@ -1424,6 +1511,10 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
}
|
||||
const renderFlags = this.targetRenderFlags.get(data.targetId);
|
||||
if (renderFlags != null) entity.targetRenderFlags = renderFlags;
|
||||
const skin = this.targetSkins.get(data.targetId);
|
||||
if (skin) entity.skinName = skin;
|
||||
const skinPref = this.targetSkinPrefs.get(data.targetId);
|
||||
if (skinPref) entity.skinPrefName = skinPref;
|
||||
}
|
||||
|
||||
// ShapeBase sound slots — store on entity for component-level playback.
|
||||
|
|
@ -1599,8 +1690,14 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
for (const entity of this.entities.values()) {
|
||||
const phys = entity.itemPhysics;
|
||||
// Binary-verified: Item::processTick skips physics when
|
||||
// mStatic || mAtRest || isHidden. We check static and atRest.
|
||||
if (!phys || phys.atRest || entity.isStaticItem || !entity.position)
|
||||
// mStatic || mAtRest || isHidden.
|
||||
if (
|
||||
!phys ||
|
||||
phys.atRest ||
|
||||
entity.isStaticItem ||
|
||||
entity.hidden ||
|
||||
!entity.position
|
||||
)
|
||||
continue;
|
||||
const v = phys.velocity;
|
||||
const p = entity.position;
|
||||
|
|
@ -2225,7 +2322,7 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
? (this.targetRenderFlags.get(entity.targetId) ??
|
||||
entity.targetRenderFlags)
|
||||
: entity.targetRenderFlags;
|
||||
if (entity.type === "Player" && !entity.carryingFlag) {
|
||||
if (entity.type === "Player" && !entity.flagShape) {
|
||||
renderFlags = renderFlags != null ? renderFlags & ~0x2 : renderFlags;
|
||||
}
|
||||
|
||||
|
|
@ -2240,11 +2337,15 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
shapeHint: entity.shapeHint,
|
||||
dataBlock: entity.dataBlock,
|
||||
weaponShape: entity.weaponShape,
|
||||
imageDataBlockIds: entity.imageDataBlockIds,
|
||||
imageSkinNames: entity.imageSkinNames,
|
||||
packShape: entity.packShape,
|
||||
flagShape: entity.flagShape,
|
||||
falling: entity.falling,
|
||||
jetting: entity.jetting,
|
||||
playerName: entity.playerName,
|
||||
skinName: entity.skinName,
|
||||
skinPrefName: entity.skinPrefName,
|
||||
targetRenderFlags: renderFlags,
|
||||
iffColor:
|
||||
(entity.type === "Player" || ((renderFlags ?? 0) & 0x2) !== 0) &&
|
||||
|
|
@ -2265,6 +2366,7 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
actionAtEnd: entity.actionAtEnd,
|
||||
armAction: entity.armAction,
|
||||
damageState: entity.damageState,
|
||||
hidden: entity.hidden,
|
||||
faceViewer: entity.faceViewer,
|
||||
threads: entity.threads,
|
||||
explosionDataBlockId: entity.explosionDataBlockId,
|
||||
|
|
|
|||
|
|
@ -339,6 +339,8 @@ class StreamingPlayback extends StreamEngine {
|
|||
targetEntries: Array<{
|
||||
targetId: number;
|
||||
name?: string;
|
||||
skin?: string;
|
||||
skinPref?: string;
|
||||
sensorGroup: number;
|
||||
targetData: number;
|
||||
}>;
|
||||
|
|
@ -513,6 +515,9 @@ class StreamingPlayback extends StreamEngine {
|
|||
stripTaggedStringMarkup(entry.name).trim(),
|
||||
);
|
||||
}
|
||||
if (entry.skin) this.targetSkins.set(entry.targetId, entry.skin);
|
||||
if (entry.skinPref)
|
||||
this.targetSkinPrefs.set(entry.targetId, entry.skinPref);
|
||||
this.targetTeams.set(entry.targetId, entry.sensorGroup);
|
||||
this.targetRenderFlags.set(entry.targetId, entry.targetData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,165 +94,159 @@ export function streamEntityToGameEntity(
|
|||
}
|
||||
|
||||
// Projectile visuals
|
||||
if (entity.visual?.kind === "tracer") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Tracer",
|
||||
visual: entity.visual,
|
||||
dataBlock: entity.dataBlock,
|
||||
direction: entity.direction,
|
||||
} satisfies TracerEntity;
|
||||
}
|
||||
if (entity.visual?.kind === "sprite") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Sprite",
|
||||
visual: entity.visual,
|
||||
} satisfies SpriteEntity;
|
||||
}
|
||||
|
||||
// Player
|
||||
if (entity.type === "Player") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Player",
|
||||
shapeName: entity.dataBlock,
|
||||
dataBlock: entity.dataBlock,
|
||||
emap: entity.emap,
|
||||
weaponShape: entity.weaponShape,
|
||||
armAction: entity.armAction,
|
||||
packShape: entity.packShape,
|
||||
flagShape: entity.flagShape,
|
||||
falling: entity.falling,
|
||||
jetting: entity.jetting,
|
||||
playerName: entity.playerName,
|
||||
iffColor: entity.iffColor,
|
||||
threads: entity.threads,
|
||||
weaponImageState: entity.weaponImageState,
|
||||
weaponImageStates: entity.weaponImageStates,
|
||||
headPitch: entity.headPitch,
|
||||
headYaw: entity.headYaw,
|
||||
targetRenderFlags: entity.targetRenderFlags,
|
||||
soundSlots: entity.soundSlots,
|
||||
} satisfies PlayerEntity;
|
||||
}
|
||||
|
||||
// Explosion — only render a shape if the datablock specifies one; particle-only
|
||||
// explosions (e.g. BlasterExplosion) still exist as entities for ParticleEffects.
|
||||
if (entity.type === "Explosion") {
|
||||
if (entity.dataBlock) {
|
||||
switch (entity.visual?.kind) {
|
||||
case "tracer":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Explosion",
|
||||
renderType: "Tracer",
|
||||
visual: entity.visual,
|
||||
dataBlock: entity.dataBlock,
|
||||
direction: entity.direction,
|
||||
} satisfies TracerEntity;
|
||||
case "sprite":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Sprite",
|
||||
visual: entity.visual,
|
||||
} satisfies SpriteEntity;
|
||||
}
|
||||
|
||||
switch (entity.className) {
|
||||
case "Player":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Player",
|
||||
shapeName: entity.dataBlock,
|
||||
dataBlock: entity.dataBlock,
|
||||
explosionDataBlockId: entity.explosionDataBlockId,
|
||||
faceViewer: entity.faceViewer,
|
||||
} satisfies ExplosionEntity;
|
||||
}
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "None",
|
||||
} satisfies NoneEntity;
|
||||
}
|
||||
imageDataBlockIds: entity.imageDataBlockIds,
|
||||
imageSkinNames: entity.imageSkinNames,
|
||||
weaponShape: entity.weaponShape,
|
||||
armAction: entity.armAction,
|
||||
packShape: entity.packShape,
|
||||
flagShape: entity.flagShape,
|
||||
falling: entity.falling,
|
||||
jetting: entity.jetting,
|
||||
playerName: entity.playerName,
|
||||
skinName: entity.skinName,
|
||||
skinPrefName: entity.skinPrefName,
|
||||
iffColor: entity.iffColor,
|
||||
threads: entity.threads,
|
||||
weaponImageState: entity.weaponImageState,
|
||||
weaponImageStates: entity.weaponImageStates,
|
||||
headPitch: entity.headPitch,
|
||||
headYaw: entity.headYaw,
|
||||
targetRenderFlags: entity.targetRenderFlags,
|
||||
soundSlots: entity.soundSlots,
|
||||
} satisfies PlayerEntity;
|
||||
|
||||
// Force field
|
||||
if (entity.className === "ForceFieldBare") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "ForceFieldBare",
|
||||
forceFieldData: entity.forceFieldData
|
||||
? {
|
||||
textures: entity.forceFieldData.textures,
|
||||
color: entity.forceFieldData.color,
|
||||
baseTranslucency: entity.forceFieldData.baseTranslucency,
|
||||
numFrames: entity.forceFieldData.textures.length,
|
||||
framesPerSec: entity.forceFieldData.framesPerSec,
|
||||
scrollSpeed: entity.forceFieldData.scrollSpeed,
|
||||
umapping: entity.forceFieldData.umapping,
|
||||
vmapping: entity.forceFieldData.vmapping,
|
||||
dimensions: entity.forceFieldData.dimensions,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ForceFieldBareEntity;
|
||||
}
|
||||
case "Explosion":
|
||||
// Only render a shape if the datablock specifies one; particle-only explosions
|
||||
// (e.g. BlasterExplosion) still exist as entities for ParticleEffects.
|
||||
if (entity.dataBlock) {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Explosion",
|
||||
shapeName: entity.dataBlock,
|
||||
dataBlock: entity.dataBlock,
|
||||
explosionDataBlockId: entity.explosionDataBlockId,
|
||||
faceViewer: entity.faceViewer,
|
||||
} satisfies ExplosionEntity;
|
||||
}
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "None",
|
||||
} satisfies NoneEntity;
|
||||
|
||||
// Audio emitter
|
||||
if (entity.className === "AudioEmitter") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "AudioEmitter",
|
||||
audioFileName: entity.audioFileName,
|
||||
audioVolume: entity.audioVolume,
|
||||
audioIs3D: entity.audioIs3D,
|
||||
audioIsLooping: entity.audioIsLooping ?? true,
|
||||
audioMinDistance: entity.audioMinDistance,
|
||||
audioMaxDistance: entity.audioMaxDistance,
|
||||
audioMinLoopGap: entity.audioMinLoopGap,
|
||||
audioMaxLoopGap: entity.audioMaxLoopGap,
|
||||
} satisfies AudioEmitterEntity;
|
||||
}
|
||||
case "ForceFieldBare":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "ForceFieldBare",
|
||||
forceFieldData: entity.forceFieldData
|
||||
? {
|
||||
textures: entity.forceFieldData.textures,
|
||||
color: entity.forceFieldData.color,
|
||||
baseTranslucency: entity.forceFieldData.baseTranslucency,
|
||||
numFrames: entity.forceFieldData.textures.length,
|
||||
framesPerSec: entity.forceFieldData.framesPerSec,
|
||||
scrollSpeed: entity.forceFieldData.scrollSpeed,
|
||||
umapping: entity.forceFieldData.umapping,
|
||||
vmapping: entity.forceFieldData.vmapping,
|
||||
dimensions: entity.forceFieldData.dimensions,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ForceFieldBareEntity;
|
||||
|
||||
// WayPoint
|
||||
if (entity.className === "WayPoint") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "WayPoint",
|
||||
label: entity.label,
|
||||
} satisfies WayPointEntity;
|
||||
}
|
||||
case "AudioEmitter":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "AudioEmitter",
|
||||
audioFileName: entity.audioFileName,
|
||||
audioVolume: entity.audioVolume,
|
||||
audioIs3D: entity.audioIs3D,
|
||||
audioIsLooping: entity.audioIsLooping ?? true,
|
||||
audioMinDistance: entity.audioMinDistance,
|
||||
audioMaxDistance: entity.audioMaxDistance,
|
||||
audioMinLoopGap: entity.audioMinLoopGap,
|
||||
audioMaxLoopGap: entity.audioMaxLoopGap,
|
||||
} satisfies AudioEmitterEntity;
|
||||
|
||||
// Non-rendered objects: editor-only markers, AI objectives, vehicle blockers.
|
||||
// MissionMarker::onAdd only calls addToScene when gEditingMission is true.
|
||||
// AIObjective and VehicleBlocker are server-side logic objects with no visuals.
|
||||
if (
|
||||
entity.className === "MissionMarker" ||
|
||||
entity.className === "SpawnSphere" ||
|
||||
entity.className === "AIObjective" ||
|
||||
entity.className === "VehicleBlocker"
|
||||
) {
|
||||
return {
|
||||
id: entity.id,
|
||||
className: entity.className ?? entity.type,
|
||||
ghostIndex: entity.ghostIndex,
|
||||
dataBlockId: entity.dataBlockId,
|
||||
shapeHint: entity.shapeHint,
|
||||
spawnTime,
|
||||
renderType: "None",
|
||||
} satisfies NoneEntity;
|
||||
}
|
||||
case "WayPoint":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "WayPoint",
|
||||
label: entity.label,
|
||||
} satisfies WayPointEntity;
|
||||
|
||||
// Camera
|
||||
if (entity.className === "Camera") {
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Camera",
|
||||
} satisfies CameraEntity;
|
||||
}
|
||||
// Non-rendered objects: editor-only markers, AI objectives, vehicle blockers.
|
||||
// MissionMarker::onAdd only calls addToScene when gEditingMission is true.
|
||||
// AIObjective and VehicleBlocker are server-side logic objects with no visuals.
|
||||
case "AIObjective":
|
||||
case "MissionMarker":
|
||||
case "PhysicalZone":
|
||||
case "SpawnSphere":
|
||||
case "VehicleBlocker":
|
||||
return {
|
||||
id: entity.id,
|
||||
className: entity.className ?? entity.type,
|
||||
ghostIndex: entity.ghostIndex,
|
||||
dataBlockId: entity.dataBlockId,
|
||||
shapeHint: entity.shapeHint,
|
||||
spawnTime,
|
||||
renderType: "None",
|
||||
} satisfies NoneEntity;
|
||||
|
||||
// Default: generic DTS shape
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Shape",
|
||||
shapeName: entity.dataBlock,
|
||||
shapeType:
|
||||
entity.className === "Turret"
|
||||
? "Turret"
|
||||
: entity.className === "Item"
|
||||
? "Item"
|
||||
: "StaticShape",
|
||||
dataBlock: entity.dataBlock,
|
||||
emap: entity.emap,
|
||||
damageState: entity.damageState,
|
||||
weaponShape: entity.weaponShape,
|
||||
armAction: entity.armAction,
|
||||
threads: entity.threads,
|
||||
targetRenderFlags: entity.targetRenderFlags,
|
||||
iffColor: entity.iffColor,
|
||||
wheels: entity.wheels,
|
||||
steeringYaw: entity.steeringYaw,
|
||||
frozen: entity.frozen,
|
||||
maxSteeringAngle: entity.maxSteeringAngle,
|
||||
soundSlots: entity.soundSlots,
|
||||
} satisfies ShapeEntity;
|
||||
case "Camera":
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Camera",
|
||||
} satisfies CameraEntity;
|
||||
|
||||
default:
|
||||
// Default: generic DTS shape
|
||||
return {
|
||||
...positionedBase(entity, spawnTime),
|
||||
renderType: "Shape",
|
||||
shapeName: entity.dataBlock,
|
||||
shapeType:
|
||||
entity.className === "Turret"
|
||||
? "Turret"
|
||||
: entity.className === "Item"
|
||||
? "Item"
|
||||
: "StaticShape",
|
||||
dataBlock: entity.dataBlock,
|
||||
imageDataBlockIds: entity.imageDataBlockIds,
|
||||
skinName: entity.skinName,
|
||||
damageState: entity.damageState,
|
||||
weaponShape: entity.weaponShape,
|
||||
armAction: entity.armAction,
|
||||
threads: entity.threads,
|
||||
targetRenderFlags: entity.targetRenderFlags,
|
||||
iffColor: entity.iffColor,
|
||||
wheels: entity.wheels,
|
||||
steeringYaw: entity.steeringYaw,
|
||||
frozen: entity.frozen,
|
||||
maxSteeringAngle: entity.maxSteeringAngle,
|
||||
soundSlots: entity.soundSlots,
|
||||
} satisfies ShapeEntity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
WayPointEntity,
|
||||
} from "../state/gameEntityTypes";
|
||||
import { getPosition, getProperty, getScale } from "../mission";
|
||||
import { DEFAULT_FLAG_SKINS } from "../stringUtils";
|
||||
import { parseColorTuple } from "../colorUtils";
|
||||
import {
|
||||
terrainFromMis,
|
||||
|
|
@ -211,10 +212,6 @@ function buildShapeEntity(
|
|||
shapeName,
|
||||
shapeType,
|
||||
dataBlock: datablockName || undefined,
|
||||
emap:
|
||||
getProperty(datablock, "emap") != null
|
||||
? isTruthy(getProperty(datablock, "emap"))
|
||||
: undefined,
|
||||
teamId,
|
||||
};
|
||||
|
||||
|
|
@ -222,13 +219,17 @@ function buildShapeEntity(
|
|||
entity.rotate = isTruthy(
|
||||
getProperty(object, "rotate") ?? getProperty(datablock, "rotate"),
|
||||
);
|
||||
// Flag Items get a team skin (e.g. "beagle" for Blood Eagle).
|
||||
if (datablockName.toLowerCase() === "flag" && teamId != null) {
|
||||
entity.skinName = DEFAULT_FLAG_SKINS[teamId];
|
||||
}
|
||||
}
|
||||
|
||||
if (className === "Turret") {
|
||||
const barrelName = getProperty(object, "initialBarrel");
|
||||
if (barrelName) {
|
||||
const barrelDb = resolveDatablock(runtime, barrelName);
|
||||
entity.barrelShapeName = getProperty(barrelDb, "shapeFile");
|
||||
entity.weaponShape = getProperty(barrelDb, "shapeFile");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
loadIflAtlas,
|
||||
getFrameIndexForTime,
|
||||
updateAtlasFrame,
|
||||
} from "../components/useIflTexture";
|
||||
} from "../components/iflAtlas";
|
||||
import { loadTexture, setupTexture } from "../textureUtils";
|
||||
import { textureToUrl } from "../loaders";
|
||||
import type { Keyframe } from "./types";
|
||||
|
|
@ -50,11 +50,6 @@ const _tracerOrientMat = new Matrix4();
|
|||
const _upY = new Vector3(0, 1, 0);
|
||||
|
||||
/** ShapeRenderer's 90° Y rotation and its inverse, used for mount transforms. */
|
||||
export const _r90 = new Quaternion().setFromAxisAngle(
|
||||
new Vector3(0, 1, 0),
|
||||
Math.PI / 2,
|
||||
);
|
||||
export const _r90inv = _r90.clone().invert();
|
||||
|
||||
// ── Pure functions ──
|
||||
|
||||
|
|
@ -308,7 +303,12 @@ export function replaceWithShapeMaterial(
|
|||
mat: MeshStandardMaterial,
|
||||
vis: number,
|
||||
isOrganic = false,
|
||||
options: { anisotropy?: number; emap?: boolean } = {},
|
||||
options: {
|
||||
anisotropy?: number;
|
||||
emap?: boolean;
|
||||
skinUrl?: string;
|
||||
skinName?: string;
|
||||
} = {},
|
||||
): ShapeMaterialResult {
|
||||
const resourcePath: string | undefined = mat.userData?.resource_path;
|
||||
const flagNames = new Set<string>(mat.userData?.flag_names ?? []);
|
||||
|
|
@ -332,6 +332,7 @@ export function replaceWithShapeMaterial(
|
|||
const effectiveReflectionAmount = emapEnabled ? reflectionAmount : 0;
|
||||
|
||||
if (flagNames.has("IflMaterial")) {
|
||||
const repeat = flagNames.has("SWrap") || flagNames.has("TWrap");
|
||||
const result = createMaterialFromFlags(
|
||||
mat,
|
||||
null,
|
||||
|
|
@ -347,21 +348,51 @@ export function replaceWithShapeMaterial(
|
|||
material,
|
||||
backMaterial: result[0],
|
||||
initialize: (mesh, getTime) =>
|
||||
initializeIflMaterial(material, resourcePath, mesh, getTime),
|
||||
initializeIflMaterial(material, resourcePath, mesh, getTime, repeat),
|
||||
};
|
||||
}
|
||||
return {
|
||||
material: result,
|
||||
initialize: (mesh, getTime) =>
|
||||
initializeIflMaterial(result, resourcePath, mesh, getTime),
|
||||
initializeIflMaterial(result, resourcePath, mesh, getTime, repeat),
|
||||
};
|
||||
}
|
||||
|
||||
// Load texture via ImageBitmapLoader (decodes off main thread). The returned
|
||||
// Texture is empty initially and gets populated when the image arrives;
|
||||
// Three.js re-renders automatically once loaded.
|
||||
const url = textureToUrl(resourcePath);
|
||||
const texture = loadTexture(url);
|
||||
// Torque reSkin: replace "base." prefix with "{skinName}." in the resource
|
||||
// path, then resolve to a URL. skinUrl takes precedence (pre-computed URL
|
||||
// for player custom skins from external sources). skinName does the
|
||||
// replacement locally (for flag team skins and other built-in skins).
|
||||
const isBaseTexture = /[/\\]base\./i.test(resourcePath);
|
||||
let skinTextureUrl: string | undefined;
|
||||
if (isBaseTexture && options.skinUrl) {
|
||||
skinTextureUrl = options.skinUrl;
|
||||
} else if (isBaseTexture && options.skinName && options.skinName !== "base") {
|
||||
const skinnedPath = resourcePath.replace(
|
||||
/\bbase\./i,
|
||||
`${options.skinName}.`,
|
||||
);
|
||||
try {
|
||||
skinTextureUrl = textureToUrl(skinnedPath, null);
|
||||
} catch {
|
||||
// Skin texture not found — fall through to default.
|
||||
}
|
||||
}
|
||||
const usingSkin = !!skinTextureUrl;
|
||||
const url = skinTextureUrl ?? textureToUrl(resourcePath);
|
||||
const texture = loadTexture(
|
||||
url,
|
||||
undefined,
|
||||
usingSkin
|
||||
? () => {
|
||||
// Skin failed (404) — load the default texture into the same object.
|
||||
const fallbackUrl = textureToUrl(resourcePath);
|
||||
loadTexture(fallbackUrl, (loaded) => {
|
||||
texture.image = loaded.image;
|
||||
texture.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const isTranslucent = flagNames.has("Translucent");
|
||||
if (isOrganic || isTranslucent) {
|
||||
setupTexture(texture, {
|
||||
|
|
@ -397,9 +428,10 @@ async function initializeIflMaterial(
|
|||
resourcePath: string,
|
||||
mesh: Object3D,
|
||||
getTime: () => number,
|
||||
repeat = false,
|
||||
): Promise<() => void> {
|
||||
const iflPath = `textures/${resourcePath}.ifl`;
|
||||
const atlas = await loadIflAtlas(iflPath);
|
||||
const atlas = await loadIflAtlas(iflPath, { repeat });
|
||||
|
||||
(material as any).map = atlas.texture;
|
||||
material.needsUpdate = true;
|
||||
|
|
@ -413,6 +445,12 @@ async function initializeIflMaterial(
|
|||
prevOnBeforeRender?.apply(this, args);
|
||||
if (disposed) return;
|
||||
updateAtlasFrame(atlas, getFrameIndexForTime(atlas, getTime()));
|
||||
// In swap mode, always sync material.map — multiple meshes may share
|
||||
// the same atlas but have different material instances.
|
||||
if (atlas.swapMode && (material as any).map !== atlas.texture) {
|
||||
(material as any).map = atlas.texture;
|
||||
material.needsUpdate = true;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
|
|
@ -429,7 +467,12 @@ async function initializeIflMaterial(
|
|||
export function processShapeScene(
|
||||
scene: Object3D,
|
||||
shapeName?: string,
|
||||
options: { anisotropy?: number; emap?: boolean } = {},
|
||||
options: {
|
||||
anisotropy?: number;
|
||||
emap?: boolean;
|
||||
skinUrl?: string;
|
||||
skinName?: string;
|
||||
} = {},
|
||||
): IflInitializer[] {
|
||||
const iflInitializers: IflInitializer[] = [];
|
||||
const isOrganic = shapeName ? isOrganicShape(shapeName) : false;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,61 @@ export function torqueQuatToThreeJS(q: {
|
|||
return [x * invLen, y * invLen, z * invLen, w * invLen];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a quaternion from a Torque row-major MatrixF (16 floats) and
|
||||
* convert to Three.js coordinate system. The 3x3 upper-left submatrix
|
||||
* contains the rotation. MatrixF is row-major: m[row*4+col].
|
||||
*/
|
||||
export function matrixFToThreeJSQuat(
|
||||
elements: number[],
|
||||
): [number, number, number, number] | null {
|
||||
if (elements.length < 12) return null;
|
||||
|
||||
// Extract 3x3 rotation from row-major MatrixF.
|
||||
// Row i, col j = elements[i*4 + j]
|
||||
const m00 = elements[0],
|
||||
m01 = elements[1],
|
||||
m02 = elements[2];
|
||||
const m10 = elements[4],
|
||||
m11 = elements[5],
|
||||
m12 = elements[6];
|
||||
const m20 = elements[8],
|
||||
m21 = elements[9],
|
||||
m22 = elements[10];
|
||||
|
||||
// Shepperd's method for matrix → quaternion.
|
||||
const trace = m00 + m11 + m22;
|
||||
let qx: number, qy: number, qz: number, qw: number;
|
||||
if (trace > 0) {
|
||||
const s = 0.5 / Math.sqrt(trace + 1);
|
||||
qw = 0.25 / s;
|
||||
qx = (m21 - m12) * s;
|
||||
qy = (m02 - m20) * s;
|
||||
qz = (m10 - m01) * s;
|
||||
} else if (m00 > m11 && m00 > m22) {
|
||||
const s = 2 * Math.sqrt(1 + m00 - m11 - m22);
|
||||
qw = (m21 - m12) / s;
|
||||
qx = 0.25 * s;
|
||||
qy = (m01 + m10) / s;
|
||||
qz = (m02 + m20) / s;
|
||||
} else if (m11 > m22) {
|
||||
const s = 2 * Math.sqrt(1 + m11 - m00 - m22);
|
||||
qw = (m02 - m20) / s;
|
||||
qx = (m01 + m10) / s;
|
||||
qy = 0.25 * s;
|
||||
qz = (m20 + m12) / s;
|
||||
} else {
|
||||
const s = 2 * Math.sqrt(1 + m22 - m00 - m11);
|
||||
qw = (m10 - m01) / s;
|
||||
qx = (m02 + m20) / s;
|
||||
qy = (m12 + m21) / s;
|
||||
qz = 0.25 * s;
|
||||
}
|
||||
|
||||
// Convert Torque quat to Three.js.
|
||||
return torqueQuatToThreeJS({ x: qx, y: qy, z: qz, w: qw });
|
||||
}
|
||||
|
||||
/** Extract heading (yaw around Torque Z axis) from a Torque quaternion. */
|
||||
export function torqueQuatHeading(q: {
|
||||
x: number;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ export interface StreamEntity {
|
|||
visual?: StreamVisual;
|
||||
direction?: [number, number, number];
|
||||
weaponShape?: string;
|
||||
/** Datablock IDs for each mounted image slot (0-3). */
|
||||
imageDataBlockIds?: (number | undefined)[];
|
||||
/** Skin names for each mounted image slot (0-3), from ghost ImageMask skinTag. */
|
||||
imageSkinNames?: (string | undefined)[];
|
||||
playerName?: string;
|
||||
/** IFF color resolved from the sensor group color table (sRGB 0-255). */
|
||||
iffColor?: { r: number; g: number; b: number };
|
||||
|
|
@ -108,8 +112,6 @@ export interface StreamEntity {
|
|||
className?: string;
|
||||
dataBlockId?: number;
|
||||
shapeHint?: string;
|
||||
/** Whether the datablock enables environment map reflections. */
|
||||
emap?: boolean;
|
||||
/** Position in Torque space [x, y, z]. */
|
||||
position?: [number, number, number];
|
||||
/** Quaternion in Three.js space [x, y, z, w]. */
|
||||
|
|
@ -121,6 +123,8 @@ export interface StreamEntity {
|
|||
actionAnim?: number;
|
||||
actionAtEnd?: boolean;
|
||||
damageState?: number;
|
||||
/** ShapeBase hidden state (binaryCloak). True = invisible. */
|
||||
hidden?: boolean;
|
||||
faceViewer?: boolean;
|
||||
/** DTS animation thread states from ghost ThreadMask data. */
|
||||
threads?: ThreadState[];
|
||||
|
|
@ -136,6 +140,10 @@ export interface StreamEntity {
|
|||
packShape?: string;
|
||||
/** DTS shape name for the carried flag (slot 3, Mount2 bone). */
|
||||
flagShape?: string;
|
||||
/** Player skin (team skin like "base", "baseb"). */
|
||||
skinName?: string;
|
||||
/** Player preferred skin (chosen skin like "RandySavage"). */
|
||||
skinPrefName?: string;
|
||||
/** True when the player has no ground contact and is falling. */
|
||||
falling?: boolean;
|
||||
/** True when the player is using jetpack thrust. */
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ export const DEFAULT_TEAM_NAMES: Record<number, string> = {
|
|||
6: "Phoenix",
|
||||
};
|
||||
|
||||
/**
|
||||
* Default flag skin names per team from serverDefaults.cs ($Host::teamSkin).
|
||||
* Used by CTFGame::getTeamSkin() as fallback when custom skins are disabled.
|
||||
*/
|
||||
export const DEFAULT_FLAG_SKINS: Record<number, string> = {
|
||||
1: "base",
|
||||
2: "baseb",
|
||||
3: "swolf",
|
||||
4: "dsword",
|
||||
5: "beagle",
|
||||
6: "cotp",
|
||||
};
|
||||
|
||||
/**
|
||||
* Replicates `GameBase::getGameName()` from gameBase.cs.
|
||||
* Combines targetNameTag and targetTypeTag from the datablock into a
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const _textureCache = new Map<string, Texture>();
|
|||
export function loadTexture(
|
||||
url: string,
|
||||
onLoad?: (texture: Texture) => void,
|
||||
onError?: (url: string) => void,
|
||||
): Texture {
|
||||
const cached = _textureCache.get(url);
|
||||
if (cached) {
|
||||
|
|
@ -42,11 +43,20 @@ export function loadTexture(
|
|||
// This matches our codebase where all textures use flipY = false.
|
||||
texture.flipY = false;
|
||||
_textureCache.set(url, texture);
|
||||
_bitmapLoader.load(url, (bitmap) => {
|
||||
texture.image = bitmap;
|
||||
texture.needsUpdate = true;
|
||||
onLoad?.(texture);
|
||||
});
|
||||
_bitmapLoader.load(
|
||||
url,
|
||||
(bitmap) => {
|
||||
texture.image = bitmap;
|
||||
texture.needsUpdate = true;
|
||||
onLoad?.(texture);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
// Remove failed URL from cache so fallback can be tried.
|
||||
_textureCache.delete(url);
|
||||
onError?.(url);
|
||||
},
|
||||
);
|
||||
return texture;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue