Compare commits

..

No commits in common. "main" and "fresh" have entirely different histories.
main ... fresh

78 changed files with 828 additions and 4574 deletions

33
.gitignore vendored
View File

@ -1,30 +1,3 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
.vite
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Packed devtools
devtools.zip
temp.*
bundle.js
dist/
temp.ts

3
.temp/.gitignore vendored
View File

@ -1,3 +0,0 @@
bundle.js
dist/
temp.ts

View File

@ -1,18 +0,0 @@
{
"compilerOptions": {
"lib": [
"deno.ns",
"deno.window",
"dom",
"dom.iterable",
"ES2021",
"ESNext"
]
},
"tasks": {
"dev": "deno run -RWEN --allow-run dev.ts dev"
},
"imports": {
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5"
}
}

238
.temp/deno.lock generated
View File

@ -1,238 +0,0 @@
{
"version": "4",
"specifiers": {
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
"jsr:@std/assert@*": "1.0.10",
"jsr:@std/assert@^1.0.10": "1.0.10",
"jsr:@std/bytes@^1.0.2": "1.0.2",
"jsr:@std/cli@^1.0.8": "1.0.9",
"jsr:@std/encoding@^1.0.5": "1.0.6",
"jsr:@std/fmt@^1.0.3": "1.0.3",
"jsr:@std/html@^1.0.3": "1.0.3",
"jsr:@std/http@*": "1.0.12",
"jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@^1.0.6": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8",
"jsr:@std/streams@^1.0.8": "1.0.8",
"jsr:@std/testing@*": "1.0.8",
"npm:esbuild@*": "0.24.2"
},
"jsr": {
"@bearmetal/doodler@0.0.4": {
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
},
"@luca/esbuild-deno-loader@0.11.0": {
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
"dependencies": [
"jsr:@std/bytes",
"jsr:@std/encoding",
"jsr:@std/path@^1.0.6"
]
},
"@luca/esbuild-deno-loader@0.11.1": {
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
"dependencies": [
"jsr:@std/bytes",
"jsr:@std/encoding",
"jsr:@std/path@^1.0.6"
]
},
"@std/assert@1.0.10": {
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/bytes@1.0.2": {
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
},
"@std/cli@1.0.9": {
"integrity": "557e5865af000efbf3f737dcfea5b8ab86453594f4a9cd8d08c9fa83d8e3f3bc"
},
"@std/encoding@1.0.6": {
"integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069"
},
"@std/fmt@1.0.3": {
"integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
},
"@std/html@1.0.3": {
"integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
},
"@std/http@1.0.12": {
"integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa",
"dependencies": [
"jsr:@std/cli",
"jsr:@std/encoding",
"jsr:@std/fmt",
"jsr:@std/html",
"jsr:@std/media-types",
"jsr:@std/net",
"jsr:@std/path@^1.0.8",
"jsr:@std/streams"
]
},
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
"@std/net@1.0.4": {
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
},
"@std/streams@1.0.8": {
"integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3"
},
"@std/testing@1.0.8": {
"integrity": "ceef535808fb7568e91b0f8263599bd29b1c5603ffb0377227f00a8ca9fe42a2",
"dependencies": [
"jsr:@std/assert@^1.0.10",
"jsr:@std/internal"
]
}
},
"npm": {
"@esbuild/aix-ppc64@0.24.2": {
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="
},
"@esbuild/android-arm64@0.24.2": {
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="
},
"@esbuild/android-arm@0.24.2": {
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="
},
"@esbuild/android-x64@0.24.2": {
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="
},
"@esbuild/darwin-arm64@0.24.2": {
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="
},
"@esbuild/darwin-x64@0.24.2": {
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="
},
"@esbuild/freebsd-arm64@0.24.2": {
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="
},
"@esbuild/freebsd-x64@0.24.2": {
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="
},
"@esbuild/linux-arm64@0.24.2": {
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="
},
"@esbuild/linux-arm@0.24.2": {
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="
},
"@esbuild/linux-ia32@0.24.2": {
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="
},
"@esbuild/linux-loong64@0.24.2": {
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="
},
"@esbuild/linux-mips64el@0.24.2": {
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="
},
"@esbuild/linux-ppc64@0.24.2": {
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="
},
"@esbuild/linux-riscv64@0.24.2": {
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="
},
"@esbuild/linux-s390x@0.24.2": {
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="
},
"@esbuild/linux-x64@0.24.2": {
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="
},
"@esbuild/netbsd-arm64@0.24.2": {
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="
},
"@esbuild/netbsd-x64@0.24.2": {
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="
},
"@esbuild/openbsd-arm64@0.24.2": {
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="
},
"@esbuild/openbsd-x64@0.24.2": {
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="
},
"@esbuild/sunos-x64@0.24.2": {
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="
},
"@esbuild/win32-arm64@0.24.2": {
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="
},
"@esbuild/win32-ia32@0.24.2": {
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="
},
"@esbuild/win32-x64@0.24.2": {
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="
},
"esbuild@0.24.2": {
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
"@esbuild/android-x64",
"@esbuild/darwin-arm64",
"@esbuild/darwin-x64",
"@esbuild/freebsd-arm64",
"@esbuild/freebsd-x64",
"@esbuild/linux-arm",
"@esbuild/linux-arm64",
"@esbuild/linux-ia32",
"@esbuild/linux-loong64",
"@esbuild/linux-mips64el",
"@esbuild/linux-ppc64",
"@esbuild/linux-riscv64",
"@esbuild/linux-s390x",
"@esbuild/linux-x64",
"@esbuild/netbsd-arm64",
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
]
}
},
"redirects": {
"https://deno.land/x/clipboard/mod.ts": "https://deno.land/x/clipboard@v0.0.3/mod.ts"
},
"remote": {
"https://deno.land/x/clipboard@v0.0.3/mod.ts": "5b68fe3b710b852de5273c135fc3c11b2b4050577401ee994161380bd8cab219",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/canvas.ts": "aadfb4b2e9acce34d4a5da3f9027be642c93229bbfc2641cb55301542cbb87bf",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/geometry/constants.ts": "4f4cf7bf49ac871d984e9b43896783b0cc8ab0ea60d0fc4c8c582f7e00c3df5a",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/geometry/vector.ts": "a08ecff64c5436a28c6451a31c68fc912d25f941aabafb79418fa0a1aeffa9d2",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.0.7a/mod.ts": "766bdedc7e28b89d3cb3e83ee55c612bfaeabe252a14ff47e5e676535e033d88",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/animation/gif.ts": "6f8b77cb55b252bd7c18b04fa7ff4e88b4459cf1158d8daef538b2e471433420",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/animation/sprite.ts": "64adc3843b48a0d74ad96cbf4a4d26426c1e909a03ca935f73d5ec5545080f20",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/canvas.ts": "5af9d684e1144a374f0fbee46c710f9d493d5491e90b17356d910c6ade32bb50",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/aa.ts": "c27a1deee0b2ed02e3a88e4e0b370ca2dfa0f57bf783724fa5c099e9eeabc5c9",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/circular.ts": "962703eacb19cc849f3fb355815edfd71e12d06f8e72f517a7c038ff2d1c1729",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/collision/sat.ts": "f221540a984c908c96b4cc86a8eddacf3d3a5dfa5367ba538c02bcf7f7038247",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/constants.ts": "4f4cf7bf49ac871d984e9b43896783b0cc8ab0ea60d0fc4c8c582f7e00c3df5a",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/polygon.ts": "6c7edf576bebd7f24b1358ecba70d561d5905e0185701e12437ba7ccdacc66a9",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/spline.ts": "3521ea5b57902001fb9a248580bd66f12f563a581eff137f5c67e2edc0305ba0",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/geometry/vector.ts": "0143daf300032d6faf5a073fffa5c298fdcd74ba2d6bcd10a2d96ab54e55bc69",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/init.ts": "0e08fdf4c896f88308e6a6a2fb8842fe3a67a3a47a5ad722ecbce37737f8694d",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/mod.ts": "ffcbd74b612db108d50f5e2e1ba7425c7e6fac87f3fe7fb43c10a5283501513e",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/processing/gif.ts": "e97456fd55806086aa90d9bc46193d355c2f6093f376f4141ca959942193e4dc",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts": "9eba3d8f5bf5e03220c93916cff6f0bbc24ecdf7550f21fd99e3aaf310f625b0",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts": "3948648f8bdf8f1ecea83120c41211f5543c7933dbe3e49b367285a98ed50a9a",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts": "395f80ddaef83e2b37a2884d7fffefae80c2bcecb72269405f53899d5dfc9956"
},
"workspace": {
"dependencies": [
"jsr:@bearmetal/doodler@0.0.5"
]
}
}

View File

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRAINS!</title>
<style>
html, body {
background-color: black;
color: white;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
}
#context {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
display: flex;
gap: 10px;
max-height: 50vh;
overflow-y: auto;
}
#fps {
position: absolute;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
display: flex;
gap: 10px;
max-height: 50vh;
overflow-y: auto;
}
</style>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

View File

