openvibers/openvibe.games Commits

Top-down worlds: a press on the game canvas acts without pointer lock (tools, gathering, the hotbar wheel); the HUD and menus keep their own clicks (#170)

OP OpenVibers committed 17 days ago · 4e65aa023e · 3 files, +68 −3

Changes

ddiff --git a/apps/client/src/input/inputTracker.test.ts b/apps/client/src/input/inputTracker.test.ts
nnew file mode 100644
iindex 0000000..5edc0ca
---- /dev/null
++++ b/apps/client/src/input/inputTracker.test.ts
@@@ -0,0 +1,49 @@
++// @vitest-environment jsdom
++import { describe, expect, it } from 'vitest'
++import { InputTracker } from './inputTracker.js'
++
++function setup(pointerLock: boolean) {
++ document.body.innerHTML = '<canvas id="game"></canvas><button id="hud">Inventory</button>'
++ const canvas = document.getElementById('game') as HTMLCanvasElement
++ const hud = document.getElementById('hud') as HTMLButtonElement
++ canvas.requestPointerLock = (() => undefined) as never
++ const input = new InputTracker(canvas)
++ input.pointerLock = pointerLock
++ const actions: string[] = []
++ const wheel: number[] = []
++ input.onAction = (a) => actions.push(a.kind)
++ input.onWheel = (d) => wheel.push(d)
++ const press = (target: Element, buttons: number) =>
++ target.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, buttons }))
++ const release = (target: Element) =>
++ target.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, buttons: 0 }))
++ const scroll = (target: Element) =>
++ target.dispatchEvent(new WheelEvent('wheel', { bubbles: true, deltaY: 120 }))
++ return { canvas, hud, actions, wheel, press, release, scroll }
++}
++
++describe('input tracker presses', () => {
++ it('acts on a press on the game canvas in a world seen from above (no pointer lock)', () => {
++ const { canvas, hud, actions, wheel, press, release, scroll } = setup(false)
++ press(canvas, 1)
++ release(canvas)
++ press(canvas, 2)
++ expect(actions).toEqual(['primary_down', 'primary_up', 'rmb_down'])
++ scroll(canvas)
++ expect(wheel).toEqual([1])
++ // The HUD and the menus keep their own clicks and wheel.
++ actions.length = 0
++ press(hud, 1)
++ scroll(hud)
++ expect(actions.filter((a) => a !== 'primary_up')).toEqual([])
++ expect(wheel).toEqual([1])
++ })
++
++ it('still waits for the pointer lock in first person', () => {
++ const { canvas, actions, wheel, press, scroll } = setup(true)
++ press(canvas, 1)
++ scroll(canvas)
++ expect(actions).toEqual([])
++ expect(wheel).toEqual([])
++ })
++})
ddiff --git a/apps/client/src/input/inputTracker.ts b/apps/client/src/input/inputTracker.ts
iindex 84e5deb..5e6d530 100644
---- a/apps/client/src/input/inputTracker.ts
++++ b/apps/client/src/input/inputTracker.ts
@@@ -17,6 +17,17 @@ export class InputTracker {
uiCapture = false
/** False for a world seen from above: the pointer stays visible and aims instead of turning the view. */
pointerLock = true
++ /** Whether the current press started on the game canvas (not the HUD or a menu). */
++ private pressOnCanvas = false
++
++ /**
++ * Whether presses and the wheel act in the game: with pointer lock, while it is held; in a world seen from above
++ * (no lock), when the press is on the game canvas. A top-down player used to walk but never swing, gather or scroll
++ * the hotbar, because every press waited for a lock that never comes.
++ */
++ private engaged(onCanvas = this.pressOnCanvas): boolean {
++ return this.locked || (!this.pointerLock && onCanvas)
++ }
onAction: ((action: InputAction) => void) | null = null
onWheel: ((delta: number) => void) | null = null
@@@ -36,7 +47,7 @@ export class InputTracker {
// Releases always fire — a grab must never stick because pointer lock
// or the menu state changed mid-hold. New presses are gameplay-gated.
if (released & 1) this.onAction?.({ kind: 'primary_up' })
-- if (this.uiCapture || !this.locked) return
++ if (this.uiCapture || !this.engaged()) return
if (pressed & 1) this.onAction?.({ kind: 'primary_down' })
if (pressed & 2) this.onAction?.({ kind: 'rmb_down' })
}
@@@ -113,7 +124,8 @@ export class InputTracker {
})
document.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'touch') return
-- if (!this.locked || this.uiCapture) {
++ this.pressOnCanvas = e.target === this.canvas
++ if (!this.engaged() || this.uiCapture) {
this.buttonsState = e.buttons
return
}
@@@ -137,7 +149,8 @@ export class InputTracker {
document.addEventListener(
'wheel',
(e) => {
-- if (this.locked && !this.uiCapture) this.onWheel?.(Math.sign(e.deltaY))
++ if (this.engaged(e.target === this.canvas) && !this.uiCapture)
++ this.onWheel?.(Math.sign(e.deltaY))
},
{ passive: true },
)
ddiff --git a/test/apps__client__src__input__inputTracker.test.js b/test/apps__client__src__input__inputTracker.test.js
nnew file mode 100644
iindex 0000000..3babf84
---- /dev/null
++++ b/test/apps__client__src__input__inputTracker.test.js
@@@ -0,0 +1,3 @@
++// file: 'apps/client/src/input/inputTracker.test.ts'
++// covers: 'apps/client/src/input'
++// Generated by node test/run.js --index; do not edit.