@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,159 @@
1
+ import { geometryDetailModelUrl } from '../../../three/geometry-detail'
2
+ import * as THREE from 'three'
3
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
4
+
5
+ export const EGYPT_ELEVATOR_SHELL_MODEL_URL = '/models/elevators/egypt-elevator-shell-v5.glb'
6
+ export const EGYPT_ELEVATOR_PLATFORM_MODEL_URL = '/models/elevators/egypt-elevator-platform-v5.glb'
7
+ export const EGYPT_ELEVATOR_SHELL_RUNTIME_YAW = Math.PI
8
+
9
+ export interface EgyptElevatorBlenderMaterials {
10
+ sandstone: THREE.Material
11
+ carvedStone: THREE.Material
12
+ pyramidStone: THREE.Material
13
+ pyramidGold: THREE.Material
14
+ bronze: THREE.Material
15
+ paving: THREE.Material
16
+ }
17
+
18
+ type EgyptRole = 'SANDSTONE' | 'CARVED' | 'LIMESTONE' | 'GOLD' | 'BRONZE' | 'PAVING'
19
+
20
+ const PLACEHOLDER = new THREE.MeshBasicMaterial({ visible: false })
21
+ let shellTemplatePromise: Promise<THREE.Object3D> | undefined
22
+ let platformTemplatePromise: Promise<THREE.Object3D> | undefined
23
+
24
+ function roleFromName(name: string): EgyptRole | undefined {
25
+ return (['SANDSTONE', 'CARVED', 'LIMESTONE', 'GOLD', 'BRONZE', 'PAVING'] as const).find(role =>
26
+ name.toUpperCase().includes(`_${role}`),
27
+ )
28
+ }
29
+
30
+ function loadTemplate(url: string, name: string): Promise<THREE.Object3D> {
31
+ return new GLTFLoader().loadAsync(geometryDetailModelUrl(url)).then(gltf => {
32
+ gltf.scene.name = name
33
+ gltf.scene.traverse(object => {
34
+ if (!(object instanceof THREE.Mesh)) return
35
+ const embedded = Array.isArray(object.material) ? object.material : [object.material]
36
+ embedded.forEach(material => material.dispose())
37
+ object.material = PLACEHOLDER
38
+ })
39
+ return gltf.scene
40
+ })
41
+ }
42
+
43
+ function shellTemplate(): Promise<THREE.Object3D> {
44
+ shellTemplatePromise ??= loadTemplate(EGYPT_ELEVATOR_SHELL_MODEL_URL, 'egypt-elevator-shell-template')
45
+ return shellTemplatePromise
46
+ }
47
+
48
+ function platformTemplate(): Promise<THREE.Object3D> {
49
+ platformTemplatePromise ??= loadTemplate(EGYPT_ELEVATOR_PLATFORM_MODEL_URL, 'egypt-elevator-platform-template')
50
+ return platformTemplatePromise
51
+ }
52
+
53
+ function materialForRole(role: EgyptRole, materials: EgyptElevatorBlenderMaterials): THREE.Material {
54
+ if (role === 'SANDSTONE') return materials.sandstone
55
+ if (role === 'CARVED') return materials.carvedStone
56
+ if (role === 'LIMESTONE') return materials.pyramidStone
57
+ if (role === 'GOLD') return materials.pyramidGold
58
+ if (role === 'BRONZE') return materials.bronze
59
+ return materials.paving
60
+ }
61
+
62
+ export function applyEgyptElevatorBlenderMaterials(
63
+ root: THREE.Object3D,
64
+ materials: EgyptElevatorBlenderMaterials,
65
+ ): void {
66
+ root.traverse(object => {
67
+ if (!(object instanceof THREE.Mesh)) return
68
+ const role = object.userData.egyptElevatorRole as EgyptRole | undefined
69
+ if (role) object.material = materialForRole(role, materials)
70
+ })
71
+ }
72
+
73
+ function attachModel(
74
+ host: THREE.Group,
75
+ template: THREE.Object3D,
76
+ name: string,
77
+ materials: EgyptElevatorBlenderMaterials,
78
+ ): THREE.Object3D {
79
+ const model = template.clone(true)
80
+ model.name = name
81
+ let meshCount = 0
82
+ let triangleCount = 0
83
+ model.traverse(object => {
84
+ object.userData.decorative = true
85
+ object.userData.visualOnly = true
86
+ object.userData.elevatorStyle = 'approved-egypt-blender-v5'
87
+ if (!(object instanceof THREE.Mesh)) return
88
+ const role = roleFromName(object.name)
89
+ if (!role) {
90
+ object.visible = false
91
+ return
92
+ }
93
+ object.material = materialForRole(role, materials)
94
+ object.castShadow = true
95
+ object.receiveShadow = true
96
+ object.frustumCulled = true
97
+ object.userData.egyptElevatorRole = role
98
+ object.userData.sharedGeometry = true
99
+ meshCount += 1
100
+ const position = object.geometry.getAttribute('position')
101
+ triangleCount += object.geometry.index ? object.geometry.index.count / 3 : position.count / 3
102
+ })
103
+ host.userData.loadedMeshCount = meshCount
104
+ host.userData.loadedTriangleCount = triangleCount
105
+ host.add(model)
106
+ return model
107
+ }
108
+
109
+ export function addEgyptElevatorBlenderShell(
110
+ host: THREE.Group,
111
+ name: string,
112
+ materials: EgyptElevatorBlenderMaterials,
113
+ scale: THREE.Vector3,
114
+ template?: THREE.Object3D,
115
+ ): Promise<THREE.Group> {
116
+ host.userData.modelUrl = EGYPT_ELEVATOR_SHELL_MODEL_URL
117
+ host.userData.approvedReference = '/docs/material-contracts/theme-elevator-egypt-approved-v3.png'
118
+ host.userData.constructionContract = 'egypt-elevator-approved-blender-v5'
119
+ host.userData.visualOnly = true
120
+ if (typeof document === 'undefined' && !template) {
121
+ host.userData.modelReady = Promise.resolve(host)
122
+ return host.userData.modelReady as Promise<THREE.Group>
123
+ }
124
+ const ready = template ? Promise.resolve(template) : shellTemplate()
125
+ host.userData.modelReady = ready.then(source => {
126
+ const model = attachModel(host, source, `${name}-egypt-approved-blender-shell`, materials)
127
+ model.scale.copy(scale)
128
+ // Blender +Y exports as Three.js -Z. The conveyor loading/front side is
129
+ // local +Z, so turn the authored shell around while leaving the symmetric
130
+ // moving platform cassettes and all mechanism physics unchanged.
131
+ model.rotation.y = EGYPT_ELEVATOR_SHELL_RUNTIME_YAW
132
+ model.userData.runtimeFront = 'local-positive-z'
133
+ model.userData.runtimeRelease = 'local-negative-z'
134
+ return host
135
+ })
136
+ return host.userData.modelReady as Promise<THREE.Group>
137
+ }
138
+
139
+ export function addEgyptElevatorBlenderPlatform(
140
+ host: THREE.Group,
141
+ name: string,
142
+ materials: EgyptElevatorBlenderMaterials,
143
+ scale: THREE.Vector3,
144
+ template?: THREE.Object3D,
145
+ ): Promise<THREE.Group> {
146
+ host.userData.modelUrl = EGYPT_ELEVATOR_PLATFORM_MODEL_URL
147
+ host.userData.visualOnly = true
148
+ if (typeof document === 'undefined' && !template) {
149
+ host.userData.modelReady = Promise.resolve(host)
150
+ return host.userData.modelReady as Promise<THREE.Group>
151
+ }
152
+ const ready = template ? Promise.resolve(template) : platformTemplate()
153
+ host.userData.modelReady = ready.then(source => {
154
+ const model = attachModel(host, source, `${name}-egypt-approved-blender-platform`, materials)
155
+ model.scale.copy(scale)
156
+ return host
157
+ })
158
+ return host.userData.modelReady as Promise<THREE.Group>
159
+ }
@@ -0,0 +1,382 @@
1
+ import * as THREE from 'three'
2
+
3
+ import { VisualCylinderGeometry, VisualTorusGeometry, VisualCircleGeometry, VisualExtrudeGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
4
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
5
+
6
+ import { PORTAL_APERTURE_HALF_WIDTH, PORTAL_THEME_FRAME_X_SCALE } from './portal-proportions'
7
+
8
+ export const EGYPT_PORTAL_FRAME_THEME_ID = 'ancient-egypt'
9
+
10
+ export interface EgyptPortalFrameMaterials {
11
+ sandstone: THREE.Material
12
+ carvedStone: THREE.Material
13
+ agedGold: THREE.Material
14
+ }
15
+
16
+ export interface EgyptPortalFrameOptions {
17
+ /** Portal-pair identity crystal and light; architectural reuse should disable this. */
18
+ includePairSignal?: boolean
19
+ }
20
+
21
+ interface GeometryBatch {
22
+ name: string
23
+ material: THREE.Material
24
+ parts: THREE.BufferGeometry[]
25
+ }
26
+
27
+ const IDENTITY_QUATERNION = new THREE.Quaternion()
28
+ const UNIT_SCALE = new THREE.Vector3(1, 1, 1)
29
+ const STONE_FRONT_Z = 0.5
30
+ const CARVED_PANEL_FRONT_Z = 0.6
31
+
32
+ /**
33
+ * Adds the visual-only Ancient Egypt housing around the shared portal aperture.
34
+ * The live camera surface, black aperture, rift glow and particles deliberately
35
+ * remain owned by portal-visual.ts.
36
+ */
37
+ export function addEgyptPortalFrame(
38
+ root: THREE.Group,
39
+ name: string,
40
+ color: string,
41
+ materials: EgyptPortalFrameMaterials,
42
+ options: EgyptPortalFrameOptions = {},
43
+ ): THREE.Group {
44
+ const frame = new THREE.Group()
45
+ frame.name = `${name}-egypt-temple-frame`
46
+ frame.userData.themeOnly = EGYPT_PORTAL_FRAME_THEME_ID
47
+ frame.userData.decorative = true
48
+ frame.userData.visualOnly = true
49
+ frame.userData.portalFrameStyle = 'egypt-sandstone-pylon'
50
+ frame.scale.x = PORTAL_THEME_FRAME_X_SCALE
51
+ frame.userData.portalApertureClearHalfWidth = PORTAL_APERTURE_HALF_WIDTH
52
+ frame.userData.drawCallBudget = 4
53
+ frame.userData.triangleBudget = 8_000
54
+
55
+ const stone: THREE.BufferGeometry[] = []
56
+ const carvings: THREE.BufferGeometry[] = []
57
+ const gold: THREE.BufferGeometry[] = []
58
+
59
+ addPylonStone(stone)
60
+ addHieroglyphRelief(carvings)
61
+ addGoldInlays(gold)
62
+
63
+ addBatch(frame, name, { name: 'egypt-sandstone-pylon', material: materials.sandstone, parts: stone })
64
+ const carvedMesh = addBatch(frame, name, {
65
+ name: 'egypt-hieroglyph-relief',
66
+ material: materials.carvedStone,
67
+ parts: carvings,
68
+ })
69
+ carvedMesh.userData.motifs = ['ankh', 'lotus', 'reed-glyphs']
70
+ const goldMesh = addBatch(frame, name, {
71
+ name: 'egypt-aged-gold-inlays',
72
+ material: materials.agedGold,
73
+ parts: gold,
74
+ })
75
+ goldMesh.userData.motifs = ['winged-sun', 'pylon-bands', 'lotus-caps']
76
+ goldMesh.userData.crownTrimStyle = 'continuous-swept-gold-wings'
77
+ goldMesh.userData.stoneContactBackZ = STONE_FRONT_Z
78
+ goldMesh.userData.carvedPanelContactBackZ = CARVED_PANEL_FRONT_Z
79
+
80
+ if (options.includePairSignal !== false) addPairCrystal(frame, name, color)
81
+
82
+ frame.traverse(object => {
83
+ object.userData.themeOnly = EGYPT_PORTAL_FRAME_THEME_ID
84
+ object.userData.decorative = true
85
+ object.userData.visualOnly = true
86
+ object.userData.portalFrameStyle = 'egypt-sandstone-pylon'
87
+ if (object instanceof THREE.Mesh) {
88
+ object.castShadow = true
89
+ object.receiveShadow = true
90
+ }
91
+ })
92
+ root.add(frame)
93
+ return frame
94
+ }
95
+
96
+ function addPylonStone(parts: THREE.BufferGeometry[]): void {
97
+ parts.push(createPylonUpright(-1), createPylonUpright(1))
98
+ parts.push(roundedBox(4.92, 0.42, 0.9, 0.06, new THREE.Vector3(0, 1.42, 0)))
99
+ parts.push(roundedBox(5.22, 0.2, 1.02, 0.045, new THREE.Vector3(0, 1.73, -0.01)))
100
+ parts.push(roundedBox(4.76, 0.17, 0.94, 0.04, new THREE.Vector3(0, 1.12, 0.01)))
101
+
102
+ // Projecting masonry courses provide the macro depth visible in the approved
103
+ // pylon concept. Fine erosion remains in the aligned sandstone maps.
104
+ for (const side of [-1, 1] as const) {
105
+ for (let course = 0; course < 6; course += 1) {
106
+ const y = -1.42 + course * 0.47
107
+ const taper = (5 - course) * 0.025
108
+ const width = course % 2 === 0 ? 0.78 : 0.68
109
+ parts.push(
110
+ roundedBox(
111
+ width,
112
+ 0.39,
113
+ 0.98 + (course % 2) * 0.035,
114
+ 0.055,
115
+ new THREE.Vector3(side * (2.16 + taper), y, 0.015 + (course % 2) * 0.018),
116
+ ),
117
+ )
118
+ }
119
+ parts.push(roundedBox(1.02, 0.28, 1.12, 0.055, new THREE.Vector3(side * 2.22, -1.67, -0.02)))
120
+ parts.push(roundedBox(0.9, 0.19, 1.02, 0.045, new THREE.Vector3(side * 2.18, -1.46, 0)))
121
+ parts.push(roundedBox(0.88, 0.24, 1.04, 0.05, new THREE.Vector3(side * 2.13, 1.18, 0)))
122
+ }
123
+
124
+ // Slightly uneven foot and crown blocks make the silhouette read as worn,
125
+ // assembled temple masonry without paying for individual wall bricks.
126
+ for (const side of [-1, 1] as const) {
127
+ parts.push(roundedBox(0.48, 0.32, 0.92, 0.045, new THREE.Vector3(side * 1.88, 1.02, 0.01)))
128
+ }
129
+ }
130
+
131
+ function createPylonUpright(side: -1 | 1): THREE.BufferGeometry {
132
+ const shape = new THREE.Shape()
133
+ if (side < 0) {
134
+ shape.moveTo(-2.56, -1.75)
135
+ shape.lineTo(-1.92, -1.75)
136
+ shape.lineTo(-1.82, 1.24)
137
+ shape.lineTo(-2.4, 1.24)
138
+ } else {
139
+ shape.moveTo(1.92, -1.75)
140
+ shape.lineTo(2.56, -1.75)
141
+ shape.lineTo(2.4, 1.24)
142
+ shape.lineTo(1.82, 1.24)
143
+ }
144
+ shape.closePath()
145
+ const geometry = new VisualExtrudeGeometry(shape, {
146
+ depth: 0.9,
147
+ bevelEnabled: true,
148
+ bevelSegments: 2,
149
+ bevelSize: 0.035,
150
+ bevelThickness: 0.025,
151
+ curveSegments: 1,
152
+ steps: 1,
153
+ })
154
+ geometry.translate(0, 0, -0.45)
155
+ return metricPlanarUvGeometry(geometry)
156
+ }
157
+
158
+ function addHieroglyphRelief(parts: THREE.BufferGeometry[]): void {
159
+ const reliefZ = 0.625
160
+ for (const side of [-1, 1] as const) {
161
+ const x = side * 2.14
162
+
163
+ // A separate recessed carved slab makes the inscription read as an
164
+ // architectural panel rather than lines floating on wallpaper.
165
+ parts.push(roundedBox(0.55, 2.2, 0.16, 0.045, new THREE.Vector3(x, -0.16, 0.52)))
166
+
167
+ // Ankh: loop, crossbar and stem.
168
+ parts.push(
169
+ transformed(
170
+ new VisualTorusGeometry(0.095, 0.026, 5, 14),
171
+ new THREE.Vector3(x, 0.58, reliefZ),
172
+ ),
173
+ )
174
+ parts.push(roundedBox(0.055, 0.36, 0.045, 0.012, new THREE.Vector3(x, 0.28, reliefZ)))
175
+ parts.push(roundedBox(0.24, 0.05, 0.045, 0.012, new THREE.Vector3(x, 0.37, reliefZ)))
176
+
177
+ // A compact reed/river glyph stack.
178
+ parts.push(roundedBox(0.055, 0.34, 0.045, 0.012, new THREE.Vector3(x, -0.48, reliefZ)))
179
+ for (const offset of [-0.13, 0.13]) {
180
+ parts.push(
181
+ roundedBox(
182
+ 0.055,
183
+ 0.2,
184
+ 0.045,
185
+ 0.012,
186
+ new THREE.Vector3(x + offset, -0.42, reliefZ),
187
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), side * offset * 2.6),
188
+ ),
189
+ )
190
+ }
191
+
192
+ // Shallow lotus at the foot: one stem and three low-poly petals.
193
+ parts.push(roundedBox(0.045, 0.28, 0.045, 0.01, new THREE.Vector3(x, -1.18, reliefZ)))
194
+ for (const [petalIndex, offset] of [-0.1, 0, 0.1].entries()) {
195
+ const rotation = (petalIndex - 1) * -0.42
196
+ parts.push(
197
+ transformed(
198
+ new VisualCircleGeometry(0.095, 5),
199
+ new THREE.Vector3(x + offset, -1.01, reliefZ + 0.024),
200
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), rotation),
201
+ new THREE.Vector3(0.66, 1.28, 1),
202
+ ),
203
+ )
204
+ }
205
+ }
206
+ }
207
+
208
+ function addGoldInlays(parts: THREE.BufferGeometry[]): void {
209
+ const trimDepth = 0.055
210
+ const stoneTrimCenterZ = STONE_FRONT_Z + trimDepth * 0.5 - 0.004
211
+ const panelTrimCenterZ = CARVED_PANEL_FRONT_Z + trimDepth * 0.5 - 0.004
212
+ for (const side of [-1, 1] as const) {
213
+ parts.push(
214
+ roundedBox(0.72, 0.085, trimDepth, 0.018, new THREE.Vector3(side * 2.17, 0.91, panelTrimCenterZ)),
215
+ )
216
+ parts.push(
217
+ roundedBox(0.76, 0.085, trimDepth, 0.018, new THREE.Vector3(side * 2.21, -0.86, panelTrimCenterZ)),
218
+ )
219
+ }
220
+ parts.push(roundedBox(4.7, 0.07, trimDepth, 0.016, new THREE.Vector3(0, 1.3, stoneTrimCenterZ)))
221
+ parts.push(roundedBox(4.98, 0.06, trimDepth, 0.014, new THREE.Vector3(0, 1.72, stoneTrimCenterZ)))
222
+
223
+ // Geometry-first winged sun socket: a central disc with two continuous
224
+ // swept wings. The previous stack of short offset bars read as a dark
225
+ // staircase/bridge instead of Egyptian gold ornament.
226
+ parts.push(
227
+ transformed(
228
+ new VisualCylinderGeometry(0.3, 0.3, 0.065, 20),
229
+ new THREE.Vector3(0, 1.49, STONE_FRONT_Z + 0.028),
230
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
231
+ ),
232
+ )
233
+ for (const side of [-1, 1] as const) {
234
+ parts.push(createSweptGoldWing(side, STONE_FRONT_Z))
235
+ }
236
+ }
237
+
238
+ function createSweptGoldWing(side: -1 | 1, z: number): THREE.BufferGeometry {
239
+ const shape = new THREE.Shape()
240
+ shape.moveTo(side * 0.26, 1.48)
241
+ shape.quadraticCurveTo(side * 0.94, 1.68, side * 1.76, 1.59)
242
+ shape.lineTo(side * 1.76, 1.48)
243
+ shape.quadraticCurveTo(side * 0.98, 1.45, side * 0.26, 1.4)
244
+ shape.closePath()
245
+ const wing = new VisualExtrudeGeometry(shape, {
246
+ depth: 0.075,
247
+ bevelEnabled: true,
248
+ bevelSegments: 2,
249
+ bevelSize: 0.014,
250
+ bevelThickness: 0.012,
251
+ curveSegments: 8,
252
+ steps: 1,
253
+ })
254
+ wing.translate(0, 0, z)
255
+ return metricPlanarUvGeometry(wing)
256
+ }
257
+
258
+ function addPairCrystal(frame: THREE.Group, name: string, color: string): void {
259
+ const crystalMaterial = new THREE.MeshPhysicalMaterial({
260
+ color,
261
+ emissive: color,
262
+ emissiveIntensity: 1.25,
263
+ metalness: 0.04,
264
+ roughness: 0.16,
265
+ // A tiny decorative gem must not trigger a full-scene refraction pass.
266
+ transmission: 0,
267
+ thickness: 0.2,
268
+ clearcoat: 1,
269
+ clearcoatRoughness: 0.1,
270
+ })
271
+ crystalMaterial.name = `${name}-egypt-pair-crystal-material`
272
+
273
+ const crystalParts = [
274
+ transformed(
275
+ new THREE.OctahedronGeometry(0.22, 0),
276
+ new THREE.Vector3(0, 1.5, 0.66),
277
+ IDENTITY_QUATERNION,
278
+ new THREE.Vector3(0.82, 1.18, 0.56),
279
+ ),
280
+ transformed(
281
+ new THREE.OctahedronGeometry(0.105, 0),
282
+ new THREE.Vector3(-0.2, 1.46, 0.61),
283
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -0.5),
284
+ new THREE.Vector3(0.65, 1.25, 0.5),
285
+ ),
286
+ transformed(
287
+ new THREE.OctahedronGeometry(0.105, 0),
288
+ new THREE.Vector3(0.2, 1.46, 0.61),
289
+ new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), 0.5),
290
+ new THREE.Vector3(0.65, 1.25, 0.5),
291
+ ),
292
+ ]
293
+ const crystalGeometry = mergeGeometries(crystalParts, false)
294
+ crystalParts.forEach(part => part.dispose())
295
+ if (!crystalGeometry) throw new Error('Unable to merge Egypt portal pair crystal geometry.')
296
+ crystalGeometry.computeBoundingBox()
297
+ crystalGeometry.computeBoundingSphere()
298
+ crystalGeometry.userData.visualOnly = true
299
+ const crystal = new THREE.Mesh(crystalGeometry, crystalMaterial)
300
+ crystal.name = `${name}-egypt-pair-crystal`
301
+ crystal.userData.ownsMaterial = true
302
+ crystal.userData.portalPairColor = color
303
+ crystal.userData.portalLightSource = 'faceted-scarab-crystal'
304
+ frame.add(crystal)
305
+
306
+ const light = new THREE.PointLight(color, 0.55, 3.2, 2)
307
+ light.name = `${name}-egypt-pair-crystal-light`
308
+ light.position.set(0, 1.45, 0.78)
309
+ light.userData.portalPairColor = color
310
+ light.userData.portalLightSource = 'faceted-scarab-crystal'
311
+ frame.add(light)
312
+ }
313
+
314
+ function addBatch(frame: THREE.Group, name: string, batch: GeometryBatch): THREE.Mesh {
315
+ const geometry = mergeGeometries(batch.parts, false)
316
+ batch.parts.forEach(part => part.dispose())
317
+ if (!geometry) throw new Error(`Unable to merge Egypt portal frame batch "${batch.name}".`)
318
+ geometry.name = `${name}-${batch.name}-geometry`
319
+ geometry.computeBoundingBox()
320
+ geometry.computeBoundingSphere()
321
+ geometry.userData.visualOnly = true
322
+ const mesh = new THREE.Mesh(geometry, batch.material)
323
+ mesh.name = `${name}-${batch.name}`
324
+ frame.add(mesh)
325
+ return mesh
326
+ }
327
+
328
+ function roundedBox(
329
+ width: number,
330
+ height: number,
331
+ depth: number,
332
+ radius: number,
333
+ position: THREE.Vector3,
334
+ rotation = IDENTITY_QUATERNION,
335
+ ): THREE.BufferGeometry {
336
+ return transformed(
337
+ new VisualBoxGeometry(width, height, depth, 2, Math.min(radius, width / 4, height / 4, depth / 4)),
338
+ position,
339
+ rotation,
340
+ )
341
+ }
342
+
343
+ function transformed(
344
+ geometry: THREE.BufferGeometry,
345
+ position: THREE.Vector3,
346
+ rotation = IDENTITY_QUATERNION,
347
+ scale = UNIT_SCALE,
348
+ ): THREE.BufferGeometry {
349
+ geometry.applyMatrix4(new THREE.Matrix4().compose(position, rotation, scale))
350
+ return metricPlanarUvGeometry(geometry)
351
+ }
352
+
353
+ function metricPlanarUvGeometry(geometry: THREE.BufferGeometry): THREE.BufferGeometry {
354
+ const prepared = geometry.index ? geometry.toNonIndexed() : geometry
355
+ if (prepared !== geometry) geometry.dispose()
356
+ const position = prepared.getAttribute('position')
357
+ const normal = prepared.getAttribute('normal')
358
+ const uvs = new Float32Array(position.count * 2)
359
+ const metricUvs = new Float32Array(position.count * 4)
360
+ for (let index = 0; index < position.count; index += 1) {
361
+ const x = position.getX(index)
362
+ const y = position.getY(index)
363
+ const z = position.getZ(index)
364
+ const nx = Math.abs(normal.getX(index))
365
+ const ny = Math.abs(normal.getY(index))
366
+ const nz = Math.abs(normal.getZ(index))
367
+ if (nx >= ny && nx >= nz) {
368
+ uvs[index * 2] = z
369
+ uvs[index * 2 + 1] = y
370
+ } else if (ny >= nz) {
371
+ uvs[index * 2] = x
372
+ uvs[index * 2 + 1] = z
373
+ } else {
374
+ uvs[index * 2] = x
375
+ uvs[index * 2 + 1] = y
376
+ }
377
+ metricUvs[index * 4 + 2] = 1
378
+ }
379
+ prepared.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
380
+ prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(metricUvs, 4))
381
+ return prepared
382
+ }
@@ -0,0 +1 @@
1
+ export { hasMarbleCrossedFinishPlane, type FinishPlaneCheck } from '@marble.fun/physics-engine'