@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,122 @@
|
|
|
1
|
+
import { MSG } from '../protocol/MessageTypes.js'
|
|
2
|
+
import { pack } from '../protocol/msgpack.js'
|
|
3
|
+
import { QualityMonitor } from '../connection/QualityMonitor.js'
|
|
4
|
+
import { StateInspector } from '../debug/StateInspector.js'
|
|
5
|
+
import { createMessageRouter } from './ClientMessageHandler.js'
|
|
6
|
+
import { EventEmitter } from '../protocol/EventEmitter.js'
|
|
7
|
+
|
|
8
|
+
export function createClient(config = {}) {
|
|
9
|
+
const serverUrl = config.serverUrl || 'ws://localhost:8080'
|
|
10
|
+
const emitter = new EventEmitter()
|
|
11
|
+
const quality = new QualityMonitor()
|
|
12
|
+
const stateInspector = new StateInspector()
|
|
13
|
+
|
|
14
|
+
const state = {
|
|
15
|
+
ws: null, transportType: 'websocket',
|
|
16
|
+
playerId: null, sessionToken: null, tick: 0,
|
|
17
|
+
entities: new Map(), players: new Map(),
|
|
18
|
+
connected: false, reconnecting: false, reconnectAttempts: 0,
|
|
19
|
+
reconnectTimer: null, heartbeatTimer: null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _sendRaw(data) {
|
|
23
|
+
if (state.ws && state.ws.readyState === 1) { state.ws.send(data); return true }
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function send(type, payload) {
|
|
28
|
+
const frame = pack({ type, payload })
|
|
29
|
+
quality.recordBytesOut(frame.length)
|
|
30
|
+
return _sendRaw(frame)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const onMessage = createMessageRouter({
|
|
34
|
+
emitter, quality, stateInspector, send,
|
|
35
|
+
getState: (k) => state[k],
|
|
36
|
+
setState: (k, v) => { state[k] = v }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
function _startHeartbeats() {
|
|
40
|
+
state.heartbeatTimer = setInterval(() => {
|
|
41
|
+
quality.recordHeartbeatSent()
|
|
42
|
+
send(MSG.HEARTBEAT, { ts: Date.now() })
|
|
43
|
+
}, 1000)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _attemptReconnect() {
|
|
47
|
+
if (state.reconnectAttempts >= 10) { emitter.emit('reconnectFailed'); return }
|
|
48
|
+
state.reconnecting = true
|
|
49
|
+
const delay = Math.min(100 * Math.pow(2, state.reconnectAttempts), 5000)
|
|
50
|
+
state.reconnectAttempts++
|
|
51
|
+
state.reconnectTimer = setTimeout(() => {
|
|
52
|
+
_connect().then(() => {
|
|
53
|
+
if (state.sessionToken) send(MSG.RECONNECT, { sessionToken: state.sessionToken })
|
|
54
|
+
}).catch(() => _attemptReconnect())
|
|
55
|
+
}, delay)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _connect() {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
try {
|
|
61
|
+
const WS = config.WebSocket || (typeof WebSocket !== 'undefined' ? WebSocket : null)
|
|
62
|
+
if (!WS) return reject(new Error('No WebSocket available'))
|
|
63
|
+
state.ws = new WS(serverUrl)
|
|
64
|
+
state.transportType = 'websocket'
|
|
65
|
+
if (state.ws.binaryType !== undefined) state.ws.binaryType = 'arraybuffer'
|
|
66
|
+
const onOpen = () => { state.connected = true; _startHeartbeats(); resolve() }
|
|
67
|
+
const onClose = () => {
|
|
68
|
+
state.connected = false
|
|
69
|
+
if (state.heartbeatTimer) clearInterval(state.heartbeatTimer)
|
|
70
|
+
emitter.emit('disconnect')
|
|
71
|
+
if (!state.reconnecting && state.sessionToken) _attemptReconnect()
|
|
72
|
+
}
|
|
73
|
+
const onErr = (e) => reject(e)
|
|
74
|
+
if (state.ws.on) {
|
|
75
|
+
state.ws.on('open', onOpen); state.ws.on('message', onMessage)
|
|
76
|
+
state.ws.on('close', onClose); state.ws.on('error', onErr)
|
|
77
|
+
} else {
|
|
78
|
+
state.ws.onopen = onOpen; state.ws.onmessage = (e) => onMessage(e.data)
|
|
79
|
+
state.ws.onclose = onClose; state.ws.onerror = onErr
|
|
80
|
+
}
|
|
81
|
+
} catch (e) { reject(e) }
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const api = {
|
|
86
|
+
get playerId() { return state.playerId },
|
|
87
|
+
get tick() { return state.tick },
|
|
88
|
+
get entities() { return state.entities },
|
|
89
|
+
get players() { return state.players },
|
|
90
|
+
get isConnected() { return state.connected },
|
|
91
|
+
get sessionToken() { return state.sessionToken },
|
|
92
|
+
get transportType() { return state.transportType },
|
|
93
|
+
quality, stateInspector,
|
|
94
|
+
on: emitter.on.bind(emitter),
|
|
95
|
+
off: emitter.off.bind(emitter),
|
|
96
|
+
connect() { return _connect() },
|
|
97
|
+
connectNode(WS) { config.WebSocket = WS; return _connect() },
|
|
98
|
+
sendInput(input) { send(MSG.INPUT, { input }) },
|
|
99
|
+
interact(entityId) { send(MSG.APP_EVENT, { entityId }) },
|
|
100
|
+
disconnect() {
|
|
101
|
+
state.reconnecting = true
|
|
102
|
+
if (state.reconnectTimer) clearTimeout(state.reconnectTimer)
|
|
103
|
+
if (state.heartbeatTimer) clearInterval(state.heartbeatTimer)
|
|
104
|
+
state.connected = false
|
|
105
|
+
if (state.ws) state.ws.close()
|
|
106
|
+
},
|
|
107
|
+
getStats() {
|
|
108
|
+
return {
|
|
109
|
+
quality: quality.getStats(), stateSync: stateInspector.getStats(),
|
|
110
|
+
transport: state.transportType
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
getEntity(id) { return state.entities.get(id) || null },
|
|
114
|
+
getPlayer(id) { return state.players.get(id) || null }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof globalThis !== 'undefined') {
|
|
118
|
+
if (!globalThis.__DEBUG__) globalThis.__DEBUG__ = {}
|
|
119
|
+
globalThis.__DEBUG__.client = api
|
|
120
|
+
}
|
|
121
|
+
return api
|
|
122
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { join, dirname, resolve } from 'node:path'
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
|
+
import { MSG } from '../protocol/MessageTypes.js'
|
|
4
|
+
import { ConnectionManager } from '../connection/ConnectionManager.js'
|
|
5
|
+
import { SessionStore } from '../connection/SessionStore.js'
|
|
6
|
+
import { Inspector } from '../debug/Inspector.js'
|
|
7
|
+
import { TickSystem } from '../netcode/TickSystem.js'
|
|
8
|
+
import { PlayerManager } from '../netcode/PlayerManager.js'
|
|
9
|
+
import { NetworkState } from '../netcode/NetworkState.js'
|
|
10
|
+
import { LagCompensator } from '../netcode/LagCompensator.js'
|
|
11
|
+
import { PhysicsIntegration } from '../netcode/PhysicsIntegration.js'
|
|
12
|
+
import { PhysicsWorld } from '../physics/World.js'
|
|
13
|
+
import { AppRuntime } from '../apps/AppRuntime.js'
|
|
14
|
+
import { AppLoader } from '../apps/AppLoader.js'
|
|
15
|
+
import { StageLoader } from '../stage/StageLoader.js'
|
|
16
|
+
import { createTickHandler } from './TickHandler.js'
|
|
17
|
+
import { EventEmitter } from '../protocol/EventEmitter.js'
|
|
18
|
+
import { EventBus } from '../apps/EventBus.js'
|
|
19
|
+
import { EventLog } from '../netcode/EventLog.js'
|
|
20
|
+
import { FSAdapter } from '../storage/FSAdapter.js'
|
|
21
|
+
import { ReloadManager } from './ReloadManager.js'
|
|
22
|
+
import { createReloadHandlers } from './ReloadHandlers.js'
|
|
23
|
+
import { createServerAPI } from './ServerAPI.js'
|
|
24
|
+
import { createConnectionHandlers } from './ServerHandlers.js'
|
|
25
|
+
|
|
26
|
+
export async function boot(overrides = {}) {
|
|
27
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
28
|
+
const worldPath = resolve(ROOT, 'apps/world/index.js')
|
|
29
|
+
const worldUrl = pathToFileURL(worldPath).href + `?t=${Date.now()}`
|
|
30
|
+
const worldMod = await import(worldUrl)
|
|
31
|
+
const worldDef = worldMod.default || worldMod
|
|
32
|
+
const config = {
|
|
33
|
+
port: parseInt(process.env.PORT || String(worldDef.port || 8080), 10),
|
|
34
|
+
tickRate: worldDef.tickRate || 128,
|
|
35
|
+
appsDir: join(ROOT, 'apps'),
|
|
36
|
+
gravity: worldDef.gravity,
|
|
37
|
+
movement: worldDef.movement,
|
|
38
|
+
staticDirs: [
|
|
39
|
+
{ prefix: '/src/', dir: join(ROOT, 'src') },
|
|
40
|
+
{ prefix: '/world/', dir: join(ROOT, 'world') },
|
|
41
|
+
{ prefix: '/node_modules/', dir: join(ROOT, 'node_modules') },
|
|
42
|
+
{ prefix: '/', dir: join(ROOT, 'client') }
|
|
43
|
+
],
|
|
44
|
+
...overrides
|
|
45
|
+
}
|
|
46
|
+
const server = await createServer(config)
|
|
47
|
+
await server.loadWorld(worldDef)
|
|
48
|
+
server.on('playerJoin', ({ id }) => console.log())
|
|
49
|
+
server.on('playerLeave', ({ id }) => console.log())
|
|
50
|
+
const info = await server.start()
|
|
51
|
+
console.log()
|
|
52
|
+
return server
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function createServer(config = {}) {
|
|
56
|
+
const port = config.port || 8080
|
|
57
|
+
const tickRate = config.tickRate || 128
|
|
58
|
+
const appsDir = config.appsDir || './apps'
|
|
59
|
+
const gravity = config.gravity || [0, -9.81, 0]
|
|
60
|
+
const movement = config.movement || {}
|
|
61
|
+
const staticDirs = config.staticDirs || []
|
|
62
|
+
|
|
63
|
+
const storageDir = config.storageDir || './data'
|
|
64
|
+
const physics = new PhysicsWorld({ gravity })
|
|
65
|
+
await physics.init()
|
|
66
|
+
|
|
67
|
+
const emitter = new EventEmitter()
|
|
68
|
+
const eventBus = new EventBus()
|
|
69
|
+
const eventLog = new EventLog()
|
|
70
|
+
const storage = new FSAdapter(storageDir)
|
|
71
|
+
const tickSystem = new TickSystem(tickRate)
|
|
72
|
+
const playerManager = new PlayerManager()
|
|
73
|
+
const networkState = new NetworkState()
|
|
74
|
+
const lagCompensator = new LagCompensator()
|
|
75
|
+
const physicsIntegration = new PhysicsIntegration({ gravity, physicsWorld: physics })
|
|
76
|
+
const connections = new ConnectionManager({
|
|
77
|
+
heartbeatInterval: config.heartbeatInterval || 1000,
|
|
78
|
+
heartbeatTimeout: config.heartbeatTimeout || 3000
|
|
79
|
+
})
|
|
80
|
+
const sessions = new SessionStore({ ttl: config.sessionTTL || 30000 })
|
|
81
|
+
const inspector = new Inspector()
|
|
82
|
+
const reloadManager = new ReloadManager()
|
|
83
|
+
|
|
84
|
+
const appRuntime = new AppRuntime({ gravity, playerManager, physics, physicsIntegration, connections, eventBus, eventLog, storage })
|
|
85
|
+
appRuntime.setPlayerManager(playerManager)
|
|
86
|
+
const appLoader = new AppLoader(appRuntime, { dir: appsDir })
|
|
87
|
+
const stageLoader = new StageLoader(appRuntime)
|
|
88
|
+
appRuntime.setStageLoader(stageLoader)
|
|
89
|
+
|
|
90
|
+
appLoader._onReloadCallback = (name, code) => {
|
|
91
|
+
connections.broadcast(MSG.APP_MODULE, { app: name, code })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ctx = {
|
|
95
|
+
config,
|
|
96
|
+
port,
|
|
97
|
+
tickRate,
|
|
98
|
+
appsDir,
|
|
99
|
+
gravity,
|
|
100
|
+
movement,
|
|
101
|
+
staticDirs,
|
|
102
|
+
physics,
|
|
103
|
+
emitter,
|
|
104
|
+
tickSystem,
|
|
105
|
+
playerManager,
|
|
106
|
+
networkState,
|
|
107
|
+
lagCompensator,
|
|
108
|
+
physicsIntegration,
|
|
109
|
+
connections,
|
|
110
|
+
sessions,
|
|
111
|
+
inspector,
|
|
112
|
+
reloadManager,
|
|
113
|
+
eventBus,
|
|
114
|
+
eventLog,
|
|
115
|
+
storage,
|
|
116
|
+
appRuntime,
|
|
117
|
+
appLoader,
|
|
118
|
+
stageLoader,
|
|
119
|
+
currentWorldDef: null,
|
|
120
|
+
worldSpawnPoint: [0, 5, 0],
|
|
121
|
+
snapshotSeq: 0,
|
|
122
|
+
httpServer: null,
|
|
123
|
+
wss: null,
|
|
124
|
+
wtServer: null,
|
|
125
|
+
handlerState: { fn: null },
|
|
126
|
+
onTick: (tick, dt) => { if (ctx.handlerState.fn) ctx.handlerState.fn(tick, dt) },
|
|
127
|
+
setTickHandler: (fn) => { ctx.handlerState.fn = fn }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const reloadHandlers = createReloadHandlers({
|
|
131
|
+
networkState,
|
|
132
|
+
playerManager,
|
|
133
|
+
physicsIntegration,
|
|
134
|
+
lagCompensator,
|
|
135
|
+
physics,
|
|
136
|
+
appRuntime,
|
|
137
|
+
connections
|
|
138
|
+
})
|
|
139
|
+
ctx.reloadHandlers = reloadHandlers
|
|
140
|
+
|
|
141
|
+
ctx.setTickHandler(createTickHandler({
|
|
142
|
+
networkState,
|
|
143
|
+
playerManager,
|
|
144
|
+
physicsIntegration,
|
|
145
|
+
lagCompensator,
|
|
146
|
+
physics,
|
|
147
|
+
appRuntime,
|
|
148
|
+
connections,
|
|
149
|
+
movement,
|
|
150
|
+
stageLoader,
|
|
151
|
+
eventLog
|
|
152
|
+
}))
|
|
153
|
+
|
|
154
|
+
const { onClientConnect } = createConnectionHandlers(ctx)
|
|
155
|
+
ctx.onClientConnect = onClientConnect
|
|
156
|
+
|
|
157
|
+
ctx.setupSDKWatchers = () => {
|
|
158
|
+
const reloadTick = async () => { ctx.setTickHandler(await reloadHandlers.reloadTickHandler()) }
|
|
159
|
+
const w = [
|
|
160
|
+
['tick-handler', './src/sdk/TickHandler.js', reloadTick],
|
|
161
|
+
['physics-integration', './src/netcode/PhysicsIntegration.js', reloadHandlers.reloadPhysicsIntegration],
|
|
162
|
+
['lag-compensator', './src/netcode/LagCompensator.js', reloadHandlers.reloadLagCompensator],
|
|
163
|
+
['player-manager', './src/netcode/PlayerManager.js', reloadHandlers.reloadPlayerManager],
|
|
164
|
+
['network-state', './src/netcode/NetworkState.js', reloadHandlers.reloadNetworkState]
|
|
165
|
+
]
|
|
166
|
+
for (const [id, path, reload] of w) reloadManager.addWatcher(id, path, reload)
|
|
167
|
+
const clientReload = () => { connections.broadcast(MSG.HOT_RELOAD, { timestamp: Date.now() }) }
|
|
168
|
+
const clientFiles = [
|
|
169
|
+
['client-app', './client/app.js'],
|
|
170
|
+
['client-camera', './client/camera.js'],
|
|
171
|
+
['client-input', './src/client/InputHandler.js'],
|
|
172
|
+
['client-network', './src/client/PhysicsNetworkClient.js'],
|
|
173
|
+
['client-prediction', './src/client/PredictionEngine.js'],
|
|
174
|
+
['client-reconciliation', './src/client/ReconciliationEngine.js'],
|
|
175
|
+
['client-index', './src/index.client.js']
|
|
176
|
+
]
|
|
177
|
+
for (const [id, path] of clientFiles) reloadManager.addWatcher(id, path, clientReload)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const api = createServerAPI(ctx)
|
|
181
|
+
if (typeof globalThis.__DEBUG__ === 'undefined') globalThis.__DEBUG__ = {}
|
|
182
|
+
globalThis.__DEBUG__.server = api
|
|
183
|
+
return api
|
|
184
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function applyMovement(state, input, movement, dt) {
|
|
2
|
+
const { maxSpeed, groundAccel, airAccel, friction, stopSpeed, jumpImpulse } = movement
|
|
3
|
+
let vx = state.velocity[0], vz = state.velocity[2]
|
|
4
|
+
let wishX = 0, wishZ = 0, wishSpeed = 0, jumped = false
|
|
5
|
+
|
|
6
|
+
if (input) {
|
|
7
|
+
let fx = 0, fz = 0
|
|
8
|
+
if (input.forward) fz += 1
|
|
9
|
+
if (input.backward) fz -= 1
|
|
10
|
+
if (input.left) fx -= 1
|
|
11
|
+
if (input.right) fx += 1
|
|
12
|
+
const flen = Math.sqrt(fx * fx + fz * fz)
|
|
13
|
+
if (flen > 0) { fx /= flen; fz /= flen }
|
|
14
|
+
const yaw = input.yaw || 0
|
|
15
|
+
const cy = Math.cos(yaw), sy = Math.sin(yaw)
|
|
16
|
+
wishX = fz * sy - fx * cy
|
|
17
|
+
wishZ = fx * sy + fz * cy
|
|
18
|
+
wishSpeed = flen > 0 ? maxSpeed : 0
|
|
19
|
+
if (input.jump && state.onGround) {
|
|
20
|
+
state.velocity[1] = jumpImpulse
|
|
21
|
+
state.onGround = false
|
|
22
|
+
jumped = true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (state.onGround && !jumped) {
|
|
27
|
+
const speed = Math.sqrt(vx * vx + vz * vz)
|
|
28
|
+
if (speed > 0.1) {
|
|
29
|
+
const control = speed < stopSpeed ? stopSpeed : speed
|
|
30
|
+
const drop = control * friction * dt
|
|
31
|
+
let newSpeed = speed - drop
|
|
32
|
+
if (newSpeed < 0) newSpeed = 0
|
|
33
|
+
const scale = newSpeed / speed
|
|
34
|
+
vx *= scale; vz *= scale
|
|
35
|
+
} else { vx = 0; vz = 0 }
|
|
36
|
+
if (wishSpeed > 0) {
|
|
37
|
+
const cur = vx * wishX + vz * wishZ
|
|
38
|
+
let add = wishSpeed - cur
|
|
39
|
+
if (add > 0) {
|
|
40
|
+
let as = groundAccel * wishSpeed * dt
|
|
41
|
+
if (as > add) as = add
|
|
42
|
+
vx += as * wishX; vz += as * wishZ
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
if (wishSpeed > 0) {
|
|
47
|
+
const cur = vx * wishX + vz * wishZ
|
|
48
|
+
let add = wishSpeed - cur
|
|
49
|
+
if (add > 0) {
|
|
50
|
+
let as = airAccel * wishSpeed * dt
|
|
51
|
+
if (as > add) as = add
|
|
52
|
+
vx += as * wishX; vz += as * wishZ
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
state.velocity[0] = vx
|
|
58
|
+
state.velocity[2] = vz
|
|
59
|
+
return { wishX, wishZ, wishSpeed, jumped }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_MOVEMENT = {
|
|
63
|
+
maxSpeed: 8.0,
|
|
64
|
+
groundAccel: 10.0,
|
|
65
|
+
airAccel: 1.0,
|
|
66
|
+
friction: 6.0,
|
|
67
|
+
stopSpeed: 2.0,
|
|
68
|
+
jumpImpulse: 4.5
|
|
69
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { octree } from 'd3-octree'
|
|
2
|
+
|
|
3
|
+
export class SpatialIndex {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this._tree = octree()
|
|
6
|
+
this._entities = new Map()
|
|
7
|
+
this._relevanceRadius = config.relevanceRadius || 200
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
insert(id, position) {
|
|
11
|
+
this.remove(id)
|
|
12
|
+
const point = [position[0], position[1], position[2]]
|
|
13
|
+
point._entityId = id
|
|
14
|
+
this._entities.set(id, point)
|
|
15
|
+
this._tree.add(point)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
remove(id) {
|
|
19
|
+
const existing = this._entities.get(id)
|
|
20
|
+
if (!existing) return
|
|
21
|
+
this._tree.remove(existing)
|
|
22
|
+
this._entities.delete(id)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
update(id, position) {
|
|
26
|
+
this.insert(id, position)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
has(id) {
|
|
30
|
+
return this._entities.has(id)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getPosition(id) {
|
|
34
|
+
const p = this._entities.get(id)
|
|
35
|
+
return p ? [p[0], p[1], p[2]] : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
nearby(position, radius) {
|
|
39
|
+
const cx = position[0], cy = position[1], cz = position[2]
|
|
40
|
+
const r2 = radius * radius
|
|
41
|
+
const results = []
|
|
42
|
+
this._tree.visit((node, x0, y0, z0, x1, y1, z1) => {
|
|
43
|
+
if (!node.length) {
|
|
44
|
+
let d = node
|
|
45
|
+
do {
|
|
46
|
+
const p = d.data
|
|
47
|
+
const dx = p[0] - cx, dy = p[1] - cy, dz = p[2] - cz
|
|
48
|
+
if (dx * dx + dy * dy + dz * dz <= r2) {
|
|
49
|
+
results.push(p._entityId)
|
|
50
|
+
}
|
|
51
|
+
} while (d = d.next)
|
|
52
|
+
}
|
|
53
|
+
const nx = Math.max(x0, Math.min(cx, x1))
|
|
54
|
+
const ny = Math.max(y0, Math.min(cy, y1))
|
|
55
|
+
const nz = Math.max(z0, Math.min(cz, z1))
|
|
56
|
+
const ddx = nx - cx, ddy = ny - cy, ddz = nz - cz
|
|
57
|
+
return ddx * ddx + ddy * ddy + ddz * ddz > r2
|
|
58
|
+
})
|
|
59
|
+
return results
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
nearest(position, radius) {
|
|
63
|
+
const p = this._tree.find(position[0], position[1], position[2], radius)
|
|
64
|
+
return p ? p._entityId : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get size() {
|
|
68
|
+
return this._entities.size
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
this._tree = octree()
|
|
73
|
+
this._entities.clear()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
rebuild() {
|
|
77
|
+
const entries = Array.from(this._entities.entries())
|
|
78
|
+
this._tree = octree()
|
|
79
|
+
for (const [, point] of entries) {
|
|
80
|
+
this._tree.add(point)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get relevanceRadius() {
|
|
85
|
+
return this._relevanceRadius
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
set relevanceRadius(v) {
|
|
89
|
+
this._relevanceRadius = v
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SpatialIndex } from '../spatial/Octree.js'
|
|
2
|
+
|
|
3
|
+
export class Stage {
|
|
4
|
+
constructor(name, config = {}) {
|
|
5
|
+
this.name = name
|
|
6
|
+
this.entityIds = new Set()
|
|
7
|
+
this.spatial = new SpatialIndex({ relevanceRadius: config.relevanceRadius || 200 })
|
|
8
|
+
this.gravity = config.gravity || null
|
|
9
|
+
this.spawnPoint = config.spawnPoint || null
|
|
10
|
+
this.playerModel = config.playerModel || null
|
|
11
|
+
this._runtime = null
|
|
12
|
+
this._staticIds = new Set()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
bind(runtime) {
|
|
16
|
+
this._runtime = runtime
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
addEntity(id, config = {}) {
|
|
20
|
+
if (!this._runtime) return null
|
|
21
|
+
const entity = this._runtime.spawnEntity(id, config)
|
|
22
|
+
this.entityIds.add(entity.id)
|
|
23
|
+
const pos = entity.position || [0, 0, 0]
|
|
24
|
+
this.spatial.insert(entity.id, pos)
|
|
25
|
+
if (entity.bodyType === 'static' || config.autoTrimesh) {
|
|
26
|
+
this._staticIds.add(entity.id)
|
|
27
|
+
}
|
|
28
|
+
return entity
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
removeEntity(id) {
|
|
32
|
+
if (!this._runtime) return
|
|
33
|
+
this.spatial.remove(id)
|
|
34
|
+
this._staticIds.delete(id)
|
|
35
|
+
this.entityIds.delete(id)
|
|
36
|
+
this._runtime.destroyEntity(id)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updateEntityPosition(id, position) {
|
|
40
|
+
if (!this.entityIds.has(id)) return
|
|
41
|
+
this.spatial.update(id, position)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getNearbyEntities(position, radius) {
|
|
45
|
+
return this.spatial.nearby(position, radius || this.spatial.relevanceRadius)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getRelevantEntities(position, radius) {
|
|
49
|
+
const nearby = this.spatial.nearby(position, radius || this.spatial.relevanceRadius)
|
|
50
|
+
const set = new Set(nearby)
|
|
51
|
+
for (const sid of this._staticIds) set.add(sid)
|
|
52
|
+
return Array.from(set)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getStaticIds() {
|
|
56
|
+
return Array.from(this._staticIds)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
hasEntity(id) {
|
|
60
|
+
return this.entityIds.has(id)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get entityCount() {
|
|
64
|
+
return this.entityIds.size
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clear() {
|
|
68
|
+
if (!this._runtime) return
|
|
69
|
+
for (const id of [...this.entityIds]) {
|
|
70
|
+
this._runtime.destroyEntity(id)
|
|
71
|
+
}
|
|
72
|
+
this.entityIds.clear()
|
|
73
|
+
this._staticIds.clear()
|
|
74
|
+
this.spatial.clear()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
syncPositions() {
|
|
78
|
+
if (!this._runtime) return
|
|
79
|
+
for (const id of this.entityIds) {
|
|
80
|
+
const e = this._runtime.getEntity(id)
|
|
81
|
+
if (e && e.bodyType !== 'static') {
|
|
82
|
+
this.spatial.update(id, e.position)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getAllEntityIds() {
|
|
88
|
+
return Array.from(this.entityIds)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Stage } from './Stage.js'
|
|
2
|
+
|
|
3
|
+
export class StageLoader {
|
|
4
|
+
constructor(runtime) {
|
|
5
|
+
this._runtime = runtime
|
|
6
|
+
this._stages = new Map()
|
|
7
|
+
this._activeStage = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
loadFromDefinition(name, worldDef) {
|
|
11
|
+
const stage = new Stage(name, {
|
|
12
|
+
relevanceRadius: worldDef.relevanceRadius || 200,
|
|
13
|
+
gravity: worldDef.gravity,
|
|
14
|
+
spawnPoint: worldDef.spawnPoint,
|
|
15
|
+
playerModel: worldDef.playerModel
|
|
16
|
+
})
|
|
17
|
+
stage.bind(this._runtime)
|
|
18
|
+
|
|
19
|
+
if (worldDef.gravity) {
|
|
20
|
+
this._runtime.gravity = [...worldDef.gravity]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const entDef of worldDef.entities || []) {
|
|
24
|
+
const cfg = {
|
|
25
|
+
model: entDef.model,
|
|
26
|
+
position: entDef.position || [0, 0, 0],
|
|
27
|
+
rotation: entDef.rotation,
|
|
28
|
+
scale: entDef.scale,
|
|
29
|
+
app: entDef.app,
|
|
30
|
+
config: entDef.config || null
|
|
31
|
+
}
|
|
32
|
+
if (entDef.model && !entDef.app) {
|
|
33
|
+
cfg.autoTrimesh = true
|
|
34
|
+
}
|
|
35
|
+
stage.addEntity(entDef.id || null, cfg)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._stages.set(name, stage)
|
|
39
|
+
if (!this._activeStage) this._activeStage = stage
|
|
40
|
+
return stage
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getStage(name) {
|
|
44
|
+
return this._stages.get(name) || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getActiveStage() {
|
|
48
|
+
return this._activeStage
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setActiveStage(name) {
|
|
52
|
+
const stage = this._stages.get(name)
|
|
53
|
+
if (stage) this._activeStage = stage
|
|
54
|
+
return stage
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
removeStage(name) {
|
|
58
|
+
const stage = this._stages.get(name)
|
|
59
|
+
if (!stage) return
|
|
60
|
+
stage.clear()
|
|
61
|
+
this._stages.delete(name)
|
|
62
|
+
if (this._activeStage === stage) {
|
|
63
|
+
this._activeStage = this._stages.values().next().value || null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
swapStage(oldName, newName, newDef) {
|
|
68
|
+
this.removeStage(oldName)
|
|
69
|
+
return this.loadFromDefinition(newName, newDef)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getAllStageNames() {
|
|
73
|
+
return Array.from(this._stages.keys())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get stageCount() {
|
|
77
|
+
return this._stages.size
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
syncAllPositions() {
|
|
81
|
+
for (const stage of this._stages.values()) {
|
|
82
|
+
stage.syncPositions()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getNearbyEntities(position, radius) {
|
|
87
|
+
if (!this._activeStage) return []
|
|
88
|
+
return this._activeStage.getNearbyEntities(position, radius)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getRelevantEntities(position, radius) {
|
|
92
|
+
if (!this._activeStage) return []
|
|
93
|
+
return this._activeStage.getRelevantEntities(position, radius)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile, writeFile, unlink, readdir, mkdir, rename, access } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { StorageAdapter } from './StorageAdapter.js'
|
|
4
|
+
|
|
5
|
+
export class FSAdapter extends StorageAdapter {
|
|
6
|
+
constructor(baseDir = './data') {
|
|
7
|
+
super()
|
|
8
|
+
this._dir = baseDir
|
|
9
|
+
this._ready = false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async _ensureDir() {
|
|
13
|
+
if (this._ready) return
|
|
14
|
+
await mkdir(this._dir, { recursive: true }).catch(() => {})
|
|
15
|
+
this._ready = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_path(key) {
|
|
19
|
+
const safe = key.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
20
|
+
return join(this._dir, `${safe}.json`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async get(key) {
|
|
24
|
+
try {
|
|
25
|
+
const data = await readFile(this._path(key), 'utf-8')
|
|
26
|
+
return JSON.parse(data)
|
|
27
|
+
} catch { return undefined }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async set(key, value) {
|
|
31
|
+
await this._ensureDir()
|
|
32
|
+
const fp = this._path(key)
|
|
33
|
+
const tmp = fp + '.tmp'
|
|
34
|
+
await writeFile(tmp, JSON.stringify(value), 'utf-8')
|
|
35
|
+
await rename(tmp, fp)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async delete(key) {
|
|
39
|
+
try { await unlink(this._path(key)) } catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async list(prefix = '') {
|
|
43
|
+
await this._ensureDir()
|
|
44
|
+
try {
|
|
45
|
+
const files = await readdir(this._dir)
|
|
46
|
+
const safe = prefix.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
47
|
+
return files
|
|
48
|
+
.filter(f => f.endsWith('.json') && f.startsWith(safe))
|
|
49
|
+
.map(f => f.slice(0, -5))
|
|
50
|
+
} catch { return [] }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async has(key) {
|
|
54
|
+
try { await access(this._path(key)); return true } catch { return false }
|
|
55
|
+
}
|
|
56
|
+
}
|