initial commit

This commit is contained in:
Rowan 2025-02-26 12:17:16 -06:00
commit e365a7565d
19 changed files with 17823 additions and 0 deletions

BIN
assets/room.blend Normal file

Binary file not shown.

BIN
assets/room.blend1 Normal file

Binary file not shown.

BIN
assets/room.glb Normal file

Binary file not shown.

30
index.html Normal file
View 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
View 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
View 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": ""
}

File diff suppressed because it is too large Load diff

49
server.pem Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
AFRAME.registerComponent("is-controlled", {
tick(time, delta) {
}
})

336
src/input.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
export * as Buffer from './buffer.js'
export * as Input from './input.js'

36
test/units/input.js Normal file
View 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'))
})
]