@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,142 @@
1
+ import * as THREE from 'three'
2
+
3
+ export interface PathPatternFrame {
4
+ point: THREE.Vector3
5
+ tangent: THREE.Vector3
6
+ distance: number
7
+ }
8
+
9
+ export interface PathPatternDomain {
10
+ frames: readonly PathPatternFrame[]
11
+ length: number
12
+ closed: boolean
13
+ includeStart?: boolean
14
+ includeEnd?: boolean
15
+ }
16
+
17
+ export interface PathPatternPolicy {
18
+ maximumSpacing: number
19
+ dedupeDistance?: number
20
+ dedupeSpace?: 'world' | 'horizontal'
21
+ }
22
+
23
+ export interface PathPatternPlacement {
24
+ domainIndex: number
25
+ distance: number
26
+ point: THREE.Vector3
27
+ tangent: THREE.Vector3
28
+ terminal: boolean
29
+ }
30
+
31
+ export interface LinearPatternPolicy {
32
+ targetPitch: number
33
+ gap: number
34
+ minimumElementSize?: number
35
+ }
36
+
37
+ export interface LinearPatternLayout {
38
+ count: number
39
+ pitch: number
40
+ elementSize: number
41
+ centers: number[]
42
+ terminalGap: number
43
+ }
44
+
45
+ const EPSILON = 0.000_001
46
+
47
+ /**
48
+ * Keeps repeated elements on one fixed arc-length cadence. Any remainder is
49
+ * split symmetrically between the two terminals; refitting the pitch per
50
+ * piece makes plank courses visibly accelerate and breaks their module-locked
51
+ * rhythm at a connector.
52
+ */
53
+ export function distributeLinearPattern(
54
+ length: number,
55
+ policy: LinearPatternPolicy,
56
+ ): LinearPatternLayout {
57
+ const safeLength = Math.max(EPSILON, length)
58
+ const targetPitch = Math.max(EPSILON, policy.targetPitch)
59
+ const gap = Math.max(0, policy.gap)
60
+ const minimumElementSize = Math.max(EPSILON, policy.minimumElementSize ?? EPSILON)
61
+ const count = Math.max(1, Math.floor(safeLength / targetPitch))
62
+ const pitch = safeLength < targetPitch ? safeLength : targetPitch
63
+ const usableElementSize = Math.max(EPSILON, Math.min(pitch, safeLength) - gap)
64
+ const elementSize = Math.min(safeLength, Math.max(minimumElementSize, usableElementSize))
65
+ const remainder = Math.max(0, safeLength - count * pitch)
66
+ const terminalRemainder = remainder / 2
67
+ return {
68
+ count,
69
+ pitch,
70
+ elementSize,
71
+ centers: Array.from({ length: count }, (_, index) => terminalRemainder + (index + 0.5) * pitch),
72
+ terminalGap: terminalRemainder + Math.min(gap, Math.max(0, pitch - minimumElementSize)) / 2,
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Distributes repeated visual elements over canonical paths using the same
78
+ * rule as the Wild West stockade: spacing is a maximum gap, and the chosen
79
+ * number of intervals is re-phased evenly over the complete run.
80
+ *
81
+ * An open connected run normally owns its end and not its start. That gives a
82
+ * shared terminal exactly one owner when independently rendered track pieces
83
+ * meet, without any track-template-specific seam code.
84
+ */
85
+ export function distributePathPattern(
86
+ domains: readonly PathPatternDomain[],
87
+ policy: PathPatternPolicy,
88
+ ): PathPatternPlacement[] {
89
+ const maximumSpacing = Math.max(0.001, policy.maximumSpacing)
90
+ const dedupeDistance = Math.max(0, policy.dedupeDistance ?? 0.025)
91
+ const placements: PathPatternPlacement[] = []
92
+
93
+ for (const [domainIndex, domain] of domains.entries()) {
94
+ if (domain.frames.length < 2 || domain.length <= EPSILON) continue
95
+ const intervalCount = domain.closed
96
+ ? Math.max(3, Math.ceil(domain.length / maximumSpacing))
97
+ : Math.max(1, Math.ceil(domain.length / maximumSpacing))
98
+ const stationCount = domain.closed ? intervalCount : intervalCount + 1
99
+ const stations = Array.from({ length: stationCount }, (_, index) => (index / intervalCount) * domain.length)
100
+
101
+ for (const distance of stations) {
102
+ if (!domain.closed && distance <= EPSILON && domain.includeStart === false) continue
103
+ if (!domain.closed && domain.length - distance <= EPSILON && domain.includeEnd === false) continue
104
+ const sample = samplePathPatternDomain(domain, distance)
105
+ const terminal = !domain.closed && (distance <= EPSILON || domain.length - distance <= EPSILON)
106
+ const duplicate = placements.find(placement => {
107
+ const dx = placement.point.x - sample.point.x
108
+ const dy = policy.dedupeSpace === 'horizontal' ? 0 : placement.point.y - sample.point.y
109
+ const dz = placement.point.z - sample.point.z
110
+ return dx * dx + dy * dy + dz * dz <= dedupeDistance * dedupeDistance
111
+ })
112
+ if (duplicate) {
113
+ duplicate.terminal ||= terminal
114
+ continue
115
+ }
116
+ placements.push({ domainIndex, distance, ...sample, terminal })
117
+ }
118
+ }
119
+
120
+ return placements
121
+ }
122
+
123
+ function samplePathPatternDomain(
124
+ domain: PathPatternDomain,
125
+ requestedDistance: number,
126
+ ): { point: THREE.Vector3; tangent: THREE.Vector3 } {
127
+ const distance = THREE.MathUtils.clamp(requestedDistance, 0, domain.length)
128
+ let segment = 0
129
+ while (segment < domain.frames.length - 2 && domain.frames[segment + 1]!.distance < distance) segment += 1
130
+ const start = domain.frames[segment]!
131
+ const end = domain.frames[segment + 1]!
132
+ const span = end.distance - start.distance
133
+ const progress = span <= EPSILON ? 0 : (distance - start.distance) / span
134
+ const tangent = start.tangent.clone().lerp(end.tangent, progress)
135
+ if (tangent.lengthSq() <= EPSILON) tangent.copy(end.point).sub(start.point)
136
+ if (tangent.lengthSq() <= EPSILON) tangent.set(0, 0, 1)
137
+ tangent.normalize()
138
+ return {
139
+ point: start.point.clone().lerp(end.point, progress),
140
+ tangent,
141
+ }
142
+ }
@@ -0,0 +1,171 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { VisualSphereGeometry, VisualTorusGeometry } from '../../../three/visual-geometry'
4
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
5
+
6
+ export interface PegObstacleVisualFactoryOptions {
7
+ radius: number
8
+ height: number
9
+ colliderShape?: 'cone' | 'cylinder'
10
+ bodyMaterial: THREE.Material
11
+ trimMaterial: THREE.Material
12
+ }
13
+
14
+ export interface PegObstacleVisual {
15
+ group: THREE.Group
16
+ }
17
+
18
+ interface RevolvedProfilePoint {
19
+ radius: number
20
+ y: number
21
+ }
22
+
23
+ /** Builds the one shared high-quality peg/deflector visual used by every theme. */
24
+ export function createPegObstacleVisualFactory(
25
+ options: PegObstacleVisualFactoryOptions,
26
+ ): (seed: string) => PegObstacleVisual {
27
+ const isPost = options.colliderShape === 'cylinder'
28
+ const profile = isPost
29
+ ? createRoundedPostProfile(options.radius, options.height)
30
+ : createTaperedDeflectorProfile(options.radius, options.height)
31
+ const bodyGeometry = createClosedRevolvedGeometry(
32
+ profile,
33
+ 8,
34
+ isPost ? 'rounded-peg-body-geometry' : 'tapered-deflector-body-geometry',
35
+ )
36
+ const trimGeometry = createDeflectorTrimGeometry(options.radius, options.height, isPost)
37
+ const style = isPost ? 'rounded-peg' : 'tapered-deflector'
38
+
39
+ return () => {
40
+ const group = new THREE.Group()
41
+ group.userData.obstacleVisualStyle = style
42
+
43
+ const body = new THREE.Mesh(bodyGeometry, options.bodyMaterial)
44
+ body.name = 'deflector-body'
45
+ body.castShadow = true
46
+ body.receiveShadow = true
47
+ group.add(body)
48
+
49
+ const trim = new THREE.Mesh(trimGeometry, options.trimMaterial)
50
+ trim.name = 'deflector-ribs-and-bolts'
51
+ trim.userData.decorative = true
52
+ trim.castShadow = true
53
+ trim.receiveShadow = true
54
+ group.add(trim)
55
+ return { group }
56
+ }
57
+ }
58
+
59
+ function createTaperedDeflectorProfile(radius: number, height: number): RevolvedProfilePoint[] {
60
+ const halfHeight = height / 2
61
+ return [
62
+ { radius: radius * 0.72, y: -halfHeight },
63
+ { radius: radius * 0.94, y: -halfHeight + height * 0.045 },
64
+ { radius, y: -halfHeight + height * 0.12 },
65
+ { radius: radius * 0.96, y: -halfHeight + height * 0.2 },
66
+ { radius: radius * 0.72, y: -halfHeight + height * 0.28 },
67
+ { radius: radius * 0.59, y: -halfHeight + height * 0.58 },
68
+ { radius: radius * 0.43, y: -halfHeight + height * 0.82 },
69
+ { radius: radius * 0.25, y: halfHeight - height * 0.055 },
70
+ ]
71
+ }
72
+
73
+ function createRoundedPostProfile(radius: number, height: number): RevolvedProfilePoint[] {
74
+ const halfHeight = height / 2
75
+ return [
76
+ { radius: radius * 0.72, y: -halfHeight },
77
+ { radius: radius * 0.94, y: -halfHeight + height * 0.055 },
78
+ { radius, y: -halfHeight + height * 0.14 },
79
+ { radius: radius * 0.96, y: halfHeight - height * 0.22 },
80
+ { radius: radius * 0.78, y: halfHeight - height * 0.08 },
81
+ { radius: radius * 0.38, y: halfHeight - height * 0.018 },
82
+ ]
83
+ }
84
+
85
+ function createClosedRevolvedGeometry(
86
+ profile: readonly RevolvedProfilePoint[],
87
+ segments: number,
88
+ name: string,
89
+ ): THREE.BufferGeometry {
90
+ const positions: number[] = []
91
+ const uvs: number[] = []
92
+ const indices: number[] = []
93
+ for (const point of profile) {
94
+ for (let segment = 0; segment < segments; segment += 1) {
95
+ const angle = (segment / segments) * Math.PI * 2
96
+ const x = Math.cos(angle) * point.radius
97
+ const z = Math.sin(angle) * point.radius
98
+ positions.push(x, point.y, z)
99
+ // Theme materials use metric triplanar projection; this planar UV is a
100
+ // harmless fallback for non-patterned materials without adding a seam.
101
+ uvs.push(x, point.y)
102
+ }
103
+ }
104
+
105
+ for (let ring = 0; ring < profile.length - 1; ring += 1) {
106
+ for (let segment = 0; segment < segments; segment += 1) {
107
+ const next = (segment + 1) % segments
108
+ const lower = ring * segments + segment
109
+ const lowerNext = ring * segments + next
110
+ const upper = (ring + 1) * segments + segment
111
+ const upperNext = (ring + 1) * segments + next
112
+ indices.push(lower, upper, lowerNext, lowerNext, upper, upperNext)
113
+ }
114
+ }
115
+
116
+ const bottomCenter = positions.length / 3
117
+ positions.push(0, profile[0]!.y, 0)
118
+ uvs.push(0, profile[0]!.y)
119
+ const topCenter = positions.length / 3
120
+ positions.push(0, profile.at(-1)!.y + Math.abs(profile.at(-1)!.radius) * 0.24, 0)
121
+ uvs.push(0, profile.at(-1)!.y)
122
+ const topRingOffset = (profile.length - 1) * segments
123
+ for (let segment = 0; segment < segments; segment += 1) {
124
+ const next = (segment + 1) % segments
125
+ indices.push(bottomCenter, segment, next)
126
+ indices.push(topCenter, topRingOffset + next, topRingOffset + segment)
127
+ }
128
+
129
+ const geometry = new THREE.BufferGeometry()
130
+ geometry.name = name
131
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
132
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
133
+ geometry.setIndex(indices)
134
+ geometry.computeVertexNormals()
135
+ geometry.computeBoundingBox()
136
+ geometry.computeBoundingSphere()
137
+ geometry.userData.closedSurface = true
138
+ geometry.userData.visualOnly = true
139
+ return geometry
140
+ }
141
+
142
+ function createDeflectorTrimGeometry(radius: number, height: number, isPost: boolean): THREE.BufferGeometry {
143
+ const pieces: THREE.BufferGeometry[] = []
144
+ const ringLevels = isPost ? [-0.3, 0.22] : [-0.31, -0.18]
145
+ for (const level of ringLevels) {
146
+ const progress = level + 0.5
147
+ const ringRadius = isPost ? radius * 0.985 : radius * THREE.MathUtils.lerp(0.98, 0.46, progress)
148
+ const ring = new VisualTorusGeometry(ringRadius, radius * 0.045, 6, 24)
149
+ ring.rotateX(Math.PI / 2)
150
+ ring.translate(0, level * height, 0)
151
+ pieces.push(ring)
152
+ }
153
+
154
+ const boltRadius = Math.max(0.025, radius * 0.075)
155
+ const boltY = -height * 0.38
156
+ for (let bolt = 0; bolt < 8; bolt += 1) {
157
+ const angle = (bolt / 8) * Math.PI * 2
158
+ const boltGeometry = new VisualSphereGeometry(boltRadius, 7, 5)
159
+ boltGeometry.translate(Math.cos(angle) * radius * 0.965, boltY, Math.sin(angle) * radius * 0.965)
160
+ pieces.push(boltGeometry)
161
+ }
162
+
163
+ const geometry = mergeGeometries(pieces, false)
164
+ pieces.forEach(piece => piece.dispose())
165
+ if (!geometry) throw new Error('Unable to create deflector trim geometry.')
166
+ geometry.name = 'deflector-rings-and-bolts-geometry'
167
+ geometry.computeBoundingBox()
168
+ geometry.computeBoundingSphere()
169
+ geometry.userData.visualOnly = true
170
+ return geometry
171
+ }
@@ -0,0 +1 @@
1
+ export * from '@marble.fun/physics-engine'
@@ -0,0 +1,90 @@
1
+ import {
2
+ CURRENT_RACE_ENGINE_VERSION,
3
+ createPreparedRaceSimulation,
4
+ createRaceTuning,
5
+ preparePhysicsTrack,
6
+ type PhysicsDebugReport,
7
+ type PhysicsMechanismDiagnostics,
8
+ type PhysicsSimulationCheckpoint,
9
+ } from '@marble.fun/physics-engine'
10
+
11
+ import type { RaceConfiguration, RaceWinner } from './race-types'
12
+ import type { TrackDefinition } from './track-definition'
13
+ import type { ThemeMaterialRegistry } from '../../../three/theme-material-registry'
14
+ import type { CanonicalTrackAssembly } from '../../track-lab/engine/canonical-track-assembly'
15
+
16
+ export interface PhysicsDryRunResult {
17
+ simulationKey: string
18
+ simulationTick: number
19
+ elapsedMs: number
20
+ replayFingerprint: string
21
+ winner: RaceWinner
22
+ debug: PhysicsDebugReport
23
+ mechanisms: PhysicsMechanismDiagnostics[]
24
+ finishOrder: Array<{
25
+ rank: number
26
+ slotId: string
27
+ label: string
28
+ finishTimeMs: number
29
+ }>
30
+ checkpoints: PhysicsDryRunCheckpoint[]
31
+ }
32
+
33
+ export type PhysicsDryRunCheckpoint = PhysicsSimulationCheckpoint
34
+
35
+ export interface PhysicsDryRunOptions {
36
+ canonicalAssembly?: CanonicalTrackAssembly
37
+ checkpointIntervalTicks?: number
38
+ materialRegistry?: ThemeMaterialRegistry
39
+ track?: TrackDefinition
40
+ }
41
+
42
+ export async function runPhysicsDryRun(
43
+ configuration: RaceConfiguration,
44
+ options: PhysicsDryRunOptions = {},
45
+ ): Promise<PhysicsDryRunResult> {
46
+ const track = options.track
47
+ if (!track) throw new Error('A compiled catalog track is required for deterministic simulation')
48
+ const tuning = createRaceTuning(configuration, track)
49
+ const simulation = await createPreparedRaceSimulation(
50
+ {
51
+ engineVersion: CURRENT_RACE_ENGINE_VERSION,
52
+ seed: configuration.seed,
53
+ trackId: track.id,
54
+ trackJson: track.sourceDocument ? JSON.stringify(track.sourceDocument) : '{}',
55
+ entrants: configuration.assignments,
56
+ },
57
+ preparePhysicsTrack(track),
58
+ undefined,
59
+ {
60
+ enabled: true,
61
+ checkpointIntervalTicks: options.checkpointIntervalTicks,
62
+ },
63
+ )
64
+ try {
65
+ while (!simulation.complete) {
66
+ simulation.advance()
67
+ simulation.drainEvents()
68
+ }
69
+ const result = simulation.result()!
70
+ const assignments = new Map(configuration.assignments.map(assignment => [assignment.slotId, assignment]))
71
+ return {
72
+ simulationKey: tuning.simulationKey,
73
+ simulationTick: result.simulationTicks,
74
+ elapsedMs: result.durationMs,
75
+ replayFingerprint: result.replayFingerprint,
76
+ winner: result.winner,
77
+ debug: simulation.debugReport()!,
78
+ mechanisms: simulation.mechanismDiagnostics(),
79
+ finishOrder: result.finishOrder.map(finisher => ({
80
+ rank: finisher.rank,
81
+ slotId: finisher.slotId,
82
+ label: assignments.get(finisher.slotId)?.label ?? finisher.slotId,
83
+ finishTimeMs: finisher.finishTimeMs,
84
+ })),
85
+ checkpoints: [...simulation.diagnosticCheckpoints()],
86
+ }
87
+ } finally {
88
+ simulation.dispose()
89
+ }
90
+ }