diff --git a/cafe/.env b/cafe/.env index 1809f91e..ee96ce7b 100644 --- a/cafe/.env +++ b/cafe/.env @@ -7,5 +7,7 @@ ZSS_SHOW_CODE=false ZSS_TRACE_CODE= ZSS_LOG_DEBUG=false ZSS_FORCE_CRT_OFF=false +ZSS_FORCE_LOW_REZ=false +ZSS_FORCE_TOUCH_UI=false ZSS_HMR_ONLY=false ZSS_ANALYZER=false diff --git a/package.json b/package.json index 2a677245..f24c63db 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zed-software-system", "private": true, - "version": "0.21.11", + "version": "0.21.13", "type": "module", "scripts": { "sloc": "npx sloc zss", @@ -67,7 +67,6 @@ "msgpackr": "^1.11.2", "nanoid": "^5.0.3", "nanoid-dictionary": "^4.3.0", - "nipplejs": "^0.10.2", "path-browserify": "^1.0.0", "peerjs": "^1.5.4", "postprocessing": "^6.36.4", diff --git a/yarn.lock b/yarn.lock index 332c0b18..2c45ce98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4463,11 +4463,6 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -nipplejs@^0.10.2: - version "0.10.2" - resolved "https://registry.npmjs.org/nipplejs/-/nipplejs-0.10.2.tgz#0e8f5346bd60f7a0fe0d44c856bc1654cde2b70c" - integrity sha512-XGxFY8C2DOtobf1fK+MXINTzkkXJLjZDDpfQhOUZf4TSytbc9s4bmA0lB9eKKM8iDivdr9NQkO7DpIQfsST+9g== - node-gyp-build-optional-packages@5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz#522f50c2d53134d7f3a76cd7255de4ab6c96a3a4" diff --git a/zss/config.ts b/zss/config.ts index 0f0654e9..31757aae 100644 --- a/zss/config.ts +++ b/zss/config.ts @@ -8,6 +8,8 @@ const SHOW_CODE = !!JSON.parse(import.meta.env.ZSS_SHOW_CODE) const TRACE_CODE = `${import.meta.env.ZSS_TRACE_CODE}` const LOG_DEBUG = !!JSON.parse(import.meta.env.ZSS_LOG_DEBUG) const FORCE_CRT_OFF = !!JSON.parse(import.meta.env.ZSS_FORCE_CRT_OFF) +const FORCE_LOW_REZ = !!JSON.parse(import.meta.env.ZSS_FORCE_LOW_REZ) +const FORCE_TOUCH_UI = !!JSON.parse(import.meta.env.ZSS_FORCE_TOUCH_UI) // runtime config export const RUNTIME = { @@ -31,4 +33,6 @@ export { TRACE_CODE, LOG_DEBUG, FORCE_CRT_OFF, + FORCE_LOW_REZ, + FORCE_TOUCH_UI, } diff --git a/zss/device/api.ts b/zss/device/api.ts index c6604ce8..a565e118 100644 --- a/zss/device/api.ts +++ b/zss/device/api.ts @@ -187,6 +187,18 @@ export function tape_editor_close(sender: string, player: string) { hub.emit('tape:editor:close', sender, undefined, player) } +export function userinput_up(sender: string, input: INPUT, player: string) { + hub.emit('userinput:up', sender, input, player) +} + +export function userinput_down(sender: string, input: INPUT, player: string) { + hub.emit('userinput:down', sender, input, player) +} + +export function userinput_update(sender: string, player: string) { + hub.emit('userinput:update', sender, undefined, player) +} + export function vm_books( sender: string, books: string, diff --git a/zss/device/vm.ts b/zss/device/vm.ts index cee5236f..9c65768a 100644 --- a/zss/device/vm.ts +++ b/zss/device/vm.ts @@ -131,7 +131,6 @@ const vm = createdevice('vm', ['init', 'tick', 'second'], (message) => { break case 'input': if (message.player) { - console.info(message) // player input const flags = memoryreadflags(message.player) const [input = INPUT.NONE, mods = 0] = message.data ?? [INPUT.NONE, 0] diff --git a/zss/gadget/terminal.tsx b/zss/gadget/terminal.tsx index 82ed4608..291b7ccc 100644 --- a/zss/gadget/terminal.tsx +++ b/zss/gadget/terminal.tsx @@ -5,7 +5,13 @@ import { deviceType, primaryInput } from 'detect-it' import { useEffect, useState } from 'react' import Stats from 'stats.js' import { NearestFilter, OrthographicCamera } from 'three' -import { FORCE_CRT_OFF, RUNTIME, STATS_DEV } from 'zss/config' +import { + FORCE_CRT_OFF, + FORCE_LOW_REZ, + FORCE_TOUCH_UI, + RUNTIME, + STATS_DEV, +} from 'zss/config' import { CRTShape } from 'zss/gadget/fx/crt' import decoimageurl from 'zss/gadget/fx/scratches.gif' import { useTexture } from 'zss/gadget/usetexture' @@ -54,12 +60,13 @@ export function Terminal() { // config DRAW_CHAR_SCALE const minrez = Math.min(viewwidth, viewheight) - const islowrez = minrez < 600 + const islowrez = minrez < 600 || FORCE_LOW_REZ RUNTIME.DRAW_CHAR_SCALE = islowrez ? 1 : 2 // config LAYOUT const islandscape = viewwidth > viewheight - const showtouchcontrols = deviceType === 'hybrid' || primaryInput === 'touch' + const showtouchcontrols = + FORCE_TOUCH_UI || deviceType === 'hybrid' || primaryInput === 'touch' // grit texture const splat = useTexture(decoimageurl) diff --git a/zss/gadget/touchui/component.tsx b/zss/gadget/touchui/component.tsx index c798fe5c..ca1d0e32 100644 --- a/zss/gadget/touchui/component.tsx +++ b/zss/gadget/touchui/component.tsx @@ -1,10 +1,16 @@ /* eslint-disable react/no-unknown-property */ -import nipplejs from 'nipplejs' -import { useEffect, useRef } from 'react' +import { radToDeg } from 'maath/misc' +import { useState } from 'react' +import { Vector2, Vector3 } from 'three' import { RUNTIME } from 'zss/config' -import { tape_terminal_open, vm_input } from 'zss/device/api' +import { + tape_terminal_open, + userinput_down, + userinput_up, + vm_input, +} from 'zss/device/api' import { registerreadplayer } from 'zss/device/register' -import { ispresent } from 'zss/mapping/types' +import { snap } from 'zss/mapping/number' import { WRITE_TEXT_CONTEXT, createwritetextcontext, @@ -13,29 +19,149 @@ import { } from 'zss/words/textformat' import { COLOR } from 'zss/words/types' -import { Clickable } from '../clickable' import { INPUT } from '../data/types' import { ShadeBoxDither } from '../framed/dither' import { useTiles } from '../hooks' +import { Rect } from '../rect' import { useScreenSize } from '../userscreen' import { TilesData, TilesRender } from '../usetiles' +const motion = new Vector2() +const corner = new Vector3() + export type TouchUIProps = { width: number height: number } +function ptwithin( + x: number, + y: number, + top: number, + right: number, + bottom: number, + left: number, +) { + return x >= left && x <= right && y >= top && y <= bottom +} + export function TouchUI({ width, height }: TouchUIProps) { const screensize = useScreenSize() - const sticksref = useRef( - nipplejs.create({ - zone: document.getElementById('frame') ?? undefined, - color: '#00A', - mode: 'dynamic', - dataOnly: true, - }), - ) const player = registerreadplayer() + const [movestick] = useState({ + startx: -1, + starty: -1, + tipx: -1, + tipy: -1, + pointerId: -1 as any, + }) + // const [drawstick, setdrawstick] = useState(-1) + + function clearmovestick(cx: number, cy: number) { + if (movestick.tipx === -1) { + // check touch targets + if (ptwithin(cx, cy, 3, 6, 6, 1)) { + // top-left button + tape_terminal_open('touchui', player) + console.info('top-left') + } + if (ptwithin(cx, cy, 3, width - 2, 6, width - 6)) { + // top-right button + vm_input('touchui', INPUT.MENU_BUTTON, 0, player) + console.info('top-right') + } + if (ptwithin(cx, cy, height - 5, 6, height - 2, 1)) { + // bottom-left button + vm_input('touchui', INPUT.OK_BUTTON, 0, player) + console.info('bottom-left') + } + if (ptwithin(cx, cy, height - 5, width - 2, height - 2, width - 6)) { + // bottom-right button + vm_input('touchui', INPUT.CANCEL_BUTTON, 0, player) + console.info('bottom-right') + } + } else { + // reset input + userinput_up('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + } + // reset + movestick.startx = -1 + movestick.starty = -1 + movestick.tipx = -1 + movestick.tipy = -1 + movestick.pointerId = -1 + } + + function handlestickdir(snapdir: number) { + switch (snapdir) { + case 0: + // left + userinput_down('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + case 45: + // left up + userinput_down('touchui', INPUT.MOVE_UP, player) + userinput_down('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + case 90: + // up + userinput_down('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + break + case 135: + // up right + userinput_down('touchui', INPUT.MOVE_UP, player) + userinput_down('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + case 180: + // right + userinput_down('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + case 225: + // right down + userinput_down('touchui', INPUT.MOVE_DOWN, player) + userinput_down('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + case 270: + // down + userinput_down('touchui', INPUT.MOVE_DOWN, player) + userinput_up('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + break + case 315: + // down left + userinput_down('touchui', INPUT.MOVE_DOWN, player) + userinput_down('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_UP, player) + break + case 360: + // left + userinput_down('touchui', INPUT.MOVE_LEFT, player) + userinput_up('touchui', INPUT.MOVE_RIGHT, player) + userinput_up('touchui', INPUT.MOVE_UP, player) + userinput_up('touchui', INPUT.MOVE_DOWN, player) + break + } + } const FG = COLOR.PURPLE const BG = COLOR.ONCLEAR @@ -45,46 +171,6 @@ export function TouchUI({ width, height }: TouchUIProps) { ...store.getState(), } - const BUTTON_WIDTH = 5 - const BUTTON_HEIGHT = 3 - - useEffect(() => { - const { current } = sticksref - if (!ispresent(current)) { - return - } - - function handledirevt(evt: any) { - switch (evt.type) { - case 'removed': - break - case 'dir:up': - vm_input('touchui', INPUT.MOVE_UP, 0, player) - break - case 'dir:down': - vm_input('touchui', INPUT.MOVE_DOWN, 0, player) - break - case 'dir:left': - vm_input('touchui', INPUT.MOVE_LEFT, 0, player) - break - case 'dir:right': - vm_input('touchui', INPUT.MOVE_RIGHT, 0, player) - break - } - } - - current.on('dir:up', handledirevt) - current.on('dir:down', handledirevt) - current.on('dir:left', handledirevt) - current.on('dir:right', handledirevt) - return () => { - current.off('dir:up', handledirevt) - current.off('dir:down', handledirevt) - current.off('dir:left', handledirevt) - current.off('dir:right', handledirevt) - } - }, [player]) - // bail on odd states if (screensize.cols < 10 || screensize.rows < 10) { return null @@ -93,28 +179,73 @@ export function TouchUI({ width, height }: TouchUIProps) { // render ui textformatedges(1, 1, width - 2, height - 2, context) - // action button targets + // draw action button targets context.y = 3 for (let i = 0; i < 3; ++i) { context.x = context.active.leftedge = 1 tokenizeandwritetextformat(`$BLUE$177$177$177$177$177`, context, false) context.x = context.active.leftedge = width - 7 - tokenizeandwritetextformat(`$PURPLE$177$177$177$177$177`, context, false) - ++context.y + tokenizeandwritetextformat(`$PURPLE$177$177$177$177$177\n`, context, false) } - context.y = height - 5 for (let i = 0; i < 3; ++i) { context.x = context.active.leftedge = 1 tokenizeandwritetextformat(`$GREEN$177$177$177$177$177`, context, false) context.x = context.active.leftedge = width - 7 - tokenizeandwritetextformat(`$RED$177$177$177$177$177`, context, false) - ++context.y + tokenizeandwritetextformat(`$RED$177$177$177$177$177\n`, context, false) } return ( + { + if (movestick.startx === -1) { + movestick.startx = e.x + movestick.starty = e.y + movestick.tipx = -1 + movestick.tipy = -1 + movestick.pointerId = e.pointerId + } else { + // flag as shooting now + userinput_down('touchui', INPUT.SHIFT, player) + } + }} + onPointerMove={(e) => { + if (e.pointerId === movestick.pointerId) { + // calc angle + motion.set(movestick.startx - e.x, movestick.starty - e.y) + if (motion.length() > 42) { + const snapdir = snap(radToDeg(motion.angle()), 45) + // track for visuals + movestick.tipx = e.x + movestick.tipy = e.y + // invoke input directions + handlestickdir(snapdir) + } + } + }} + onPointerUp={(e) => { + if (e.pointerId === movestick.pointerId) { + corner.copy(e.intersections[0].point) + e.intersections[0].object.worldToLocal(corner) + const dx = + Math.floor(width * 0.5) + + Math.floor(corner.x / RUNTIME.DRAW_CHAR_WIDTH()) + const dy = + Math.floor(height * 0.5) + + Math.floor(corner.y / RUNTIME.DRAW_CHAR_HEIGHT()) + clearmovestick(dx, dy) + } else { + // flag off shift + userinput_up('touchui', INPUT.SHIFT, player) + } + }} + /> - - { - // top-left button - tape_terminal_open('touchui', player) - }} - /> - - - { - // top-right button - vm_input('touchui', INPUT.MENU_BUTTON, 0, player) - }} - /> - - - { - // bottom-left button - vm_input('touchui', INPUT.OK_BUTTON, 0, player) - }} - /> - - - { - // bottom-right button - vm_input('touchui', INPUT.CANCEL_BUTTON, 0, player) - }} - /> - ) diff --git a/zss/gadget/userinput.tsx b/zss/gadget/userinput.tsx index 297e5630..5df7d9fa 100644 --- a/zss/gadget/userinput.tsx +++ b/zss/gadget/userinput.tsx @@ -9,9 +9,11 @@ import { useEffect, useState, } from 'react' +import { createdevice } from 'zss/device' import { vm_cli } from 'zss/device/api' import { registerreadplayer } from 'zss/device/register' import { INPUT } from 'zss/gadget/data/types' +import { isnumber } from 'zss/mapping/types' import { ismac } from 'zss/words/system' import { NAME } from 'zss/words/types' @@ -254,6 +256,54 @@ document.addEventListener( { capture: true }, ) +createdevice('userinput', ['tock'], (message) => { + switch (message.target) { + case 'up': + if (isnumber(message.data)) { + inputup(message.data) + } + break + case 'down': + if (isnumber(message.data)) { + inputdown(message.data) + } + break + case 'update': { + const now = performance.now() + const delta = now - previous + + acc += delta + if (acc >= INPUT_RATE) { + acc %= INPUT_RATE + // signal input state + const mods: UserInputMods = { + alt: !!inputstate[INPUT.ALT], + ctrl: !!inputstate[INPUT.CTRL], + shift: !!inputstate[INPUT.SHIFT], + } + const inputs = [ + INPUT.MOVE_UP, + INPUT.MOVE_DOWN, + INPUT.MOVE_LEFT, + INPUT.MOVE_RIGHT, + INPUT.OK_BUTTON, + INPUT.CANCEL_BUTTON, + INPUT.MENU_BUTTON, + ] + inputs.forEach((input) => { + if (inputstate[input]) { + userinputinvoke(input, mods) + } + }) + } + + previous = now + GamepadHelper.update() + break + } + } +}) + // gamepad input const BUTTON_A = 0 @@ -280,9 +330,9 @@ const buttonlookup: Record = { [BUTTON_X]: INPUT.OK_BUTTON, [BUTTON_Y]: INPUT.CANCEL_BUTTON, [BUTTON_LEFT_SHOULDER]: INPUT.ALT, - [BUTTON_RIGHT_SHOULDER]: INPUT.ALT, - [BUTTON_LEFT_TRIGGER]: INPUT.CTRL, - [BUTTON_RIGHT_TRIGGER]: INPUT.CTRL, + [BUTTON_RIGHT_SHOULDER]: INPUT.CTRL, + [BUTTON_LEFT_TRIGGER]: INPUT.SHIFT, + [BUTTON_RIGHT_TRIGGER]: INPUT.SHIFT, [BUTTON_MENU]: INPUT.MENU_BUTTON, [BUTTON_UP]: INPUT.MOVE_UP, [BUTTON_DOWN]: INPUT.MOVE_DOWN, @@ -302,41 +352,6 @@ document.addEventListener('gamepadbuttonup', (event: GamepadEvent) => { inputup(buttonlookup[event.detail.button]) }) -function inputpoll() { - const now = performance.now() - const delta = now - previous - - acc += delta - if (acc >= INPUT_RATE) { - acc %= INPUT_RATE - // signal input state - const mods: UserInputMods = { - alt: !!inputstate[INPUT.ALT], - ctrl: !!inputstate[INPUT.CTRL], - shift: !!inputstate[INPUT.SHIFT], - } - const inputs = [ - INPUT.MOVE_UP, - INPUT.MOVE_DOWN, - INPUT.MOVE_LEFT, - INPUT.MOVE_RIGHT, - INPUT.OK_BUTTON, - INPUT.CANCEL_BUTTON, - INPUT.MENU_BUTTON, - ] - inputs.forEach((input) => { - if (inputstate[input]) { - userinputinvoke(input, mods) - } - }) - } - - previous = now - GamepadHelper.update() - setTimeout(inputpoll, 1) -} -inputpoll() - // mouse && touch input - used to activate :tap labels // components diff --git a/zss/userspace.ts b/zss/userspace.ts index f09270ba..2f764651 100644 --- a/zss/userspace.ts +++ b/zss/userspace.ts @@ -2,6 +2,15 @@ import './device/peer' import './device/gadgetclient' import './device/modem' -import './device/register' import './device/synth' import './device/tape' +import { userinput_update } from './device/api' +import { registerreadplayer } from './device/register' + +function inputpolling() { + const player = registerreadplayer() + userinput_update('userspace', player) + setTimeout(inputpolling, 10) +} + +inputpolling()