spoint 0.1.47 → 0.1.49

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
@@ -9,6 +9,227 @@ Complete reference for building apps in a spawnpoint project. Engine source code
9
9
 
10
10
  ---
11
11
 
12
+ ## Quick Start — Minimal Working Arena
13
+
14
+ This is the fastest path to a playable scene. Copy this pattern exactly.
15
+
16
+ ### Step 1: World config (`apps/world/index.js`)
17
+
18
+ ```js
19
+ export default {
20
+ port: 3001,
21
+ tickRate: 128,
22
+ gravity: [0, -9.81, 0],
23
+ movement: { maxSpeed: 4.0, groundAccel: 10.0, airAccel: 1.0, friction: 6.0, stopSpeed: 2.0, jumpImpulse: 4.0 },
24
+ player: { health: 100, capsuleRadius: 0.4, capsuleHalfHeight: 0.9, modelScale: 1.323, feetOffset: 0.212 },
25
+ scene: { skyColor: 0x87ceeb, fogColor: 0x87ceeb, fogNear: 80, fogFar: 200, sunIntensity: 1.5, sunPosition: [20, 40, 20] },
26
+ entities: [
27
+ { id: 'arena', position: [0, 0, 0], app: 'arena' }
28
+ ],
29
+ spawnPoint: [0, 2, 0]
30
+ }
31
+ ```
32
+
33
+ ### Step 2: Arena app (`apps/arena/index.js`)
34
+
35
+ This creates a ground plane and 4 walls using box primitives (visual + physics), no GLB required:
36
+
37
+ ```js
38
+ const HALF = 12, WALL_H = 3, WALL_T = 0.5
39
+ const WALLS = [
40
+ { id: 'wall-n', x: 0, y: WALL_H/2, z: -HALF, hx: HALF, hy: WALL_H/2, hz: WALL_T/2 },
41
+ { id: 'wall-s', x: 0, y: WALL_H/2, z: HALF, hx: HALF, hy: WALL_H/2, hz: WALL_T/2 },
42
+ { id: 'wall-e', x: HALF, y: WALL_H/2, z: 0, hx: WALL_T/2, hy: WALL_H/2, hz: HALF },
43
+ { id: 'wall-w', x: -HALF, y: WALL_H/2, z: 0, hx: WALL_T/2, hy: WALL_H/2, hz: HALF },
44
+ ]
45
+
46
+ export default {
47
+ server: {
48
+ setup(ctx) {
49
+ ctx.state.ids = ctx.state.ids || []
50
+ if (ctx.state.ids.length > 0) return // hot-reload guard
51
+
52
+ // Ground — this entity IS the ground
53
+ ctx.entity.custom = { mesh: 'box', color: 0x5a7a4a, roughness: 1, sx: HALF*2, sy: 0.5, sz: HALF*2 }
54
+ ctx.physics.setStatic(true)
55
+ ctx.physics.addBoxCollider([HALF, 0.25, HALF])
56
+
57
+ // Walls — spawn children each with box-static app
58
+ for (const w of WALLS) {
59
+ const e = ctx.world.spawn(w.id, {
60
+ position: [w.x, w.y, w.z],
61
+ app: 'box-static',
62
+ config: { hx: w.hx, hy: w.hy, hz: w.hz, color: 0x7a6a5a }
63
+ })
64
+ if (e) ctx.state.ids.push(w.id)
65
+ }
66
+ },
67
+ teardown(ctx) {
68
+ for (const id of ctx.state.ids || []) ctx.world.destroy(id)
69
+ ctx.state.ids = []
70
+ }
71
+ },
72
+ client: {
73
+ render(ctx) {
74
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Step 3: box-static reusable app (`apps/box-static/index.js`)
81
+
82
+ Reusable app for any static box primitive with physics. Config drives size and color:
83
+
84
+ ```js
85
+ export default {
86
+ server: {
87
+ setup(ctx) {
88
+ const c = ctx.config
89
+ ctx.entity.custom = {
90
+ mesh: 'box',
91
+ color: c.color ?? 0x888888,
92
+ roughness: c.roughness ?? 0.9,
93
+ sx: (c.hx ?? 1) * 2, sy: (c.hy ?? 1) * 2, sz: (c.hz ?? 1) * 2
94
+ }
95
+ ctx.physics.setStatic(true)
96
+ ctx.physics.addBoxCollider([c.hx ?? 1, c.hy ?? 1, c.hz ?? 1])
97
+ }
98
+ },
99
+ client: {
100
+ render(ctx) {
101
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Asset Loading — How It Works
110
+
111
+ ### Loading Sequence Gate
112
+
113
+ The loading screen stays up until ALL of these pass simultaneously:
114
+ 1. WebSocket connected
115
+ 2. Player VRM model downloaded
116
+ 3. Environment model loaded (first entity with a `model` field, OR any entity without a model that creates a mesh)
117
+ 4. First snapshot received from server
118
+ 5. All entities from the **world config `entities` array** that have a `model` field are loaded
119
+
120
+ **Critical:** Entities declared in `world.entities` that have a `model` are tracked by the client loading gate. Entities spawned later by `ctx.world.spawn()` at runtime are NOT part of the loading gate — they appear after the game starts.
121
+
122
+ If you want a model to be part of the loading sequence, declare it in `world.entities`:
123
+
124
+ ```js
125
+ // world/index.js
126
+ entities: [
127
+ { id: 'environment', model: './apps/tps-game/schwust.glb', app: 'environment' },
128
+ // ^^ This model blocks the loading screen until loaded
129
+ ]
130
+ ```
131
+
132
+ ### Local Models
133
+
134
+ Place GLB files inside your app folder. Reference with `./apps/<app-name>/file.glb`:
135
+
136
+ ```
137
+ apps/my-arena/
138
+ index.js
139
+ floor.glb <-- served automatically by StaticHandler
140
+ wall.glb
141
+ ```
142
+
143
+ ```js
144
+ // In world/index.js:
145
+ { id: 'env', model: './apps/my-arena/floor.glb', app: 'environment' }
146
+
147
+ // Or spawned at runtime:
148
+ ctx.world.spawn('floor', { model: './apps/my-arena/floor.glb', ... })
149
+ ```
150
+
151
+ The StaticHandler serves everything under `apps/` at the same path. No CDN, no hosting needed.
152
+
153
+ ### Remote Models (Verified Asset Library)
154
+
155
+ The `https://github.com/anEntrypoint/assets` repository contains ~170 free GLB models.
156
+
157
+ **URL pattern:** `https://raw.githubusercontent.com/anEntrypoint/assets/main/FILENAME.glb`
158
+
159
+ **Verified filenames** (use these exactly — wrong filenames silently 404):
160
+
161
+ ```
162
+ Vehicles & Junk:
163
+ broken_car_b6d2e66d_v1.glb broken_car_b6d2e66d_v2.glb
164
+ crashed_car_f2b577ae_v1.glb crashed_car_f2b577ae_v2.glb
165
+ crashed_pickup_truck_ae555020_v1.glb
166
+ crashed_rusty_minivan_f872ff37_v1.glb
167
+ Bus_junk_1.glb
168
+
169
+ Containers & Industrial:
170
+ blue_shipping_container_60b5ea93_v1.glb
171
+ blue_shipping_container_63cc3905_v1.glb
172
+ dumpster_b076662a_v1.glb dumpster_b076662a_v2.glb
173
+ garbage_can_6b3d052b_v1.glb garbage_can_6b3d052b_v2.glb
174
+ crushed_oil_barrel_e450f43f_v1.glb crushed_oil_barrel_e450f43f_v2.glb
175
+ fire_hydrant_ba0175c1_v1.glb fire_hydrant_ba0175c1_v2.glb
176
+ fire_extinguisher_wall_mounted_bc0dddd4_v1.glb
177
+
178
+ Office & Furniture:
179
+ break_room_chair_14a39c7b_v1.glb break_room_chair_14a39c7b_v2.glb
180
+ break_room_couch_444abf63_v1.glb break_room_table_09b9fd0d_v1.glb
181
+ filing_cabinet_0194476c_v1.glb filing_cabinet_0194476c_v2.glb
182
+ fancy_reception_desk_58fde71d_v1.glb
183
+ cash_register_0c0dcad2_v1.glb
184
+ espresso_machine_e722ed8c_v1.glb
185
+ Couch.glb Couch_2.glb 3chairs.glb
186
+
187
+ Natural & Rocks:
188
+ large_rock_051293c4_v1.glb large_rock_051293c4_v2.glb
189
+
190
+ Misc:
191
+ Tin_Man_1.glb Tin_Man_2.glb Plants_3.glb Urinals.glb V_Machine_2.glb
192
+ ```
193
+
194
+ **Remote models are NOT part of the loading gate.** They appear after game starts. Use them for decorative props, not required environment geometry.
195
+
196
+ **NEVER guess asset filenames.** Only use names from the verified list above. Wrong URLs silently 404 — no model appears, no error thrown.
197
+
198
+ ### prop-static reusable app
199
+
200
+ For remote GLB props that need convex hull physics:
201
+
202
+ ```js
203
+ // apps/prop-static/index.js
204
+ export default {
205
+ server: {
206
+ setup(ctx) {
207
+ ctx.physics.setStatic(true)
208
+ if (ctx.entity.model) ctx.physics.addConvexFromModel(0)
209
+ }
210
+ },
211
+ client: {
212
+ render(ctx) {
213
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, model: ctx.entity.model }
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ Spawn remote props:
220
+
221
+ ```js
222
+ const BASE = 'https://raw.githubusercontent.com/anEntrypoint/assets/main'
223
+ ctx.world.spawn('dumpster-1', {
224
+ model: `${BASE}/dumpster_b076662a_v1.glb`,
225
+ position: [5, 0, -3],
226
+ rotation: [0, Math.sin(0.5), 0, Math.cos(0.5)],
227
+ app: 'prop-static'
228
+ })
229
+ ```
230
+
231
+ ---
232
+
12
233
  ## Setup
13
234
 
14
235
  When no `apps/` directory exists in the working directory, scaffold it:
@@ -799,7 +1020,13 @@ App reloads never happen mid-tick. Queue drains at end of each tick. After each
799
1020
 
800
1021
  ### GLB Shader Stall Prevention
801
1022
 
802
- The engine automatically calls `renderer.compileAsync(object, camera)` immediately after adding any GLB or procedural mesh to the scene. This prevents first-draw GPU stall for dynamically loaded entities (environment models, physics crates, power crates, smart objects, drag-and-drop models). No action is needed from app code — warmup is handled in `loadEntityModel` and `loadQueuedModels`. VRM players use a separate one-time warmup path.
1023
+ The engine handles GPU shader warmup in two phases no action is needed from app code:
1024
+
1025
+ 1. **Initial load**: After the loading screen gates pass (assets, environment, first snapshot, first-snapshot entities all loaded), the loading screen hides and `warmupShaders()` runs asynchronously. It calls `renderer.compileAsync(scene, camera)`, disables frustum culling, renders twice to upload GPU data, then restores culling. This covers all entities present at startup.
1026
+
1027
+ 2. **Post-load dynamic entities**: For GLBs added after the loading screen is hidden, `loadEntityModel` calls `renderer.compileAsync(scene, camera)` immediately after adding the mesh to the scene.
1028
+
1029
+ VRM players use a separate one-time warmup (`_vrmWarmupDone`) that fires `renderer.compileAsync(scene, camera)` after the first player model loads.
803
1030
 
804
1031
  ### render(ctx) Return Value
805
1032
 
@@ -815,16 +1042,16 @@ return {
815
1042
 
816
1043
  ### Entity custom Field - Procedural Mesh Conventions
817
1044
 
818
- When no GLB model is set, `custom` drives procedural geometry:
1045
+ When no GLB model is set, `custom` drives procedural geometry. This is the primary way to create walls, floors, and visible primitives without any GLB file.
819
1046
 
820
1047
  ```js
821
- // Box
822
- custom: { mesh: 'box', color: 0xff8800, sx: 1, sy: 1, sz: 1 }
1048
+ // Box — sx/sy/sz are FULL dimensions (width/height/depth in world units)
1049
+ custom: { mesh: 'box', color: 0xff8800, roughness: 0.8, sx: 2, sy: 1, sz: 2 }
823
1050
 
824
- // Sphere
825
- custom: { mesh: 'sphere', color: 0x00ff00, radius: 1 }
1051
+ // Sphere — r is radius
1052
+ custom: { mesh: 'sphere', color: 0x00ff00, r: 1, seg: 16 }
826
1053
 
827
- // Cylinder
1054
+ // Cylinder — r is radius, h is full height, seg is polygon count
828
1055
  custom: {
829
1056
  mesh: 'cylinder',
830
1057
  r: 0.4, h: 0.1, seg: 16,
@@ -847,6 +1074,10 @@ custom: { ..., glow: true, glowColor: 0x00ff88, glowIntensity: 0.5 }
847
1074
  custom: { mesh: 'box', label: 'PRESS E' }
848
1075
  ```
849
1076
 
1077
+ **Note on sx/sy/sz vs collider half-extents:** The `custom.sx/sy/sz` fields are the FULL visual size. The `addBoxCollider([hx, hy, hz])` takes HALF-extents. Always halve the visual size when computing the collider: `sx: 4, sy: 2, sz: 4` → `addBoxCollider([2, 1, 2])`.
1078
+
1079
+ **Primitives with physics require an app.** Setting `entity.custom` only affects rendering. Physics colliders only activate when `ctx.physics.addBoxCollider()` etc. is called inside an app's `setup()`. Use the `box-static` app pattern (see Quick Start above) for primitive walls/floors.
1080
+
850
1081
  ---
851
1082
 
852
1083
  ## AppLoader Security Restrictions
@@ -1057,6 +1288,29 @@ ctx.bus.on('combat.*', (event) => {
1057
1288
 
1058
1289
  ## Critical Caveats
1059
1290
 
1291
+ ### Spawned entities need their own app to have physics
1292
+
1293
+ Calling `ctx.world.spawn()` creates an entity but does NOT create a physics body. Physics is only created when `ctx.physics.addBoxCollider()` (or any other physics call) runs inside that entity's app `setup()`.
1294
+
1295
+ To create a wall/floor with physics via `ctx.world.spawn()`, give it an app:
1296
+
1297
+ ```js
1298
+ // WRONG — entity appears visually but has no physics, players fall through
1299
+ const e = ctx.world.spawn('floor', { position: [0, 0, 0] })
1300
+ e.custom = { mesh: 'box', sx: 10, sy: 0.5, sz: 10 }
1301
+ e.bodyType = 'static' // does nothing without an app physics call
1302
+ e.collider = { ... } // does nothing without an app physics call
1303
+
1304
+ // CORRECT — use box-static app which calls ctx.physics.addBoxCollider in setup
1305
+ ctx.world.spawn('floor', {
1306
+ position: [0, 0, 0],
1307
+ app: 'box-static',
1308
+ config: { hx: 5, hy: 0.25, hz: 5, color: 0x448844 }
1309
+ })
1310
+ ```
1311
+
1312
+ Alternatively, the entity's own app can create child entities with their own apps, or the entity itself can call `ctx.physics.*` for its own body (the entity the app is attached to).
1313
+
1060
1314
  ### ctx.state survives hot reload; timers and bus subscriptions do not
1061
1315
 
1062
1316
  Re-register all timers and bus subscriptions in `setup`. Use `||` to preserve state:
@@ -1097,9 +1351,9 @@ The engine manually applies `gravity[1] * dt` to Y velocity. This is already han
1097
1351
 
1098
1352
  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`.
1099
1353
 
1100
- ### Animation library is cached globally
1354
+ ### Animation library uses two-phase cache
1101
1355
 
1102
- `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.
1356
+ `preloadAnimationLibrary()` kicks off the `/anim-lib.glb` fetch and caches the promise (`_gltfPromise`). `loadAnimationLibrary(vrmVersion, vrmHumanoid)` awaits that fetch and caches the normalized clip result (`_normalizedCache`). The engine calls `preloadAnimationLibrary()` early during asset init so the GLB is already fetching while the VRM downloads. Subsequent calls to `loadAnimationLibrary()` return the normalized cache immediately. Both functions are idempotent and safe to call concurrently.
1103
1357
 
1104
1358
  ### Tick drops under load
1105
1359
 
@@ -1121,6 +1375,14 @@ If any blocked string (including in comments) appears anywhere in the source, th
1121
1375
 
1122
1376
  All `import` statements in client app source are stripped by regex before evaluation. Use `engine.THREE`, `engine.scene`, etc. for all dependencies.
1123
1377
 
1378
+ ### GLB/VRM assets are cached in IndexedDB
1379
+
1380
+ On repeat page loads, `fetchCached()` in `client/ModelCache.js` validates cached GLB/VRM ArrayBuffers against the server ETag via a HEAD request. If the ETag matches, the cached bytes are returned without a network fetch. Cache misses or stale entries trigger a full fetch and re-store. Cache failures (quota, unavailable) fall back to normal fetch transparently. This is fully automatic — no app code needed.
1381
+
1382
+ ### Loading screen hides before shader warmup completes
1383
+
1384
+ After the four gate conditions pass, the loading screen hides immediately. `warmupShaders()` then runs asynchronously in the background. The very first rendered frame after the loading screen hides may have a brief GPU stall if shader compilation is not yet complete. This is a deliberate tradeoff to avoid the loading screen adding warmup time on top of actual asset loading.
1385
+
1124
1386
  ### setTimeout not cleared on hot reload
1125
1387
 
1126
1388
  `ctx.time.after/every` timers are cleared on teardown. `setTimeout` and `setInterval` are NOT. Use `ctx.time` for game logic. Use `setTimeout` only for external timing (e.g., reload cooldown) and manage cleanup in teardown manually.
@@ -0,0 +1,83 @@
1
+ const ASSET_BASE = 'https://raw.githubusercontent.com/anEntrypoint/assets/main'
2
+
3
+ const JUNK_MODELS = [
4
+ `${ASSET_BASE}/dumpster_b076662a_v1.glb`,
5
+ `${ASSET_BASE}/garbage_can_6b3d052b_v1.glb`,
6
+ `${ASSET_BASE}/fire_hydrant_ba0175c1_v1.glb`,
7
+ `${ASSET_BASE}/crushed_oil_barrel_e450f43f_v1.glb`,
8
+ ]
9
+
10
+ const HALF = 12
11
+ const WALL_H = 3
12
+ const WALL_T = 0.5
13
+
14
+ const WALLS = [
15
+ { id: 'arena-wall-n', x: 0, y: WALL_H / 2, z: -HALF, hx: HALF, hy: WALL_H / 2, hz: WALL_T / 2 },
16
+ { id: 'arena-wall-s', x: 0, y: WALL_H / 2, z: HALF, hx: HALF, hy: WALL_H / 2, hz: WALL_T / 2 },
17
+ { id: 'arena-wall-e', x: HALF, y: WALL_H / 2, z: 0, hx: WALL_T / 2, hy: WALL_H / 2, hz: HALF },
18
+ { id: 'arena-wall-w', x: -HALF, y: WALL_H / 2, z: 0, hx: WALL_T / 2, hy: WALL_H / 2, hz: HALF },
19
+ ]
20
+
21
+ const PROPS = [
22
+ { model: 0, x: -6, z: -6, rot: 0.5 },
23
+ { model: 0, x: 7, z: 5, rot: 2.1 },
24
+ { model: 1, x: -8, z: 3, rot: 1.0 },
25
+ { model: 1, x: 5, z: -7, rot: 3.2 },
26
+ { model: 1, x: 3, z: 8, rot: 0.3 },
27
+ { model: 2, x: -4, z: -9, rot: 1.5 },
28
+ { model: 2, x: 9, z: -3, rot: 4.0 },
29
+ { model: 3, x: -7, z: 7, rot: 0.8 },
30
+ { model: 3, x: 6, z: 2, rot: 2.7 },
31
+ { model: 3, x: -3, z: -5, rot: 1.2 },
32
+ ]
33
+
34
+ export default {
35
+ server: {
36
+ setup(ctx) {
37
+ ctx.state.ids = ctx.state.ids || []
38
+ if (ctx.state.ids.length > 0) return
39
+
40
+ // Ground (this entity itself)
41
+ ctx.entity.custom = { mesh: 'box', color: 0x5a7a4a, roughness: 1, sx: HALF * 2, sy: 0.5, sz: HALF * 2 }
42
+ ctx.physics.setStatic(true)
43
+ ctx.physics.addBoxCollider([HALF, 0.25, HALF])
44
+
45
+ // Walls - each gets box-static app which adds its own collider
46
+ for (const w of WALLS) {
47
+ const e = ctx.world.spawn(w.id, {
48
+ position: [w.x, w.y, w.z],
49
+ app: 'box-static',
50
+ config: { hx: w.hx, hy: w.hy, hz: w.hz, color: 0x7a6a5a, roughness: 0.9 }
51
+ })
52
+ if (e) ctx.state.ids.push(w.id)
53
+ }
54
+
55
+ // Props from remote asset repo (static, convex hull from model)
56
+ for (let i = 0; i < PROPS.length; i++) {
57
+ const p = PROPS[i]
58
+ const id = `arena-prop-${i}`
59
+ const a = p.rot / 2
60
+ const e = ctx.world.spawn(id, {
61
+ model: JUNK_MODELS[p.model],
62
+ position: [p.x, 0, p.z],
63
+ rotation: [0, Math.sin(a), 0, Math.cos(a)],
64
+ app: 'prop-static'
65
+ })
66
+ if (e) ctx.state.ids.push(id)
67
+ }
68
+
69
+ ctx.debug.log(`[arena] setup: ground + ${WALLS.length} walls + ${PROPS.length} props`)
70
+ },
71
+
72
+ teardown(ctx) {
73
+ for (const id of ctx.state.ids || []) ctx.world.destroy(id)
74
+ ctx.state.ids = []
75
+ }
76
+ },
77
+
78
+ client: {
79
+ render(ctx) {
80
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,24 @@
1
+ export default {
2
+ server: {
3
+ setup(ctx) {
4
+ const c = ctx.config
5
+ if (c.color !== undefined) {
6
+ ctx.entity.custom = {
7
+ mesh: 'box',
8
+ color: c.color,
9
+ roughness: c.roughness ?? 0.9,
10
+ sx: (c.hx ?? 1) * 2,
11
+ sy: (c.hy ?? 1) * 2,
12
+ sz: (c.hz ?? 1) * 2
13
+ }
14
+ }
15
+ ctx.physics.setStatic(true)
16
+ ctx.physics.addBoxCollider([c.hx ?? 1, c.hy ?? 1, c.hz ?? 1])
17
+ }
18
+ },
19
+ client: {
20
+ render(ctx) {
21
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,15 @@
1
+ export default {
2
+ server: {
3
+ setup(ctx) {
4
+ ctx.physics.setStatic(true)
5
+ if (ctx.entity.model) {
6
+ ctx.physics.addConvexFromModel(0)
7
+ }
8
+ }
9
+ },
10
+ client: {
11
+ render(ctx) {
12
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, model: ctx.entity.model }
13
+ }
14
+ }
15
+ }
@@ -62,6 +62,8 @@ export default {
62
62
  { id: 'game', position: [0, 0, 0], app: 'tps-game' },
63
63
  { id: 'power-crates', position: [0, 0, 0], app: 'power-crate' },
64
64
  { id: 'interact-box', position: [-100, 3, -100], app: 'interactable' }
65
+ // To use the primitive arena instead of schwust.glb, replace above with:
66
+ // { id: 'arena', position: [0, 0, 0], app: 'arena' }
65
67
  ],
66
68
  playerModel: './apps/tps-game/cleetus.vrm',
67
69
  spawnPoint: [-35, 3, -65]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",