@ -1 +0,0 @@
[{"p":[[200,24,0],[233,24,0],[264.87555226753926,32.541028488383176,0],[293.45439059242574,49.041028488383176,0]],"id":"11d7561a-3172-4ad7-9d53-6f65f49ce8c3","bNeighbors":["93e4d69d-10f2-4ecc-a4d0-560ee71708e8"],"fNeighbors":["e44c3a93-01f0-42f1-aee9-939cbde5903b"]},{"p":[[293.45439059242574,49.041028488383176,0],[322.0332289173122,65.54102848838318,0],[345.3677526964683,88.87555226753923,0],[361.8677526964683,117.45439059242571,0]],"id":"e44c3a93-01f0-42f1-aee9-939cbde5903b","bNeighbors":["11d7561a-3172-4ad7-9d53-6f65f49ce8c3"],"fNeighbors":["bf9833f2-fd66-45fb-924d-f40b7c863c26"]},{"p":[[361.8677526964683,117.45439059242571,0],[378.3677526964683,146.0332289173122,0],[386.9087811848515,177.90878118485145,0],[386.9087811848515,210.90878118485148,0]],"id":"bf9833f2-fd66-45fb-924d-f40b7c863c26","bNeighbors":["e44c3a93-01f0-42f1-aee9-939cbde5903b"],"fNeighbors":["081c81f3-8fe7-4c71-babd-1d02d52c3385"]},{"p":[[386.9087811848515,210.90878118485148,0],[386.9087811848515,243.90878118485148,0],[378.3677526964683,275.78433345239074,0],[361.8677526964683,304.3631717772772,0]],"id":"081c81f3-8fe7-4c71-babd-1d02d52c3385","bNeighbors":["bf9833f2-fd66-45fb-924d-f40b7c863c26"],"fNeighbors":["d1380635-1dba-4180-8038-931289a56d17"]},{"p":[[361.8677526964683,304.3631717772772,0],[345.3677526964683,332.9420101021637,0],[322.0332289173122,356.2765338813198,0],[293.45439059242574,372.7765338813198,0]],"id":"d1380635-1dba-4180-8038-931289a56d17","bNeighbors":["081c81f3-8fe7-4c71-babd-1d02d52c3385"],"fNeighbors":["8fe94c53-c0be-4f0d-986b-f37e143e62d7"]},{"p":[[293.45439059242574,372.7765338813198,0],[264.87555226753926,389.2765338813198,0],[233,397.81756236970296,0],[200,397.81756236970296,0]],"id":"8fe94c53-c0be-4f0d-986b-f37e143e62d7","bNeighbors":["d1380635-1dba-4180-8038-931289a56d17"],"fNeighbors":["1542c063-b548-4d27-9fd8-c7a8344b05cb"]},{"p":[[200,397.81756236970296,0],[167,397.81756236970296,0],[135.12444773246074,389.2765338813198,0],[106.54560940757426,372.7765338813198,0]],"id":"1542c063-b548-4d27-9fd8-c7a8344b05cb","bNeighbors":["8fe94c53-c0be-4f0d-986b-f37e143e62d7"],"fNeighbors":["9be20051-651f-4cca-b6f1-ca80d5db7635"]},{"p":[[106.54560940757426,372.7765338813198,0],[77.96677108268779,356.2765338813198,0],[54.6322473035317,332.94201010216375,0],[38.1322473035317,304.3631717772772,0]],"id":"9be20051-651f-4cca-b6f1-ca80d5db7635","bNeighbors":["1542c063-b548-4d27-9fd8-c7a8344b05cb"],"fNeighbors":["4b7ff960-9fe7-48b3-b64e-9e32f60b0d2f"]},{"p":[[38.1322473035317,304.3631717772772,0],[21.632247303531692,275.7843334523908,0],[13.091218815148487,243.9087811848515,0],[13.09121881514848,210.90878118485153,0]],"id":"4b7ff960-9fe7-48b3-b64e-9e32f60b0d2f","bNeighbors":["9be20051-651f-4cca-b6f1-ca80d5db7635"],"fNeighbors":["0c481aa2-92de-4517-8e76-6c3f163f3dde"]},{"p":[[13.09121881514848,210.90878118485153,0],[13.091218815148467,177.90878118485153,0],[21.632247303531646,146.03322891731227,0],[38.132247303531635,117.45439059242577,0]],"id":"0c481aa2-92de-4517-8e76-6c3f163f3dde","bNeighbors":["4b7ff960-9fe7-48b3-b64e-9e32f60b0d2f"],"fNeighbors":["c62384ad-3fec-4479-9596-1173a6e651bb"]},{"p":[[38.132247303531635,117.45439059242577,0],[54.63224730353162,88.87555226753928,0],[77.96677108268767,65.54102848838318,0],[106.54560940757415,49.041028488383176,0]],"id":"c62384ad-3fec-4479-9596-1173a6e651bb","bNeighbors":["0c481aa2-92de-4517-8e76-6c3f163f3dde"],"fNeighbors":["93e4d69d-10f2-4ecc-a4d0-560ee71708e8"]},{"p":[[106.54560940757415,49.041028488383176,0],[135.12444773246062,32.541028488383176,0],[166.9999999999999,23.99999999999997,0],[199.9999999999999,23.99999999999997,0]],"id":"93e4d69d-10f2-4ecc-a4d0-560ee71708e8","bNeighbors":["c62384ad-3fec-4479-9596-1173a6e651bb"],"fNeighbors":["11d7561a-3172-4ad7-9d53-6f65f49ce8c3"]}]

View File

@ -1,3 +0,0 @@
{
"recommendations": ["denoland.vscode-deno"]
}

View File

@ -1,22 +1,18 @@
{
"tasks": {
"dev": "deno run -A --node-modules-dir npm:vite",
"build": "deno run -A --node-modules-dir npm:vite build",
"preview": "deno run -A --node-modules-dir npm:vite preview",
"serve": "deno run --allow-net --allow-read jsr:@std/http@1/file-server dist/",
"pack-devtools": "rm -rf devtools.zip && deno run -A npm:web-ext build -o --source-dir devtools --artifacts-dir devtools.zip"
},
"compilerOptions": {
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
"deno.ns",
"deno.window",
"dom",
"dom.iterable",
"ES2021",
"ESNext"
]
},
"imports": {
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-e",
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4",
"vite": "npm:vite@^6.0.1"
"tasks": {
"dev": "deno run -RWEN --allow-run dev.ts dev"
},
"nodeModulesDir": "auto"
"imports": {
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b"
}
}

2071
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,54 +0,0 @@
console.log("background.js loaded");
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_CONTEXT_STACK") {
browser.tabs.sendMessage(message.tabId, message)
.then((res) => {
console.log("RESPONSE", res);
sendResponse(res);
})
.catch((err) => {
console.log("Error sending message to content script:", err);
sendResponse({ error: err });
});
// browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
// if (tabs.length) {
// }
// });
return true;
} else if (message.type === "UPDATE_CONTEXT_VALUE") {
browser.tabs.sendMessage(message.tabId, message).then(sendResponse);
// browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
// if (tabs.length) {
// }
// });
return true;
}
});
let devtoolsPort = null;
browser.runtime.onConnect.addListener((port) => {
if (port.name === "devtools") {
devtoolsPort = port;
console.log("Devtools panel connected.");
// port.onMessage.addListener((msg) => {
// console.log("Received message from devtools panel:", msg);
// });
port.onDisconnect.addListener(() => {
devtoolsPort = null;
});
}
});
// Relay messages from content scripts to the devtools panel.
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "PAGE_LOADED") {
console.log("Background received PAGE_LOADED message from content script.");
if (devtoolsPort) {
devtoolsPort.postMessage({ type: "PAGE_LOADED" });
}
}
});

View File

@ -1,20 +0,0 @@
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_CONTEXT_STACK") {
const contextStack = window.wrappedJSObject.getContextStack();
sendResponse({ contextStack });
} else if (message.type === "UPDATE_CONTEXT_VALUE") {
const { key, value, depth } = message;
if (window.wrappedJSObject.updateContextValue) {
window.wrappedJSObject.updateContextValue(key, value, depth);
sendResponse({ success: true });
} else {
sendResponse({
success: false,
error: "updateContextValue not defined",
});
}
}
});
browser.runtime.sendMessage({ type: "PAGE_LOADED" });

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="devtools.js"></script>
</head>
<body></body>
</html>

View File

@ -1,7 +0,0 @@
// Create a new devtools panel named "Context Stack"
browser.devtools.panels.create(
"Smoke and Rails", // Tab title
"train icon.png", // Icon for your panel (optional)
"panel.html", // HTML page for your panel content
);
console.log("devtools loaded");

View File

@ -1,37 +0,0 @@
{
"manifest_version": 2,
"name": "SNR DevTools",
"version": "1.0",
"description": "A devtools panel to view and edit context stack values.",
"author": "Emmaline Autumn",
"devtools_page": "devtools.html",
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"all_frames": true
}
],
"permissions": [
"devtools",
"tabs",
"*://*/*"
],
"icons": {
"48": "train icon.png"
},
"browser_specific_settings": {
"gecko": {
"id": "snrdt@cyborggrizzly.com"
}
}
}

View File

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Smoke and Rails</title>
<style>
body {
font-family: sans-serif;
/* margin: 10px; */
/* width: 100%;
height: 100%; */
background-color: #302040;
color: #fff;
/* text-align: right; */
}
.context-row {
display: flex;
margin-bottom: 8px;
}
.context-key {
width: 150px;
font-weight: bold;
}
.context-value input {
width: 100%;
}
form {
margin-left: 1rem;
margin-right: 1rem;
}
label {
display: inline-block;
margin-right: 1rem;
}
</style>
</head>
<body>
<h2>Smoke and Rails Debugger</h2>
<button id="refresh">Refresh</button>
<div id="contextContainer"></div>
<script src="panel.js"></script>
</body>
</html>

View File

@ -1,127 +0,0 @@
const port = browser.runtime.connect({ name: "devtools" });
port.onMessage.addListener((message) => {
if (message.type === "PAGE_LOADED") {
loadContextStack(); // Refresh your context stack here.
}
});
const tabId = browser.devtools.inspectedWindow.tabId;
document.getElementById("refresh").addEventListener("click", loadContextStack);
function loadContextStack() {
const container = document.getElementById("contextContainer");
container.innerHTML = "";
browser.runtime.sendMessage({ type: "GET_CONTEXT_STACK", tabId }).then(
(response) => {
if (response.error) {
container.innerHTML = `<p>Error: ${response.error}</p>`;
return;
}
if (!response || !response.contextStack) {
container.innerHTML = "<p>No context stack found.</p>";
return;
}
const contextStack = coalesceContextStack(response.contextStack);
const form = generateObjectForm(contextStack);
container.appendChild(form);
},
);
}
function generateObjectForm(obj, path = "") {
const detail = document.createElement("details");
detail.open = path === "";
const summary = document.createElement("summary");
summary.textContent = path.split(".").at(-1);
detail.appendChild(summary);
const form = document.createElement("form");
let count = 0;
for (const [key, value] of Object.entries(obj)) {
if (value == undefined || value === "[Circular]") continue;
const isObject = value.constructor === Object;
const isArray = Array.isArray(value);
if (isObject || isArray) {
const nestedForm = generateObjectForm(
value,
path ? path + "." + key : key,
);
if (!nestedForm) {
continue;
}
const div = document.createElement("div");
div.appendChild(nestedForm);
form.appendChild(div);
count++;
continue;
}
const div = document.createElement("div");
const label = document.createElement("label");
label.textContent = key;
div.appendChild(label);
const input = document.createElement("input");
input.name = key;
const isBoolean = typeof value === "boolean";
if (isBoolean) {
input.type = "checkbox";
input.checked = value;
input.addEventListener("change", () => {
browser.runtime.sendMessage({
type: "UPDATE_CONTEXT_VALUE",
key: path ? path + "." + key : key,
value: input.checked,
tabId,
}).then((updateResponse) => {
if (updateResponse && updateResponse.success) {
console.log(`Updated ${key} to ${input.checked}`);
} else {
console.error(`Failed to update ${key}:`, updateResponse.error);
}
});
});
} else {
input.type = "text";
input.value = value;
input.addEventListener("change", () => {
browser.runtime.sendMessage({
type: "UPDATE_CONTEXT_VALUE",
key: path ? path + "." + key : key,
value: input.value,
tabId,
}).then((updateResponse) => {
if (updateResponse && updateResponse.success) {
console.log(`Updated ${key} to ${input.value}`);
} else {
console.error(`Failed to update ${key}:`, updateResponse.error);
}
});
});
}
div.appendChild(input);
form.appendChild(div);
count++;
}
if (count === 0) {
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(obj, null, 2);
detail.appendChild(pre);
} else {
detail.appendChild(form);
}
return detail;
}
function coalesceContextStack(contextStack) {
const obj = {};
for (const ctx of contextStack) {
Object.assign(obj, ctx);
}
return obj;
}
document.addEventListener("DOMContentLoaded", () => {
loadContextStack();
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -5,10 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TRAINS!</title>
<link rel="shortcut icon" href="train icon.png" type="image/x-icon">
<style>
html,
body {
html, body {
background-color: black;
color: white;
margin: 0;
@ -16,7 +14,6 @@
height: 100%;
width: 100%;
}
#context {
position: absolute;
top: 0;
@ -29,7 +26,6 @@
max-height: 50vh;
overflow-y: auto;
}
#fps {
position: absolute;
top: 0;
@ -44,8 +40,7 @@
}
</style>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
<script src="bundle.js"></script>
</body>
</html>

17
inputs.ts Normal file
View File

@ -0,0 +1,17 @@
import { getContextItem } from "./lib/context.ts";
import { InputManager } from "./lib/input.ts";
import { StateMachine } from "./state/machine.ts";
import { States } from "./state/states/index.ts";
export function bootstrapInputs() {
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.onKey("e", () => {
const state = getContextItem<StateMachine<States>>("state");
state.transitionTo(States.EDIT_TRACK);
});
inputManager.onKey("Delete", () => {
if (inputManager.getKeyState("Control")) {
localStorage.removeItem("track");
}
});
}

94
lib/context.ts Normal file
View File

@ -0,0 +1,94 @@
type ContextStore = Record<string, any>;
const defaultContext: ContextStore = {};
const contextStack: ContextStore[] = [defaultContext];
const debug = JSON.parse(localStorage.getItem("debug") || "false");
export function setDefaultContext(context: ContextStore) {
Object.assign(defaultContext, context);
}
export function withContext<T>(context: ContextStore, fn: () => T): T {
contextStack.push(context);
try {
return fn();
} finally {
contextStack.pop();
}
}
export const ctx = new Proxy(
{},
{
get(_, prop: string) {
for (let i = contextStack.length - 1; i >= 0; i--) {
if (prop in contextStack[i]) return contextStack[i][prop];
}
if (prop in defaultContext) return defaultContext[prop];
throw new Error(`Context variable '${prop}' is not defined.`);
},
},
) as Record<string, unknown>;
export function getContext() {
return ctx;
}
export function getContextItem<T>(prop: string): T {
return ctx[prop] as T;
}
export function getContextItemOrDefault<T>(prop: string, defaultValue: T): T {
try {
return ctx[prop] as T;
} catch {
return defaultValue;
}
}
export function setContextItem<T>(prop: string, value: T) {
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
[prop]: value,
});
}
if (debug) {
setInterval(() => {
let ctxEl = document.getElementById("context");
if (!ctxEl) {
ctxEl = document.createElement("div");
ctxEl.id = "context";
document.body.append(ctxEl);
}
ctxEl.innerHTML = "";
const div = document.createElement("div");
const pre = document.createElement("pre");
const h3 = document.createElement("h3");
h3.textContent = "Default";
div.append(h3);
pre.textContent = safeStringify(defaultContext);
div.append(pre);
ctxEl.append(div);
for (const [idx, ctx] of contextStack.entries()) {
const div = document.createElement("div");
const pre = document.createElement("pre");
const h3 = document.createElement("h3");
h3.textContent = "CTX " + idx;
div.append(h3);
pre.textContent = safeStringify(ctx);
div.append(pre);
ctxEl.append(div);
}
}, 1000);
}
function safeStringify(obj: any) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]"; // Replace circular references
}
seen.add(value);
}
return value;
}, 2);
}

