From 4f95714a927f9d70c7a67345c07deac15cffda7b Mon Sep 17 00:00:00 2001
From: Nyeogmi <economicsbat@gmail.com>
Date: Sat, 1 Feb 2025 13:13:44 -0800
Subject: [PATCH] Initial commit

---
 .gitignore                                    |  24 +
 index.html                                    |  11 +
 package-lock.json                             | 906 ++++++++++++++++++
 package.json                                  |  15 +
 src/art/bgs/sampleroom1.png                   | Bin 0 -> 874 bytes
 src/art/characters/bat.png                    | Bin 0 -> 950 bytes
 src/art/characters/kobold.png                 | Bin 0 -> 835 bytes
 src/art/characters/raccoon walking 2.aseprite | Bin 0 -> 3761 bytes
 src/art/characters/raccoon.png                | Bin 0 -> 840 bytes
 src/art/characters/raccoon_walking.png        | Bin 0 -> 1734 bytes
 src/art/characters/robot.png                  | Bin 0 -> 772 bytes
 src/art/characters/snake.png                  | Bin 0 -> 885 bytes
 src/art/fonts/vga_8x16.png                    | Bin 0 -> 7267 bytes
 src/art/mapdata/sampleroom1.ldtk              |  71 ++
 src/assets.ts                                 |  41 +
 src/clock.ts                                  |  44 +
 src/colors.ts                                 |   1 +
 src/counter.ts                                |   9 +
 src/font.ts                                   | 146 +++
 src/game.ts                                   | 172 ++++
 src/input.ts                                  | 108 +++
 src/main.ts                                   |  87 ++
 src/screen.ts                                 |  62 ++
 src/sprite.ts                                 |  48 +
 src/sprites.ts                                |  14 +
 src/style.css                                 |  12 +
 src/vite-env.d.ts                             |   1 +
 tsconfig.json                                 |  24 +
 28 files changed, 1796 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 index.html
 create mode 100644 package-lock.json
 create mode 100644 package.json
 create mode 100644 src/art/bgs/sampleroom1.png
 create mode 100644 src/art/characters/bat.png
 create mode 100644 src/art/characters/kobold.png
 create mode 100644 src/art/characters/raccoon walking 2.aseprite
 create mode 100644 src/art/characters/raccoon.png
 create mode 100644 src/art/characters/raccoon_walking.png
 create mode 100644 src/art/characters/robot.png
 create mode 100644 src/art/characters/snake.png
 create mode 100644 src/art/fonts/vga_8x16.png
 create mode 100644 src/art/mapdata/sampleroom1.ldtk
 create mode 100644 src/assets.ts
 create mode 100644 src/clock.ts
 create mode 100644 src/colors.ts
 create mode 100644 src/counter.ts
 create mode 100644 src/font.ts
 create mode 100644 src/game.ts
 create mode 100644 src/input.ts
 create mode 100644 src/main.ts
 create mode 100644 src/screen.ts
 create mode 100644 src/sprite.ts
 create mode 100644 src/sprites.ts
 create mode 100644 src/style.css
 create mode 100644 src/vite-env.d.ts
 create mode 100644 tsconfig.json

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..05ebd27
--- /dev/null
+++ b/index.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Prototype 2</title>
+  </head>
+  <body>
+    <canvas id="game" style="cursor: none"></canvas>
+    <script type="module" src="src/main.ts"></script>
+  </body>
+</html>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c140d79
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,906 @@
+{
+  "name": "prototype1",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "prototype1",
+      "version": "0.0.0",
+      "devDependencies": {
+        "typescript": "~5.6.2",
+        "vite": "^6.0.5"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+      "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+      "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+      "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+      "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+      "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+      "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+      "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+      "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+      "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+      "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+      "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+      "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+      "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+      "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+      "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+      "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+      "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+      "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+      "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+      "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+      "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+      "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+      "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
+      "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
+      "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
+      "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
+      "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
+      "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
+      "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
+      "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
+      "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
+      "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
+      "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
+      "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
+      "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
+      "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
+      "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
+      "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
+      "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
+      "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
+      "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
+      "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "dev": true
+    },
+    "node_modules/esbuild": {
+      "version": "0.24.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+      "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.24.2",
+        "@esbuild/android-arm": "0.24.2",
+        "@esbuild/android-arm64": "0.24.2",
+        "@esbuild/android-x64": "0.24.2",
+        "@esbuild/darwin-arm64": "0.24.2",
+        "@esbuild/darwin-x64": "0.24.2",
+        "@esbuild/freebsd-arm64": "0.24.2",
+        "@esbuild/freebsd-x64": "0.24.2",
+        "@esbuild/linux-arm": "0.24.2",
+        "@esbuild/linux-arm64": "0.24.2",
+        "@esbuild/linux-ia32": "0.24.2",
+        "@esbuild/linux-loong64": "0.24.2",
+        "@esbuild/linux-mips64el": "0.24.2",
+        "@esbuild/linux-ppc64": "0.24.2",
+        "@esbuild/linux-riscv64": "0.24.2",
+        "@esbuild/linux-s390x": "0.24.2",
+        "@esbuild/linux-x64": "0.24.2",
+        "@esbuild/netbsd-arm64": "0.24.2",
+        "@esbuild/netbsd-x64": "0.24.2",
+        "@esbuild/openbsd-arm64": "0.24.2",
+        "@esbuild/openbsd-x64": "0.24.2",
+        "@esbuild/sunos-x64": "0.24.2",
+        "@esbuild/win32-arm64": "0.24.2",
+        "@esbuild/win32-ia32": "0.24.2",
+        "@esbuild/win32-x64": "0.24.2"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true
+    },
+    "node_modules/postcss": {
+      "version": "8.5.1",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+      "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.8",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.31.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
+      "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.6"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.31.0",
+        "@rollup/rollup-android-arm64": "4.31.0",
+        "@rollup/rollup-darwin-arm64": "4.31.0",
+        "@rollup/rollup-darwin-x64": "4.31.0",
+        "@rollup/rollup-freebsd-arm64": "4.31.0",
+        "@rollup/rollup-freebsd-x64": "4.31.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.31.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.31.0",
+        "@rollup/rollup-linux-arm64-musl": "4.31.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.31.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.31.0",
+        "@rollup/rollup-linux-x64-gnu": "4.31.0",
+        "@rollup/rollup-linux-x64-musl": "4.31.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.31.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.31.0",
+        "@rollup/rollup-win32-x64-msvc": "4.31.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/vite": {
+      "version": "6.0.9",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.9.tgz",
+      "integrity": "sha512-MSgUxHcaXLtnBPktkbUSoQUANApKYuxZ6DrbVENlIorbhL2dZydTLaZ01tjUoE3szeFzlFk9ANOKk0xurh4MKA==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.24.2",
+        "postcss": "^8.4.49",
+        "rollup": "^4.23.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..00fedad
--- /dev/null
+++ b/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "prototype1",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "typescript": "~5.6.2",
+    "vite": "^6.0.5"
+  }
+}
diff --git a/src/art/bgs/sampleroom1.png b/src/art/bgs/sampleroom1.png
new file mode 100644
index 0000000000000000000000000000000000000000..921912328eb00ceb1977adb3cad38dd6eff49edf
GIT binary patch
literal 874
zcmeAS@N?(olHy`uVBq!ia0y~yU;#2295|SOBzMN7)eH=bt(nfw0iMpz3I#>^X_+~x
z3=A3*YbV-z9Cna78XxRBv8&nDg~d2TT-QUiaEg}LL8naZEw2)aUW%J!dIZJDuwHL!
zmhTQ_7f+9sjbU~Cz<lK9LC=e>UVq|)G6WwVEVRFOxBAXr=9a3NE7OjvEXbNRvB&kO
zPg&@jDpv+&i31i&LdIV!vJ838e=OyfkAHv2=JfA7^B0GooXPW{^GLCexy~crM>BoB
zpYQRPNpba5v{RpF@S^ERf6VmQy2mjE5f5fMD$kvx&L`B}YBufm3{l>x6|*dstdM%j
zEjeBET=bl)MN>`RpNy9{k*Te@?y`&<k9UVMqoC0$jfFhiiBYCOJ}Q~^uO7-5{oyP+
z`9!O6Vlt!XiU~6IKji<syLQIk_0AFpkry=^-}VPC$QIAIlJoobjcwBE4gV!~T#EYV
zB|GD<%HC_2oZKDmn-vt^z4XNPK@qz|%_UXmA{BdI#rf>_&cB$w$>G25h0V{tUAuGq
zCG&)3>!#&5G&?7(ak%+f?)|&G_iyhTpZ!<IAn#f9X6eNxyMXb+S>O>_42&LO5N2eU
zHAey{$X?><>&pI;QItVP^(ya0Ibg(9c)B=-RNQ)dYa>^Kf{4RKuKic{nl4gYu{Hi3
zze$r*<?}UGD=zPTutTmAs6T0j4I={s2ax3e#0;SL7Xac0AO@$24_iYbZcpF&baDRk
z(7Ts^h%zudcpg~!s=Q>~ybE9W8613CJ#UCB08MZKYG?prur37<186QDgF#9hfS7@4
zf#+|d{SizI4I6^qiyORPW;js152(2TB*_BA3Lplth5?nLxF5G>+%gW_j2q@ozYcVQ
toM_w6LfcL3K*uT6FaiSyw=?X@nYTO;E4s+fz8UBS22WQ%mvv4FO#qf@4@v+4

literal 0
HcmV?d00001

diff --git a/src/art/characters/bat.png b/src/art/characters/bat.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d51fb872571b11c4b0c24ecb8b79767fc974cb1
GIT binary patch
literal 950
zcmV;n14;aeP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0004nX+uL$Nkc;*
zaB^>EX>4Tx04R}tkv&MmKpe$iQ$>-AgGEFHGgKEXNELCEDi*;)X)CnqU~=gfG-*gu
zTpR`0f`cE6RR<SmT^(EnLGS~_*}+NCMN0f%QfLw5!Ery{-Fw`<1B7~+sb<F{plX(p
zj77yvc2(?pMF1lhz&IijGxd0CF%8f0bq^n3@4`IG``n+SSIL<S@QK88OgAjz4dR(i
zOXs{#9ArgFAwDObFzABBk6f2se&bwlSm2pKBb}Tl4ibxnHkR9%6%CbmnmDAW8s!Ta
zmle)ioYiubHSft^7|3ZWDX!BTMhr`cBLNXIswklh3t?I{QcNUiKkDHha{LK$$>b`5
zkz)ZBsE`~#_#gc4*33^u+@wGh=zOv5k5Qm&7iiRM`}^3o8z+GO8Mx9~{z@H~{Up8C
z(n3c--!^b@-O}Ve;Bp5Te9|RDawI=ZA)g1{&*+=7K>sb!v*z~J+{ftykfyGdZ-9eC
zV5~sd>mKj!Ztv~iGtK^f03S(m?4{A9x&QzG32;bRa{vGf6951U69E94oEQKA00(qQ
zO+^Rk0Tlo?8q-Dw_y7O^8FWQhbVF}#ZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b
z0iQ`kK~#9!?U=o7!!Qtqp9BwK4PdC?1|C2os1+dBE)A8b1uu{y37}RRBm-oEs4zT$
z7f9v8OeKJ(sh<GxSz_J?7_dbG^4+~V9uFXd5JE&K(@d9Xru){V|5G4kH~<`)696Dq
z5dfg5tH8C-etu6vsTZ}z+uu#&UC+I7z;7HSnF`^5G4!DhDC#O`^S-M_*eN3AoXMqt
zx2J%sz?rHr*JBllodjaXjeQi_4zN?k>mq0xzN?Se)yErWU9i>{thF}^aH-&>`Mz4;
zr>m>qtz$xb23{zjm3FM6PWd026C@iV@f0xg+l=@=T{*kOdq!V81%@s@cI4lhkH9+x
zxRlV!ezQE|_3Itm=O=8IXMg$t2RG5rRU#h!N7Bv(`aa(F0eVW2SV$Y6s|0-)BkAFY
zNS@^?(XLLgsH?91_P+TxLbmbdaaX1phKk{wQ$UX~M%rL01q>QLrrkA^qUYiOJvQ<~
zSK_%1M08gH8;bszefJ!YEdJ8?v6b7Fbl2V~3{43kgb+dqA%qY@2qA<JLI@#*5Hf#$
Y09YEs*_#bbTL1t607*qoM6N<$f@bQPW&i*H

literal 0
HcmV?d00001

diff --git a/src/art/characters/kobold.png b/src/art/characters/kobold.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2f4f96fd4a4349ed37225cf70ac7a9759e4e9fa
GIT binary patch
literal 835
zcmV-J1HAl+P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0004nX+uL$Nkc;*
zaB^>EX>4Tx04R}tkv&MmKpe$iQ$>-AgGEFHGgKEXNELCEDi*;)X)CnqU~=gfG-*gu
zTpR`0f`cE6RR<SmT^(EnLGS~_*}+NCMN0f%QfLw5!Ery{-Fw`<1B7~+sb<F{plX(p
zj77yvc2(?pMF1lhz&IijGxd0CF%8f0bq^n3@4`IG``n+SSIL<S@QK88OgAjz4dR(i
zOXs{#9ArgFAwDObFzABBk6f2se&bwlSm2pKBb}Tl4ibxnHkR9%6%CbmnmDAW8s!Ta
zmle)ioYiubHSft^7|3ZWDX!BTMhr`cBLNXIswklh3t?I{QcNUiKkDHha{LK$$>b`5
zkz)ZBsE`~#_#gc4*33^u+@wGh=zOv5k5Qm&7iiRM`}^3o8z+GO8Mx9~{z@H~{Up8C
z(n3c--!^b@-O}Ve;Bp5Te9|RDawI=ZA)g1{&*+=7K>sb!v*z~J+{ftykfyGdZ-9eC
zV5~sd>mKj!Ztv~iGtK^f03S(m?4{A9x&QzG32;bRa{vGf6951U69E94oEQKA00(qQ
zO+^Rk0Tlo<ByUjf761SM8FWQhbVF}#ZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b
z0W3*GK~#9!?btC+!$1&);kQ=mG?D0>ThNe#)Jra4Xw0Rk)1X|yavch)G@Jo<NEC1Y
zh6Y;}ib=(e75|?U$(ACm=I!ik6bS$T0Kk8$8ndb~n{?4*3?ZZlp?%)uAsh=}lZSA&
z0sIbOh*t<%ibakU7xR^3zUtn;bmek1W~p}n_5D-J#eDF4b@QxBveX@s^a!*eoC@RY
z0vMVF7f<`{`SNb9!{c6W*XyM7Ao<`qH4r^-gycayynML-CdeGtw4aZbi@Az2x`q%O
zitl$p+x9NUQI5NBlB^}kygDKC+WoP}rd+(&5FC}6=i;~bH<Rgr@Yk(Vlu^5F?Q#ov
zT|HKmv3v0<-YWp>@4R;iu0wFQt^3DdTK^XU0000000000004kwz5%Udc)7oqq~`zt
N002ovPDHLkV1ft{Z*%|v

literal 0
HcmV?d00001

diff --git a/src/art/characters/raccoon walking 2.aseprite b/src/art/characters/raccoon walking 2.aseprite
new file mode 100644
index 0000000000000000000000000000000000000000..ed9497b2604245cacee47aff1d9a2806721e2f90
GIT binary patch
literal 3761
zcmchZdrZ?;6vt2B1xs7SF@#Eg4mPp~uE1tMQ5YI@Nx=?KK}zh)=b+S5Cr;d|IK>SF
z!;-=v5yz%kBvFi;nI@Bh4;FAS5hqh((D*>-Kh$XumEX?&wb+L|wg|gTZ%^;N=bn2%
z_uTLA*DMC;{Xq!H_@RIW00#WJiyyE^BS`Tx8RZ5SV6fkxiRzC5PO$+56jolPfoWhj
z<m(HHxC(#*qs5%DB!$Y(Sw#u@Km>9Kg4qzO)fbu4GE=j>oW1@$`GeUcT#hDld3BBd
zC%~|#NDsiGaA>mLWVWEKL3{2Ni;37ZXiwL#*CIkz6WUQ`Tp-%rq+RE<9};^VV>6qY
zZqCYB1i+q-?bJFCTjycx*P9D)%=<Vd!cbtu`Ptb{+d%9RVi)9Z#<ekpoS0~had{^G
zM&MTlOW-3&0~H>z4mQIE$Oj8V<F650iXa2~Q%3O*Vf<C-r9-a`phu6zPzVK}W&DAh
z0gl1qW?Qs2E|zIxiBbS~H)#6N+jy?k057U&`q@F6e(@aurw5?L==Lvc0EmB#?N#o!
zhywtUT>w9yb+_qk04MhV1pjH$nzc+E4%Y0oYXKaG0RnOW0xtph=Q_DzAL)n2s{mQZ
zN_i2W_FI5=*5TahQT8oFhnL*O9OL4rLWdcY9|?qoxQ|e1fcYvl)`r!2SZ5Y~f%Oo~
z!}?rt4%W64=dr$g;|@I&-MJLAaMwiG0X$G@x2`eEN9+_hJE_>QyJN=gj!PzSD1cA|
zKr=~-4d4e{D6ZFQG#cUWM`=l`NTr!6eI(zdEwOCPxYE*P@9NDIfbK!$0z!gEu^<qF
zJ^Y0rVEmJL_NR~O87Axf+<h+uzcn6V^?zt?cTjVc;=HiDXr-ubmEm;rFFaeW_EQe<
zAEr-T$;`*b^#qMHAN*ylp%vv3ou(6SIg*6o9i;>9W~zRge9DFPTLZTb7bfs8v^y}U
zM!7WXHec<a62KT}+Cy#&4*`dQ#A8}NJi6lgx?k^f)Av=KR_>wjy`?78=L>?{x(<GA
zALtuA)L2{DB*}ZP(^%7!#GkE5y&Ha7k{8e=4nF@tlE1OaerMaVk6YRB<GqRxG%TWS
zTt?lv7;X_0l8_Dc2r0x<C1kDE-sgif)oJB5bmR!mJ{M@!gBtU|?fq98I5mkv4#e%A
z6_d_PF&bRw;UsPEh*9vHXH-F1<($eUzAdq=qKRLZcs!xc+}*SDVw7d8<tGTPUDZ!i
zgokoF7Ft{of8UD5Ljo->Vl1{f0jCn%w?pho(@nftF1AvSjlM2hP0Kv$F74vmajh~`
z>BDy+6BZ!Mi!aZ`SED&5E_*7kUoAN=QYDuMmsLjbTNN?V-S;`rCJWz`>OwmWLOd4j
z4}9iyS|o=G6Q0Oz#UvR@yA=zSVR^Dp`GF8!*R1PbcBEUja3GOqn6-RFq=Mn>XR_53
zX<0>-xb@xsW{tAE=kMqXL#2x2Y7Fs}3^UoTn418Nch%eehg@Cm!_rBEh~S91>fFb?
z_%lOE!Z?amB~ymgg_Jn@$W<0qFImaqKzw`Tf~78|fnH4ghM9^9)1>$_Vdb0lHXhlr
z_u!Eo{o0L}&S~>Y9EyaR`NwK2L~toJ*Pi8~DuXvX&W7)zYISM=URP(LR>ha4261kF
zGmyRW*5#A3XOcFmzxVlP^|vU|@hLg7?}H#zvdr#=D)yN3b(7Rf0DPe)Gus$lT1kQu
zX_eNmrhTO~CN0u7(bAgn604`Q{%5RxW@?jE4o?HpuDhX5nyRkw$<%RFb@;}is`Bn#
zrC(9i{bwdxRfnm1tLnd(fHsA44&I5QGLgQT1LxMOpSY<`q@H@E>M|dy<7p{*3n-i^
z<-Dq;`=*A!IWbx~S&t_-&PhwsY#*HC$f@(ue+{2+qsnR0Cy};^l2i99m8YD(d_{B0
M>Fc~wO`4+q1Dy{llK=n!

literal 0
HcmV?d00001

diff --git a/src/art/characters/raccoon.png b/src/art/characters/raccoon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f9273c1e24f17676830eb78aaf2d28a1ac6bf3c
GIT binary patch
literal 840
zcmV-O1GoH%P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0004nX+uL$Nkc;*
zaB^>EX>4Tx04R}tkv&MmKpe$iQ$>-AgGEFHGgKEXNELCEDi*;)X)CnqU~=gfG-*gu
zTpR`0f`cE6RR<SmT^(EnLGS~_*}+NCMN0f%QfLw5!Ery{-Fw`<1B7~+sb<F{plX(p
zj77yvc2(?pMF1lhz&IijGxd0CF%8f0bq^n3@4`IG``n+SSIL<S@QK88OgAjz4dR(i
zOXs{#9ArgFAwDObFzABBk6f2se&bwlSm2pKBb}Tl4ibxnHkR9%6%CbmnmDAW8s!Ta
zmle)ioYiubHSft^7|3ZWDX!BTMhr`cBLNXIswklh3t?I{QcNUiKkDHha{LK$$>b`5
zkz)ZBsE`~#_#gc4*33^u+@wGh=zOv5k5Qm&7iiRM`}^3o8z+GO8Mx9~{z@H~{Up8C
z(n3c--!^b@-O}Ve;Bp5Te9|RDawI=ZA)g1{&*+=7K>sb!v*z~J+{ftykfyGdZ-9eC
zV5~sd>mKj!Ztv~iGtK^f03S(m?4{A9x&QzHen~_@RCt{2*P&0tKorOEZwG$ha3qir
z_AhM6Ei)5K76i%ufeBJaa0`wgNT#Y$hy^#Zbawp%K!9Nhf&>RB6gdPO8?4(*cSpkS
zquQ&u_pa|=uOK2KA|fIp^6vyLCyRQCvDd9yF<3P2LeAx60E4*CAnt!B9rY4p$9!1z
zF=@F{1;%gQ-uvLR++h61Et~ux?sL^SaUPHJ3b4s{J23$JU*p|Q?0!Gup$DXud%032
zM*?7`9xj>ll7pXJ-)hy0_R>pC&Xwv-Q>r)3&FImjEr=^#R(p7SU;vKpRscBLeW!GK
z=_MBDJ-{XpP{;=q^1;-&OFkO!pW@^*81)ikkGR)+hvNs`2!Qjq9T#DwTFL@U9(L?!
zuMxJ5kAu!4nQnnb*q+<b8)4hbI`jKloXjeq5w;gLv56$j`pXD_+gHQz!e3=L=>ciK
zxX)`_JU!gIn0~Izw!o|sp7(#hvAGR=9*VMxLqtSGL_|bHL_|bHL_|bH<hKty8hy?S
S*x5b+0000<MNUMnLSTY|$$u#T

literal 0
HcmV?d00001

diff --git a/src/art/characters/raccoon_walking.png b/src/art/characters/raccoon_walking.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e6dd0b3bceeae3f3fe7c2e8ed4d4ae852dc0801
GIT binary patch
literal 1734
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

literal 0
HcmV?d00001

diff --git a/src/art/characters/robot.png b/src/art/characters/robot.png
new file mode 100644
index 0000000000000000000000000000000000000000..0867c769e21c75773d1dab3ec929ea8c69109633
GIT binary patch
literal 772
zcmV+f1N;1mP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0004nX+uL$Nkc;*
zaB^>EX>4Tx04R}tkv&MmKpe$iQ$>-AgGEFHGgKEXNELCEDi*;)X)CnqU~=gfG-*gu
zTpR`0f`cE6RR<SmT^(EnLGS~_*}+NCMN0f%QfLw5!Ery{-Fw`<1B7~+sb<F{plX(p
zj77yvc2(?pMF1lhz&IijGxd0CF%8f0bq^n3@4`IG``n+SSIL<S@QK88OgAjz4dR(i
zOXs{#9ArgFAwDObFzABBk6f2se&bwlSm2pKBb}Tl4ibxnHkR9%6%CbmnmDAW8s!Ta
zmle)ioYiubHSft^7|3ZWDX!BTMhr`cBLNXIswklh3t?I{QcNUiKkDHha{LK$$>b`5
zkz)ZBsE`~#_#gc4*33^u+@wGh=zOv5k5Qm&7iiRM`}^3o8z+GO8Mx9~{z@H~{Up8C
z(n3c--!^b@-O}Ve;Bp5Te9|RDawI=ZA)g1{&*+=7K>sb!v*z~J+{ftykfyGdZ-9eC
zV5~sd>mKj!Ztv~iGtK^f03S(m?4{A9x&QzG32;bRa{vGf6951U69E94oEQKA00(qQ
zO+^Rk0Tlo-H`&<CN&o-=8FWQhbVF}#ZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b
z0PRUcK~#9!?bgu^gCG!q;ludOB2M5O9kX*d0gJeR^hJ!dkk%#~1pY5EqAz;K9UuY#
z0000TGMXRr&idOwIIX6cZ0`wpyJGJyS34Dussb~u$9R)jL_`Kzj^}-x<zGkjp1YLv
zx3&n0$YrdpO>zhP`Mo;UhCX(IRQ#zVm}xcJWA?*yX=f++WoPK30#b`#Q~UnSSy!L^
z;=4ei@B5hj>-{utH}F`{kz};j0^V64O4~bIRhg?Fglg;Yp+GAXJ^868;9UMSR>7z_
z^ReK=noz1LjK=K$8(bHsLI3~&0000000000klhO?{Ds3)#sp#j0000<MNUMnLSTZk
C*-g>_

literal 0
HcmV?d00001

diff --git a/src/art/characters/snake.png b/src/art/characters/snake.png
new file mode 100644
index 0000000000000000000000000000000000000000..59924e57b0c3feeefb7a04e9acc9b25275aeef97
GIT binary patch
literal 885
zcmV-*1B(2KP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0004nX+uL$Nkc;*
zaB^>EX>4Tx04R}tkv&MmKpe$iQ$>-AgGEFHGgKEXNELCEDi*;)X)CnqU~=gfG-*gu
zTpR`0f`cE6RR<SmT^(EnLGS~_*}+NCMN0f%QfLw5!Ery{-Fw`<1B7~+sb<F{plX(p
zj77yvc2(?pMF1lhz&IijGxd0CF%8f0bq^n3@4`IG``n+SSIL<S@QK88OgAjz4dR(i
zOXs{#9ArgFAwDObFzABBk6f2se&bwlSm2pKBb}Tl4ibxnHkR9%6%CbmnmDAW8s!Ta
zmle)ioYiubHSft^7|3ZWDX!BTMhr`cBLNXIswklh3t?I{QcNUiKkDHha{LK$$>b`5
zkz)ZBsE`~#_#gc4*33^u+@wGh=zOv5k5Qm&7iiRM`}^3o8z+GO8Mx9~{z@H~{Up8C
z(n3c--!^b@-O}Ve;Bp5Te9|RDawI=ZA)g1{&*+=7K>sb!v*z~J+{ftykfyGdZ-9eC
zV5~sd>mKj!Ztv~iGtK^f03S(m?4{A9x&QzG32;bRa{vGf6951U69E94oEQKA00(qQ
zO+^Rk0Tlo>685m&2><{98FWQhbVF}#ZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b
z0bWT&K~#9!?bp3d!!Q&D;A5*|ho#+l3#PC#Ai-89rb@g)e_*JJl}A8A4B6O<#K77t
z@Ca<|^^^xdiWv}<o2F_9@TKwjrZgSgb91lV07OJYL_|?i5{nu)bN=U%I#y8>pc=eN
zq7W^q@B=kkT&K<e07@J=jW^ZE0&F971_p2;yFrHvw{4)(765?BeCjrh%ua5C#?4%5
z3+VR#uNH2nMqpF;Arjcn0-e1B)wmAuyTEtTIT!gY!4`e(cbDclPkShBfy2A!#l4Gh
z7Q6@=LXyEfuG?qgCr>L>%jaJ3s*kJF?~8GU#&<xT_SSwknNM9bBM{}w(9a*6A(Ey0
zxAsZwBnZA>_wr<=%^$NDvA4mXBTzTvlHr3EVBH2+*AY={g3?buXaOXSw|}z<`r+UE
z4=WwukAOs1=)Vl^hp!%Sb+a$M9}p1{5fKp)5fKp)5fKp)Q8Rr3BW!y3yFbVM00000
LNkvXXu0mjfw61-S

literal 0
HcmV?d00001

diff --git a/src/art/fonts/vga_8x16.png b/src/art/fonts/vga_8x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d995ccf24dd72d30516974e0fbe287e11b506fb
GIT binary patch
literal 7267
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
F{5N)#^-TZ(

literal 0
HcmV?d00001

diff --git a/src/art/mapdata/sampleroom1.ldtk b/src/art/mapdata/sampleroom1.ldtk
new file mode 100644
index 0000000..d676f67
--- /dev/null
+++ b/src/art/mapdata/sampleroom1.ldtk
@@ -0,0 +1,71 @@
+{
+	"__header__": {
+		"fileType": "LDtk Project JSON",
+		"app": "LDtk",
+		"doc": "https://ldtk.io/json",
+		"schema": "https://ldtk.io/files/JSON_SCHEMA.json",
+		"appAuthor": "Sebastien 'deepnight' Benard",
+		"appVersion": "1.5.3",
+		"url": "https://ldtk.io"
+	},
+	"iid": "018dc430-c210-11ef-bffb-99d47c7356b6",
+	"jsonVersion": "1.5.3",
+	"appBuildId": 473703,
+	"nextUid": 1,
+	"identifierStyle": "Capitalize",
+	"toc": [],
+	"worldLayout": "Free",
+	"worldGridWidth": 256,
+	"worldGridHeight": 256,
+	"defaultLevelWidth": 256,
+	"defaultLevelHeight": 256,
+	"defaultPivotX": 0,
+	"defaultPivotY": 0,
+	"defaultGridSize": 16,
+	"defaultEntityWidth": 16,
+	"defaultEntityHeight": 16,
+	"bgColor": "#40465B",
+	"defaultLevelBgColor": "#696A79",
+	"minifyJson": false,
+	"externalLevels": false,
+	"exportTiled": false,
+	"simplifiedExport": false,
+	"imageExportMode": "None",
+	"exportLevelBg": true,
+	"pngFilePattern": null,
+	"backupOnSave": false,
+	"backupLimit": 10,
+	"backupRelPath": null,
+	"levelNamePattern": "Level_%idx",
+	"tutorialDesc": null,
+	"customCommands": [],
+	"flags": [],
+	"defs": { "layers": [], "entities": [], "tilesets": [], "enums": [], "externalEnums": [], "levelFields": [] },
+	"levels": [
+		{
+			"identifier": "Level_0",
+			"iid": "018deb40-c210-11ef-bffb-151cf6f68a2d",
+			"uid": 0,
+			"worldX": 0,
+			"worldY": 0,
+			"worldDepth": 0,
+			"pxWid": 256,
+			"pxHei": 256,
+			"__bgColor": "#696A79",
+			"bgColor": null,
+			"useAutoIdentifier": true,
+			"bgRelPath": null,
+			"bgPos": null,
+			"bgPivotX": 0.5,
+			"bgPivotY": 0.5,
+			"__smartColor": "#ADADB5",
+			"__bgPos": null,
+			"externalRelPath": null,
+			"fieldInstances": [],
+			"layerInstances": [],
+			"__neighbours": []
+		}
+	],
+	"worlds": [],
+	"dummyWorldIid": "018dc431-c210-11ef-bffb-cb15d2e5f164"
+}
\ No newline at end of file
diff --git a/src/assets.ts b/src/assets.ts
new file mode 100644
index 0000000..37822fa
--- /dev/null
+++ b/src/assets.ts
@@ -0,0 +1,41 @@
+class Assets {
+  #images: Record<string, HTMLImageElement>;
+
+  constructor() {
+    this.#images = {};
+  }
+
+  isLoaded(): boolean {
+    // you could use this, if so inclined, to check if a certain
+    // list of assets had been loaded prior to game start
+    //
+    // (to do so, you would call getImage() for each desired asset
+    //  and then wait for isLoaded to return true)
+    for (let filename in this.#images) {
+      if (!this.#images[filename].complete) {
+        return false
+      }
+    }
+
+    return true;
+  }
+
+  getImage(filename: string): HTMLImageElement {
+    let element: HTMLImageElement;
+    if (this.#images[filename]) {
+      element = this.#images[filename];
+    } else {
+      element = document.createElement("img");
+      element.src = filename;
+      this.#images[filename] = element;
+    }
+    return element
+  }
+}
+
+let active: Assets = new Assets();
+
+export function getAssets(): Assets {
+  return active;
+}
+
diff --git a/src/clock.ts b/src/clock.ts
new file mode 100644
index 0000000..ba841fa
--- /dev/null
+++ b/src/clock.ts
@@ -0,0 +1,44 @@
+const MAX_UPDATES_BANKED: number = 20.0;
+
+// always run physics at 240 hz
+const UPDATES_PER_MS: number = 1/(1000.0/240.0);
+
+class Clock {
+  #lastTimestamp: number | undefined;
+  #updatesBanked: number
+
+  constructor() {
+    this.#lastTimestamp = undefined;
+    this.#updatesBanked = 0.0
+  }
+
+
+  recordTimestamp(timestamp: number) {
+    if (this.#lastTimestamp) {
+      let delta = timestamp - this.#lastTimestamp;
+      this.#bankDelta(delta);
+    }
+    this.#lastTimestamp = timestamp;
+  }
+
+  popUpdate() {
+    // return true if we should do an update right now
+    // and remove one draw from the bank
+    if (this.#updatesBanked > 1) {
+      this.#updatesBanked -= 1;
+      return true
+    }
+    return false;
+  }
+  #bankDelta(delta: number) {
+    this.#updatesBanked = Math.min(delta * UPDATES_PER_MS, MAX_UPDATES_BANKED);
+  }
+}
+
+let active: Clock = new Clock();
+
+export function getClock(): Clock {
+  return active;
+}
+
+
diff --git a/src/colors.ts b/src/colors.ts
new file mode 100644
index 0000000..3a58941
--- /dev/null
+++ b/src/colors.ts
@@ -0,0 +1 @@
+export const BG_OUTER = "#000";
\ No newline at end of file
diff --git a/src/counter.ts b/src/counter.ts
new file mode 100644
index 0000000..09e5afd
--- /dev/null
+++ b/src/counter.ts
@@ -0,0 +1,9 @@
+export function setupCounter(element: HTMLButtonElement) {
+  let counter = 0
+  const setCounter = (count: number) => {
+    counter = count
+    element.innerHTML = `count is ${counter}`
+  }
+  element.addEventListener('click', () => setCounter(counter + 1))
+  setCounter(0)
+}
diff --git a/src/font.ts b/src/font.ts
new file mode 100644
index 0000000..8935b44
--- /dev/null
+++ b/src/font.ts
@@ -0,0 +1,146 @@
+import {getAssets} from "./assets.ts";
+import fontSheet from './art/fonts/vga_8x16.png';
+
+export enum AlignX {
+  Left = 0,
+  Center = 1,
+  Right = 2
+}
+
+export enum AlignY {
+  Top = 0,
+  Middle = 1,
+  Right = 2,
+}
+
+class Font {
+  #filename: string;
+  #cx: number;
+  #cy: number;
+  #px: number;
+  #py: number;
+  #tintingCanvas: HTMLCanvasElement;
+  #tintedVersions: Record<string, HTMLImageElement>;
+
+  constructor(filename: string, cx: number, cy: number, px: number, py: number) {
+    this.#filename = filename;
+    this.#cx = cx;
+    this.#cy = cy;
+    this.#px = px;
+    this.#py = py;
+    this.#tintingCanvas = document.createElement("canvas");
+    this.#tintedVersions = {}
+  }
+
+  #getTintedImage(color: string): HTMLImageElement | null {
+    let image = getAssets().getImage(this.#filename);
+
+    if (!image.complete) { return null; }
+
+    let tintedVersion = this.#tintedVersions[color];
+    if (tintedVersion != undefined) {
+      return tintedVersion;
+    }
+
+    let w = image.width;
+    let h = image.height;
+
+    if (!(w == this.#cx * this.#px && h == this.#cy * this.#py)) {
+      throw `unexpected image dimensions for font ${this.#filename}: ${w} x ${h}`
+    }
+
+    this.#tintingCanvas.width = w;
+    this.#tintingCanvas.height = h;
+    let ctx = this.#tintingCanvas.getContext("2d")!;
+    ctx.clearRect(0, 0, w, h);
+    ctx.drawImage(image, 0, 0);
+    ctx.globalCompositeOperation = "source-in";
+    ctx.fillStyle = color;
+    ctx.rect(0, 0, w, h);
+    ctx.fill();
+
+    let result = new Image();
+    result.src = this.#tintingCanvas.toDataURL();
+    this.#tintedVersions[color] = result;
+    return result;
+  }
+
+  drawText({ctx, text, x, y, alignX, alignY, forceWidth, color}: {
+    ctx: CanvasRenderingContext2D,
+    text: string,
+    x: number, y: number, alignX?: AlignX, alignY?: AlignY,
+    forceWidth?: number, color?: string
+  }) {
+    alignX = alignX == undefined ? AlignX.Left : alignX;
+    alignY = alignY == undefined ? AlignY.Top : alignY;
+    forceWidth = forceWidth == undefined ? 65535 : forceWidth;
+    color = color == undefined ? "#ffffff" : color;
+
+    let image = this.#getTintedImage(color)
+    if (image == null) {
+      return;
+    }
+
+    let wcx = Math.floor(forceWidth / this.#px);
+
+    let sz = this.#glyphwise(text, wcx, () => {});
+    let offsetX = x;
+    let offsetY = y;
+    offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w)
+    offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h)
+
+    this.#glyphwise(text, wcx, (cx, cy, char) => {
+      let srcIx = char.charCodeAt(0);
+      this.#drawGlyph({ctx: ctx, image: image, ix: srcIx, x: offsetX + cx * this.#px, y: offsetY + cy * this.#py});
+    })
+  }
+
+  #drawGlyph({ctx, image, ix, x, y}: {ctx: CanvasRenderingContext2D, image: HTMLImageElement, ix: number, x: number, y: number}) {
+    let srcCx = ix % this.#cx;
+    let srcCy = Math.floor(ix / this.#cx);
+    let srcPx = srcCx * this.#px;
+    let srcPy = srcCy * this.#py;
+    ctx.drawImage(
+      image,
+      srcPx, srcPy, this.#px, this.#py,
+      Math.floor(x), Math.floor(y), this.#px, this.#py
+    );
+  }
+
+  measureText({text, forceWidth}: {text: string, forceWidth?: number}): {w: number, h: number} {
+    return this.#glyphwise(text, forceWidth, () => {});
+  }
+
+  #glyphwise(text: string, forceWidth: number | undefined, callback: (x: number, y: number, char: string) => void): {w: number, h: number} {
+    let cx = 0;
+    let cy = 0;
+    let cw = 0;
+    let ch = 0;
+    let wcx = forceWidth == undefined ? undefined : Math.floor(forceWidth / this.#px);
+
+    for (let i = 0; i < text.length; i++) {
+      let char = text[i]
+      if (char == '\n') {
+        cx = 0;
+        cy += 1;
+        ch = cy + 1;
+      } else {
+        // console.log("cx, cy, char", [cx, cy, char])
+        callback(cx, cy, char)
+        cx += 1;
+        cw = Math.max(cw, cx);
+        ch = cy + 1;
+
+        if (wcx != undefined && cx > wcx) {
+          cx = 0;
+          cy += 1;
+          ch = cy + 1;
+        }
+      }
+    }
+
+    return { w: cw * this.#px, h: ch * this.#py };
+  }
+}
+
+export let mainFont = new Font(fontSheet, 32, 8, 8, 16);
\ No newline at end of file
diff --git a/src/game.ts b/src/game.ts
new file mode 100644
index 0000000..c6ddb95
--- /dev/null
+++ b/src/game.ts
@@ -0,0 +1,172 @@
+import {desiredHeight, desiredWidth, getScreen} from "./screen.ts";
+import {BG_OUTER} from "./colors.ts";
+import {mainFont} from "./font.ts";
+import {getInput} from "./input.ts";
+
+type Point = {x: number, y: number}
+
+class MenuCamera {
+  // measured in whole screens
+  position: Point;
+  target: Point
+
+  constructor({position, target}: {position: Point, target: Point}) {
+    this.position = position;
+    this.target = target;
+  }
+
+  update() {
+    let adjust = (x0: number, x1: number) => {
+      if (Math.abs(x1 - x0) < 0.01) { return x1; }
+      return (x0 * 8 + x1 * 2) / 10;
+    }
+    this.position.x = adjust(this.position.x, this.target.x);
+    this.position.y = adjust(this.position.y, this.target.y);
+  }
+}
+
+type GameState = "Overview" | "Gameplay" | "Thralls";
+
+function getScreenLocation(state: GameState): {x: number, y: number} {
+  if (state === "Overview") {
+    return {x: 0.0, y: 0.0}
+  }
+  if (state === "Gameplay") {
+    return {x: 1.0, y: 0.0}
+  }
+  if (state === "Thralls") {
+    return {x: 0.0, y: 1.0}
+  }
+
+  throw `invalid state: ${state}`
+}
+
+export class Game {
+  msg: string;
+  camera: MenuCamera;
+  state: GameState;
+
+  constructor() {
+    this.msg = "You have been given a gift.";
+    this.camera = new MenuCamera({
+      position: {x: 0.0, y: 0.0},
+      target: {x: 0.0, y: 0.0}
+    });
+    this.state = "Overview";
+  }
+
+  update() {
+    if (getInput().isPressed("a") || getInput().isPressed("w")) {
+      this.state = "Overview"
+    }
+    if (getInput().isPressed("d")) {
+      this.state = "Gameplay"
+    }
+    if (getInput().isPressed("s")) {
+      this.state = "Thralls"
+    }
+
+    if (getInput().isPressed("leftMouse")) {
+      this.msg = "Left click, asshole!"
+    }
+    if (getInput().isReleased("leftMouse")) {
+      this.msg = "Un-left click, asshole!"
+    }
+
+    this.camera.target = getScreenLocation(this.state);
+    this.camera.update();
+  }
+
+  draw() {
+    let screen = getScreen();
+    let ctx = screen.makeContext();
+
+    // draw screen background
+    ctx.fillStyle = BG_OUTER;
+    ctx.fillRect(0, 0, screen.w, screen.h);
+
+    this.drawOverview();
+    // this.drawGameplay();
+
+    // we draw all states at once and pan between them
+    // mainFont.drawText({ctx: ctx, text: "You have been given a gift.", x: 0, y: 0})
+    let xy = this.getCameraMouseXy();
+    if (xy != null) {
+      ctx = this.makeCameraContext();
+      ctx.fillStyle = "#fff";
+      ctx.strokeStyle = "#fff";
+      ctx.fillRect(xy.x - 1, xy.y - 1, 3, 3);
+    }
+  }
+
+  getCameraOffset(): Point {
+    let screen = getScreen();
+    let {w, h} = screen;
+    return {
+      x: Math.floor(w * this.camera.position.x),
+      y: Math.floor(h * this.camera.position.y)
+    };
+  }
+
+  getCameraMouseXy(): Point | null {
+    let xy = getInput().mouseXy();
+    if (xy == null) {
+      return null;
+    }
+    let {x: dx, y: dy} = this.getCameraOffset();
+    return {
+      x: xy.x + dx,
+      y: xy.y + dy,
+    };
+  }
+
+  makeCameraContext() {
+    let screen = getScreen();
+    let ctx = screen.makeContext();
+    let {x, y} = this.getCameraOffset();
+
+    ctx.translate(-x, -y);
+    return ctx;
+  }
+
+  getPaneRegionForGameState(gameState: GameState) {
+    let screen = getScreen();
+    let {w, h} = screen;
+    let overallScreenLocation = getScreenLocation(gameState);
+
+    let bigPaneX = overallScreenLocation.x * w;
+    let bigPaneY = overallScreenLocation.y * h;
+    let bigPaneW = w;
+    let bigPaneH = h;
+
+    let smallPaneW = desiredWidth;
+    let smallPaneH = desiredHeight;
+    let smallPaneX = Math.floor(bigPaneX + (bigPaneW - smallPaneW) / 2)
+    let smallPaneY = Math.floor(bigPaneY + (bigPaneH - smallPaneH) / 2)
+
+    return {
+      big: {
+        position: {x: bigPaneX, y: bigPaneY},
+        size: {x: bigPaneW, y: bigPaneH}
+      },
+      small: {
+        position: {x: smallPaneX, y: smallPaneY},
+        size: {x: smallPaneW, y: smallPaneH}
+      }
+    }
+
+  }
+
+  drawOverview() {
+    let region = this.getPaneRegionForGameState("Overview")
+    let ctx = this.makeCameraContext()
+    ctx.translate(region.small.position.x, region.small.position.y)
+
+    // ctx.strokeStyle = "#ffffff";
+    // ctx.strokeRect(0.5, 0.5, region.small.size.x - 1, region.small.size.y - 1);
+
+    mainFont.drawText({ctx: ctx, text: this.msg, x: 0, y: 0})
+  }
+}
+
+export let game = new Game();
diff --git a/src/input.ts b/src/input.ts
new file mode 100644
index 0000000..bacf65b
--- /dev/null
+++ b/src/input.ts
@@ -0,0 +1,108 @@
+import {getScreen} from "./screen.ts";
+
+function handleKey(e: KeyboardEvent, down: boolean) {
+  active.handleDown(e.key, down);
+}
+function handleMouseOut() {
+  active.handleMouseMove(-1, -1);
+}
+
+function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) {
+  if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
+    return;
+  }
+  active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
+}
+
+function handleMouseButton(canvas: HTMLCanvasElement, m: MouseEvent, down: boolean) {
+  if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) {
+    return;
+  }
+  active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight);
+  let button: KnownButton | null = (
+    m.button == 0 ? "leftMouse" :
+      m.button == 1 ? "rightMouse" :
+        null
+  )
+  if (button != null) {
+    active.handleDown(button, down);
+  }
+}
+
+
+export function setupInput(canvas: HTMLCanvasElement) {
+  canvas.addEventListener("keyup", (k) => handleKey(k, false));
+  document.addEventListener("keyup", (k) => handleKey(k, false));
+  canvas.addEventListener("keydown", (k) => handleKey(k, true));
+  document.addEventListener("keydown", (k) => handleKey(k, true));
+  canvas.addEventListener("mouseout", (_) => handleMouseOut());
+  canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m));
+  canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true));
+  canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false));
+}
+
+type KnownKey = "w" | "a" | "s" | "d";
+type KnownButton = "leftMouse" | "rightMouse";
+
+class Input {
+  #down: Record<string, boolean>;
+  #previousDown: Record<string, boolean>;
+  #mouseXy: {x: number, y: number} | null;
+
+  constructor() {
+    this.#down = {};
+    this.#previousDown = {};
+    this.#mouseXy = null;
+  }
+
+  update() {
+    this.#previousDown = {...this.#down};
+  }
+
+  handleDown(name: string, down: boolean) {
+    if (down) {
+      this.#down[name] = down;
+    }
+    else {
+      delete this.#down[name];
+    }
+  }
+
+  handleMouseMove(x: number, y: number) {
+    let screen = getScreen();
+    if (x < 0.0 || x >= 1.0) { this.#mouseXy = null; }
+    if (y < 0.0 || y >= 1.0) { this.#mouseXy = null; }
+
+    let w = screen.w;
+    let h = screen.h;
+    this.#mouseXy = {
+      x: Math.floor(x * w),
+      y: Math.floor(y * h),
+    }
+  }
+
+  isDown(key: KnownKey | KnownButton) : boolean {
+    return this.#down[key];
+  }
+
+  isPressed(key: KnownKey | KnownButton) : boolean {
+    return this.#down[key] && !this.#previousDown[key];
+  }
+
+  isReleased(key: KnownKey | KnownButton) : boolean {
+    return !this.#down[key] && this.#previousDown[key];
+  }
+
+  mouseXy(): {x: number, y: number} | null {
+    if (this.#mouseXy == null) {
+      return null;
+    }
+    return {...this.#mouseXy}
+  }
+}
+
+let active = new Input();
+
+export function getInput(): Input {
+  return active;
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..9139762
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,87 @@
+import './style.css'
+import {pollAndTouch} from "./screen.ts";
+import {getClock} from "./clock.ts";
+import {game} from "./game.ts";
+import {getInput, setupInput} from "./input.ts";
+// import typescriptLogo from './typescript.svg'
+// import viteLogo from '/vite.svg'
+// import { setupCounter } from './counter.ts'
+
+// import {AlignX, mainFont} from "./font.ts";
+
+
+function setupGame() {
+  let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
+  setupInput(gameCanvas);
+  onFrame(undefined);  // start on-frame draw loop, set up screen
+}
+
+function onFrame(timestamp: number | undefined) {
+  let gameCanvas = document.getElementById("game") as HTMLCanvasElement;
+  requestAnimationFrame(onFrame);
+
+  if (timestamp) {
+    getClock().recordTimestamp(timestamp);
+  }
+  onFrameFixScreen(gameCanvas);
+
+  while (getClock().popUpdate()) {
+    game.update();
+    getInput().update();
+  }
+
+  game.draw();
+
+  /*
+  let ctx = getScreen().canvas.getContext("2d")!;
+  ctx.fillStyle = "#000";
+  ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
+
+  // ctx.drawImage(getAssets().getImage(font), 0, frame % (getScreen().h + 256) - 128);
+  // console.log(mainFont.measureText({text: "a!\nb\n"}));
+  mainFont.drawText({
+    ctx: ctx,
+    text: "Hello, world!\nI wish you luck!",
+    x: gameCanvas.width,
+    y: 0,
+    color: "#f00",
+    alignX: AlignX.Right,
+  });
+  mainFont.drawText({
+    ctx: ctx,
+    text: "^._.^",
+    x: gameCanvas.width/2,
+    y: 32,
+    color: "#0ff",
+    alignX: AlignX.Center,
+  });
+   */
+}
+
+function onFrameFixScreen(canvas: HTMLCanvasElement) {
+  pollAndTouch(canvas);
+}
+
+setupGame();
+
+/*
+document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
+  <div>
+    <a href="https://vite.dev" target="_blank">
+      <img src="${viteLogo}" class="logo" alt="Vite logo" />
+    </a>
+    <a href="https://www.typescriptlang.org/" target="_blank">
+      <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
+    </a>
+    <h1>Vite + TypeScript</h1>
+    <div class="card">
+      <button id="counter" type="button"></button>
+    </div>
+    <p class="read-the-docs">
+      Click on the Vite and TypeScript logos to learn more
+    </p>
+  </div>
+`
+
+setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
+ */
\ No newline at end of file
diff --git a/src/screen.ts b/src/screen.ts
new file mode 100644
index 0000000..19e1936
--- /dev/null
+++ b/src/screen.ts
@@ -0,0 +1,62 @@
+class Screen {
+  #canvas: HTMLCanvasElement
+  w: number
+  h: number
+
+  constructor(canvas: HTMLCanvasElement, w: number, h: number) {
+    this.#canvas = canvas;
+    this.w = w;
+    this.h = h;
+  }
+
+  makeContext(): CanvasRenderingContext2D {
+    let ctx = this.#canvas.getContext("2d")!;
+
+    // TODO: Other stuff to do here?
+    ctx.strokeStyle = "#000";
+    ctx.fillStyle = "#000";
+    ctx.resetTransform();
+    ctx.imageSmoothingEnabled = false;
+
+    return ctx;
+  }
+}
+
+
+let active: Screen | undefined = undefined
+export let desiredWidth = 384;
+export let desiredHeight = 384;
+
+export function pollAndTouch(canvas: HTMLCanvasElement) {
+  let realWidth = canvas.offsetWidth;
+  let realHeight = canvas.offsetHeight;
+
+  let divisors = [0.25, 0.5, 1, 2, 3, 4, 5, 6];
+  for (let i = 0; i < divisors.length; i++) {
+    // TODO: Is this necessary?
+    divisors[i] /= window.devicePixelRatio;
+  }
+
+  let div = 0;
+  while (
+    (div < divisors.length - 1) &&
+    (realWidth / divisors[div + 1] >= desiredWidth) &&
+    (realHeight / divisors[div + 1] >= desiredHeight)
+  ) {
+    div += 1;
+  }
+  realWidth = Math.floor(canvas.offsetWidth / divisors[div]);
+  realHeight = Math.floor(canvas.offsetHeight / divisors[div]);
+  canvas.width = realWidth;
+  canvas.height = realHeight;
+  active = new Screen(canvas, realWidth, realHeight);
+}
+
+export function getScreen(): Screen {
+  if (active === undefined) {
+    throw `screen should have been defined: ${active}`
+  }
+  return active;
+}
+
+
diff --git a/src/sprite.ts b/src/sprite.ts
new file mode 100644
index 0000000..2b84c5f
--- /dev/null
+++ b/src/sprite.ts
@@ -0,0 +1,48 @@
+import {getAssets} from "./assets.ts";
+
+
+export class Sprite {
+  readonly imageSet: string;
+  // spritesheet params
+  // image size (px, py)
+  readonly px: number; readonly py: number;
+  // origin (ox, oy)
+  readonly ox: number; readonly oy: number;
+  // dimension in cells (cx, cy)
+  readonly cx: number; readonly cy: number;
+  // number of frames
+  readonly nFrames: number;
+
+  constructor(imageSet: string, px: number, py: number, ox: number, oy: number, cx: number, cy: number, nFrames: number) {
+    this.imageSet = imageSet;
+    this.px = px; this.py = py;
+    this.ox = ox; this.oy = oy;
+    this.cx = cx; this.cy = cy;
+    this.nFrames = nFrames;
+
+    if (this.nFrames < this.cx * this.cy) {
+      throw `can't have ${this.nFrames} with a spritesheet dimension of ${this.cx * this.cy}`;
+    }
+  }
+
+  draw(ctx: CanvasRenderingContext2D, {x, y, ix, xScale, yScale, angle}: {x: number, y: number, ix?: number, xScale?: number, yScale?: number, angle?: number}) {
+    ix = ix == undefined ? 0 : ix;
+    xScale = xScale == undefined ? 1.0 : xScale;
+    yScale = yScale == undefined ? 1.0 : yScale;
+    angle = angle == undefined ? 0.0 : angle;
+
+    // ctx.translate(Math.floor(x), Math.floor(y));
+    ctx.translate(x, y);
+    ctx.rotate(angle * Math.PI / 180);
+    ctx.scale(xScale, yScale);
+    ctx.translate(-this.ox, -this.oy);
+
+    let me = getAssets().getImage(this.imageSet);
+    let srcCx = ix % this.cx;
+    let srcCy = Math.floor(ix / this.cx);
+    let srcPx = srcCx * this.px;
+    let srcPy = srcCy * this.py;
+    console.log(`src px and py ${srcPx} ${srcPy}`)
+    ctx.drawImage(me, srcPx, srcPy, this.px, this.py, 0, 0, this.px, this.py);
+  }
+}
diff --git a/src/sprites.ts b/src/sprites.ts
new file mode 100644
index 0000000..fea8d68
--- /dev/null
+++ b/src/sprites.ts
@@ -0,0 +1,14 @@
+import {Sprite} from "./sprite.ts";
+import imgBat from "./art/characters/bat.png";
+import imgKobold from "./art/characters/kobold.png";
+import imgRaccoon from "./art/characters/raccoon.png";
+import imgRaccoonWalking from "./art/characters/raccoon_walking.png";
+import imgRobot from "./art/characters/robot.png";
+import imgSnake from "./art/characters/snake.png";
+
+export let sprBat = new Sprite(imgBat, 64, 64, 32, 32, 1, 1, 1);
+export let sprKobold = new Sprite(imgKobold, 64, 64, 32, 32, 1, 1, 1);
+export let sprRaccoon = new Sprite(imgRaccoon, 64, 64, 32, 32, 1, 1, 1);
+export let sprRaccoonWalking = new Sprite(imgRaccoonWalking, 64, 64, 32, 32, 8, 1, 8);
+export let sprRobot = new Sprite(imgRobot, 64, 64, 32, 32, 1, 1, 1);
+export let sprSnake = new Sprite(imgSnake, 64, 64, 32, 32, 1, 1, 1);
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..42ed2b5
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,12 @@
+html, body {
+  padding: 0;
+  margin: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+#game {
+  image-rendering: pixelated;
+  width: 100%;
+  height: 100%;
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..a4883f2
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["src"]
+}