@lanmower/entrypoint 0.16.0

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.
Files changed (70) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +250 -0
  3. package/apps/environment/index.js +17 -0
  4. package/apps/interactive-door/index.js +33 -0
  5. package/apps/patrol-npc/index.js +37 -0
  6. package/apps/physics-crate/index.js +23 -0
  7. package/apps/power-crate/index.js +169 -0
  8. package/apps/tps-game/index.js +168 -0
  9. package/apps/tps-game/schwust.glb +0 -0
  10. package/apps/world/index.js +22 -0
  11. package/client/app.js +181 -0
  12. package/client/camera.js +116 -0
  13. package/client/index.html +24 -0
  14. package/client/style.css +11 -0
  15. package/package.json +52 -0
  16. package/server.js +3 -0
  17. package/src/apps/AppContext.js +172 -0
  18. package/src/apps/AppLoader.js +160 -0
  19. package/src/apps/AppRuntime.js +192 -0
  20. package/src/apps/EventBus.js +62 -0
  21. package/src/apps/HotReloadQueue.js +63 -0
  22. package/src/client/InputHandler.js +85 -0
  23. package/src/client/PhysicsNetworkClient.js +171 -0
  24. package/src/client/PredictionEngine.js +123 -0
  25. package/src/client/ReconciliationEngine.js +54 -0
  26. package/src/connection/ConnectionManager.js +133 -0
  27. package/src/connection/QualityMonitor.js +46 -0
  28. package/src/connection/SessionStore.js +67 -0
  29. package/src/debug/CliDebugger.js +93 -0
  30. package/src/debug/Inspector.js +52 -0
  31. package/src/debug/StateInspector.js +42 -0
  32. package/src/index.client.js +8 -0
  33. package/src/index.js +1 -0
  34. package/src/index.server.js +27 -0
  35. package/src/math.js +21 -0
  36. package/src/netcode/EventLog.js +74 -0
  37. package/src/netcode/LagCompensator.js +78 -0
  38. package/src/netcode/NetworkState.js +66 -0
  39. package/src/netcode/PhysicsIntegration.js +132 -0
  40. package/src/netcode/PlayerManager.js +109 -0
  41. package/src/netcode/SnapshotEncoder.js +49 -0
  42. package/src/netcode/TickSystem.js +66 -0
  43. package/src/physics/GLBLoader.js +29 -0
  44. package/src/physics/World.js +195 -0
  45. package/src/protocol/Codec.js +60 -0
  46. package/src/protocol/EventEmitter.js +60 -0
  47. package/src/protocol/MessageTypes.js +73 -0
  48. package/src/protocol/SequenceTracker.js +71 -0
  49. package/src/protocol/msgpack.js +119 -0
  50. package/src/sdk/ClientMessageHandler.js +80 -0
  51. package/src/sdk/ReloadHandlers.js +68 -0
  52. package/src/sdk/ReloadManager.js +126 -0
  53. package/src/sdk/ServerAPI.js +133 -0
  54. package/src/sdk/ServerHandlers.js +76 -0
  55. package/src/sdk/StaticHandler.js +32 -0
  56. package/src/sdk/TickHandler.js +84 -0
  57. package/src/sdk/client.js +122 -0
  58. package/src/sdk/server.js +184 -0
  59. package/src/shared/movement.js +69 -0
  60. package/src/spatial/Octree.js +91 -0
  61. package/src/stage/Stage.js +90 -0
  62. package/src/stage/StageLoader.js +95 -0
  63. package/src/storage/FSAdapter.js +56 -0
  64. package/src/storage/StorageAdapter.js +7 -0
  65. package/src/transport/TransportWrapper.js +25 -0
  66. package/src/transport/WebSocketTransport.js +55 -0
  67. package/src/transport/WebTransportServer.js +83 -0
  68. package/src/transport/WebTransportTransport.js +94 -0
  69. package/world/kaira.glb +0 -0
  70. package/world/schwust.glb +0 -0
