spoint 0.1.16 → 0.1.18

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.
@@ -2,57 +2,25 @@ export default {
2
2
  server: {
3
3
  setup(ctx) {
4
4
  ctx.entity.custom = { mesh: 'box', color: 0x00ff88, sx: 1.5, sy: 0.5, sz: 1.5, label: 'INTERACT' }
5
- ctx.state.interactionRadius = 3.5
6
- ctx.state.interactionCooldown = new Map()
7
5
  ctx.state.interactionCount = 0
8
6
 
9
7
  ctx.physics.setStatic(true)
10
8
  ctx.physics.addBoxCollider([0.75, 0.25, 0.75])
11
-
12
- ctx.time.every(0.1, () => {
13
- const nearby = ctx.players.getNearest(ctx.entity.position, ctx.state.interactionRadius)
14
- if (!nearby) return
15
-
16
- const now = Date.now()
17
- const playerId = nearby.id
18
- const lastInteract = ctx.state.interactionCooldown.get(playerId) || 0
19
-
20
- if (nearby.state?.interact && now - lastInteract > 500) {
21
- ctx.state.interactionCooldown.set(playerId, now)
22
- ctx.state.interactionCount++
23
-
24
- const messages = [
25
- 'Hello there!',
26
- 'You found the interact button!',
27
- 'Nice to meet you!',
28
- 'This button works!',
29
- `Interacted ${ctx.state.interactionCount} times total`
30
- ]
31
- const msg = messages[ctx.state.interactionCount % messages.length]
32
-
33
- ctx.players.send(playerId, {
34
- type: 'interact_response',
35
- message: msg,
36
- count: ctx.state.interactionCount
37
- })
38
-
39
- ctx.network.broadcast({
40
- type: 'interact_effect',
41
- position: ctx.entity.position,
42
- playerId: playerId
43
- })
44
- }
45
- })
9
+ ctx.physics.setInteractable(3.5)
46
10
  },
47
11
 
48
- teardown(ctx) {
49
- ctx.state.interactionCooldown?.clear()
50
- },
51
-
52
- onMessage(ctx, msg) {
53
- if (msg.type === 'player_leave') {
54
- ctx.state.interactionCooldown?.delete(msg.playerId)
55
- }
12
+ onInteract(ctx, player) {
13
+ ctx.state.interactionCount++
14
+ const messages = [
15
+ 'Hello there!',
16
+ 'You found the interact button!',
17
+ 'Nice to meet you!',
18
+ 'This button works!',
19
+ `Interacted ${ctx.state.interactionCount} times total`
20
+ ]
21
+ const msg = messages[ctx.state.interactionCount % messages.length]
22
+ ctx.players.send(player.id, { type: 'interact_response', message: msg, count: ctx.state.interactionCount })
23
+ ctx.network.broadcast({ type: 'interact_effect', position: ctx.entity.position, playerId: player.id })
56
24
  }
57
25
  },
58
26
 
@@ -60,8 +28,7 @@ export default {
60
28
  setup(engine) {
61
29
  this._lastMessage = null
62
30
  this._messageExpire = 0
63
- this._showingInteract = false
64
- this._isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
31
+ this._canInteract = false
65
32
  this._wasRegistered = false
66
33
  },
67
34
 
@@ -70,86 +37,37 @@ export default {
70
37
  const pos = ent?.position
71
38
  const local = engine.client?.state?.players?.find(p => p.id === engine.playerId)
72
39
  if (!pos || !local?.position) {
73
- if (this._wasRegistered) {
74
- engine.mobileControls?.unregisterInteractable(ent?.id || 'interactable')
75
- this._wasRegistered = false
76
- }
40
+ if (this._wasRegistered) { engine.mobileControls?.unregisterInteractable(this._wasRegistered); this._wasRegistered = false }
77
41
  return
78
42
  }
79
-
80
- const dx = pos[0] - local.position[0]
81
- const dy = pos[1] - local.position[1]
82
- const dz = pos[2] - local.position[2]
83
- const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
84
- const canInteract = dist < 3.5
85
-
86
- if (canInteract && !this._wasRegistered) {
87
- engine.mobileControls?.registerInteractable(ent.id, 'INTERACT')
88
- this._wasRegistered = ent.id
89
- } else if (!canInteract && this._wasRegistered) {
90
- engine.mobileControls?.unregisterInteractable(this._wasRegistered)
91
- this._wasRegistered = false
92
- }
93
-
43
+ const dx = pos[0]-local.position[0], dy = pos[1]-local.position[1], dz = pos[2]-local.position[2]
44
+ const canInteract = dx*dx+dy*dy+dz*dz < 3.5*3.5
45
+ if (canInteract && !this._wasRegistered) { engine.mobileControls?.registerInteractable(ent.id, 'INTERACT'); this._wasRegistered = ent.id }
46
+ else if (!canInteract && this._wasRegistered) { engine.mobileControls?.unregisterInteractable(this._wasRegistered); this._wasRegistered = false }
94
47
  this._canInteract = canInteract
95
48
  },
96
49
 
97
50
  teardown(engine) {
98
- if (this._wasRegistered) {
99
- engine.mobileControls?.unregisterInteractable('interactable')
100
- this._wasRegistered = false
101
- }
102
- },
103
-
104
- onInput(input, engine) {
105
- if (input.interact && !this._wasInteracting) {
106
- this._showingInteract = true
107
- this._messageExpire = Date.now() + 2000
108
- }
109
- this._wasInteracting = input.interact
51
+ if (this._wasRegistered) { engine.mobileControls?.unregisterInteractable(this._wasRegistered); this._wasRegistered = false }
110
52
  },
111
53
 
112
54
  onEvent(payload, engine) {
113
- if (payload.type === 'interact_response') {
114
- this._lastMessage = payload.message
115
- this._messageExpire = Date.now() + 3000
116
- }
117
- if (payload.type === 'interact_effect') {
118
- this._lastMessage = 'Someone interacted!'
119
- this._messageExpire = Date.now() + 1500
120
- }
55
+ if (payload.type === 'interact_response') { this._lastMessage = payload.message; this._messageExpire = Date.now() + 3000 }
56
+ if (payload.type === 'interact_effect') { this._lastMessage = 'Someone interacted!'; this._messageExpire = Date.now() + 1500 }
121
57
  },
122
58
 
123
59
  render(ctx) {
124
60
  const h = ctx.h
125
61
  const pos = ctx.entity.position
126
62
  if (!h || !pos) return { position: pos }
127
-
128
63
  const ui = []
129
-
130
64
  if (this._lastMessage && Date.now() < this._messageExpire) {
131
- const timeLeft = (this._messageExpire - Date.now()) / 3000
132
- const opacity = Math.min(1, timeLeft * 2)
133
- ui.push(
134
- h('div', {
135
- style: `position:fixed;top:30%;left:50%;transform:translate(-50%,-50%);padding:16px 32px;background:rgba(0,0,0,0.8);border-radius:12px;color:#0f0;font-weight:bold;font-size:20px;text-align:center;border:2px solid #0f0;opacity:${opacity}`
136
- }, this._lastMessage)
137
- )
65
+ const opacity = Math.min(1, ((this._messageExpire - Date.now()) / 3000) * 2)
66
+ ui.push(h('div', { style: `position:fixed;top:30%;left:50%;transform:translate(-50%,-50%);padding:16px 32px;background:rgba(0,0,0,0.8);border-radius:12px;color:#0f0;font-weight:bold;font-size:20px;text-align:center;border:2px solid #0f0;opacity:${opacity}` }, this._lastMessage))
138
67
  }
139
-
140
68
  const custom = { ...ctx.entity.custom }
141
- if (this._canInteract) {
142
- custom.glow = true
143
- custom.glowColor = 0x00ff88
144
- custom.glowIntensity = 0.5
145
- }
146
-
147
- return {
148
- position: pos,
149
- rotation: ctx.entity.rotation,
150
- custom,
151
- ui: ui.length > 0 ? h('div', null, ...ui) : null
152
- }
69
+ if (this._canInteract) { custom.glow = true; custom.glowColor = 0x00ff88; custom.glowIntensity = 0.5 }
70
+ return { position: pos, rotation: ctx.entity.rotation, custom, ui: ui.length > 0 ? h('div', null, ...ui) : null }
153
71
  }
154
72
  }
155
73
  }
package/client/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as THREE from 'three'
2
2
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
3
+ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
3
4
  import { VRMLoaderPlugin, VRMUtils } from '@pixiv/three-vrm'
4
5
  import { PhysicsNetworkClient, InputHandler, MSG } from '/src/index.client.js'
5
6
  import { createElement, applyDiff } from 'webjsx'
@@ -657,6 +658,9 @@ ground.receiveShadow = true
657
658
  scene.add(ground)
658
659
 
659
660
  const gltfLoader = new GLTFLoader()
661
+ const dracoLoader = new DRACOLoader()
662
+ dracoLoader.setDecoderPath('https://esm.sh/v135/three@0.171.0/examples/jsm/libs/draco/')
663
+ gltfLoader.setDRACOLoader(dracoLoader)
660
664
  gltfLoader.register((parser) => new VRMLoaderPlugin(parser))
661
665
  const playerMeshes = new Map()
662
666
  const playerAnimators = new Map()
@@ -1154,12 +1158,10 @@ function startInputLoop() {
1154
1158
  const input = inputHandler.getInput()
1155
1159
  latestInput = input
1156
1160
 
1157
- if (input.editToggle && !cam.getEditMode()) {
1158
- cam.setEditMode(true)
1159
- console.log('[EditMode] Enabled')
1160
- } else if (!input.editToggle && cam.getEditMode() && inputHandler.editModeCooldown === false) {
1161
- cam.setEditMode(false)
1162
- console.log('[EditMode] Disabled')
1161
+ const wantsEdit = !!input.editToggle
1162
+ if (wantsEdit !== cam.getEditMode()) {
1163
+ cam.setEditMode(wantsEdit)
1164
+ console.log('[EditMode]', wantsEdit ? 'Enabled' : 'Disabled')
1163
1165
  }
1164
1166
 
1165
1167
  if (input.yaw !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -40,6 +40,7 @@ export class AppContext {
40
40
  const ent = this._entity
41
41
  const runtime = this._runtime
42
42
  return {
43
+ setInteractable: (radius = 3) => { ent._interactable = true; ent._interactRadius = radius },
43
44
  setStatic: (v) => { ent.bodyType = v ? 'static' : ent.bodyType },
44
45
  setDynamic: (v) => { ent.bodyType = v ? 'dynamic' : ent.bodyType },
45
46
  setKinematic: (v) => { ent.bodyType = v ? 'kinematic' : ent.bodyType },
@@ -13,7 +13,7 @@ export class AppRuntime {
13
13
  this.currentTick = 0; this.deltaTime = 0; this.elapsed = 0
14
14
  this._playerManager = c.playerManager || null; this._physics = c.physics || null; this._physicsIntegration = c.physicsIntegration || null
15
15
  this._connections = c.connections || null; this._stageLoader = c.stageLoader || null
16
- this._nextEntityId = 1; this._appDefs = new Map(); this._timers = new Map()
16
+ this._nextEntityId = 1; this._appDefs = new Map(); this._timers = new Map(); this._interactCooldowns = new Map()
17
17
  this._hotReload = new HotReloadQueue(this)
18
18
  this._eventBus = c.eventBus || new EventBus()
19
19
  this._eventLog = c.eventLog || null
@@ -124,7 +124,7 @@ export class AppRuntime {
124
124
  const ctx = this.contexts.get(entityId); if (!ctx) continue
125
125
  this._safeCall(appDef.server || appDef, 'update', [ctx, dt], `update(${entityId})`)
126
126
  }
127
- this._tickTimers(dt); this._spatialSync(); this._tickCollisions()
127
+ this._tickTimers(dt); this._spatialSync(); this._tickCollisions(); this._tickInteractables()
128
128
  }
129
129
 
130
130
  _encodeEntity(id, e) {
@@ -183,6 +183,27 @@ export class AppRuntime {
183
183
  }
184
184
  }
185
185
 
186
+ _tickInteractables() {
187
+ const now = Date.now()
188
+ for (const e of this.entities.values()) {
189
+ if (!e._interactable) continue
190
+ const players = this.getPlayers()
191
+ for (const p of players) {
192
+ const pp = p.state?.position; if (!pp) continue
193
+ const dx = pp[0]-e.position[0], dy = pp[1]-e.position[1], dz = pp[2]-e.position[2]
194
+ if (dx*dx+dy*dy+dz*dz > e._interactRadius**2) continue
195
+ const key = `${e.id}:${p.id}`
196
+ const last = this._interactCooldowns.get(key) || 0
197
+ if (p.lastInput?.interact && now - last > 500) {
198
+ this._interactCooldowns.set(key, now)
199
+ this.fireEvent(e.id, 'onInteract', p)
200
+ const bus = this._eventBus.scope ? this._eventBus : null
201
+ if (bus) bus.emit(`interact.${e.id}`, { player: p, entity: e })
202
+ }
203
+ }
204
+ }
205
+ }
206
+
186
207
  _colR(c) { return !c ? 0 : c.type === 'sphere' ? (c.radius||1) : c.type === 'capsule' ? Math.max(c.radius||0.5,(c.height||1)/2) : c.type === 'box' ? Math.max(...(c.size||c.halfExtents||[1,1,1])) : 1 }
187
208
  setPlayerManager(pm) { this._playerManager = pm }
188
209
  setStageLoader(sl) { this._stageLoader = sl }
@@ -18,6 +18,8 @@ export class InputHandler {
18
18
  this.menuCooldown = false
19
19
  this.mobileControls = null
20
20
  this.mobileInput = null
21
+ this._editActive = false
22
+ this._pWasDown = false
21
23
  this.editModeCooldown = false
22
24
  this.lastEditModeToggle = 0
23
25
 
@@ -123,12 +125,12 @@ export class InputHandler {
123
125
 
124
126
  const now = Date.now()
125
127
  const pPressed = this.keys.get('p') || false
126
- if (pPressed && !this.editModeCooldown && now - this.lastEditModeToggle > 200) {
127
- this.editModeCooldown = true
128
+ if (pPressed && !this._pWasDown && now - this.lastEditModeToggle > 200) {
129
+ this._editActive = !this._editActive
128
130
  this.lastEditModeToggle = now
129
- } else if (!pPressed) {
130
- this.editModeCooldown = false
131
131
  }
132
+ this._pWasDown = pPressed
133
+ this.editModeCooldown = this._editActive
132
134
 
133
135
  return {
134
136
  forward: this.keys.get('w') || this.keys.get('arrowup') || false,
@@ -74,7 +74,7 @@ export class MessageHandler {
74
74
 
75
75
  _handleHeartbeat(payload) {
76
76
  if (this._smoothInterp) {
77
- this._smoothInterp.updateRTT(payload.pingTime || 0, Date.now())
77
+ this._smoothInterp.updateRTT(payload.timestamp || 0, Date.now())
78
78
  }
79
79
  }
80
80
 
Binary file
Binary file