From b66da2007969a2a68a74cf6600ad62784e199a9a Mon Sep 17 00:00:00 2001 From: Tobias Nauen Date: Thu, 5 Mar 2026 21:24:59 +0100 Subject: [PATCH] add stats view --- package-lock.json | 385 ++++++++++++++++++++++++++++++---------------- package.json | 2 +- src/lib/game.ts | 182 +++++++++++++++++++++- src/main.ts | 221 +++++++++++++++++++++++++- src/style.css | 334 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 987 insertions(+), 137 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3dc0f6f..4c632cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "devDependencies": { "typescript": "^5.3.3", - "vite": "^5.0.12" + "vite": "^7.3.1" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "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" ], @@ -26,13 +26,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "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" ], @@ -43,13 +43,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "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" ], @@ -60,13 +60,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "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" ], @@ -77,13 +77,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "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" ], @@ -94,13 +94,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "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" ], @@ -111,13 +111,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -128,13 +128,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -145,13 +145,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "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" ], @@ -162,13 +162,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "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" ], @@ -179,13 +179,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "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" ], @@ -196,13 +196,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "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" ], @@ -213,13 +213,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "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" ], @@ -230,13 +230,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "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" ], @@ -247,13 +247,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -264,13 +264,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -281,13 +281,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "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" ], @@ -298,13 +298,30 @@ "linux" ], "engines": { - "node": ">=12" + "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.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "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" ], @@ -315,13 +332,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "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.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "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" ], @@ -332,13 +366,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "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.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "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" ], @@ -349,13 +400,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "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" ], @@ -366,13 +417,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "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" ], @@ -383,13 +434,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "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" ], @@ -400,7 +451,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -761,9 +812,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "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", @@ -771,32 +822,53 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@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/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/fsevents": { @@ -840,6 +912,19 @@ "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", + "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", @@ -924,6 +1009,23 @@ "node": ">=0.10.0" } }, + "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -939,21 +1041,24 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "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.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "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": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -962,19 +1067,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.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 }, @@ -995,6 +1106,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } } diff --git a/package.json b/package.json index 4d38887..feb16ea 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,6 @@ }, "devDependencies": { "typescript": "^5.3.3", - "vite": "^5.0.12" + "vite": "^7.3.1" } } diff --git a/src/lib/game.ts b/src/lib/game.ts index f4d2989..a85e0fe 100644 --- a/src/lib/game.ts +++ b/src/lib/game.ts @@ -1,8 +1,32 @@ -import { getTypeInfo, getEffectiveness } from './types'; +import { getTypeInfo, getEffectiveness, TypeName, TYPES } from './types'; import { Question, Sampler } from './sampler'; const STORAGE_KEY = 'pokemon-type-quiz-data'; +export interface CombinationStats { + key: string; + attackType: TypeName; + defenderTypes: TypeName[]; + total: number; + correct: number; + accuracy: number; +} + +export interface TypeChartStats { + attackType: TypeName; + total: number; + correct: number; + accuracy: number; +} + +export interface FullTypeChartCell { + attackType: TypeName; + defenderTypes: TypeName[]; + total: number; + correct: number; + accuracy: number; +} + export interface GameState { currentQuestion: Question | null; selectedAnswer: number | null; @@ -184,4 +208,160 @@ export class Game { return explanation; } + + getCombinationStats(): CombinationStats[] { + const history = this.sampler.getHistory(); + const comboMap = new Map(); + + for (const record of history) { + const existing = comboMap.get(record.key) || { total: 0, correct: 0 }; + existing.total++; + if (record.correct) existing.correct++; + comboMap.set(record.key, existing); + } + + const stats: CombinationStats[] = []; + for (const [key, data] of comboMap) { + const [attackType, ...defTypes] = key.split(':'); + const defenderTypes = defTypes.join(':').split('/') as TypeName[]; + stats.push({ + key, + attackType: attackType as TypeName, + defenderTypes, + total: data.total, + correct: data.correct, + accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0, + }); + } + + return stats.sort((a, b) => b.accuracy - a.accuracy); + } + + getBestCombinations(limit: number = 5): CombinationStats[] { + const stats = this.getCombinationStats().filter(s => s.total >= 3); + return stats.slice(0, limit); + } + + getWorstCombinations(limit: number = 5): CombinationStats[] { + const stats = this.getCombinationStats().filter(s => s.total >= 3); + return stats.slice(-limit).reverse(); + } + + getTypeChartStats(): TypeChartStats[] { + const history = this.sampler.getHistory(); + const typeMap = new Map(); + + for (const type of TYPES) { + typeMap.set(type.name, { total: 0, correct: 0 }); + } + + for (const record of history) { + const [attackType] = record.key.split(':'); + const data = typeMap.get(attackType as TypeName); + if (data) { + data.total++; + if (record.correct) data.correct++; + } + } + + const stats: TypeChartStats[] = []; + for (const type of TYPES) { + const data = typeMap.get(type.name)!; + stats.push({ + attackType: type.name, + total: data.total, + correct: data.correct, + accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0, + }); + } + + return stats.sort((a, b) => b.accuracy - a.accuracy); + } + + getDefendingTypeStats(): TypeChartStats[] { + const history = this.sampler.getHistory(); + const typeMap = new Map(); + + for (const type of TYPES) { + typeMap.set(type.name, { total: 0, correct: 0 }); + } + + for (const record of history) { + // Parse defender types from key format: "AttackType:DefType1/DefType2" + const parts = record.key.split(':'); + const defenderPart = parts[1]; + const defenderTypes = defenderPart.split('/'); + + // Track each defender type + for (const defType of defenderTypes) { + const data = typeMap.get(defType as TypeName); + if (data) { + data.total++; + if (record.correct) data.correct++; + } + } + } + + const stats: TypeChartStats[] = []; + for (const type of TYPES) { + const data = typeMap.get(type.name)!; + stats.push({ + attackType: type.name, + total: data.total, + correct: data.correct, + accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0, + }); + } + + return stats.sort((a, b) => b.accuracy - a.accuracy); + } + + private getAllDefenderTypes(): TypeName[][] { + const result: TypeName[][] = []; + // Single types + for (let i = 0; i < TYPES.length; i++) { + result.push([TYPES[i].name]); + } + // Dual types + for (let i = 0; i < TYPES.length; i++) { + for (let j = i + 1; j < TYPES.length; j++) { + result.push([TYPES[i].name, TYPES[j].name]); + } + } + return result; + } + + getFullTypeChartStats(): FullTypeChartCell[] { + const history = this.sampler.getHistory(); + const defenderTypesList = this.getAllDefenderTypes(); + + // Build a map for quick lookup + const statsMap = new Map(); + + for (const record of history) { + const existing = statsMap.get(record.key) || { total: 0, correct: 0 }; + existing.total++; + if (record.correct) existing.correct++; + statsMap.set(record.key, existing); + } + + const cells: FullTypeChartCell[] = []; + + for (const attackType of TYPES) { + for (const defenderTypes of defenderTypesList) { + const key = `${attackType.name}:${defenderTypes.sort().join('/')}`; + const data = statsMap.get(key) || { total: 0, correct: 0 }; + + cells.push({ + attackType: attackType.name, + defenderTypes, + total: data.total, + correct: data.correct, + accuracy: data.total > 0 ? (data.correct / data.total) * 100 : 0, + }); + } + } + + return cells; + } } diff --git a/src/main.ts b/src/main.ts index 8937eae..63e3b4f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,11 @@ -import { Game } from './lib/game'; +import { Game, CombinationStats, TypeChartStats, FullTypeChartCell } from './lib/game'; import { getTypeInfo, TypeName } from './lib/types'; import './style.css'; const app = document.getElementById('app')!; +let currentView: 'quiz' | 'stats' = 'quiz'; +let typeChartView: 'attacking' | 'defending' = 'attacking'; const game = new Game(render); game.nextQuestion(); @@ -14,6 +16,11 @@ function renderTypeBadge(name: TypeName, size: 'small' | 'large' = 'small'): str } function render(): void { + if (currentView === 'stats') { + renderStatsPage(); + return; + } + const state = game.getState(); const { options, startKey } = game.getOptions(); const stats = game.getStats(); @@ -70,6 +77,7 @@ function render(): void { Streak: ${state.streak} Accuracy: ${stats.accuracy.toFixed(1)}% Total: ${stats.total} + @@ -105,6 +113,209 @@ function render(): void { attachEventListeners(); } +function renderCombinationItem(combo: CombinationStats): string { + const defenderHtml = combo.defenderTypes.length === 1 + ? renderTypeBadge(combo.defenderTypes[0], 'small') + : `${renderTypeBadge(combo.defenderTypes[0], 'small')} / ${renderTypeBadge(combo.defenderTypes[1], 'small')}`; + + const accuracyClass = combo.accuracy >= 80 ? 'high' : combo.accuracy >= 50 ? 'medium' : 'low'; + + return ` +
+
+ ${renderTypeBadge(combo.attackType, 'small')} + vs + ${defenderHtml} +
+
+ ${combo.accuracy.toFixed(0)}% + (${combo.correct}/${combo.total}) +
+
+ `; +} + +function renderTypeChartCell(typeStat: TypeChartStats): string { + const info = getTypeInfo(typeStat.attackType); + const accuracyClass = typeStat.total === 0 ? 'no-data' : typeStat.accuracy >= 80 ? 'high' : typeStat.accuracy >= 50 ? 'medium' : 'low'; + const display = typeStat.total === 0 ? '-' : `${typeStat.accuracy.toFixed(0)}%`; + + return ` +
+ ${typeStat.attackType.slice(0, 3)} + ${display} +
+ `; +} + +function renderFullTypeChart(cells: FullTypeChartCell[]): string { + const defenderTypes = ['Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', 'Steel', 'Fairy']; + + // Create a map for quick cell lookup + const cellMap = new Map(); + for (const cell of cells) { + const defenderKey = cell.defenderTypes.join('/'); + const key = `${cell.attackType}:${defenderKey}`; + cellMap.set(key, cell); + } + + // Generate header row + const headerCells = defenderTypes.map(def => { + const info = getTypeInfo(def as TypeName); + return `
+ ${def.slice(0, 3)} +
`; + }).join(''); + + // Generate rows + const rows = defenderTypes.map(rowType => { + const rowInfo = getTypeInfo(rowType as TypeName); + const dataCells = defenderTypes.map(colType => { + const cell = cellMap.get(`${rowType}:${colType}`); + const total = cell?.total || 0; + const accuracy = cell?.accuracy || 0; + + let bgColor = 'var(--bg-accent)'; + let textColor = 'var(--text-secondary)'; + let display = '-'; + + if (total > 0) { + display = `${accuracy.toFixed(0)}`; + if (accuracy >= 80) { + bgColor = 'rgba(74, 222, 128, 0.3)'; + textColor = 'var(--success)'; + } else if (accuracy >= 50) { + bgColor = 'rgba(251, 191, 36, 0.3)'; + textColor = '#fbbf24'; + } else { + bgColor = 'rgba(248, 113, 113, 0.3)'; + textColor = 'var(--error)'; + } + } + + return `
+ ${display} +
`; + }).join(''); + + return ` +
+
+ ${rowType.slice(0, 3)} +
+ ${dataCells} +
+ `; + }).join(''); + + return ` +
+
+
+ ${headerCells} +
+ ${rows} +
+ `; +} + +function renderStatsPage(): void { + const stats = game.getStats(); + const bestCombos = game.getBestCombinations(5); + const worstCombos = game.getWorstCombinations(5); + const typeChartStats = typeChartView === 'attacking' ? game.getTypeChartStats() : game.getDefendingTypeStats(); + const fullTypeChartCells = game.getFullTypeChartStats(); + + const hasData = stats.total > 0; + + const bestCombosHtml = bestCombos.length > 0 + ? bestCombos.map(renderCombinationItem).join('') + : '

Answer at least 3 questions to see stats

'; + + const worstCombosHtml = worstCombos.length > 0 + ? worstCombos.map(renderCombinationItem).join('') + : '

Answer at least 3 questions to see stats

'; + + const typeChartHtml = typeChartStats.map(renderTypeChartCell).join(''); + + app.innerHTML = ` +
+
+ +

Your Statistics

+
+ +
+
+
${stats.total}
+
Total Questions
+
+
+
${stats.accuracy.toFixed(1)}%
+
Accuracy
+
+
+
${stats.correct}
+
Correct
+
+
+ + ${hasData ? ` +
+

Best Combinations

+

Highest accuracy (min. 3 attempts)

+
+ ${bestCombosHtml} +
+
+ +
+

Needs Practice

+

Lowest accuracy (min. 3 attempts)

+
+ ${worstCombosHtml} +
+
+ +
+

Type Chart Performance

+
+ + +
+

Accuracy by ${typeChartView} type

+
+ ${typeChartHtml} +
+
+ +
+

Full Type Chart

+

Accuracy for each type combination

+ ${renderFullTypeChart(fullTypeChartCells)} +
+ ` : ` +
+

No data yet. Start playing to track your statistics!

+
+ `} +
+ `; + + document.getElementById('backBtn')?.addEventListener('click', () => { + currentView = 'quiz'; + render(); + }); + + document.querySelectorAll('.chart-toggle').forEach(btn => { + btn.addEventListener('click', () => { + const view = btn.getAttribute('data-view') as 'attacking' | 'defending'; + typeChartView = view; + render(); + }); + }); +} + function attachEventListeners(): void { document.querySelectorAll('.option-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -126,6 +337,14 @@ function attachEventListeners(): void { game.nextQuestion(); }); } + + const statsBtn = document.getElementById('statsBtn'); + if (statsBtn) { + statsBtn.addEventListener('click', () => { + currentView = 'stats'; + render(); + }); + } } document.addEventListener('keydown', (e) => { diff --git a/src/style.css b/src/style.css index 1298f0c..a95f6aa 100644 --- a/src/style.css +++ b/src/style.css @@ -309,6 +309,340 @@ body { color: var(--text-secondary); } +/* Stats Button */ +.stats-btn { + padding: 6px 14px; + background: var(--bg-accent); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 20px; + color: var(--text-primary); + font-family: 'Outfit', sans-serif; + font-size: 0.8rem; + cursor: pointer; + transition: var(--transition); +} + +.stats-btn:hover { + background: var(--accent); + border-color: var(--accent); +} + +/* Stats Page */ +.stats-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +.stats-header { + display: flex; + align-items: center; + gap: 16px; +} + +.stats-header h1 { + font-size: 1.8rem; + font-weight: 700; + background: linear-gradient(135deg, var(--accent), #ff6b6b); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.back-btn { + padding: 10px 16px; + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: var(--text-primary); + font-family: 'Outfit', sans-serif; + font-size: 0.9rem; + cursor: pointer; + transition: var(--transition); +} + +.back-btn:hover { + border-color: var(--accent); + background: rgba(233, 69, 96, 0.1); +} + +.stats-overview { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.stat-card { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.stat-card .stat-value { + font-size: 2rem; + font-weight: 700; + color: var(--accent); + font-family: 'JetBrains Mono', monospace; +} + +.stat-card .stat-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-top: 4px; +} + +.stats-section { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.stats-section h2 { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 4px; +} + +.section-subtitle { + font-size: 0.8rem; + color: var(--text-secondary); + margin-bottom: 16px; +} + +.combo-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.combo-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--bg-accent); + border-radius: 8px; +} + +.combo-types { + display: flex; + align-items: center; + gap: 8px; +} + +.combo-vs { + font-size: 0.75rem; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; +} + +.combo-stats { + display: flex; + align-items: center; + gap: 8px; + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; +} + +.combo-accuracy { + font-weight: 600; +} + +.combo-accuracy.high { + color: var(--success); +} + +.combo-accuracy.medium { + color: #fbbf24; +} + +.combo-accuracy.low { + color: var(--error); +} + +.combo-count { + color: var(--text-secondary); + font-size: 0.75rem; +} + +/* Type Chart */ +.type-chart { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 6px; +} + +.type-cell { + aspect-ratio: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 8px; + font-size: 0.7rem; + font-weight: 600; + transition: var(--transition); +} + +.type-cell:hover { + transform: scale(1.05); +} + +.type-cell-name { + text-transform: uppercase; + letter-spacing: 0.5px; + opacity: 0.9; +} + +.type-cell-accuracy { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + margin-top: 2px; + padding: 2px 4px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); +} + +.type-cell-accuracy.high { + color: #4ade80; +} + +.type-cell-accuracy.medium { + color: #fbbf24; +} + +.type-cell-accuracy.low { + color: #f87171; +} + +.type-cell-accuracy.no-data { + color: rgba(255, 255, 255, 0.5); +} + +.no-data-message { + text-align: center; + padding: 48px 24px; + background: var(--bg-card); + border-radius: var(--border-radius); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.no-data-message p { + color: var(--text-secondary); + font-size: 1rem; +} + +.no-data-text { + color: var(--text-secondary); + font-size: 0.9rem; + text-align: center; + padding: 16px; +} + +/* Full Type Chart */ +.full-chart-section { + overflow: hidden; +} + +.chart-toggle-row { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.chart-toggle { + padding: 8px 16px; + background: var(--bg-accent); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: var(--text-secondary); + font-family: 'Outfit', sans-serif; + font-size: 0.85rem; + cursor: pointer; + transition: var(--transition); +} + +.chart-toggle:hover { + border-color: var(--accent); +} + +.chart-toggle.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.full-type-chart { + display: grid; + grid-template-columns: 50px repeat(18, 1fr); + gap: 2px; + font-size: 0.65rem; +} + +.full-chart-header-row { + display: contents; +} + +.full-chart-corner { + width: 50px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-accent); + border-radius: 3px; +} + +.full-chart-header { + width: 100%; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.55rem; + font-weight: 600; + text-transform: uppercase; + border-radius: 3px; +} + +.full-chart-row { + display: contents; +} + +.full-chart-row-header { + width: 50px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.55rem; + font-weight: 600; + text-transform: uppercase; + border-radius: 3px; +} + +.full-chart-cell { + width: 100%; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-family: 'JetBrains Mono', monospace; + font-size: 0.55rem; + font-weight: 600; + border-radius: 3px; + transition: var(--transition); +} + +.full-chart-cell:hover { + transform: scale(1.15); + z-index: 1; + border: 1px solid rgba(255, 255, 255, 0.5); +} + @media (max-width: 480px) { body { padding: 12px;