spoint 0.1.601 → 0.1.603

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.
@@ -37,19 +37,19 @@ import { createComponentPool } from './ComponentPool.js'
37
37
  // step()/followPath() themselves stay pure functions of their arguments (from/target/dt/peers) --
38
38
  // there is no PER-TICK mutable numeric state to pool here (unlike health.js's hp/alive/lastHitAt);
39
39
  // followPath's cursor is already caller-owned (`state`), not instance-owned.
40
- const _pool = createComponentPool({ fields: { speed: 'f64', arriveRadius: 'f64', separation: 'f64', clampToTerrain: 'f64', yOffset: 'f64' } })
40
+ const _pool = createComponentPool({ fields: { speed: 'f64', arriveRadius: 'f64', separation: 'f64', clampToTerrain: 'f64', yOffset: 'f64', useNavCost: 'f64' } })
41
41
  // Cached column references + the epoch they were captured at -- see health.js/ComponentPool.js's
42
42
  // header comment for why (plain fixed-length typed arrays are ~2.5x faster per-access than the
43
43
  // identity-stable resizable-ArrayBuffer alternative this module tried first and reverted; every hot-path
44
44
  // entry point calls `_sync()` first -- a single integer compare in the common case, only re-fetching the
45
45
  // five column references on the rare epoch mismatch that means a grow() just happened).
46
46
  let _speed = _pool.column('speed'), _arriveRadius = _pool.column('arriveRadius'), _separation = _pool.column('separation')
47
- let _clampToTerrain = _pool.column('clampToTerrain'), _yOffset = _pool.column('yOffset')
47
+ let _clampToTerrain = _pool.column('clampToTerrain'), _yOffset = _pool.column('yOffset'), _useNavCost = _pool.column('useNavCost')
48
48
  let _epoch = _pool.epoch
49
49
  function _sync() {
50
50
  if (_epoch === _pool.epoch) return
51
51
  _speed = _pool.column('speed'); _arriveRadius = _pool.column('arriveRadius'); _separation = _pool.column('separation')
52
- _clampToTerrain = _pool.column('clampToTerrain'); _yOffset = _pool.column('yOffset')
52
+ _clampToTerrain = _pool.column('clampToTerrain'); _yOffset = _pool.column('yOffset'); _useNavCost = _pool.column('useNavCost')
53
53
  _epoch = _pool.epoch
54
54
  }
55
55
 
