REDROCK MOTO TRIALS — CORRESPONDING SOURCE Modified November 2, 2026. GPL-2.0. Upstream: https://github.com/yurkagon/gravity-defied-web Commit: aad47d0019b7d8feb4d71d1bdbdc9f3a200e243a Reconstruction: create each file at the path shown below. Place all locally served PNG and WebP assets into src/assets/ under the same filenames. Install the locked dev dependencies with npm ci, use Node 22+, and run npm run build. The game has no runtime packages or server requirement. levels.json is an exact byte-for-byte representation of upstream levels.mrg; no separate binary is needed. The source modifications add a canyon background, semantic menus, responsive frame, pause/retry and focus loss handling. Artwork generated using OpenAI's built-in GPT image model; canyon.webp is locally served. ===== FILE: index.html ===== Redrock · Moto Trials

A LITTLE THROTTLE. A LOT OF BALANCE.

Redrockmoto trials

30 TRAILS · 4 BIKES
One ridge at a time.
TRAIL LoadingPreparing the trail…0.00s0%
01

Find your balance

↑ accelerate · ↓ brake
← lean back · → lean forward

02

Read the ridge

Lean into slopes. Ease off before crests.
Keep your rider clear of the ground.

03

Ride a little further

Finish tracks to unlock the next.
Enter selects · Esc pauses · Bests save here.

