ehm
This commit is contained in:
@@ -52,29 +52,29 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.5.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.3", "@biomejs/cli-darwin-x64": "2.5.3", "@biomejs/cli-linux-arm64": "2.5.3", "@biomejs/cli-linux-arm64-musl": "2.5.3", "@biomejs/cli-linux-x64": "2.5.3", "@biomejs/cli-linux-x64-musl": "2.5.3", "@biomejs/cli-win32-arm64": "2.5.3", "@biomejs/cli-win32-x64": "2.5.3" }, "bin": { "biome": "bin/biome" } }, "sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A=="],
|
"@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA=="],
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
"@emnapi/core": ["@emnapi/core@2.0.0-alpha.3", "", { "dependencies": { "@emnapi/wasi-threads": "2.0.1", "tslib": "^2.4.0" } }, "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
"@emnapi/runtime": ["@emnapi/runtime@2.0.0-alpha.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA=="],
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@2.0.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
@@ -86,39 +86,39 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.1", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" } }, "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
|
"@oxc-project/types": ["@oxc-project/types@0.142.0", "", {}, "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A=="],
|
||||||
|
|
||||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.1", "", { "os": "none", "cpu": "arm64" }, "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw=="],
|
||||||
|
|
||||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.1", "", { "dependencies": { "@emnapi/core": "2.0.0-alpha.3", "@emnapi/runtime": "2.0.0-alpha.3", "@napi-rs/wasm-runtime": "^1.2.0" } }, "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
@@ -218,29 +218,29 @@
|
|||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||||
|
|
||||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||||
|
|
||||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||||
|
|
||||||
@@ -260,9 +260,9 @@
|
|||||||
|
|
||||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
|
"postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
|
"rolldown": ["rolldown@1.2.1", "", { "dependencies": { "@oxc-project/types": "=0.142.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.1", "@rolldown/binding-darwin-arm64": "1.2.1", "@rolldown/binding-darwin-x64": "1.2.1", "@rolldown/binding-freebsd-x64": "1.2.1", "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", "@rolldown/binding-linux-arm64-gnu": "1.2.1", "@rolldown/binding-linux-arm64-musl": "1.2.1", "@rolldown/binding-linux-ppc64-gnu": "1.2.1", "@rolldown/binding-linux-s390x-gnu": "1.2.1", "@rolldown/binding-linux-x64-gnu": "1.2.1", "@rolldown/binding-linux-x64-musl": "1.2.1", "@rolldown/binding-openharmony-arm64": "1.2.1", "@rolldown/binding-wasm32-wasi": "1.2.1", "@rolldown/binding-win32-arm64-msvc": "1.2.1", "@rolldown/binding-win32-x64-msvc": "1.2.1" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw=="],
|
||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
@@ -286,9 +286,9 @@
|
|||||||
|
|
||||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
|
"vite": ["vite@8.2.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.23", "rolldown": "~1.2.0", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ=="],
|
||||||
|
|
||||||
"vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
|
"vite-plugin-solid": ["vite-plugin-solid@2.11.14", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
||||||
|
|
||||||
|
|||||||
+37
-18
@@ -154,26 +154,45 @@ function sweptAABB(
|
|||||||
const vx = velocityA.x * delta; // scale x by delta for collider a
|
const vx = velocityA.x * delta; // scale x by delta for collider a
|
||||||
const vy = velocityA.y * delta; // scale y by delta for collider a
|
const vy = velocityA.y * delta; // scale y by delta for collider a
|
||||||
|
|
||||||
let txNear = (inflAABB.x - ax) / vx;
|
// setup interval for x
|
||||||
let txFar = (inflAABB.x + inflAABB.width - ax) / vx;
|
const minX = inflAABB.x;
|
||||||
|
const maxX = inflAABB.x + inflAABB.width;
|
||||||
|
const inRangeX = minX <= ax && ax <= maxX;
|
||||||
|
let txNear = inRangeX ? -Infinity : null;
|
||||||
|
let txFar = inRangeX ? Infinity : null;
|
||||||
|
|
||||||
let tyNear = (inflAABB.y - ay) / vy;
|
if (vx !== 0) {
|
||||||
let tyFar = (inflAABB.y + inflAABB.height - ay) / vy;
|
// setup the span for x
|
||||||
|
txNear = (minX - ax) / vx;
|
||||||
if (txNear > txFar) {
|
txFar = (maxX - ax) / vx;
|
||||||
[txNear, txFar] = [txFar, txNear];
|
if (txNear > txFar) {
|
||||||
}
|
[txNear, txFar] = [txFar, txNear];
|
||||||
if (tyNear > tyFar) {
|
}
|
||||||
[tyNear, tyFar] = [tyFar, tyNear];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// no collision at all
|
// setup interval for y
|
||||||
if (txNear > tyFar || tyNear > txFar) {
|
const minY = inflAABB.y;
|
||||||
|
const maxY = inflAABB.y + inflAABB.height;
|
||||||
|
const inRangeY = minY <= ay && ay <= maxY;
|
||||||
|
let tyNear = inRangeY ? -Infinity : null;
|
||||||
|
let tyFar = inRangeY ? Infinity : null;
|
||||||
|
|
||||||
|
if (vy !== 0) {
|
||||||
|
// setup the span for y
|
||||||
|
tyNear = (minY - ay) / vy;
|
||||||
|
tyFar = (maxY - ay) / vy;
|
||||||
|
if (tyNear > tyFar) {
|
||||||
|
[tyNear, tyFar] = [tyFar, tyNear];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no collision possible
|
||||||
|
if (!inRangeX || !inRangeY) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const thNear = Math.max(txNear, tyNear);
|
const thNear = Math.max(txNear!, tyNear!);
|
||||||
const thFar = Math.min(txFar, tyFar);
|
const thFar = Math.min(txFar!, tyFar!);
|
||||||
|
|
||||||
// thFar < 0, collision is behind a frame
|
// thFar < 0, collision is behind a frame
|
||||||
// thNear > 1, collision is ahead a frame
|
// thNear > 1, collision is ahead a frame
|
||||||
@@ -183,13 +202,12 @@ function sweptAABB(
|
|||||||
|
|
||||||
collision.normal.x = 0;
|
collision.normal.x = 0;
|
||||||
collision.normal.y = 0;
|
collision.normal.y = 0;
|
||||||
if (txNear > tyNear) {
|
if (txNear! > tyNear!) {
|
||||||
collision.normal.x = vx > 0 ? -1 : 1;
|
collision.normal.x = vx > 0 ? -1 : 1;
|
||||||
} else {
|
} else {
|
||||||
collision.normal.y = vy > 0 ? -1 : 1;
|
collision.normal.y = vy > 0 ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// thNear == -Infinity if velocity is 0
|
|
||||||
const time = Math.max(0, thNear);
|
const time = Math.max(0, thNear);
|
||||||
collision.time = time;
|
collision.time = time;
|
||||||
|
|
||||||
@@ -524,7 +542,7 @@ export function moveAndSlide(
|
|||||||
let i = 0;
|
let i = 0;
|
||||||
while (i++ < 4 && timeLeft > 0) {
|
while (i++ < 4 && timeLeft > 0) {
|
||||||
const deltaLeft = delta * timeLeft;
|
const deltaLeft = delta * timeLeft;
|
||||||
const collisions = moveAndCollide(id, velocity, delta);
|
const collisions = moveAndCollide(id, velocity, deltaLeft);
|
||||||
if (collisions.length < 1) {
|
if (collisions.length < 1) {
|
||||||
position.x += velocity.x * deltaLeft;
|
position.x += velocity.x * deltaLeft;
|
||||||
position.y += velocity.y * deltaLeft;
|
position.y += velocity.y * deltaLeft;
|
||||||
@@ -540,7 +558,8 @@ export function moveAndSlide(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const time = closest.time;
|
const time = closest.time;
|
||||||
const scalar = Math.max(0, time - EPSILON) * deltaLeft;
|
// const scalar = Math.max(0, time - EPSILON) * deltaLeft;
|
||||||
|
const scalar = Math.max(0, time - EPSILON) * timeLeft;
|
||||||
position.x += velocity.x * scalar;
|
position.x += velocity.x * scalar;
|
||||||
position.y += velocity.y * scalar;
|
position.y += velocity.y * scalar;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -8,13 +8,13 @@
|
|||||||
"check": "biome check --write"
|
"check": "biome check --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@biomejs/biome": "2.5.3",
|
"@biomejs/biome": "2.5.6",
|
||||||
"@types/bun": "1.3.14",
|
"@types/bun": "1.3.14",
|
||||||
"lz-string": "1.5.0",
|
"lz-string": "1.5.0",
|
||||||
"solid-js": "1.9.14",
|
"solid-js": "1.9.14",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.1.4",
|
"vite": "8.2.0",
|
||||||
"vite-plugin-solid": "2.11.12"
|
"vite-plugin-solid": "2.11.14"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#/": "./*"
|
"#/": "./*"
|
||||||
|
|||||||
@@ -53,5 +53,9 @@ the top rung *is* a working engine.
|
|||||||
- [x] `10-move-and-slide` — the full loop: multiple obstacles, iteration cap (relative velocity explained)
|
- [x] `10-move-and-slide` — the full loop: multiple obstacles, iteration cap (relative velocity explained)
|
||||||
- [x] `11-capstone` — a canvas demo (no test — just run it and play)
|
- [x] `11-capstone` — a canvas demo (no test — just run it and play)
|
||||||
|
|
||||||
|
**Batch 5 — hardening (unlocked by your own capstone finds):**
|
||||||
|
- [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
|
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.
|
back into `sweptAABB` should feel like reading your own handwriting again.
|
||||||
|
|||||||
@@ -133,6 +133,51 @@ function drawBox(b, fill, stroke) {
|
|||||||
ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);
|
ctx.strokeRect(b.x + 0.5, b.y + 0.5, b.w - 1, b.h - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* How to push `a` OUT of `b`, by the smallest possible single-axis move.
|
||||||
|
*
|
||||||
|
* Returns the push as a vector to ADD to a's position, or null if the boxes
|
||||||
|
* don't strictly overlap (merely touching edges is not overlapping).
|
||||||
|
*
|
||||||
|
* There are four ways out (left, right, up, down) — measure all four
|
||||||
|
* distances and return the shortest one. See the README.
|
||||||
|
*/
|
||||||
|
function penetrationVector(a, b) {
|
||||||
|
const outLeft = a.x + a.w - b.x;
|
||||||
|
const outRight = b.x + b.w - a.x;
|
||||||
|
const outUp = a.y + a.h - b.y;
|
||||||
|
const outDown = b.y + b.h - a.y;
|
||||||
|
|
||||||
|
const minX = Math.min(outLeft, outRight);
|
||||||
|
const minY = Math.min(outUp, outDown);
|
||||||
|
|
||||||
|
if (minX > 0 && minY > 0) {
|
||||||
|
return minX > minY
|
||||||
|
? { x: 0, y: outDown > outUp ? -minY : minY }
|
||||||
|
: { x: outLeft > outRight ? minX : -minX, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* moveAndSlide, but immune to the overlap trap: first push `box` out of any
|
||||||
|
* wall it is already inside (plus an EPSILON of slack, same idea as the
|
||||||
|
* backoff in the loop), THEN run the normal sweep loop from the safe spot.
|
||||||
|
*
|
||||||
|
* Everything you need is penetrationVector above and given.ts.
|
||||||
|
*/
|
||||||
|
function safeMoveAndSlide(box, v, walls) {
|
||||||
|
for (const wall of walls) {
|
||||||
|
const pv = penetrationVector(box, wall);
|
||||||
|
if (pv) {
|
||||||
|
box.x += pv.x > 0 ? pv.x + EPSILON : pv.x - EPSILON;
|
||||||
|
box.y += pv.y > 0 ? pv.y + EPSILON : pv.y - EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moveAndSlide(box, v, walls);
|
||||||
|
}
|
||||||
|
|
||||||
let then = performance.now();
|
let then = performance.now();
|
||||||
function frame(now) {
|
function frame(now) {
|
||||||
@@ -156,7 +201,7 @@ function frame(now) {
|
|||||||
|
|
||||||
// full-frame displacement is velocity * delta — exactly what moveAndSlide expects
|
// full-frame displacement is velocity * delta — exactly what moveAndSlide expects
|
||||||
const displacement = scale(velocity, delta);
|
const displacement = scale(velocity, delta);
|
||||||
const next = moveAndSlide(player, displacement, walls);
|
const next = safeMoveAndSlide(player, displacement, walls);
|
||||||
player.x = next.x;
|
player.x = next.x;
|
||||||
player.y = next.y;
|
player.y = next.y;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Step 12 — The overlap trap (when the sweep has no answer)
|
||||||
|
|
||||||
|
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
|
||||||
|
**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
|
||||||
|
> `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
|
||||||
|
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:
|
||||||
|
what sign does `entry` have when `p` is already between `min` and `max`? Every
|
||||||
|
function above `sweepInterval` trusts that number without checking it.
|
||||||
|
|
||||||
|
## The autopsy (do this before reading on)
|
||||||
|
|
||||||
|
The `NaN` isn't born where you see it — it's the end of a chain, and each link
|
||||||
|
is one line you wrote. Reproduce it in a scratch file and trace it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { moveAndSlide, sweptAABB } from "./given.ts";
|
||||||
|
|
||||||
|
const box = { x: 10, y: 10, w: 2, h: 2 };
|
||||||
|
const wall = { x: 0, y: 0, w: 32, h: 32 };
|
||||||
|
console.log(sweptAABB(box, { x: 5, y: 0 }, wall));
|
||||||
|
console.log(moveAndSlide(box, { x: 5, y: 0 }, [wall]));
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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*?
|
||||||
|
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?
|
||||||
|
|
||||||
|
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,
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
outLeft = (a.x + a.w) - b.x // slide a left this far -> separated
|
||||||
|
outRight = (b.x + b.w) - a.x // slide a right this far -> separated
|
||||||
|
outUp = (a.y + a.h) - b.y
|
||||||
|
outDown = (b.y + b.h) - a.y
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
lie to you — one of the tests is exactly that case.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Two functions in `overlap.ts`:
|
||||||
|
|
||||||
|
1. `penetrationVector(a, b)` — the smallest single-axis push that separates the
|
||||||
|
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
|
||||||
|
frame's sweep starts half-trapped again). Then run the given `moveAndSlide`
|
||||||
|
from the safe position.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun test workshop/steps/12-overlap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make the capstone unbreakable (optional)
|
||||||
|
|
||||||
|
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?
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// The whole kernel you've built, steps 01-10, finished. Reuse it.
|
||||||
|
export type Vec = { x: number; y: number };
|
||||||
|
export type AABB = { x: number; y: number; w: number; h: number };
|
||||||
|
export type Span = { entry: number; exit: number };
|
||||||
|
export type Hit = { time: number; normal: Vec };
|
||||||
|
|
||||||
|
/** A hair of slack so we stop just short of a wall instead of inside it. */
|
||||||
|
export const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
export const vec = (x: number, y: number): Vec => ({ x, y });
|
||||||
|
export const add = (a: Vec, b: Vec): Vec => ({ x: a.x + b.x, y: a.y + b.y });
|
||||||
|
export const sub = (a: Vec, b: Vec): Vec => ({ x: a.x - b.x, y: a.y - b.y });
|
||||||
|
export const scale = (a: Vec, s: number): Vec => ({ x: a.x * s, y: a.y * s });
|
||||||
|
export const dot = (a: Vec, b: Vec): number => a.x * b.x + a.y * b.y;
|
||||||
|
|
||||||
|
/** Step 04, finished. Strict overlap — merely touching edges is false. */
|
||||||
|
export function aabbOverlap(a: AABB, b: AABB): boolean {
|
||||||
|
return (
|
||||||
|
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sweepInterval(
|
||||||
|
p: number,
|
||||||
|
v: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): Span | null {
|
||||||
|
if (v === 0) {
|
||||||
|
return min <= p && p <= max ? { entry: -Infinity, exit: Infinity } : null;
|
||||||
|
}
|
||||||
|
let t1 = (min - p) / v;
|
||||||
|
let t2 = (max - p) / v;
|
||||||
|
if (t1 > t2) {
|
||||||
|
[t1, t2] = [t2, t1];
|
||||||
|
}
|
||||||
|
return { entry: t1, exit: t2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rayVsAABB(p: Vec, v: Vec, box: AABB): Hit | null {
|
||||||
|
const spanX = sweepInterval(p.x, v.x, box.x, box.x + box.w);
|
||||||
|
const spanY = sweepInterval(p.y, v.y, box.y, box.y + box.h);
|
||||||
|
if (spanX === null || spanY === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entry = Math.max(spanX.entry, spanY.entry);
|
||||||
|
const exit = Math.min(spanX.exit, spanY.exit);
|
||||||
|
if (entry > exit || entry >= 1 || exit <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normal =
|
||||||
|
spanX.entry > spanY.entry
|
||||||
|
? { x: v.x > 0 ? -1 : 1, y: 0 }
|
||||||
|
: { x: 0, y: v.y > 0 ? -1 : 1 };
|
||||||
|
return { time: entry, normal };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sweptAABB(a: AABB, v: Vec, b: AABB): Hit | null {
|
||||||
|
const inflated: AABB = {
|
||||||
|
x: b.x - a.w,
|
||||||
|
y: b.y - a.h,
|
||||||
|
w: b.w + a.w,
|
||||||
|
h: b.h + a.h,
|
||||||
|
};
|
||||||
|
return rayVsAABB({ x: a.x, y: a.y }, v, inflated);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slide(v: Vec, normal: Vec): Vec {
|
||||||
|
return sub(v, scale(normal, dot(v, normal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Step 10, finished — your loop, exactly as you wrote it. */
|
||||||
|
export function moveAndSlide(box: AABB, v: Vec, walls: AABB[]): Vec {
|
||||||
|
let pos: Vec = { x: box.x, y: box.y };
|
||||||
|
let vel: Vec = { x: v.x, y: v.y };
|
||||||
|
|
||||||
|
let timeLeft = 1;
|
||||||
|
let i = 0;
|
||||||
|
while (i++ < 4 && timeLeft > 0) {
|
||||||
|
const move = scale(vel, timeLeft);
|
||||||
|
let nearest: Hit = {
|
||||||
|
normal: { x: 0, y: 0 },
|
||||||
|
time: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const wall of walls) {
|
||||||
|
const hit = sweptAABB(
|
||||||
|
{
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
w: box.w,
|
||||||
|
h: box.h,
|
||||||
|
},
|
||||||
|
move,
|
||||||
|
wall,
|
||||||
|
);
|
||||||
|
if (hit !== null) {
|
||||||
|
nearest = hit.time < nearest.time ? hit : nearest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearest.time === 2) {
|
||||||
|
pos = add(pos, move);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
pos = add(pos, scale(move, Math.max(0, nearest.time - EPSILON)));
|
||||||
|
vel = slide(vel, nearest.normal);
|
||||||
|
timeLeft = timeLeft * (1 - nearest.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { type AABB, aabbOverlap, vec } from "./given.ts";
|
||||||
|
import { penetrationVector, safeMoveAndSlide } from "./overlap.ts";
|
||||||
|
|
||||||
|
// --- penetrationVector -------------------------------------------------------
|
||||||
|
|
||||||
|
test("separated boxes -> null", () => {
|
||||||
|
const a: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||||
|
const b: AABB = { x: 5, y: 5, w: 2, h: 2 };
|
||||||
|
expect(penetrationVector(a, b)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("touching edges (no strict overlap) -> null", () => {
|
||||||
|
const a: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||||
|
const b: AABB = { x: 2, y: 0, w: 2, h: 2 };
|
||||||
|
expect(penetrationVector(a, b)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shallow on x, deep on y -> pushes left (the short way out)", () => {
|
||||||
|
// a pokes 1 unit into b's left side; getting out via y would cost 6
|
||||||
|
const a: AABB = { x: 0, y: 0, w: 4, h: 4 };
|
||||||
|
const b: AABB = { x: 3, y: -2, w: 4, h: 8 };
|
||||||
|
expect(penetrationVector(a, b)).toEqual(vec(-1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mirrored -> pushes right", () => {
|
||||||
|
const a: AABB = { x: 4, y: 0, w: 4, h: 4 };
|
||||||
|
const b: AABB = { x: 1, y: -2, w: 4, h: 8 };
|
||||||
|
expect(penetrationVector(a, b)).toEqual(vec(1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shallow on y -> pushes up", () => {
|
||||||
|
const a: AABB = { x: 0, y: 0, w: 4, h: 4 };
|
||||||
|
const b: AABB = { x: -2, y: 3, w: 8, h: 4 };
|
||||||
|
expect(penetrationVector(a, b)).toEqual(vec(0, -1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fully contained -> still gets out (all the way past the nearest edge)", () => {
|
||||||
|
// a is swallowed by b: the push must clear the whole distance to an edge,
|
||||||
|
// not just the 2-unit "overlap" of the intervals
|
||||||
|
const a: AABB = { x: 10, y: 10, w: 2, h: 2 };
|
||||||
|
const b: AABB = { x: 0, y: 0, w: 32, h: 32 };
|
||||||
|
const p = penetrationVector(a, b);
|
||||||
|
expect(p).not.toBeNull();
|
||||||
|
if (p !== null) {
|
||||||
|
const moved: AABB = { ...a, x: a.x + p.x, y: a.y + p.y };
|
||||||
|
expect(aabbOverlap(moved, b)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- safeMoveAndSlide --------------------------------------------------------
|
||||||
|
|
||||||
|
const player: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||||
|
|
||||||
|
test("no overlap at start -> behaves exactly like moveAndSlide", () => {
|
||||||
|
// step 10's diagonal test: x blocked at 3, y keeps going
|
||||||
|
const walls: AABB[] = [{ x: 5, y: -10, w: 2, h: 40 }];
|
||||||
|
const p = safeMoveAndSlide(player, vec(10, 10), walls);
|
||||||
|
expect(p.x).toBeCloseTo(3);
|
||||||
|
expect(p.y).toBeCloseTo(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("starting inside a wall -> position stays a number", () => {
|
||||||
|
const inside: AABB = { x: 10, y: 10, w: 2, h: 2 };
|
||||||
|
const wall: AABB = { x: 0, y: 0, w: 32, h: 32 };
|
||||||
|
const p = safeMoveAndSlide(inside, vec(5, 0), [wall]);
|
||||||
|
expect(Number.isFinite(p.x)).toBe(true);
|
||||||
|
expect(Number.isFinite(p.y)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("starting inside a wall -> ends free of it", () => {
|
||||||
|
const inside: AABB = { x: 10, y: 10, w: 2, h: 2 };
|
||||||
|
const wall: AABB = { x: 0, y: 0, w: 32, h: 32 };
|
||||||
|
const p = safeMoveAndSlide(inside, vec(5, 0), [wall]);
|
||||||
|
expect(aabbOverlap({ ...inside, x: p.x, y: p.y }, wall)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overlapping and walking AWAY -> actually gets away", () => {
|
||||||
|
// pokes 1 into the wall's left side; pushed out to x≈8, then walks -5
|
||||||
|
const stuck: AABB = { x: 9, y: 0, w: 2, h: 2 };
|
||||||
|
const wall: AABB = { x: 10, y: -5, w: 4, h: 10 };
|
||||||
|
const p = safeMoveAndSlide(stuck, vec(-5, 0), [wall]);
|
||||||
|
expect(p.x).toBeCloseTo(3);
|
||||||
|
expect(p.y).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overlapping and pushing IN -> freed, then held at the wall", () => {
|
||||||
|
// pushed out to x≈8 first; the sweep then stops the rightward move there
|
||||||
|
const stuck: AABB = { x: 9, y: 0, w: 2, h: 2 };
|
||||||
|
const wall: AABB = { x: 10, y: -5, w: 4, h: 10 };
|
||||||
|
const p = safeMoveAndSlide(stuck, vec(5, 0), [wall]);
|
||||||
|
expect(p.x).toBeCloseTo(8);
|
||||||
|
expect(p.y).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overlapping and pushing IN (rtl) -> freed, then held at the wall", () => {
|
||||||
|
const stuck: AABB = { x: 9, y: 0, w: 2, h: 2 };
|
||||||
|
const wall: AABB = { x: 10, y: -5, w: 4, h: 10 };
|
||||||
|
const p = safeMoveAndSlide(stuck, vec(-5, 0), [wall]);
|
||||||
|
expect(p.x).toBeCloseTo(3);
|
||||||
|
expect(p.y).toBeCloseTo(0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { type AABB, EPSILON, moveAndSlide, type Vec } from "./given.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How to push `a` OUT of `b`, by the smallest possible single-axis move.
|
||||||
|
*
|
||||||
|
* Returns the push as a vector to ADD to a's position, or null if the boxes
|
||||||
|
* don't strictly overlap (merely touching edges is not overlapping).
|
||||||
|
*
|
||||||
|
* There are four ways out (left, right, up, down) — measure all four
|
||||||
|
* distances and return the shortest one. See the README.
|
||||||
|
*/
|
||||||
|
export function penetrationVector(a: AABB, b: AABB): Vec | null {
|
||||||
|
const outLeft = a.x + a.w - b.x;
|
||||||
|
const outRight = b.x + b.w - a.x;
|
||||||
|
const outUp = a.y + a.h - b.y;
|
||||||
|
const outDown = b.y + b.h - a.y;
|
||||||
|
|
||||||
|
const minX = Math.min(outLeft, outRight);
|
||||||
|
const minY = Math.min(outUp, outDown);
|
||||||
|
|
||||||
|
if (minX > 0 && minY > 0) {
|
||||||
|
return minX > minY
|
||||||
|
? { x: 0, y: outDown > outUp ? -minY : minY }
|
||||||
|
: { x: outLeft > outRight ? minX : -minX, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* moveAndSlide, but immune to the overlap trap: first push `box` out of any
|
||||||
|
* wall it is already inside (plus an EPSILON of slack, same idea as the
|
||||||
|
* backoff in the loop), THEN run the normal sweep loop from the safe spot.
|
||||||
|
*
|
||||||
|
* Everything you need is penetrationVector above and given.ts.
|
||||||
|
*/
|
||||||
|
export function safeMoveAndSlide(box: AABB, v: Vec, walls: AABB[]): Vec {
|
||||||
|
for (const wall of walls) {
|
||||||
|
const pv = penetrationVector(box, wall);
|
||||||
|
if (pv) {
|
||||||
|
box.x += pv.x > 0 ? pv.x + EPSILON : pv.x - EPSILON;
|
||||||
|
box.y += pv.y > 0 ? pv.y + EPSILON : pv.y - EPSILON;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moveAndSlide(box, v, walls);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Step 13 — The crush (when there is no way out)
|
||||||
|
|
||||||
|
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*.
|
||||||
|
|
||||||
|
> 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
|
||||||
|
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.
|
||||||
|
|
||||||
|
Two experiments in a scratch file, **predicting each outcome before running**
|
||||||
|
(use your step-12 code as-is):
|
||||||
|
|
||||||
|
1. Hero `{x: 9, y: 1, w: 2, h: 2}`, walls `A = {x: 10, y: 0, w: 4, h: 4}` and
|
||||||
|
`B = {x: 4, y: 0, w: 4.5, h: 1.4}`. The hero overlaps only `A`. Run one pass
|
||||||
|
with the array `[A, B]`, then with `[B, A]`. Where does the hero end up in
|
||||||
|
each case, and is it free? Explain the difference before moving on.
|
||||||
|
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*?
|
||||||
|
|
||||||
|
## The negotiation, and when it honestly fails
|
||||||
|
|
||||||
|
The fix for experiment 1 is patience: don't do one pass — **repeat whole passes
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
**detection** and **policy** separate is the actual lesson of this step.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Two functions in `crush.ts`:
|
||||||
|
|
||||||
|
1. `resolveOverlaps(box, walls)` — the negotiation: passes until clean or
|
||||||
|
capped, returning `{x, y, settled}`.
|
||||||
|
2. `safeMoveAndSlide(box, v, walls)` — step 12's version rebuilt on top of it.
|
||||||
|
Settled → sweep as usual. Crushed → **don't feed the sweep an overlapping
|
||||||
|
box** (you know its opinion of those); the box stays where the resolver left
|
||||||
|
it and waits.
|
||||||
|
|
||||||
|
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.)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Optional 2 — the capstone payoff
|
||||||
|
|
||||||
|
Port `resolveOverlaps` into `11-capstone/game.js` and rebuild its
|
||||||
|
`safeMoveAndSlide` on it. Now let the ledge squeeze the hero against the pillar,
|
||||||
|
and against the border. Watch closely: instead of vanishing, the hero should
|
||||||
|
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.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { resolveOverlaps, safeMoveAndSlide } from "./crush.ts";
|
||||||
|
import { type AABB, aabbOverlap, vec } from "./given.ts";
|
||||||
|
|
||||||
|
// --- resolveOverlaps ---------------------------------------------------------
|
||||||
|
|
||||||
|
test("free box -> untouched, settled", () => {
|
||||||
|
const r = resolveOverlaps({ x: 0, y: 0, w: 2, h: 2 }, [
|
||||||
|
{ x: 5, y: 5, w: 2, h: 2 },
|
||||||
|
]);
|
||||||
|
expect(r.settled).toBe(true);
|
||||||
|
expect(r.x).toBe(0);
|
||||||
|
expect(r.y).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("single overlap -> pushed out, settled", () => {
|
||||||
|
const r = resolveOverlaps({ x: 9, y: 0, w: 2, h: 2 }, [
|
||||||
|
{ x: 10, y: -5, w: 4, h: 10 },
|
||||||
|
]);
|
||||||
|
expect(r.settled).toBe(true);
|
||||||
|
expect(r.x).toBeCloseTo(8);
|
||||||
|
// the push was x-only: the axis that needed no fixing must come back
|
||||||
|
// EXACTLY untouched — not "close to" untouched
|
||||||
|
expect(r.y).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// pushed out of A (left) -> lands inside B -> a second push (downward, B is
|
||||||
|
// shallow) frees it. One pass can't see this coming; the negotiation can.
|
||||||
|
const chainA: AABB = { x: 10, y: 0, w: 4, h: 4 };
|
||||||
|
const chainB: AABB = { x: 4, y: 0, w: 4.5, h: 1.4 };
|
||||||
|
|
||||||
|
test("a push that lands you in the NEXT wall still resolves", () => {
|
||||||
|
const hero: AABB = { x: 9, y: 1, w: 2, h: 2 };
|
||||||
|
const r = resolveOverlaps(hero, [chainA, chainB]);
|
||||||
|
expect(r.settled).toBe(true);
|
||||||
|
const moved = { ...hero, x: r.x, y: r.y };
|
||||||
|
expect(aabbOverlap(moved, chainA)).toBe(false);
|
||||||
|
expect(aabbOverlap(moved, chainB)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("...and the answer must not depend on wall order", () => {
|
||||||
|
const hero: AABB = { x: 9, y: 1, w: 2, h: 2 };
|
||||||
|
const r = resolveOverlaps(hero, [chainB, chainA]);
|
||||||
|
expect(r.settled).toBe(true);
|
||||||
|
const moved = { ...hero, x: r.x, y: r.y };
|
||||||
|
expect(aabbOverlap(moved, chainA)).toBe(false);
|
||||||
|
expect(aabbOverlap(moved, chainB)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// the gap between these two walls is 1.5 wide; the hero is 2 wide.
|
||||||
|
// there is NO overlap-free position — the pushes ping-pong forever.
|
||||||
|
const vice: AABB[] = [
|
||||||
|
{ x: 10, y: -5, w: 4, h: 10 },
|
||||||
|
{ x: 6.5, y: -5, w: 2, h: 10 },
|
||||||
|
];
|
||||||
|
|
||||||
|
test("gap narrower than the box -> reports the crush, stays finite", () => {
|
||||||
|
const r = resolveOverlaps({ x: 9, y: 0, w: 2, h: 2 }, vice);
|
||||||
|
expect(r.settled).toBe(false);
|
||||||
|
expect(Number.isFinite(r.x)).toBe(true);
|
||||||
|
expect(Number.isFinite(r.y)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- safeMoveAndSlide --------------------------------------------------------
|
||||||
|
|
||||||
|
test("no overlap at start -> behaves exactly like moveAndSlide", () => {
|
||||||
|
// step 10's diagonal test: x blocked at 3, y keeps going
|
||||||
|
const walls: AABB[] = [{ x: 5, y: -10, w: 2, h: 40 }];
|
||||||
|
const p = safeMoveAndSlide({ x: 0, y: 0, w: 2, h: 2 }, vec(10, 10), walls);
|
||||||
|
expect(p.x).toBeCloseTo(3);
|
||||||
|
expect(p.y).toBeCloseTo(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolvable overlap, then walking away -> freed and gone", () => {
|
||||||
|
const p = safeMoveAndSlide({ x: 9, y: 0, w: 2, h: 2 }, vec(-5, 0), [
|
||||||
|
{ x: 10, y: -5, w: 4, h: 10 },
|
||||||
|
]);
|
||||||
|
expect(p.x).toBeCloseTo(3);
|
||||||
|
expect(p.y).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("crushed with zero velocity -> a number, not a ghost", () => {
|
||||||
|
// the capstone bug: ledge pushes hero into the pillar, hero holds still
|
||||||
|
const p = safeMoveAndSlide({ x: 9, y: 0, w: 2, h: 2 }, vec(0, 0), vice);
|
||||||
|
expect(Number.isFinite(p.x)).toBe(true);
|
||||||
|
expect(Number.isFinite(p.y)).toBe(true);
|
||||||
|
// still pinched between the walls — not flung across the room
|
||||||
|
expect(p.x).toBeGreaterThan(7);
|
||||||
|
expect(p.x).toBeLessThan(9.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("crushed and still pushing -> the walls win", () => {
|
||||||
|
const p = safeMoveAndSlide({ x: 9, y: 0, w: 2, h: 2 }, vec(5, 0), vice);
|
||||||
|
expect(Number.isFinite(p.x)).toBe(true);
|
||||||
|
expect(Number.isFinite(p.y)).toBe(true);
|
||||||
|
expect(p.x).toBeGreaterThan(7);
|
||||||
|
expect(p.x).toBeLessThan(9.5);
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
type AABB,
|
||||||
|
moveAndSlide,
|
||||||
|
penetrationVector,
|
||||||
|
type Vec,
|
||||||
|
} from "./given.ts";
|
||||||
|
|
||||||
|
export type Resolution = { x: number; y: number; settled: boolean };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push `box` out of EVERY wall — even when one push shoves it into another.
|
||||||
|
*
|
||||||
|
* One pass over the walls is not enough: a push is a teleport, and a teleport
|
||||||
|
* can land you inside a wall the loop already cleared. So: run whole passes
|
||||||
|
* over the wall list, applying pushes (with the EPSILON slack — on the pushed
|
||||||
|
* axis only!), until a full pass finds nothing to fix. That pass proves you're
|
||||||
|
* settled.
|
||||||
|
*
|
||||||
|
* But some arrangements have NO free spot (a gap narrower than the box), and
|
||||||
|
* the passes would ping-pong forever. Cap them — 8 is plenty — and if the cap
|
||||||
|
* fires, report `settled: false`. You already believe in caps: your step-10
|
||||||
|
* loop has one for exactly the same reason.
|
||||||
|
*
|
||||||
|
* Returns where the box ended up and whether it truly got free.
|
||||||
|
*/
|
||||||
|
export function resolveOverlaps(box: AABB, walls: AABB[]): Resolution {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 12's safeMoveAndSlide, rebuilt on resolveOverlaps.
|
||||||
|
*
|
||||||
|
* Settled -> sweep from the safe spot, business as usual.
|
||||||
|
* Crushed (not settled) -> the sweep would be handed an overlapping box and
|
||||||
|
* you know exactly what it does with one of those. Don't feed it. Our crush
|
||||||
|
* policy: the walls win — the box stays where the resolver left it, finite,
|
||||||
|
* and waits for the gap to open.
|
||||||
|
*/
|
||||||
|
export function safeMoveAndSlide(box: AABB, v: Vec, walls: AABB[]): Vec {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// The whole kernel you've built, steps 01-10, finished. Reuse it.
|
||||||
|
export type Vec = { x: number; y: number };
|
||||||
|
export type AABB = { x: number; y: number; w: number; h: number };
|
||||||
|
export type Span = { entry: number; exit: number };
|
||||||
|
export type Hit = { time: number; normal: Vec };
|
||||||
|
|
||||||
|
/** A hair of slack so we stop just short of a wall instead of inside it. */
|
||||||
|
export const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
export const vec = (x: number, y: number): Vec => ({ x, y });
|
||||||
|
export const add = (a: Vec, b: Vec): Vec => ({ x: a.x + b.x, y: a.y + b.y });
|
||||||
|
export const sub = (a: Vec, b: Vec): Vec => ({ x: a.x - b.x, y: a.y - b.y });
|
||||||
|
export const scale = (a: Vec, s: number): Vec => ({ x: a.x * s, y: a.y * s });
|
||||||
|
export const dot = (a: Vec, b: Vec): number => a.x * b.x + a.y * b.y;
|
||||||
|
|
||||||
|
/** Step 04, finished. Strict overlap — merely touching edges is false. */
|
||||||
|
export function aabbOverlap(a: AABB, b: AABB): boolean {
|
||||||
|
return (
|
||||||
|
a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sweepInterval(
|
||||||
|
p: number,
|
||||||
|
v: number,
|
||||||
|
min: number,
|
||||||
|
max: number,
|
||||||
|
): Span | null {
|
||||||
|
if (v === 0) {
|
||||||
|
return min <= p && p <= max ? { entry: -Infinity, exit: Infinity } : null;
|
||||||
|
}
|
||||||
|
let t1 = (min - p) / v;
|
||||||
|
let t2 = (max - p) / v;
|
||||||
|
if (t1 > t2) {
|
||||||
|
[t1, t2] = [t2, t1];
|
||||||
|
}
|
||||||
|
return { entry: t1, exit: t2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rayVsAABB(p: Vec, v: Vec, box: AABB): Hit | null {
|
||||||
|
const spanX = sweepInterval(p.x, v.x, box.x, box.x + box.w);
|
||||||
|
const spanY = sweepInterval(p.y, v.y, box.y, box.y + box.h);
|
||||||
|
if (spanX === null || spanY === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const entry = Math.max(spanX.entry, spanY.entry);
|
||||||
|
const exit = Math.min(spanX.exit, spanY.exit);
|
||||||
|
if (entry > exit || entry >= 1 || exit <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normal =
|
||||||
|
spanX.entry > spanY.entry
|
||||||
|
? { x: v.x > 0 ? -1 : 1, y: 0 }
|
||||||
|
: { x: 0, y: v.y > 0 ? -1 : 1 };
|
||||||
|
return { time: entry, normal };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sweptAABB(a: AABB, v: Vec, b: AABB): Hit | null {
|
||||||
|
const inflated: AABB = {
|
||||||
|
x: b.x - a.w,
|
||||||
|
y: b.y - a.h,
|
||||||
|
w: b.w + a.w,
|
||||||
|
h: b.h + a.h,
|
||||||
|
};
|
||||||
|
return rayVsAABB({ x: a.x, y: a.y }, v, inflated);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slide(v: Vec, normal: Vec): Vec {
|
||||||
|
return sub(v, scale(normal, dot(v, normal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Step 10, finished — your loop, exactly as you wrote it. */
|
||||||
|
export function moveAndSlide(box: AABB, v: Vec, walls: AABB[]): Vec {
|
||||||
|
let pos: Vec = { x: box.x, y: box.y };
|
||||||
|
let vel: Vec = { x: v.x, y: v.y };
|
||||||
|
|
||||||
|
let timeLeft = 1;
|
||||||
|
let i = 0;
|
||||||
|
while (i++ < 4 && timeLeft > 0) {
|
||||||
|
const move = scale(vel, timeLeft);
|
||||||
|
let nearest: Hit = {
|
||||||
|
normal: { x: 0, y: 0 },
|
||||||
|
time: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const wall of walls) {
|
||||||
|
const hit = sweptAABB(
|
||||||
|
{
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
w: box.w,
|
||||||
|
h: box.h,
|
||||||
|
},
|
||||||
|
move,
|
||||||
|
wall,
|
||||||
|
);
|
||||||
|
if (hit !== null) {
|
||||||
|
nearest = hit.time < nearest.time ? hit : nearest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearest.time === 2) {
|
||||||
|
pos = add(pos, move);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
pos = add(pos, scale(move, Math.max(0, nearest.time - EPSILON)));
|
||||||
|
vel = slide(vel, nearest.normal);
|
||||||
|
timeLeft = timeLeft * (1 - nearest.time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Step 12, finished — your penetrationVector, exactly as you wrote it. */
|
||||||
|
export function penetrationVector(a: AABB, b: AABB): Vec | null {
|
||||||
|
const outLeft = a.x + a.w - b.x;
|
||||||
|
const outRight = b.x + b.w - a.x;
|
||||||
|
const outUp = a.y + a.h - b.y;
|
||||||
|
const outDown = b.y + b.h - a.y;
|
||||||
|
|
||||||
|
const minX = Math.min(outLeft, outRight);
|
||||||
|
const minY = Math.min(outUp, outDown);
|
||||||
|
|
||||||
|
if (minX > 0 && minY > 0) {
|
||||||
|
return minX > minY
|
||||||
|
? { x: 0, y: outDown > outUp ? -minY : minY }
|
||||||
|
: { x: outLeft > outRight ? minX : -minX, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user