38
lib/resources.ts Normal file
View File

@ -0,0 +1,38 @@
export class ResourceManager {
private resources: Map<string, unknown> = new Map();
private statuses: Map<string, Promise<boolean>> = new Map();
get<T>(name: string): T {
if (!this.resources.has(name)) {
throw new Error(`Resource ${name} not found`);
}
return this.resources.get(name) as T;
}
set(name: string, value: unknown) {
if (typeof (value as EventSource).addEventListener === "function") {
this.statuses.set(
name,
new Promise((resolve) => {
const onload = () => {
this.resources.set(name, value);
resolve(true);
(value as EventSource).removeEventListener("load", onload);
};
(value as EventSource).addEventListener("load", onload);
}),
);
} else {
console.warn("Resource added was not a loadable resource");
}
this.resources.set(name, value);
}
delete(name: string) {
this.resources.delete(name);
}
ready() {
return Promise.all(Array.from(this.statuses.values()));
}
}

View File

@ -20,14 +20,11 @@ const resources = new ResourceManager();
const doodler = new ZoomableDoodler({
fillScreen: true,
bg: "#302040",
noSmooth: true,
}, () => {});
setTimeout(() => {
});
(doodler as any as { ctx: CanvasRenderingContext2D }).ctx
.imageSmoothingEnabled = false;
}, 0);
// doodler.minScale = 0.1;
// (doodler as any).scale = 3.14;
// (doodler as any).scale = doodler.maxScale;
const colors = [
"red",
@ -40,39 +37,12 @@ const colors = [
"violet",
];
const _fullDebug: Debug = {
track: false,
train: false,
path: false,
car: false,
bogies: false,
angles: false,
aabb: false,
segment: false,
};
const storedDebug = JSON.parse(localStorage.getItem("debug") || "0");
const _debug: Debug = Object.assign({}, _fullDebug, storedDebug);
const debug = new Proxy(_debug, {
get: (_, prop: string) => {
// if (prop !in _debug) {
// (_debug as any)[prop] = false;
// }
return prop in _debug ? (_debug as any)[prop] : false;
},
set: (_, prop: string, value: boolean) => {
(_debug as any)[prop] = value;
localStorage.setItem("debug", JSON.stringify(_debug));
return true;
},
});
setDefaultContext({
inputManager,
doodler,
resources,
debug,
debug: true,
showEnds: true,
colors,
});
@ -113,12 +83,3 @@ setInterval(() => {
const gameLoop = new GameLoop();
gameLoop.start(state);
if (import.meta.env.DEV) {
console.log("Running in development mode");
}
globalThis.TWO_PI = Math.PI * 2;
declare global {
var TWO_PI: number;
}

11
math/lerp.ts Normal file
View File

@ -0,0 +1,11 @@
export const lerp = (a: number, b: number, t: number) => {
return (a * t) + (b * (1 - t));
};
export const map = (
value: number,
x1: number,
y1: number,
x2: number,
y2: number,
) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;

View File

@ -1,10 +1,11 @@
import { Constants } from "../math/constants.ts";
import { map } from "../math/lerp.ts";
import { ComplexPath, PathSegment } from "../math/path.ts";
import { Vector } from "@bearmetal/doodler";
import { Vector } from "doodler";
import { Mover } from "./mover.ts";
export class Follower extends Mover {
export class
Follower extends Mover {
debug = true;
follow(toFollow: ComplexPath | PathSegment) {
@ -12,11 +13,10 @@ export class Follower extends Mover {
const predict = this.velocity.copy();
predict.normalize();
predict.mult(25);
const predictpos = Vector.add(this.position, predict);
const predictpos = Vector.add(this.position, predict)
if (this.ctx) {
Mover.edges(predict, this.ctx.canvas.width, this.ctx.canvas.height);
}
if (this.ctx)
Mover.edges(predict, this.ctx.canvas.width, this.ctx.canvas.height)
let normal = null;
let target = null;
let worldRecord = 1000000;
@ -69,10 +69,10 @@ export class Follower extends Mover {
if (this.debug && this.ctx) {
// Draw predicted future position
this.ctx.strokeStyle = "red";
this.ctx.fillStyle = "pink";
this.ctx.strokeStyle = 'red';
this.ctx.fillStyle = 'pink';
this.ctx.beginPath();
this.ctx.moveTo(this.position.x, this.position.y);
this.ctx.moveTo(this.position.x, this.position.y)
this.ctx.lineTo(predictpos.x, predictpos.y);
this.ctx.stroke();
@ -89,7 +89,7 @@ export class Follower extends Mover {
// Draw actual target (red if steering towards it)
this.ctx.beginPath();
this.ctx.moveTo(predictpos.x, predictpos.y);
this.ctx.moveTo(predictpos.x, predictpos.y)
this.ctx.lineTo(target!.x, target!.y);
this.ctx.stroke();

View File

@ -1,5 +1,4 @@
import { Doodler, Vector } from "@bearmetal/doodler";
import { getContextItem } from "../lib/context.ts";
import { Vector } from "doodler";
export class Mover {
position: Vector;
@ -30,25 +29,25 @@ export class Mover {
boundingBox: {
pos: Vector;
size: Vector;
};
}
constructor();
constructor(random: boolean);
constructor(pos?: Vector, vel?: Vector, acc?: Vector);
constructor(posOrRandom?: Vector | boolean, vel?: Vector, acc?: Vector) {
if (typeof posOrRandom === "boolean" && posOrRandom) {
if (typeof posOrRandom === 'boolean' && posOrRandom) {
this.position = Vector.random2D(new Vector());
this.velocity = Vector.random2D(new Vector());
this.acceleration = new Vector();
this.acceleration = new Vector()
} else {
this.position = posOrRandom || new Vector();
this.velocity = vel || new Vector();
this.acceleration = acc || new Vector();
this.acceleration = acc || new Vector()
}
this.boundingBox = {
size: new Vector(20, 10),
pos: new Vector(this.position.x - 10, this.position.y - 5),
};
pos: new Vector(this.position.x - 10, this.position.y - 5)
}
this.maxSpeed = 3;
this.maxForce = .3;
@ -82,34 +81,20 @@ export class Mover {
}
draw() {
const doodler = getContextItem<Doodler>("doodler");
doodler.drawRotated(this.position, this.velocity.heading() || 0, () => {
doodler.fillCenteredRect(
this.position,
this.boundingBox.size.x,
this.boundingBox.size.y,
{ fillColor: "white" },
);
doodler.fillCenteredRect(this.position, this.boundingBox.size.x, this.boundingBox.size.y, {fillColor: 'white'})
});
if (!this.ctx) return;
this.ctx.fillStyle = "white";
this.ctx.fillStyle = 'white'
this.ctx.save();
this.ctx.translate(this.position.x, this.position.y);
this.ctx.rotate(this.velocity.heading() || 0);
this.ctx.translate(-this.position.x, -this.position.y);
// this.ctx.rotate(Math.PI)
// this.ctx.rotate(.5);
this.ctx.translate(
-(this.boundingBox.size.x / 2),
-(this.boundingBox.size.y / 2),
);
this.ctx.fillRect(
this.position.x,
this.position.y,
this.boundingBox.size.x,
this.boundingBox.size.y,
);
this.ctx.translate(-(this.boundingBox.size.x / 2), -(this.boundingBox.size.y / 2));
this.ctx.fillRect(this.position.x, this.position.y, this.boundingBox.size.x, this.boundingBox.size.y);
this.ctx.restore();
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,39 +0,0 @@
import { getContextItem } from "./lib/context.ts";
import { InputManager } from "./lib/input.ts";
import { StateMachine } from "./state/machine.ts";
import { States } from "./state/states/index.ts";
import { TrackSystem } from "./track/system.ts";
export function bootstrapInputs() {
addEventListener("keydown", (e) => {
e.preventDefault();
});
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.onKey("e", () => {
const state = getContextItem<StateMachine<States>>("state");
state.transitionTo(States.EDIT_TRACK);
});
inputManager.onKey("Delete", () => {
if (inputManager.getKeyState("Control")) {
localStorage.removeItem("track");
}
});
inputManager.onKey("c", () => {
if (inputManager.getKeyState("Control")) {
const currentTrack = localStorage.getItem("track");
navigator.clipboard.writeText(currentTrack ?? "[]");
}
});
addEventListener("paste", async (e) => {
let data = e.clipboardData?.getData("text/plain");
if (!data) return;
try {
// data = data.trim().replace(/^"|"$/g, "").replace(/\\"/g, '"');
console.log(data);
const track = TrackSystem.deserialize(JSON.parse(data));
localStorage.setItem("track", track.serialize());
} catch (e) {
console.error(e);
}
});
}

View File

@ -1,170 +0,0 @@
import { ZoomableDoodler } from "@bearmetal/doodler";
import { TrackSegment } from "../track/system.ts";
import { Train, TrainCar } from "../train/train.ts";
import { InputManager } from "./input.ts";
import { ResourceManager } from "./resources.ts";
interface ContextMap {
inputManager: InputManager;
doodler: ZoomableDoodler;
resources: ResourceManager;
debug: Debug;
colors: [
string,
string,
string,
string,
string,
string,
string,
string,
...string[],
];
[key: string]: unknown;
}
type ContextStore = Record<string, any>;
const defaultContext: ContextStore = {};
const contextStack: ContextStore[] = [defaultContext];
export function setDefaultContext(context: ContextStore) {
Object.assign(defaultContext, context);
}
export function withContext<T>(context: ContextStore, fn: () => T): T {
contextStack.push(context);
try {
return fn();
} finally {
contextStack.pop();
}
}
export const ctx = new Proxy(
{},
{
get(_, prop: string) {
for (let i = contextStack.length - 1; i >= 0; i--) {
if (prop in contextStack[i]) return contextStack[i][prop];
}
if (prop in defaultContext) return defaultContext[prop];
throw new Error(`Context variable '${prop}' is not defined.`);
},
},
) as Record<string, unknown>;
export function getContext() {
return ctx;
}
export function getContextItem<K extends keyof ContextMap>(
prop: K,
): ContextMap[K];
export function getContextItem<T>(prop: string): T;
export function getContextItem<T>(prop: string): T {
return ctx[prop] as T;
}
export function getContextItemOrDefault<T>(prop: string, defaultValue: T): T {
try {
return ctx[prop] as T;
} catch {
return defaultValue;
}
}
export function setContextItem<T>(prop: string, value: T) {
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
[prop]: value,
});
}
function safeStringify(obj: any) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]"; // Replace circular references
}
seen.add(value);
}
if (value instanceof Map) {
const val: Record<string, unknown> = {};
for (const [k, v] of value) {
if (typeof k !== "string") continue;
val[k] = v;
}
seen.add(value);
return val;
}
if (value instanceof Set) {
seen.add(value);
value = Array.from(value);
return value;
}
if (value instanceof TrackSegment) {
seen.add(value);
const val = { ...value };
(val as any).frontNeighbours = value.frontNeighbours.map((n) => n.id);
(val as any).backNeighbours = value.backNeighbours.map((n) => n.id);
return val;
}
// if (value instanceof Train) {
// const val = { ...value };
// // val.segments = value.segments.map((s) => s.id);
// delete (val as any).path;
// delete (val as any).nodes;
// delete (val as any).cars;
// delete (val as any).t;
// // val.t
// // val.
// return null;
// }
// if (value instanceof InputManager) {
// return {};
// }
// if (value instanceof TrainCar) {
// const val = { ...value };
// return val;
// }
return value;
}, 2);
}
const getContextStack = () => JSON.parse(safeStringify(contextStack));
const updateContextValue = (
key: string,
value: unknown,
depth = contextStack.length - 1,
) => {
const keys = key.split(".");
let context = contextStack[depth] ?? defaultContext;
for (let i = 0; i < keys.length - 1; i++) {
if (context instanceof Map) {
context = context.get(keys[i]);
} else {
context = context[keys[i]];
}
}
if (context instanceof Map) {
context.set(keys.at(-1)!, value);
} else {
context[keys.at(-1)!] = value;
}
};
if (location.hostname === "localhost") {
globalThis.getContextStack = getContextStack;
globalThis.updateContextValue = updateContextValue;
globalThis.contextStack = contextStack;
}
declare global {
var getContextStack: () => ContextStore[];
var updateContextValue: (key: string, value: unknown, depth?: number) => void;
var contextStack: ContextStore[];
}

View File

@ -1,35 +0,0 @@
import { getContextItem } from "./context.ts";
export abstract class Drawable {
abstract draw(...args: unknown[]): void;
}
export abstract class Debuggable extends Drawable {
constructor(...debugKeys: [boolean | keyof Debug, ...(keyof Debug)[]]) {
super();
if (import.meta.env.DEV) {
let drawFirst = false;
if (typeof debugKeys[0] === "boolean") {
drawFirst = debugKeys.shift() as boolean;
}
const draw = this.draw.bind(this);
this.draw = drawFirst
? (...args: unknown[]) => {
draw(...args);
const debug = getContextItem<Debug>("debug");
if (debugKeys.some((k) => debug[k as keyof Debug])) {
this.debugDraw();
}
}
: (...args: unknown[]) => {
const debug = getContextItem<Debug>("debug");
if (debugKeys.some((k) => debug[k as keyof Debug])) {
this.debugDraw();
}
draw(...args);
};
}
}
abstract debugDraw(...args: unknown[]): void;
}

View File

@ -1,103 +0,0 @@
// namespace:type/location
type sprite = "sprite";
type img = "img";
type audio = "audio";
type ResourceType = keyof ResourceMap;
type SpriteId = `${string}:${sprite}/${string}`;
interface ResourceMap {
sprite: HTMLImageElement;
img: HTMLImageElement;
audio: HTMLAudioElement;
}
type NamespacedId<T extends keyof ResourceMap> = `${string}:${T}/${string}`;
/**
* Resources are stored in namespaces, and can be accessed by their namespaced id.
* Sprites are located in blob storage as a single png file.
* Audio is located in blob storage as a single mp3 file.
*
* Custom resources can be loaded via the public API, however they will not be loaded on other clients.
* Ideally, engine and car definitions should be stored in the resource manager so that custom cars can be created.
*/
export class ResourceManager {
private resources: Map<string, unknown> = new Map();
private statuses: Map<string, Promise<boolean>> = new Map();
get<K extends ResourceType>(name: NamespacedId<K>): ResourceMap[K];
get<T>(name: string): T;
get<T>(
name: string,
): T {
if (!this.resources.has(name)) {
throw new Error(`Resource ${name} not found`);
}
return this.resources.get(name) as T;
}
set<T extends keyof ResourceMap>(
name: NamespacedId<T>,
value: unknown,
) {
const identifier = parseNamespacedId(name);
if (typeof (value as EventSource).addEventListener === "function") {
if (value instanceof Image || value instanceof Audio) {
// During development, we can use the local file system
value.src =
`/blobs/${identifier.namespace}/${identifier.type}/${identifier.name}${
extensionByType(identifier.type)
}`;
console.log(value.src);
}
this.statuses.set(
name,
new Promise((resolve) => {
const onload = () => {
this.resources.set(name, value);
resolve(true);
(value as EventSource).removeEventListener("loadeddata", onload);
(value as EventSource).removeEventListener("load", onload);
};
(value as EventSource).addEventListener("loadeddata", onload);
(value as EventSource).addEventListener("load", onload);
}),
);
} else {
console.warn("Resource added was not a loadable resource");
}
this.resources.set(name, value);
}
delete(name: string) {
this.resources.delete(name);
}
ready() {
return Promise.all(Array.from(this.statuses.values()));
}
}
function extensionByType(type: ResourceType) {
switch (type) {
case "img":
return ".png";
case "audio":
return ".mp3";
case "sprite":
return ".png";
}
}
type NamespaceIdentifier<T extends ResourceType> = {
namespace: string;
type: T;
name: string;
};
function parseNamespacedId<T extends ResourceType>(
id: NamespacedId<T>,
): NamespaceIdentifier<T> {
const [namespace, location] = id.split(":");
const [type, ...name] = location.split("/");
return { namespace, type: type as T, name: name.join("/") };
}

View File

@ -1,7 +0,0 @@
export function angleToRadians(angle: number) {
return angle / 180 * Math.PI;
}
export function angleToDegrees(angle: number) {
return angle * 180 / Math.PI;
}

View File

@ -1,27 +0,0 @@
export const lerp = (a: number, b: number, t: number) => {
return (a * t) + (b * (1 - t));
};
export const map = (
value: number,
x1: number,
y1: number,
x2: number,
y2: number,
) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
export function lerpAngle(a: number, b: number, t: number) {
let diff = b - a;
// Wrap difference to [-PI, PI]
while (diff < -Math.PI) diff += 2 * Math.PI;
while (diff > Math.PI) diff -= 2 * Math.PI;
return a + diff * t;
}
export function averageAngles(angle1: number, angle2: number) {
// Convert angles to unit vectors
const x = Math.cos(angle1) + Math.cos(angle2);
const y = Math.sin(angle1) + Math.sin(angle2);
// Compute the angle of the resulting vector
return Math.atan2(y, x);
}

View File

@ -1,32 +0,0 @@
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import { averageAngles, lerpAngle } from "../math/lerp.ts";
import { testPerformance } from "./bench.ts";
Deno.test("angle math", () => {
console.log("Average angles");
testPerformance(
() => {
const a = Math.random() * Math.PI * 2;
const b = Math.random() * Math.PI * 2;
const avg = averageAngles(a, b);
},
10000,
60,
);
console.log("Lerp angles");
testPerformance(
() => {
const a = Math.random() * Math.PI * 2;
const b = Math.random() * Math.PI * 2;
const avg = lerpAngle(a, b, .5);
},
10000,
60,
);
});

View File

@ -1,194 +0,0 @@
import { Vector } from "@bearmetal/doodler";
import { TrackSegment } from "./system.ts";
export class StraightTrack extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(25, 0),
start.copy().add(75, 0),
start.copy().add(100, 0),
]);
}
}
export class SBendLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(80, 0),
start.copy().add(120, -25),
start.copy().add(200, -25),
]);
}
}
export class SBendRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(80, 0),
start.copy().add(120, 25),
start.copy().add(200, 25),
]);
}
}
export class BankLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 66;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class WideBankLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 70.4;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class TightBankLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 61.57;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class TightBankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 61.57;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class WideBankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 70.4;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class BankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 66;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}

