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
TRAIL Loading Preparing the trail… 0.00s 0% Retry Menu / Back
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]'}}}})