@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,483 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { VisualSphereGeometry, VisualCylinderGeometry } from '../../../three/visual-geometry'
4
+
5
+ import {
6
+ PAINTED_VALLEY_THEME_ID,
7
+ PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID,
8
+ } from '../../../three/painted-valley-appearance'
9
+ import {
10
+ canonicalWallFrameAt,
11
+ type CanonicalWallRun,
12
+ type RawWallFrame,
13
+ } from './track-surface-topology'
14
+
15
+ export type JungleFlowerHedgePathFrame = RawWallFrame
16
+ export type JungleFlowerHedgePathRun = CanonicalWallRun
17
+
18
+ type FlowerType = 'orange' | 'pink' | 'magenta' | 'coral' | 'gold'
19
+
20
+ interface HedgeAssets {
21
+ leafGeometry: THREE.BufferGeometry
22
+ petalGeometry: THREE.BufferGeometry
23
+ sphereGeometry: THREE.SphereGeometry
24
+ stemGeometry: THREE.CylinderGeometry
25
+ stamenGeometry: THREE.CylinderGeometry
26
+ trumpetGeometry: THREE.CylinderGeometry
27
+ leafMaterials: THREE.MeshStandardMaterial[]
28
+ leafMatrices: THREE.Matrix4[][]
29
+ flowerMaterials: Record<string, THREE.MeshStandardMaterial>
30
+ centreMaterials: Record<string, THREE.MeshStandardMaterial>
31
+ stemMaterial: THREE.MeshStandardMaterial
32
+ calyxMaterial: THREE.MeshStandardMaterial
33
+ }
34
+
35
+ type SampledRun = CanonicalWallRun
36
+
37
+ const HEDGE_DEPTH = 0.82
38
+ const FLOWER_SCHEDULE: readonly FlowerType[] = ['orange', 'coral', 'magenta', 'gold', 'orange', 'pink']
39
+ const REAR_FLOWER_SCHEDULE: readonly FlowerType[] = ['pink', 'gold', 'orange', 'magenta', 'coral', 'orange']
40
+ const CROWN_FLOWER_SCHEDULE: readonly FlowerType[] = ['pink', 'orange', 'magenta', 'gold']
41
+
42
+ function hash01(value: number): number {
43
+ const result = Math.sin(value * 91.3458 + 17.147) * 47_453.5453
44
+ return result - Math.floor(result)
45
+ }
46
+
47
+ function standardMaterial(color: string, roughness = 0.88, emissive = '#000000', emissiveIntensity = 0): THREE.MeshStandardMaterial {
48
+ return new THREE.MeshStandardMaterial({ color, roughness, metalness: 0, emissive, emissiveIntensity, side: THREE.FrontSide })
49
+ }
50
+
51
+ function createLeafGeometry(): THREE.BufferGeometry {
52
+ const steps = 3
53
+ const positions: number[] = []
54
+ const colors: number[] = []
55
+ const indices: number[] = []
56
+ for (let step = 0; step <= steps; step += 1) {
57
+ const t = step / steps
58
+ const serration = 1 + Math.sin(t * Math.PI * 7) * 0.045
59
+ const width = (0.1 * (1 - t) + Math.pow(Math.sin(Math.PI * t), 0.76)) * 0.37 * serration
60
+ const spine = Math.sin(Math.PI * t) * 0.075 + t * t * 0.035
61
+ const edge = spine - width * 0.105
62
+ positions.push(-width, t, edge, 0, t, spine, width, t, edge)
63
+ positions.push(-width, t, edge - 0.028, 0, t, spine - 0.032, width, t, edge - 0.028)
64
+ colors.push(0.7, 0.76, 0.58, 1.26, 1.18, 0.72, 0.7, 0.76, 0.58)
65
+ colors.push(0.5, 0.56, 0.43, 0.72, 0.7, 0.48, 0.5, 0.56, 0.43)
66
+ }
67
+ const stride = 6
68
+ for (let step = 0; step < steps; step += 1) {
69
+ const a = step * stride
70
+ const b = a + stride
71
+ indices.push(a, a + 1, b, a + 1, b + 1, b, a + 1, a + 2, b + 1, a + 2, b + 2, b + 1)
72
+ indices.push(a + 3, b + 3, a + 4, a + 4, b + 3, b + 4, a + 4, b + 4, a + 5, a + 5, b + 4, b + 5)
73
+ indices.push(a, b, a + 3, a + 3, b, b + 3, a + 2, a + 5, b + 2, a + 5, b + 5, b + 2)
74
+ }
75
+ indices.push(0, 3, 1, 1, 3, 4)
76
+ const end = steps * stride
77
+ indices.push(end, end + 1, end + 3, end + 1, end + 4, end + 3)
78
+ const geometry = new THREE.BufferGeometry()
79
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
80
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
81
+ geometry.setIndex(indices)
82
+ geometry.computeVertexNormals()
83
+ return geometry
84
+ }
85
+
86
+ function createPetalGeometry(): THREE.BufferGeometry {
87
+ const steps = 3
88
+ const positions: number[] = []
89
+ const indices: number[] = []
90
+ for (let step = 0; step <= steps; step += 1) {
91
+ const t = step / steps
92
+ const width = Math.pow(Math.sin(Math.PI * Math.pow(t, 0.92)), 0.78) * (0.44 + t * 0.1)
93
+ const centerZ = Math.sin(Math.PI * t) * 0.1 + t * t * 0.045
94
+ const edgeZ = centerZ - width * 0.17
95
+ positions.push(-width, t, edgeZ, 0, t, centerZ, width, t, edgeZ)
96
+ positions.push(-width, t, edgeZ - 0.035, 0, t, centerZ - 0.04, width, t, edgeZ - 0.035)
97
+ }
98
+ const stride = 6
99
+ for (let step = 0; step < steps; step += 1) {
100
+ const a = step * stride
101
+ const b = a + stride
102
+ indices.push(a, a + 1, b, a + 1, b + 1, b, a + 1, a + 2, b + 1, a + 2, b + 2, b + 1)
103
+ indices.push(a + 3, b + 3, a + 4, a + 4, b + 3, b + 4, a + 4, b + 4, a + 5, a + 5, b + 4, b + 5)
104
+ indices.push(a, b, a + 3, a + 3, b, b + 3, a + 2, a + 5, b + 2, a + 5, b + 5, b + 2)
105
+ }
106
+ const geometry = new THREE.BufferGeometry()
107
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
108
+ geometry.setIndex(indices)
109
+ geometry.computeVertexNormals()
110
+ return geometry
111
+ }
112
+
113
+ function frameQuaternion(tangent: THREE.Vector3, up: THREE.Vector3, normal: THREE.Vector3): THREE.Quaternion {
114
+ const x = tangent.clone().normalize()
115
+ let z = normal.clone().addScaledVector(x, -normal.dot(x)).normalize()
116
+ if (z.lengthSq() < 0.5) z = new THREE.Vector3().crossVectors(x, up).normalize()
117
+ const y = new THREE.Vector3().crossVectors(z, x).normalize()
118
+ z = new THREE.Vector3().crossVectors(x, y).normalize()
119
+ return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(x, y, z))
120
+ }
121
+
122
+ function sampleAt(run: SampledRun, distance: number): JungleFlowerHedgePathFrame {
123
+ return canonicalWallFrameAt(run, distance)
124
+ }
125
+
126
+ function createSweptCoreGeometry(run: SampledRun, height: number): THREE.BufferGeometry {
127
+ const crossSegments = 32
128
+ const ringCount = Math.max(2, Math.ceil(run.length / 0.12) + 1)
129
+ const positions: number[] = []
130
+ const uvs: number[] = []
131
+ const indices: number[] = []
132
+ const exponent = 0.34
133
+ for (let ring = 0; ring < ringCount; ring += 1) {
134
+ const distance = run.length * ring / (ringCount - 1)
135
+ const frame = sampleAt(run, distance)
136
+ const endScale = ring === 0 || ring === ringCount - 1 ? 0.94 : 1
137
+ for (let segment = 0; segment <= crossSegments; segment += 1) {
138
+ const angle = segment / crossSegments * Math.PI * 2 - Math.PI / 2
139
+ const zProfile = Math.sign(Math.cos(angle)) * Math.pow(Math.abs(Math.cos(angle)), exponent)
140
+ const yProfile = Math.sign(Math.sin(angle)) * Math.pow(Math.abs(Math.sin(angle)), exponent)
141
+ let y = height * (0.5 + yProfile * 0.5)
142
+ if (y < height * 0.075) y *= 0.35
143
+ y = height / 2 + (y - height / 2) * endScale
144
+ const z = zProfile * HEDGE_DEPTH / 2 * endScale
145
+ const broad = Math.sin(distance * 1.47 + z * 3.1) * Math.cos(y * 5.2 - distance * 0.37)
146
+ const fine = Math.sin(distance * 6.7 + y * 9.3 + z * 5.8) * 0.36
147
+ const displacement = y < height * 0.07 ? 0 : (broad + fine) * 0.013
148
+ const point = frame.base.clone().addScaledVector(frame.up, y).addScaledVector(frame.outward, z + displacement * Math.sign(z || 1))
149
+ positions.push(point.x, point.y, point.z)
150
+ uvs.push(run.length > 0 ? distance / run.length : 0, segment / crossSegments)
151
+ }
152
+ }
153
+ const stride = crossSegments + 1
154
+ for (let ring = 0; ring < ringCount - 1; ring += 1) {
155
+ for (let segment = 0; segment < crossSegments; segment += 1) {
156
+ const a = ring * stride + segment
157
+ const b = a + stride
158
+ indices.push(a, b, a + 1, a + 1, b, b + 1)
159
+ }
160
+ }
161
+ const addCap = (ring: number, reverse: boolean) => {
162
+ const frame = sampleAt(run, ring === 0 ? 0 : run.length)
163
+ const center = positions.length / 3
164
+ positions.push(frame.base.x + frame.up.x * height / 2, frame.base.y + frame.up.y * height / 2, frame.base.z + frame.up.z * height / 2)
165
+ uvs.push(ring === 0 ? 0 : 1, 0.5)
166
+ for (let segment = 0; segment < crossSegments; segment += 1) {
167
+ const a = ring * stride + segment
168
+ if (reverse) indices.push(center, a, a + 1)
169
+ else indices.push(center, a + 1, a)
170
+ }
171
+ }
172
+ addCap(0, true)
173
+ addCap(ringCount - 1, false)
174
+ const geometry = new THREE.BufferGeometry()
175
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
176
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
177
+ geometry.setIndex(indices)
178
+ geometry.computeVertexNormals()
179
+ geometry.computeBoundingSphere()
180
+ return geometry
181
+ }
182
+
183
+ function createAssets(): HedgeAssets {
184
+ const leafMaterials = [
185
+ standardMaterial('#3b5429', 0.9, '#081307', 0.15), standardMaterial('#4e6931', 0.9, '#0a1708', 0.14),
186
+ standardMaterial('#304823', 0.92, '#071106', 0.16), standardMaterial('#60733a', 0.88, '#0c1908', 0.12),
187
+ ]
188
+ leafMaterials.forEach(material => { material.vertexColors = true; material.flatShading = true })
189
+ const flowerMaterials = {
190
+ orange: standardMaterial('#d56a38', 0.76, '#3f1209', 0.18), coral: standardMaterial('#cf6043', 0.78, '#42130b', 0.17),
191
+ coralLight: standardMaterial('#dd805b', 0.76, '#48170d', 0.16), magenta: standardMaterial('#ad3564', 0.78, '#35081c', 0.19),
192
+ orchidPink: standardMaterial('#cf6895', 0.76, '#3b0d28', 0.17), deepPink: standardMaterial('#7d2849', 0.8, '#260512', 0.17),
193
+ gold: standardMaterial('#d2a044', 0.8, '#442807', 0.16), cream: standardMaterial('#e4d0b8', 0.78, '#432718', 0.1),
194
+ }
195
+ Object.values(flowerMaterials).forEach(material => { material.flatShading = true })
196
+ return {
197
+ leafGeometry: createLeafGeometry(), petalGeometry: createPetalGeometry(), sphereGeometry: new VisualSphereGeometry(1, 12, 8),
198
+ stemGeometry: new VisualCylinderGeometry(0.012, 0.016, 0.12, 7), stamenGeometry: new VisualCylinderGeometry(0.007, 0.006, 0.08, 6),
199
+ trumpetGeometry: new VisualCylinderGeometry(0.11, 0.038, 0.21, 10, 3, true), leafMaterials, leafMatrices: [[], [], [], []], flowerMaterials,
200
+ centreMaterials: { amber: standardMaterial('#77311d', 0.82, '#210502', 0.12), gold: standardMaterial('#d8b05a', 0.78, '#402806', 0.14) },
201
+ stemMaterial: standardMaterial('#315716', 0.94), calyxMaterial: standardMaterial('#3c681b', 0.94),
202
+ }
203
+ }
204
+
205
+ function pushLeaf(assets: HedgeAssets, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, length: number, roll: number, seed: number): void {
206
+ const leaf = new THREE.Object3D()
207
+ leaf.position.copy(position)
208
+ leaf.quaternion.copy(frameQuaternion(frame.tangent, frame.up, normal))
209
+ leaf.rotateZ(roll)
210
+ leaf.scale.set(length, length, length * (0.82 + hash01(seed * 7 + 13) * 0.26))
211
+ leaf.updateMatrix()
212
+ assets.leafMatrices[Math.abs(seed) % assets.leafMatrices.length]!.push(leaf.matrix.clone())
213
+ }
214
+
215
+ function addRosette(assets: HedgeAssets, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, radius: number, phase: number, seed: number): void {
216
+ for (let index = 0; index < 5; index += 1) {
217
+ const angle = phase + index * Math.PI * 0.4 + (hash01(seed + index * 3) - 0.5) * 0.24
218
+ pushLeaf(assets, frame, position, normal, radius * (0.78 + hash01(seed + index * 7 + 1) * 0.28), angle, seed + index)
219
+ }
220
+ }
221
+
222
+ function populateFoliage(assets: HedgeAssets, run: SampledRun, height: number, runSeed: number): void {
223
+ const columns = Math.max(2, Math.ceil(run.length / 0.26))
224
+ for (const face of [-1, 1]) {
225
+ for (let column = 0; column <= columns; column += 1) {
226
+ const distance = run.length * column / columns
227
+ const frame = sampleAt(run, distance)
228
+ for (let row = 0; row < 5; row += 1) {
229
+ const seed = runSeed + column * 17 + row * 41 + face * 113
230
+ const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.08 + hash01(seed + 4) * 0.14).normalize()
231
+ const length = 0.21 + hash01(seed + 5) * 0.12
232
+ const position = frame.base.clone().addScaledVector(frame.up, 0.025 + row * height * 0.2 + (hash01(seed + 2) - 0.5) * 0.085)
233
+ .addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.018 + hash01(seed + 3) * 0.018))
234
+ .addScaledVector(frame.tangent, (hash01(seed) - 0.5) * 0.12)
235
+ const roll = (hash01(seed + 6) - 0.5) * 1.38 + (row % 2 ? 0.16 : -0.16)
236
+ pushLeaf(assets, frame, position, normal, length, roll, seed)
237
+ if (hash01(seed + 9) > 0.48) pushLeaf(assets, frame, position.clone().addScaledVector(normal, 0.012), normal, length * (0.68 + hash01(seed + 11) * 0.12), roll + (hash01(seed + 12) > 0.5 ? 0.92 : -0.92), seed + 1)
238
+ }
239
+ const shoulderSeed = runSeed + 971 + column * 37 + face * 83
240
+ const shoulderNormal = frame.up.clone().multiplyScalar(0.72).addScaledVector(frame.outward, face * 0.7).normalize()
241
+ const shoulderPosition = frame.base.clone().addScaledVector(frame.up, height - 0.075 + (hash01(shoulderSeed + 1) - 0.5) * 0.055)
242
+ .addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 - 0.02))
243
+ pushLeaf(assets, frame, shoulderPosition, shoulderNormal, 0.22 + hash01(shoulderSeed + 3) * 0.1, (hash01(shoulderSeed + 4) - 0.5) * 1.5, shoulderSeed)
244
+ }
245
+ const rosettes = Math.max(2, Math.ceil(run.length * 4.5))
246
+ for (let index = 0; index < rosettes; index += 1) {
247
+ const seed = runSeed + 1801 + index * 53 + face * 127
248
+ const frame = sampleAt(run, 0.14 + hash01(seed) * Math.max(0, run.length - 0.28))
249
+ const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.1).normalize()
250
+ const position = frame.base.clone().addScaledVector(frame.up, 0.1 + hash01(seed + 1) * (height - 0.22)).addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.045))
251
+ addRosette(assets, frame, position, normal, 0.16 + hash01(seed + 2) * 0.07, hash01(seed + 3) * Math.PI * 2, seed)
252
+ }
253
+ }
254
+ const crownLeaves = Math.max(4, Math.ceil(run.length * 26.75))
255
+ for (let index = 0; index < crownLeaves; index += 1) {
256
+ const seed = runSeed + 701 + index * 43
257
+ const frame = sampleAt(run, 0.08 + hash01(seed) * Math.max(0, run.length - 0.16))
258
+ const position = frame.base.clone().addScaledVector(frame.up, height + 0.01 + hash01(seed + 2) * 0.024).addScaledVector(frame.outward, -HEDGE_DEPTH * 0.39 + hash01(seed + 1) * HEDGE_DEPTH * 0.78)
259
+ const length = 0.19 + hash01(seed + 3) * 0.12
260
+ pushLeaf(assets, frame, position, frame.up, length, hash01(seed + 4) * Math.PI * 2, seed)
261
+ if (hash01(seed + 8) > 0.58) pushLeaf(assets, frame, position.clone().addScaledVector(frame.up, 0.008), frame.up, length * 0.7, hash01(seed + 4) * Math.PI * 2 + 1, seed + 2)
262
+ }
263
+ for (const end of [0, run.length]) {
264
+ const frame = sampleAt(run, end)
265
+ const normal = frame.tangent.clone().multiplyScalar(end === 0 ? -1 : 1).addScaledVector(frame.up, 0.08).normalize()
266
+ for (let index = 0; index < 12; index += 1) {
267
+ const seed = runSeed + 3209 + index * 71 + (end === 0 ? -101 : 101)
268
+ const position = frame.base.clone().addScaledVector(frame.up, 0.12 + hash01(seed) * (height - 0.25)).addScaledVector(frame.outward, -HEDGE_DEPTH * 0.27 + hash01(seed + 1) * HEDGE_DEPTH * 0.54)
269
+ addRosette(assets, frame, position, normal, 0.15 + hash01(seed + 2) * 0.06, hash01(seed + 3) * Math.PI * 2, seed)
270
+ }
271
+ }
272
+ }
273
+
274
+ function petal(assets: HedgeAssets, material: THREE.Material, angle: number, width: number, length: number, lift = 0, twist = 0): THREE.Mesh {
275
+ const mesh = new THREE.Mesh(assets.petalGeometry, material)
276
+ mesh.scale.set(width, length, Math.min(width, length) * 1.1)
277
+ mesh.rotation.set(lift, 0, -angle + twist)
278
+ mesh.position.set(Math.sin(angle) * 0.012, Math.cos(angle) * 0.012, 0.018)
279
+ mesh.castShadow = true
280
+ mesh.receiveShadow = true
281
+ return mesh
282
+ }
283
+
284
+ function addCentre(group: THREE.Group, assets: HedgeAssets, color: string, radius = 0.035, z = 0.065): void {
285
+ const centre = new THREE.Mesh(assets.sphereGeometry, assets.centreMaterials[color]!)
286
+ centre.scale.set(radius, radius, radius * 0.72)
287
+ centre.position.z = z
288
+ centre.castShadow = true
289
+ group.add(centre)
290
+ }
291
+
292
+ function makeLily(assets: HedgeAssets): THREE.Group {
293
+ const group = new THREE.Group()
294
+ for (let index = 0; index < 6; index += 1) {
295
+ const angle = index * Math.PI / 3 + 0.08
296
+ group.add(petal(assets, assets.flowerMaterials.orange!, angle, 0.09, 0.19 + (index % 2) * 0.018, (index % 3 - 1) * 0.06))
297
+ }
298
+ addCentre(group, assets, 'amber', 0.039, 0.075)
299
+ for (let index = 0; index < 5; index += 1) {
300
+ const stamen = new THREE.Mesh(assets.stamenGeometry, assets.centreMaterials.gold!)
301
+ const angle = index * Math.PI * 0.4
302
+ stamen.position.set(Math.sin(angle) * 0.04, Math.cos(angle) * 0.04, 0.105)
303
+ stamen.rotation.x = Math.PI / 2
304
+ stamen.castShadow = true
305
+ group.add(stamen)
306
+ }
307
+ return group
308
+ }
309
+
310
+ function makePlumeria(assets: HedgeAssets): THREE.Group {
311
+ const group = new THREE.Group()
312
+ for (let index = 0; index < 5; index += 1) {
313
+ const angle = index * Math.PI * 0.4 + 0.16
314
+ group.add(petal(assets, assets.flowerMaterials.cream!, angle, 0.095, 0.17, -0.04, 0.18))
315
+ }
316
+ addCentre(group, assets, 'gold', 0.035, 0.07)
317
+ return group
318
+ }
319
+
320
+ function makeOrchid(assets: HedgeAssets): THREE.Group {
321
+ const group = new THREE.Group()
322
+ ;[0, 2.08, 4.19].forEach((angle, index) => group.add(petal(assets, assets.flowerMaterials.magenta!, angle, 0.11, 0.18 + index * 0.008, -0.05 + index * 0.04)))
323
+ for (const angle of [-1.05, 1.05]) group.add(petal(assets, assets.flowerMaterials.orchidPink!, angle, 0.075, 0.13, 0.02))
324
+ const lip = new THREE.Mesh(assets.sphereGeometry, assets.flowerMaterials.deepPink!)
325
+ lip.scale.set(0.066, 0.046, 0.045)
326
+ lip.position.set(0, -0.038, 0.085)
327
+ lip.castShadow = true
328
+ group.add(lip)
329
+ addCentre(group, assets, 'gold', 0.026, 0.11)
330
+ return group
331
+ }
332
+
333
+ function makeTrumpet(assets: HedgeAssets): THREE.Group {
334
+ const group = new THREE.Group()
335
+ const bell = new THREE.Mesh(assets.trumpetGeometry, assets.flowerMaterials.coral!)
336
+ bell.rotation.x = Math.PI / 2
337
+ bell.position.z = 0.1
338
+ bell.castShadow = true
339
+ group.add(bell)
340
+ for (let index = 0; index < 5; index += 1) {
341
+ const angle = index * Math.PI * 0.4 + 0.18
342
+ const flare = petal(assets, assets.flowerMaterials.coralLight!, angle, 0.045, 0.095, -0.04, 0.08)
343
+ flare.position.set(Math.sin(angle) * 0.035, Math.cos(angle) * 0.035, 0.185)
344
+ group.add(flare)
345
+ }
346
+ addCentre(group, assets, 'amber', 0.038, 0.218)
347
+ group.rotation.x = -0.24
348
+ return group
349
+ }
350
+
351
+ function makeGoldCluster(assets: HedgeAssets): THREE.Group {
352
+ const group = new THREE.Group()
353
+ const offsets = [[-0.09, -0.02, 0], [0.085, -0.025, 0.012], [-0.035, 0.085, 0.018], [0.055, 0.075, 0.026], [0, -0.105, 0.02]]
354
+ offsets.forEach(([x, y, z], bloomIndex) => {
355
+ const bloom = new THREE.Group()
356
+ for (let index = 0; index < 5; index += 1) bloom.add(petal(assets, assets.flowerMaterials.gold!, index * Math.PI * 0.4, 0.038, 0.07, 0, 0.1))
357
+ addCentre(bloom, assets, 'amber', 0.016, 0.04)
358
+ bloom.position.set(x!, y!, z!)
359
+ bloom.rotation.z = bloomIndex * 0.48
360
+ group.add(bloom)
361
+ })
362
+ return group
363
+ }
364
+
365
+ function makeBotanicalCluster(assets: HedgeAssets, type: FlowerType): THREE.Group {
366
+ const group = new THREE.Group()
367
+ ;[[-0.78, 0.14, -0.05], [0.72, 0.13, 0.045], [2.45, 0.105, 0]].forEach(([roll, length, offset]) => {
368
+ const leaf = new THREE.Mesh(assets.leafGeometry, assets.leafMaterials[2]!)
369
+ leaf.scale.setScalar(length!)
370
+ leaf.rotation.z = roll!
371
+ leaf.position.set(offset!, -0.035, -0.045)
372
+ leaf.castShadow = true
373
+ group.add(leaf)
374
+ })
375
+ const stem = new THREE.Mesh(assets.stemGeometry, assets.stemMaterial)
376
+ stem.rotation.x = Math.PI / 2
377
+ stem.position.z = -0.015
378
+ group.add(stem)
379
+ const calyx = new THREE.Mesh(assets.sphereGeometry, assets.calyxMaterial)
380
+ calyx.scale.set(0.075, 0.075, 0.032)
381
+ calyx.position.z = 0.022
382
+ group.add(calyx)
383
+ const flower = type === 'orange' ? makeLily(assets) : type === 'pink' ? makePlumeria(assets) : type === 'magenta' ? makeOrchid(assets) : type === 'coral' ? makeTrumpet(assets) : makeGoldCluster(assets)
384
+ flower.position.z = 0.035
385
+ group.add(flower)
386
+ if (type === 'magenta' || type === 'coral') {
387
+ const maker = type === 'magenta' ? makeOrchid : makeTrumpet
388
+ const left = maker(assets)
389
+ left.scale.setScalar(type === 'magenta' ? 0.58 : 0.62)
390
+ left.position.set(type === 'magenta' ? -0.145 : -0.12, 0.05, 0.05)
391
+ left.rotation.z = type === 'magenta' ? -0.28 : -0.48
392
+ group.add(left)
393
+ const right = maker(assets)
394
+ right.scale.setScalar(type === 'magenta' ? 0.52 : 0.56)
395
+ right.position.set(type === 'magenta' ? 0.145 : 0.115, 0, 0.045)
396
+ right.rotation.z = type === 'magenta' ? 0.32 : 0.52
397
+ group.add(right)
398
+ }
399
+ return group
400
+ }
401
+
402
+ function addFlowerCluster(root: THREE.Group, assets: HedgeAssets, type: FlowerType, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, scale: number, roll: number): void {
403
+ const cluster = makeBotanicalCluster(assets, type)
404
+ cluster.name = `jungle-v4-${type}-cluster`
405
+ cluster.position.copy(position)
406
+ cluster.quaternion.copy(frameQuaternion(frame.tangent, frame.up, normal))
407
+ cluster.rotateZ(roll)
408
+ cluster.scale.setScalar(scale * 1.1)
409
+ cluster.userData.decorative = true
410
+ cluster.userData.visualOnly = true
411
+ root.add(cluster)
412
+ }
413
+
414
+ function populateFlowers(root: THREE.Group, assets: HedgeAssets, run: SampledRun, height: number, runSeed: number): void {
415
+ const faceSlots = Math.max(1, Math.round(run.length * 0.75))
416
+ for (const face of [-1, 1]) {
417
+ const schedule = face > 0 ? FLOWER_SCHEDULE : REAR_FLOWER_SCHEDULE
418
+ for (let index = 0; index < faceSlots; index += 1) {
419
+ const seed = runSeed + 5003 + index * 89 + face * 137
420
+ const distance = (index + 0.5) * run.length / faceSlots
421
+ const frame = sampleAt(run, distance)
422
+ const type = schedule[index % schedule.length]!
423
+ const y = height * (0.31 + hash01(seed) * 0.36)
424
+ const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.08).normalize()
425
+ const position = frame.base.clone().addScaledVector(frame.up, y).addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.105))
426
+ addFlowerCluster(root, assets, type, frame, position, normal, 0.88 + hash01(seed + 2) * 0.12, (hash01(seed + 3) - 0.5) * 0.4 * face)
427
+ }
428
+ }
429
+ const crownSlots = Math.max(1, Math.round(run.length * 0.5))
430
+ for (let index = 0; index < crownSlots; index += 1) {
431
+ const seed = runSeed + 6101 + index * 97
432
+ const frame = sampleAt(run, (index + 0.5) * run.length / crownSlots)
433
+ const position = frame.base.clone().addScaledVector(frame.up, height + 0.09).addScaledVector(frame.outward, (hash01(seed) - 0.5) * HEDGE_DEPTH * 0.28)
434
+ addFlowerCluster(root, assets, CROWN_FLOWER_SCHEDULE[index % CROWN_FLOWER_SCHEDULE.length]!, frame, position, frame.up, 0.86 + hash01(seed + 1) * 0.09, (hash01(seed + 2) - 0.5) * 0.4)
435
+ }
436
+ }
437
+
438
+ /** Adds the approved tropical hedge as visual-only geometry over unchanged wall colliders. */
439
+ export function addTropicalFlowerHedgeVisual(root: THREE.Group, id: string, runs: readonly JungleFlowerHedgePathRun[], wallHeight: number, coreMaterial: THREE.Material): THREE.Group | undefined {
440
+ if (wallHeight <= 0.18) return undefined
441
+ const height = Math.min(0.95, wallHeight)
442
+ const group = new THREE.Group()
443
+ group.name = `${id}-jungle-flower-hedge`
444
+ group.userData.themeOnly = PAINTED_VALLEY_THEME_ID
445
+ group.userData.decorative = true
446
+ group.userData.visualOnly = true
447
+ group.userData.appearance = 'tropical-flower-hedge'
448
+ group.userData.approvedVisualContract = 'jungle-short-flower-hedge-v4.2'
449
+ const assets = createAssets()
450
+ runs.forEach((pathRun, runIndex) => {
451
+ if (pathRun.frames.length < 2) return
452
+ const run = pathRun
453
+ if (run.length < 0.02) return
454
+ const core = new THREE.Mesh(createSweptCoreGeometry(run, height), coreMaterial)
455
+ core.name = `${id}-jungle-v4-clipped-hedge-core-${runIndex}`
456
+ core.castShadow = true
457
+ core.receiveShadow = true
458
+ core.userData.visualOnly = true
459
+ group.add(core)
460
+ populateFoliage(assets, run, height, runIndex * 10_007)
461
+ populateFlowers(group, assets, run, height, runIndex * 10_007)
462
+ })
463
+ assets.leafMatrices.forEach((matrices, index) => {
464
+ const mesh = new THREE.InstancedMesh(assets.leafGeometry, assets.leafMaterials[index]!, matrices.length)
465
+ matrices.forEach((matrix, matrixIndex) => mesh.setMatrixAt(matrixIndex, matrix))
466
+ mesh.instanceMatrix.needsUpdate = true
467
+ mesh.name = `${id}-jungle-v4-foliage-family-${index}`
468
+ mesh.castShadow = true
469
+ mesh.receiveShadow = true
470
+ mesh.userData.visualOnly = true
471
+ group.add(mesh)
472
+ })
473
+ group.traverse(object => {
474
+ if (object instanceof THREE.Mesh) {
475
+ object.userData.appearanceMaterialId = PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID
476
+ }
477
+ })
478
+ root.add(group)
479
+ return group
480
+ }
481
+
482
+ /** Backwards-compatible export for existing Track World call sites. */
483
+ export const addJungleFlowerHedgeVisual = addTropicalFlowerHedgeVisual