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.
- package/apps/interactable/index.js +26 -108
- package/client/app.js +8 -6
- package/package.json +1 -1
- package/src/apps/AppContext.js +1 -0
- package/src/apps/AppRuntime.js +23 -2
- package/src/client/InputHandler.js +6 -4
- package/src/client/MessageHandler.js +1 -1
- package/apps/tps-game/$GDUPI.vrm +0 -0
- package/apps/tps-game/Cleetus.vrm +0 -0
|
@@ -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
|
-
|
|
49
|
-
ctx.state.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
package/src/apps/AppContext.js
CHANGED
|
@@ -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 },
|
package/src/apps/AppRuntime.js
CHANGED
|
@@ -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.
|
|
127
|
-
this.
|
|
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,
|
package/apps/tps-game/$GDUPI.vrm
DELETED
|
Binary file
|
|
Binary file
|