spoint 0.1.644 → 0.1.645
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.
|
@@ -65,75 +65,9 @@
|
|
|
65
65
|
// Call destructible.tick(dt) once per server tick from the owning app's update(ctx, dt) -- this object
|
|
66
66
|
// owns its own debris-lifetime/respawn/impact-scan timers, it does not register a runtime-level watch.
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v) }
|
|
71
|
-
|
|
72
|
-
function _validateSpec(spec) {
|
|
73
|
-
if (spec !== null && typeof spec !== 'object') throw new TypeError('[destructible] spec must be an object')
|
|
74
|
-
const s = spec || {}
|
|
75
|
-
if (s.health != null && (typeof s.health !== 'number' || !Number.isFinite(s.health) || s.health <= 0)) {
|
|
76
|
-
throw new TypeError('[destructible] health must be a positive finite number')
|
|
77
|
-
}
|
|
78
|
-
if (s.impactThreshold != null && (typeof s.impactThreshold !== 'number' || !Number.isFinite(s.impactThreshold) || s.impactThreshold < 0)) {
|
|
79
|
-
throw new TypeError('[destructible] impactThreshold must be a non-negative finite number')
|
|
80
|
-
}
|
|
81
|
-
if (s.impactRadius != null && (typeof s.impactRadius !== 'number' || !Number.isFinite(s.impactRadius) || s.impactRadius <= 0)) {
|
|
82
|
-
throw new TypeError('[destructible] impactRadius must be a positive finite number')
|
|
83
|
-
}
|
|
84
|
-
if (s.debrisCount != null && (!Number.isInteger(s.debrisCount) || s.debrisCount < 1)) {
|
|
85
|
-
throw new TypeError('[destructible] debrisCount must be a positive integer')
|
|
86
|
-
}
|
|
87
|
-
if (s.debrisLifetime != null && (typeof s.debrisLifetime !== 'number' || !Number.isFinite(s.debrisLifetime) || s.debrisLifetime < 0)) {
|
|
88
|
-
throw new TypeError('[destructible] debrisLifetime must be a non-negative finite number (seconds); 0 = never despawn')
|
|
89
|
-
}
|
|
90
|
-
if (s.debrisSettleGrace != null && (typeof s.debrisSettleGrace !== 'number' || !Number.isFinite(s.debrisSettleGrace) || s.debrisSettleGrace < 0)) {
|
|
91
|
-
throw new TypeError('[destructible] debrisSettleGrace must be a non-negative finite number (seconds)')
|
|
92
|
-
}
|
|
93
|
-
if (s.debrisFreezeAfter != null && (typeof s.debrisFreezeAfter !== 'number' || !Number.isFinite(s.debrisFreezeAfter) || s.debrisFreezeAfter < 0)) {
|
|
94
|
-
throw new TypeError('[destructible] debrisFreezeAfter must be a non-negative finite number (seconds); 0 = disable force-freeze')
|
|
95
|
-
}
|
|
96
|
-
if (s.respawnDelay != null && (typeof s.respawnDelay !== 'number' || !Number.isFinite(s.respawnDelay) || s.respawnDelay < 0)) {
|
|
97
|
-
throw new TypeError('[destructible] respawnDelay must be a non-negative finite number (seconds); 0 = never respawn')
|
|
98
|
-
}
|
|
99
|
-
if (s.debrisImpulsePattern != null && typeof s.debrisImpulsePattern !== 'function' && !['outward', 'outward-up', 'up'].includes(s.debrisImpulsePattern)) {
|
|
100
|
-
throw new TypeError('[destructible] debrisImpulsePattern must be "outward", "outward-up", "up", or a function(i, n, rng) -> [x,y,z]')
|
|
101
|
-
}
|
|
102
|
-
if (s.debrisShape != null && !_isPlainObject(s.debrisShape)) throw new TypeError('[destructible] debrisShape must be a plain object')
|
|
103
|
-
if (s.fracturedAsset != null && typeof s.fracturedAsset !== 'string') throw new TypeError('[destructible] fracturedAsset must be a string path to a scripts/fracture-glb.mjs-baked GLB')
|
|
104
|
-
if (s.fracturedPieceCount != null && (!Number.isInteger(s.fracturedPieceCount) || s.fracturedPieceCount < 1)) {
|
|
105
|
-
throw new TypeError('[destructible] fracturedPieceCount must be a positive integer (the number of baked pieces in fracturedAsset)')
|
|
106
|
-
}
|
|
107
|
-
if (s.fracturedAsset != null && s.fracturedPieceCount == null) {
|
|
108
|
-
throw new TypeError('[destructible] fracturedPieceCount is required when fracturedAsset is set (read it from the baked <asset>.pieces.json sidecar\'s pieceCount field)')
|
|
109
|
-
}
|
|
110
|
-
if (s.onDestroyed != null && typeof s.onDestroyed !== 'function') throw new TypeError('[destructible] onDestroyed must be a function')
|
|
111
|
-
if (s.onRespawn != null && typeof s.onRespawn !== 'function') throw new TypeError('[destructible] onRespawn must be a function')
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// cosmetic launch-direction jitter only, not gameplay-critical -- plain Math.random() is fine here.
|
|
115
|
-
function _jitter(spread) { return (Math.random() * 2 - 1) * spread }
|
|
68
|
+
import { validateDestructibleSpec, resolveDebrisImpulsePattern, jitter } from './destructibleSpec.js'
|
|
116
69
|
|
|
117
|
-
|
|
118
|
-
if (typeof pattern === 'function') return pattern
|
|
119
|
-
if (pattern === 'up') {
|
|
120
|
-
return () => [0, 6 + Math.random() * 3, 0]
|
|
121
|
-
}
|
|
122
|
-
if (pattern === 'outward') {
|
|
123
|
-
return (i, n) => {
|
|
124
|
-
const angle = (i / n) * Math.PI * 2 + _jitter(0.3)
|
|
125
|
-
const mag = 3 + Math.random() * 2
|
|
126
|
-
return [Math.cos(angle) * mag, 0, Math.sin(angle) * mag]
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// 'outward-up' (default): radial spread around the object plus a strong upward component,
|
|
130
|
-
// matching the prototype's "outward+up launch impulses on impact" behavior.
|
|
131
|
-
return (i, n) => {
|
|
132
|
-
const angle = (i / n) * Math.PI * 2 + _jitter(0.4)
|
|
133
|
-
const mag = 2.5 + Math.random() * 2.5
|
|
134
|
-
return [Math.cos(angle) * mag, 5 + Math.random() * 4, Math.sin(angle) * mag]
|
|
135
|
-
}
|
|
136
|
-
}
|
|
70
|
+
const _PARK_OFFSET = [0, -5000, 0] // far enough below any reasonable playfield that stray collisions are impossible
|
|
137
71
|
|
|
138
72
|
// spec = {
|
|
139
73
|
// health?: number -- total damage capacity before destruction (default 100)
|
|
@@ -181,7 +115,7 @@ function _resolveImpulsePattern(pattern) {
|
|
|
181
115
|
// onRespawn?: (ctx) => void -- fires once the intact object respawns
|
|
182
116
|
// }
|
|
183
117
|
export function createDestructible(spec = {}, appCtx = null) {
|
|
184
|
-
|
|
118
|
+
validateDestructibleSpec(spec)
|
|
185
119
|
if (!appCtx) throw new TypeError('[destructible] appCtx is required')
|
|
186
120
|
|
|
187
121
|
const health = spec.health ?? 100
|
|
@@ -192,7 +126,7 @@ export function createDestructible(spec = {}, appCtx = null) {
|
|
|
192
126
|
const debrisSettleGrace = spec.debrisSettleGrace ?? 0.5
|
|
193
127
|
const debrisFreezeAfter = spec.debrisFreezeAfter ?? 3
|
|
194
128
|
const respawnDelay = spec.respawnDelay ?? 0
|
|
195
|
-
const impulseFn =
|
|
129
|
+
const impulseFn = resolveDebrisImpulsePattern(spec.debrisImpulsePattern ?? 'outward-up')
|
|
196
130
|
|
|
197
131
|
// captured once at build time -- the intact object's true home/look, independent of later parking.
|
|
198
132
|
const _homePosition = [...appCtx.entity.position]
|
|
@@ -359,7 +293,7 @@ export function createDestructible(spec = {}, appCtx = null) {
|
|
|
359
293
|
const ids = []
|
|
360
294
|
for (let i = 0; i < debrisCount; i++) {
|
|
361
295
|
// scatter pieces slightly within the intact object's footprint so they don't all spawn co-located
|
|
362
|
-
const jx =
|
|
296
|
+
const jx = jitter(hx * 0.5), jy = jitter(hy * 0.5), jz = jitter(hz * 0.5)
|
|
363
297
|
const id = _acquirePoolPiece(
|
|
364
298
|
[px + jx, py + jy, pz + jz],
|
|
365
299
|
appCtx.entity.rotation,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Pure spec-validation and launch-impulse-pattern helpers for createDestructible (destructible.js) --
|
|
2
|
+
// split out because they carry no closure state and no dependency on appCtx/the engine, unlike every
|
|
3
|
+
// other function in destructible.js which closes over the per-instance pool/timer state.
|
|
4
|
+
|
|
5
|
+
function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v) }
|
|
6
|
+
|
|
7
|
+
export function validateDestructibleSpec(spec) {
|
|
8
|
+
if (spec !== null && typeof spec !== 'object') throw new TypeError('[destructible] spec must be an object')
|
|
9
|
+
const s = spec || {}
|
|
10
|
+
if (s.health != null && (typeof s.health !== 'number' || !Number.isFinite(s.health) || s.health <= 0)) {
|
|
11
|
+
throw new TypeError('[destructible] health must be a positive finite number')
|
|
12
|
+
}
|
|
13
|
+
if (s.impactThreshold != null && (typeof s.impactThreshold !== 'number' || !Number.isFinite(s.impactThreshold) || s.impactThreshold < 0)) {
|
|
14
|
+
throw new TypeError('[destructible] impactThreshold must be a non-negative finite number')
|
|
15
|
+
}
|
|
16
|
+
if (s.impactRadius != null && (typeof s.impactRadius !== 'number' || !Number.isFinite(s.impactRadius) || s.impactRadius <= 0)) {
|
|
17
|
+
throw new TypeError('[destructible] impactRadius must be a positive finite number')
|
|
18
|
+
}
|
|
19
|
+
if (s.debrisCount != null && (!Number.isInteger(s.debrisCount) || s.debrisCount < 1)) {
|
|
20
|
+
throw new TypeError('[destructible] debrisCount must be a positive integer')
|
|
21
|
+
}
|
|
22
|
+
if (s.debrisLifetime != null && (typeof s.debrisLifetime !== 'number' || !Number.isFinite(s.debrisLifetime) || s.debrisLifetime < 0)) {
|
|
23
|
+
throw new TypeError('[destructible] debrisLifetime must be a non-negative finite number (seconds); 0 = never despawn')
|
|
24
|
+
}
|
|
25
|
+
if (s.debrisSettleGrace != null && (typeof s.debrisSettleGrace !== 'number' || !Number.isFinite(s.debrisSettleGrace) || s.debrisSettleGrace < 0)) {
|
|
26
|
+
throw new TypeError('[destructible] debrisSettleGrace must be a non-negative finite number (seconds)')
|
|
27
|
+
}
|
|
28
|
+
if (s.debrisFreezeAfter != null && (typeof s.debrisFreezeAfter !== 'number' || !Number.isFinite(s.debrisFreezeAfter) || s.debrisFreezeAfter < 0)) {
|
|
29
|
+
throw new TypeError('[destructible] debrisFreezeAfter must be a non-negative finite number (seconds); 0 = disable force-freeze')
|
|
30
|
+
}
|
|
31
|
+
if (s.respawnDelay != null && (typeof s.respawnDelay !== 'number' || !Number.isFinite(s.respawnDelay) || s.respawnDelay < 0)) {
|
|
32
|
+
throw new TypeError('[destructible] respawnDelay must be a non-negative finite number (seconds); 0 = never respawn')
|
|
33
|
+
}
|
|
34
|
+
if (s.debrisImpulsePattern != null && typeof s.debrisImpulsePattern !== 'function' && !['outward', 'outward-up', 'up'].includes(s.debrisImpulsePattern)) {
|
|
35
|
+
throw new TypeError('[destructible] debrisImpulsePattern must be "outward", "outward-up", "up", or a function(i, n, rng) -> [x,y,z]')
|
|
36
|
+
}
|
|
37
|
+
if (s.debrisShape != null && !_isPlainObject(s.debrisShape)) throw new TypeError('[destructible] debrisShape must be a plain object')
|
|
38
|
+
if (s.fracturedAsset != null && typeof s.fracturedAsset !== 'string') throw new TypeError('[destructible] fracturedAsset must be a string path to a scripts/fracture-glb.mjs-baked GLB')
|
|
39
|
+
if (s.fracturedPieceCount != null && (!Number.isInteger(s.fracturedPieceCount) || s.fracturedPieceCount < 1)) {
|
|
40
|
+
throw new TypeError('[destructible] fracturedPieceCount must be a positive integer (the number of baked pieces in fracturedAsset)')
|
|
41
|
+
}
|
|
42
|
+
if (s.fracturedAsset != null && s.fracturedPieceCount == null) {
|
|
43
|
+
throw new TypeError('[destructible] fracturedPieceCount is required when fracturedAsset is set (read it from the baked <asset>.pieces.json sidecar\'s pieceCount field)')
|
|
44
|
+
}
|
|
45
|
+
if (s.onDestroyed != null && typeof s.onDestroyed !== 'function') throw new TypeError('[destructible] onDestroyed must be a function')
|
|
46
|
+
if (s.onRespawn != null && typeof s.onRespawn !== 'function') throw new TypeError('[destructible] onRespawn must be a function')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// cosmetic launch-direction jitter only, not gameplay-critical -- plain Math.random() is fine here.
|
|
50
|
+
// Exported: destructible.js's own _spawnDebris also uses this for its per-piece scatter offset.
|
|
51
|
+
export function jitter(spread) { return (Math.random() * 2 - 1) * spread }
|
|
52
|
+
|
|
53
|
+
export function resolveDebrisImpulsePattern(pattern) {
|
|
54
|
+
if (typeof pattern === 'function') return pattern
|
|
55
|
+
if (pattern === 'up') {
|
|
56
|
+
return () => [0, 6 + Math.random() * 3, 0]
|
|
57
|
+
}
|
|
58
|
+
if (pattern === 'outward') {
|
|
59
|
+
return (i, n) => {
|
|
60
|
+
const angle = (i / n) * Math.PI * 2 + jitter(0.3)
|
|
61
|
+
const mag = 3 + Math.random() * 2
|
|
62
|
+
return [Math.cos(angle) * mag, 0, Math.sin(angle) * mag]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 'outward-up' (default): radial spread around the object plus a strong upward component,
|
|
66
|
+
// matching the prototype's "outward+up launch impulses on impact" behavior.
|
|
67
|
+
return (i, n) => {
|
|
68
|
+
const angle = (i / n) * Math.PI * 2 + jitter(0.4)
|
|
69
|
+
const mag = 2.5 + Math.random() * 2.5
|
|
70
|
+
return [Math.cos(angle) * mag, 5 + Math.random() * 4, Math.sin(angle) * mag]
|
|
71
|
+
}
|
|
72
|
+
}
|