View File

@ -1,194 +0,0 @@
import { Doodler, Vector } from "@bearmetal/doodler";
import { Train, TrainCar } from "./train.ts";
import { getContextItem } from "../lib/context.ts";
import { ResourceManager } from "../lib/resources.ts";
import { debug } from "node:console";
import { averageAngles, lerpAngle } from "../math/lerp.ts";
export class LargeLady extends TrainCar {
scale = 1;
constructor() {
const resources = getContextItem<ResourceManager>("resources");
const img = resources.get<HTMLImageElement>("snr:sprite/LargeLady")!;
super(50, 10, img, 132, 23, {
at: new Vector(0, 0),
width: 132,
height: 23,
});
this.bogies = [
{
pos: new Vector(0, 0),
angle: 0,
length: 35 * this.scale,
sprite: {
at: new Vector(0, 24),
width: 35,
height: 23,
offset: new Vector(-23, -11.5),
},
},
{
pos: new Vector(0, 0),
angle: 0,
length: 64 * this.scale,
// sprite: {
// at: new Vector(0, 23),
// width: 33,
// height: 19,
// offset: new Vector(-19, -9.5),
// },
sprite: {
at: new Vector(36, 24),
width: 60,
height: 23,
offset: new Vector(-35, -11.5),
},
},
{
pos: new Vector(0, 0),
angle: 0,
length: 35 * this.scale,
sprite: {
at: new Vector(36, 24),
width: 60,
height: 23,
offset: new Vector(-35, -11.5),
},
},
{
pos: new Vector(0, 0),
angle: 0,
length: 28,
sprite: {
at: new Vector(97, 24),
width: 22,
height: 23,
offset: new Vector(-11, -11.5),
},
},
];
this.leading = 23;
}
drawAngle?: number;
override draw(): void {
const doodler = getContextItem<Doodler>("doodler");
for (const b of this.bogies) {
if (!b.sprite) continue;
doodler.drawRotated(b.pos, b.angle + (b.rotate ? 0 : Math.PI), () => {
doodler.drawSprite(
this.img,
b.sprite!.at,
b.sprite!.width,
b.sprite!.height,
b.pos.copy().add(b.sprite!.offset ?? new Vector(0, 0)),
b.sprite!.width,
b.sprite!.height,
);
});
}
const b = this.bogies[2];
const a = this.bogies[1];
// const origin = Vector.add(Vector.sub(a.pos, b.pos).div(2), b.pos);
// const angle = Vector.sub(b.pos, a.pos).heading();
const debug = getContextItem<Debug>("debug");
if (debug.bogies) return;
const difAngle = Vector.sub(a.pos, b.pos).heading();
if (this.drawAngle == undefined) this.drawAngle = b.angle + Math.PI;
const origin = b.pos.copy().add(new Vector(33, 0).rotate(difAngle));
const angle = b.angle;
const avgAngle = averageAngles(difAngle, angle) + Math.PI;
this.drawAngle = lerpAngle(this.drawAngle, avgAngle, .2);
doodler.drawRotated(origin, this.drawAngle, () => {
this.sprite
? doodler.drawSprite(
this.img,
this.sprite.at,
this.sprite.width,
this.sprite.height,
origin.copy().sub(
this.imgWidth * this.scale / 2,
this.imgHeight * this.scale / 2,
),
this.imgWidth * this.scale,
this.imgHeight * this.scale,
)
: doodler.drawImage(
this.img,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
);
});
// doodler.drawCircle(origin, 4, { color: "blue" });
doodler.deferDrawing(() => {
doodler.drawRotated(origin, this.drawAngle! + Math.PI, () => {
doodler.drawSprite(
this.img,
new Vector(133, 0),
28,
23,
origin.copy().sub(93, this.imgHeight / 2),
28,
23,
);
});
});
}
}
export class LargeLadyTender extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
const sprite = resources.get("snr:sprite/LargeLady");
super(40, 39, sprite, 98, 23, {
at: new Vector(0, 48),
width: 98,
height: 23,
});
this.leading = 19;
}
override draw(): void {
const doodler = getContextItem<Doodler>("doodler");
const b = this.bogies[0];
doodler.drawRotated(b.pos, b.angle, () => {
doodler.drawSprite(
this.img,
new Vector(97, 24),
22,
23,
b.pos.copy().sub(11, 11.5),
22,
23,
);
});
const angle = Vector.sub(this.bogies[1].pos, this.bogies[0].pos).heading();
const origin = this.bogies[1].pos.copy().add(
new Vector(-11, 0).rotate(angle),
);
doodler.drawRotated(origin, angle, () => {
this.sprite
? doodler.drawSprite(
this.img,
this.sprite.at,
this.sprite.width,
this.sprite.height,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
this.imgWidth,
this.imgHeight,
)
: doodler.drawImage(
this.img,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
);
});
}
}

