WIP
This commit is contained in:
@@ -4,13 +4,13 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_benchmark:
|
||||
description: 'Run benchmark job'
|
||||
description: "Run CI"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
concurrency:
|
||||
group: bench-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
group: test-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -23,23 +23,3 @@ jobs:
|
||||
key: bun-${{ hashFiles('bun.lockb') }}
|
||||
- run: bun install --frozen-lockfile
|
||||
- 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
|
||||
@@ -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());
|
||||
@@ -1,20 +1,19 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "nage",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.11",
|
||||
"@biomejs/biome": "2.5.12",
|
||||
"@types/bun": "1.4.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"lz-string": "1.5.0",
|
||||
"solid-js": "1.9.15",
|
||||
"tinybench": "^6.1.3",
|
||||
"typescript": "7.0.2",
|
||||
"vite": "8.2.2",
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -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-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-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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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__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/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=="],
|
||||
|
||||
@@ -197,19 +194,9 @@
|
||||
|
||||
"@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/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=="],
|
||||
"@vitest/spy": ["@vitest/spy@5.0.0", "", {}, "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -243,7 +230,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -305,7 +292,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -321,19 +308,17 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -357,14 +342,12 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -34,7 +34,9 @@ function mapSceneEntities(sceneData: SceneData): string[] {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -13,7 +13,7 @@ test("RingBuffer", () => {
|
||||
expect(ring.buffer.length).toBe(4);
|
||||
|
||||
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);
|
||||
|
||||
@@ -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];
|
||||
reset(instance);
|
||||
ring.cursor = (ring.cursor + 1) & ring.mask;
|
||||
|
||||
return reset(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,21 +4,21 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite build && vite preview",
|
||||
"test": "vitest",
|
||||
"check": "biome check --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.5.11",
|
||||
"@biomejs/biome": "2.5.12",
|
||||
"@types/bun": "1.4.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"lz-string": "1.5.0",
|
||||
"solid-js": "1.9.15",
|
||||
"tinybench": "^6.1.3",
|
||||
"typescript": "7.0.2",
|
||||
"vite": "8.2.2",
|
||||
"vite-plugin-solid": "2.11.14",
|
||||
"vitest": "^4.1.11"
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"imports": {
|
||||
"#/": "./*"
|
||||
|
||||
+36
-20
@@ -10,9 +10,10 @@ Each folder under `steps/` is one self-contained kata:
|
||||
|
||||
- `README.md` — the concept (taught from zero) and the task.
|
||||
- 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.
|
||||
- sometimes a `given.ts` — prerequisites from earlier steps, already finished, so
|
||||
you only ever implement the **one new idea** of this step.
|
||||
- a `*.test.ts` file — the validator. Red until you implement it, green when
|
||||
you're done.
|
||||
- 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
|
||||
|
||||
@@ -22,40 +23,55 @@ From the project root:
|
||||
bun test workshop/steps/01-vectors
|
||||
```
|
||||
|
||||
Red (failing) is the starting state. Implement the stub until it goes green, then
|
||||
ping me and I'll validate + unlock the next batch.
|
||||
Red (failing) is the starting state. Implement the stub until it goes green,
|
||||
then ping me and I'll validate + unlock the next batch.
|
||||
|
||||
> Skip nothing silently, but blast through what you already know — the early steps
|
||||
> are deliberately trivial so the test loop becomes muscle memory before the hard
|
||||
> rungs.
|
||||
> Skip nothing silently, but blast through what you already know — the early
|
||||
> steps are deliberately trivial so the test loop becomes muscle memory before
|
||||
> the hard rungs.
|
||||
|
||||
## The ladder
|
||||
|
||||
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):**
|
||||
|
||||
- [x] `01-vectors` — vectors as pairs of numbers: add, sub, scale
|
||||
- [x] `02-vectors-length` — length, normalize (and the zero-vector trap), dot
|
||||
- [x] `03-integration` — `pos += vel * delta`, and why framerate independence matters
|
||||
- [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.
|
||||
- [x] `03-integration` — `pos += vel * delta`, and why framerate independence
|
||||
matters
|
||||
- [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):**
|
||||
- [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):**
|
||||
|
||||
- [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):**
|
||||
- [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)
|
||||
|
||||
**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
|
||||
back into `sweptAABB` should feel like reading your own handwriting again.
|
||||
- [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 back into `sweptAABB` should feel like reading your own handwriting
|
||||
again.
|
||||
|
||||
@@ -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:
|
||||
|
||||
- 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
|
||||
pairs.
|
||||
|
||||
- `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.
|
||||
|
||||
> Note: we return **new** objects (pure functions) here for clarity. Your real
|
||||
|
||||
@@ -9,7 +9,7 @@ An arrow `(x, y)` has a length: how far it reaches. Pythagoras:
|
||||
|
||||
### 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:
|
||||
`(x / len, y / len)`.
|
||||
|
||||
@@ -20,14 +20,15 @@ straight movement.
|
||||
> ⚠️ **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
|
||||
> `NaN`, and `NaN` spreads through every later calculation silently. A correct
|
||||
> `normalize` must check for zero length and return `(0, 0)` instead of dividing.
|
||||
> Remember this trap — it is exactly the kind of bug that hides in a real engine.
|
||||
> `normalize` must check for zero length and return `(0, 0)` instead of
|
||||
> dividing. Remember this trap — it is exactly the kind of bug that hides in a
|
||||
> real engine.
|
||||
|
||||
### Dot product
|
||||
|
||||
`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
|
||||
along arrow B?" — the key to sliding along a wall.
|
||||
implement it; in step 09 you'll learn that it answers "how much of arrow A
|
||||
points along arrow B?" — the key to sliding along a wall.
|
||||
|
||||
## Task
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ every game's update loop.
|
||||
### Why `delta`?
|
||||
|
||||
`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
|
||||
frame* instead of *per millisecond*, your game would run faster on a fast
|
||||
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
|
||||
computer and slower on a slow one.
|
||||
|
||||
By storing velocity as **units-per-millisecond** and multiplying by `delta`, the
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
## Concept
|
||||
|
||||
**AABB** = **A**xis-**A**ligned **B**ounding **B**ox: a rectangle whose sides are
|
||||
parallel to the x and y axes (never rotated). They're cheap to test, which is why
|
||||
almost every 2D engine — including yours — uses them as the base collision shape.
|
||||
**AABB** = **A**xis-**A**ligned **B**ounding **B**ox: a rectangle whose sides
|
||||
are parallel to the x and y axes (never rotated). They're cheap to test, which
|
||||
is why almost every 2D engine — including yours — uses them as the base
|
||||
collision shape.
|
||||
|
||||
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
|
||||
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
|
||||
@@ -32,10 +33,10 @@ behind swept collision.
|
||||
|
||||
### 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
|
||||
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.
|
||||
|
||||
## Task
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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."
|
||||
|
||||
## Concept
|
||||
@@ -9,26 +9,28 @@ Forget 2D. Forget boxes. We have:
|
||||
|
||||
- a **point** sitting at position `p` on a number line,
|
||||
- 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.
|
||||
|
||||
Question: **during this frame, for which `t` is the point inside `[min, max]`?**
|
||||
|
||||
### The slab math
|
||||
|
||||
The point reaches `min` when `p + v*t = min`, i.e. `t = (min - p) / v`.
|
||||
Likewise it reaches `max` at `t = (max - p) / v`.
|
||||
The point reaches `min` when `p + v*t = min`, i.e. `t = (min - p) / v`. Likewise
|
||||
it reaches `max` at `t = (max - p) / v`.
|
||||
|
||||
```
|
||||
t1 = (min - 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
|
||||
**swap them if they're out of order**. Then:
|
||||
|
||||
- `entry` = the time the point *enters* the interval,
|
||||
- `exit` = the time it *leaves*.
|
||||
- `entry` = the time the point _enters_ the interval,
|
||||
- `exit` = the time it _leaves_.
|
||||
|
||||
> 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
|
||||
@@ -37,7 +39,7 @@ If `v` is **negative** (moving left), the point hits `max` *before* `min`, so
|
||||
|
||||
### 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
|
||||
whole frame, or never. So:
|
||||
|
||||
|
||||
@@ -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
|
||||
(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
|
||||
time*.
|
||||
**box** only while it's inside the x-interval **and** the y-interval _at the
|
||||
same time_.
|
||||
|
||||
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
|
||||
*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**:
|
||||
|
||||
```
|
||||
@@ -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:
|
||||
|
||||
- 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`.
|
||||
- You **leave** the box the instant you exit **either** strip → the **earlier**
|
||||
exit → `min`.
|
||||
@@ -36,28 +36,29 @@ Read those two lines until they feel obvious — they're the whole algorithm:
|
||||
### When is there NO hit?
|
||||
|
||||
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
|
||||
strip, then the other, but never both at once. That's the classic "flies past
|
||||
the corner" miss.
|
||||
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
|
||||
this frame.
|
||||
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 this frame.
|
||||
|
||||
### The normal (which wall did we hit?)
|
||||
|
||||
When you do collide, you also want to know **which face** you hit, so the response
|
||||
later can push you back the right way. That's the `normal` — a unit vector
|
||||
pointing out of the surface you struck.
|
||||
When you do collide, you also want to know **which face** you hit, so the
|
||||
response later can push you back the right way. That's the `normal` — a unit
|
||||
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:
|
||||
|
||||
- if `spanX.entry > spanY.entry` → you hit a **vertical** wall (left/right face).
|
||||
The normal is horizontal, pointing back against your x-motion:
|
||||
- if `spanX.entry > spanY.entry` → you hit a **vertical** wall (left/right
|
||||
face). The normal is horizontal, pointing back against your x-motion:
|
||||
`normal = { x: v.x > 0 ? -1 : 1, y: 0 }`.
|
||||
- otherwise → you hit a **horizontal** wall (top/bottom). The normal is vertical:
|
||||
`normal = { x: 0, y: v.y > 0 ? -1 : 1 }`.
|
||||
- otherwise → you hit a **horizontal** wall (top/bottom). The normal is
|
||||
vertical: `normal = { x: 0, y: v.y > 0 ? -1 : 1 }`.
|
||||
|
||||
(Moving right and hitting something → the surface pushes you left → normal `-1`.
|
||||
That sign rule is all there is to it.)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Step 07 — Swept AABB (the Minkowski trick)
|
||||
|
||||
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"
|
||||
into "moving point vs box" so you can reuse step 06 *unchanged*. That conversion
|
||||
is the single cleverest idea in the whole engine.
|
||||
moves is a **box** (the player), not a point. This step turns "moving box vs
|
||||
box" into "moving point vs box" so you can reuse step 06 _unchanged_. That
|
||||
conversion is the single cleverest idea in the whole engine.
|
||||
|
||||
## The problem
|
||||
|
||||
Box A (the player) sits at corner `(a.x, a.y)` with size `a.w × a.h`, and moves by
|
||||
`v` this frame. Box B (a wall) is static. When do they touch?
|
||||
Box A (the player) sits at corner `(a.x, a.y)` with size `a.w × a.h`, and moves
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
Rearrange the second one (`b.x - a.w < a.x`) and you get a statement purely about
|
||||
**`a.x`**, the corner of A:
|
||||
Rearrange the second one (`b.x - a.w < a.x`) and you get a statement purely
|
||||
about **`a.x`**, the corner of A:
|
||||
|
||||
```
|
||||
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.
|
||||
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
|
||||
```
|
||||
|
||||
This grown box is the **Minkowski sum** of B with A. And "does this point, moving
|
||||
by `v`, hit `inflated`?" is *exactly* `rayVsAABB` from step 06. You're done in
|
||||
three lines.
|
||||
This grown box is the **Minkowski sum** of B with A. And "does this point,
|
||||
moving by `v`, hit `inflated`?" is _exactly_ `rayVsAABB` from step 06. You're
|
||||
done in three lines.
|
||||
|
||||
> Sanity picture: player box 2 wide with its right edge at x=2, wall left edge at
|
||||
> x=5 → real gap is 3. Inflate: `inflated.x = 5 - 2 = 3`, and the player's corner
|
||||
> sits at x=0, so the corner-to-inflated-edge gap is also 3. Same answer, simpler
|
||||
> shape. The inflation *bakes A's size into the wall* so the corner can pretend to
|
||||
> be a point.
|
||||
> Sanity picture: player box 2 wide with its right edge at x=2, wall left edge
|
||||
> at x=5 → real gap is 3. Inflate: `inflated.x = 5 - 2 = 3`, and the player's
|
||||
> corner sits at x=0, so the corner-to-inflated-edge gap is also 3. Same answer,
|
||||
> simpler shape. The inflation _bakes A's size into the wall_ so the corner can
|
||||
> pretend to be a point.
|
||||
|
||||
This is the heart of your real engine's `sweptAABB` — the `inflAABB` it builds is
|
||||
this very inflated box, and `(ax, ay)` is this corner point.
|
||||
This is the heart of your real engine's `sweptAABB` — the `inflAABB` it builds
|
||||
is this very inflated box, and `(ax, ay)` is this corner point.
|
||||
|
||||
## Task
|
||||
|
||||
|
||||
@@ -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
|
||||
half is almost embarrassingly small, but it introduces the idea the whole loop
|
||||
(step 10) is built on: **the frame isn't all-or-nothing.**
|
||||
Detection is done. Now for **response** — actually reacting to the hit. This
|
||||
first half is almost embarrassingly small, but it introduces the idea the whole
|
||||
loop (step 10) is built on: **the frame isn't all-or-nothing.**
|
||||
|
||||
## Concept
|
||||
|
||||
`sweptAABB` hands you a `Hit` with a `time` between 0 and 1 — the fraction of 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
|
||||
*before* impact:
|
||||
`sweptAABB` hands you a `Hit` with a `time` between 0 and 1 — the fraction of
|
||||
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 _before_ impact:
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
exactly why sliding (step 09) and the loop (step 10) exist. A collision doesn't
|
||||
end the frame; it **interrupts** it.
|
||||
moving for the rest of the frame, just not _into_ the wall. That leftover time
|
||||
is exactly why sliding (step 09) and the loop (step 10) exist. A collision
|
||||
doesn't end the frame; it **interrupts** it.
|
||||
|
||||
> 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
|
||||
> the wall, where the next frame's sweep would start already-overlapping. Your
|
||||
> `nage` does this with `Math.max(0, time - EPSILON)`. We keep the kata exact so
|
||||
> the numbers stay clean — just know that tiny backoff is there for a real reason.
|
||||
> _short_ of contact) so floating-point error can't leave the box a sliver
|
||||
> inside the wall, where the next frame's sweep would start already-overlapping.
|
||||
> Your `nage` does this with `Math.max(0, time - EPSILON)`. We keep the kata
|
||||
> exact so the numbers stay clean — just know that tiny backoff is there for a
|
||||
> real reason.
|
||||
|
||||
## Task
|
||||
|
||||
Implement `resolve(pos, v, hit)` in `resolve.ts`: return the full move when `hit`
|
||||
is `null`, otherwise the position at contact. `vec/add/scale` and the `Hit` type
|
||||
are in `given.ts`.
|
||||
Implement `resolve(pos, v, hit)` in `resolve.ts`: return the full move when
|
||||
`hit` is `null`, otherwise the position at contact. `vec/add/scale` and the
|
||||
`Hit` type are in `given.ts`.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/08-resolve
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
# 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
|
||||
later. Sliding is the difference between a game that feels good and one where you
|
||||
stick to every wall like glue.
|
||||
later. Sliding is the difference between a game that feels good and one where
|
||||
you stick to every wall like glue.
|
||||
|
||||
## The problem
|
||||
|
||||
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
|
||||
into a wall diagonally and all motion dies, even the part that was parallel to
|
||||
the 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
|
||||
If you just _stop_ (velocity → 0), the player jams against the wall — press into
|
||||
a wall diagonally and all motion dies, even the part that was parallel to the
|
||||
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
|
||||
slide.
|
||||
|
||||
## The math (projection)
|
||||
|
||||
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 wall surface** (perpendicular to the normal) — this is fine.
|
||||
- the part **along the normal** (into/out of the wall) — this is what the wall
|
||||
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
|
||||
`dot(v, n)`. That single number is "how much of `v` goes straight into the wall."
|
||||
The vector piece pointing into the wall is `n * dot(v, n)`. Subtract it off:
|
||||
Because `n` is a **unit vector**, the amount of `v` pointing along `n` is
|
||||
exactly `dot(v, n)`. That single number is "how much of `v` goes straight into
|
||||
the wall." The vector piece pointing into the wall is `n * dot(v, n)`. Subtract
|
||||
it off:
|
||||
|
||||
```
|
||||
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
|
||||
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
|
||||
into the wall survives.)
|
||||
share a direction. Subtract the shared-with-the-normal part, and nothing
|
||||
pointing into the wall survives.)
|
||||
|
||||
### Feel it with numbers
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@ give you the skeleton — you write the code.
|
||||
|
||||
## The idea
|
||||
|
||||
A single collision doesn't end the frame (step 08's "leftover that matters"). You
|
||||
hit a wall at `t = 0.3`, slide, and **70% of the frame is still owed** — during
|
||||
which you might hit *another* wall, slide again, and so on. So moving is a small
|
||||
**loop**: sweep → stop at the nearest hit → slide → repeat with the leftover time.
|
||||
A single collision doesn't end the frame (step 08's "leftover that matters").
|
||||
You hit a wall at `t = 0.3`, slide, and **70% of the frame is still owed** —
|
||||
during which you might hit _another_ wall, slide again, and so on. So moving is
|
||||
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
|
||||
(hit a wall, slide, hit the perpendicular wall, slide, stop) without ever risking
|
||||
an infinite loop.
|
||||
(hit a wall, slide, hit the perpendicular wall, slide, stop) without ever
|
||||
risking an infinite loop.
|
||||
|
||||
## The algorithm
|
||||
|
||||
@@ -46,29 +47,32 @@ return pos
|
||||
|
||||
Two things worth understanding, not just copying:
|
||||
|
||||
- **`move = vel * timeLeft`.** `vel` is a *full-frame* displacement (how far you'd
|
||||
go in a whole frame at this velocity). You only have `timeLeft` of the frame
|
||||
left, so the actual travel is `vel * timeLeft`. `sweptAABB`'s returned `time` is
|
||||
then a fraction *of that sub-move*, which is why `pos + move * time` is correct.
|
||||
- **`move = vel * timeLeft`.** `vel` is a _full-frame_ displacement (how far
|
||||
you'd go in a whole frame at this velocity). You only have `timeLeft` of the
|
||||
frame left, so the actual travel is `vel * timeLeft`. `sweptAABB`'s returned
|
||||
`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 wall. If you land exactly on it, floating-point error can leave you a sliver
|
||||
inside — and next iteration's sweep would start already-overlapping, reporting a
|
||||
garbage negative-time "collision" that makes you stick or jitter. That tiny gap
|
||||
is exactly the `Math.max(0, time - EPSILON)` in your real `moveAndSlide`. Now you
|
||||
know *why* it's there. `EPSILON` is provided in `given.ts`.
|
||||
- **The `EPSILON` backoff** (`max(0, hit.time - EPSILON)`). Stop a hair _short_
|
||||
of the wall. If you land exactly on it, floating-point error can leave you a
|
||||
sliver inside — and next iteration's sweep would start already-overlapping,
|
||||
reporting a garbage negative-time "collision" that makes you stick or jitter.
|
||||
That tiny gap is exactly the `Math.max(0, time - EPSILON)` in your real
|
||||
`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
|
||||
> walls are static, so we sweep with plain `vel`. When the *other* body also 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
|
||||
> point of view it's standing still. That's the only difference between this kata
|
||||
> and the full engine. The loop itself doesn't change.
|
||||
> **Moving-vs-moving (why your real engine has `velocity - otherVel`).** Here
|
||||
> the walls are static, so we sweep with plain `vel`. When the _other_ body also
|
||||
> 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 point of view it's standing still. That's the only difference between
|
||||
> this kata and the full engine. The loop itself doesn't change.
|
||||
|
||||
## Task
|
||||
|
||||
Implement `moveAndSlide(box, v, walls)` in `moveAndSlide.ts`. Everything you need —
|
||||
`sweptAABB`, `slide`, the vector ops, `EPSILON` — is finished in `given.ts`.
|
||||
Implement `moveAndSlide(box, v, walls)` in `moveAndSlide.ts`. Everything you
|
||||
need — `sweptAABB`, `slide`, the vector ops, `EPSILON` — is finished in
|
||||
`given.ts`.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/10-move-and-slide
|
||||
|
||||
@@ -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
|
||||
bar. Push diagonally into a wall and watch it **slide** along instead of sticking.
|
||||
That sliding is your step-09 `dot`-product projection. The fact that it stops
|
||||
*at* the wall instead of tunneling through, even at speed, is your step-07 swept
|
||||
detection. The clean corners are your step-10 loop running twice in one frame.
|
||||
bar. Push diagonally into a wall and watch it **slide** along instead of
|
||||
sticking. That sliding is your step-09 `dot`-product projection. The fact that
|
||||
it stops _at_ the wall instead of tunneling through, even at speed, is your
|
||||
step-07 swept detection. The clean corners are your step-10 loop running twice
|
||||
in one frame.
|
||||
|
||||
## Make it yours (optional)
|
||||
|
||||
- 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
|
||||
identical (it will).
|
||||
what you wrote. Swap in your own `moveAndSlide` from step 10 and check it
|
||||
feels identical (it will).
|
||||
- Add a wall to the `walls` array. Change `SPEED`. Make the player bigger.
|
||||
- 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
|
||||
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
|
||||
|
||||
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
|
||||
can play. Every rung is a function that exists, by name, inside your real
|
||||
Minkowski → detecting the hit → stopping and sliding → the full loop → a thing
|
||||
you can play. Every rung is a function that exists, by name, inside your real
|
||||
`engine/system/physics.ts`.
|
||||
|
||||
Now go open the real `sweptAABB` with fresh eyes. You know exactly what every line
|
||||
is *supposed* to do — so the two lines that don't should stand out. Happy hunting.
|
||||
Now go open the real `sweptAABB` with fresh eyes. You know exactly what every
|
||||
line is _supposed_ to do — so the two lines that don't should stand out. Happy
|
||||
hunting.
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
> 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.
|
||||
|
||||
## Why the sweep can't see it
|
||||
|
||||
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
|
||||
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
|
||||
future — that you start the frame **outside**.
|
||||
|
||||
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
|
||||
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):
|
||||
|
||||
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.)
|
||||
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
|
||||
you `slide` against that normal? And what does `timeLeft = timeLeft * (1 - time)`
|
||||
do when `time` is negative — shrink, or *grow*?
|
||||
you `slide` against that normal? And what does
|
||||
`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
|
||||
`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
|
||||
does `timeLeft` become after multiplying by `(1 - entry)`?
|
||||
4. Last link. In JavaScript, what is `0 * Infinity`? That's `scale(vel, timeLeft)`
|
||||
on iteration three. And once one `NaN` exists, every comparison against it is
|
||||
`false` — so which branch of the loop does the poisoned move fall into, and
|
||||
what does `pos = add(pos, move)` do then?
|
||||
4. Last link. In JavaScript, what is `0 * Infinity`? That's
|
||||
`scale(vel, timeLeft)` on iteration three. And once one `NaN` exists, every
|
||||
comparison against it is `false` — so which branch of the loop does the
|
||||
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 →
|
||||
`0 × ∞`**. When you can retell that chain from memory, you own it.
|
||||
|
||||
## The fix: measure the overlap, push out
|
||||
|
||||
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,
|
||||
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,
|
||||
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
|
||||
translation vector*). For two overlapping AABBs there are exactly four escapes —
|
||||
That shortest-way-out is the **penetration vector** (the famous _minimum
|
||||
translation vector_). For two overlapping AABBs there are exactly four escapes —
|
||||
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
|
||||
`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
|
||||
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
|
||||
`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.
|
||||
|
||||
## Task
|
||||
@@ -92,7 +93,7 @@ Two functions in `overlap.ts`:
|
||||
boxes, or `null` if they don't strictly overlap.
|
||||
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**
|
||||
(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`
|
||||
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
|
||||
`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
|
||||
about: does your *real* engine survive the same experiment?
|
||||
about: does your _real_ engine survive the same experiment?
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
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
|
||||
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
|
||||
up inside a wall again in the very same frame*.
|
||||
last time is unchanged — the only new thing is _how a box you just freed ends up
|
||||
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.
|
||||
|
||||
## Why one pass isn't enough
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
`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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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:
|
||||
crushed = death. Some engines let the wall shove you *through* its partner.
|
||||
every game answers it differently. Mario between a Thwomp and the floor: crushed
|
||||
= death. Some engines let the wall shove you _through_ its partner.
|
||||
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:
|
||||
**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.
|
||||
|
||||
Last session you proposed armoring `sweepInterval` against the `Infinity`
|
||||
directly. You can — see the optional section — but notice what that answer
|
||||
skips over: even with the `NaN` gone, *what should a crushed hero do?* The
|
||||
kernel can't know; it only measures. Deciding is the resolver's job. Keeping
|
||||
directly. You can — see the optional section — but notice what that answer skips
|
||||
over: even with the `NaN` gone, _what should a crushed hero do?_ The kernel
|
||||
can't know; it only measures. Deciding is the resolver's job. Keeping
|
||||
**detection** and **policy** separate is the actual lesson of this step.
|
||||
|
||||
## Task
|
||||
@@ -67,8 +67,8 @@ Two functions in `crush.ts`:
|
||||
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
|
||||
`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
|
||||
to look away.)
|
||||
"bonus" question last session — it's still open, and one of the tests refuses to
|
||||
look away.)
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/13-crush
|
||||
@@ -76,13 +76,13 @@ bun test workshop/steps/13-crush
|
||||
|
||||
## Optional 1 — the airbag
|
||||
|
||||
Defense in depth: even if some future caller hands `moveAndSlide` an
|
||||
overlapping box directly, it should return finite numbers — wrong-ish, maybe,
|
||||
but *finite*. You traced in step 12 exactly which value poisons the well. The
|
||||
loop already clamps it once (`Math.max(0, …)`) — find the **other** line that
|
||||
trusts `nearest.time` to be non-negative. The fix is almost nothing. Then write
|
||||
the test step 12 should have had: `moveAndSlide` (not `safe…`) with an
|
||||
overlapping start returns finite coordinates.
|
||||
Defense in depth: even if some future caller hands `moveAndSlide` an overlapping
|
||||
box directly, it should return finite numbers — wrong-ish, maybe, but _finite_.
|
||||
You traced in step 12 exactly which value poisons the well. The loop already
|
||||
clamps it once (`Math.max(0, …)`) — find the **other** line that trusts
|
||||
`nearest.time` to be non-negative. The fix is almost nothing. Then write the
|
||||
test step 12 should have had: `moveAndSlide` (not `safe…`) with an overlapping
|
||||
start returns finite coordinates.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user