-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Expand file tree
/
Copy pathlevel-utils.ts
More file actions
128 lines (110 loc) · 3.89 KB
/
Copy pathlevel-utils.ts
File metadata and controls
128 lines (110 loc) · 3.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import {
type CeilingNode,
type LevelNode,
sceneRegistry,
useScene,
type WallNode,
} from '@pascal-app/core'
export const DEFAULT_LEVEL_HEIGHT = 2.5
type LevelWithBaseElevation = Pick<LevelNode, 'baseElevation'>
export function getLevelBaseElevation(level: LevelWithBaseElevation | null | undefined): number {
return level?.baseElevation ?? 0
}
export function getLevelTargetY(
cumulativeY: number,
level: LevelWithBaseElevation | null | undefined,
explodedExtra = 0,
): number {
return cumulativeY + getLevelBaseElevation(level) + explodedExtra
}
export function getNextLevelCumulativeY(
cumulativeY: number,
levelHeight: number,
level: LevelWithBaseElevation | null | undefined,
): number {
return cumulativeY + levelHeight + getLevelBaseElevation(level)
}
// Cache: levelId → computed height. Invalidated when the nodes reference changes.
// Zustand produces a new `nodes` object on every mutation, so reference equality
// is a zero-cost way to detect stale data without any subscription overhead.
const heightCache = new Map<string, number>()
let lastNodesRef: object | null = null
export function getLevelHeight(
levelId: string,
nodes: ReturnType<typeof useScene.getState>['nodes'],
): number {
if (nodes !== lastNodesRef) {
heightCache.clear()
lastNodesRef = nodes
}
if (heightCache.has(levelId)) return heightCache.get(levelId)!
const level = nodes[levelId as LevelNode['id']] as LevelNode | undefined
if (!level) return DEFAULT_LEVEL_HEIGHT
let maxTop = 0
for (const childId of level.children) {
const child = nodes[childId as keyof typeof nodes]
if (!child) continue
if (child.type === 'ceiling') {
const ch = (child as CeilingNode).height ?? DEFAULT_LEVEL_HEIGHT
if (ch > maxTop) maxTop = ch
} else if (child.type === 'wall') {
let meshY = sceneRegistry.nodes.get(childId as any)?.position.y ?? 0
if (meshY < 0) meshY = 0
const top = meshY + ((child as WallNode).height ?? DEFAULT_LEVEL_HEIGHT)
if (top > maxTop) maxTop = top
}
}
const height = maxTop > 0 ? maxTop : DEFAULT_LEVEL_HEIGHT
heightCache.set(levelId, height)
return height
}
/**
* Instantly snaps all level Objects3D to their true stacked Y positions
* (ignores levelMode — always uses stacked, no exploded gap).
*
* Returns a restore function that reverts each level's Y to what it was
* before the snap, so lerp animations in LevelSystem can continue undisturbed.
*
* Usage:
* const restore = snapLevelsToTruePositions()
* renderer.render(scene, camera)
* restore()
*/
export function snapLevelsToTruePositions(): () => void {
const nodes = useScene.getState().nodes
type LevelEntry = {
obj: NonNullable<ReturnType<typeof sceneRegistry.nodes.get>>
levelId: string
index: number
}
const entries: LevelEntry[] = []
sceneRegistry.byType.level.forEach((levelId) => {
const obj = sceneRegistry.nodes.get(levelId)
const level = nodes[levelId as LevelNode['id']]
if (obj && level) {
entries.push({ levelId, index: (level as any).level ?? 0, obj })
}
})
entries.sort((a, b) => a.index - b.index)
// Snapshot current Y and visibility so we can restore them after the render
const snapshot = new Map(
entries.map(({ levelId, obj }) => [levelId, { y: obj.position.y, visible: obj.visible }]),
)
// Snap to true stacked positions and make all levels visible
let cumulativeY = 0
for (const { levelId, obj } of entries) {
const level = nodes[levelId as LevelNode['id']] as LevelNode | undefined
obj.position.y = getLevelTargetY(cumulativeY, level)
obj.visible = true
cumulativeY = getNextLevelCumulativeY(cumulativeY, getLevelHeight(levelId, nodes), level)
}
return () => {
for (const { levelId, obj } of entries) {
const saved = snapshot.get(levelId)
if (saved !== undefined) {
obj.position.y = saved.y
obj.visible = saved.visible
}
}
}
}