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.
package/apps/_lib/steering.js
CHANGED
|
@@ -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 = {
|
package/apps/combat-bot/index.js
CHANGED
|
@@ -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
package/src/apps/AppContext.js
CHANGED
|
@@ -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
|
|
155
|
+
const maxHalfWidth = (width * 0.5) * (1 + (isRiver ? widthVariance : 0)) + (isRiver ? bankErosion : 0)
|
|
145
156
|
let touched = 0
|
|
146
|
-
|
|
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
|
|
149
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
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
|
}
|