spoint 0.1.586 → 0.1.588

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.586",
3
+ "version": "0.1.588",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -0,0 +1,92 @@
1
+ import { marchRockSurface } from './RockShapes.js'
2
+
3
+ export function makeSphereCaveSDF(cx, cy, cz, radius) {
4
+ return (x, y, z) => {
5
+ const dx = x - cx, dy = y - cy, dz = z - cz
6
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) - radius
7
+ }
8
+ }
9
+
10
+ export function polygonizeCaveSDF(sdf, res = 24) {
11
+ return marchRockSurface(res, sdf)
12
+ }
13
+
14
+ export function createCaveVolume(worldX, worldY, worldZ, worldRadius) {
15
+ const sdf = makeSphereCaveSDF(0, 0, 0, 1)
16
+ const localToWorld = (lx, ly, lz) => [worldX + lx * worldRadius, worldY + ly * worldRadius, worldZ + lz * worldRadius]
17
+ const worldToLocal = (wx, wy, wz) => [(wx - worldX) / worldRadius, (wy - worldY) / worldRadius, (wz - worldZ) / worldRadius]
18
+ const worldSDF = (wx, wy, wz) => {
19
+ const [lx, ly, lz] = worldToLocal(wx, wy, wz)
20
+ return sdf(lx, ly, lz) * worldRadius
21
+ }
22
+ return { sdf: worldSDF, localToWorld, worldToLocal, worldX, worldY, worldZ, worldRadius }
23
+ }
24
+
25
+ export function polygonizeCaveVolume(caveVolume, res = 24) {
26
+ const local = polygonizeCaveSDF((lx, ly, lz) => makeSphereCaveSDF(0, 0, 0, 1)(lx, ly, lz), res)
27
+ const positions = new Float32Array(local.positions.length)
28
+ for (let v = 0; v < local.vc; v++) {
29
+ const [wx, wy, wz] = caveVolume.localToWorld(local.positions[v * 3], local.positions[v * 3 + 1], local.positions[v * 3 + 2])
30
+ positions[v * 3] = wx
31
+ positions[v * 3 + 1] = wy
32
+ positions[v * 3 + 2] = wz
33
+ }
34
+ return { positions: positions.subarray(0, local.vc * 3), vc: local.vc, indices: local.indices, ic: local.ic }
35
+ }
36
+
37
+ export function createCaveCarveLayer() {
38
+ const volumes = []
39
+
40
+ function addSphereCave(worldX, worldY, worldZ, worldRadius) {
41
+ if (!Number.isFinite(worldX) || !Number.isFinite(worldY) || !Number.isFinite(worldZ)) return { added: false }
42
+ if (!Number.isFinite(worldRadius) || worldRadius <= 0) return { added: false }
43
+ volumes.push(createCaveVolume(worldX, worldY, worldZ, worldRadius))
44
+ return { added: true, count: volumes.length }
45
+ }
46
+
47
+ function heightDeltaAt(x, z, surfaceY) {
48
+ if (volumes.length === 0) return 0
49
+ let delta = 0
50
+ for (const vol of volumes) {
51
+ const dx = x - vol.worldX, dz = z - vol.worldZ
52
+ const horizontalDist = Math.hypot(dx, dz)
53
+ if (horizontalDist > vol.worldRadius) continue
54
+ const verticalSpan = Math.sqrt(Math.max(0, vol.worldRadius * vol.worldRadius - horizontalDist * horizontalDist))
55
+ const caveTop = vol.worldY + verticalSpan
56
+ if (caveTop < surfaceY) continue
57
+ delta -= (caveTop - surfaceY) + verticalSpan
58
+ }
59
+ return delta
60
+ }
61
+
62
+ function wrapHeightFn(baseHeightFn) {
63
+ if (typeof baseHeightFn !== 'function') return baseHeightFn
64
+ return function caveWrappedHeightFn(x, z) {
65
+ const base = baseHeightFn(x, z)
66
+ if (!Number.isFinite(base)) return base
67
+ return base + heightDeltaAt(x, z, base)
68
+ }
69
+ }
70
+
71
+ function toJSON() {
72
+ return { version: 1, volumes: volumes.map(v => ({ worldX: v.worldX, worldY: v.worldY, worldZ: v.worldZ, worldRadius: v.worldRadius })) }
73
+ }
74
+
75
+ function clear() { volumes.length = 0 }
76
+
77
+ return {
78
+ addSphereCave, heightDeltaAt, wrapHeightFn, toJSON, clear,
79
+ get volumeCount() { return volumes.length },
80
+ }
81
+ }
82
+
83
+ export function loadCaveCarveLayer(json) {
84
+ const layer = createCaveCarveLayer()
85
+ if (json && Array.isArray(json.volumes)) {
86
+ for (const v of json.volumes) {
87
+ if (!v || !Number.isFinite(v.worldX) || !Number.isFinite(v.worldY) || !Number.isFinite(v.worldZ) || !Number.isFinite(v.worldRadius)) continue
88
+ layer.addSphereCave(v.worldX, v.worldY, v.worldZ, v.worldRadius)
89
+ }
90
+ }
91
+ return layer
92
+ }
@@ -6,6 +6,7 @@ import { createCachedAnchorField } from './ClimateCache.js'
6
6
  import { createHeightDelta, loadHeightDelta } from './HeightDelta.js'
