spoint 0.1.42 → 0.1.43

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/SKILL.md CHANGED
@@ -239,6 +239,14 @@ ctx.physics.addCapsuleCollider(radius, fullHeight)
239
239
  ctx.physics.addTrimeshCollider()
240
240
  // Builds static mesh from entity.model path. Static only.
241
241
 
242
+ ctx.physics.addConvexCollider(points)
243
+ // points: flat Float32Array or Array of [x,y,z,x,y,z,...] vertex positions
244
+ // Builds a ConvexHullShape from the provided point cloud. Supports all motion types.
245
+
246
+ ctx.physics.addConvexFromModel(meshIndex = 0)
247
+ // Extracts vertex positions from entity.model GLB and builds ConvexHullShape.
248
+ // Simpler and faster than trimesh for dynamic objects like vehicles/crates.
249
+
242
250
  ctx.physics.addForce([fx, fy, fz]) // Impulse: velocity += force / mass
243
251
  ctx.physics.setVelocity([vx, vy, vz]) // Set velocity directly
244
252
  ```
@@ -544,6 +552,12 @@ ctx.physics.addCapsuleCollider(radius, fullHeight)
544
552
  ctx.physics.addTrimeshCollider()
545
553
  // Static trimesh from entity.model. Only for static bodies.
546
554
 
555
+ ctx.physics.addConvexCollider(points)
556
+ // points: flat array [x,y,z,x,y,z,...]. Supports all motion types (dynamic/kinematic/static).
557
+
558
+ ctx.physics.addConvexFromModel(meshIndex = 0)
559
+ // Extracts vertices from entity.model GLB and builds ConvexHullShape. Good for dynamic vehicles/crates.
560
+
547
561
  ctx.physics.addForce([fx, fy, fz]) // velocity += force / mass
548
562
  ctx.physics.setVelocity([vx, vy, vz])
549
563
  ```
@@ -1068,6 +1082,14 @@ The engine manually applies `gravity[1] * dt` to Y velocity. This is already han
1068
1082
 
1069
1083
  `addTrimeshCollider()` creates a static mesh. No dynamic or kinematic trimesh support.
1070
1084
 
1085
+ ### Convex hull for dynamic objects
1086
+
1087
+ Use `addConvexCollider(points)` or `addConvexFromModel()` for dynamic/kinematic bodies that need shape-accurate physics (vehicles, crates). Convex hulls support all motion types unlike trimesh. `addConvexFromModel()` reads vertices from the entity's GLB at setup time - call it after setting `entity.model`.
1088
+
1089
+ ### Animation library is cached globally
1090
+
1091
+ `loadAnimationLibrary()` loads `/anim-lib.glb` only once and caches the result. All subsequent calls return the cached result immediately. The library is also pre-fetched in parallel with the VRM download during initialization.
1092
+
1071
1093
  ### Tick drops under load
1072
1094
 
1073
1095
  TickSystem processes max 4 ticks per loop. If the server falls more than 4 ticks behind (31ms at 128 TPS), those ticks are dropped silently.
@@ -123,12 +123,21 @@ function normalizeClips(gltf, vrmVersion, vrmHumanoid) {
123
123
  return clips
124
124
  }
125
125
 
126
+ let _animLibCache = null
127
+ let _animLibPromise = null
128
+
126
129
  export async function loadAnimationLibrary(vrmVersion, vrmHumanoid) {
127
- const loader = new GLTFLoader()
128
- const gltf = await loader.loadAsync('/anim-lib.glb')
129
- const normalizedClips = normalizeClips(gltf, vrmVersion || '1', vrmHumanoid)
130
- console.log(`[anim] Loaded animation library (${normalizedClips.size} clips):`, [...normalizedClips.keys()])
131
- return { normalizedClips }
130
+ if (_animLibCache) return _animLibCache
131
+ if (_animLibPromise) return _animLibPromise
132
+ _animLibPromise = (async () => {
133
+ const loader = new GLTFLoader()
134
+ const gltf = await loader.loadAsync('/anim-lib.glb')
135
+ const normalizedClips = normalizeClips(gltf, vrmVersion || '1', vrmHumanoid)
136
+ console.log(`[anim] Loaded animation library (${normalizedClips.size} clips):`, [...normalizedClips.keys()])
137
+ _animLibCache = { normalizedClips }
138
+ return _animLibCache
139
+ })()
140
+ return _animLibPromise
132
141
  }
133
142
 
