@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/assets.json +346 -0
  4. package/package.json +32 -0
  5. package/release.json +135 -0
  6. package/src/audio/game-audio-preference.ts +9 -0
  7. package/src/authoritative.ts +6 -0
  8. package/src/catalog.ts +1 -0
  9. package/src/index.ts +14 -0
  10. package/src/legacy-stock-catalog.ts +130 -0
  11. package/src/pages/lobby/lobby-types.ts +140 -0
  12. package/src/pages/lobby/marble-factory.ts +931 -0
  13. package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
  14. package/src/pages/play/race/egypt-portal-frame.ts +382 -0
  15. package/src/pages/play/race/finish-detection.ts +1 -0
  16. package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
  17. package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
  18. package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
  19. package/src/pages/play/race/jungle-portal-frame.ts +308 -0
  20. package/src/pages/play/race/jungle-temple-lift.ts +366 -0
  21. package/src/pages/play/race/marble-entity.ts +257 -0
  22. package/src/pages/play/race/marble-material.ts +20 -0
  23. package/src/pages/play/race/marble-race-engine.ts +2652 -0
  24. package/src/pages/play/race/path-pattern-layout.ts +142 -0
  25. package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
  26. package/src/pages/play/race/physics-debug.ts +1 -0
  27. package/src/pages/play/race/physics-dry-run.ts +90 -0
  28. package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
  29. package/src/pages/play/race/portal-pair-color.ts +20 -0
  30. package/src/pages/play/race/portal-proportions.ts +25 -0
  31. package/src/pages/play/race/portal-visual.ts +1108 -0
  32. package/src/pages/play/race/race-camera-director.ts +586 -0
  33. package/src/pages/play/race/race-camera-obstruction.ts +37 -0
  34. package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
  35. package/src/pages/play/race/race-camera.ts +206 -0
  36. package/src/pages/play/race/race-completion.ts +5 -0
  37. package/src/pages/play/race/race-configuration.ts +57 -0
  38. package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
  39. package/src/pages/play/race/race-physics-profile.ts +7 -0
  40. package/src/pages/play/race/race-physics.ts +16 -0
  41. package/src/pages/play/race/race-playback.ts +41 -0
  42. package/src/pages/play/race/race-rolling-audio.ts +616 -0
  43. package/src/pages/play/race/race-simulation-renderer.ts +221 -0
  44. package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
  45. package/src/pages/play/race/race-transform-interpolation.ts +50 -0
  46. package/src/pages/play/race/race-types.ts +12 -0
  47. package/src/pages/play/race/rebound-visual.ts +70 -0
  48. package/src/pages/play/race/seeded-random.ts +23 -0
  49. package/src/pages/play/race/space-station-portal-frame.ts +275 -0
  50. package/src/pages/play/race/track-appearance-canopy.ts +946 -0
  51. package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
  52. package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
  53. package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
  54. package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
  55. package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
  56. package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
  57. package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
  58. package/src/pages/play/race/track-appearance-support.ts +983 -0
  59. package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
  60. package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
  61. package/src/pages/play/race/track-course.ts +271 -0
  62. package/src/pages/play/race/track-definition.ts +443 -0
  63. package/src/pages/play/race/track-fence-architecture.ts +33 -0
  64. package/src/pages/play/race/track-floor-material-rules.ts +51 -0
  65. package/src/pages/play/race/track-mechanisms.ts +1350 -0
  66. package/src/pages/play/race/track-physics-surface.ts +6 -0
  67. package/src/pages/play/race/track-route-network.ts +9 -0
  68. package/src/pages/play/race/track-shadow-batching.ts +760 -0
  69. package/src/pages/play/race/track-surface-basis.ts +1 -0
  70. package/src/pages/play/race/track-surface-topology.ts +326 -0
  71. package/src/pages/play/race/track-world.ts +10181 -0
  72. package/src/pages/play/race/under-track-support-architecture.ts +122 -0
  73. package/src/pages/play/race/vortex-profile.ts +5 -0
  74. package/src/pages/play/race/vortex-water-effect.ts +863 -0
  75. package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
  76. package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
  77. package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
  78. package/src/pages/track-lab/cone-field-layout.ts +1 -0
  79. package/src/pages/track-lab/conveyor-layout.ts +1 -0
  80. package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
  81. package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
  82. package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
  83. package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
  84. package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
  85. package/src/pages/track-lab/flipper-component.ts +127 -0
  86. package/src/pages/track-lab/rising-wall-layout.ts +1 -0
  87. package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
  88. package/src/pages/track-lab/track-piece-layout.ts +28 -0
  89. package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
  90. package/src/three/geometry-attribute-aliases.ts +24 -0
  91. package/src/three/geometry-detail.ts +36 -0
  92. package/src/three/homepage-marble-geometry.ts +15 -0
  93. package/src/three/japanese-sakura-appearance.ts +252 -0
  94. package/src/three/jungle-flower-hedge-appearance.ts +7 -0
  95. package/src/three/jungle-temple-appearance.ts +5 -0
  96. package/src/three/low-quality-scene-renderer.ts +132 -0
  97. package/src/three/marble-sphere-geometry.ts +13 -0
  98. package/src/three/mobile-rendering-budget.ts +11 -0
  99. package/src/three/painted-valley-appearance.ts +270 -0
  100. package/src/three/railroad-appearance.ts +71 -0
  101. package/src/three/renderer-resize.ts +32 -0
  102. package/src/three/rope-wall-appearance.ts +91 -0
  103. package/src/three/rugged-track-appearance.ts +2 -0
  104. package/src/three/scene-lighting.ts +183 -0
  105. package/src/three/scene-shader-preparation.ts +64 -0
  106. package/src/three/scene-theme-controller.ts +461 -0
  107. package/src/three/shadow-shader-preparation.ts +162 -0
  108. package/src/three/shared-obstacle-metal.ts +44 -0
  109. package/src/three/soft-additive-particles.ts +57 -0
  110. package/src/three/space-station-appearance.ts +213 -0
  111. package/src/three/static-track-batched-mesh.ts +132 -0
  112. package/src/three/stone-track-shell-appearance.ts +12 -0
  113. package/src/three/theme-ambience-controller.ts +226 -0
  114. package/src/three/theme-asset-cache.ts +120 -0
  115. package/src/three/theme-audio-preference.ts +43 -0
  116. package/src/three/theme-catalog.ts +1457 -0
  117. package/src/three/theme-material-assets.ts +258 -0
  118. package/src/three/theme-material-ktx2-manifest.ts +190 -0
  119. package/src/three/theme-material-registry.ts +1952 -0
  120. package/src/three/theme-world-factory.ts +590 -0
  121. package/src/three/track-connector-batch.ts +125 -0
  122. package/src/three/track-material-continuity.ts +442 -0
  123. package/src/three/track-theme.ts +535 -0
  124. package/src/three/vampire-castle-appearance.ts +258 -0
  125. package/src/three/visual-frame-pacing.ts +19 -0
  126. package/src/three/visual-geometry.ts +120 -0
  127. package/src/three/visual-quality.ts +83 -0
  128. package/src/three/water-appearance.ts +29 -0
  129. 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
+ }
@@ -0,0 +1,2 @@
1
+ export const WILD_WEST_SAND_FLOOR_ID = 'wild-west.sandy-track'
2
+ export const WILD_WEST_SAND_SHOULDER_MATERIAL_ID = 'wild-west.sand-shoulder'