View File

@ -1,90 +0,0 @@
import { Vector } from "@bearmetal/doodler";
import { TrainCar } from "./train.ts";
import { ResourceManager } from "../lib/resources.ts";
import { getContextItem } from "../lib/context.ts";
export class Tender extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
25,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
40,
20,
{
at: new Vector(80, 0),
width: 40,
height: 20,
},
);
}
}
export class Tank extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
50,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
70,
20,
{
at: new Vector(80, 20),
width: 70,
height: 20,
},
);
}
}
export class YellowDumpCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
50,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
70,
20,
{
at: new Vector(80, 40),
width: 70,
height: 20,
},
);
}
}
export class GrayDumpCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
50,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
70,
20,
{
at: new Vector(80, 60),
width: 70,
height: 20,
},
);
}
}
export class NullCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
50,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
70,
20,
{
at: new Vector(80, 80),
width: 70,
height: 20,
},
);
}
}

View File

@ -1,90 +0,0 @@
import { Vector } from "@bearmetal/doodler";
import { TrainCar } from "./train.ts";
import { getContextItem } from "../lib/context.ts";
import { ResourceManager } from "../lib/resources.ts";
export class RedEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
55,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
80,
20,
{
at: new Vector(0, 60),
width: 80,
height: 20,
},
);
}
}
export class PurpleEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
55,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
80,
20,
{
at: new Vector(0, 60),
width: 80,
height: 20,
},
);
}
}
export class GreenEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
55,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
80,
20,
{
at: new Vector(0, 40),
width: 80,
height: 20,
},
);
}
}
export class GrayEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
55,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
80,
20,
{
at: new Vector(0, 20),
width: 80,
height: 20,
},
);
}
}
export class BlueEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(
55,
10,
resources.get<HTMLImageElement>("snr:sprite/engine")!,
80,
20,
{
at: new Vector(0, 0),
width: 80,
height: 20,
},
);
}
}

View File

@ -1,439 +0,0 @@
import { getContextItem } from "../lib/context.ts";
import { Doodler, Vector } from "@bearmetal/doodler";
import { Spline, TrackSegment, TrackSystem } from "../track/system.ts";
import { Debuggable } from "../lib/debuggable.ts";
import { lerp, lerpAngle, map } from "../math/lerp.ts";
export class Train extends Debuggable {
nodes: Vector[] = [];
cars: TrainCar[] = [];
path: Spline<TrackSegment>;
t: number;
spacing = 0;
speed = 5;
aabb!: AABB;
get segments() {
return Array.from(
new Set(this.cars.flatMap((c) => c.segments.values().toArray())),
);
}
constructor(track: Spline<TrackSegment>, cars: TrainCar[], t = 0) {
super("train", "path");
this.path = track;
this.path.pointSpacing = 4;
this.cars = cars;
this.t = this.cars.reduce((acc, c) => acc + c.length, 0) +
(this.cars.length - 1) * this.spacing;
this.t = this.t / this.path.pointSpacing;
let currentOffset = 0;
// try {
for (const car of this.cars) {
car.train = this;
currentOffset += car.moveAlongPath(this.t - currentOffset, true) +
this.spacing / this.path.pointSpacing;
}
// } catch {
// currentOffset = 0;
// console.log("Reversed");
// for (const car of this.cars.toReversed()) {
// for (const [i, bogie] of car.bogies.entries().toArray().reverse()) {
// currentOffset += bogie.length;
// const a = this.path.followEvenPoints(this.t - currentOffset);
// car.setBogiePosition(a.p, i);
// this.nodes.push(a.p);
// car.segments.add(a.segmentId);
// }
// }
// }
this.updateAABB();
}
move(dTime: number) {
if (!this.speed) return;
this.t = this.t + (this.speed / this.path.pointSpacing) * dTime * 10;
// % this.path.evenPoints.length; // This should probably be on the track system
let currentOffset = 0;
for (const car of this.cars) {
// This needs to be moved to the car itself
// if (!car.points) return;
// const [a, b] = car.points;
// const nA = this.path.followEvenPoints(this.t - currentOffset);
// a.set(nA.p);
// currentOffset += car.length;
// const nB = this.path.followEvenPoints(this.t - currentOffset);
// b.set(nB.p);
// currentOffset += this.spacing;
// car.segments = [nA.segmentId, nB.segmentId];
// car.draw();
currentOffset += car.moveAlongPath(this.t - currentOffset) +
(this.spacing / this.path.pointSpacing);
}
// this.draw();
this.updateAABB();
}
updateAABB() {
const minX = Math.min(...this.cars.map((c) => c.aabb.x));
const maxX = Math.max(...this.cars.map((c) => c.aabb.x + c.aabb.width));
const minY = Math.min(...this.cars.map((c) => c.aabb.y));
const maxY = Math.max(...this.cars.map((c) => c.aabb.y + c.aabb.height));
this.aabb = {
pos: new Vector(minX, minY),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
center: new Vector(minX, minY).add(
new Vector(maxX - minX, maxY - minY).div(2),
),
};
}
// draw() {
// const doodler = getContextItem<Doodler>("doodler");
// this.path.draw();
// for (const [i, node] of this.nodes.entries()) {
// // doodler.drawCircle(node, 10, { color: "purple", weight: 3 });
// doodler.fillCircle(node, 2, { color: "purple" });
// // const next = this.nodes[i + 1];
// // if (next) {
// // const to = Vector.sub(node.point, next.point);
// // to.setMag(40);
// // doodler.line(next.point, Vector.add(to, next.point))
// // }
// }
// }
draw() {
for (const car of this.cars) {
car.draw();
}
}
override debugDraw(): void {
const debug = getContextItem<Debug>("debug");
const doodler = getContextItem<Doodler>("doodler");
if (debug.path) {
// doodler.drawLine(this.path.points, {
// color: "red",
// weight: 3,
// });
const colors = getContextItem<string[]>("colors");
for (const [i, p] of this.path.evenPoints.entries()) {
const color = colors[
Math.floor(
map(i, 0, this.path.evenPoints.length, 0, colors.length),
)
];
doodler.drawCircle(p.p, 2, { color, weight: .5 });
}
}
if (debug.train) {
const track = getContextItem<TrackSystem>("track");
const colors = getContextItem<string[]>("colors").slice();
colors.push(colors.shift()!);
colors.push(colors.shift()!);
colors.push(colors.shift()!);
for (const [i, segmentId] of this.segments.entries().toArray()) {
const segment = track.getSegment(segmentId);
segment &&
doodler.drawBezier(...segment.points, {
color: colors[i % colors.length],
weight: 3,
});
}
}
if (debug.aabb) {
doodler.deferDrawing(() => {
doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
color: "orange",
});
doodler.drawCircle(this.aabb.center, 2, {
color: "lime",
});
});
}
}
real2Track(length: number) {
return length / this.path.pointSpacing;
}
}
interface Bogie {
pos: Vector;
angle: number;
length: number;
sprite?: ISprite & { offset?: Vector };
rotate?: boolean;
}
export class TrainCar extends Debuggable {
img: HTMLImageElement;
imgWidth: number;
imgHeight: number;
sprite?: ISprite;
points?: [Vector, Vector, ...Vector[]];
_length: number;
leading: number = 0;
bogies: Bogie[] = [];
segments: Set<string> = new Set();
train?: Train;
aabb!: AABB;
constructor(
length: number,
trailing: number,
img: HTMLImageElement,
w: number,
h: number,
sprite?: ISprite,
) {
super(true, "car", "bogies", "angles");
this.img = img;
this.sprite = sprite;
this.imgWidth = w;
this.imgHeight = h;
this._length = length;
this.bogies = [
{
pos: new Vector(0, 0),
angle: 0,
length: length,
},
{
pos: new Vector(0, 0),
angle: 0,
length: trailing,
},
];
this.updateAABB();
}
get length() {
return this.bogies.reduce((acc, b) => acc + b.length, 0) + this.leading;
}
setBogiePosition(pos: Vector, idx: number) {
this.bogies[idx].pos.set(pos);
}
update(dTime: number, t: number) {
if (this.train) {
for (const [i, bogie] of this.bogies.entries()) {
const a = this.train.path.followEvenPoints(t - this._length * i);
}
}
}
updateAABB() {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
this.bogies.forEach((bogie, index) => {
// Unit vector in the direction the bogie is facing.
const u = new Vector(Math.cos(bogie.angle), Math.sin(bogie.angle));
// Perpendicular vector (to thicken the rectangle).
const v = new Vector(-Math.sin(bogie.angle), Math.cos(bogie.angle));
// For the first bogie, extend in the opposite direction by this.leading.
let front = bogie.pos.copy();
if (index === 0) {
front = front.sub(u.copy().rotate(Math.PI).mult(this.leading));
}
// Rear point is at bogie.pos plus the bogie length.
const rear = bogie.pos.copy().add(
u.copy().rotate(Math.PI).mult(bogie.length),
);
// Calculate half the height to offset from the center line.
const halfHeight = this.imgHeight / 2;
// Calculate the four corners of the rectangle.
const corners = [
front.copy().add(v.copy().mult(halfHeight)),
front.copy().add(v.copy().mult(-halfHeight)),
rear.copy().add(v.copy().mult(halfHeight)),
rear.copy().add(v.copy().mult(-halfHeight)),
];
// Update the overall AABB limits.
corners.forEach((corner) => {
minX = Math.min(minX, corner.x);
minY = Math.min(minY, corner.y);
maxX = Math.max(maxX, corner.x);
maxY = Math.max(maxY, corner.y);
});
});
this.aabb = {
pos: new Vector(minX, minY),
center: new Vector(minX, minY).add(
new Vector(maxX - minX, maxY - minY).div(2),
),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
moveAlongPath(t: number, initial = false): number {
if (!this.train) return 0;
let offset = this.leading / this.train.path.pointSpacing;
this.segments.clear();
for (const [i, bogie] of this.bogies.entries()) {
const a = this.train.path.followEvenPoints(t - offset);
a.tangent.rotate(TWO_PI);
offset += bogie.length / this.train.path.pointSpacing;
this.setBogiePosition(a.p, i);
if (initial) bogie.angle = a.tangent.heading();
else {
bogie.angle = lerpAngle(a.tangent.heading(), bogie.angle, .1);
}
this.segments.add(a.segmentId);
}
this.updateAABB();
return offset;
}
draw() {
const doodler = getContextItem<Doodler>("doodler");
const [a, b] = this.bogies;
const origin = Vector.add(Vector.sub(a.pos, b.pos).div(2), b.pos);
const angle = Vector.sub(b.pos, a.pos).heading();
doodler.drawCircle(origin, 4, { color: "blue" });
doodler.drawRotated(origin, angle, () => {
this.sprite
? doodler.drawSprite(
this.img,
this.sprite.at,
this.sprite.width,
this.sprite.height,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
this.imgWidth,
this.imgHeight,
)
: doodler.drawImage(
this.img,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
);
});
}
override debugDraw(...args: unknown[]): void {
const doodler = getContextItem<Doodler>("doodler");
const debug = getContextItem<Debug>("debug");
if (debug.bogies) {
doodler.deferDrawing(() => {
for (const [i, b] of this.bogies.entries()) {
const next = this.bogies[i + 1];
if (!next) continue;
const dist = Vector.dist(b.pos, next.pos);
doodler.drawCircle(b.pos, 5, { color: "red" });
doodler.fillText(
dist.toFixed(1).toString(),
b.pos.copy().add(10, 10),
100,
{
color: "white",
},
);
}
});
}
if (debug.car) {
doodler.deferDrawing(() => {
doodler.drawLine(this.bogies.map((b) => b.pos), {
color: "blue",
weight: 2,
});
doodler.deferDrawing(() => {
const colors = getContextItem<string[]>("colors");
for (const [i, b] of this.bogies.entries()) {
doodler.drawCircle(b.pos, 5, { color: colors[i % colors.length] });
doodler.fillText(
b.length.toString(),
b.pos.copy().add(10, 0),
100,
{
color: "white",
},
);
}
});
});
if (debug.aabb) {
doodler.deferDrawing(() => {
doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
color: "white",
weight: .5,
});
doodler.drawCircle(this.aabb.center, 2, {
color: "yellow",
});
doodler.fillText(
this.aabb.width.toFixed(1).toString(),
this.aabb.center.copy().add(10, 10),
100,
{
color: "white",
},
);
});
}
}
if (debug.angles) {
doodler.deferDrawing(() => {
const ps: { pos: Vector; angle: number }[] = [];
for (const [i, b] of this.bogies.entries()) {
ps.push({ pos: b.pos, angle: b.angle });
const next = this.bogies[i + 1];
if (!next) continue;
const heading = Vector.sub(next.pos, b.pos);
const p = b.pos.copy().add(heading.mult(.5));
ps.push({ pos: p, angle: heading.heading() });
}
for (const p of ps) {
doodler.dot(p.pos, { color: "green" });
doodler.fillText(
p.angle.toFixed(2).toString(),
p.pos.copy().add(0, 20),
100,
{
color: "white",
},
);
}
});
}
}
}
interface ISprite {
at: Vector;
width: number;
height: number;
}

