@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,1350 @@
|
|
|
1
|
+
import RAPIER from '@dimforge/rapier3d-deterministic-compat'
|
|
2
|
+
import {
|
|
3
|
+
ReboundPhysicsMechanism,
|
|
4
|
+
ConveyorPhysicsMechanism,
|
|
5
|
+
RisingWallPhysicsMechanism,
|
|
6
|
+
FlipperBankPhysicsMechanism,
|
|
7
|
+
SplitterPhysicsMechanism,
|
|
8
|
+
SPLITTER_CYCLE_SECONDS_RANGE,
|
|
9
|
+
SPLITTER_MAX_TIP_SPEED,
|
|
10
|
+
type SplitterMotionProfile,
|
|
11
|
+
VortexPhysicsMechanism,
|
|
12
|
+
} from '@marble.fun/physics-engine'
|
|
13
|
+
export { SPLITTER_CYCLE_SECONDS_RANGE, SPLITTER_MAX_TIP_SPEED } from '@marble.fun/physics-engine'
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
|
|
16
|
+
import { VisualSphereGeometry, VisualCylinderGeometry, VisualTorusGeometry } from '../../../three/visual-geometry'
|
|
17
|
+
|
|
18
|
+
import type { MaterialSurfaceTreatment, TrackAppearanceDetailStyle } from '../../../three/track-theme'
|
|
19
|
+
import { visualQualityPolicy, type VisualQualityPolicy } from '../../../three/visual-quality'
|
|
20
|
+
|
|
21
|
+
import { FIXED_TIMESTEP } from './race-physics'
|
|
22
|
+
import type {
|
|
23
|
+
ConveyorPieceDefinition,
|
|
24
|
+
DipperPieceDefinition,
|
|
25
|
+
ReboundPieceDefinition,
|
|
26
|
+
FlipperBankPieceDefinition,
|
|
27
|
+
PortalPieceDefinition,
|
|
28
|
+
RisingWallForestPieceDefinition,
|
|
29
|
+
SplitterPieceDefinition,
|
|
30
|
+
TrackVector,
|
|
31
|
+
TurntablePieceDefinition,
|
|
32
|
+
VortexPieceDefinition,
|
|
33
|
+
} from './track-definition'
|
|
34
|
+
import { trackSurfaceBasis } from './track-surface-basis'
|
|
35
|
+
import { markDynamicTrackShadowRoot } from './track-shadow-batching'
|
|
36
|
+
import { risingWallExposedHeight } from '../../track-lab/rising-wall-layout'
|
|
37
|
+
import { addWildWestMineLiftFrame, decorateWildWestMineLiftShelf, WILD_WEST_THEME_ID } from './wild-west-mine-lift'
|
|
38
|
+
import { addJungleTempleLiftSkin, decorateJungleTempleShelf, JUNGLE_TEMPLE_THEME_ID } from './jungle-temple-lift'
|
|
39
|
+
import {
|
|
40
|
+
addPortalDerivedElevatorSkins,
|
|
41
|
+
decoratePortalDerivedElevatorShelf,
|
|
42
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
43
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
44
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
45
|
+
} from './portal-derived-elevator-skins'
|
|
46
|
+
import { createReboundVisualFactory } from './rebound-visual'
|
|
47
|
+
import {
|
|
48
|
+
createPortalEndpointVisual,
|
|
49
|
+
PORTAL_LIVE_VIEW_FADE_START_RATIO,
|
|
50
|
+
portalLiveViewBlend,
|
|
51
|
+
positionFixedPortalViewCamera,
|
|
52
|
+
renderFixedPortalView,
|
|
53
|
+
type PortalEndpointVisual,
|
|
54
|
+
} from './portal-visual'
|
|
55
|
+
import { PORTAL_LIVE_VIEW_HEIGHT, PORTAL_LIVE_VIEW_WIDTH } from './portal-proportions'
|
|
56
|
+
import { SPACE_STATION_PORTAL_FRAME_THEME_ID } from './space-station-portal-frame'
|
|
57
|
+
import {
|
|
58
|
+
createEndPivotedFlipperPaddleVisual,
|
|
59
|
+
createFlipperHubVisual,
|
|
60
|
+
createFlipperPaddleVisual,
|
|
61
|
+
} from '../../track-lab/flipper-component'
|
|
62
|
+
|
|
63
|
+
export type FloorSurfaceTreatment = MaterialSurfaceTreatment & {
|
|
64
|
+
material: THREE.Material
|
|
65
|
+
themeId: string | readonly string[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TrackMaterials {
|
|
69
|
+
/** Active theme identity used to gate theme-specific visual geometry. */
|
|
70
|
+
activeThemeId?: string
|
|
71
|
+
/** Track Lab keeps reusable theme geometry resident so theme changes do not rebuild pieces. */
|
|
72
|
+
prebuildThemeVariants?: boolean
|
|
73
|
+
top: THREE.Material
|
|
74
|
+
edge: THREE.Material
|
|
75
|
+
rail: THREE.Material
|
|
76
|
+
railBacking: THREE.Material
|
|
77
|
+
/** Transparent visual shell used only by the open shared rope wall. */
|
|
78
|
+
ropeWallBacking?: THREE.Material
|
|
79
|
+
rope?: THREE.Material
|
|
80
|
+
ropeWallEndGrain?: THREE.Material
|
|
81
|
+
ropeWallTimber?: THREE.Material
|
|
82
|
+
structure: THREE.Material
|
|
83
|
+
moving: THREE.Material
|
|
84
|
+
signal: THREE.Material
|
|
85
|
+
hazard: THREE.Material
|
|
86
|
+
portal: THREE.Material
|
|
87
|
+
energy: THREE.Material
|
|
88
|
+
railroad?: THREE.Material
|
|
89
|
+
/** Geometry family selected by the independent rail/overlay appearance. */
|
|
90
|
+
railDetailStyle?: TrackAppearanceDetailStyle
|
|
91
|
+
railroadTimber?: THREE.Material
|
|
92
|
+
obstacle?: THREE.Material
|
|
93
|
+
/** Generic obstacle trim/end-cap role; Japanese Sakura binds dark gunmetal here. */
|
|
94
|
+
obstacleAccent?: THREE.Material
|
|
95
|
+
/** Generic obstacle mounting/base role; Japanese Sakura binds dark gunmetal here. */
|
|
96
|
+
obstacleBase?: THREE.Material
|
|
97
|
+
/** Rectangular flipper-bar role; Japanese Sakura binds polished silver here. */
|
|
98
|
+
flipperPrimary?: THREE.Material
|
|
99
|
+
/** Flipper base/hub role; Japanese Sakura binds dark gunmetal here. */
|
|
100
|
+
flipperSecondary?: THREE.Material
|
|
101
|
+
mineTimber?: THREE.Material
|
|
102
|
+
coveredMineTimber?: THREE.Material
|
|
103
|
+
coveredMineIron?: THREE.Material
|
|
104
|
+
portalIron?: THREE.Material
|
|
105
|
+
support?: THREE.Material
|
|
106
|
+
bone?: THREE.Material
|
|
107
|
+
floorSurfaceTreatment?: FloorSurfaceTreatment
|
|
108
|
+
/** Theme-gated treatments kept resident so a material-only theme swap can reveal them. */
|
|
109
|
+
floorSurfaceTreatments?: readonly FloorSurfaceTreatment[]
|
|
110
|
+
supportMetal?: THREE.Material
|
|
111
|
+
wildWestBrass?: THREE.Material
|
|
112
|
+
jungleTempleStone?: THREE.Material
|
|
113
|
+
jungleTempleFacade?: THREE.Material
|
|
114
|
+
jungleTempleTimber?: THREE.Material
|
|
115
|
+
jungleTemplePlatform?: THREE.Material
|
|
116
|
+
jungleTempleMoss?: THREE.Material
|
|
117
|
+
jungleTempleBronze?: THREE.Material
|
|
118
|
+
stoneTrackBlock?: THREE.Material
|
|
119
|
+
egyptPortalSandstone?: THREE.Material
|
|
120
|
+
egyptPortalCarvedSandstone?: THREE.Material
|
|
121
|
+
egyptPortalAgedGold?: THREE.Material
|
|
122
|
+
egyptPyramidLimestone?: THREE.Material
|
|
123
|
+
egyptPyramidGold?: THREE.Material
|
|
124
|
+
sakuraCastleTimber?: THREE.Material
|
|
125
|
+
sakuraCastleMasonry?: THREE.Material
|
|
126
|
+
sakuraCastleKawara?: THREE.Material
|
|
127
|
+
sakuraPortalFoundationStone?: THREE.Material
|
|
128
|
+
sakuraBridgeTimber?: THREE.Material
|
|
129
|
+
sakuraBridgeHardware?: THREE.Material
|
|
130
|
+
spaceStationWallPanel?: THREE.Material
|
|
131
|
+
spaceStationWallChassis?: THREE.Material
|
|
132
|
+
spaceStationWallRib?: THREE.Material
|
|
133
|
+
spaceStationWallTrim?: THREE.Material
|
|
134
|
+
spaceStationWallLight?: THREE.Material
|
|
135
|
+
spaceStationCenterGuide?: THREE.Material
|
|
136
|
+
spaceStationFloorDeck?: THREE.Material
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface PortalTeleportEvent {
|
|
140
|
+
slotId: string
|
|
141
|
+
portalId: string
|
|
142
|
+
tick: number
|
|
143
|
+
entryPosition: THREE.Vector3
|
|
144
|
+
exitPosition: THREE.Vector3
|
|
145
|
+
exitDirection: THREE.Vector3
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface TrackRenderBudget {
|
|
149
|
+
portalViewsRemaining: number
|
|
150
|
+
portalCandidateKey?: string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface TrackPortalRenderCandidate {
|
|
154
|
+
key: string
|
|
155
|
+
score: number
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface TrackMechanismVisualAdapter {
|
|
159
|
+
readonly id: string
|
|
160
|
+
readonly type: string
|
|
161
|
+
readonly bodyCount: number
|
|
162
|
+
readonly colliderCount: number
|
|
163
|
+
readonly sensorCount: number
|
|
164
|
+
portalRenderCandidate?(mainCamera: THREE.Camera): TrackPortalRenderCandidate | undefined
|
|
165
|
+
invalidatePortalViewCache?(): void
|
|
166
|
+
prepareRender?(
|
|
167
|
+
renderer: THREE.WebGLRenderer,
|
|
168
|
+
scene: THREE.Object3D,
|
|
169
|
+
mainCamera: THREE.Camera,
|
|
170
|
+
budget?: TrackRenderBudget,
|
|
171
|
+
): void
|
|
172
|
+
setVisualQualityPolicy?(policy: VisualQualityPolicy): void
|
|
173
|
+
dispose?(): void
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toVector3(value: TrackVector): THREE.Vector3 {
|
|
177
|
+
return new THREE.Vector3(value[0], value[1], value[2])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function toRapierRotation(quaternion: THREE.Quaternion): RAPIER.Rotation {
|
|
181
|
+
return { x: quaternion.x, y: quaternion.y, z: quaternion.z, w: quaternion.w }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createPortalRenderTarget(name: string, scale: number): THREE.WebGLRenderTarget {
|
|
185
|
+
const target = new THREE.WebGLRenderTarget(
|
|
186
|
+
Math.max(1, Math.round(PORTAL_LIVE_VIEW_WIDTH * scale)),
|
|
187
|
+
Math.max(1, Math.round(PORTAL_LIVE_VIEW_HEIGHT * scale)),
|
|
188
|
+
{
|
|
189
|
+
depthBuffer: true,
|
|
190
|
+
stencilBuffer: false,
|
|
191
|
+
minFilter: THREE.LinearFilter,
|
|
192
|
+
magFilter: THREE.LinearFilter,
|
|
193
|
+
generateMipmaps: false,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
target.texture.name = name
|
|
197
|
+
target.texture.colorSpace = THREE.SRGBColorSpace
|
|
198
|
+
return target
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const FLIPPER_BANK_MAX_TIP_SPEED = 4.5
|
|
202
|
+
|
|
203
|
+
function flipperMaterialsFor(materials: TrackMaterials): { primary: THREE.Material; secondary: THREE.Material } {
|
|
204
|
+
const fallback = materials.moving
|
|
205
|
+
return {
|
|
206
|
+
primary: materials.flipperPrimary ?? fallback,
|
|
207
|
+
secondary: materials.flipperSecondary ?? fallback,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function surfaceRotation(direction: TrackVector): THREE.Quaternion {
|
|
212
|
+
const basis = trackSurfaceBasis(direction)
|
|
213
|
+
return new THREE.Quaternion().setFromRotationMatrix(
|
|
214
|
+
new THREE.Matrix4().makeBasis(toVector3(basis.right), toVector3(basis.up), toVector3(basis.forward).negate()),
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class SplitterMechanism implements TrackMechanismVisualAdapter {
|
|
219
|
+
readonly type = 'splitter'
|
|
220
|
+
readonly bodyCount = 1
|
|
221
|
+
readonly colliderCount = 4
|
|
222
|
+
readonly sensorCount = 1
|
|
223
|
+
private readonly physics: SplitterPhysicsMechanism
|
|
224
|
+
private readonly visual: THREE.Group
|
|
225
|
+
|
|
226
|
+
constructor(
|
|
227
|
+
readonly id: string,
|
|
228
|
+
world: RAPIER.World,
|
|
229
|
+
root: THREE.Group,
|
|
230
|
+
definition: SplitterPieceDefinition,
|
|
231
|
+
motion: SplitterMotionProfile,
|
|
232
|
+
materials: TrackMaterials,
|
|
233
|
+
existingVisual?: THREE.Group,
|
|
234
|
+
) {
|
|
235
|
+
const center = toVector3(definition.center)
|
|
236
|
+
this.physics = new SplitterPhysicsMechanism(id, world, definition, motion)
|
|
237
|
+
const dimensions = {
|
|
238
|
+
length: definition.barLength,
|
|
239
|
+
width: definition.barDepth,
|
|
240
|
+
height: definition.barHeight,
|
|
241
|
+
}
|
|
242
|
+
const componentMaterials = flipperMaterialsFor(materials)
|
|
243
|
+
this.visual =
|
|
244
|
+
existingVisual ??
|
|
245
|
+
createEndPivotedFlipperPaddleVisual(
|
|
246
|
+
`${id}-splitter-paddle`,
|
|
247
|
+
componentMaterials.primary,
|
|
248
|
+
dimensions,
|
|
249
|
+
componentMaterials.secondary,
|
|
250
|
+
`${id}-splitter-hub`,
|
|
251
|
+
0,
|
|
252
|
+
)
|
|
253
|
+
this.visual.userData.cycleSecondsRange = [
|
|
254
|
+
Math.max(this.physics.minimumCycleSeconds, SPLITTER_CYCLE_SECONDS_RANGE[0]),
|
|
255
|
+
Math.max(this.physics.minimumCycleSeconds, SPLITTER_CYCLE_SECONDS_RANGE[1]),
|
|
256
|
+
]
|
|
257
|
+
this.visual.userData.maximumTipSpeed = SPLITTER_MAX_TIP_SPEED
|
|
258
|
+
this.visual.userData.motionProfile = 'seeded-smooth-variable-cycle-sweep'
|
|
259
|
+
this.visual.userData.rapierBodyType = 'kinematic-position'
|
|
260
|
+
this.visual.userData.sweepPhaseCycles = this.physics.sweepPhaseCycles
|
|
261
|
+
this.visual.userData.initialSide = this.physics.initialSide
|
|
262
|
+
this.visual.position.copy(center)
|
|
263
|
+
this.visual.frustumCulled = false
|
|
264
|
+
markDynamicTrackShadowRoot(this.visual)
|
|
265
|
+
if (!existingVisual) root.add(this.visual)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class RisingWallForestMechanism implements TrackMechanismVisualAdapter {
|
|
270
|
+
readonly type = 'rising-wall-forest'
|
|
271
|
+
readonly bodyCount: number
|
|
272
|
+
readonly colliderCount: number
|
|
273
|
+
readonly sensorCount = 0
|
|
274
|
+
private readonly physics: RisingWallPhysicsMechanism
|
|
275
|
+
private readonly visuals: THREE.Mesh[] = []
|
|
276
|
+
|
|
277
|
+
constructor(
|
|
278
|
+
readonly id: string,
|
|
279
|
+
world: RAPIER.World,
|
|
280
|
+
private readonly definition: RisingWallForestPieceDefinition,
|
|
281
|
+
simulationKey: string,
|
|
282
|
+
root: THREE.Group,
|
|
283
|
+
materials: TrackMaterials,
|
|
284
|
+
existingVisuals?: readonly THREE.Mesh[],
|
|
285
|
+
) {
|
|
286
|
+
const reusesExistingVisuals = existingVisuals?.length === definition.anchors.length
|
|
287
|
+
this.physics = new RisingWallPhysicsMechanism(id, world, definition, simulationKey)
|
|
288
|
+
this.bodyCount = this.physics.bodyCount
|
|
289
|
+
this.colliderCount = this.physics.colliderCount
|
|
290
|
+
const basis = trackSurfaceBasis(definition.direction)
|
|
291
|
+
const slotUp = toVector3(basis.up)
|
|
292
|
+
const slotRotation = surfaceRotation(definition.direction)
|
|
293
|
+
const wallGeometry = reusesExistingVisuals
|
|
294
|
+
? undefined
|
|
295
|
+
: new THREE.BoxGeometry(definition.wallWidth, definition.wallHeight, definition.wallDepth)
|
|
296
|
+
const slotGeometry = reusesExistingVisuals
|
|
297
|
+
? undefined
|
|
298
|
+
: new THREE.BoxGeometry(definition.wallWidth + 0.14, 0.035, definition.wallDepth + 0.14)
|
|
299
|
+
definition.anchors.forEach((anchor, wallIndex) => {
|
|
300
|
+
const fraction = this.physics.heights[wallIndex]!
|
|
301
|
+
const wall = reusesExistingVisuals
|
|
302
|
+
? existingVisuals![wallIndex]!
|
|
303
|
+
: new THREE.Mesh(wallGeometry!, materials.obstacle ?? materials.hazard)
|
|
304
|
+
if (!reusesExistingVisuals) {
|
|
305
|
+
wall.name = `${id}-wall-${wallIndex}`
|
|
306
|
+
wall.castShadow = true
|
|
307
|
+
wall.receiveShadow = true
|
|
308
|
+
root.add(wall)
|
|
309
|
+
const slot = new THREE.Mesh(slotGeometry!, materials.obstacleBase ?? materials.signal)
|
|
310
|
+
slot.name = `${id}-wall-slot-${wallIndex}`
|
|
311
|
+
slot.position.copy(toVector3(anchor.position)).addScaledVector(slotUp, 0.018)
|
|
312
|
+
slot.quaternion.copy(slotRotation)
|
|
313
|
+
slot.receiveShadow = true
|
|
314
|
+
root.add(slot)
|
|
315
|
+
}
|
|
316
|
+
wall.userData.risingWallVisual = {
|
|
317
|
+
anchor: [...anchor.position],
|
|
318
|
+
up: [...this.physics.up],
|
|
319
|
+
wallHeight: definition.wallHeight,
|
|
320
|
+
}
|
|
321
|
+
markDynamicTrackShadowRoot(wall)
|
|
322
|
+
this.visuals.push(wall)
|
|
323
|
+
this.updateWallVisual(wallIndex, fraction)
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private updateWallVisual(index: number, fraction: number): void {
|
|
328
|
+
const exposedHeight = risingWallExposedHeight(this.definition.wallHeight, fraction)
|
|
329
|
+
const wall = this.visuals[index]!
|
|
330
|
+
wall.position
|
|
331
|
+
.copy(toVector3(this.definition.anchors[index]!.position))
|
|
332
|
+
.addScaledVector(toVector3(this.physics.up), exposedHeight / 2)
|
|
333
|
+
wall.quaternion.set(...this.physics.rotation)
|
|
334
|
+
wall.scale.set(1, exposedHeight / this.definition.wallHeight, 1)
|
|
335
|
+
wall.visible = exposedHeight > 0.001
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export { deterministicPortalExitVariation } from '@marble.fun/physics-engine'
|
|
340
|
+
|
|
341
|
+
class PortalMechanism implements TrackMechanismVisualAdapter {
|
|
342
|
+
readonly type = 'portal'
|
|
343
|
+
readonly bodyCount = 0
|
|
344
|
+
readonly colliderCount = 0
|
|
345
|
+
readonly sensorCount = 0
|
|
346
|
+
private readonly visual: THREE.Group
|
|
347
|
+
private readonly entryVisual: PortalEndpointVisual
|
|
348
|
+
private readonly exitVisual: PortalEndpointVisual
|
|
349
|
+
private readonly liveTargets: Partial<Record<'entry' | 'exit', THREE.WebGLRenderTarget>> = {}
|
|
350
|
+
private readonly liveCamera = new THREE.PerspectiveCamera(62, 1, 0.08, 260)
|
|
351
|
+
private readonly liveViewProjection = new THREE.Matrix4()
|
|
352
|
+
private readonly liveViewFrustum = new THREE.Frustum()
|
|
353
|
+
private readonly liveViewBounds = new THREE.Sphere()
|
|
354
|
+
private readonly liveViewProjectedCenter = new THREE.Vector3()
|
|
355
|
+
private readonly liveViewWorldQuaternion = new THREE.Quaternion()
|
|
356
|
+
private readonly liveViewWorldNormal = new THREE.Vector3()
|
|
357
|
+
private readonly liveViewCameraPosition = new THREE.Vector3()
|
|
358
|
+
private readonly liveViewToCamera = new THREE.Vector3()
|
|
359
|
+
private qualityPolicy: VisualQualityPolicy
|
|
360
|
+
private readonly portalColor: string
|
|
361
|
+
|
|
362
|
+
constructor(
|
|
363
|
+
readonly id: string,
|
|
364
|
+
root: THREE.Group,
|
|
365
|
+
definition: PortalPieceDefinition,
|
|
366
|
+
materials: TrackMaterials,
|
|
367
|
+
qualityPolicy: VisualQualityPolicy,
|
|
368
|
+
existingVisuals?: { entry: PortalEndpointVisual; exit: PortalEndpointVisual },
|
|
369
|
+
) {
|
|
370
|
+
this.qualityPolicy = qualityPolicy
|
|
371
|
+
this.portalColor = definition.color
|
|
372
|
+
this.visual = new THREE.Group()
|
|
373
|
+
this.visual.name = `${id}-portal-pair`
|
|
374
|
+
const activeThemeId = materials.activeThemeId
|
|
375
|
+
const endpointMaterials = {
|
|
376
|
+
activeThemeId,
|
|
377
|
+
structure: materials.structure,
|
|
378
|
+
timber: materials.mineTimber ?? materials.structure,
|
|
379
|
+
metal: materials.portalIron ?? materials.railroad ?? materials.edge,
|
|
380
|
+
portal: materials.portal,
|
|
381
|
+
energy: materials.energy,
|
|
382
|
+
jungle:
|
|
383
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
384
|
+
? {
|
|
385
|
+
stone: materials.jungleTempleStone ?? materials.structure,
|
|
386
|
+
facade: materials.jungleTempleFacade ?? materials.railBacking,
|
|
387
|
+
bronze: materials.jungleTempleBronze ?? materials.signal,
|
|
388
|
+
foliage: materials.jungleTempleMoss ?? materials.structure,
|
|
389
|
+
}
|
|
390
|
+
: undefined,
|
|
391
|
+
egypt:
|
|
392
|
+
activeThemeId === EGYPT_ELEVATOR_THEME_ID
|
|
393
|
+
? {
|
|
394
|
+
sandstone: materials.egyptPortalSandstone ?? materials.structure,
|
|
395
|
+
carvedStone: materials.egyptPortalCarvedSandstone ?? materials.railBacking,
|
|
396
|
+
agedGold: materials.egyptPortalAgedGold ?? materials.signal,
|
|
397
|
+
}
|
|
398
|
+
: undefined,
|
|
399
|
+
sakura:
|
|
400
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
401
|
+
? {
|
|
402
|
+
vermilion: materials.sakuraBridgeTimber ?? materials.structure,
|
|
403
|
+
plaster: materials.railBacking,
|
|
404
|
+
foundationStone: materials.sakuraPortalFoundationStone ?? materials.structure,
|
|
405
|
+
kawara: materials.sakuraCastleKawara ?? materials.edge,
|
|
406
|
+
hardware: materials.sakuraBridgeHardware ?? materials.portalIron ?? materials.edge,
|
|
407
|
+
}
|
|
408
|
+
: undefined,
|
|
409
|
+
spaceStation:
|
|
410
|
+
activeThemeId === SPACE_STATION_PORTAL_FRAME_THEME_ID
|
|
411
|
+
? {
|
|
412
|
+
chassis: materials.spaceStationWallChassis ?? materials.railBacking,
|
|
413
|
+
panel: materials.spaceStationWallPanel ?? materials.rail,
|
|
414
|
+
rib: materials.spaceStationWallRib ?? materials.structure,
|
|
415
|
+
trim: materials.spaceStationWallTrim ?? materials.edge,
|
|
416
|
+
light: materials.spaceStationWallLight ?? materials.energy,
|
|
417
|
+
}
|
|
418
|
+
: undefined,
|
|
419
|
+
}
|
|
420
|
+
this.entryVisual =
|
|
421
|
+
existingVisuals?.entry ??
|
|
422
|
+
createPortalEndpointVisual(`${id}-entry`, 'live-window', definition.color, endpointMaterials)
|
|
423
|
+
this.exitVisual =
|
|
424
|
+
existingVisuals?.exit ??
|
|
425
|
+
createPortalEndpointVisual(`${id}-exit`, 'live-window', definition.color, endpointMaterials)
|
|
426
|
+
if (!existingVisuals) {
|
|
427
|
+
for (const [endpoint, center, direction] of [
|
|
428
|
+
[this.entryVisual, toVector3(definition.entryVisualCenter), toVector3(definition.entryVisualDirection)],
|
|
429
|
+
[this.exitVisual, toVector3(definition.exitVisualCenter), toVector3(definition.exitVisualDirection)],
|
|
430
|
+
] as const) {
|
|
431
|
+
const rotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), direction.normalize())
|
|
432
|
+
endpoint.group.position.copy(center)
|
|
433
|
+
endpoint.group.quaternion.copy(rotation)
|
|
434
|
+
this.visual.add(endpoint.group)
|
|
435
|
+
}
|
|
436
|
+
root.add(this.visual)
|
|
437
|
+
}
|
|
438
|
+
this.entryVisual.setFallback(this.portalColor)
|
|
439
|
+
this.exitVisual.setFallback(this.portalColor)
|
|
440
|
+
this.configureLiveTargets()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
dispose(): void {
|
|
444
|
+
this.disposeLiveTargets()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
setVisualQualityPolicy(policy: VisualQualityPolicy): void {
|
|
448
|
+
if (this.qualityPolicy === policy) return
|
|
449
|
+
this.qualityPolicy = policy
|
|
450
|
+
this.disposeLiveTargets()
|
|
451
|
+
this.entryVisual.setFallback(this.portalColor)
|
|
452
|
+
this.exitVisual.setFallback(this.portalColor)
|
|
453
|
+
this.configureLiveTargets()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
invalidatePortalViewCache(): void {
|
|
457
|
+
this.entryVisual.setFallback(this.portalColor)
|
|
458
|
+
this.exitVisual.setFallback(this.portalColor)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
prepareRender(
|
|
462
|
+
renderer: THREE.WebGLRenderer,
|
|
463
|
+
scene: THREE.Object3D,
|
|
464
|
+
mainCamera: THREE.Camera,
|
|
465
|
+
budget: TrackRenderBudget = { portalViewsRemaining: 2 },
|
|
466
|
+
): void {
|
|
467
|
+
// Rift animation is independent of destination-camera rendering.
|
|
468
|
+
const renderTimeMs = globalThis.performance.now()
|
|
469
|
+
this.entryVisual.update(renderTimeMs / 1_000)
|
|
470
|
+
this.exitVisual.update(renderTimeMs / 1_000)
|
|
471
|
+
if (budget.portalViewsRemaining <= 0) return
|
|
472
|
+
if (!this.liveTargets.entry && !this.liveTargets.exit) return
|
|
473
|
+
const candidate = this.portalRenderCandidate(mainCamera)
|
|
474
|
+
if (!candidate || (budget.portalCandidateKey && candidate.key !== budget.portalCandidateKey)) return
|
|
475
|
+
const endpoint: 'entry' | 'exit' = candidate.key.endsWith(':entry') ? 'entry' : 'exit'
|
|
476
|
+
const previousTarget = renderer.getRenderTarget()
|
|
477
|
+
const previousXr = renderer.xr.enabled
|
|
478
|
+
const previousEntryVisibility = this.entryVisual.group.visible
|
|
479
|
+
const previousExitVisibility = this.exitVisual.group.visible
|
|
480
|
+
this.entryVisual.group.visible = false
|
|
481
|
+
this.exitVisual.group.visible = false
|
|
482
|
+
renderer.xr.enabled = false
|
|
483
|
+
this.liveCamera.layers.mask = mainCamera.layers.mask
|
|
484
|
+
try {
|
|
485
|
+
if (endpoint === 'entry' && this.liveTargets.entry && budget.portalViewsRemaining > 0) {
|
|
486
|
+
this.renderRemoteView(
|
|
487
|
+
renderer,
|
|
488
|
+
scene,
|
|
489
|
+
mainCamera,
|
|
490
|
+
this.exitVisual.group,
|
|
491
|
+
this.liveTargets.entry,
|
|
492
|
+
)
|
|
493
|
+
this.entryVisual.setLiveTexture(this.liveTargets.entry.texture)
|
|
494
|
+
this.entryVisual.setLiveBlend(portalLiveViewBlend(candidate.score))
|
|
495
|
+
budget.portalViewsRemaining -= 1
|
|
496
|
+
} else if (endpoint === 'exit' && this.liveTargets.exit && budget.portalViewsRemaining > 0) {
|
|
497
|
+
this.renderRemoteView(
|
|
498
|
+
renderer,
|
|
499
|
+
scene,
|
|
500
|
+
mainCamera,
|
|
501
|
+
this.entryVisual.group,
|
|
502
|
+
this.liveTargets.exit,
|
|
503
|
+
)
|
|
504
|
+
this.exitVisual.setLiveTexture(this.liveTargets.exit.texture)
|
|
505
|
+
this.exitVisual.setLiveBlend(portalLiveViewBlend(candidate.score))
|
|
506
|
+
budget.portalViewsRemaining -= 1
|
|
507
|
+
}
|
|
508
|
+
} finally {
|
|
509
|
+
renderer.setRenderTarget(previousTarget)
|
|
510
|
+
renderer.xr.enabled = previousXr
|
|
511
|
+
this.entryVisual.group.visible = previousEntryVisibility
|
|
512
|
+
this.exitVisual.group.visible = previousExitVisibility
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
portalRenderCandidate(mainCamera: THREE.Camera): TrackPortalRenderCandidate | undefined {
|
|
517
|
+
if (!this.liveTargets.entry && !this.liveTargets.exit) return undefined
|
|
518
|
+
mainCamera.updateWorldMatrix(true, false)
|
|
519
|
+
this.liveViewProjection.multiplyMatrices(mainCamera.projectionMatrix, mainCamera.matrixWorldInverse)
|
|
520
|
+
this.liveViewFrustum.setFromProjectionMatrix(this.liveViewProjection)
|
|
521
|
+
const rawEntryScore = this.liveTargets.entry ? this.endpointScreenRatio(this.entryVisual, mainCamera) : 0
|
|
522
|
+
const rawExitScore = this.liveTargets.exit ? this.endpointScreenRatio(this.exitVisual, mainCamera) : 0
|
|
523
|
+
this.entryVisual.setLiveBlend(portalLiveViewBlend(rawEntryScore))
|
|
524
|
+
this.exitVisual.setLiveBlend(portalLiveViewBlend(rawExitScore))
|
|
525
|
+
const entryScore =
|
|
526
|
+
!this.entryVisual.screen.userData.liveTextureReady && rawEntryScore >= PORTAL_LIVE_VIEW_FADE_START_RATIO
|
|
527
|
+
? rawEntryScore
|
|
528
|
+
: 0
|
|
529
|
+
const exitScore =
|
|
530
|
+
!this.exitVisual.screen.userData.liveTextureReady && rawExitScore >= PORTAL_LIVE_VIEW_FADE_START_RATIO
|
|
531
|
+
? rawExitScore
|
|
532
|
+
: 0
|
|
533
|
+
if (entryScore <= 0 && exitScore <= 0) return undefined
|
|
534
|
+
return entryScore >= exitScore
|
|
535
|
+
? { key: `${this.id}:entry`, score: entryScore }
|
|
536
|
+
: { key: `${this.id}:exit`, score: exitScore }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private endpointScreenRatio(endpoint: PortalEndpointVisual, camera: THREE.Camera): number {
|
|
540
|
+
let ancestor: THREE.Object3D | null = endpoint.screen
|
|
541
|
+
while (ancestor) {
|
|
542
|
+
if (!ancestor.visible) return 0
|
|
543
|
+
ancestor = ancestor.parent
|
|
544
|
+
}
|
|
545
|
+
if (!camera.layers.test(endpoint.screen.layers)) return 0
|
|
546
|
+
endpoint.screen.updateWorldMatrix(true, false)
|
|
547
|
+
if (!endpoint.screen.geometry.boundingSphere) endpoint.screen.geometry.computeBoundingSphere()
|
|
548
|
+
const localBounds = endpoint.screen.geometry.boundingSphere
|
|
549
|
+
if (!localBounds) return 0
|
|
550
|
+
this.liveViewBounds.copy(localBounds).applyMatrix4(endpoint.screen.matrixWorld)
|
|
551
|
+
if (!this.liveViewFrustum.intersectsSphere(this.liveViewBounds)) return 0
|
|
552
|
+
this.liveViewProjectedCenter.copy(this.liveViewBounds.center).project(camera)
|
|
553
|
+
if (
|
|
554
|
+
Math.abs(this.liveViewProjectedCenter.x) > 1 ||
|
|
555
|
+
Math.abs(this.liveViewProjectedCenter.y) > 1 ||
|
|
556
|
+
this.liveViewProjectedCenter.z < -1 ||
|
|
557
|
+
this.liveViewProjectedCenter.z > 1
|
|
558
|
+
) {
|
|
559
|
+
return 0
|
|
560
|
+
}
|
|
561
|
+
endpoint.group.getWorldQuaternion(this.liveViewWorldQuaternion)
|
|
562
|
+
this.liveViewWorldNormal.set(0, 0, 1).applyQuaternion(this.liveViewWorldQuaternion)
|
|
563
|
+
camera.getWorldPosition(this.liveViewCameraPosition)
|
|
564
|
+
this.liveViewToCamera.subVectors(this.liveViewCameraPosition, this.liveViewBounds.center)
|
|
565
|
+
const distance = Math.max(0.001, this.liveViewToCamera.length())
|
|
566
|
+
if (this.liveViewWorldNormal.dot(this.liveViewToCamera) <= 0) return 0
|
|
567
|
+
return this.liveViewBounds.radius / distance
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private renderRemoteView(
|
|
571
|
+
renderer: THREE.WebGLRenderer,
|
|
572
|
+
scene: THREE.Object3D,
|
|
573
|
+
viewerCamera: THREE.Camera,
|
|
574
|
+
destination: THREE.Group,
|
|
575
|
+
target: THREE.WebGLRenderTarget,
|
|
576
|
+
): void {
|
|
577
|
+
positionFixedPortalViewCamera(
|
|
578
|
+
this.liveCamera,
|
|
579
|
+
viewerCamera,
|
|
580
|
+
destination,
|
|
581
|
+
target.width / target.height,
|
|
582
|
+
)
|
|
583
|
+
renderFixedPortalView(renderer, scene, this.liveCamera, target)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private releaseLiveTargetTexture(endpoint: PortalEndpointVisual, target: THREE.WebGLRenderTarget | undefined): void {
|
|
587
|
+
if (!target || !(endpoint.screen.material instanceof THREE.MeshBasicMaterial)) return
|
|
588
|
+
if (endpoint.screen.material.map !== target.texture) return
|
|
589
|
+
endpoint.screen.material.map = null
|
|
590
|
+
endpoint.screen.material.needsUpdate = true
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private configureLiveTargets(): void {
|
|
594
|
+
if (this.qualityPolicy.portal.mode === 'static') return
|
|
595
|
+
const scale = this.qualityPolicy.portal.renderTargetScale
|
|
596
|
+
this.liveTargets.entry = createPortalRenderTarget(`${this.id}-entry-window`, scale)
|
|
597
|
+
this.liveTargets.exit = createPortalRenderTarget(`${this.id}-exit-window`, scale)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private disposeLiveTargets(): void {
|
|
601
|
+
this.releaseLiveTargetTexture(this.entryVisual, this.liveTargets.entry)
|
|
602
|
+
this.releaseLiveTargetTexture(this.exitVisual, this.liveTargets.exit)
|
|
603
|
+
this.liveTargets.entry?.dispose()
|
|
604
|
+
this.liveTargets.exit?.dispose()
|
|
605
|
+
delete this.liveTargets.entry
|
|
606
|
+
delete this.liveTargets.exit
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const FLIPPER_LOCAL_UP = new THREE.Vector3(0, 1, 0)
|
|
611
|
+
|
|
612
|
+
class FlipperBankMechanism implements TrackMechanismVisualAdapter {
|
|
613
|
+
readonly type = 'flipper-bank'
|
|
614
|
+
readonly bodyCount: number
|
|
615
|
+
readonly colliderCount: number
|
|
616
|
+
readonly sensorCount = 0
|
|
617
|
+
private readonly physics: FlipperBankPhysicsMechanism
|
|
618
|
+
|
|
619
|
+
constructor(
|
|
620
|
+
readonly id: string,
|
|
621
|
+
world: RAPIER.World,
|
|
622
|
+
root: THREE.Group,
|
|
623
|
+
definition: FlipperBankPieceDefinition,
|
|
624
|
+
simulationKey: string,
|
|
625
|
+
materials: TrackMaterials,
|
|
626
|
+
existingVisuals?: readonly THREE.Group[],
|
|
627
|
+
) {
|
|
628
|
+
const reusesExistingVisuals = existingVisuals?.length === definition.mounts.length
|
|
629
|
+
this.physics = new FlipperBankPhysicsMechanism(id, world, definition, simulationKey)
|
|
630
|
+
this.bodyCount = this.physics.bodyCount
|
|
631
|
+
this.colliderCount = this.physics.colliderCount
|
|
632
|
+
|
|
633
|
+
const dimensions = {
|
|
634
|
+
length: definition.paddleLength,
|
|
635
|
+
width: definition.paddleWidth,
|
|
636
|
+
height: definition.paddleHeight,
|
|
637
|
+
}
|
|
638
|
+
const flipperMaterials = flipperMaterialsFor(materials)
|
|
639
|
+
definition.mounts.forEach((mount, index) => {
|
|
640
|
+
const tangent = toVector3(mount.direction ?? definition.direction).normalize()
|
|
641
|
+
const right = new THREE.Vector3(tangent.z, 0, -tangent.x).normalize()
|
|
642
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
643
|
+
const state = this.physics.flippers[index]!
|
|
644
|
+
const baseRotation = new THREE.Quaternion(...state.baseRotation)
|
|
645
|
+
const hubRotation = new THREE.Quaternion().setFromUnitVectors(FLIPPER_LOCAL_UP, up)
|
|
646
|
+
const hinge = toVector3(state.hinge)
|
|
647
|
+
const bodyPosition = state.body.translation()
|
|
648
|
+
const center = new THREE.Vector3(bodyPosition.x, bodyPosition.y, bodyPosition.z)
|
|
649
|
+
|
|
650
|
+
const visual =
|
|
651
|
+
(reusesExistingVisuals ? existingVisuals![index] : undefined) ??
|
|
652
|
+
createFlipperPaddleVisual(`${id}-paddle-${index + 1}`, flipperMaterials.primary, dimensions)
|
|
653
|
+
visual.position.copy(center)
|
|
654
|
+
visual.quaternion.copy(baseRotation)
|
|
655
|
+
visual.userData.maximumTipSpeed = FLIPPER_BANK_MAX_TIP_SPEED
|
|
656
|
+
visual.userData.strikeSeconds = this.physics.strikeTicks * FIXED_TIMESTEP
|
|
657
|
+
visual.userData.returnSeconds = this.physics.returnTicks * FIXED_TIMESTEP
|
|
658
|
+
visual.userData.motionProfile = 'deterministic-bounded-strike'
|
|
659
|
+
markDynamicTrackShadowRoot(visual)
|
|
660
|
+
if (!reusesExistingVisuals) root.add(visual)
|
|
661
|
+
|
|
662
|
+
if (!reusesExistingVisuals) {
|
|
663
|
+
const hub = createFlipperHubVisual(`${id}-hub-${index + 1}`, flipperMaterials.secondary, dimensions)
|
|
664
|
+
hub.position.copy(hinge)
|
|
665
|
+
hub.quaternion.copy(hubRotation)
|
|
666
|
+
hub.castShadow = true
|
|
667
|
+
hub.receiveShadow = true
|
|
668
|
+
root.add(hub)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
void visual
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
class ReboundMechanism implements TrackMechanismVisualAdapter {
|
|
677
|
+
readonly type = 'rebound'
|
|
678
|
+
readonly bodyCount: number
|
|
679
|
+
readonly colliderCount: number
|
|
680
|
+
readonly sensorCount = 0
|
|
681
|
+
private readonly physics: ReboundPhysicsMechanism
|
|
682
|
+
|
|
683
|
+
constructor(
|
|
684
|
+
readonly id: string,
|
|
685
|
+
world: RAPIER.World,
|
|
686
|
+
root: THREE.Group,
|
|
687
|
+
definition: ReboundPieceDefinition,
|
|
688
|
+
materials: TrackMaterials,
|
|
689
|
+
existingVisuals?: readonly THREE.Group[],
|
|
690
|
+
) {
|
|
691
|
+
const reusesExistingVisuals = existingVisuals?.length === definition.mounts.length
|
|
692
|
+
this.physics = new ReboundPhysicsMechanism(id, world, definition)
|
|
693
|
+
this.bodyCount = this.physics.bodyCount
|
|
694
|
+
this.colliderCount = this.physics.colliderCount
|
|
695
|
+
const createVisual = createReboundVisualFactory({
|
|
696
|
+
reboundLength: definition.reboundLength,
|
|
697
|
+
reboundWidth: definition.reboundWidth,
|
|
698
|
+
reboundHeight: definition.reboundHeight,
|
|
699
|
+
bodyMaterial: materials.obstacle ?? materials.hazard,
|
|
700
|
+
accentMaterial: materials.obstacleAccent ?? materials.railroad ?? materials.edge,
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
definition.mounts.forEach((_mount, index) => {
|
|
704
|
+
const state = this.physics.rebounds[index]!
|
|
705
|
+
const bodyPosition = state.body.translation()
|
|
706
|
+
const bodyRotation = state.body.rotation()
|
|
707
|
+
|
|
708
|
+
const visual = (reusesExistingVisuals ? existingVisuals![index] : undefined) ?? createVisual()
|
|
709
|
+
visual.name = `${id}-rebound-${index}`
|
|
710
|
+
visual.position.set(bodyPosition.x, bodyPosition.y, bodyPosition.z)
|
|
711
|
+
visual.quaternion.set(bodyRotation.x, bodyRotation.y, bodyRotation.z, bodyRotation.w)
|
|
712
|
+
visual.frustumCulled = false
|
|
713
|
+
markDynamicTrackShadowRoot(visual)
|
|
714
|
+
if (!reusesExistingVisuals) root.add(visual)
|
|
715
|
+
|
|
716
|
+
void visual
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
class TurntableMechanism implements TrackMechanismVisualAdapter {
|
|
722
|
+
readonly type = 'turntable'
|
|
723
|
+
readonly bodyCount = 1
|
|
724
|
+
readonly colliderCount = 3
|
|
725
|
+
readonly sensorCount = 1
|
|
726
|
+
private readonly body: RAPIER.RigidBody
|
|
727
|
+
private readonly visual: THREE.Group
|
|
728
|
+
private readonly center: THREE.Vector3
|
|
729
|
+
private readonly baseRotation: THREE.Quaternion
|
|
730
|
+
private readonly rotation = new THREE.Quaternion()
|
|
731
|
+
|
|
732
|
+
constructor(
|
|
733
|
+
readonly id: string,
|
|
734
|
+
world: RAPIER.World,
|
|
735
|
+
root: THREE.Group,
|
|
736
|
+
definition: TurntablePieceDefinition,
|
|
737
|
+
materials: TrackMaterials,
|
|
738
|
+
) {
|
|
739
|
+
if (definition.routeAngles.length < 2) {
|
|
740
|
+
throw new Error(`Turntable ${id} requires at least two route angles`)
|
|
741
|
+
}
|
|
742
|
+
const tangent = toVector3(definition.direction).normalize()
|
|
743
|
+
const right = new THREE.Vector3(tangent.z, 0, -tangent.x).normalize()
|
|
744
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
745
|
+
this.baseRotation = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(right, up, tangent))
|
|
746
|
+
// Sink the rotating disk by its half-height so its top is exactly flush
|
|
747
|
+
// with the authored entry rail. This avoids a hidden curb at the rim.
|
|
748
|
+
this.center = toVector3(definition.center).addScaledVector(up, -0.11)
|
|
749
|
+
this.rotation
|
|
750
|
+
.copy(this.baseRotation)
|
|
751
|
+
.multiply(new THREE.Quaternion().setFromAxisAngle(FLIPPER_LOCAL_UP, definition.routeAngles[0]!))
|
|
752
|
+
|
|
753
|
+
this.body = world.createRigidBody(
|
|
754
|
+
RAPIER.RigidBodyDesc.kinematicPositionBased()
|
|
755
|
+
.setTranslation(this.center.x, this.center.y, this.center.z)
|
|
756
|
+
.setRotation(toRapierRotation(this.rotation)),
|
|
757
|
+
)
|
|
758
|
+
world.createCollider(
|
|
759
|
+
RAPIER.ColliderDesc.cylinder(0.11, definition.radius).setFriction(0.18).setRestitution(0.01),
|
|
760
|
+
this.body,
|
|
761
|
+
)
|
|
762
|
+
const guideLength = definition.radius * 0.95
|
|
763
|
+
for (const side of [-1, 1] as const) {
|
|
764
|
+
world.createCollider(
|
|
765
|
+
RAPIER.ColliderDesc.cuboid(0.11, definition.wallHeight / 2, guideLength / 2)
|
|
766
|
+
.setTranslation(side * (definition.channelWidth / 2 - 0.11), definition.wallHeight / 2 + 0.1, guideLength / 2)
|
|
767
|
+
.setFriction(0.08)
|
|
768
|
+
.setRestitution(0.02),
|
|
769
|
+
this.body,
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
world.createCollider(
|
|
773
|
+
RAPIER.ColliderDesc.ball(definition.triggerRadius)
|
|
774
|
+
.setTranslation(this.center.x + up.x * 0.48, this.center.y + up.y * 0.48, this.center.z + up.z * 0.48)
|
|
775
|
+
.setSensor(true),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
this.visual = new THREE.Group()
|
|
779
|
+
this.visual.name = `${id}-rotating-channel`
|
|
780
|
+
this.visual.position.copy(this.center)
|
|
781
|
+
this.visual.quaternion.copy(this.rotation)
|
|
782
|
+
this.visual.frustumCulled = false
|
|
783
|
+
const disk = new THREE.Mesh(
|
|
784
|
+
new VisualCylinderGeometry(definition.radius, definition.radius, 0.22, 36),
|
|
785
|
+
materials.structure,
|
|
786
|
+
)
|
|
787
|
+
disk.castShadow = true
|
|
788
|
+
disk.receiveShadow = true
|
|
789
|
+
this.visual.add(disk)
|
|
790
|
+
const guideGeometry = new THREE.BoxGeometry(0.22, definition.wallHeight, guideLength)
|
|
791
|
+
for (const side of [-1, 1] as const) {
|
|
792
|
+
const guide = new THREE.Mesh(guideGeometry, materials.hazard)
|
|
793
|
+
guide.position.set(side * (definition.channelWidth / 2 - 0.11), definition.wallHeight / 2 + 0.1, guideLength / 2)
|
|
794
|
+
guide.castShadow = true
|
|
795
|
+
guide.receiveShadow = true
|
|
796
|
+
this.visual.add(guide)
|
|
797
|
+
}
|
|
798
|
+
const hub = new THREE.Mesh(
|
|
799
|
+
new VisualCylinderGeometry(0.48, 0.48, definition.wallHeight + 0.34, 24),
|
|
800
|
+
materials.signal,
|
|
801
|
+
)
|
|
802
|
+
hub.position.y = definition.wallHeight / 2 + 0.1
|
|
803
|
+
hub.castShadow = true
|
|
804
|
+
this.visual.add(hub)
|
|
805
|
+
markDynamicTrackShadowRoot(this.visual)
|
|
806
|
+
root.add(this.visual)
|
|
807
|
+
|
|
808
|
+
const rimGroup = new THREE.Group()
|
|
809
|
+
rimGroup.name = `${id}-fixed-rim`
|
|
810
|
+
rimGroup.position.copy(this.center)
|
|
811
|
+
rimGroup.quaternion.copy(this.baseRotation)
|
|
812
|
+
const rim = new THREE.Mesh(new VisualTorusGeometry(definition.radius + 0.16, 0.14, 10, 40), materials.signal)
|
|
813
|
+
rim.rotation.x = Math.PI / 2
|
|
814
|
+
rim.castShadow = true
|
|
815
|
+
rimGroup.add(rim)
|
|
816
|
+
root.add(rimGroup)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
class DipperMechanism implements TrackMechanismVisualAdapter {
|
|
821
|
+
readonly type = 'dipper'
|
|
822
|
+
readonly bodyCount = 1
|
|
823
|
+
readonly colliderCount = 3
|
|
824
|
+
readonly sensorCount = 1
|
|
825
|
+
private readonly body: RAPIER.RigidBody
|
|
826
|
+
private readonly visual: THREE.Group
|
|
827
|
+
private readonly hinge: THREE.Vector3
|
|
828
|
+
private readonly baseRotation: THREE.Quaternion
|
|
829
|
+
|
|
830
|
+
constructor(
|
|
831
|
+
readonly id: string,
|
|
832
|
+
world: RAPIER.World,
|
|
833
|
+
root: THREE.Group,
|
|
834
|
+
definition: DipperPieceDefinition,
|
|
835
|
+
materials: TrackMaterials,
|
|
836
|
+
) {
|
|
837
|
+
const tangent = toVector3(definition.direction).normalize()
|
|
838
|
+
const right = new THREE.Vector3(tangent.z, 0, -tangent.x).normalize()
|
|
839
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
840
|
+
this.hinge = toVector3(definition.hinge)
|
|
841
|
+
const trigger = this.hinge
|
|
842
|
+
.clone()
|
|
843
|
+
.addScaledVector(tangent, definition.length * 0.58)
|
|
844
|
+
.addScaledVector(up, 0.62)
|
|
845
|
+
this.baseRotation = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(right, up, tangent))
|
|
846
|
+
|
|
847
|
+
this.body = world.createRigidBody(
|
|
848
|
+
RAPIER.RigidBodyDesc.kinematicPositionBased()
|
|
849
|
+
.setTranslation(this.hinge.x, this.hinge.y, this.hinge.z)
|
|
850
|
+
.setRotation(toRapierRotation(this.baseRotation)),
|
|
851
|
+
)
|
|
852
|
+
const floorThickness = 0.22
|
|
853
|
+
world.createCollider(
|
|
854
|
+
RAPIER.ColliderDesc.cuboid(definition.width / 2, floorThickness / 2, definition.length / 2)
|
|
855
|
+
.setTranslation(0, -floorThickness / 2, definition.length / 2)
|
|
856
|
+
.setFriction(0.62)
|
|
857
|
+
.setRestitution(0.01),
|
|
858
|
+
this.body,
|
|
859
|
+
)
|
|
860
|
+
for (const side of [-1, 1] as const) {
|
|
861
|
+
world.createCollider(
|
|
862
|
+
RAPIER.ColliderDesc.cuboid(0.12, definition.wallHeight / 2, definition.length / 2)
|
|
863
|
+
.setTranslation(side * (definition.width / 2 - 0.12), definition.wallHeight / 2, definition.length / 2)
|
|
864
|
+
.setFriction(0.08)
|
|
865
|
+
.setRestitution(0.01),
|
|
866
|
+
this.body,
|
|
867
|
+
)
|
|
868
|
+
}
|
|
869
|
+
world.createCollider(
|
|
870
|
+
RAPIER.ColliderDesc.ball(definition.triggerRadius)
|
|
871
|
+
.setTranslation(trigger.x, trigger.y, trigger.z)
|
|
872
|
+
.setSensor(true),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
this.visual = new THREE.Group()
|
|
876
|
+
this.visual.name = `${id}-lowering-cup`
|
|
877
|
+
this.visual.position.copy(this.hinge)
|
|
878
|
+
this.visual.quaternion.copy(this.baseRotation)
|
|
879
|
+
this.visual.frustumCulled = false
|
|
880
|
+
const floor = new THREE.Mesh(
|
|
881
|
+
new THREE.BoxGeometry(definition.width, floorThickness, definition.length),
|
|
882
|
+
materials.structure,
|
|
883
|
+
)
|
|
884
|
+
floor.position.set(0, -floorThickness / 2, definition.length / 2)
|
|
885
|
+
floor.castShadow = true
|
|
886
|
+
floor.receiveShadow = true
|
|
887
|
+
this.visual.add(floor)
|
|
888
|
+
const sideGeometry = new THREE.BoxGeometry(0.24, definition.wallHeight, definition.length)
|
|
889
|
+
for (const side of [-1, 1] as const) {
|
|
890
|
+
const wall = new THREE.Mesh(sideGeometry, materials.hazard)
|
|
891
|
+
wall.position.set(side * (definition.width / 2 - 0.12), definition.wallHeight / 2, definition.length / 2)
|
|
892
|
+
wall.castShadow = true
|
|
893
|
+
wall.receiveShadow = true
|
|
894
|
+
this.visual.add(wall)
|
|
895
|
+
}
|
|
896
|
+
const counterweight = new THREE.Mesh(new VisualSphereGeometry(0.68, 20, 14), materials.signal)
|
|
897
|
+
counterweight.position.set(0, definition.wallHeight * 0.72, -0.72)
|
|
898
|
+
counterweight.castShadow = true
|
|
899
|
+
this.visual.add(counterweight)
|
|
900
|
+
markDynamicTrackShadowRoot(this.visual)
|
|
901
|
+
root.add(this.visual)
|
|
902
|
+
|
|
903
|
+
const support = new THREE.Group()
|
|
904
|
+
support.name = `${id}-support`
|
|
905
|
+
support.position.copy(this.hinge)
|
|
906
|
+
support.quaternion.copy(this.baseRotation)
|
|
907
|
+
const axle = new THREE.Mesh(new VisualCylinderGeometry(0.22, 0.22, definition.width + 0.6, 18), materials.signal)
|
|
908
|
+
axle.rotation.z = Math.PI / 2
|
|
909
|
+
axle.castShadow = true
|
|
910
|
+
support.add(axle)
|
|
911
|
+
root.add(support)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
class ConveyorMechanism implements TrackMechanismVisualAdapter {
|
|
916
|
+
readonly type = 'conveyor'
|
|
917
|
+
readonly bodyCount: number
|
|
918
|
+
readonly colliderCount: number
|
|
919
|
+
readonly sensorCount = 0
|
|
920
|
+
private readonly bottom: THREE.Vector3
|
|
921
|
+
private readonly top: THREE.Vector3
|
|
922
|
+
private readonly height: number
|
|
923
|
+
private readonly baseRotation: THREE.Quaternion
|
|
924
|
+
private readonly physics: ConveyorPhysicsMechanism
|
|
925
|
+
|
|
926
|
+
constructor(
|
|
927
|
+
readonly id: string,
|
|
928
|
+
world: RAPIER.World,
|
|
929
|
+
root: THREE.Group,
|
|
930
|
+
definition: ConveyorPieceDefinition,
|
|
931
|
+
phase: number,
|
|
932
|
+
materials: TrackMaterials,
|
|
933
|
+
existingVisuals?: readonly THREE.Mesh[],
|
|
934
|
+
) {
|
|
935
|
+
const reusesCanonicalVisuals = existingVisuals?.length === definition.shelfCount
|
|
936
|
+
this.physics = new ConveyorPhysicsMechanism(id, world, definition, phase)
|
|
937
|
+
this.bottom = toVector3(definition.bottom)
|
|
938
|
+
this.top = toVector3(definition.top)
|
|
939
|
+
this.height = this.physics.height
|
|
940
|
+
this.bodyCount = this.physics.bodyCount
|
|
941
|
+
this.colliderCount = this.physics.colliderCount
|
|
942
|
+
this.baseRotation = new THREE.Quaternion(...this.physics.baseRotation)
|
|
943
|
+
|
|
944
|
+
const towerCenterY = (this.bottom.y + this.top.y) / 2
|
|
945
|
+
const towerHeight = this.height + 1
|
|
946
|
+
const wallThickness = 0.24
|
|
947
|
+
const activeThemeId = materials.activeThemeId
|
|
948
|
+
for (const side of [-1, 1] as const) {
|
|
949
|
+
const position = this.bottom
|
|
950
|
+
.clone()
|
|
951
|
+
.add(
|
|
952
|
+
new THREE.Vector3(
|
|
953
|
+
side * (definition.width / 2 - wallThickness / 2),
|
|
954
|
+
towerCenterY - this.bottom.y,
|
|
955
|
+
0,
|
|
956
|
+
).applyQuaternion(this.baseRotation),
|
|
957
|
+
)
|
|
958
|
+
if (!reusesCanonicalVisuals) {
|
|
959
|
+
const wall = new THREE.Mesh(
|
|
960
|
+
new THREE.BoxGeometry(wallThickness, towerHeight, definition.shelfDepth + 0.16),
|
|
961
|
+
materials.jungleTempleStone ?? materials.rail,
|
|
962
|
+
)
|
|
963
|
+
wall.name = `${id}-side-${side < 0 ? 'left' : 'right'}`
|
|
964
|
+
wall.position.copy(position)
|
|
965
|
+
wall.quaternion.copy(this.baseRotation)
|
|
966
|
+
wall.castShadow = true
|
|
967
|
+
wall.receiveShadow = true
|
|
968
|
+
wall.userData.themeHidden = [
|
|
969
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
970
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
971
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
972
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
973
|
+
]
|
|
974
|
+
root.add(wall)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const outputWallBottom = this.bottom.y - 0.2
|
|
979
|
+
const outputWallTop = definition.releaseHeight - 0.3
|
|
980
|
+
const outputWallHeight = outputWallTop - outputWallBottom
|
|
981
|
+
const outputWallPosition = this.bottom
|
|
982
|
+
.clone()
|
|
983
|
+
.add(
|
|
984
|
+
new THREE.Vector3(
|
|
985
|
+
0,
|
|
986
|
+
outputWallBottom + outputWallHeight / 2 - this.bottom.y,
|
|
987
|
+
-definition.shelfDepth / 2 + wallThickness / 2,
|
|
988
|
+
).applyQuaternion(this.baseRotation),
|
|
989
|
+
)
|
|
990
|
+
if (!reusesCanonicalVisuals) {
|
|
991
|
+
const outputWall = new THREE.Mesh(
|
|
992
|
+
new THREE.BoxGeometry(definition.width, outputWallHeight, wallThickness),
|
|
993
|
+
materials.jungleTempleStone ?? materials.rail,
|
|
994
|
+
)
|
|
995
|
+
outputWall.name = `${id}-release-wall`
|
|
996
|
+
outputWall.position.copy(outputWallPosition)
|
|
997
|
+
outputWall.quaternion.copy(this.baseRotation)
|
|
998
|
+
outputWall.castShadow = true
|
|
999
|
+
outputWall.receiveShadow = true
|
|
1000
|
+
outputWall.userData.themeHidden = [
|
|
1001
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
1002
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
1003
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
1004
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
1005
|
+
]
|
|
1006
|
+
root.add(outputWall)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const mineLiftMaterials = {
|
|
1010
|
+
timber:
|
|
1011
|
+
activeThemeId === WILD_WEST_THEME_ID ? (materials.mineTimber ?? materials.structure) : materials.structure,
|
|
1012
|
+
iron: activeThemeId === WILD_WEST_THEME_ID ? (materials.railroad ?? materials.edge) : materials.edge,
|
|
1013
|
+
signal: materials.signal,
|
|
1014
|
+
}
|
|
1015
|
+
const jungleTempleMaterials = {
|
|
1016
|
+
stone:
|
|
1017
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID ? (materials.jungleTempleStone ?? materials.rail) : materials.rail,
|
|
1018
|
+
facade:
|
|
1019
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1020
|
+
? (materials.jungleTempleFacade ?? materials.jungleTempleStone ?? materials.rail)
|
|
1021
|
+
: materials.rail,
|
|
1022
|
+
timber:
|
|
1023
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1024
|
+
? (materials.jungleTempleTimber ?? materials.structure)
|
|
1025
|
+
: materials.structure,
|
|
1026
|
+
platform:
|
|
1027
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1028
|
+
? (materials.jungleTemplePlatform ?? materials.moving)
|
|
1029
|
+
: materials.moving,
|
|
1030
|
+
moss:
|
|
1031
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1032
|
+
? (materials.jungleTempleMoss ?? materials.structure)
|
|
1033
|
+
: materials.structure,
|
|
1034
|
+
accent:
|
|
1035
|
+
activeThemeId === JUNGLE_TEMPLE_THEME_ID
|
|
1036
|
+
? (materials.jungleTempleBronze ?? materials.signal)
|
|
1037
|
+
: materials.signal,
|
|
1038
|
+
}
|
|
1039
|
+
const portalDerivedMaterials = {
|
|
1040
|
+
egypt: {
|
|
1041
|
+
get sandstone() {
|
|
1042
|
+
return materials.egyptPortalSandstone ?? materials.rail
|
|
1043
|
+
},
|
|
1044
|
+
get carvedStone() {
|
|
1045
|
+
return materials.egyptPortalCarvedSandstone ?? materials.rail
|
|
1046
|
+
},
|
|
1047
|
+
get pyramidStone() {
|
|
1048
|
+
return materials.egyptPyramidLimestone ?? materials.railBacking
|
|
1049
|
+
},
|
|
1050
|
+
get pyramidGold() {
|
|
1051
|
+
return materials.egyptPyramidGold ?? materials.egyptPortalAgedGold ?? materials.signal
|
|
1052
|
+
},
|
|
1053
|
+
get bronze() {
|
|
1054
|
+
return materials.edge
|
|
1055
|
+
},
|
|
1056
|
+
get gold() {
|
|
1057
|
+
return materials.egyptPortalAgedGold ?? materials.signal
|
|
1058
|
+
},
|
|
1059
|
+
get paving() {
|
|
1060
|
+
return materials.top
|
|
1061
|
+
},
|
|
1062
|
+
get signal() {
|
|
1063
|
+
return materials.signal
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
japan: {
|
|
1067
|
+
vermilion:
|
|
1068
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1069
|
+
? (materials.sakuraBridgeTimber ?? materials.structure)
|
|
1070
|
+
: materials.structure,
|
|
1071
|
+
timber:
|
|
1072
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1073
|
+
? (materials.sakuraCastleTimber ?? materials.moving)
|
|
1074
|
+
: materials.moving,
|
|
1075
|
+
plaster: materials.railBacking,
|
|
1076
|
+
stone:
|
|
1077
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1078
|
+
? (materials.sakuraPortalFoundationStone ?? materials.sakuraCastleMasonry ?? materials.structure)
|
|
1079
|
+
: materials.structure,
|
|
1080
|
+
kawara:
|
|
1081
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1082
|
+
? (materials.sakuraCastleKawara ?? materials.structure)
|
|
1083
|
+
: materials.structure,
|
|
1084
|
+
hardware:
|
|
1085
|
+
activeThemeId === JAPAN_ELEVATOR_THEME_ID
|
|
1086
|
+
? (materials.sakuraBridgeHardware ?? materials.edge)
|
|
1087
|
+
: materials.edge,
|
|
1088
|
+
signal: materials.signal,
|
|
1089
|
+
},
|
|
1090
|
+
space: {
|
|
1091
|
+
chassis:
|
|
1092
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1093
|
+
? (materials.spaceStationWallChassis ?? materials.structure)
|
|
1094
|
+
: materials.structure,
|
|
1095
|
+
panel:
|
|
1096
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1097
|
+
? (materials.spaceStationWallPanel ?? materials.rail)
|
|
1098
|
+
: materials.rail,
|
|
1099
|
+
rib:
|
|
1100
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1101
|
+
? (materials.spaceStationWallRib ?? materials.structure)
|
|
1102
|
+
: materials.structure,
|
|
1103
|
+
trim:
|
|
1104
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1105
|
+
? (materials.spaceStationWallTrim ?? materials.edge)
|
|
1106
|
+
: materials.edge,
|
|
1107
|
+
light:
|
|
1108
|
+
activeThemeId === SPACE_ELEVATOR_THEME_ID
|
|
1109
|
+
? (materials.spaceStationWallLight ?? materials.energy)
|
|
1110
|
+
: materials.energy,
|
|
1111
|
+
},
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (!reusesCanonicalVisuals) {
|
|
1115
|
+
const beltBandGeometry = new THREE.BoxGeometry(0.3, this.height, 0.16)
|
|
1116
|
+
for (const side of [-1, 1] as const) {
|
|
1117
|
+
const beltBand = new THREE.Mesh(beltBandGeometry, materials.jungleTempleTimber ?? materials.structure)
|
|
1118
|
+
beltBand.name = `${id}-vertical-belt-${side < 0 ? 'left' : 'right'}`
|
|
1119
|
+
beltBand.position
|
|
1120
|
+
.copy(this.bottom)
|
|
1121
|
+
.add(
|
|
1122
|
+
new THREE.Vector3(
|
|
1123
|
+
side * (definition.width / 2 - 0.55),
|
|
1124
|
+
towerCenterY - this.bottom.y,
|
|
1125
|
+
definition.shelfDepth / 2 + 0.12,
|
|
1126
|
+
).applyQuaternion(this.baseRotation),
|
|
1127
|
+
)
|
|
1128
|
+
beltBand.quaternion.copy(this.baseRotation)
|
|
1129
|
+
beltBand.castShadow = true
|
|
1130
|
+
beltBand.receiveShadow = true
|
|
1131
|
+
beltBand.userData.themeHidden = [
|
|
1132
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
1133
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
1134
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
1135
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
1136
|
+
]
|
|
1137
|
+
root.add(beltBand)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const drumGeometry = new VisualCylinderGeometry(0.55, 0.55, definition.width - 0.7, 20)
|
|
1141
|
+
for (const [label, y] of [
|
|
1142
|
+
['bottom', this.bottom.y],
|
|
1143
|
+
['top', this.top.y],
|
|
1144
|
+
] as const) {
|
|
1145
|
+
const drum = new THREE.Mesh(drumGeometry, materials.jungleTempleTimber ?? materials.signal)
|
|
1146
|
+
drum.name = `${id}-${label}-drum`
|
|
1147
|
+
const verticalOffset = label === 'bottom' ? -0.55 : 0.55
|
|
1148
|
+
drum.position
|
|
1149
|
+
.copy(this.bottom)
|
|
1150
|
+
.add(
|
|
1151
|
+
new THREE.Vector3(0, y + verticalOffset - this.bottom.y, definition.shelfDepth / 2 + 0.2).applyQuaternion(
|
|
1152
|
+
this.baseRotation,
|
|
1153
|
+
),
|
|
1154
|
+
)
|
|
1155
|
+
drum.quaternion
|
|
1156
|
+
.copy(this.baseRotation)
|
|
1157
|
+
.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
|
|
1158
|
+
drum.castShadow = true
|
|
1159
|
+
drum.userData.decorative = true
|
|
1160
|
+
drum.userData.themeHidden = [
|
|
1161
|
+
WILD_WEST_THEME_ID,
|
|
1162
|
+
JUNGLE_TEMPLE_THEME_ID,
|
|
1163
|
+
EGYPT_ELEVATOR_THEME_ID,
|
|
1164
|
+
JAPAN_ELEVATOR_THEME_ID,
|
|
1165
|
+
SPACE_ELEVATOR_THEME_ID,
|
|
1166
|
+
]
|
|
1167
|
+
root.add(drum)
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (activeThemeId === WILD_WEST_THEME_ID)
|
|
1171
|
+
addWildWestMineLiftFrame(root, {
|
|
1172
|
+
id,
|
|
1173
|
+
bottom: this.bottom,
|
|
1174
|
+
rotation: this.baseRotation,
|
|
1175
|
+
width: definition.width,
|
|
1176
|
+
height: this.height,
|
|
1177
|
+
depth: definition.shelfDepth,
|
|
1178
|
+
releaseHeight: definition.releaseHeight - this.bottom.y,
|
|
1179
|
+
materials: mineLiftMaterials,
|
|
1180
|
+
})
|
|
1181
|
+
if (activeThemeId === JUNGLE_TEMPLE_THEME_ID)
|
|
1182
|
+
addJungleTempleLiftSkin(root, {
|
|
1183
|
+
id,
|
|
1184
|
+
bottom: this.bottom,
|
|
1185
|
+
rotation: this.baseRotation,
|
|
1186
|
+
width: definition.width,
|
|
1187
|
+
height: this.height,
|
|
1188
|
+
depth: definition.shelfDepth,
|
|
1189
|
+
releaseHeight: definition.releaseHeight - this.bottom.y,
|
|
1190
|
+
materials: jungleTempleMaterials,
|
|
1191
|
+
})
|
|
1192
|
+
addPortalDerivedElevatorSkins(root, {
|
|
1193
|
+
activeThemeId,
|
|
1194
|
+
id,
|
|
1195
|
+
bottom: this.bottom,
|
|
1196
|
+
rotation: this.baseRotation,
|
|
1197
|
+
width: definition.width,
|
|
1198
|
+
height: this.height,
|
|
1199
|
+
depth: definition.shelfDepth,
|
|
1200
|
+
releaseHeight: definition.releaseHeight - this.bottom.y,
|
|
1201
|
+
materials: portalDerivedMaterials,
|
|
1202
|
+
})
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const shelfThickness = 0.2
|
|
1206
|
+
for (let index = 0; index < definition.shelfCount; index += 1) {
|
|
1207
|
+
const body = this.physics.bodies[index]!
|
|
1208
|
+
const bodyPosition = body.translation()
|
|
1209
|
+
const bodyRotation = body.rotation()
|
|
1210
|
+
const position = new THREE.Vector3(bodyPosition.x, bodyPosition.y, bodyPosition.z)
|
|
1211
|
+
const shelfRotation = new THREE.Quaternion(bodyRotation.x, bodyRotation.y, bodyRotation.z, bodyRotation.w)
|
|
1212
|
+
const visual = reusesCanonicalVisuals
|
|
1213
|
+
? existingVisuals![index]!
|
|
1214
|
+
: new THREE.Mesh(
|
|
1215
|
+
new THREE.BoxGeometry(definition.width - 0.56, shelfThickness, definition.shelfDepth),
|
|
1216
|
+
materials.jungleTemplePlatform ?? materials.moving,
|
|
1217
|
+
)
|
|
1218
|
+
visual.name ||= `${id}-shelf-${index + 1}`
|
|
1219
|
+
visual.position.copy(position)
|
|
1220
|
+
visual.quaternion.copy(shelfRotation)
|
|
1221
|
+
visual.castShadow = true
|
|
1222
|
+
visual.receiveShadow = true
|
|
1223
|
+
markDynamicTrackShadowRoot(visual)
|
|
1224
|
+
if (!reusesCanonicalVisuals) {
|
|
1225
|
+
if (activeThemeId === WILD_WEST_THEME_ID)
|
|
1226
|
+
decorateWildWestMineLiftShelf(
|
|
1227
|
+
visual,
|
|
1228
|
+
`${id}-shelf-${index + 1}`,
|
|
1229
|
+
definition.width - 0.56,
|
|
1230
|
+
definition.shelfDepth,
|
|
1231
|
+
mineLiftMaterials,
|
|
1232
|
+
)
|
|
1233
|
+
if (activeThemeId === JUNGLE_TEMPLE_THEME_ID)
|
|
1234
|
+
decorateJungleTempleShelf(
|
|
1235
|
+
visual,
|
|
1236
|
+
`${id}-shelf-${index + 1}`,
|
|
1237
|
+
definition.width - 0.56,
|
|
1238
|
+
definition.shelfDepth,
|
|
1239
|
+
jungleTempleMaterials,
|
|
1240
|
+
)
|
|
1241
|
+
decoratePortalDerivedElevatorShelf(
|
|
1242
|
+
visual,
|
|
1243
|
+
`${id}-shelf-${index + 1}`,
|
|
1244
|
+
definition.width - 0.56,
|
|
1245
|
+
definition.shelfDepth,
|
|
1246
|
+
portalDerivedMaterials,
|
|
1247
|
+
activeThemeId,
|
|
1248
|
+
)
|
|
1249
|
+
root.add(visual)
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function createSplitterMechanism(
|
|
1256
|
+
world: RAPIER.World,
|
|
1257
|
+
root: THREE.Group,
|
|
1258
|
+
definition: SplitterPieceDefinition,
|
|
1259
|
+
motion: SplitterMotionProfile,
|
|
1260
|
+
materials: TrackMaterials,
|
|
1261
|
+
existingVisual?: THREE.Group,
|
|
1262
|
+
): TrackMechanismVisualAdapter {
|
|
1263
|
+
return new SplitterMechanism(definition.id, world, root, definition, motion, materials, existingVisual)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export function createRisingWallForestMechanism(
|
|
1267
|
+
world: RAPIER.World,
|
|
1268
|
+
root: THREE.Group,
|
|
1269
|
+
definition: RisingWallForestPieceDefinition,
|
|
1270
|
+
simulationKey: string,
|
|
1271
|
+
materials: TrackMaterials,
|
|
1272
|
+
existingVisuals?: readonly THREE.Mesh[],
|
|
1273
|
+
): TrackMechanismVisualAdapter {
|
|
1274
|
+
return new RisingWallForestMechanism(
|
|
1275
|
+
definition.id,
|
|
1276
|
+
world,
|
|
1277
|
+
definition,
|
|
1278
|
+
simulationKey,
|
|
1279
|
+
root,
|
|
1280
|
+
materials,
|
|
1281
|
+
existingVisuals,
|
|
1282
|
+
)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
export function createVortexMechanism(
|
|
1286
|
+
world: RAPIER.World,
|
|
1287
|
+
definition: VortexPieceDefinition,
|
|
1288
|
+
): TrackMechanismVisualAdapter {
|
|
1289
|
+
return new VortexPhysicsMechanism(definition.id, world, definition)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
export function createPortalMechanism(
|
|
1293
|
+
root: THREE.Group,
|
|
1294
|
+
definition: PortalPieceDefinition,
|
|
1295
|
+
materials: TrackMaterials,
|
|
1296
|
+
qualityPolicy: VisualQualityPolicy = visualQualityPolicy('quality'),
|
|
1297
|
+
existingVisuals?: { entry: PortalEndpointVisual; exit: PortalEndpointVisual },
|
|
1298
|
+
): TrackMechanismVisualAdapter {
|
|
1299
|
+
return new PortalMechanism(definition.id, root, definition, materials, qualityPolicy, existingVisuals)
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
export function createConveyorMechanism(
|
|
1303
|
+
world: RAPIER.World,
|
|
1304
|
+
root: THREE.Group,
|
|
1305
|
+
definition: ConveyorPieceDefinition,
|
|
1306
|
+
phase: number,
|
|
1307
|
+
materials: TrackMaterials,
|
|
1308
|
+
existingVisuals?: readonly THREE.Mesh[],
|
|
1309
|
+
): TrackMechanismVisualAdapter {
|
|
1310
|
+
return new ConveyorMechanism(definition.id, world, root, definition, phase, materials, existingVisuals)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export function createFlipperBankMechanism(
|
|
1314
|
+
world: RAPIER.World,
|
|
1315
|
+
root: THREE.Group,
|
|
1316
|
+
definition: FlipperBankPieceDefinition,
|
|
1317
|
+
simulationKey: string,
|
|
1318
|
+
materials: TrackMaterials,
|
|
1319
|
+
existingVisuals?: readonly THREE.Group[],
|
|
1320
|
+
): TrackMechanismVisualAdapter {
|
|
1321
|
+
return new FlipperBankMechanism(definition.id, world, root, definition, simulationKey, materials, existingVisuals)
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
export function createReboundMechanism(
|
|
1325
|
+
world: RAPIER.World,
|
|
1326
|
+
root: THREE.Group,
|
|
1327
|
+
definition: ReboundPieceDefinition,
|
|
1328
|
+
materials: TrackMaterials,
|
|
1329
|
+
existingVisuals?: readonly THREE.Group[],
|
|
1330
|
+
): TrackMechanismVisualAdapter {
|
|
1331
|
+
return new ReboundMechanism(definition.id, world, root, definition, materials, existingVisuals)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export function createTurntableMechanism(
|
|
1335
|
+
world: RAPIER.World,
|
|
1336
|
+
root: THREE.Group,
|
|
1337
|
+
definition: TurntablePieceDefinition,
|
|
1338
|
+
materials: TrackMaterials,
|
|
1339
|
+
): TrackMechanismVisualAdapter {
|
|
1340
|
+
return new TurntableMechanism(definition.id, world, root, definition, materials)
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export function createDipperMechanism(
|
|
1344
|
+
world: RAPIER.World,
|
|
1345
|
+
root: THREE.Group,
|
|
1346
|
+
definition: DipperPieceDefinition,
|
|
1347
|
+
materials: TrackMaterials,
|
|
1348
|
+
): TrackMechanismVisualAdapter {
|
|
1349
|
+
return new DipperMechanism(definition.id, world, root, definition, materials)
|
|
1350
|
+
}
|