@@ -79,6 +79,10 @@ function _stepImpl(slot, appCtx, from, target, dt, peers) {
79
79
  const dist = Math.hypot(vx, vz)
80
80
  let sp = speed
81
81
  if (dist <= arriveRadius) sp = dist > 1e-4 ? speed * (dist / arriveRadius) : 0 // arrive: ease to a stop
82
+ if (_useNavCost[slot] === 1 && typeof appCtx.navCostAt === 'function' && dist > 1e-4) {
83
+ const cost = appCtx.navCostAt(fx, fz)
84
+ if (typeof cost === 'number' && Number.isFinite(cost) && cost > 0) sp = sp / cost
85
+ }
82
86
  if (dist > 1e-4) { vx = (vx / dist) * sp; vz = (vz / dist) * sp } else { vx = 0; vz = 0 }
83
87
  // separation: sum push-away from nearby peers
84
88
  if (separation > 0 && peers && peers.length) {
@@ -106,6 +110,7 @@ export function defineSteering(spec = {}, appCtx) {
106
110
  _separation[slot] = spec.separation ?? 0
107
111
  _clampToTerrain[slot] = spec.clampToTerrain ? 1 : 0
108
112
  _yOffset[slot] = spec.yOffset ?? 0
113
+ _useNavCost[slot] = spec.useNavCost ? 1 : 0
109
114
  let _disposed = false
110
115
 
111
116
  const steering = {
@@ -36,7 +36,7 @@ export default {
36
36
  const home = [...ctx.entity.position]
37
37
  ctx.entity.custom = { ...(ctx.entity.custom || {}), mesh: 'capsule', color: c.color ?? '#dd3344', sx: 0.5, sy: 1.7, sz: 0.5, _isBot: true }
38
38
  ctx.state.health = ctx.defineHealth({ max: c.health ?? 100, invulnMs: 300, onDeath: () => _onDeath(ctx) })
39
- ctx.state.steering = ctx.defineSteering({ speed: c.speed ?? 3.5, arriveRadius: 1, clampToTerrain: true, yOffset: 0.9 })
39
+ ctx.state.steering = ctx.defineSteering({ speed: c.speed ?? 3.5, arriveRadius: 1, clampToTerrain: true, yOffset: 0.9, useNavCost: true })
40
40
  ctx.state.weapon = ctx.defineWeapon({ damage: c.damage ?? 20, range: c.range ?? 80, fireRateMs: c.fireRateMs ?? 250, magazine: 0 })
41
41
  ctx.state.weaponRange = c.range ?? 80
42
42
  ctx.state.home = home
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.601",
3
+ "version": "0.1.603",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -398,6 +398,17 @@ export class AppContext {
398
398
  return this._runtime._physics?.terrainHeightAt(x, z) ?? null
399
399
  }
400
400
 
401
+ terrainKindAt(x, z) {
402
+ return this._runtime._physics?._terrainStreamer?.splineCarve?.kindAt(x, z) ?? null
403
+ }
404
+
405
+ navCostAt(x, z) {
406
+ const kind = this.terrainKindAt(x, z)
407
+ if (kind === 'road') return 0.5
408
+ if (kind === 'river') return 3
409
+ return 1
410
+ }
411
+
401
412
  // Real per-world sea-level Y, in the SAME local scene-space entity.position[1]/terrainHeightAt already
402
413
  // use. Reuses the identical formula the client's underwater fog-tint shader splices in at terrain-ready
403
414
  // (client/core/UnderwaterTint.js setSeaLevelY, fed by client/app.js: `(f.offsetY||0) - (f.anchorHeight||0)`)
@@ -6,6 +6,15 @@ function cellKey(cx, cz) {
6
6
  return (cx + OFF) * BIG + (cz + OFF)
7
7
  }
8
8
 
9
+ function carveHash(seed, i) {
10
+ let h = seed | 0
11
+ h = Math.imul(h ^ (i | 0), 0x27d4eb2d) >>> 0
12
+ h ^= h >>> 15; h = Math.imul(h, 0x2c1b3c6d) >>> 0
13
+ h ^= h >>> 12; h = Math.imul(h, 0x297a2d39) >>> 0
14
+ h ^= h >>> 15
15
+ return (h >>> 0) / 4294967296
16
+ }
17
+
9
18
  function catmullRom(p0, p1, p2, p3, t) {
10
19
  const getX = (p) => Array.isArray(p) ? p[0] : p.x
11
20
  const getZ = (p) => Array.isArray(p) ? p[1] : p.z
@@ -134,26 +143,47 @@ export function createSplineCarveLayer() {
134
143
  const cells = new Map()
135
144
  const appliedSplines = []
136
145
 
137
- function carve(controlPoints, width, depth, kind, baseHeightFn) {
146
+ function carve(controlPoints, width, depth, kind, baseHeightFn, opts = {}) {
138
147
  if (!Array.isArray(controlPoints) || controlPoints.length < 2) return { touched: 0 }
139
148
  if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(depth)) return { touched: 0 }
140
149
  if (kind !== 'river' && kind !== 'road') return { touched: 0 }
150
+ const { seed = 0, widthVariance = 0, bankErosion = 0 } = opts
151
+ const isRiver = kind === 'river'
141
152
  const stepSize = Math.max(1, width * 0.5)
142
153
  const spinePoints = sampleSpline(controlPoints, stepSize)
143
154
  if (spinePoints.length === 0) return { touched: 0 }
144
- const halfWidth = width * 0.5
155
+ const maxHalfWidth = (width * 0.5) * (1 + (isRiver ? widthVariance : 0)) + (isRiver ? bankErosion : 0)
145
156
  let touched = 0
146
- for (const sp of spinePoints) {
157
+ let arcLen = 0
158
+ for (let si = 0; si < spinePoints.length; si++) {
159
+ const sp = spinePoints[si]
147
160
  const sx = sp.x, sz = sp.z
148
- const cx0 = Math.floor((sx - halfWidth) / CELL_M), cx1 = Math.ceil((sx + halfWidth) / CELL_M)
149
- const cz0 = Math.floor((sz - halfWidth) / CELL_M), cz1 = Math.ceil((sz + halfWidth) / CELL_M)
161
+ if (si > 0) { const px = spinePoints[si - 1].x, pz = spinePoints[si - 1].z; arcLen += Math.hypot(sx - px, sz - pz) }
162
+ let halfWidth = width * 0.5
163
+ if (isRiver && widthVariance > 0) {
164
+ const t = carveHash(seed, Math.round(arcLen * 0.1))
165
+ halfWidth *= 1 + (t * 2 - 1) * widthVariance
166
+ }
167
+ const cx0 = Math.floor((sx - maxHalfWidth) / CELL_M), cx1 = Math.ceil((sx + maxHalfWidth) / CELL_M)
168
+ const cz0 = Math.floor((sz - maxHalfWidth) / CELL_M), cz1 = Math.ceil((sz + maxHalfWidth) / CELL_M)
150
169
  for (let cz = cz0; cz <= cz1; cz++) {
151
170
  for (let cx = cx0; cx <= cx1; cx++) {
152
- const ccx = cx * CELL_M + CELL_M * 0.5, ccz = cz * CELL_M + CELL_M * 0.5
153
- const dx = ccx - sx, dz = ccz - sz
154
- const d = Math.hypot(dx, dz)
155
- if (d > halfWidth) continue
156
- const falloff = 0.5 * (1 + Math.cos((d / halfWidth) * Math.PI))
171
+ const cMinX = cx * CELL_M, cMinZ = cz * CELL_M
172
+ const ccx = cMinX + CELL_M * 0.5, ccz = cMinZ + CELL_M * 0.5
173
+ let bankOffset = 0
174
+ if (isRiver && bankErosion > 0) {
175
+ const angle = carveHash(seed ^ 0x5bd1e995, cx * 92821 + cz) * Math.PI * 2
176
+ bankOffset = (carveHash(seed ^ 0x1b873593, cx * 15485863 + cz) * 2 - 1) * bankErosion * (0.5 + 0.5 * Math.cos(angle))
177
+ }
178
+ const cellHalfWidth = halfWidth + bankOffset
179
+ if (cellHalfWidth <= 0) continue
180
+ const nearestX = Math.max(cMinX, Math.min(sx, cMinX + CELL_M))
181
+ const nearestZ = Math.max(cMinZ, Math.min(sz, cMinZ + CELL_M))
182
+ const d = Math.hypot(nearestX - sx, nearestZ - sz)
183
+ if (d > cellHalfWidth) continue
184
+ const dCenter = Math.hypot(ccx - sx, ccz - sz)
185
+ const falloffDist = Math.min(dCenter, cellHalfWidth)
186
+ const falloff = 0.5 * (1 + Math.cos((falloffDist / cellHalfWidth) * Math.PI))
157
187
  const key = cellKey(cx, cz)
158
188
  const base = typeof baseHeightFn === 'function' ? baseHeightFn(ccx, ccz) : null
159
189
  const targetDelta = Number.isFinite(base) ? -depth * falloff : 0
@@ -165,7 +195,7 @@ export function createSplineCarveLayer() {
165
195
  }
166
196
  }
167
197
  }
168
- if (touched > 0) appliedSplines.push({ controlPoints: controlPoints.map(p => Array.isArray(p) ? [p[0], p[1]] : [p.x, p.z]), width, depth, kind })
198
+ if (touched > 0) appliedSplines.push({ controlPoints: controlPoints.map(p => Array.isArray(p) ? [p[0], p[1]] : [p.x, p.z]), width, depth, kind, seed, widthVariance, bankErosion })
169
199
  return { touched }
170
200
  }
171
201
 
@@ -220,7 +250,7 @@ export function loadSplineCarveLayer(json, baseHeightFn) {
220
250
  if (json && Array.isArray(json.splines)) {
221
251
  for (const s of json.splines) {
222
252
  if (!s || !Array.isArray(s.controlPoints) || !Number.isFinite(s.width) || !Number.isFinite(s.depth)) continue
223
- layer.carve(s.controlPoints, s.width, s.depth, s.kind, baseHeightFn)
253
+ layer.carve(s.controlPoints, s.width, s.depth, s.kind, baseHeightFn, { seed: s.seed ?? 0, widthVariance: s.widthVariance ?? 0, bankErosion: s.bankErosion ?? 0 })
224
254
  }
225
255
  }
226
256
  return layer
@@ -386,5 +386,6 @@ export async function setupTerrainStreaming({ physics, playerManager, worldDef,
386
386
  const c = streamer.center || (getCenter ? getCenter() : null) || tcfg.center || [0, 0]
387
387
  await streamer._rebuild(c[0], c[1])
388
388
  }
389
+ physics._terrainStreamer = streamer
389
390
  return streamer
390
391
  }