View File

@ -1,50 +0,0 @@
import { Vector } from "@bearmetal/doodler";
import { TrackSegment } from "./track/system.ts";
declare global {
type End = {
pos: Vector;
segment: TrackSegment;
tangent: Vector;
frontOrBack: "front" | "back";
};
type SerializedTrackSegment = {
p: [number, number, number][];
id: string;
bNeighbors: string[];
fNeighbors: string[];
};
type Debug = {
track: boolean;
segment: boolean;
train: boolean;
car: boolean;
path: boolean;
bogies: boolean;
angles: boolean;
aabb: boolean;
};
type AABB = {
pos: Vector;
x: number;
y: number;
width: number;
height: number;
center: Vector;
};
}
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ?? {},
);
});
});
}

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,6 +1,6 @@
import { getContext } from "../lib/context.ts";
import { TrackSystem } from "../track/system.ts";
import { Train } from "../train/train.ts";
import { Train } from "../train.old.ts";
export class StateMachine<T> {
private _states: Map<T, State<T>> = new Map();

View File

@ -14,10 +14,6 @@ import {
SBendLeft,
SBendRight,
StraightTrack,
TightBankLeft,
TightBankRight,
WideBankLeft,
WideBankRight,
} from "../../track/shapes.ts";
import { TrackSegment } from "../../track/system.ts";
import { clamp } from "../../math/clamp.ts";
@ -44,6 +40,18 @@ export class EditTrackState extends State<States> {
const track = getContextItem<TrackSystem>("track");
const doodler = getContextItem<Doodler>("doodler");
// For moving a segment, i.e. the currently active one
// const segment = track.lastSegment;
// if (segment) {
// const firstPoint = segment.points[0].copy();
// const { x, y } = inputManager.getMouseLocation();
// segment.points.forEach((p, i) => {
// const relativePoint = Vector.sub(p, firstPoint);
// p.set(x, y);
// p.add(relativePoint);
// });
// }
if (this.selectedSegment) {
const segment = this.selectedSegment;
const firstPoint = segment.points[0].copy();
@ -53,7 +61,6 @@ export class EditTrackState extends State<States> {
p.set(mousePos);
p.add(relativePoint);
});
segment.update();
const ends = track.findEnds();
setContextItem("showEnds", true);
@ -102,21 +109,18 @@ export class EditTrackState extends State<States> {
this.ghostRotated = false;
}
switch (this.closestEnd.frontOrBack) {
case "front": {
case "front":
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
this.ghostSegment.points[0],
);
// this.ghostSegment.points[0] = this.closestEnd.pos;
const ghostEndTangent = this.ghostSegment.tangent(0);
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
this.closestEnd.tangent.heading() - ghostEndTangent.heading(),
this.closestEnd.tangent.heading(),
this.ghostSegment.points[0],
);
this.ghostRotated = true;
break;
}
case "back": {
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
@ -132,8 +136,6 @@ export class EditTrackState extends State<States> {
break;
}
}
this.ghostSegment.update();
// } else if (closestEnd) {
// this.closestEnd = closestEnd;
} else if (!this.closestEnd || !closestEnd) {
@ -247,7 +249,6 @@ export class EditTrackState extends State<States> {
if (translation.x !== 0 || translation.y !== 0) {
track.translate(translation);
track.recalculateAll();
}
// TODO
@ -285,10 +286,6 @@ export class EditTrackState extends State<States> {
new SBendRight(),
new BankLeft(),
new BankRight(),
new WideBankLeft(),
new WideBankRight(),
new TightBankLeft(),
new TightBankRight(),
]);
const inputManager = getContextItem<InputManager>("inputManager");
@ -309,6 +306,14 @@ export class EditTrackState extends State<States> {
state.transitionTo(States.RUNNING);
});
inputManager.onKey(" ", () => {
if (this.selectedSegment) {
this.selectedSegment = undefined;
} else {
this.selectedSegment = new StraightTrack();
}
});
inputManager.onMouse("left", () => {
const track = getContextItem<TrackSystem>("track");
if (this.ghostSegment && this.closestEnd) {
@ -378,18 +383,6 @@ export class EditTrackState extends State<States> {
}
});
inputManager.onKey("r", () => {
if (!this.selectedSegment) return;
const segment = this.selectedSegment;
let angle = Math.PI / 12;
segment.rotate(angle);
});
inputManager.onKey("R", () => {
if (!this.selectedSegment) return;
const segment = this.selectedSegment;
let angle = -Math.PI / 12;
segment.rotate(angle);
});
// TODO
// Cache trains and save
@ -411,9 +404,6 @@ export class EditTrackState extends State<States> {
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.offKey("e");
inputManager.offKey("w");
inputManager.offKey("z");
inputManager.offKey("r");
inputManager.offKey("R");
inputManager.offKey("Escape");
inputManager.offMouse("left");
if (this.heldEvents.size > 0) {

View File

@ -34,24 +34,20 @@ export class LoadState extends State<States> {
bootstrapInputs();
// This should be driven by a manifest
resources.set("snr:sprite/engine", new Image());
resources.set("snr:sprite/LargeLady", new Image());
// resources.get<HTMLImageElement>("snr:sprite/engine")!.src =
// "/sprites/EngineSprites.png";
resources.set("snr:audio/ding", new Audio());
resources.set("engine-sprites", new Image());
resources.get<HTMLImageElement>("engine-sprites")!.src =
"/sprites/EngineSprites.png";
resources.ready().then(() => {
this.stateMachine.transitionTo(States.RUNNING);
}).catch((e) => console.error(e));
});
const doodler = getContextItem<Doodler>("doodler");
// this.layers.push(doodler.createLayer((_, __, dTime) => {
// doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
// doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
// color: "#302040",
// });
// }));
this.layers.push(doodler.createLayer((_, __, dTime) => {
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
color: "#302040",
});
}));
}
override stop(): void {
// noop
@ -61,7 +57,7 @@ export class LoadState extends State<States> {
const track = TrackSystem.deserialize(
JSON.parse(localStorage.getItem("track") || "[]"),
);
return track ?? new TrackSystem([new StraightTrack()]);
return track;
}
private loadTrains() {

View File

@ -1,4 +1,4 @@
import { Doodler, Point, Vector, ZoomableDoodler } from "@bearmetal/doodler";
import { Doodler } from "@bearmetal/doodler";
import { getContext, getContextItem } from "../../lib/context.ts";
import { InputManager } from "../../lib/input.ts";
import { TrackSystem } from "../../track/system.ts";
@ -8,7 +8,6 @@ import { DotFollower } from "../../train/newTrain.ts";
import { Train } from "../../train/train.ts";
import { State } from "../machine.ts";
import { States } from "./index.ts";
import { LargeLady, LargeLadyTender } from "../../train/LargeLady.ts";
export class RunningState extends State<States> {
override name: States = States.RUNNING;
@ -19,32 +18,20 @@ export class RunningState extends State<States> {
layers: number[] = [];
activeTrain: Train | undefined;
override update(dt: number): void {
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
const doodler = getContextItem<ZoomableDoodler>(
"doodler",
);
if (this.activeTrain) {
// (doodler as any).origin = doodler.worldToScreen(
// doodler.width - this.activeTrain.aabb.center.x,
// doodler.height - this.activeTrain.aabb.center.y,
// );
doodler.centerCameraOn(this.activeTrain.aabb.center);
}
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
// TODO
// Update trains
// Update world
// Handle input
// Draw (maybe via a layer system that syncs with doodler)
// Monitor world events
for (const train of ctx.trains) {
train.move(dt);
}
}
override start(): void {
console.log("Starting running state");
const doodler = getContextItem<Doodler>("doodler");
this.layers.push(
doodler.createLayer(() => {
@ -71,24 +58,12 @@ export class RunningState extends State<States> {
// const path = track.path;
// const follower = new DotFollower(path, path.points[0].copy());
// ctx.trains.push(follower);
const train = new Train(track.path, [
new LargeLady(),
new LargeLadyTender(),
]);
const train = new Train(track.path, [new RedEngine(), new Tender()]);
ctx.trains.push(train);
});
// const train = new Train(track.path, [
// new LargeLady(),
// new LargeLadyTender(),
// ]);
// ctx.trains.push(train);
// this.activeTr0ain = train;
// const trainCount = 1000;
// const trainCount = 2000;
// for (let i = 0; i < trainCount; i++) {
// const train = new Train(track.path, [
// new LargeLady(),
// new LargeLadyTender(),
// ]);
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
// ctx.trains.push(train);
// }
@ -97,9 +72,6 @@ export class RunningState extends State<States> {
for (const train of trains) {
train.speed += 1;
}
// for (const [i, train] of trains.entries()) {
// train.speed += .01 * i;
// }
});
inputManager.onKey("ArrowDown", () => {
const trains = getContextItem<Train[]>("trains");

View File

@ -1,4 +1,5 @@
import { StateMachine } from "../machine.ts";
import { Track } from "../../track.ts";
import { EditTrainState } from "./EditTrainState.ts";
import { EditTrackState } from "./EditTrackState.ts";
import { PausedState } from "./PausedState.ts";

90
track/shapes.ts Normal file
View File

@ -0,0 +1,90 @@
import { Vector } from "@bearmetal/doodler";
import { TrackSegment } from "./system.ts";
export class StraightTrack extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(25, 0),
start.copy().add(75, 0),
start.copy().add(100, 0),
]);
}
}
export class SBendLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(60, 0),
start.copy().add(90, -25),
start.copy().add(150, -25),
]);
}
}
export class SBendRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(60, 0),
start.copy().add(90, 25),
start.copy().add(150, 25),
]);
}
}
export class BankLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 33;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class BankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 33;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}

