spoint 0.1.600 → 0.1.602

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.600",
3
+ "version": "0.1.602",
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)`)
@@ -149,11 +149,15 @@ export function createSplineCarveLayer() {
149
149
  const cz0 = Math.floor((sz - halfWidth) / CELL_M), cz1 = Math.ceil((sz + halfWidth) / CELL_M)
150
150
  for (let cz = cz0; cz <= cz1; cz++) {
151
151
  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)
152
+ const cMinX = cx * CELL_M, cMinZ = cz * CELL_M
153
+ const nearestX = Math.max(cMinX, Math.min(sx, cMinX + CELL_M))
154
+ const nearestZ = Math.max(cMinZ, Math.min(sz, cMinZ + CELL_M))
155
+ const d = Math.hypot(nearestX - sx, nearestZ - sz)
155
156
  if (d > halfWidth) continue
156
- const falloff = 0.5 * (1 + Math.cos((d / halfWidth) * Math.PI))
157
+ const ccx = cMinX + CELL_M * 0.5, ccz = cMinZ + CELL_M * 0.5
158
+ const dCenter = Math.hypot(ccx - sx, ccz - sz)
159
+ const falloffDist = Math.min(dCenter, halfWidth)
160
+ const falloff = 0.5 * (1 + Math.cos((falloffDist / halfWidth) * Math.PI))
157
161
  const key = cellKey(cx, cz)
158
162
  const base = typeof baseHeightFn === 'function' ? baseHeightFn(ccx, ccz) : null
159
163
  const targetDelta = Number.isFinite(base) ? -depth * falloff : 0
@@ -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
  }