7
7
  import { createBiomeOverride, loadBiomeOverride } from './BiomeOverride.js'
8
8
  import { loadSplineCarveLayer } from './SplineCarve.js'
9
+ import { loadCaveCarveLayer } from './CaveSDF.js'
9
10
 
10
11
  const _now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now())
11
12
 
@@ -235,7 +236,7 @@ async function createGpuPatchHeightFn({ frame, tcfg, offsetY }) {
235
236
  // (re)build -- kept strictly separate from tcfg/seed (never merged into it) so seeds stay shareable.
236
237
  // biomeOverrideJSON (optional): a previously-serialized src/terrain/BiomeOverride.js toJSON() payload
237
238
  // (paint-biome strokes), same replay discipline as heightDeltaJSON.
238
- export async function setupTerrainStreaming({ physics, playerManager, worldDef, terrain, heightDeltaJSON, biomeOverrideJSON, splineCarveJSON }) {
239
+ export async function setupTerrainStreaming({ physics, playerManager, worldDef, terrain, heightDeltaJSON, biomeOverrideJSON, splineCarveJSON, caveCarveJSON }) {
239
240
  // accepts either the terrain app's merged tcfg directly, or legacy worldDef.terrain.
240
241
  const tcfg = terrain || (worldDef && worldDef.terrain) || null
241
242
  if (!tcfg || tcfg.enabled === false || !physics || typeof physics.addHeightField !== 'function') return null
@@ -285,7 +286,8 @@ export async function setupTerrainStreaming({ physics, playerManager, worldDef,
285
286
  // stays byte-identical, sculpting is purely an on-top layer. heightDeltaJSON (if any) replays prior
286
287
  // strokes so a rebuild (reseed/streamer restart) keeps existing sculpting intact.
287
288
  const heightDelta = loadHeightDelta(heightDeltaJSON, baseHeightFn)
288
- const heightFn = splineCarve.wrapHeightFn(heightDelta.wrapHeightFn(baseHeightFn))
289
+ const caveCarve = loadCaveCarveLayer(caveCarveJSON)
290
+ const heightFn = caveCarve.wrapHeightFn(splineCarve.wrapHeightFn(heightDelta.wrapHeightFn(baseHeightFn)))
289
291
  const getCenter = () => {
290
292
  let sx = 0, sz = 0, n = 0
291
293
  const players = playerManager && playerManager.players
@@ -362,6 +364,7 @@ export async function setupTerrainStreaming({ physics, playerManager, worldDef,
362
364
  // keep serving pre-paint species/density for already-visited territory.
363
365
  streamer.biomeOverride = biomeOverride
364
366
  streamer.splineCarve = splineCarve
367
+ streamer.caveCarve = caveCarve
365
368
  streamer.repaintBiome = async function repaintBiome() {
366
369
  // Re-cook the FULL current multi-player ring (not just one point) so every connected player's
367
370
  // territory picks up the repaint, matching this row's "around each player" streaming policy.