@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,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
|
+
}
|