From e96188795398bc86ef83a60ae550c91ef7089746 Mon Sep 17 00:00:00 2001 From: Schluffe Date: Fri, 4 Sep 2026 13:57:52 +0200 Subject: [PATCH] WIP --- .gitea/workflows/ci.yaml | 26 +---- bench/index.ts | 17 ---- bun.lock | 101 ++++++++----------- engine/dev/main.tsx | 4 +- engine/lib/RingBuffer.test.ts | 2 +- engine/lib/RingBuffer.ts | 5 +- engine/lib/missingTexture.svg | 6 +- index.html | 18 ++-- package.json | 6 +- workshop/README.md | 56 +++++++---- workshop/steps/01-vectors/README.md | 5 +- workshop/steps/02-vectors-length/README.md | 11 ++- workshop/steps/03-integration/README.md | 4 +- workshop/steps/04-aabb/README.md | 13 +-- workshop/steps/05-sweep-1d/README.md | 18 ++-- workshop/steps/06-ray-vs-aabb/README.md | 33 ++++--- workshop/steps/07-swept-aabb/README.md | 38 +++---- workshop/steps/08-resolve/README.md | 37 +++---- workshop/steps/09-slide/README.md | 29 +++--- workshop/steps/10-move-and-slide/README.md | 52 +++++----- workshop/steps/11-capstone/README.md | 24 ++--- workshop/steps/11-capstone/index.html | 110 ++++++++++----------- workshop/steps/12-overlap/README.md | 43 ++++---- workshop/steps/13-crush/README.md | 42 ++++---- 24 files changed, 340 insertions(+), 360 deletions(-) delete mode 100644 bench/index.ts diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 6315415..6fe47ee 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -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 \ No newline at end of file diff --git a/bench/index.ts b/bench/index.ts deleted file mode 100644 index 73942bf..0000000 --- a/bench/index.ts +++ /dev/null @@ -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()); diff --git a/bun.lock b/bun.lock index f86f15f..b3a7ea1 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/engine/dev/main.tsx b/engine/dev/main.tsx index bd2ff60..f2d1f34 100644 --- a/engine/dev/main.tsx +++ b/engine/dev/main.tsx @@ -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(); diff --git a/engine/lib/RingBuffer.test.ts b/engine/lib/RingBuffer.test.ts index a2ff66c..c412ec2 100644 --- a/engine/lib/RingBuffer.test.ts +++ b/engine/lib/RingBuffer.test.ts @@ -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); diff --git a/engine/lib/RingBuffer.ts b/engine/lib/RingBuffer.ts index 023a334..ad729f6 100644 --- a/engine/lib/RingBuffer.ts +++ b/engine/lib/RingBuffer.ts @@ -26,9 +26,10 @@ export function createRingBuffer( }; } -export function claim(ring: RingBuffer, reset: (instance: T) => T): T { +export function claim(ring: RingBuffer, 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; } diff --git a/engine/lib/missingTexture.svg b/engine/lib/missingTexture.svg index 086052d..f1b9d06 100644 --- a/engine/lib/missingTexture.svg +++ b/engine/lib/missingTexture.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/index.html b/index.html index 67361d9..4e57602 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,12 @@ - - - - - - - nage - - + + + + + + + nage + + diff --git a/package.json b/package.json index 773c630..1b74748 100644 --- a/package.json +++ b/package.json @@ -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": { "#/": "./*" diff --git a/workshop/README.md b/workshop/README.md index 9f161ff..97dc568 100644 --- a/workshop/README.md +++ b/workshop/README.md @@ -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. diff --git a/workshop/steps/01-vectors/README.md b/workshop/steps/01-vectors/README.md index 0c55e53..2ddf8b4 100644 --- a/workshop/steps/01-vectors/README.md +++ b/workshop/steps/01-vectors/README.md @@ -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 diff --git a/workshop/steps/02-vectors-length/README.md b/workshop/steps/02-vectors-length/README.md index 6490077..7e7e591 100644 --- a/workshop/steps/02-vectors-length/README.md +++ b/workshop/steps/02-vectors-length/README.md @@ -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 diff --git a/workshop/steps/03-integration/README.md b/workshop/steps/03-integration/README.md index 9cf099f..4b4f2b0 100644 --- a/workshop/steps/03-integration/README.md +++ b/workshop/steps/03-integration/README.md @@ -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 diff --git a/workshop/steps/04-aabb/README.md b/workshop/steps/04-aabb/README.md index a2cb2f7..7f4d182 100644 --- a/workshop/steps/04-aabb/README.md +++ b/workshop/steps/04-aabb/README.md @@ -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 diff --git a/workshop/steps/05-sweep-1d/README.md b/workshop/steps/05-sweep-1d/README.md index 7f7f4cd..d2a1f33 100644 --- a/workshop/steps/05-sweep-1d/README.md +++ b/workshop/steps/05-sweep-1d/README.md @@ -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: diff --git a/workshop/steps/06-ray-vs-aabb/README.md b/workshop/steps/06-ray-vs-aabb/README.md index c7c56b9..b83e534 100644 --- a/workshop/steps/06-ray-vs-aabb/README.md +++ b/workshop/steps/06-ray-vs-aabb/README.md @@ -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.) diff --git a/workshop/steps/07-swept-aabb/README.md b/workshop/steps/07-swept-aabb/README.md index cf6e1d3..694c8c6 100644 --- a/workshop/steps/07-swept-aabb/README.md +++ b/workshop/steps/07-swept-aabb/README.md @@ -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 diff --git a/workshop/steps/08-resolve/README.md b/workshop/steps/08-resolve/README.md index e7d6530..fd99a7e 100644 --- a/workshop/steps/08-resolve/README.md +++ b/workshop/steps/08-resolve/README.md @@ -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 diff --git a/workshop/steps/09-slide/README.md b/workshop/steps/09-slide/README.md index 6d83a0a..59e707c 100644 --- a/workshop/steps/09-slide/README.md +++ b/workshop/steps/09-slide/README.md @@ -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 diff --git a/workshop/steps/10-move-and-slide/README.md b/workshop/steps/10-move-and-slide/README.md index ebc5274..0b36382 100644 --- a/workshop/steps/10-move-and-slide/README.md +++ b/workshop/steps/10-move-and-slide/README.md @@ -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 diff --git a/workshop/steps/11-capstone/README.md b/workshop/steps/11-capstone/README.md index 71c0d3e..1d3fd9e 100644 --- a/workshop/steps/11-capstone/README.md +++ b/workshop/steps/11-capstone/README.md @@ -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. diff --git a/workshop/steps/11-capstone/index.html b/workshop/steps/11-capstone/index.html index 79ec1a1..15b13c8 100644 --- a/workshop/steps/11-capstone/index.html +++ b/workshop/steps/11-capstone/index.html @@ -1,61 +1,61 @@ - - - - nage physics kernel — capstone - - - -
-

your swept-AABB kernel, live

- -

+ + + + nage physics kernel — capstone + + + +

+

your swept-AABB kernel, live

+ +

↑ ↓ ← → to move — run into the walls and feel it slide

-
- - +
+ + diff --git a/workshop/steps/12-overlap/README.md b/workshop/steps/12-overlap/README.md index 43d0c64..7800502 100644 --- a/workshop/steps/12-overlap/README.md +++ b/workshop/steps/12-overlap/README.md @@ -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? diff --git a/workshop/steps/13-crush/README.md b/workshop/steps/13-crush/README.md index 1a012cd..7cf1d9a 100644 --- a/workshop/steps/13-crush/README.md +++ b/workshop/steps/13-crush/README.md @@ -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.