This commit is contained in:
Schluffe
2026-09-04 13:57:52 +02:00
parent 71cfa64793
commit e961887953
24 changed files with 340 additions and 360 deletions
+3 -23
View File
@@ -4,13 +4,13 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
run_benchmark: run_benchmark:
description: 'Run benchmark job' description: "Run CI"
type: boolean type: boolean
default: true default: true
concurrency: concurrency:
group: bench-${{ github.ref }} group: test-${{ github.ref }}
cancel-in-progress: false cancel-in-progress: true
jobs: jobs:
test: test:
@@ -23,23 +23,3 @@ jobs:
key: bun-${{ hashFiles('bun.lockb') }} key: bun-${{ hashFiles('bun.lockb') }}
- run: bun install --frozen-lockfile - run: bun install --frozen-lockfile
- run: bun test - run: bun test
benchmark:
if: (github.event_name == 'workflow_dispatch' && inputs.run_benchmark)
runs-on: ubuntu-bun-deno
strategy:
matrix:
runtime: [bun, deno]
max-parallel: 1
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('bun.lockb') }}
- run: bun install --frozen-lockfile
- run: bun run bench/index.ts --json > result-${{ matrix.runtime }}.json
- uses: actions/upload-artifact@v3
with:
name: bench-results-${{ matrix.runtime }}
path: result-${{ matrix.runtime }}.json
-17
View File
@@ -1,17 +0,0 @@
import { Bench } from "tinybench";
const bench = new Bench({ name: "bench", time: 1000 });
let sink = 0;
bench
.add("a", () => {
sink++;
})
.add("b", () => {
sink++;
});
await bench.run();
console.log("sink (ignore):", sink);
console.table(bench.table());
+41 -60
View File
@@ -1,20 +1,19 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 2,
"configVersion": 1, "configVersion": 1,
"workspaces": { "workspaces": {
"": { "": {
"name": "nage", "name": "nage",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.11", "@biomejs/biome": "2.5.12",
"@types/bun": "1.4.0", "@types/bun": "1.4.0",
"jsdom": "^30.0.1", "jsdom": "^30.0.1",
"lz-string": "1.5.0", "lz-string": "1.5.0",
"solid-js": "1.9.15", "solid-js": "1.9.15",
"tinybench": "^6.1.3",
"typescript": "7.0.2", "typescript": "7.0.2",
"vite": "8.2.2", "vite": "8.2.2",
"vite-plugin-solid": "2.11.14", "vite-plugin-solid": "2.11.14",
"vitest": "^4.1.11", "vitest": "^5.0.0",
}, },
}, },
}, },
@@ -59,23 +58,23 @@
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="], "@biomejs/biome": ["@biomejs/biome@2.5.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.12", "@biomejs/cli-darwin-x64": "2.5.12", "@biomejs/cli-linux-arm64": "2.5.12", "@biomejs/cli-linux-arm64-musl": "2.5.12", "@biomejs/cli-linux-x64": "2.5.12", "@biomejs/cli-linux-x64-musl": "2.5.12", "@biomejs/cli-win32-arm64": "2.5.12", "@biomejs/cli-win32-x64": "2.5.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Lw4VHZRebrReBBnlHa12JQjnIBm3JJAA55PDB9LbBVBF0q4RYphm6KfmIjqtPhf61MxZ5Q9KoK8R8x+7per5Aw=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lCRY1rwgNeWNgTr4DI/u6ZwXTRwRLHAvbaio1YLLGS+4r1nhvB2ssyPqIpfUSmRveNfv0fn/N58C7CAdK2XVrg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="], "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-vhPgwnh+6tN3ArdAXuET99xaNbFt7CG82Bqn+omHVLC5xdVx45JsYjGPmUIGNzjDek5XdNCP1HKksK7fn8+3bQ=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="], "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-2gp8aVwXYKdAtmBfRFCUuyDMcfN1ahHqUkGfLYrZlNRFmryMATLVvJgWKvyA8wu4Rwn5OSxM1UcUmOuOFNGeBQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="], "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-couHYjFLL5uuI8ne6zhT7KwEsXo5YP7ry/2xmEqah7qanu0YmfDi3mwJg47YXSuv/NpZj22CZzcRH/5c4gjPSQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="], "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-SnvOs3TSTiuia4SQOUNe1aWC9RT4+YkjcKnOhL/nsKOV0k5ycgBkDzF0lUxKn1V7Q8CLTRq6iV23ZAivHomRoA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="], "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8A0oDW58/w9f/PQNYuq0sGUZtGtGrkNF4Z6n0PUoXpLCshi85vtKTv1XSznQawhdE4MXJ8ufpzHXyLFe87M/+w=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="], "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-b9vtoZFsuZt1pdjNwJvXl0f+BpayRzV008uS2+JpmwIKdSE2qdu4A/l04FESwLoou5g2E/Qlec0xwJydZplH+A=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="], "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-B1R/l+CwEpKFSuqiwePzPNRk1EiJN8kc0UhdafNz6MZN9v5OFP9HYP1irptvWzHrwVI4blVNGMbxc5zt70m3IA=="],
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
@@ -83,11 +82,11 @@
"@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="], "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.1", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A=="], "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.2", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.9", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A=="], "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.12", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
@@ -103,42 +102,40 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], "@oxc-project/types": ["@oxc-project/types@0.148.0", "", {}, "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="], "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.7", "", { "os": "android", "cpu": "arm" }, "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.7", "", { "os": "android", "cpu": "arm64" }, "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="], "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="], "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="], "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.7", "", { "os": "linux", "cpu": "arm" }, "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="], "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="], "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="], "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="], "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="], "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="], "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.7", "", { "os": "linux", "cpu": "x64" }, "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="], "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.7", "", { "os": "none", "cpu": "arm64" }, "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="], "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="], "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.7", "", { "os": "win32", "cpu": "x64" }, "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -155,7 +152,7 @@
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], "@types/node": ["@types/node@26.4.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA=="],
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
@@ -197,19 +194,9 @@
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], "@vitest/mocker": ["@vitest/mocker@5.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.0", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA=="],
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], "@vitest/spy": ["@vitest/spy@5.0.0", "", {}, "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
@@ -217,7 +204,7 @@
"babel-preset-solid": ["babel-preset-solid@1.9.15", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.10" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.15" }, "optionalPeers": ["solid-js"] }, "sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg=="], "babel-preset-solid": ["babel-preset-solid@1.9.15", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.10" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.15" }, "optionalPeers": ["solid-js"] }, "sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
@@ -243,7 +230,7 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"electron-to-chromium": ["electron-to-chromium@1.5.416", "", {}, "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA=="], "electron-to-chromium": ["electron-to-chromium@1.5.422", "", {}, "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA=="],
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
@@ -305,7 +292,7 @@
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
@@ -321,19 +308,17 @@
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="], "rolldown": ["rolldown@1.2.7", "", { "dependencies": { "@oxc-project/types": "=0.148.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.7", "@rolldown/binding-android-arm64": "1.2.7", "@rolldown/binding-darwin-arm64": "1.2.7", "@rolldown/binding-darwin-x64": "1.2.7", "@rolldown/binding-freebsd-x64": "1.2.7", "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", "@rolldown/binding-linux-arm64-gnu": "1.2.7", "@rolldown/binding-linux-arm64-musl": "1.2.7", "@rolldown/binding-linux-ppc64-gnu": "1.2.7", "@rolldown/binding-linux-s390x-gnu": "1.2.7", "@rolldown/binding-linux-x64-gnu": "1.2.7", "@rolldown/binding-linux-x64-musl": "1.2.7", "@rolldown/binding-openharmony-arm64": "1.2.7", "@rolldown/binding-win32-arm64-msvc": "1.2.7", "@rolldown/binding-win32-x64-msvc": "1.2.7" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
@@ -357,14 +342,12 @@
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tinybench": ["tinybench@6.1.3", "", {}, "sha512-8k25iNSZHnzkjp3nrpEmx6YkrTNs41PzOHYc28DR5Z2l/CId/Nzw+Ume/41jCeDDmCUMPuK5GkQ/VxJaJ2P6Qg=="], "tinybench": ["tinybench@6.1.4", "", {}, "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ=="],
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
"tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="], "tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="],
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="], "tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
@@ -375,7 +358,7 @@
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="], "undici": ["undici@8.10.1", "", {}, "sha512-YQ3WlbqjYMmNpdvDH64jAgLjxuAR9+649calDWhbshYaeQGO2bR4nI94ORJmwI3J9YhoKQnpyGOK+0zlWS5N5Q=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
@@ -387,7 +370,7 @@
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], "vitest": ["vitest@5.0.0", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.0", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "6.1.4", "tinyexec": "1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.0", "@vitest/browser-preview": "5.0.0", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.0", "@vitest/coverage-v8": "5.0.0", "@vitest/ui": "5.0.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
@@ -413,8 +396,6 @@
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], "data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"vitest/tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"babel-plugin-jsx-dom-expressions/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "babel-plugin-jsx-dom-expressions/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
} }
} }
+3 -1
View File
@@ -34,7 +34,9 @@ function mapSceneEntities(sceneData: SceneData): string[] {
} }
function adjustGameContainer(gameContainer: HTMLElement, width: number): void { function adjustGameContainer(gameContainer: HTMLElement, width: number): void {
gameContainer.style = `position: relative; top: 0; left: ${width}px; width: ${document.body.offsetWidth - width}px;`; gameContainer.style = `position: relative; top: 0; left: ${width}px; width: ${
document.body.offsetWidth - width
}px;`;
} }
const scenesRaw = await fetchScenes(); const scenesRaw = await fetchScenes();
+1 -1
View File
@@ -13,7 +13,7 @@ test("RingBuffer", () => {
expect(ring.buffer.length).toBe(4); expect(ring.buffer.length).toBe(4);
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
expect(claim(ring, (i) => i)).toBe(ring.buffer[i % 4]); expect(claim(ring, (_) => {})).toBe(ring.buffer[i % 4]);
} }
expect(ring.cursor).toBe(0); expect(ring.cursor).toBe(0);
+3 -2
View File
@@ -26,9 +26,10 @@ export function createRingBuffer<T extends object>(
}; };
} }
export function claim<T>(ring: RingBuffer<T>, reset: (instance: T) => T): T { export function claim<T>(ring: RingBuffer<T>, reset: (instance: T) => void): T {
const instance = ring.buffer[ring.cursor]; const instance = ring.buffer[ring.cursor];
reset(instance);
ring.cursor = (ring.cursor + 1) & ring.mask; ring.cursor = (ring.cursor + 1) & ring.mask;
return reset(instance); return instance;
} }
+3 -3
View File
@@ -4,21 +4,21 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build",
"preview": "vite build && vite preview", "preview": "vite build && vite preview",
"test": "vitest", "test": "vitest",
"check": "biome check --write" "check": "biome check --write"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.5.11", "@biomejs/biome": "2.5.12",
"@types/bun": "1.4.0", "@types/bun": "1.4.0",
"jsdom": "^30.0.1", "jsdom": "^30.0.1",
"lz-string": "1.5.0", "lz-string": "1.5.0",
"solid-js": "1.9.15", "solid-js": "1.9.15",
"tinybench": "^6.1.3",
"typescript": "7.0.2", "typescript": "7.0.2",
"vite": "8.2.2", "vite": "8.2.2",
"vite-plugin-solid": "2.11.14", "vite-plugin-solid": "2.11.14",
"vitest": "^4.1.11" "vitest": "^5.0.0"
}, },
"imports": { "imports": {
"#/": "./*" "#/": "./*"
+36 -20
View File
@@ -10,9 +10,10 @@ Each folder under `steps/` is one self-contained kata:
- `README.md` — the concept (taught from zero) and the task. - `README.md` — the concept (taught from zero) and the task.
- a stub file — the function(s) **you** implement. They start by `throw`ing. - a stub file — the function(s) **you** implement. They start by `throw`ing.
- a `*.test.ts` file — the validator. Red until you implement it, green when you're done. - a `*.test.ts` file — the validator. Red until you implement it, green when
- sometimes a `given.ts` — prerequisites from earlier steps, already finished, so you're done.
you only ever implement the **one new idea** of this step. - sometimes a `given.ts` — prerequisites from earlier steps, already finished,
so you only ever implement the **one new idea** of this step.
### Run one step ### Run one step
@@ -22,40 +23,55 @@ From the project root:
bun test workshop/steps/01-vectors bun test workshop/steps/01-vectors
``` ```
Red (failing) is the starting state. Implement the stub until it goes green, then Red (failing) is the starting state. Implement the stub until it goes green,
ping me and I'll validate + unlock the next batch. then ping me and I'll validate + unlock the next batch.
> Skip nothing silently, but blast through what you already know — the early steps > Skip nothing silently, but blast through what you already know — the early
> are deliberately trivial so the test loop becomes muscle memory before the hard > steps are deliberately trivial so the test loop becomes muscle memory before
> rungs. > the hard rungs.
## The ladder ## The ladder
Each rung is a concept that the real `nage` physics depends on. We climb until Each rung is a concept that the real `nage` physics depends on. We climb until
the top rung *is* a working engine. the top rung _is_ a working engine.
**Batch 1 — foundations (these files exist now):** **Batch 1 — foundations (these files exist now):**
- [x] `01-vectors` — vectors as pairs of numbers: add, sub, scale - [x] `01-vectors` — vectors as pairs of numbers: add, sub, scale
- [x] `02-vectors-length` — length, normalize (and the zero-vector trap), dot - [x] `02-vectors-length` — length, normalize (and the zero-vector trap), dot
- [x] `03-integration` — `pos += vel * delta`, and why framerate independence matters - [x] `03-integration` — `pos += vel * delta`, and why framerate independence
- [x] `04-aabb` — axis-aligned boxes, point-in-box, box overlap (the *discrete* test) matters
- [x] `05-sweep-1d` — the **entry/exit time** of a moving point against an interval. The seed of everything. - [x] `04-aabb` — axis-aligned boxes, point-in-box, box overlap (the _discrete_
test)
- [x] `05-sweep-1d` — the **entry/exit time** of a moving point against an
interval. The seed of everything.
**Batch 2 — the swept core (these files exist now):** **Batch 2 — the swept core (these files exist now):**
- [x] `06-ray-vs-aabb` — combine two 1D sweeps into one: ray vs box, with the surface **normal**
- [x] `07-swept-aabb` — the **Minkowski** trick: shrink the moving box to a point, reuse step 06 - [x] `06-ray-vs-aabb` — combine two 1D sweeps into one: ray vs box, with the
surface **normal**
- [x] `07-swept-aabb` — the **Minkowski** trick: shrink the moving box to a
point, reuse step 06
**Batch 3 — response (these files exist now):** **Batch 3 — response (these files exist now):**
- [x] `08-resolve` — stop at the moment of contact (`t`), not after - [x] `08-resolve` — stop at the moment of contact (`t`), not after
- [x] `09-slide` — subtract the into-the-wall part of velocity and keep going along the wall - [x] `09-slide` — subtract the into-the-wall part of velocity and keep going
along the wall
**Batch 4 — the real loop + payoff (these files exist now):** **Batch 4 — the real loop + payoff (these files exist now):**
- [x] `10-move-and-slide` — the full loop: multiple obstacles, iteration cap (relative velocity explained)
- [x] `10-move-and-slide` — the full loop: multiple obstacles, iteration cap
(relative velocity explained)
- [x] `11-capstone` — a canvas demo (no test — just run it and play) - [x] `11-capstone` — a canvas demo (no test — just run it and play)
**Batch 5 — hardening (unlocked by your own capstone finds):** **Batch 5 — hardening (unlocked by your own capstone finds):**
- [x] `12-overlap` — the overlap trap: why positions go `NaN` when you start inside a collider, and the depenetration (minimum-translation-vector) fix
- [ ] `13-crush` — when one push lands you in the next wall: iterative depenetration, and the honest answer for gaps narrower than the hero
When step 11 is green you'll have re-derived your own engine's heart — and walking - [x] `12-overlap` — the overlap trap: why positions go `NaN` when you start
back into `sweptAABB` should feel like reading your own handwriting again. inside a collider, and the depenetration (minimum-translation-vector) fix
- [ ] `13-crush` — when one push lands you in the next wall: iterative
depenetration, and the honest answer for gaps narrower than the hero
When step 11 is green you'll have re-derived your own engine's heart — and
walking back into `sweptAABB` should feel like reading your own handwriting
again.
+3 -2
View File
@@ -9,13 +9,14 @@ A **vector** here is nothing mystical: a pair of numbers `(x, y)`. We use the
same value to mean two different things depending on context: same value to mean two different things depending on context:
- a **position** — a point in the world. - a **position** — a point in the world.
- a **displacement / velocity** — an arrow: "move this much in x, this much in y." - a **displacement / velocity** — an arrow: "move this much in x, this much in
y."
That's it. All of 2D physics is built on adding, subtracting, and scaling these That's it. All of 2D physics is built on adding, subtracting, and scaling these
pairs. pairs.
- `add(a, b)` → `(a.x + b.x, a.y + b.y)` — apply an arrow to a point. - `add(a, b)` → `(a.x + b.x, a.y + b.y)` — apply an arrow to a point.
- `sub(a, b)` → `(a.x - b.x, a.y - b.y)` — the arrow that points *from b to a*. - `sub(a, b)` → `(a.x - b.x, a.y - b.y)` — the arrow that points _from b to a_.
- `scale(a, s)` → `(a.x * s, a.y * s)` — make an arrow longer/shorter. - `scale(a, s)` → `(a.x * s, a.y * s)` — make an arrow longer/shorter.
> Note: we return **new** objects (pure functions) here for clarity. Your real > Note: we return **new** objects (pure functions) here for clarity. Your real
+6 -5
View File
@@ -9,7 +9,7 @@ An arrow `(x, y)` has a length: how far it reaches. Pythagoras:
### Normalize ### Normalize
Often you want *just the direction* of an arrow, with length exactly 1 (a "unit Often you want _just the direction_ of an arrow, with length exactly 1 (a "unit
vector"). You get it by dividing the arrow by its own length: vector"). You get it by dividing the arrow by its own length:
`(x / len, y / len)`. `(x / len, y / len)`.
@@ -20,14 +20,15 @@ straight movement.
> ⚠️ **The zero-vector trap.** What is the length of `(0, 0)`? Zero. What is > ⚠️ **The zero-vector trap.** What is the length of `(0, 0)`? Zero. What is
> `0 / 0`? `NaN`. If you normalize a zero vector naively, you poison it with > `0 / 0`? `NaN`. If you normalize a zero vector naively, you poison it with
> `NaN`, and `NaN` spreads through every later calculation silently. A correct > `NaN`, and `NaN` spreads through every later calculation silently. A correct
> `normalize` must check for zero length and return `(0, 0)` instead of dividing. > `normalize` must check for zero length and return `(0, 0)` instead of
> Remember this trap — it is exactly the kind of bug that hides in a real engine. > dividing. Remember this trap — it is exactly the kind of bug that hides in a
> real engine.
### Dot product ### Dot product
`dot(a, b) = a.x*b.x + a.y*b.y`. One number out of two vectors. For now just `dot(a, b) = a.x*b.x + a.y*b.y`. One number out of two vectors. For now just
implement it; in step 09 you'll learn that it answers "how much of arrow A points implement it; in step 09 you'll learn that it answers "how much of arrow A
along arrow B?" — the key to sliding along a wall. points along arrow B?" — the key to sliding along a wall.
## Task ## Task
+2 -2
View File
@@ -16,8 +16,8 @@ every game's update loop.
### Why `delta`? ### Why `delta`?
`delta` is the number of **milliseconds since the last frame**. Frames are not `delta` is the number of **milliseconds since the last frame**. Frames are not
evenly spaced — a busy frame takes longer. If you moved a fixed amount *per evenly spaced — a busy frame takes longer. If you moved a fixed amount _per
frame* instead of *per millisecond*, your game would run faster on a fast frame_ instead of _per millisecond_, your game would run faster on a fast
computer and slower on a slow one. computer and slower on a slow one.
By storing velocity as **units-per-millisecond** and multiplying by `delta`, the By storing velocity as **units-per-millisecond** and multiplying by `delta`, the
+7 -6
View File
@@ -2,9 +2,10 @@
## Concept ## Concept
**AABB** = **A**xis-**A**ligned **B**ounding **B**ox: a rectangle whose sides are **AABB** = **A**xis-**A**ligned **B**ounding **B**ox: a rectangle whose sides
parallel to the x and y axes (never rotated). They're cheap to test, which is why are parallel to the x and y axes (never rotated). They're cheap to test, which
almost every 2D engine — including yours — uses them as the base collision shape. is why almost every 2D engine — including yours — uses them as the base
collision shape.
We represent one as a corner plus a size: We represent one as a corner plus a size:
@@ -18,7 +19,7 @@ So the box spans `x .. x+w` horizontally and `y .. y+h` vertically.
This is the key insight you'll reuse for the rest of the workshop. Think of each This is the key insight you'll reuse for the rest of the workshop. Think of each
box as a **shadow on the x-axis** and a **shadow on the y-axis**. Two boxes box as a **shadow on the x-axis** and a **shadow on the y-axis**. Two boxes
intersect only if *both* pairs of shadows intersect: intersect only if _both_ pairs of shadows intersect:
``` ```
overlapX: a.x < b.x + b.w AND b.x < a.x + a.w overlapX: a.x < b.x + b.w AND b.x < a.x + a.w
@@ -32,10 +33,10 @@ behind swept collision.
### The discrete trap (why this test alone isn't enough) ### The discrete trap (why this test alone isn't enough)
`aabbOverlap` only answers "are they overlapping *right now*?" If a fast object `aabbOverlap` only answers "are they overlapping _right now_?" If a fast object
jumps from one side of a thin wall to the other in a single frame, it never jumps from one side of a thin wall to the other in a single frame, it never
overlaps the wall at any sampled instant — so this test says "no collision" and overlaps the wall at any sampled instant — so this test says "no collision" and
the object tunnels straight through. Steps 05+ fix that by testing the *path*, the object tunnels straight through. Steps 05+ fix that by testing the _path_,
not the endpoints. Feel the gap here first; it's why everything after exists. not the endpoints. Feel the gap here first; it's why everything after exists.
## Task ## Task
+10 -8
View File
@@ -1,6 +1,6 @@
# Step 05 — Sweeping in 1D (entry & exit time) # Step 05 — Sweeping in 1D (entry & exit time)
This is the seed of the whole engine. Get this one *in your bones* and the scary This is the seed of the whole engine. Get this one _in your bones_ and the scary
2D `sweptAABB` becomes "do this twice and combine." 2D `sweptAABB` becomes "do this twice and combine."
## Concept ## Concept
@@ -9,26 +9,28 @@ Forget 2D. Forget boxes. We have:
- a **point** sitting at position `p` on a number line, - a **point** sitting at position `p` on a number line,
- moving with velocity `v` — meaning over this one frame it travels a total of - moving with velocity `v` — meaning over this one frame it travels a total of
`v` units (so at fraction `t` of the frame, it's at `p + v*t`, for `t` from 0 to 1), `v` units (so at fraction `t` of the frame, it's at `p + v*t`, for `t` from 0
to 1),
- and a static **interval** `[min, max]` on that same line. - and a static **interval** `[min, max]` on that same line.
Question: **during this frame, for which `t` is the point inside `[min, max]`?** Question: **during this frame, for which `t` is the point inside `[min, max]`?**
### The slab math ### The slab math
The point reaches `min` when `p + v*t = min`, i.e. `t = (min - p) / v`. The point reaches `min` when `p + v*t = min`, i.e. `t = (min - p) / v`. Likewise
Likewise it reaches `max` at `t = (max - p) / v`. it reaches `max` at `t = (max - p) / v`.
``` ```
t1 = (min - p) / v t1 = (min - p) / v
t2 = (max - p) / v t2 = (max - p) / v
``` ```
If `v` is **negative** (moving left), the point hits `max` *before* `min`, so If `v` is **negative** (moving left), the point hits `max` _before_ `min`, so
`t1 > t2`. We always want `entry` to be the smaller and `exit` the larger, so `t1 > t2`. We always want `entry` to be the smaller and `exit` the larger, so
**swap them if they're out of order**. Then: **swap them if they're out of order**. Then:
- `entry` = the time the point *enters* the interval, - `entry` = the time the point _enters_ the interval,
- `exit` = the time it *leaves*. - `exit` = the time it _leaves_.
> These can be negative or greater than 1 — that just means the crossing happens > These can be negative or greater than 1 — that just means the crossing happens
> before this frame started or after it ends. Don't clamp here; the caller (step > before this frame started or after it ends. Don't clamp here; the caller (step
@@ -37,7 +39,7 @@ If `v` is **negative** (moving left), the point hits `max` *before* `min`, so
### The `v == 0` edge case ### The `v == 0` edge case
If the point isn't moving (`v == 0`), it never *crosses* an edge — dividing by If the point isn't moving (`v == 0`), it never _crosses_ an edge — dividing by
zero is meaningless. Instead: it's either already inside the interval for the zero is meaningless. Instead: it's either already inside the interval for the
whole frame, or never. So: whole frame, or never. So:
+17 -16
View File
@@ -7,8 +7,8 @@ entry/exit time of one sweep") finally fuse. A **moving point vs a static box**.
A point at `p` moves by `v` over the frame. A static box has a left/right edge A point at `p` moves by `v` over the frame. A static box has a left/right edge
(its x-interval) and a top/bottom edge (its y-interval). The point is inside the (its x-interval) and a top/bottom edge (its y-interval). The point is inside the
**box** only while it's inside the x-interval **and** the y-interval *at the same **box** only while it's inside the x-interval **and** the y-interval _at the
time*. same time_.
So run `sweepInterval` twice: So run `sweepInterval` twice:
@@ -18,7 +18,7 @@ spanY = sweepInterval(p.y, v.y, box.y, box.y + box.h) // the y-edges
``` ```
Each gives you a time-window `[entry, exit]` during which the point is inside Each gives you a time-window `[entry, exit]` during which the point is inside
*that one axis's* strip. You're inside the box during the **overlap of the two _that one axis's_ strip. You're inside the box during the **overlap of the two
windows**: windows**:
``` ```
@@ -28,7 +28,7 @@ exit = min(spanX.exit, spanY.exit) // out of the box once you leave the FIR
Read those two lines until they feel obvious — they're the whole algorithm: Read those two lines until they feel obvious — they're the whole algorithm:
- You're only truly *inside the box* once you've entered **both** strips, so the - You're only truly _inside the box_ once you've entered **both** strips, so the
real entry is the **later** of the two entries → `max`. real entry is the **later** of the two entries → `max`.
- You **leave** the box the instant you exit **either** strip → the **earlier** - You **leave** the box the instant you exit **either** strip → the **earlier**
exit → `min`. exit → `min`.
@@ -36,28 +36,29 @@ Read those two lines until they feel obvious — they're the whole algorithm:
### When is there NO hit? ### When is there NO hit?
1. **A span is `null`** — on some axis the point isn't moving and is already 1. **A span is `null`** — on some axis the point isn't moving and is already
outside that strip. It can never be inside the box. Return `null` immediately. outside that strip. It can never be inside the box. Return `null`
immediately.
2. **`entry > exit`** — the two windows never overlap. The point is inside one 2. **`entry > exit`** — the two windows never overlap. The point is inside one
strip, then the other, but never both at once. That's the classic "flies past strip, then the other, but never both at once. That's the classic "flies past
the corner" miss. the corner" miss.
3. **`entry >= 1` or `exit <= 0`** — the windows overlap, but not *during this 3. **`entry >= 1` or `exit <= 0`** — the windows overlap, but not _during this
frame* (it's entirely in the future, or entirely in the past). Not our problem frame_ (it's entirely in the future, or entirely in the past). Not our
this frame. problem this frame.
### The normal (which wall did we hit?) ### The normal (which wall did we hit?)
When you do collide, you also want to know **which face** you hit, so the response When you do collide, you also want to know **which face** you hit, so the
later can push you back the right way. That's the `normal` — a unit vector response later can push you back the right way. That's the `normal` — a unit
pointing out of the surface you struck. vector pointing out of the surface you struck.
The trick: **the axis you entered *last* is the axis you actually hit.** Compare The trick: **the axis you entered _last_ is the axis you actually hit.** Compare
the two entry times — whichever is larger is the blocking axis: the two entry times — whichever is larger is the blocking axis:
- if `spanX.entry > spanY.entry` → you hit a **vertical** wall (left/right face). - if `spanX.entry > spanY.entry` → you hit a **vertical** wall (left/right
The normal is horizontal, pointing back against your x-motion: face). The normal is horizontal, pointing back against your x-motion:
`normal = { x: v.x > 0 ? -1 : 1, y: 0 }`. `normal = { x: v.x > 0 ? -1 : 1, y: 0 }`.
- otherwise → you hit a **horizontal** wall (top/bottom). The normal is vertical: - otherwise → you hit a **horizontal** wall (top/bottom). The normal is
`normal = { x: 0, y: v.y > 0 ? -1 : 1 }`. vertical: `normal = { x: 0, y: v.y > 0 ? -1 : 1 }`.
(Moving right and hitting something → the surface pushes you left → normal `-1`. (Moving right and hitting something → the surface pushes you left → normal `-1`.
That sign rule is all there is to it.) That sign rule is all there is to it.)
+19 -19
View File
@@ -1,16 +1,16 @@
# Step 07 — Swept AABB (the Minkowski trick) # Step 07 — Swept AABB (the Minkowski trick)
Step 06 handled a moving **point** vs a box. But in a real game the thing that Step 06 handled a moving **point** vs a box. But in a real game the thing that
moves is a **box** (the player), not a point. This step turns "moving box vs box" moves is a **box** (the player), not a point. This step turns "moving box vs
into "moving point vs box" so you can reuse step 06 *unchanged*. That conversion box" into "moving point vs box" so you can reuse step 06 _unchanged_. That
is the single cleverest idea in the whole engine. conversion is the single cleverest idea in the whole engine.
## The problem ## The problem
Box A (the player) sits at corner `(a.x, a.y)` with size `a.w × a.h`, and moves by Box A (the player) sits at corner `(a.x, a.y)` with size `a.w × a.h`, and moves
`v` this frame. Box B (a wall) is static. When do they touch? by `v` this frame. Box B (a wall) is static. When do they touch?
It's fiddly because *both* shapes have size. You'd have to track four edges of A It's fiddly because _both_ shapes have size. You'd have to track four edges of A
against four edges of B. Ugh. against four edges of B. Ugh.
## The trick: grow B, shrink A to a point ## The trick: grow B, shrink A to a point
@@ -22,14 +22,14 @@ spans `[b.x, b.x + b.w]`. They overlap when:
a.x < b.x + b.w AND b.x < a.x + a.w a.x < b.x + b.w AND b.x < a.x + a.w
``` ```
Rearrange the second one (`b.x - a.w < a.x`) and you get a statement purely about Rearrange the second one (`b.x - a.w < a.x`) and you get a statement purely
**`a.x`**, the corner of A: about **`a.x`**, the corner of A:
``` ```
b.x - a.w < a.x < b.x + b.w b.x - a.w < a.x < b.x + b.w
``` ```
Read that: A's *corner* `a.x` behaves exactly like a **point** sliding inside a Read that: A's _corner_ `a.x` behaves exactly like a **point** sliding inside a
**wider interval** — one that starts `a.w` earlier and is `a.w` longer than B. **wider interval** — one that starts `a.w` earlier and is `a.w` longer than B.
The same happens on y with `a.h`. The same happens on y with `a.h`.
@@ -45,18 +45,18 @@ inflated = {
point = { x: a.x, y: a.y } // A is now just its corner point = { x: a.x, y: a.y } // A is now just its corner
``` ```
This grown box is the **Minkowski sum** of B with A. And "does this point, moving This grown box is the **Minkowski sum** of B with A. And "does this point,
by `v`, hit `inflated`?" is *exactly* `rayVsAABB` from step 06. You're done in moving by `v`, hit `inflated`?" is _exactly_ `rayVsAABB` from step 06. You're
three lines. done in three lines.
> Sanity picture: player box 2 wide with its right edge at x=2, wall left edge at > Sanity picture: player box 2 wide with its right edge at x=2, wall left edge
> x=5 → real gap is 3. Inflate: `inflated.x = 5 - 2 = 3`, and the player's corner > at x=5 → real gap is 3. Inflate: `inflated.x = 5 - 2 = 3`, and the player's
> sits at x=0, so the corner-to-inflated-edge gap is also 3. Same answer, simpler > corner sits at x=0, so the corner-to-inflated-edge gap is also 3. Same answer,
> shape. The inflation *bakes A's size into the wall* so the corner can pretend to > simpler shape. The inflation _bakes A's size into the wall_ so the corner can
> be a point. > pretend to be a point.
This is the heart of your real engine's `sweptAABB` — the `inflAABB` it builds is This is the heart of your real engine's `sweptAABB` — the `inflAABB` it builds
this very inflated box, and `(ax, ay)` is this corner point. is this very inflated box, and `(ax, ay)` is this corner point.
## Task ## Task
+19 -18
View File
@@ -1,15 +1,15 @@
# Step 08 — Resolve (move *to* the wall, not through it) # Step 08 — Resolve (move _to_ the wall, not through it)
Detection is done. Now for **response** — actually reacting to the hit. This first Detection is done. Now for **response** — actually reacting to the hit. This
half is almost embarrassingly small, but it introduces the idea the whole loop first half is almost embarrassingly small, but it introduces the idea the whole
(step 10) is built on: **the frame isn't all-or-nothing.** loop (step 10) is built on: **the frame isn't all-or-nothing.**
## Concept ## Concept
`sweptAABB` hands you a `Hit` with a `time` between 0 and 1 — the fraction of the `sweptAABB` hands you a `Hit` with a `time` between 0 and 1 — the fraction of
frame at which you'd collide. So instead of moving the full displacement `v` the frame at which you'd collide. So instead of moving the full displacement `v`
(which would bury you inside the wall), you move only the part of it that happens (which would bury you inside the wall), you move only the part of it that
*before* impact: happens _before_ impact:
``` ```
no hit → newPos = pos + v (nothing in the way: take the whole move) no hit → newPos = pos + v (nothing in the way: take the whole move)
@@ -22,21 +22,22 @@ That's it — you already have `add` and `scale`; this is them, gated on the hit
Here's the seed for step 10: if you hit at `t = 0.3`, you only used **30%** of Here's the seed for step 10: if you hit at `t = 0.3`, you only used **30%** of
this frame. The other **70%** is still owed to the player — they should keep this frame. The other **70%** is still owed to the player — they should keep
moving for the rest of the frame, just not *into* the wall. That leftover time is moving for the rest of the frame, just not _into_ the wall. That leftover time
exactly why sliding (step 09) and the loop (step 10) exist. A collision doesn't is exactly why sliding (step 09) and the loop (step 10) exist. A collision
end the frame; it **interrupts** it. doesn't end the frame; it **interrupts** it.
> Real-engine footnote: production code usually moves to `t - EPSILON` (a hair > Real-engine footnote: production code usually moves to `t - EPSILON` (a hair
> *short* of contact) so floating-point error can't leave the box a sliver inside > _short_ of contact) so floating-point error can't leave the box a sliver
> the wall, where the next frame's sweep would start already-overlapping. Your > inside the wall, where the next frame's sweep would start already-overlapping.
> `nage` does this with `Math.max(0, time - EPSILON)`. We keep the kata exact so > Your `nage` does this with `Math.max(0, time - EPSILON)`. We keep the kata
> the numbers stay clean — just know that tiny backoff is there for a real reason. > exact so the numbers stay clean — just know that tiny backoff is there for a
> real reason.
## Task ## Task
Implement `resolve(pos, v, hit)` in `resolve.ts`: return the full move when `hit` Implement `resolve(pos, v, hit)` in `resolve.ts`: return the full move when
is `null`, otherwise the position at contact. `vec/add/scale` and the `Hit` type `hit` is `null`, otherwise the position at contact. `vec/add/scale` and the
are in `given.ts`. `Hit` type are in `given.ts`.
```sh ```sh
bun test workshop/steps/08-resolve bun test workshop/steps/08-resolve
+16 -13
View File
@@ -1,28 +1,31 @@
# Step 09 — Slide (this is what `dot` was for) # Step 09 — Slide (this is what `dot` was for)
Back in step 02 you implemented `dot` and I said "you'll see later." This is Back in step 02 you implemented `dot` and I said "you'll see later." This is
later. Sliding is the difference between a game that feels good and one where you later. Sliding is the difference between a game that feels good and one where
stick to every wall like glue. you stick to every wall like glue.
## The problem ## The problem
You're moving with velocity `v` and you hit a wall whose outward normal is `n`. You're moving with velocity `v` and you hit a wall whose outward normal is `n`.
If you just *stop* (velocity → 0), the player jams against the wall — press If you just _stop_ (velocity → 0), the player jams against the wall — press into
into a wall diagonally and all motion dies, even the part that was parallel to a wall diagonally and all motion dies, even the part that was parallel to the
the wall and perfectly fine. What we actually want: **cancel only the part of `v` wall and perfectly fine. What we actually want: **cancel only the part of `v`
that pushes *into* the wall, and keep the part that runs *along* it.** That's a that pushes _into_ the wall, and keep the part that runs _along_ it.** That's a
slide. slide.
## The math (projection) ## The math (projection)
Any velocity `v` can be split into two pieces relative to the wall: Any velocity `v` can be split into two pieces relative to the wall:
- the part **along the normal** (into/out of the wall) — this is what the wall forbids, - the part **along the normal** (into/out of the wall) — this is what the wall
- the part **along the wall surface** (perpendicular to the normal) — this is fine. forbids,
- the part **along the wall surface** (perpendicular to the normal) — this is
fine.
Because `n` is a **unit vector**, the amount of `v` pointing along `n` is exactly Because `n` is a **unit vector**, the amount of `v` pointing along `n` is
`dot(v, n)`. That single number is "how much of `v` goes straight into the wall." exactly `dot(v, n)`. That single number is "how much of `v` goes straight into
The vector piece pointing into the wall is `n * dot(v, n)`. Subtract it off: the wall." The vector piece pointing into the wall is `n * dot(v, n)`. Subtract
it off:
``` ```
vSlide = v - n * dot(v, n) vSlide = v - n * dot(v, n)
@@ -30,8 +33,8 @@ vSlide = v - n * dot(v, n)
What's left has **zero** component along the normal — it lies flat against the What's left has **zero** component along the normal — it lies flat against the
wall. (That's the geometric meaning of `dot`: it measures how much two vectors wall. (That's the geometric meaning of `dot`: it measures how much two vectors
share a direction. Subtract the shared-with-the-normal part, and nothing pointing share a direction. Subtract the shared-with-the-normal part, and nothing
into the wall survives.) pointing into the wall survives.)
### Feel it with numbers ### Feel it with numbers
+28 -24
View File
@@ -6,14 +6,15 @@ give you the skeleton — you write the code.
## The idea ## The idea
A single collision doesn't end the frame (step 08's "leftover that matters"). You A single collision doesn't end the frame (step 08's "leftover that matters").
hit a wall at `t = 0.3`, slide, and **70% of the frame is still owed** — during You hit a wall at `t = 0.3`, slide, and **70% of the frame is still owed** —
which you might hit *another* wall, slide again, and so on. So moving is a small during which you might hit _another_ wall, slide again, and so on. So moving is
**loop**: sweep → stop at the nearest hit → slide → repeat with the leftover time. a small **loop**: sweep → stop at the nearest hit → slide → repeat with the
leftover time.
We loop a **maximum of 4 times** (your engine's cap) — enough to handle a corner We loop a **maximum of 4 times** (your engine's cap) — enough to handle a corner
(hit a wall, slide, hit the perpendicular wall, slide, stop) without ever risking (hit a wall, slide, hit the perpendicular wall, slide, stop) without ever
an infinite loop. risking an infinite loop.
## The algorithm ## The algorithm
@@ -46,29 +47,32 @@ return pos
Two things worth understanding, not just copying: Two things worth understanding, not just copying:
- **`move = vel * timeLeft`.** `vel` is a *full-frame* displacement (how far you'd - **`move = vel * timeLeft`.** `vel` is a _full-frame_ displacement (how far
go in a whole frame at this velocity). You only have `timeLeft` of the frame you'd go in a whole frame at this velocity). You only have `timeLeft` of the
left, so the actual travel is `vel * timeLeft`. `sweptAABB`'s returned `time` is frame left, so the actual travel is `vel * timeLeft`. `sweptAABB`'s returned
then a fraction *of that sub-move*, which is why `pos + move * time` is correct. `time` is then a fraction _of that sub-move_, which is why `pos + move * time`
is correct.
- **The `EPSILON` backoff** (`max(0, hit.time - EPSILON)`). Stop a hair *short* of - **The `EPSILON` backoff** (`max(0, hit.time - EPSILON)`). Stop a hair _short_
the wall. If you land exactly on it, floating-point error can leave you a sliver of the wall. If you land exactly on it, floating-point error can leave you a
inside — and next iteration's sweep would start already-overlapping, reporting a sliver inside — and next iteration's sweep would start already-overlapping,
garbage negative-time "collision" that makes you stick or jitter. That tiny gap reporting a garbage negative-time "collision" that makes you stick or jitter.
is exactly the `Math.max(0, time - EPSILON)` in your real `moveAndSlide`. Now you That tiny gap is exactly the `Math.max(0, time - EPSILON)` in your real
know *why* it's there. `EPSILON` is provided in `given.ts`. `moveAndSlide`. Now you know _why_ it's there. `EPSILON` is provided in
`given.ts`.
> **Moving-vs-moving (why your real engine has `velocity - otherVel`).** Here the > **Moving-vs-moving (why your real engine has `velocity - otherVel`).** Here
> walls are static, so we sweep with plain `vel`. When the *other* body also moves, > the walls are static, so we sweep with plain `vel`. When the _other_ body also
> you sweep in its frame of reference by using the **relative** velocity > moves, you sweep in its frame of reference by using the **relative** velocity
> `vel - otherVel` — then the exact same loop works, because from the other body's > `vel - otherVel` — then the exact same loop works, because from the other
> point of view it's standing still. That's the only difference between this kata > body's point of view it's standing still. That's the only difference between
> and the full engine. The loop itself doesn't change. > this kata and the full engine. The loop itself doesn't change.
## Task ## Task
Implement `moveAndSlide(box, v, walls)` in `moveAndSlide.ts`. Everything you need — Implement `moveAndSlide(box, v, walls)` in `moveAndSlide.ts`. Everything you
`sweptAABB`, `slide`, the vector ops, `EPSILON` — is finished in `given.ts`. need — `sweptAABB`, `slide`, the vector ops, `EPSILON` — is finished in
`given.ts`.
```sh ```sh
bun test workshop/steps/10-move-and-slide bun test workshop/steps/10-move-and-slide
+13 -11
View File
@@ -19,27 +19,29 @@ bunx serve workshop/steps/11-capstone
``` ```
Arrow keys move the pink box. Run it into the border, the ledge, the pillar, the Arrow keys move the pink box. Run it into the border, the ledge, the pillar, the
bar. Push diagonally into a wall and watch it **slide** along instead of sticking. bar. Push diagonally into a wall and watch it **slide** along instead of
That sliding is your step-09 `dot`-product projection. The fact that it stops sticking. That sliding is your step-09 `dot`-product projection. The fact that
*at* the wall instead of tunneling through, even at speed, is your step-07 swept it stops _at_ the wall instead of tunneling through, even at speed, is your
detection. The clean corners are your step-10 loop running twice in one frame. step-07 swept detection. The clean corners are your step-10 loop running twice
in one frame.
## Make it yours (optional) ## Make it yours (optional)
- Open `game.js`. The top half is your kernel — read it and confirm it matches - Open `game.js`. The top half is your kernel — read it and confirm it matches
what you wrote. Swap in your own `moveAndSlide` from step 10 and check it feels what you wrote. Swap in your own `moveAndSlide` from step 10 and check it
identical (it will). feels identical (it will).
- Add a wall to the `walls` array. Change `SPEED`. Make the player bigger. - Add a wall to the `walls` array. Change `SPEED`. Make the player bigger.
- Try **deleting the `EPSILON` backoff** (`Math.max(0, nearest.time - EPSILON)` - Try **deleting the `EPSILON` backoff** (`Math.max(0, nearest.time - EPSILON)`
→ `nearest.time`) and push into a wall. Watch it stick and jitter. Then put it → `nearest.time`) and push into a wall. Watch it stick and jitter. Then put it
back. Now you've *felt* why that line exists in your real engine. back. Now you've _felt_ why that line exists in your real engine.
## You're back ## You're back
That's the whole climb: pairs of numbers → sweeping a point → sweeping a box via That's the whole climb: pairs of numbers → sweeping a point → sweeping a box via
Minkowski → detecting the hit → stopping and sliding → the full loop → a thing you Minkowski → detecting the hit → stopping and sliding → the full loop → a thing
can play. Every rung is a function that exists, by name, inside your real you can play. Every rung is a function that exists, by name, inside your real
`engine/system/physics.ts`. `engine/system/physics.ts`.
Now go open the real `sweptAABB` with fresh eyes. You know exactly what every line Now go open the real `sweptAABB` with fresh eyes. You know exactly what every
is *supposed* to do — so the two lines that don't should stand out. Happy hunting. line is _supposed_ to do — so the two lines that don't should stand out. Happy
hunting.
+22 -21
View File
@@ -2,21 +2,21 @@
You found this one yourself, in the capstone: start a box **inside** another You found this one yourself, in the capstone: start a box **inside** another
collider, press a key, and the position turns into `NaN`. That's not a typo in collider, press a key, and the position turns into `NaN`. That's not a typo in
your code from steps 01–10 — the kernel is *correct* and still does this. It's a your code from steps 01–10 — the kernel is _correct_ and still does this. It's a
**blind spot in the whole approach**, and every real engine has to patch it. **blind spot in the whole approach**, and every real engine has to patch it.
> This is a genuine hole in the finished kernel, shared by the capstone's > This is a genuine hole in the finished kernel, shared by the capstone's
> `game.js`. It is a *third* thing, separate from the two bugs you're hunting in > `game.js`. It is a _third_ thing, separate from the two bugs you're hunting in
> `engine/system/physics.ts` — no spoilers here. > `engine/system/physics.ts` — no spoilers here.
## Why the sweep can't see it ## Why the sweep can't see it
Everything since step 05 answers one question: *"when, during this frame, will I Everything since step 05 answers one question: _"when, during this frame, will I
**enter** the box?"* The whole ladder quietly assumes the answer lies in the **enter** the box?"_ The whole ladder quietly assumes the answer lies in the
future — that you start the frame **outside**. future — that you start the frame **outside**.
Start inside, and "when will I enter?" has no sane answer. The math doesn't Start inside, and "when will I enter?" has no sane answer. The math doesn't
refuse — it cheerfully reports that you entered *in the past*. Remember step 05: refuse — it cheerfully reports that you entered _in the past_. Remember step 05:
what sign does `entry` have when `p` is already between `min` and `max`? Every what sign does `entry` have when `p` is already between `min` and `max`? Every
function above `sweepInterval` trusts that number without checking it. function above `sweepInterval` trusts that number without checking it.
@@ -38,33 +38,34 @@ Work through these **in order, predicting each answer before checking** (add
`console.log`s inside your step-10 loop — it's your code, instrument it): `console.log`s inside your step-10 loop — it's your code, instrument it):
1. What `time` does `sweptAABB` report? Now flip the velocity so the box moves 1. What `time` does `sweptAABB` report? Now flip the velocity so the box moves
*away* from the wall — why do you *still* get a hit? (This is why you can't _away_ from the wall — why do you _still_ get a hit? (This is why you can't
even walk out of a wall you're stuck in.) even walk out of a wall you're stuck in.)
2. Follow that `time` into the `else` branch of `moveAndSlide`. Three lines use 2. Follow that `time` into the `else` branch of `moveAndSlide`. Three lines use
it. Which line is saved by the `Math.max(0, …)`? What happens to `vel` when it. Which line is saved by the `Math.max(0, …)`? What happens to `vel` when
you `slide` against that normal? And what does `timeLeft = timeLeft * (1 - time)` you `slide` against that normal? And what does
do when `time` is negative — shrink, or *grow*? `timeLeft = timeLeft * (1 - time)` do when `time` is negative — shrink, or
_grow_?
3. Next iteration: `vel` is now `(0, 0)` but the loop keeps going. What does 3. Next iteration: `vel` is now `(0, 0)` but the loop keeps going. What does
`sweepInterval` return for `v = 0` while inside the interval (look at the `sweepInterval` return for `v = 0` while inside the interval (look at the
first branch — you wrote it in step 05)? So what is `entry` now, and what first branch — you wrote it in step 05)? So what is `entry` now, and what
does `timeLeft` become after multiplying by `(1 - entry)`? does `timeLeft` become after multiplying by `(1 - entry)`?
4. Last link. In JavaScript, what is `0 * Infinity`? That's `scale(vel, timeLeft)` 4. Last link. In JavaScript, what is `0 * Infinity`? That's
on iteration three. And once one `NaN` exists, every comparison against it is `scale(vel, timeLeft)` on iteration three. And once one `NaN` exists, every
`false` — so which branch of the loop does the poisoned move fall into, and comparison against it is `false` — so which branch of the loop does the
what does `pos = add(pos, move)` do then? poisoned move fall into, and what does `pos = add(pos, move)` do then?
Four links: **overlap → a hit in the past → dead velocity + growing time debt → Four links: **overlap → a hit in the past → dead velocity + growing time debt →
`0 × ∞`**. When you can retell that chain from memory, you own it. `0 × ∞`**. When you can retell that chain from memory, you own it.
## The fix: measure the overlap, push out ## The fix: measure the overlap, push out
The sweep is *continuous* detection — it prevents overlap but can't recover from The sweep is _continuous_ detection — it prevents overlap but can't recover from
it. So real engines pair it with a *discrete* partner: if you're already inside, it. So real engines pair it with a _discrete_ partner: if you're already inside,
don't ask "when do I enter?" — ask **"how deep am I, and what's the shortest way don't ask "when do I enter?" — ask **"how deep am I, and what's the shortest way
out?"**, then teleport that far and *only then* sweep. out?"**, then teleport that far and _only then_ sweep.
That shortest-way-out is the **penetration vector** (the famous *minimum That shortest-way-out is the **penetration vector** (the famous _minimum
translation vector*). For two overlapping AABBs there are exactly four escapes — translation vector_). For two overlapping AABBs there are exactly four escapes —
push `a` left, right, up, or down until the boxes just separate: push `a` left, right, up, or down until the boxes just separate:
``` ```
@@ -79,9 +80,9 @@ Two things to convince yourself of (don't skip — the tests check both):
- The boxes strictly overlap **iff all four distances are positive**. (What is - The boxes strictly overlap **iff all four distances are positive**. (What is
`outLeft` when `a` sits fully to the right of `b`? When they merely touch?) `outLeft` when `a` sits fully to the right of `b`? When they merely touch?)
- The answer is the **smallest** of the four, as a vector, with the sign that - The answer is the **smallest** of the four, as a vector, with the sign that
moves `a` *away*. Smallest, because depenetration is a teleport the player can moves `a` _away_. Smallest, because depenetration is a teleport the player can
see — one pixel of pop beats being flung across the room. Note this handles see — one pixel of pop beats being flung across the room. Note this handles
`a` fully *swallowed* by `b` too, where "the overlap of the intervals" would `a` fully _swallowed_ by `b` too, where "the overlap of the intervals" would
lie to you — one of the tests is exactly that case. lie to you — one of the tests is exactly that case.
## Task ## Task
@@ -92,7 +93,7 @@ Two functions in `overlap.ts`:
boxes, or `null` if they don't strictly overlap. boxes, or `null` if they don't strictly overlap.
2. `safeMoveAndSlide(box, v, walls)` — check every wall; if the box is inside 2. `safeMoveAndSlide(box, v, walls)` — check every wall; if the box is inside
one, apply the push **plus an `EPSILON` of slack in the push direction** one, apply the push **plus an `EPSILON` of slack in the push direction**
(same idea as the backoff in the loop: land *flush* on the wall and next (same idea as the backoff in the loop: land _flush_ on the wall and next
frame's sweep starts half-trapped again). Then run the given `moveAndSlide` frame's sweep starts half-trapped again). Then run the given `moveAndSlide`
from the safe position. from the safe position.
@@ -105,4 +106,4 @@ bun test workshop/steps/12-overlap
Port both functions into `11-capstone/game.js`, swap the `moveAndSlide` call for Port both functions into `11-capstone/game.js`, swap the `moveAndSlide` call for
`safeMoveAndSlide`, and set the player's spawn inside the pillar. It should pop `safeMoveAndSlide`, and set the player's spawn inside the pillar. It should pop
out and play on like nothing happened. Then the question you actually care out and play on like nothing happened. Then the question you actually care
about: does your *real* engine survive the same experiment? about: does your _real_ engine survive the same experiment?
+21 -21
View File
@@ -3,17 +3,17 @@
You found this one yourself too, chasing the capstone's ledge: the hero gets You found this one yourself too, chasing the capstone's ledge: the hero gets
pushed out of the moving ledge, lands **inside the pillar**, and the `NaN` you pushed out of the moving ledge, lands **inside the pillar**, and the `NaN` you
buried in step 12 climbs right back out of its grave. Your autopsy chain from buried in step 12 climbs right back out of its grave. Your autopsy chain from
last time is unchanged — the only new thing is *how a box you just freed ends last time is unchanged — the only new thing is _how a box you just freed ends up
up inside a wall again in the very same frame*. inside a wall again in the very same frame_.
> Still a *fourth* thing, separate from the two bugs you're hunting in > Still a _fourth_ thing, separate from the two bugs you're hunting in
> `engine/system/physics.ts` — no spoilers there. > `engine/system/physics.ts` — no spoilers there.
## Why one pass isn't enough ## Why one pass isn't enough
Step 12's `safeMoveAndSlide` walks the walls **once, in array order**, fixing Step 12's `safeMoveAndSlide` walks the walls **once, in array order**, fixing
each overlap it meets. For one wall that's airtight. But a depenetration push is each overlap it meets. For one wall that's airtight. But a depenetration push is
a *teleport* — and a teleport can land you inside a wall the loop already a _teleport_ — and a teleport can land you inside a wall the loop already
checked and cleared, or one it hasn't reached yet (in which case it works, by checked and cleared, or one it hasn't reached yet (in which case it works, by
luck of the ordering). A resolver whose correctness depends on the order of the luck of the ordering). A resolver whose correctness depends on the order of the
wall array isn't a resolver — it's a coin flip. wall array isn't a resolver — it's a coin flip.
@@ -28,7 +28,7 @@ Two experiments in a scratch file, **predicting each outcome before running**
2. Walls `{x: 10, y: -5, w: 4, h: 10}` and `{x: 6.5, y: -5, w: 2, h: 10}` — a 2. Walls `{x: 10, y: -5, w: 4, h: 10}` and `{x: 6.5, y: -5, w: 2, h: 10}` — a
gap 1.5 wide. Hero (2 wide) at `{x: 9, y: 0}`. Apply `penetrationVector` gap 1.5 wide. Hero (2 wide) at `{x: 9, y: 0}`. Apply `penetrationVector`
pushes in a loop and log `x` each time. Does it converge? What number does pushes in a loop and log `x` each time. Does it converge? What number does
`x` bounce between, and *why will it never stop*? `x` bounce between, and _why will it never stop_?
## The negotiation, and when it honestly fails ## The negotiation, and when it honestly fails
@@ -36,11 +36,11 @@ The fix for experiment 1 is patience: don't do one pass — **repeat whole passe
until a full pass finds nothing to fix**. That clean pass is your proof of until a full pass finds nothing to fix**. That clean pass is your proof of
freedom. Each pass is cheap, and in sane geometry it settles in one or two. freedom. Each pass is cheap, and in sane geometry it settles in one or two.
But experiment 2 shows the negotiation can be *unwinnable*: when the gap is But experiment 2 shows the negotiation can be _unwinnable_: when the gap is
narrower than the box, **no overlap-free position exists**. No amount of math narrower than the box, **no overlap-free position exists**. No amount of math
fixes that, because it isn't a math problem — it's a game-design question, and fixes that, because it isn't a math problem — it's a game-design question, and
every game answers it differently. Mario between a Thwomp and the floor: every game answers it differently. Mario between a Thwomp and the floor: crushed
crushed = death. Some engines let the wall shove you *through* its partner. = death. Some engines let the wall shove you _through_ its partner.
Zelda-flavored games mostly refuse the situation: solid wins, the hero holds Zelda-flavored games mostly refuse the situation: solid wins, the hero holds
still until the gap opens. We take that one — it's the smallest honest answer: still until the gap opens. We take that one — it's the smallest honest answer:
**cap the passes, and if the cap fires, report it** (`settled: false`) instead **cap the passes, and if the cap fires, report it** (`settled: false`) instead
@@ -48,9 +48,9 @@ of pretending. You already believe in caps; your step-10 loop carries one for
exactly the same reason. exactly the same reason.
Last session you proposed armoring `sweepInterval` against the `Infinity` Last session you proposed armoring `sweepInterval` against the `Infinity`
directly. You can — see the optional section — but notice what that answer directly. You can — see the optional section — but notice what that answer skips
skips over: even with the `NaN` gone, *what should a crushed hero do?* The over: even with the `NaN` gone, _what should a crushed hero do?_ The kernel
kernel can't know; it only measures. Deciding is the resolver's job. Keeping can't know; it only measures. Deciding is the resolver's job. Keeping
**detection** and **policy** separate is the actual lesson of this step. **detection** and **policy** separate is the actual lesson of this step.
## Task ## Task
@@ -67,8 +67,8 @@ Two functions in `crush.ts`:
One warning on the `EPSILON` slack: apply it **only along the axis you actually One warning on the `EPSILON` slack: apply it **only along the axis you actually
pushed**. Before you port your step-12 slack code verbatim, play computer with pushed**. Before you port your step-12 slack code verbatim, play computer with
`pv = { x: -3, y: 0 }` and watch what your two lines do to `y`. (That was my `pv = { x: -3, y: 0 }` and watch what your two lines do to `y`. (That was my
"bonus" question last session — it's still open, and one of the tests refuses "bonus" question last session — it's still open, and one of the tests refuses to
to look away.) look away.)
```sh ```sh
bun test workshop/steps/13-crush bun test workshop/steps/13-crush
@@ -76,13 +76,13 @@ bun test workshop/steps/13-crush
## Optional 1 — the airbag ## Optional 1 — the airbag
Defense in depth: even if some future caller hands `moveAndSlide` an Defense in depth: even if some future caller hands `moveAndSlide` an overlapping
overlapping box directly, it should return finite numbers — wrong-ish, maybe, box directly, it should return finite numbers — wrong-ish, maybe, but _finite_.
but *finite*. You traced in step 12 exactly which value poisons the well. The You traced in step 12 exactly which value poisons the well. The loop already
loop already clamps it once (`Math.max(0, …)`) — find the **other** line that clamps it once (`Math.max(0, …)`) — find the **other** line that trusts
trusts `nearest.time` to be non-negative. The fix is almost nothing. Then write `nearest.time` to be non-negative. The fix is almost nothing. Then write the
the test step 12 should have had: `moveAndSlide` (not `safe…`) with an test step 12 should have had: `moveAndSlide` (not `safe…`) with an overlapping
overlapping start returns finite coordinates. start returns finite coordinates.
## Optional 2 — the capstone payoff ## Optional 2 — the capstone payoff
@@ -93,4 +93,4 @@ squirt around the ledge like a watermelon seed pinched between two fingers.
Then earn the effect: the ledge is 16 tall and the hero is 12. As the ledge digs Then earn the effect: the ledge is 16 tall and the hero is 12. As the ledge digs
deeper, which of `penetrationVector`'s four escapes wins, and at what depth does deeper, which of `penetrationVector`'s four escapes wins, and at what depth does
the winner change? That flip *is* the watermelon seed. the winner change? That flip _is_ the watermelon seed.