spoint 0.1.685 → 0.1.687

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.
@@ -136,7 +136,7 @@ export const tpsGameClient = {
136
136
  if (lp && lp.velocity && dir) { lp.velocity[0] -= dir[0] * 2; lp.velocity[2] -= dir[2] * 2 }
137
137
  const animator = engine.players.getAnimator(engine.playerId)
138
138
  if (animator) animator.shoot()
139
- tps.flash.color.setHex(0xffaa00); tps.flash.position.set(pos[0], pos[1] + 0.5, pos[2]); tps.flash.intensity = 5; tps.flash.distance = 12; tps.flashOff = Date.now() + 60
139
+ tps.flash.color.setHex(0xffaa00); tps.flash.position.set(pos[0], pos[1] + 0.5, pos[2]); tps.flash.intensity = 0; tps.flash.distance = 0; tps.flashOff = Date.now() + 60
140
140
  tps.ammo = Math.max(0, tps.ammo - 1)
141
141
  if (tps.juice) tps.juice.tone(160, 0.05, 0.22, 90)
142
142
  // tracer: origin -> full weapon range along dir; a real 'hit' event (below) shortens it to the actual impact point once the server responds, but the muzzle-to-somewhere streak reads correctly even before that RTT.
@@ -170,7 +170,7 @@ export const tpsGameClient = {
170
170
  tps._predHitAt = 0
171
171
  if (payload.headshot) { tps.headshotMarkerTime = now; if (window.__funJuice) window.__funJuice.headshot++; if (!justPredicted) tps.juice?.tone(1100, 0.08, 0.2); mobileVibrate(engine, [15, 30, 15]) }
172
172
  else { if (window.__funJuice) window.__funJuice.hit++; if (!justPredicted) tps.juice?.tone(820, 0.06, 0.18); mobileVibrate(engine, 20) }
173
- if (payload.pos && tps.flash) { tps.flash.position.set(payload.pos[0], payload.pos[1], payload.pos[2]); tps.flash.color.setHex(0xffffff); tps.flash.intensity = 4; tps.flashOff = now + 80 }
173
+ if (payload.pos && tps.flash) { tps.flash.position.set(payload.pos[0], payload.pos[1], payload.pos[2]); tps.flash.color.setHex(0xffffff); tps.flash.intensity = 0; tps.flashOff = now + 80 }
174
174
  // A player hit doesn't get a scorch decal (blood-optional per roadmap #48 -- this engine has no
175
175
  // gore toggle yet, so player hits stay decal-free; only a miss against world geometry decals below).
176
176
  // celebrate the kill now on the lethal 'hit' (RTT sooner); the 'death' path below de-dupes against this
package/client/app.js CHANGED
@@ -905,7 +905,7 @@ const engineCtx = {
905
905
  get worldConfig() { return worldConfig }, get inputConfig() { return inputConfig },
906
906
  playerVrms: pm.playerVrms, entityAppMap, kit: _designKit,
907
907
  network: { send: msg => client.send(0x33, msg) },
908
- setInputConfig(cfg) { Object.assign(inputConfig,cfg); if (!inputConfig.pointerLock) { if (clickPrompt) clickPrompt.style.display='none'; if (document.pointerLockElement) document.exitPointerLock() } },
908
+ setInputConfig(cfg) { Object.assign(inputConfig,cfg); if (!inputConfig.pointerLock) { if (clickPrompt) clickPrompt.style.display='none'; if (document.pointerLockElement) _safeExitPointerLock() } },
909
909
  players: { getMesh: id=>pm.playerMeshes.get(id), getState: id=>pm.playerStates.get(id), getAnimator: id=>pm.playerAnimators.get(id), setExpression: (id,n,v)=>pm.setVRMExpression(id,n,v), setAiming: (id,v)=>{ const s=pm.playerStates.get(id); if (s) s._aiming=v } },
910
910
  // Pooled hit decals + hitscan tracers (roadmap #48/#93). point/normal/origin/target are [x,y,z] world-space.
911
911
  decals: { spawnDecal: (point, normal) => decalSystem.spawnDecal(point, normal), spawnTracer: (origin, target) => decalSystem.spawnTracer(origin, target) },
@@ -1941,7 +1941,7 @@ editor.onSelectionChange((id,data) => {
1941
1941
  // selectEntity's machine.send('DESELECT')) clears this client's presence for every other connected editor.
1942
1942
  editorPresence.sendPresence(id, false)
1943
1943
  })
1944
- editor.onEditModeChange(on => { cam.setEditMode(on, pm.playerMeshes.get(client.playerId)); if (on) { if (document.pointerLockElement) document.exitPointerLock(); editPanel.show(); client.send(MSG.SCENE_GRAPH,{}); client.send(MSG.LIST_APPS,{}) } else { editPanel.hide(); editorPresence.hide(); editorPresence.sendPresence(null, false) } })
1944
+ editor.onEditModeChange(on => { cam.setEditMode(on, pm.playerMeshes.get(client.playerId)); if (on) { if (document.pointerLockElement) _safeExitPointerLock(); editPanel.show(); client.send(MSG.SCENE_GRAPH,{}); client.send(MSG.LIST_APPS,{}) } else { editPanel.hide(); editorPresence.hide(); editorPresence.sendPresence(null, false) } })
1945
1945
  // Reverse sync: editor.js's own Y/Alt+P keyboard shortcuts change _gizmoSpace/_pivotMode directly
1946
1946
  // (drag math needs zero round-trip latency), so mirror the toolbar's displayed value after the fact.
1947
1947
  editor.onGizmoSpaceChange(space => { try { editPanel.setGizmoSpace(space) } catch (_) {} })
@@ -2200,11 +2200,24 @@ document.addEventListener('keydown', e => { const _mod=e.ctrlKey||e.metaKey; con
2200
2200
  // Match/Invite Friends, shown on Esc when pointer-lock exits DURING ACTIVE GAMEPLAY (not the
2201
2201
  // initial click-to-play prompt -- _hasEverLocked below distinguishes the two, since pointerlockchange
2202
2202
  // fires on both the very first click-prompt dismissal and every later in-match Esc).
2203
+ // document.exitPointerLock() can throw the same class of error as requestPointerLock() in unusual
2204
+ // document/element states (observed live: "The root document of this element is not valid for
2205
+ // pointer lock" from an exitPointerLock() call, not just request). 5 call sites across this file
2206
+ // called it raw; centralize the guard here so every future call site gets it for free.
2207
+ function _safeExitPointerLock() {
2208
+ try { document.exitPointerLock() } catch (e) { console.warn('[input] exitPointerLock failed:', e?.message || e) }
2209
+ }
2210
+ function _safeRequestPointerLock() {
2211
+ try {
2212
+ const p = renderer.domElement.requestPointerLock()
2213
+ if (p && typeof p.catch === 'function') p.catch(e => console.warn('[input] requestPointerLock rejected:', e?.message || e))
2214
+ } catch (e) { console.warn('[input] requestPointerLock failed:', e?.message || e) }
2215
+ }
2203
2216
  const settingsMenu = createSettingsMenu({ getCam: () => cam, getRenderer: () => renderer })
2204
2217
  if (window.__app) window.__app.settingsMenu = settingsMenu
2205
2218
  let _hasEverLocked = false
2206
2219
  const pauseMenu = createPauseMenu({
2207
- requestPointerLock: () => { try { renderer.domElement.requestPointerLock() } catch (e) { console.warn('[input] requestPointerLock failed:', e?.message || e) } },
2220
+ requestPointerLock: _safeRequestPointerLock,
2208
2221
  settingsMenu,
2209
2222
  getRoomInfo: () => _wwRoom ? { code: _wwRoom, joinLink: `${location.origin}${location.pathname}?wwjoin&room=${_wwRoom}` } : null,
2210
2223
  })
@@ -2219,7 +2232,7 @@ document.addEventListener('keydown', e => {
2219
2232
  // Editor has its own Esc-adjacent flows (deselect etc.) via editor.onKeyDown above; don't also pop
2220
2233
  // the gameplay pause menu while editing, and don't pop it over the lobby overlay either.
2221
2234
  if (clientMachine.isEditor || clientMachine.isLobby) return
2222
- if (_hasEverLocked && document.pointerLockElement === renderer.domElement) { document.exitPointerLock() }
2235
+ if (_hasEverLocked && document.pointerLockElement === renderer.domElement) { _safeExitPointerLock() }
2223
2236
  })
2224
2237
  // Spectator mode entry/exit + follow-target-cycle keybinds. O enters spectator (S/Space are already
2225
2238
  // gameplay movement keys); inside spectator: F toggles free<->follow, [ and ] cycle the follow target.
@@ -2247,7 +2260,7 @@ function _setViewportRelaxed(on) { if (_viewportMeta) _viewportMeta.setAttribute
2247
2260
  clientMachine.subscribe(() => {
2248
2261
  const wantLobby = clientMachine.isLobby
2249
2262
  if (wantLobby && !(_lobby && _lobby.isOpen)) {
2250
- if (document.pointerLockElement) document.exitPointerLock()
2263
+ if (document.pointerLockElement) _safeExitPointerLock()
2251
2264
  _setViewportRelaxed(true)
2252
2265
  // Re-check isLobby after the async import gap in case the player toggled again before it resolved.
2253
2266
  _getLobby().then(lobby => { if (clientMachine.isLobby && !lobby.isOpen) lobby.open() })
@@ -2274,7 +2287,7 @@ let _specWasSpectator = false
2274
2287
  clientMachine.subscribe(() => {
2275
2288
  const isSpec = clientMachine.isSpectator
2276
2289
  if (isSpec && !_specWasSpectator) {
2277
- if (document.pointerLockElement) document.exitPointerLock()
2290
+ if (document.pointerLockElement) _safeExitPointerLock()
2278
2291
  if (clientMachine.spectatorSubmode === 'free') cam.setEditMode(true)
2279
2292
  } else if (!isSpec && _specWasSpectator) {
2280
2293
  cam.setEditMode(false)
@@ -2426,15 +2439,9 @@ renderer.domElement.addEventListener('click', ()=>{
2426
2439
  if (!inputConfig.pointerLock || document.pointerLockElement) return
2427
2440
  // requestPointerLock() can both throw synchronously AND return a Promise that rejects
2428
2441
  // asynchronously (spec-dependent by browser/context) -- e.g. "The root document of this
2429
- // element is not valid for pointer lock". A try/catch alone only catches the synchronous
2430
- // case; the async rejection surfaces as an unhandledrejection that still trips the app's
2431
- // global error boundary, converting a benign lock-denial into a full page crash (reproduced
2432
- // live even after the sync try/catch was added). Guard both paths. The keydown fallback below
2433
- // still recovers gameplay either way.
2434
- try {
2435
- const p = renderer.domElement.requestPointerLock()
2436
- if (p && typeof p.catch === 'function') p.catch(e => console.warn('[input] requestPointerLock rejected:', e?.message || e))
2437
- } catch (e) { console.warn('[input] requestPointerLock failed:', e?.message || e) }
2442
+ // element is not valid for pointer lock". _safeRequestPointerLock guards both. The keydown
2443
+ // fallback below still recovers gameplay either way.
2444
+ _safeRequestPointerLock()
2438
2445
  })
2439
2446
  // requestPointerLock() is a trusted-gesture-gated browser API that can silently reject (denied
2440
2447
  // engagement heuristic, automation-driven click, embedded/iframe context) with no visible error --
@@ -57,7 +57,7 @@ export async function createGrass(opts = {}) {
57
57
  // terrain-shader shell/noise band) would duplicate work the streaming ring + fade already do, for a
58
58
  // regime (grass blades near the world's own draw-distance edge) that's rarely on-screen densely enough
59
59
  // to matter. This is the documented "simpler, safely achievable" branch named in the task.
60
- const LOD_NEAR_DIST = Number.isFinite(cfg.grassLodNearDistance) ? cfg.grassLodNearDistance : 15
60
+ const LOD_NEAR_DIST = Number.isFinite(cfg.grassLodNearDistance) ? cfg.grassLodNearDistance : 8
61
61
  const geoNear = makeBladeGeo(5) // full 5-segment curved blade, 20 tris/blade (10 quads x 2 tris)
62
62
  const geoMid = makeBladeGeo(1) // flat single-segment crossed quad, 4 tris/blade
63
63
  const mat = makeGrassMaterial(wind)
@@ -290,7 +290,7 @@ export async function probeAndCreateWebGPURenderer(isMobile) {
290
290
  }
291
291
 
292
292
  export function setupLights(scene) {
293
- const ambient = new THREE.AmbientLight(0xfff4d6, 0.3)
293
+ const ambient = new THREE.AmbientLight(0xfff4d6, 0.5)
294
294
  scene.add(ambient)
295
295
  ambient.updateMatrix(); ambient.matrixAutoUpdate = false
296
296
  const studio = new THREE.DirectionalLight(0x4488ff, 0.4)
@@ -39,11 +39,11 @@ const _DEG = Math.PI / 180
39
39
  const KEYFRAMES = [
40
40
  { deg: -90, sunColor: 0x0a1030, sunIntensity: 0.0, ambientColor: 0x0d1428, ambientIntensity: 0.10 }, // deep night
41
41
  { deg: -6, sunColor: 0x1a2550, sunIntensity: 0.0, ambientColor: 0x1a2440, ambientIntensity: 0.14 }, // astronomical twilight, sun still below horizon (no direct light)
42
- { deg: 0, sunColor: 0xff7a3c, sunIntensity: 0.55, ambientColor: 0x4a4060, ambientIntensity: 0.22 }, // horizon: warm orange sunrise/sunset
43
- { deg: 8, sunColor: 0xffa552, sunIntensity: 1.8, ambientColor: 0xffcf9e, ambientIntensity: 0.55 }, // low sun: golden hour (matches tps-game.js's intended fill-light-visible palette)
44
- { deg: 30, sunColor: 0xfff0dc, sunIntensity: 1.45, ambientColor: 0xc9d4e8, ambientIntensity: 0.32 }, // mid-morning/afternoon
45
- { deg: 70, sunColor: 0xffffff, sunIntensity: 1.6, ambientColor: 0xfff4d6, ambientIntensity: 0.3 }, // near-noon, matches SceneSetup's original static values
46
- { deg: 90, sunColor: 0xffffff, sunIntensity: 1.55, ambientColor: 0xfff4d6, ambientIntensity: 0.3 }, // straight overhead
42
+ { deg: 0, sunColor: 0xff7a3c, sunIntensity: 0.55, ambientColor: 0x4a4060, ambientIntensity: 0.34 }, // horizon: warm orange sunrise/sunset
43
+ { deg: 8, sunColor: 0xffa552, sunIntensity: 1.8, ambientColor: 0xffcf9e, ambientIntensity: 0.65 }, // low sun: golden hour (matches tps-game.js's intended fill-light-visible palette)
44
+ { deg: 30, sunColor: 0xfff0dc, sunIntensity: 1.45, ambientColor: 0xc9d4e8, ambientIntensity: 0.48 }, // mid-morning/afternoon
45
+ { deg: 70, sunColor: 0xffffff, sunIntensity: 1.6, ambientColor: 0xfff4d6, ambientIntensity: 0.5 }, // near-noon, Performance-Mode flat-ambient interior floor (was 0.3, too dark indoors with no bounce/GI)
46
+ { deg: 90, sunColor: 0xffffff, sunIntensity: 1.55, ambientColor: 0xfff4d6, ambientIntensity: 0.5 }, // straight overhead
47
47
  ]
48
48
 
49
49
  const _c1 = new THREE.Color(), _c2 = new THREE.Color(), _cOut = new THREE.Color()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.685",
3
+ "version": "0.1.687",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "workspaces": [