spoint 0.1.0 → 0.1.10

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 (204) hide show
  1. package/README.md +134 -209
  2. package/SKILL.md +95 -0
  3. package/apps/environment/index.js +200 -1
  4. package/apps/environment/models/decorative/.gitkeep +0 -0
  5. package/apps/environment/models/hazards/.gitkeep +0 -0
  6. package/apps/environment/models/interactive/.gitkeep +0 -0
  7. package/apps/environment/models/structures/.gitkeep +0 -0
  8. package/apps/environment/smartObjects.js +114 -0
  9. package/apps/interactable/index.js +155 -0
  10. package/apps/physics-crate/index.js +15 -9
  11. package/apps/power-crate/index.js +18 -12
  12. package/apps/tps-game/$GDUPI.vrm +0 -0
  13. package/apps/tps-game/Cleetus.vrm +0 -0
  14. package/apps/tps-game/index.js +185 -27
  15. package/apps/world/index.js +68 -22
  16. package/bin/create-app.js +337 -0
  17. package/client/ARControls.js +301 -0
  18. package/client/LoadingManager.js +117 -0
  19. package/client/MobileControls.js +1122 -0
  20. package/client/anim-lib.glb +0 -0
  21. package/client/animation.js +306 -0
  22. package/client/app.js +1341 -65
  23. package/client/camera.js +191 -33
  24. package/client/createLoadingScreen.js +69 -0
  25. package/client/editor/bridge.js +113 -0
  26. package/client/editor/css/main.css +794 -0
  27. package/client/editor/images/rotate.svg +4 -0
  28. package/client/editor/images/scale.svg +4 -0
  29. package/client/editor/images/translate.svg +4 -0
  30. package/client/editor/index.html +103 -0
  31. package/client/editor/js/Command.js +41 -0
  32. package/client/editor/js/Config.js +81 -0
  33. package/client/editor/js/Editor.js +785 -0
  34. package/client/editor/js/EditorControls.js +438 -0
  35. package/client/editor/js/History.js +321 -0
  36. package/client/editor/js/Loader.js +987 -0
  37. package/client/editor/js/LoaderUtils.js +90 -0
  38. package/client/editor/js/Menubar.Add.js +510 -0
  39. package/client/editor/js/Menubar.Edit.js +145 -0
  40. package/client/editor/js/Menubar.File.js +466 -0
  41. package/client/editor/js/Menubar.Help.js +73 -0
  42. package/client/editor/js/Menubar.Status.js +51 -0
  43. package/client/editor/js/Menubar.View.js +183 -0
  44. package/client/editor/js/Menubar.js +27 -0
  45. package/client/editor/js/Player.js +53 -0
  46. package/client/editor/js/Resizer.js +58 -0
  47. package/client/editor/js/Script.js +503 -0
  48. package/client/editor/js/Selector.js +102 -0
  49. package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
  50. package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
  51. package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
  52. package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
  53. package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
  54. package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
  55. package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
  56. package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
  57. package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
  58. package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
  59. package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
  60. package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
  61. package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
  62. package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
  63. package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
  64. package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
  65. package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
  66. package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
  67. package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
  68. package/client/editor/js/Sidebar.Geometry.js +332 -0
  69. package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
  70. package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
  71. package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
  72. package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
  73. package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
  74. package/client/editor/js/Sidebar.Material.Program.js +73 -0
  75. package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
  76. package/client/editor/js/Sidebar.Material.js +751 -0
  77. package/client/editor/js/Sidebar.Object.Animation.js +102 -0
  78. package/client/editor/js/Sidebar.Object.js +898 -0
  79. package/client/editor/js/Sidebar.Project.App.js +165 -0
  80. package/client/editor/js/Sidebar.Project.Image.js +225 -0
  81. package/client/editor/js/Sidebar.Project.Materials.js +82 -0
  82. package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
  83. package/client/editor/js/Sidebar.Project.Video.js +242 -0
  84. package/client/editor/js/Sidebar.Project.js +31 -0
  85. package/client/editor/js/Sidebar.Properties.js +73 -0
  86. package/client/editor/js/Sidebar.Scene.js +585 -0
  87. package/client/editor/js/Sidebar.Script.js +129 -0
  88. package/client/editor/js/Sidebar.Settings.History.js +146 -0
  89. package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
  90. package/client/editor/js/Sidebar.Settings.js +60 -0
  91. package/client/editor/js/Sidebar.js +41 -0
  92. package/client/editor/js/Storage.js +98 -0
  93. package/client/editor/js/Strings.js +2028 -0
  94. package/client/editor/js/Toolbar.js +84 -0
  95. package/client/editor/js/Viewport.Controls.js +92 -0
  96. package/client/editor/js/Viewport.Info.js +136 -0
  97. package/client/editor/js/Viewport.Pathtracer.js +91 -0
  98. package/client/editor/js/Viewport.ViewHelper.js +39 -0
  99. package/client/editor/js/Viewport.XR.js +222 -0
  100. package/client/editor/js/Viewport.js +900 -0
  101. package/client/editor/js/commands/AddObjectCommand.js +68 -0
  102. package/client/editor/js/commands/AddScriptCommand.js +75 -0
  103. package/client/editor/js/commands/Commands.js +23 -0
  104. package/client/editor/js/commands/MoveObjectCommand.js +111 -0
  105. package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
  106. package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
  107. package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
  108. package/client/editor/js/commands/SetColorCommand.js +73 -0
  109. package/client/editor/js/commands/SetGeometryCommand.js +87 -0
  110. package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
  111. package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
  112. package/client/editor/js/commands/SetMaterialCommand.js +79 -0
  113. package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
  114. package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
  115. package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
  116. package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
  117. package/client/editor/js/commands/SetPositionCommand.js +84 -0
  118. package/client/editor/js/commands/SetRotationCommand.js +84 -0
  119. package/client/editor/js/commands/SetScaleCommand.js +84 -0
  120. package/client/editor/js/commands/SetSceneCommand.js +103 -0
  121. package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
  122. package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
  123. package/client/editor/js/commands/SetUuidCommand.js +70 -0
  124. package/client/editor/js/commands/SetValueCommand.js +75 -0
  125. package/client/editor/js/libs/acorn/acorn.js +3236 -0
  126. package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
  127. package/client/editor/js/libs/acorn/walk.js +344 -0
  128. package/client/editor/js/libs/app/index.html +57 -0
  129. package/client/editor/js/libs/app.js +251 -0
  130. package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
  131. package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
  132. package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
  133. package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
  134. package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
  135. package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
  136. package/client/editor/js/libs/codemirror/codemirror.css +344 -0
  137. package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
  138. package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
  139. package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
  140. package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
  141. package/client/editor/js/libs/esprima.js +6401 -0
  142. package/client/editor/js/libs/jsonlint.js +453 -0
  143. package/client/editor/js/libs/signals.min.js +14 -0
  144. package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
  145. package/client/editor/js/libs/ternjs/comment.js +87 -0
  146. package/client/editor/js/libs/ternjs/def.js +588 -0
  147. package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
  148. package/client/editor/js/libs/ternjs/infer.js +1635 -0
  149. package/client/editor/js/libs/ternjs/polyfill.js +80 -0
  150. package/client/editor/js/libs/ternjs/signal.js +26 -0
  151. package/client/editor/js/libs/ternjs/tern.js +993 -0
  152. package/client/editor/js/libs/ui.js +1346 -0
  153. package/client/editor/js/libs/ui.three.js +855 -0
  154. package/client/facial-animation.js +455 -0
  155. package/client/index.html +7 -4
  156. package/client/loading.css +147 -0
  157. package/client/loading.html +25 -0
  158. package/client/style.css +251 -0
  159. package/package.json +7 -3
  160. package/server.js +9 -1
  161. package/src/apps/AppContext.js +1 -1
  162. package/src/apps/AppLoader.js +50 -37
  163. package/src/apps/AppRuntime.js +32 -8
  164. package/src/client/InputHandler.js +233 -0
  165. package/src/client/JitterBuffer.js +207 -0
  166. package/src/client/KalmanFilter.js +125 -0
  167. package/src/client/MessageHandler.js +101 -0
  168. package/src/client/PhysicsNetworkClient.js +141 -68
  169. package/src/client/ReconnectManager.js +62 -0
  170. package/src/client/SmoothInterpolation.js +127 -0
  171. package/src/client/SnapshotProcessor.js +144 -0
  172. package/src/connection/ConnectionManager.js +21 -3
  173. package/src/connection/SessionStore.js +13 -3
  174. package/src/index.client.js +4 -6
  175. package/src/netcode/EventLog.js +29 -15
  176. package/src/netcode/LagCompensator.js +25 -26
  177. package/src/netcode/NetworkState.js +4 -1
  178. package/src/netcode/PhysicsIntegration.js +20 -6
  179. package/src/netcode/PlayerManager.js +10 -2
  180. package/src/netcode/SnapshotEncoder.js +66 -19
  181. package/src/netcode/TickSystem.js +13 -4
  182. package/src/physics/World.js +66 -13
  183. package/src/protocol/msgpack.js +90 -63
  184. package/src/sdk/ReloadHandlers.js +12 -2
  185. package/src/sdk/ReloadManager.js +5 -0
  186. package/src/sdk/ServerHandlers.js +50 -11
  187. package/src/sdk/StaticHandler.js +22 -6
  188. package/src/sdk/TickHandler.js +101 -34
  189. package/src/sdk/scaffold.js +28 -0
  190. package/src/sdk/server.js +59 -33
  191. package/src/shared/movement.js +2 -1
  192. package/src/spatial/Octree.js +5 -0
  193. package/apps/interactive-door/index.js +0 -33
  194. package/apps/patrol-npc/index.js +0 -37
  195. package/src/connection/QualityMonitor.js +0 -46
  196. package/src/debug/StateInspector.js +0 -42
  197. package/src/index.js +0 -1
  198. package/src/index.server.js +0 -27
  199. package/src/protocol/Codec.js +0 -60
  200. package/src/protocol/SequenceTracker.js +0 -71
  201. package/src/sdk/ClientMessageHandler.js +0 -80
  202. package/src/sdk/client.js +0 -122
  203. package/world/kaira.glb +0 -0
  204. package/world/schwust.glb +0 -0
@@ -1,17 +1,216 @@
1
+ import { SMART_OBJECT_TEMPLATES, getTemplate, isValidTemplate, getPlaceholderColor, getPlaceholderDimensions } from './smartObjects.js'
2
+ import { readdirSync, statSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url))
7
+ const modelsDir = join(__dirname, 'models')
8
+
9
+ function discoverModels() {
10
+ const registry = {}
11
+ try {
12
+ const categories = readdirSync(modelsDir).filter(f => statSync(join(modelsDir, f)).isDirectory())
13
+ for (const cat of categories) {
14
+ const catPath = join(modelsDir, cat)
15
+ const files = readdirSync(catPath).filter(f => f.endsWith('.glb') || f.endsWith('.gltf'))
16
+ registry[cat] = files
17
+ }
18
+ } catch (e) {
19
+ console.log('[Environment] Model discovery skipped (models dir not found)')
20
+ }
21
+ return registry
22
+ }
23
+
1
24
  export default {
2
25
  server: {
3
26
  setup(ctx) {
4
27
  ctx.physics.setStatic(true)
5
28
  ctx.physics.addTrimeshCollider()
29
+
30
+ ctx.state.smartObjects = new Map()
31
+ ctx.state.editorMode = false
32
+ ctx.state.nextSmartObjectId = 0
33
+ ctx.state.modelRegistry = discoverModels()
34
+
35
+ ctx.debug.log(`[Environment] Initialized with ${Object.keys(SMART_OBJECT_TEMPLATES).length} smart object templates, ${Object.values(ctx.state.modelRegistry).flat().length} models discovered`)
36
+ },
37
+
38
+ update(ctx, dt) {
39
+ for (const [id, obj] of ctx.state.smartObjects) {
40
+ if (obj.template === 'platform') updatePlatform(ctx, id, obj, dt)
41
+ if (obj.template === 'hazard') updateHazard(ctx, id, obj, dt)
42
+ }
43
+ },
44
+
45
+ onEvent(payload, ctx) {
46
+ if (payload.type === 'dropModel' && payload.position && payload.modelPath) {
47
+ const id = `dropped_model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
48
+ const entity = ctx.world.spawn(id, {
49
+ position: payload.position,
50
+ rotation: payload.rotation || [0, 0, 0, 1],
51
+ model: payload.modelPath
52
+ })
53
+ if (entity) {
54
+ entity.custom = {
55
+ droppedModel: true,
56
+ modelPath: payload.modelPath,
57
+ scale: payload.scale || [1, 1, 1]
58
+ }
59
+ ctx.debug.log(`[Environment] Dropped model spawned: ${id}`)
60
+ }
61
+ }
62
+ },
63
+
64
+ teardown(ctx) {
65
+ for (const id of ctx.state.smartObjects.keys()) {
66
+ ctx.world.destroy(id)
67
+ }
68
+ ctx.state.smartObjects.clear()
6
69
  }
7
70
  },
71
+
8
72
  client: {
9
73
  render(ctx) {
10
74
  return {
11
75
  model: ctx.entity.model,
12
76
  position: ctx.entity.position,
13
- rotation: ctx.entity.rotation
77
+ rotation: ctx.entity.rotation,
78
+ custom: ctx.entity.custom
14
79
  }
15
80
  }
16
81
  }
17
82
  }
83
+
84
+ function addCollider(ctx, collider) {
85
+ if (!collider) return
86
+
87
+ switch (collider.type) {
88
+ case 'box':
89
+ ctx.physics.addBoxCollider(collider.size)
90
+ break
91
+ case 'sphere':
92
+ ctx.physics.addSphereCollider(collider.radius)
93
+ break
94
+ case 'capsule':
95
+ ctx.physics.addCapsuleCollider(collider.radius, collider.halfHeight * 2)
96
+ break
97
+ case 'trimesh':
98
+ ctx.physics.addTrimeshCollider()
99
+ break
100
+ }
101
+ }
102
+
103
+ function updatePlatform(ctx, id, obj, dt) {
104
+ if (!obj.config || !obj.config.waypoints || obj.config.waypoints.length < 2) return
105
+
106
+ const waypoints = obj.config.waypoints
107
+ const speed = obj.config.speed || 5
108
+ const waitTime = obj.config.waitTime || 1
109
+
110
+ if (obj.waitTimer === undefined) obj.waitTimer = 0
111
+ if (obj.currentWaypoint === undefined) obj.currentWaypoint = 0
112
+
113
+ obj.waitTimer -= dt
114
+ if (obj.waitTimer > 0) return
115
+
116
+ const entity = ctx.world.getEntity(id)
117
+ if (!entity) return
118
+
119
+ const current = waypoints[obj.currentWaypoint]
120
+ const next = waypoints[(obj.currentWaypoint + 1) % waypoints.length]
121
+
122
+ const dx = next[0] - entity.position[0]
123
+ const dy = next[1] - entity.position[1]
124
+ const dz = next[2] - entity.position[2]
125
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
126
+
127
+ if (dist < 0.1) {
128
+ obj.currentWaypoint = (obj.currentWaypoint + 1) % waypoints.length
129
+ obj.waitTimer = waitTime
130
+ return
131
+ }
132
+
133
+ const moveDistance = Math.min(speed * dt, dist)
134
+ entity.position[0] += (dx / dist) * moveDistance
135
+ entity.position[1] += (dy / dist) * moveDistance
136
+ entity.position[2] += (dz / dist) * moveDistance
137
+ }
138
+
139
+ function updateHazard(ctx, id, obj, dt) {
140
+ if (!obj.config) return
141
+ if (obj.damageTimer === undefined) obj.damageTimer = 0
142
+
143
+ obj.damageTimer -= dt
144
+ if (obj.damageTimer > 0) return
145
+
146
+ obj.damageTimer = obj.config.damageInterval || 0.5
147
+
148
+ const entity = ctx.world.getEntity(id)
149
+ if (!entity) return
150
+
151
+ const damage = obj.config.damage || 10
152
+ const players = ctx.players.getAll()
153
+
154
+ for (const player of players) {
155
+ if (!player.state) continue
156
+ const pp = player.state.position
157
+ const dx = pp[0] - entity.position[0]
158
+ const dy = pp[1] - entity.position[1]
159
+ const dz = pp[2] - entity.position[2]
160
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
161
+ const radius = obj.collider?.radius || 2
162
+
163
+ if (dist < radius) {
164
+ player.state.health = Math.max(0, (player.state.health || 100) - damage * dt)
165
+ ctx.network.broadcast({ type: 'hazard_damage', playerId: player.id, damage: Math.round(damage * dt) })
166
+ }
167
+ }
168
+ }
169
+
170
+ function spawnSmartObject(ctx, templateName, position, rotation = [0, 0, 0, 1], parent = null) {
171
+ if (!isValidTemplate(templateName)) {
172
+ ctx.debug.log(`[Environment] Invalid template: ${templateName}`)
173
+ return null
174
+ }
175
+
176
+ const template = getTemplate(templateName)
177
+ const id = `smart_${templateName}_${ctx.state.nextSmartObjectId++}`
178
+
179
+ const entity = ctx.world.spawn(id, {
180
+ position,
181
+ rotation,
182
+ parent,
183
+ model: template.model
184
+ })
185
+
186
+ if (!entity) return null
187
+
188
+ const isEditor = ctx.state.editorMode
189
+
190
+ entity.custom = {
191
+ smartObject: true,
192
+ template: templateName,
193
+ editorPlaceholder: isEditor,
194
+ color: getPlaceholderColor(templateName)
195
+ }
196
+
197
+ ctx.state.smartObjects.set(id, {
198
+ template: templateName,
199
+ config: { ...template.config },
200
+ collider: template.collider
201
+ })
202
+
203
+ if (!isEditor && template.collider) {
204
+ addCollider(ctx, template.collider)
205
+ }
206
+
207
+ return entity
208
+ }
209
+
210
+ export const spawn = (ctx, template, position, config) => {
211
+ const obj = spawnSmartObject(ctx, template, position)
212
+ if (obj && config) Object.assign(ctx.state.smartObjects.get(obj.id).config, config)
213
+ return obj
214
+ }
215
+
216
+ export const setEditorMode = (ctx, enabled) => { ctx.state.editorMode = enabled }
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,114 @@
1
+ // Smart Object Templates
2
+ // Define behavior, physics, and visual properties for environment entities
3
+
4
+ export const SMART_OBJECT_TEMPLATES = {
5
+ door: {
6
+ displayName: 'Door',
7
+ model: null,
8
+ collider: { type: 'box', size: [1.5, 2.5, 0.1] },
9
+ physics: 'kinematic',
10
+ editorPlaceholder: { color: 0x0066ff, dims: [1.5, 2.5, 0.1] },
11
+ config: {
12
+ open: false,
13
+ openTime: 0.5,
14
+ closeTime: 0.5,
15
+ openAngle: Math.PI / 2
16
+ }
17
+ },
18
+
19
+ platform: {
20
+ displayName: 'Moving Platform',
21
+ model: null,
22
+ collider: { type: 'box', size: [4, 0.5, 4] },
23
+ physics: 'kinematic',
24
+ editorPlaceholder: { color: 0x00cc00, dims: [4, 0.5, 4] },
25
+ config: {
26
+ waypoints: [[0, 0, 0]],
27
+ speed: 5,
28
+ waitTime: 1
29
+ }
30
+ },
31
+
32
+ trigger: {
33
+ displayName: 'Trigger Volume',
34
+ model: null,
35
+ collider: { type: 'box', size: [2, 3, 2] },
36
+ physics: 'trigger',
37
+ editorPlaceholder: { color: 0xffff00, dims: [2, 3, 2] },
38
+ config: {
39
+ eventName: 'trigger',
40
+ oneshot: false
41
+ }
42
+ },
43
+
44
+ hazard: {
45
+ displayName: 'Hazard Zone',
46
+ model: null,
47
+ collider: { type: 'sphere', radius: 2 },
48
+ physics: 'trigger',
49
+ editorPlaceholder: { color: 0xff0000, dims: [2, 2, 2] },
50
+ config: {
51
+ damage: 10,
52
+ damageInterval: 0.5
53
+ }
54
+ },
55
+
56
+ lootBox: {
57
+ displayName: 'Loot Box',
58
+ model: null,
59
+ collider: { type: 'box', size: [1, 1.5, 1] },
60
+ physics: 'dynamic',
61
+ editorPlaceholder: { color: 0x885533, dims: [1, 1.5, 1] },
62
+ config: {
63
+ lootType: 'ammo',
64
+ quantity: 30,
65
+ openSound: 'open'
66
+ }
67
+ },
68
+
69
+ pillar: {
70
+ displayName: 'Pillar/Column',
71
+ model: null,
72
+ collider: { type: 'capsule', radius: 0.5, halfHeight: 2 },
73
+ physics: 'static',
74
+ editorPlaceholder: { color: 0x888888, dims: [1, 4, 1] },
75
+ config: {
76
+ decorative: true
77
+ }
78
+ }
79
+ }
80
+
81
+ // Editor placeholder color mapping
82
+ export const PLACEHOLDER_COLORS = {
83
+ door: 0x0066ff, // Blue
84
+ platform: 0x00cc00, // Green
85
+ trigger: 0xffff00, // Yellow
86
+ hazard: 0xff0000, // Red
87
+ lootBox: 0x885533, // Brown
88
+ pillar: 0x888888, // Gray
89
+ unknown: 0xcccccc // Light gray
90
+ }
91
+
92
+ // Get placeholder color for a template
93
+ export function getPlaceholderColor(templateName) {
94
+ return PLACEHOLDER_COLORS[templateName] || PLACEHOLDER_COLORS.unknown
95
+ }
96
+
97
+ // Get placeholder dimensions for a template
98
+ export function getPlaceholderDimensions(templateName) {
99
+ const template = SMART_OBJECT_TEMPLATES[templateName]
100
+ if (!template || !template.editorPlaceholder) {
101
+ return [1, 1, 1]
102
+ }
103
+ return template.editorPlaceholder.dims
104
+ }
105
+
106
+ // Validate template exists and is accessible
107
+ export function isValidTemplate(templateName) {
108
+ return templateName in SMART_OBJECT_TEMPLATES
109
+ }
110
+
111
+ // Get template definition
112
+ export function getTemplate(templateName) {
113
+ return SMART_OBJECT_TEMPLATES[templateName] || null
114
+ }
@@ -0,0 +1,155 @@
1
+ export default {
2
+ server: {
3
+ setup(ctx) {
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
+ ctx.state.interactionCount = 0
8
+
9
+ ctx.physics.setStatic(true)
10
+ 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
+ })
46
+ },
47
+
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
+ }
56
+ }
57
+ },
58
+
59
+ client: {
60
+ setup(engine) {
61
+ this._lastMessage = null
62
+ this._messageExpire = 0
63
+ this._showingInteract = false
64
+ this._isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
65
+ this._wasRegistered = false
66
+ },
67
+
68
+ onFrame(dt, engine) {
69
+ const ent = engine.client?.state?.entities?.find(e => e.app === 'interactable')
70
+ const pos = ent?.position
71
+ const local = engine.client?.state?.players?.find(p => p.id === engine.playerId)
72
+ if (!pos || !local?.position) {
73
+ if (this._wasRegistered) {
74
+ engine.mobileControls?.unregisterInteractable(ent?.id || 'interactable')
75
+ this._wasRegistered = false
76
+ }
77
+ return
78
+ }
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
+
94
+ this._canInteract = canInteract
95
+ },
96
+
97
+ 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
110
+ },
111
+
112
+ 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
+ }
121
+ },
122
+
123
+ render(ctx) {
124
+ const h = ctx.h
125
+ const pos = ctx.entity.position
126
+ if (!h || !pos) return { position: pos }
127
+
128
+ const ui = []
129
+
130
+ 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
+ )
138
+ }
139
+
140
+ 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
+ }
153
+ }
154
+ }
155
+ }
@@ -1,23 +1,29 @@
1
1
  export default {
2
2
  server: {
3
3
  setup(ctx) {
4
+ ctx.entity.custom = { mesh: 'box', color: 0xff8800, sx: 1, sy: 1, sz: 1 }
4
5
  ctx.physics.setDynamic(true)
5
6
  ctx.physics.setMass(10)
6
- ctx.physics.addBoxCollider([1, 1, 1])
7
+ ctx.physics.addBoxCollider([0.5, 0.5, 0.5])
7
8
  },
8
- onCollision(ctx, other) {
9
- if (other.velocity > 5) {
10
- ctx.entity.destroy()
9
+ teardown(ctx) {
10
+ const ent = ctx._entity
11
+ if (ent?._physicsBodyId && ctx._runtime?._physics) {
12
+ ctx._runtime._physics.removeBody(ent._physicsBodyId)
13
+ ent._physicsBodyId = null
11
14
  }
15
+ },
16
+ update(ctx, dt) {
17
+ const ent = ctx._entity
18
+ if (!ent?._physicsBodyId || !ctx._runtime?._physics) return
19
+ const pw = ctx._runtime._physics
20
+ ent.position = pw.getBodyPosition(ent._physicsBodyId)
21
+ ent.rotation = pw.getBodyRotation(ent._physicsBodyId)
12
22
  }
13
23
  },
14
24
  client: {
15
25
  render(ctx) {
16
- return {
17
- model: ctx.entity.model,
18
- position: ctx.entity.position,
19
- rotation: ctx.entity.rotation
20
- }
26
+ return { position: ctx.entity.position, rotation: ctx.entity.rotation, custom: ctx.entity.custom }
21
27
  }
22
28
  }
23
29
  }
@@ -1,7 +1,8 @@
1
1
  const CONFIG = {
2
- spawnInterval: 120,
2
+ spawnInterval: 30,
3
3
  pickupRadius: 2.5,
4
4
  pickupLifetime: 30,
5
+ maxCrates: 20,
5
6
  crateHitRadius: 1.5,
6
7
  buffDuration: 45,
7
8
  speedMultiplier: 1.2,
@@ -19,7 +20,7 @@ const CONFIG = {
19
20
  export default {
20
21
  server: {
21
22
  setup(ctx) {
22
- ctx.state.crates = ctx.state.crates || new Map()
23
+ ctx.state.crates = ctx.state.crates || new Set()
23
24
  ctx.state.pickups = ctx.state.pickups || new Map()
24
25
  ctx.state.spawnPoints = findSpawnPoints(ctx)
25
26
  ctx.state.nextCrateId = ctx.state.nextCrateId || 0
@@ -30,6 +31,8 @@ export default {
30
31
  })
31
32
 
32
33
  console.log(`[power-crate] ${ctx.state.spawnPoints.length} spawn points, interval ${CONFIG.spawnInterval}s`)
34
+ const initialCount = Math.min(5, ctx.state.spawnPoints.length)
35
+ for (let i = 0; i < initialCount; i++) spawnCrate(ctx)
33
36
  },
34
37
 
35
38
  update(ctx, dt) {
@@ -44,7 +47,7 @@ export default {
44
47
  },
45
48
 
46
49
  teardown(ctx) {
47
- for (const id of ctx.state.crates.keys()) ctx.world.destroy(id)
50
+ for (const id of ctx.state.crates) ctx.world.destroy(id)
48
51
  for (const id of ctx.state.pickups.keys()) ctx.world.destroy(id)
49
52
  ctx.state.crates.clear()
50
53
  ctx.state.pickups.clear()
@@ -78,14 +81,12 @@ function findSpawnPoints(ctx) {
78
81
  function spawnCrate(ctx) {
79
82
  const sp = ctx.state.spawnPoints
80
83
  if (sp.length === 0) return
84
+ if (ctx.state.crates.size >= CONFIG.maxCrates) return
81
85
  const pos = sp[Math.floor(Math.random() * sp.length)]
82
86
  const id = `power_crate_${ctx.state.nextCrateId++}`
83
- ctx.world.spawn(id, {
84
- model: './world/crate.glb',
85
- position: [...pos],
86
- app: 'physics-crate'
87
- })
88
- ctx.state.crates.set(id, [...pos])
87
+ const e = ctx.world.spawn(id, { position: [...pos] })
88
+ if (e) e.custom = { mesh: 'box', color: 0xff8800, sx: 1, sy: 1, sz: 1, hover: 0.15, spin: 1 }
89
+ ctx.state.crates.add(id)
89
90
  }
90
91
 
91
92
  function handleFireEvent(ctx, data) {
@@ -93,7 +94,10 @@ function handleFireEvent(ctx, data) {
93
94
  const origin = data.origin
94
95
  const direction = data.direction
95
96
 
96
- for (const [crateId, cratePos] of ctx.state.crates) {
97
+ for (const crateId of ctx.state.crates) {
98
+ const ent = ctx.world.getEntity(crateId)
99
+ if (!ent) continue
100
+ const cratePos = ent.position
97
101
  const toTarget = [
98
102
  cratePos[0] - origin[0],
99
103
  cratePos[1] - origin[1],
@@ -113,16 +117,18 @@ function handleFireEvent(ctx, data) {
113
117
  )
114
118
  if (dist > CONFIG.crateHitRadius) continue
115
119
 
120
+ const hitPos = [cratePos[0], cratePos[1], cratePos[2]]
116
121
  ctx.world.destroy(crateId)
117
122
  ctx.state.crates.delete(crateId)
118
- spawnPickup(ctx, cratePos)
123
+ spawnPickup(ctx, hitPos)
119
124
  break
120
125
  }
121
126
  }
122
127
 
123
128
  function spawnPickup(ctx, pos) {
124
129
  const id = `powerup_${ctx.state.nextCrateId++}`
125
- ctx.world.spawn(id, { position: [...pos] })
130
+ const e = ctx.world.spawn(id, { position: [...pos] })
131
+ if (e) e.custom = { mesh: 'cylinder', r: 0.4, h: 0.1, seg: 16, color: 0xffd700, roughness: 0.3, metalness: 0.8, emissive: 0xffa000, emissiveIntensity: 0.3, rotZ: Math.PI / 2, light: 0xffd700, lightIntensity: 1, lightRange: 4, spin: 3, hover: 0.3 }
126
132
  ctx.state.pickups.set(id, { position: [...pos], lifetime: CONFIG.pickupLifetime })
127
133
  }
128
134
 
Binary file
Binary file