134
143
  export function createPlayerAnimator(vrm, allClips, vrmVersion, animConfig = {}) {
package/client/app.js CHANGED
@@ -713,16 +713,16 @@ function detectVrmVersion(buffer) {
713
713
 
714
714
  function initAssets(playerModelUrl) {
715
715
  loadingMgr.setStage('DOWNLOAD')
716
- assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(b => {
716
+ const animLibPromise = loadAnimationLibrary('1', null)
717
+ assetsReady = loadingMgr.fetchWithProgress(playerModelUrl).then(async b => {
717
718
  vrmBuffer = b
718
719
  loadingMgr.setStage('PROCESS')
719
- return loadAnimationLibrary(detectVrmVersion(b), null)
720
- }).then(result => {
721
- animAssets = result
720
+ animAssets = await animLibPromise
722
721
  assetsLoaded = true
723
722
  checkAllLoaded()
724
723
  }).catch(err => {
725
724
  console.warn('[assets] player model unavailable:', err.message)
725
+ animLibPromise.then(r => { animAssets = r }).catch(() => {})
726
726
  assetsLoaded = true
727
727
  checkAllLoaded()
728
728
  })
@@ -840,7 +840,16 @@ function evaluateAppModule(code) {
840
840
  try {
841
841
  let stripped = code.replace(/^import\s+.*$/gm, '')
842
842
  stripped = stripped.replace(/const\s+__dirname\s*=.*import\.meta\.url.*$/gm, 'const __dirname = "/"')
843
- const wrapped = stripped.replace(/export\s+default\s*/, 'return ').replace(/export\s+/g, '')
843
+ stripped = stripped.replace(/export\s+/g, '')
844
+ const exportDefaultIdx = stripped.search(/\bdefault\s*[\{(]/)
845
+ let wrapped
846
+ if (exportDefaultIdx !== -1) {
847
+ const before = stripped.slice(0, exportDefaultIdx)
848
+ const after = stripped.slice(exportDefaultIdx + 'default'.length).trimStart()
849
+ wrapped = before + '\nreturn ' + after + '\n//# sourceURL=app-module.js'
850
+ } else {
851
+ wrapped = stripped.replace(/\bdefault\s*/, 'return ') + '\n//# sourceURL=app-module.js'
852
+ }
844
853
  const join = (...parts) => parts.filter(Boolean).join('/')
845
854
  const readdirSync = () => []
846
855
  const statSync = () => ({ isDirectory: () => false })
@@ -969,6 +978,7 @@ function loadEntityModel(entityId, entityState) {
969
978
  fitShadowFrustum()
970
979
  pendingLoads.delete(entityId)
971
980
  if (!environmentLoaded) { environmentLoaded = true; checkAllLoaded() }
981
+ if (loadingScreenHidden) renderer.compileAsync(scene, camera).catch(() => renderer.compile(scene, camera))
972
982
  }, (progress) => { if (progress.total > 0) console.log('[gltf]', url, Math.round(progress.loaded / progress.total * 100) + '%') }, (err) => { console.error('[gltf]', url, err); pendingLoads.delete(entityId) })
973
983
  }
974
984
 
@@ -1117,7 +1127,8 @@ function checkAllLoaded() {
1117
1127
  loadingMgr.setStage('INIT')
1118
1128
  loadingMgr.complete()
1119
1129
  loadingScreenHidden = true
1120
- warmupShaders().then(() => loadingScreen.hide()).catch(() => loadingScreen.hide())
1130
+ loadingScreen.hide()
1131
+ warmupShaders().catch(() => {})
1121
1132
  }
1122
1133
 
1123
1134
  function initInputHandler() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,4 +1,5 @@
1
1
  import { CliDebugger } from '../debug/CliDebugger.js'
2
+ import { extractMeshFromGLB } from '../physics/GLBLoader.js'
2
3
 
3
4
  export class AppContext {
4
5
  constructor(entity, runtime) {
@@ -75,6 +76,23 @@ export class AppContext {
75
76
  ent._physicsBodyId = bodyId
76
77
  }
77
78
  },
79
+ addConvexCollider: (points) => {
80
+ ent.collider = { type: 'convex', points }
81
+ if (runtime._physics) {
82
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
83
+ ent._physicsBodyId = runtime._physics.addBody('convex', points, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
84
+ }
85
+ },
86
+ addConvexFromModel: (meshIndex = 0) => {
87
+ if (!ent.model) return
88
+ const mesh = extractMeshFromGLB(runtime.resolveAssetPath(ent.model), meshIndex)
89
+ const points = Array.from(mesh.vertices)
90
+ ent.collider = { type: 'convex', points }
91
+ if (runtime._physics) {
92
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
93
+ ent._physicsBodyId = runtime._physics.addBody('convex', points, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
94
+ }
95
+ },
78
96
  addForce: (f) => {
79
97
  const mass = ent.mass || 1
80
98
  ent.velocity[0] += f[0] / mass
@@ -59,6 +59,13 @@ export class PhysicsWorld {
59
59
  if (shapeType === 'box') shape = new J.BoxShape(new J.Vec3(params[0], params[1], params[2]), 0.05, null)
60
60
  else if (shapeType === 'sphere') shape = new J.SphereShape(params)
61
61
  else if (shapeType === 'capsule') shape = new J.CapsuleShape(params[1], params[0])
62
+ else if (shapeType === 'convex') {
63
+ const pts = new J.VertexList()
64
+ for (let i = 0; i < params.length; i += 3) pts.push_back(new J.Float3(params[i], params[i + 1], params[i + 2]))
65
+ const cvx = new J.ConvexHullShapeSettings(pts)
66
+ shape = cvx.Create().Get()
67
+ J.destroy(pts); J.destroy(cvx)
68
+ }
62
69
  else return null
63
70
  const mt = motionType === 'dynamic' ? J.EMotionType_Dynamic : motionType === 'kinematic' ? J.EMotionType_Kinematic : J.EMotionType_Static
64
71
  layer = motionType === 'static' ? LAYER_STATIC : LAYER_DYNAMIC