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
- const _PARK_OFFSET = [0, -5000, 0] // far enough below any reasonable playfield that stray collisions are impossible
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
- function _resolveImpulsePattern(pattern) {
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
- _validateSpec(spec)
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 = _resolveImpulsePattern(spec.debrisImpulsePattern ?? 'outward-up')
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 = _jitter(hx * 0.5), jy = _jitter(hy * 0.5), jz = _jitter(hz * 0.5)
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.644",
3
+ "version": "0.1.645",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [