@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,257 @@
1
+ import RAPIER from '@dimforge/rapier3d-deterministic-compat'
2
+ import type { MarbleSimulationSnapshot, PhysicsCourse } from '@marble.fun/physics-engine'
3
+ import * as THREE from 'three'
4
+
5
+ import { MarbleSphereGeometry } from '../../../three/marble-sphere-geometry'
6
+
7
+
8
+ import { createLobbyMarble, type LobbyMarble } from '../../lobby/marble-factory'
9
+ import { RACE_STOCK_MARBLES, STOCK_MARBLES } from '../../lobby/lobby-types'
10
+ import { createGlassMarbleMaterial } from './marble-material'
11
+ import { createMarblePhysicsState, type MarblePhysicsState } from './race-physics'
12
+ import type { MarbleAssignment, MarbleTuning } from './race-types'
13
+ import type { TrackCourse } from './track-course'
14
+ import { RigidBodyTransformInterpolator } from './race-transform-interpolation'
15
+
16
+ const TRAIL_POINT_COUNT = 22
17
+ const MARBLE_LABEL_CLEARANCE = 1.12
18
+
19
+ export interface MarbleEntity extends MarblePhysicsState {
20
+ visual: THREE.Group
21
+ visualReady: Promise<void>
22
+ disposeVisual(): void
23
+ label: THREE.Sprite
24
+ trail: THREE.Line
25
+ trailPositions: Float32Array
26
+ transformInterpolator: RigidBodyTransformInterpolator
27
+ }
28
+
29
+ function createLabelSprite(assignment: MarbleAssignment): THREE.Sprite {
30
+ const labelText = assignment.label.slice(0, 8).toUpperCase()
31
+ const canvas = document.createElement('canvas')
32
+ canvas.width = 256
33
+ canvas.height = 72
34
+ const context = canvas.getContext('2d')
35
+
36
+ if (!context) {
37
+ throw new Error('Canvas 2D context is unavailable')
38
+ }
39
+
40
+ context.font = '700 28px Inter, system-ui, sans-serif'
41
+ const pillWidth = THREE.MathUtils.clamp(Math.ceil(context.measureText(labelText).width + 64), 152, 248)
42
+ const pillX = (canvas.width - pillWidth) / 2
43
+ context.clearRect(0, 0, canvas.width, canvas.height)
44
+ const pillColor = new THREE.Color(assignment.color).multiplyScalar(0.34)
45
+ context.fillStyle = `#${pillColor.getHexString()}`
46
+ context.beginPath()
47
+ context.roundRect(pillX, 7, pillWidth, 58, 29)
48
+ context.fill()
49
+ context.fillStyle = '#f7fafb'
50
+ context.font = '700 28px Inter, system-ui, sans-serif'
51
+ context.textAlign = 'center'
52
+ context.textBaseline = 'middle'
53
+ context.fillText(labelText, canvas.width / 2, 36)
54
+
55
+ const texture = new THREE.CanvasTexture(canvas)
56
+ texture.colorSpace = THREE.SRGBColorSpace
57
+ const material = new THREE.SpriteMaterial({
58
+ map: texture,
59
+ transparent: true,
60
+ depthTest: false,
61
+ depthWrite: false,
62
+ toneMapped: false,
63
+ })
64
+ const sprite = new THREE.Sprite(material)
65
+ sprite.scale.set(2.35, 0.66, 1)
66
+ sprite.renderOrder = 1_000
67
+
68
+ return sprite
69
+ }
70
+
71
+ export type RaceMarbleVisualStyle = 'default' | 'stock' | 'stock-no-logo'
72
+
73
+ function createDefaultMarbleVisual(assignment: MarbleAssignment, radius: number): THREE.Group {
74
+ const root = new THREE.Group()
75
+ root.name = `${assignment.slotId}-${assignment.label}`
76
+ const shell = new THREE.Mesh(
77
+ new MarbleSphereGeometry(radius, 36, 24),
78
+ createGlassMarbleMaterial(assignment.color, radius),
79
+ )
80
+ shell.castShadow = true
81
+ shell.receiveShadow = true
82
+ root.add(shell)
83
+
84
+ return root
85
+ }
86
+
87
+ function createStockMarbleVisual(
88
+ assignment: MarbleAssignment,
89
+ radius: number,
90
+ includeLogo: boolean,
91
+ ): { marble: LobbyMarble; visual: THREE.Group } | undefined {
92
+ const slotIndex = Number.parseInt(assignment.slotId.slice(1), 10) - 1
93
+ const spec = RACE_STOCK_MARBLES.find(candidate => candidate.id === assignment.label) ?? STOCK_MARBLES[slotIndex]
94
+ if (!spec) return undefined
95
+
96
+ const marble = createLobbyMarble(
97
+ {
98
+ ...spec,
99
+ interactive: false,
100
+ withPodium: false,
101
+ },
102
+ { includeLogo },
103
+ )
104
+ marble.marble.position.set(0, 0, 0)
105
+ marble.marble.scale.setScalar(1)
106
+ marble.root.scale.setScalar(radius / 0.73)
107
+ marble.root.name = `${assignment.slotId}-${assignment.label}`
108
+ return { marble, visual: marble.root }
109
+ }
110
+
111
+ function createTrail(color: string): { line: THREE.Line; positions: Float32Array } {
112
+ const positions = new Float32Array(TRAIL_POINT_COUNT * 3)
113
+ const geometry = new THREE.BufferGeometry()
114
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
115
+ const material = new THREE.LineBasicMaterial({
116
+ color,
117
+ transparent: true,
118
+ opacity: 0.42,
119
+ depthWrite: false,
120
+ })
121
+ const line = new THREE.Line(geometry, material)
122
+ line.frustumCulled = false
123
+
124
+ return { line, positions }
125
+ }
126
+
127
+ export function createMarbleEntity(
128
+ scene: THREE.Object3D,
129
+ world: RAPIER.World,
130
+ course: TrackCourse,
131
+ assignment: MarbleAssignment,
132
+ tuning: MarbleTuning,
133
+ visualStyle: RaceMarbleVisualStyle = 'default',
134
+ ): MarbleEntity {
135
+ const physics = createMarblePhysicsState(world, course, assignment, tuning)
136
+ const translation = physics.body.translation()
137
+ const position = new THREE.Vector3(translation.x, translation.y, translation.z)
138
+ const stockVisual =
139
+ visualStyle === 'stock' || visualStyle === 'stock-no-logo'
140
+ ? createStockMarbleVisual(assignment, tuning.radius, visualStyle === 'stock')
141
+ : undefined
142
+ const visual = stockVisual?.visual ?? createDefaultMarbleVisual(assignment, tuning.radius)
143
+ const label = createLabelSprite(assignment)
144
+ const { line: trail, positions: trailPositions } = createTrail(assignment.color)
145
+ visual.position.copy(position)
146
+ label.position.copy(position)
147
+ label.position.y += tuning.radius + MARBLE_LABEL_CLEARANCE
148
+ scene.add(trail, visual, label)
149
+
150
+ for (let index = 0; index < TRAIL_POINT_COUNT; index += 1) {
151
+ trailPositions[index * 3] = position.x
152
+ trailPositions[index * 3 + 1] = position.y
153
+ trailPositions[index * 3 + 2] = position.z
154
+ }
155
+
156
+ return {
157
+ ...physics,
158
+ visual,
159
+ visualReady: stockVisual?.marble.ready ?? Promise.resolve(),
160
+ disposeVisual: () => {
161
+ if (stockVisual) stockVisual.marble.dispose()
162
+ else {
163
+ visual.traverse(object => {
164
+ if (!(object instanceof THREE.Mesh)) return
165
+ object.geometry.dispose()
166
+ const materials = Array.isArray(object.material) ? object.material : [object.material]
167
+ materials.forEach(material => material.dispose())
168
+ })
169
+ }
170
+ visual.removeFromParent()
171
+ label.material.map?.dispose()
172
+ label.material.dispose()
173
+ label.removeFromParent()
174
+ trail.geometry.dispose()
175
+ const trailMaterials = Array.isArray(trail.material) ? trail.material : [trail.material]
176
+ trailMaterials.forEach(material => material.dispose())
177
+ trail.removeFromParent()
178
+ },
179
+ label,
180
+ trail,
181
+ trailPositions,
182
+ transformInterpolator: new RigidBodyTransformInterpolator(physics.body),
183
+ }
184
+ }
185
+
186
+ export function captureMarblePhysicsTransform(entity: MarbleEntity): void {
187
+ // Portals are authoritative discontinuities. Snapping render history avoids
188
+ // drawing a one-frame flight across the world between linked endpoints.
189
+ entity.transformInterpolator.capture(entity.body, 2)
190
+ }
191
+
192
+ /**
193
+ * Copies an immutable authoritative core snapshot into the runtime's render/query
194
+ * mirror. The mirrored body is never stepped and cannot affect core physics.
195
+ */
196
+ export function applyMarbleSimulationSnapshot(
197
+ entity: MarbleEntity,
198
+ snapshot: MarbleSimulationSnapshot,
199
+ course: PhysicsCourse,
200
+ captureTransform = true,
201
+ ): void {
202
+ entity.body.setTranslation({ x: snapshot.position[0], y: snapshot.position[1], z: snapshot.position[2] }, false)
203
+ entity.body.setRotation(
204
+ {
205
+ x: snapshot.rotation[0],
206
+ y: snapshot.rotation[1],
207
+ z: snapshot.rotation[2],
208
+ w: snapshot.rotation[3],
209
+ },
210
+ false,
211
+ )
212
+ entity.body.setLinvel(
213
+ {
214
+ x: snapshot.linearVelocity[0],
215
+ y: snapshot.linearVelocity[1],
216
+ z: snapshot.linearVelocity[2],
217
+ },
218
+ false,
219
+ )
220
+ entity.body.setAngvel(
221
+ {
222
+ x: snapshot.angularVelocity[0],
223
+ y: snapshot.angularVelocity[1],
224
+ z: snapshot.angularVelocity[2],
225
+ },
226
+ false,
227
+ )
228
+ course.projectPoint(
229
+ { x: snapshot.position[0], y: snapshot.position[1], z: snapshot.position[2] },
230
+ entity.projection.sampleIndex,
231
+ entity.projection,
232
+ )
233
+ entity.projection.distance = snapshot.route.distance
234
+ course.writePoseAtDistance(snapshot.route.distance, entity.pose)
235
+ entity.progress = snapshot.route.distance
236
+ entity.bestProgress = snapshot.bestProgressMeters
237
+ entity.speed = snapshot.speedMetersPerSecond
238
+ entity.finishedAtTick = snapshot.finishedAtTick
239
+ if (captureTransform) captureMarblePhysicsTransform(entity)
240
+ }
241
+
242
+ export function syncMarbleVisual(entity: MarbleEntity, interpolationAlpha = 1): void {
243
+ entity.transformInterpolator.write(entity.visual, interpolationAlpha)
244
+ entity.label.position.copy(entity.visual.position)
245
+ entity.label.position.y += entity.tuning.radius + MARBLE_LABEL_CLEARANCE
246
+ }
247
+
248
+ export function updateMarbleTrail(entity: MarbleEntity): void {
249
+ const translation = entity.body.translation()
250
+ entity.trailPositions.copyWithin(0, 3)
251
+ const finalIndex = (TRAIL_POINT_COUNT - 1) * 3
252
+ entity.trailPositions[finalIndex] = translation.x
253
+ entity.trailPositions[finalIndex + 1] = translation.y
254
+ entity.trailPositions[finalIndex + 2] = translation.z
255
+ const attribute = entity.trail.geometry.getAttribute('position')
256
+ attribute.needsUpdate = true
257
+ }
@@ -0,0 +1,20 @@
1
+ import * as THREE from 'three'
2
+
3
+ export function createGlassMarbleMaterial(color: string, radius: number): THREE.MeshPhysicalMaterial {
4
+ const marbleColor = new THREE.Color(color)
5
+ return new THREE.MeshPhysicalMaterial({
6
+ color: marbleColor,
7
+ emissive: marbleColor,
8
+ emissiveIntensity: 0.035,
9
+ metalness: 0,
10
+ roughness: 0.075,
11
+ transmission: 0,
12
+ thickness: radius * 1.4,
13
+ ior: 1.52,
14
+ specularIntensity: 1,
15
+ specularColor: new THREE.Color(0xffffff),
16
+ clearcoat: 1,
17
+ clearcoatRoughness: 0.025,
18
+ envMapIntensity: 1.25,
19
+ })
20
+ }