@marble.fun/track-catalog 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/assets.json +346 -0
- package/package.json +32 -0
- package/release.json +135 -0
- package/src/audio/game-audio-preference.ts +9 -0
- package/src/authoritative.ts +6 -0
- package/src/catalog.ts +1 -0
- package/src/index.ts +14 -0
- package/src/legacy-stock-catalog.ts +130 -0
- package/src/pages/lobby/lobby-types.ts +140 -0
- package/src/pages/lobby/marble-factory.ts +931 -0
- package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
- package/src/pages/play/race/egypt-portal-frame.ts +382 -0
- package/src/pages/play/race/finish-detection.ts +1 -0
- package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
- package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
- package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
- package/src/pages/play/race/jungle-portal-frame.ts +308 -0
- package/src/pages/play/race/jungle-temple-lift.ts +366 -0
- package/src/pages/play/race/marble-entity.ts +257 -0
- package/src/pages/play/race/marble-material.ts +20 -0
- package/src/pages/play/race/marble-race-engine.ts +2652 -0
- package/src/pages/play/race/path-pattern-layout.ts +142 -0
- package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
- package/src/pages/play/race/physics-debug.ts +1 -0
- package/src/pages/play/race/physics-dry-run.ts +90 -0
- package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
- package/src/pages/play/race/portal-pair-color.ts +20 -0
- package/src/pages/play/race/portal-proportions.ts +25 -0
- package/src/pages/play/race/portal-visual.ts +1108 -0
- package/src/pages/play/race/race-camera-director.ts +586 -0
- package/src/pages/play/race/race-camera-obstruction.ts +37 -0
- package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
- package/src/pages/play/race/race-camera.ts +206 -0
- package/src/pages/play/race/race-completion.ts +5 -0
- package/src/pages/play/race/race-configuration.ts +57 -0
- package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
- package/src/pages/play/race/race-physics-profile.ts +7 -0
- package/src/pages/play/race/race-physics.ts +16 -0
- package/src/pages/play/race/race-playback.ts +41 -0
- package/src/pages/play/race/race-rolling-audio.ts +616 -0
- package/src/pages/play/race/race-simulation-renderer.ts +221 -0
- package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
- package/src/pages/play/race/race-transform-interpolation.ts +50 -0
- package/src/pages/play/race/race-types.ts +12 -0
- package/src/pages/play/race/rebound-visual.ts +70 -0
- package/src/pages/play/race/seeded-random.ts +23 -0
- package/src/pages/play/race/space-station-portal-frame.ts +275 -0
- package/src/pages/play/race/track-appearance-canopy.ts +946 -0
- package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
- package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
- package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
- package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
- package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
- package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
- package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
- package/src/pages/play/race/track-appearance-support.ts +983 -0
- package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
- package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
- package/src/pages/play/race/track-course.ts +271 -0
- package/src/pages/play/race/track-definition.ts +443 -0
- package/src/pages/play/race/track-fence-architecture.ts +33 -0
- package/src/pages/play/race/track-floor-material-rules.ts +51 -0
- package/src/pages/play/race/track-mechanisms.ts +1350 -0
- package/src/pages/play/race/track-physics-surface.ts +6 -0
- package/src/pages/play/race/track-route-network.ts +9 -0
- package/src/pages/play/race/track-shadow-batching.ts +760 -0
- package/src/pages/play/race/track-surface-basis.ts +1 -0
- package/src/pages/play/race/track-surface-topology.ts +326 -0
- package/src/pages/play/race/track-world.ts +10181 -0
- package/src/pages/play/race/under-track-support-architecture.ts +122 -0
- package/src/pages/play/race/vortex-profile.ts +5 -0
- package/src/pages/play/race/vortex-water-effect.ts +863 -0
- package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
- package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
- package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
- package/src/pages/track-lab/cone-field-layout.ts +1 -0
- package/src/pages/track-lab/conveyor-layout.ts +1 -0
- package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
- package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
- package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
- package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
- package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
- package/src/pages/track-lab/flipper-component.ts +127 -0
- package/src/pages/track-lab/rising-wall-layout.ts +1 -0
- package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
- package/src/pages/track-lab/track-piece-layout.ts +28 -0
- package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
- package/src/three/geometry-attribute-aliases.ts +24 -0
- package/src/three/geometry-detail.ts +36 -0
- package/src/three/homepage-marble-geometry.ts +15 -0
- package/src/three/japanese-sakura-appearance.ts +252 -0
- package/src/three/jungle-flower-hedge-appearance.ts +7 -0
- package/src/three/jungle-temple-appearance.ts +5 -0
- package/src/three/low-quality-scene-renderer.ts +132 -0
- package/src/three/marble-sphere-geometry.ts +13 -0
- package/src/three/mobile-rendering-budget.ts +11 -0
- package/src/three/painted-valley-appearance.ts +270 -0
- package/src/three/railroad-appearance.ts +71 -0
- package/src/three/renderer-resize.ts +32 -0
- package/src/three/rope-wall-appearance.ts +91 -0
- package/src/three/rugged-track-appearance.ts +2 -0
- package/src/three/scene-lighting.ts +183 -0
- package/src/three/scene-shader-preparation.ts +64 -0
- package/src/three/scene-theme-controller.ts +461 -0
- package/src/three/shadow-shader-preparation.ts +162 -0
- package/src/three/shared-obstacle-metal.ts +44 -0
- package/src/three/soft-additive-particles.ts +57 -0
- package/src/three/space-station-appearance.ts +213 -0
- package/src/three/static-track-batched-mesh.ts +132 -0
- package/src/three/stone-track-shell-appearance.ts +12 -0
- package/src/three/theme-ambience-controller.ts +226 -0
- package/src/three/theme-asset-cache.ts +120 -0
- package/src/three/theme-audio-preference.ts +43 -0
- package/src/three/theme-catalog.ts +1457 -0
- package/src/three/theme-material-assets.ts +258 -0
- package/src/three/theme-material-ktx2-manifest.ts +190 -0
- package/src/three/theme-material-registry.ts +1952 -0
- package/src/three/theme-world-factory.ts +590 -0
- package/src/three/track-connector-batch.ts +125 -0
- package/src/three/track-material-continuity.ts +442 -0
- package/src/three/track-theme.ts +535 -0
- package/src/three/vampire-castle-appearance.ts +258 -0
- package/src/three/visual-frame-pacing.ts +19 -0
- package/src/three/visual-geometry.ts +120 -0
- package/src/three/visual-quality.ts +83 -0
- package/src/three/water-appearance.ts +29 -0
- package/src/three/wild-west-sand-appearance.ts +2 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createThemeMaterialSet, type MaterialRecipe, type TrackThemeDefinition } from './track-theme'
|
|
2
|
+
import { createSharedObstacleMetalMaterials } from './shared-obstacle-metal'
|
|
3
|
+
|
|
4
|
+
export const VAMPIRE_CASTLE_THEME_ID = 'vampire-castle'
|
|
5
|
+
export const VAMPIRE_COBBLESTONE_FLOOR_ID = 'vampire-castle.cobblestone-floor'
|
|
6
|
+
export const VAMPIRE_DARK_OAK_FLOOR_ID = 'vampire-castle.dark-oak-floor'
|
|
7
|
+
export const VAMPIRE_CHECKER_MARBLE_FLOOR_ID = 'vampire-castle.checker-marble-floor'
|
|
8
|
+
export const VAMPIRE_CRIMSON_CARPET_ID = 'vampire-castle.crimson-carpet'
|
|
9
|
+
export const VAMPIRE_DARK_STONE_WALL_ID = 'vampire-castle.dark-stone-wall'
|
|
10
|
+
export const VAMPIRE_CARVED_OAK_WALL_ID = 'vampire-castle.carved-oak-wall'
|
|
11
|
+
|
|
12
|
+
const textureRoot = '/themes/textures/vampire-castle'
|
|
13
|
+
|
|
14
|
+
function pbrRecipe(
|
|
15
|
+
stem: string,
|
|
16
|
+
options: {
|
|
17
|
+
kind?: 'standard' | 'physical'
|
|
18
|
+
roughness: number
|
|
19
|
+
envMapIntensity: number
|
|
20
|
+
textureWorldSize: readonly [number, number]
|
|
21
|
+
bumpScale: number
|
|
22
|
+
normalScale: readonly [number, number]
|
|
23
|
+
aoMapIntensity: number
|
|
24
|
+
clearcoat?: number
|
|
25
|
+
clearcoatRoughness?: number
|
|
26
|
+
version?: number
|
|
27
|
+
},
|
|
28
|
+
): MaterialRecipe {
|
|
29
|
+
const version = options.version ?? 1
|
|
30
|
+
const shared = {
|
|
31
|
+
color: '#ffffff',
|
|
32
|
+
roughness: options.roughness,
|
|
33
|
+
metalness: 0,
|
|
34
|
+
envMapIntensity: options.envMapIntensity,
|
|
35
|
+
textureUrl: `${textureRoot}-${stem}-v${version}-albedo.webp`,
|
|
36
|
+
normalTextureUrl: `${textureRoot}-${stem}-v${version}-normal.png`,
|
|
37
|
+
heightTextureUrl: `${textureRoot}-${stem}-v${version}-height.png`,
|
|
38
|
+
roughnessTextureUrl: `${textureRoot}-${stem}-v${version}-roughness.png`,
|
|
39
|
+
aoTextureUrl: `${textureRoot}-${stem}-v${version}-ao.png`,
|
|
40
|
+
textureWorldSize: options.textureWorldSize,
|
|
41
|
+
bumpScale: options.bumpScale,
|
|
42
|
+
normalScale: options.normalScale,
|
|
43
|
+
aoMapIntensity: options.aoMapIntensity,
|
|
44
|
+
}
|
|
45
|
+
return options.kind === 'physical'
|
|
46
|
+
? {
|
|
47
|
+
kind: 'physical',
|
|
48
|
+
...shared,
|
|
49
|
+
clearcoat: options.clearcoat,
|
|
50
|
+
clearcoatRoughness: options.clearcoatRoughness,
|
|
51
|
+
}
|
|
52
|
+
: { kind: 'standard', ...shared }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const VAMPIRE_COBBLESTONE_FLOOR_MATERIAL = pbrRecipe('cobblestone-floor', {
|
|
56
|
+
roughness: 0.84,
|
|
57
|
+
envMapIntensity: 0.5,
|
|
58
|
+
textureWorldSize: [3, 3],
|
|
59
|
+
bumpScale: 0.055,
|
|
60
|
+
normalScale: [0.72, 0.72],
|
|
61
|
+
aoMapIntensity: 0.62,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const VAMPIRE_DARK_OAK_FLOOR_MATERIAL = pbrRecipe('dark-oak-floor', {
|
|
65
|
+
kind: 'physical',
|
|
66
|
+
roughness: 0.68,
|
|
67
|
+
envMapIntensity: 0.55,
|
|
68
|
+
textureWorldSize: [4, 5],
|
|
69
|
+
bumpScale: 0.026,
|
|
70
|
+
normalScale: [0.48, 0.48],
|
|
71
|
+
aoMapIntensity: 0.42,
|
|
72
|
+
clearcoat: 0.22,
|
|
73
|
+
clearcoatRoughness: 0.5,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const VAMPIRE_CHECKER_MARBLE_FLOOR_MATERIAL = pbrRecipe('checker-marble-floor', {
|
|
77
|
+
kind: 'physical',
|
|
78
|
+
roughness: 0.24,
|
|
79
|
+
envMapIntensity: 0.96,
|
|
80
|
+
textureWorldSize: [2.88, 2.88],
|
|
81
|
+
bumpScale: 0.008,
|
|
82
|
+
normalScale: [0.18, 0.18],
|
|
83
|
+
aoMapIntensity: 0.24,
|
|
84
|
+
clearcoat: 0.88,
|
|
85
|
+
clearcoatRoughness: 0.13,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export const VAMPIRE_CRIMSON_CARPET_MATERIAL = pbrRecipe('crimson-carpet', {
|
|
89
|
+
roughness: 0.94,
|
|
90
|
+
envMapIntensity: 0.24,
|
|
91
|
+
textureWorldSize: [1.5, 1.5],
|
|
92
|
+
bumpScale: 0.014,
|
|
93
|
+
normalScale: [0.32, 0.32],
|
|
94
|
+
aoMapIntensity: 0.28,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export const VAMPIRE_DARK_STONE_WALL_MATERIAL = pbrRecipe('dark-stone-wall', {
|
|
98
|
+
roughness: 0.87,
|
|
99
|
+
envMapIntensity: 0.46,
|
|
100
|
+
textureWorldSize: [1.8, 1.8],
|
|
101
|
+
bumpScale: 0.055,
|
|
102
|
+
normalScale: [0.7, 0.7],
|
|
103
|
+
aoMapIntensity: 0.58,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export const VAMPIRE_CARVED_OAK_WALL_MATERIAL = pbrRecipe('plain-oak-wall', {
|
|
107
|
+
kind: 'physical',
|
|
108
|
+
version: 2,
|
|
109
|
+
roughness: 0.72,
|
|
110
|
+
envMapIntensity: 0.52,
|
|
111
|
+
textureWorldSize: [3.4, 1.28],
|
|
112
|
+
bumpScale: 0.014,
|
|
113
|
+
normalScale: [0.38, 0.38],
|
|
114
|
+
aoMapIntensity: 0.34,
|
|
115
|
+
clearcoat: 0.18,
|
|
116
|
+
clearcoatRoughness: 0.58,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export function isVampireCastleCarvedOakWallAppearance(id: string | undefined): boolean {
|
|
120
|
+
return id === VAMPIRE_CARVED_OAK_WALL_ID
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Approved Track Lab theme. Lobby/race rotation remains a separate promotion. */
|
|
124
|
+
export const VAMPIRE_CASTLE_THEME: TrackThemeDefinition = {
|
|
125
|
+
id: VAMPIRE_CASTLE_THEME_ID,
|
|
126
|
+
version: 1,
|
|
127
|
+
label: 'Vampire Castle',
|
|
128
|
+
shortLabel: 'Vampire',
|
|
129
|
+
description: 'Moonlit Gothic castle grounds, carved oak halls, dark stone, and crimson carpet.',
|
|
130
|
+
thumbnailUrl: '/themes/previews/vampire-castle-thumbnail.webp',
|
|
131
|
+
pickerPreviewUrl: '/themes/previews/vampire-castle-picker.webp',
|
|
132
|
+
loadingPreviewUrl: '/themes/previews/vampire-castle-loading.webp',
|
|
133
|
+
previewUrl: '/images/social/og/vampire-castle.webp',
|
|
134
|
+
panoramaUrl: '/themes/panoramas/vampire-castle.ktx2',
|
|
135
|
+
materials: {
|
|
136
|
+
...createThemeMaterialSet(
|
|
137
|
+
{
|
|
138
|
+
laneSurface: '#323941',
|
|
139
|
+
laneEdge: '#2b1518',
|
|
140
|
+
laneGuard: '#ffffff',
|
|
141
|
+
structure: '#241614',
|
|
142
|
+
structureSecondary: '#202932',
|
|
143
|
+
mechanism: '#2d333b',
|
|
144
|
+
moving: '#761c28',
|
|
145
|
+
route: '#a32b3b',
|
|
146
|
+
hazard: '#c83d46',
|
|
147
|
+
finish: '#c9d6e4',
|
|
148
|
+
energy: '#b9d7ff',
|
|
149
|
+
portal: '#7b1f35',
|
|
150
|
+
glass: '#b7cce2',
|
|
151
|
+
worldGround: '#15191f',
|
|
152
|
+
worldPrimary: '#232a32',
|
|
153
|
+
worldSecondary: '#47242a',
|
|
154
|
+
foliage: '#22382f',
|
|
155
|
+
accent: '#8e2434',
|
|
156
|
+
dark: '#07090d',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
trackRoughness: 0.84,
|
|
160
|
+
trackMetalness: 0,
|
|
161
|
+
guardMetalness: 0,
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
'lane.surface': VAMPIRE_COBBLESTONE_FLOOR_MATERIAL,
|
|
165
|
+
'lane.guard': VAMPIRE_DARK_STONE_WALL_MATERIAL,
|
|
166
|
+
'lane.guard.backing': VAMPIRE_DARK_STONE_WALL_MATERIAL,
|
|
167
|
+
'structure.primary': VAMPIRE_CARVED_OAK_WALL_MATERIAL,
|
|
168
|
+
'structure.secondary': VAMPIRE_DARK_STONE_WALL_MATERIAL,
|
|
169
|
+
...createSharedObstacleMetalMaterials({ light: '#a53845', dark: '#6f202b' }),
|
|
170
|
+
},
|
|
171
|
+
appearancePresets: {
|
|
172
|
+
floor: [
|
|
173
|
+
{
|
|
174
|
+
id: VAMPIRE_COBBLESTONE_FLOOR_ID,
|
|
175
|
+
label: 'Cobblestone',
|
|
176
|
+
default: true,
|
|
177
|
+
material: VAMPIRE_COBBLESTONE_FLOOR_MATERIAL,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: VAMPIRE_DARK_OAK_FLOOR_ID,
|
|
181
|
+
label: 'Oak Boards',
|
|
182
|
+
material: VAMPIRE_DARK_OAK_FLOOR_MATERIAL,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: VAMPIRE_CHECKER_MARBLE_FLOOR_ID,
|
|
186
|
+
label: 'Checker Marble',
|
|
187
|
+
material: VAMPIRE_CHECKER_MARBLE_FLOOR_MATERIAL,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
rail: [
|
|
191
|
+
{
|
|
192
|
+
id: VAMPIRE_CRIMSON_CARPET_ID,
|
|
193
|
+
label: 'Crimson Carpet',
|
|
194
|
+
detailStyle: 'centered-floor-runner',
|
|
195
|
+
material: VAMPIRE_CRIMSON_CARPET_MATERIAL,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
wall: [
|
|
199
|
+
{
|
|
200
|
+
id: VAMPIRE_DARK_STONE_WALL_ID,
|
|
201
|
+
label: 'Castle Masonry',
|
|
202
|
+
default: true,
|
|
203
|
+
detailStyle: 'shared-stone-block-shell',
|
|
204
|
+
material: VAMPIRE_DARK_STONE_WALL_MATERIAL,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: VAMPIRE_CARVED_OAK_WALL_ID,
|
|
208
|
+
label: 'Carved Oak',
|
|
209
|
+
material: VAMPIRE_CARVED_OAK_WALL_MATERIAL,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
detailMaterials: {
|
|
214
|
+
[VAMPIRE_DARK_STONE_WALL_ID]: VAMPIRE_DARK_STONE_WALL_MATERIAL,
|
|
215
|
+
[VAMPIRE_CARVED_OAK_WALL_ID]: VAMPIRE_CARVED_OAK_WALL_MATERIAL,
|
|
216
|
+
},
|
|
217
|
+
environment: {
|
|
218
|
+
background: '#070a10',
|
|
219
|
+
fog: '#111a25',
|
|
220
|
+
fogNear: 48,
|
|
221
|
+
fogFar: 205,
|
|
222
|
+
environmentIntensity: 1.08,
|
|
223
|
+
},
|
|
224
|
+
lighting: {
|
|
225
|
+
skyColor: '#b9d2ee',
|
|
226
|
+
groundColor: '#090b10',
|
|
227
|
+
ambientIntensity: 2.32,
|
|
228
|
+
keyColor: '#d7e8ff',
|
|
229
|
+
keyIntensity: 4.65,
|
|
230
|
+
keyPosition: [-28, 38, 18],
|
|
231
|
+
rimColor: '#6d1727',
|
|
232
|
+
rimIntensity: 1.9,
|
|
233
|
+
rimPosition: [24, 14, -28],
|
|
234
|
+
exposure: 1.16,
|
|
235
|
+
practical: {
|
|
236
|
+
color: '#d67d68',
|
|
237
|
+
intensity: 12,
|
|
238
|
+
distance: 26,
|
|
239
|
+
decay: 2,
|
|
240
|
+
count: 4,
|
|
241
|
+
heightOffset: 2.1,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
world: { kit: 'ancient-egypt', density: 0 },
|
|
245
|
+
particles: {
|
|
246
|
+
style: 'dust',
|
|
247
|
+
color: '#bdcee0',
|
|
248
|
+
secondaryColor: '#6f1828',
|
|
249
|
+
count: 110,
|
|
250
|
+
size: 0.11,
|
|
251
|
+
speed: 0.18,
|
|
252
|
+
opacity: 0.18,
|
|
253
|
+
},
|
|
254
|
+
ambience: { style: 'studio', volume: 0.035 },
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Kept as a compatibility alias for the standalone material-proof page. */
|
|
258
|
+
export const VAMPIRE_CASTLE_PREVIEW_THEME = VAMPIRE_CASTLE_THEME
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accept browser callbacks just before the 60 Hz deadline. RAF timestamps are
|
|
3
|
+
* rounded and fluctuate around vsync; a strict comparison can drop a complete
|
|
4
|
+
* display frame (or alternate 8/25 ms intervals on a 120 Hz display).
|
|
5
|
+
* Keep deadlines on the original cadence, including after early frames or
|
|
6
|
+
* stalls, rather than moving the target with each callback's jitter.
|
|
7
|
+
*/
|
|
8
|
+
export function nextVisualFrameDeadline(
|
|
9
|
+
timeMs: number,
|
|
10
|
+
deadlineMs: number,
|
|
11
|
+
framesPerSecond: number,
|
|
12
|
+
): number | undefined {
|
|
13
|
+
const intervalMs = 1_000 / framesPerSecond
|
|
14
|
+
const toleranceMs = Math.min(1, intervalMs * 0.1)
|
|
15
|
+
if (deadlineMs === 0) return timeMs + intervalMs
|
|
16
|
+
if (timeMs + toleranceMs < deadlineMs) return undefined
|
|
17
|
+
const elapsedIntervals = Math.max(1, Math.floor((timeMs - deadlineMs + toleranceMs) / intervalMs) + 1)
|
|
18
|
+
return deadlineMs + elapsedIntervals * intervalMs
|
|
19
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { geometryDetailProfile } from './geometry-detail'
|
|
4
|
+
|
|
5
|
+
/** Render tessellation only. Never use these constructors for Rapier inputs. */
|
|
6
|
+
export const VISUAL_TRIANGLE_RATIO = geometryDetailProfile.triangleRatio
|
|
7
|
+
const AXIS_RATIO = Math.sqrt(VISUAL_TRIANGLE_RATIO)
|
|
8
|
+
|
|
9
|
+
function segments(value: number, minimum: number, ratio = AXIS_RATIO): number {
|
|
10
|
+
const profileMinimum = Math.max(Math.min(minimum, 3), Math.ceil(minimum * geometryDetailProfile.minimumScale))
|
|
11
|
+
return Math.min(value, Math.max(profileMinimum, Math.floor(value * ratio)))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class VisualSphereGeometry extends THREE.SphereGeometry {
|
|
15
|
+
constructor(
|
|
16
|
+
radius = 1,
|
|
17
|
+
width = 32,
|
|
18
|
+
height = 16,
|
|
19
|
+
phiStart = 0,
|
|
20
|
+
phiLength = Math.PI * 2,
|
|
21
|
+
thetaStart = 0,
|
|
22
|
+
thetaLength = Math.PI,
|
|
23
|
+
) {
|
|
24
|
+
super(radius, segments(width, 8), segments(height, 4), phiStart, phiLength, thetaStart, thetaLength)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class VisualCylinderGeometry extends THREE.CylinderGeometry {
|
|
29
|
+
constructor(
|
|
30
|
+
top = 1,
|
|
31
|
+
bottom = 1,
|
|
32
|
+
height = 1,
|
|
33
|
+
radial = 32,
|
|
34
|
+
vertical = 1,
|
|
35
|
+
open = false,
|
|
36
|
+
start = 0,
|
|
37
|
+
length = Math.PI * 2,
|
|
38
|
+
) {
|
|
39
|
+
super(
|
|
40
|
+
top,
|
|
41
|
+
bottom,
|
|
42
|
+
height,
|
|
43
|
+
segments(radial, 6, vertical === 1 ? VISUAL_TRIANGLE_RATIO : AXIS_RATIO),
|
|
44
|
+
segments(vertical, 1),
|
|
45
|
+
open,
|
|
46
|
+
start,
|
|
47
|
+
length,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class VisualConeGeometry extends THREE.ConeGeometry {
|
|
53
|
+
constructor(radius = 1, height = 1, radial = 32, vertical = 1, open = false, start = 0, length = Math.PI * 2) {
|
|
54
|
+
super(
|
|
55
|
+
radius,
|
|
56
|
+
height,
|
|
57
|
+
segments(radial, 5, vertical === 1 ? VISUAL_TRIANGLE_RATIO : AXIS_RATIO),
|
|
58
|
+
segments(vertical, 1),
|
|
59
|
+
open,
|
|
60
|
+
start,
|
|
61
|
+
length,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class VisualTorusGeometry extends THREE.TorusGeometry {
|
|
67
|
+
constructor(radius = 1, tube = 0.4, radial = 12, tubular = 48, arc = Math.PI * 2) {
|
|
68
|
+
super(radius, tube, segments(radial, 3), segments(tubular, 8), arc)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class VisualTubeGeometry extends THREE.TubeGeometry {
|
|
73
|
+
constructor(path?: THREE.Curve<THREE.Vector3>, tubular = 64, radius = 1, radial = 8, closed = false) {
|
|
74
|
+
super(path, segments(tubular, 3), radius, segments(radial, 3), closed)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class VisualLatheGeometry extends THREE.LatheGeometry {
|
|
79
|
+
constructor(points?: THREE.Vector2[], radial = 12, start = 0, length = Math.PI * 2) {
|
|
80
|
+
super(points, segments(radial, 8, VISUAL_TRIANGLE_RATIO), start, length)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class VisualCapsuleGeometry extends THREE.CapsuleGeometry {
|
|
85
|
+
constructor(radius = 1, length = 1, caps = 4, radial = 8) {
|
|
86
|
+
super(radius, length, segments(caps, 2), segments(radial, 8))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class VisualCircleGeometry extends THREE.CircleGeometry {
|
|
91
|
+
constructor(radius = 1, radial = 32, start = 0, length = Math.PI * 2) {
|
|
92
|
+
super(radius, segments(radial, 8, VISUAL_TRIANGLE_RATIO), start, length)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class VisualIcosahedronGeometry extends THREE.IcosahedronGeometry {
|
|
97
|
+
constructor(radius = 1, detail = 0) {
|
|
98
|
+
super(radius, Math.max(0, Math.floor((detail + 1) * AXIS_RATIO) - 1))
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Omit tiny architectural bevels while retaining the authoring call signature. */
|
|
103
|
+
export class VisualBoxGeometry extends THREE.BoxGeometry {
|
|
104
|
+
constructor(width = 1, height = 1, depth = 1, _segments = 2, _radius = 0.1) {
|
|
105
|
+
super(width, height, depth)
|
|
106
|
+
void _segments
|
|
107
|
+
void _radius
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class VisualExtrudeGeometry extends THREE.ExtrudeGeometry {
|
|
112
|
+
constructor(shapes?: THREE.Shape | THREE.Shape[], options: THREE.ExtrudeGeometryOptions = {}) {
|
|
113
|
+
super(shapes, {
|
|
114
|
+
...options,
|
|
115
|
+
curveSegments: segments(options.curveSegments ?? 12, 1, VISUAL_TRIANGLE_RATIO),
|
|
116
|
+
steps: segments(options.steps ?? 1, 1),
|
|
117
|
+
bevelSegments: options.bevelSegments === 0 ? 0 : 1,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type VisualQualityTier = 'quality' | 'balanced' | 'performance' | 'low'
|
|
2
|
+
|
|
3
|
+
export type PortalRenderMode = 'live' | 'reduced' | 'static'
|
|
4
|
+
|
|
5
|
+
export interface VisualQualityPolicy {
|
|
6
|
+
tier: VisualQualityTier
|
|
7
|
+
visualFramesPerSecond: 60
|
|
8
|
+
pixelRatioScale: number
|
|
9
|
+
particleScale: number
|
|
10
|
+
shadowMapSize: 512 | 1024
|
|
11
|
+
shadowRefreshRate: 10 | 20 | 30
|
|
12
|
+
portal: {
|
|
13
|
+
mode: PortalRenderMode
|
|
14
|
+
refreshRate: number
|
|
15
|
+
renderTargetScale: number
|
|
16
|
+
viewsPerFrame: 0 | 1 | 2
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Every tier uses animated coloured rifts; destination cameras stay disabled.
|
|
21
|
+
export const VISUAL_QUALITY_POLICIES: Readonly<Record<VisualQualityTier, VisualQualityPolicy>> = {
|
|
22
|
+
// Explicit diagnostic preset only: automatic adaptation never selects it.
|
|
23
|
+
low: {
|
|
24
|
+
tier: 'low',
|
|
25
|
+
visualFramesPerSecond: 60,
|
|
26
|
+
pixelRatioScale: 0.5,
|
|
27
|
+
particleScale: 0,
|
|
28
|
+
shadowMapSize: 512,
|
|
29
|
+
shadowRefreshRate: 10,
|
|
30
|
+
portal: {
|
|
31
|
+
mode: 'static',
|
|
32
|
+
refreshRate: 0,
|
|
33
|
+
renderTargetScale: 0.25,
|
|
34
|
+
viewsPerFrame: 0,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
quality: {
|
|
38
|
+
tier: 'quality',
|
|
39
|
+
visualFramesPerSecond: 60,
|
|
40
|
+
pixelRatioScale: 1,
|
|
41
|
+
particleScale: 1,
|
|
42
|
+
shadowMapSize: 1024,
|
|
43
|
+
shadowRefreshRate: 30,
|
|
44
|
+
portal: {
|
|
45
|
+
mode: 'static',
|
|
46
|
+
refreshRate: 0,
|
|
47
|
+
renderTargetScale: 1,
|
|
48
|
+
viewsPerFrame: 0,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
balanced: {
|
|
52
|
+
tier: 'balanced',
|
|
53
|
+
visualFramesPerSecond: 60,
|
|
54
|
+
pixelRatioScale: 0.9,
|
|
55
|
+
particleScale: 0.6,
|
|
56
|
+
shadowMapSize: 1024,
|
|
57
|
+
shadowRefreshRate: 20,
|
|
58
|
+
portal: {
|
|
59
|
+
mode: 'static',
|
|
60
|
+
refreshRate: 0,
|
|
61
|
+
renderTargetScale: 0.7,
|
|
62
|
+
viewsPerFrame: 0,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
performance: {
|
|
66
|
+
tier: 'performance',
|
|
67
|
+
visualFramesPerSecond: 60,
|
|
68
|
+
pixelRatioScale: 0.72,
|
|
69
|
+
particleScale: 0.25,
|
|
70
|
+
shadowMapSize: 512,
|
|
71
|
+
shadowRefreshRate: 10,
|
|
72
|
+
portal: {
|
|
73
|
+
mode: 'static',
|
|
74
|
+
refreshRate: 0,
|
|
75
|
+
renderTargetScale: 0.45,
|
|
76
|
+
viewsPerFrame: 0,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function visualQualityPolicy(tier: VisualQualityTier): VisualQualityPolicy {
|
|
82
|
+
return VISUAL_QUALITY_POLICIES[tier]
|
|
83
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MaterialSlot } from '@marble.fun/physics-engine'
|
|
2
|
+
|
|
3
|
+
export const FLOWING_WATER_APPEARANCE_ID = 'effect.flowing-water'
|
|
4
|
+
|
|
5
|
+
export type FluidSurfaceStyle = 'water'
|
|
6
|
+
|
|
7
|
+
export function supportsFlowingWaterOverlay(runtimeKind: string, materialSlots: readonly MaterialSlot[]): boolean {
|
|
8
|
+
return runtimeKind !== 'vortex' && materialSlots.includes('lane.surface')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isFlowingWaterAppearance(materialId: string | undefined): boolean {
|
|
12
|
+
return materialId === FLOWING_WATER_APPEARANCE_ID
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function fluidSurfaceStyleFromId(materialId: string | undefined): FluidSurfaceStyle | undefined {
|
|
16
|
+
if (materialId === FLOWING_WATER_APPEARANCE_ID) return 'water'
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isFluidSurfaceAppearance(materialId: string | undefined): boolean {
|
|
21
|
+
return fluidSurfaceStyleFromId(materialId) !== undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function supportsFloorAppearanceChoices(runtimeKind: string, materialSlots: readonly string[]): boolean {
|
|
25
|
+
// These mechanisms expose their own bowl/lift surface treatment. They have
|
|
26
|
+
// short lane collars, but no continuous base floor that can truthfully be
|
|
27
|
+
// replaced by a routed plank/sand family.
|
|
28
|
+
return runtimeKind !== 'vortex' && runtimeKind !== 'conveyor' && materialSlots.includes('lane.surface')
|
|
29
|
+
}
|