mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-03-06 22:10:42 +00:00
various fixes and performance improvements
This commit is contained in:
parent
cb28b66dad
commit
0c9ddb476a
62 changed files with 3109 additions and 1286 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -137,3 +137,5 @@ dist
|
|||
# list of files. Once someone builds this, it's not really necessary for other
|
||||
# developers to have this folder.
|
||||
GameData
|
||||
|
||||
docs/dev
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,11 +1,11 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
3:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js","/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js"],"default"]
|
||||
3:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js"],"default"]
|
||||
6:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
7:"$Sreact.suspense"
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","async":true}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","async":true}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","async":true}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","async":true}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","async":true}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","async":true}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","async":true}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true}],["$","script","script-7",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"loading":null,"isPartial":false}
|
||||
4:{}
|
||||
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
|
||||
8:null
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
5:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
6:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js","/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js"],"default"]
|
||||
6:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js"],"default"]
|
||||
9:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
10:I[68027,[],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"IconMark"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
38
docs/_next/static/chunks/29cbf5720c3c6313.js
Normal file
38
docs/_next/static/chunks/29cbf5720c3c6313.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
|
|
@ -6,6 +6,7 @@
|
|||
.CopyCoordinatesButton-module__BxovtG__Root{}.CopyCoordinatesButton-module__BxovtG__Root[data-copied=true]{background:#0075d5e6;border-color:#fff6}.CopyCoordinatesButton-module__BxovtG__ClipboardCheck{opacity:1;display:none}.CopyCoordinatesButton-module__BxovtG__Root[data-copied=true] .CopyCoordinatesButton-module__BxovtG__ClipboardCheck{animation:.22s linear infinite CopyCoordinatesButton-module__BxovtG__showClipboardCheck;display:block}.CopyCoordinatesButton-module__BxovtG__Root[data-copied=true] .CopyCoordinatesButton-module__BxovtG__MapPin{display:none}.CopyCoordinatesButton-module__BxovtG__ButtonLabel{}@keyframes CopyCoordinatesButton-module__BxovtG__showClipboardCheck{0%{opacity:1}to{opacity:.2}}
|
||||
.LoadDemoButton-module__kGZaoW__Root{}.LoadDemoButton-module__kGZaoW__ButtonLabel{}.LoadDemoButton-module__kGZaoW__DemoIcon{font-size:19px}
|
||||
.PlayerNameplate-module__zYDm0a__Root{pointer-events:none;white-space:nowrap;flex-direction:column;align-items:center;display:inline-flex}.PlayerNameplate-module__zYDm0a__Top{padding-bottom:20px;}.PlayerNameplate-module__zYDm0a__Bottom{padding-top:20px;}.PlayerNameplate-module__zYDm0a__IffArrow{width:12px;height:12px;image-rendering:pixelated;filter:drop-shadow(0 1px 2px #000000b3)}.PlayerNameplate-module__zYDm0a__Name{color:#fff;text-shadow:0 1px 3px #000000e6,0 0 1px #000000b3;font-size:11px}.PlayerNameplate-module__zYDm0a__HealthBar{background:#00000080;border:1px solid #fff3;width:60px;height:4px;margin:2px auto 0;overflow:hidden}.PlayerNameplate-module__zYDm0a__HealthFill{background:#2ecc40;height:100%}
|
||||
.FlagMarker-module__INpLba__Root{pointer-events:none;white-space:nowrap;flex-direction:column;align-items:center;gap:1px;display:inline-flex}.FlagMarker-module__INpLba__Distance{color:#fff;text-shadow:0 1px 3px #000000e6,0 0 1px #000000b3;opacity:.5;font-size:10px}.FlagMarker-module__INpLba__Icon{width:16px;height:16px;image-rendering:pixelated;opacity:.5;filter:drop-shadow(0 1px 3px #000c);-webkit-mask-image:var(--flag-icon-url);mask-image:var(--flag-icon-url);-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--flag-icon-url);-webkit-mask-position:50%;-webkit-mask-size:contain;-webkit-mask-repeat:no-repeat}
|
||||
.DemoControls-module__PjV4fq__Root{color:#fff;z-index:2;background:#000000b3;align-items:center;gap:10px;padding:8px 12px;font-size:13px;display:flex;position:fixed;bottom:0;left:0;right:0}.DemoControls-module__PjV4fq__PlayPause{color:#fff;cursor:pointer;background:#03529399;border:1px solid #ffffff4d;border-radius:4px;flex-shrink:0;justify-content:center;align-items:center;width:32px;height:32px;padding:0;font-size:14px;display:flex}@media (hover:hover){.DemoControls-module__PjV4fq__PlayPause:hover{background:#0062b3cc}}.DemoControls-module__PjV4fq__Time{font-variant-numeric:tabular-nums;white-space:nowrap;flex-shrink:0}.DemoControls-module__PjV4fq__Seek[type=range]{flex:1 1 0;min-width:0;max-width:none}.DemoControls-module__PjV4fq__Speed{color:#fff;background:#0009;border:1px solid #ffffff4d;border-radius:3px;flex-shrink:0;padding:2px 4px;font-size:12px}
|
||||
.PlayerHUD-module__-E1Scq__PlayerHUD{z-index:1;pointer-events:none;position:absolute;inset:0}.PlayerHUD-module__-E1Scq__TopRight{align-items:flex-start;gap:6px;display:flex;position:absolute;top:56px;right:8px}.PlayerHUD-module__-E1Scq__Compass{flex-shrink:0;width:64px;height:64px;position:relative}.PlayerHUD-module__-E1Scq__CompassRing{image-rendering:auto;width:100%;height:100%;position:absolute;top:0;left:0}.PlayerHUD-module__-E1Scq__CompassNSEW{width:100%;height:100%;image-rendering:pixelated;position:absolute;top:0;left:0}.PlayerHUD-module__-E1Scq__Bars{flex-direction:column;gap:3px;padding-top:10px;display:flex}.PlayerHUD-module__-E1Scq__BarTrack{background:#00000080;border:1px solid #ffffff26;width:120px;height:10px;overflow:hidden}.PlayerHUD-module__-E1Scq__BarFillHealth{background:#2ecc40;height:100%;transition:width .15s ease-out}.PlayerHUD-module__-E1Scq__BarFillEnergy{background:#0af;height:100%;transition:width .15s ease-out}.PlayerHUD-module__-E1Scq__WeaponHUD{flex-direction:column;gap:2px;display:flex;position:absolute;top:50%;right:8px;transform:translateY(-50%)}.PlayerHUD-module__-E1Scq__WeaponSeparator{height:6px}.PlayerHUD-module__-E1Scq__ChatWindow{background:#00323ca6;max-width:420px;padding:4px 8px;font-size:12px;line-height:1.3;position:absolute;top:56px;left:0}.PlayerHUD-module__-E1Scq__ChatMessage{color:#2cacb5;padding:1px 0;transition:opacity .3s ease-out}.PlayerHUD-module__-E1Scq__ChatColor0{color:#2cacb5}.PlayerHUD-module__-E1Scq__ChatColor1{color:#04eb69}.PlayerHUD-module__-E1Scq__ChatColor2{color:#dbc880}.PlayerHUD-module__-E1Scq__ChatColor3{color:#4dfd5f}.PlayerHUD-module__-E1Scq__ChatColor4{color:#28e7f0}.PlayerHUD-module__-E1Scq__ChatColor5{color:#c8c832}.PlayerHUD-module__-E1Scq__ChatColor6{color:#c8c8c8}.PlayerHUD-module__-E1Scq__ChatColor7{color:#dcdc14}.PlayerHUD-module__-E1Scq__ChatColor8{color:#9696fa}.PlayerHUD-module__-E1Scq__ChatColor9{color:#3cdc96}.PlayerHUD-module__-E1Scq__TeamScores{font-family:monospace;font-size:12px;position:absolute;bottom:130px;left:0}.PlayerHUD-module__-E1Scq__TeamRow{background:#00323ca6;gap:6px;padding:2px 8px;display:flex}.PlayerHUD-module__-E1Scq__TeamRow+.PlayerHUD-module__-E1Scq__TeamRow{border-top:1px solid #80ffc826}.PlayerHUD-module__-E1Scq__TeamNameFriendly{color:#2ecc40;min-width:60px}.PlayerHUD-module__-E1Scq__TeamNameEnemy{color:#e44;min-width:60px}.PlayerHUD-module__-E1Scq__TeamScore{color:#fff;text-align:right;min-width:24px;font-weight:700}.PlayerHUD-module__-E1Scq__TeamCount{color:#9ba;text-align:right;min-width:24px}.PlayerHUD-module__-E1Scq__PackInventoryHUD{align-items:center;gap:4px;display:flex;position:absolute;bottom:100px;right:8px}.PlayerHUD-module__-E1Scq__PackInvItem{background:#00323ca6;border:1px solid #80ffc826;flex-direction:column;justify-content:center;align-items:center;gap:1px;padding:4px;display:flex}.PlayerHUD-module__-E1Scq__PackInvItemActive{border-color:#80ffc880;box-shadow:0 0 6px #80ffc84d}.PlayerHUD-module__-E1Scq__PackInvItemDim{opacity:.5}.PlayerHUD-module__-E1Scq__PackInvIcon{image-rendering:pixelated;display:block}.PlayerHUD-module__-E1Scq__PackInvCount{color:#bfe;text-align:center;min-width:12px;font-family:monospace;font-size:11px}.PlayerHUD-module__-E1Scq__PackInvInfinity{image-rendering:pixelated;opacity:.8;display:block}.PlayerHUD-module__-E1Scq__Reticle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.PlayerHUD-module__-E1Scq__ReticleImage{opacity:.85;width:64px;height:64px;image-rendering:pixelated}.PlayerHUD-module__-E1Scq__ReticleDot{background:#2ecc40b3;border-radius:50%;width:4px;height:4px;box-shadow:0 0 4px #2ecc4080}
|
||||
.page-module__E0kJGG__CanvasContainer{z-index:0;position:absolute;inset:0}.page-module__E0kJGG__LoadingIndicator{pointer-events:none;z-index:1;opacity:.8;flex-direction:column;align-items:center;gap:16px;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.page-module__E0kJGG__LoadingIndicator[data-complete=true]{animation:.3s ease-out forwards page-module__E0kJGG__loadingComplete}.page-module__E0kJGG__Spinner{border:4px solid #fff3;border-top-color:#fff;border-radius:50%;width:48px;height:48px;animation:1s linear infinite page-module__E0kJGG__spin}.page-module__E0kJGG__Progress{background:#fff3;border-radius:2px;width:200px;height:4px;overflow:hidden}.page-module__E0kJGG__ProgressBar{background:#fff;border-radius:2px;height:100%;transition:width .1s ease-out}.page-module__E0kJGG__ProgressText{color:#ffffffb3;font-variant-numeric:tabular-nums;font-size:14px}@keyframes page-module__E0kJGG__spin{to{transform:rotate(360deg)}}@keyframes page-module__E0kJGG__loadingComplete{0%{opacity:1}to{opacity:0}}
|
||||
174
docs/_next/static/chunks/90c5f23d057a7dda.js
Normal file
174
docs/_next/static/chunks/90c5f23d057a7dda.js
Normal file
File diff suppressed because one or more lines are too long
211
docs/_next/static/chunks/91476c9d2f29d071.js
Normal file
211
docs/_next/static/chunks/91476c9d2f29d071.js
Normal file
File diff suppressed because one or more lines are too long
8
docs/_next/static/chunks/c0475cead0a67c33.js
Normal file
8
docs/_next/static/chunks/c0475cead0a67c33.js
Normal file
File diff suppressed because one or more lines are too long
609
docs/_next/static/chunks/d96f10e4606ed566.js
Normal file
609
docs/_next/static/chunks/d96f10e4606ed566.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
8
docs/_next/static/chunks/fe8d2153bfb8c263.js
Normal file
8
docs/_next/static/chunks/fe8d2153bfb8c263.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@
|
|||
a:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],null]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],null]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
d:I[27201,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"IconMark"]
|
||||
7:null
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"IconMark"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
3:"$Sreact.suspense"
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"loading":null,"isPartial":false}
|
||||
4:null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
3:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"/_not-found","paramType":null,"paramKey":"/_not-found","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"/_not-found","paramType":null,"paramKey":"/_not-found","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@
|
|||
a:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
c:I[68027,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],null]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["","_not-found",""],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L5",null,{"children":["$","$6",null,{"name":"Next.MetadataOutlet","children":"$@7"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$6",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],null]}],false]],"m":"$undefined","G":["$c","$undefined"],"S":true}
|
||||
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
d:I[27201,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"IconMark"]
|
||||
7:null
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,16 +3,16 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
5:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
6:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js","/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js"],"default"]
|
||||
6:I[31713,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/94212136ebe55507.js"],"default"]
|
||||
9:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
10:I[68027,[],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/f4c5b6b3116ac3bb.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/19dee6e9ded353fa.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/1f24b5e35a3d7706.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3a7943ba4f8effca.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/29cbf5720c3c6313.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/90c5f23d057a7dda.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/c0475cead0a67c33.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/07f1e4bb8e7d8066.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/d96f10e4606ed566.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/44bbdd420cb3ec27.js","async":true,"nonce":"$undefined"}],["$","script","script-6",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-7",{"src":"/t2-mapper/_next/static/chunks/94212136ebe55507.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
5:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
6:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/325897a61efa2ade.js"],"default"]
|
||||
6:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js"],"default"]
|
||||
9:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
10:I[68027,[],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["","shapes",""],"q":"","i":false,"f":[[["",{"children":["shapes",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/325897a61efa2ade.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["","shapes",""],"q":"","i":false,"f":[[["",{"children":["shapes",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"IconMark"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"MapGenius – Explore maps for Tribes 2"}],["$","meta","1",{"name":"description","content":"Tribes 2 forever."}],["$","link","2",{"rel":"icon","href":"/t2-mapper/icon.png?icon.2911bba1.png","sizes":"108x128","type":"image/png"}],["$","$L5","3",{}]]}]}]}],null]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","template":["$","$L4",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"shapes","paramType":null,"paramKey":"shapes","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","tree":{"name":"","paramType":null,"paramKey":"","hasRuntimePrefetch":false,"slots":{"children":{"name":"shapes","paramType":null,"paramKey":"shapes","hasRuntimePrefetch":false,"slots":{"children":{"name":"__PAGE__","paramType":null,"paramKey":"__PAGE__","hasRuntimePrefetch":false,"slots":null,"isRootLayout":false}},"isRootLayout":false}},"isRootLayout":true},"staleTime":300}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
3:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/325897a61efa2ade.js"],"default"]
|
||||
3:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js"],"default"]
|
||||
6:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
7:"$Sreact.suspense"
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","async":true}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/325897a61efa2ade.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"loading":null,"isPartial":false}
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","async":true}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"loading":null,"isPartial":false}
|
||||
4:{}
|
||||
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
|
||||
8:null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
3:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
0:{"buildId":"Dy_yQyXNreDeI2LPQRYzt","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false}
|
||||
0:{"buildId":"6xhnTWazjCi9htjLDl3f1","rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"loading":null,"isPartial":false}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,15 +3,15 @@
|
|||
3:I[39756,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
4:I[37457,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"default"]
|
||||
5:I[47257,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ClientPageRoot"]
|
||||
6:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/325897a61efa2ade.js"],"default"]
|
||||
6:I[39724,["/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","/t2-mapper/_next/static/chunks/153d5796298dee1e.js","/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js"],"default"]
|
||||
9:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"OutletBoundary"]
|
||||
a:"$Sreact.suspense"
|
||||
c:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"ViewportBoundary"]
|
||||
e:I[97367,["/t2-mapper/_next/static/chunks/2f236954d6a65e12.js"],"MetadataBoundary"]
|
||||
10:I[68027,[],"default"]
|
||||
:HL["/t2-mapper/_next/static/chunks/e620039d1c837dab.css","style"]
|
||||
:HL["/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","style"]
|
||||
0:{"P":null,"b":"Dy_yQyXNreDeI2LPQRYzt","c":["","shapes",""],"q":"","i":false,"f":[[["",{"children":["shapes",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/b238e7ff5406aa8f.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/f299abcdb625bb54.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/325897a61efa2ade.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
:HL["/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","style"]
|
||||
0:{"P":null,"b":"6xhnTWazjCi9htjLDl3f1","c":["","shapes",""],"q":"","i":false,"f":[[["",{"children":["shapes",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/e620039d1c837dab.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/89fcb9c19e93d0ef.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"defaultOptions":{"clearOnDefault":false},"children":["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L3",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L4",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$L5",null,{"Component":"$6","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@7","$@8"]}}],[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/chunks/3e57999e46d7efb4.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/t2-mapper/_next/static/chunks/b00acbf8afd8b4b6.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/t2-mapper/_next/static/chunks/153d5796298dee1e.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/t2-mapper/_next/static/chunks/91476c9d2f29d071.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/t2-mapper/_next/static/chunks/53a9c1169e187e33.js","async":true,"nonce":"$undefined"}],["$","script","script-4",{"src":"/t2-mapper/_next/static/chunks/20b7c805b0b1f5f3.js","async":true,"nonce":"$undefined"}],["$","script","script-5",{"src":"/t2-mapper/_next/static/chunks/fe8d2153bfb8c263.js","async":true,"nonce":"$undefined"}]],["$","$L9",null,{"children":["$","$a",null,{"name":"Next.MetadataOutlet","children":"$@b"}]}]]}],{},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$Lc",null,{"children":"$Ld"}],["$","div",null,{"hidden":true,"children":["$","$Le",null,{"children":["$","$a",null,{"name":"Next.Metadata","children":"$Lf"}]}]}],null]}],false]],"m":"$undefined","G":["$10",[]],"S":true}
|
||||
7:{}
|
||||
8:"$0:f:0:1:1:children:1:children:0:props:children:0:props:serverProvidedParams:params"
|
||||
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"}]]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { AudioListener, AudioLoader } from "three";
|
||||
import { engineStore } from "../state";
|
||||
|
||||
interface AudioContextType {
|
||||
audioLoader: AudioLoader | null;
|
||||
|
|
@ -44,6 +45,26 @@ export function AudioProvider({ children }: { children: ReactNode }) {
|
|||
audioLoader,
|
||||
audioListener: listener,
|
||||
});
|
||||
|
||||
// Suspend/resume the Web AudioContext when demo playback pauses/resumes.
|
||||
// This freezes all playing sounds at their current position rather than
|
||||
// stopping them, so they resume seamlessly.
|
||||
const unsubscribe = engineStore.subscribe(
|
||||
(state) => state.playback.status,
|
||||
(status) => {
|
||||
const ctx = listener?.context;
|
||||
if (!ctx) return;
|
||||
if (status === "paused") {
|
||||
ctx.suspend();
|
||||
} else if (status === "playing" && ctx.state === "suspended") {
|
||||
ctx.resume();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [camera]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -14,10 +14,44 @@ import { audioToUrl } from "../loaders";
|
|||
import { useAudio } from "./AudioContext";
|
||||
import { useDebug, useSettings } from "./SettingsProvider";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import { engineStore } from "../state";
|
||||
|
||||
// Global audio buffer cache shared across all audio components.
|
||||
export const audioBufferCache = new Map<string, AudioBuffer>();
|
||||
|
||||
// ── Demo sound rate tracking ──
|
||||
// Track active demo sounds so their playbackRate can be updated when the
|
||||
// playback rate changes (e.g. slow-motion or fast-forward).
|
||||
// Maps each sound to its intrinsic pitch (1.0 for normal sounds, or the
|
||||
// voice pitch multiplier for chat sounds).
|
||||
const _activeDemoSounds = new Map<Audio<GainNode | PannerNode>, number>();
|
||||
|
||||
/** Register a sound for automatic playback rate tracking. */
|
||||
export function trackDemoSound(
|
||||
sound: Audio<GainNode | PannerNode>,
|
||||
basePitch = 1,
|
||||
): void {
|
||||
_activeDemoSounds.set(sound, basePitch);
|
||||
}
|
||||
|
||||
/** Unregister a tracked demo sound. */
|
||||
export function untrackDemoSound(sound: Audio<GainNode | PannerNode>): void {
|
||||
_activeDemoSounds.delete(sound);
|
||||
}
|
||||
|
||||
engineStore.subscribe(
|
||||
(state) => state.playback.rate,
|
||||
(rate) => {
|
||||
for (const [sound, basePitch] of _activeDemoSounds) {
|
||||
try {
|
||||
sound.setPlaybackRate(basePitch * rate);
|
||||
} catch {
|
||||
// Sound may have been disposed.
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export interface ResolvedAudioProfile {
|
||||
filename: string;
|
||||
is3D: boolean;
|
||||
|
|
@ -73,6 +107,7 @@ export function playOneShotSound(
|
|||
// File not in manifest — skip silently.
|
||||
return;
|
||||
}
|
||||
const rate = engineStore.getState().playback.rate;
|
||||
getCachedAudioBuffer(url, audioLoader, (buffer) => {
|
||||
try {
|
||||
if (resolved.is3D && parent) {
|
||||
|
|
@ -85,12 +120,15 @@ export function playOneShotSound(
|
|||
sound.setMaxDistance(resolved.maxDist);
|
||||
sound.setRolloffFactor(1);
|
||||
sound.setVolume(resolved.volume);
|
||||
sound.setPlaybackRate(rate);
|
||||
if (position) {
|
||||
sound.position.copy(position);
|
||||
}
|
||||
parent.add(sound);
|
||||
_activeDemoSounds.set(sound, 1);
|
||||
sound.play();
|
||||
sound.source!.onended = () => {
|
||||
_activeDemoSounds.delete(sound);
|
||||
sound.disconnect();
|
||||
parent.remove(sound);
|
||||
};
|
||||
|
|
@ -98,8 +136,11 @@ export function playOneShotSound(
|
|||
const sound = new Audio(audioListener);
|
||||
sound.setBuffer(buffer);
|
||||
sound.setVolume(resolved.volume);
|
||||
sound.setPlaybackRate(rate);
|
||||
_activeDemoSounds.set(sound, 1);
|
||||
sound.play();
|
||||
sound.source!.onended = () => {
|
||||
_activeDemoSounds.delete(sound);
|
||||
sound.disconnect();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { useEffect, useRef } from "react";
|
|||
import { Audio } from "three";
|
||||
import { audioToUrl } from "../loaders";
|
||||
import { useAudio } from "./AudioContext";
|
||||
import { getCachedAudioBuffer } from "./AudioEmitter";
|
||||
import { getCachedAudioBuffer, trackDemoSound, untrackDemoSound } from "./AudioEmitter";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { useEngineSelector } from "../state";
|
||||
import { engineStore, useEngineSelector } from "../state";
|
||||
import type { DemoChatMessage } from "../demo/types";
|
||||
|
||||
/**
|
||||
|
|
@ -21,7 +21,12 @@ export function ChatSoundPlayer() {
|
|||
const timeSec = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.timeSec,
|
||||
);
|
||||
const playedCountRef = useRef(0);
|
||||
const playedSetRef = useRef(new WeakSet<DemoChatMessage>());
|
||||
// Track active voice chat sound per sender so a new voice bind from the
|
||||
// same player stops their previous one (matching Tribes 2 behavior).
|
||||
const activeBySenderRef = useRef(
|
||||
new Map<string, Audio<GainNode>>(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -33,32 +38,51 @@ export function ChatSoundPlayer() {
|
|||
) {
|
||||
return;
|
||||
}
|
||||
const startIdx = playedCountRef.current;
|
||||
for (let i = startIdx; i < messages.length; i++) {
|
||||
const msg: DemoChatMessage = messages[i];
|
||||
const played = playedSetRef.current;
|
||||
const activeBySender = activeBySenderRef.current;
|
||||
for (const msg of messages) {
|
||||
if (played.has(msg)) continue;
|
||||
played.add(msg);
|
||||
if (!msg.soundPath) continue;
|
||||
// Skip sounds that are too old (e.g. after seeking).
|
||||
if (Math.abs(timeSec - msg.timeSec) > 2) continue;
|
||||
try {
|
||||
const url = audioToUrl(msg.soundPath);
|
||||
const pitch = msg.soundPitch ?? 1;
|
||||
const rate = engineStore.getState().playback.rate;
|
||||
const sender = msg.sender;
|
||||
getCachedAudioBuffer(url, audioLoader, (buffer) => {
|
||||
// Stop the sender's previous voice chat sound.
|
||||
if (sender) {
|
||||
const prev = activeBySender.get(sender);
|
||||
if (prev) {
|
||||
try { prev.stop(); } catch {}
|
||||
untrackDemoSound(prev);
|
||||
prev.disconnect();
|
||||
activeBySender.delete(sender);
|
||||
}
|
||||
}
|
||||
const sound = new Audio(audioListener);
|
||||
sound.setBuffer(buffer);
|
||||
if (pitch !== 1) {
|
||||
sound.setPlaybackRate(pitch);
|
||||
sound.setPlaybackRate(pitch * rate);
|
||||
trackDemoSound(sound, pitch);
|
||||
if (sender) {
|
||||
activeBySender.set(sender, sound);
|
||||
}
|
||||
sound.play();
|
||||
// Clean up the source node once playback finishes.
|
||||
sound.source!.onended = () => {
|
||||
untrackDemoSound(sound);
|
||||
sound.disconnect();
|
||||
if (sender && activeBySender.get(sender) === sound) {
|
||||
activeBySender.delete(sender);
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
// File not in manifest — skip silently.
|
||||
}
|
||||
}
|
||||
playedCountRef.current = messages.length;
|
||||
}, [audioEnabled, audioLoader, audioListener, messages, timeSec]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { Component, Suspense } from "react";
|
||||
import { Component, memo, Suspense } from "react";
|
||||
import type { ErrorInfo, MutableRefObject, ReactNode } from "react";
|
||||
import { entityTypeColor } from "../demo/demoPlaybackUtils";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import { useDebug } from "./SettingsProvider";
|
||||
import { DemoPlayerModel } from "./DemoPlayerModel";
|
||||
import { DemoShapeModel, DemoWeaponModel } from "./DemoShapeModel";
|
||||
import { DemoShapeModel, DemoWeaponModel, DemoExplosionShape } from "./DemoShapeModel";
|
||||
import { DemoSpriteProjectile, DemoTracerProjectile } from "./DemoProjectiles";
|
||||
import { PlayerNameplate } from "./PlayerNameplate";
|
||||
import { FlagMarker } from "./FlagMarker";
|
||||
import { useEngineSelector } from "../state";
|
||||
import type { DemoEntity } from "../demo/types";
|
||||
import type { DemoEntity, DemoStreamingPlayback } from "../demo/types";
|
||||
|
||||
/**
|
||||
* Renders a non-camera demo entity.
|
||||
|
|
@ -16,12 +17,14 @@ import type { DemoEntity } from "../demo/types";
|
|||
* Player entities use DemoPlayerModel for skeletal animation; others use
|
||||
* DemoShapeModel.
|
||||
*/
|
||||
export function DemoEntityGroup({
|
||||
export const DemoEntityGroup = memo(function DemoEntityGroup({
|
||||
entity,
|
||||
timeRef,
|
||||
playback,
|
||||
}: {
|
||||
entity: DemoEntity;
|
||||
timeRef: MutableRefObject<number>;
|
||||
playback?: DemoStreamingPlayback;
|
||||
}) {
|
||||
const debug = useDebug();
|
||||
const debugMode = debug?.debugMode ?? false;
|
||||
|
|
@ -57,6 +60,7 @@ export function DemoEntityGroup({
|
|||
}
|
||||
|
||||
if (!entity.dataBlock) {
|
||||
const isFlag = ((entity.targetRenderFlags ?? 0) & 0x2) !== 0;
|
||||
return (
|
||||
<group name={name}>
|
||||
<group name="model">
|
||||
|
|
@ -66,6 +70,11 @@ export function DemoEntityGroup({
|
|||
</mesh>
|
||||
{debugMode ? <DemoMissingShapeLabel entity={entity} /> : null}
|
||||
</group>
|
||||
{isFlag && (
|
||||
<Suspense fallback={null}>
|
||||
<FlagMarker entity={entity} timeRef={timeRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
|
@ -80,6 +89,7 @@ export function DemoEntityGroup({
|
|||
// Player entities use skeleton-preserving DemoPlayerModel for animation.
|
||||
if (entity.type === "Player") {
|
||||
const isControlPlayer = entity.id === controlPlayerGhostId;
|
||||
const hasFlag = ((entity.targetRenderFlags ?? 0) & 0x2) !== 0;
|
||||
return (
|
||||
<group name={name}>
|
||||
<group name="model">
|
||||
|
|
@ -93,11 +103,34 @@ export function DemoEntityGroup({
|
|||
<PlayerNameplate entity={entity} timeRef={timeRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
{hasFlag && (
|
||||
<Suspense fallback={null}>
|
||||
<FlagMarker entity={entity} timeRef={timeRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
// Explosion entities with DTS shapes use a specialized renderer
|
||||
// that handles faceViewer, size keyframes, and fade-out.
|
||||
if (entity.type === "Explosion" && entity.dataBlock && playback) {
|
||||
return (
|
||||
<group name={name}>
|
||||
<group name="model">
|
||||
<ShapeErrorBoundary fallback={null}>
|
||||
<Suspense fallback={null}>
|
||||
<DemoExplosionShape entity={entity as any} playback={playback} />
|
||||
</Suspense>
|
||||
</ShapeErrorBoundary>
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
const isFlag = ((entity.targetRenderFlags ?? 0) & 0x2) !== 0;
|
||||
|
||||
return (
|
||||
<group name={name}>
|
||||
<group name="model">
|
||||
|
|
@ -119,9 +152,14 @@ export function DemoEntityGroup({
|
|||
</ShapeErrorBoundary>
|
||||
</group>
|
||||
)}
|
||||
{isFlag && (
|
||||
<Suspense fallback={null}>
|
||||
<FlagMarker entity={entity} timeRef={timeRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export function DemoMissingShapeLabel({ entity }: { entity: DemoEntity }) {
|
||||
const id = String(entity.id);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|||
import {
|
||||
AdditiveBlending,
|
||||
BoxGeometry,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
CanvasTexture,
|
||||
DataTexture,
|
||||
DoubleSide,
|
||||
Float32BufferAttribute,
|
||||
|
|
@ -15,6 +17,8 @@ import {
|
|||
RGBAFormat,
|
||||
ShaderMaterial,
|
||||
SphereGeometry,
|
||||
Sprite,
|
||||
SpriteMaterial,
|
||||
Texture,
|
||||
Uint16BufferAttribute,
|
||||
UnsignedByteType,
|
||||
|
|
@ -42,7 +46,10 @@ import {
|
|||
resolveAudioProfile,
|
||||
playOneShotSound,
|
||||
getCachedAudioBuffer,
|
||||
trackDemoSound,
|
||||
untrackDemoSound,
|
||||
} from "./AudioEmitter";
|
||||
import { demoEffectNow, engineStore } from "../state";
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
|
|
@ -92,10 +99,357 @@ const _debugOriginMat = new MeshBasicMaterial({ color: 0xff0000, wireframe: true
|
|||
const _debugParticleGeo = new BoxGeometry(0.3, 0.3, 0.3);
|
||||
const _debugParticleMat = new MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
|
||||
|
||||
// ── sRGB → linear conversion for shader attributes ──
|
||||
// ── Explosion wireframe sphere geometry (reusable) ──
|
||||
|
||||
function srgbToLinear(c: number): number {
|
||||
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
const _explosionSphereGeo = new SphereGeometry(1, 12, 8);
|
||||
|
||||
interface ActiveExplosionSphere {
|
||||
entityId: string;
|
||||
mesh: Mesh;
|
||||
material: MeshBasicMaterial;
|
||||
label: Sprite;
|
||||
labelMaterial: SpriteMaterial;
|
||||
creationTime: number;
|
||||
lifetimeMS: number;
|
||||
targetRadius: number;
|
||||
}
|
||||
|
||||
/** Create a text label sprite for an explosion sphere. */
|
||||
function createExplosionLabel(text: string, color: number): { sprite: Sprite; material: SpriteMaterial } {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const fontSize = 32;
|
||||
ctx.font = `bold ${fontSize}px monospace`;
|
||||
const metrics = ctx.measureText(text);
|
||||
const padding = 8;
|
||||
canvas.width = Math.ceil(metrics.width) + padding * 2;
|
||||
canvas.height = fontSize + padding * 2;
|
||||
|
||||
// Redraw with correct canvas size.
|
||||
ctx.font = `bold ${fontSize}px monospace`;
|
||||
ctx.fillStyle = `#${color.toString(16).padStart(6, "0")}`;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(text, padding, canvas.height / 2);
|
||||
|
||||
const texture = new CanvasTexture(canvas);
|
||||
const material = new SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sprite = new Sprite(material);
|
||||
// Scale to be readable in world space (roughly 1 unit tall).
|
||||
const aspect = canvas.width / canvas.height;
|
||||
sprite.scale.set(aspect * 2, 2, 1);
|
||||
return { sprite, material };
|
||||
}
|
||||
|
||||
// ── Shockwave ring rendering ──
|
||||
|
||||
interface ShockwaveData {
|
||||
width: number;
|
||||
numSegments: number;
|
||||
velocity: number;
|
||||
height: number;
|
||||
verticalCurve: number;
|
||||
acceleration: number;
|
||||
texWrap: number;
|
||||
lifetimeMS: number;
|
||||
is2D: boolean;
|
||||
renderSquare: boolean;
|
||||
renderBottom: boolean;
|
||||
mapToTerrain: boolean;
|
||||
colors: { r: number; g: number; b: number; a: number }[];
|
||||
times: number[];
|
||||
textureName: string;
|
||||
mapToTexture: string;
|
||||
}
|
||||
|
||||
interface ActiveShockwave {
|
||||
entityId: string;
|
||||
mesh: Mesh;
|
||||
bottomMesh: Mesh | null;
|
||||
geometry: BufferGeometry;
|
||||
bottomGeometry: BufferGeometry | null;
|
||||
material: ShaderMaterial;
|
||||
creationTime: number;
|
||||
lifetimeMS: number;
|
||||
data: ShockwaveData;
|
||||
radius: number;
|
||||
velocity: number;
|
||||
}
|
||||
|
||||
/** Resolve a ShockwaveData datablock from an explosion's shockwave ref. */
|
||||
function resolveShockwaveData(
|
||||
shockwaveId: number,
|
||||
getDataBlockData: (id: number) => Record<string, unknown> | undefined,
|
||||
): ShockwaveData | null {
|
||||
const raw = getDataBlockData(shockwaveId);
|
||||
if (!raw) return null;
|
||||
|
||||
const colors = (raw.colors as ShockwaveData["colors"]) ?? [];
|
||||
const times = (raw.times as number[]) ?? [0, 0.5, 1, 1];
|
||||
|
||||
return {
|
||||
width: (raw.width as number) ?? 1,
|
||||
numSegments: Math.max((raw.numSegments as number) ?? 16, 4),
|
||||
velocity: (raw.velocity as number) ?? 0,
|
||||
height: (raw.height as number) ?? 0,
|
||||
verticalCurve: (raw.verticalCurve as number) ?? 0,
|
||||
acceleration: (raw.acceleration as number) ?? 0,
|
||||
texWrap: (raw.texWrap as number) ?? 1,
|
||||
lifetimeMS: (raw.lifetimeMS as number) ?? 500,
|
||||
is2D: !!raw.is2D,
|
||||
renderSquare: !!raw.renderSquare,
|
||||
renderBottom: !!raw.renderBottom,
|
||||
mapToTerrain: !!raw.mapToTerrain,
|
||||
colors,
|
||||
times,
|
||||
textureName: (raw.textureName as string) ?? "",
|
||||
mapToTexture: (raw.mapToTexture as string) ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/** Interpolate RGBA color from shockwave keyframes at normalized time t. */
|
||||
function interpolateShockwaveColor(
|
||||
data: ShockwaveData,
|
||||
t: number,
|
||||
): [number, number, number, number] {
|
||||
const { colors, times } = data;
|
||||
if (colors.length === 0) return [1, 1, 1, 1];
|
||||
|
||||
// Find the active keyframe segment.
|
||||
let idx = 0;
|
||||
for (let i = 0; i < times.length - 1; i++) {
|
||||
if (t >= times[i]) idx = i;
|
||||
}
|
||||
const nextIdx = Math.min(idx + 1, colors.length - 1);
|
||||
|
||||
const t0 = times[idx] ?? 0;
|
||||
const t1 = times[nextIdx] ?? 1;
|
||||
const span = t1 - t0;
|
||||
const frac = span > 0 ? Math.min((t - t0) / span, 1) : 0;
|
||||
|
||||
const c0 = colors[idx] ?? colors[0];
|
||||
const c1 = colors[nextIdx] ?? colors[0];
|
||||
|
||||
return [
|
||||
c0.r + (c1.r - c0.r) * frac,
|
||||
c0.g + (c1.g - c0.g) * frac,
|
||||
c0.b + (c1.b - c0.b) * frac,
|
||||
c0.a + (c1.a - c0.a) * frac,
|
||||
];
|
||||
}
|
||||
|
||||
// Shockwave ring shader — additive blending, vertex colors with alpha.
|
||||
const shockwaveVertexShader = /* glsl */ `
|
||||
attribute vec4 vertexColor;
|
||||
attribute vec2 texCoord;
|
||||
varying vec4 vColor;
|
||||
varying vec2 vUV;
|
||||
void main() {
|
||||
vColor = vertexColor;
|
||||
vUV = texCoord;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const shockwaveFragmentShader = /* glsl */ `
|
||||
uniform sampler2D uTexture;
|
||||
varying vec4 vColor;
|
||||
varying vec2 vUV;
|
||||
void main() {
|
||||
vec4 tex = texture2D(uTexture, vUV);
|
||||
gl_FragColor = vec4(vColor.rgb * tex.rgb, vColor.a * tex.a);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Create ring geometry buffers for a shockwave with the given segment count.
|
||||
* Each segment is a quad (2 triangles) between inner and outer ring vertices.
|
||||
* Returns the geometry with position, texCoord, vertexColor attributes and
|
||||
* index buffer pre-allocated for numSegments quads.
|
||||
*/
|
||||
function createShockwaveGeometry(numSegments: number): BufferGeometry {
|
||||
// 2 vertices per segment (inner + outer) + 2 to close the loop.
|
||||
const numVerts = (numSegments + 1) * 2;
|
||||
const positions = new Float32Array(numVerts * 3);
|
||||
const texCoords = new Float32Array(numVerts * 2);
|
||||
const vertexColors = new Float32Array(numVerts * 4);
|
||||
|
||||
// 2 triangles per segment = 6 indices.
|
||||
const numIndices = numSegments * 6;
|
||||
const indices = new Uint16Array(numIndices);
|
||||
|
||||
for (let i = 0; i < numSegments; i++) {
|
||||
const base = i * 2;
|
||||
const j = i * 6;
|
||||
// Outer-inner-outer, inner-inner-outer (CCW winding).
|
||||
indices[j] = base;
|
||||
indices[j + 1] = base + 1;
|
||||
indices[j + 2] = base + 2;
|
||||
indices[j + 3] = base + 1;
|
||||
indices[j + 4] = base + 3;
|
||||
indices[j + 5] = base + 2;
|
||||
}
|
||||
|
||||
const geo = new BufferGeometry();
|
||||
const posAttr = new BufferAttribute(positions, 3);
|
||||
posAttr.setUsage(35048); // DynamicDrawUsage
|
||||
geo.setAttribute("position", posAttr);
|
||||
|
||||
const texAttr = new BufferAttribute(texCoords, 2);
|
||||
texAttr.setUsage(35048);
|
||||
geo.setAttribute("texCoord", texAttr);
|
||||
|
||||
const colorAttr = new BufferAttribute(vertexColors, 4);
|
||||
colorAttr.setUsage(35048);
|
||||
geo.setAttribute("vertexColor", colorAttr);
|
||||
|
||||
geo.setIndex(new BufferAttribute(indices, 1));
|
||||
|
||||
return geo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update shockwave ring vertex positions, UVs, and colors for the current
|
||||
* frame. Implements the V12 renderWave algorithm: an expanding annular ring
|
||||
* with optional height on the outer edge.
|
||||
*/
|
||||
function updateShockwaveGeometry(
|
||||
geo: BufferGeometry,
|
||||
sw: ShockwaveData,
|
||||
radius: number,
|
||||
color: [number, number, number, number],
|
||||
is2D: boolean,
|
||||
): void {
|
||||
const posArr = (geo.getAttribute("position") as BufferAttribute)
|
||||
.array as Float32Array;
|
||||
const texArr = (geo.getAttribute("texCoord") as BufferAttribute)
|
||||
.array as Float32Array;
|
||||
const colArr = (geo.getAttribute("vertexColor") as BufferAttribute)
|
||||
.array as Float32Array;
|
||||
|
||||
const innerRad = Math.max(radius - sw.width * 0.5, 0);
|
||||
const outerRad = radius + sw.width * 0.5;
|
||||
const numSegs = sw.numSegments;
|
||||
|
||||
// Pass colors as-is (gamma space) — ShaderMaterial has no automatic
|
||||
// output encoding, matching V12's direct gamma-space rendering.
|
||||
const lr = color[0];
|
||||
const lg = color[1];
|
||||
const lb = color[2];
|
||||
const la = color[3];
|
||||
|
||||
for (let i = 0; i <= numSegs; i++) {
|
||||
const angle = (i / numSegs) * Math.PI * 2;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
|
||||
// In Three.js space: ring lies in XZ plane, Y is up.
|
||||
const outerIdx = i * 2;
|
||||
const innerIdx = outerIdx + 1;
|
||||
|
||||
// Outer vertex — raised by height along Y.
|
||||
const opi = outerIdx * 3;
|
||||
posArr[opi] = cos * outerRad;
|
||||
posArr[opi + 1] = is2D ? 0 : sw.height;
|
||||
posArr[opi + 2] = sin * outerRad;
|
||||
|
||||
// Inner vertex — on ground plane.
|
||||
const ipi = innerIdx * 3;
|
||||
posArr[ipi] = cos * innerRad;
|
||||
posArr[ipi + 1] = 0;
|
||||
posArr[ipi + 2] = sin * innerRad;
|
||||
|
||||
// UV: U wraps around ring, V spans inner→outer.
|
||||
const u = (i / numSegs) * sw.texWrap;
|
||||
const oti = outerIdx * 2;
|
||||
texArr[oti] = u;
|
||||
texArr[oti + 1] = 0.05; // outer edge
|
||||
|
||||
const iti = innerIdx * 2;
|
||||
texArr[iti] = u;
|
||||
texArr[iti + 1] = 0.95; // inner edge
|
||||
|
||||
// Vertex colors (uniform across ring).
|
||||
const oci = outerIdx * 4;
|
||||
colArr[oci] = lr;
|
||||
colArr[oci + 1] = lg;
|
||||
colArr[oci + 2] = lb;
|
||||
colArr[oci + 3] = la;
|
||||
|
||||
const ici = innerIdx * 4;
|
||||
colArr[ici] = lr;
|
||||
colArr[ici + 1] = lg;
|
||||
colArr[ici + 2] = lb;
|
||||
colArr[ici + 3] = la;
|
||||
}
|
||||
|
||||
geo.getAttribute("position").needsUpdate = true;
|
||||
geo.getAttribute("texCoord").needsUpdate = true;
|
||||
geo.getAttribute("vertexColor").needsUpdate = true;
|
||||
geo.computeBoundingSphere();
|
||||
}
|
||||
|
||||
/** Create the ShaderMaterial for a shockwave ring. */
|
||||
function createShockwaveMaterial(texture: Texture): ShaderMaterial {
|
||||
return new ShaderMaterial({
|
||||
vertexShader: shockwaveVertexShader,
|
||||
fragmentShader: shockwaveFragmentShader,
|
||||
uniforms: { uTexture: { value: texture } },
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
blending: AdditiveBlending,
|
||||
side: DoubleSide,
|
||||
});
|
||||
}
|
||||
|
||||
/** Map explosion dataBlock shape name to a debug wireframe color. */
|
||||
function getExplosionColor(dataBlock: string | undefined): number {
|
||||
if (!dataBlock) return 0xff00ff;
|
||||
const name = dataBlock.toLowerCase();
|
||||
if (name.includes("disc")) return 0x4488ff;
|
||||
if (name.includes("grenade")) return 0xff8800;
|
||||
if (name.includes("mortar")) return 0xff4400;
|
||||
if (name.includes("plasma")) return 0x44ff44;
|
||||
if (name.includes("laser")) return 0xff2222;
|
||||
if (name.includes("blaster")) return 0xffff00;
|
||||
if (name.includes("missile")) return 0xff6600;
|
||||
if (name.includes("bomb")) return 0xff0000;
|
||||
if (name.includes("mine")) return 0xff8844;
|
||||
if (name.includes("concussion")) return 0xffaa00;
|
||||
if (name.includes("shocklance")) return 0x8844ff;
|
||||
if (name.includes("chaingun") || name.includes("bullet")) return 0xcccccc;
|
||||
return 0xff00ff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract approximate radius from an ExplosionData datablock's `sizes` array.
|
||||
* Each entry is `{x, y, z}` with values in range 0–16000 (scale multiplier).
|
||||
* Falls back to `particleRadius` or a default of 5.
|
||||
*/
|
||||
function getExplosionRadius(
|
||||
expBlock: Record<string, unknown>,
|
||||
): number {
|
||||
const sizes = expBlock.sizes as Array<{ x: number; y: number; z: number }> | undefined;
|
||||
if (Array.isArray(sizes) && sizes.length > 0) {
|
||||
let maxVal = 0;
|
||||
for (const s of sizes) {
|
||||
maxVal = Math.max(maxVal, s.x, s.y, s.z);
|
||||
}
|
||||
if (maxVal > 0) {
|
||||
// Values are in 0–16000 range, treat as a scale factor.
|
||||
// Typical explosions have values like 2000–8000; map to reasonable world radii.
|
||||
return maxVal / 1000;
|
||||
}
|
||||
}
|
||||
const particleRadius = expBlock.particleRadius as number | undefined;
|
||||
if (typeof particleRadius === "number" && particleRadius > 0) {
|
||||
return particleRadius;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
// ── Geometry builder ──
|
||||
|
|
@ -129,6 +483,7 @@ function createParticleGeometry(maxParticles: number): BufferGeometry {
|
|||
const colors = new Float32Array(vertCount * 4);
|
||||
const sizes = new Float32Array(vertCount);
|
||||
const spins = new Float32Array(vertCount);
|
||||
const orientDirs = new Float32Array(vertCount * 3);
|
||||
|
||||
geo.setIndex(new Uint16BufferAttribute(indices, 1));
|
||||
geo.setAttribute("quadCorner", new Float32BufferAttribute(corners, 2));
|
||||
|
|
@ -136,6 +491,7 @@ function createParticleGeometry(maxParticles: number): BufferGeometry {
|
|||
geo.setAttribute("particleColor", new Float32BufferAttribute(colors, 4));
|
||||
geo.setAttribute("particleSize", new Float32BufferAttribute(sizes, 1));
|
||||
geo.setAttribute("particleSpin", new Float32BufferAttribute(spins, 1));
|
||||
geo.setAttribute("orientDir", new Float32BufferAttribute(orientDirs, 3));
|
||||
|
||||
geo.setDrawRange(0, 0);
|
||||
return geo;
|
||||
|
|
@ -144,6 +500,7 @@ function createParticleGeometry(maxParticles: number): BufferGeometry {
|
|||
function createParticleMaterial(
|
||||
texture: Texture,
|
||||
useInvAlpha: boolean,
|
||||
orientParticles = false,
|
||||
): ShaderMaterial {
|
||||
// Use the placeholder until the real texture's image data is ready.
|
||||
const ready = _texturesReady.has(texture);
|
||||
|
|
@ -153,6 +510,8 @@ function createParticleMaterial(
|
|||
uniforms: {
|
||||
particleTexture: { value: ready ? texture : _placeholderTexture },
|
||||
hasTexture: { value: true },
|
||||
debugOpacity: { value: 1.0 },
|
||||
uOrientParticles: { value: orientParticles },
|
||||
},
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
|
|
@ -178,6 +537,8 @@ interface ActiveEmitter {
|
|||
shaderChecked?: boolean;
|
||||
/** Entity ID this emitter follows (for projectile trails). */
|
||||
followEntityId?: string;
|
||||
/** Emission axis in Torque space (defaults to [0,0,1] = up). */
|
||||
emitAxis?: [number, number, number];
|
||||
/** Debug: origin marker mesh. */
|
||||
debugOriginMesh?: Mesh;
|
||||
/** Debug: particle marker meshes. */
|
||||
|
|
@ -268,13 +629,16 @@ function syncBuffers(active: ActiveEmitter): void {
|
|||
const colorAttr = geo.getAttribute("particleColor") as Float32BufferAttribute;
|
||||
const sizeAttr = geo.getAttribute("particleSize") as Float32BufferAttribute;
|
||||
const spinAttr = geo.getAttribute("particleSpin") as Float32BufferAttribute;
|
||||
const orientAttr = geo.getAttribute("orientDir") as Float32BufferAttribute;
|
||||
|
||||
const posArr = posAttr.array as Float32Array;
|
||||
const colArr = colorAttr.array as Float32Array;
|
||||
const sizeArr = sizeAttr.array as Float32Array;
|
||||
const spinArr = spinAttr.array as Float32Array;
|
||||
const orientArr = orientAttr.array as Float32Array;
|
||||
|
||||
const count = Math.min(particles.length, MAX_PARTICLES_PER_EMITTER);
|
||||
const useVelocity = active.emitter.data.orientOnVelocity;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const p = particles[i];
|
||||
|
|
@ -284,10 +648,19 @@ function syncBuffers(active: ActiveEmitter): void {
|
|||
const ty = p.pos[2];
|
||||
const tz = p.pos[0];
|
||||
|
||||
// Convert sRGB particle colors to linear for the shader.
|
||||
const lr = srgbToLinear(p.r);
|
||||
const lg = srgbToLinear(p.g);
|
||||
const lb = srgbToLinear(p.b);
|
||||
// Orient direction: use velocity or initial orientDir, swizzled.
|
||||
const dir = useVelocity ? p.vel : p.orientDir;
|
||||
const odx = dir[1];
|
||||
const ody = dir[2];
|
||||
const odz = dir[0];
|
||||
|
||||
// Pass particle colors as-is (sRGB / gamma space). ShaderMaterial does
|
||||
// not get automatic linear→sRGB output encoding, so linearizing here
|
||||
// would darken colors without compensation — matching V12's direct
|
||||
// gamma-space rendering.
|
||||
const lr = p.r;
|
||||
const lg = p.g;
|
||||
const lb = p.b;
|
||||
const la = p.a;
|
||||
|
||||
// Write the same values to all 4 vertices of the quad.
|
||||
|
|
@ -304,6 +677,11 @@ function syncBuffers(active: ActiveEmitter): void {
|
|||
colArr[ci + 2] = lb;
|
||||
colArr[ci + 3] = la;
|
||||
|
||||
const oi = vi * 3;
|
||||
orientArr[oi] = odx;
|
||||
orientArr[oi + 1] = ody;
|
||||
orientArr[oi + 2] = odz;
|
||||
|
||||
sizeArr[vi] = p.size;
|
||||
spinArr[vi] = p.currentSpin;
|
||||
}
|
||||
|
|
@ -320,6 +698,7 @@ function syncBuffers(active: ActiveEmitter): void {
|
|||
colorAttr.needsUpdate = true;
|
||||
sizeAttr.needsUpdate = true;
|
||||
spinAttr.needsUpdate = true;
|
||||
orientAttr.needsUpdate = true;
|
||||
|
||||
geo.setDrawRange(0, count * 6);
|
||||
}
|
||||
|
|
@ -349,12 +728,20 @@ export function DemoParticleEffects({
|
|||
const projectileSoundsRef = useRef<Map<string, PositionalAudio>>(new Map());
|
||||
/** Track processed audio event keys to prevent replays on seek. */
|
||||
const processedAudioEventsRef = useRef<Set<string>>(new Set());
|
||||
useFrame((_, delta) => {
|
||||
/** Active wireframe explosion spheres. */
|
||||
const activeExplosionSpheresRef = useRef<ActiveExplosionSphere[]>([]);
|
||||
/** Active shockwave ring effects. */
|
||||
const activeShockwavesRef = useRef<ActiveShockwave[]>([]);
|
||||
useFrame((state, delta) => {
|
||||
const group = groupRef.current;
|
||||
const snapshot = snapshotRef.current;
|
||||
if (!group || !snapshot) return;
|
||||
|
||||
const dtMS = delta * 1000;
|
||||
const playbackState = engineStore.getState().playback;
|
||||
const isPlaying = playbackState.status === "playing";
|
||||
// Scale delta by playback rate; 0 when paused.
|
||||
const effectDelta = isPlaying ? delta * playbackState.rate : 0;
|
||||
const dtMS = effectDelta * 1000;
|
||||
const getDataBlockData = playback.getDataBlockData.bind(playback);
|
||||
|
||||
// Detect new explosion entities and create emitters.
|
||||
|
|
@ -390,6 +777,7 @@ export function DemoParticleEffects({
|
|||
const material = createParticleMaterial(
|
||||
texture,
|
||||
burst.data.particles.useInvAlpha,
|
||||
burst.data.orientParticles,
|
||||
);
|
||||
const mesh = new Mesh(geometry, material);
|
||||
mesh.frustumCulled = false;
|
||||
|
|
@ -420,6 +808,7 @@ export function DemoParticleEffects({
|
|||
const material = createParticleMaterial(
|
||||
texture,
|
||||
emitterData.particles.useInvAlpha,
|
||||
emitterData.orientParticles,
|
||||
);
|
||||
const mesh = new Mesh(geometry, material);
|
||||
mesh.frustumCulled = false;
|
||||
|
|
@ -436,6 +825,91 @@ export function DemoParticleEffects({
|
|||
hasBurst: false,
|
||||
});
|
||||
}
|
||||
|
||||
const expBlock = getDataBlockData(entity.explosionDataBlockId);
|
||||
|
||||
// Debug mode: show wireframe spheres and labels.
|
||||
if (debugMode) {
|
||||
const radius = expBlock ? getExplosionRadius(expBlock) : 5;
|
||||
const color = getExplosionColor(entity.dataBlock);
|
||||
const sphereMat = new MeshBasicMaterial({
|
||||
color,
|
||||
wireframe: true,
|
||||
transparent: true,
|
||||
opacity: 1,
|
||||
depthWrite: false,
|
||||
});
|
||||
const sphereMesh = new Mesh(_explosionSphereGeo, sphereMat);
|
||||
sphereMesh.frustumCulled = false;
|
||||
sphereMesh.scale.setScalar(radius);
|
||||
sphereMesh.position.set(origin[1], origin[2], origin[0]);
|
||||
group.add(sphereMesh);
|
||||
|
||||
const labelText = `${entity.id}: ${entity.dataBlock ?? `expId:${entity.explosionDataBlockId}`}`;
|
||||
const { sprite: labelSprite, material: labelMat } = createExplosionLabel(labelText, color);
|
||||
labelSprite.position.set(origin[1], origin[2] + radius + 2, origin[0]);
|
||||
labelSprite.frustumCulled = false;
|
||||
group.add(labelSprite);
|
||||
|
||||
activeExplosionSpheresRef.current.push({
|
||||
entityId: entity.id as string,
|
||||
mesh: sphereMesh,
|
||||
material: sphereMat,
|
||||
label: labelSprite,
|
||||
labelMaterial: labelMat,
|
||||
creationTime: demoEffectNow(),
|
||||
lifetimeMS: Math.max(resolved.lifetimeMS, 3000),
|
||||
targetRadius: radius,
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn shockwave ring if the explosion datablock references one.
|
||||
const shockwaveId = expBlock?.shockwave as number | null | undefined;
|
||||
if (typeof shockwaveId === "number") {
|
||||
const swData = resolveShockwaveData(shockwaveId, getDataBlockData);
|
||||
if (swData) {
|
||||
const texture = getParticleTexture(swData.textureName);
|
||||
const geo = createShockwaveGeometry(swData.numSegments);
|
||||
const mat = createShockwaveMaterial(texture);
|
||||
const mesh = new Mesh(geo, mat);
|
||||
mesh.frustumCulled = false;
|
||||
mesh.position.set(origin[1], origin[2], origin[0]);
|
||||
group.add(mesh);
|
||||
|
||||
// Optional bottom face (renders the underside of the ring).
|
||||
let bottomMesh: Mesh | null = null;
|
||||
let bottomGeo: BufferGeometry | null = null;
|
||||
if (swData.renderBottom) {
|
||||
bottomGeo = createShockwaveGeometry(swData.numSegments);
|
||||
bottomMesh = new Mesh(bottomGeo, mat);
|
||||
bottomMesh.frustumCulled = false;
|
||||
bottomMesh.position.set(origin[1], origin[2], origin[0]);
|
||||
// Flip Y to render the underside.
|
||||
bottomMesh.scale.y = -1;
|
||||
group.add(bottomMesh);
|
||||
}
|
||||
|
||||
// Clamp denormalized velocity values (parser bug workaround).
|
||||
const initVelocity = Math.abs(swData.velocity) > 1e-10
|
||||
? swData.velocity
|
||||
: 0;
|
||||
|
||||
activeShockwavesRef.current.push({
|
||||
entityId: entity.id as string,
|
||||
mesh,
|
||||
bottomMesh,
|
||||
geometry: geo,
|
||||
bottomGeometry: bottomGeo,
|
||||
material: mat,
|
||||
creationTime: demoEffectNow(),
|
||||
lifetimeMS: swData.lifetimeMS,
|
||||
data: swData,
|
||||
radius: 0,
|
||||
velocity: initVelocity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Detect projectile entities with trail emitters (maintainEmitterId).
|
||||
|
|
@ -465,6 +939,7 @@ export function DemoParticleEffects({
|
|||
const material = createParticleMaterial(
|
||||
texture,
|
||||
emitterData.particles.useInvAlpha,
|
||||
emitterData.orientParticles,
|
||||
);
|
||||
const mesh = new Mesh(geometry, material);
|
||||
mesh.frustumCulled = false;
|
||||
|
|
@ -508,7 +983,7 @@ export function DemoParticleEffects({
|
|||
entry.shaderChecked = true;
|
||||
}
|
||||
|
||||
// Update trail emitter origin to follow the projectile's position.
|
||||
// Update trail emitter origin and direction to follow the projectile.
|
||||
if (entry.followEntityId) {
|
||||
const tracked = snapshot.entities.find(
|
||||
(e) => e.id === entry.followEntityId,
|
||||
|
|
@ -518,11 +993,14 @@ export function DemoParticleEffects({
|
|||
entry.origin[1] = tracked.position[1];
|
||||
entry.origin[2] = tracked.position[2];
|
||||
}
|
||||
if (tracked?.direction) {
|
||||
entry.emitAxis = tracked.direction;
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming emitters emit periodically.
|
||||
if (!entry.isBurst) {
|
||||
entry.emitter.emitPeriodic(entry.origin, dtMS);
|
||||
entry.emitter.emitPeriodic(entry.origin, dtMS, entry.emitAxis);
|
||||
}
|
||||
|
||||
// Advance physics and interpolation.
|
||||
|
|
@ -536,6 +1014,9 @@ export function DemoParticleEffects({
|
|||
entry.material.uniforms.particleTexture.value = entry.targetTexture;
|
||||
}
|
||||
|
||||
// Reduce particle opacity in debug mode for visibility.
|
||||
entry.material.uniforms.debugOpacity.value = debugMode ? 0.2 : 1.0;
|
||||
|
||||
// Sync GPU buffers.
|
||||
syncBuffers(entry);
|
||||
|
||||
|
|
@ -604,8 +1085,87 @@ export function DemoParticleEffects({
|
|||
}
|
||||
}
|
||||
|
||||
// ── Update explosion wireframe spheres ──
|
||||
const spheres = activeExplosionSpheresRef.current;
|
||||
const now = demoEffectNow();
|
||||
for (let i = spheres.length - 1; i >= 0; i--) {
|
||||
const sphere = spheres[i];
|
||||
const elapsed = now - sphere.creationTime;
|
||||
const frac = Math.min(elapsed / sphere.lifetimeMS, 1);
|
||||
|
||||
// Quick scale-up in first 10%, then hold.
|
||||
const scaleFrac = Math.min(frac / 0.1, 1);
|
||||
sphere.mesh.scale.setScalar(sphere.targetRadius * scaleFrac);
|
||||
|
||||
// Fade opacity over lifetime.
|
||||
sphere.material.opacity = 1 - frac;
|
||||
sphere.labelMaterial.opacity = 1 - frac;
|
||||
|
||||
// Remove when lifetime expires.
|
||||
if (frac >= 1) {
|
||||
group.remove(sphere.mesh);
|
||||
group.remove(sphere.label);
|
||||
sphere.material.dispose();
|
||||
sphere.labelMaterial.dispose();
|
||||
spheres.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Update shockwave rings ──
|
||||
const shockwaves = activeShockwavesRef.current;
|
||||
for (let i = shockwaves.length - 1; i >= 0; i--) {
|
||||
const sw = shockwaves[i];
|
||||
const elapsed = now - sw.creationTime;
|
||||
const t = Math.min(elapsed / sw.lifetimeMS, 1);
|
||||
const dtSec = effectDelta;
|
||||
|
||||
// V12 expansion physics: velocity += acceleration * dt; radius += velocity * dt
|
||||
sw.velocity += sw.data.acceleration * dtSec;
|
||||
sw.radius += sw.velocity * dtSec;
|
||||
|
||||
// Interpolate color from keyframes.
|
||||
const color = interpolateShockwaveColor(sw.data, t);
|
||||
|
||||
// Update ring geometry.
|
||||
updateShockwaveGeometry(
|
||||
sw.geometry,
|
||||
sw.data,
|
||||
sw.radius,
|
||||
color,
|
||||
sw.data.is2D,
|
||||
);
|
||||
|
||||
// Update bottom ring if present.
|
||||
if (sw.bottomGeometry) {
|
||||
updateShockwaveGeometry(
|
||||
sw.bottomGeometry,
|
||||
sw.data,
|
||||
sw.radius,
|
||||
color,
|
||||
sw.data.is2D,
|
||||
);
|
||||
}
|
||||
|
||||
// For is2D mode: billboard the ring to face the camera.
|
||||
if (sw.data.is2D) {
|
||||
sw.mesh.lookAt(state.camera.position);
|
||||
}
|
||||
|
||||
// Remove when lifetime expires.
|
||||
if (t >= 1) {
|
||||
group.remove(sw.mesh);
|
||||
if (sw.bottomMesh) group.remove(sw.bottomMesh);
|
||||
sw.geometry.dispose();
|
||||
sw.bottomGeometry?.dispose();
|
||||
sw.material.dispose();
|
||||
shockwaves.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Audio: explosion impact sounds ──
|
||||
if (audioEnabled && audioLoader && audioListener && groupRef.current) {
|
||||
// Only process new audio events while playing to avoid triggering
|
||||
// sounds during pause (existing sounds are frozen via AudioContext.suspend).
|
||||
if (isPlaying && audioEnabled && audioLoader && audioListener && groupRef.current) {
|
||||
for (const entity of snapshot.entities) {
|
||||
if (
|
||||
entity.type !== "Explosion" ||
|
||||
|
|
@ -684,6 +1244,7 @@ export function DemoParticleEffects({
|
|||
sound.setMaxDistance(resolved.maxDist);
|
||||
sound.setRolloffFactor(1);
|
||||
sound.setVolume(resolved.volume);
|
||||
sound.setPlaybackRate(playbackState.rate);
|
||||
sound.setLoop(true);
|
||||
sound.position.set(
|
||||
entity.position![1],
|
||||
|
|
@ -691,6 +1252,7 @@ export function DemoParticleEffects({
|
|||
entity.position![0],
|
||||
);
|
||||
group.add(sound);
|
||||
trackDemoSound(sound);
|
||||
sound.play();
|
||||
projSounds.set(entity.id, sound);
|
||||
});
|
||||
|
|
@ -702,6 +1264,7 @@ export function DemoParticleEffects({
|
|||
// Despawn: stop sounds for entities no longer present.
|
||||
for (const [entityId, sound] of projSounds) {
|
||||
if (!currentEntityIds.has(entityId)) {
|
||||
untrackDemoSound(sound);
|
||||
try { sound.stop(); } catch {}
|
||||
sound.disconnect();
|
||||
groupRef.current?.remove(sound);
|
||||
|
|
@ -768,10 +1331,32 @@ export function DemoParticleEffects({
|
|||
entry.material.dispose();
|
||||
}
|
||||
activeEmittersRef.current = [];
|
||||
// Clean up explosion spheres.
|
||||
for (const sphere of activeExplosionSpheresRef.current) {
|
||||
if (group) {
|
||||
group.remove(sphere.mesh);
|
||||
group.remove(sphere.label);
|
||||
}
|
||||
sphere.material.dispose();
|
||||
sphere.labelMaterial.dispose();
|
||||
}
|
||||
activeExplosionSpheresRef.current = [];
|
||||
// Clean up shockwave rings.
|
||||
for (const sw of activeShockwavesRef.current) {
|
||||
if (group) {
|
||||
group.remove(sw.mesh);
|
||||
if (sw.bottomMesh) group.remove(sw.bottomMesh);
|
||||
}
|
||||
sw.geometry.dispose();
|
||||
sw.bottomGeometry?.dispose();
|
||||
sw.material.dispose();
|
||||
}
|
||||
activeShockwavesRef.current = [];
|
||||
processedExplosionsRef.current.clear();
|
||||
trailEntitiesRef.current.clear();
|
||||
// Clean up projectile sounds.
|
||||
for (const [, sound] of projectileSoundsRef.current) {
|
||||
untrackDemoSound(sound);
|
||||
try { sound.stop(); } catch {}
|
||||
sound.disconnect();
|
||||
if (group) group.remove(sound);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { TickProvider } from "./TickProvider";
|
|||
import { DemoEntityGroup } from "./DemoEntities";
|
||||
import { DemoParticleEffects } from "./DemoParticleEffects";
|
||||
import { PlayerEyeOffset } from "./DemoPlayerModel";
|
||||
import { useEngineStoreApi } from "../state";
|
||||
import { useEngineStoreApi, advanceEffectClock } from "../state";
|
||||
import type {
|
||||
DemoEntity,
|
||||
DemoRecording,
|
||||
|
|
@ -67,14 +67,13 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
|
||||
const prevMap = entityMapRef.current;
|
||||
const nextMap = new Map<string, DemoEntity>();
|
||||
// Derive shouldRebuild from the entity loop itself instead of computing
|
||||
// an O(n) string signature every frame. Entity count change catches
|
||||
// add/remove; identity check catches per-entity changes.
|
||||
let shouldRebuild = snapshot.entities.length !== prevMap.size;
|
||||
let shouldRebuild = false;
|
||||
|
||||
for (const entity of snapshot.entities) {
|
||||
let renderEntity = prevMap.get(entity.id);
|
||||
if (
|
||||
|
||||
// Identity change → new component (unmount/remount)
|
||||
const needsNewIdentity =
|
||||
!renderEntity ||
|
||||
renderEntity.type !== entity.type ||
|
||||
renderEntity.dataBlock !== entity.dataBlock ||
|
||||
|
|
@ -82,8 +81,9 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
renderEntity.className !== entity.className ||
|
||||
renderEntity.ghostIndex !== entity.ghostIndex ||
|
||||
renderEntity.dataBlockId !== entity.dataBlockId ||
|
||||
renderEntity.shapeHint !== entity.shapeHint
|
||||
) {
|
||||
renderEntity.shapeHint !== entity.shapeHint;
|
||||
|
||||
if (needsNewIdentity) {
|
||||
renderEntity = buildStreamDemoEntity(
|
||||
entity.id,
|
||||
entity.type,
|
||||
|
|
@ -96,26 +96,57 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
entity.ghostIndex,
|
||||
entity.dataBlockId,
|
||||
entity.shapeHint,
|
||||
entity.explosionDataBlockId,
|
||||
entity.faceViewer,
|
||||
);
|
||||
renderEntity.playerName = entity.playerName;
|
||||
renderEntity.iffColor = entity.iffColor;
|
||||
renderEntity.targetRenderFlags = entity.targetRenderFlags;
|
||||
renderEntity.threads = entity.threads;
|
||||
renderEntity.weaponImageState = entity.weaponImageState;
|
||||
renderEntity.weaponImageStates = entity.weaponImageStates;
|
||||
renderEntity.headPitch = entity.headPitch;
|
||||
renderEntity.headYaw = entity.headYaw;
|
||||
renderEntity.direction = entity.direction;
|
||||
renderEntity.visual = entity.visual;
|
||||
renderEntity.explosionDataBlockId = entity.explosionDataBlockId;
|
||||
renderEntity.faceViewer = entity.faceViewer;
|
||||
renderEntity.spawnTime = snapshot.timeSec;
|
||||
shouldRebuild = true;
|
||||
} else if (
|
||||
renderEntity.playerName !== entity.playerName ||
|
||||
renderEntity.iffColor !== entity.iffColor ||
|
||||
renderEntity.targetRenderFlags !== entity.targetRenderFlags ||
|
||||
renderEntity.threads !== entity.threads ||
|
||||
renderEntity.weaponImageState !== entity.weaponImageState ||
|
||||
renderEntity.weaponImageStates !== entity.weaponImageStates ||
|
||||
renderEntity.headPitch !== entity.headPitch ||
|
||||
renderEntity.headYaw !== entity.headYaw ||
|
||||
renderEntity.direction !== entity.direction ||
|
||||
renderEntity.visual !== entity.visual
|
||||
) {
|
||||
// Render-affecting field changed → new object so React.memo sees
|
||||
// a different reference and re-renders this entity's component.
|
||||
renderEntity = {
|
||||
...renderEntity,
|
||||
playerName: entity.playerName,
|
||||
iffColor: entity.iffColor,
|
||||
targetRenderFlags: entity.targetRenderFlags,
|
||||
threads: entity.threads,
|
||||
weaponImageState: entity.weaponImageState,
|
||||
weaponImageStates: entity.weaponImageStates,
|
||||
headPitch: entity.headPitch,
|
||||
headYaw: entity.headYaw,
|
||||
direction: entity.direction,
|
||||
visual: entity.visual,
|
||||
};
|
||||
shouldRebuild = true;
|
||||
}
|
||||
// else: no render-affecting changes, keep same object reference
|
||||
// so React.memo can skip re-rendering this entity.
|
||||
|
||||
renderEntity.playerName = entity.playerName;
|
||||
renderEntity.iffColor = entity.iffColor;
|
||||
renderEntity.dataBlock = entity.dataBlock;
|
||||
renderEntity.visual = entity.visual;
|
||||
renderEntity.direction = entity.direction;
|
||||
renderEntity.weaponShape = entity.weaponShape;
|
||||
renderEntity.className = entity.className;
|
||||
renderEntity.ghostIndex = entity.ghostIndex;
|
||||
renderEntity.dataBlockId = entity.dataBlockId;
|
||||
renderEntity.shapeHint = entity.shapeHint;
|
||||
renderEntity.threads = entity.threads;
|
||||
renderEntity.weaponImageState = entity.weaponImageState;
|
||||
renderEntity.weaponImageStates = entity.weaponImageStates;
|
||||
renderEntity.headPitch = entity.headPitch;
|
||||
renderEntity.headYaw = entity.headYaw;
|
||||
|
||||
// Keyframe update (mutable — only used as fallback position for
|
||||
// retained explosion entities; useFrame reads from snapshot entities).
|
||||
if (renderEntity.keyframes.length === 0) {
|
||||
renderEntity.keyframes.push({
|
||||
time: snapshot.timeSec,
|
||||
|
|
@ -123,7 +154,6 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
rotation: entity.rotation ?? [0, 0, 0, 1],
|
||||
});
|
||||
}
|
||||
|
||||
const kf = renderEntity.keyframes[0];
|
||||
kf.time = snapshot.timeSec;
|
||||
if (entity.position) kf.position = entity.position;
|
||||
|
|
@ -138,6 +168,28 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
nextMap.set(entity.id, renderEntity);
|
||||
}
|
||||
|
||||
// Retain explosion entities with DTS shapes after they leave the snapshot.
|
||||
// These entities are ephemeral (~1 tick) but the visual effect lasts seconds.
|
||||
for (const [id, entity] of prevMap) {
|
||||
if (nextMap.has(id)) continue;
|
||||
if (
|
||||
entity.type === "Explosion" &&
|
||||
entity.dataBlock &&
|
||||
entity.spawnTime != null
|
||||
) {
|
||||
const age = snapshot.timeSec - entity.spawnTime;
|
||||
if (age < 5) {
|
||||
nextMap.set(id, entity);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Entity removed (or retention expired).
|
||||
shouldRebuild = true;
|
||||
}
|
||||
|
||||
// Detect new entities added.
|
||||
if (nextMap.size !== prevMap.size) shouldRebuild = true;
|
||||
|
||||
entityMapRef.current = nextMap;
|
||||
if (shouldRebuild) {
|
||||
setEntities(Array.from(nextMap.values()));
|
||||
|
|
@ -209,7 +261,10 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
playbackClockRef.current = requestedTimeSec;
|
||||
}
|
||||
|
||||
// Advance the shared effect clock so all effect timers (particles,
|
||||
// explosions, shockwaves, shape animations) respect pause and rate.
|
||||
if (isPlaying) {
|
||||
advanceEffectClock(delta, playback.rate);
|
||||
playbackClockRef.current += delta * playback.rate;
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +324,12 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
renderCurrent.camera?.orbitTargetId !==
|
||||
publishedSnapshot.camera?.orbitTargetId ||
|
||||
renderCurrent.chatMessages.length !== publishedSnapshot.chatMessages.length ||
|
||||
renderCurrent.teamScores !== publishedSnapshot.teamScores;
|
||||
renderCurrent.teamScores.length !== publishedSnapshot.teamScores.length ||
|
||||
renderCurrent.teamScores.some(
|
||||
(ts, i) =>
|
||||
ts.score !== publishedSnapshot.teamScores[i]?.score ||
|
||||
ts.playerCount !== publishedSnapshot.teamScores[i]?.playerCount,
|
||||
);
|
||||
|
||||
if (shouldPublish) {
|
||||
publishedSnapshotRef.current = renderCurrent;
|
||||
|
|
@ -335,10 +395,23 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
|
||||
const currentEntities = getEntityMap(renderCurrent);
|
||||
const previousEntities = getEntityMap(renderPrev);
|
||||
const renderEntities = entityMapRef.current;
|
||||
const root = rootRef.current;
|
||||
if (root) {
|
||||
for (const child of root.children) {
|
||||
const entity = currentEntities.get(child.name);
|
||||
let entity = currentEntities.get(child.name);
|
||||
// Retained entities (e.g. explosion shapes kept alive past their
|
||||
// snapshot lifetime) won't be in the snapshot entity map. Fall back
|
||||
// to their last-known keyframe position from the render entity.
|
||||
if (!entity) {
|
||||
const renderEntity = renderEntities.get(child.name);
|
||||
if (renderEntity?.keyframes[0]?.position) {
|
||||
const kf = renderEntity.keyframes[0];
|
||||
child.visible = true;
|
||||
child.position.set(kf.position[1], kf.position[2], kf.position[0]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!entity?.position) {
|
||||
child.visible = false;
|
||||
continue;
|
||||
|
|
@ -448,7 +521,7 @@ export function StreamingDemoPlayback({ recording }: { recording: DemoRecording
|
|||
<TickProvider>
|
||||
<group ref={rootRef}>
|
||||
{entities.map((entity) => (
|
||||
<DemoEntityGroup key={entity.id} entity={entity} timeRef={timeRef} />
|
||||
<DemoEntityGroup key={entity.id} entity={entity} timeRef={timeRef} playback={recording.streamingPlayback} />
|
||||
))}
|
||||
</group>
|
||||
<DemoParticleEffects
|
||||
|
|
|
|||
|
|
@ -33,12 +33,27 @@ import {
|
|||
resolveAudioProfile,
|
||||
playOneShotSound,
|
||||
getCachedAudioBuffer,
|
||||
trackDemoSound,
|
||||
untrackDemoSound,
|
||||
} from "./AudioEmitter";
|
||||
import { audioToUrl } from "../loaders";
|
||||
import { useSettings } from "./SettingsProvider";
|
||||
import { useEngineStoreApi, useEngineSelector } from "../state";
|
||||
import type { DemoEntity } from "../demo/types";
|
||||
|
||||
/**
|
||||
* Map weapon shape to the arm blend animation (armThread).
|
||||
* Only missile launcher and sniper rifle have custom arm poses; all others
|
||||
* use the default `lookde`.
|
||||
*/
|
||||
function getArmThread(weaponShape: string | undefined): string {
|
||||
if (!weaponShape) return "lookde";
|
||||
const lower = weaponShape.toLowerCase();
|
||||
if (lower.includes("missile")) return "lookms";
|
||||
if (lower.includes("sniper")) return "looksn";
|
||||
return "lookde";
|
||||
}
|
||||
|
||||
/** Stop, disconnect, and remove a looping PositionalAudio from its parent. */
|
||||
function stopLoopingSound(
|
||||
soundRef: React.MutableRefObject<PositionalAudio | null>,
|
||||
|
|
@ -47,6 +62,7 @@ function stopLoopingSound(
|
|||
) {
|
||||
const sound = soundRef.current;
|
||||
if (!sound) return;
|
||||
untrackDemoSound(sound);
|
||||
try { sound.stop(); } catch {}
|
||||
sound.disconnect();
|
||||
parent?.remove(sound);
|
||||
|
|
@ -105,10 +121,12 @@ export function DemoPlayerModel({
|
|||
// Build case-insensitive clip lookup with alias support.
|
||||
const animActionsRef = useRef(new Map<string, AnimationAction>());
|
||||
const blendActionsRef = useRef<{
|
||||
look: AnimationAction | null;
|
||||
head: AnimationAction | null;
|
||||
headside: AnimationAction | null;
|
||||
}>({ look: null, head: null, headside: null });
|
||||
}>({ head: null, headside: null });
|
||||
// Arm pose blend actions keyed by animation name (lookde, lookms, looksn).
|
||||
const armActionsRef = useRef(new Map<string, AnimationAction>());
|
||||
const activeArmRef = useRef<string | null>(null);
|
||||
const currentAnimRef = useRef({ name: "root", timeScale: 1 });
|
||||
const isDeadRef = useRef(false);
|
||||
|
||||
|
|
@ -126,20 +144,18 @@ export function DemoPlayerModel({
|
|||
// Set up additive blend animations for aim/head articulation.
|
||||
// These clips must be cloned before makeClipAdditive (which mutates in
|
||||
// place) since multiple player entities share the same GLTF cache.
|
||||
const blendNames: Array<{ key: keyof typeof blendActionsRef.current; names: string[] }> = [
|
||||
{ key: "look", names: ["lookde", "look"] },
|
||||
{ key: "head", names: ["head"] },
|
||||
{ key: "headside", names: ["headside"] },
|
||||
];
|
||||
const blendRefs: typeof blendActionsRef.current = { look: null, head: null, headside: null };
|
||||
for (const { key, names } of blendNames) {
|
||||
|
||||
// Head blend actions.
|
||||
const blendRefs: typeof blendActionsRef.current = { head: null, headside: null };
|
||||
for (const { key, names } of [
|
||||
{ key: "head" as const, names: ["head"] },
|
||||
{ key: "headside" as const, names: ["headside"] },
|
||||
]) {
|
||||
const clip = gltf.animations.find((c) =>
|
||||
names.includes(c.name.toLowerCase()),
|
||||
);
|
||||
if (!clip) continue;
|
||||
const cloned = clip.clone();
|
||||
// Reference frame at clip midpoint = neutral pose. The second arg is a
|
||||
// frame index (not time), so convert via fps.
|
||||
const fps = 30;
|
||||
const neutralFrame = Math.round((clip.duration * fps) / 2);
|
||||
AnimationUtils.makeClipAdditive(cloned, neutralFrame, clip, fps);
|
||||
|
|
@ -152,13 +168,53 @@ export function DemoPlayerModel({
|
|||
}
|
||||
blendActionsRef.current = blendRefs;
|
||||
|
||||
// Arm pose blend actions: create one per available arm animation so we
|
||||
// can switch between them when the equipped weapon changes.
|
||||
// All arm clips use the lookde midpoint as the additive reference, so
|
||||
// switching from lookde to lookms captures the shoulder repositioning.
|
||||
const armActions = new Map<string, AnimationAction>();
|
||||
const lookdeClip = gltf.animations.find(
|
||||
(c) => c.name.toLowerCase() === "lookde",
|
||||
);
|
||||
const fps = 30;
|
||||
const lookdeRefFrame = lookdeClip
|
||||
? Math.round((lookdeClip.duration * fps) / 2)
|
||||
: 0;
|
||||
for (const armName of ["lookde", "lookms", "looksn"]) {
|
||||
const clip = gltf.animations.find(
|
||||
(c) => c.name.toLowerCase() === armName,
|
||||
);
|
||||
if (!clip) continue;
|
||||
const cloned = clip.clone();
|
||||
// Use lookde's midpoint as reference for all arm clips so that
|
||||
// lookms/looksn capture the absolute shoulder offset.
|
||||
const refClip = lookdeClip ?? clip;
|
||||
AnimationUtils.makeClipAdditive(cloned, lookdeRefFrame, refClip, fps);
|
||||
const action = mixer.clipAction(cloned);
|
||||
action.blendMode = AdditiveAnimationBlendMode;
|
||||
action.timeScale = 0;
|
||||
action.weight = 0;
|
||||
action.play();
|
||||
armActions.set(armName, action);
|
||||
}
|
||||
armActionsRef.current = armActions;
|
||||
|
||||
// Start with default arm pose.
|
||||
const defaultArm = armActions.get("lookde");
|
||||
if (defaultArm) {
|
||||
defaultArm.weight = 1;
|
||||
activeArmRef.current = "lookde";
|
||||
}
|
||||
|
||||
// Force initial pose evaluation.
|
||||
mixer.update(0);
|
||||
|
||||
return () => {
|
||||
mixer.stopAllAction();
|
||||
animActionsRef.current = new Map();
|
||||
blendActionsRef.current = { look: null, head: null, headside: null };
|
||||
blendActionsRef.current = { head: null, headside: null };
|
||||
armActionsRef.current = new Map();
|
||||
activeArmRef.current = null;
|
||||
};
|
||||
}, [mixer, gltf.animations, shapeAliases]);
|
||||
|
||||
|
|
@ -252,8 +308,26 @@ export function DemoPlayerModel({
|
|||
}
|
||||
}
|
||||
|
||||
// Switch arm blend animation based on equipped weapon.
|
||||
const desiredArm = getArmThread(entity.weaponShape);
|
||||
if (desiredArm !== activeArmRef.current) {
|
||||
const armActions = armActionsRef.current;
|
||||
const prev = activeArmRef.current
|
||||
? armActions.get(activeArmRef.current)
|
||||
: null;
|
||||
const next = armActions.get(desiredArm);
|
||||
if (next) {
|
||||
if (prev) prev.weight = 0;
|
||||
next.weight = isDead ? 0 : 1;
|
||||
activeArmRef.current = desiredArm;
|
||||
}
|
||||
}
|
||||
|
||||
// Drive additive blend animations for aim/head articulation.
|
||||
const { look, head, headside } = blendActionsRef.current;
|
||||
const { head, headside } = blendActionsRef.current;
|
||||
const armAction = activeArmRef.current
|
||||
? armActionsRef.current.get(activeArmRef.current)
|
||||
: null;
|
||||
const blendWeight = isDead ? 0 : 1;
|
||||
|
||||
const headPitch = entity.headPitch ?? 0;
|
||||
|
|
@ -261,9 +335,9 @@ export function DemoPlayerModel({
|
|||
const pitchPos = (headPitch + 1) / 2;
|
||||
const yawPos = (headYaw + 1) / 2;
|
||||
|
||||
if (look) {
|
||||
look.time = pitchPos * look.getClip().duration;
|
||||
look.weight = blendWeight;
|
||||
if (armAction) {
|
||||
armAction.time = pitchPos * armAction.getClip().duration;
|
||||
armAction.weight = blendWeight;
|
||||
}
|
||||
if (head) {
|
||||
head.time = pitchPos * head.getClip().duration;
|
||||
|
|
@ -545,8 +619,10 @@ function AnimatedWeaponModel({
|
|||
sound.setMaxDistance(resolved.maxDist);
|
||||
sound.setRolloffFactor(1);
|
||||
sound.setVolume(resolved.volume);
|
||||
sound.setPlaybackRate(playback.rate);
|
||||
sound.setLoop(true);
|
||||
weaponClone.add(sound);
|
||||
trackDemoSound(sound);
|
||||
sound.play();
|
||||
loopingSoundRef.current = sound;
|
||||
loopingSoundStateRef.current = currentIdx;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,34 @@
|
|||
import { useMemo } from "react";
|
||||
import { Quaternion, Vector3 } from "three";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import {
|
||||
AnimationMixer,
|
||||
LoopOnce,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import type { Group, Material } from "three";
|
||||
import { demoEffectNow, engineStore } from "../state";
|
||||
import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import {
|
||||
_r90,
|
||||
_r90inv,
|
||||
getPosedNodeTransform,
|
||||
processShapeScene,
|
||||
} from "../demo/demoPlaybackUtils";
|
||||
import {
|
||||
loadIflAtlas,
|
||||
getFrameIndexForTime,
|
||||
updateAtlasFrame,
|
||||
} from "./useIflTexture";
|
||||
import type { IflAtlas } from "./useIflTexture";
|
||||
import {
|
||||
ShapeRenderer,
|
||||
useStaticShape,
|
||||
} from "./GenericShape";
|
||||
import { ShapeInfoProvider } from "./ShapeInfoProvider";
|
||||
import type { TorqueObject } from "../torqueScript";
|
||||
import type { DemoThreadState } from "../demo/types";
|
||||
import type { DemoThreadState, DemoStreamEntity } from "../demo/types";
|
||||
import type { DemoStreamingPlayback } from "../demo/types";
|
||||
|
||||
/** Renders a shape model for a demo entity using the existing shape pipeline. */
|
||||
export function DemoShapeModel({
|
||||
|
|
@ -43,13 +60,27 @@ export function DemoShapeModel({
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map weapon shape to the arm blend animation (armThread).
|
||||
* Only missile launcher and sniper rifle have custom arm poses; all others
|
||||
* use the default `lookde`.
|
||||
*/
|
||||
function getArmThread(weaponShape: string | undefined): string {
|
||||
if (!weaponShape) return "lookde";
|
||||
const lower = weaponShape.toLowerCase();
|
||||
if (lower.includes("missile")) return "lookms";
|
||||
if (lower.includes("sniper")) return "looksn";
|
||||
return "lookde";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. The mount transform is conjugated by ShapeRenderer's 90° Y
|
||||
* rotation: T_mount = R90 * M0 * MP^(-1) * R90^(-1).
|
||||
* 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 DemoWeaponModel({
|
||||
shapeName,
|
||||
|
|
@ -62,11 +93,13 @@ export function DemoWeaponModel({
|
|||
const weaponGltf = useStaticShape(shapeName);
|
||||
|
||||
const mountTransform = useMemo(() => {
|
||||
// Get Mount0 from the player's posed (Root animation) skeleton.
|
||||
// Get Mount0 from the player's posed skeleton with arm animation applied.
|
||||
const armThread = getArmThread(shapeName);
|
||||
const m0 = getPosedNodeTransform(
|
||||
playerGltf.scene,
|
||||
playerGltf.animations,
|
||||
"Mount0",
|
||||
[armThread],
|
||||
);
|
||||
if (!m0) return { position: undefined, quaternion: undefined };
|
||||
|
||||
|
|
@ -126,3 +159,322 @@ export function DemoWeaponModel({
|
|||
</ShapeInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Explosion shape rendering ──
|
||||
//
|
||||
// Explosion DTS shapes are flat billboard planes with IFL-animated textures,
|
||||
// vis-keyframed opacity, and size keyframe interpolation. They use
|
||||
// useStaticShape (shared GLTF cache via drei's useGLTF) but render directly
|
||||
// rather than through ShapeRenderer, because:
|
||||
// - faceViewer billboarding needs to control the shape orientation
|
||||
// - ShapeModel's fixed 90° Y rotation conflicts with billboard orientation
|
||||
// - Explosion shapes need LoopOnce animation, not the deploy/ambient lifecycle
|
||||
|
||||
interface VisNode {
|
||||
mesh: any;
|
||||
keyframes: number[];
|
||||
duration: number;
|
||||
cyclic: boolean;
|
||||
}
|
||||
|
||||
interface IflInfo {
|
||||
mesh: any;
|
||||
iflPath: string;
|
||||
sequenceName?: string;
|
||||
duration?: number;
|
||||
cyclic?: boolean;
|
||||
toolBegin?: number;
|
||||
}
|
||||
|
||||
function extractSizeKeyframes(expBlock: Record<string, unknown>): {
|
||||
times: number[];
|
||||
sizes: [number, number, number][];
|
||||
} {
|
||||
const rawSizes = expBlock.sizes as
|
||||
| Array<{ x: number; y: number; z: number }>
|
||||
| undefined;
|
||||
const rawTimes = expBlock.times as number[] | undefined;
|
||||
|
||||
if (!Array.isArray(rawSizes) || rawSizes.length === 0) {
|
||||
return { times: [0, 1], sizes: [[1, 1, 1], [1, 1, 1]] };
|
||||
}
|
||||
|
||||
// sizes are packed as value*100 integers on the wire; divide by 100.
|
||||
const sizes: [number, number, number][] = rawSizes.map((s) => [
|
||||
s.x / 100,
|
||||
s.y / 100,
|
||||
s.z / 100,
|
||||
]);
|
||||
// times are written via writeFloat(8) and are already [0,1] floats.
|
||||
const times = Array.isArray(rawTimes)
|
||||
? rawTimes
|
||||
: sizes.map((_, i) => i / Math.max(sizes.length - 1, 1));
|
||||
|
||||
return { times, sizes };
|
||||
}
|
||||
|
||||
function interpolateSize(
|
||||
keyframes: { times: number[]; sizes: [number, number, number][] },
|
||||
t: number,
|
||||
): [number, number, number] {
|
||||
const { times, sizes } = keyframes;
|
||||
if (times.length === 0) return [1, 1, 1];
|
||||
if (t <= times[0]) return sizes[0];
|
||||
if (t >= times[times.length - 1]) return sizes[sizes.length - 1];
|
||||
|
||||
for (let i = 0; i < times.length - 1; i++) {
|
||||
if (t >= times[i] && t <= times[i + 1]) {
|
||||
const frac = (t - times[i]) / (times[i + 1] - times[i]);
|
||||
return [
|
||||
sizes[i][0] + (sizes[i + 1][0] - sizes[i][0]) * frac,
|
||||
sizes[i][1] + (sizes[i + 1][1] - sizes[i][1]) * frac,
|
||||
sizes[i][2] + (sizes[i + 1][2] - sizes[i][2]) * frac,
|
||||
];
|
||||
}
|
||||
}
|
||||
return sizes[sizes.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an explosion DTS shape using useStaticShape (shared GLTF cache)
|
||||
* with custom rendering for faceViewer, vis/IFL animation, and size keyframes.
|
||||
*/
|
||||
export function DemoExplosionShape({
|
||||
entity,
|
||||
playback,
|
||||
}: {
|
||||
entity: DemoStreamEntity;
|
||||
playback: DemoStreamingPlayback;
|
||||
}) {
|
||||
const gltf = useStaticShape(entity.dataBlock!);
|
||||
const groupRef = useRef<Group>(null);
|
||||
const startTimeRef = useRef(demoEffectNow());
|
||||
const randAngleRef = useRef(Math.random() * Math.PI * 2);
|
||||
const iflAtlasesRef = useRef<Array<{ atlas: IflAtlas; info: IflInfo }>>([]);
|
||||
|
||||
const expBlock = useMemo(() => {
|
||||
if (!entity.explosionDataBlockId) return undefined;
|
||||
return playback.getDataBlockData(entity.explosionDataBlockId);
|
||||
}, [entity.explosionDataBlockId, playback]);
|
||||
|
||||
const sizeKeyframes = useMemo(
|
||||
() => (expBlock ? extractSizeKeyframes(expBlock) : undefined),
|
||||
[expBlock],
|
||||
);
|
||||
|
||||
const baseScale = useMemo<[number, number, number]>(() => {
|
||||
const explosionScale = expBlock?.explosionScale as
|
||||
| { x: number; y: number; z: number }
|
||||
| undefined;
|
||||
return explosionScale
|
||||
? [explosionScale.x / 100, explosionScale.y / 100, explosionScale.z / 100]
|
||||
: [1, 1, 1];
|
||||
}, [expBlock]);
|
||||
|
||||
// lifetimeMS is packed as value >> 5 (ticks); recover with << 5 (× 32).
|
||||
const lifetimeTicks = (expBlock?.lifetimeMS as number) ?? 31;
|
||||
const lifetimeMS = lifetimeTicks * 32;
|
||||
const faceViewer = entity.faceViewer !== false;
|
||||
|
||||
// Clone scene, process materials, collect vis nodes and IFL info.
|
||||
const { scene, mixer, visNodes, iflInfos, materials } = useMemo(() => {
|
||||
const scene = SkeletonUtils.clone(gltf.scene) as Group;
|
||||
|
||||
// Collect IFL info BEFORE processShapeScene replaces materials.
|
||||
const iflInfos: IflInfo[] = [];
|
||||
scene.traverse((node: any) => {
|
||||
if (!node.isMesh || !node.material) return;
|
||||
const mat = Array.isArray(node.material) ? node.material[0] : node.material;
|
||||
if (!mat?.userData) return;
|
||||
const flags = new Set<string>(mat.userData.flag_names ?? []);
|
||||
const rp: string | undefined = mat.userData.resource_path;
|
||||
if (flags.has("IflMaterial") && rp) {
|
||||
const ud = node.userData;
|
||||
iflInfos.push({
|
||||
mesh: node,
|
||||
iflPath: `textures/${rp}.ifl`,
|
||||
sequenceName: ud?.ifl_sequence
|
||||
? String(ud.ifl_sequence).toLowerCase()
|
||||
: undefined,
|
||||
duration: ud?.ifl_duration ? Number(ud.ifl_duration) : undefined,
|
||||
cyclic: ud?.ifl_sequence ? !!ud.ifl_cyclic : undefined,
|
||||
toolBegin: ud?.ifl_tool_begin != null ? Number(ud.ifl_tool_begin) : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
processShapeScene(scene);
|
||||
|
||||
// Collect vis-animated nodes keyed by sequence name.
|
||||
const visNodes: VisNode[] = [];
|
||||
scene.traverse((node: any) => {
|
||||
if (!node.isMesh) return;
|
||||
const ud = node.userData;
|
||||
if (!ud) return;
|
||||
const kf = ud.vis_keyframes;
|
||||
const dur = ud.vis_duration;
|
||||
const seqName = (ud.vis_sequence ?? "").toLowerCase();
|
||||
if (!seqName || !Array.isArray(kf) || kf.length <= 1 || !dur || dur <= 0)
|
||||
return;
|
||||
// Only include vis nodes tied to the "ambient" sequence.
|
||||
if (seqName === "ambient") {
|
||||
visNodes.push({ mesh: node, keyframes: kf, duration: dur, cyclic: !!ud.vis_cyclic });
|
||||
}
|
||||
});
|
||||
|
||||
// Activate vis nodes: make visible, ensure transparent material.
|
||||
for (const v of visNodes) {
|
||||
v.mesh.visible = true;
|
||||
if (v.mesh.material && !Array.isArray(v.mesh.material)) {
|
||||
v.mesh.material.transparent = true;
|
||||
v.mesh.material.depthWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Also un-hide IFL meshes that don't have vis_sequence (always visible).
|
||||
for (const info of iflInfos) {
|
||||
if (!info.mesh.userData?.vis_sequence) {
|
||||
info.mesh.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set up animation mixer with the ambient clip (LoopOnce).
|
||||
const clips = new Map<string, any>();
|
||||
for (const clip of gltf.animations) {
|
||||
clips.set(clip.name.toLowerCase(), clip);
|
||||
}
|
||||
const ambientClip = clips.get("ambient");
|
||||
let mixer: AnimationMixer | null = null;
|
||||
if (ambientClip) {
|
||||
mixer = new AnimationMixer(scene);
|
||||
const action = mixer.clipAction(ambientClip);
|
||||
action.setLoop(LoopOnce, 1);
|
||||
action.clampWhenFinished = true;
|
||||
// playSpeed is packed as value*20 on the wire; divide by 20.
|
||||
const playSpeed = ((expBlock?.playSpeed as number) ?? 20) / 20;
|
||||
action.timeScale = playSpeed;
|
||||
action.play();
|
||||
}
|
||||
|
||||
// Collect all materials for fade-out.
|
||||
const materials: Material[] = [];
|
||||
scene.traverse((child: any) => {
|
||||
if (!child.isMesh) return;
|
||||
if (Array.isArray(child.material)) {
|
||||
materials.push(...child.material);
|
||||
} else if (child.material) {
|
||||
materials.push(child.material);
|
||||
}
|
||||
});
|
||||
|
||||
// Disable frustum culling (explosion may scale beyond bounds).
|
||||
scene.traverse((child) => { child.frustumCulled = false; });
|
||||
|
||||
return { scene, mixer, visNodes, iflInfos, materials };
|
||||
}, [gltf, expBlock]);
|
||||
|
||||
// Load IFL texture atlases.
|
||||
useEffect(() => {
|
||||
iflAtlasesRef.current = [];
|
||||
for (const info of iflInfos) {
|
||||
loadIflAtlas(info.iflPath)
|
||||
.then((atlas) => {
|
||||
const mat = Array.isArray(info.mesh.material)
|
||||
? info.mesh.material[0]
|
||||
: info.mesh.material;
|
||||
if (mat) {
|
||||
mat.map = atlas.texture;
|
||||
mat.needsUpdate = true;
|
||||
}
|
||||
iflAtlasesRef.current.push({ atlas, info });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [iflInfos]);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
|
||||
const playbackState = engineStore.getState().playback;
|
||||
const effectDelta = playbackState.status === "playing"
|
||||
? delta * playbackState.rate : 0;
|
||||
|
||||
const elapsed = demoEffectNow() - startTimeRef.current;
|
||||
const t = Math.min(elapsed / lifetimeMS, 1);
|
||||
const elapsedSec = elapsed / 1000;
|
||||
|
||||
// Advance skeleton animation.
|
||||
if (mixer) {
|
||||
mixer.update(effectDelta);
|
||||
}
|
||||
|
||||
// Fade multiplier for the last 20% of lifetime.
|
||||
const fadeAlpha = t > 0.8 ? 1 - (t - 0.8) / 0.2 : 1;
|
||||
|
||||
// Drive vis opacity animation.
|
||||
for (const { mesh, keyframes, duration, cyclic } of visNodes) {
|
||||
const mat = mesh.material;
|
||||
if (!mat || Array.isArray(mat)) continue;
|
||||
const rawT = elapsedSec / duration;
|
||||
const vt = cyclic ? rawT % 1 : Math.min(rawT, 1);
|
||||
const n = keyframes.length;
|
||||
const pos = vt * n;
|
||||
const lo = Math.floor(pos) % n;
|
||||
const hi = (lo + 1) % n;
|
||||
const frac = pos - Math.floor(pos);
|
||||
const visOpacity = keyframes[lo] + (keyframes[hi] - keyframes[lo]) * frac;
|
||||
mat.opacity = visOpacity * fadeAlpha;
|
||||
}
|
||||
|
||||
// Also fade non-vis materials.
|
||||
if (fadeAlpha < 1) {
|
||||
for (const mat of materials) {
|
||||
if ("opacity" in mat) {
|
||||
mat.transparent = true;
|
||||
(mat as any).opacity *= fadeAlpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance IFL texture atlases.
|
||||
for (const { atlas, info } of iflAtlasesRef.current) {
|
||||
let iflTime: number;
|
||||
if (info.sequenceName && info.duration) {
|
||||
const pos = info.cyclic
|
||||
? (elapsedSec / info.duration) % 1
|
||||
: Math.min(elapsedSec / info.duration, 1);
|
||||
iflTime = pos * info.duration + (info.toolBegin ?? 0);
|
||||
} else {
|
||||
iflTime = elapsedSec;
|
||||
}
|
||||
updateAtlasFrame(atlas, getFrameIndexForTime(atlas, iflTime));
|
||||
}
|
||||
|
||||
// Size keyframe interpolation.
|
||||
if (sizeKeyframes) {
|
||||
const size = interpolateSize(sizeKeyframes, t);
|
||||
group.scale.set(
|
||||
size[0] * baseScale[0],
|
||||
size[1] * baseScale[1],
|
||||
size[2] * baseScale[2],
|
||||
);
|
||||
}
|
||||
|
||||
// faceViewer: billboard toward camera with random Z rotation.
|
||||
if (faceViewer) {
|
||||
group.lookAt(state.camera.position);
|
||||
group.rotateZ(randAngleRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
{/* Flip 180° around Y so the face (GLB +Z normal) points toward the
|
||||
camera after the parent group's lookAt (which aims -Z at camera). */}
|
||||
<group rotation={[0, Math.PI, 0]}>
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
32
src/components/FlagMarker.module.css
Normal file
32
src/components/FlagMarker.module.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
.Root {
|
||||
pointer-events: none;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
gap: 1px;
|
||||
}
|
||||
.Distance {
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
text-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.9),
|
||||
0 0 1px rgba(0, 0, 0, 0.7);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
image-rendering: pixelated;
|
||||
opacity: 0.5;
|
||||
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.8));
|
||||
mask-image: var(--flag-icon-url);
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-image: var(--flag-icon-url);
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
}
|
||||
67
src/components/FlagMarker.tsx
Normal file
67
src/components/FlagMarker.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useRef } from "react";
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { Group, Vector3 } from "three";
|
||||
import { textureToUrl } from "../loaders";
|
||||
import type { DemoEntity } from "../demo/types";
|
||||
import styles from "./FlagMarker.module.css";
|
||||
|
||||
const FLAG_ICON_HEIGHT = 1.5;
|
||||
|
||||
const FLAG_ICON_URL = textureToUrl("commander/MiniIcons/com_flag_grey");
|
||||
|
||||
const _tmpVec = new Vector3();
|
||||
|
||||
/**
|
||||
* Floating flag icon above a flag entity, tinted by IFF color (green for
|
||||
* friendly, red for enemy — matching Tribes 2's sensor group color system).
|
||||
* Always visible regardless of distance.
|
||||
*/
|
||||
export function FlagMarker({
|
||||
entity,
|
||||
}: {
|
||||
entity: DemoEntity;
|
||||
timeRef: MutableRefObject<number>;
|
||||
}) {
|
||||
const markerRef = useRef<Group>(null);
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const distRef = useRef<HTMLSpanElement>(null);
|
||||
const { camera } = useThree();
|
||||
|
||||
useFrame(() => {
|
||||
// Tint imperatively — iffColor is mutated in-place by streaming playback.
|
||||
if (iconRef.current && entity.iffColor) {
|
||||
const { r, g, b } = entity.iffColor;
|
||||
iconRef.current.style.backgroundColor = `rgb(${r},${g},${b})`;
|
||||
}
|
||||
// Update distance label.
|
||||
if (distRef.current && markerRef.current) {
|
||||
markerRef.current.getWorldPosition(_tmpVec);
|
||||
const distance = camera.position.distanceTo(_tmpVec);
|
||||
distRef.current.textContent = distance.toFixed(1);
|
||||
}
|
||||
});
|
||||
|
||||
const initialColor = entity.iffColor
|
||||
? `rgb(${entity.iffColor.r},${entity.iffColor.g},${entity.iffColor.b})`
|
||||
: "rgb(200,200,200)";
|
||||
|
||||
return (
|
||||
<group ref={markerRef}>
|
||||
<Html position={[0, FLAG_ICON_HEIGHT, 0]} center>
|
||||
<div className={styles.Root}>
|
||||
<span ref={distRef} className={styles.Distance} />
|
||||
<div
|
||||
ref={iconRef}
|
||||
className={styles.Icon}
|
||||
style={{
|
||||
backgroundColor: initialColor,
|
||||
"--flag-icon-url": `url(${FLAG_ICON_URL})`,
|
||||
} as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
</Html>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ import * as SkeletonUtils from "three/examples/jsm/utils/SkeletonUtils.js";
|
|||
import { setupTexture } from "../textureUtils";
|
||||
import { useDebug, useSettings } from "./SettingsProvider";
|
||||
import { useShapeInfo, isOrganicShape } from "./ShapeInfoProvider";
|
||||
import { useEngineSelector } from "../state";
|
||||
import { useEngineSelector, demoEffectNow, engineStore } from "../state";
|
||||
import { FloatingLabel } from "./FloatingLabel";
|
||||
import {
|
||||
useIflTexture,
|
||||
|
|
@ -39,6 +39,14 @@ import {
|
|||
} from "../demo/demoPlaybackUtils";
|
||||
import type { DemoThreadState } from "../demo/types";
|
||||
|
||||
/** Returns pausable time in seconds for demo mode, real time otherwise. */
|
||||
function shapeNowSec(): number {
|
||||
const status = engineStore.getState().playback.status;
|
||||
return status !== "stopped"
|
||||
? demoEffectNow() / 1000
|
||||
: performance.now() / 1000;
|
||||
}
|
||||
|
||||
/** Shared props for texture rendering components */
|
||||
interface TextureProps {
|
||||
material: MeshStandardMaterial;
|
||||
|
|
@ -687,7 +695,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
const vNodes = visNodesBySequence.get(seqLower);
|
||||
const thread: ThreadState = {
|
||||
sequence: seqLower,
|
||||
startTime: performance.now() / 1000,
|
||||
startTime: shapeNowSec(),
|
||||
};
|
||||
|
||||
if (clip && mixer) {
|
||||
|
|
@ -773,7 +781,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
for (const seqName of autoPlaySequences) {
|
||||
const vNodes = visNodesBySequence.get(seqName);
|
||||
if (vNodes) {
|
||||
const startTime = performance.now() / 1000;
|
||||
const startTime = shapeNowSec();
|
||||
for (const v of vNodes) prepareVisNode(v);
|
||||
const slot = seqName === "power" ? 0 : 1;
|
||||
threads.set(slot, { sequence: seqName, visNodes: vNodes, startTime });
|
||||
|
|
@ -791,7 +799,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
threads.set(slot, {
|
||||
sequence: seqName,
|
||||
action,
|
||||
startTime: performance.now() / 1000,
|
||||
startTime: shapeNowSec(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -893,6 +901,13 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
useFrame((_, delta) => {
|
||||
const threads = threadsRef.current;
|
||||
|
||||
// In demo mode, scale animation by playback rate; freeze when paused.
|
||||
const inDemo = demoThreadsRef.current != null;
|
||||
const playbackState = engineStore.getState().playback;
|
||||
const effectDelta = !inDemo ? delta
|
||||
: playbackState.status === "playing" ? delta * playbackState.rate
|
||||
: 0;
|
||||
|
||||
// React to demo thread state changes. The ghost ThreadMask data tells us
|
||||
// exactly which DTS sequences are playing/stopped on each of 4 thread slots.
|
||||
const currentDemoThreads = demoThreadsRef.current;
|
||||
|
|
@ -976,7 +991,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
}
|
||||
|
||||
if (animationEnabled) {
|
||||
mixer.update(delta);
|
||||
mixer.update(effectDelta);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -993,7 +1008,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
continue;
|
||||
}
|
||||
|
||||
const elapsed = performance.now() / 1000 - thread.startTime;
|
||||
const elapsed = shapeNowSec() - thread.startTime;
|
||||
const t = cyclic
|
||||
? (elapsed % duration) / duration
|
||||
: Math.min(elapsed / duration, 1);
|
||||
|
|
@ -1016,7 +1031,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
// with the desired frames (e.g. skipping a long "off" period).
|
||||
const iflAnimInfos = iflAnimInfosRef.current;
|
||||
if (iflAnimInfos.length > 0) {
|
||||
iflTimeRef.current += delta;
|
||||
iflTimeRef.current += effectDelta;
|
||||
for (const info of iflAnimInfos) {
|
||||
if (!animationEnabled) {
|
||||
updateAtlasFrame(info.atlas, 0);
|
||||
|
|
@ -1029,7 +1044,7 @@ export const ShapeModel = memo(function ShapeModel({
|
|||
let iflTime = 0;
|
||||
for (const [, thread] of threads) {
|
||||
if (thread.sequence === info.sequenceName) {
|
||||
const elapsed = performance.now() / 1000 - thread.startTime;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import {
|
|||
type TouchMode,
|
||||
} from "./SettingsProvider";
|
||||
import { MissionSelect } from "./MissionSelect";
|
||||
import { RefObject, useEffect, useState, useRef } from "react";
|
||||
import { Camera } from "three";
|
||||
import { useEffect, useState, useRef, RefObject } from "react";
|
||||
import { CopyCoordinatesButton } from "./CopyCoordinatesButton";
|
||||
import { LoadDemoButton } from "./LoadDemoButton";
|
||||
import { useDemoRecording } from "./DemoProvider";
|
||||
import { FiInfo, FiSettings } from "react-icons/fi";
|
||||
import { Camera } from "three";
|
||||
import styles from "./InspectorControls.module.css";
|
||||
|
||||
export function InspectorControls({
|
||||
|
|
@ -18,8 +18,8 @@ export function InspectorControls({
|
|||
missionType,
|
||||
onChangeMission,
|
||||
onOpenMapInfo,
|
||||
cameraRef,
|
||||
isTouch,
|
||||
cameraRef,
|
||||
}: {
|
||||
missionName: string;
|
||||
missionType: string;
|
||||
|
|
@ -31,8 +31,8 @@ export function InspectorControls({
|
|||
missionType: string;
|
||||
}) => void;
|
||||
onOpenMapInfo: () => void;
|
||||
cameraRef: RefObject<Camera | null>;
|
||||
isTouch: boolean | null;
|
||||
cameraRef: RefObject<Camera>;
|
||||
}) {
|
||||
const {
|
||||
fogEnabled,
|
||||
|
|
@ -117,9 +117,9 @@ export function InspectorControls({
|
|||
>
|
||||
<div className={styles.Group}>
|
||||
<CopyCoordinatesButton
|
||||
cameraRef={cameraRef}
|
||||
missionName={missionName}
|
||||
missionType={missionType}
|
||||
cameraRef={cameraRef}
|
||||
/>
|
||||
<LoadDemoButton />
|
||||
<button
|
||||
|
|
@ -181,35 +181,39 @@ export function InspectorControls({
|
|||
</div>
|
||||
</div>
|
||||
<div className={styles.Group}>
|
||||
<div className={styles.Field}>
|
||||
<label htmlFor="fovInput">FOV</label>
|
||||
<input
|
||||
id="fovInput"
|
||||
type="range"
|
||||
min={75}
|
||||
max={120}
|
||||
step={5}
|
||||
value={fov}
|
||||
disabled={isDemoLoaded}
|
||||
onChange={(event) => setFov(parseInt(event.target.value))}
|
||||
/>
|
||||
<output htmlFor="fovInput">{fov}</output>
|
||||
</div>
|
||||
<div className={styles.Field}>
|
||||
<label htmlFor="speedInput">Speed</label>
|
||||
<input
|
||||
id="speedInput"
|
||||
type="range"
|
||||
min={0.1}
|
||||
max={5}
|
||||
step={0.05}
|
||||
value={speedMultiplier}
|
||||
disabled={isDemoLoaded}
|
||||
onChange={(event) =>
|
||||
setSpeedMultiplier(parseFloat(event.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{isDemoLoaded ? null : (
|
||||
<div className={styles.Field}>
|
||||
<label htmlFor="fovInput">FOV</label>
|
||||
<input
|
||||
id="fovInput"
|
||||
type="range"
|
||||
min={75}
|
||||
max={120}
|
||||
step={5}
|
||||
value={fov}
|
||||
disabled={isDemoLoaded}
|
||||
onChange={(event) => setFov(parseInt(event.target.value))}
|
||||
/>
|
||||
<output htmlFor="fovInput">{fov}</output>
|
||||
</div>
|
||||
)}
|
||||
{isDemoLoaded ? null : (
|
||||
<div className={styles.Field}>
|
||||
<label htmlFor="speedInput">Speed</label>
|
||||
<input
|
||||
id="speedInput"
|
||||
type="range"
|
||||
min={0.1}
|
||||
max={5}
|
||||
step={0.05}
|
||||
value={speedMultiplier}
|
||||
disabled={isDemoLoaded}
|
||||
onChange={(event) =>
|
||||
setSpeedMultiplier(parseFloat(event.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isTouch && (
|
||||
<div className={styles.Group}>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,9 @@ import type {
|
|||
WeaponsHudSlot,
|
||||
} from "../demo/types";
|
||||
import styles from "./PlayerHUD.module.css";
|
||||
|
||||
// ── Compass ──
|
||||
|
||||
const COMPASS_URL = textureToUrl("gui/hud_new_compass");
|
||||
const NSEW_URL = textureToUrl("gui/hud_new_NSEW");
|
||||
|
||||
function Compass({ yaw }: { yaw: number | undefined }) {
|
||||
if (yaw == null) return null;
|
||||
// The ring notch is the fixed heading indicator (always "forward" at top).
|
||||
|
|
@ -34,9 +31,7 @@ function Compass({ yaw }: { yaw: number | undefined }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Health / Energy bars ──
|
||||
|
||||
function HealthBar({ value }: { value: number }) {
|
||||
const pct = Math.max(0, Math.min(100, value * 100));
|
||||
return (
|
||||
|
|
@ -45,7 +40,6 @@ function HealthBar({ value }: { value: number }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EnergyBar({ value }: { value: number }) {
|
||||
const pct = Math.max(0, Math.min(100, value * 100));
|
||||
return (
|
||||
|
|
@ -54,20 +48,16 @@ function EnergyBar({ value }: { value: number }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Reticle ──
|
||||
|
||||
const RETICLE_TEXTURES: Record<string, string> = {
|
||||
weapon_sniper: "gui/hud_ret_sniper",
|
||||
weapon_shocklance: "gui/hud_ret_shocklance",
|
||||
weapon_targeting: "gui/hud_ret_targlaser",
|
||||
};
|
||||
|
||||
function normalizeWeaponName(shape: string | undefined): string {
|
||||
if (!shape) return "";
|
||||
return shape.replace(/\.dts$/i, "").toLowerCase();
|
||||
}
|
||||
|
||||
function Reticle() {
|
||||
const weaponShape = useEngineSelector((state) => {
|
||||
const snap = state.playback.streamSnapshot;
|
||||
|
|
@ -98,9 +88,7 @@ function Reticle() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Weapon HUD (right side weapon list) ──
|
||||
|
||||
/** Maps $WeaponsHudData indices to simple icon textures (no baked background)
|
||||
* and labels. Mortar uses hud_new_ because no simple variant exists. */
|
||||
const WEAPON_HUD_SLOTS: Record<number, { icon: string; label: string }> = {
|
||||
|
|
@ -124,7 +112,6 @@ const WEAPON_HUD_SLOTS: Record<number, { icon: string; label: string }> = {
|
|||
16: { icon: "gui/hud_shocklance", label: "Shocklance" },
|
||||
17: { icon: "gui/hud_new_mortar", label: "Mortar" },
|
||||
};
|
||||
|
||||
// Precompute URLs so we don't call textureToUrl on every render.
|
||||
const WEAPON_HUD_ICON_URLS = new Map(
|
||||
Object.entries(WEAPON_HUD_SLOTS).map(([idx, w]) => [
|
||||
|
|
@ -132,12 +119,9 @@ const WEAPON_HUD_ICON_URLS = new Map(
|
|||
textureToUrl(w.icon),
|
||||
]),
|
||||
);
|
||||
|
||||
/** Targeting laser HUD indices (standard + TR2 variants). */
|
||||
const TARGETING_LASER_INDICES = new Set([9, 14, 15]);
|
||||
|
||||
const INFINITY_ICON_URL = textureToUrl("gui/hud_infinity");
|
||||
|
||||
function WeaponSlotIcon({
|
||||
slot,
|
||||
isSelected,
|
||||
|
|
@ -171,7 +155,6 @@ function WeaponSlotIcon({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WeaponHUD() {
|
||||
const weaponsHud = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.weaponsHud,
|
||||
|
|
@ -206,9 +189,7 @@ function WeaponHUD() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Team Scores (bottom-left) ──
|
||||
|
||||
/** Default team names from serverDefaults.cs. */
|
||||
const DEFAULT_TEAM_NAMES: Record<number, string> = {
|
||||
1: "Storm",
|
||||
|
|
@ -218,7 +199,6 @@ const DEFAULT_TEAM_NAMES: Record<number, string> = {
|
|||
5: "Blood Eagle",
|
||||
6: "Phoenix",
|
||||
};
|
||||
|
||||
function TeamScores() {
|
||||
const teamScores = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.teamScores,
|
||||
|
|
@ -227,7 +207,6 @@ function TeamScores() {
|
|||
(state) => state.playback.streamSnapshot?.playerSensorGroup,
|
||||
);
|
||||
if (!teamScores?.length) return null;
|
||||
|
||||
// Sort: friendly team first (if known), then by teamId.
|
||||
const sorted = [...teamScores].sort((a, b) => {
|
||||
if (playerSensorGroup) {
|
||||
|
|
@ -236,7 +215,6 @@ function TeamScores() {
|
|||
}
|
||||
return a.teamId - b.teamId;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.TeamScores}>
|
||||
{sorted.map((team: TeamScore) => {
|
||||
|
|
@ -262,9 +240,7 @@ function TeamScores() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Chat Window (top-left) ──
|
||||
|
||||
/** Map a colorCode to a CSS module class name (c0–c9 GuiChatHudProfile). */
|
||||
const CHAT_COLOR_CLASSES: Record<number, string> = {
|
||||
0: styles.ChatColor0,
|
||||
|
|
@ -278,11 +254,9 @@ const CHAT_COLOR_CLASSES: Record<number, string> = {
|
|||
8: styles.ChatColor8,
|
||||
9: styles.ChatColor9,
|
||||
};
|
||||
|
||||
function segmentColorClass(colorCode: number): string {
|
||||
return CHAT_COLOR_CLASSES[colorCode] ?? CHAT_COLOR_CLASSES[0];
|
||||
}
|
||||
|
||||
function chatColorClass(msg: DemoChatMessage): string {
|
||||
if (msg.colorCode != null && CHAT_COLOR_CLASSES[msg.colorCode]) {
|
||||
return CHAT_COLOR_CLASSES[msg.colorCode];
|
||||
|
|
@ -292,7 +266,6 @@ function chatColorClass(msg: DemoChatMessage): string {
|
|||
// byte color code, so the correct default for server messages is c0.
|
||||
return CHAT_COLOR_CLASSES[0];
|
||||
}
|
||||
|
||||
function ChatWindow() {
|
||||
const messages = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.chatMessages,
|
||||
|
|
@ -340,9 +313,7 @@ function ChatWindow() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Backpack + Inventory HUD (bottom-right) ──
|
||||
|
||||
/** Maps $BackpackHudData indices to icon textures. */
|
||||
const BACKPACK_ICONS: Record<number, string> = {
|
||||
0: "gui/hud_new_packammo",
|
||||
|
|
@ -366,7 +337,6 @@ const BACKPACK_ICONS: Record<number, string> = {
|
|||
18: "gui/hud_satchel_unarmed",
|
||||
19: "gui/hud_new_packenergy",
|
||||
};
|
||||
|
||||
/** Pack indices that have an armed/activated icon variant. */
|
||||
const BACKPACK_ARMED_ICONS: Record<number, string> = {
|
||||
1: "gui/hud_new_packcloak_armed",
|
||||
|
|
@ -375,7 +345,6 @@ const BACKPACK_ARMED_ICONS: Record<number, string> = {
|
|||
5: "gui/hud_new_packshield_armed",
|
||||
11: "gui/hud_new_packsensjam_armed",
|
||||
};
|
||||
|
||||
// Precompute URLs.
|
||||
const BACKPACK_ICON_URLS = new Map(
|
||||
Object.entries(BACKPACK_ICONS).map(([idx, tex]) => [
|
||||
|
|
@ -389,7 +358,6 @@ const BACKPACK_ARMED_ICON_URLS = new Map(
|
|||
textureToUrl(tex),
|
||||
]),
|
||||
);
|
||||
|
||||
/** Simple icons per inventory display slot (no baked-in background). */
|
||||
const INVENTORY_SLOT_ICONS: Record<number, { icon: string; label: string }> = {
|
||||
0: { icon: "gui/hud_handgren", label: "Grenade" },
|
||||
|
|
@ -397,14 +365,12 @@ const INVENTORY_SLOT_ICONS: Record<number, { icon: string; label: string }> = {
|
|||
2: { icon: "gui/hud_beacon", label: "Beacon" },
|
||||
3: { icon: "gui/hud_medpack", label: "Repair Kit" },
|
||||
};
|
||||
|
||||
const INVENTORY_ICON_URLS = new Map(
|
||||
Object.entries(INVENTORY_SLOT_ICONS).map(([slot, info]) => [
|
||||
Number(slot),
|
||||
textureToUrl(info.icon),
|
||||
]),
|
||||
);
|
||||
|
||||
function PackAndInventoryHUD() {
|
||||
const backpackHud = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.backpackHud,
|
||||
|
|
@ -412,9 +378,7 @@ function PackAndInventoryHUD() {
|
|||
const inventoryHud = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot?.inventoryHud,
|
||||
);
|
||||
|
||||
const hasPack = backpackHud && backpackHud.packIndex >= 0;
|
||||
|
||||
// Resolve pack icon.
|
||||
let packIconUrl: string | undefined;
|
||||
if (hasPack) {
|
||||
|
|
@ -423,7 +387,6 @@ function PackAndInventoryHUD() {
|
|||
: undefined;
|
||||
packIconUrl = armedUrl ?? BACKPACK_ICON_URLS.get(backpackHud.packIndex);
|
||||
}
|
||||
|
||||
// Build count lookup from snapshot data.
|
||||
const countBySlot = new Map<number, number>();
|
||||
if (inventoryHud) {
|
||||
|
|
@ -431,14 +394,11 @@ function PackAndInventoryHUD() {
|
|||
countBySlot.set(s.slot, s.count);
|
||||
}
|
||||
}
|
||||
|
||||
// Always show all inventory slot types, defaulting to 0.
|
||||
const allSlotIds = Object.keys(INVENTORY_SLOT_ICONS)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (!hasPack && !countBySlot.size) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.PackInventoryHUD}>
|
||||
{packIconUrl && (
|
||||
|
|
@ -473,19 +433,15 @@ function PackAndInventoryHUD() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main HUD ──
|
||||
|
||||
export function PlayerHUD() {
|
||||
const recording = useDemoRecording();
|
||||
const streamSnapshot = useEngineSelector(
|
||||
(state) => state.playback.streamSnapshot,
|
||||
);
|
||||
|
||||
if (!recording) return null;
|
||||
const status = streamSnapshot?.status;
|
||||
if (!status) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.PlayerHUD}>
|
||||
<ChatWindow />
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export function getPosedNodeTransform(
|
|||
scene: Group,
|
||||
animations: AnimationClip[],
|
||||
nodeName: string,
|
||||
overrideClipNames?: string[],
|
||||
): { position: Vector3; quaternion: Quaternion } | null {
|
||||
const clone = scene.clone(true);
|
||||
|
||||
|
|
@ -158,6 +159,21 @@ export function getPosedNodeTransform(
|
|||
if (rootClip) {
|
||||
const mixer = new AnimationMixer(clone);
|
||||
mixer.clipAction(rootClip).play();
|
||||
// Play override clips (e.g. arm pose) which replace bone transforms
|
||||
// on the bones they animate, at clip midpoint (neutral pose).
|
||||
if (overrideClipNames) {
|
||||
for (const name of overrideClipNames) {
|
||||
const clip = animations.find(
|
||||
(a) => a.name.toLowerCase() === name.toLowerCase(),
|
||||
);
|
||||
if (clip) {
|
||||
const action = mixer.clipAction(clip);
|
||||
action.time = clip.duration / 2;
|
||||
action.setEffectiveTimeScale(0);
|
||||
action.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
mixer.setTime(0);
|
||||
}
|
||||
|
||||
|
|
@ -398,6 +414,8 @@ export function buildStreamDemoEntity(
|
|||
ghostIndex: number | undefined,
|
||||
dataBlockId: number | undefined,
|
||||
shapeHint: string | undefined,
|
||||
explosionDataBlockId?: number,
|
||||
faceViewer?: boolean,
|
||||
): DemoEntity {
|
||||
return {
|
||||
id,
|
||||
|
|
@ -411,6 +429,8 @@ export function buildStreamDemoEntity(
|
|||
ghostIndex,
|
||||
dataBlockId,
|
||||
shapeHint,
|
||||
explosionDataBlockId,
|
||||
faceViewer,
|
||||
keyframes: [
|
||||
{
|
||||
time: 0,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
import { Matrix4, Quaternion } from "three";
|
||||
import { getTerrainHeightAt } from "../terrainHeight";
|
||||
import type {
|
||||
BackpackHudState,
|
||||
ChatSegment,
|
||||
DemoChatMessage,
|
||||
DemoThreadState,
|
||||
|
|
@ -96,6 +97,10 @@ interface MutableStreamEntity {
|
|||
headPitch?: number;
|
||||
/** Head yaw for blend animations (freelook), normalized [-1,1]. */
|
||||
headYaw?: number;
|
||||
/** Target render flags bitmask from the Target Manager. */
|
||||
targetRenderFlags?: number;
|
||||
/** True when FlagImage is mounted in slot 3 (player is carrying a flag). */
|
||||
carryingFlag?: boolean;
|
||||
/** Item physics simulation state (dropped weapons/items). */
|
||||
itemPhysics?: {
|
||||
velocity: [number, number, number];
|
||||
|
|
@ -153,6 +158,8 @@ interface StreamState {
|
|||
};
|
||||
/** Team scores aggregated from the PLAYERLIST demoValues section. */
|
||||
teamScores: TeamScore[];
|
||||
/** Live player roster keyed by clientId, updated by ServerMessage events. */
|
||||
playerRoster: Map<number, { name: string; teamId: number }>;
|
||||
}
|
||||
|
||||
const TICK_DURATION_MS = 32;
|
||||
|
|
@ -293,7 +300,10 @@ interface ParsedDemoValues {
|
|||
activeSlot: number;
|
||||
} | null;
|
||||
teamScores: TeamScore[];
|
||||
/** Initial player roster from PLAYERLIST section, keyed by clientId. */
|
||||
playerRoster: Map<number, { name: string; teamId: number }>;
|
||||
chatMessages: string[];
|
||||
gravity: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -309,7 +319,9 @@ function parseDemoValues(demoValues: string[]): ParsedDemoValues {
|
|||
backpackHud: null,
|
||||
inventoryHud: null,
|
||||
teamScores: [],
|
||||
playerRoster: new Map(),
|
||||
chatMessages: [],
|
||||
gravity: -20,
|
||||
};
|
||||
if (!demoValues.length) return result;
|
||||
|
||||
|
|
@ -330,7 +342,12 @@ function parseDemoValues(demoValues: string[]): ParsedDemoValues {
|
|||
const playerCountByTeam = new Map<number, number>();
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const fields = next().split("\t");
|
||||
const name = fields[0] ?? "";
|
||||
const clientId = parseInt(fields[2], 10);
|
||||
const teamId = parseInt(fields[4], 10);
|
||||
if (!isNaN(clientId) && !isNaN(teamId)) {
|
||||
result.playerRoster.set(clientId, { name, teamId });
|
||||
}
|
||||
if (!isNaN(teamId) && teamId > 0) {
|
||||
playerCountByTeam.set(teamId, (playerCountByTeam.get(teamId) ?? 0) + 1);
|
||||
}
|
||||
|
|
@ -458,7 +475,13 @@ function parseDemoValues(demoValues: string[]): ParsedDemoValues {
|
|||
}
|
||||
}
|
||||
|
||||
// GRAVITY: 1 value — skip
|
||||
// GRAVITY: 1 value (the server's getGravity() value).
|
||||
if (idx < demoValues.length) {
|
||||
const g = parseFloat(next());
|
||||
if (Number.isFinite(g)) {
|
||||
result.gravity = g;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -983,6 +1006,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
targetId: number;
|
||||
name?: string;
|
||||
sensorGroup: number;
|
||||
targetData: number;
|
||||
}>;
|
||||
sensorGroupColors: Array<{
|
||||
group: number;
|
||||
|
|
@ -1002,6 +1026,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
private readonly netStrings = new Map<number, string>();
|
||||
private readonly targetNames = new Map<number, string>();
|
||||
private readonly targetTeams = new Map<number, number>();
|
||||
private readonly targetRenderFlags = new Map<number, number>();
|
||||
/** IFF color map: for the viewer's sensorGroup, map target sensorGroup → RGB. */
|
||||
private readonly sensorGroupColors = new Map<
|
||||
number,
|
||||
|
|
@ -1009,6 +1034,31 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
>();
|
||||
private state: StreamState;
|
||||
|
||||
// Generation counters for derived-array caching in buildSnapshot().
|
||||
private _teamScoresGen = 0;
|
||||
private _rosterGen = 0;
|
||||
private _weaponsHudGen = 0;
|
||||
private _inventoryHudGen = 0;
|
||||
|
||||
// Cached snapshot returned when no ticks advance between stepToTime() calls.
|
||||
private _cachedSnapshot: DemoStreamSnapshot | null = null;
|
||||
private _cachedSnapshotTick = -1;
|
||||
|
||||
// Cached derived arrays from the last buildSnapshot() call.
|
||||
private _snap: {
|
||||
teamScoresGen: number;
|
||||
rosterGen: number;
|
||||
teamScores: TeamScore[];
|
||||
weaponsHudGen: number;
|
||||
weaponsHud: { slots: WeaponsHudSlot[]; activeIndex: number };
|
||||
inventoryHudGen: number;
|
||||
inventoryHud: { slots: InventoryHudSlot[]; activeSlot: number };
|
||||
backpackPackIndex: number;
|
||||
backpackActive: boolean;
|
||||
backpackText: string;
|
||||
backpackHud: BackpackHudState | null;
|
||||
} | null = null;
|
||||
|
||||
constructor(parser: DemoParser) {
|
||||
this.parser = parser;
|
||||
this.registry = parser.getRegistry();
|
||||
|
|
@ -1055,6 +1105,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
backpackHud: { packIndex: -1, active: false, text: "" },
|
||||
inventoryHud: { slots: new Map(), activeSlot: -1 },
|
||||
teamScores: [],
|
||||
playerRoster: new Map(),
|
||||
};
|
||||
|
||||
this.reset();
|
||||
|
|
@ -1062,10 +1113,14 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
|
||||
reset(): void {
|
||||
this.parser.reset();
|
||||
this._cachedSnapshot = null;
|
||||
this._cachedSnapshotTick = -1;
|
||||
this._snap = null;
|
||||
|
||||
this.netStrings.clear();
|
||||
this.targetNames.clear();
|
||||
this.targetTeams.clear();
|
||||
this.targetRenderFlags.clear();
|
||||
this.sensorGroupColors.clear();
|
||||
this.state.entitiesById.clear();
|
||||
this.state.entityIdByGhostIndex.clear();
|
||||
|
|
@ -1081,6 +1136,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
);
|
||||
}
|
||||
this.targetTeams.set(entry.targetId, entry.sensorGroup);
|
||||
this.targetRenderFlags.set(entry.targetId, entry.targetData);
|
||||
}
|
||||
// Seed IFF color table from the initial block.
|
||||
for (const c of this.initialBlock.sensorGroupColors) {
|
||||
|
|
@ -1099,6 +1155,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
this.state.backpackHud = { packIndex: -1, active: false, text: "" };
|
||||
this.state.inventoryHud = { slots: new Map(), activeSlot: -1 };
|
||||
this.state.teamScores = [];
|
||||
this.state.playerRoster = new Map();
|
||||
this.state.moveTicks = 0;
|
||||
this.state.absoluteYaw = 0;
|
||||
this.state.absolutePitch = 0;
|
||||
|
|
@ -1227,6 +1284,9 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
evt.parsedData.funcName as string,
|
||||
);
|
||||
const args = evt.parsedData.args as string[];
|
||||
if (funcName === "ServerMessage") {
|
||||
this.handleServerMessage(args);
|
||||
}
|
||||
this.handleHudRemoteCommand(funcName, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -1248,6 +1308,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
this.state.inventoryHud.activeSlot = parsed.inventoryHud.activeSlot;
|
||||
}
|
||||
this.state.teamScores = parsed.teamScores;
|
||||
this.state.playerRoster = new Map(parsed.playerRoster);
|
||||
// Seed chat messages at time 0 so they appear at start and fade naturally.
|
||||
// Raw lines from HudMessageVector contain Torque control chars: collapsed
|
||||
// color bytes (0x02–0x0e via collapseRemap), tagged string markup
|
||||
|
|
@ -1293,17 +1354,37 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
}
|
||||
|
||||
getSnapshot(): DemoStreamSnapshot {
|
||||
return this.buildSnapshot();
|
||||
if (this._cachedSnapshot && this._cachedSnapshotTick === this.state.moveTicks) {
|
||||
return this._cachedSnapshot;
|
||||
}
|
||||
const snapshot = this.buildSnapshot();
|
||||
this._cachedSnapshot = snapshot;
|
||||
this._cachedSnapshotTick = this.state.moveTicks;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
getEffectShapes(): string[] {
|
||||
const shapes = new Set<string>();
|
||||
const collectShapesFromExplosion = (expBlock: Record<string, unknown>) => {
|
||||
const shape = expBlock.dtsFileName as string | undefined;
|
||||
if (shape) shapes.add(shape);
|
||||
// Sub-explosions also have DTS shapes (e.g. mortar sub-explosions).
|
||||
const subExplosions = expBlock.subExplosions as (number | null)[] | undefined;
|
||||
if (Array.isArray(subExplosions)) {
|
||||
for (const subId of subExplosions) {
|
||||
if (subId == null) continue;
|
||||
const subBlock = this.getDataBlockData(subId);
|
||||
if (subBlock?.dtsFileName) {
|
||||
shapes.add(subBlock.dtsFileName as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const [, block] of this.initialBlock.dataBlocks) {
|
||||
const explosionId = block.data?.explosion as number | undefined;
|
||||
if (explosionId == null) continue;
|
||||
const expBlock = this.getDataBlockData(explosionId);
|
||||
const shape = expBlock?.dtsFileName as string | undefined;
|
||||
if (shape) shapes.add(shape);
|
||||
if (expBlock) collectShapesFromExplosion(expBlock);
|
||||
}
|
||||
return [...shapes];
|
||||
}
|
||||
|
|
@ -1317,10 +1398,13 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
: 0;
|
||||
const targetTicks = Math.floor((safeTargetSec * 1000) / TICK_DURATION_MS);
|
||||
|
||||
let didReset = false;
|
||||
if (targetTicks < this.state.moveTicks) {
|
||||
this.reset();
|
||||
didReset = true;
|
||||
}
|
||||
|
||||
const wasExhausted = this.state.exhausted;
|
||||
let movesProcessed = 0;
|
||||
while (
|
||||
!this.state.exhausted &&
|
||||
|
|
@ -1333,7 +1417,20 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
movesProcessed += 1;
|
||||
}
|
||||
|
||||
return this.buildSnapshot();
|
||||
if (
|
||||
movesProcessed === 0 &&
|
||||
!didReset &&
|
||||
wasExhausted === this.state.exhausted &&
|
||||
this._cachedSnapshot &&
|
||||
this._cachedSnapshotTick === this.state.moveTicks
|
||||
) {
|
||||
return this._cachedSnapshot;
|
||||
}
|
||||
|
||||
const snapshot = this.buildSnapshot();
|
||||
this._cachedSnapshot = snapshot;
|
||||
this._cachedSnapshotTick = this.state.moveTicks;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private stepOneMoveTick(): boolean {
|
||||
|
|
@ -1468,6 +1565,18 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
if (targetId != null && sensorGroup != null) {
|
||||
this.targetTeams.set(targetId, sensorGroup);
|
||||
}
|
||||
const renderFlags = evt.parsedData.renderFlags as number | undefined;
|
||||
if (targetId != null && renderFlags != null) {
|
||||
this.targetRenderFlags.set(targetId, renderFlags);
|
||||
// Propagate to any entity bound to this target so render flags
|
||||
// take effect immediately (e.g. clearing bit 0x2 when a player
|
||||
// drops a flag) rather than waiting for the next ghost update.
|
||||
for (const entity of this.state.entitiesById.values()) {
|
||||
if (entity.targetId === targetId) {
|
||||
entity.targetRenderFlags = renderFlags;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (eventName === "SetSensorGroupEvent" && evt.parsedData) {
|
||||
const sg = evt.parsedData.sensorGroup as number | undefined;
|
||||
if (sg != null) this.state.playerSensorGroup = sg;
|
||||
|
|
@ -1633,6 +1742,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
});
|
||||
}
|
||||
} else if (funcName === "ServerMessage" && args.length >= 2) {
|
||||
this.handleServerMessage(args);
|
||||
const rawTemplate = this.resolveNetString(args[1]);
|
||||
const serverColorCode = detectColorCode(rawTemplate);
|
||||
const rawText = this.formatRemoteArgs(args[1], args.slice(2));
|
||||
|
|
@ -1718,19 +1828,18 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
|
||||
// When a projectile entity is being removed (ghost delete, ghost index
|
||||
// reuse, or same-class index reuse), spawn an explosion at its last known
|
||||
// position if it hasn't already exploded. The Torque engine's KillGhost
|
||||
// mechanism silently drops pending ExplosionMask data when a ghost goes
|
||||
// out of scope, so explosion positions almost never arrive in the demo
|
||||
// stream. The original client compensated with client-side raycast
|
||||
// collision detection in processTick(); we approximate by triggering the
|
||||
// explosion when the ghost disappears.
|
||||
// position if it hasn't already exploded. Explosion positions usually
|
||||
// arrive via ExplosionMask in ghost updates (the server has a 13-tick /
|
||||
// 416ms DeleteWaitTicks window before KillGhost fires), but this fallback
|
||||
// catches cases where the explicit data was missed — e.g. network
|
||||
// congestion or the projectile going out of scope before the update.
|
||||
if (prevEntityId) {
|
||||
const prevEntity = this.state.entitiesById.get(prevEntityId);
|
||||
if (
|
||||
prevEntity &&
|
||||
prevEntity.type === "Projectile" &&
|
||||
!prevEntity.hasExploded &&
|
||||
prevEntity.explosionShape &&
|
||||
prevEntity.explosionDataBlockId != null &&
|
||||
prevEntity.position &&
|
||||
// Ghost is being deleted or its index is being reassigned to a new
|
||||
// ghost (either a different class or a fresh create of the same class).
|
||||
|
|
@ -1784,6 +1893,27 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
existingEntity.dataBlockId = undefined;
|
||||
existingEntity.shapeHint = undefined;
|
||||
existingEntity.visual = undefined;
|
||||
existingEntity.targetId = undefined;
|
||||
existingEntity.targetRenderFlags = undefined;
|
||||
existingEntity.carryingFlag = undefined;
|
||||
existingEntity.sensorGroup = undefined;
|
||||
existingEntity.playerName = undefined;
|
||||
existingEntity.weaponShape = undefined;
|
||||
existingEntity.weaponImageState = undefined;
|
||||
existingEntity.weaponImageStates = undefined;
|
||||
existingEntity.weaponImageStatesDbId = undefined;
|
||||
existingEntity.itemPhysics = undefined;
|
||||
existingEntity.threads = undefined;
|
||||
existingEntity.headPitch = undefined;
|
||||
existingEntity.headYaw = undefined;
|
||||
existingEntity.health = undefined;
|
||||
existingEntity.energy = undefined;
|
||||
existingEntity.maxEnergy = undefined;
|
||||
existingEntity.damageState = undefined;
|
||||
existingEntity.actionAnim = undefined;
|
||||
existingEntity.actionAtEnd = undefined;
|
||||
existingEntity.explosionDataBlockId = undefined;
|
||||
existingEntity.maintainEmitterId = undefined;
|
||||
entity = existingEntity;
|
||||
} else if (existingEntity) {
|
||||
entity = existingEntity;
|
||||
|
|
@ -1866,12 +1996,15 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
}
|
||||
| undefined {
|
||||
const projBlock = this.getDataBlockData(projDataBlockId);
|
||||
const explosionId = projBlock?.explosion as number | undefined;
|
||||
if (!projBlock) return undefined;
|
||||
const explosionId = projBlock.explosion as number | undefined;
|
||||
if (explosionId == null) return undefined;
|
||||
const expBlock = this.getDataBlockData(explosionId);
|
||||
if (!expBlock) return undefined;
|
||||
const shape = expBlock.dtsFileName as string | undefined;
|
||||
if (!shape) return undefined;
|
||||
// dtsFileName may be empty for particle-only explosions (e.g. grenades,
|
||||
// energy projectiles). Still return info so we can spawn the explosion
|
||||
// entity for position tracking and particle effects.
|
||||
const shape = (expBlock.dtsFileName as string | undefined) || undefined;
|
||||
// The parser's lifetimeMS field is actually in ticks (32ms each), not ms.
|
||||
const lifetimeTicks = (expBlock.lifetimeMS as number | undefined) ?? 31;
|
||||
return {
|
||||
|
|
@ -1915,14 +2048,15 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
entity.projectilePhysics = "linear";
|
||||
} else if (ballisticProjectileClassNames.has(entity.className)) {
|
||||
entity.projectilePhysics = "ballistic";
|
||||
entity.gravityMod = getNumberField(blockData, ["gravityMod"]) ?? 1.0;
|
||||
entity.gravityMod =
|
||||
getNumberField(blockData, ["gravityMod"]) ?? 1.0;
|
||||
} else if (seekerProjectileClassNames.has(entity.className)) {
|
||||
entity.projectilePhysics = "seeker";
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve explosion shape info for projectiles (once per entity).
|
||||
if (entity.type === "Projectile" && !entity.explosionShape) {
|
||||
if (entity.type === "Projectile" && entity.explosionDataBlockId == null) {
|
||||
const info = this.resolveExplosionInfo(dataBlockId);
|
||||
if (info) {
|
||||
entity.explosionShape = info.shape;
|
||||
|
|
@ -2000,6 +2134,24 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
entity.weaponImageState = undefined;
|
||||
entity.weaponImageStates = undefined;
|
||||
}
|
||||
|
||||
// Track FlagImage in slot 3 ($FlagSlot). The server mounts FlagImage
|
||||
// when a player picks up a flag and unmounts it on drop. buildSnapshot
|
||||
// uses carryingFlag to gate the flag icon on Players, which prevents
|
||||
// dead corpses (sharing the same targetId) from showing duplicate icons.
|
||||
const flagImage = images.find((img) => img.index === 3);
|
||||
if (flagImage) {
|
||||
const hasFlag = !!flagImage.dataBlockId && flagImage.dataBlockId > 0;
|
||||
entity.carryingFlag = hasFlag;
|
||||
if (entity.targetId != null && entity.targetId >= 0) {
|
||||
const prev = this.targetRenderFlags.get(entity.targetId) ?? 0;
|
||||
const updated = hasFlag ? prev | 0x2 : prev & ~0x2;
|
||||
if (updated !== prev) {
|
||||
this.targetRenderFlags.set(entity.targetId, updated);
|
||||
entity.targetRenderFlags = updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2093,8 +2245,10 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
// Item physics: simulate dropped items falling under gravity and bouncing.
|
||||
if (entity.type === "Item") {
|
||||
const atRest = data.atRest as boolean | undefined;
|
||||
if (atRest === true) {
|
||||
// Server says item is at rest — stop simulating.
|
||||
const warp = data.warp as boolean | undefined;
|
||||
if (atRest === true || warp === false) {
|
||||
// At rest, or position authoritatively set (e.g. flag returned to
|
||||
// base via setTransform → NoWarpMask) — stop simulating.
|
||||
entity.itemPhysics = undefined;
|
||||
} else if (atRest === false && isVec3Like(data.velocity)) {
|
||||
// Item is moving — initialize or update physics simulation.
|
||||
|
|
@ -2115,7 +2269,11 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
}
|
||||
}
|
||||
|
||||
// Compute simulatedVelocity for projectile physics.
|
||||
// Compute simulatedVelocity for projectile physics. Mirrors the engine's
|
||||
// unpackUpdate: velocity is only set on InitialUpdateMask or BounceMask,
|
||||
// so we only (re)initialize when this update actually transmits velocity
|
||||
// or direction data. Between updates, advanceProjectiles() accumulates
|
||||
// gravity on the existing simulatedVelocity.
|
||||
if (entity.projectilePhysics) {
|
||||
if (entity.projectilePhysics === "linear") {
|
||||
// Linear projectiles transmit direction + dryVelocity from datablock,
|
||||
|
|
@ -2147,14 +2305,18 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
vz += excessDir.z * excessVel;
|
||||
}
|
||||
entity.simulatedVelocity = [vx, vy, vz];
|
||||
} else if (entity.velocity) {
|
||||
// Ballistic and seeker: use the transmitted velocity directly.
|
||||
} else if (isVec3Like(data.velocity)) {
|
||||
// Ballistic/seeker: set velocity only when this ghost update transmits
|
||||
// it (initial create or BounceMask). The engine's unpackUpdate only
|
||||
// writes mCurrVelocity on these two mask bits.
|
||||
entity.simulatedVelocity = [
|
||||
entity.velocity[0],
|
||||
entity.velocity[1],
|
||||
entity.velocity[2],
|
||||
data.velocity.x,
|
||||
data.velocity.y,
|
||||
data.velocity.z,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (entity.projectilePhysics) {
|
||||
|
||||
// Fast-forward by currTick: the initial position is the firing point
|
||||
// and currTick tells us how many ticks have already elapsed.
|
||||
|
|
@ -2172,10 +2334,12 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
entity.position[2] += v[2] * dt;
|
||||
// For ballistic projectiles, also apply gravity during fast-forward.
|
||||
if (entity.projectilePhysics === "ballistic") {
|
||||
const g = 9.81 * (entity.gravityMod ?? 1);
|
||||
// GrenadeProjectile::computeNewState uses -9.81 * gravityMod
|
||||
// (globalGravity * 0.4905 * gravityMod, where 0.4905 = 9.81/20).
|
||||
const g = -9.81 * (entity.gravityMod ?? 1);
|
||||
// v.z changes linearly, position.z changes quadratically.
|
||||
entity.position[2] -= 0.5 * g * dt * dt;
|
||||
v[2] -= g * dt;
|
||||
entity.position[2] += 0.5 * g * dt * dt;
|
||||
v[2] += g * dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2193,7 +2357,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
entity.type === "Projectile" &&
|
||||
!entity.hasExploded &&
|
||||
explodePos &&
|
||||
entity.explosionShape
|
||||
entity.explosionDataBlockId != null
|
||||
) {
|
||||
this.spawnExplosion(entity, [explodePos.x, explodePos.y, explodePos.z]);
|
||||
}
|
||||
|
|
@ -2252,6 +2416,10 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
this.state.playerSensorGroup = team;
|
||||
}
|
||||
}
|
||||
const renderFlags = this.targetRenderFlags.get(data.targetId);
|
||||
if (renderFlags != null) {
|
||||
entity.targetRenderFlags = renderFlags;
|
||||
}
|
||||
}
|
||||
|
||||
// SoundMask: ghost-level playAudio() calls (e.g. station activation).
|
||||
|
|
@ -2285,8 +2453,8 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
const p = entity.position;
|
||||
|
||||
if (entity.projectilePhysics === "ballistic") {
|
||||
const g = 9.81 * (entity.gravityMod ?? 1);
|
||||
v[2] -= g * dt;
|
||||
// GrenadeProjectile::computeNewState: -9.81 * gravityMod per tick.
|
||||
v[2] += -9.81 * (entity.gravityMod ?? 1) * dt;
|
||||
}
|
||||
|
||||
p[0] += v[0] * dt;
|
||||
|
|
@ -2346,8 +2514,10 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
position: [number, number, number],
|
||||
): void {
|
||||
entity.hasExploded = true;
|
||||
const fxId = `fx_${this.state.nextExplosionId++}`;
|
||||
const lifetimeTicks = entity.explosionLifetimeTicks ?? 31;
|
||||
|
||||
// Spawn the main explosion entity.
|
||||
const fxId = `fx_${this.state.nextExplosionId++}`;
|
||||
const fxEntity: MutableStreamEntity = {
|
||||
id: fxId,
|
||||
ghostIndex: -1,
|
||||
|
|
@ -2363,6 +2533,56 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
expiryTick: this.state.moveTicks + lifetimeTicks,
|
||||
};
|
||||
this.state.entitiesById.set(fxId, fxEntity);
|
||||
|
||||
// Spawn sub-explosion entities (e.g. MortarSubExplosion1/2/3 carry the
|
||||
// actual DTS shapes while the main MortarExplosion has none).
|
||||
if (entity.explosionDataBlockId != null) {
|
||||
const expBlock = this.getDataBlockData(entity.explosionDataBlockId);
|
||||
const subExplosions = expBlock?.subExplosions as
|
||||
| (number | null)[]
|
||||
| undefined;
|
||||
if (Array.isArray(subExplosions)) {
|
||||
for (const subId of subExplosions) {
|
||||
if (subId == null) continue;
|
||||
const subBlock = this.getDataBlockData(subId);
|
||||
if (!subBlock) continue;
|
||||
const subShape =
|
||||
(subBlock.dtsFileName as string | undefined) || undefined;
|
||||
if (!subShape) continue;
|
||||
|
||||
const subLifetimeTicks =
|
||||
(subBlock.lifetimeMS as number | undefined) ?? 31;
|
||||
const offset = (subBlock.offset as number | undefined) ?? 0;
|
||||
|
||||
// Randomize position offset in XY plane (Torque convention).
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const subPos: [number, number, number] = [
|
||||
position[0] + Math.cos(angle) * offset,
|
||||
position[1] + Math.sin(angle) * offset,
|
||||
position[2],
|
||||
];
|
||||
|
||||
const subFxId = `fx_${this.state.nextExplosionId++}`;
|
||||
const subFxEntity: MutableStreamEntity = {
|
||||
id: subFxId,
|
||||
ghostIndex: -1,
|
||||
className: "Explosion",
|
||||
spawnTick: this.state.moveTicks,
|
||||
type: "Explosion",
|
||||
dataBlock: subShape,
|
||||
explosionDataBlockId: subId,
|
||||
position: subPos,
|
||||
rotation: [0, 0, 0, 1],
|
||||
isExplosion: true,
|
||||
faceViewer:
|
||||
subBlock.faceViewer !== false && subBlock.faceViewer !== 0,
|
||||
expiryTick: this.state.moveTicks + subLifetimeTicks,
|
||||
};
|
||||
this.state.entitiesById.set(subFxId, subFxEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the projectile — the explosion takes over visually.
|
||||
entity.position = undefined;
|
||||
entity.simulatedVelocity = undefined;
|
||||
|
|
@ -2517,6 +2737,81 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
}
|
||||
}
|
||||
|
||||
/** Process ServerMessage events that update team scores and player roster. */
|
||||
private handleServerMessage(args: string[]): void {
|
||||
if (args.length < 2) return;
|
||||
const msgType = this.resolveNetString(args[0]);
|
||||
|
||||
if (msgType === "MsgTeamScoreIs" && args.length >= 4) {
|
||||
// args: [msgType, "", teamId, newScore]
|
||||
const teamId = parseInt(this.resolveNetString(args[2]), 10);
|
||||
const newScore = parseInt(this.resolveNetString(args[3]), 10);
|
||||
if (!isNaN(teamId) && !isNaN(newScore)) {
|
||||
const entry = this.state.teamScores.find((t) => t.teamId === teamId);
|
||||
if (entry) {
|
||||
entry.score = newScore;
|
||||
this._teamScoresGen++;
|
||||
}
|
||||
}
|
||||
} else if (msgType === "MsgCTFAddTeam" && args.length >= 6) {
|
||||
// args: [msgType, "", teamIdx, teamName, flagStatus, score]
|
||||
const teamIdx = parseInt(this.resolveNetString(args[2]), 10);
|
||||
const teamName = stripTaggedStringMarkup(this.resolveNetString(args[3]));
|
||||
const score = parseInt(this.resolveNetString(args[5]), 10);
|
||||
if (!isNaN(teamIdx)) {
|
||||
const teamId = teamIdx + 1;
|
||||
const existing = this.state.teamScores.find(
|
||||
(t) => t.teamId === teamId,
|
||||
);
|
||||
if (existing) {
|
||||
existing.name = teamName;
|
||||
existing.score = isNaN(score) ? existing.score : score;
|
||||
this._teamScoresGen++;
|
||||
} else {
|
||||
this.state.teamScores.push({
|
||||
teamId,
|
||||
name: teamName,
|
||||
score: isNaN(score) ? 0 : score,
|
||||
playerCount: 0,
|
||||
});
|
||||
this._teamScoresGen++;
|
||||
}
|
||||
}
|
||||
} else if (msgType === "MsgClientJoin" && args.length >= 4) {
|
||||
// args: [msgType, "", clientId, name, ...]
|
||||
const clientId = parseInt(this.resolveNetString(args[2]), 10);
|
||||
const name = stripTaggedStringMarkup(this.resolveNetString(args[3]));
|
||||
if (!isNaN(clientId)) {
|
||||
const existing = this.state.playerRoster.get(clientId);
|
||||
this.state.playerRoster.set(clientId, {
|
||||
name,
|
||||
teamId: existing?.teamId ?? 0,
|
||||
});
|
||||
this._rosterGen++;
|
||||
}
|
||||
} else if (msgType === "MsgClientDrop" && args.length >= 3) {
|
||||
// args: [msgType, "", clientId, ...]
|
||||
const clientId = parseInt(this.resolveNetString(args[2]), 10);
|
||||
if (!isNaN(clientId)) {
|
||||
this.state.playerRoster.delete(clientId);
|
||||
this._rosterGen++;
|
||||
}
|
||||
} else if (msgType === "MsgClientJoinTeam" && args.length >= 4) {
|
||||
// args: [msgType, "", clientId, teamId, ...]
|
||||
const clientId = parseInt(this.resolveNetString(args[2]), 10);
|
||||
const teamId = parseInt(this.resolveNetString(args[3]), 10);
|
||||
if (!isNaN(clientId) && !isNaN(teamId)) {
|
||||
const existing = this.state.playerRoster.get(clientId);
|
||||
if (existing) {
|
||||
existing.teamId = teamId;
|
||||
} else {
|
||||
this.state.playerRoster.set(clientId, { name: "", teamId });
|
||||
}
|
||||
this._rosterGen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleHudRemoteCommand(funcName: string, args: string[]): void {
|
||||
// ── Weapons HUD ──
|
||||
if (funcName === "setWeaponsHudItem" && args.length >= 3) {
|
||||
|
|
@ -2529,6 +2824,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
} else {
|
||||
this.state.weaponsHud.slots.delete(slot);
|
||||
}
|
||||
this._weaponsHudGen++;
|
||||
}
|
||||
} else if (funcName === "setWeaponsHudAmmo" && args.length >= 2) {
|
||||
const slot = parseInt(args[0], 10);
|
||||
|
|
@ -2537,6 +2833,7 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
// Treat ammo updates as implicit inventory presence — the
|
||||
// initial setWeaponsHudItem may have been sent before recording.
|
||||
this.state.weaponsHud.slots.set(slot, isNaN(ammo) ? -1 : ammo);
|
||||
this._weaponsHudGen++;
|
||||
}
|
||||
} else if (funcName === "setWeaponsHudActive" && args.length >= 1) {
|
||||
const slot = parseInt(args[0], 10);
|
||||
|
|
@ -2547,9 +2844,11 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
this.state.weaponsHud.slots.set(slot, -1);
|
||||
}
|
||||
}
|
||||
this._weaponsHudGen++;
|
||||
} else if (funcName === "setWeaponsHudClearAll") {
|
||||
this.state.weaponsHud.slots.clear();
|
||||
this.state.weaponsHud.activeIndex = -1;
|
||||
this._weaponsHudGen++;
|
||||
|
||||
// ── Backpack HUD ──
|
||||
} else if (funcName === "setBackpackHudItem" && args.length >= 2) {
|
||||
|
|
@ -2594,16 +2893,19 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
} else {
|
||||
this.state.inventoryHud.slots.delete(slot);
|
||||
}
|
||||
this._inventoryHudGen++;
|
||||
}
|
||||
} else if (funcName === "setInventoryHudAmount" && args.length >= 2) {
|
||||
const slot = parseInt(args[0], 10);
|
||||
const amount = parseInt(args[1], 10);
|
||||
if (!isNaN(slot) && !isNaN(amount)) {
|
||||
this.state.inventoryHud.slots.set(slot, amount);
|
||||
this._inventoryHudGen++;
|
||||
}
|
||||
} else if (funcName === "setInventoryHudClearAll") {
|
||||
this.state.inventoryHud.slots.clear();
|
||||
this.state.inventoryHud.activeSlot = -1;
|
||||
this._inventoryHudGen++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2613,6 +2915,20 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
if (!shouldRenderGhostEntity(entity)) {
|
||||
continue;
|
||||
}
|
||||
// Read the latest targetRenderFlags from the map (source of truth
|
||||
// from TargetInfoEvents and FlagImage slot tracking) rather than the
|
||||
// entity cache, which may be stale.
|
||||
let renderFlags =
|
||||
entity.targetId != null && entity.targetId >= 0
|
||||
? (this.targetRenderFlags.get(entity.targetId) ??
|
||||
entity.targetRenderFlags)
|
||||
: entity.targetRenderFlags;
|
||||
// For Players, only show the flag icon if this specific entity has
|
||||
// FlagImage mounted in slot 3. Dead corpses share the same targetId as
|
||||
// the alive player but don't have FlagImage, so this prevents duplicates.
|
||||
if (entity.type === "Player" && !entity.carryingFlag) {
|
||||
renderFlags = renderFlags != null ? renderFlags & ~0x2 : renderFlags;
|
||||
}
|
||||
entities.push({
|
||||
id: entity.id,
|
||||
type: entity.type,
|
||||
|
|
@ -2625,8 +2941,11 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
dataBlock: entity.dataBlock,
|
||||
weaponShape: entity.weaponShape,
|
||||
playerName: entity.playerName,
|
||||
targetRenderFlags: renderFlags,
|
||||
iffColor:
|
||||
entity.type === "Player" && entity.sensorGroup != null
|
||||
(entity.type === "Player" ||
|
||||
((renderFlags ?? 0) & 0x2) !== 0) &&
|
||||
entity.sensorGroup != null
|
||||
? this.resolveIffColor(entity.sensorGroup)
|
||||
: undefined,
|
||||
// Only clone position for entities whose position is mutated in-place
|
||||
|
|
@ -2658,6 +2977,80 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
}
|
||||
|
||||
const timeSec = this.state.moveTicks * (TICK_DURATION_MS / 1000);
|
||||
const prev = this._snap;
|
||||
|
||||
const chatMessages = this.state.chatMessages.filter(
|
||||
(m) => m.timeSec > timeSec - 15,
|
||||
);
|
||||
|
||||
const audioEvents = this.state.pendingAudioEvents.filter(
|
||||
(e) => e.timeSec > timeSec - 0.5 && e.timeSec <= timeSec,
|
||||
);
|
||||
|
||||
const weaponsHud =
|
||||
prev && prev.weaponsHudGen === this._weaponsHudGen
|
||||
? prev.weaponsHud
|
||||
: {
|
||||
slots: Array.from(this.state.weaponsHud.slots.entries()).map(
|
||||
([index, ammo]): WeaponsHudSlot => ({ index, ammo }),
|
||||
),
|
||||
activeIndex: this.state.weaponsHud.activeIndex,
|
||||
};
|
||||
|
||||
const inventoryHud =
|
||||
prev && prev.inventoryHudGen === this._inventoryHudGen
|
||||
? prev.inventoryHud
|
||||
: {
|
||||
slots: Array.from(this.state.inventoryHud.slots.entries()).map(
|
||||
([slot, count]): InventoryHudSlot => ({ slot, count }),
|
||||
),
|
||||
activeSlot: this.state.inventoryHud.activeSlot,
|
||||
};
|
||||
|
||||
const backpackHud =
|
||||
prev &&
|
||||
prev.backpackPackIndex === this.state.backpackHud.packIndex &&
|
||||
prev.backpackActive === this.state.backpackHud.active &&
|
||||
prev.backpackText === this.state.backpackHud.text
|
||||
? prev.backpackHud
|
||||
: this.state.backpackHud.packIndex >= 0
|
||||
? { ...this.state.backpackHud }
|
||||
: null;
|
||||
|
||||
let teamScores: TeamScore[];
|
||||
if (
|
||||
prev &&
|
||||
prev.teamScoresGen === this._teamScoresGen &&
|
||||
prev.rosterGen === this._rosterGen
|
||||
) {
|
||||
teamScores = prev.teamScores;
|
||||
} else {
|
||||
teamScores = this.state.teamScores.map((ts) => ({ ...ts }));
|
||||
const teamCounts = new Map<number, number>();
|
||||
for (const { teamId } of this.state.playerRoster.values()) {
|
||||
if (teamId > 0) {
|
||||
teamCounts.set(teamId, (teamCounts.get(teamId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
for (const ts of teamScores) {
|
||||
ts.playerCount = teamCounts.get(ts.teamId) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
this._snap = {
|
||||
teamScoresGen: this._teamScoresGen,
|
||||
rosterGen: this._rosterGen,
|
||||
teamScores,
|
||||
weaponsHudGen: this._weaponsHudGen,
|
||||
weaponsHud,
|
||||
inventoryHudGen: this._inventoryHudGen,
|
||||
inventoryHud,
|
||||
backpackPackIndex: this.state.backpackHud.packIndex,
|
||||
backpackActive: this.state.backpackHud.active,
|
||||
backpackText: this.state.backpackHud.text,
|
||||
backpackHud,
|
||||
};
|
||||
|
||||
return {
|
||||
timeSec,
|
||||
exhausted: this.state.exhausted,
|
||||
|
|
@ -2666,29 +3059,12 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
controlPlayerGhostId: this.state.controlPlayerGhostId,
|
||||
playerSensorGroup: this.state.playerSensorGroup,
|
||||
status: this.state.lastStatus,
|
||||
chatMessages: this.state.chatMessages.filter(
|
||||
(m) => m.timeSec > timeSec - 15,
|
||||
),
|
||||
audioEvents: this.state.pendingAudioEvents.filter(
|
||||
(e) => e.timeSec > timeSec - 0.5 && e.timeSec <= timeSec,
|
||||
),
|
||||
weaponsHud: {
|
||||
slots: Array.from(this.state.weaponsHud.slots.entries()).map(
|
||||
([index, ammo]): WeaponsHudSlot => ({ index, ammo }),
|
||||
),
|
||||
activeIndex: this.state.weaponsHud.activeIndex,
|
||||
},
|
||||
backpackHud:
|
||||
this.state.backpackHud.packIndex >= 0
|
||||
? { ...this.state.backpackHud }
|
||||
: null,
|
||||
inventoryHud: {
|
||||
slots: Array.from(this.state.inventoryHud.slots.entries()).map(
|
||||
([slot, count]): InventoryHudSlot => ({ slot, count }),
|
||||
),
|
||||
activeSlot: this.state.inventoryHud.activeSlot,
|
||||
},
|
||||
teamScores: this.state.teamScores,
|
||||
chatMessages,
|
||||
audioEvents,
|
||||
weaponsHud,
|
||||
backpackHud,
|
||||
inventoryHud,
|
||||
teamScores,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2787,6 +3163,10 @@ class StreamingPlayback implements DemoStreamingPlayback {
|
|||
);
|
||||
}
|
||||
}
|
||||
// Torque drops trailing empty string args from commandToClient, so the
|
||||
// template may reference %N tokens beyond the supplied args (e.g. a
|
||||
// plural "s" that evaluates to ""). Replace any remaining placeholders.
|
||||
resolved = resolved.replace(/%\d+/g, "");
|
||||
return stripTaggedStringMarkup(resolved);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ export interface DemoEntity {
|
|||
playerName?: string;
|
||||
/** IFF color resolved from the sensor group color table (sRGB 0-255). */
|
||||
iffColor?: { r: number; g: number; b: number };
|
||||
/** Target render flags bitmask from the Target Manager. */
|
||||
targetRenderFlags?: number;
|
||||
/** Weapon image condition flags from ghost ImageMask data. */
|
||||
weaponImageState?: WeaponImageState;
|
||||
/** Weapon image state machine states from the ShapeBaseImageData datablock. */
|
||||
|
|
@ -125,6 +127,10 @@ export interface DemoEntity {
|
|||
headPitch?: number;
|
||||
/** Head yaw for blend animations (freelook), normalized [-1,1]. -1 = max right, 1 = max left. */
|
||||
headYaw?: number;
|
||||
/** Numeric ID of the ExplosionData datablock (for explosion shape rendering). */
|
||||
explosionDataBlockId?: number;
|
||||
/** Billboard toward camera (Torque's faceViewer). */
|
||||
faceViewer?: boolean;
|
||||
}
|
||||
|
||||
export interface DemoRecording {
|
||||
|
|
@ -147,6 +153,8 @@ export interface DemoStreamEntity {
|
|||
playerName?: string;
|
||||
/** IFF color resolved from the sensor group color table (sRGB 0-255). */
|
||||
iffColor?: { r: number; g: number; b: number };
|
||||
/** Target render flags bitmask from the Target Manager. */
|
||||
targetRenderFlags?: number;
|
||||
ghostIndex?: number;
|
||||
className?: string;
|
||||
dataBlockId?: number;
|
||||
|
|
|
|||
|
|
@ -295,6 +295,17 @@ export class EmitterInstance {
|
|||
|
||||
if (this.particles.length < this.maxParticles) {
|
||||
this.addParticle(pos, axis);
|
||||
|
||||
// V12: when overrideAdvances is false, immediately age the newly
|
||||
// spawned particle by the remaining time in this frame. If that
|
||||
// exceeds its lifetime, kill it immediately (never rendered).
|
||||
if (!this.data.overrideAdvances && timeLeft > 0) {
|
||||
const p = this.particles[this.particles.length - 1];
|
||||
p.currentAge += timeLeft;
|
||||
if (p.currentAge >= p.totalLifetime) {
|
||||
this.particles.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute next emission time.
|
||||
|
|
@ -310,10 +321,10 @@ export class EmitterInstance {
|
|||
update(dtMS: number): void {
|
||||
this.emitterAge += dtMS;
|
||||
|
||||
// Check emitter lifetime.
|
||||
// Check emitter lifetime (V12 uses strictly greater).
|
||||
if (
|
||||
this.emitterLifetime > 0 &&
|
||||
this.emitterAge >= this.emitterLifetime
|
||||
this.emitterAge > this.emitterLifetime
|
||||
) {
|
||||
this.emitterDead = true;
|
||||
}
|
||||
|
|
@ -339,9 +350,9 @@ export class EmitterInstance {
|
|||
|
||||
// a = acc - vel*drag - wind*windCoeff + gravity*gravCoeff
|
||||
// We skip wind for now (no wind system yet).
|
||||
const ax = -p.vel[0] * drag;
|
||||
const ay = -p.vel[1] * drag;
|
||||
const az = -p.vel[2] * drag + GRAVITY_Z * gravCoeff;
|
||||
const ax = p.acc[0] - p.vel[0] * drag;
|
||||
const ay = p.acc[1] - p.vel[1] * drag;
|
||||
const az = p.acc[2] - p.vel[2] * drag + GRAVITY_Z * gravCoeff;
|
||||
|
||||
// Symplectic Euler: update vel first, then pos with new vel.
|
||||
p.vel[0] += ax * dt;
|
||||
|
|
@ -430,14 +441,13 @@ export class EmitterInstance {
|
|||
ejZ * speed,
|
||||
];
|
||||
|
||||
// V12: acc = vel * constantAcceleration (set once, never changes).
|
||||
// We fold this into the initial velocity for simplicity since the
|
||||
// constant acceleration just biases the starting velocity direction.
|
||||
// Actually, in V12 acc is a separate constant vector added each frame.
|
||||
// For faithfulness, we should track it. But since it's constant, we can
|
||||
// just apply it in the update loop as a per-particle property.
|
||||
// For now, bake it into the velocity since most Tribes 2 datablocks
|
||||
// have constantAcceleration = 0.
|
||||
// V12: acc = vel * constantAcceleration, set once at spawn, applied every frame.
|
||||
const ca = pData.constantAcceleration;
|
||||
const acc: [number, number, number] = [
|
||||
vel[0] * ca,
|
||||
vel[1] * ca,
|
||||
vel[2] * ca,
|
||||
];
|
||||
|
||||
// Particle lifetime with variance.
|
||||
let lifetime = pData.lifetimeMS;
|
||||
|
|
@ -456,6 +466,7 @@ export class EmitterInstance {
|
|||
this.particles.push({
|
||||
pos: spawnPos,
|
||||
vel,
|
||||
acc,
|
||||
orientDir: [ejX, ejY, ejZ],
|
||||
currentAge: 0,
|
||||
totalLifetime: lifetime,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ attribute vec4 particleColor;
|
|||
attribute float particleSize;
|
||||
attribute float particleSpin;
|
||||
attribute vec2 quadCorner; // (-0.5,-0.5) to (0.5,0.5)
|
||||
attribute vec3 orientDir;
|
||||
|
||||
uniform bool uOrientParticles;
|
||||
// cameraPosition is a built-in Three.js uniform.
|
||||
|
||||
varying vec2 vUv;
|
||||
varying vec4 vColor;
|
||||
|
|
@ -12,27 +16,47 @@ void main() {
|
|||
vUv = quadCorner + 0.5; // [0,1] range
|
||||
vColor = particleColor;
|
||||
|
||||
// Transform particle center to view space for billboarding.
|
||||
vec3 viewPos = (modelViewMatrix * vec4(position, 1.0)).xyz;
|
||||
if (uOrientParticles) {
|
||||
if (length(orientDir) < 0.0001) {
|
||||
// V12: don't render oriented particles with zero velocity.
|
||||
gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
// V12 oriented particle: quad aligned along direction, facing camera.
|
||||
vec3 worldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
||||
vec3 dir = normalize(orientDir);
|
||||
vec3 dirFromCam = worldPos - cameraPosition;
|
||||
vec3 crossDir = normalize(cross(dirFromCam, dir));
|
||||
|
||||
// Apply spin rotation to quad corner.
|
||||
float c = cos(particleSpin);
|
||||
float s = sin(particleSpin);
|
||||
vec2 rotated = vec2(
|
||||
c * quadCorner.x - s * quadCorner.y,
|
||||
s * quadCorner.x + c * quadCorner.y
|
||||
);
|
||||
// V12 maps U along dir (velocity) — match by using quadCorner.x for dir.
|
||||
vec3 offset = dir * quadCorner.x + crossDir * quadCorner.y;
|
||||
worldPos += offset * particleSize;
|
||||
|
||||
// Offset in view space (camera-facing billboard).
|
||||
viewPos.xy += rotated * particleSize;
|
||||
gl_Position = projectionMatrix * viewMatrix * vec4(worldPos, 1.0);
|
||||
} else {
|
||||
// Standard camera-facing billboard.
|
||||
vec3 viewPos = (modelViewMatrix * vec4(position, 1.0)).xyz;
|
||||
|
||||
gl_Position = projectionMatrix * vec4(viewPos, 1.0);
|
||||
// Apply spin rotation to quad corner.
|
||||
float c = cos(particleSpin);
|
||||
float s = sin(particleSpin);
|
||||
vec2 rotated = vec2(
|
||||
c * quadCorner.x - s * quadCorner.y,
|
||||
s * quadCorner.x + c * quadCorner.y
|
||||
);
|
||||
|
||||
// Offset in view space (camera-facing billboard).
|
||||
viewPos.xy += rotated * particleSize;
|
||||
|
||||
gl_Position = projectionMatrix * vec4(viewPos, 1.0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const particleFragmentShader = /* glsl */ `
|
||||
uniform sampler2D particleTexture;
|
||||
uniform bool hasTexture;
|
||||
uniform float debugOpacity;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying vec4 vColor;
|
||||
|
|
@ -44,5 +68,6 @@ void main() {
|
|||
} else {
|
||||
gl_FragColor = vColor;
|
||||
}
|
||||
gl_FragColor.a *= debugOpacity;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ export interface EmitterDataResolved {
|
|||
export interface Particle {
|
||||
pos: [number, number, number];
|
||||
vel: [number, number, number];
|
||||
/** V12: constant acceleration = vel * constantAcceleration, set once at spawn. */
|
||||
acc: [number, number, number];
|
||||
orientDir: [number, number, number];
|
||||
currentAge: number;
|
||||
totalLifetime: number;
|
||||
|
|
|
|||
|
|
@ -305,6 +305,49 @@ export const engineStore = createStore<EngineStoreState>()(
|
|||
})),
|
||||
);
|
||||
|
||||
// ── Rate-scaled effect clock ──
|
||||
//
|
||||
// A monotonic clock that advances by (frameDelta × playbackRate) each frame.
|
||||
// Components use demoEffectNow() instead of performance.now() so that effect
|
||||
// timers (explosions, particles, shockwaves, animation threads) automatically
|
||||
// pause when the demo is paused and speed up / slow down with the playback
|
||||
// rate. The main DemoPlaybackStreaming component calls advanceEffectClock()
|
||||
// once per frame.
|
||||
|
||||
let _effectClockMs = 0;
|
||||
|
||||
/**
|
||||
* Returns the current effect clock value in milliseconds.
|
||||
* Analogous to performance.now() but only advances when playing,
|
||||
* scaled by the playback rate.
|
||||
*/
|
||||
export function demoEffectNow(): number {
|
||||
return _effectClockMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the effect clock. Called once per frame from
|
||||
* DemoPlaybackStreaming before other useFrame callbacks run.
|
||||
*/
|
||||
export function advanceEffectClock(deltaSec: number, rate: number): void {
|
||||
_effectClockMs += deltaSec * rate * 1000;
|
||||
}
|
||||
|
||||
/** Reset the effect clock (call when demo recording changes or stops). */
|
||||
export function resetEffectClock(): void {
|
||||
_effectClockMs = 0;
|
||||
}
|
||||
|
||||
// Reset on stop.
|
||||
engineStore.subscribe(
|
||||
(state) => state.playback.status,
|
||||
(status) => {
|
||||
if (status === "stopped") {
|
||||
resetEffectClock();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export function useEngineStoreApi() {
|
||||
return engineStore;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export type {
|
|||
|
||||
export {
|
||||
engineStore,
|
||||
demoEffectNow,
|
||||
advanceEffectClock,
|
||||
resetEffectClock,
|
||||
useEngineSelector,
|
||||
useEngineStoreApi,
|
||||
useRuntimeObjectById,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue