spoint 0.1.48 → 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:
@@ -821,16 +1042,16 @@ return {
821
1042
 
822
1043
  ### Entity custom Field - Procedural Mesh Conventions
823
1044
 
824
- 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.
825
1046
 
826
1047
  ```js
827
- // Box
828
- 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 }
829
1050
 
830
- // Sphere
831
- custom: { mesh: 'sphere', color: 0x00ff00, radius: 1 }
1051
+ // Sphere — r is radius
1052
+ custom: { mesh: 'sphere', color: 0x00ff00, r: 1, seg: 16 }
832
1053
 
833
- // Cylinder
1054
+ // Cylinder — r is radius, h is full height, seg is polygon count
834
1055
  custom: {
835
1056
  mesh: 'cylinder',
836
1057
  r: 0.4, h: 0.1, seg: 16,
@@ -853,6 +1074,10 @@ custom: { ..., glow: true, glowColor: 0x00ff88, glowIntensity: 0.5 }
853
1074
  custom: { mesh: 'box', label: 'PRESS E' }
854
1075
  ```
855
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
+
856
1081
  ---
857
1082
 
858
1083
  ## AppLoader Security Restrictions
@@ -1063,6 +1288,29 @@ ctx.bus.on('combat.*', (event) => {
1063
1288
 
1064
1289
  ## Critical Caveats
1065
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
+
1066
1314
  ### ctx.state survives hot reload; timers and bus subscriptions do not
1067
1315
 
1068
1316
  Re-register all timers and bus subscriptions in `setup`. Use `||` to preserve state:
@@ -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.48",
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",