@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,24 @@
1
+ import type * as THREE from 'three'
2
+
3
+ /** Preserve intentional attribute aliases when Three copies/merges buffers. */
4
+ export function preserveGeometryAttributeAliases(
5
+ target: THREE.BufferGeometry,
6
+ sources: readonly THREE.BufferGeometry[],
7
+ ): void {
8
+ if (sources.length === 0) return
9
+ const names = Object.keys(target.attributes)
10
+ for (let index = 1; index < names.length; index += 1) {
11
+ const name = names[index]!
12
+ for (let previous = 0; previous < index; previous += 1) {
13
+ const other = names[previous]!
14
+ if (
15
+ sources.every(
16
+ source => source.getAttribute(name) !== undefined && source.getAttribute(name) === source.getAttribute(other),
17
+ )
18
+ ) {
19
+ target.setAttribute(name, target.getAttribute(other))
20
+ break
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,36 @@
1
+ /** Development experiment: geometry only; renderer policy stays at High. */
2
+ export type GeometryDetail = 'max' | 'mid' | 'lo'
3
+ export const GEOMETRY_DETAIL_STORAGE_KEY = 'marble.geometry-detail'
4
+ export const GEOMETRY_DETAIL_PROFILES = {
5
+ max: { triangleRatio: 0.2, minimumScale: 1, surfaceSpacing: 1, ribbonTolerance: 1 },
6
+ mid: { triangleRatio: 0.08, minimumScale: 0.75, surfaceSpacing: 2, ribbonTolerance: 1.5 },
7
+ lo: { triangleRatio: 0.025, minimumScale: 0.5, surfaceSpacing: 4, ribbonTolerance: 2.5 },
8
+ } as const
9
+
10
+ export function parseGeometryDetail(value: string | null | undefined): GeometryDetail | undefined {
11
+ return value === 'max' || value === 'mid' || value === 'lo' ? value : undefined
12
+ }
13
+
14
+ function initialGeometryDetail(): GeometryDetail {
15
+ const query = parseGeometryDetail(new URLSearchParams(globalThis.location?.search ?? '').get('geometryDetail'))
16
+ if (query) return query
17
+ try {
18
+ return parseGeometryDetail(globalThis.localStorage?.getItem(GEOMETRY_DETAIL_STORAGE_KEY)) ?? 'max'
19
+ } catch {
20
+ return 'max'
21
+ }
22
+ }
23
+
24
+ // Deliberately immutable for the page lifetime: caches and async model loads
25
+ // must use the same profile. Changing the dial requires a full page reload.
26
+ export const geometryDetail = initialGeometryDetail()
27
+ export const geometryDetailProfile = GEOMETRY_DETAIL_PROFILES[geometryDetail]
28
+
29
+ export function geometryDetailModelUrl(url: string): string {
30
+ return geometryDetail === 'max' ? url : url.replace('/models/', `/models/geometry/${geometryDetail}/`)
31
+ }
32
+
33
+ /** Counts for custom render-only grids. Never use for collision geometry. */
34
+ export function geometryDetailSegments(current: number, minimum = 1): number {
35
+ return Math.max(minimum, Math.floor(current / geometryDetailProfile.surfaceSpacing))
36
+ }
@@ -0,0 +1,15 @@
1
+ import { SphereGeometry } from 'three'
2
+
3
+ const geometries = new Map<string, SphereGeometry>()
4
+
5
+ /** Original authored homepage tessellation, independent of course detail. */
6
+ export function homepageMarbleGeometry(radius: number, width: number, height: number): SphereGeometry {
7
+ const key = `${radius}:${width}:${height}`
8
+ let geometry = geometries.get(key)
9
+ if (!geometry) {
10
+ geometry = new SphereGeometry(radius, width, height)
11
+ geometry.userData.shared = true
12
+ geometries.set(key, geometry)
13
+ }
14
+ return geometry
15
+ }
@@ -0,0 +1,252 @@
1
+ import type { MaterialRecipe } from './track-theme'
2
+
3
+ export const JAPANESE_SAKURA_CASTLE_WALL_ID = 'japanese-sakura.castle-wall'
4
+ export const JAPANESE_SAKURA_CASTLE_TIMBER_ID = 'japanese-sakura.castle-wall-timber'
5
+ export const JAPANESE_SAKURA_CASTLE_MASONRY_ID = 'japanese-sakura.castle-wall-masonry'
6
+ export const JAPANESE_SAKURA_CASTLE_KAWARA_ID = 'japanese-sakura.castle-wall-kawara'
7
+ export const JAPANESE_SAKURA_BRIDGE_FENCE_ID = 'japanese-sakura.vermilion-bridge-fence'
8
+ export const JAPANESE_SAKURA_BRIDGE_TIMBER_ID = 'japanese-sakura.vermilion-bridge-timber'
9
+ export const JAPANESE_SAKURA_BRIDGE_HARDWARE_ID = 'japanese-sakura.vermilion-bridge-hardware'
10
+ export const JAPANESE_SAKURA_CASTLE_PAVERS_ID = 'japanese-sakura.castle-pavers'
11
+ export const JAPANESE_SAKURA_REDWOOD_PLANKS_ID = 'japanese-sakura.redwood-planks'
12
+ export const JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_ID = 'japanese-sakura.portal-foundation-stone'
13
+ export const JAPANESE_SAKURA_VERMILION_UNDERBRIDGE_SUPPORT_ID = 'japanese-sakura.vermilion-underbridge-support'
14
+ export const JAPANESE_SAKURA_DRAGON_STONE_SUPPORT_ID = 'japanese-sakura.dragon-stone-support'
15
+
16
+ const textureRoot = '/themes/textures/japanese-sakura-castle'
17
+
18
+ /** Weathered bridge-plank grain recolored as deep Japanese vermilion redwood. */
19
+ export const JAPANESE_SAKURA_REDWOOD_PLANKS_MATERIAL: MaterialRecipe = {
20
+ kind: 'physical',
21
+ color: '#ffffff',
22
+ roughness: 0.48,
23
+ metalness: 0,
24
+ envMapIntensity: 0.86,
25
+ clearcoat: 0.34,
26
+ clearcoatRoughness: 0.42,
27
+ pattern: {
28
+ kind: 'bridge-planks',
29
+ primary: '#8f2f25',
30
+ secondary: '#2c0d0a',
31
+ worldSize: [24, 14.4],
32
+ },
33
+ textureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-albedo.webp',
34
+ normalTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-normal.png',
35
+ heightTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-height.png',
36
+ roughnessTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-roughness.png',
37
+ aoTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-ao.png',
38
+ textureWorldSize: [5.4, 2.16],
39
+ normalScale: [0.62, 0.62],
40
+ bumpScale: 0.025,
41
+ aoMapIntensity: 0.38,
42
+ }
43
+
44
+ export const JAPANESE_SAKURA_CASTLE_PLASTER_MATERIAL: MaterialRecipe = {
45
+ kind: 'standard',
46
+ color: '#ffffff',
47
+ roughness: 0.88,
48
+ metalness: 0,
49
+ envMapIntensity: 0.68,
50
+ pattern: {
51
+ kind: 'painted-stone',
52
+ primary: '#f3e4c4',
53
+ secondary: '#d8c29b',
54
+ worldSize: [1.6, 1.6],
55
+ },
56
+ textureUrl: `${textureRoot}-plaster-v1-albedo.webp`,
57
+ normalTextureUrl: `${textureRoot}-plaster-v1-normal.png`,
58
+ heightTextureUrl: `${textureRoot}-plaster-v1-height.png`,
59
+ roughnessTextureUrl: `${textureRoot}-plaster-v1-roughness.png`,
60
+ aoTextureUrl: `${textureRoot}-plaster-v1-ao.png`,
61
+ textureWorldSize: [1.6, 1.6],
62
+ normalScale: [0.32, 0.32],
63
+ bumpScale: 0.024,
64
+ aoMapIntensity: 0.42,
65
+ }
66
+
67
+ export const JAPANESE_SAKURA_CASTLE_TIMBER_MATERIAL: MaterialRecipe = {
68
+ kind: 'standard',
69
+ color: '#ffffff',
70
+ roughness: 0.74,
71
+ metalness: 0,
72
+ envMapIntensity: 0.72,
73
+ pattern: {
74
+ kind: 'wood',
75
+ primary: '#57311f',
76
+ secondary: '#21130e',
77
+ worldSize: [0.55, 1.8],
78
+ },
79
+ textureUrl: `${textureRoot}-timber-v1-albedo.webp`,
80
+ normalTextureUrl: `${textureRoot}-timber-v1-normal.png`,
81
+ heightTextureUrl: `${textureRoot}-timber-v1-height.png`,
82
+ roughnessTextureUrl: `${textureRoot}-timber-v1-roughness.png`,
83
+ aoTextureUrl: `${textureRoot}-timber-v1-ao.png`,
84
+ textureWorldSize: [0.55, 1.8],
85
+ normalScale: [0.4, 0.4],
86
+ bumpScale: 0.035,
87
+ aoMapIntensity: 0.5,
88
+ }
89
+
90
+ export const JAPANESE_SAKURA_CASTLE_MASONRY_MATERIAL: MaterialRecipe = {
91
+ kind: 'standard',
92
+ color: '#ffffff',
93
+ roughness: 0.8,
94
+ metalness: 0,
95
+ envMapIntensity: 0.6,
96
+ pattern: {
97
+ kind: 'ancient-bricks',
98
+ primary: '#75402b',
99
+ secondary: '#301d18',
100
+ worldSize: [1.4, 0.7],
101
+ },
102
+ textureUrl: `${textureRoot}-masonry-sill-v1-albedo.webp`,
103
+ normalTextureUrl: `${textureRoot}-masonry-sill-v1-normal.png`,
104
+ heightTextureUrl: `${textureRoot}-masonry-sill-v1-height.png`,
105
+ roughnessTextureUrl: `${textureRoot}-masonry-sill-v1-roughness.png`,
106
+ aoTextureUrl: `${textureRoot}-masonry-sill-v1-ao.png`,
107
+ textureWorldSize: [1.4, 0.7],
108
+ normalScale: [0.58, 0.58],
109
+ bumpScale: 0.055,
110
+ aoMapIntensity: 0.62,
111
+ }
112
+
113
+ export const JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_MATERIAL: MaterialRecipe = {
114
+ kind: 'standard',
115
+ color: '#ffffff',
116
+ roughness: 0.84,
117
+ metalness: 0,
118
+ envMapIntensity: 0.66,
119
+ pattern: {
120
+ kind: 'painted-stone',
121
+ primary: '#34363a',
122
+ secondary: '#1f2125',
123
+ worldSize: [1.6, 1.6],
124
+ },
125
+ textureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-albedo.webp',
126
+ normalTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-normal.png',
127
+ heightTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-height.png',
128
+ roughnessTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-roughness.png',
129
+ aoTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-ao.png',
130
+ textureWorldSize: [1.6, 1.6],
131
+ normalScale: [0.42, 0.42],
132
+ bumpScale: 0.028,
133
+ aoMapIntensity: 0.5,
134
+ }
135
+
136
+ export const JAPANESE_SAKURA_CASTLE_KAWARA_MATERIAL: MaterialRecipe = {
137
+ kind: 'physical',
138
+ color: '#ffffff',
139
+ roughness: 0.5,
140
+ metalness: 0,
141
+ envMapIntensity: 0.9,
142
+ clearcoat: 0.22,
143
+ clearcoatRoughness: 0.46,
144
+ pattern: {
145
+ kind: 'panels',
146
+ primary: '#4d5054',
147
+ secondary: '#202328',
148
+ worldSize: [2.7, 1.8],
149
+ },
150
+ textureUrl: `${textureRoot}-kawara-v1-albedo.webp`,
151
+ normalTextureUrl: `${textureRoot}-kawara-v1-normal.png`,
152
+ heightTextureUrl: `${textureRoot}-kawara-v1-height.png`,
153
+ roughnessTextureUrl: `${textureRoot}-kawara-v1-roughness.png`,
154
+ aoTextureUrl: `${textureRoot}-kawara-v1-ao.png`,
155
+ textureWorldSize: [2.7, 1.8],
156
+ normalScale: [0.7, 0.7],
157
+ bumpScale: 0.07,
158
+ aoMapIntensity: 0.58,
159
+ }
160
+
161
+ /** Dedicated low-contrast vermilion lacquer; deliberately independent of the castle timber. */
162
+ export const JAPANESE_SAKURA_BRIDGE_TIMBER_MATERIAL: MaterialRecipe = {
163
+ kind: 'physical',
164
+ color: '#ffffff',
165
+ roughness: 0.3,
166
+ metalness: 0,
167
+ envMapIntensity: 1.3,
168
+ clearcoat: 0.84,
169
+ clearcoatRoughness: 0.14,
170
+ pattern: {
171
+ kind: 'wood',
172
+ primary: '#8a2119',
173
+ secondary: '#651611',
174
+ worldSize: [3.2, 3.2],
175
+ },
176
+ textureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-albedo.webp',
177
+ normalTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-normal.png',
178
+ heightTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-height.png',
179
+ roughnessTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-roughness.png',
180
+ aoTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-ao.png',
181
+ textureWorldSize: [3.2, 3.2],
182
+ normalScale: [0.18, 0.18],
183
+ bumpScale: 0.004,
184
+ aoMapIntensity: 0.08,
185
+ }
186
+
187
+ export const JAPANESE_SAKURA_BRIDGE_HARDWARE_MATERIAL: MaterialRecipe = {
188
+ kind: 'physical',
189
+ color: '#6d7476',
190
+ roughness: 0.3,
191
+ metalness: 0.92,
192
+ envMapIntensity: 1.3,
193
+ clearcoat: 0.12,
194
+ clearcoatRoughness: 0.24,
195
+ pattern: {
196
+ kind: 'gunmetal-silver',
197
+ primary: '#8e9799',
198
+ secondary: '#252a2c',
199
+ worldSize: [0.42, 0.42],
200
+ },
201
+ bumpScale: 0.008,
202
+ }
203
+
204
+ /** Pale aligned ashlar used by the approved castle under-track support. */
205
+ export const JAPANESE_SAKURA_DRAGON_STONE_MATERIAL: MaterialRecipe = {
206
+ kind: 'standard',
207
+ color: '#ffffff',
208
+ roughness: 0.78,
209
+ metalness: 0,
210
+ envMapIntensity: 0.58,
211
+ textureUrl: '/themes/textures/japanese-sakura-castle-ashlar-albedo-v1.png',
212
+ normalTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-normal-v1.png',
213
+ heightTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-height-v1.png',
214
+ roughnessTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-roughness-v1.png',
215
+ aoTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-ao-v1.png',
216
+ textureWorldSize: [4, 4],
217
+ normalScale: [0.38, 0.38],
218
+ bumpScale: 0.03,
219
+ aoMapIntensity: 0.42,
220
+ }
221
+
222
+ export const JAPANESE_SAKURA_CASTLE_PAVERS_MATERIAL: MaterialRecipe = {
223
+ kind: 'standard',
224
+ color: '#ffffff',
225
+ roughness: 0.78,
226
+ metalness: 0,
227
+ envMapIntensity: 0.72,
228
+ pattern: {
229
+ kind: 'ancient-bricks',
230
+ primary: '#ead6aa',
231
+ secondary: '#c7ad7f',
232
+ worldSize: [3.8, 3.8],
233
+ },
234
+ textureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-albedo.webp',
235
+ normalTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-normal.png',
236
+ heightTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-height.png',
237
+ roughnessTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-roughness.png',
238
+ aoTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-ao.png',
239
+ textureWorldSize: [3.8, 3.8],
240
+ normalScale: [0.48, 0.48],
241
+ bumpScale: 0.035,
242
+ aoMapIntensity: 0.48,
243
+ visualRelief: { scale: 0.12, bias: -0.11, targetEdgeLength: 0.09 },
244
+ }
245
+
246
+ export function isJapaneseSakuraCastleWallAppearance(appearanceId: string | undefined): boolean {
247
+ return appearanceId === JAPANESE_SAKURA_CASTLE_WALL_ID
248
+ }
249
+
250
+ export function isJapaneseSakuraBridgeFenceAppearance(appearanceId: string | undefined): boolean {
251
+ return appearanceId === JAPANESE_SAKURA_BRIDGE_FENCE_ID
252
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_CORE_MATERIAL as JUNGLE_FLOWER_HEDGE_CORE_MATERIAL,
3
+ PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID as JUNGLE_FLOWER_HEDGE_WALL_ID,
4
+ PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_FLOOR_ID as JUNGLE_GRASS_DIRT_PATH_FLOOR_ID,
5
+ PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_MATERIAL as JUNGLE_GRASS_DIRT_PATH_MATERIAL,
6
+ isTropicalFlowerHedgeAppearance as isJungleFlowerHedgeAppearance,
7
+ } from './painted-valley-appearance'
@@ -0,0 +1,5 @@
1
+ export const JUNGLE_TEMPLE_STONE_MATERIAL_ID = 'jungle.temple-stone'
2
+ export const JUNGLE_TEMPLE_FACADE_MATERIAL_ID = 'jungle.temple-serpent-facade'
3
+ export const JUNGLE_TEMPLE_TIMBER_MATERIAL_ID = 'jungle.temple-timber'
4
+ export const JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID = 'jungle.temple-platform'
5
+ export const JUNGLE_TEMPLE_BRONZE_MATERIAL_ID = 'jungle.temple-bronze'
@@ -0,0 +1,132 @@
1
+ import * as THREE from 'three'
2
+
3
+ import type { VisualQualityPolicy } from './visual-quality'
4
+
5
+ type LitMaterial =
6
+ THREE.MeshStandardMaterial | THREE.MeshPhongMaterial | THREE.MeshLambertMaterial | THREE.MeshToonMaterial
7
+
8
+ /** Render-only experiment. Source materials, geometry and simulation are never edited. */
9
+ export class LowQualitySceneRenderer {
10
+ private readonly materials = new Map<
11
+ LitMaterial,
12
+ { material: THREE.MeshBasicMaterial; release: () => void; frame: number }
13
+ >()
14
+ private readonly arrays = new WeakMap<THREE.Material[], THREE.Material[]>()
15
+ private readonly meshes: THREE.Mesh[] = []
16
+ private readonly originals: Array<THREE.Material | THREE.Material[]> = []
17
+ private readonly particles: THREE.Points[] = []
18
+ private frame = 0
19
+ private cameraLayers = 1
20
+
21
+ render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, policy: VisualQualityPolicy): void {
22
+ if (policy.tier !== 'low') {
23
+ renderer.render(scene, camera)
24
+ return
25
+ }
26
+ this.frame += 1
27
+ this.cameraLayers = camera.layers.mask
28
+ const shadowsEnabled = renderer.shadowMap.enabled
29
+ renderer.shadowMap.enabled = false
30
+ try {
31
+ scene.traverseVisible(this.prepareObject)
32
+ renderer.render(scene, camera)
33
+ } finally {
34
+ for (let index = 0; index < this.meshes.length; index += 1) {
35
+ this.meshes[index]!.material = this.originals[index]!
36
+ }
37
+ for (const particle of this.particles) particle.visible = true
38
+ this.meshes.length = 0
39
+ this.originals.length = 0
40
+ this.particles.length = 0
41
+ renderer.shadowMap.enabled = shadowsEnabled
42
+ }
43
+ }
44
+
45
+ dispose(): void {
46
+ for (const { release } of this.materials.values()) release()
47
+ this.materials.clear()
48
+ }
49
+
50
+ private readonly prepareObject = (object: THREE.Object3D): void => {
51
+ if (object instanceof THREE.Points) {
52
+ this.particles.push(object)
53
+ object.visible = false
54
+ } else if (object instanceof THREE.Mesh && (object.layers.mask & this.cameraLayers) !== 0) {
55
+ const original = object.material
56
+ let replacement: THREE.Material | THREE.Material[]
57
+ if (Array.isArray(original)) {
58
+ let array = this.arrays.get(original)
59
+ if (!array) {
60
+ array = []
61
+ this.arrays.set(original, array)
62
+ }
63
+ array.length = original.length
64
+ for (let index = 0; index < original.length; index += 1) array[index] = this.simplify(original[index]!)
65
+ replacement = array
66
+ } else {
67
+ replacement = this.simplify(original)
68
+ }
69
+ if (replacement === original) return
70
+ this.meshes.push(object)
71
+ this.originals.push(original)
72
+ object.material = replacement
73
+ }
74
+ }
75
+
76
+ private simplify(source: THREE.Material): THREE.Material {
77
+ if (!(
78
+ source instanceof THREE.MeshStandardMaterial ||
79
+ source instanceof THREE.MeshPhongMaterial ||
80
+ source instanceof THREE.MeshLambertMaterial ||
81
+ source instanceof THREE.MeshToonMaterial
82
+ ))
83
+ return source
84
+ let entry = this.materials.get(source)
85
+ if (!entry) {
86
+ const material = new THREE.MeshBasicMaterial()
87
+ material.name = `low:${source.name}`
88
+ const release = (): void => {
89
+ source.removeEventListener('dispose', release)
90
+ material.dispose()
91
+ this.materials.delete(source)
92
+ }
93
+ source.addEventListener('dispose', release)
94
+ entry = { material, release, frame: -1 }
95
+ this.materials.set(source, entry)
96
+ }
97
+ // Synchronize shared materials once per render, not once per mesh.
98
+ if (entry.frame === this.frame) return entry.material
99
+ entry.frame = this.frame
100
+ const target = entry.material
101
+ // Keep labels, foliage cutouts, vertex colors and presentation fades. Normal,
102
+ // roughness, metalness, environment, transmission and lighting are omitted.
103
+ if (
104
+ target.map !== source.map ||
105
+ target.alphaMap !== source.alphaMap ||
106
+ target.vertexColors !== source.vertexColors ||
107
+ target.side !== source.side ||
108
+ target.transparent !== source.transparent ||
109
+ target.alphaTest !== source.alphaTest
110
+ )
111
+ target.needsUpdate = true
112
+ target.color.copy(source.color)
113
+ target.map = source.map
114
+ target.alphaMap = source.alphaMap
115
+ target.vertexColors = source.vertexColors
116
+ target.side = source.side
117
+ target.transparent = source.transparent
118
+ target.opacity = source.opacity
119
+ target.alphaTest = source.alphaTest
120
+ target.depthTest = source.depthTest
121
+ target.depthWrite = source.depthWrite
122
+ target.colorWrite = source.colorWrite
123
+ target.blending = source.blending
124
+ target.polygonOffset = source.polygonOffset
125
+ target.polygonOffsetFactor = source.polygonOffsetFactor
126
+ target.polygonOffsetUnits = source.polygonOffsetUnits
127
+ target.visible = source.visible
128
+ target.toneMapped = source.toneMapped
129
+ target.fog = source.fog
130
+ return target
131
+ }
132
+ }
@@ -0,0 +1,13 @@
1
+ import { SphereGeometry } from 'three'
2
+
3
+ /** Preserve the pre-experiment High marble silhouette at every course detail. */
4
+ export class MarbleSphereGeometry extends SphereGeometry {
5
+ constructor(radius: number, width: number, height: number) {
6
+ const highAxisRatio = Math.sqrt(0.2)
7
+ super(
8
+ radius,
9
+ Math.min(width, Math.max(8, Math.floor(width * highAxisRatio))),
10
+ Math.min(height, Math.max(4, Math.floor(height * highAxisRatio))),
11
+ )
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ export const MOBILE_STATIC_BATCH_ALLOCATION_BUDGET_BYTES = 32 * 1024 * 1024
2
+
3
+ /** Phone/tablet rendering budgets must survive landscape and browser rotation. */
4
+ export function usesMobileRenderingBudget(
5
+ viewportWidth: number = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : window.innerWidth,
6
+ coarsePrimaryPointer: boolean = typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches,
7
+ ): boolean {
8
+ // Narrow desktop windows retain the existing compact budget. Device input
9
+ // capability is independent of layout width, unlike a CSS breakpoint.
10
+ return viewportWidth < 768 || coarsePrimaryPointer
11
+ }