View File

@ -2,66 +2,47 @@ import { Doodler, Point, Vector } from "@bearmetal/doodler";
import { ComplexPath, PathSegment } from "../math/path.ts";
import { getContextItem, setDefaultContext } from "../lib/context.ts";
import { clamp } from "../math/clamp.ts";
import { Debuggable } from "../lib/debuggable.ts";
export class TrackSystem extends Debuggable {
private _segments: Map<string, TrackSegment> = new Map();
export class TrackSystem {
private segments: Map<string, TrackSegment> = new Map();
private doodler: Doodler;
constructor(segments: TrackSegment[]) {
super("track");
this.doodler = getContextItem<Doodler>("doodler");
for (const segment of segments) {
this._segments.set(segment.id, segment);
this.segments.set(segment.id, segment);
}
}
getSegment(id: string) {
return this._segments.get(id);
}
get firstSegment() {
return this._segments.values().next().value;
return this.segments.values().next().value;
}
get lastSegment() {
return this._segments.values().toArray().pop();
}
getNearestSegment(pos: Vector) {
let minDistance = Infinity;
let nearestSegment: TrackSegment | undefined;
for (const segment of this._segments.values()) {
const distance = segment.getDistanceTo(pos);
if (distance < minDistance) {
minDistance = distance;
nearestSegment = segment;
}
}
return nearestSegment;
return this.segments.values().toArray().pop();
}
optimize(percent: number) {
console.log("Optimizing track", percent * 100 / 4);
for (const segment of this._segments.values()) {
for (const segment of this.segments.values()) {
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
}
}
recalculateAll() {
for (const segment of this._segments.values()) {
segment.update();
for (const segment of this.segments.values()) {
segment.recalculateRailPoints();
segment.length = segment.calculateApproxLength();
}
}
registerSegment(segment: TrackSegment) {
segment.setTrack(this);
this._segments.set(segment.id, segment);
this.segments.set(segment.id, segment);
}
unregisterSegment(segment: TrackSegment) {
this._segments.delete(segment.id);
for (const s of this._segments.values()) {
this.segments.delete(segment.id);
for (const s of this.segments.values()) {
s.backNeighbours = s.backNeighbours.filter((n) => n !== segment);
s.frontNeighbours = s.frontNeighbours.filter((n) => n !== segment);
}
@ -71,7 +52,7 @@ export class TrackSystem extends Debuggable {
}
draw(showControls = false) {
for (const [i, segment] of this._segments.entries()) {
for (const [i, segment] of this.segments.entries()) {
segment.draw(showControls);
}
@ -100,20 +81,11 @@ export class TrackSystem extends Debuggable {
// }
}
override debugDraw(): void {
const debug = getContextItem("debug");
if (debug.track) {
for (const segment of this._segments.values()) {
segment.drawAABB();
}
}
}
ends: Map<TrackSegment, [End, End]> = new Map();
endArray: End[] = [];
findEnds() {
for (const segment of this._segments.values()) {
for (const segment of this.segments.values()) {
if (this.ends.has(segment)) continue;
const ends: [End, End] = [
{
@ -137,37 +109,37 @@ export class TrackSystem extends Debuggable {
serialize() {
return JSON.stringify(
this._segments.values().map((s) => s.serialize()).toArray(),
this.segments.values().map((s) => s.serialize()).toArray(),
);
}
copy() {
const track = new TrackSystem([]);
for (const segment of this._segments.values()) {
track._segments.set(segment.id, segment.copy());
for (const segment of this.segments.values()) {
track.segments.set(segment.id, segment.copy());
}
return track;
}
static deserialize(data: SerializedTrackSegment[]) {
if (data.length === 0) return new TrackSystem([]);
if (data.length === 0) return undefined;
const track = new TrackSystem([]);
const neighborMap = new Map<string, [string[], string[]]>();
for (const segment of data) {
track._segments.set(segment.id, TrackSegment.deserialize(segment));
track.segments.set(segment.id, TrackSegment.deserialize(segment));
neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]);
}
for (const segment of track._segments.values()) {
for (const segment of track.segments.values()) {
segment.setTrack(track);
const neighbors = neighborMap.get(segment.id);
if (neighbors) {
segment.backNeighbours = neighbors[1].map((id) =>
track._segments.get(id)
track.segments.get(id)
).filter((s) => s) as TrackSegment[];
segment.frontNeighbours = neighbors[0].map((id) =>
track._segments.get(id)
track.segments.get(id)
).filter((s) => s) as TrackSegment[];
}
}
@ -176,7 +148,7 @@ export class TrackSystem extends Debuggable {
}
translate(v: Vector) {
for (const segment of this._segments.values()) {
for (const segment of this.segments.values()) {
segment.translate(v);
}
}
@ -192,7 +164,7 @@ export class TrackSystem extends Debuggable {
generatePath() {
if (!this.firstSegment) throw new Error("No first segment");
const flags = { looping: false };
const flags = { looping: true };
const rightOnlyPath = [
this.firstSegment.copy(),
...this.findRightPath(
@ -304,82 +276,26 @@ export class TrackSegment extends PathSegment {
doodler: Doodler;
normalPoints: Vector[] = [];
antiNormalPoints: Vector[] = [];
evenPoints: [Vector, number][] = [];
aabb!: AABB;
private trackGuage = 12;
constructor(p: VectorSet, id?: string) {
super(p);
this.doodler = getContextItem<Doodler>("doodler");
this.id = id ?? crypto.randomUUID();
this.update();
this.recalculateRailPoints();
}
getDistanceTo(pos: Vector) {
return Vector.dist(this.aabb.center, pos);
}
updateAABB() {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
[...this.normalPoints, ...this.antiNormalPoints].forEach((p) => {
minX = Math.min(minX, p.x);
maxX = Math.max(maxX, p.x);
minY = Math.min(minY, p.y);
maxY = Math.max(maxY, p.y);
});
const width = maxX - minX;
const height = maxY - minY;
if (width < this.trackGuage) {
const extra = (this.trackGuage - width) / 2;
minX -= extra;
maxX += extra;
}
if (height < this.trackGuage) {
const extra = (this.trackGuage - height) / 2;
minY -= extra;
maxY += extra;
}
this.aabb = {
pos: new Vector(minX, minY),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
center: new Vector(minX, minY).add(
new Vector(maxX - minX, maxY - minY).div(2),
),
};
}
recalculateRailPoints(resolution = 60) {
recalculateRailPoints(resolution = 100) {
this.normalPoints = [];
this.antiNormalPoints = [];
for (let i = 0; i <= resolution; i++) {
const t = i / resolution;
const normal = this.tangent(t).rotate(Math.PI / 2);
normal.setMag(this.trackGuage / 2);
normal.setMag(6);
const p = this.getPointAtT(t);
this.normalPoints.push(p.copy().add(normal));
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
}
recalculateTiePoints() {
const spacing = Math.ceil(this.length / 10);
this.evenPoints = this.calculateEvenlySpacedPoints(this.length / spacing);
}
update() {
this.recalculateRailPoints();
this.recalculateTiePoints();
this.updateAABB();
}
setTrack(t: TrackSystem) {
this.track = t;
@ -414,12 +330,12 @@ export class TrackSegment extends PathSegment {
});
}
// const spacing = Math.ceil(this.length / 10);
// const points = this.calculateEvenlySpacedPoints(this.length / spacing);
for (let i = 0; i < this.evenPoints.length - 1; i++) {
const spacing = Math.ceil(this.length / 10);
const points = this.calculateEvenlySpacedPoints(this.length / spacing);
for (let i = 0; i < points.length - 1; i++) {
// const t = i / ties;
// const p = this.getPointAtT(t);
const [p, t] = this.evenPoints[i];
const [p, t] = points[i];
// this.doodler.drawCircle(p, 2, {
// color: "red",
// weight: 3,
@ -465,15 +381,6 @@ export class TrackSegment extends PathSegment {
// });
}
drawAABB() {
this.doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
color: "lime",
});
this.doodler.drawCircle(this.aabb.center, 2, {
color: "cyan",
});
}
serialize(): SerializedTrackSegment {
return {
p: this.points.map((p) => p.array()),
@ -516,7 +423,6 @@ export class TrackSegment extends PathSegment {
rotate(angle: number | Vector) {
const [p1, p2, p3, p4] = this.points;
let newP2;
if (angle instanceof Vector) {
const tan = angle;
angle = tan.heading() - (this.lastHeading ?? 0);
@ -553,7 +459,7 @@ export class TrackSegment extends PathSegment {
}
rotateAboutPoint(angle: number, point: Vector) {
// if (!this.points.includes(point)) return;
if (!this.points.includes(point)) return;
point = point.copy();
this.points.forEach((p, i) => {
const relativePoint = Vector.sub(p, point);
@ -574,25 +480,12 @@ export class TrackSegment extends PathSegment {
}
}
interface PathPoint {
p: Vector;
segmentId: string;
tangent: Vector;
}
export class Spline<T extends PathSegment = PathSegment> {
segments: T[] = [];
ctx?: CanvasRenderingContext2D;
evenPoints: PathPoint[];
_pointSpacing: number;
get pointSpacing() {
return this._pointSpacing;
}
set pointSpacing(value: number) {
this._pointSpacing = value;
this.evenPoints = this.calculateEvenlySpacedPoints(value);
}
evenPoints: Vector[];
pointSpacing: number;
get points() {
return Array.from(new Set(this.segments.flatMap((s) => s.points)));
@ -607,8 +500,8 @@ export class Spline<T extends PathSegment = PathSegment> {
if (this.segments.at(-1)?.next === this.segments[0]) {
this.looped = true;
}
this._pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(this._pointSpacing);
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(1);
this.nodes = [];
// for (let i = 0; i < this.points.length; i += 3) {
// const node: IControlNode = {
@ -641,17 +534,13 @@ export class Spline<T extends PathSegment = PathSegment> {
}
}
calculateEvenlySpacedPoints(spacing: number, resolution = 1): PathPoint[] {
// this._pointSpacing = 1;
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
this.pointSpacing = 1;
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
const points: PathPoint[] = [];
const points: Vector[] = [];
points.push({
p: this.segments[0].points[0],
segmentId: this.segments[0].id,
tangent: this.segments[0].tangent(0),
});
let prev = points[0].p;
points.push(this.segments[0].points[0]);
let prev = points[0];
let distSinceLastEvenPoint = 0;
for (const seg of this.segments) {
let t = 0;
@ -669,35 +558,26 @@ export class Spline<T extends PathSegment = PathSegment> {
Vector.sub(point, prev).normalize().mult(overshoot),
);
distSinceLastEvenPoint = overshoot;
points.push({
p: evenPoint,
segmentId: seg.id,
tangent: seg.tangent(t),
});
points.push(evenPoint);
prev = evenPoint;
continue;
}
prev = point;
}
}
// this.evenPoints = points;
this.evenPoints = points;
return points;
}
followEvenPoints(t: number): PathPoint {
followEvenPoints(t: number) {
if (this.looped) {
if (t < 0) t += this.evenPoints.length;
const i = Math.floor(t) % this.evenPoints.length;
const a = this.evenPoints[i];
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
return {
p: Vector.lerp(a.p, b.p, t % 1),
segmentId: b.segmentId,
tangent: b.tangent,
};
return Vector.lerp(a, b, t % 1);
}
t = clamp(t, 0, this.evenPoints.length - 1);
const i = clamp(Math.floor(t), 0, this.evenPoints.length - 1);
@ -706,11 +586,7 @@ export class Spline<T extends PathSegment = PathSegment> {
.evenPoints[
clamp((i + 1) % this.evenPoints.length, 0, this.evenPoints.length - 1)
];
return {
p: Vector.lerp(a.p, b.p, t % 1),
segmentId: b.segmentId,
tangent: b.tangent,
};
return Vector.lerp(a, b, t % 1);
}
calculateApproxLength() {

55
train/cars.ts Normal file
View File

@ -0,0 +1,55 @@
import { Vector } from "https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts";
import { TrainCar } from "./train.ts";
import { ResourceManager } from "../lib/resources.ts";
import { getContextItem } from "../lib/context.ts";
export class Tender extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(25, resources.get<HTMLImageElement>("engine-sprites")!, 40, 20, {
at: new Vector(80, 0),
width: 40,
height: 20,
});
}
}
export class Tank extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
at: new Vector(80, 20),
width: 70,
height: 20,
});
}
}
export class YellowDumpCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
at: new Vector(80, 40),
width: 70,
height: 20,
});
}
}
export class GrayDumpCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
at: new Vector(80, 60),
width: 70,
height: 20,
});
}
}
export class NullCar extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(50, resources.get<HTMLImageElement>("engine-sprites")!, 70, 20, {
at: new Vector(80, 80),
width: 70,
height: 20,
});
}
}

55
train/engines.ts Normal file
View File

@ -0,0 +1,55 @@
import { Vector } from "@bearmetal/doodler";
import { TrainCar } from "./train.ts";
import { getContextItem } from "../lib/context.ts";
import { ResourceManager } from "../lib/resources.ts";
export class RedEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
at: new Vector(0, 60),
width: 80,
height: 20,
});
}
}
export class PurpleEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
at: new Vector(0, 60),
width: 80,
height: 20,
});
}
}
export class GreenEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
at: new Vector(0, 40),
width: 80,
height: 20,
});
}
}
export class GrayEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
at: new Vector(0, 20),
width: 80,
height: 20,
});
}
}
export class BlueEngine extends TrainCar {
constructor() {
const resources = getContextItem<ResourceManager>("resources");
super(55, resources.get<HTMLImageElement>("engine-sprites")!, 80, 20, {
at: new Vector(0, 0),
width: 80,
height: 20,
});
}
}

