@marble.fun/track-catalog 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/assets.json +346 -0
- package/package.json +32 -0
- package/release.json +135 -0
- package/src/audio/game-audio-preference.ts +9 -0
- package/src/authoritative.ts +6 -0
- package/src/catalog.ts +1 -0
- package/src/index.ts +14 -0
- package/src/legacy-stock-catalog.ts +130 -0
- package/src/pages/lobby/lobby-types.ts +140 -0
- package/src/pages/lobby/marble-factory.ts +931 -0
- package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
- package/src/pages/play/race/egypt-portal-frame.ts +382 -0
- package/src/pages/play/race/finish-detection.ts +1 -0
- package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
- package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
- package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
- package/src/pages/play/race/jungle-portal-frame.ts +308 -0
- package/src/pages/play/race/jungle-temple-lift.ts +366 -0
- package/src/pages/play/race/marble-entity.ts +257 -0
- package/src/pages/play/race/marble-material.ts +20 -0
- package/src/pages/play/race/marble-race-engine.ts +2652 -0
- package/src/pages/play/race/path-pattern-layout.ts +142 -0
- package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
- package/src/pages/play/race/physics-debug.ts +1 -0
- package/src/pages/play/race/physics-dry-run.ts +90 -0
- package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
- package/src/pages/play/race/portal-pair-color.ts +20 -0
- package/src/pages/play/race/portal-proportions.ts +25 -0
- package/src/pages/play/race/portal-visual.ts +1108 -0
- package/src/pages/play/race/race-camera-director.ts +586 -0
- package/src/pages/play/race/race-camera-obstruction.ts +37 -0
- package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
- package/src/pages/play/race/race-camera.ts +206 -0
- package/src/pages/play/race/race-completion.ts +5 -0
- package/src/pages/play/race/race-configuration.ts +57 -0
- package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
- package/src/pages/play/race/race-physics-profile.ts +7 -0
- package/src/pages/play/race/race-physics.ts +16 -0
- package/src/pages/play/race/race-playback.ts +41 -0
- package/src/pages/play/race/race-rolling-audio.ts +616 -0
- package/src/pages/play/race/race-simulation-renderer.ts +221 -0
- package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
- package/src/pages/play/race/race-transform-interpolation.ts +50 -0
- package/src/pages/play/race/race-types.ts +12 -0
- package/src/pages/play/race/rebound-visual.ts +70 -0
- package/src/pages/play/race/seeded-random.ts +23 -0
- package/src/pages/play/race/space-station-portal-frame.ts +275 -0
- package/src/pages/play/race/track-appearance-canopy.ts +946 -0
- package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
- package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
- package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
- package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
- package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
- package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
- package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
- package/src/pages/play/race/track-appearance-support.ts +983 -0
- package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
- package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
- package/src/pages/play/race/track-course.ts +271 -0
- package/src/pages/play/race/track-definition.ts +443 -0
- package/src/pages/play/race/track-fence-architecture.ts +33 -0
- package/src/pages/play/race/track-floor-material-rules.ts +51 -0
- package/src/pages/play/race/track-mechanisms.ts +1350 -0
- package/src/pages/play/race/track-physics-surface.ts +6 -0
- package/src/pages/play/race/track-route-network.ts +9 -0
- package/src/pages/play/race/track-shadow-batching.ts +760 -0
- package/src/pages/play/race/track-surface-basis.ts +1 -0
- package/src/pages/play/race/track-surface-topology.ts +326 -0
- package/src/pages/play/race/track-world.ts +10181 -0
- package/src/pages/play/race/under-track-support-architecture.ts +122 -0
- package/src/pages/play/race/vortex-profile.ts +5 -0
- package/src/pages/play/race/vortex-water-effect.ts +863 -0
- package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
- package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
- package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
- package/src/pages/track-lab/cone-field-layout.ts +1 -0
- package/src/pages/track-lab/conveyor-layout.ts +1 -0
- package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
- package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
- package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
- package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
- package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
- package/src/pages/track-lab/flipper-component.ts +127 -0
- package/src/pages/track-lab/rising-wall-layout.ts +1 -0
- package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
- package/src/pages/track-lab/track-piece-layout.ts +28 -0
- package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
- package/src/three/geometry-attribute-aliases.ts +24 -0
- package/src/three/geometry-detail.ts +36 -0
- package/src/three/homepage-marble-geometry.ts +15 -0
- package/src/three/japanese-sakura-appearance.ts +252 -0
- package/src/three/jungle-flower-hedge-appearance.ts +7 -0
- package/src/three/jungle-temple-appearance.ts +5 -0
- package/src/three/low-quality-scene-renderer.ts +132 -0
- package/src/three/marble-sphere-geometry.ts +13 -0
- package/src/three/mobile-rendering-budget.ts +11 -0
- package/src/three/painted-valley-appearance.ts +270 -0
- package/src/three/railroad-appearance.ts +71 -0
- package/src/three/renderer-resize.ts +32 -0
- package/src/three/rope-wall-appearance.ts +91 -0
- package/src/three/rugged-track-appearance.ts +2 -0
- package/src/three/scene-lighting.ts +183 -0
- package/src/three/scene-shader-preparation.ts +64 -0
- package/src/three/scene-theme-controller.ts +461 -0
- package/src/three/shadow-shader-preparation.ts +162 -0
- package/src/three/shared-obstacle-metal.ts +44 -0
- package/src/three/soft-additive-particles.ts +57 -0
- package/src/three/space-station-appearance.ts +213 -0
- package/src/three/static-track-batched-mesh.ts +132 -0
- package/src/three/stone-track-shell-appearance.ts +12 -0
- package/src/three/theme-ambience-controller.ts +226 -0
- package/src/three/theme-asset-cache.ts +120 -0
- package/src/three/theme-audio-preference.ts +43 -0
- package/src/three/theme-catalog.ts +1457 -0
- package/src/three/theme-material-assets.ts +258 -0
- package/src/three/theme-material-ktx2-manifest.ts +190 -0
- package/src/three/theme-material-registry.ts +1952 -0
- package/src/three/theme-world-factory.ts +590 -0
- package/src/three/track-connector-batch.ts +125 -0
- package/src/three/track-material-continuity.ts +442 -0
- package/src/three/track-theme.ts +535 -0
- package/src/three/vampire-castle-appearance.ts +258 -0
- package/src/three/visual-frame-pacing.ts +19 -0
- package/src/three/visual-geometry.ts +120 -0
- package/src/three/visual-quality.ts +83 -0
- package/src/three/water-appearance.ts +29 -0
- package/src/three/wild-west-sand-appearance.ts +2 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createThemeMaterialSet,
|
|
3
|
+
type MaterialPatternRecipe,
|
|
4
|
+
type MaterialRecipe,
|
|
5
|
+
type ThemeMaterialSlot,
|
|
6
|
+
} from './track-theme'
|
|
7
|
+
|
|
8
|
+
export const PAINTED_VALLEY_THEME_ID = 'painted-valley'
|
|
9
|
+
export const PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_FLOOR_ID =
|
|
10
|
+
'painted-valley.grass-orange-dirt-path'
|
|
11
|
+
export const PAINTED_VALLEY_GRASS_SHOULDER_MATERIAL_ID = 'painted-valley.grass-shoulder'
|
|
12
|
+
export const PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID =
|
|
13
|
+
'painted-valley.tropical-flower-hedge'
|
|
14
|
+
export const LEGACY_JUNGLE_GRASS_DIRT_PATH_FLOOR_ID = 'jungle.grass-dirt-path'
|
|
15
|
+
export const LEGACY_JUNGLE_FLOWER_HEDGE_WALL_ID = 'jungle.flower-hedge'
|
|
16
|
+
|
|
17
|
+
export const PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_MATERIAL: MaterialRecipe = {
|
|
18
|
+
kind: 'standard',
|
|
19
|
+
color: '#ffffff',
|
|
20
|
+
roughness: 0.9,
|
|
21
|
+
metalness: 0,
|
|
22
|
+
envMapIntensity: 0.52,
|
|
23
|
+
textureUrl: '/themes/textures/painted-valley-orange-dirt-v1-albedo.ktx2',
|
|
24
|
+
normalTextureUrl: '/themes/textures/painted-valley-orange-dirt-v1-normal.ktx2',
|
|
25
|
+
heightTextureUrl: '/themes/textures/painted-valley-orange-dirt-v1-height.ktx2',
|
|
26
|
+
roughnessTextureUrl: '/themes/textures/painted-valley-orange-dirt-v1-roughness.ktx2',
|
|
27
|
+
aoTextureUrl: '/themes/textures/painted-valley-orange-dirt-v1-ao.ktx2',
|
|
28
|
+
textureWorldSize: [4.8, 4.8],
|
|
29
|
+
normalScale: [0.58, 0.58],
|
|
30
|
+
bumpScale: 0.026,
|
|
31
|
+
aoMapIntensity: 0.52,
|
|
32
|
+
surfaceTreatment: {
|
|
33
|
+
kind: 'floor-perimeter-overlay',
|
|
34
|
+
materialId: PAINTED_VALLEY_GRASS_SHOULDER_MATERIAL_ID,
|
|
35
|
+
bandWidth: 0.62,
|
|
36
|
+
featherWidth: 0.36,
|
|
37
|
+
normalOffset: 0.004,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const PAINTED_VALLEY_GRASS_SHOULDER_MATERIAL: MaterialRecipe = {
|
|
42
|
+
kind: 'standard',
|
|
43
|
+
color: '#ffffff',
|
|
44
|
+
roughness: 0.96,
|
|
45
|
+
metalness: 0,
|
|
46
|
+
envMapIntensity: 0.45,
|
|
47
|
+
textureUrl: '/themes/textures/painted-valley-grass-shoulder-v1-albedo.ktx2',
|
|
48
|
+
normalTextureUrl: '/themes/textures/painted-valley-grass-shoulder-v1-normal.ktx2',
|
|
49
|
+
heightTextureUrl: '/themes/textures/painted-valley-grass-shoulder-v1-height.ktx2',
|
|
50
|
+
roughnessTextureUrl: '/themes/textures/painted-valley-grass-shoulder-v1-roughness.ktx2',
|
|
51
|
+
aoTextureUrl: '/themes/textures/painted-valley-grass-shoulder-v1-ao.ktx2',
|
|
52
|
+
textureWorldSize: [2.8, 2.8],
|
|
53
|
+
normalScale: [0.7, 0.7],
|
|
54
|
+
bumpScale: 0.032,
|
|
55
|
+
aoMapIntensity: 0.58,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_CORE_MATERIAL: MaterialRecipe = {
|
|
59
|
+
kind: 'standard',
|
|
60
|
+
color: '#3b5029',
|
|
61
|
+
roughness: 0.98,
|
|
62
|
+
metalness: 0,
|
|
63
|
+
emissive: '#0d180a',
|
|
64
|
+
emissiveIntensity: 0.24,
|
|
65
|
+
envMapIntensity: 0.38,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const PAINTED_VALLEY_BIRCH_PLANK_MATERIAL: MaterialRecipe = {
|
|
69
|
+
kind: 'toon',
|
|
70
|
+
color: '#ffffff',
|
|
71
|
+
roughness: 0.9,
|
|
72
|
+
metalness: 0,
|
|
73
|
+
envMapIntensity: 0.72,
|
|
74
|
+
textureUrl: '/themes/textures/painted-valley-wild-west-birch-planks-v1-albedo.webp',
|
|
75
|
+
textureWorldSize: [2.4, 2.4],
|
|
76
|
+
bumpScale: 0.035,
|
|
77
|
+
gradientSteps: 5,
|
|
78
|
+
inkColor: '#73654a',
|
|
79
|
+
inkStrength: 0.055,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const PAINTED_VALLEY_BIRCH_RANCH_FENCE_MATERIAL: MaterialRecipe = {
|
|
83
|
+
kind: 'toon',
|
|
84
|
+
color: '#ffffff',
|
|
85
|
+
roughness: 0.92,
|
|
86
|
+
metalness: 0,
|
|
87
|
+
envMapIntensity: 0.7,
|
|
88
|
+
textureUrl: '/themes/textures/painted-valley-wild-west-birch-fence-v1-albedo.webp',
|
|
89
|
+
textureWorldSize: [2.1, 2.1],
|
|
90
|
+
bumpScale: 0.04,
|
|
91
|
+
gradientSteps: 5,
|
|
92
|
+
inkColor: '#716248',
|
|
93
|
+
inkStrength: 0.06,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const paintedCedar: MaterialPatternRecipe = {
|
|
97
|
+
kind: 'painted-wood',
|
|
98
|
+
primary: '#ead7ad',
|
|
99
|
+
secondary: '#9d7b56',
|
|
100
|
+
worldSize: [1.4, 4.8],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const paintedStone: MaterialPatternRecipe = {
|
|
104
|
+
kind: 'painted-stone',
|
|
105
|
+
primary: '#c4c8a6',
|
|
106
|
+
secondary: '#728a6d',
|
|
107
|
+
worldSize: [2.2, 3.4],
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const meadowWash: MaterialPatternRecipe = {
|
|
111
|
+
kind: 'watercolor',
|
|
112
|
+
primary: '#6f985d',
|
|
113
|
+
secondary: '#b8bd72',
|
|
114
|
+
worldSize: [2.6, 2.6],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function paintedToonMaterial(
|
|
118
|
+
recipe: MaterialRecipe,
|
|
119
|
+
options: {
|
|
120
|
+
pattern?: MaterialPatternRecipe
|
|
121
|
+
gradientSteps?: number
|
|
122
|
+
inkColor?: string
|
|
123
|
+
inkStrength?: number
|
|
124
|
+
transparent?: boolean
|
|
125
|
+
opacity?: number
|
|
126
|
+
depthWrite?: boolean
|
|
127
|
+
} = {},
|
|
128
|
+
): MaterialRecipe {
|
|
129
|
+
const pattern = options.pattern ?? recipe.pattern
|
|
130
|
+
return {
|
|
131
|
+
kind: 'toon',
|
|
132
|
+
color: pattern ? '#ffffff' : recipe.color,
|
|
133
|
+
roughness: recipe.roughness,
|
|
134
|
+
metalness: 0,
|
|
135
|
+
...(recipe.emissive ? { emissive: recipe.emissive } : {}),
|
|
136
|
+
...(recipe.emissiveIntensity !== undefined ? { emissiveIntensity: recipe.emissiveIntensity } : {}),
|
|
137
|
+
transparent: options.transparent ?? recipe.transparent,
|
|
138
|
+
opacity: options.opacity ?? recipe.opacity,
|
|
139
|
+
depthWrite: options.depthWrite ?? recipe.depthWrite,
|
|
140
|
+
pattern,
|
|
141
|
+
bumpScale: recipe.bumpScale,
|
|
142
|
+
gradientSteps: options.gradientSteps ?? 4,
|
|
143
|
+
inkColor: options.inkColor ?? '#29352a',
|
|
144
|
+
inkStrength: options.inkStrength ?? 0.2,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createPaintedValleyMaterials(): Record<ThemeMaterialSlot, MaterialRecipe> {
|
|
149
|
+
const base = createThemeMaterialSet(
|
|
150
|
+
{
|
|
151
|
+
laneSurface: '#edc58d',
|
|
152
|
+
laneEdge: '#f7ebcf',
|
|
153
|
+
laneGuard: '#f1e7cf',
|
|
154
|
+
structure: '#aab7a0',
|
|
155
|
+
structureSecondary: '#617d79',
|
|
156
|
+
mechanism: '#91abb0',
|
|
157
|
+
moving: '#d66a4d',
|
|
158
|
+
route: '#e4c866',
|
|
159
|
+
hazard: '#c94432',
|
|
160
|
+
finish: '#fff0b6',
|
|
161
|
+
energy: '#78b8c9',
|
|
162
|
+
portal: '#8a78ad',
|
|
163
|
+
glass: '#b8d8d5',
|
|
164
|
+
worldGround: '#7fa45b',
|
|
165
|
+
worldPrimary: '#82634d',
|
|
166
|
+
worldSecondary: '#8d987b',
|
|
167
|
+
foliage: '#4f7f4c',
|
|
168
|
+
accent: '#cb5338',
|
|
169
|
+
dark: '#30443d',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
surfacePattern: paintedCedar,
|
|
173
|
+
surfaceBumpScale: 0.045,
|
|
174
|
+
guardPattern: paintedCedar,
|
|
175
|
+
transparentGuardBacking: true,
|
|
176
|
+
structurePattern: paintedStone,
|
|
177
|
+
trackRoughness: 0.92,
|
|
178
|
+
trackMetalness: 0,
|
|
179
|
+
guardMetalness: 0,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const materials = Object.fromEntries(
|
|
184
|
+
(Object.entries(base) as [ThemeMaterialSlot, MaterialRecipe][]).map(([slot, recipe]) => [
|
|
185
|
+
slot,
|
|
186
|
+
paintedToonMaterial(recipe),
|
|
187
|
+
]),
|
|
188
|
+
) as Record<ThemeMaterialSlot, MaterialRecipe>
|
|
189
|
+
|
|
190
|
+
materials['lane.surface'] = PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_MATERIAL
|
|
191
|
+
materials['lane.edge'] = {
|
|
192
|
+
...paintedToonMaterial(base['lane.edge'], {
|
|
193
|
+
inkColor: '#88775f',
|
|
194
|
+
inkStrength: 0.05,
|
|
195
|
+
}),
|
|
196
|
+
emissive: '#705f45',
|
|
197
|
+
emissiveIntensity: 0.34,
|
|
198
|
+
}
|
|
199
|
+
// The theme-default guard is the same birch stock used by the canonical
|
|
200
|
+
// short ranch-fence helper. The backing stays transparent and visual-only.
|
|
201
|
+
materials['lane.guard'] = PAINTED_VALLEY_BIRCH_RANCH_FENCE_MATERIAL
|
|
202
|
+
materials['lane.guard.backing'] = {
|
|
203
|
+
...paintedToonMaterial(base['lane.guard.backing'], {
|
|
204
|
+
pattern: paintedCedar,
|
|
205
|
+
inkColor: '#8d8069',
|
|
206
|
+
inkStrength: 0.035,
|
|
207
|
+
transparent: true,
|
|
208
|
+
opacity: 0,
|
|
209
|
+
depthWrite: false,
|
|
210
|
+
}),
|
|
211
|
+
emissive: '#665b48',
|
|
212
|
+
emissiveIntensity: 0.12,
|
|
213
|
+
}
|
|
214
|
+
materials['structure.primary'] = paintedToonMaterial(base['structure.primary'], {
|
|
215
|
+
pattern: paintedStone,
|
|
216
|
+
inkColor: '#53685f',
|
|
217
|
+
inkStrength: 0.1,
|
|
218
|
+
})
|
|
219
|
+
materials['structure.secondary'] = paintedToonMaterial(base['structure.secondary'], {
|
|
220
|
+
inkColor: '#3f5953',
|
|
221
|
+
inkStrength: 0.12,
|
|
222
|
+
})
|
|
223
|
+
materials['mechanism.body'] = paintedToonMaterial(base['mechanism.body'], {
|
|
224
|
+
pattern: paintedStone,
|
|
225
|
+
inkColor: '#4a6266',
|
|
226
|
+
inkStrength: 0.1,
|
|
227
|
+
})
|
|
228
|
+
materials['mechanism.moving'] = paintedToonMaterial(base['mechanism.moving'], {
|
|
229
|
+
inkColor: '#793d31',
|
|
230
|
+
inkStrength: 0.12,
|
|
231
|
+
})
|
|
232
|
+
materials['world.ground'] = paintedToonMaterial(base['world.ground'], {
|
|
233
|
+
pattern: meadowWash,
|
|
234
|
+
inkStrength: 0.12,
|
|
235
|
+
})
|
|
236
|
+
materials['world.primary'] = paintedToonMaterial(base['world.primary'], {
|
|
237
|
+
pattern: paintedCedar,
|
|
238
|
+
inkStrength: 0.2,
|
|
239
|
+
})
|
|
240
|
+
materials['world.secondary'] = paintedToonMaterial(base['world.secondary'], {
|
|
241
|
+
pattern: paintedStone,
|
|
242
|
+
inkStrength: 0.18,
|
|
243
|
+
})
|
|
244
|
+
materials['world.foliage'] = paintedToonMaterial(base['world.foliage'], {
|
|
245
|
+
pattern: meadowWash,
|
|
246
|
+
gradientSteps: 3,
|
|
247
|
+
inkStrength: 0.04,
|
|
248
|
+
})
|
|
249
|
+
materials['world.accent'] = paintedToonMaterial(base['world.accent'], {
|
|
250
|
+
gradientSteps: 3,
|
|
251
|
+
inkColor: '#653329',
|
|
252
|
+
inkStrength: 0.24,
|
|
253
|
+
})
|
|
254
|
+
materials.glass = paintedToonMaterial(base.glass, {
|
|
255
|
+
gradientSteps: 3,
|
|
256
|
+
inkColor: '#3d676b',
|
|
257
|
+
inkStrength: 0.26,
|
|
258
|
+
transparent: true,
|
|
259
|
+
opacity: 0.48,
|
|
260
|
+
depthWrite: false,
|
|
261
|
+
})
|
|
262
|
+
return materials
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function isTropicalFlowerHedgeAppearance(id: string | undefined): boolean {
|
|
266
|
+
return (
|
|
267
|
+
id === PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID ||
|
|
268
|
+
id === LEGACY_JUNGLE_FLOWER_HEDGE_WALL_ID
|
|
269
|
+
)
|
|
270
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { TrackPieceAppearance } from '@marble.fun/physics-engine'
|
|
2
|
+
|
|
3
|
+
import { JAPANESE_SAKURA_REDWOOD_PLANKS_ID } from './japanese-sakura-appearance'
|
|
4
|
+
import type { TrackThemeDefinition } from './track-theme'
|
|
5
|
+
|
|
6
|
+
export const WILD_WEST_WOODEN_PLANKS_FLOOR_ID = 'wild-west.wooden-planks'
|
|
7
|
+
export const WILD_WEST_WEATHERED_BOARDS_FLOOR_ID = 'wild-west.weathered-boards'
|
|
8
|
+
export const WILD_WEST_RAIL_MATERIAL_ID = 'wild-west.railroad-iron'
|
|
9
|
+
export const WILD_WEST_RAIL_TIE_MATERIAL_ID = 'wild-west.railroad-tie-timber'
|
|
10
|
+
export const LEGACY_WILD_WEST_RAILROAD_FLOOR_ID = 'wild-west.railroad-planks'
|
|
11
|
+
export const STANDARD_RAILROAD_LANE_WIDTH = 4
|
|
12
|
+
export const STANDARD_RAILROAD_HALF_GAUGE = 1.16
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Half the visual railway gauge carried by a logical lane. Any lane at least
|
|
16
|
+
* as wide as the standard four-unit connector keeps one invariant gauge;
|
|
17
|
+
* widening carrier floors and routing unions must never spread the rails.
|
|
18
|
+
* Only an explicitly narrower corridor is allowed to reduce its gauge.
|
|
19
|
+
*/
|
|
20
|
+
export function railroadHalfGaugeForLaneWidth(width: number): number {
|
|
21
|
+
const safeWidth = Number.isFinite(width) ? Math.max(0, width) : STANDARD_RAILROAD_LANE_WIDTH
|
|
22
|
+
if (safeWidth >= STANDARD_RAILROAD_LANE_WIDTH) return STANDARD_RAILROAD_HALF_GAUGE
|
|
23
|
+
return Math.max(0.34, safeWidth * 0.29)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Rails are a routed floor treatment, not a `lane.edge` material slot. Every
|
|
28
|
+
* ordinary track piece can carry them; only the two vertical/bowl mechanisms
|
|
29
|
+
* have no continuous lane on which rails could be laid.
|
|
30
|
+
*/
|
|
31
|
+
export function supportsRailAppearanceChoices(runtimeKind: string): boolean {
|
|
32
|
+
return runtimeKind !== 'vortex' && runtimeKind !== 'conveyor'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolvedBaseFloorMaterialId(materialId: string | undefined): string | undefined {
|
|
36
|
+
return materialId === LEGACY_WILD_WEST_RAILROAD_FLOOR_ID ? WILD_WEST_WOODEN_PLANKS_FLOOR_ID : materialId
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function usesSeparatedPlankFloorAppearance(materialId: string | undefined): boolean {
|
|
40
|
+
return (
|
|
41
|
+
materialId === WILD_WEST_WOODEN_PLANKS_FLOOR_ID ||
|
|
42
|
+
materialId === WILD_WEST_WEATHERED_BOARDS_FLOOR_ID ||
|
|
43
|
+
materialId === JAPANESE_SAKURA_REDWOOD_PLANKS_ID ||
|
|
44
|
+
materialId === LEGACY_WILD_WEST_RAILROAD_FLOOR_ID
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves a saved floor choice into the active theme by geometry family.
|
|
50
|
+
* This prevents a stale theme-specific id from combining plank geometry with
|
|
51
|
+
* the active theme's fallback surface material (or the reverse).
|
|
52
|
+
*/
|
|
53
|
+
export function resolvedFloorMaterialIdForTheme(
|
|
54
|
+
theme: TrackThemeDefinition,
|
|
55
|
+
materialId: string | undefined,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
const resolvedId = resolvedBaseFloorMaterialId(materialId)
|
|
58
|
+
if (!resolvedId) return undefined
|
|
59
|
+
|
|
60
|
+
const presets = (theme.appearancePresets?.floor ?? []).filter(preset => !preset.hidden)
|
|
61
|
+
if (presets.some(preset => preset.id === resolvedId)) return resolvedId
|
|
62
|
+
|
|
63
|
+
const wantsPlanks = usesSeparatedPlankFloorAppearance(resolvedId)
|
|
64
|
+
return presets.find(preset => usesSeparatedPlankFloorAppearance(preset.id) === wantsPlanks)?.id
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolvedRailMaterialId(appearance: TrackPieceAppearance | undefined): string | undefined {
|
|
68
|
+
if (appearance?.waterOverlayId) return undefined
|
|
69
|
+
if (appearance?.railMaterialId) return appearance.railMaterialId
|
|
70
|
+
return appearance?.floorMaterialId === LEGACY_WILD_WEST_RAILROAD_FLOOR_ID ? WILD_WEST_RAIL_MATERIAL_ID : undefined
|
|
71
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface DrawingBufferRenderer {
|
|
2
|
+
readonly domElement: Pick<HTMLCanvasElement, 'height' | 'width'>
|
|
3
|
+
getPixelRatio(): number
|
|
4
|
+
setDrawingBufferSize(width: number, height: number, pixelRatio: number): void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resize a renderer with at most one drawing-buffer allocation.
|
|
9
|
+
*
|
|
10
|
+
* WebGLRenderer.setPixelRatio() calls setSize() internally, so pairing those
|
|
11
|
+
* methods reallocates and clears the canvas twice. Comparing physical buffer
|
|
12
|
+
* dimensions first also keeps duplicate ResizeObserver notifications inert.
|
|
13
|
+
*/
|
|
14
|
+
export function resizeRendererDrawingBuffer(
|
|
15
|
+
renderer: DrawingBufferRenderer,
|
|
16
|
+
width: number,
|
|
17
|
+
height: number,
|
|
18
|
+
pixelRatio: number,
|
|
19
|
+
): boolean {
|
|
20
|
+
const drawingBufferWidth = Math.floor(width * pixelRatio)
|
|
21
|
+
const drawingBufferHeight = Math.floor(height * pixelRatio)
|
|
22
|
+
if (
|
|
23
|
+
renderer.domElement.width === drawingBufferWidth &&
|
|
24
|
+
renderer.domElement.height === drawingBufferHeight &&
|
|
25
|
+
renderer.getPixelRatio() === pixelRatio
|
|
26
|
+
) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
renderer.setDrawingBufferSize(width, height, pixelRatio)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MaterialRecipe } from './track-theme'
|
|
2
|
+
|
|
3
|
+
export const SHARED_ROPE_WALL_MATERIAL_ID = 'shared.rope-wall'
|
|
4
|
+
export const SHARED_ROPE_WALL_BACKING_MATERIAL_ID = 'shared.rope-wall-backing'
|
|
5
|
+
export const SHARED_ROPE_WALL_TIMBER_MATERIAL_ID = 'shared.rope-wall-timber'
|
|
6
|
+
export const SHARED_ROPE_WALL_END_GRAIN_MATERIAL_ID = 'shared.rope-wall-end-grain'
|
|
7
|
+
|
|
8
|
+
export const ROPE_WALL_SUPPORTED_THEME_IDS = ['low-poly-jungle', 'ancient-egypt'] as const
|
|
9
|
+
|
|
10
|
+
/** One art-directed recipe shared verbatim by the Jungle and Egyptian themes. */
|
|
11
|
+
export const SHARED_ROPE_WALL_MATERIAL: MaterialRecipe = {
|
|
12
|
+
kind: 'standard',
|
|
13
|
+
color: '#c49a6d',
|
|
14
|
+
roughness: 0.92,
|
|
15
|
+
metalness: 0,
|
|
16
|
+
envMapIntensity: 0.28,
|
|
17
|
+
pattern: {
|
|
18
|
+
kind: 'braided-rope',
|
|
19
|
+
primary: '#c58a38',
|
|
20
|
+
secondary: '#56300f',
|
|
21
|
+
worldSize: [0.18, 0.22],
|
|
22
|
+
},
|
|
23
|
+
textureUrl: '/themes/textures/old-assets/shared-rope-bridge-fibre-v2-albedo.webp',
|
|
24
|
+
normalTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-fibre-v2-normal.png',
|
|
25
|
+
roughnessTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-fibre-v2-roughness.png',
|
|
26
|
+
aoTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-fibre-v2-ao.png',
|
|
27
|
+
textureWorldSize: [0.42, 0.58],
|
|
28
|
+
normalScale: [1, 1],
|
|
29
|
+
aoMapIntensity: 0.62,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SHARED_ROPE_WALL_TIMBER_MATERIAL: MaterialRecipe = {
|
|
33
|
+
kind: 'standard',
|
|
34
|
+
color: '#b59a82',
|
|
35
|
+
roughness: 0.78,
|
|
36
|
+
metalness: 0,
|
|
37
|
+
envMapIntensity: 0.26,
|
|
38
|
+
pattern: {
|
|
39
|
+
kind: 'rough-wood',
|
|
40
|
+
primary: '#59331d',
|
|
41
|
+
secondary: '#160b07',
|
|
42
|
+
worldSize: [0.72, 1.55],
|
|
43
|
+
},
|
|
44
|
+
textureUrl: '/themes/textures/old-assets/shared-rope-bridge-post-timber-v2-albedo.webp',
|
|
45
|
+
normalTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-post-timber-v2-normal.png',
|
|
46
|
+
roughnessTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-post-timber-v2-roughness.png',
|
|
47
|
+
aoTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-post-timber-v2-ao.png',
|
|
48
|
+
textureWorldSize: [0.55, 0.85],
|
|
49
|
+
normalScale: [0.82, 0.82],
|
|
50
|
+
aoMapIntensity: 0.62,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SHARED_ROPE_WALL_END_GRAIN_MATERIAL: MaterialRecipe = {
|
|
54
|
+
kind: 'standard',
|
|
55
|
+
color: '#c39a72',
|
|
56
|
+
roughness: 0.72,
|
|
57
|
+
metalness: 0,
|
|
58
|
+
envMapIntensity: 0.24,
|
|
59
|
+
pattern: {
|
|
60
|
+
kind: 'wood',
|
|
61
|
+
primary: '#bc8247',
|
|
62
|
+
secondary: '#4b2916',
|
|
63
|
+
worldSize: [1, 1],
|
|
64
|
+
},
|
|
65
|
+
textureUrl: '/themes/textures/old-assets/shared-rope-bridge-end-grain-v2-albedo.webp',
|
|
66
|
+
normalTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-end-grain-v2-normal.png',
|
|
67
|
+
roughnessTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-end-grain-v2-roughness.png',
|
|
68
|
+
aoTextureUrl: '/themes/textures/old-assets/shared-rope-bridge-end-grain-v2-ao.png',
|
|
69
|
+
textureWorldSize: [1, 1],
|
|
70
|
+
normalScale: [0.72, 0.72],
|
|
71
|
+
aoMapIntensity: 0.58,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The existing continuous wall remains the sole physical barrier. This
|
|
76
|
+
* transparent recipe hides only its visual shell behind the open rope work.
|
|
77
|
+
*/
|
|
78
|
+
export const SHARED_ROPE_WALL_BACKING_MATERIAL: MaterialRecipe = {
|
|
79
|
+
kind: 'standard',
|
|
80
|
+
color: '#ffffff',
|
|
81
|
+
roughness: 1,
|
|
82
|
+
metalness: 0,
|
|
83
|
+
transparent: true,
|
|
84
|
+
opacity: 0,
|
|
85
|
+
depthWrite: false,
|
|
86
|
+
envMapIntensity: 0,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isSharedRopeWallAppearance(materialId: string | undefined): boolean {
|
|
90
|
+
return materialId === SHARED_ROPE_WALL_MATERIAL_ID
|
|
91
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { usesMobileRenderingBudget } from './mobile-rendering-budget'
|
|
4
|
+
|
|
5
|
+
import type { ThemeId, TrackThemeDefinition } from './track-theme'
|
|
6
|
+
|
|
7
|
+
export const SCENE_LIGHTING_MOBILE_BREAKPOINT = 700
|
|
8
|
+
export const STATIC_SHADOW_PROXY_LAYER = 31
|
|
9
|
+
|
|
10
|
+
export function usesMobileSceneLighting(viewportWidth: number): boolean {
|
|
11
|
+
return viewportWidth < SCENE_LIGHTING_MOBILE_BREAKPOINT || usesMobileRenderingBudget(Number.POSITIVE_INFINITY)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveDynamicShadowRefreshRate(
|
|
15
|
+
viewportWidth: number,
|
|
16
|
+
deltaSeconds: number,
|
|
17
|
+
currentRate = 60,
|
|
18
|
+
): 30 | 60 {
|
|
19
|
+
if (usesMobileSceneLighting(viewportWidth)) return 30
|
|
20
|
+
// Hysteresis keeps crisp shadow motion on fast desktops but backs off
|
|
21
|
+
// before shadow rendering can amplify an already slow frame.
|
|
22
|
+
if (deltaSeconds >= 1 / 45) return 30
|
|
23
|
+
if (deltaSeconds <= 1 / 55) return 60
|
|
24
|
+
return currentRate === 30 ? 30 : 60
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SceneLightingRig {
|
|
28
|
+
environmentIntensity: number
|
|
29
|
+
exposure: number
|
|
30
|
+
hemisphere: {
|
|
31
|
+
skyColor: string
|
|
32
|
+
groundColor: string
|
|
33
|
+
intensity: number
|
|
34
|
+
}
|
|
35
|
+
key: {
|
|
36
|
+
color: string
|
|
37
|
+
intensity: number
|
|
38
|
+
position: readonly [number, number, number]
|
|
39
|
+
}
|
|
40
|
+
rim: {
|
|
41
|
+
color: string
|
|
42
|
+
intensity: number
|
|
43
|
+
position: readonly [number, number, number]
|
|
44
|
+
}
|
|
45
|
+
shadow: {
|
|
46
|
+
mapSize: number
|
|
47
|
+
extent: number
|
|
48
|
+
near: number
|
|
49
|
+
far: number
|
|
50
|
+
bias: number
|
|
51
|
+
normalBias: number
|
|
52
|
+
intensity: number
|
|
53
|
+
focusDistance: number
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SHADOW_INTENSITY_BY_THEME: Partial<Record<ThemeId, number>> = {
|
|
58
|
+
'rocky-mountain-wild-west': 0.82,
|
|
59
|
+
'ancient-egypt': 0.82,
|
|
60
|
+
'japanese-sakura-lake': 0.8,
|
|
61
|
+
'intergalactic-space-station': 0.74,
|
|
62
|
+
'low-poly-jungle': 0.82,
|
|
63
|
+
'vampire-castle': 0.58,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const GAMEPLAY_ENVIRONMENT_SCALE = 0.72
|
|
67
|
+
const GAMEPLAY_HEMISPHERE_SCALE = 0.34
|
|
68
|
+
const GAMEPLAY_KEY_SCALE = 0.82
|
|
69
|
+
const GAMEPLAY_RIM_SCALE = 0.28
|
|
70
|
+
const GAMEPLAY_EXPOSURE_SCALE = 0.97
|
|
71
|
+
|
|
72
|
+
export function resolveSceneLightingRig(theme: TrackThemeDefinition, mobile: boolean): SceneLightingRig {
|
|
73
|
+
const vampireCastle = theme.id === 'vampire-castle'
|
|
74
|
+
const spaceStation = theme.id === 'intergalactic-space-station'
|
|
75
|
+
return {
|
|
76
|
+
environmentIntensity: THREE.MathUtils.clamp(
|
|
77
|
+
theme.environment.environmentIntensity * GAMEPLAY_ENVIRONMENT_SCALE,
|
|
78
|
+
0.38,
|
|
79
|
+
vampireCastle ? 0.92 : 0.86,
|
|
80
|
+
),
|
|
81
|
+
exposure: THREE.MathUtils.clamp(
|
|
82
|
+
theme.lighting.exposure * GAMEPLAY_EXPOSURE_SCALE,
|
|
83
|
+
0.82,
|
|
84
|
+
vampireCastle || spaceStation ? 1.15 : 1.05,
|
|
85
|
+
),
|
|
86
|
+
hemisphere: {
|
|
87
|
+
skyColor: theme.lighting.skyColor,
|
|
88
|
+
groundColor: theme.lighting.groundColor,
|
|
89
|
+
intensity: THREE.MathUtils.clamp(
|
|
90
|
+
theme.lighting.ambientIntensity * GAMEPLAY_HEMISPHERE_SCALE,
|
|
91
|
+
0.3,
|
|
92
|
+
vampireCastle ? 0.82 : 0.62,
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
key: {
|
|
96
|
+
color: theme.lighting.keyColor,
|
|
97
|
+
intensity: theme.lighting.keyIntensity * GAMEPLAY_KEY_SCALE,
|
|
98
|
+
position: theme.lighting.keyPosition,
|
|
99
|
+
},
|
|
100
|
+
rim: {
|
|
101
|
+
color: theme.lighting.rimColor,
|
|
102
|
+
intensity: theme.lighting.rimIntensity * GAMEPLAY_RIM_SCALE,
|
|
103
|
+
position: theme.lighting.rimPosition,
|
|
104
|
+
},
|
|
105
|
+
shadow: {
|
|
106
|
+
mapSize: mobile ? 1024 : 1536,
|
|
107
|
+
extent: mobile ? 36 : 52,
|
|
108
|
+
near: 1,
|
|
109
|
+
far: 220,
|
|
110
|
+
bias: -0.00008,
|
|
111
|
+
normalBias: 0.028,
|
|
112
|
+
intensity: SHADOW_INTENSITY_BY_THEME[theme.id as ThemeId] ?? 0.8,
|
|
113
|
+
focusDistance: mobile ? 12 : 18,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function configureSceneRenderer(renderer: THREE.WebGLRenderer, exposure = 1): void {
|
|
119
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
120
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
|
121
|
+
renderer.toneMappingExposure = exposure
|
|
122
|
+
renderer.shadowMap.enabled = true
|
|
123
|
+
renderer.shadowMap.type = THREE.PCFShadowMap
|
|
124
|
+
renderer.shadowMap.autoUpdate = false
|
|
125
|
+
renderer.shadowMap.needsUpdate = true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function configureKeyShadow(light: THREE.DirectionalLight, rig: SceneLightingRig): void {
|
|
129
|
+
const shadow = rig.shadow
|
|
130
|
+
light.castShadow = true
|
|
131
|
+
light.shadow.mapSize.set(shadow.mapSize, shadow.mapSize)
|
|
132
|
+
light.shadow.camera.left = -shadow.extent
|
|
133
|
+
light.shadow.camera.right = shadow.extent
|
|
134
|
+
light.shadow.camera.top = shadow.extent
|
|
135
|
+
light.shadow.camera.bottom = -shadow.extent
|
|
136
|
+
light.shadow.camera.near = shadow.near
|
|
137
|
+
light.shadow.camera.far = shadow.far
|
|
138
|
+
light.shadow.bias = shadow.bias
|
|
139
|
+
light.shadow.normalBias = shadow.normalBias
|
|
140
|
+
light.shadow.intensity = shadow.intensity
|
|
141
|
+
light.shadow.camera.layers.enable(STATIC_SHADOW_PROXY_LAYER)
|
|
142
|
+
light.shadow.camera.updateProjectionMatrix()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Dispose GPU shadow targets before removing a themed light rig. */
|
|
146
|
+
export function disposeSceneLightShadows(root: THREE.Object3D): void {
|
|
147
|
+
root.traverse(object => {
|
|
148
|
+
if (
|
|
149
|
+
object instanceof THREE.DirectionalLight ||
|
|
150
|
+
object instanceof THREE.PointLight ||
|
|
151
|
+
object instanceof THREE.SpotLight
|
|
152
|
+
) {
|
|
153
|
+
object.shadow.dispose()
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function computeShadowFocus(
|
|
159
|
+
camera: THREE.Camera,
|
|
160
|
+
rig: SceneLightingRig,
|
|
161
|
+
target = new THREE.Vector3(),
|
|
162
|
+
focusPoint?: THREE.Vector3,
|
|
163
|
+
shadowMapSize = rig.shadow.mapSize,
|
|
164
|
+
): THREE.Vector3 {
|
|
165
|
+
if (focusPoint) {
|
|
166
|
+
target.copy(focusPoint)
|
|
167
|
+
} else {
|
|
168
|
+
camera.getWorldDirection(target).multiplyScalar(rig.shadow.focusDistance)
|
|
169
|
+
const cameraMatrix = camera.matrixWorld.elements
|
|
170
|
+
target.x += cameraMatrix[12]
|
|
171
|
+
target.y += cameraMatrix[13]
|
|
172
|
+
target.z += cameraMatrix[14]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Stable, texel-sized steps prevent the shadow projection from shimmering as the camera eases forward.
|
|
176
|
+
const texelWorldSize = (rig.shadow.extent * 2) / shadowMapSize
|
|
177
|
+
target.set(
|
|
178
|
+
Math.round(target.x / texelWorldSize) * texelWorldSize,
|
|
179
|
+
Math.round(target.y / texelWorldSize) * texelWorldSize,
|
|
180
|
+
Math.round(target.z / texelWorldSize) * texelWorldSize,
|
|
181
|
+
)
|
|
182
|
+
return target
|
|
183
|
+
}
|