@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,590 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualCylinderGeometry, VisualConeGeometry, VisualTorusGeometry, VisualCircleGeometry, VisualIcosahedronGeometry } from './visual-geometry'
|
|
4
|
+
|
|
5
|
+
import type { ThemeMaterialRegistry } from './theme-material-registry'
|
|
6
|
+
import type { ThemeParticleStyle, TrackThemeDefinition } from './track-theme'
|
|
7
|
+
|
|
8
|
+
export interface ThemeWorldState {
|
|
9
|
+
root: THREE.Group
|
|
10
|
+
update(deltaSeconds: number, elapsedSeconds: number): void
|
|
11
|
+
dispose(): void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createThemeWorld(
|
|
15
|
+
theme: TrackThemeDefinition,
|
|
16
|
+
materials: ThemeMaterialRegistry,
|
|
17
|
+
mobile: boolean,
|
|
18
|
+
): ThemeWorldState {
|
|
19
|
+
const root = new THREE.Group()
|
|
20
|
+
root.name = `theme-world:${theme.id}`
|
|
21
|
+
root.userData.themeWorld = true
|
|
22
|
+
const scale = mobile ? 0.62 : 1
|
|
23
|
+
if (theme.world.kit !== 'studio') {
|
|
24
|
+
addGround(root, materials, theme.world.kit)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (theme.world.kit) {
|
|
28
|
+
case 'wild-west':
|
|
29
|
+
addWildWestKit(root, materials, scale * theme.world.density)
|
|
30
|
+
break
|
|
31
|
+
case 'ancient-egypt':
|
|
32
|
+
addAncientEgyptKit(root, materials, scale * theme.world.density)
|
|
33
|
+
break
|
|
34
|
+
case 'japanese-sakura-lake':
|
|
35
|
+
addJapaneseSakuraLakeKit(root, materials, scale * theme.world.density)
|
|
36
|
+
break
|
|
37
|
+
case 'space-station':
|
|
38
|
+
addSpaceStationKit(root, materials, scale * theme.world.density)
|
|
39
|
+
break
|
|
40
|
+
case 'jungle':
|
|
41
|
+
addJungleKit(root, materials, scale * theme.world.density)
|
|
42
|
+
break
|
|
43
|
+
case 'studio':
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const particles = createThemeParticles(theme, mobile)
|
|
48
|
+
if (particles) root.add(particles.points)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
update: (deltaSeconds, elapsedSeconds) => {
|
|
53
|
+
particles?.update(deltaSeconds, elapsedSeconds)
|
|
54
|
+
},
|
|
55
|
+
dispose: () => disposeThemeWorld(root),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function addGround(
|
|
60
|
+
root: THREE.Group,
|
|
61
|
+
materials: ThemeMaterialRegistry,
|
|
62
|
+
kit: TrackThemeDefinition['world']['kit'],
|
|
63
|
+
): void {
|
|
64
|
+
const geometry =
|
|
65
|
+
kit === 'space-station' ? new VisualCylinderGeometry(100, 100, 0.22, 64) : new VisualCircleGeometry(105, 48)
|
|
66
|
+
const ground = new THREE.Mesh(geometry, materials.get('world.ground'))
|
|
67
|
+
ground.name = 'theme-ground'
|
|
68
|
+
if (geometry instanceof THREE.CircleGeometry) ground.rotation.x = -Math.PI / 2
|
|
69
|
+
ground.position.y = -0.12
|
|
70
|
+
ground.receiveShadow = true
|
|
71
|
+
root.add(ground)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function addWildWestKit(root: THREE.Group, materials: ThemeMaterialRegistry, density: number): void {
|
|
75
|
+
const count = Math.max(10, Math.round(34 * density))
|
|
76
|
+
const trunkGeometry = new VisualCylinderGeometry(0.3, 0.46, 4.6, 7)
|
|
77
|
+
const armGeometry = new VisualCylinderGeometry(0.16, 0.22, 2.1, 6)
|
|
78
|
+
const foliage = materials.get('world.foliage')
|
|
79
|
+
const trunks = new THREE.InstancedMesh(trunkGeometry, foliage, count)
|
|
80
|
+
const arms = new THREE.InstancedMesh(armGeometry, foliage, count * 2)
|
|
81
|
+
const matrix = new THREE.Matrix4()
|
|
82
|
+
let armIndex = 0
|
|
83
|
+
|
|
84
|
+
for (let index = 0; index < count; index += 1) {
|
|
85
|
+
const angle = (index / count) * Math.PI * 2 + seeded(index, 11) * 0.28
|
|
86
|
+
const radius = 28 + seeded(index, 17) * 62
|
|
87
|
+
const height = 0.72 + seeded(index, 29) * 0.65
|
|
88
|
+
const position = new THREE.Vector3(Math.cos(angle) * radius, 2.2 * height, Math.sin(angle) * radius)
|
|
89
|
+
matrix.compose(position, new THREE.Quaternion(), new THREE.Vector3(height, height, height))
|
|
90
|
+
trunks.setMatrixAt(index, matrix)
|
|
91
|
+
for (const side of [-1, 1]) {
|
|
92
|
+
const armPosition = position
|
|
93
|
+
.clone()
|
|
94
|
+
.add(new THREE.Vector3(side * 0.66 * height, 0.35 * height, seeded(index, side + 40) * 0.3))
|
|
95
|
+
const quaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, side * Math.PI * 0.48))
|
|
96
|
+
matrix.compose(armPosition, quaternion, new THREE.Vector3(height, height, height))
|
|
97
|
+
arms.setMatrixAt(armIndex, matrix)
|
|
98
|
+
armIndex += 1
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
trunks.instanceMatrix.needsUpdate = true
|
|
102
|
+
arms.instanceMatrix.needsUpdate = true
|
|
103
|
+
root.add(trunks, arms)
|
|
104
|
+
|
|
105
|
+
addPerimeterFence(root, materials, 46, Math.max(18, Math.round(42 * density)), 1.65)
|
|
106
|
+
addRockField(root, materials, Math.max(12, Math.round(30 * density)), 30, 86)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function addAncientEgyptKit(root: THREE.Group, materials: ThemeMaterialRegistry, density: number): void {
|
|
110
|
+
const monumentCount = Math.max(6, Math.round(10 * density))
|
|
111
|
+
const obeliskGeometry = new VisualCylinderGeometry(0.18, 0.72, 7.2, 4)
|
|
112
|
+
const capGeometry = new VisualConeGeometry(0.51, 1.2, 4)
|
|
113
|
+
const obelisks = new THREE.InstancedMesh(obeliskGeometry, materials.get('world.primary'), monumentCount)
|
|
114
|
+
const caps = new THREE.InstancedMesh(capGeometry, materials.get('world.accent'), monumentCount)
|
|
115
|
+
const matrix = new THREE.Matrix4()
|
|
116
|
+
|
|
117
|
+
for (let index = 0; index < monumentCount; index += 1) {
|
|
118
|
+
const angle = (index / monumentCount) * Math.PI * 2 + 0.18
|
|
119
|
+
const radius = 42 + (index % 3) * 12
|
|
120
|
+
const scale = 0.76 + seeded(index, 83) * 0.46
|
|
121
|
+
const position = new THREE.Vector3(Math.cos(angle) * radius, 3.6 * scale, Math.sin(angle) * radius)
|
|
122
|
+
const quaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI / 4, 0))
|
|
123
|
+
matrix.compose(position, quaternion, new THREE.Vector3(scale, scale, scale))
|
|
124
|
+
obelisks.setMatrixAt(index, matrix)
|
|
125
|
+
matrix.compose(position.clone().setY(7.72 * scale), quaternion, new THREE.Vector3(scale, scale, scale))
|
|
126
|
+
caps.setMatrixAt(index, matrix)
|
|
127
|
+
}
|
|
128
|
+
obelisks.name = 'ancient-egypt-obelisks'
|
|
129
|
+
caps.name = 'ancient-egypt-obelisk-caps'
|
|
130
|
+
obelisks.userData.decorative = true
|
|
131
|
+
caps.userData.decorative = true
|
|
132
|
+
obelisks.instanceMatrix.needsUpdate = true
|
|
133
|
+
caps.instanceMatrix.needsUpdate = true
|
|
134
|
+
root.add(obelisks, caps)
|
|
135
|
+
|
|
136
|
+
const pyramidSpecs = [
|
|
137
|
+
[-58, 0, -34, 12],
|
|
138
|
+
[52, 0, 43, 9],
|
|
139
|
+
[68, 0, -22, 7],
|
|
140
|
+
] as const
|
|
141
|
+
for (const [index, [x, y, z, size]] of pyramidSpecs.entries()) {
|
|
142
|
+
const pyramid = new THREE.Mesh(new VisualConeGeometry(size, size * 0.92, 4), materials.get('world.secondary'))
|
|
143
|
+
pyramid.name = `ancient-egypt-pyramid-${index + 1}`
|
|
144
|
+
pyramid.userData.decorative = true
|
|
145
|
+
pyramid.position.set(x, y + size * 0.46, z)
|
|
146
|
+
pyramid.rotation.y = Math.PI / 4
|
|
147
|
+
root.add(pyramid)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
addRockField(root, materials, Math.max(10, Math.round(22 * density)), 34, 88)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function addJapaneseSakuraLakeKit(root: THREE.Group, materials: ThemeMaterialRegistry, density: number): void {
|
|
154
|
+
const toriiCount = Math.max(4, Math.round(7 * density))
|
|
155
|
+
const postGeometry = new THREE.BoxGeometry(0.42, 5.4, 0.42)
|
|
156
|
+
const beamGeometry = new THREE.BoxGeometry(5.8, 0.38, 0.55)
|
|
157
|
+
const upperBeamGeometry = new THREE.BoxGeometry(6.7, 0.3, 0.68)
|
|
158
|
+
const toriiPosts = new THREE.InstancedMesh(postGeometry, materials.get('world.primary'), toriiCount * 2)
|
|
159
|
+
const toriiBeams = new THREE.InstancedMesh(beamGeometry, materials.get('world.dark'), toriiCount)
|
|
160
|
+
const toriiUpperBeams = new THREE.InstancedMesh(upperBeamGeometry, materials.get('world.primary'), toriiCount)
|
|
161
|
+
const parentMatrix = new THREE.Matrix4()
|
|
162
|
+
const localMatrix = new THREE.Matrix4()
|
|
163
|
+
const instanceMatrix = new THREE.Matrix4()
|
|
164
|
+
const position = new THREE.Vector3()
|
|
165
|
+
const quaternion = new THREE.Quaternion()
|
|
166
|
+
const scale = new THREE.Vector3(1, 1, 1)
|
|
167
|
+
let postIndex = 0
|
|
168
|
+
|
|
169
|
+
for (let index = 0; index < toriiCount; index += 1) {
|
|
170
|
+
const angle = (index / toriiCount) * Math.PI * 2 + 0.24
|
|
171
|
+
const radius = 39 + (index % 2) * 17
|
|
172
|
+
position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius)
|
|
173
|
+
quaternion.setFromEuler(new THREE.Euler(0, -angle + Math.PI / 2, 0))
|
|
174
|
+
parentMatrix.compose(position, quaternion, scale)
|
|
175
|
+
|
|
176
|
+
for (const x of [-2.15, 2.15]) {
|
|
177
|
+
localMatrix.makeTranslation(x, 2.7, 0)
|
|
178
|
+
toriiPosts.setMatrixAt(postIndex, instanceMatrix.multiplyMatrices(parentMatrix, localMatrix))
|
|
179
|
+
postIndex += 1
|
|
180
|
+
}
|
|
181
|
+
localMatrix.makeTranslation(0, 4.55, 0)
|
|
182
|
+
toriiBeams.setMatrixAt(index, instanceMatrix.multiplyMatrices(parentMatrix, localMatrix))
|
|
183
|
+
localMatrix.makeTranslation(0, 5.35, 0)
|
|
184
|
+
toriiUpperBeams.setMatrixAt(index, instanceMatrix.multiplyMatrices(parentMatrix, localMatrix))
|
|
185
|
+
}
|
|
186
|
+
toriiPosts.name = 'japanese-sakura-torii-posts'
|
|
187
|
+
toriiBeams.name = 'japanese-sakura-torii-beams'
|
|
188
|
+
toriiUpperBeams.name = 'japanese-sakura-torii-upper-beams'
|
|
189
|
+
for (const toriiPart of [toriiPosts, toriiBeams, toriiUpperBeams]) {
|
|
190
|
+
toriiPart.userData.decorative = true
|
|
191
|
+
toriiPart.instanceMatrix.needsUpdate = true
|
|
192
|
+
}
|
|
193
|
+
root.add(toriiPosts, toriiBeams, toriiUpperBeams)
|
|
194
|
+
|
|
195
|
+
const pagodaSpecs = [
|
|
196
|
+
[-58, 0, -35, 1.15],
|
|
197
|
+
[55, 0, 42, 0.95],
|
|
198
|
+
[67, 0, -24, 0.78],
|
|
199
|
+
] as const
|
|
200
|
+
for (let tier = 0; tier < 3; tier += 1) {
|
|
201
|
+
const bodies = new THREE.InstancedMesh(
|
|
202
|
+
new THREE.BoxGeometry(3.5 - tier * 0.55, 1.35, 3.5 - tier * 0.55),
|
|
203
|
+
materials.get('world.secondary'),
|
|
204
|
+
pagodaSpecs.length,
|
|
205
|
+
)
|
|
206
|
+
const roofs = new THREE.InstancedMesh(
|
|
207
|
+
new VisualCylinderGeometry(1.6 - tier * 0.2, 2.9 - tier * 0.36, 0.65, 4),
|
|
208
|
+
materials.get('world.dark'),
|
|
209
|
+
pagodaSpecs.length,
|
|
210
|
+
)
|
|
211
|
+
for (const [index, [x, y, z, pagodaScale]] of pagodaSpecs.entries()) {
|
|
212
|
+
position.set(x, y, z)
|
|
213
|
+
quaternion.identity()
|
|
214
|
+
scale.setScalar(pagodaScale)
|
|
215
|
+
parentMatrix.compose(position, quaternion, scale)
|
|
216
|
+
localMatrix.makeTranslation(0, 0.8 + tier * 1.55, 0)
|
|
217
|
+
bodies.setMatrixAt(index, instanceMatrix.multiplyMatrices(parentMatrix, localMatrix))
|
|
218
|
+
localMatrix.compose(
|
|
219
|
+
new THREE.Vector3(0, 1.65 + tier * 1.55, 0),
|
|
220
|
+
quaternion.setFromAxisAngle(THREE.Object3D.DEFAULT_UP, Math.PI / 4),
|
|
221
|
+
new THREE.Vector3(1, 1, 1),
|
|
222
|
+
)
|
|
223
|
+
roofs.setMatrixAt(index, instanceMatrix.multiplyMatrices(parentMatrix, localMatrix))
|
|
224
|
+
}
|
|
225
|
+
bodies.name = `japanese-sakura-pagoda-tier-${tier + 1}-bodies`
|
|
226
|
+
roofs.name = `japanese-sakura-pagoda-tier-${tier + 1}-roofs`
|
|
227
|
+
bodies.userData.decorative = true
|
|
228
|
+
roofs.userData.decorative = true
|
|
229
|
+
bodies.instanceMatrix.needsUpdate = true
|
|
230
|
+
roofs.instanceMatrix.needsUpdate = true
|
|
231
|
+
root.add(bodies, roofs)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function addSpaceStationKit(root: THREE.Group, materials: ThemeMaterialRegistry, density: number): void {
|
|
236
|
+
const segments = Math.max(12, Math.round(28 * density))
|
|
237
|
+
const pylonGeometry = new THREE.BoxGeometry(1.1, 6.5, 1.1)
|
|
238
|
+
const lightGeometry = new THREE.BoxGeometry(0.22, 2.8, 0.28)
|
|
239
|
+
const pylons = new THREE.InstancedMesh(pylonGeometry, materials.get('world.primary'), segments)
|
|
240
|
+
const lights = new THREE.InstancedMesh(lightGeometry, materials.get('world.accent'), segments)
|
|
241
|
+
const matrix = new THREE.Matrix4()
|
|
242
|
+
|
|
243
|
+
for (let index = 0; index < segments; index += 1) {
|
|
244
|
+
const angle = (index / segments) * Math.PI * 2
|
|
245
|
+
const radius = 42 + (index % 3) * 8
|
|
246
|
+
const position = new THREE.Vector3(Math.cos(angle) * radius, 3.25, Math.sin(angle) * radius)
|
|
247
|
+
const quaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, -angle, 0))
|
|
248
|
+
matrix.compose(position, quaternion, new THREE.Vector3(1, 1, 1))
|
|
249
|
+
pylons.setMatrixAt(index, matrix)
|
|
250
|
+
matrix.compose(position.clone().add(new THREE.Vector3(0, 0.35, 0)), quaternion, new THREE.Vector3(1, 1, 1))
|
|
251
|
+
lights.setMatrixAt(index, matrix)
|
|
252
|
+
}
|
|
253
|
+
pylons.instanceMatrix.needsUpdate = true
|
|
254
|
+
lights.instanceMatrix.needsUpdate = true
|
|
255
|
+
root.add(pylons, lights)
|
|
256
|
+
|
|
257
|
+
for (const radius of [34, 62]) {
|
|
258
|
+
const ring = new THREE.Mesh(
|
|
259
|
+
new VisualTorusGeometry(radius, radius === 34 ? 0.26 : 0.42, 6, 96),
|
|
260
|
+
materials.get(radius === 34 ? 'world.accent' : 'world.secondary'),
|
|
261
|
+
)
|
|
262
|
+
ring.rotation.x = Math.PI / 2
|
|
263
|
+
ring.position.y = radius === 34 ? 1.2 : 8
|
|
264
|
+
root.add(ring)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const planetSpecs = [
|
|
268
|
+
{ position: [-18, 9, -22], radius: 5.5, slot: 'world.secondary' },
|
|
269
|
+
{ position: [12, 8, -14], radius: 3.2, slot: 'world.primary' },
|
|
270
|
+
{ position: [-30, 5, 2], radius: 2.8, slot: 'world.accent' },
|
|
271
|
+
] as const
|
|
272
|
+
for (const [index, planetSpec] of planetSpecs.entries()) {
|
|
273
|
+
const planet = new THREE.Mesh(new VisualIcosahedronGeometry(planetSpec.radius, 3), materials.get(planetSpec.slot))
|
|
274
|
+
planet.name = `space-background-planet-${index + 1}`
|
|
275
|
+
planet.position.set(planetSpec.position[0], planetSpec.position[1], planetSpec.position[2])
|
|
276
|
+
root.add(planet)
|
|
277
|
+
if (index === 0) {
|
|
278
|
+
const planetRing = new THREE.Mesh(
|
|
279
|
+
new VisualTorusGeometry(planetSpec.radius * 1.45, 0.32, 8, 72),
|
|
280
|
+
materials.get('world.accent'),
|
|
281
|
+
)
|
|
282
|
+
planetRing.name = 'space-background-planet-ring'
|
|
283
|
+
planetRing.position.copy(planet.position)
|
|
284
|
+
planetRing.rotation.set(Math.PI * 0.62, 0.18, 0.25)
|
|
285
|
+
root.add(planetRing)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function addJungleKit(root: THREE.Group, materials: ThemeMaterialRegistry, density: number): void {
|
|
291
|
+
addJungleGreenPerimeter(root, materials, 36, density)
|
|
292
|
+
addRockField(root, materials, Math.max(18, Math.round(38 * density)), 30, 82)
|
|
293
|
+
|
|
294
|
+
const leafCount = Math.max(30, Math.round(86 * density))
|
|
295
|
+
const leafGeometry = new VisualConeGeometry(0.62, 2.8, 4)
|
|
296
|
+
leafGeometry.rotateZ(Math.PI / 2)
|
|
297
|
+
const leaves = new THREE.InstancedMesh(leafGeometry, materials.get('world.foliage'), leafCount)
|
|
298
|
+
const matrix = new THREE.Matrix4()
|
|
299
|
+
for (let index = 0; index < leafCount; index += 1) {
|
|
300
|
+
const angle = (index / leafCount) * Math.PI * 2 + seeded(index, 19) * 0.3
|
|
301
|
+
const radius = 24 + seeded(index, 31) * 68
|
|
302
|
+
const position = new THREE.Vector3(
|
|
303
|
+
Math.cos(angle) * radius,
|
|
304
|
+
0.45 + seeded(index, 7) * 2.3,
|
|
305
|
+
Math.sin(angle) * radius,
|
|
306
|
+
)
|
|
307
|
+
const quaternion = new THREE.Quaternion().setFromEuler(
|
|
308
|
+
new THREE.Euler(seeded(index, 23) * 0.6, angle + seeded(index, 37), seeded(index, 41) * 0.6),
|
|
309
|
+
)
|
|
310
|
+
const size = 0.65 + seeded(index, 43) * 1.35
|
|
311
|
+
matrix.compose(position, quaternion, new THREE.Vector3(size, size, size))
|
|
312
|
+
leaves.setMatrixAt(index, matrix)
|
|
313
|
+
}
|
|
314
|
+
leaves.name = 'jungle-broad-leaves'
|
|
315
|
+
leaves.userData.decorative = true
|
|
316
|
+
leaves.instanceMatrix.needsUpdate = true
|
|
317
|
+
root.add(leaves)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function addJungleGreenPerimeter(
|
|
321
|
+
root: THREE.Group,
|
|
322
|
+
materials: ThemeMaterialRegistry,
|
|
323
|
+
radius: number,
|
|
324
|
+
density: number,
|
|
325
|
+
): void {
|
|
326
|
+
// Decorative only: the authored track guards remain the sole wall colliders.
|
|
327
|
+
const clusterCount = Math.max(144, Math.round(280 * density))
|
|
328
|
+
const clusterGeometry = new THREE.DodecahedronGeometry(0.72, 0)
|
|
329
|
+
const clusters = new THREE.InstancedMesh(clusterGeometry, materials.get('world.foliage'), clusterCount)
|
|
330
|
+
clusters.name = 'jungle-perimeter-leaf-clusters'
|
|
331
|
+
clusters.userData.decorative = true
|
|
332
|
+
const matrix = new THREE.Matrix4()
|
|
333
|
+
const foliageColors = [
|
|
334
|
+
new THREE.Color('#256f35'),
|
|
335
|
+
new THREE.Color('#3d8f3b'),
|
|
336
|
+
new THREE.Color('#61a844'),
|
|
337
|
+
new THREE.Color('#8dbc4b'),
|
|
338
|
+
new THREE.Color('#b2cf5b'),
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
for (let index = 0; index < clusterCount; index += 1) {
|
|
342
|
+
const angle = (index / clusterCount) * Math.PI * 2 + (seeded(index, 47) - 0.5) * 0.045
|
|
343
|
+
const clusterRadius = radius + (seeded(index, 53) - 0.5) * 1.15
|
|
344
|
+
const band = index % 3
|
|
345
|
+
const height = 0.48 + band * 0.78 + seeded(index, 59) * 0.58
|
|
346
|
+
const position = new THREE.Vector3(Math.cos(angle) * clusterRadius, height, Math.sin(angle) * clusterRadius)
|
|
347
|
+
const quaternion = new THREE.Quaternion().setFromEuler(
|
|
348
|
+
new THREE.Euler(seeded(index, 67) * 1.2, -angle + seeded(index, 71), seeded(index, 79) * 1.1),
|
|
349
|
+
)
|
|
350
|
+
const width = 0.78 + seeded(index, 61) * 0.92
|
|
351
|
+
const heightScale = 0.65 + seeded(index, 83) * 0.9
|
|
352
|
+
const depth = 0.72 + seeded(index, 89) * 0.84
|
|
353
|
+
matrix.compose(position, quaternion, new THREE.Vector3(width, heightScale, depth))
|
|
354
|
+
clusters.setMatrixAt(index, matrix)
|
|
355
|
+
clusters.setColorAt(index, foliageColors[Math.floor(seeded(index, 73) * foliageColors.length)]!)
|
|
356
|
+
}
|
|
357
|
+
clusters.instanceMatrix.needsUpdate = true
|
|
358
|
+
if (clusters.instanceColor) clusters.instanceColor.needsUpdate = true
|
|
359
|
+
root.add(clusters)
|
|
360
|
+
|
|
361
|
+
const broadLeafCount = Math.max(90, Math.round(180 * density))
|
|
362
|
+
const broadLeafGeometry = new VisualConeGeometry(0.46, 1.9, 4)
|
|
363
|
+
broadLeafGeometry.rotateZ(Math.PI / 2)
|
|
364
|
+
const broadLeaves = new THREE.InstancedMesh(broadLeafGeometry, materials.get('world.foliage'), broadLeafCount)
|
|
365
|
+
broadLeaves.name = 'jungle-perimeter-broad-leaves'
|
|
366
|
+
broadLeaves.userData.decorative = true
|
|
367
|
+
for (let index = 0; index < broadLeafCount; index += 1) {
|
|
368
|
+
const angle = (index / broadLeafCount) * Math.PI * 2 + (seeded(index, 97) - 0.5) * 0.08
|
|
369
|
+
const leafRadius = radius + (seeded(index, 101) - 0.5) * 1.4
|
|
370
|
+
const position = new THREE.Vector3(
|
|
371
|
+
Math.cos(angle) * leafRadius,
|
|
372
|
+
0.38 + seeded(index, 103) * 2.6,
|
|
373
|
+
Math.sin(angle) * leafRadius,
|
|
374
|
+
)
|
|
375
|
+
const quaternion = new THREE.Quaternion().setFromEuler(
|
|
376
|
+
new THREE.Euler(seeded(index, 107) * 1.1, -angle + seeded(index, 109) * 1.5, seeded(index, 113) * 1.3),
|
|
377
|
+
)
|
|
378
|
+
const size = 0.62 + seeded(index, 127) * 0.92
|
|
379
|
+
matrix.compose(position, quaternion, new THREE.Vector3(size, size, size))
|
|
380
|
+
broadLeaves.setMatrixAt(index, matrix)
|
|
381
|
+
broadLeaves.setColorAt(index, foliageColors[Math.floor(seeded(index, 131) * foliageColors.length)]!)
|
|
382
|
+
}
|
|
383
|
+
broadLeaves.instanceMatrix.needsUpdate = true
|
|
384
|
+
if (broadLeaves.instanceColor) broadLeaves.instanceColor.needsUpdate = true
|
|
385
|
+
root.add(broadLeaves)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function addPerimeterFence(
|
|
389
|
+
root: THREE.Group,
|
|
390
|
+
materials: ThemeMaterialRegistry,
|
|
391
|
+
radius: number,
|
|
392
|
+
count: number,
|
|
393
|
+
height: number,
|
|
394
|
+
): void {
|
|
395
|
+
const postGeometry = new VisualCylinderGeometry(0.13, 0.19, height, 5)
|
|
396
|
+
const railGeometry = new THREE.BoxGeometry((Math.PI * 2 * radius) / count + 0.24, 0.12, 0.15)
|
|
397
|
+
const posts = new THREE.InstancedMesh(postGeometry, materials.get('world.primary'), count)
|
|
398
|
+
const rails = new THREE.InstancedMesh(railGeometry, materials.get('world.primary'), count * 2)
|
|
399
|
+
const ties = new THREE.InstancedMesh(new VisualTorusGeometry(0.2, 0.035, 4, 8), materials.get('world.accent'), count)
|
|
400
|
+
const matrix = new THREE.Matrix4()
|
|
401
|
+
let railIndex = 0
|
|
402
|
+
for (let index = 0; index < count; index += 1) {
|
|
403
|
+
const angle = (index / count) * Math.PI * 2
|
|
404
|
+
const position = new THREE.Vector3(Math.cos(angle) * radius, height / 2 - 0.04, Math.sin(angle) * radius)
|
|
405
|
+
const lean = (seeded(index, 13) - 0.5) * 0.22
|
|
406
|
+
const postQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(lean, -angle, lean * 0.7))
|
|
407
|
+
matrix.compose(position, postQuaternion, new THREE.Vector3(1, 1, 1))
|
|
408
|
+
posts.setMatrixAt(index, matrix)
|
|
409
|
+
|
|
410
|
+
for (const railHeight of [height * 0.38, height * 0.72]) {
|
|
411
|
+
const railPosition = new THREE.Vector3(Math.cos(angle) * radius, railHeight, Math.sin(angle) * radius)
|
|
412
|
+
const railQuaternion = new THREE.Quaternion().setFromEuler(
|
|
413
|
+
new THREE.Euler(0, -angle, (seeded(index, railIndex + 3) - 0.5) * 0.08),
|
|
414
|
+
)
|
|
415
|
+
matrix.compose(railPosition, railQuaternion, new THREE.Vector3(1, 1, 1))
|
|
416
|
+
rails.setMatrixAt(railIndex, matrix)
|
|
417
|
+
railIndex += 1
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
matrix.compose(
|
|
421
|
+
position.clone().setY(height * 0.72),
|
|
422
|
+
new THREE.Quaternion().setFromEuler(new THREE.Euler(Math.PI / 2, -angle, 0)),
|
|
423
|
+
new THREE.Vector3(1, 1, 1),
|
|
424
|
+
)
|
|
425
|
+
ties.setMatrixAt(index, matrix)
|
|
426
|
+
}
|
|
427
|
+
posts.instanceMatrix.needsUpdate = true
|
|
428
|
+
rails.instanceMatrix.needsUpdate = true
|
|
429
|
+
ties.instanceMatrix.needsUpdate = true
|
|
430
|
+
root.add(posts, rails, ties)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function addRockField(
|
|
434
|
+
root: THREE.Group,
|
|
435
|
+
materials: ThemeMaterialRegistry,
|
|
436
|
+
count: number,
|
|
437
|
+
minimumRadius: number,
|
|
438
|
+
maximumRadius: number,
|
|
439
|
+
): void {
|
|
440
|
+
const geometry = new THREE.DodecahedronGeometry(1.25, 0)
|
|
441
|
+
const rocks = new THREE.InstancedMesh(geometry, materials.get('world.secondary'), count)
|
|
442
|
+
const matrix = new THREE.Matrix4()
|
|
443
|
+
for (let index = 0; index < count; index += 1) {
|
|
444
|
+
const angle = (index / count) * Math.PI * 2 + seeded(index, 5) * 0.4
|
|
445
|
+
const radius = THREE.MathUtils.lerp(minimumRadius, maximumRadius, seeded(index, 9))
|
|
446
|
+
const scale = new THREE.Vector3(
|
|
447
|
+
0.6 + seeded(index, 21) * 1.8,
|
|
448
|
+
0.45 + seeded(index, 27) * 1.4,
|
|
449
|
+
0.6 + seeded(index, 33) * 1.8,
|
|
450
|
+
)
|
|
451
|
+
matrix.compose(
|
|
452
|
+
new THREE.Vector3(Math.cos(angle) * radius, scale.y * 0.45, Math.sin(angle) * radius),
|
|
453
|
+
new THREE.Quaternion().setFromEuler(new THREE.Euler(seeded(index, 15), seeded(index, 25) * Math.PI, 0)),
|
|
454
|
+
scale,
|
|
455
|
+
)
|
|
456
|
+
rocks.setMatrixAt(index, matrix)
|
|
457
|
+
}
|
|
458
|
+
rocks.instanceMatrix.needsUpdate = true
|
|
459
|
+
root.add(rocks)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
interface ParticleState {
|
|
463
|
+
points: THREE.Points
|
|
464
|
+
update(deltaSeconds: number, elapsedSeconds: number): void
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function createThemeParticles(theme: TrackThemeDefinition, mobile: boolean): ParticleState | undefined {
|
|
468
|
+
if (theme.particles.style === 'none' || theme.particles.count === 0) return undefined
|
|
469
|
+
const count = Math.max(64, Math.round(theme.particles.count * (mobile ? 0.58 : 1)))
|
|
470
|
+
const positions = new Float32Array(count * 3)
|
|
471
|
+
const velocities = new Float32Array(count * 3)
|
|
472
|
+
const colors = new Float32Array(count * 3)
|
|
473
|
+
const primary = new THREE.Color(theme.particles.color)
|
|
474
|
+
const secondary = new THREE.Color(theme.particles.secondaryColor)
|
|
475
|
+
const random = createRandom(`${theme.id}:particles`)
|
|
476
|
+
|
|
477
|
+
for (let index = 0; index < count; index += 1) {
|
|
478
|
+
const offset = index * 3
|
|
479
|
+
positions[offset] = (random() - 0.5) * 190
|
|
480
|
+
positions[offset + 1] = random() * 72 - 8
|
|
481
|
+
positions[offset + 2] = (random() - 0.5) * 190
|
|
482
|
+
const velocity = particleVelocity(theme.particles.style, random)
|
|
483
|
+
velocities.set(velocity, offset)
|
|
484
|
+
const color = primary.clone().lerp(secondary, random())
|
|
485
|
+
colors[offset] = color.r
|
|
486
|
+
colors[offset + 1] = color.g
|
|
487
|
+
colors[offset + 2] = color.b
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const geometry = new THREE.BufferGeometry()
|
|
491
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
|
492
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
|
|
493
|
+
const material = new THREE.PointsMaterial({
|
|
494
|
+
size: theme.particles.size,
|
|
495
|
+
transparent: true,
|
|
496
|
+
opacity: theme.particles.opacity,
|
|
497
|
+
depthWrite: false,
|
|
498
|
+
vertexColors: true,
|
|
499
|
+
blending: theme.particles.style === 'space-drift' ? THREE.AdditiveBlending : THREE.NormalBlending,
|
|
500
|
+
sizeAttenuation: true,
|
|
501
|
+
})
|
|
502
|
+
const points = new THREE.Points(geometry, material)
|
|
503
|
+
points.name = `theme-particles:${theme.particles.style}`
|
|
504
|
+
points.frustumCulled = false
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
points,
|
|
508
|
+
update: (deltaSeconds, elapsedSeconds) => {
|
|
509
|
+
const positionAttribute = geometry.getAttribute('position') as THREE.BufferAttribute
|
|
510
|
+
for (let index = 0; index < count; index += 1) {
|
|
511
|
+
const offset = index * 3
|
|
512
|
+
const speed = theme.particles.speed
|
|
513
|
+
let x = positions[offset]!
|
|
514
|
+
let y = positions[offset + 1]!
|
|
515
|
+
let z = positions[offset + 2]!
|
|
516
|
+
x += velocities[offset]! * deltaSeconds * speed
|
|
517
|
+
y += velocities[offset + 1]! * deltaSeconds * speed
|
|
518
|
+
z += velocities[offset + 2]! * deltaSeconds * speed
|
|
519
|
+
|
|
520
|
+
if (theme.particles.style === 'space-drift') {
|
|
521
|
+
x += Math.sin(elapsedSeconds * 0.7 + index) * deltaSeconds * 0.18
|
|
522
|
+
} else if (theme.particles.style === 'fireflies') {
|
|
523
|
+
y += Math.sin(elapsedSeconds * 1.4 + index * 0.7) * deltaSeconds * 0.3
|
|
524
|
+
} else if (theme.particles.style === 'petals') {
|
|
525
|
+
x += Math.sin(elapsedSeconds * 0.9 + index * 0.73) * deltaSeconds * 0.52
|
|
526
|
+
z += Math.cos(elapsedSeconds * 0.64 + index * 0.41) * deltaSeconds * 0.3
|
|
527
|
+
y += Math.sin(elapsedSeconds * 1.2 + index * 0.37) * deltaSeconds * 0.08
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (x > 96) x = -96
|
|
531
|
+
if (x < -96) x = 96
|
|
532
|
+
if (y > 72) y = -8
|
|
533
|
+
if (y < -8) y = 72
|
|
534
|
+
if (z > 96) z = -96
|
|
535
|
+
if (z < -96) z = 96
|
|
536
|
+
positions[offset] = x
|
|
537
|
+
positions[offset + 1] = y
|
|
538
|
+
positions[offset + 2] = z
|
|
539
|
+
}
|
|
540
|
+
positionAttribute.needsUpdate = true
|
|
541
|
+
},
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function particleVelocity(style: ThemeParticleStyle, random: () => number): [number, number, number] {
|
|
546
|
+
switch (style) {
|
|
547
|
+
case 'space-drift':
|
|
548
|
+
return [0.45 + random() * 0.9, (random() - 0.5) * 0.18, 0.8 + random() * 1.4]
|
|
549
|
+
case 'dust':
|
|
550
|
+
return [0.5 + random(), random() * 0.1, (random() - 0.5) * 0.25]
|
|
551
|
+
case 'fireflies':
|
|
552
|
+
return [(random() - 0.5) * 0.18, (random() - 0.5) * 0.08, (random() - 0.5) * 0.18]
|
|
553
|
+
case 'petals':
|
|
554
|
+
return [0.42 + random() * 0.38, -0.08 - random() * 0.16, (random() - 0.5) * 0.32]
|
|
555
|
+
case 'none':
|
|
556
|
+
return [0, 0, 0]
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function disposeThemeWorld(root: THREE.Group): void {
|
|
561
|
+
root.traverse(object => {
|
|
562
|
+
if (!(object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Points)) return
|
|
563
|
+
object.geometry.dispose()
|
|
564
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
565
|
+
materials.forEach(material => {
|
|
566
|
+
if (!material.userData.themeRegistryManaged) material.dispose()
|
|
567
|
+
})
|
|
568
|
+
})
|
|
569
|
+
root.removeFromParent()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function seeded(index: number, salt: number): number {
|
|
573
|
+
const value = Math.sin(index * 12.9898 + salt * 78.233) * 43_758.5453
|
|
574
|
+
return value - Math.floor(value)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function createRandom(seed: string): () => number {
|
|
578
|
+
let state = 2_166_136_261
|
|
579
|
+
for (const char of seed) {
|
|
580
|
+
state ^= char.charCodeAt(0)
|
|
581
|
+
state = Math.imul(state, 16_777_619)
|
|
582
|
+
}
|
|
583
|
+
return () => {
|
|
584
|
+
state += 0x6d2b79f5
|
|
585
|
+
let value = state
|
|
586
|
+
value = Math.imul(value ^ (value >>> 15), value | 1)
|
|
587
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61)
|
|
588
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296
|
|
589
|
+
}
|
|
590
|
+
}
|