164
train/train.ts Normal file
View File

@ -0,0 +1,164 @@
import { ComplexPath, PathSegment } from "../math/path.ts";
import { Follower } from "../physics/follower.ts";
import { Mover } from "../physics/mover.ts";
import { getContextItem } from "../lib/context.ts";
import { Doodler, Vector } from "@bearmetal/doodler";
import { Spline, TrackSegment } from "../track/system.ts";
import { ResourceManager } from "../lib/resources.ts";
export class Train {
nodes: Vector[] = [];
cars: TrainCar[] = [];
path: Spline<TrackSegment>;
t: number;
engineLength = 40;
spacing = 30;
speed = 10;
constructor(track: Spline<TrackSegment>, cars: TrainCar[]) {
this.path = track;
this.t = 0;
const resources = getContextItem<ResourceManager>("resources");
this.cars = cars;
// this.cars.push(
// new TrainCar(
// 55,
// engineSprites,
// 80,
// 20,
// { at: new Vector(0, 60), width: 80, height: 20 },
// ),
// new TrainCar(
// 25,
// engineSprites,
// 40,
// 20,
// { at: new Vector(80, 0), width: 40, height: 20 },
// ),
// );
let currentOffset = 0;
try {
for (const car of this.cars) {
currentOffset += this.spacing;
const a = this.path.followEvenPoints(this.t - currentOffset);
currentOffset += car.length;
const b = this.path.followEvenPoints(this.t - currentOffset);
car.points = [a, b];
this.nodes.push(a, b);
}
} catch {
currentOffset = 0;
for (const car of this.cars.toReversed()) {
currentOffset += this.spacing;
const a = this.path.followEvenPoints(this.t - currentOffset);
currentOffset += car.length;
const b = this.path.followEvenPoints(this.t - currentOffset);
car.points = [a, b];
this.nodes.push(a, b);
}
}
}
move(dTime: number) {
this.t = this.t + this.speed * dTime * 10;
// % this.path.evenPoints.length; // This should probably be on the track system
// console.log(this.t);
let currentOffset = 0;
for (const car of this.cars) {
if (!car.points) return;
const [a, b] = car.points;
a.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += car.length;
b.set(this.path.followEvenPoints(this.t - currentOffset));
currentOffset += this.spacing;
// car.draw();
}
// this.draw();
}
// draw() {
// const doodler = getContextItem<Doodler>("doodler");
// this.path.draw();
// for (const [i, node] of this.nodes.entries()) {
// // doodler.drawCircle(node, 10, { color: "purple", weight: 3 });
// doodler.fillCircle(node, 2, { color: "purple" });
// // const next = this.nodes[i + 1];
// // if (next) {
// // const to = Vector.sub(node.point, next.point);
// // to.setMag(40);
// // doodler.line(next.point, Vector.add(to, next.point))
// // }
// }
// }
draw() {
for (const car of this.cars) {
car.draw();
}
}
real2Track(length: number) {
return length / this.path.pointSpacing;
}
}
export class TrainCar {
img: HTMLImageElement;
imgWidth: number;
imgHeight: number;
sprite?: ISprite;
points?: [Vector, Vector];
length: number;
constructor(
length: number,
img: HTMLImageElement,
w: number,
h: number,
sprite?: ISprite,
) {
this.img = img;
this.sprite = sprite;
this.imgWidth = w;
this.imgHeight = h;
this.length = length;
}
draw() {
if (!this.points) return;
const doodler = getContextItem<Doodler>("doodler");
const [a, b] = this.points;
const origin = Vector.add(Vector.sub(a, b).div(2), b);
const angle = Vector.sub(b, a).heading();
doodler.drawCircle(origin, 4, { color: "blue" });
doodler.drawRotated(origin, angle, () => {
this.sprite
? doodler.drawSprite(
this.img,
this.sprite.at,
this.sprite.width,
this.sprite.height,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
this.imgWidth,
this.imgHeight,
)
: doodler.drawImage(
this.img,
origin.copy().sub(this.imgWidth / 2, this.imgHeight / 2),
);
});
}
}
interface ISprite {
at: Vector;
width: number;
height: number;
}

18
types.ts Normal file
View File

@ -0,0 +1,18 @@
import { Vector } from "@bearmetal/doodler";
import { TrackSegment } from "./track/system.ts";
declare global {
type End = {
pos: Vector;
segment: TrackSegment;
tangent: Vector;
frontOrBack: "front" | "back";
};
type SerializedTrackSegment = {
p: [number, number, number][];
id: string;
bNeighbors: string[];
fNeighbors: string[];
};
}

View File

@ -1,8 +0,0 @@
import { defineConfig } from "vite";
import deno from "@deno/vite-plugin";
import { strip } from "./vite/plugins/strip.ts";
// https://vite.dev/config/
export default defineConfig({
plugins: [deno(), strip()],
});

View File

@ -1,67 +0,0 @@
import { Plugin } from "vite";
export function strip(): Plugin {
const p: Plugin = {
name: "debug-strip",
enforce: "pre",
apply: "build",
transform(code: string, id: string) {
if (!id.endsWith(".ts") || import.meta.env.DEV) {
return code;
}
const keyword = "override debugDraw";
const results = [];
let currentIndex = 0;
while (true) {
// Find the next occurrence of the keyword starting from currentIndex.
const startIndex = code.indexOf(keyword, currentIndex);
if (startIndex === -1) {
break; // No more occurrences.
}
// Find the first opening brace '{' after the keyword.
const braceStart = code.indexOf("{", startIndex);
if (braceStart === -1) {
// No opening brace found; skip this occurrence.
currentIndex = startIndex + keyword.length;
continue;
}
// Use a counter to find the matching closing brace.
let openBraces = 0;
let endIndex = -1;
for (let i = braceStart; i < code.length; i++) {
if (code[i] === "{") {
openBraces++;
} else if (code[i] === "}") {
openBraces--;
}
// When openBraces returns to 0, we found the matching closing brace.
if (openBraces === 0) {
endIndex = i;
break;
}
}
// If a matching closing brace was found, extract the substring.
if (endIndex !== -1) {
results.push(code.substring(startIndex, endIndex + 1));
// Move the currentIndex past the extracted block.
currentIndex = endIndex + 1;
} else {
// If no matching closing brace is found, skip this occurrence.
currentIndex = startIndex + keyword.length;
}
}
for (const result of results) {
code = code.replace(result, "");
}
return code;
},
};
return p;
}