initial commit
This commit is contained in:
commit
e365a7565d
19 changed files with 17823 additions and 0 deletions
BIN
assets/room.blend
Normal file
BIN
assets/room.blend
Normal file
Binary file not shown.
BIN
assets/room.blend1
Normal file
BIN
assets/room.blend1
Normal file
Binary file not shown.
BIN
assets/room.glb
Normal file
BIN
assets/room.glb
Normal file
Binary file not shown.
30
index.html
Normal file
30
index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"
|
||||
integrity="sha512-avLcnGxl5mqAX/wIKERdb1gFNkOLHh2W5JNCfJm5OugpEPBz7LNXJJ3BDjjwO00AxEY1MqdNjtEmiYhKC0ld7g=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<!-- <script
|
||||
src="https://cdn.jsdelivr.net/gh/c-frame/aframe-physics-system@v4.2.2/dist/aframe-physics-system.js"></script> -->
|
||||
<script src="/public/js/aframe-physics-system.js"></script>
|
||||
<script src="/src/index.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a-scene physics="restitution: 0; stats: panel" stats tick-time-display="sceneOutputEl: #tickTimerScene"
|
||||
renderer="physicallyCorrectLights: true">
|
||||
<a-assets>
|
||||
<a-asset-item id="room" src="/assets/room.glb"></a-asset-item>
|
||||
</a-assets>
|
||||
<!-- <a-entity gltf-model="#room" static-body></a-entity> -->
|
||||
<a-box scale="2 2 2" position="0 -1 0" static-body></a-box>
|
||||
|
||||
<a-box is-controlled position="0 1.6 0" dynamic-body>
|
||||
<a-entity camera="active: true"></a-entity>
|
||||
</a-box>
|
||||
</a-scene>
|
||||
</body>
|
||||
|
||||
</html>
|
28
key.pem
Normal file
28
key.pem
Normal file
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCU1tVf88lt1Jl9
|
||||
xk/3xLN9dc4mSegOXvsXa1UidgskfDea6SIuCNHQshOfB1FbByFi2rjXvh4/RWaJ
|
||||
mc+ogBhWdwqIIjJ5IrS+5SSAD2tnZ47mCvEKGFo8rMBCMH+NSeGxw7FTr41o9ahz
|
||||
2lD7gABlNNRz/X7KVZMjYwRU/l3RFDuR1Aa71o6adBa6cGwS08O4vylSKmuMWifs
|
||||
u6ki651vtTg5K4W9Sxcqe+LHV+mok/U4+29GyR1fM6yTl6d3wK0bYrFiZgNmigel
|
||||
lKgjPwwmMAr0R8w52boTi0chLBiD8AYfVieiPNrIJalgZO6la/56ec3uzUN0lpMs
|
||||
/pAOKYcHAgMBAAECggEAM9O0/BLzNnPbuMCn0uJjD9JIJOwCSpLHQynkPLo2+g7o
|
||||
+9+nP7lhehawH2WYbiaG+RmpWLKCbMy5PjvZY9aq4tzTxYCG68hqgLhgo9j6sMiO
|
||||
KUtxEG9XaJ5M2h9yItwxo0k9w6KzDnPQIvtHzLOPL66cNgF0t3bh4+YUOZZTS7yE
|
||||
ZwPQ15d/7IxSrYfqkQRv0nlTJnkhB6tHNACHIan+sAvBps2NahKt7iOZzwLcrbKT
|
||||
tPF3qrZ3xJmcxI8fSfzFPAmjYH+zywr/HgqtIl9ivOuIQGjN7lyiSQVOsOiBTYhw
|
||||
RT6YLpKUJt9b/79/yBvctB9tYMzmkT8h7Sp1HuvYJQKBgQDF++jhuxux8KI5ZV6r
|
||||
CXH/BsNlEnzB22S8BLpXF1oZ2KEofa0+7m89/xl5zAosxnOVJB+Fh5h3VD7sPk3Q
|
||||
Rq+0we7kdwOpumu/MBZeeCI3mrzewhILvTEWjamG0g76HrIs4hRqH7V0nrcp1D1f
|
||||
WMtLC69gC+OsfW4LwhdBG3mHtQKBgQDAdD7MdGlbhumJoBasCv3c3vomWi7u6gEY
|
||||
Yp7owyNZd1Zz77XwEyWvX5CEPua83xVh/QH1J0wJm3TM6vXDdT+fi15B3YvAqXH2
|
||||
ODB45UQ0Qj0BMsDfnt90K424dDtavJC2YglWSpT29tDjEn1qkJiMK4VIFbcwVJoU
|
||||
wVbj9TTRSwKBgQCv28wHPfEixHEEdIN7f19PvU0waRSCCYPX8ocmNurkjGTSF5gc
|
||||
jyQmuIKDinb7QKeIMVcCf+gxDpb5t3nc5/zInX66VvDdAWcVovwrm6AhLgRYBaCL
|
||||
5thEhT6xL8L37u4rKgIiJdpDJXvi3tBI8hFZrp5vvP/VxnBKZ3PKdEImBQKBgQCK
|
||||
8yLoEOCEkZqJoAjefC0fy+mFyTT7j/3scj/TRBiAnrXPjPGp/NkHjpLTuGp1UQeq
|
||||
MLa6Hn46rX+d98IrNbsS4NfT7DXbiztZjuYIU1dz96L9+3vfuGATCuMOmvTDpUNC
|
||||
OvQM1lpJGJhmadIdH3rUMRPQoMUbYunANHscqMxSkwKBgQDE5u38mdqv3k4xb0Wf
|
||||
wgmz0P9GA3C786toADZJXoPgg5f4hSAZJ0DcShD6ThRa/X3XwcrXqAYCeaD+wp4j
|
||||
iGFNh8MamnqesumEZLagDbbc8QyRPmYRQZ1Uip+aFnuC89ceDPoztbK/36V03qMo
|
||||
CuY1EZf360FRN880r3UJ3R1YHQ==
|
||||
-----END PRIVATE KEY-----
|
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "messenger",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": ""
|
||||
}
|
17054
public/js/aframe-physics-system.js
Normal file
17054
public/js/aframe-physics-system.js
Normal file
File diff suppressed because it is too large
Load diff
49
server.pem
Normal file
49
server.pem
Normal file
|
@ -0,0 +1,49 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChRWdIBdkHuR19
|
||||
JevHyDjgIa4H2ZuXaXvDOvjC4XtAvVrNeuYBMC6GPTqH+YgPAQ2E7wWcI90Jy+nb
|
||||
6rfo4o7hH+TyaydWZXagnmuMIYNRBQ7QjrvIsdeK3ZxIOF1u86qarjN24aIp9w/+
|
||||
FRwmEFBoQg7HynjV8A9sij6Rd7zrjWapMh5UiUkS0L4QGaam4WpKTJ+Nxww/IDDx
|
||||
gaKOpSoKwLbanpv33dVy65XUhxkqL6m1s8eCMmgJqS/xjNEndgO+up+yOFOHVSs2
|
||||
5F75F/pYzS5cnYtp8PxYq03l5XOjp7jM3l6JcK0AN5eEm9PK7YVtAoEnJmzO3p1m
|
||||
6+fsJF8vAgMBAAECggEAAZEKGfPF0hZO3minla3YOqtKUN6+bRnLXfnR55BCY8gk
|
||||
VQx/V3kmzjyOVU5EdGNRnWCneob2CrZR6gFHOekjVd0dc01kFpYlHXh2VNSa0Qb4
|
||||
Knyf2DLuXMSBGQ7OL7JXeodU0xxYc4kjKWbWyVB8LZ+0L4NrX9dpIqM9wtBK8r2S
|
||||
e7ha5B/0cFV7BEXlc2fDLxBfGBc0s+Z9wJnoomg0Nc2PJrIvwd1TdU5kxR+nXfPl
|
||||
jUuU5xrr2ALpHcH38Iz0oDxiDoSWbt5iy1UWKdGI9Boxr9BjBC6cMaAVbAgukykm
|
||||
+XMaT/NcYPwyna+k5YFQ1UlakVRmL1SbLSV5Xj0l4QKBgQDNOdpUtRArIQw7xc//
|
||||
IDzaevzGso1yxfbYXtNU4X2mav0NuhpVr5g6F0iR1wmxSDs7afyTKlnmFjLournR
|
||||
lDBffEHMsZz8e40wCkLWuyrq6ZVOlho6ptEQy0o7Sp2M/qItZ7j2L136T5kHTGGo
|
||||
aLP006r7jKF56Kv+tY3Ujsd0LQKBgQDJK6JWXCHoQK3BjdlbCBKhmqhQMebgHK/r
|
||||
b43CmWQ0H7u6t8wKvzypRVzkI04RHqG65qqQ7WJFoNDEaK9Mwqwc6diJVEyivZ/3
|
||||
RDAWem2dr4e0KCtRZkp0zJ41NG8Quuh5pSVhL1TirqfBksF7mCXw7dZIBRiht2sl
|
||||
mPpzXzJuSwKBgAL8xprZD+Uw0/bCyGZUV85uX+OckcaL7y3zI2xm4+daQ1jBzsRU
|
||||
jAi8cgf9c1yrSoFw54ZU5X4d8JvGrBHK8HFTY7nRFBHw0ntSiuqJSvSkddHxCWy1
|
||||
JPHzo579VRHKWx4CFoUu51VPd1DN4dYLwCLeOezzfR5DLs3EYs4nTev5AoGBAJcD
|
||||
f2jO3tFHhGgxAMJ6S+oufD0/SK5iOplFc6hql7MdQ0LGaJCEwJfdLv8/lYH6ebQh
|
||||
JleHK+dS8oUOhNHdrXBDBz8IHkNoj+YBtQuauiDOPNO799CyNiB53iXYh6uKc6ul
|
||||
kmrjKhjEvb/tV/tc2taC5AHKeZQkaXWe4hurkeMDAoGBAJQGeheJ8wON5Cwlgufr
|
||||
WVS3lSpCB6G9G1mbZuXAPrR+SELpneDlCwId+hDMos2GAoUmKQNz0p2JOBn4tn77
|
||||
3iarxaZpdsGT8J4psaim1amIcnj08Xpls/ksrioGcWESqprs+dsxKVv99yB3sK/h
|
||||
m1mTEq6zT9KZ4Q8hs3p07aVp
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUDlkUxhN5QbSER3NibDuBtMOchlAwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTAyMjYwOTE3NDRaFw0yNjAy
|
||||
MjYwOTE3NDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQChRWdIBdkHuR19JevHyDjgIa4H2ZuXaXvDOvjC4XtA
|
||||
vVrNeuYBMC6GPTqH+YgPAQ2E7wWcI90Jy+nb6rfo4o7hH+TyaydWZXagnmuMIYNR
|
||||
BQ7QjrvIsdeK3ZxIOF1u86qarjN24aIp9w/+FRwmEFBoQg7HynjV8A9sij6Rd7zr
|
||||
jWapMh5UiUkS0L4QGaam4WpKTJ+Nxww/IDDxgaKOpSoKwLbanpv33dVy65XUhxkq
|
||||
L6m1s8eCMmgJqS/xjNEndgO+up+yOFOHVSs25F75F/pYzS5cnYtp8PxYq03l5XOj
|
||||
p7jM3l6JcK0AN5eEm9PK7YVtAoEnJmzO3p1m6+fsJF8vAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBSmi8GRzzJQKWE8OkGV2pJpn8AliDAfBgNVHSMEGDAWgBSmi8GRzzJQKWE8
|
||||
OkGV2pJpn8AliDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAB
|
||||
6v7FIFsF03PORzhdxeK4opzcNHeHHcaI18fpy/sGERhWhewXHmU7Zp+PLyZArIOI
|
||||
glYbDFQcT+L+NwpfQ/jk0hviOhxLlNPW9qADBbhc9zAMfBgimKd7jCdnT/kD/q9n
|
||||
Rvui7NAxmaJ0FwON7a/CmmTa7lJBXtjNHK1Ree7XRQptG3AMCSQUohgMLUsa+rlH
|
||||
acoCdQquEcnIlIOToByTJdK2TUdLn2yVikhVuTN+8SOB4fCPXBLl0DmpeDJ7NcNk
|
||||
MpVmRzc4Jijd4On6kb2CyNSpgfSgCDRiE7fG+Mi7SeCUhgQcO3SlgFrzRwHZKAhT
|
||||
pcrHNSvoXE25MUX6LQuu
|
||||
-----END CERTIFICATE-----
|
18
server.py
Executable file
18
server.py
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
|
||||
port = 4443
|
||||
|
||||
httpd = HTTPServer(("localhost", port), SimpleHTTPRequestHandler)
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(Path(__file__).parent / "server.pem")
|
||||
httpd.socket = ssl_context.wrap_socket(
|
||||
httpd.socket,
|
||||
server_side=True,
|
||||
)
|
||||
|
||||
print(f"Serving on https://localhost:{port}")
|
||||
httpd.serve_forever()
|
24
src/hash.js
Normal file
24
src/hash.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
export class Hash {
|
||||
#algorithm
|
||||
#buffer
|
||||
|
||||
constructor(buf, algorithm = 'md5') {
|
||||
this.#algorithm = algorithm
|
||||
this.#buffer = crypto.subtle.digest(algo, buf)
|
||||
}
|
||||
|
||||
static fromString(str, algorithm = 'md5') {
|
||||
const enc = new TextEncoder('utf8')
|
||||
const buf = enc.encode(str)
|
||||
return new Hash(buf, algorithm)
|
||||
}
|
||||
|
||||
get buffer() {
|
||||
return this.#buffer
|
||||
}
|
||||
|
||||
get algorithm() {
|
||||
return this.#algorithm
|
||||
}
|
||||
}
|
||||
|
5
src/index.js
Normal file
5
src/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
AFRAME.registerComponent("is-controlled", {
|
||||
tick(time, delta) {
|
||||
}
|
||||
})
|
||||
|
336
src/input.js
Normal file
336
src/input.js
Normal file
|
@ -0,0 +1,336 @@
|
|||
import Buffer from './ring-buffer.js'
|
||||
|
||||
export const Button = Object.freeze({
|
||||
South: 0,
|
||||
East: 1,
|
||||
West: 2,
|
||||
North: 3,
|
||||
BumperLeft: 4,
|
||||
BumperRight: 5,
|
||||
TriggerLeft: 6,
|
||||
TriggerRight: 7,
|
||||
Select: 8,
|
||||
Start: 9,
|
||||
AnalogLeft: 10,
|
||||
AnalogRight: 11,
|
||||
DPadUp: 12,
|
||||
DPadDown: 13,
|
||||
DPadLeft: 14,
|
||||
DPadRight: 15,
|
||||
Logo: 16
|
||||
})
|
||||
|
||||
export const Axis = Object.freeze({
|
||||
LeftX: 0,
|
||||
LeftY: 1,
|
||||
RightX: 2,
|
||||
RightY: 3
|
||||
})
|
||||
|
||||
export const InputState = Object.freeze({
|
||||
Disabled: 0,
|
||||
Waiting: 1,
|
||||
Started: 2,
|
||||
Performed: 3,
|
||||
Canceled: 4
|
||||
})
|
||||
|
||||
export const Device = Object.freeze({
|
||||
Any: 0,
|
||||
KeyboardAndMouse: 1,
|
||||
Gamepad: 2
|
||||
})
|
||||
|
||||
class InputEvent extends CustomEvent {
|
||||
constructor(value, type, source, data) {
|
||||
super('oninput', {
|
||||
detail: {
|
||||
value,
|
||||
type,
|
||||
source,
|
||||
...data
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class GamepadEvent extends InputEvent {
|
||||
constructor(value, source) {
|
||||
super(value, Device.Gamepad, source)
|
||||
}
|
||||
}
|
||||
|
||||
class KeyboardEvent extends InputEvent {
|
||||
constructor(value, source, state, modifiers) {
|
||||
super(value, Device.KeyboardAndMouse, source, {
|
||||
modifiers,
|
||||
state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class InputDevice extends EventTarget { }
|
||||
|
||||
class Gamepads extends InputDevice {
|
||||
#indices
|
||||
#raf
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.#indices = new Set()
|
||||
this.#raf = undefined
|
||||
|
||||
window.addEventListener('gamepadconnected', e => {
|
||||
this.#indices.add(e.gamepad.index)
|
||||
})
|
||||
|
||||
window.addEventListener('gamepaddisconnected', e => {
|
||||
this.#indices.delete(e.gamepad.index)
|
||||
})
|
||||
}
|
||||
|
||||
static get Button() {
|
||||
return Button
|
||||
}
|
||||
|
||||
static get Axis() {
|
||||
return Axis
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
this.#poll()
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
cancelAnimationFrame(this.#raf)
|
||||
}
|
||||
|
||||
#poll() {
|
||||
this.#raf = requestAnimationFrame(() => {
|
||||
this.#testInputs()
|
||||
this.#poll()
|
||||
})
|
||||
}
|
||||
|
||||
#testInputs() {
|
||||
const gamepads = navigator.getGamepads()
|
||||
for (const index of this.#indices) {
|
||||
const gamepad = gamepads[index]
|
||||
for (const button of gamepad.buttons) {
|
||||
this.dispatchEvent(new GamepadEvent(button, gamepad))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const KeyState = Object.freeze({
|
||||
Up: 0,
|
||||
Down: 1
|
||||
})
|
||||
|
||||
export class KeyModifier {
|
||||
static Alt = 1
|
||||
static Ctrl = 2
|
||||
static Meta = 4
|
||||
static Shift = 8
|
||||
|
||||
constructor() {
|
||||
throw new TypeError("can't use 'new' with an enum")
|
||||
}
|
||||
|
||||
static fromString(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce((acc, x) => acc | KeyModifier.fromString(x), 0)
|
||||
}
|
||||
|
||||
switch (value.toLowerCase()) {
|
||||
case "alt": return 1
|
||||
case "ctrl": return 2
|
||||
case "meta": return 4
|
||||
case "shift": return 8
|
||||
}
|
||||
}
|
||||
|
||||
static fromEvent(e) {
|
||||
let result = 0
|
||||
|
||||
if (e.altKey) { result |= KeyModifier.Alt }
|
||||
if (e.ctrlKey) { result |= KeyModifier.Ctrl }
|
||||
if (e.metaKey) { result |= KeyModifier.Meta }
|
||||
if (e.shiftKey) { result |= KeyModifier.Shift }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static has(flags, value) {
|
||||
return flags & value
|
||||
}
|
||||
|
||||
static set(flags, value) {
|
||||
return flags | value
|
||||
}
|
||||
|
||||
static toString(value) {
|
||||
let result = []
|
||||
|
||||
if (value & 1) { result += "Alt" }
|
||||
if (value & 2) { result += "Ctrl" }
|
||||
if (value & 4) { result += "Meta" }
|
||||
if (value & 8) { result += "Shift" }
|
||||
|
||||
return result.join(',')
|
||||
}
|
||||
}
|
||||
|
||||
class Keyboard extends InputDevice {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
const state = e.repeat ? InputState.Performed : InputState.Started
|
||||
|
||||
this.dispatchEvent(
|
||||
new KeyboardEvent(e.code, e, state, KeyModifier.fromEvent(e))
|
||||
)
|
||||
})
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
this.dispatchEvent(
|
||||
new KeyboardEvent(e.code, e, InputState.Canceled, KeyModifier.fromEvent(e))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const filterProp = (prop, value, xs) => xs.filter(x => x[prop] === value)
|
||||
|
||||
class BaseAction {
|
||||
#values
|
||||
|
||||
constructor(values) {
|
||||
this.#values = new Set(values)
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.#values
|
||||
}
|
||||
|
||||
get device() {
|
||||
return Device.Any
|
||||
}
|
||||
|
||||
*find(inputs) {
|
||||
for (const value of this.#values) {
|
||||
for (const input of inputs) {
|
||||
if (input.value === value) {
|
||||
yield input
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findFirst(inputs) {
|
||||
return this.find(inputs).next()
|
||||
}
|
||||
|
||||
test(input) {
|
||||
return this.findFirst(input) ? true : false
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardAction extends BaseAction {
|
||||
#modifiers
|
||||
|
||||
constructor(values) {
|
||||
const parts = values.split('+')
|
||||
super(parts.slice(-1))
|
||||
this.#modifiers = KeyModifier.fromString(parts.slice(0, -1))
|
||||
}
|
||||
|
||||
get device() {
|
||||
return Device.KeyboardAndMouse
|
||||
}
|
||||
|
||||
test(input) {
|
||||
const filtered = input.filter(v => v.type === this.device && v.state === InputState.Started)
|
||||
for (const match of this.find(filtered)) {
|
||||
if (match.modifiers == this.#modifiers) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export class GamepadAction extends BaseAction {
|
||||
constructor(values) {
|
||||
super(values)
|
||||
}
|
||||
|
||||
get device() {
|
||||
return Device.Gamepad
|
||||
}
|
||||
|
||||
test(input) {
|
||||
super.test(
|
||||
filterProp('device', this.device, input)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class Action {
|
||||
static keyboard(input) {
|
||||
return new KeyboardAction(input)
|
||||
}
|
||||
|
||||
static gamepad(input) {
|
||||
return new GamepadAction(input)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ActionMap {
|
||||
constructor(devices, bufferSize = 16) {
|
||||
this.buffer = Buffer(bufferSize)
|
||||
this.actions = new Map()
|
||||
this.devices = !devices ? this.initializeDefaults() : devices
|
||||
|
||||
this.devices.forEach(device => device.addEventListener('oninput', this.#handle.bind(this)))
|
||||
}
|
||||
|
||||
initializeDefaults() {
|
||||
return [
|
||||
new Keyboard(),
|
||||
new Gamepads()
|
||||
]
|
||||
}
|
||||
|
||||
bind(action, callback) {
|
||||
this.actions.set(action, callback)
|
||||
return this
|
||||
}
|
||||
|
||||
unbind(action) {
|
||||
this.actions.delete(action)
|
||||
}
|
||||
|
||||
#handle(e) {
|
||||
this.buffer.enqueue(e.detail)
|
||||
this.#test()
|
||||
}
|
||||
|
||||
#test() {
|
||||
for (const [action, callback] of this.actions.entries()) {
|
||||
if (action.test(this.buffer.valueOf())) {
|
||||
this.buffer.clear()
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearBuffer() {
|
||||
this.buffer.clear()
|
||||
}
|
||||
}
|
||||
|
22
src/pubsub.js
Normal file
22
src/pubsub.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const topics = new Map()
|
||||
|
||||
export const publish = (topic, data) => {
|
||||
topics.has(topic) && topics.get(topic).forEach(sub => {
|
||||
sub(data !== undefined ? data : {})
|
||||
})
|
||||
}
|
||||
|
||||
export const subscribe = (topic, subscription) => {
|
||||
if (!topics.has(topic)) {
|
||||
topics.set(topic, new Set())
|
||||
}
|
||||
|
||||
topics.get(topic).add(subscription)
|
||||
|
||||
return {
|
||||
remove: () => {
|
||||
topics.get(topic).delete(subscription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/ring-buffer.js
Normal file
66
src/ring-buffer.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
export default (capacity, ctr = Array) => {
|
||||
const buffer = new ctr(capacity)
|
||||
let head = 0
|
||||
let tail = 0
|
||||
let size = 0
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
enqueue(item) {
|
||||
if (this.isFull()) {
|
||||
tail = (tail + 1) % capacity
|
||||
}
|
||||
|
||||
buffer[head] = item
|
||||
head = (head + 1) % capacity
|
||||
size = Math.min(capacity, size + 1)
|
||||
},
|
||||
dequeue() {
|
||||
if (this.isEmpty()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const item = buffer[tail]
|
||||
tail = (tail + 1) % capacity
|
||||
size--
|
||||
return item
|
||||
},
|
||||
|
||||
peek() {
|
||||
if (this.isEmpty()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return buffer[tail]
|
||||
},
|
||||
|
||||
isFull: () => capacity === size,
|
||||
isEmpty: () => size === 0,
|
||||
size() { return size },
|
||||
|
||||
valueOf() {
|
||||
const result = []
|
||||
|
||||
for (const x of this) {
|
||||
result.push(x)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
clear() {
|
||||
head = 0
|
||||
tail = 0
|
||||
size = 0
|
||||
},
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
let i = tail
|
||||
for (let j = 0; j < size; j++) {
|
||||
yield buffer[i]
|
||||
i = (i + 1) % capacity
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
58
test.html
Normal file
58
test.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="module">
|
||||
import van from "https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.5.3.min.js"
|
||||
import * as Tests from '/test/units/index.js'
|
||||
|
||||
const kebab = str => str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (x, ofs) => (ofs ? "-" : "") + x.toLowerCase())
|
||||
const toCss = s => Object.entries(s).reduce((acc, [k, v]) => acc + `${kebab(k)}:${v};`, '')
|
||||
|
||||
const {div, ul, li, pre, p, span, h1} = van.tags
|
||||
|
||||
const Check = '\u2713'
|
||||
const Cross = '\u2717'
|
||||
const pathname = url => new URL(url).pathname
|
||||
|
||||
const Err = err => pre(`${pathname(err.fileName)} ${err.lineNumber}:${err.columnNumber} ${err}`)
|
||||
const Item = (icon, color, item) => li({
|
||||
style: `display: list-item; list-style-type: '${icon}'; color: ${color}`
|
||||
}, item)
|
||||
|
||||
const Pass = ({description}) => Item(Check, 'green', description)
|
||||
const Fail = ({description, error}) => Item(
|
||||
Cross,
|
||||
'red',
|
||||
div(
|
||||
description,
|
||||
Err(error)
|
||||
)
|
||||
)
|
||||
|
||||
const Test = test => {
|
||||
const result = test()
|
||||
return result.success ? Pass(result) : Fail(result)
|
||||
}
|
||||
|
||||
const Suite = (name, tests) => div(
|
||||
h1(name),
|
||||
ul(...tests.map(Test))
|
||||
)
|
||||
|
||||
const App = () => div(
|
||||
Object.entries(Tests)
|
||||
.map(([name, tests]) => Suite(name, tests.default))
|
||||
)
|
||||
|
||||
document.body.style = toCss({backgroundColor: '#2a2a2a', color: 'white'})
|
||||
|
||||
van.add(document.body, App())
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
48
test/test.js
Normal file
48
test/test.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
export class AssertionError extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export const assert = (value, message = `assertion error: ${value} is not true`) => {
|
||||
assertEq(value, true, message)
|
||||
}
|
||||
|
||||
export const assertEq = (a, b, message = `assertion error: ${a} !== ${b}`) => {
|
||||
if (a === b) {
|
||||
return
|
||||
}
|
||||
else if (Array.isArray(b) && a.length === b.length) {
|
||||
b.forEach((n, i) => assertEq(a[i], n))
|
||||
return
|
||||
} else {
|
||||
throw new AssertionError(message)
|
||||
}
|
||||
}
|
||||
|
||||
export const assertCallback = () => {
|
||||
let result = false
|
||||
setTimeout(() => assert(result), 0)
|
||||
return () => result = true
|
||||
}
|
||||
|
||||
export const it = (description, test) => {
|
||||
return () => {
|
||||
try {
|
||||
test()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
description,
|
||||
error: undefined
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
description,
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
test/units/buffer.js
Normal file
34
test/units/buffer.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { it, assert, assertEq } from '../test.js'
|
||||
import RingBuffer from '/src/ring-buffer.js'
|
||||
|
||||
const enqueue = (buf, xs) => {
|
||||
if (Array.isArray(xs)) {
|
||||
return xs.forEach(x => enqueue(buf, x))
|
||||
}
|
||||
buf.enqueue(xs)
|
||||
}
|
||||
|
||||
export default [
|
||||
it('should enqueue and dequeue items', () => {
|
||||
const buf = RingBuffer(5)
|
||||
enqueue(buf, [0, 1, 2, 3, 4])
|
||||
const result = buf.valueOf()
|
||||
assertEq(result, [0, 1, 2, 3, 4])
|
||||
}),
|
||||
|
||||
it('should wrap on overflow', () => {
|
||||
const buf = RingBuffer(5)
|
||||
enqueue(buf, [0, 1, 2, 3, 4, 5])
|
||||
const result = buf.valueOf()
|
||||
assertEq(result, [1, 2, 3, 4, 5])
|
||||
}),
|
||||
|
||||
it('should be empty on clear', () => {
|
||||
const buf = RingBuffer(5)
|
||||
enqueue(buf, [0, 1, 2, 3, 4])
|
||||
buf.clear()
|
||||
assert(buf.isEmpty(), 'buffer is not empty')
|
||||
assertEq(buf.size(), 0, 'buffer size is greater than 0')
|
||||
})
|
||||
]
|
||||
|
3
test/units/index.js
Normal file
3
test/units/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * as Buffer from './buffer.js'
|
||||
export * as Input from './input.js'
|
||||
|
36
test/units/input.js
Normal file
36
test/units/input.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Action } from '../../src/input.js'
|
||||
import { it, assert, assertCallback } from '../test.js'
|
||||
import { ActionMap, Button } from '/src/input.js'
|
||||
|
||||
const dispatch = (name, event) => {
|
||||
document.dispatchEvent(name, event)
|
||||
}
|
||||
|
||||
const keyEvent = (type, code, args = {}) => {
|
||||
dispatch(new KeyboardEvent(type, { code, ...args }))
|
||||
}
|
||||
|
||||
const pressKey = (code, args) => {
|
||||
keyEvent('keydown', code, args)
|
||||
}
|
||||
|
||||
const releaseKey = (code, args) => {
|
||||
keyEvent('keyup', code, args)
|
||||
}
|
||||
|
||||
export default [
|
||||
it('should detect keyboard presses', () => {
|
||||
const map = new ActionMap()
|
||||
map.bind(Action.keyboard('KeyV'), assertCallback())
|
||||
pressKey('KeyV')
|
||||
|
||||
map.bind(Action.keyboard('Ctrl+Shift+KeyV'), assertCallback())
|
||||
pressKey('KeyV', { ctrlKey: true, shiftKey: true })
|
||||
}),
|
||||
|
||||
it('should detect gamepad presses', () => {
|
||||
const map = new ActionMap()
|
||||
map.bind(Action.gamepad([Button.ButtonSouth]), () => console.log('a'))
|
||||
})
|
||||
]
|
||||
|
Loading…
Add table
Reference in a new issue