===== FILE: package-lock.json ===== { "name": "gravity-defied-web", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gravity-defied-web", "version": "0.0.0", "license": "GPL-2.0", "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "cross-env": "^7.0.3", "globals": "^16.5.0", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1" } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openharmony" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "cpu": [ "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "24.12.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" }, "engines": { "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/@typescript-eslint/utils": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" }, "engines": { "node": ">=10.14", "npm": ">=6", "yarn": ">=1" } }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" }, "peerDependencies": { "jiti": "*" }, "peerDependenciesMeta": { "jiti": { "optional": true } } }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, "engines": { "node": ">=0.10" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" }, "peerDependencies": { "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { "picomatch": { "optional": true } } }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" }, "engines": { "node": ">=16" } }, "node_modules/flatted": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.0.tgz", "integrity": "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, "engines": { "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" }, "funding": { "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { "node": ">=18.12" }, "peerDependencies": { "typescript": ">=4.8.4" } }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=14.17" } }, "node_modules/typescript-eslint": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, "jiti": { "optional": true }, "less": { "optional": true }, "lightningcss": { "optional": true }, "sass": { "optional": true }, "sass-embedded": { "optional": true }, "stylus": { "optional": true }, "sugarss": { "optional": true }, "terser": { "optional": true }, "tsx": { "optional": true }, "yaml": { "optional": true } } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } } } } ===== FILE: package.json ===== { "name": "gravity-defied-web", "private": true, "version": "0.0.0", "license": "GPL-2.0", "type": "module", "keywords": [ "gravity-defied", "game", "browser-game", "typescript", "vite", "canvas", "retro-game", "j2me-port" ], "scripts": { "dev": "vite", "build": "tsc -b && vite build", "build:gh-pages": "cross-env GITHUB_PAGES=true vite build", "lint": "eslint .", "preview": "vite preview", "deploy": "npm run build:gh-pages && npx gh-pages -d dist" }, "dependencies": {}, "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "cross-env": "^7.0.3", "globals": "^16.5.0", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1" } } ===== FILE: src/GameCanvas.ts ===== import { MathF16 } from './MathF16.ts' import type { MenuManager } from './MenuManager.ts' import { Micro } from './Micro.ts' import { Timer } from './Timer.ts' import { toInt } from './cpp.ts' import type { GamePhysics } from './GamePhysics.ts' import { Font } from './lcdui/Font.ts' import { FontStorage } from './lcdui/FontStorage.ts' import { Graphics } from './lcdui/Graphics.ts' import { Image } from './lcdui/Image.ts' import SPLASH_URL from './assets/splash.png?url' import LOGO_URL from './assets/logo.png?url' import HELMET_URL from './assets/helmet.png?url' import SPRITES_URL from './assets/sprites.png?url' import BLUEARM_URL from './assets/bluearm.png?url' import BLUELEG_URL from './assets/blueleg.png?url' import BLUEBODY_URL from './assets/bluebody.png?url' import ENGINE_URL from './assets/engine.png?url' import FENDER_URL from './assets/fender.png?url' import CANYON_URL from './assets/canyon.webp?url' type GameCanvasAssetCaches = { splashImage: Image logoImage: Image helmetImage: Image spritesImage: Image bluearmImage: Image bluelegImage: Image bluebodyImage: Image engineImage: Image fenderImage: Image } export class GameCanvas { static readonly spriteOffsetX = [0, 0, 15, 15, 15, 0, 6, 12, 18, 18, 25, 25, 25, 37, 37, 37, 15, 32] static readonly spriteOffsetY = [10, 25, 16, 20, 10, 0, 0, 0, 8, 0, 0, 6, 12, 0, 6, 12, 29, 18] static readonly spriteSizeX = [15, 15, 8, 8, 3, 6, 6, 6, 7, 7, 12, 12, 12, 12, 12, 12, 16, 17] static readonly spriteSizeY = [15, 15, 4, 4, 3, 10, 10, 10, 8, 8, 6, 6, 6, 6, 6, 6, 11, 22] private readonly canvas: HTMLCanvasElement private readonly canyon = new window.Image() private readonly ctx: CanvasRenderingContext2D private readonly graphics: Graphics private readonly micro: Micro private readonly assetCaches: GameCanvasAssetCaches private menuManager: MenuManager | null = null private dx = 0 private dy = 0 private engineSpriteWidth = 0 private engineSpriteHeight = 0 private fenderSpriteWidth = 0 private fenderSpriteHeight = 0 private gamePhysics: GamePhysics | null = null private cameraOffsetX = 0 private cameraOffsetY = 0 private loadingScreenMode = 1 private readonly bodyPartsSpriteWidth = [0, 0, 0] private readonly bodyPartsSpriteHeight = [0, 0, 0] private static defaultFontWidth00 = 25 private timerTriggered = false private screenFont = FontStorage.getFont(Font.STYLE_BOLD, Font.SIZE_MEDIUM) private isUiOverlayEnabled = true private timerMessage = '' private timerId = 0 private readonly timers: Timer[] = [] private isMenuButtonVisible = false private isBackButtonVisible = false private repaintHandler: (() => void) | null = null private zoom = 1 private static stringWithTime = '' private readonly time10MsToStringCache = new Array(100).fill('') private timeInSeconds = -1 private static flagAnimationTime = 0 private static flagAnimationPhase = 0 private readonly startFlagAnimationTimeToSpriteNo = [12, 10, 11, 10] private readonly finishFlagAnumationTimeToSpriteNo = [14, 13, 15, 13] private readonly actionInputDelta = [[0, 0], [1, 0], [0, -1], [0, 0], [0, 0], [0, 1], [-1, 0]] private readonly keyInputDeltaByMode = [ [[0, 0], [1, -1], [1, 0], [1, 1], [0, -1], [-1, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]], [[0, 0], [1, 0], [0, 0], [0, 0], [-1, 0], [0, -1], [0, 1], [0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0], [1, 0], [0, -1], [0, 1], [-1, 0], [0, 0], [0, 0], [0, 0]], ] private inputMode = 2 private readonly activeActions = new Array(7).fill(false) private readonly activeKeys = new Array(10).fill(false) width: number height2: number height: number helmetImage: Image helmetSpriteWidth: number helmetSpriteHeight: number isDrawingTime = true splashImage: Image | null logoImage: Image | null bodyPartsImages: Array = [null, null, null] engineImage: Image | null = null fenderImage: Image | null = null spritesImage: Image static async create(canvas: HTMLCanvasElement, micro: Micro): Promise { const [ splashImage, logoImage, helmetImage, spritesImage, bluearmImage, bluelegImage, bluebodyImage, engineImage, fenderImage, ] = await Promise.all([ Image.load(SPLASH_URL), Image.load(LOGO_URL), Image.load(HELMET_URL), Image.load(SPRITES_URL), Image.load(BLUEARM_URL), Image.load(BLUELEG_URL), Image.load(BLUEBODY_URL), Image.load(ENGINE_URL), Image.load(FENDER_URL), ]) return new GameCanvas(canvas, micro, { splashImage, logoImage, helmetImage, spritesImage, bluearmImage, bluelegImage, bluebodyImage, engineImage, fenderImage, }) } private constructor(canvas: HTMLCanvasElement, micro: Micro, assetCaches: GameCanvasAssetCaches) { const ctx = canvas.getContext('2d') if (ctx === null) { throw new Error('Canvas 2D context is not available') } this.canyon.src = CANYON_URL this.canvas = canvas this.ctx = ctx this.graphics = new Graphics(ctx) this.micro = micro this.assetCaches = assetCaches this.splashImage = assetCaches.splashImage this.logoImage = assetCaches.logoImage this.helmetImage = assetCaches.helmetImage this.spritesImage = assetCaches.spritesImage this.helmetSpriteWidth = this.helmetImage.getWidth() / 6 this.helmetSpriteHeight = this.helmetImage.getHeight() / 6 this.width = canvas.width this.height = canvas.height this.height2 = canvas.height this.dy = this.height2 GameCanvas.defaultFontWidth00 = 25 } resize(width: number, height: number, zoom: number): void { this.canvas.width = width this.canvas.height = height this.zoom = zoom this.width = Math.ceil(width / zoom) this.height = Math.ceil(height / zoom) this.height2 = this.height } getZoom(): number { return this.zoom } getWidth(): number { return this.width } getHeight(): number { return this.height2 } getGraphics(): Graphics { return this.graphics } beginFrame(): void { void this.micro void this.cameraOffsetX void this.cameraOffsetY void this.loadingScreenMode this.ctx.setTransform(1, 0, 0, 1, 0, 0) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) this.ctx.setTransform(this.zoom, 0, 0, this.zoom, 0, 0) this.ctx.imageSmoothingEnabled = false this.processTimers() } drawSprite(g: Graphics, spriteNo: number, x: number, y: number): void { g.setClip(x, y, GameCanvas.spriteSizeX[spriteNo], GameCanvas.spriteSizeY[spriteNo]) g.drawImage( this.spritesImage, x - GameCanvas.spriteOffsetX[spriteNo], y - GameCanvas.spriteOffsetY[spriteNo], Graphics.LEFT | Graphics.TOP, ) g.setClip(0, 0, this.getWidth(), this.getHeight()) } requestRepaint(var1: number): void { this.loadingScreenMode = var1 if (var1 === 0) { this.splashImage = null this.logoImage = null } else { this.repaint() this.serviceRepaints() } } setUiOverlayEnabled(var1: boolean): void { this.isUiOverlayEnabled = var1 void this.isUiOverlayEnabled this.repaint() } init(gamePhysics: GamePhysics): void { this.gamePhysics = gamePhysics gamePhysics.setMinimalScreenWH(this.width < this.height2 ? this.width : this.height2) } async loadSprites(flags: number): Promise { if ((flags & 1) !== 0) { this.fenderImage = this.assetCaches.fenderImage this.engineImage = this.assetCaches.engineImage this.fenderSpriteWidth = this.fenderImage.getWidth() / 6 this.fenderSpriteHeight = this.fenderImage.getHeight() / 6 this.engineSpriteWidth = this.engineImage.getWidth() / 6 this.engineSpriteHeight = this.engineImage.getHeight() / 6 } else { this.fenderImage = null this.engineImage = null } if ((flags & 2) !== 0) { this.bodyPartsImages[1] = this.assetCaches.bluelegImage this.bodyPartsSpriteWidth[1] = this.bodyPartsImages[1].getWidth() / 6 this.bodyPartsSpriteHeight[1] = this.bodyPartsImages[1].getHeight() / 3 this.bodyPartsImages[0] = this.assetCaches.bluearmImage this.bodyPartsSpriteWidth[0] = this.bodyPartsImages[0].getWidth() / 6 this.bodyPartsSpriteHeight[0] = this.bodyPartsImages[0].getHeight() / 3 this.bodyPartsImages[2] = this.assetCaches.bluebodyImage this.bodyPartsSpriteWidth[2] = this.bodyPartsImages[2].getWidth() / 6 this.bodyPartsSpriteHeight[2] = this.bodyPartsImages[2].getHeight() / 3 } else { this.bodyPartsImages[0] = null this.bodyPartsImages[1] = null this.bodyPartsImages[2] = null } return flags } resetInputState(): void { this.clearActiveInputs() } setViewPosition(dx: number, dy: number): void { this.dx = dx this.dy = dy this.gamePhysics?.setRenderMinMaxX(-dx, -dx + this.width) } getDx(): number { return this.dx } addDx(x: number): number { return x + this.dx } addDy(y: number): number { return -y + this.dy } drawLine(x: number, y: number, x2: number, y2: number): void { this.graphics.drawLine(this.addDx(x), this.addDy(y), this.addDx(x2), this.addDy(y2)) } drawLineF16(x: number, y: number, x2: number, y2: number): void { this.graphics.drawLine(this.addDx((x << 2) >> 16), this.addDy((y << 2) >> 16), this.addDx((x2 << 2) >> 16), this.addDy((y2 << 2) >> 16)) } renderBodyPart(x1F16: number, y1F16: number, x2F16: number, y2F16: number, bodyPartNo: number, tF16 = 32768): void { const x = this.addDx((toInt((BigInt(x2F16) * BigInt(tF16)) >> 16n) + toInt((BigInt(x1F16) * BigInt(65536 - tF16)) >> 16n)) >> 16) const y = this.addDy((toInt((BigInt(y2F16) * BigInt(tF16)) >> 16n) + toInt((BigInt(y1F16) * BigInt(65536 - tF16)) >> 16n)) >> 16) const angleFP16 = MathF16.atan2F16(x2F16 - x1F16, y2F16 - y1F16) const spriteNo = this.calcSpriteNo(angleFP16, 0, 205887, 16, false) if (this.bodyPartsImages[bodyPartNo] !== null) { let drawX = x - Math.trunc(this.bodyPartsSpriteWidth[bodyPartNo] / 2) let drawY = y - Math.trunc(this.bodyPartsSpriteHeight[bodyPartNo] / 2) this.graphics.setClip(drawX, drawY, this.bodyPartsSpriteWidth[bodyPartNo], this.bodyPartsSpriteHeight[bodyPartNo]) this.graphics.drawImage( this.bodyPartsImages[bodyPartNo] as Image, drawX - this.bodyPartsSpriteWidth[bodyPartNo] * (spriteNo % 6), drawY - this.bodyPartsSpriteHeight[bodyPartNo] * Math.trunc(spriteNo / 6), Graphics.LEFT | Graphics.TOP, ) this.graphics.setClip(0, 0, this.width, this.getHeight()) drawX = drawX drawY = drawY } } drawWheelArc(var1: number, var2: number, var3: number, var4: number): void { ++var3 const var5 = this.addDx(var1 - var3) const var6 = this.addDy(var2 + var3) const var7 = var3 << 1 let angle = -toInt(((BigInt(toInt((BigInt(var4) * 11796480n) >> 16n)) << 32n) / 205887n) >> 16n) if (angle < 0) { angle += 360 } this.graphics.drawArc(var5, var6, var7, var7, (angle >> 16) + 170, 90) } drawCircle(x: number, y: number, size: number): void { const radius = Math.trunc(size / 2) const localX = this.addDx(x - radius) const localY = this.addDy(y + radius) this.graphics.drawArc(localX, localY, size, size, 0, 360) } fillRect(x: number, y: number, w: number, h: number): void { this.graphics.fillRect(this.addDx(x), this.addDy(y), w, h) } drawForthSpriteByCenter(centerX: number, centerY: number): void { const halfSizeX = Math.trunc(GameCanvas.spriteSizeX[4] / 2) const halfSizeY = Math.trunc(GameCanvas.spriteSizeY[4] / 2) this.drawSprite(this.graphics, 4, this.addDx(centerX - halfSizeX), this.addDy(centerY + halfSizeY)) } drawHelmet(x: number, y: number, angleF16: number): void { const var4 = this.calcSpriteNo(angleF16, -102943, 411774, 32, true) const var5 = this.addDx(x) - Math.trunc(this.helmetSpriteWidth / 2) const var6 = this.addDy(y) - Math.trunc(this.helmetSpriteHeight / 2) this.graphics.setClip(var5, var6, this.helmetSpriteWidth, this.helmetSpriteHeight) this.graphics.drawImage( this.helmetImage, var5 - this.helmetSpriteWidth * (var4 % 6), var6 - this.helmetSpriteHeight * Math.trunc(var4 / 6), Graphics.LEFT | Graphics.TOP, ) this.graphics.setClip(0, 0, this.width, this.getHeight()) } drawTime(time10Ms: number): void { const seconds = Math.trunc(time10Ms / 100) const time10MsPart = Math.trunc(time10Ms % 100) if (this.timeInSeconds !== seconds || GameCanvas.stringWithTime.length === 0) { const zeroPadding = seconds % 60 >= 10 ? '' : '0' GameCanvas.stringWithTime = `${Math.trunc(seconds / 60)}:${zeroPadding}${seconds % 60}.` this.timeInSeconds = seconds } if (this.time10MsToStringCache[time10MsPart].length === 0) { const zeroPadding = time10MsPart >= 10 ? '' : '0' this.time10MsToStringCache[time10MsPart] = `${zeroPadding}${time10Ms % 100}` } this.setColor(0, 0, 0) this.graphics.setFont(FontStorage.getFont(Font.STYLE_BOLD, Font.SIZE_MEDIUM)) if (time10Ms > 3600000) { this.graphics.drawString('0:00.', this.width - GameCanvas.defaultFontWidth00, this.height2 - 5, Graphics.RIGHT | Graphics.TOP) this.graphics.drawString('00', this.width - GameCanvas.defaultFontWidth00, this.height2 - 5, Graphics.LEFT | Graphics.TOP) } else { this.graphics.drawString(GameCanvas.stringWithTime, this.width - GameCanvas.defaultFontWidth00, this.height2 - 5, Graphics.RIGHT | Graphics.TOP) this.graphics.drawString(this.time10MsToStringCache[time10MsPart], this.width - GameCanvas.defaultFontWidth00, this.height2 - 5, Graphics.LEFT | Graphics.TOP) } } handleTimerFired(var1: number): void { if (this.timerId === var1) { this.timerTriggered = true } } static advanceFlagAnimation(): void { GameCanvas.flagAnimationPhase += 655 const sinVal = MathF16.sinF16(GameCanvas.flagAnimationPhase) const var0 = 32768 + ((sinVal < 0 ? -sinVal : sinVal) >> 1) GameCanvas.flagAnimationTime += (6553 * var0) >> 16 } renderStartFlag(x: number, y: number): void { if (GameCanvas.flagAnimationTime > 229376) { GameCanvas.flagAnimationTime = 0 } this.setColor(0, 0, 0) this.drawLine(x, y, x, y + 32) this.drawSprite(this.graphics, this.startFlagAnimationTimeToSpriteNo[GameCanvas.flagAnimationTime >> 16], this.addDx(x), this.addDy(y) - 32) } renderFinishFlag(x: number, y: number): void { if (GameCanvas.flagAnimationTime > 229376) { GameCanvas.flagAnimationTime = 0 } this.setColor(0, 0, 0) this.drawLine(x, y, x, y + 32) this.drawSprite(this.graphics, this.finishFlagAnumationTimeToSpriteNo[GameCanvas.flagAnimationTime >> 16], this.addDx(x), this.addDy(y) - 32) } drawWheelTires(x: number, y: number, wheelIsThin: number): void { const spriteNo = wheelIsThin === 1 ? 0 : 1 const spriteHalfX = Math.trunc(GameCanvas.spriteSizeX[spriteNo] / 2) const spriteHalfY = Math.trunc(GameCanvas.spriteSizeY[spriteNo] / 2) this.drawSprite(this.graphics, spriteNo, this.addDx(x - spriteHalfX), this.addDy(y + spriteHalfY)) } calcSpriteNo(angleF16: number, var2: number, var3: number, var4: number, var5: boolean): number { for (angleF16 += var2; angleF16 < 0; angleF16 += var3) {} while (angleF16 >= var3) { angleF16 -= var3 } if (var5) { angleF16 = var3 - angleF16 } const var6 = toInt((BigInt(toInt((BigInt(angleF16) << 32n) / BigInt(var3) >> 16n)) * BigInt(var4 << 16)) >> 16n) return (var6 >> 16) < var4 - 1 ? var6 >> 16 : var4 - 1 } renderEngine(x: number, y: number, angleF16: number): void { if (this.engineImage === null) { return } const spriteNo = this.calcSpriteNo(angleF16, -247063, 411774, 32, true) const centerX = this.addDx(x) - Math.trunc(this.engineSpriteWidth / 2) const centerY = this.addDy(y) - Math.trunc(this.engineSpriteHeight / 2) this.graphics.setClip(centerX, centerY, this.engineSpriteWidth, this.engineSpriteHeight) this.graphics.drawImage( this.engineImage, centerX - this.engineSpriteWidth * (spriteNo % 6), centerY - this.engineSpriteHeight * Math.trunc(spriteNo / 6), Graphics.LEFT | Graphics.TOP, ) this.graphics.setClip(0, 0, this.width, this.getHeight()) } renderFender(x: number, y: number, angleF16: number): void { if (this.fenderImage === null) { return } const spriteNo = this.calcSpriteNo(angleF16, -185297, 411774, 32, true) const centerX = this.addDx(x) - Math.trunc(this.fenderSpriteWidth / 2) const centerY = this.addDy(y) - Math.trunc(this.fenderSpriteHeight / 2) this.graphics.setClip(centerX, centerY, this.fenderSpriteWidth, this.fenderSpriteHeight) this.graphics.drawImage( this.fenderImage, centerX - this.fenderSpriteWidth * (spriteNo % 6), centerY - this.fenderSpriteHeight * Math.trunc(spriteNo / 6), Graphics.LEFT | Graphics.TOP, ) this.graphics.setClip(0, 0, this.width, this.getHeight()) } clearScreenWithWhite(): void { this.graphics.setColor(255, 247, 228) this.graphics.fillRect(0, 0, this.width, this.height2) if (this.canyon.complete && this.canyon.naturalWidth) { this.ctx.save() this.ctx.globalAlpha = 0.55 this.ctx.imageSmoothingEnabled = true this.ctx.drawImage(this.canyon, 0, 0, this.width, this.height2) this.ctx.restore() } } setColor(red: number, green: number, blue: number): void { if (Micro.isInGameMenu) { red += 128 green += 128 blue += 128 if (red > 240) { red = 240 } if (green > 240) { green = 240 } if (blue > 240) { blue = 240 } } this.graphics.setColor(red, green, blue) } drawProgressBar(var1: number, mode: boolean): void { const h = mode ? this.height : this.height2 this.setColor(0, 0, 0) this.graphics.fillRect(1, h - 4, this.width - 2, 3) this.setColor(255, 255, 255) this.graphics.fillRect(2, h - 3, toInt((BigInt((this.width - 4) << 16) * BigInt(var1)) >> 16n) >> 16, 1) } drawTimerMessage(): void { if (this.timerMessage.length === 0) { return } this.setColor(0, 0, 0) this.graphics.setFont(this.screenFont) if (this.height2 <= 128) { this.graphics.drawString(this.timerMessage, Math.trunc(this.width / 2), 1, Graphics.HCENTER | Graphics.TOP) } else { this.graphics.drawString(this.timerMessage, Math.trunc(this.width / 2), Math.trunc(this.height2 / 4), Graphics.HCENTER | Graphics.VCENTER) } if (this.timerTriggered) { this.timerTriggered = false this.timerMessage = '' } } setInputMode(var1: number): void { this.inputMode = var1 } drawGame(g: Graphics): void { if (!Micro.isGameVisible || this.micro.isLoadingBlocked || this.gamePhysics === null) { return } if (this.loadingScreenMode !== 0) { g.setColor(255, 255, 255) g.fillRect(0, 0, this.getWidth(), this.getHeight()) if (this.loadingScreenMode === 1) { if (this.logoImage !== null) { g.drawImage(this.logoImage, this.getWidth() >> 1, this.getHeight() >> 1, Graphics.HCENTER | Graphics.VCENTER) this.drawSprite(g, 16, this.getWidth() - GameCanvas.spriteSizeX[16] - 5, this.getHeight() - GameCanvas.spriteSizeY[16] - 7) this.drawSprite( g, 17, this.getWidth() - GameCanvas.spriteSizeX[17] - 4, this.getHeight() - GameCanvas.spriteSizeY[17] - GameCanvas.spriteSizeY[16] - 9, ) } } else if (this.splashImage !== null) { g.drawImage(this.splashImage, this.getWidth() >> 1, this.getHeight() >> 1, Graphics.HCENTER | Graphics.VCENTER) } const var3 = toInt(((BigInt(Micro.gameLoadingStateStage << 16) << 32n) / 655360n) >> 16n) this.drawProgressBar(var3, true) return } this.gamePhysics.setMotoComponents() this.setViewPosition(-this.gamePhysics.getCamPosX() + this.cameraOffsetX + (this.width >> 1), this.gamePhysics.getCamPosY() + this.cameraOffsetY + (this.height2 >> 1)) this.gamePhysics.renderGame(this) // Race time is displayed in the responsive DOM HUD in this edition. this.drawTimerMessage() this.drawProgressBar(this.gamePhysics.getProgressF16(), false) } paint(g: Graphics): void { this.beginFrame() if (Micro.isInGameMenu && this.menuManager !== null) { this.menuManager.renderCurrentMenu(g) return } this.drawGame(g) } clearActiveInputs(): void { for (let var1 = 0; var1 < 10; ++var1) { this.activeKeys[var1] = false } for (let var1 = 0; var1 < 7; ++var1) { this.activeActions[var1] = false } } handleUpdatedInput(): void { let var1 = 0 let var2 = 0 const var3 = this.inputMode for (let var4 = 0; var4 < 10; ++var4) { if (this.activeKeys[var4]) { var1 += this.keyInputDeltaByMode[var3][var4][0] var2 += this.keyInputDeltaByMode[var3][var4][1] } } for (let var4 = 0; var4 < 7; ++var4) { if (this.activeActions[var4]) { var1 += this.actionInputDelta[var4][0] var2 += this.actionInputDelta[var4][1] } } this.gamePhysics?.setInputDirection(var1, var2) } processTimers(): void { for (let i = 0; i < this.timers.length; ) { if (this.timers[i].ready()) { this.handleTimerFired(this.timers[i].getId()) this.timers.splice(i, 1) } else { ++i } } } processKeyPressed(keyCode: number): void { const action = this.getGameAction(keyCode) const numKey = keyCode - 48 if (numKey >= 0 && numKey < 10) { this.activeKeys[numKey] = true } else if (action >= 0 && action < 7) { this.activeActions[action] = true } this.handleUpdatedInput() } processKeyReleased(keyCode: number): void { const action = this.getGameAction(keyCode) const numKey = keyCode - 48 if (numKey >= 0 && numKey < 10) { this.activeKeys[numKey] = false } else if (action >= 0 && action < 7) { this.activeActions[action] = false } this.handleUpdatedInput() } getGameAction(keyCode: number): number { switch (keyCode) { case 1: case 2: case 5: case 6: case 8: return keyCode default: return -1 } } scheduleGameTimerTask(timerMessage: string, delayMs: number): void { this.timerTriggered = false ++this.timerId this.timerMessage = timerMessage this.timers.push(new Timer(this.timerId, delayMs)) } setMenuManager(menuManager: MenuManager | null): void { this.menuManager = menuManager } setRepaintHandler(repaintHandler: (() => void) | null): void { this.repaintHandler = repaintHandler } openPauseMenu(): void { if (this.menuManager !== null) { this.menuManager.isOpeningPauseMenu = true this.micro.gameToMenu() } } handleBackAction(): void { if (Micro.isInGameMenu) { this.menuManager?.handleBackAction() } } keyPressed(var1: number): void { if (Micro.isInGameMenu && this.menuManager !== null) { this.menuManager.processKeyCode(var1) } this.processKeyPressed(var1) } keyReleased(var1: number): void { this.processKeyReleased(var1) } showMenuButton(): void { this.isMenuButtonVisible = true } hideMenuButton(): void { this.isMenuButtonVisible = false } hasMenuButton(): boolean { return this.isMenuButtonVisible } showBackButton(): void { this.isBackButtonVisible = true } hideBackButton(): void { this.isBackButtonVisible = false } hasBackButton(): boolean { return this.isBackButtonVisible } repaint(): void { this.repaintHandler?.() } serviceRepaints(): void { this.repaintHandler?.() } } ===== FILE: src/GameLevel.ts ===== import { abs, divideF16, multiplyF16 } from './cpp.ts' import { GameCanvas } from './GameCanvas.ts' import { GamePhysics } from './GamePhysics.ts' import { LevelLoader } from './LevelLoader.ts' import { FileStream } from './utils/FileStream.ts' export class GameLevel { private minX = 0 private maxX = 0 private shadowStartXF16 = 0 private shadowEndXF16 = 0 private shadowProjectionYF16 = 0 private shadowBlendF16 = 0 startPosX = 0 startPosY = 0 finishPosX = 13107200 startFlagPoint = 0 finishFlagPoint = 0 finishPosY = 0 pointsCount = 0 unusedLevelFlag = 0 pointPositions: number[][] = [] constructor() { this.init() } init(): void { this.startPosX = 0 this.startPosY = 0 this.finishPosX = 13107200 this.pointsCount = 0 this.unusedLevelFlag = 0 } setStartAndFinishPositions(var1: number, var2: number, var3: number, var4: number): void { this.startPosX = (var1 << 16) >> 3 this.startPosY = (var2 << 16) >> 3 this.finishPosX = (var3 << 16) >> 3 this.finishPosY = (var4 << 16) >> 3 } getStartPosX(): number { return (this.startPosX << 3) >> 16 } getStartPosY(): number { return (this.startPosY << 3) >> 16 } getFinishPosX(): number { return (this.finishPosX << 3) >> 16 } getFinishPosY(): number { return (this.finishPosY << 3) >> 16 } getPointX(pointNo: number): number { return (this.pointPositions[pointNo][0] << 3) >> 16 } getPointY(pointNo: number): number { return (this.pointPositions[pointNo][1] << 3) >> 16 } getProgressF16(var1: number): number { const var2 = var1 - this.pointPositions[this.startFlagPoint][0] const var3 = this.pointPositions[this.finishFlagPoint][0] - this.pointPositions[this.startFlagPoint][0] return abs(var3) >= 3 && var2 <= var3 ? divideF16(var2, var3) : 65536 } setMinMaxX(minX: number, maxX: number): void { this.minX = (minX << 16) >> 3 this.maxX = (maxX << 16) >> 3 } setShadowProjectionRange(var1: number, var2: number): void { this.shadowStartXF16 = var1 >> 1 this.shadowEndXF16 = var2 >> 1 } setShadowProjectionState(var1: number, var2: number, var3: number): void { this.shadowStartXF16 = var1 this.shadowEndXF16 = var2 this.shadowProjectionYF16 = var3 } renderShadow(gameCanvas: GameCanvas, var2: number, var3: number): void { if (var3 <= this.pointsCount - 1) { let var4 = this.shadowProjectionYF16 - ((this.pointPositions[var2][1] + this.pointPositions[var3 + 1][1]) >> 1) < 0 ? 0 : this.shadowProjectionYF16 - ((this.pointPositions[var2][1] + this.pointPositions[var3 + 1][1]) >> 1) if (this.shadowProjectionYF16 <= this.pointPositions[var2][1] || this.shadowProjectionYF16 <= this.pointPositions[var3 + 1][1]) { var4 = var4 < 327680 ? var4 : 327680 } this.shadowBlendF16 = multiplyF16(this.shadowBlendF16, 49152) + multiplyF16(var4, 16384) if (this.shadowBlendF16 <= 557056) { const var5 = (multiplyF16(1638400, this.shadowBlendF16) >> 16) gameCanvas.setColor(var5, var5, var5) let var6 = this.pointPositions[var2][0] - this.pointPositions[var2 + 1][0] let var8 = divideF16(this.pointPositions[var2][1] - this.pointPositions[var2 + 1][1], var6) let var9 = this.pointPositions[var2][1] - multiplyF16(this.pointPositions[var2][0], var8) const var10 = multiplyF16(this.shadowStartXF16, var8) + var9 var6 = this.pointPositions[var3][0] - this.pointPositions[var3 + 1][0] var8 = divideF16(this.pointPositions[var3][1] - this.pointPositions[var3 + 1][1], var6) var9 = this.pointPositions[var3][1] - multiplyF16(this.pointPositions[var3][0], var8) const var11 = multiplyF16(this.shadowEndXF16, var8) + var9 if (var2 === var3) { gameCanvas.drawLine((this.shadowStartXF16 << 3) >> 16, ((var10 + 65536) << 3) >> 16, (this.shadowEndXF16 << 3) >> 16, ((var11 + 65536) << 3) >> 16) return } gameCanvas.drawLine( (this.shadowStartXF16 << 3) >> 16, ((var10 + 65536) << 3) >> 16, (this.pointPositions[var2 + 1][0] << 3) >> 16, ((this.pointPositions[var2 + 1][1] + 65536) << 3) >> 16, ) for (let i = var2 + 1; i < var3; ++i) { gameCanvas.drawLine( (this.pointPositions[i][0] << 3) >> 16, ((this.pointPositions[i][1] + 65536) << 3) >> 16, (this.pointPositions[i + 1][0] << 3) >> 16, ((this.pointPositions[i + 1][1] + 65536) << 3) >> 16, ) } gameCanvas.drawLine( (this.pointPositions[var3][0] << 3) >> 16, ((this.pointPositions[var3][1] + 65536) << 3) >> 16, (this.shadowEndXF16 << 3) >> 16, ((var11 + 65536) << 3) >> 16, ) } } } renderLevel3D(gameCanvas: GameCanvas, xF16: number, yF16: number): void { let var7 = 0 let var8 = 0 let lineNo = 0 for (lineNo = 0; lineNo < this.pointsCount - 1 && this.pointPositions[lineNo][0] <= this.minX; ++lineNo) {} if (lineNo > 0) { --lineNo } let var9 = xF16 - this.pointPositions[lineNo][0] let var10 = yF16 + 3276800 - this.pointPositions[lineNo][1] let var11 = GamePhysics.getSmthLikeMaxAbs(var9, var10) var9 = divideF16(var9, var11 >> 1 >> 1) var10 = divideF16(var10, var11 >> 1 >> 1) gameCanvas.setColor(110, 61, 34) while (lineNo < this.pointsCount - 1) { const var4 = var9 const var5 = var10 var9 = xF16 - this.pointPositions[lineNo + 1][0] var10 = yF16 + 3276800 - this.pointPositions[lineNo + 1][1] var11 = GamePhysics.getSmthLikeMaxAbs(var9, var10) var9 = divideF16(var9, var11 >> 1 >> 1) var10 = divideF16(var10, var11 >> 1 >> 1) gameCanvas.drawLine( ((this.pointPositions[lineNo][0] + var4) << 3) >> 16, ((this.pointPositions[lineNo][1] + var5) << 3) >> 16, ((this.pointPositions[lineNo + 1][0] + var9) << 3) >> 16, ((this.pointPositions[lineNo + 1][1] + var10) << 3) >> 16, ) gameCanvas.drawLine( (this.pointPositions[lineNo][0] << 3) >> 16, (this.pointPositions[lineNo][1] << 3) >> 16, ((this.pointPositions[lineNo][0] + var4) << 3) >> 16, ((this.pointPositions[lineNo][1] + var5) << 3) >> 16, ) if (lineNo > 1) { if (this.pointPositions[lineNo][0] > this.shadowStartXF16 && var7 === 0) { var7 = lineNo - 1 } if (this.pointPositions[lineNo][0] > this.shadowEndXF16 && var8 === 0) { var8 = lineNo - 1 } } if (this.startFlagPoint === lineNo) { gameCanvas.renderStartFlag( ((this.pointPositions[this.startFlagPoint][0] + var4) << 3) >> 16, ((this.pointPositions[this.startFlagPoint][1] + var5) << 3) >> 16, ) gameCanvas.setColor(110, 61, 34) } if (this.finishFlagPoint === lineNo) { gameCanvas.renderFinishFlag( ((this.pointPositions[this.finishFlagPoint][0] + var4) << 3) >> 16, ((this.pointPositions[this.finishFlagPoint][1] + var5) << 3) >> 16, ) gameCanvas.setColor(110, 61, 34) } if (this.pointPositions[lineNo][0] > this.maxX) { break } ++lineNo } gameCanvas.drawLine( (this.pointPositions[this.pointsCount - 1][0] << 3) >> 16, (this.pointPositions[this.pointsCount - 1][1] << 3) >> 16, ((this.pointPositions[this.pointsCount - 1][0] + var9) << 3) >> 16, ((this.pointPositions[this.pointsCount - 1][1] + var10) << 3) >> 16, ) if (LevelLoader.isEnabledShadows) { this.renderShadow(gameCanvas, var7, var8) } } renderTrackNearestGreenLine(gameCanvas: GameCanvas): void { let pointNo = 0 for (pointNo = 0; pointNo < this.pointsCount - 1 && this.pointPositions[pointNo][0] <= this.minX; ++pointNo) {} if (pointNo > 0) { --pointNo } while (pointNo < this.pointsCount - 1) { gameCanvas.drawLine( (this.pointPositions[pointNo][0] << 3) >> 16, (this.pointPositions[pointNo][1] << 3) >> 16, (this.pointPositions[pointNo + 1][0] << 3) >> 16, (this.pointPositions[pointNo + 1][1] << 3) >> 16, ) if (this.startFlagPoint === pointNo) { gameCanvas.renderStartFlag( (this.pointPositions[this.startFlagPoint][0] << 3) >> 16, (this.pointPositions[this.startFlagPoint][1] << 3) >> 16, ) gameCanvas.setColor(156, 97, 53) } if (this.finishFlagPoint === pointNo) { gameCanvas.renderFinishFlag( (this.pointPositions[this.finishFlagPoint][0] << 3) >> 16, (this.pointPositions[this.finishFlagPoint][1] << 3) >> 16, ) gameCanvas.setColor(156, 97, 53) } if (this.pointPositions[pointNo][0] > this.maxX) { break } ++pointNo } } addPointSimple(var1: number, var2: number): void { this.addPoint((var1 << 16) >> 3, (var2 << 16) >> 3) } addPoint(x: number, y: number): void { if (this.pointPositions.length === 0 || this.pointPositions.length <= this.pointsCount) { let var3 = 100 if (this.pointPositions.length !== 0) { var3 = var3 < this.pointPositions.length + 30 ? this.pointPositions.length + 30 : var3 } const resized = new Array(var3) for (let i = 0; i < var3; ++i) { resized[i] = i < this.pointPositions.length ? this.pointPositions[i] : [0, 0] } this.pointPositions = resized } if (this.pointsCount === 0 || this.pointPositions[this.pointsCount - 1][0] < x) { this.pointPositions[this.pointsCount][0] = x this.pointPositions[this.pointsCount][1] = y ++this.pointsCount } } load(inStream: FileStream): void { this.init() const c = inStream.readInt8() if (c === 50) { inStream.readBytes(20) } this.finishFlagPoint = 0 this.startFlagPoint = 0 this.startPosX = inStream.readInt32(true) this.startPosY = inStream.readInt32(true) this.finishPosX = inStream.readInt32(true) this.finishPosY = inStream.readInt32(true) const pointsCount = inStream.readInt16(true) let pointX = inStream.readInt32(true) let pointY = inStream.readInt32(true) let offsetX = pointX let offsetY = pointY this.addPointSimple(pointX, pointY) for (let i = 1; i < pointsCount; ++i) { const modeOrDx = inStream.readInt8() if (modeOrDx === -1) { offsetY = 0 offsetX = 0 pointX = inStream.readInt32(true) pointY = inStream.readInt32(true) } else { pointX = modeOrDx pointY = inStream.readInt8() } offsetX += pointX offsetY += pointY this.addPointSimple(offsetX, offsetY) } } } ===== FILE: src/GameMenu.ts ===== import type { IGameMenuElement } from './IGameMenuElement.ts' import { TextRender } from './TextRender.ts' import type { Micro } from './Micro.ts' import { Font } from './lcdui/Font.ts' import { FontStorage } from './lcdui/FontStorage.ts' import { Graphics } from './lcdui/Graphics.ts' import { GameCanvas } from './GameCanvas.ts' export class GameMenu { private parentMenu: GameMenu | null private title: string private selectedIndex: number private readonly vector: IGameMenuElement[] = [] private readonly micro: Micro private readonly font: Font private readonly font2: Font private readonly font3: Font private topPadding: number private rowSpacing = 0 private contentX: number private firstVisibleIndex = 0 private lastVisibleIndex = 0 private visibleItemCount: number private readonly canvasWidth: number private readonly canvasHeight: number private cursorAnimationFrame: number private readonly isNameEntryMenu: boolean private nameCursorPos = 0 private readonly strArr: Uint8Array | null xPos: number constructor(var1: string, micro: Micro, var3: GameMenu | null, inputString?: Uint8Array | null) { this.title = var1 this.selectedIndex = -1 this.micro = micro this.parentMenu = var3 this.canvasWidth = micro.gameCanvas?.getWidth() ?? 0 this.canvasHeight = micro.gameCanvas?.getHeight() ?? 0 this.font = FontStorage.getFont(Font.STYLE_BOLD, Font.SIZE_LARGE) this.font3 = FontStorage.getFont(Font.STYLE_PLAIN, Font.SIZE_SMALL) if (this.canvasWidth >= 128) { this.font2 = FontStorage.getFont(Font.STYLE_BOLD, Font.SIZE_MEDIUM) } else { this.font2 = this.font3 } TextRender.setDefaultFont(this.font3) TextRender.setMaxArea(this.canvasWidth, this.canvasHeight) this.topPadding = 1 this.xPos = this.canvasWidth <= 100 ? 6 : 9 if (this.canvasHeight <= 100) { this.title = '' } this.contentX = this.xPos + 7 this.rowSpacing = 2 this.cursorAnimationFrame = 0 if (this.title !== '') { this.visibleItemCount = Math.trunc((this.canvasHeight - (this.topPadding << 1) - 10 - this.font.getBaselinePosition()) / (this.font2.getBaselinePosition() + this.rowSpacing)) } else { this.visibleItemCount = Math.trunc((this.canvasHeight - (this.topPadding << 1) - 10) / (this.font2.getBaselinePosition() + this.rowSpacing)) } if (inputString !== undefined && inputString !== null) { this.isNameEntryMenu = true this.nameCursorPos = 0 this.xPos = 8 this.strArr = inputString } else { this.isNameEntryMenu = false this.strArr = null } if (this.visibleItemCount > 13) { this.visibleItemCount = 13 } } setRowSpacing(var1: number): void { this.rowSpacing = var1 } setTitle(var1: string): void { this.title = var1 } selectFirstMenuItem(): void { if (this.isNameEntryMenu) { this.nameCursorPos = 0 } else if (this.vector.length !== 0) { this.selectedIndex = 0 for (let var1 = 0; var1 < this.vector.length && var1 < this.visibleItemCount; ++var1) { if (this.vector[var1].isNotTextRender()) { this.selectedIndex = var1 break } } this.firstVisibleIndex = 0 this.lastVisibleIndex = this.vector.length - 1 if (this.lastVisibleIndex > this.visibleItemCount - 1) { this.lastVisibleIndex = this.visibleItemCount - 1 } } } selectLastMenuItem(): void { this.selectedIndex = this.vector.length - 1 for (let var1 = this.vector.length - 1; var1 > 0; --var1) { if (this.vector[var1].isNotTextRender()) { this.selectedIndex = var1 break } } this.firstVisibleIndex = this.vector.length - this.visibleItemCount if (this.firstVisibleIndex < 0) { this.firstVisibleIndex = 0 } this.lastVisibleIndex = this.vector.length - 1 if (this.lastVisibleIndex > this.selectedIndex + this.visibleItemCount) { this.lastVisibleIndex = this.selectedIndex + this.visibleItemCount } } addMenuElement(var1: IGameMenuElement): void { let var2 = this.topPadding this.visibleItemCount = 1 this.vector.push(var1) if (this.title !== '') { var2 = this.font.getBaselinePosition() + 2 } if (this.canvasHeight < 100) { ++var2 } else { var2 += 4 } for (let var3 = 0; var3 < this.vector.length - 1; ++var3) { if (this.vector[var3].isNotTextRender()) { var2 += this.font2.getBaselinePosition() + this.rowSpacing } else { var2 += (TextRender.getBaselinePosition() < GameCanvas.spriteSizeY[5] ? GameCanvas.spriteSizeY[5] : TextRender.getBaselinePosition()) + this.rowSpacing } if (var2 > this.canvasHeight - (this.topPadding << 1) - 10) { break } ++this.visibleItemCount } if (this.visibleItemCount > 13) { this.visibleItemCount = 13 } this.selectFirstMenuItem() } processGameActionDown(): void { if (this.isNameEntryMenu && this.strArr !== null) { if (this.strArr[this.nameCursorPos] === 32) { this.strArr[this.nameCursorPos] = 90 return } --this.strArr[this.nameCursorPos] if (this.strArr[this.nameCursorPos] < 65) { this.strArr[this.nameCursorPos] = 32 } return } if (this.vector.length === 0) { return } if (!this.vector[this.selectedIndex].isNotTextRender()) { ++this.lastVisibleIndex this.selectedIndex = this.lastVisibleIndex ++this.firstVisibleIndex return } ++this.selectedIndex if (this.selectedIndex > this.vector.length - 1) { this.selectFirstMenuItem() return } let var3 = false let var2 = this.selectedIndex for (var2 = this.selectedIndex; var2 <= this.lastVisibleIndex + 1 && var2 < this.vector.length; ++var2) { if (this.vector[var2].isNotTextRender()) { var3 = true break } } if (var3) { this.selectedIndex = var2 } else if (this.lastVisibleIndex < this.vector.length - 1) { ++this.lastVisibleIndex ++this.firstVisibleIndex } else { --this.selectedIndex } if (this.selectedIndex > this.lastVisibleIndex) { ++this.firstVisibleIndex ++this.lastVisibleIndex if (this.lastVisibleIndex > this.vector.length - 1) { this.lastVisibleIndex = this.vector.length - 1 } this.selectedIndex = this.lastVisibleIndex } } processGameActionUp(): void { if (this.isNameEntryMenu && this.strArr !== null) { if (this.strArr[this.nameCursorPos] === 32) { this.strArr[this.nameCursorPos] = 65 return } ++this.strArr[this.nameCursorPos] if (this.strArr[this.nameCursorPos] > 90) { this.strArr[this.nameCursorPos] = 32 } return } if (this.vector.length === 0) { return } --this.selectedIndex if (this.selectedIndex < 0) { this.selectLastMenuItem() return } let var3 = false let var2 = this.selectedIndex for (var2 = this.selectedIndex; var2 >= this.firstVisibleIndex; --var2) { if (this.vector[var2].isNotTextRender()) { var3 = true break } } if (!var3) { if (this.firstVisibleIndex > 0) { --this.firstVisibleIndex if (this.vector.length > this.visibleItemCount - 1) { --this.lastVisibleIndex return } } else { this.selectLastMenuItem() } return } this.selectedIndex = var2 if (this.selectedIndex < this.firstVisibleIndex) { --this.firstVisibleIndex if (this.firstVisibleIndex < 0) { this.selectedIndex = 0 this.firstVisibleIndex = 0 } if (this.vector.length > this.visibleItemCount - 1) { --this.lastVisibleIndex } } } processGameActionUpd(var1: number): void { if (this.isNameEntryMenu) { switch (var1) { case 1: if (this.nameCursorPos === 2) { ;(this.micro.menuManager as { openMenu(menu: GameMenu | null, preserveSelection: boolean): void } | null)?.openMenu(this.parentMenu, false) return } ++this.nameCursorPos return case 2: ++this.nameCursorPos if (this.nameCursorPos > 2) { this.nameCursorPos = 2 } return case 3: --this.nameCursorPos if (this.nameCursorPos < 0) { this.nameCursorPos = 0 } return default: return } } if (this.selectedIndex !== -1) { for (let var2 = this.selectedIndex; var2 < this.vector.length; ++var2) { const var3 = this.vector[var2] if (var3 !== null && var3.isNotTextRender()) { var3.menuElemMethod(var1) return } } } } getView() { return { title: this.title, nameEntry: this.isNameEntryMenu, selected: this.selectedIndex, items: this.vector.map((item, index) => ({ index, text: item.getText().replace(/>$/, ''), action: item.isNotTextRender() })) } } activateItem(index: number): void { const item = this.vector[index] if (item?.isNotTextRender()) { this.selectedIndex = index; item.menuElemMethod(1) } } render(graphics: Graphics): void { if (this.isNameEntryMenu && this.strArr !== null) { graphics.setColor(0, 0, 20) graphics.setFont(this.font) const var7 = 1 graphics.drawString('Enter Name', this.xPos, var7, Graphics.LEFT | Graphics.TOP) const var2 = var7 + this.font.getHeight() + (this.rowSpacing << 2) graphics.setFont(this.font2) for (let i = 0; i < 3; ++i) { graphics.drawChar(String.fromCharCode(this.strArr[i]), this.xPos + i * this.font2.charWidth('W') + 1, var2, Graphics.HCENTER | Graphics.TOP) if (i === this.nameCursorPos) { graphics.drawChar('^', this.xPos + i * this.font2.charWidth('W') + 1, var2 + this.font2.getHeight(), Graphics.HCENTER | Graphics.TOP) } } return } graphics.setColor(0, 0, 0) let var2 = this.topPadding if (this.title !== '') { graphics.setFont(this.font) graphics.drawString(this.title, this.xPos, var2, Graphics.LEFT | Graphics.TOP) var2 += this.font.getBaselinePosition() + 2 } if (this.firstVisibleIndex > 0 && this.micro.gameCanvas !== null) { this.micro.gameCanvas.drawSprite(graphics, 2, this.xPos - 3, var2) } if (this.canvasHeight < 100) { ++var2 } else { var2 += 4 } graphics.setFont(this.font2) for (let i = this.firstVisibleIndex; i < this.lastVisibleIndex + 1; ++i) { const var4 = this.vector[i] graphics.setColor(0, 0, 0) var4.render(graphics, var2, this.contentX) if (i === this.selectedIndex && var4.isNotTextRender() && this.micro.gameCanvas !== null) { const var5 = this.xPos - Math.trunc(this.micro.gameCanvas.helmetSpriteWidth / 2) const var6 = var2 + Math.trunc(this.font2.getBaselinePosition() / 2) - Math.trunc(this.micro.gameCanvas.helmetSpriteHeight / 2) graphics.setClip(var5, var6, this.micro.gameCanvas.helmetSpriteWidth, this.micro.gameCanvas.helmetSpriteHeight) graphics.drawImage( this.micro.gameCanvas.helmetImage, var5 - this.micro.gameCanvas.helmetSpriteWidth * (this.cursorAnimationFrame % 6), var6 - this.micro.gameCanvas.helmetSpriteHeight * Math.trunc(this.cursorAnimationFrame / 6), Graphics.LEFT | Graphics.TOP, ) graphics.setClip(0, 0, this.canvasWidth, this.canvasHeight) ++this.cursorAnimationFrame if (this.cursorAnimationFrame > 30) { this.cursorAnimationFrame = 0 } } if (var4.isNotTextRender()) { var2 += this.font2.getBaselinePosition() + this.rowSpacing } else { var2 += (TextRender.getBaselinePosition() < GameCanvas.spriteSizeY[5] ? GameCanvas.spriteSizeY[5] : TextRender.getBaselinePosition()) + this.rowSpacing } } if (this.vector.length > this.lastVisibleIndex && this.lastVisibleIndex !== this.vector.length - 1 && this.micro.gameCanvas !== null) { if (GameCanvas.spriteSizeY[3] + var2 > this.canvasHeight) { this.micro.gameCanvas.drawSprite(graphics, 3, this.xPos - 3, this.canvasHeight - GameCanvas.spriteSizeY[3]) return } this.micro.gameCanvas.drawSprite(graphics, 3, this.xPos - 3, var2 - 2) } } setParentMenu(gameMenu: GameMenu | null): void { this.parentMenu = gameMenu } getParentMenu(): GameMenu | null { return this.parentMenu } getSelectedIndex(): number { return this.selectedIndex } clearVector(): void { this.vector.length = 0 this.firstVisibleIndex = 0 this.lastVisibleIndex = 0 this.selectedIndex = -1 } makeString(): string { return this.strArr === null ? '' : String.fromCharCode(...this.strArr) } getStrArr(): Uint8Array | null { return this.strArr } scrollToSelection(var1: number): void { this.selectedIndex = var1 this.firstVisibleIndex = var1 - Math.trunc(this.visibleItemCount / 2) if (this.firstVisibleIndex < 0) { this.firstVisibleIndex = 0 } this.lastVisibleIndex = this.firstVisibleIndex + this.visibleItemCount - 1 if (this.lastVisibleIndex > this.vector.length - 1) { this.lastVisibleIndex = this.vector.length - 1 this.firstVisibleIndex = this.lastVisibleIndex - this.visibleItemCount + 1 if (this.firstVisibleIndex < 0) { this.firstVisibleIndex = 0 } } } } ===== FILE: src/GamePhysics.ts ===== import { LevelLoader } from './LevelLoader.ts' import { MathF16 } from './MathF16.ts' import { Micro } from './Micro.ts' import { TimerOrMotoPartOrMenuElem } from './TimerOrMotoPartOrMenuElem.ts' import { class_10 } from './class_10.ts' import { abs, divideF16, multiplyF16 } from './cpp.ts' import { GameCanvas } from './GameCanvas.ts' function max3(a: number, b: number, c: number): number { return Math.max(a, b, c) } function min3(a: number, b: number, c: number): number { return Math.min(a, b, c) } export class GamePhysics { private index01 = 0 private index10 = 1 private collisionBodyIndex = -1 private constraints: TimerOrMotoPartOrMenuElem[] = [] private wheelTorqueF16 = 0 private readonly levelLoader: LevelLoader private collisionNormalXF16 = 0 private collisionNormalYF16 = 0 isCrashed = false private isRiderDown = false private riderPoseBlendF16 = 32768 private readonly leanStepF16 = 3276 private rotationSpeedF16 = 0 private unusedFlag42 = false private readonly motoComponents = new Array(6) private unusedCounter44 = 0 private isInputAcceleration = false private isInputBreak = false private isInputBack = false private isInputForward = false private isInputUp = false private isInputDown = false private isInputLeft = false private isInputRight = false private finishTriggerLatched = false private isEnableLookAhead = true private camShiftX = 0 private camShiftY = 0 private lookAheadClampF16 = 655360 private readonly hardcodedArr1 = [[183500, -52428], [262144, -163840], [406323, -65536], [445644, -39321], [235929, 39321], [16384, -144179], [13107, -78643], [288358, 81920]] private readonly hardcodedArr2 = [[190054, -111411], [308019, -235929], [334233, -114688], [393216, -58982], [262144, 98304], [65536, -124518], [13107, -78643], [288358, 81920]] private readonly hardcodedArr3 = [[157286, 13107], [294912, -13107], [367001, 91750], [406323, 190054], [347340, 72089], [39321, -98304], [13107, -52428], [294912, 81920]] private readonly hardcodedArr4 = [[183500, -39321], [262144, -131072], [393216, -65536], [458752, -39321], [294912, 6553], [16384, -144179], [13107, -78643], [288358, 85196]] private readonly hardcodedArr5 = [[190054, -91750], [255590, -235929], [334233, -114688], [393216, -42598], [301465, 6553], [65536, -78643], [13107, -78643], [288358, 85196]] private readonly hardcodedArr6 = [[157286, 13107], [294912, -13107], [367001, 104857], [406323, 176947], [347340, 72089], [39321, -98304], [13107, -52428], [288358, 85196]] private riderPoseBlendTable: number[][] static physicsStepCount = 0 static gravityForceF16 = 0 static tireGripF16 = 0 static tireDampingF16 = 0 static tireAngularDampingF16 = 0 static motoParam1 = 0 static motoParam2 = 0 static massScaleF16 = 0 static motoParam10 = 0 static constraintAngleF16 = 0 static const175_1_half = [114688, 65536, 32768] static motoParam3 = 0 static torqueDampingF16 = 0 static motoParam4 = 0 static motoParam5 = 0 static motoParam6 = 0 static motoParam7 = 0 static motoParam8 = 0 static motoParam9 = 0 bikeParts: class_10[] = [] unusedFlag41 = false mode = 0 isRenderDriverWithSprites = false isRenderMotoWithSprites = false static curentMotoLeague = 0 isTrackFinished = false isGenerateInputAI = false constructor(levelLoader: LevelLoader) { void this.leanStepF16 void this.unusedFlag42 void this.unusedCounter44 for (let var2 = 0; var2 < 6; ++var2) { this.motoComponents[var2] = new TimerOrMotoPartOrMenuElem() } this.riderPoseBlendTable = [[45875], [32768], [52428]] this.levelLoader = levelLoader this.resetSmth(true) this.isGenerateInputAI = false this.syncRenderStateFromSimulation() this.isCrashed = false } getLoadedSpriteFlags(): number { if (this.isRenderDriverWithSprites && this.isRenderMotoWithSprites) { return 3 } if (this.isRenderMotoWithSprites) { return 1 } return this.isRenderDriverWithSprites ? 2 : 0 } applyLoadedSpriteFlags(var1: number): void { this.isRenderDriverWithSprites = false this.isRenderMotoWithSprites = false if ((var1 & 2) !== 0) { this.isRenderDriverWithSprites = true } if ((var1 & 1) !== 0) { this.isRenderMotoWithSprites = true } } setMode(mode: number): void { this.mode = mode switch (mode) { case 1: default: GamePhysics.physicsStepCount = 1310 GamePhysics.gravityForceF16 = 1638400 this.setMotoLeague(1) this.resetSmth(true) } } setMotoLeague(league: number): void { GamePhysics.curentMotoLeague = league GamePhysics.tireGripF16 = 45875 GamePhysics.tireDampingF16 = 13107 GamePhysics.tireAngularDampingF16 = 39321 GamePhysics.massScaleF16 = 1310720 GamePhysics.constraintAngleF16 = 262144 GamePhysics.torqueDampingF16 = 6553 switch (league) { case 0: default: GamePhysics.motoParam1 = 19660 GamePhysics.motoParam2 = 19660 GamePhysics.motoParam3 = 1114112 GamePhysics.motoParam4 = 52428800 GamePhysics.motoParam5 = 3276800 GamePhysics.motoParam6 = 327 GamePhysics.motoParam7 = 0 GamePhysics.motoParam8 = 32768 GamePhysics.motoParam9 = 327680 GamePhysics.motoParam10 = 19660800 break case 1: GamePhysics.motoParam1 = 32768 GamePhysics.motoParam2 = 32768 GamePhysics.motoParam3 = 1114112 GamePhysics.motoParam4 = 65536000 GamePhysics.motoParam5 = 3276800 GamePhysics.motoParam6 = 6553 GamePhysics.motoParam7 = 26214 GamePhysics.motoParam8 = 26214 GamePhysics.motoParam9 = 327680 GamePhysics.motoParam10 = 19660800 break case 2: GamePhysics.motoParam1 = 32768 GamePhysics.motoParam2 = 32768 GamePhysics.motoParam3 = 1310720 GamePhysics.motoParam4 = 75366400 GamePhysics.motoParam5 = 3473408 GamePhysics.motoParam6 = 6553 GamePhysics.motoParam7 = 26214 GamePhysics.motoParam8 = 39321 GamePhysics.motoParam9 = 327680 GamePhysics.motoParam10 = 21626880 break case 3: GamePhysics.motoParam1 = 32768 GamePhysics.motoParam2 = 32768 GamePhysics.motoParam3 = 1441792 GamePhysics.motoParam4 = 78643200 GamePhysics.motoParam5 = 3538944 GamePhysics.motoParam6 = 6553 GamePhysics.motoParam7 = 26214 GamePhysics.motoParam8 = 65536 GamePhysics.motoParam9 = 1310720 GamePhysics.motoParam10 = 21626880 } this.resetSmth(true) } resetSmth(unused: boolean): void { void unused this.unusedCounter44 = 0 this.initializeBikeState(this.levelLoader.getStartPosX(), this.levelLoader.getStartPosY()) this.wheelTorqueF16 = 0 this.rotationSpeedF16 = 0 this.isCrashed = false this.isRiderDown = false this.finishTriggerLatched = false this.isTrackFinished = false this.isGenerateInputAI = false this.unusedFlag41 = false this.unusedFlag42 = false this.levelLoader.gameLevel?.setShadowProjectionRange( this.bikeParts[2].motoComponents[5].xF16 + 98304 - GamePhysics.const175_1_half[0], this.bikeParts[1].motoComponents[5].xF16 - 98304 + GamePhysics.const175_1_half[0], ) } applyPerspectiveOffset(var1: boolean): void { const var2 = (var1 ? 65536 : -65536) << 1 for (let var3 = 0; var3 < 6; ++var3) { for (let var4 = 0; var4 < 6; ++var4) { this.bikeParts[var3].motoComponents[var4].yF16 += var2 } } } private initializeBikeState(var1: number, var2: number): void { if (this.bikeParts.length === 0) { this.bikeParts = new Array(6) } if (this.constraints.length === 0) { this.constraints = new Array(10) } let var4 = 0 let var5 = 0 let var6 = 0 let var7 = 0 for (let i = 0; i < 6; ++i) { let var8 = 0 switch (i) { case 0: var5 = 1 var4 = 360448 var6 = 0 var7 = 0 break case 1: var5 = 0 var4 = 98304 var6 = 229376 var7 = 0 break case 2: var5 = 0 var4 = 360448 var6 = -229376 var7 = 0 var8 = 21626 break case 3: var5 = 1 var4 = 229376 var6 = 131072 var7 = 196608 break case 4: var5 = 1 var4 = 229376 var6 = -131072 var7 = 196608 break case 5: var5 = 2 var4 = 294912 var6 = 0 var7 = 327680 } if (!this.bikeParts[i]) { this.bikeParts[i] = new class_10() } this.bikeParts[i].reset() this.bikeParts[i].collisionRadiusF16 = GamePhysics.const175_1_half[var5] this.bikeParts[i].collisionShapeIndex = var5 this.bikeParts[i].inverseMassF16 = multiplyF16(divideF16(65536, var4), GamePhysics.massScaleF16) this.bikeParts[i].motoComponents[this.index01].xF16 = var1 + var6 this.bikeParts[i].motoComponents[this.index01].yF16 = var2 + var7 this.bikeParts[i].motoComponents[5].xF16 = var1 + var6 this.bikeParts[i].motoComponents[5].yF16 = var2 + var7 this.bikeParts[i].angularForceFactorF16 = var8 } for (let i = 0; i < 10; ++i) { if (!this.constraints[i]) { this.constraints[i] = new TimerOrMotoPartOrMenuElem() } this.constraints[i].resetState() this.constraints[i].xF16 = GamePhysics.motoParam10 this.constraints[i].angleF16 = GamePhysics.constraintAngleF16 } this.constraints[0].yF16 = 229376 this.constraints[1].yF16 = 229376 this.constraints[2].yF16 = 236293 this.constraints[3].yF16 = 236293 this.constraints[4].yF16 = 262144 this.constraints[5].yF16 = 219814 this.constraints[6].yF16 = 219814 this.constraints[7].yF16 = 185363 this.constraints[8].yF16 = 185363 this.constraints[9].yF16 = 327680 this.constraints[5].angleF16 = multiplyF16(GamePhysics.constraintAngleF16, 45875) this.constraints[6].xF16 = multiplyF16(6553, GamePhysics.motoParam10) this.constraints[5].xF16 = multiplyF16(6553, GamePhysics.motoParam10) this.constraints[9].xF16 = multiplyF16(72089, GamePhysics.motoParam10) this.constraints[8].xF16 = multiplyF16(72089, GamePhysics.motoParam10) this.constraints[7].xF16 = multiplyF16(72089, GamePhysics.motoParam10) } setRenderMinMaxX(minX: number, maxX: number): void { this.levelLoader.setMinMaxX(minX, maxX) } processPointerReleased(): void { this.isInputUp = false this.isInputDown = false this.isInputRight = false this.isInputLeft = false } setInputDirection(var1: number, var2: number): void { if (!this.isGenerateInputAI) { this.isInputUp = false this.isInputDown = false this.isInputRight = false this.isInputLeft = false if (var1 > 0) { this.isInputUp = true } else if (var1 < 0) { this.isInputDown = true } if (var2 > 0) { this.isInputRight = true return } if (var2 < 0) { this.isInputLeft = true } } } enableGenerateInputAI(): void { this.resetSmth(true) this.isGenerateInputAI = true } disableGenerateInputAI(): void { this.isGenerateInputAI = false } private setInputFromAI(): void { const var1 = this.bikeParts[1].motoComponents[this.index01].xF16 - this.bikeParts[2].motoComponents[this.index01].xF16 let var2 = this.bikeParts[1].motoComponents[this.index01].yF16 - this.bikeParts[2].motoComponents[this.index01].yF16 const var3 = GamePhysics.getSmthLikeMaxAbs(var1, var2) var2 = divideF16(var2, var3) this.isInputBreak = false if (var2 < 0) { this.isInputBack = true this.isInputForward = false } else if (var2 > 0) { this.isInputForward = true this.isInputBack = false } const var4 = (this.bikeParts[2].motoComponents[this.index01].yF16 - this.bikeParts[0].motoComponents[this.index01].yF16 > 0 ? 1 : -1) * (this.bikeParts[2].motoComponents[this.index01].velocityXF16 - this.bikeParts[0].motoComponents[this.index01].velocityXF16 > 0 ? 1 : -1) > 0 if ((!var4 || !this.isInputForward) && (var4 || !this.isInputBack)) { this.isInputAcceleration = false } else { this.isInputAcceleration = true } } private updateBikeControl(): void { if (!this.isCrashed) { let var1 = this.bikeParts[1].motoComponents[this.index01].xF16 - this.bikeParts[2].motoComponents[this.index01].xF16 let var2 = this.bikeParts[1].motoComponents[this.index01].yF16 - this.bikeParts[2].motoComponents[this.index01].yF16 const var3 = GamePhysics.getSmthLikeMaxAbs(var1, var2) var1 = divideF16(var1, var3) var2 = divideF16(var2, var3) if (this.isInputAcceleration && this.wheelTorqueF16 >= -GamePhysics.motoParam4) { this.wheelTorqueF16 -= GamePhysics.motoParam5 } if (this.isInputBreak) { this.wheelTorqueF16 = 0 this.bikeParts[1].motoComponents[this.index01].angularVelocityF16 = multiplyF16(this.bikeParts[1].motoComponents[this.index01].angularVelocityF16, 65536 - GamePhysics.motoParam6) this.bikeParts[2].motoComponents[this.index01].angularVelocityF16 = multiplyF16(this.bikeParts[2].motoComponents[this.index01].angularVelocityF16, 65536 - GamePhysics.motoParam6) if (this.bikeParts[1].motoComponents[this.index01].angularVelocityF16 < 6553) { this.bikeParts[1].motoComponents[this.index01].angularVelocityF16 = 0 } if (this.bikeParts[2].motoComponents[this.index01].angularVelocityF16 < 6553) { this.bikeParts[2].motoComponents[this.index01].angularVelocityF16 = 0 } } this.bikeParts[0].inverseMassF16 = multiplyF16(11915, GamePhysics.massScaleF16) this.bikeParts[0].inverseMassF16 = multiplyF16(11915, GamePhysics.massScaleF16) this.bikeParts[4].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[3].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[1].inverseMassF16 = multiplyF16(43690, GamePhysics.massScaleF16) this.bikeParts[2].inverseMassF16 = multiplyF16(11915, GamePhysics.massScaleF16) this.bikeParts[5].inverseMassF16 = multiplyF16(14563, GamePhysics.massScaleF16) if (this.isInputBack) { this.bikeParts[0].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[4].inverseMassF16 = multiplyF16(14563, GamePhysics.massScaleF16) this.bikeParts[3].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[1].inverseMassF16 = multiplyF16(43690, GamePhysics.massScaleF16) this.bikeParts[2].inverseMassF16 = multiplyF16(10082, GamePhysics.massScaleF16) } else if (this.isInputForward) { this.bikeParts[0].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[4].inverseMassF16 = multiplyF16(18724, GamePhysics.massScaleF16) this.bikeParts[3].inverseMassF16 = multiplyF16(14563, GamePhysics.massScaleF16) this.bikeParts[1].inverseMassF16 = multiplyF16(26214, GamePhysics.massScaleF16) this.bikeParts[2].inverseMassF16 = multiplyF16(11915, GamePhysics.massScaleF16) } if (this.isInputBack || this.isInputForward) { const var4 = -var2 let var6: number let var7: number let var8: number let var9: number let var10: number let var11: number if (this.isInputBack && this.rotationSpeedF16 > -GamePhysics.motoParam9) { var6 = 65536 if (this.rotationSpeedF16 < 0) { var6 = divideF16(GamePhysics.motoParam9 - abs(this.rotationSpeedF16), GamePhysics.motoParam9) } var7 = multiplyF16(GamePhysics.motoParam8, var6) var8 = multiplyF16(var4, var7) var9 = multiplyF16(var1, var7) var10 = multiplyF16(var1, var7) var11 = multiplyF16(var2, var7) if (this.riderPoseBlendF16 > 32768) { this.riderPoseBlendF16 = this.riderPoseBlendF16 - 1638 < 0 ? 0 : this.riderPoseBlendF16 - 1638 } else { this.riderPoseBlendF16 = this.riderPoseBlendF16 - 3276 < 0 ? 0 : this.riderPoseBlendF16 - 3276 } this.bikeParts[4].motoComponents[this.index01].velocityXF16 -= var8 this.bikeParts[4].motoComponents[this.index01].velocityYF16 -= var9 this.bikeParts[3].motoComponents[this.index01].velocityXF16 += var8 this.bikeParts[3].motoComponents[this.index01].velocityYF16 += var9 this.bikeParts[5].motoComponents[this.index01].velocityXF16 -= var10 this.bikeParts[5].motoComponents[this.index01].velocityYF16 -= var11 } if (this.isInputForward && this.rotationSpeedF16 < GamePhysics.motoParam9) { var6 = 65536 if (this.rotationSpeedF16 > 0) { var6 = divideF16(GamePhysics.motoParam9 - this.rotationSpeedF16, GamePhysics.motoParam9) } var7 = multiplyF16(GamePhysics.motoParam8, var6) var8 = multiplyF16(var4, var7) var9 = multiplyF16(var1, var7) var10 = multiplyF16(var1, var7) var11 = multiplyF16(var2, var7) if (this.riderPoseBlendF16 > 32768) { this.riderPoseBlendF16 = this.riderPoseBlendF16 + 1638 < 65536 ? this.riderPoseBlendF16 + 1638 : 65536 } else { this.riderPoseBlendF16 = this.riderPoseBlendF16 + 3276 < 65536 ? this.riderPoseBlendF16 + 3276 : 65536 } this.bikeParts[4].motoComponents[this.index01].velocityXF16 += var8 this.bikeParts[4].motoComponents[this.index01].velocityYF16 += var9 this.bikeParts[3].motoComponents[this.index01].velocityXF16 -= var8 this.bikeParts[3].motoComponents[this.index01].velocityYF16 -= var9 this.bikeParts[5].motoComponents[this.index01].velocityXF16 += var10 this.bikeParts[5].motoComponents[this.index01].velocityYF16 += var11 } return } if (this.riderPoseBlendF16 < 26214) { this.riderPoseBlendF16 += 3276 return } if (this.riderPoseBlendF16 > 39321) { this.riderPoseBlendF16 -= 3276 return } this.riderPoseBlendF16 = 32768 } } updatePhysics(): number { this.isInputAcceleration = this.isInputUp this.isInputBreak = this.isInputDown this.isInputBack = this.isInputLeft this.isInputForward = this.isInputRight if (this.isGenerateInputAI) { this.setInputFromAI() } GameCanvas.advanceFlagAnimation() this.updateBikeControl() const var1 = this.solvePhysicsStep(GamePhysics.physicsStepCount) if (var1 !== 5 && !this.isRiderDown) { if (this.isCrashed) { return 3 } if (this.isTrackStarted()) { this.isTrackFinished = false return 4 } return var1 } return 5 } isTrackStarted(): boolean { return this.bikeParts[1].motoComponents[this.index01].xF16 < this.levelLoader.getStartX() } hasPassedFinishLine(): boolean { return this.bikeParts[1].motoComponents[this.index10].xF16 > this.levelLoader.getFinishX() || this.bikeParts[2].motoComponents[this.index10].xF16 > this.levelLoader.getFinishX() } private solvePhysicsStep(var1: number): number { const var2 = this.finishTriggerLatched let var3 = 0 let var4 = var1 while (true) { while (var3 < var1) { this.stepSimulation(var4 - var3) const var5 = !var2 && this.hasPassedFinishLine() ? 3 : this.detectCollisionForStep(this.index10) if (!var2 && this.finishTriggerLatched) { return var5 !== 3 ? 2 : 1 } if (var5 === 0) { var4 = (var3 + var4) >> 1 continue } if (var5 === 3) { this.finishTriggerLatched = true var4 = (var3 + var4) >> 1 } else { if (var5 === 1) { let var6 = 0 do { this.resolveCollision(this.index10) var6 = this.detectCollisionForStep(this.index10) if (var6 === 0) { return 5 } } while (var6 !== 2) } var3 = var4 var4 = var1 this.index01 = this.index01 === 1 ? 0 : 1 this.index10 = this.index10 === 1 ? 0 : 1 } } const var5 = multiplyF16( this.bikeParts[1].motoComponents[this.index01].xF16 - this.bikeParts[2].motoComponents[this.index01].xF16, this.bikeParts[1].motoComponents[this.index01].xF16 - this.bikeParts[2].motoComponents[this.index01].xF16, ) + multiplyF16( this.bikeParts[1].motoComponents[this.index01].yF16 - this.bikeParts[2].motoComponents[this.index01].yF16, this.bikeParts[1].motoComponents[this.index01].yF16 - this.bikeParts[2].motoComponents[this.index01].yF16, ) if (var5 < 983040) { this.isCrashed = true } if (var5 > 4587520) { this.isCrashed = true } return 0 } } private accumulateForces(var1: number): void { for (let var4 = 0; var4 < 6; ++var4) { const var2 = this.bikeParts[var4] const var3 = var2.motoComponents[var1] var3.forceXF16 = 0 var3.forceYF16 = 0 var3.torqueF16 = 0 var3.forceYF16 -= divideF16(GamePhysics.gravityForceF16, var2.inverseMassF16) } if (!this.isCrashed) { this.solveConstraint(this.bikeParts[0], this.constraints[1], this.bikeParts[2], var1, 65536) this.solveConstraint(this.bikeParts[0], this.constraints[0], this.bikeParts[1], var1, 65536) this.solveConstraint(this.bikeParts[2], this.constraints[6], this.bikeParts[4], var1, 131072) this.solveConstraint(this.bikeParts[1], this.constraints[5], this.bikeParts[3], var1, 131072) } this.solveConstraint(this.bikeParts[0], this.constraints[2], this.bikeParts[3], var1, 65536) this.solveConstraint(this.bikeParts[0], this.constraints[3], this.bikeParts[4], var1, 65536) this.solveConstraint(this.bikeParts[3], this.constraints[4], this.bikeParts[4], var1, 65536) this.solveConstraint(this.bikeParts[5], this.constraints[8], this.bikeParts[3], var1, 65536) this.solveConstraint(this.bikeParts[5], this.constraints[7], this.bikeParts[4], var1, 65536) this.solveConstraint(this.bikeParts[5], this.constraints[9], this.bikeParts[0], var1, 65536) const var3 = this.bikeParts[2].motoComponents[var1] this.wheelTorqueF16 = multiplyF16(this.wheelTorqueF16, 65536 - GamePhysics.torqueDampingF16) var3.torqueF16 = this.wheelTorqueF16 if (var3.angularVelocityF16 > GamePhysics.motoParam3) { var3.angularVelocityF16 = GamePhysics.motoParam3 } if (var3.angularVelocityF16 < -GamePhysics.motoParam3) { var3.angularVelocityF16 = -GamePhysics.motoParam3 } let var4 = 0 let var5 = 0 for (let var6 = 0; var6 < 6; ++var6) { var4 += this.bikeParts[var6].motoComponents[var1].velocityXF16 var5 += this.bikeParts[var6].motoComponents[var1].velocityYF16 } var4 = divideF16(var4, 393216) var5 = divideF16(var5, 393216) let var10 = 0 for (let var11 = 0; var11 < 6; ++var11) { const var6 = this.bikeParts[var11].motoComponents[var1].velocityXF16 - var4 const var7 = this.bikeParts[var11].motoComponents[var1].velocityYF16 - var5 if ((var10 = GamePhysics.getSmthLikeMaxAbs(var6, var7)) > 1966080) { const var8 = divideF16(var6, var10) const var9 = divideF16(var7, var10) this.bikeParts[var11].motoComponents[var1].velocityXF16 -= var8 this.bikeParts[var11].motoComponents[var1].velocityYF16 -= var9 } } const var11 = this.bikeParts[2].motoComponents[var1].yF16 - this.bikeParts[0].motoComponents[var1].yF16 >= 0 ? 1 : -1 const var12 = this.bikeParts[2].motoComponents[var1].velocityXF16 - this.bikeParts[0].motoComponents[var1].velocityXF16 >= 0 ? 1 : -1 this.rotationSpeedF16 = var11 * var12 > 0 ? var10 : -var10 } static getSmthLikeMaxAbs(xF16: number, yF16: number): number { const absXF16 = abs(xF16) const absYF16 = abs(yF16) let maxAbs: number let minAbs: number if (absYF16 >= absXF16) { maxAbs = absYF16 minAbs = absXF16 } else { maxAbs = absXF16 minAbs = absYF16 } return multiplyF16(64448, maxAbs) + multiplyF16(28224, minAbs) } private solveConstraint(var1: class_10, var2: TimerOrMotoPartOrMenuElem, var3: class_10, var4: number, var5: number): void { const var6 = var1.motoComponents[var4] const var7 = var3.motoComponents[var4] let var8 = var6.xF16 - var7.xF16 let var9 = var6.yF16 - var7.yF16 const var10 = GamePhysics.getSmthLikeMaxAbs(var8, var9) if (abs(var10) >= 3) { var8 = divideF16(var8, var10) var9 = divideF16(var9, var10) const var11 = var10 - var2.yF16 let var12 = multiplyF16(var8, multiplyF16(var11, var2.xF16)) let var13 = multiplyF16(var9, multiplyF16(var11, var2.xF16)) const var14 = var6.velocityXF16 - var7.velocityXF16 const var15 = var6.velocityYF16 - var7.velocityYF16 const var16 = multiplyF16(multiplyF16(var8, var14) + multiplyF16(var9, var15), var2.angleF16) var12 += multiplyF16(var8, var16) var13 += multiplyF16(var9, var16) var12 = multiplyF16(var12, var5) var13 = multiplyF16(var13, var5) var6.forceXF16 -= var12 var6.forceYF16 -= var13 var7.forceXF16 += var12 var7.forceYF16 += var13 } } private integrateVelocities(var1: number, var2: number, var3: number): void { for (let var7 = 0; var7 < 6; ++var7) { const var4 = this.bikeParts[var7].motoComponents[var1] const var5 = this.bikeParts[var7].motoComponents[var2] var5.xF16 = multiplyF16(var4.velocityXF16, var3) var5.yF16 = multiplyF16(var4.velocityYF16, var3) const var6 = multiplyF16(var3, this.bikeParts[var7].inverseMassF16) var5.velocityXF16 = multiplyF16(var4.forceXF16, var6) var5.velocityYF16 = multiplyF16(var4.forceYF16, var6) } } private blendState(var1: number, var2: number, var3: number): void { for (let var7 = 0; var7 < 6; ++var7) { const var4 = this.bikeParts[var7].motoComponents[var1] const var5 = this.bikeParts[var7].motoComponents[var2] const var6 = this.bikeParts[var7].motoComponents[var3] var4.xF16 = var5.xF16 + (var6.xF16 >> 1) var4.yF16 = var5.yF16 + (var6.yF16 >> 1) var4.velocityXF16 = var5.velocityXF16 + (var6.velocityXF16 >> 1) var4.velocityYF16 = var5.velocityYF16 + (var6.velocityYF16 >> 1) } } private stepSimulation(var1: number): void { this.accumulateForces(this.index01) this.integrateVelocities(this.index01, 2, var1) this.blendState(4, this.index01, 2) this.accumulateForces(4) this.integrateVelocities(4, 3, var1 >> 1) this.blendState(4, this.index01, 3) this.blendState(this.index10, this.index01, 2) this.blendState(this.index10, this.index10, 3) for (let var4 = 1; var4 <= 2; ++var4) { const var2 = this.bikeParts[var4].motoComponents[this.index01] const var3 = this.bikeParts[var4].motoComponents[this.index10] var3.angleF16 = var2.angleF16 + multiplyF16(var1, var2.angularVelocityF16) var3.angularVelocityF16 = var2.angularVelocityF16 + multiplyF16(var1, multiplyF16(this.bikeParts[var4].angularForceFactorF16, var2.torqueF16)) } } private detectCollisionForStep(var1: number): number { let var2 = 2 const var4 = max3(this.bikeParts[1].motoComponents[var1].xF16, this.bikeParts[2].motoComponents[var1].xF16, this.bikeParts[5].motoComponents[var1].xF16) const var5 = min3(this.bikeParts[1].motoComponents[var1].xF16, this.bikeParts[2].motoComponents[var1].xF16, this.bikeParts[5].motoComponents[var1].xF16) this.levelLoader.updateVisiblePointRange(var5 - GamePhysics.const175_1_half[0], var4 + GamePhysics.const175_1_half[0], this.bikeParts[5].motoComponents[var1].yF16) let var6 = this.bikeParts[1].motoComponents[var1].xF16 - this.bikeParts[2].motoComponents[var1].xF16 const var7 = this.bikeParts[1].motoComponents[var1].yF16 - this.bikeParts[2].motoComponents[var1].yF16 const var8 = GamePhysics.getSmthLikeMaxAbs(var6, var7) var6 = divideF16(var6, var8) const var9 = -divideF16(var7, var8) const var10 = var6 for (let var11 = 0; var11 < 6; ++var11) { if (var11 !== 4 && var11 !== 3) { const var3 = this.bikeParts[var11].motoComponents[var1] if (var11 === 0) { var3.xF16 += var9 var3.yF16 += var10 } const var12 = this.levelLoader.detectCollision(var3, this.bikeParts[var11].collisionShapeIndex) if (var11 === 0) { var3.xF16 -= var9 var3.yF16 -= var10 } this.collisionNormalXF16 = this.levelLoader.collisionNormalXF16 this.collisionNormalYF16 = this.levelLoader.collisionNormalYF16 if (var11 === 5 && var12 !== 2) { this.isRiderDown = true } if (var11 === 1 && var12 !== 2) { this.isTrackFinished = true } if (var12 === 1) { this.collisionBodyIndex = var11 var2 = 1 } else if (var12 === 0) { this.collisionBodyIndex = var11 var2 = 0 break } } } return var2 } private resolveCollision(var1: number): void { const var2 = this.bikeParts[this.collisionBodyIndex] const var3 = var2.motoComponents[var1] var3.xF16 += multiplyF16(this.collisionNormalXF16, 3276) var3.yF16 += multiplyF16(this.collisionNormalYF16, 3276) let var4: number let var5: number let var6: number let var7: number let var8: number if (this.isInputBreak && (this.collisionBodyIndex === 2 || this.collisionBodyIndex === 1) && var3.angularVelocityF16 < 6553) { var4 = GamePhysics.tireGripF16 - GamePhysics.motoParam7 var5 = 13107 var6 = 39321 var7 = 26214 - GamePhysics.motoParam7 var8 = 26214 - GamePhysics.motoParam7 } else { var4 = GamePhysics.tireGripF16 var5 = GamePhysics.tireDampingF16 var6 = GamePhysics.tireAngularDampingF16 var7 = GamePhysics.motoParam1 var8 = GamePhysics.motoParam2 } const var9 = GamePhysics.getSmthLikeMaxAbs(this.collisionNormalXF16, this.collisionNormalYF16) this.collisionNormalXF16 = divideF16(this.collisionNormalXF16, var9) this.collisionNormalYF16 = divideF16(this.collisionNormalYF16, var9) const var10 = var3.velocityXF16 const var11 = var3.velocityYF16 const var12 = -(multiplyF16(var10, this.collisionNormalXF16) + multiplyF16(var11, this.collisionNormalYF16)) const var13 = -(multiplyF16(var10, -this.collisionNormalYF16) + multiplyF16(var11, this.collisionNormalXF16)) const var14 = multiplyF16(var4, var3.angularVelocityF16) - multiplyF16(var5, divideF16(var13, var2.collisionRadiusF16)) const var15 = multiplyF16(var7, var13) - multiplyF16(var6, multiplyF16(var3.angularVelocityF16, var2.collisionRadiusF16)) const var16 = -multiplyF16(var8, var12) const var17 = multiplyF16(-var15, -this.collisionNormalYF16) const var18 = multiplyF16(-var15, this.collisionNormalXF16) const var19 = multiplyF16(-var16, this.collisionNormalXF16) const var20 = multiplyF16(-var16, this.collisionNormalYF16) var3.angularVelocityF16 = var14 var3.velocityXF16 = var17 + var19 var3.velocityYF16 = var18 + var20 } setEnableLookAhead(value: boolean): void { this.isEnableLookAhead = value } setMinimalScreenWH(minWH: number): void { this.lookAheadClampF16 = divideF16(multiplyF16(655360, minWH << 16), 8388608) } getCamPosX(): number { if (this.isEnableLookAhead) { this.camShiftX = divideF16(this.motoComponents[0].velocityXF16, 1572864) + multiplyF16(this.camShiftX, 57344) } else { this.camShiftX = 0 } this.camShiftX = this.camShiftX < this.lookAheadClampF16 ? this.camShiftX : this.lookAheadClampF16 this.camShiftX = this.camShiftX < -this.lookAheadClampF16 ? -this.lookAheadClampF16 : this.camShiftX return ((this.motoComponents[0].xF16 + this.camShiftX) << 2) >> 16 } getCamPosY(): number { if (this.isEnableLookAhead) { this.camShiftY = divideF16(this.motoComponents[0].velocityYF16, 1572864) + multiplyF16(this.camShiftY, 57344) } else { this.camShiftY = 0 } this.camShiftY = this.camShiftY < this.lookAheadClampF16 ? this.camShiftY : this.lookAheadClampF16 this.camShiftY = this.camShiftY < -this.lookAheadClampF16 ? -this.lookAheadClampF16 : this.camShiftY return ((this.motoComponents[0].yF16 + this.camShiftY) << 2) >> 16 } getProgressF16(): number { const var1 = this.motoComponents[1].xF16 < this.motoComponents[2].xF16 ? this.motoComponents[2].xF16 : this.motoComponents[1].xF16 return this.isCrashed ? this.levelLoader.getProgressF16(this.motoComponents[0].xF16) : this.levelLoader.getProgressF16(var1) } syncRenderStateFromSimulation(): void { for (let var2 = 0; var2 < 6; ++var2) { this.bikeParts[var2].motoComponents[5].xF16 = this.bikeParts[var2].motoComponents[this.index01].xF16 this.bikeParts[var2].motoComponents[5].yF16 = this.bikeParts[var2].motoComponents[this.index01].yF16 this.bikeParts[var2].motoComponents[5].angleF16 = this.bikeParts[var2].motoComponents[this.index01].angleF16 } this.bikeParts[0].motoComponents[5].velocityXF16 = this.bikeParts[0].motoComponents[this.index01].velocityXF16 this.bikeParts[0].motoComponents[5].velocityYF16 = this.bikeParts[0].motoComponents[this.index01].velocityYF16 this.bikeParts[2].motoComponents[5].angularVelocityF16 = this.bikeParts[2].motoComponents[this.index01].angularVelocityF16 } setMotoComponents(): void { for (let i = 0; i < 6; ++i) { this.motoComponents[i].xF16 = this.bikeParts[i].motoComponents[5].xF16 this.motoComponents[i].yF16 = this.bikeParts[i].motoComponents[5].yF16 this.motoComponents[i].angleF16 = this.bikeParts[i].motoComponents[5].angleF16 } this.motoComponents[0].velocityXF16 = this.bikeParts[0].motoComponents[5].velocityXF16 this.motoComponents[0].velocityYF16 = this.bikeParts[0].motoComponents[5].velocityYF16 this.motoComponents[2].angularVelocityF16 = this.bikeParts[2].motoComponents[5].angularVelocityF16 } private renderEngine(gameCanvas: GameCanvas, var2: number, var3: number): void { const engineAngle4F16 = MathF16.atan2F16(this.motoComponents[0].xF16 - this.motoComponents[3].xF16, this.motoComponents[0].yF16 - this.motoComponents[3].yF16) const fenderAngle4F16 = MathF16.atan2F16(this.motoComponents[0].xF16 - this.motoComponents[4].xF16, this.motoComponents[0].yF16 - this.motoComponents[4].yF16) let engineXF16 = (this.motoComponents[0].xF16 >> 1) + (this.motoComponents[3].xF16 >> 1) let engineYF16 = (this.motoComponents[0].yF16 >> 1) + (this.motoComponents[3].yF16 >> 1) let fenderXF16 = (this.motoComponents[0].xF16 >> 1) + (this.motoComponents[4].xF16 >> 1) let fenderYF16 = (this.motoComponents[0].yF16 >> 1) + (this.motoComponents[4].yF16 >> 1) const var10 = -var3 engineXF16 += multiplyF16(var10, 65536) - multiplyF16(var2, 32768) engineYF16 += multiplyF16(var2, 65536) - multiplyF16(var3, 32768) fenderXF16 += multiplyF16(var10, 65536) - multiplyF16(var2, 117964) fenderYF16 += multiplyF16(var2, 65536) - multiplyF16(var3, 131072) gameCanvas.renderFender((fenderXF16 << 2) >> 16, (fenderYF16 << 2) >> 16, fenderAngle4F16) gameCanvas.renderEngine((engineXF16 << 2) >> 16, (engineYF16 << 2) >> 16, engineAngle4F16) } private renderMotoFork(canvas: GameCanvas): void { canvas.setColor(128, 128, 128) canvas.drawLineF16(this.motoComponents[3].xF16, this.motoComponents[3].yF16, this.motoComponents[1].xF16, this.motoComponents[1].yF16) } private renderWheelTires(canvas: GameCanvas): void { let backWheelIsThin = 1 let forwardWheelIsThin = 1 switch (GamePhysics.curentMotoLeague) { case 1: backWheelIsThin = 0 break case 2: case 3: forwardWheelIsThin = 0 backWheelIsThin = 0 } canvas.drawWheelTires((this.motoComponents[2].xF16 << 2) >> 16, (this.motoComponents[2].yF16 << 2) >> 16, backWheelIsThin) canvas.drawWheelTires((this.motoComponents[1].xF16 << 2) >> 16, (this.motoComponents[1].yF16 << 2) >> 16, forwardWheelIsThin) } private renderWheelSpokes(gameCanvas: GameCanvas): void { const var2 = this.bikeParts[1].collisionRadiusF16 const xxxF16 = multiplyF16(var2, 58982) const yyyF16 = multiplyF16(var2, 45875) gameCanvas.setColor(0, 0, 0) if (Micro.isInGameMenu) { gameCanvas.drawCircle((this.motoComponents[1].xF16 << 2) >> 16, (this.motoComponents[1].yF16 << 2) >> 16, ((var2 + var2) << 2) >> 16) gameCanvas.drawCircle((this.motoComponents[1].xF16 << 2) >> 16, (this.motoComponents[1].yF16 << 2) >> 16, ((xxxF16 + xxxF16) << 2) >> 16) gameCanvas.drawCircle((this.motoComponents[2].xF16 << 2) >> 16, (this.motoComponents[2].yF16 << 2) >> 16, ((var2 + var2) << 2) >> 16) gameCanvas.drawCircle((this.motoComponents[2].xF16 << 2) >> 16, (this.motoComponents[2].yF16 << 2) >> 16, ((yyyF16 + yyyF16) << 2) >> 16) } let var6 = 0 let angle = this.motoComponents[1].angleF16 let cosF16 = MathF16.cosF16(angle) let sinF16 = MathF16.sinF16(angle) let dxF16 = multiplyF16(cosF16, xxxF16) + multiplyF16(-sinF16, var6) let dyF16 = multiplyF16(sinF16, xxxF16) + multiplyF16(cosF16, var6) angle = 82354 cosF16 = MathF16.cosF16(82354) sinF16 = MathF16.sinF16(angle) for (let i = 0; i < 5; ++i) { gameCanvas.drawLineF16(this.motoComponents[1].xF16, this.motoComponents[1].yF16, this.motoComponents[1].xF16 + dxF16, this.motoComponents[1].yF16 + dyF16) const var10 = dxF16 dxF16 = multiplyF16(cosF16, dxF16) + multiplyF16(-sinF16, dyF16) dyF16 = multiplyF16(sinF16, var10) + multiplyF16(cosF16, dyF16) } var6 = 0 angle = this.motoComponents[2].angleF16 cosF16 = MathF16.cosF16(angle) sinF16 = MathF16.sinF16(angle) dxF16 = multiplyF16(cosF16, xxxF16) + multiplyF16(-sinF16, var6) dyF16 = multiplyF16(sinF16, xxxF16) + multiplyF16(cosF16, var6) angle = 82354 cosF16 = MathF16.cosF16(82354) sinF16 = MathF16.sinF16(angle) for (let i = 0; i < 5; ++i) { gameCanvas.drawLineF16(this.motoComponents[2].xF16, this.motoComponents[2].yF16, this.motoComponents[2].xF16 + dxF16, this.motoComponents[2].yF16 + dyF16) const var10 = dxF16 dxF16 = multiplyF16(cosF16, dxF16) + multiplyF16(-sinF16, dyF16) dyF16 = multiplyF16(sinF16, var10) + multiplyF16(cosF16, dyF16) } if (GamePhysics.curentMotoLeague > 0) { gameCanvas.setColor(255, 0, 0) if (GamePhysics.curentMotoLeague > 2) { gameCanvas.setColor(100, 100, 255) } gameCanvas.drawCircle((this.motoComponents[2].xF16 << 2) >> 16, (this.motoComponents[2].yF16 << 2) >> 16, 4) gameCanvas.drawCircle((this.motoComponents[1].xF16 << 2) >> 16, (this.motoComponents[1].yF16 << 2) >> 16, 4) } } private renderSmth(gameCanvas: GameCanvas, var2: number, var3: number, var4: number, var5: number): void { let var6 = 0 let var7 = 65536 const var8 = this.motoComponents[0].xF16 const var9 = this.motoComponents[0].yF16 let x6F16 = 0 let y6F16 = 0 let xF16 = 0 let yF16 = 0 let var14 = 0 let var15 = 0 let x2F16 = 0 let y2F16 = 0 let x3F16 = 0 let y3F16 = 0 let x4F16 = 0 let y4F16 = 0 let circleXF16 = 0 let circleYF16 = 0 let x5F16 = 0 let y5F16 = 0 let var27: number[][] = [] let var28: number[][] = [] let var29: number[][] = [] if (this.isRenderDriverWithSprites) { if (this.riderPoseBlendF16 < 32768) { var28 = this.hardcodedArr2 var29 = this.hardcodedArr1 var7 = multiplyF16(this.riderPoseBlendF16, 131072) } else if (this.riderPoseBlendF16 > 32768) { var6 = 1 var28 = this.hardcodedArr1 var29 = this.hardcodedArr3 var7 = multiplyF16(this.riderPoseBlendF16 - 32768, 131072) } else { var27 = this.hardcodedArr1 } } else if (this.riderPoseBlendF16 < 32768) { var28 = this.hardcodedArr5 var29 = this.hardcodedArr4 var7 = multiplyF16(this.riderPoseBlendF16, 131072) } else if (this.riderPoseBlendF16 > 32768) { var6 = 1 var28 = this.hardcodedArr4 var29 = this.hardcodedArr6 var7 = multiplyF16(this.riderPoseBlendF16 - 32768, 131072) } else { var27 = this.hardcodedArr4 } for (let var30 = 0; var30 < this.hardcodedArr1.length; ++var30) { let var31: number let var32: number if (var28.length !== 0) { var32 = multiplyF16(var28[var30][0], 65536 - var7) + multiplyF16(var29[var30][0], var7) var31 = multiplyF16(var28[var30][1], 65536 - var7) + multiplyF16(var29[var30][1], var7) } else { var32 = var27[var30][0] var31 = var27[var30][1] } const xxF16 = var8 + multiplyF16(var4, var32) + multiplyF16(var2, var31) const yyF16 = var9 + multiplyF16(var5, var32) + multiplyF16(var3, var31) switch (var30) { case 0: x2F16 = xxF16 y2F16 = yyF16 break case 1: x3F16 = xxF16 y3F16 = yyF16 break case 2: x4F16 = xxF16 y4F16 = yyF16 break case 3: circleXF16 = xxF16 circleYF16 = yyF16 break case 4: x5F16 = xxF16 y5F16 = yyF16 break case 5: xF16 = xxF16 yF16 = yyF16 break case 6: var14 = xxF16 var15 = yyF16 break case 7: x6F16 = xxF16 y6F16 = yyF16 } } const var26 = multiplyF16(this.riderPoseBlendTable[var6][0], 65536 - var7) + multiplyF16(this.riderPoseBlendTable[var6 + 1][0], var7) if (this.isRenderDriverWithSprites) { gameCanvas.renderBodyPart(xF16 << 2, yF16 << 2, x2F16 << 2, y2F16 << 2, 1) gameCanvas.renderBodyPart(x2F16 << 2, y2F16 << 2, x3F16 << 2, y3F16 << 2, 1) gameCanvas.renderBodyPart(x3F16 << 2, y3F16 << 2, x4F16 << 2, y4F16 << 2, 2, var26) gameCanvas.renderBodyPart(x4F16 << 2, y4F16 << 2, x5F16 << 2, y5F16 << 2, 0) let var30 = MathF16.atan2F16(var2, var3) if (this.riderPoseBlendF16 > 32768) { var30 += 20588 } gameCanvas.drawHelmet((circleXF16 << 2) >> 16, (circleYF16 << 2) >> 16, var30) } else { gameCanvas.setColor(0, 0, 0) gameCanvas.drawLineF16(xF16, yF16, x2F16, y2F16) gameCanvas.drawLineF16(x2F16, y2F16, x3F16, y3F16) gameCanvas.setColor(0, 0, 128) gameCanvas.drawLineF16(x3F16, y3F16, x4F16, y4F16) gameCanvas.drawLineF16(x4F16, y4F16, x5F16, y5F16) gameCanvas.drawLineF16(x5F16, y5F16, x6F16, y6F16) const var30 = 65536 gameCanvas.setColor(156, 0, 0) gameCanvas.drawCircle((circleXF16 << 2) >> 16, (circleYF16 << 2) >> 16, ((var30 + var30) << 2) >> 16) } gameCanvas.setColor(0, 0, 0) gameCanvas.drawForthSpriteByCenter((x6F16 << 2) >> 16, (y6F16 << 2) >> 16) gameCanvas.drawForthSpriteByCenter((var14 << 2) >> 16, (var15 << 2) >> 16) } private renderMotoAsLines(gameCanvas: GameCanvas, var2: number, var3: number, var4: number, var5: number): void { const var7 = this.motoComponents[2].xF16 const var8 = this.motoComponents[2].yF16 const var9 = var7 + multiplyF16(var4, 32768) const var10 = var8 + multiplyF16(var5, 32768) const var11 = var7 - multiplyF16(var4, 32768) const var12 = var8 - multiplyF16(var5, 32768) const var13 = this.motoComponents[0].xF16 + multiplyF16(var2, 32768) const var14 = this.motoComponents[0].yF16 + multiplyF16(var3, 32768) const var15 = var13 - multiplyF16(var2, 131072) const var16 = var14 - multiplyF16(var3, 131072) const var17 = var15 + multiplyF16(var4, 65536) const var18 = var16 + multiplyF16(var5, 65536) const var19 = var15 + multiplyF16(var2, 49152) + multiplyF16(var4, 49152) const var20 = var16 + multiplyF16(var3, 49152) + multiplyF16(var5, 49152) const var21 = var15 + multiplyF16(var4, 32768) const var22 = var16 + multiplyF16(var5, 32768) const var23 = this.motoComponents[1].xF16 const var24 = this.motoComponents[1].yF16 const var25 = this.motoComponents[4].xF16 - multiplyF16(var2, 49152) const var26 = this.motoComponents[4].yF16 - multiplyF16(var3, 49152) const var27 = var25 - multiplyF16(var4, 32768) const var28 = var26 - multiplyF16(var5, 32768) const var29 = var25 - multiplyF16(var2, 131072) + multiplyF16(var4, 16384) const var30 = var26 - multiplyF16(var3, 131072) + multiplyF16(var5, 16384) const var31 = this.motoComponents[3].xF16 const var32 = this.motoComponents[3].yF16 const var33 = var31 + multiplyF16(var4, 32768) const var34 = var32 + multiplyF16(var5, 32768) const var35 = var31 + multiplyF16(var4, 114688) - multiplyF16(var2, 32768) const var36 = var32 + multiplyF16(var5, 114688) - multiplyF16(var3, 32768) gameCanvas.setColor(50, 50, 50) gameCanvas.drawCircle((var21 << 2) >> 16, (var22 << 2) >> 16, ((32768 + 32768) << 2) >> 16) if (!this.isCrashed) { gameCanvas.drawLineF16(var9, var10, var17, var18) gameCanvas.drawLineF16(var11, var12, var15, var16) } gameCanvas.drawLineF16(var13, var14, var15, var16) gameCanvas.drawLineF16(var13, var14, var31, var32) gameCanvas.drawLineF16(var19, var20, var33, var34) gameCanvas.drawLineF16(var33, var34, var35, var36) if (!this.isCrashed) { gameCanvas.drawLineF16(var31, var32, var23, var24) gameCanvas.drawLineF16(var35, var36, var23, var24) } gameCanvas.drawLineF16(var17, var18, var27, var28) gameCanvas.drawLineF16(var19, var20, var25, var26) gameCanvas.drawLineF16(var25, var26, var29, var30) gameCanvas.drawLineF16(var27, var28, var29, var30) } renderGame(gameCanvas: GameCanvas): void { gameCanvas.clearScreenWithWhite() let xxF16 = this.motoComponents[3].xF16 - this.motoComponents[4].xF16 let yyF16 = this.motoComponents[3].yF16 - this.motoComponents[4].yF16 const maxAbs = GamePhysics.getSmthLikeMaxAbs(xxF16, yyF16) if (maxAbs !== 0) { xxF16 = divideF16(xxF16, maxAbs) yyF16 = divideF16(yyF16, maxAbs) } const var5 = -yyF16 if (this.isCrashed) { let var8 = this.motoComponents[4].xF16 let var7 = this.motoComponents[3].xF16 if (var7 >= var8) { const var9 = var7 var7 = var8 var8 = var9 } this.levelLoader.gameLevel?.setShadowProjectionRange(var7, var8) } if (LevelLoader.isEnabledPerspective) { this.levelLoader.renderLevel3D(gameCanvas, this.motoComponents[0].xF16, this.motoComponents[0].yF16) } if (this.isRenderMotoWithSprites) { this.renderEngine(gameCanvas, xxF16, yyF16) } if (!Micro.isInGameMenu) { this.renderWheelTires(gameCanvas) } this.renderWheelSpokes(gameCanvas) if (this.isRenderMotoWithSprites) { gameCanvas.setColor(170, 0, 0) } else { gameCanvas.setColor(50, 50, 50) } gameCanvas.drawWheelArc((this.motoComponents[1].xF16 << 2) >> 16, (this.motoComponents[1].yF16 << 2) >> 16, (GamePhysics.const175_1_half[0] << 2) >> 16, MathF16.atan2F16(xxF16, yyF16)) if (!this.isCrashed) { this.renderMotoFork(gameCanvas) } this.renderSmth(gameCanvas, xxF16, yyF16, var5, xxF16) if (!this.isRenderMotoWithSprites) { this.renderMotoAsLines(gameCanvas, xxF16, yyF16, var5, xxF16) } this.levelLoader.renderTrackNearestLine(gameCanvas) } } ===== FILE: src/IGameMenuElement.ts ===== import type { Graphics } from './lcdui/Graphics.ts' export interface IGameMenuElement { getText(): string setText(text: string): void render(graphics: Graphics, y: number, x: number): void isNotTextRender(): boolean menuElemMethod(var1: number): void } ===== FILE: src/IMenuManager.ts ===== import type { GameMenu } from './GameMenu.ts' import type { IGameMenuElement } from './IGameMenuElement.ts' export interface IMenuManager { getCurrentMenu(): GameMenu | null openMenu(menu: GameMenu | null, preserveSelection: boolean): void saveAndClose(): void handleMenuSelection(element: IGameMenuElement): void } ===== FILE: src/LevelLoader.ts ===== import { INT_MAX, INT_MIN, abs, divideF16, multiplyF16 } from './cpp.ts' import { GameCanvas } from './GameCanvas.ts' import { GameLevel } from './GameLevel.ts' import { GamePhysics } from './GamePhysics.ts' import { TimerOrMotoPartOrMenuElem } from './TimerOrMotoPartOrMenuElem.ts' import { FileStream } from './utils/FileStream.ts' export class LevelLoader { static readonly shapeIndex0 = 0 static readonly shapeIndex1 = 1 static readonly shapeIndex2 = 2 static readonly collisionResult0 = 0 static readonly collisionResult1 = 1 static isEnabledPerspective = true static isEnabledShadows = true private edgeNormalsF16: number[][] = [] private readonly outerCollisionRadiusSqByShape = [0, 0, 0] private readonly innerCollisionRadiusSqByShape = [0, 0, 0] private static levelOffsetInFile: number[][] = [[], [], []] private edgeNormalsCapacity = 0 private static visibleStartPointIndex = 0 private static visibleEndPointIndex = 0 private static visibleStartPointX = 0 private static visibleEndPointX = 0 private readonly levelFileStream: FileStream gameLevel: GameLevel | null = null currentLeagueIndex = 0 currentTrackIndex = -1 levelNames: string[][] = [[], [], []] startPosXF16 = 0 startPosYF16 = 0 maxTrackPointX = 0 collisionNormalXF16 = 0 collisionNormalYF16 = 0 static async create(levelsData: number[]): Promise { const levelFileStream = new FileStream(new Uint8Array(levelsData).buffer) return new LevelLoader(levelFileStream) } constructor(levelFileStream: FileStream) { for (let i = 0; i < 3; ++i) { const var1 = (GamePhysics.const175_1_half[i] + 19660) >> 1 const var2 = (GamePhysics.const175_1_half[i] - 19660) >> 1 this.outerCollisionRadiusSqByShape[i] = multiplyF16(var1, var1) this.innerCollisionRadiusSqByShape[i] = multiplyF16(var2, var2) } this.levelFileStream = levelFileStream this.loadLevels() this.loadNextLevel() } private loadLevels(): void { const var4 = [0, 0, 0] const decoder = new TextDecoder('ascii') for (let league = 0; league < 3; ++league) { var4[league] = this.levelFileStream.readInt32(true) LevelLoader.levelOffsetInFile[league] = new Array(var4[league]) this.levelNames[league] = new Array(var4[league]) for (let levelNo = 0; levelNo < var4[league]; ++levelNo) { const var7 = this.levelFileStream.readInt32(true) LevelLoader.levelOffsetInFile[league][levelNo] = var7 const raw = new Uint8Array(40) let zeroIndex = 40 for (let var8 = 0; var8 < 40; ++var8) { raw[var8] = this.levelFileStream.readInt8() if (raw[var8] === 0) { zeroIndex = var8 break } } const s = decoder.decode(raw.slice(0, zeroIndex)).replaceAll('_', ' ') this.levelNames[league][levelNo] = s } } } getName(league: number, level: number): string { return league < 3 && level < this.levelNames[league].length ? this.levelNames[league][level] : '---' } loadNextLevel(): void { this.loadLevel(this.currentLeagueIndex, this.currentTrackIndex + 1) } loadLevel(var1: number, var2: number): number { this.currentLeagueIndex = var1 this.currentTrackIndex = var2 if (this.currentTrackIndex >= this.levelNames[this.currentLeagueIndex].length) { this.currentTrackIndex = 0 } this.readLevelFromArchive(this.currentLeagueIndex + 1, this.currentTrackIndex + 1) return this.currentTrackIndex } readLevelFromArchive(var1: number, var2: number): void { this.levelFileStream.setPos(LevelLoader.levelOffsetInFile[var1 - 1][var2 - 1]) if (this.gameLevel === null) { this.gameLevel = new GameLevel() } this.gameLevel.load(this.levelFileStream) this.prepareLevelGeometry(this.gameLevel) } cacheStartPosition(var1: number): void { void var1 if (this.gameLevel === null) { return } this.startPosXF16 = this.gameLevel.startPosX << 1 this.startPosYF16 = this.gameLevel.startPosY << 1 } getFinishX(): number { if (this.gameLevel === null) { return 0 } return this.gameLevel.pointPositions[this.gameLevel.finishFlagPoint][0] << 1 } getStartX(): number { if (this.gameLevel === null) { return 0 } return this.gameLevel.pointPositions[this.gameLevel.startFlagPoint][0] << 1 } getStartPosX(): number { return this.gameLevel === null ? 0 : this.gameLevel.startPosX << 1 } getStartPosY(): number { return this.gameLevel === null ? 0 : this.gameLevel.startPosY << 1 } getProgressF16(var1: number): number { return this.gameLevel === null ? 0 : this.gameLevel.getProgressF16(var1 >> 1) } prepareLevelGeometry(gameLevel: GameLevel): void { this.maxTrackPointX = INT_MIN this.gameLevel = gameLevel const var2 = gameLevel.pointsCount if (this.edgeNormalsF16.length === 0 || this.edgeNormalsCapacity < var2) { this.edgeNormalsCapacity = var2 < 100 ? 100 : var2 this.edgeNormalsF16 = new Array(this.edgeNormalsCapacity) for (let i = 0; i < this.edgeNormalsCapacity; ++i) { this.edgeNormalsF16[i] = [0, 0] } } LevelLoader.visibleStartPointIndex = 0 LevelLoader.visibleEndPointIndex = 0 LevelLoader.visibleStartPointX = gameLevel.pointPositions[LevelLoader.visibleStartPointIndex][0] LevelLoader.visibleEndPointX = gameLevel.pointPositions[LevelLoader.visibleEndPointIndex][0] for (let var3 = 0; var3 < var2; ++var3) { const var4 = gameLevel.pointPositions[(var3 + 1) % var2][0] - gameLevel.pointPositions[var3][0] const var5 = gameLevel.pointPositions[(var3 + 1) % var2][1] - gameLevel.pointPositions[var3][1] if (var3 !== 0 && var3 !== var2 - 1) { this.maxTrackPointX = this.maxTrackPointX < gameLevel.pointPositions[var3][0] ? gameLevel.pointPositions[var3][0] : this.maxTrackPointX } const var6 = -var5 const var8 = GamePhysics.getSmthLikeMaxAbs(var6, var4) this.edgeNormalsF16[var3][0] = divideF16(var6, var8) this.edgeNormalsF16[var3][1] = divideF16(var4, var8) if (gameLevel.startFlagPoint === 0 && gameLevel.pointPositions[var3][0] > gameLevel.startPosX) { gameLevel.startFlagPoint = var3 + 1 } if (gameLevel.finishFlagPoint === 0 && gameLevel.pointPositions[var3][0] > gameLevel.finishPosX) { gameLevel.finishFlagPoint = var3 } } LevelLoader.visibleStartPointIndex = 0 LevelLoader.visibleEndPointIndex = 0 LevelLoader.visibleStartPointX = 0 LevelLoader.visibleEndPointX = 0 } setMinMaxX(minX: number, maxX: number): void { this.gameLevel?.setMinMaxX(minX, maxX) } renderLevel3D(gameCanvas: GameCanvas, xF16: number, yF16: number): void { if (this.gameLevel === null) { return } gameCanvas.setColor(110, 61, 34) xF16 >>= 1 yF16 >>= 1 this.gameLevel.renderLevel3D(gameCanvas, xF16, yF16) } renderTrackNearestLine(canvas: GameCanvas): void { if (this.gameLevel === null) { return } canvas.setColor(156, 97, 53) this.gameLevel.renderTrackNearestGreenLine(canvas) } updateVisiblePointRange(var1: number, var2: number, var3: number): void { if (this.gameLevel === null) { return } this.gameLevel.setShadowProjectionState((var1 + 98304) >> 1, (var2 - 98304) >> 1, var3 >> 1) var2 >>= 1 var1 >>= 1 LevelLoader.visibleEndPointIndex = LevelLoader.visibleEndPointIndex < this.gameLevel.pointsCount - 1 ? LevelLoader.visibleEndPointIndex : this.gameLevel.pointsCount - 1 LevelLoader.visibleStartPointIndex = LevelLoader.visibleStartPointIndex < 0 ? 0 : LevelLoader.visibleStartPointIndex if (var2 > LevelLoader.visibleEndPointX) { while (LevelLoader.visibleEndPointIndex < this.gameLevel.pointsCount - 1 && var2 > this.gameLevel.pointPositions[++LevelLoader.visibleEndPointIndex][0]) {} } else if (var1 < LevelLoader.visibleStartPointX) { while (LevelLoader.visibleStartPointIndex > 0 && var1 < this.gameLevel.pointPositions[--LevelLoader.visibleStartPointIndex][0]) {} } else { while (LevelLoader.visibleStartPointIndex < this.gameLevel.pointsCount && var1 > this.gameLevel.pointPositions[++LevelLoader.visibleStartPointIndex][0]) {} if (LevelLoader.visibleStartPointIndex > 0) { --LevelLoader.visibleStartPointIndex } while (LevelLoader.visibleEndPointIndex > 0 && var2 < this.gameLevel.pointPositions[--LevelLoader.visibleEndPointIndex][0]) {} LevelLoader.visibleEndPointIndex = LevelLoader.visibleEndPointIndex + 1 < this.gameLevel.pointsCount - 1 ? LevelLoader.visibleEndPointIndex + 1 : this.gameLevel.pointsCount - 1 } LevelLoader.visibleStartPointX = this.gameLevel.pointPositions[LevelLoader.visibleStartPointIndex][0] LevelLoader.visibleEndPointX = this.gameLevel.pointPositions[LevelLoader.visibleEndPointIndex][0] } detectCollision(var1: TimerOrMotoPartOrMenuElem, var2: number): number { if (this.gameLevel === null) { return 2 } let var16 = 0 let var17 = 2 const var18 = var1.xF16 >> 1 let var19 = var1.yF16 >> 1 if (LevelLoader.isEnabledPerspective) { var19 -= 65536 } let var20 = 0 let var21 = 0 for (let var22 = LevelLoader.visibleStartPointIndex; var22 < LevelLoader.visibleEndPointIndex; ++var22) { let var4 = this.gameLevel.pointPositions[var22][0] let var5 = this.gameLevel.pointPositions[var22][1] let var6 = this.gameLevel.pointPositions[var22 + 1][0] const var7 = this.gameLevel.pointPositions[var22 + 1][1] if (var18 - this.outerCollisionRadiusSqByShape[var2] <= var6 && var18 + this.outerCollisionRadiusSqByShape[var2] >= var4) { let var8 = var4 - var6 let var9 = var5 - var7 const var10 = multiplyF16(var8, var8) + multiplyF16(var9, var9) const var11 = multiplyF16(var18 - var4, -var8) + multiplyF16(var19 - var5, -var9) let var12: number if (abs(var10) >= 3) { var12 = divideF16(var11, var10) } else { var12 = (var11 > 0 ? 1 : -1) * (var10 > 0 ? 1 : -1) * INT_MAX } if (var12 < 0) { var12 = 0 } if (var12 > 65536) { var12 = 65536 } const var13 = var4 + multiplyF16(var12, -var8) const var14 = var5 + multiplyF16(var12, -var9) var8 = var18 - var13 var9 = var19 - var14 let var3: number const var23 = BigInt(multiplyF16(var8, var8)) + BigInt(multiplyF16(var9, var9)) if (var23 < BigInt(this.outerCollisionRadiusSqByShape[var2])) { if (var23 >= BigInt(this.innerCollisionRadiusSqByShape[var2])) { var3 = 1 } else { var3 = 0 } } else { var3 = 2 } if (var3 === 0 && multiplyF16(this.edgeNormalsF16[var22][0], var1.velocityXF16) + multiplyF16(this.edgeNormalsF16[var22][1], var1.velocityYF16) < 0) { this.collisionNormalXF16 = this.edgeNormalsF16[var22][0] this.collisionNormalYF16 = this.edgeNormalsF16[var22][1] return 0 } if (var3 === 1 && multiplyF16(this.edgeNormalsF16[var22][0], var1.velocityXF16) + multiplyF16(this.edgeNormalsF16[var22][1], var1.velocityYF16) < 0) { ++var16 var17 = 1 if (var16 === 1) { var20 = this.edgeNormalsF16[var22][0] var21 = this.edgeNormalsF16[var22][1] } else { var20 += this.edgeNormalsF16[var22][0] var21 += this.edgeNormalsF16[var22][1] } } } } if (var17 === 1) { if (multiplyF16(var20, var1.velocityXF16) + multiplyF16(var21, var1.velocityYF16) >= 0) { return 2 } this.collisionNormalXF16 = var20 this.collisionNormalYF16 = var21 } return var17 } } ===== FILE: src/MathF16.ts ===== import { abs, divideF16 as divideF16Compat, roundfToInt, } from './cpp.ts' export class MathF16 { static readonly PiHalfF16 = 102944 static readonly PiF16 = 205887 private static divideF16(a: number, b: number): number { return divideF16Compat(a, b) } private static atanF16(angle: number): number { return roundfToInt(Math.atan(angle / 0xffff) * 65536) } static sinF16(angle: number): number { return roundfToInt(Math.sin(angle / 0xffff) * 65536) } static cosF16(angle: number): number { return MathF16.sinF16(MathF16.PiHalfF16 - angle) } static atan2F16(dx: number, dy: number): number { if (abs(dy) < 3) { return (dx > 0 ? 1 : -1) * MathF16.PiHalfF16 } const atanVal = MathF16.atanF16(MathF16.divideF16(dx, dy)) if (dx > 0) { return dy > 0 ? atanVal : MathF16.PiF16 + atanVal } return dy > 0 ? atanVal : atanVal - MathF16.PiF16 } } ===== FILE: src/MenuManager.ts ===== import { GameCanvas } from './GameCanvas.ts' import { GameMenu } from './GameMenu.ts' import type { IGameMenuElement } from './IGameMenuElement.ts' import type { IMenuManager } from './IMenuManager.ts' import { LevelLoader } from './LevelLoader.ts' import { Micro } from './Micro.ts' import { RecordManager } from './RecordManager.ts' import { SettingsStringRender } from './SettingsStringRender.ts' import { TextRender } from './TextRender.ts' import { TimerOrMotoPartOrMenuElem } from './TimerOrMotoPartOrMenuElem.ts' import { Font } from './lcdui/Font.ts' import { FontStorage } from './lcdui/FontStorage.ts' import { Graphics } from './lcdui/Graphics.ts' import { Image } from './lcdui/Image.ts' import { RecordStore } from './rms/RecordStore.ts' import { Time } from './utils/Time.ts' import RASTER_URL from './assets/raster.png?url' export class MenuManager implements IMenuManager { private readonly persistedStateBuffer = new Int8Array(19) private readonly micro: Micro private recordManager: RecordManager | null = null private gameMenuMain: GameMenu | null = null private gameMenuPlay: GameMenu | null = null private gameMenuOptions: GameMenu | null = null private gameMenuAbout: GameMenu | null = null private gameMenuHelp: GameMenu | null = null private gameMenuConfirmClear: GameMenu | null = null private gameMenuConfirmReset: GameMenu | null = null private gameMenuFinished: GameMenu | null = null private gameMenuIngame: GameMenu | null = null private taskPlayMenu: TimerOrMotoPartOrMenuElem | null = null private taskOptions: TimerOrMotoPartOrMenuElem | null = null private taskHelp: TimerOrMotoPartOrMenuElem | null = null private settingStringLevel: SettingsStringRender | null = null private gameMenuStringLevel: GameMenu | null = null private settingsStringTrack: SettingsStringRender | null = null private trackSelectionMenu: GameMenu | null = null private settingsStringLeague: SettingsStringRender | null = null private gameMenuLeague: GameMenu | null = null private gameMenuHighscore: GameMenu | null = null private gameTimerTaskHighscore: TimerOrMotoPartOrMenuElem | null = null private taskStart: SettingsStringRender | null = null private perspectiveSetting: SettingsStringRender | null = null private shadowsSetting: SettingsStringRender | null = null private driverSpriteSetting: SettingsStringRender | null = null private bikeSpriteSetting: SettingsStringRender | null = null private inputSetting: SettingsStringRender | null = null private lookAheadSetting: SettingsStringRender | null = null private zoomSetting: SettingsStringRender | null = null private clearHighscoreSetting: TimerOrMotoPartOrMenuElem | null = null private fullResetItem: TimerOrMotoPartOrMenuElem | null = null private confirmYes: SettingsStringRender | null = null private confirmNo: SettingsStringRender | null = null private taskAbout: TimerOrMotoPartOrMenuElem | null = null private objectiveMenu: GameMenu | null = null private objectiveItem: TimerOrMotoPartOrMenuElem | null = null private keysMenu: GameMenu | null = null private keysItem: TimerOrMotoPartOrMenuElem | null = null private unlockingMenu: GameMenu | null = null private unlockingItem: TimerOrMotoPartOrMenuElem | null = null private gameMenuOptionsHighscoreDescription: GameMenu | null = null private taskHighscore: TimerOrMotoPartOrMenuElem | null = null private gameMenuOptions2: GameMenu | null = null private optionsHelpItem: TimerOrMotoPartOrMenuElem | null = null private gameMenuEnterName: GameMenu | null = null private settingStringBack: SettingsStringRender | null = null private settingStringPlayMenu: SettingsStringRender | null = null private settingStringContinue: SettingsStringRender | null = null private settingStringGoToMain: SettingsStringRender | null = null private settingStringExitGame: SettingsStringRender | null = null private restartTrackAction: SettingsStringRender | null = null private nextTrackAction: SettingsStringRender | null = null private finishOkAction: SettingsStringRender | null = null private finishNameAction: SettingsStringRender | null = null private lastFinishTime = -1 private lastFinishSeconds = -1 private lastFinishCentiseconds = -1 private lastFinishTimeString = '' private playerNameBytes: Uint8Array = new Uint8Array([65, 65, 65]) private readonly unlockedTracksByLevel = new Int8Array(4) private readonly defaultInputString: Uint8Array = new Uint8Array([65, 65, 65]) private availableLeagues = 0 private maxAvailableLevel = 0 private readonly selectedTrackByLevel = [0, 0, 0] private levelNames: string[][] = [] private leagueNames = new Array(3) private leagueNamesAll4: string[] = [] private recordStore: RecordStore | null = null private recordStoreRecordId = -1 private isRecordStoreOpened = false private rasterImage: Image | null = null private textRenderCodeBrewLink: TextRender | null = null private resumeLevelIndex = 0 private resumeTrackIndex = 0 private completedLastTrack = false private restartRequested = false private readonly levelDifficultyNames = ['Easy', 'Medium', 'Pro'] private finishMenuOpenedAt = 0 private isDisablePerspective = 0 private isDisabledShadows = 0 private isDisabledDriverSprite = 0 private isDisabledBikeSprite = 0 private inputMode = 0 private isDisableLookAhead = 0 private zoomIndex = window.innerWidth > 650 ? 1 : 0 private selectedTrackIndex = 0 private selectedLevelIndex = 0 private selectedLeagueIndex = 0 private uiOverlayMode = 0 private reservedSetting15 = 0 private toggleOptionNames: string[] = [] private inputModeNames: string[] = [] private readonly zoomNames = ['1x', '2x', '3x', '4x'] private readonly spacerTextRender: TextRender currentGameMenu: GameMenu | null = null highscoreLeagueIndex = 0 isOpeningPauseMenu = false constructor(var1: Micro) { this.micro = var1 this.spacerTextRender = new TextRender('', var1) } initPart(var1: number): void { let var4 = 0 switch (var1) { case 1: this.playerNameBytes = this.defaultInputString this.toggleOptionNames = ['On', 'Off'] this.inputModeNames = ['Keyset 1', 'Keyset 2', 'Keyset 3'] this.recordManager = new RecordManager() this.lastFinishTime = -1 this.lastFinishSeconds = -1 this.lastFinishCentiseconds = -1 this.lastFinishTimeString = '' this.isRecordStoreOpened = false for (let var11 = 0; var11 < 19; ++var11) { this.persistedStateBuffer[var11] = -127 } try { this.recordStore = RecordStore.openRecordStore('GWTRStates', true) this.isRecordStoreOpened = true } catch { this.isRecordStoreOpened = false } return case 2: { this.recordStoreRecordId = -1 if (this.recordStore === null) { return } let records try { records = this.recordStore.enumerateRecords(null, null, false) } catch { return } if (records.numRecords() > 0) { try { const var3 = records.nextRecord() records.reset() this.recordStoreRecordId = records.nextRecordId() if (var3.length <= 19) { for (let i = 0; i < var3.length; ++i) { this.persistedStateBuffer[i] = var3[i] } } } catch { return } records.destroy() } const var3 = this.readStoredNameBytes(16, -1) if (var3.length !== 0 && var3[0] !== -1) { for (var4 = 0; var4 < 3; ++var4) { this.playerNameBytes[var4] = var3[var4] } } if (this.playerNameBytes[0] === 82 && this.playerNameBytes[1] === 75 && this.playerNameBytes[2] === 69) { this.availableLeagues = 3 this.maxAvailableLevel = 2 if (this.micro.levelLoader !== null) { this.unlockedTracksByLevel[0] = this.micro.levelLoader.levelNames[0].length - 1 this.unlockedTracksByLevel[1] = this.micro.levelLoader.levelNames[1].length - 1 this.unlockedTracksByLevel[2] = this.micro.levelLoader.levelNames[2].length - 1 } return } this.availableLeagues = 0 this.maxAvailableLevel = 1 this.unlockedTracksByLevel[0] = 0 this.unlockedTracksByLevel[1] = 0 this.unlockedTracksByLevel[2] = -1 return } case 3: this.isDisablePerspective = this.readStoredValue(0, this.isDisablePerspective) this.isDisabledShadows = this.readStoredValue(1, this.isDisabledShadows) this.isDisabledDriverSprite = this.readStoredValue(2, this.isDisabledDriverSprite) this.isDisabledBikeSprite = this.readStoredValue(3, this.isDisabledBikeSprite) this.inputMode = this.readStoredValue(14, this.inputMode) this.isDisableLookAhead = this.readStoredValue(4, this.isDisableLookAhead) this.zoomIndex = this.readStoredValue(13, this.zoomIndex) this.selectedTrackIndex = this.readStoredValue(11, this.selectedTrackIndex) this.selectedLevelIndex = this.readStoredValue(10, this.selectedLevelIndex) this.selectedLeagueIndex = this.readStoredValue(12, this.selectedLeagueIndex) this.reservedSetting15 = this.readStoredValue(15, this.reservedSetting15) this.resumeLevelIndex = this.selectedLevelIndex this.resumeTrackIndex = this.selectedTrackIndex if (this.playerNameBytes[0] !== 82 || this.playerNameBytes[1] !== 75 || this.playerNameBytes[2] !== 69) { this.availableLeagues = this.readStoredValue(5, this.availableLeagues) this.maxAvailableLevel = this.readStoredValue(6, this.maxAvailableLevel) for (var4 = 0; var4 < 3; ++var4) { this.unlockedTracksByLevel[var4] = this.readStoredValue(7 + var4, this.unlockedTracksByLevel[var4]) } } try { this.selectedTrackByLevel[this.selectedLevelIndex] = this.selectedTrackIndex } catch { this.selectedLevelIndex = 0 this.selectedTrackIndex = 0 this.selectedTrackByLevel[this.selectedLevelIndex] = this.selectedTrackIndex } LevelLoader.isEnabledPerspective = this.isDisablePerspective === 0 LevelLoader.isEnabledShadows = this.isDisabledShadows === 0 if (this.micro.gamePhysics !== null) { this.micro.gamePhysics.setEnableLookAhead(this.isDisableLookAhead === 0) } if (this.micro.gameCanvas !== null) { this.micro.gameCanvas.setInputMode(this.inputMode) this.micro.gameCanvas.setUiOverlayEnabled(this.uiOverlayMode === 0) } this.leagueNamesAll4 = ['100cc', '175cc', '220cc', '325cc'] this.levelNames = this.micro.levelLoader?.levelNames ?? [] if (this.availableLeagues < 3) { this.leagueNames = ['100cc', '175cc', '220cc'] } else { this.leagueNames = this.leagueNamesAll4 } this.highscoreLeagueIndex = this.selectedLeagueIndex return case 4: { this.gameMenuMain = new GameMenu('Trail lodge', this.micro, null) this.gameMenuPlay = new GameMenu('Play', this.micro, this.gameMenuMain) this.gameMenuOptions = new GameMenu('Options', this.micro, this.gameMenuMain) this.gameMenuAbout = new GameMenu('About', this.micro, this.gameMenuMain) this.gameMenuHelp = new GameMenu('Help', this.micro, this.gameMenuMain) this.settingStringBack = new SettingsStringRender('Back', 0, this, [], false, this.micro, this.gameMenuMain, true) this.settingStringGoToMain = new SettingsStringRender('Go to Main', 0, this, [], false, this.micro, this.gameMenuMain, true) this.settingStringContinue = new SettingsStringRender('Continue', 0, this, [], false, this.micro, this.gameMenuMain, true) this.settingStringPlayMenu = new SettingsStringRender('Play Menu', 0, this, [], false, this.micro, this.gameMenuMain, true) const boldSmallFont = FontStorage.getFont(Font.STYLE_BOLD, Font.SIZE_SMALL) if ((this.gameMenuAbout?.xPos ?? 0) + boldSmallFont.stringWidth('http://www.codebrew.se/') >= this.getCanvasWidth()) { this.textRenderCodeBrewLink = new TextRender('www.codebrew.se', this.micro) } else { this.textRenderCodeBrewLink = new TextRender('http://www.codebrew.se/', this.micro) } this.textRenderCodeBrewLink.setFont(boldSmallFont) this.gameMenuHighscore = new GameMenu('Highscore', this.micro, this.gameMenuPlay) this.gameMenuFinished = new GameMenu('Finished!', this.micro, this.gameMenuPlay) return } case 5: this.gameMenuIngame = new GameMenu('Ingame', this.micro, this.gameMenuPlay) this.gameMenuEnterName = new GameMenu('Enter Name', this.micro, this.gameMenuFinished, this.playerNameBytes) this.gameMenuConfirmClear = new GameMenu('Confirm Clear', this.micro, this.gameMenuOptions) this.gameMenuConfirmReset = new GameMenu('Confirm Reset', this.micro, this.gameMenuConfirmClear) this.taskPlayMenu = new TimerOrMotoPartOrMenuElem('Choose a trail', this.gameMenuPlay, this) this.taskOptions = new TimerOrMotoPartOrMenuElem('Options', this.gameMenuOptions, this) this.taskHelp = new TimerOrMotoPartOrMenuElem('Help', this.gameMenuHelp, this) this.taskAbout = new TimerOrMotoPartOrMenuElem('About', this.gameMenuAbout, this) this.settingStringExitGame = new SettingsStringRender('Exit Game', 0, this, [], false, this.micro, this.gameMenuMain, true) this.gameMenuMain?.addMenuElement(this.taskPlayMenu) this.gameMenuMain?.addMenuElement(this.taskOptions) this.gameMenuMain?.addMenuElement(this.taskHelp) this.gameMenuMain?.addMenuElement(this.taskAbout) // Browser edition stays on this page; no exit action. this.settingStringLevel = new SettingsStringRender('Level', this.selectedLevelIndex, this, this.levelDifficultyNames, false, this.micro, this.gameMenuPlay, false) this.settingsStringTrack = new SettingsStringRender('Track', this.selectedTrackByLevel[this.selectedLevelIndex], this, this.levelNames[this.selectedLevelIndex], false, this.micro, this.gameMenuPlay, false) this.settingsStringLeague = new SettingsStringRender('League', this.selectedLeagueIndex, this, this.leagueNames, false, this.micro, this.gameMenuPlay, false) try { this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.selectedLevelIndex]) } catch { this.settingsStringTrack.setAvailableOptions(0) } this.settingStringLevel.setAvailableOptions(this.maxAvailableLevel) this.settingsStringLeague.setAvailableOptions(this.availableLeagues) this.gameTimerTaskHighscore = new TimerOrMotoPartOrMenuElem('Highscore', this.gameMenuHighscore, this) this.gameMenuHighscore?.addMenuElement(this.settingStringBack!) this.taskStart = new SettingsStringRender('Start>', 0, this, [], false, this.micro, this.gameMenuMain, true) this.gameMenuPlay?.addMenuElement(this.taskStart) this.gameMenuPlay?.addMenuElement(this.settingStringLevel) this.gameMenuPlay?.addMenuElement(this.settingsStringTrack) this.gameMenuPlay?.addMenuElement(this.settingsStringLeague) this.gameMenuPlay?.addMenuElement(this.gameTimerTaskHighscore) this.gameMenuPlay?.addMenuElement(this.settingStringGoToMain!) this.perspectiveSetting = new SettingsStringRender('Perspective', this.isDisablePerspective, this, this.toggleOptionNames, true, this.micro, this.gameMenuOptions, false) this.shadowsSetting = new SettingsStringRender('Shadows', this.isDisabledShadows, this, this.toggleOptionNames, true, this.micro, this.gameMenuOptions, false) this.driverSpriteSetting = new SettingsStringRender('Driver sprite', this.isDisabledDriverSprite, this, this.toggleOptionNames, true, this.micro, this.gameMenuOptions, false) this.bikeSpriteSetting = new SettingsStringRender('Bike sprite', this.isDisabledBikeSprite, this, this.toggleOptionNames, true, this.micro, this.gameMenuOptions, false) this.inputSetting = new SettingsStringRender('Input', this.inputMode, this, this.inputModeNames, false, this.micro, this.gameMenuOptions, false) this.lookAheadSetting = new SettingsStringRender('Look ahead', this.isDisableLookAhead, this, this.toggleOptionNames, true, this.micro, this.gameMenuOptions, false) this.zoomSetting = new SettingsStringRender('Zoom', this.zoomIndex, this, this.zoomNames, false, this.micro, this.gameMenuOptions, false) this.clearHighscoreSetting = new TimerOrMotoPartOrMenuElem('Clear highscore', this.gameMenuConfirmClear, this) return case 6: this.gameMenuOptions?.addMenuElement(this.perspectiveSetting!) this.gameMenuOptions?.addMenuElement(this.shadowsSetting!) this.gameMenuOptions?.addMenuElement(this.driverSpriteSetting!) this.gameMenuOptions?.addMenuElement(this.bikeSpriteSetting!) this.gameMenuOptions?.addMenuElement(this.inputSetting!) this.gameMenuOptions?.addMenuElement(this.lookAheadSetting!) this.gameMenuOptions?.addMenuElement(this.zoomSetting!) this.gameMenuOptions?.addMenuElement(this.clearHighscoreSetting!) this.gameMenuOptions?.addMenuElement(this.settingStringBack!) this.confirmNo = new SettingsStringRender('No', 0, this, [], false, this.micro, this.gameMenuMain, true) this.confirmYes = new SettingsStringRender('Yes', 0, this, [], false, this.micro, this.gameMenuMain, true) this.fullResetItem = new TimerOrMotoPartOrMenuElem('Full Reset', this.gameMenuConfirmReset, this) this.addTextRender(this.gameMenuConfirmClear!, 'Clearing the highscores can not be undone. It will remove all the registered times on all tracks.') this.addTextRender(this.gameMenuConfirmClear!, 'Would you like to clear the highscores?') this.gameMenuConfirmClear?.addMenuElement(this.confirmNo) this.gameMenuConfirmClear?.addMenuElement(this.confirmYes) this.gameMenuConfirmClear?.addMenuElement(this.fullResetItem) this.addTextRender(this.gameMenuConfirmReset!, 'A full reset can not be undone. It will relock all tracks and leagues and clear back all settings to default. A full reset will exit the application.') this.addTextRender(this.gameMenuConfirmReset!, 'Would you like to do a full reset?') this.gameMenuConfirmReset?.addMenuElement(this.confirmNo) this.gameMenuConfirmReset?.addMenuElement(this.confirmYes) this.objectiveMenu = new GameMenu('Objective', this.micro, this.gameMenuHelp) this.objectiveItem = new TimerOrMotoPartOrMenuElem('Objective', this.objectiveMenu, this) this.addTextRender(this.objectiveMenu, "Race to the finish line as fast as you can without crashing. By leaning forward and backward you can adjust the rotation of your bike. By landing on both wheels after jumping, your bike won't crash as easily. Beware, the levels tend to get harder and harder...") this.objectiveMenu.addMenuElement(this.settingStringBack!) this.gameMenuHelp?.addMenuElement(this.objectiveItem) this.keysMenu = new GameMenu('Keys', this.micro, this.gameMenuHelp) this.keysItem = new TimerOrMotoPartOrMenuElem('Keys', this.keysMenu, this) this.addTextRender(this.keysMenu, `- ${this.inputModeNames[0]} -`) this.addTextRender(this.keysMenu, 'UP accelerates, DOWN brakes, RIGHT leans forward and LEFT leans backward. 1 accelerates and leans backward. 3 accelerates and leans forward. 7 brakes and leans backward. 9 brakes and leans forward.') this.keysMenu.addMenuElement(this.spacerTextRender) this.addTextRender(this.keysMenu, `- ${this.inputModeNames[1]} -`) this.addTextRender(this.keysMenu, '1 accelerates, 4 brakes, 6 leans forward and 5 leans backward.') this.keysMenu.addMenuElement(this.spacerTextRender) this.addTextRender(this.keysMenu, `- ${this.inputModeNames[2]} -`) this.addTextRender(this.keysMenu, '3 accelerates, 6 brakes, 5 leans forward and 4 leans backward.') this.keysMenu.addMenuElement(this.settingStringBack!) this.gameMenuHelp?.addMenuElement(this.keysItem) this.unlockingMenu = new GameMenu('Unlocking', this.micro, this.gameMenuHelp) this.unlockingItem = new TimerOrMotoPartOrMenuElem('Unlocking', this.unlockingMenu, this) this.addTextRender(this.unlockingMenu, 'By completing the easier levels, new levels will be unlocked. You will also gain access to higher leagues where more advanced bikes with different characteristics are available.') this.unlockingMenu.addMenuElement(this.settingStringBack!) this.gameMenuHelp?.addMenuElement(this.unlockingItem) this.gameMenuOptionsHighscoreDescription = new GameMenu('Highscore', this.micro, this.gameMenuHelp) this.taskHighscore = new TimerOrMotoPartOrMenuElem('Highscore', this.gameMenuOptionsHighscoreDescription, this) this.addTextRender(this.gameMenuOptionsHighscoreDescription, 'The three best times on every track are saved for each league. When beating a time on a track you will be asked to enter your name. The highscores can be viewed from the Play Menu. By pressing left and right in the highscore view you can view the highscore for a specific league. The highscore can be cleared from the options menu.') this.gameMenuOptionsHighscoreDescription.addMenuElement(this.settingStringBack!) this.gameMenuHelp?.addMenuElement(this.taskHighscore) return case 7: this.gameMenuOptions2 = new GameMenu('Options', this.micro, this.gameMenuHelp) this.optionsHelpItem = new TimerOrMotoPartOrMenuElem('Options', this.gameMenuOptions2, this) this.addTextRender(this.gameMenuOptions2, 'Perspective: On/Off') this.addTextRender(this.gameMenuOptions2, 'Default: . Turns on and off the perspective view of the tracks.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Shadows: On/Off') this.addTextRender(this.gameMenuOptions2, 'Default: . Turns on and off the shadows.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Driver Sprite: On / Off') this.addTextRender(this.gameMenuOptions2, 'Default: . uses a texture for the driver. uses line graphics.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Bike Sprite: On / Off') this.addTextRender(this.gameMenuOptions2, 'Default: . uses a texture for the bike. uses line graphics.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Input: Keyset 1,2,3 ') this.addTextRender(this.gameMenuOptions2, 'Default: <1>. Determines which type of input should be used when playing. See "Keys" in the help menu for more info.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Look ahead: On/Off') this.addTextRender(this.gameMenuOptions2, 'Default: . Turns on and off smart camera movement.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Zoom: 1x,2x,3x,4x') this.addTextRender(this.gameMenuOptions2, 'Default: <1x>. Enlarges the game graphics.') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.addTextRender(this.gameMenuOptions2, 'Clear highscore') this.addTextRender(this.gameMenuOptions2, 'Lets you clear the highscores. Here you can also do a "Full Reset" which will reset the game to original state (clear settings, highscores, unlocked levels and leagues).') this.gameMenuOptions2.addMenuElement(this.spacerTextRender) this.gameMenuOptions2.addMenuElement(this.settingStringBack!) this.gameMenuHelp?.addMenuElement(this.optionsHelpItem) this.gameMenuHelp?.addMenuElement(this.settingStringBack!) this.addTextRender(this.gameMenuAbout!, '"Gravity Defied"') this.addTextRender(this.gameMenuAbout!, 'brought 2 you by pascha. For information visit:') if (this.textRenderCodeBrewLink !== null) { this.gameMenuAbout?.addMenuElement(this.textRenderCodeBrewLink) } this.gameMenuAbout?.addMenuElement(this.settingStringBack!) if (this.micro.levelLoader !== null) { this.nextTrackAction = new SettingsStringRender(`Track: ${this.micro.levelLoader.getName(0, 1)}`, 0, this, [], false, this.micro, this.gameMenuMain, true) this.restartTrackAction = new SettingsStringRender(`Restart: ${this.micro.levelLoader.getName(0, 0)}`, 0, this, [], false, this.micro, this.gameMenuMain, true) } else { this.nextTrackAction = new SettingsStringRender('Track', 0, this, [], false, this.micro, this.gameMenuMain, true) this.restartTrackAction = new SettingsStringRender('Restart', 0, this, [], false, this.micro, this.gameMenuMain, true) } this.gameMenuIngame?.addMenuElement(this.settingStringContinue!) this.gameMenuIngame?.addMenuElement(this.restartTrackAction) this.gameMenuIngame?.addMenuElement(this.taskOptions!) this.gameMenuIngame?.addMenuElement(this.taskHelp!) this.gameMenuIngame?.addMenuElement(this.settingStringPlayMenu!) this.finishOkAction = new SettingsStringRender('Ok', 0, this, [], false, this.micro, this.gameMenuMain, true) this.finishNameAction = new SettingsStringRender(`Name - ${this.makeString(this.playerNameBytes)}`, 0, this, [], false, this.micro, this.gameMenuMain, true) this.openMenu(this.gameMenuMain, false) this.rasterImage = Image.fromSrc(RASTER_URL) return default: return } } private addTextRender(gameMenu: GameMenu, text: string): void { const var3 = TextRender.makeMultilineTextRenders(text, this.micro) for (let var4 = 0; var4 < var3.length; ++var4) { gameMenu.addMenuElement(var3[var4]) } } getCurrentLevel(): number { return this.settingStringLevel?.getCurrentOptionPos() ?? 0 } getCurrentTrack(): number { return this.settingsStringTrack?.getCurrentOptionPos() ?? 0 } consumeRestartRequested(): boolean { if (this.restartRequested) { this.restartRequested = false return true } return false } private finalizeFinishedMenu(): void { if (this.recordManager === null || this.gameMenuFinished === null || this.settingsStringLeague === null || this.settingsStringTrack === null || this.settingStringLevel === null || this.restartTrackAction === null || this.settingStringPlayMenu === null || this.micro.levelLoader === null) { return } this.recordManager.addRecordIfNeeded(this.settingsStringLeague.getCurrentOptionPos(), this.playerNameBytes, this.lastFinishTime) this.recordManager.writeRecordInfo() this.completedLastTrack = false this.gameMenuFinished.clearVector() this.gameMenuFinished.addMenuElement(new TextRender(`Time: ${this.lastFinishTimeString}`, this.micro)) const var1 = this.recordManager.getRecordDescription(this.settingsStringLeague.getCurrentOptionPos()) for (let var2 = 0; var2 < var1.length; ++var2) { if (var1[var2] !== '') { this.gameMenuFinished.addMenuElement(new TextRender(`${var2 + 1}.${var1[var2]}`, this.micro)) } } this.recordManager.closeRecordStore() let unlockedLeague = -1 if (this.settingsStringTrack.getMaxAvailableOptionPos() >= this.settingsStringTrack.getCurrentOptionPos()) { this.settingsStringTrack.setAvailableOptions( this.settingsStringTrack.getCurrentOptionPos() + 1 < this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()] ? this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()] : this.settingsStringTrack.getCurrentOptionPos() + 1, ) this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()] = this.settingsStringTrack.getMaxAvailableOptionPos() < this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()] ? this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()] : this.settingsStringTrack.getMaxAvailableOptionPos() } if (this.settingsStringTrack.getCurrentOptionPos() === this.settingsStringTrack.getMaxOptionPos()) { this.completedLastTrack = true switch (this.settingStringLevel.getCurrentOptionPos()) { case 0: if (this.settingsStringLeague.getMaxAvailableOptionPos() < 1) { unlockedLeague = 1 this.settingsStringLeague.setAvailableOptions(unlockedLeague) } break case 1: if (this.settingsStringLeague.getMaxAvailableOptionPos() < 2) { unlockedLeague = 2 this.settingsStringLeague.setAvailableOptions(unlockedLeague) } break case 2: if (this.settingsStringLeague.getMaxAvailableOptionPos() < 3) { unlockedLeague = 3 this.settingsStringLeague.setOptionsList(this.leagueNamesAll4) this.leagueNames = this.leagueNamesAll4 this.settingsStringLeague.setAvailableOptions(unlockedLeague) } } this.settingStringLevel.setAvailableOptions(this.settingStringLevel.getMaxAvailableOptionPos() + 1) if (this.unlockedTracksByLevel[this.settingStringLevel.getMaxAvailableOptionPos()] === -1) { this.unlockedTracksByLevel[this.settingStringLevel.getMaxAvailableOptionPos()] = 0 } } const var3 = this.getCountOfRecordStoresWithPrefix(this.settingStringLevel.getCurrentOptionPos()) this.addTextRender( this.gameMenuFinished, `${var3} of ${this.levelNames[this.settingStringLevel.getCurrentOptionPos()].length} tracks in ${this.levelDifficultyNames[this.settingStringLevel.getCurrentOptionPos()]} completed.`, ) if (!this.completedLastTrack) { this.restartTrackAction.setText(`Restart: ${this.micro.levelLoader.getName(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos())}`) this.nextTrackAction?.setText(`Next: ${this.micro.levelLoader.getName(this.resumeLevelIndex, this.resumeTrackIndex + 1)}`) } else { if (this.settingStringLevel.getCurrentOptionPos() < this.settingStringLevel.getMaxOptionPos()) { this.settingStringLevel.setCurrentOptionPos(this.settingStringLevel.getCurrentOptionPos() + 1) this.settingsStringTrack.setCurrentOptionPos(0) this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()]) } if (unlockedLeague !== -1) { this.addTextRender(this.gameMenuFinished, `Congratultions! You have successfully unlocked a new league: ${this.leagueNames[unlockedLeague]}`) if (unlockedLeague === 3) { this.gameMenuFinished.addMenuElement(new TextRender('Enjoy...', this.micro)) } this.showAlert('League unlocked', `You have successfully unlocked a new league: ${this.leagueNames[unlockedLeague]}`, null) } else { let var4 = true if (this.micro.levelLoader !== null) { for (let var5 = 0; var5 < 3; ++var5) { if (this.unlockedTracksByLevel[var5] !== this.micro.levelLoader.levelNames[var5].length - 1) { var4 = false } } } if (!var4) { this.addTextRender(this.gameMenuFinished, 'You have completed all tracks at this level.') } } } if (!this.completedLastTrack && this.nextTrackAction !== null) { this.gameMenuFinished.addMenuElement(this.nextTrackAction) } this.restartTrackAction.setText(`Restart: ${this.micro.levelLoader.getName(this.resumeLevelIndex, this.resumeTrackIndex)}`) this.gameMenuFinished.addMenuElement(this.restartTrackAction) this.gameMenuFinished.addMenuElement(this.settingStringPlayMenu) this.openMenu(this.gameMenuFinished, false) this.persistState() } repaint(): void { this.micro.gameCanvas?.repaint() } getCanvasHeight(): number { return this.micro.gameCanvas?.getHeight() ?? 0 } getCanvasWidth(): number { return this.micro.gameCanvas?.getWidth() ?? 0 } showMenuScreen(var1: number): void { void this.finishMenuOpenedAt this.isOpeningPauseMenu = false switch (var1) { case 0: this.openMenu(this.gameMenuMain, false) this.micro.gamePhysics?.enableGenerateInputAI() break case 1: if (this.settingStringLevel !== null && this.settingsStringTrack !== null && this.restartTrackAction !== null && this.micro.levelLoader !== null) { this.resumeLevelIndex = this.settingStringLevel.getCurrentOptionPos() this.resumeTrackIndex = this.settingsStringTrack.getCurrentOptionPos() this.restartTrackAction.setText(`Restart: ${this.micro.levelLoader.getName(this.resumeLevelIndex, this.resumeTrackIndex)}`) } this.restartRequested = false this.openMenu(this.gameMenuIngame, false) break case 2: { this.finishMenuOpenedAt = Time.currentTimeMillis() this.gameMenuFinished?.clearVector() if (this.settingStringLevel === null || this.settingsStringTrack === null || this.recordManager === null || this.settingsStringLeague === null || this.gameMenuFinished === null || this.finishOkAction === null || this.finishNameAction === null) { break } this.resumeLevelIndex = this.settingStringLevel.getCurrentOptionPos() this.resumeTrackIndex = this.settingsStringTrack.getCurrentOptionPos() this.recordManager.openRecordStoreForTrack(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos()) const var2 = this.recordManager.getPosOfNewRecord(this.settingsStringLeague.getCurrentOptionPos(), this.lastFinishTime) this.lastFinishTimeString = this.timeToString(this.lastFinishTime) if (var2 >= 0 && var2 <= 2) { const var3 = new TextRender('', this.micro) var3.setDx(GameCanvas.spriteSizeX[5] + 1) switch (var2) { case 0: var3.setText('First place!') var3.setDrawSprite(true, 5) break case 1: var3.setText('Second place!') var3.setDrawSprite(true, 6) break case 2: var3.setText('Third place!') var3.setDrawSprite(true, 7) } this.gameMenuFinished.addMenuElement(var3) const var4 = new TextRender(`${this.lastFinishTimeString}`, this.micro) var4.setDx(GameCanvas.spriteSizeX[5] + 1) this.gameMenuFinished.addMenuElement(var4) this.gameMenuFinished.addMenuElement(this.finishOkAction) this.gameMenuFinished.addMenuElement(this.finishNameAction) this.openMenu(this.gameMenuFinished, false) this.isOpeningPauseMenu = false } else { this.finalizeFinishedMenu() } break } default: this.openMenu(this.gameMenuMain, false) } this.micro.gameCanvas!.isDrawingTime = false this.micro.gamePhysics?.syncRenderStateFromSimulation() this.micro.gameToMenu() } renderCurrentMenu(var1: Graphics): void { if (this.currentGameMenu !== null && !this.isOpeningPauseMenu) { void this.rasterImage this.micro.gameCanvas?.drawGame(var1) if (this.currentGameMenu.getView().nameEntry) this.currentGameMenu.render(var1) } } processKeyCode(keyCode: number): void { if (this.currentGameMenu !== null && this.micro.gameCanvas !== null) { switch (this.micro.gameCanvas.getGameAction(keyCode)) { case 1: this.currentGameMenu.processGameActionUp() return case 2: this.currentGameMenu.processGameActionUpd(3) if (this.currentGameMenu === this.gameMenuHighscore && this.settingsStringLeague !== null) { --this.highscoreLeagueIndex if (this.highscoreLeagueIndex < 0) { this.highscoreLeagueIndex = 0 } this.rebuildHighscoreMenu(this.highscoreLeagueIndex) } return case 5: this.currentGameMenu.processGameActionUpd(2) if (this.currentGameMenu === this.gameMenuHighscore && this.settingsStringLeague !== null) { ++this.highscoreLeagueIndex if (this.highscoreLeagueIndex > this.settingsStringLeague.getMaxAvailableOptionPos()) { this.highscoreLeagueIndex = this.settingsStringLeague.getMaxAvailableOptionPos() } this.rebuildHighscoreMenu(this.highscoreLeagueIndex) } return case 6: this.currentGameMenu.processGameActionDown() return case 8: this.currentGameMenu.processGameActionUpd(1) return default: return } } } handleBackAction(): void { if (this.currentGameMenu === null) { return } if (this.currentGameMenu === this.gameMenuIngame) { this.micro.menuToGame() return } this.openMenu(this.currentGameMenu.getParentMenu(), true) } getCurrentMenu(): GameMenu | null { return this.currentGameMenu } openMenu(gm: GameMenu | null, preserveSelection: boolean): void { this.micro.gameCanvas?.hideBackButton() if (gm !== this.gameMenuMain && gm !== this.gameMenuFinished && gm !== null) { this.micro.gameCanvas?.showBackButton() } if (gm === this.gameMenuHighscore) { if (this.settingsStringLeague !== null) { this.highscoreLeagueIndex = this.settingsStringLeague.getCurrentOptionPos() this.rebuildHighscoreMenu(this.highscoreLeagueIndex) } } else if (gm === this.gameMenuFinished) { this.playerNameBytes = this.gameMenuEnterName?.getStrArr() ?? this.playerNameBytes this.finishNameAction?.setText(`Name - ${this.makeString(this.playerNameBytes)}`) } else if (gm === this.gameMenuPlay) { if (this.settingStringLevel !== null && this.settingsStringTrack !== null && this.micro.levelLoader !== null) { this.settingsStringTrack.setOptionsList(this.micro.levelLoader.levelNames[this.settingStringLevel.getCurrentOptionPos()]) if (this.currentGameMenu === this.trackSelectionMenu) { this.selectedTrackByLevel[this.settingStringLevel.getCurrentOptionPos()] = this.settingsStringTrack.getCurrentOptionPos() } this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()]) this.settingsStringTrack.setCurrentOptionPos(this.selectedTrackByLevel[this.settingStringLevel.getCurrentOptionPos()]) } } if (gm === this.gameMenuMain || gm === this.gameMenuPlay) { this.micro.gamePhysics?.enableGenerateInputAI() } this.currentGameMenu = gm if (this.currentGameMenu !== null && !preserveSelection) { this.currentGameMenu.selectFirstMenuItem() } this.isOpeningPauseMenu = false } rebuildHighscoreMenu(var1: number): void { if (this.gameMenuHighscore === null || this.recordManager === null || this.settingStringLevel === null || this.settingsStringTrack === null || this.settingsStringLeague === null || this.micro.levelLoader === null || this.settingStringBack === null) { return } this.gameMenuHighscore.clearVector() this.recordManager.openRecordStoreForTrack(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos()) this.gameMenuHighscore.addMenuElement(new TextRender(this.micro.levelLoader.getName(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos()), this.micro)) this.gameMenuHighscore.addMenuElement(new TextRender(`LEAGUE: ${this.settingsStringLeague.getOptionsList()[var1]}`, this.micro)) const var2 = this.recordManager.getRecordDescription(var1) for (let var3 = 0; var3 < var2.length; ++var3) { if (var2[var3] !== '') { const var4 = new TextRender(`${var3 + 1}.${var2[var3]}`, this.micro) var4.setDx(GameCanvas.spriteSizeX[5] + 1) if (var3 === 0) { var4.setDrawSprite(true, 5) } else if (var3 === 1) { var4.setDrawSprite(true, 6) } else if (var3 === 2) { var4.setDrawSprite(true, 7) } this.gameMenuHighscore.addMenuElement(var4) } } this.recordManager.closeRecordStore() if (var2[0] === '') { this.gameMenuHighscore.addMenuElement(new TextRender('No Highscores', this.micro)) } this.gameMenuHighscore.addMenuElement(this.settingStringBack) } saveAndClose(): void { if (this.isRecordStoreOpened) { this.persistState() try { this.recordStore?.closeRecordStore() this.isRecordStoreOpened = false } catch {} } this.currentGameMenu = null } persistState(): void { this.copyThreeBytesFromArr(16, this.playerNameBytes) this.setValue(0, this.perspectiveSetting?.getCurrentOptionPos() ?? 0) this.setValue(1, this.shadowsSetting?.getCurrentOptionPos() ?? 0) this.setValue(2, this.driverSpriteSetting?.getCurrentOptionPos() ?? 0) this.setValue(3, this.bikeSpriteSetting?.getCurrentOptionPos() ?? 0) this.setValue(14, this.inputSetting?.getCurrentOptionPos() ?? 0) this.setValue(4, this.lookAheadSetting?.getCurrentOptionPos() ?? 0) this.setValue(13, this.zoomSetting?.getCurrentOptionPos() ?? 0) this.setValue(5, this.settingsStringLeague?.getMaxAvailableOptionPos() ?? 0) this.setValue(6, this.settingStringLevel?.getMaxAvailableOptionPos() ?? 0) this.setValue(10, this.settingStringLevel?.getCurrentOptionPos() ?? 0) this.setValue(11, this.settingsStringTrack?.getCurrentOptionPos() ?? 0) this.setValue(12, this.settingsStringLeague?.getCurrentOptionPos() ?? 0) for (let i = 0; i < 3; ++i) { this.setValue(7 + i, this.unlockedTracksByLevel[i]) } if (this.recordStore === null) { return } if (this.recordStoreRecordId === -1) { try { this.recordStoreRecordId = this.recordStore.addRecord(this.persistedStateBuffer, 0, 19) } catch {} } else { try { this.recordStore.setRecord(this.recordStoreRecordId, this.persistedStateBuffer, 0, 19) } catch {} } } run(): void {} showAlert(title: string, alertText: string, image: Image | null): void { void image if (title !== '') { this.micro.gameCanvas?.scheduleGameTimerTask(title, 2000) } console.info(alertText) } handleMenuSelection(menuElement: IGameMenuElement): void { if (menuElement === this.taskStart) { if ( this.settingStringLevel !== null && this.settingsStringTrack !== null && this.settingsStringLeague !== null && this.settingStringLevel.getCurrentOptionPos() <= this.settingStringLevel.getMaxAvailableOptionPos() && this.settingsStringTrack.getCurrentOptionPos() <= this.settingsStringTrack.getMaxAvailableOptionPos() && this.settingsStringLeague.getCurrentOptionPos() <= this.settingsStringLeague.getMaxAvailableOptionPos() ) { this.micro.gamePhysics?.disableGenerateInputAI() this.micro.levelLoader?.loadLevel(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos()) this.micro.gamePhysics?.setMotoLeague(this.settingsStringLeague.getCurrentOptionPos()) this.restartRequested = true this.micro.menuToGame() } else { this.showAlert('GWTR', 'Complete more tracks to unlock this track/league combo.', null) } return } if (menuElement === this.perspectiveSetting) { this.micro.gamePhysics?.applyPerspectiveOffset(this.perspectiveSetting.getCurrentOptionPos() === 0) LevelLoader.isEnabledPerspective = this.perspectiveSetting.getCurrentOptionPos() === 0 return } if (menuElement === this.shadowsSetting) { LevelLoader.isEnabledShadows = this.shadowsSetting.getCurrentOptionPos() === 0 return } if (menuElement === this.driverSpriteSetting) { if (this.driverSpriteSetting.consumeSelectionMenuRequested()) { this.driverSpriteSetting.setCurrentOptionPos(this.driverSpriteSetting.getCurrentOptionPos() + 1) } return } if (menuElement === this.bikeSpriteSetting) { if (this.bikeSpriteSetting.consumeSelectionMenuRequested()) { this.bikeSpriteSetting.setCurrentOptionPos(this.bikeSpriteSetting.getCurrentOptionPos() + 1) } return } if (menuElement === this.inputSetting) { if (this.inputSetting.consumeSelectionMenuRequested()) { this.inputSetting.setCurrentOptionPos(this.inputSetting.getCurrentOptionPos() + 1) } this.micro.gameCanvas?.setInputMode(this.inputSetting.getCurrentOptionPos()) return } if (menuElement === this.lookAheadSetting) { this.micro.gamePhysics?.setEnableLookAhead(this.lookAheadSetting.getCurrentOptionPos() === 0) return } if (menuElement === this.zoomSetting) { if (this.zoomSetting.consumeSelectionMenuRequested()) { this.zoomSetting.setCurrentOptionPos(this.zoomSetting.getCurrentOptionPos() + 1) } return } if (menuElement === this.confirmYes) { if (this.currentGameMenu === this.gameMenuConfirmClear) { this.recordManager?.deleteRecordStores() this.showAlert('Cleared', 'Highscores have been cleared', null) } else if (this.currentGameMenu === this.gameMenuConfirmReset) { this.exit() this.showAlert('Reset', 'Master reset. Application will be closed.', null) } this.openMenu(this.currentGameMenu?.getParentMenu() ?? null, false) return } if (menuElement === this.confirmNo) { this.openMenu(this.currentGameMenu?.getParentMenu() ?? null, false) return } if (menuElement === this.settingStringBack) { this.openMenu(this.currentGameMenu?.getParentMenu() ?? null, true) return } if (menuElement === this.settingStringPlayMenu) { if (this.settingStringLevel !== null && this.settingsStringTrack !== null) { this.settingStringLevel.setCurrentOptionPos(this.resumeLevelIndex) this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.resumeLevelIndex]) this.settingsStringTrack.setCurrentOptionPos(this.resumeTrackIndex) } this.openMenu(this.currentGameMenu?.getParentMenu() ?? null, false) return } if (menuElement === this.settingStringGoToMain) { this.openMenu(this.gameMenuMain, false) return } if (menuElement === this.settingStringExitGame) { this.openMenu(this.currentGameMenu?.getParentMenu() ?? null, false) return } if (menuElement === this.restartTrackAction) { if (this.settingsStringLeague !== null && this.settingStringLevel !== null && this.settingsStringTrack !== null && this.settingsStringLeague.getCurrentOptionPos() <= this.settingsStringLeague.getMaxAvailableOptionPos()) { this.settingStringLevel.setCurrentOptionPos(this.resumeLevelIndex) this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.resumeLevelIndex]) this.settingsStringTrack.setCurrentOptionPos(this.resumeTrackIndex) this.micro.gamePhysics?.setMotoLeague(this.settingsStringLeague.getCurrentOptionPos()) this.restartRequested = true this.micro.menuToGame() } return } if (menuElement === this.nextTrackAction) { if (!this.completedLastTrack) { this.settingsStringTrack?.menuElemMethod(2) } if (this.settingStringLevel !== null && this.settingsStringTrack !== null && this.settingsStringLeague !== null) { this.micro.levelLoader?.loadLevel(this.settingStringLevel.getCurrentOptionPos(), this.settingsStringTrack.getCurrentOptionPos()) this.micro.gamePhysics?.setMotoLeague(this.settingsStringLeague.getCurrentOptionPos()) this.persistState() this.restartRequested = true this.micro.menuToGame() } return } if (menuElement === this.settingStringContinue) { this.repaint() this.micro.menuToGame() return } if (menuElement === this.finishNameAction) { this.gameMenuEnterName?.selectFirstMenuItem() this.openMenu(this.gameMenuEnterName, false) return } if (menuElement === this.finishOkAction) { this.finalizeFinishedMenu() return } if (menuElement === this.settingsStringTrack) { if (this.settingsStringTrack.consumeSelectionMenuRequested()) { this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.settingStringLevel?.getCurrentOptionPos() ?? 0]) this.settingsStringTrack.init() this.trackSelectionMenu = this.settingsStringTrack.getCurrentMenu() this.openMenu(this.trackSelectionMenu, false) this.trackSelectionMenu?.scrollToSelection(this.settingsStringTrack.getCurrentOptionPos()) } if (this.settingStringLevel !== null) { this.selectedTrackByLevel[this.settingStringLevel.getCurrentOptionPos()] = this.settingsStringTrack.getCurrentOptionPos() } return } if (menuElement === this.settingStringLevel) { if (this.settingStringLevel.consumeSelectionMenuRequested()) { this.gameMenuStringLevel = this.settingStringLevel.getCurrentMenu() this.openMenu(this.gameMenuStringLevel, false) this.gameMenuStringLevel?.scrollToSelection(this.settingStringLevel.getCurrentOptionPos()) } if (this.micro.levelLoader !== null && this.settingsStringTrack !== null) { this.settingsStringTrack.setOptionsList(this.micro.levelLoader.levelNames[this.settingStringLevel.getCurrentOptionPos()]) this.settingsStringTrack.setAvailableOptions(this.unlockedTracksByLevel[this.settingStringLevel.getCurrentOptionPos()]) this.settingsStringTrack.setCurrentOptionPos(this.selectedTrackByLevel[this.settingStringLevel.getCurrentOptionPos()]) this.settingsStringTrack.init() } return } if (menuElement === this.settingsStringLeague && this.settingsStringLeague.consumeSelectionMenuRequested()) { this.gameMenuLeague = this.settingsStringLeague.getCurrentMenu() this.settingsStringLeague.setParentGameMenu(this.currentGameMenu) this.openMenu(this.gameMenuLeague, false) this.gameMenuLeague?.scrollToSelection(this.settingsStringLeague.getCurrentOptionPos()) } } getLoadedSpriteFlags(): number { let var1 = 0 if ((this.driverSpriteSetting?.getCurrentOptionPos() ?? 1) === 0) { var1 |= 2 } if ((this.bikeSpriteSetting?.getCurrentOptionPos() ?? 1) === 0) { var1 |= 1 } return var1 } applyLoadedSpriteFlags(var1: number): void { this.bikeSpriteSetting?.setCurrentOptionPos(1) this.driverSpriteSetting?.setCurrentOptionPos(1) if ((var1 & 1) > 0) { this.bikeSpriteSetting?.setCurrentOptionPos(0) } if ((var1 & 2) > 0) { this.driverSpriteSetting?.setCurrentOptionPos(0) } } getSelectedLevel(): number { return this.settingStringLevel?.getCurrentOptionPos() ?? 0 } getSelectedTrack(): number { return this.settingsStringTrack?.getCurrentOptionPos() ?? 0 } getSelectedLeague(): number { return this.settingsStringLeague?.getCurrentOptionPos() ?? 0 } getZoom(): number { return (this.zoomSetting?.getCurrentOptionPos() ?? 0) + 1 } setFinishTime(var1: number): void { this.lastFinishTime = var1 } private readStoredNameBytes(var1: number, var2: number): Int8Array { switch (var1) { case 16: { const var3 = new Int8Array(3) for (let var4 = 0; var4 < 3; ++var4) { var3[var4] = this.persistedStateBuffer[16 + var4] } if (var3[0] === -127) { var3[0] = var2 } return var3 } default: return new Int8Array(0) } } private readStoredValue(var1: number, var2: number): number { return this.persistedStateBuffer[var1] === -127 ? var2 : this.persistedStateBuffer[var1] } private copyThreeBytesFromArr(var1: number, var2: Uint8Array): void { if (this.isRecordStoreOpened && var1 === 16) { for (let i = 0; i < 3; ++i) { this.persistedStateBuffer[16 + i] = var2[i] } } } private timeToString(time: number): string { this.lastFinishSeconds = Math.trunc(time / 100) this.lastFinishCentiseconds = Math.trunc(time % 100) let timeStr = this.lastFinishSeconds / 60 < 10 ? ` 0${Math.trunc(this.lastFinishSeconds / 60)}` : ` ${Math.trunc(this.lastFinishSeconds / 60)}` if (this.lastFinishSeconds % 60 < 10) { timeStr += `:0${this.lastFinishSeconds % 60}` } else { timeStr += `:${this.lastFinishSeconds % 60}` } if (this.lastFinishCentiseconds < 10) { timeStr += `.0${this.lastFinishCentiseconds}` } else { timeStr += `.${this.lastFinishCentiseconds}` } return timeStr } private setValue(pos: number, value: number): void { if (this.isRecordStoreOpened) { this.persistedStateBuffer[pos] = value } } private exit(): void { this.perspectiveSetting?.setCurrentOptionPos(0) this.shadowsSetting?.setCurrentOptionPos(0) this.driverSpriteSetting?.setCurrentOptionPos(0) this.bikeSpriteSetting?.setCurrentOptionPos(0) this.lookAheadSetting?.setCurrentOptionPos(0) this.zoomSetting?.setCurrentOptionPos(0) this.settingsStringLeague?.setCurrentOptionPos(0) this.settingsStringLeague?.setAvailableOptions(0) this.settingStringLevel?.setCurrentOptionPos(0) this.settingStringLevel?.setAvailableOptions(1) this.settingsStringTrack?.setCurrentOptionPos(0) this.playerNameBytes[0] = 65 this.playerNameBytes[1] = 65 this.playerNameBytes[2] = 65 this.inputSetting?.setCurrentOptionPos(0) this.unlockedTracksByLevel[0] = 0 this.unlockedTracksByLevel[1] = 0 this.unlockedTracksByLevel[2] = -1 this.availableLeagues = 0 this.persistState() this.recordManager?.deleteRecordStores() } private getCountOfRecordStoresWithPrefix(prefixNumber: number): number { const storeNames = RecordStore.listRecordStores() if (this.recordManager !== null && storeNames.length !== 0) { let count = 0 for (let i = 0; i < storeNames.length; ++i) { if (storeNames[i].startsWith(String(prefixNumber))) { ++count } } return count } return 0 } private makeString(value: Uint8Array): string { return String.fromCharCode(value[0], value[1], value[2]) } } ===== FILE: src/Micro.ts ===== import type { GameCanvas } from './GameCanvas.ts' import type { GamePhysics } from './GamePhysics.ts' import type { LevelLoader } from './LevelLoader.ts' import type { MenuManager } from './MenuManager.ts' export class Micro { gameCanvas: GameCanvas | null = null levelLoader: LevelLoader | null = null gamePhysics: GamePhysics | null = null menuManager: MenuManager | null = null isLoadingBlocked = false numPhysicsLoops = 2 timeMs = 0 gameTimeMs = 0 crashRestartDeadlineMs = 0 isInited = false isTimerRunning = false static isGameVisible = false static isInGameMenu = false static gameLoadingStateStage = 0 gameToMenu(): void { this.gameCanvas?.hideMenuButton() if (this.gameCanvas !== null) { this.gameCanvas.isDrawingTime = false this.gameCanvas.hideBackButton() } Micro.isInGameMenu = true } menuToGame(): void { Micro.isInGameMenu = false if (this.gameCanvas !== null) { this.gameCanvas.isDrawingTime = true this.gameCanvas.hideBackButton() } this.gameCanvas?.showMenuButton() } } ===== FILE: src/RecordManager.ts ===== import { RecordStore } from './rms/RecordStore.ts' export class RecordManager { static readonly LEAGUES_MAX = 4 static readonly RECORD_NO_MAX = 3 static readonly PLAYER_NAME_MAX = 3 static readonly unused = 3 private readonly recordTimeMs = Array.from({ length: 4 }, () => Array(3).fill(0)) private readonly recordName = Array.from({ length: RecordManager.LEAGUES_MAX }, () => Array.from({ length: RecordManager.RECORD_NO_MAX }, () => new Uint8Array(RecordManager.PLAYER_NAME_MAX + 1)), ) private recordStore: RecordStore | null = null private packedRecordInfoRecordId = -1 private readonly packedRecordInfo = new Int8Array(96) private str = '' openRecordStoreForTrack(var1: number, var2: number): void { this.resetRecordsTime() this.str = `${var1}${var2}` this.recordStore = RecordStore.openRecordStore(this.str, true) this.packedRecordInfoRecordId = -1 const recordEnum = this.recordStore.enumerateRecords(null, null, false) if (recordEnum.numRecords() > 0) { const var4 = recordEnum.nextRecord() recordEnum.reset() this.packedRecordInfoRecordId = recordEnum.nextRecordId() this.loadRecordInfo(var4) recordEnum.destroy() } } private load5BytesAsLong(var1: Int8Array, offset: number): number { let result = 0 let mult = 1 for (let var7 = offset; var7 < 5 + offset; ++var7) { const var8 = (var1[var7] + 256) % 256 result += mult * var8 mult *= 256 } return result } private pushLongAs5Bytes(var1: Int8Array, var2: number, var3: number): void { for (let var5 = var2; var5 < 5 + var2; ++var5) { var1[var5] = var3 % 256 var3 = Math.trunc(var3 / 256) } } private loadRecordInfo(var1: Int8Array): void { let offset = 0 for (let league = 0; league < 4; ++league) { for (let pos = 0; pos < 3; ++pos) { this.recordTimeMs[league][pos] = this.load5BytesAsLong(var1, offset) offset += 5 } } for (let league = 0; league < RecordManager.LEAGUES_MAX; ++league) { for (let pos = 0; pos < RecordManager.RECORD_NO_MAX; ++pos) { for (let i = 0; i < RecordManager.PLAYER_NAME_MAX; ++i) { this.recordName[league][pos][i] = var1[offset++] } } } } private getLevelInfo(var1: Int8Array): void { let shift = 0 for (let league = 0; league < 4; ++league) { for (let recordNo = 0; recordNo < 3; ++recordNo) { this.pushLongAs5Bytes(var1, shift, this.recordTimeMs[league][recordNo]) shift += 5 } } for (let league = 0; league < RecordManager.LEAGUES_MAX; ++league) { for (let recordNo = 0; recordNo < RecordManager.RECORD_NO_MAX; ++recordNo) { for (let i = 0; i < RecordManager.PLAYER_NAME_MAX; ++i) { var1[shift++] = this.recordName[league][recordNo][i] } } } } private resetRecordsTime(): void { for (let league = 0; league < 4; ++league) { for (let pos = 0; pos < 3; ++pos) { this.recordTimeMs[league][pos] = 0 } } } getRecordDescription(var1: number): string[] { const var2 = new Array(3).fill('') for (let var3 = 0; var3 < 3; ++var3) { if (this.recordTimeMs[var1][var3] !== 0) { const var4 = Math.trunc(this.recordTimeMs[var1][var3] / 100) const var5 = this.recordTimeMs[var1][var3] % 100 const name = String.fromCharCode(...Array.from(this.recordName[var1][var3].slice(0, 3))).replace(/\0/g, '') var2[var3] = `${name} ` if (Math.trunc(var4 / 60) < 10) { var2[var3] += ` 0${Math.trunc(var4 / 60)}` } else { var2[var3] += ` ${Math.trunc(var4 / 60)}` } if (var4 % 60 < 10) { var2[var3] += `:0${var4 % 60}` } else { var2[var3] += `:${var4 % 60}` } if (var5 < 10) { var2[var3] += `.0${var5}` } else { var2[var3] += `.${var5}` } } else { var2[var3] = '' } } return var2 } writeRecordInfo(): void { this.getLevelInfo(this.packedRecordInfo) if (this.recordStore === null) { return } if (this.packedRecordInfoRecordId === -1) { this.packedRecordInfoRecordId = this.recordStore.addRecord(this.packedRecordInfo, 0, 96) } else { this.recordStore.setRecord(this.packedRecordInfoRecordId, this.packedRecordInfo, 0, 96) } } getPosOfNewRecord(league: number, timeMs: number): number { for (let i = 0; i < 3; ++i) { if (this.recordTimeMs[league][i] > timeMs || this.recordTimeMs[league][i] === 0) { return i } } return 3 } addRecordIfNeeded(league: number, values: Uint8Array, timeMs: number): void { const newRecordPos = this.getPosOfNewRecord(league, timeMs) if (newRecordPos !== 3) { if (timeMs > 16777000) { timeMs = 16777000 } this.addNewRecord(league, newRecordPos) this.recordTimeMs[league][newRecordPos] = timeMs for (let i = 0; i < RecordManager.PLAYER_NAME_MAX; ++i) { this.recordName[league][newRecordPos][i] = values[i] } } } private addNewRecord(gameLevel: number, position: number): void { for (let pos = 2; pos > position; --pos) { this.recordTimeMs[gameLevel][pos] = this.recordTimeMs[gameLevel][pos - 1] for (let i = 0; i < RecordManager.PLAYER_NAME_MAX; ++i) { this.recordName[gameLevel][pos][i] = this.recordName[gameLevel][pos - 1][i] } } } deleteRecordStores(): void { const names = RecordStore.listRecordStores() for (const name of names) { if (name !== 'GWTRStates') { RecordStore.deleteRecordStore(name) } } } closeRecordStore(): void { this.recordStore?.closeRecordStore() } } ===== FILE: src/SettingsStringRender.ts ===== import { GameCanvas } from './GameCanvas.ts' import type { GameMenu } from './GameMenu.ts' import type { IGameMenuElement } from './IGameMenuElement.ts' import type { IMenuManager } from './IMenuManager.ts' import type { Micro } from './Micro.ts' import { Graphics } from './lcdui/Graphics.ts' import { GameMenu as GameMenuClass } from './GameMenu.ts' export class SettingsStringRender implements IGameMenuElement, IMenuManager { private optionsList: string[] = [] private currentOptionPos = 0 private maxAvailableOption = 0 private text: string private readonly menuManager: IMenuManager private currentGameMenu: GameMenu | null = null private parentGameMenu: GameMenu | null = null private readonly isToggleSetting: boolean private selectionMenuRequested = false private selectedOptionName = '' private readonly micro: Micro private settingsStringRenders: SettingsStringRender[] = [] private hasSprite = false private isDrawSprite8 = false private readonly useColon: boolean constructor(text: string, isDisabled: number, menuManager: IMenuManager, optionsList: string[], isToggleSetting: boolean, micro: Micro, parentGameMenu: GameMenu | null, useColon: boolean) { this.micro = micro this.menuManager = menuManager this.useColon = useColon this.isToggleSetting = isToggleSetting this.parentGameMenu = parentGameMenu if (useColon) { this.text = text this.isDrawSprite8 = true return } this.text = `${text}:` this.currentOptionPos = isDisabled this.optionsList = optionsList.length === 0 ? [''] : optionsList this.maxAvailableOption = optionsList.length - 1 this.setCurrentOptionPos(isDisabled) if (isToggleSetting) { this.selectedOptionName = isDisabled === 1 ? 'Off' : 'On' } else { this.selectCurrentOptionName() this.init() } } setFlags(hasSprite: boolean, isDrawSprite8: boolean): void { this.hasSprite = hasSprite this.isDrawSprite8 = isDrawSprite8 } setOptionsList(var1: string[]): void { this.optionsList = var1 if (this.currentOptionPos > this.optionsList.length - 1) { this.currentOptionPos = this.optionsList.length - 1 } if (this.maxAvailableOption > this.optionsList.length - 1) { this.maxAvailableOption = this.optionsList.length - 1 } this.selectCurrentOptionName() this.init() } init(): void { this.currentGameMenu = new GameMenuClass(this.text, this.micro, this.parentGameMenu) this.settingsStringRenders = new Array(this.optionsList.length) for (let var1 = 0; var1 < this.settingsStringRenders.length; ++var1) { this.settingsStringRenders[var1] = new SettingsStringRender(this.optionsList[var1], 0, this, [], false, this.micro, this.parentGameMenu, true) if (var1 > this.maxAvailableOption) { this.settingsStringRenders[var1].setFlags(true, true) } this.currentGameMenu.addMenuElement(this.settingsStringRenders[var1]) } } setParentGameMenu(parentGameMenu: GameMenu | null): void { this.parentGameMenu = parentGameMenu } getText(): string { return this.text + (this.selectedOptionName ? ' ' + this.selectedOptionName : '') } setText(text: string): void { this.text = this.useColon ? text : `${text}:` } isNotTextRender(): boolean { return true } menuElemMethod(var1: number): void { if (this.useColon) { if (var1 === 1) { this.menuManager.handleMenuSelection(this) } return } switch (var1) { case 1: if (this.isToggleSetting) { ++this.currentOptionPos if (this.currentOptionPos > 1) { this.currentOptionPos = 0 } this.selectedOptionName = this.currentOptionPos === 1 ? 'Off' : 'On' this.menuManager.handleMenuSelection(this) return } this.selectionMenuRequested = true this.menuManager.handleMenuSelection(this) return case 2: if (this.isToggleSetting) { if (this.currentOptionPos === 1) { this.currentOptionPos = 0 this.selectedOptionName = 'On' this.menuManager.handleMenuSelection(this) } return } ++this.currentOptionPos if (this.currentOptionPos > this.optionsList.length - 1) { this.currentOptionPos = this.optionsList.length - 1 } else { this.menuManager.handleMenuSelection(this) } this.selectCurrentOptionName() return case 3: if (this.isToggleSetting) { if (this.currentOptionPos === 0) { this.currentOptionPos = 1 this.selectedOptionName = 'Off' this.menuManager.handleMenuSelection(this) } return } --this.currentOptionPos if (this.currentOptionPos < 0) { this.currentOptionPos = 0 } else { this.selectCurrentOptionName() this.menuManager.handleMenuSelection(this) } this.selectCurrentOptionName() } } private selectCurrentOptionName(): void { this.selectedOptionName = this.optionsList[this.currentOptionPos] } render(graphics: Graphics, y: number, x: number): void { if (this.useColon) { if (!this.hasSprite) { graphics.drawString(this.text, x, y, Graphics.LEFT | Graphics.TOP) } else if (this.micro.gameCanvas !== null) { graphics.drawString(this.text, x + GameCanvas.spriteSizeX[8] + 3, y, Graphics.LEFT | Graphics.TOP) this.micro.gameCanvas.drawSprite(graphics, this.isDrawSprite8 ? 8 : 9, x, y - Math.trunc(GameCanvas.spriteSizeY[this.isDrawSprite8 ? 8 : 9] / 2) + Math.trunc(graphics.getFont().getHeight() / 2)) } return } graphics.drawString(this.text, x, y, Graphics.LEFT | Graphics.TOP) let shiftedX = x + graphics.getFont().stringWidth(this.text) if (this.currentOptionPos > this.maxAvailableOption && !this.isToggleSetting && this.micro.gameCanvas !== null) { this.micro.gameCanvas.drawSprite(graphics, 8, shiftedX + 1, y - Math.trunc(GameCanvas.spriteSizeY[8] / 2) + Math.trunc(graphics.getFont().getHeight() / 2)) shiftedX += GameCanvas.spriteSizeX[9] + 1 } shiftedX += 2 graphics.drawString(this.selectedOptionName, shiftedX, y, Graphics.LEFT | Graphics.TOP) } setAvailableOptions(maxAvailableOption: number): void { this.maxAvailableOption = maxAvailableOption if (maxAvailableOption > this.optionsList.length - 1) { maxAvailableOption = this.optionsList.length - 1 } if (this.currentGameMenu !== null) { for (let i = 0; i < this.settingsStringRenders.length; ++i) { this.settingsStringRenders[i].setFlags(i > maxAvailableOption, i > maxAvailableOption) } } } getMaxAvailableOptionPos(): number { return this.maxAvailableOption } getMaxOptionPos(): number { return this.optionsList.length - 1 } getOptionsList(): string[] { return this.optionsList } setCurrentOptionPos(pos: number): void { this.currentOptionPos = pos if (this.currentOptionPos > this.optionsList.length - 1) { this.currentOptionPos = 0 } if (this.currentOptionPos < 0) { this.currentOptionPos = this.optionsList.length - 1 } this.selectCurrentOptionName() } getCurrentOptionPos(): number { return this.currentOptionPos } getCurrentMenu(): GameMenu | null { return this.currentGameMenu } openMenu(menu: GameMenu | null, preserveSelection: boolean): void { this.menuManager.openMenu(menu, preserveSelection) } saveAndClose(): void { this.menuManager.saveAndClose() } handleMenuSelection(var1: IGameMenuElement): void { for (let var2 = 0; var2 < this.settingsStringRenders.length; ++var2) { if (var1 === this.settingsStringRenders[var2]) { this.currentOptionPos = var2 this.selectCurrentOptionName() break } } this.menuManager.openMenu(this.parentGameMenu, true) this.menuManager.handleMenuSelection(this) } getSettingsStringRenders(): SettingsStringRender[] { return this.settingsStringRenders } consumeSelectionMenuRequested(): boolean { const var1 = this.selectionMenuRequested this.selectionMenuRequested = false return var1 } } ===== FILE: src/TextRender.ts ===== import type { IGameMenuElement } from './IGameMenuElement.ts' import { Micro } from './Micro.ts' import { Font } from './lcdui/Font.ts' import { Graphics } from './lcdui/Graphics.ts' export class TextRender implements IGameMenuElement { private text: string private static defaultFont: Font private font: Font | null = null private dx = 0 private static fieldMaxWidth = 100 private static fieldMaxHeightUnused = 100 private isDrawSprite = false private spriteNo = 0 private readonly micro: Micro constructor(text: string, var2: Micro) { this.text = text this.micro = var2 } static getBaselinePosition(): number { return TextRender.defaultFont.getBaselinePosition() } setFont(font: Font): void { this.font = font } static setDefaultFont(font: Font): void { TextRender.defaultFont = font } static setMaxArea(w: number, h: number): void { TextRender.fieldMaxWidth = w TextRender.fieldMaxHeightUnused = h void TextRender.fieldMaxHeightUnused } getText(): string { return this.text } setText(text: string): void { this.text = text } isNotTextRender(): boolean { return false } menuElemMethod(_var1: number): void {} render(graphics: Graphics, y: number, x: number): void { const preservedFont = graphics.getFont() graphics.setFont(TextRender.defaultFont) if (this.font !== null) { graphics.setFont(this.font) } graphics.drawString(this.text, x + this.dx, y, Graphics.LEFT | Graphics.TOP) if (this.isDrawSprite && this.micro.gameCanvas !== null) { this.micro.gameCanvas.drawSprite(graphics, this.spriteNo, x, y) } graphics.setFont(preservedFont) } static makeMultilineTextRenders(text: string, micro: Micro): TextRender[] { let startPos = 0 let endPos = 0 const var4 = 25 const vector: TextRender[] = [] while (endPos < text.length) { let var6 = text.indexOf(' ', startPos) if (var6 === -1) { endPos = text.length var6 = text.length } while (endPos < text.length && TextRender.defaultFont.substringWidth(text, startPos, var6 - startPos) < TextRender.fieldMaxWidth - var4) { endPos = var6 + 1 var6 = text.indexOf(' ', var6 + 1) if (var6 === -1) { if (TextRender.defaultFont.substringWidth(text, startPos, text.length - 1 - startPos) <= TextRender.fieldMaxWidth - var4) { endPos = text.length } break } } vector.push(new TextRender(text.substring(startPos, endPos), micro)) startPos = ++endPos - 1 } return vector } setDx(var1: number): void { this.dx = var1 } setDrawSprite(isDrawSprite: boolean, spriteNo: number): void { this.isDrawSprite = isDrawSprite this.spriteNo = spriteNo } } ===== FILE: src/Timer.ts ===== import { Time } from './utils/Time.ts' export class Timer { private readonly id: number private readonly startTimeMs: number private readonly timeoutMs: number constructor(id: number, timeoutMs: number) { this.id = id this.timeoutMs = timeoutMs this.startTimeMs = Time.currentTimeMillis() } ready(): boolean { return Time.currentTimeMillis() - this.startTimeMs > this.timeoutMs } getId(): number { return this.id } } ===== FILE: src/TimerOrMotoPartOrMenuElem.ts ===== import type { IGameMenuElement } from './IGameMenuElement.ts' import type { GameMenu } from './GameMenu.ts' import type { IMenuManager } from './IMenuManager.ts' import { Graphics } from './lcdui/Graphics.ts' export class TimerOrMotoPartOrMenuElem implements IGameMenuElement { private text = '' private targetMenu: GameMenu | null = null private menuManager: IMenuManager | null = null xF16 = 0 yF16 = 0 angleF16 = 0 velocityXF16 = 0 velocityYF16 = 0 angularVelocityF16 = 0 forceXF16 = 0 forceYF16 = 0 torqueF16 = 0 timerNo = 0 micro: unknown = null constructor() constructor(timerNo: number, micro: unknown) constructor(text: string, targetMenu: GameMenu | null, menuManager: IMenuManager | null) constructor(var1?: number | string, var2?: unknown, var3?: unknown) { this.resetState() if (typeof var1 === 'number') { this.micro = var2 this.timerNo = var1 return } if (typeof var1 === 'string') { this.text = var1 + '>' this.targetMenu = (var2 as GameMenu | null) ?? null this.menuManager = (var3 as IMenuManager | null) ?? null } } resetState(): void { this.xF16 = 0 this.yF16 = 0 this.angleF16 = 0 this.velocityXF16 = 0 this.velocityYF16 = 0 this.angularVelocityF16 = 0 this.forceXF16 = 0 this.forceYF16 = 0 this.torqueF16 = 0 } setText(text: string): void { this.text = text + '>' } getText(): string { return this.text } isNotTextRender(): boolean { return true } menuElemMethod(_var1: number): void { switch (_var1) { case 1: case 2: this.menuManager?.handleMenuSelection(this) this.targetMenu?.setParentMenu(this.menuManager?.getCurrentMenu() ?? null) this.menuManager?.openMenu(this.targetMenu, false) break default: break } } setParentMenu(parentMenu: GameMenu | null): void { this.targetMenu = parentMenu } render(graphics: Graphics, y: number, x: number): void { graphics.drawString(this.text, x, y, Graphics.LEFT | Graphics.TOP) } } ===== FILE: src/TouchControls.ts ===== type TouchControlsHandlers = { keyPressed: (keyCode: number) => void keyReleased: (keyCode: number) => void back: () => void } export class TouchControls { private readonly container: HTMLDivElement constructor(root: HTMLElement, handlers: TouchControlsHandlers) { this.container = document.createElement('div') this.container.className = 'touch-controls' this.container.addEventListener('contextmenu', (event) => event.preventDefault()) const keys: Array<[string, string, number]> = [ ['touch-left', 'Lean backward', 2], ['touch-right', 'Lean forward', 5], ['touch-up', 'Accelerate', 1], ['touch-down', 'Brake', 6], ['touch-ok', 'Select', 8], ] for (const [className, label, keyCode] of keys) { const button = this.addButton(className, label) button.addEventListener('pointerdown', (event) => { event.preventDefault() button.setPointerCapture(event.pointerId) button.classList.add('pressed') handlers.keyPressed(keyCode) }) const release = (): void => { button.classList.remove('pressed') handlers.keyReleased(keyCode) } button.addEventListener('pointerup', release) button.addEventListener('pointercancel', release) } this.addButton('touch-back', 'Menu or back').addEventListener('pointerdown', (event) => { event.preventDefault() handlers.back() }) root.append(this.container) } update(isInMenu: boolean, isBackAvailable: boolean): void { const className = `touch-controls${isInMenu ? ' in-menu' : ''}${isBackAvailable ? ' has-back' : ''}` if (this.container.className !== className) { this.container.className = className } } private addButton(className: string, label: string): HTMLButtonElement { const button = document.createElement('button') button.type = 'button' button.className = className button.setAttribute('aria-label', label) this.container.append(button) return button } } ===== FILE: src/app.ts ===== import { GameCanvas } from './GameCanvas.ts' import { GamePhysics } from './GamePhysics.ts' import { LevelLoader } from './LevelLoader.ts' import { MenuManager } from './MenuManager.ts' import { Micro } from './Micro.ts' import { TouchControls } from './TouchControls.ts' import LEVELS_DATA from './assets/levels.json' type AppState = { loadedSpriteFlags: number lastOuterStepMs: number outerAccumulatorMs: number lastMenuStepMs: number isGoalLoopActive: boolean goalLoopEndMs: number lastGoalLoopStepMs: number forcedRestartMs: number wasInGameMenu: boolean } export async function startGravityDefiedApp(root: HTMLDivElement): Promise { root.className = 'app-root' const canvasElement = document.createElement('canvas') canvasElement.className = 'game-canvas' root.replaceChildren(canvasElement) const micro = new Micro() Micro.isGameVisible = true const levelLoader = await LevelLoader.create(LEVELS_DATA) const gamePhysics = new GamePhysics(levelLoader) const gameCanvas = await GameCanvas.create(canvasElement, micro) const menuManager = new MenuManager(micro) micro.levelLoader = levelLoader micro.gamePhysics = gamePhysics micro.gameCanvas = gameCanvas micro.menuManager = menuManager gameCanvas.init(gamePhysics) for (let i = 1; i <= 7; ++i) { menuManager.initPart(i) } gameCanvas.setMenuManager(menuManager) const state: AppState = { loadedSpriteFlags: await gameCanvas.loadSprites(menuManager.getLoadedSpriteFlags()), lastOuterStepMs: performance.now(), outerAccumulatorMs: 0, lastMenuStepMs: performance.now(), isGoalLoopActive: false, goalLoopEndMs: 0, lastGoalLoopStepMs: 0, forcedRestartMs: 0, wasInGameMenu: false, } gamePhysics.applyLoadedSpriteFlags(state.loadedSpriteFlags) menuManager.applyLoadedSpriteFlags(state.loadedSpriteFlags) gamePhysics.setMode(1) const touchControls = new TouchControls(root, { keyPressed: (keyCode) => { gameCanvas.keyPressed(keyCode) render() }, keyReleased: (keyCode) => gameCanvas.keyReleased(keyCode), back: () => { handleBack() render() }, }) function resize(): void { const rect = root.getBoundingClientRect() let width = Math.floor(rect.width) let height = Math.floor(rect.height) if (width <= 0 || height <= 0) { width = window.innerWidth height = window.innerHeight } gameCanvas.resize(width, height, menuManager.getZoom()) gamePhysics.setMinimalScreenWH(Math.min(gameCanvas.getWidth(), gameCanvas.getHeight())) } let lastMenuView = '' const panel = document.querySelector('#menu-panel')! function render(): void { const view = Micro.isInGameMenu ? menuManager.getCurrentMenu()?.getView() : null panel.hidden = !view || view.nameEntry if (view && !view.nameEntry) { const key = JSON.stringify(view) if (key !== lastMenuView) { lastMenuView = key panel.replaceChildren() const kicker = document.createElement('p'); kicker.className = 'eyebrow'; kicker.textContent = 'GRAVITY DEFIED · TRAIL EDITION'; panel.append(kicker) const title = document.createElement('h2'); title.textContent = view.title; panel.append(title) for (const item of view.items) { const el = document.createElement(item.action ? 'button' : 'p') el.textContent = item.text if (item.action) { el.className = item.index === view.selected ? 'menu-item selected' : 'menu-item' el.addEventListener('click', () => { menuManager.getCurrentMenu()?.activateItem(item.index); render() }) } panel.append(el) } } } const progress = Math.max(0, Math.min(100, Math.round(gamePhysics.getProgressF16() / 655.36))) const isRecovering = gamePhysics.isCrashed || micro.crashRestartDeadlineMs > 0 || state.forcedRestartMs > 0 const status = document.querySelector('#ride-status')! status.textContent = Micro.isInGameMenu ? 'Plan your next ride' : state.isGoalLoopActive ? 'Trail complete!' : isRecovering ? 'Took a tumble — retrying…' : 'Keep both wheels grounded. Find your balance.' document.querySelector('#race-time')!.textContent = (micro.gameTimeMs / 1000).toFixed(2) + 's' root.dataset.mode = Micro.isInGameMenu ? 'menu' : 'ride' root.dataset.time = String(micro.gameTimeMs) root.dataset.progress = String(progress) root.dataset.x = String(gamePhysics.getCamPosX()) root.dataset.crashed = String(isRecovering) document.querySelector('#trail-name')!.textContent = levelLoader.getName(menuManager.getCurrentLevel(), menuManager.getCurrentTrack()) document.querySelector('#progress')!.textContent = Micro.isInGameMenu && !view?.title.includes('Finished') ? '—' : progress + '%' gameCanvas.paint(gameCanvas.getGraphics()) touchControls.update(Micro.isInGameMenu, isBackAvailable()) } function isBackAvailable(): boolean { if (Micro.isInGameMenu) { const menu = menuManager.getCurrentMenu() return menu !== null && menu.getParentMenu() !== null } return gameCanvas.hasMenuButton() } function handleBack(): boolean { if (Micro.isInGameMenu) { gameCanvas.handleBackAction() return true } if (gameCanvas.hasMenuButton()) { gameCanvas.openPauseMenu() return true } return false } gameCanvas.setRepaintHandler(render) function restart(var1: boolean): void { gamePhysics.resetSmth(true) micro.timeMs = 0 micro.gameTimeMs = 0 micro.crashRestartDeadlineMs = 0 micro.isTimerRunning = false state.goalLoopEndMs = 0 state.isGoalLoopActive = false state.lastGoalLoopStepMs = 0 state.forcedRestartMs = 0 if (var1) { gameCanvas.scheduleGameTimerTask(levelLoader.getName(menuManager.getCurrentLevel(), menuManager.getCurrentTrack()), 3000) } gameCanvas.resetInputState() } function openFinishedMenu(): void { menuManager.setFinishTime(micro.gameTimeMs / 10) menuManager.showMenuScreen(2) } function startGoalLoop(nowMs: number): void { state.goalLoopEndMs = nowMs + 1000 state.isGoalLoopActive = true state.lastGoalLoopStepMs = nowMs gameCanvas.scheduleGameTimerTask(gamePhysics.isTrackFinished ? 'Finished' : 'Wheelie!', 1000) } function updateSpriteMode(): void { const nextSpriteFlags = menuManager.getLoadedSpriteFlags() if (gamePhysics.getLoadedSpriteFlags() !== nextSpriteFlags) { void gameCanvas.loadSprites(nextSpriteFlags).then((var5) => { gamePhysics.applyLoadedSpriteFlags(var5) menuManager.applyLoadedSpriteFlags(var5) state.loadedSpriteFlags = var5 }) } } function outerStep(nowMs: number): void { updateSpriteMode() if (state.forcedRestartMs !== 0) { if (nowMs >= state.forcedRestartMs) { restart(true) } return } for (let i = micro.numPhysicsLoops; i > 0; --i) { if (micro.isTimerRunning) { micro.gameTimeMs += 20 } if (micro.timeMs === 0) { micro.timeMs = Date.now() } const var5 = gamePhysics.updatePhysics() if (var5 === 3 && micro.crashRestartDeadlineMs === 0) { micro.crashRestartDeadlineMs = Date.now() + 3000 gameCanvas.scheduleGameTimerTask('Crashed', 3000) gameCanvas.repaint() gameCanvas.serviceRepaints() } if (micro.crashRestartDeadlineMs !== 0 && micro.crashRestartDeadlineMs < Date.now()) { restart(true) return } if (var5 === 5) { gameCanvas.scheduleGameTimerTask('Crashed', 3000) gameCanvas.repaint() gameCanvas.serviceRepaints() let waitMs = 1000 if (micro.crashRestartDeadlineMs > 0) { waitMs = Math.min(micro.crashRestartDeadlineMs - Date.now(), 1000) } if (waitMs < 0) { waitMs = 0 } state.forcedRestartMs = nowMs + waitMs return } if (var5 === 4) { micro.timeMs = 0 micro.gameTimeMs = 0 } else if (var5 === 1 || var5 === 2) { if (var5 === 2) { micro.gameTimeMs -= 10 } startGoalLoop(nowMs) micro.isTimerRunning = true return } micro.isTimerRunning = var5 !== 4 } gamePhysics.syncRenderStateFromSimulation() } function goalLoopStep(nowMs: number): void { if (!state.isGoalLoopActive) { return } if (nowMs >= state.goalLoopEndMs) { state.isGoalLoopActive = false openFinishedMenu() return } while (nowMs - state.lastGoalLoopStepMs >= 30) { for (let i = micro.numPhysicsLoops; i > 0; --i) { if (gamePhysics.updatePhysics() === 5) { state.isGoalLoopActive = false state.goalLoopEndMs = nowMs openFinishedMenu() return } } gamePhysics.syncRenderStateFromSimulation() state.lastGoalLoopStepMs += 30 } } function menuStep(nowMs: number): void { if (!Micro.isInGameMenu) { return } while (nowMs - state.lastMenuStepMs >= 50) { if (gamePhysics.isGenerateInputAI) { const var9 = gamePhysics.updatePhysics() if (var9 !== 0 && var9 !== 4) { gamePhysics.resetSmth(true) } gamePhysics.syncRenderStateFromSimulation() } state.lastMenuStepMs += 50 if (!gamePhysics.isGenerateInputAI) { break } } } document.querySelector('#pause')!.addEventListener('click', () => { handleBack(); render() }) document.querySelector('#retry')!.addEventListener('click', () => { if (!Micro.isInGameMenu) { restart(true); render() } }) window.addEventListener('blur', () => { gameCanvas.resetInputState(); if (!Micro.isInGameMenu) handleBack() }) document.addEventListener('visibilitychange', () => { if (document.hidden) { gameCanvas.resetInputState(); if (!Micro.isInGameMenu) handleBack() } }) resize() gameCanvas.requestRepaint(0) restart(false) menuManager.showMenuScreen(0) function loop(now: number): void { if (menuManager.getZoom() !== gameCanvas.getZoom()) { resize() } if (Micro.isInGameMenu && !state.wasInGameMenu) { state.lastMenuStepMs = now } if (Micro.isInGameMenu && !state.wasInGameMenu && menuManager.isOpeningPauseMenu) { menuManager.showMenuScreen(1) } if (Micro.isInGameMenu) { menuStep(now) render() state.wasInGameMenu = true requestAnimationFrame(loop) return } if (state.wasInGameMenu) { state.lastOuterStepMs = now state.outerAccumulatorMs = 0 } state.wasInGameMenu = false if (menuManager.consumeRestartRequested()) { restart(true) } if (state.isGoalLoopActive) { goalLoopStep(now) render() requestAnimationFrame(loop) return } state.outerAccumulatorMs += now - state.lastOuterStepMs state.lastOuterStepMs = now while (state.outerAccumulatorMs >= 30) { outerStep(now) state.outerAccumulatorMs -= 30 if (Micro.isInGameMenu || state.isGoalLoopActive) { break } } if (menuManager.consumeRestartRequested()) { restart(true) } render() requestAnimationFrame(loop) } const handleResize = (): void => { resize() render() } const resizeObserver = new ResizeObserver(() => { handleResize() }) resizeObserver.observe(root) window.addEventListener('resize', handleResize) window.visualViewport?.addEventListener('resize', handleResize) window.addEventListener('keydown', (event) => { let handled = false const keyCode = browserKeyCodeToGameKeyCode(event.code) if (event.code === 'Escape') { handled = handleBack() } if (keyCode !== null) { gameCanvas.keyPressed(keyCode) handled = true } if (handled) { event.preventDefault() render() } }) window.addEventListener('keyup', (event) => { const keyCode = browserKeyCodeToGameKeyCode(event.code) if (keyCode !== null) { gameCanvas.keyReleased(keyCode) event.preventDefault() } }) window.addEventListener('pagehide', () => { menuManager.persistState() }) requestAnimationFrame(loop) } function browserKeyCodeToGameKeyCode(code: string): number | null { switch (code) { case 'ArrowUp': return 1 case 'ArrowLeft': return 2 case 'ArrowRight': return 5 case 'ArrowDown': return 6 case 'Space': case 'Enter': case 'NumpadEnter': return 8 case 'Digit0': return 48 case 'Digit1': return 49 case 'Digit2': return 50 case 'Digit3': return 51 case 'Digit4': return 52 case 'Digit5': return 53 case 'Digit6': return 54 case 'Digit7': return 55 case 'Digit8': return 56 case 'Digit9': return 57 default: return null } } ===== FILE: src/assets/levels.json ===== [0, 0, 0, 10, 0, 0, 1, 121, 73, 110, 116, 114, 111, 0, 0, 0, 1, 236, 83, 104, 111, 114, 116, 121, 0, 0, 0, 2, 92, 83, 108, 111, 112, 101, 0, 0, 0, 3, 34, 67, 114, 97, 99, 107, 108, 101, 0, 0, 0, 3, 161, 75, 110, 111, 108, 108, 115, 0, 0, 0, 4, 84, 68, 101, 101, 112, 0, 0, 0, 4, 221, 67, 108, 105, 102, 102, 0, 0, 0, 5, 74, 72, 111, 108, 101, 0, 0, 0, 5, 199, 79, 114, 105, 103, 105, 110, 97, 108, 0, 0, 0, 6, 134, 83, 97, 118, 118, 121, 0, 0, 0, 0, 10, 0, 0, 7, 9, 73, 110, 100, 111, 111, 114, 0, 0, 0, 7, 113, 83, 112, 105, 107, 101, 104, 111, 108, 101, 115, 0, 0, 0, 8, 34, 68, 111, 119, 110, 104, 105, 108, 108, 0, 0, 0, 8, 193, 72, 105, 108, 108, 99, 108, 105, 109, 98, 0, 0, 0, 9, 102, 77, 111, 100, 101, 115, 116, 121, 0, 0, 0, 10, 23, 66, 108, 111, 99, 107, 115, 0, 0, 0, 10, 188, 66, 117, 109, 112, 115, 0, 0, 0, 11, 53, 83, 112, 105, 107, 101, 104, 111, 112, 115, 0, 0, 0, 11, 220, 80, 105, 108, 108, 97, 114, 0, 0, 0, 12, 110, 84, 114, 101, 110, 99, 104, 101, 115, 0, 0, 0, 0, 10, 0, 0, 13, 3, 70, 108, 111, 111, 114, 98, 111, 97, 114, 100, 115, 0, 0, 0, 13, 162, 84, 105, 112, 95, 116, 111, 112, 0, 0, 0, 14, 35, 68, 97, 110, 116, 101, 115, 95, 80, 101, 97, 107, 0, 0, 0, 14, 222, 65, 98, 114, 117, 112, 116, 0, 0, 0, 15, 113, 85, 110, 100, 101, 114, 116, 97, 107, 101, 114, 0, 0, 0, 16, 72, 76, 105, 98, 101, 114, 116, 121, 0, 0, 0, 17, 9, 73, 110, 116, 101, 110, 115, 101, 0, 0, 0, 17, 190, 49, 48, 48, 37, 0, 0, 0, 18, 159, 84, 114, 97, 105, 110, 105, 110, 103, 95, 100, 97, 121, 0, 0, 0, 19, 82, 84, 114, 105, 97, 108, 95, 97, 103, 97, 105, 110, 0, 51, 255, 249, 224, 0, 0, 3, 0, 0, 0, 54, 32, 0, 0, 0, 0, 0, 0, 45, 255, 255, 254, 132, 0, 0, 0, 136, 75, 253, 33, 241, 23, 229, 10, 230, 18, 233, 20, 237, 25, 245, 68, 250, 44, 0, 14, 3, 14, 252, 30, 243, 45, 233, 42, 247, 27, 254, 13, 4, 16, 8, 12, 240, 15, 9, 29, 248, 23, 249, 28, 253, 26, 0, 23, 8, 19, 12, 16, 12, 14, 11, 20, 11, 19, 7, 25, 1, 34, 253, 22, 255, 21, 7, 19, 14, 15, 17, 16, 15, 11, 21, 7, 24, 12, 20, 29, 16, 33, 2, 43, 252, 38, 242, 92, 202, 51, 255, 231, 128, 0, 255, 250, 64, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 40, 255, 255, 254, 74, 255, 255, 255, 249, 55, 223, 48, 240, 26, 13, 17, 18, 37, 232, 33, 241, 30, 0, 32, 0, 23, 248, 16, 246, 21, 5, 26, 16, 23, 5, 4, 239, 7, 1, 1, 16, 25, 0, 23, 247, 32, 233, 37, 224, 28, 232, 35, 239, 28, 249, 27, 3, 21, 17, 23, 25, 31, 32, 10, 10, 3, 239, 9, 26, 16, 7, 21, 6, 49, 251, 33, 1, 36, 11, 34, 25, 46, 19, 48, 1, 255, 0, 0, 2, 209, 255, 255, 255, 243, 51, 255, 179, 128, 0, 255, 172, 224, 0, 0, 77, 224, 0, 0, 0, 0, 0, 0, 83, 255, 255, 252, 22, 255, 255, 254, 7, 255, 255, 255, 252, 173, 255, 255, 253, 231, 45, 233, 29, 203, 34, 222, 45, 229, 55, 247, 64, 1, 43, 243, 47, 235, 30, 234, 20, 234, 28, 237, 29, 250, 27, 4, 28, 10, 23, 11, 16, 15, 4, 255, 11, 226, 20, 241, 22, 249, 24, 1, 22, 11, 13, 16, 9, 18, 6, 0, 21, 235, 20, 243, 26, 253, 24, 8, 28, 25, 8, 0, 13, 243, 21, 245, 20, 6, 19, 18, 7, 2, 38, 0, 23, 7, 17, 13, 16, 10, 20, 4, 23, 254, 17, 248, 12, 245, 17, 249, 22, 254, 20, 8, 13, 14, 9, 10, 11, 12, 7, 5, 23, 0, 9, 251, 23, 242, 15, 247, 13, 254, 13, 6, 8, 8, 11, 7, 14, 253, 13, 244, 18, 251, 12, 0, 11, 9, 12, 6, 20, 254, 23, 6, 19, 12, 16, 10, 43, 0, 30, 1, 18, 4, 32, 8, 8, 7, 16, 34, 5, 6, 53, 14, 6, 6, 12, 39, 4, 6, 53, 11, 51, 255, 213, 192, 0, 255, 249, 224, 0, 0, 42, 160, 0, 0, 0, 0, 0, 0, 51, 255, 255, 253, 128, 255, 255, 255, 212, 48, 244, 40, 255, 65, 9, 20, 7, 21, 14, 32, 236, 42, 238, 45, 254, 25, 5, 20, 248, 22, 252, 16, 10, 9, 5, 13, 248, 21, 6, 20, 7, 25, 243, 9, 243, 12, 252, 12, 7, 26, 10, 26, 253, 43, 6, 21, 19, 8, 3, 19, 252, 20, 8, 21, 19, 12, 0, 13, 247, 13, 1, 8, 12, 16, 8, 25, 11, 19, 251, 21, 244, 29, 253, 19, 250, 49, 2, 20, 19, 25, 253, 23, 11, 63, 253, 35, 17, 33, 22, 29, 28, 33, 32, 33, 22, 58, 6, 82, 248, 51, 255, 235, 0, 0, 0, 2, 224, 0, 0, 106, 64, 0, 0, 0, 0, 0, 0, 77, 255, 255, 254, 101, 0, 0, 0, 49, 58, 21, 9, 1, 67, 255, 12, 253, 33, 245, 5, 251, 21, 212, 8, 251, 21, 3, 13, 0, 46, 252, 25, 2, 15, 11, 15, 27, 6, 5, 17, 1, 7, 250, 13, 245, 12, 0, 15, 4, 10, 12, 6, 3, 16, 255, 11, 251, 18, 229, 12, 238, 21, 245, 23, 250, 34, 1, 25, 8, 14, 17, 10, 17, 9, 7, 22, 2, 9, 250, 17, 251, 19, 1, 16, 4, 15, 11, 13, 3, 16, 255, 13, 247, 13, 236, 11, 229, 20, 227, 37, 237, 48, 253, 45, 9, 16, 9, 11, 16, 10, 20, 18, 15, 31, 11, 11, 11, 12, 24, 7, 5, 28, 1, 7, 253, 18, 241, 18, 252, 18, 7, 27, 20, 8, 3, 29, 3, 11, 250, 11, 241, 24, 243, 41, 0, 21, 1, 44, 7, 39, 34, 6, 3, 38, 2, 10, 251, 50, 201, 55, 223, 51, 255, 196, 96, 0, 255, 249, 160, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 49, 255, 255, 253, 17, 255, 255, 255, 199, 111, 39, 39, 10, 34, 247, 8, 250, 22, 230, 30, 236, 30, 0, 25, 0, 55, 0, 51, 0, 13, 5, 12, 12, 11, 12, 44, 0, 9, 178, 3, 194, 4, 184, 7, 207, 6, 237, 14, 67, 5, 72, 2, 102, 2, 20, 9, 23, 17, 24, 16, 16, 19, 8, 20, 1, 21, 248, 18, 242, 8, 237, 8, 162, 255, 255, 255, 255, 183, 255, 255, 254, 244, 17, 42, 255, 255, 255, 255, 215, 255, 255, 255, 219, 31, 7, 36, 0, 42, 5, 33, 8, 35, 24, 9, 5, 58, 0, 10, 250, 34, 218, 8, 252, 62, 248, 6, 2, 55, 32, 51, 255, 230, 64, 0, 255, 245, 128, 0, 0, 41, 96, 0, 0, 0, 0, 0, 0, 35, 255, 255, 254, 3, 255, 255, 255, 131, 255, 255, 255, 254, 133, 255, 255, 255, 183, 16, 188, 36, 2, 26, 73, 42, 2, 24, 218, 37, 0, 44, 0, 30, 25, 33, 0, 9, 152, 29, 252, 9, 105, 49, 5, 11, 141, 26, 248, 9, 119, 41, 12, 28, 236, 30, 225, 34, 8, 38, 229, 34, 24, 25, 220, 26, 9, 39, 0, 32, 0, 34, 56, 53, 9, 21, 229, 32, 254, 11, 84, 47, 16, 255, 0, 0, 2, 193, 255, 255, 255, 209, 51, 255, 230, 192, 0, 255, 246, 32, 0, 0, 64, 128, 0, 0, 0, 0, 0, 0, 50, 255, 255, 254, 24, 255, 255, 255, 236, 115, 8, 38, 241, 31, 227, 48, 227, 30, 243, 32, 0, 32, 0, 17, 20, 24, 14, 32, 0, 7, 226, 26, 246, 27, 11, 57, 246, 24, 5, 27, 14, 1, 210, 12, 215, 6, 32, 8, 175, 4, 69, 5, 205, 9, 66, 6, 61, 27, 9, 24, 34, 24, 28, 33, 24, 34, 27, 33, 21, 12, 34, 23, 3, 22, 242, 10, 221, 20, 225, 27, 250, 29, 22, 11, 15, 17, 3, 10, 221, 26, 254, 36, 7, 37, 5, 34, 20, 43, 21, 26, 3, 28, 28, 18, 35, 87, 49, 51, 255, 180, 64, 0, 255, 173, 32, 0, 0, 75, 64, 0, 0, 0, 0, 0, 0, 76, 255, 255, 252, 140, 255, 255, 254, 4, 63, 167, 77, 239, 47, 231, 23, 219, 10, 251, 51, 255, 33, 0, 34, 0, 27, 17, 23, 26, 10, 25, 9, 0, 9, 231, 23, 229, 24, 241, 18, 2, 15, 5, 26, 17, 20, 15, 12, 6, 14, 1, 3, 203, 46, 0, 7, 14, 29, 0, 8, 13, 24, 0, 24, 236, 35, 1, 28, 27, 21, 42, 8, 9, 14, 2, 2, 238, 255, 255, 255, 255, 203, 255, 255, 253, 1, 255, 255, 255, 255, 213, 255, 255, 253, 150, 2, 18, 15, 254, 7, 248, 23, 208, 24, 234, 22, 251, 31, 0, 17, 3, 24, 13, 27, 1, 26, 17, 4, 16, 29, 1, 31, 9, 12, 13, 13, 12, 15, 9, 36, 13, 12, 253, 9, 246, 29, 234, 13, 250, 60, 4, 17, 10, 21, 29, 7, 5, 9, 0, 8, 252, 34, 221, 13, 250, 52, 8, 12, 6, 21, 13, 20, 31, 6, 5, 31, 2, 27, 243, 59, 33, 78, 66, 51, 255, 226, 128, 0, 0, 2, 96, 0, 0, 55, 192, 0, 0, 0, 0, 0, 0, 46, 255, 255, 253, 177, 0, 0, 0, 88, 84, 161, 57, 234, 60, 23, 16, 251, 29, 50, 15, 11, 47, 246, 19, 209, 13, 8, 69, 245, 50, 21, 13, 237, 10, 237, 10, 255, 11, 21, 67, 33, 18, 239, 23, 35, 29, 0, 7, 234, 17, 244, 9, 237, 18, 248, 9, 234, 24, 251, 255, 0, 0, 0, 143, 255, 255, 255, 59, 44, 238, 255, 0, 0, 0, 196, 255, 255, 255, 174, 36, 254, 24, 22, 28, 6, 25, 250, 11, 1, 17, 18, 13, 18, 18, 18, 28, 7, 70, 241, 52, 5, 31, 52, 58, 8, 14, 249, 44, 213, 53, 7, 74, 48, 51, 255, 242, 32, 0, 255, 252, 64, 0, 0, 73, 64, 0, 0, 0, 0, 0, 0, 36, 255, 255, 254, 2, 255, 255, 255, 233, 255, 255, 255, 254, 150, 0, 0, 0, 19, 50, 8, 45, 247, 25, 217, 48, 230, 64, 0, 62, 0, 61, 0, 16, 21, 57, 11, 9, 224, 15, 0, 12, 52, 57, 1, 7, 242, 55, 9, 9, 24, 60, 6, 11, 183, 16, 0, 14, 31, 64, 252, 7, 229, 16, 0, 14, 33, 59, 5, 12, 221, 51, 1, 55, 0, 43, 2, 24, 32, 39, 21, 86, 2, 44, 38, 75, 11, 51, 255, 226, 32, 0, 255, 254, 32, 0, 0, 111, 192, 0, 0, 0, 0, 0, 0, 69, 255, 255, 253, 193, 255, 255, 255, 221, 76, 1, 54, 0, 16, 21, 4, 37, 28, 6, 13, 174, 16, 81, 35, 251, 6, 219, 22, 234, 44, 0, 44, 255, 43, 0, 22, 247, 9, 241, 20, 8, 26, 198, 29, 59, 24, 243, 12, 14, 28, 8, 28, 12, 14, 18, 25, 248, 27, 196, 30, 63, 27, 6, 8, 235, 30, 248, 21, 17, 12, 28, 32, 4, 23, 183, 23, 73, 30, 255, 10, 226, 22, 238, 40, 8, 32, 242, 20, 28, 39, 9, 27, 30, 25, 225, 41, 243, 22, 226, 35, 15, 41, 248, 23, 20, 6, 23, 24, 7, 19, 174, 26, 80, 21, 250, 7, 234, 23, 233, 41, 0, 44, 0, 44, 0, 24, 242, 4, 240, 22, 14, 255, 0, 0, 4, 7, 0, 0, 0, 64, 255, 0, 0, 4, 28, 255, 255, 255, 190, 27, 246, 8, 15, 20, 12, 47, 1, 104, 0, 51, 255, 181, 224, 0, 0, 52, 128, 0, 0, 68, 32, 0, 0, 0, 0, 0, 0, 67, 255, 255, 252, 187, 0, 0, 2, 49, 123, 199, 57, 246, 29, 202, 15, 214, 77, 3, 24, 255, 13, 252, 10, 245, 17, 238, 18, 236, 15, 238, 12, 248, 3, 179, 15, 48, 20, 236, 24, 223, 19, 231, 16, 238, 1, 194, 12, 34, 17, 233, 15, 235, 23, 223, 15, 243, 12, 255, 4, 222, 15, 239, 15, 244, 12, 249, 12, 1, 4, 219, 14, 221, 15, 21, 27, 185, 20, 217, 13, 228, 15, 232, 12, 241, 19, 239, 22, 243, 31, 245, 30, 248, 23, 251, 19, 253, 18, 254, 12, 255, 4, 17, 51, 1, 8, 20, 54, 252, 13, 19, 55, 0, 9, 236, 57, 0, 6, 237, 52, 249, 14, 241, 73, 248, 46, 6, 21, 6, 22, 8, 31, 18, 12, 15, 33, 53, 44, 85, 21, 62, 51, 255, 172, 128, 0, 255, 252, 32, 0, 0, 79, 192, 0, 0, 0, 0, 0, 0, 70, 255, 255, 252, 66, 0, 0, 0, 113, 86, 225, 65, 216, 53, 196, 63, 223, 24, 3, 40, 246, 52, 3, 41, 255, 43, 36, 6, 2, 61, 0, 6, 254, 25, 228, 43, 18, 21, 25, 23, 15, 27, 10, 7, 23, 32, 2, 19, 14, 33, 3, 17, 20, 13, 31, 30, 27, 23, 36, 23, 44, 9, 27, 18, 26, 29, 16, 37, 14, 5, 6, 16, 36, 37, 22, 27, 1, 43, 228, 15, 215, 22, 247, 14, 229, 14, 6, 12, 249, 11, 6, 6, 247, 10, 1, 38, 213, 22, 209, 32, 237, 12, 206, 27, 223, 32, 11, 34, 0, 21, 11, 33, 215, 9, 192, 16, 225, 8, 43, 16, 27, 27, 242, 16, 226, 8, 221, 23, 249, 48, 1, 26, 253, 20, 13, 41, 61, 23, 24, 37, 232, 64, 10, 54, 61, 39, 69, 51, 255, 225, 96, 0, 0, 6, 224, 0, 0, 72, 160, 0, 0, 0, 0, 0, 0, 76, 255, 255, 253, 218, 0, 0, 0, 76, 105, 1, 43, 0, 41, 1, 49, 0, 7, 250, 23, 221, 52, 0, 12, 250, 27, 5, 26, 2, 9, 26, 3, 4, 3, 2, 20, 0, 5, 253, 2, 250, 7, 223, 20, 230, 21, 2, 6, 47, 26, 8, 13, 15, 7, 5, 6, 252, 5, 236, 7, 209, 20, 0, 7, 47, 5, 20, 8, 4, 7, 251, 13, 242, 34, 253, 19, 203, 30, 255, 12, 4, 12, 10, 15, 5, 10, 9, 6, 9, 9, 25, 8, 16, 8, 193, 1, 65, 4, 4, 11, 242, 36, 236, 5, 211, 6, 208, 14, 0, 18, 5, 7, 55, 31, 14, 28, 34, 8, 21, 6, 6, 3, 197, 1, 64, 4, 5, 23, 229, 40, 234, 21, 200, 22, 1, 3, 24, 9, 13, 36, 6, 38, 6, 33, 5, 14, 30, 6, 6, 42, 0, 40, 1, 14, 33, 7, 5, 99, 2, 51, 255, 180, 32, 0, 255, 241, 192, 0, 0, 42, 32, 0, 0, 0, 0, 0, 0, 70, 255, 255, 252, 137, 255, 255, 255, 125, 71, 29, 25, 248, 56, 86, 52, 248, 30, 157, 54, 0, 33, 0, 50, 1, 7, 4, 5, 11, 40, 2, 6, 3, 7, 11, 42, 2, 5, 3, 7, 13, 26, 253, 11, 253, 47, 80, 23, 0, 20, 0, 13, 197, 5, 228, 7, 248, 11, 254, 40, 1, 30, 24, 9, 246, 13, 253, 14, 7, 17, 38, 11, 0, 9, 222, 50, 1, 10, 239, 23, 255, 24, 6, 19, 12, 37, 35, 12, 242, 16, 33, 4, 0, 5, 244, 19, 35, 3, 0, 8, 243, 14, 35, 4, 0, 7, 242, 17, 35, 22, 8, 11, 19, 17, 11, 13, 25, 7, 7, 12, 3, 98, 4, 72, 0, 15, 1, 21, 38, 6, 1, 9, 243, 29, 45, 5, 0, 13, 240, 46, 54, 6, 0, 8, 246, 91, 110, 51, 255, 235, 96, 0, 0, 26, 0, 0, 0, 56, 128, 0, 0, 0, 0, 0, 0, 48, 255, 255, 254, 44, 0, 0, 0, 213, 54, 34, 61, 15, 37, 4, 33, 241, 24, 202, 17, 235, 27, 21, 16, 235, 29, 11, 29, 4, 13, 16, 25, 247, 20, 232, 17, 233, 24, 199, 11, 44, 19, 0, 21, 224, 40, 20, 2, 240, 19, 232, 9, 252, 29, 25, 20, 230, 30, 24, 18, 252, 11, 34, 15, 254, 11, 20, 16, 20, 17, 232, 21, 23, 15, 4, 12, 17, 24, 214, 8, 23, 21, 237, 27, 254, 15, 23, 11, 24, 26, 229, 47, 243, 43, 26, 10, 80, 26, 39, 10, 35, 81, 75, 51, 255, 226, 32, 0, 255, 253, 192, 0, 0, 111, 192, 0, 0, 0, 0, 0, 0, 71, 255, 255, 253, 219, 255, 255, 255, 222, 104, 0, 16, 21, 4, 37, 28, 6, 16, 24, 13, 231, 35, 251, 6, 219, 22, 234, 44, 0, 44, 255, 43, 0, 22, 11, 9, 19, 20, 248, 30, 19, 25, 236, 24, 5, 12, 236, 28, 244, 28, 12, 14, 18, 25, 248, 28, 20, 29, 239, 27, 6, 8, 235, 30, 248, 21, 17, 12, 28, 32, 4, 23, 30, 23, 226, 30, 255, 10, 226, 22, 238, 40, 8, 32, 242, 20, 28, 39, 9, 26, 222, 26, 33, 41, 243, 22, 226, 35, 15, 41, 248, 23, 20, 6, 23, 24, 7, 23, 23, 22, 231, 21, 250, 7, 234, 23, 233, 41, 0, 44, 0, 44, 0, 24, 242, 2, 226, 25, 12, 24, 243, 20, 12, 26, 250, 8, 27, 20, 12, 38, 0, 31, 24, 1, 27, 24, 3, 34, 40, 51, 255, 176, 160, 0, 255, 243, 160, 0, 0, 85, 96, 0, 0, 0, 0, 0, 0, 57, 255, 255, 252, 139, 255, 255, 255, 239, 255, 255, 255, 253, 27, 255, 255, 255, 206, 31, 0, 18, 244, 13, 230, 23, 224, 37, 2, 25, 233, 29, 235, 59, 232, 35, 250, 46, 248, 61, 251, 32, 0, 36, 7, 22, 181, 24, 177, 77, 145, 62, 88, 23, 100, 39, 250, 33, 254, 23, 5, 33, 9, 40, 17, 28, 30, 16, 174, 20, 163, 28, 198, 52, 92, 33, 81, 4, 17, 35, 224, 23, 245, 21, 0, 31, 9, 22, 17, 22, 18, 24, 18, 14, 28, 8, 165, 8, 169, 39, 88, 20, 87, 39, 246, 17, 252, 27, 5, 20, 11, 21, 19, 15, 26, 19, 26, 6, 5, 59, 249, 91, 14, 72, 43, 51, 69, 124, 9, 51, 255, 235, 160, 0, 255, 246, 96, 0, 0, 122, 128, 0, 0, 0, 0, 0, 0, 62, 255, 255, 254, 28, 255, 255, 255, 233, 85, 255, 86, 203, 32, 235, 37, 15, 30, 18, 16, 230, 45, 255, 41, 0, 51, 12, 50, 13, 18, 231, 10, 255, 18, 37, 45, 246, 11, 226, 14, 0, 29, 39, 37, 10, 8, 15, 17, 2, 13, 189, 10, 0, 24, 25, 46, 248, 18, 238, 9, 0, 22, 34, 51, 5, 33, 219, 12, 0, 23, 53, 44, 10, 7, 15, 16, 1, 14, 176, 14, 255, 12, 31, 31, 0, 7, 228, 12, 255, 26, 40, 42, 1, 5, 212, 11, 255, 39, 79, 48, 11, 21, 164, 16, 1, 38, 40, 55, 247, 21, 224, 12, 0, 20, 32, 47, 4, 53, 3, 23, 45, 46, 253, 14, 203, 12, 5, 45, 107, 56, 251, 51, 255, 181, 96, 0, 255, 225, 32, 0, 0, 37, 96, 0, 0, 0, 0, 0, 0, 67, 255, 255, 252, 73, 255, 255, 255, 1, 93, 57, 39, 241, 9, 7, 81, 213, 4, 9, 74, 244, 5, 243, 30, 255, 33, 0, 30, 1, 31, 0, 8, 0, 4, 9, 64, 50, 8, 246, 10, 217, 5, 62, 4, 12, 92, 246, 2, 243, 5, 222, 3, 44, 2, 14, 63, 49, 6, 245, 1, 209, 14, 85, 6, 9, 52, 223, 1, 241, 16, 207, 1, 215, 18, 252, 4, 28, 7, 8, 59, 241, 2, 244, 6, 235, 12, 2, 2, 23, 1, 12, 73, 43, 3, 244, 17, 223, 19, 8, 13, 48, 1, 14, 78, 16, 4, 241, 27, 231, 12, 44, 2, 14, 73, 19, 1, 244, 39, 6, 45, 7, 4, 247, 77, 205, 5, 12, 50, 219, 4, 9, 70, 16, 2, 249, 63, 61, 4, 248, 75, 69, 51, 255, 204, 224, 0, 255, 235, 64, 0, 0, 83, 64, 0, 0, 0, 0, 0, 0, 52, 255, 255, 253, 141, 255, 255, 255, 158, 6, 235, 38, 9, 1, 15, 105, 187, 2, 242, 41, 238, 48, 0, 41, 15, 9, 16, 120, 91, 6, 37, 9, 3, 60, 137, 14, 0, 42, 153, 15, 0, 33, 158, 51, 0, 1, 232, 27, 185, 9, 1, 17, 106, 93, 242, 7, 240, 31, 1, 9, 20, 94, 75, 9, 244, 1, 224, 10, 2, 10, 83, 7, 13, 91, 192, 1, 242, 73, 87, 8, 7, 33, 245, 55, 232, 3, 245, 36, 245, 41, 0, 39, 14, 3, 14, 77, 248, 2, 241, 79, 28, 2, 15, 76, 22, 3, 245, 76, 218, 60, 69, 51, 255, 236, 128, 0, 255, 251, 160, 0, 0, 123, 64, 0, 0, 0, 0, 0, 0, 81, 255, 255, 254, 136, 255, 255, 255, 219, 26, 20, 27, 30, 17, 30, 28, 0, 14, 225, 20, 233, 26, 238, 10, 190, 18, 43, 31, 249, 29, 253, 34, 6, 27, 8, 17, 213, 12, 66, 22, 22, 18, 26, 15, 23, 24, 0, 13, 43, 15, 224, 29, 0, 27, 19, 14, 205, 12, 73, 23, 22, 20, 26, 16, 24, 27, 0, 14, 44, 18, 220, 29, 0, 31, 11, 19, 202, 15, 72, 29, 18, 25, 21, 22, 24, 14, 30, 10, 37, 10, 219, 13, 227, 20, 233, 20, 235, 25, 241, 11, 189, 15, 54, 28, 245, 25, 0, 18, 33, 16, 214, 29, 0, 14, 231, 16, 230, 19, 236, 7, 186, 14, 53, 23, 243, 30, 0, 12, 29, 13, 215, 28, 0, 13, 231, 14, 231, 19, 234, 8, 184, 14, 55, 29, 244, 28, 251, 28, 8, 27, 14, 13, 203, 14, 68, 24, 19, 21, 24, 15, 25, 28, 0, 15, 231, 22, 233, 31, 239, 51, 255, 209, 192, 0, 255, 235, 224, 0, 0, 58, 224, 0, 0, 0, 0, 0, 0, 61, 255, 255, 253, 119, 255, 255, 255, 241, 119, 193, 20, 212, 34, 61, 22, 228, 18, 205, 41, 215, 33, 0, 28, 0, 30, 36, 17, 30, 12, 234, 11, 245, 12, 221, 12, 231, 15, 13, 9, 20, 8, 227, 10, 238, 33, 243, 33, 252, 15, 237, 13, 237, 13, 4, 9, 7, 16, 248, 12, 250, 13, 246, 13, 246, 13, 9, 8, 11, 19, 255, 29, 1, 37, 43, 17, 245, 20, 0, 5, 5, 11, 83, 10, 3, 8, 6, 9, 13, 16, 18, 28, 6, 28, 241, 19, 23, 53, 29, 15, 238, 25, 7, 30, 31, 36, 227, 18, 15, 27, 253, 27, 255, 32, 245, 46, 36, 41, 229, 34, 34, 30, 45, 31, 40, 48, 189, 71, 2, 51, 255, 222, 192, 0, 255, 253, 128, 0, 0, 47, 128, 0, 0, 0, 0, 0, 0, 95, 255, 255, 253, 221, 0, 0, 0, 9, 88, 208, 38, 8, 23, 20, 19, 16, 25, 9, 26, 254, 21, 236, 22, 227, 24, 0, 21, 0, 17, 3, 13, 247, 28, 253, 23, 1, 15, 3, 18, 4, 17, 0, 7, 0, 6, 252, 2, 233, 1, 220, 3, 189, 7, 249, 8, 8, 2, 26, 2, 82, 2, 7, 3, 6, 7, 2, 10, 252, 6, 243, 3, 177, 5, 229, 7, 245, 7, 11, 2, 27, 3, 27, 1, 25, 1, 16, 3, 13, 8, 5, 7, 0, 7, 250, 3, 245, 1, 246, 2, 193, 3, 230, 6, 246, 8, 6, 2, 13, 5, 34, 2, 49, 4, 15, 5, 5, 6, 1, 8, 251, 3, 245, 5, 203, 4, 212, 11, 252, 16, 252, 13, 4, 13, 8, 15, 8, 18, 7, 16, 5, 16, 252, 5, 248, 5, 192, 11, 184, 9, 245, 7, 0, 9, 11, 14, 72, 6, 64, 9, 9, 19, 8, 20, 252, 11, 249, 24, 0, 25, 7, 21, 8, 30, 2, 41, 2, 22, 18, 9, 88, 10, 18, 15, 4, 14, 252, 12, 239, 15, 164, 31, 224, 71, 241, 92, 68, 51, 255, 221, 96, 0, 255, 228, 96, 0, 0, 84, 64, 0, 0, 0, 0, 0, 0, 84, 255, 255, 254, 23, 255, 255, 255, 137, 84, 238, 23, 206, 43, 232, 10, 12, 26, 211, 46, 5, 10, 253, 7, 11, 5, 230, 4, 245, 11, 246, 5, 231, 11, 10, 16, 251, 11, 4, 8, 233, 3, 236, 12, 6, 14, 251, 14, 4, 9, 211, 15, 227, 23, 245, 14, 230, 24, 252, 17, 180, 12, 253, 15, 7, 13, 44, 16, 11, 21, 7, 21, 13, 13, 20, 7, 227, 7, 47, 6, 13, 22, 215, 9, 228, 21, 0, 9, 252, 5, 7, 9, 247, 7, 9, 11, 199, 17, 250, 21, 6, 8, 184, 17, 245, 20, 11, 21, 242, 14, 207, 25, 206, 13, 241, 25, 252, 25, 6, 23, 13, 13, 8, 12, 10, 19, 17, 17, 216, 1, 48, 20, 216, 6, 47, 17, 216, 12, 44, 11, 211, 15, 45, 4, 208, 17, 40, 1, 207, 18, 40, 35, 227, 39, 250, 48, 0, 21, 45, 10, 223, 12, 47, 15, 212, 11, 30, 20, 218, 59, 246, 84, 72, 59, 110, 51, 255, 215, 96, 0, 255, 247, 64, 0, 0, 53, 128, 0, 0, 0, 0, 0, 0, 78, 255, 255, 253, 195, 0, 0, 0, 50, 53, 229, 25, 216, 35, 4, 9, 245, 9, 213, 21, 251, 26, 50, 6, 1, 6, 248, 27, 202, 30, 252, 29, 1, 22, 13, 15, 15, 9, 19, 6, 1, 26, 245, 1, 232, 2, 238, 10, 14, 9, 28, 6, 7, 5, 252, 2, 245, 9, 244, 15, 244, 36, 3, 10, 230, 10, 37, 14, 12, 18, 254, 9, 251, 5, 246, 3, 220, 22, 27, 17, 7, 8, 179, 20, 214, 19, 109, 17, 9, 19, 193, 3, 61, 8, 9, 20, 20, 13, 0, 3, 236, 11, 232, 21, 61, 15, 255, 1, 165, 17, 245, 1, 79, 18, 3, 18, 177, 14, 1, 6, 47, 17, 246, 19, 191, 14, 0, 10, 25, 17, 5, 17, 239, 12, 28, 27, 16, 14, 12, 20, 6, 17, 249, 18, 0, 40, 230, 17, 20, 40, 246, 12, 231, 21, 38, 44, 34, 13, 247, 69, 87, 37, 0, 51, 0, 14, 160, 0, 0, 14, 0, 0, 0, 200, 0, 0, 0, 0, 0, 0, 0, 100, 255, 255, 255, 170, 0, 0, 0, 136, 124, 216, 10, 255, 93, 255, 44, 1, 34, 3, 7, 5, 24, 33, 7, 3, 46, 22, 6, 6, 22, 62, 6, 8, 42, 26, 7, 9, 16, 49, 5, 5, 69, 1, 6, 5, 7, 24, 6, 5, 24, 0, 7, 248, 6, 234, 5, 251, 16, 210, 7, 251, 23, 25, 7, 26, 12, 30, 18, 22, 26, 17, 14, 21, 7, 5, 7, 250, 9, 220, 17, 149, 23, 106, 2, 39, 8, 4, 9, 247, 20, 232, 10, 250, 27, 249, 17, 149, 23, 211, 28, 14, 23, 90, 7, 10, 20, 11, 6, 7, 21, 56, 9, 10, 7, 178, 7, 95, 9, 8, 7, 0, 10, 237, 11, 244, 22, 246, 14, 175, 15, 189, 21, 247, 20, 9, 13, 88, 13, 4, 16, 12, 8, 11, 18, 48, 7, 9, 22, 5, 13, 5, 10, 13, 11, 22, 6, 19, 6, 6, 31, 1, 7, 248, 13, 155, 28, 244, 18, 115, 9, 7, 30, 0, 8, 252, 23, 231, 37, 238, 38, 237, 42, 5, 38, 18, 3, 5, 7, 27, 6, 7, 67, 0, 12, 5, 65, 35, 80, 22, 10, 2, 84, 48, 64, 25, 31, 5, 51, 0, 6, 96, 0, 255, 233, 192, 0, 0, 187, 224, 0, 0, 0, 0, 0, 0, 77, 255, 255, 255, 59, 255, 255, 255, 203, 108, 216, 90, 254, 30, 157, 40, 253, 13, 1, 67, 210, 94, 193, 96, 200, 50, 5, 3, 195, 3, 194, 31, 255, 4, 46, 3, 49, 26, 59, 15, 12, 10, 181, 2, 171, 12, 255, 8, 88, 23, 44, 18, 17, 3, 182, 2, 186, 9, 0, 1, 79, 26, 71, 17, 16, 3, 171, 2, 167, 21, 254, 7, 51, 46, 9, 48, 14, 10, 215, 29, 248, 15, 33, 29, 254, 34, 249, 12, 24, 11, 28, 17, 18, 8, 38, 43, 48, 32, 49, 15, 3, 6, 195, 4, 223, 10, 1, 2, 47, 12, 252, 21, 209, 22, 235, 12, 168, 23, 234, 22, 13, 28, 185, 31, 222, 29, 248, 28, 4, 24, 0, 21, 250, 32, 241, 42, 38, 33, 2, 20, 235, 28, 2, 29, 42, 44, 15, 13, 237, 30, 254, 47, 47, 11, 7, 57, 31, 51, 64, 45, 89, 51, 0, 10, 160, 0, 0, 14, 160, 0, 0, 176, 0, 0, 0, 0, 0, 0, 0, 76, 255, 255, 254, 199, 0, 0, 0, 157, 95, 0, 52, 0, 3, 211, 53, 0, 2, 209, 52, 0, 3, 235, 50, 0, 6, 22, 50, 0, 4, 22, 44, 0, 41, 0, 43, 0, 5, 25, 53, 0, 4, 25, 53, 0, 3, 25, 56, 0, 2, 210, 51, 0, 1, 47, 51, 0, 1, 206, 54, 0, 2, 49, 53, 0, 1, 207, 39, 0, 40, 0, 2, 230, 27, 0, 1, 226, 1, 198, 73, 0, 2, 26, 52, 0, 3, 230, 27, 0, 4, 54, 62, 0, 2, 201, 56, 0, 2, 54, 56, 0, 5, 25, 56, 0, 6, 26, 55, 0, 4, 25, 48, 0, 48, 0, 6, 14, 20, 0, 6, 15, 19, 0, 5, 15, 19, 0, 4, 15, 16, 0, 3, 15, 46, 0, 42, 0, 47, 0, 3, 215, 45, 0, 5, 212, 49, 0, 4, 88, 57, 0, 1, 100, 57, 0, 1, 88, 58, 0] ===== FILE: src/class_10.ts ===== import { TimerOrMotoPartOrMenuElem } from './TimerOrMotoPartOrMenuElem.ts' export class class_10 { unusedBool = true collisionRadiusF16 = 0 collisionShapeIndex = 0 inverseMassF16 = 0 angularForceFactorF16 = 0 motoComponents = new Array(6) constructor() { for (let var1 = 0; var1 < 6; ++var1) { this.motoComponents[var1] = new TimerOrMotoPartOrMenuElem() } this.reset() } reset(): void { this.collisionRadiusF16 = 0 this.inverseMassF16 = 0 this.angularForceFactorF16 = 0 this.unusedBool = true for (let i = 0; i < 6; ++i) { this.motoComponents[i].resetState() } } } ===== FILE: src/cpp.ts ===== function toBigInt(value: bigint | number): bigint { if (typeof value === 'bigint') { return value } return BigInt(Math.trunc(value)) } export const INT_MAX = 2147483647 export const INT_MIN = -2147483648 export function toInt(value: bigint | number): number { return Number(BigInt.asIntN(32, toBigInt(value))) } export function abs(value: number): number { return value < 0 ? -value : value } export function truncDiv(a: number, b: number): number { return toInt(toBigInt(a) / toBigInt(b)) } export function multiplyF16(a: number, b: number): number { return toInt((toBigInt(a) * toBigInt(b)) >> 16n) } export function divideF16(a: number, b: number): number { return toInt(((toBigInt(a) << 32n) / toBigInt(b)) >> 16n) } export function roundfToInt(value: number): number { const rounded = Math.round(Math.abs(value)) return value < 0 ? -rounded : rounded } ===== FILE: src/index.css ===== *{box-sizing:border-box} :root{font-family:"Avenir Next",Avenir,"Segoe UI",sans-serif;color:#3d3026;background:#f4eee2;font-synthesis:none} body{margin:0} button,a{-webkit-tap-highlight-color:transparent} button{font:inherit;cursor:pointer} button:focus-visible,a:focus-visible{outline:3px solid #bd4e2d;outline-offset:3px} [hidden]{display:none!important} .shell{max-width:1400px;margin:auto;padding:30px 42px 18px} header{display:flex;align-items:center;justify-content:space-between;margin-bottom:26px}.eyebrow{font-size:10px;letter-spacing:2.5px;font-weight:750;color:#93654a;margin:0 0 8px} h1{font-family:Georgia,serif;font-weight:400;font-size:58px;letter-spacing:-2px;margin:0;line-height:1}h1 span{display:inline-block;font-family:inherit;font-size:25px;font-style:italic;letter-spacing:-.5px;margin-left:18px;color:#98775e}.badge{font-size:11px;letter-spacing:2px;line-height:1.8;text-align:right}.badge small{font-size:12px;letter-spacing:0;color:#8d7563}.stage-wrap{border:1px solid #d0bda9;border-radius:16px;overflow:hidden;background:#fff5e4;box-shadow:0 16px 40px #63442512}.hud{min-height:52px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 20px;border-bottom:1px solid #dac7ad;background:#fff9ed;font-size:11px;letter-spacing:.5px}.hud strong{font-size:13px;margin:0 8px}.hud button{background:transparent;border:1px solid #d0bda9;border-radius:7px;padding:7px 10px;color:#584334;margin-left:5px;font-size:11px}.stage{height:clamp(380px,54vh,640px);position:relative;background:#ffefda}#root,.app-root{height:100%;width:100%;position:relative}.game-canvas{display:block;width:100%;height:100%;touch-action:none}#menu-panel{position:absolute;top:50%;left:24px;transform:translateY(-50%);width:310px;max-height:90%;overflow:auto;padding:24px;background:#fff9efee;backdrop-filter:blur(12px);border:1px solid #cbb598;border-radius:14px;box-shadow:0 12px 50px #65452115}#menu-panel h2{font:normal 30px Georgia,serif;margin:10px 0 18px}#menu-panel .eyebrow{font-size:8px;letter-spacing:1.5px}#menu-panel p{font-size:12px;line-height:1.5}.menu-item{display:block;text-align:left;width:100%;border:0;border-bottom:1px solid #d8c9b8;padding:11px 13px;background:transparent;color:#57402e;border-radius:6px;font-size:13px}.menu-item:hover,.menu-item.selected{background:#a4482c;color:white}.menu-item+.menu-item{margin-top:3px}.fieldnotes{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin:26px 0 20px}.fieldnotes>div{position:relative;padding-left:42px}.step{position:absolute;left:0;top:0;color:#ae7658;font:italic 24px Georgia,serif}h3{font-family:Georgia,serif;font-size:18px;font-weight:400;margin:0 0 8px}.fieldnotes p{font-size:12px;line-height:1.7;color:#8c7664;margin:0}footer{border-top:1px solid #d8cbbc;padding-top:15px;display:flex;justify-content:space-between;font-size:10px;color:#8d7765}footer a{color:inherit}.error-view{white-space:pre-wrap;color:#9b2c17;background:#fff;padding:30px} .touch-controls { display: none; } @media (pointer: coarse) { .touch-controls { display: block; } } .touch-controls button { position: fixed; display: flex; align-items: center; justify-content: center; width: 64px; height: 64px; padding: 0; border: 2px solid rgba(15, 14, 9, 0.4); border-radius: 32px; color: rgba(15, 14, 9, 0.75); background: rgba(255, 255, 255, 0.45); font: inherit; font-weight: bold; touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } .touch-controls .pressed { background: rgba(225, 194, 110, 0.75); } .touch-left::before, .touch-right::before, .touch-up::before, .touch-down::before { content: ""; width: 22px; height: 22px; background: currentColor; clip-path: polygon(50% 10%, 100% 90%, 0 90%); } .touch-left::before { transform: rotate(-90deg); } .touch-right::before { transform: rotate(90deg); } .touch-down::before { transform: rotate(180deg); } .touch-left { left: calc(16px + env(safe-area-inset-left)); bottom: calc(24px + env(safe-area-inset-bottom)); } .touch-right { left: calc(96px + env(safe-area-inset-left)); bottom: calc(24px + env(safe-area-inset-bottom)); } .touch-down { right: calc(16px + env(safe-area-inset-right)); bottom: calc(24px + env(safe-area-inset-bottom)); } .touch-up { right: calc(16px + env(safe-area-inset-right)); bottom: calc(104px + env(safe-area-inset-bottom)); } .touch-controls .touch-ok { right: calc(96px + env(safe-area-inset-right)); bottom: calc(24px + env(safe-area-inset-bottom)); display: none; } .touch-controls.in-menu .touch-ok { display: flex; } .touch-ok::before { content: "OK"; } .touch-controls .touch-back { top: calc(16px + env(safe-area-inset-top)); right: calc(16px + env(safe-area-inset-right)); width: auto; height: 40px; padding: 0 16px; display: none; } .touch-controls.has-back .touch-back { display: flex; } .touch-back::before { content: "Menu"; } .in-menu .touch-back::before { content: "Back"; } .touch-controls{display:block;position:absolute;inset:0;pointer-events:none}.touch-controls button{position:absolute;pointer-events:auto;width:48px;height:48px;border-radius:12px;border:1px solid #aa8b65;color:#6b4731;background:#fff9efda;box-shadow:0 3px 10px #69452716}.touch-controls .pressed{background:#c27750;color:#fff}.touch-left{left:18px;bottom:18px}.touch-right{left:76px;bottom:18px}.touch-down{right:18px;bottom:18px}.touch-up{right:18px;bottom:76px}.touch-controls .touch-ok{right:76px;bottom:18px}.touch-controls .touch-back{top:12px;right:12px;height:32px;font-size:11px} .touch-left::before,.touch-right::before,.touch-up::before,.touch-down::before{width:15px;height:15px} @media(max-width:650px){.shell{padding:18px 12px 20px}header{margin-bottom:18px}h1{font-size:42px}h1 span{font-size:18px;display:block;margin:5px 0 0}.eyebrow{font-size:7px;letter-spacing:1.5px}.badge{font-size:8px;letter-spacing:1px}.badge small{font-size:10px}.hud{padding:9px 10px;flex-wrap:wrap;font-size:9px;gap:6px}.hud #ride-status{order:3;width:100%;font-size:10px}.hud strong{font-size:11px;margin:0 2px}.hud button{padding:6px;font-size:10px}.stage{height:440px}#menu-panel{left:12px;top:14px;transform:none;width:calc(100% - 80px);max-height:335px;padding:16px}#menu-panel h2{font-size:25px}.menu-item{padding:9px;font-size:12px}.fieldnotes{grid-template-columns:1fr;gap:17px;margin:22px 8px}.fieldnotes br{display:none}.fieldnotes p{font-size:11px}.fieldnotes h3{font-size:17px;margin-bottom:4px}.fieldnotes>div{padding-left:36px}footer{display:block;font-size:9px;line-height:1.9}footer span{display:block}} ===== FILE: src/lcdui/Font.ts ===== export class Font { static readonly SIZE_SMALL = 8 static readonly SIZE_MEDIUM = 0 static readonly SIZE_LARGE = 16 static readonly STYLE_PLAIN = 'normal' static readonly STYLE_BOLD = 'bold' static readonly STYLE_ITALIC = 'italic' static readonly FACE_SYSTEM = 0 private static measureCanvas: HTMLCanvasElement | null = null private static measureCtx: CanvasRenderingContext2D | null = null private readonly cssFont: string private readonly height: number constructor(style: string, pointSize: number) { this.height = Font.getRealFontSize(pointSize) this.cssFont = `${style} ${this.height}px "Trebuchet MS", "Segoe UI", sans-serif` } getBaselinePosition(): number { return this.height } getHeight(): number { return this.height } getCssFont(): string { return this.cssFont } charWidth(c: string): number { return this.stringWidth(c.slice(0, 1)) } stringWidth(s: string): number { const ctx = Font.getMeasureCtx() ctx.font = this.cssFont return Math.ceil(ctx.measureText(s).width) } substringWidth(string: string, offset: number, len: number): number { return this.stringWidth(string.substring(offset, offset + len)) } private static getMeasureCtx(): CanvasRenderingContext2D { if (Font.measureCtx !== null) { return Font.measureCtx } Font.measureCanvas = document.createElement('canvas') Font.measureCtx = Font.measureCanvas.getContext('2d') if (Font.measureCtx === null) { throw new Error('Canvas 2D context is not available') } return Font.measureCtx } private static getRealFontSize(size: number): number { switch (size) { case Font.SIZE_LARGE: return 32 case Font.SIZE_MEDIUM: return 16 case Font.SIZE_SMALL: return 12 default: throw new Error(`unknown font size: ${size}`) } } } ===== FILE: src/lcdui/FontStorage.ts ===== import { Font } from './Font.ts' export class FontStorage { private static fontsMap = new Map() static getFont(style: string, size: number): Font { const key = `${style}:${size}` const existing = FontStorage.fontsMap.get(key) if (existing !== undefined) { return existing } const font = new Font(style, size) FontStorage.fontsMap.set(key, font) return font } static clearAll(): void { FontStorage.fontsMap.clear() } } ===== FILE: src/lcdui/Graphics.ts ===== import { Image } from './Image.ts' import { Font } from './Font.ts' type ClipRect = { x: number y: number w: number h: number } export class Graphics { static readonly HCENTER = 1 static readonly VCENTER = 2 static readonly LEFT = 4 static readonly RIGHT = 8 static readonly TOP = 16 static readonly BOTTOM = 32 static readonly BASELINE = 64 private readonly ctx: CanvasRenderingContext2D private font: Font = new Font(Font.STYLE_PLAIN, Font.SIZE_MEDIUM) private currentColor = 'rgb(0 0 0)' private clipRect: ClipRect | null = null constructor(ctx: CanvasRenderingContext2D) { this.ctx = ctx this.ctx.lineWidth = 1 this.ctx.textRendering = 'geometricPrecision' this.ctx.imageSmoothingEnabled = false } drawString(s: string, x: number, y: number, anchor: number): void { this.ctx.font = this.font.getCssFont() this.ctx.fillStyle = this.currentColor const metrics = this.ctx.measureText(s) const width = Math.ceil(metrics.width) const ascent = Math.ceil(metrics.actualBoundingBoxAscent || 12) const descent = Math.ceil(metrics.actualBoundingBoxDescent || 4) const drawX = Graphics.getAnchorX(x, width, anchor) const baselineY = Graphics.getAnchorTextY(y, ascent, descent, anchor) this.withClip(() => { this.ctx.fillText(s, drawX, baselineY) }) } setColor(r: number, g: number, b: number): void { this.currentColor = `rgb(${r} ${g} ${b})` this.ctx.fillStyle = this.currentColor this.ctx.strokeStyle = this.currentColor } setFont(font: Font): void { this.font = font } getFont(): Font { return this.font } drawChar(c: string, x: number, y: number, anchor: number): void { this.drawString(c.slice(0, 1), x, y, anchor) } setClip(x: number, y: number, w: number, h: number): void { this.clipRect = { x, y, w, h } } fillRect(x: number, y: number, w: number, h: number): void { this.withClip(() => { this.ctx.fillRect(x, y, w, h) }) } drawLine(x1: number, y1: number, x2: number, y2: number): void { this.withClip(() => { this.ctx.beginPath() this.ctx.moveTo(x1, y1) this.ctx.lineTo(x2, y2) this.ctx.stroke() }) } drawArc(x: number, y: number, width: number, heigth: number, startAngle: number, arcAngle: number): void { const xradius = Math.trunc(width / 2) const yradius = Math.trunc(heigth / 2) x += xradius y += yradius if (xradius === 0 && yradius === 0) { return } this.withClip(() => { for (let angle = startAngle; angle < startAngle + arcAngle; ++angle) { this.drawLine( x + Math.trunc(xradius * Math.cos((angle * Math.PI) / 180)), y - Math.trunc(yradius * Math.sin((angle * Math.PI) / 180)), x + Math.trunc(xradius * Math.cos(((angle + 1) * Math.PI) / 180)), y - Math.trunc(yradius * Math.sin(((angle + 1) * Math.PI) / 180)), ) } }) } drawImage(image: Image, x: number, y: number, anchor: number): void { const width = image.getWidth() const height = image.getHeight() const drawX = Graphics.getAnchorX(x, width, anchor) const drawY = Graphics.getAnchorY(y, height, anchor) this.withClip(() => { this.ctx.drawImage(image.getElement(), drawX, drawY) }) } private withClip(draw: () => void): void { if (this.clipRect === null) { draw() return } this.ctx.save() this.ctx.beginPath() this.ctx.rect(this.clipRect.x, this.clipRect.y, this.clipRect.w, this.clipRect.h) this.ctx.clip() draw() this.ctx.restore() } static getAnchorX(x: number, size: number, anchor: number): number { if ((anchor & Graphics.LEFT) !== 0) { return x } if ((anchor & Graphics.RIGHT) !== 0) { return x - size } if ((anchor & Graphics.HCENTER) !== 0) { return x - (size >> 1) } throw new Error(`unknown xanchor = ${anchor}`) } static getAnchorY(y: number, size: number, anchor: number): number { if ((anchor & Graphics.TOP) !== 0) { return y } if ((anchor & Graphics.BOTTOM) !== 0) { return y - size } if ((anchor & Graphics.VCENTER) !== 0) { return y - (size >> 1) } if ((anchor & Graphics.BASELINE) !== 0) { return y - size } throw new Error(`unknown yanchor = ${anchor}`) } static getAnchorTextY(y: number, ascent: number, descent: number, anchor: number): number { if ((anchor & Graphics.TOP) !== 0) { return y + ascent } if ((anchor & Graphics.BOTTOM) !== 0) { return y - descent } if ((anchor & Graphics.VCENTER) !== 0) { return y + ((ascent - descent) >> 1) } if ((anchor & Graphics.BASELINE) !== 0) { return y } throw new Error(`unknown yanchor = ${anchor}`) } } ===== FILE: src/lcdui/Image.ts ===== export class Image { private readonly image: HTMLImageElement private constructor(image: HTMLImageElement) { this.image = image } static fromSrc(src: string): Image { const image = new window.Image() image.src = src return new Image(image) } static async load(src: string): Promise { return await new Promise((resolve, reject) => { const image = new window.Image() image.onload = () => resolve(new Image(image)) image.onerror = () => reject(new Error(`Failed to load ${src}`)) image.src = src }) } getWidth(): number { return this.image.naturalWidth || this.image.width } getHeight(): number { return this.image.naturalHeight || this.image.height } getElement(): HTMLImageElement { return this.image } } ===== FILE: src/main.ts ===== import './index.css' import { startGravityDefiedApp } from './app.ts' const root = document.getElementById('root') if (!(root instanceof HTMLDivElement)) { throw new Error('Missing #root container') } startGravityDefiedApp(root).catch((error: unknown) => { const pre = document.createElement('pre') pre.className = 'error-view' pre.textContent = error instanceof Error ? error.stack ?? error.message : String(error) root.replaceChildren(pre) }) ===== FILE: src/rms/RecordEnumeration.ts ===== export interface RecordEnumeration { numRecords(): number nextRecord(): Int8Array reset(): void nextRecordId(): number destroy(): void } ===== FILE: src/rms/RecordEnumerationImpl.ts ===== import type { RecordEnumeration } from './RecordEnumeration.ts' export class RecordEnumerationImpl implements RecordEnumeration { private currentPos = 0 data: Int8Array[] constructor(data: Int8Array[] = []) { this.data = data } numRecords(): number { return this.data.length } nextRecord(): Int8Array { return this.data[this.currentPos++] } addRecord(bytes: Int8Array): number { this.data.push(bytes) return this.data.length - 1 } setRecord(index: number, bytes: Int8Array): void { if (this.data.length <= index) { throw new Error('RecordStoreException') } this.data[index] = bytes } reset(): void { this.currentPos = 0 } nextRecordId(): number { if (this.currentPos >= this.data.length) { throw new Error('RecordStoreException') } return this.currentPos } destroy(): void {} } ===== FILE: src/rms/RecordStore.ts ===== import { RecordEnumerationImpl } from './RecordEnumerationImpl.ts' import type { RecordEnumeration } from './RecordEnumeration.ts' function toStorageKey(name: string): string { return `gravity_defied_record_store:${name}` } export class RecordStore { private static opened = new Map() private readonly name: string private readonly records: RecordEnumerationImpl private constructor(name: string, records: RecordEnumerationImpl) { this.name = name this.records = records } static setRecordStoreDir(_progName: string): void {} static openRecordStore(name: string, createIfNecessary: boolean): RecordStore { const existing = RecordStore.opened.get(name) if (existing !== undefined) { return existing } const item = window.localStorage.getItem(toStorageKey(name)) if (item === null) { if (!createIfNecessary) { throw new Error('RecordStoreException') } const created = new RecordStore(name, new RecordEnumerationImpl()) created.save() RecordStore.opened.set(name, created) return created } const parsed = JSON.parse(item) as number[][] const records = new RecordEnumerationImpl(parsed.map((entry) => Int8Array.from(entry))) const store = new RecordStore(name, records) RecordStore.opened.set(name, store) return store } closeRecordStore(): void {} static deleteRecordStore(name: string): void { window.localStorage.removeItem(toStorageKey(name)) RecordStore.opened.delete(name) } static listRecordStores(): string[] { const result: string[] = [] for (let i = 0; i < window.localStorage.length; ++i) { const key = window.localStorage.key(i) if (key !== null && key.startsWith('gravity_defied_record_store:')) { result.push(key.substring('gravity_defied_record_store:'.length)) } } return result } enumerateRecords(_filter: unknown, _comparator: unknown, _keepUpdated: boolean): RecordEnumeration { return this.records } addRecord(arr: Int8Array | number[], offset: number, numBytes: number): number { if (offset !== 0) { throw new Error('RecordStoreException') } const bytes = Int8Array.from(Array.from(arr).slice(0, numBytes)) const id = this.records.addRecord(bytes) this.save() return id } setRecord(recordId: number, arr: Int8Array | number[], offset: number, numBytes: number): void { void offset const bytes = Int8Array.from(Array.from(arr).slice(0, numBytes)) this.records.setRecord(recordId, bytes) this.save() } private save(): void { const payload = JSON.stringify(this.records.data.map((entry) => Array.from(entry))) window.localStorage.setItem(toStorageKey(this.name), payload) } } ===== FILE: src/utils/FileStream.ts ===== export class FileStream { private readonly view: DataView private readonly bytes: Uint8Array private pos = 0 constructor(buffer: ArrayBuffer) { this.view = new DataView(buffer) this.bytes = new Uint8Array(buffer) } static async fromUrl(url: string): Promise { const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`) } const buffer = await response.arrayBuffer() if (buffer.byteLength === 0) { throw new Error(`Loaded empty resource from ${url}`) } return new FileStream(buffer) } isOpen(): boolean { return true } setPos(pos: number): void { this.pos = pos } getPos(): number { return this.pos } readInt8(): number { const value = this.view.getInt8(this.pos) this.pos += 1 return value } readInt16(swapEndianness = false): number { this.assertCanRead(2) const value = this.view.getInt16(this.pos, !swapEndianness) this.pos += 2 return value } readInt32(swapEndianness = false): number { this.assertCanRead(4) const value = this.view.getInt32(this.pos, !swapEndianness) this.pos += 4 return value } readBytes(length: number): Uint8Array { this.assertCanRead(length) const value = this.bytes.slice(this.pos, this.pos + length) this.pos += length return value } private assertCanRead(length: number): void { if (this.pos + length > this.view.byteLength) { throw new Error(`Unexpected end of stream at ${this.pos}, wanted ${length} more bytes, size=${this.view.byteLength}`) } } } ===== FILE: src/utils/Time.ts ===== export class Time { static currentTimeMillis(): number { return Date.now() } static sleep(ms: number): Promise { return new Promise((resolve) => { window.setTimeout(resolve, ms) }) } } ===== FILE: tsconfig.app.json ===== { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "types": ["vite/client"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["src"] } ===== FILE: tsconfig.json ===== { "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } ===== FILE: tsconfig.node.json ===== { "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2023", "lib": ["ES2023"], "module": "ESNext", "types": ["node"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["vite.config.ts"] } ===== FILE: vite.config.ts ===== import { defineConfig } from 'vite' export default defineConfig({base:'./',build:{assetsDir:'',assetsInlineLimit:0,minify:false,modulePreload:false,rollupOptions:{output:{entryFileNames:'game.js',assetFileNames:'[name][extname]'}}}})