improve mission selector

This commit is contained in:
Brian Beck 2025-12-02 16:58:35 -08:00
parent af17b43584
commit 10b4a65a87
18 changed files with 594 additions and 183 deletions

View file

@ -37,19 +37,21 @@ function MapInspector() {
<QueryClientProvider client={queryClient}>
<main>
<SettingsProvider>
<Canvas shadows frameloop="always">
<CamerasProvider>
<AudioProvider>
<Mission key={missionName} name={missionName} />
<ObserverCamera />
<DebugElements />
<ObserverControls />
</AudioProvider>
</CamerasProvider>
<EffectComposer>
<N8AO intensity={3} aoRadius={3} quality="performance" />
</EffectComposer>
</Canvas>
<div id="canvasContainer">
<Canvas shadows frameloop="always">
<CamerasProvider>
<AudioProvider>
<Mission key={missionName} name={missionName} />
<ObserverCamera />
<DebugElements />
<ObserverControls />
</AudioProvider>
</CamerasProvider>
<EffectComposer>
<N8AO intensity={3} aoRadius={3} quality="performance" />
</EffectComposer>
</Canvas>
</div>
<InspectorControls
missionName={missionName}
onChangeMission={setMissionName}

View file

@ -1,10 +1,16 @@
html,
body {
html {
box-sizing: border-box;
margin: 0;
padding: 0;
background: black;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
font-family:
system-ui,
@ -21,11 +27,25 @@ html {
font-size: 100%;
}
body {
margin: 0;
padding: 0;
}
main {
width: 100vw;
height: 100vh;
}
#canvasContainer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
#controls {
display: flex;
align-items: center;
@ -35,9 +55,10 @@ main {
left: 0;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding: 10px 12px 10px 8px;
padding: 8px 12px 8px 8px;
border-radius: 0 0 4px 0;
font-size: 13px;
z-index: 1;
}
.CheckboxField {
@ -87,3 +108,152 @@ main {
.AxisLabel[data-axis="z"] {
color: rgb(0, 153, 255);
}
/* MissionSelect combobox styles */
.MissionSelect-inputWrapper {
position: relative;
display: flex;
align-items: center;
}
.MissionSelect-shortcut {
position: absolute;
right: 7px;
font-family: system-ui, sans-serif;
font-size: 11px;
padding: 1px 4px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.6);
pointer-events: none;
}
.MissionSelect-input[aria-expanded="true"] ~ .MissionSelect-shortcut {
display: none;
}
.MissionSelect-input {
width: 240px;
padding: 6px 36px 6px 8px;
font-size: 14px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
outline: none;
}
.MissionSelect-input[aria-expanded="true"] {
padding-right: 8px;
}
.MissionSelect-input:focus {
border-color: rgba(255, 255, 255, 0.6);
}
.MissionSelect-input::placeholder {
color: #fff;
font-weight: 600;
}
.MissionSelect-popover {
z-index: 100;
min-width: 320px;
max-height: var(--popover-available-height, 90vh);
overflow-y: auto;
overscroll-behavior: contain;
background: rgba(20, 20, 20, 0.95);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 3px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.MissionSelect-list {
padding: 4px 0;
}
.MissionSelect-list:has(> .MissionSelect-group:first-child) {
padding-top: 0;
}
.MissionSelect-group {
padding-bottom: 4px;
}
.MissionSelect-groupLabel {
position: sticky;
top: 0;
padding: 6px 8px 6px 12px;
font-size: 13px;
font-weight: 600;
color: rgb(198, 202, 202);
background: rgba(58, 69, 72, 0.95);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
z-index: 1;
}
.MissionSelect-group:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.MissionSelect-item {
display: flex;
flex-direction: column;
gap: 1px;
margin: 4px 4px 0;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
outline: none;
scroll-margin-top: 32px;
}
.MissionSelect-list > .MissionSelect-item:first-child {
margin-top: 0;
}
.MissionSelect-item[data-active-item] {
background: rgba(255, 255, 255, 0.15);
}
.MissionSelect-item[aria-selected="true"] {
background: rgba(100, 150, 255, 0.3);
}
.MissionSelect-itemHeader {
display: flex;
align-items: center;
gap: 6px;
}
.MissionSelect-itemName {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.MissionSelect-itemTypes {
display: flex;
gap: 3px;
}
.MissionSelect-itemType {
font-size: 10px;
font-weight: 600;
padding: 2px 5px;
border-radius: 3px;
background: rgba(255, 157, 0, 0.4);
color: #fff;
}
.MissionSelect-itemMissionName {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
.MissionSelect-noResults {
padding: 12px 8px;
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
text-align: center;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
body,html{margin:0;padding:0;background:black}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-size:100%}main{width:100vw;height:100vh}#controls{display:flex;align-items:center;gap:20px;position:fixed;top:0;left:0;background:rgba(0,0,0,.5);color:#fff;padding:10px 12px 10px 8px;border-radius:0 0 4px 0;font-size:13px}.CheckboxField,.Field{display:flex;align-items:center;gap:6px}#fovInput,#speedInput{max-width:80px}.StaticShapeLabel{background:rgba(0,0,0,.5);color:#fff;font-size:11px;white-space:nowrap;padding:1px 3px;border-radius:1px}.StatsPanel{left:auto!important;right:0}.AxisLabel{font-size:12px;pointer-events:none}.AxisLabel[data-axis=x]{color:rgb(255,153,0)}.AxisLabel[data-axis=y]{color:rgb(153,255,0)}.AxisLabel[data-axis=z]{color:rgb(0,153,255)}

View file

@ -0,0 +1 @@
html{box-sizing:border-box;margin:0;padding:0;background:black}*,:after,:before{box-sizing:inherit}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-size:100%}body{margin:0;padding:0}main{width:100vw;height:100vh}#canvasContainer{position:absolute;top:0;left:0;right:0;bottom:0;z-index:0}#controls{display:flex;align-items:center;gap:20px;position:fixed;top:0;left:0;background:rgba(0,0,0,.5);color:#fff;padding:8px 12px 8px 8px;border-radius:0 0 4px 0;font-size:13px;z-index:1}.CheckboxField,.Field{display:flex;align-items:center;gap:6px}#fovInput,#speedInput{max-width:80px}.StaticShapeLabel{background:rgba(0,0,0,.5);color:#fff;font-size:11px;white-space:nowrap;padding:1px 3px;border-radius:1px}.StatsPanel{left:auto!important;right:0}.AxisLabel{font-size:12px;pointer-events:none}.AxisLabel[data-axis=x]{color:rgb(255,153,0)}.AxisLabel[data-axis=y]{color:rgb(153,255,0)}.AxisLabel[data-axis=z]{color:rgb(0,153,255)}.MissionSelect-inputWrapper{position:relative;display:flex;align-items:center}.MissionSelect-shortcut{position:absolute;right:7px;font-family:system-ui,sans-serif;font-size:11px;padding:1px 4px;border-radius:3px;background:rgba(255,255,255,.15);color:rgba(255,255,255,.6);pointer-events:none}.MissionSelect-input[aria-expanded=true]~.MissionSelect-shortcut{display:none}.MissionSelect-input{width:240px;padding:6px 36px 6px 8px;font-size:14px;border:1px solid rgba(255,255,255,.3);border-radius:3px;background:rgba(0,0,0,.6);color:#fff;outline:none}.MissionSelect-input[aria-expanded=true]{padding-right:8px}.MissionSelect-input:focus{border-color:rgba(255,255,255,.6)}.MissionSelect-input::placeholder{color:#fff;font-weight:600}.MissionSelect-popover{z-index:100;min-width:320px;max-height:var(--popover-available-height,90vh);overflow-y:auto;overscroll-behavior:contain;background:rgba(20,20,20,.95);border:1px solid rgba(255,255,255,.5);border-radius:3px;box-shadow:0 8px 24px rgba(0,0,0,.6)}.MissionSelect-list{padding:4px 0}.MissionSelect-list:has(>.MissionSelect-group:first-child){padding-top:0}.MissionSelect-group{padding-bottom:4px}.MissionSelect-groupLabel{position:-webkit-sticky;position:sticky;top:0;padding:6px 8px 6px 12px;font-size:13px;font-weight:600;color:rgb(198,202,202);background:rgba(58,69,72,.95);z-index:1}.MissionSelect-group:not(:last-child),.MissionSelect-groupLabel{border-bottom:1px solid rgba(255,255,255,.3)}.MissionSelect-item{display:flex;flex-direction:column;gap:1px;margin:4px 4px 0;padding:6px 8px;border-radius:4px;cursor:pointer;outline:none;scroll-margin-top:32px}.MissionSelect-list>.MissionSelect-item:first-child{margin-top:0}.MissionSelect-item[data-active-item]{background:rgba(255,255,255,.15)}.MissionSelect-item[aria-selected=true]{background:rgba(100,150,255,.3)}.MissionSelect-itemHeader{display:flex;align-items:center;gap:6px}.MissionSelect-itemName{font-size:14px;font-weight:600;color:#fff}.MissionSelect-itemTypes{display:flex;gap:3px}.MissionSelect-itemType{font-size:10px;font-weight:600;padding:2px 5px;border-radius:3px;background:rgba(255,157,0,.4);color:#fff}.MissionSelect-itemMissionName{font-size:12px;color:rgba(255,255,255,.5)}.MissionSelect-noResults{padding:12px 8px;font-size:13px;color:rgba(255,255,255,.5);text-align:center}

File diff suppressed because one or more lines are too long

View file

@ -2,15 +2,15 @@
2:I[9766,[],""]
3:I[8924,[],""]
4:I[1959,[],"ClientPageRoot"]
5:I[8517,["367","static/chunks/b536a0f1-05ee2c75df4a3b9d.js","831","static/chunks/bd904a5c-3aea2adebde6f067.js","664","static/chunks/a3cd4a83-5c5b758da206345b.js","794","static/chunks/f6211eb1-4f3105d2434536dc.js","413","static/chunks/1329d575-16915d95397758f8.js","504","static/chunks/504-86077f19f8e7280c.js","974","static/chunks/app/page-c6fae8e66dd2a27e.js"],"default"]
5:I[8283,["367","static/chunks/b536a0f1-05ee2c75df4a3b9d.js","831","static/chunks/bd904a5c-3aea2adebde6f067.js","664","static/chunks/a3cd4a83-5c5b758da206345b.js","794","static/chunks/f6211eb1-4f3105d2434536dc.js","413","static/chunks/1329d575-16915d95397758f8.js","627","static/chunks/627-9fd7bd53939c6ff2.js","974","static/chunks/app/page-fa222674b6748878.js"],"default"]
8:I[4431,[],"OutletBoundary"]
a:I[5278,[],"AsyncMetadataOutlet"]
c:I[4431,[],"ViewportBoundary"]
e:I[4431,[],"MetadataBoundary"]
f:"$Sreact.suspense"
11:I[7150,[],""]
:HL["/t2-mapper/_next/static/css/534ae28a03b00388.css","style"]
0:{"P":null,"b":"K9x0gdhs26x3eBYhFHgIi","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/css/534ae28a03b00388.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",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":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],null,["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[]],"s":false,"S":true}
:HL["/t2-mapper/_next/static/css/9e91738631ff0ad7.css","style"]
0:{"P":null,"b":"mHNhcJMqff39xhZuHO3JA","p":"/t2-mapper","c":["",""],"i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/t2-mapper/_next/static/css/9e91738631ff0ad7.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",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":["__PAGE__",["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","searchParams":{},"params":{},"promises":["$@6","$@7"]}],null,["$","$L8",null,{"children":["$L9",["$","$La",null,{"promise":"$@b"}]]}]]}],{},null,false]},null,false],["$","$1","h",{"children":[null,[["$","$Lc",null,{"children":"$Ld"}],null],["$","$Le",null,{"children":["$","div",null,{"hidden":true,"children":["$","$f",null,{"fallback":null,"children":"$L10"}]}]}]]}],false]],"m":"$undefined","G":["$11",[]],"s":false,"S":true}
6:{}
7:"$0:f:0:1:2:children:1:props:children:0:props:params"
d:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]

81
package-lock.json generated
View file

@ -9,12 +9,14 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@ariakit/react": "^0.4.20",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.3.0",
"@react-three/postprocessing": "^3.0.4",
"@tanstack/react-query": "^5.90.8",
"ignore": "^7.0.5",
"lodash.orderby": "^4.6.0",
"match-sorter": "^8.2.0",
"next": "^15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@ -38,6 +40,44 @@
"vitest": "^4.0.14"
}
},
"node_modules/@ariakit/core": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.17.tgz",
"integrity": "sha512-OmbUcVZgmQw0AvpX5urCAi3KtEuD30DG8W8gpQVzFpCUWUtJ21bmc6a4s2rm2g1oKPIVShy61FGLtKdKLaTG6g==",
"license": "MIT"
},
"node_modules/@ariakit/react": {
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.20.tgz",
"integrity": "sha512-1X44x3co7MInk5SV4lSvRdy8Nwrt56YNBreKPkcZ/LlwdmY2/2r4A26I7Kzhv+VYIxDTavZYrqOlWnix5ojceg==",
"license": "MIT",
"dependencies": {
"@ariakit/react-core": "0.4.20"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ariakit"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@ariakit/react-core": {
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.20.tgz",
"integrity": "sha512-4rfmaKgSIctHRDrA4wt8MLSI4rNA6wyD7XSTShghHrJfDEXujuOoZqvmPjtMr///kWfW9OV9USOOn8ie/7H/nw==",
"license": "MIT",
"dependencies": {
"@ariakit/core": "0.4.17",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@ -505,6 +545,31 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
@ -3089,6 +3154,16 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/match-sorter": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.2.0.tgz",
"integrity": "sha512-qRVB7wYMJXizAWR4TKo5UYwgW7oAVzA8V9jve0wGzRvV91ou9dcqL+/2gJtD0PZ/Pm2Fq6cVT4VHXHmDFVMGRA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -3657,6 +3732,12 @@
"util-deprecate": "~1.0.1"
}
},
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==",
"license": "MIT"
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",

View file

@ -22,12 +22,14 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ariakit/react": "^0.4.20",
"@react-three/drei": "^10.7.6",
"@react-three/fiber": "^9.3.0",
"@react-three/postprocessing": "^3.0.4",
"@tanstack/react-query": "^5.90.8",
"ignore": "^7.0.5",
"lodash.orderby": "^4.6.0",
"match-sorter": "^8.2.0",
"next": "^15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",

View file

@ -1,86 +1,5 @@
import { Fragment, useMemo } from "react";
import { getMissionInfo, getMissionList, getSourceAndPath } from "../manifest";
import { useControls, useDebug, useSettings } from "./SettingsProvider";
import orderBy from "lodash.orderby";
const excludeMissions = new Set([
"SkiFree",
"SkiFree_Daily",
"SkiFree_Randomizer",
]);
const sourceGroupNames = {
"missions.vl2": "Official",
"TR2final105-client.vl2": "Team Rabbit 2",
"z_mappacks/CTF/Classic_maps_v1.vl2": "Classic",
"z_mappacks/CTF/DynamixFinalPack.vl2": "Official",
"z_mappacks/CTF/KryMapPack_b3EDIT.vl2": "KryMapPack",
"z_mappacks/CTF/S5maps.vl2": "S5",
"z_mappacks/CTF/S8maps.vl2": "S8",
"z_mappacks/CTF/TWL-MapPack.vl2": "TWL",
"z_mappacks/CTF/TWL-MapPackEDIT.vl2": "TWL",
"z_mappacks/CTF/TWL2-MapPack.vl2": "TWL2",
"z_mappacks/CTF/TWL2-MapPackEDIT.vl2": "TWL2",
"z_mappacks/TWL_T2arenaOfficialMaps.vl2": "Arena",
"z_mappacks/z_DMP2-V0.6.vl2": "DMP2 (Discord Map Pack)",
"z_mappacks/zDMP-4.7.3DX.vl2": "DMP (Discord Map Pack)",
// "SkiFreeGameType.vl2": "SkiFree",
};
const dirGroupNames = {
"z_mappacks/DM": "DM",
"z_mappacks/LCTF": "LCTF",
"z_mappacks/Lak": "LakRabbit",
};
const getDirName = (sourcePath: string) => {
const match = sourcePath.match(/^(.*)(\/[^/]+)$/);
return match ? match[1] : "";
};
const groupedMissions = getMissionList().reduce(
(groupMap, missionName) => {
const missionInfo = getMissionInfo(missionName);
const [sourcePath] = getSourceAndPath(missionInfo.resourcePath);
const sourceDir = getDirName(sourcePath);
const groupName =
sourceGroupNames[sourcePath] ?? dirGroupNames[sourceDir] ?? null;
const groupMissions = groupMap.get(groupName) ?? [];
if (!excludeMissions.has(missionName)) {
groupMissions.push({
resourcePath: missionInfo.resourcePath,
missionName,
displayName: missionInfo.displayName,
sourcePath,
});
groupMap.set(groupName, groupMissions);
}
return groupMap;
},
new Map<
string | null,
Array<{
resourcePath: string;
missionName: string;
displayName: string;
sourcePath: string;
}>
>(),
);
groupedMissions.forEach((groupMissions, groupName) => {
groupedMissions.set(
groupName,
orderBy(
groupMissions,
[
(missionInfo) =>
(missionInfo.displayName || missionInfo.missionName).toLowerCase(),
],
["asc"],
),
);
});
import { MissionSelect } from "./MissionSelect";
export function InspectorControls({
missionName,
@ -102,19 +21,6 @@ export function InspectorControls({
const { speedMultiplier, setSpeedMultiplier } = useControls();
const { debugMode, setDebugMode } = useDebug();
const groupedMissionOptions = useMemo(() => {
const groups = orderBy(
Array.from(groupedMissions.entries()),
[
([groupName]) =>
groupName === "Official" ? 0 : groupName == null ? 2 : 1,
([groupName]) => (groupName ? groupName.toLowerCase() : ""),
],
["asc", "asc"],
);
return groups;
}, []);
return (
<div
id="controls"
@ -122,32 +28,7 @@ export function InspectorControls({
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<select
id="missionList"
value={missionName}
onChange={(event) => onChangeMission(event.target.value)}
>
{groupedMissionOptions.map(([groupName, groupMissions]) =>
groupName ? (
<optgroup key={groupName} label={groupName}>
{groupMissions.map((mission) => (
<option key={mission.missionName} value={mission.missionName}>
{mission.displayName || mission.missionName}
</option>
))}
</optgroup>
) : (
<Fragment key="null">
<hr />
{groupMissions.map((mission) => (
<option key={mission.missionName} value={mission.missionName}>
{mission.displayName || mission.missionName}
</option>
))}
</Fragment>
),
)}
</select>
<MissionSelect value={missionName} onChange={onChangeMission} />
<div className="CheckboxField">
<input
id="fogInput"

View file

@ -0,0 +1,271 @@
import {
Fragment,
startTransition,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Combobox,
ComboboxItem,
ComboboxList,
ComboboxPopover,
ComboboxProvider,
ComboboxGroup,
ComboboxGroupLabel,
useComboboxStore,
} from "@ariakit/react";
import { matchSorter } from "match-sorter";
import { getMissionInfo, getMissionList, getSourceAndPath } from "../manifest";
import orderBy from "lodash.orderby";
const excludeMissions = new Set([
"SkiFree",
"SkiFree_Daily",
"SkiFree_Randomizer",
]);
const sourceGroupNames: Record<string, string> = {
"missions.vl2": "Official",
"TR2final105-client.vl2": "Team Rabbit 2",
"z_mappacks/CTF/Classic_maps_v1.vl2": "Classic",
"z_mappacks/CTF/DynamixFinalPack.vl2": "Official",
"z_mappacks/CTF/KryMapPack_b3EDIT.vl2": "KryMapPack",
"z_mappacks/CTF/S5maps.vl2": "S5",
"z_mappacks/CTF/S8maps.vl2": "S8",
"z_mappacks/CTF/TWL-MapPack.vl2": "TWL",
"z_mappacks/CTF/TWL-MapPackEDIT.vl2": "TWL",
"z_mappacks/CTF/TWL2-MapPack.vl2": "TWL2",
"z_mappacks/CTF/TWL2-MapPackEDIT.vl2": "TWL2",
"z_mappacks/TWL_T2arenaOfficialMaps.vl2": "Arena",
"z_mappacks/z_DMP2-V0.6.vl2": "DMP2 (Discord Map Pack)",
"z_mappacks/zDMP-4.7.3DX.vl2": "DMP (Discord Map Pack)",
};
const dirGroupNames: Record<string, string> = {
"z_mappacks/DM": "DM",
"z_mappacks/LCTF": "LCTF",
"z_mappacks/Lak": "LakRabbit",
};
interface MissionItem {
resourcePath: string;
missionName: string;
displayName: string;
sourcePath: string;
groupName: string | null;
missionTypes: string[];
}
const getDirName = (sourcePath: string) => {
const match = sourcePath.match(/^(.*)(\/[^/]+)$/);
return match ? match[1] : "";
};
const allMissions: MissionItem[] = getMissionList()
.filter((name) => !excludeMissions.has(name))
.map((missionName) => {
const missionInfo = getMissionInfo(missionName);
const [sourcePath] = getSourceAndPath(missionInfo.resourcePath);
const sourceDir = getDirName(sourcePath);
const groupName =
sourceGroupNames[sourcePath] ?? dirGroupNames[sourceDir] ?? null;
return {
resourcePath: missionInfo.resourcePath,
missionName,
displayName: missionInfo.displayName,
sourcePath,
groupName,
missionTypes: missionInfo.missionTypes,
};
});
const missionsByName = new Map(allMissions.map((m) => [m.missionName, m]));
function groupMissions(missions: MissionItem[]) {
const groupMap = new Map<string | null, MissionItem[]>();
for (const mission of missions) {
const group = groupMap.get(mission.groupName) ?? [];
group.push(mission);
groupMap.set(mission.groupName, group);
}
groupMap.forEach((groupMissions, groupName) => {
groupMap.set(
groupName,
orderBy(
groupMissions,
[(m) => (m.displayName || m.missionName).toLowerCase()],
["asc"],
),
);
});
return orderBy(
Array.from(groupMap.entries()),
[
([groupName]) =>
groupName === "Official" ? 0 : groupName == null ? 2 : 1,
([groupName]) => (groupName ? groupName.toLowerCase() : ""),
],
["asc", "asc"],
);
}
const defaultGroups = groupMissions(allMissions);
const isMac =
typeof navigator !== "undefined" &&
/Mac|iPhone|iPad|iPod/.test(navigator.platform);
function MissionItemContent({ mission }: { mission: MissionItem }) {
return (
<>
<span className="MissionSelect-itemHeader">
<span className="MissionSelect-itemName">
{mission.displayName || mission.missionName}
</span>
{mission.missionTypes.length > 0 && (
<span className="MissionSelect-itemTypes">
{mission.missionTypes.map((type) => (
<span key={type} className="MissionSelect-itemType">
{type}
</span>
))}
</span>
)}
</span>
<span className="MissionSelect-itemMissionName">
{mission.missionName}
</span>
</>
);
}
export function MissionSelect({
value,
onChange,
}: {
value: string;
onChange: (missionName: string) => void;
}) {
const [searchValue, setSearchValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const combobox = useComboboxStore({
resetValueOnHide: true,
selectedValue: value,
setSelectedValue: (newValue) => {
if (newValue) onChange(newValue);
},
setValue: (value) => {
startTransition(() => setSearchValue(value));
},
});
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
inputRef.current?.focus();
combobox.show();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [combobox]);
const selectedMission = missionsByName.get(value);
// When searching, return flat list sorted by relevance; otherwise return grouped
const filteredResults = useMemo(() => {
if (!searchValue)
return { type: "grouped" as const, groups: defaultGroups };
const matches = matchSorter(allMissions, searchValue, {
keys: ["displayName", "missionName"],
});
return { type: "flat" as const, missions: matches };
}, [searchValue]);
const displayValue = selectedMission
? selectedMission.displayName || selectedMission.missionName
: value;
const noResults =
filteredResults.type === "flat"
? filteredResults.missions.length === 0
: filteredResults.groups.length === 0;
return (
<ComboboxProvider store={combobox}>
<div className="MissionSelect-inputWrapper">
<Combobox
ref={inputRef}
autoSelect
placeholder={displayValue}
className="MissionSelect-input"
onFocus={() => {
document.exitPointerLock();
combobox.show();
}}
/>
<kbd className="MissionSelect-shortcut">{isMac ? "⌘K" : "^K"}</kbd>
</div>
<ComboboxPopover gutter={4} fitViewport className="MissionSelect-popover">
<ComboboxList className="MissionSelect-list">
{filteredResults.type === "flat"
? filteredResults.missions.map((mission) => (
<ComboboxItem
key={mission.missionName}
value={mission.missionName}
className="MissionSelect-item"
focusOnHover
>
<MissionItemContent mission={mission} />
</ComboboxItem>
))
: filteredResults.groups.map(([groupName, missions]) =>
groupName ? (
<ComboboxGroup
key={groupName}
className="MissionSelect-group"
>
<ComboboxGroupLabel className="MissionSelect-groupLabel">
{groupName}
</ComboboxGroupLabel>
{missions.map((mission) => (
<ComboboxItem
key={mission.missionName}
value={mission.missionName}
className="MissionSelect-item"
focusOnHover
>
<MissionItemContent mission={mission} />
</ComboboxItem>
))}
</ComboboxGroup>
) : (
<Fragment key="ungrouped">
{missions.map((mission) => (
<ComboboxItem
key={mission.missionName}
value={mission.missionName}
className="MissionSelect-item"
focusOnHover
>
<MissionItemContent mission={mission} />
</ComboboxItem>
))}
</Fragment>
),
)}
{noResults && (
<div className="MissionSelect-noResults">No missions found</div>
)}
</ComboboxList>
</ComboboxPopover>
</ComboboxProvider>
);
}

View file

@ -183,6 +183,10 @@ export function ObserverControls() {
// Don't let KeyboardControls handle stuff when metaKey is held.
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
// Let Cmd/Ctrl+K pass through for search focus.
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
return;
}
if (e.metaKey) {
e.stopImmediatePropagation();
}