@@ -0,0 +1,22 @@
1
+ export default {
2
+ port: 8080,
3
+ tickRate: 128,
4
+ gravity: [0, -9.81, 0],
5
+ movement: {
6
+ maxSpeed: 8.0,
7
+ groundAccel: 10.0,
8
+ airAccel: 1.0,
9
+ friction: 7.2,
10
+ stopSpeed: 2.0,
11
+ jumpImpulse: 4.5,
12
+ collisionRestitution: 0.7,
13
+ collisionDamping: 0.5
14
+ },
15
+ entities: [
16
+ { id: 'environment', model: './world/schwust.glb', position: [0, 0, 0], app: 'environment' },
17
+ { id: 'game', position: [0, 0, 0], app: 'tps-game' },
18
+ { id: 'power-crates', position: [0, 0, 0], app: 'power-crate' }
19
+ ],
20
+ playerModel: './world/kaira.glb',
21
+ spawnPoint: [-35, 3, -65]
22
+ }
package/client/app.js ADDED
@@ -0,0 +1,181 @@
1
+ import * as THREE from 'three'
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
3
+ import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
4
+ import { createElement, applyDiff } from 'webjsx'
5
+ import { createCameraController } from './camera.js'
6
+
7
+ const scene = new THREE.Scene()
8
+ scene.background = new THREE.Color(0x87ceeb)
9
+ scene.fog = new THREE.Fog(0x87ceeb, 80, 200)
10
+ const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500)
11
+ const renderer = new THREE.WebGLRenderer({ antialias: true })
12
+ renderer.setSize(window.innerWidth, window.innerHeight)
13
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
14
+ renderer.shadowMap.enabled = true
15
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap
16
+ document.body.appendChild(renderer.domElement)
17
+
18
+ scene.add(new THREE.AmbientLight(0xffffff, 0.6))
19
+ const sun = new THREE.DirectionalLight(0xffffff, 1.0)
20
+ sun.position.set(30, 50, 20)
21
+ sun.castShadow = true
22
+ sun.shadow.mapSize.set(2048, 2048)
23
+ sun.shadow.camera.near = 0.5
24
+ sun.shadow.camera.far = 200
25
+ const sc = sun.shadow.camera
26
+ sc.left = -80; sc.right = 80; sc.top = 80; sc.bottom = -80
27
+ scene.add(sun)
28
+
29
+ const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), new THREE.MeshStandardMaterial({ color: 0x444444 }))
30
+ ground.rotation.x = -Math.PI / 2
31
+ ground.receiveShadow = true
32
+ scene.add(ground)
33
+
34
+ const gltfLoader = new GLTFLoader()
35
+ const playerMeshes = new Map()
36
+ const entityMeshes = new Map()
37
+ const appModules = new Map()
38
+ const entityAppMap = new Map()
39
+ const inputHandler = new InputHandler()
40
+ const uiRoot = document.getElementById('ui-root')
41
+ const clickPrompt = document.getElementById('click-prompt')
42
+ const cam = createCameraController(camera, scene)
43
+ cam.restore(JSON.parse(sessionStorage.getItem('cam') || 'null'))
44
+ sessionStorage.removeItem('cam')
45
+ let lastShootTime = 0
46
+ let lastFrameTime = performance.now()
47
+
48
+ function createPlayerMesh(id, isLocal) {
49
+ const group = new THREE.Group()
50
+ const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.4, 1.0, 4, 8), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00ff88 : 0xff4444 }))
51
+ body.position.y = 0.9; body.castShadow = true; group.add(body)
52
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 6), new THREE.MeshStandardMaterial({ color: isLocal ? 0x00cc66 : 0xcc3333 }))
53
+ head.position.y = 1.7; head.castShadow = true; group.add(head)
54
+ scene.add(group)
55
+ return group
56
+ }
57
+
58
+ function removePlayerMesh(id) {
59
+ const mesh = playerMeshes.get(id)
60
+ if (!mesh) return
61
+ scene.remove(mesh)
62
+ mesh.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() })
63
+ playerMeshes.delete(id)
64
+ }
65
+
66
+ function evaluateAppModule(code) {
67
+ try {
68
+ const stripped = code.replace(/^import\s+.*$/gm, '')
69
+ const wrapped = stripped.replace(/export\s+default\s+/, 'return ')
70
+ return new Function(wrapped)()
71
+ } catch (e) { console.error('[app-eval]', e.message); return null }
72
+ }
73
+
74
+ function loadEntityModel(entityId, entityState) {
75
+ if (!entityState.model) return
76
+ const url = entityState.model.startsWith('./') ? '/' + entityState.model.slice(2) : entityState.model
77
+ gltfLoader.load(url, (gltf) => {
78
+ const model = gltf.scene
79
+ model.position.set(...entityState.position)
80
+ if (entityState.rotation) model.quaternion.set(...entityState.rotation)
81
+ model.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true } })
82
+ scene.add(model)
83
+ entityMeshes.set(entityId, model)
84
+ scene.remove(ground)
85
+ }, undefined, (err) => console.error('[gltf]', entityId, err))
86
+ }
87
+
88
+ function renderAppUI(state) {
89
+ const uiFragments = []
90
+ for (const entity of state.entities) {
91
+ const appName = entityAppMap.get(entity.id)
92
+ if (!appName) continue
93
+ const appClient = appModules.get(appName)
94
+ if (!appClient?.render) continue
95
+ try {
96
+ const result = appClient.render({ entity, state: entity.custom || {}, h: createElement })
97
+ if (result?.ui) uiFragments.push({ id: entity.id, ui: result.ui })
98
+ } catch (e) { console.error('[ui]', entity.id, e.message) }
99
+ }
100
+ const local = state.players.find(p => p.id === client.playerId)
101
+ const hp = local?.health ?? 100
102
+ const hudVdom = createElement('div', { id: 'hud' },
103
+ createElement('div', { id: 'crosshair' }, '+'),
104
+ createElement('div', { id: 'health-bar' },
105
+ createElement('div', { id: 'health-fill', style: `width:${hp}%;background:${hp > 60 ? '#0f0' : hp > 30 ? '#ff0' : '#f00'}` }),
106
+ createElement('span', { id: 'health-text' }, String(hp))
107
+ ),
108
+ createElement('div', { id: 'info' }, `Players: ${state.players.length} | Tick: ${client.currentTick}`),
109
+ ...uiFragments.map(f => createElement('div', { 'data-app': f.id }, f.ui))
110
+ )
111
+ try { applyDiff(uiRoot, hudVdom) } catch (e) { console.error('[ui] diff:', e.message) }
112
+ }
113
+
114
+ const client = new PhysicsNetworkClient({
115
+ serverUrl: `ws://${window.location.host}/ws`,
116
+ onStateUpdate: (state) => {
117
+ for (const p of state.players) {
118
+ if (!playerMeshes.has(p.id)) playerMeshes.set(p.id, createPlayerMesh(p.id, p.id === client.playerId))
119
+ const mesh = playerMeshes.get(p.id)
120
+ mesh.position.set(p.position[0], p.position[1] - 1.3, p.position[2])
121
+ if (p.rotation) mesh.quaternion.set(...p.rotation)
122
+ }
123
+ renderAppUI(state)
124
+ },
125
+ onPlayerJoined: (id) => { if (!playerMeshes.has(id)) playerMeshes.set(id, createPlayerMesh(id, id === client.playerId)) },
126
+ onPlayerLeft: (id) => removePlayerMesh(id),
127
+ onEntityAdded: (id, state) => loadEntityModel(id, state),
128
+ onWorldDef: (wd) => { if (wd.entities) for (const e of wd.entities) { if (e.app) entityAppMap.set(e.id, e.app); if (e.model && !entityMeshes.has(e.id)) loadEntityModel(e.id, e) } },
129
+ onAppModule: (d) => { const a = evaluateAppModule(d.code); if (a?.client) appModules.set(d.app, a.client) },
130
+ onAssetUpdate: () => {},
131
+ onAppEvent: () => {},
132
+ onHotReload: () => { sessionStorage.setItem('cam', JSON.stringify(cam.save())); location.reload() },
133
+ debug: false
134
+ })
135
+
136
+ let inputLoopId = null
137
+ function startInputLoop() {
138
+ if (inputLoopId) return
139
+ inputLoopId = setInterval(() => {
140
+ if (!client.connected) return
141
+ const input = inputHandler.getInput()
142
+ input.yaw = cam.yaw; input.pitch = cam.pitch
143
+ client.sendInput(input)
144
+ if (input.shoot && Date.now() - lastShootTime > 100) {
145
+ lastShootTime = Date.now()
146
+ const local = client.state?.players?.find(p => p.id === client.playerId)
147
+ if (local) {
148
+ const pos = local.position
149
+ client.sendFire({ origin: [pos[0], pos[1] + 0.9, pos[2]], direction: cam.getAimDirection(pos) })
150
+ const flash = new THREE.PointLight(0xffaa00, 3, 8)
151
+ flash.position.set(pos[0], pos[1] + 0.5, pos[2])
152
+ scene.add(flash)
153
+ setTimeout(() => scene.remove(flash), 60)
154
+ }
155
+ }
156
+ }, 1000 / 60)
157
+ }
158
+
159
+ renderer.domElement.addEventListener('click', () => { if (!document.pointerLockElement) renderer.domElement.requestPointerLock() })
160
+ document.addEventListener('pointerlockchange', () => {
161
+ const locked = document.pointerLockElement === renderer.domElement
162
+ clickPrompt.style.display = locked ? 'none' : 'block'
163
+ if (locked) document.addEventListener('mousemove', cam.onMouseMove)
164
+ else document.removeEventListener('mousemove', cam.onMouseMove)
165
+ })
166
+ renderer.domElement.addEventListener('wheel', cam.onWheel, { passive: false })
167
+ window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight) })
168
+
169
+ function animate() {
170
+ requestAnimationFrame(animate)
171
+ const now = performance.now()
172
+ const frameDt = Math.min((now - lastFrameTime) / 1000, 0.1)
173
+ lastFrameTime = now
174
+ const local = client.state?.players?.find(p => p.id === client.playerId)
175
+ cam.update(local, playerMeshes.get(client.playerId), frameDt)
176
+ renderer.render(scene, camera)
177
+ }
178
+ animate()
179
+
180
+ client.connect().then(() => { console.log('Connected'); startInputLoop() }).catch(err => console.error('Connection failed:', err))
181
+ window.debug = { scene, camera, renderer, client, playerMeshes, entityMeshes, appModules, inputHandler }
@@ -0,0 +1,116 @@
1
+ import * as THREE from 'three'
2
+
3
+ const camTarget = new THREE.Vector3()
4
+ const camRaycaster = new THREE.Raycaster()
5
+ const camDir = new THREE.Vector3()
6
+ const camDesired = new THREE.Vector3()
7
+ const camLookTarget = new THREE.Vector3()
8
+ const aimRaycaster = new THREE.Raycaster()
9
+ const aimDir = new THREE.Vector3()
10
+ const shoulderOffset = 0.35
11
+ const headHeight = 0.4
12
+ const camFollowSpeed = 12.0
13
+ const camSnapSpeed = 30.0
14
+ const zoomStages = [0, 1.5, 3, 5, 8]
15
+
16
+ export function createCameraController(camera, scene) {
17
+ let yaw = 0, pitch = 0, zoomIndex = 2, camInitialized = false
18
+
19
+ function restore(saved) {
20
+ if (saved) { yaw = saved.yaw || 0; pitch = saved.pitch || 0; zoomIndex = saved.zoomIndex ?? 2 }
21
+ }
22
+
23
+ function save() { return { yaw, pitch, zoomIndex } }
24
+
25
+ function onMouseMove(e) {
26
+ yaw -= e.movementX * 0.002
27
+ pitch -= e.movementY * 0.002
28
+ pitch = Math.max(-1.4, Math.min(1.4, pitch))
29
+ }
30
+
31
+ function onWheel(e) {
32
+ if (e.deltaY > 0) zoomIndex = Math.min(zoomIndex + 1, zoomStages.length - 1)
33
+ else zoomIndex = Math.max(zoomIndex - 1, 0)
34
+ e.preventDefault()
35
+ }
36
+
37
+ function getAimDirection(playerPos) {
38
+ const sy = Math.sin(yaw), cy = Math.cos(yaw)
39
+ const sp = Math.sin(pitch), cp = Math.cos(pitch)
40
+ const fwdX = sy * cp, fwdY = sp, fwdZ = cy * cp
41
+ if (!playerPos || zoomStages[zoomIndex] < 0.01) return [fwdX, fwdY, fwdZ]
42
+ const dist = zoomStages[zoomIndex]
43
+ const rightX = -cy, rightZ = sy
44
+ const cpx = playerPos[0] - fwdX * dist + rightX * shoulderOffset
45
+ const cpy = playerPos[1] + headHeight - fwdY * dist + 0.2
46
+ const cpz = playerPos[2] - fwdZ * dist + rightZ * shoulderOffset
47
+ const tx = cpx + fwdX * 200, ty = cpy + fwdY * 200, tz = cpz + fwdZ * 200
48
+ const ox = playerPos[0], oy = playerPos[1] + 0.9, oz = playerPos[2]
49
+ const dx = tx - ox, dy = ty - oy, dz = tz - oz
50
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz)
51
+ return len > 0.001 ? [dx / len, dy / len, dz / len] : [fwdX, fwdY, fwdZ]
52
+ }
53
+
54
+ function update(localPlayer, localMesh, frameDt) {
55
+ if (!localPlayer) return
56
+ const dist = zoomStages[zoomIndex]
57
+ camTarget.set(localPlayer.position[0], localPlayer.position[1] + headHeight, localPlayer.position[2])
58
+ if (localMesh) localMesh.visible = dist > 0.5
59
+ const sy = Math.sin(yaw), cy = Math.cos(yaw)
60
+ const sp = Math.sin(pitch), cp = Math.cos(pitch)
61
+ const fwdX = sy * cp, fwdY = sp, fwdZ = cy * cp
62
+ const rightX = -cy, rightZ = sy
63
+ if (dist < 0.01) {
64
+ camera.position.copy(camTarget)
65
+ camera.lookAt(camTarget.x + fwdX, camTarget.y + fwdY, camTarget.z + fwdZ)
66
+ } else {
67
+ camDesired.set(
68
+ camTarget.x - fwdX * dist + rightX * shoulderOffset,
69
+ camTarget.y - fwdY * dist + 0.2,
70
+ camTarget.z - fwdZ * dist + rightZ * shoulderOffset
71
+ )
72
+ camDir.subVectors(camDesired, camTarget).normalize()
73
+ const fullDist = camTarget.distanceTo(camDesired)
74
+ camRaycaster.set(camTarget, camDir)
75
+ camRaycaster.far = fullDist
76
+ camRaycaster.near = 0
77
+ const hits = camRaycaster.intersectObjects(scene.children, true)
78
+ let clippedDist = fullDist
79
+ for (const hit of hits) {
80
+ if (hit.object === localMesh || localMesh?.children?.includes(hit.object)) continue
81
+ if (hit.distance < clippedDist) clippedDist = hit.distance - 0.2
82
+ }
83
+ if (clippedDist < 0.3) clippedDist = 0.3
84
+ camDesired.set(
85
+ camTarget.x + camDir.x * clippedDist,
86
+ camTarget.y + camDir.y * clippedDist,
87
+ camTarget.z + camDir.z * clippedDist
88
+ )
89
+ if (!camInitialized) { camera.position.copy(camDesired); camInitialized = true }
90
+ else {
91
+ const closer = clippedDist < camera.position.distanceTo(camTarget)
92
+ const speed = closer ? camSnapSpeed : camFollowSpeed
93
+ camera.position.lerp(camDesired, 1.0 - Math.exp(-speed * frameDt))
94
+ }
95
+ aimDir.set(fwdX, fwdY, fwdZ)
96
+ aimRaycaster.set(camera.position, aimDir)
97
+ aimRaycaster.far = 500
98
+ aimRaycaster.near = 0.5
99
+ const aimHits = aimRaycaster.intersectObjects(scene.children, true)
100
+ let aimPoint = null
101
+ for (const ah of aimHits) {
102
+ if (ah.object === localMesh || localMesh?.children?.includes(ah.object)) continue
103
+ aimPoint = ah.point; break
104
+ }
105
+ if (aimPoint) {
106
+ if (!camLookTarget.lengthSq()) camLookTarget.copy(aimPoint)
107
+ camLookTarget.lerp(aimPoint, 1.0 - Math.exp(-camFollowSpeed * frameDt))
108
+ } else {
109
+ camLookTarget.set(camera.position.x + fwdX * 200, camera.position.y + fwdY * 200, camera.position.z + fwdZ * 200)
110
+ }
111
+ camera.lookAt(camLookTarget)
112
+ }
113
+ }
114
+
115
+ return { restore, save, onMouseMove, onWheel, getAimDirection, update, get yaw() { return yaw }, get pitch() { return pitch } }
116
+ }
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Hyperfy</title>
7
+ <link rel="stylesheet" href="/style.css">
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "three": "https://esm.sh/three@0.171.0",
12
+ "three/addons/": "https://esm.sh/three@0.171.0/examples/jsm/",
13
+ "webjsx": "/node_modules/webjsx/dist/index.js",
14
+ "webjsx/jsx-runtime": "/node_modules/webjsx/dist/jsx-runtime.js"
15
+ }
16
+ }
17
+ </script>
18
+ </head>
19
+ <body>
20
+ <div id="ui-root"></div>
21
+ <div id="click-prompt">Click to play</div>
22
+ <script type="module" src="/app.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,11 @@
1
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ body { background: #000; color: #fff; overflow: hidden; font-family: monospace; }
3
+ canvas { display: block; width: 100%; height: 100vh; }
4
+ #ui-root { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
5
+ #hud { width: 100%; height: 100%; }
6
+ #crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; color: rgba(255,255,255,0.8); text-shadow: 0 0 2px #000; }
7
+ #health-bar { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 200px; height: 8px; background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.3); }
8
+ #health-fill { height: 100%; width: 100%; background: #0f0; transition: width 0.2s, background-color 0.2s; }
9
+ #health-text { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-size: 14px; color: #fff; text-shadow: 0 0 4px #000; }
10
+ #info { position: absolute; top: 10px; left: 10px; font-size: 13px; line-height: 1.6; color: #0f0; text-shadow: 0 0 2px #000; }
11
+ #click-prompt { position: absolute; top: 50%; left: 50%; transform: translate(-50%, 30px); font-size: 18px; color: rgba(255,255,255,0.7); text-shadow: 0 0 4px #000; z-index: 20; }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@lanmower/entrypoint",
3
+ "version": "0.16.0",
4
+ "description": "Physics and netcode SDK for multiplayer game servers",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "entrypoint": "./server.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node server.js"
12
+ },
13
+ "files": [
14
+ "src/",
15
+ "apps/",
16
+ "world/",
17
+ "client/",
18
+ "server.js",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "keywords": [
23
+ "gamedev",
24
+ "multiplayer",
25
+ "physics",
26
+ "netcode",
27
+ "websocket",
28
+ "jolt-physics",
29
+ "game-server"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/AnEntrypoint/entrypoint.git"
34
+ },
35
+ "license": "MIT",
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "d3-octree": "^1.1.0",
41
+ "jolt-physics": "^0.29.0",
42
+ "webjsx": "^0.0.73",
43
+ "ws": "^8.18.0"
44
+ },
45
+ "optionalDependencies": {
46
+ "@fails-components/webtransport": "^1.5.3",
47
+ "@fails-components/webtransport-transport-http3-quiche": "^1.5.3"
48
+ },
49
+ "devDependencies": {
50
+ "playwright": "^1.58.1"
51
+ }
52
+ }
package/server.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { boot } from './src/sdk/server.js'
3
+ await boot()
@@ -0,0 +1,172 @@
1
+ import { CliDebugger } from '../debug/CliDebugger.js'
2
+
3
+ export class AppContext {
4
+ constructor(entity, runtime) {
5
+ this._entity = entity
6
+ this._runtime = runtime
7
+ this._state = entity._appState || {}
8
+ entity._appState = this._state
9
+ this._entityProxy = this._buildEntityProxy()
10
+ this._debugger = new CliDebugger(`[${entity.id}]`)
11
+ this._busScope = runtime._eventBus ? runtime._eventBus.scope(entity.id) : null
12
+ }
13
+
14
+ _buildEntityProxy() {
15
+ const ent = this._entity
16
+ const runtime = this._runtime
17
+ return {
18
+ get id() { return ent.id },
19
+ get model() { return ent.model },
20
+ get position() { return ent.position },
21
+ set position(v) { ent.position = v },
22
+ get rotation() { return ent.rotation },
23
+ set rotation(v) { ent.rotation = v },
24
+ get scale() { return ent.scale },
25
+ set scale(v) { ent.scale = v },
26
+ get velocity() { return ent.velocity },
27
+ set velocity(v) { ent.velocity = v },
28
+ get custom() { return ent.custom },
29
+ set custom(v) { ent.custom = v },
30
+ get parent() { return ent.parent },
31
+ get children() { return [...ent.children] },
32
+ get worldTransform() { return runtime.getWorldTransform(ent.id) },
33
+ destroy: () => runtime.destroyEntity(ent.id)
34
+ }
35
+ }
36
+
37
+ get entity() { return this._entityProxy }
38
+
39
+ get physics() {
40
+ const ent = this._entity
41
+ const runtime = this._runtime
42
+ return {
43
+ setStatic: (v) => { ent.bodyType = v ? 'static' : ent.bodyType },
44
+ setDynamic: (v) => { ent.bodyType = v ? 'dynamic' : ent.bodyType },
45
+ setKinematic: (v) => { ent.bodyType = v ? 'kinematic' : ent.bodyType },
46
+ setMass: (v) => { ent.mass = v },
47
+ addBoxCollider: (s) => {
48
+ ent.collider = { type: 'box', size: s }
49
+ if (runtime._physics) {
50
+ const he = Array.isArray(s) ? s : [s, s, s]
51
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
52
+ ent._physicsBodyId = runtime._physics.addBody('box', he, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
53
+ }
54
+ },
55
+ addSphereCollider: (r) => {
56
+ ent.collider = { type: 'sphere', radius: r }
57
+ if (runtime._physics) {
58
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
59
+ ent._physicsBodyId = runtime._physics.addBody('sphere', r, ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
60
+ }
61
+ },
62
+ addCapsuleCollider: (r, h) => {
63
+ ent.collider = { type: 'capsule', radius: r, height: h }
64
+ if (runtime._physics) {
65
+ const mt = ent.bodyType === 'dynamic' ? 'dynamic' : ent.bodyType === 'kinematic' ? 'kinematic' : 'static'
66
+ ent._physicsBodyId = runtime._physics.addBody('capsule', [r, h / 2], ent.position, mt, { rotation: ent.rotation, mass: ent.mass })
67
+ }
68
+ },
69
+ addMeshCollider: (m) => { ent.collider = { type: 'mesh', mesh: m } },
70
+ addTrimeshCollider: () => {
71
+ ent.collider = { type: 'trimesh', model: ent.model }
72
+ if (runtime._physics && ent.model) {
73
+ const bodyId = runtime._physics.addStaticTrimesh(ent.model, 0)
74
+ ent._physicsBodyId = bodyId
75
+ }
76
+ },
77
+ addForce: (f) => {
78
+ const mass = ent.mass || 1
79
+ ent.velocity[0] += f[0] / mass
80
+ ent.velocity[1] += f[1] / mass
81
+ ent.velocity[2] += f[2] / mass
82
+ },
83
+ setVelocity: (v) => { ent.velocity = [...v] }
84
+ }
85
+ }
86
+
87
+ get world() {
88
+ const runtime = this._runtime
89
+ return {
90
+ spawn: (id, cfg) => runtime.spawnEntity(id, cfg),
91
+ destroy: (id) => runtime.destroyEntity(id),
92
+ attach: (eid, app) => runtime.attachApp(eid, app),
93
+ detach: (eid) => runtime.detachApp(eid),
94
+ reparent: (eid, parentId) => runtime.reparent(eid, parentId),
95
+ query: (filter) => runtime.queryEntities(filter),
96
+ getEntity: (id) => runtime.getEntity(id),
97
+ nearby: (pos, radius) => runtime.nearbyEntities(pos, radius),
98
+ get gravity() { return runtime.gravity }
99
+ }
100
+ }
101
+
102
+ get players() {
103
+ const runtime = this._runtime
104
+ return {
105
+ getAll: () => runtime.getPlayers(),
106
+ getNearest: (pos, r) => runtime.getNearestPlayer(pos, r),
107
+ send: (pid, msg) => runtime.sendToPlayer(pid, msg),
108
+ broadcast: (msg) => runtime.broadcastToPlayers(msg),
109
+ setPosition: (pid, pos) => runtime.setPlayerPosition(pid, pos)
110
+ }
111
+ }
112
+
113
+ get time() {
114
+ const runtime = this._runtime
115
+ const entityId = this._entity.id
116
+ return {
117
+ get tick() { return runtime.currentTick },
118
+ get deltaTime() { return runtime.deltaTime },
119
+ get elapsed() { return runtime.elapsed },
120
+ after: (seconds, fn) => runtime.addTimer(entityId, seconds, fn, false),
121
+ every: (seconds, fn) => runtime.addTimer(entityId, seconds, fn, true)
122
+ }
123
+ }
124
+
125
+ get config() { return this._entity._config || {} }
126
+
127
+ get state() { return this._state }
128
+ set state(v) { Object.assign(this._state, v) }
129
+
130
+ get network() {
131
+ const runtime = this._runtime
132
+ return {
133
+ broadcast: (msg) => runtime.broadcastToPlayers(msg),
134
+ sendTo: (id, msg) => runtime.sendToPlayer(id, msg)
135
+ }
136
+ }
137
+
138
+ get bus() { return this._busScope }
139
+
140
+ get storage() {
141
+ const runtime = this._runtime
142
+ const entity = this._entity
143
+ const ns = entity._appName || entity.id
144
+ if (!runtime._storage) return null
145
+ const adapter = runtime._storage
146
+ return {
147
+ get: (key) => adapter.get(`${ns}/${key}`),
148
+ set: (key, value) => adapter.set(`${ns}/${key}`, value),
149
+ delete: (key) => adapter.delete(`${ns}/${key}`),
150
+ list: (prefix = '') => adapter.list(`${ns}/${prefix}`),
151
+ has: (key) => adapter.has(`${ns}/${key}`)
152
+ }
153
+ }
154
+
155
+ get debug() { return this._debugger }
156
+
157
+ get collider() {
158
+ const ent = this._entity
159
+ return {
160
+ box: (hx, hy, hz) => { ent.collider = { type: 'box', halfExtents: [hx, hy, hz] } },
161
+ capsule: (r, h) => { ent.collider = { type: 'capsule', radius: r, halfHeight: h } },
162
+ sphere: (r) => { ent.collider = { type: 'sphere', radius: r } }
163
+ }
164
+ }
165
+
166
+ raycast(origin, direction, maxDistance = 1000) {
167
+ if (this._runtime._physics) {
168
+ return this._runtime._physics.raycast(origin, direction, maxDistance)
169
+ }
170
+ return { hit: false, distance: maxDistance, body: null, position: null }
171
+ }
172
+ }