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 +271 -9
- package/apps/arena/index.js +83 -0
- package/apps/box-static/index.js +24 -0
- package/apps/prop-static/index.js +15 -0
- package/apps/world/index.js +2 -0
- package/package.json +1 -1
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
|
|
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:
|
|
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,
|
|
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
|
|
1354
|
+
### Animation library uses two-phase cache
|
|
1101
1355
|
|
|
1102
|
-
`
|
|
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
|
+
}
|
package/apps/world/index.js
CHANGED
|
@@ -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]
|