@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.
- package/LICENSE +674 -0
- package/README.md +250 -0
- package/apps/environment/index.js +17 -0
- package/apps/interactive-door/index.js +33 -0
- package/apps/patrol-npc/index.js +37 -0
- package/apps/physics-crate/index.js +23 -0
- package/apps/power-crate/index.js +169 -0
- package/apps/tps-game/index.js +168 -0
- package/apps/tps-game/schwust.glb +0 -0
- package/apps/world/index.js +22 -0
- package/client/app.js +181 -0
- package/client/camera.js +116 -0
- package/client/index.html +24 -0
- package/client/style.css +11 -0
- package/package.json +52 -0
- package/server.js +3 -0
- package/src/apps/AppContext.js +172 -0
- package/src/apps/AppLoader.js +160 -0
- package/src/apps/AppRuntime.js +192 -0
- package/src/apps/EventBus.js +62 -0
- package/src/apps/HotReloadQueue.js +63 -0
- package/src/client/InputHandler.js +85 -0
- package/src/client/PhysicsNetworkClient.js +171 -0
- package/src/client/PredictionEngine.js +123 -0
- package/src/client/ReconciliationEngine.js +54 -0
- package/src/connection/ConnectionManager.js +133 -0
- package/src/connection/QualityMonitor.js +46 -0
- package/src/connection/SessionStore.js +67 -0
- package/src/debug/CliDebugger.js +93 -0
- package/src/debug/Inspector.js +52 -0
- package/src/debug/StateInspector.js +42 -0
- package/src/index.client.js +8 -0
- package/src/index.js +1 -0
- package/src/index.server.js +27 -0
- package/src/math.js +21 -0
- package/src/netcode/EventLog.js +74 -0
- package/src/netcode/LagCompensator.js +78 -0
- package/src/netcode/NetworkState.js +66 -0
- package/src/netcode/PhysicsIntegration.js +132 -0
- package/src/netcode/PlayerManager.js +109 -0
- package/src/netcode/SnapshotEncoder.js +49 -0
- package/src/netcode/TickSystem.js +66 -0
- package/src/physics/GLBLoader.js +29 -0
- package/src/physics/World.js +195 -0
- package/src/protocol/Codec.js +60 -0
- package/src/protocol/EventEmitter.js +60 -0
- package/src/protocol/MessageTypes.js +73 -0
- package/src/protocol/SequenceTracker.js +71 -0
- package/src/protocol/msgpack.js +119 -0
- package/src/sdk/ClientMessageHandler.js +80 -0
- package/src/sdk/ReloadHandlers.js +68 -0
- package/src/sdk/ReloadManager.js +126 -0
- package/src/sdk/ServerAPI.js +133 -0
- package/src/sdk/ServerHandlers.js +76 -0
- package/src/sdk/StaticHandler.js +32 -0
- package/src/sdk/TickHandler.js +84 -0
- package/src/sdk/client.js +122 -0
- package/src/sdk/server.js +184 -0
- package/src/shared/movement.js +69 -0
- package/src/spatial/Octree.js +91 -0
- package/src/stage/Stage.js +90 -0
- package/src/stage/StageLoader.js +95 -0
- package/src/storage/FSAdapter.js +56 -0
- package/src/storage/StorageAdapter.js +7 -0
- package/src/transport/TransportWrapper.js +25 -0
- package/src/transport/WebSocketTransport.js +55 -0
- package/src/transport/WebTransportServer.js +83 -0
- package/src/transport/WebTransportTransport.js +94 -0
- package/world/kaira.glb +0 -0
- 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 }
|
package/client/camera.js
ADDED
|
@@ -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>
|
package/client/style.css
ADDED
|
@@ -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,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
|
+
}
|