From f00aad2bbde568b5d25d830668192eae28b320e1 Mon Sep 17 00:00:00 2001 From: shafin-r Date: Wed, 11 Mar 2026 03:17:08 +0600 Subject: [PATCH] feat(quests): add 3d functionality for quests --- package-lock.json | 1102 +++++++++++++++++-- package.json | 4 + src/components/Island3D.tsx | 479 +++++++++ src/pages/student/QuestMap.tsx | 1854 ++++++++++++++------------------ 4 files changed, 2270 insertions(+), 1169 deletions(-) create mode 100644 src/components/Island3D.tsx diff --git a/package-lock.json b/package-lock.json index 5f19e89..9dfdae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-table": "^8.21.3", "canvas-confetti": "^1.9.4", @@ -21,6 +23,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.30.0", "katex": "^0.16.28", + "leva": "^0.10.1", "lucide-react": "^0.562.0", "radix-ui": "^1.4.3", "react": "^19.2.0", @@ -29,6 +32,7 @@ "react-router-dom": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", + "three": "^0.183.2", "vaul": "^1.1.2", "zustand": "^5.0.9" }, @@ -250,6 +254,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "dev": true, @@ -292,6 +305,12 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -950,6 +969,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "license": "MIT" @@ -4111,15 +4148,103 @@ "version": "1.1.1", "license": "MIT" }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", + "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "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" ], @@ -4130,9 +4255,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "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" ], @@ -4143,9 +4268,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "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" ], @@ -4156,9 +4281,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "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" ], @@ -4169,9 +4294,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "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" ], @@ -4182,9 +4307,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "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" ], @@ -4195,9 +4320,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "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" ], @@ -4208,9 +4333,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "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" ], @@ -4221,9 +4346,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "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" ], @@ -4234,9 +4359,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "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" ], @@ -4247,9 +4372,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "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" ], @@ -4260,9 +4385,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "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" ], @@ -4273,9 +4398,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "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" ], @@ -4286,9 +4411,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "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" ], @@ -4299,9 +4424,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "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" ], @@ -4312,9 +4437,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "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" ], @@ -4325,9 +4450,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "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" ], @@ -4338,9 +4463,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "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" ], @@ -4351,9 +4476,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "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" ], @@ -4364,9 +4489,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "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" ], @@ -4377,9 +4502,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "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" ], @@ -4390,9 +4515,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "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" ], @@ -4403,9 +4528,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "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" ], @@ -4416,7 +4541,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", + "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" ], @@ -4427,7 +4554,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", + "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" ], @@ -4437,6 +4566,15 @@ "win32" ] }, + "node_modules/@stitches/react": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", + "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "license": "MIT", @@ -4715,6 +4853,12 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -4752,6 +4896,12 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "license": "MIT" @@ -4769,9 +4919,14 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4785,6 +4940,42 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.183.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", + "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~1.0.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.55.0", "dev": true, @@ -4964,11 +5155,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5026,6 +5219,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.4", "dev": true, @@ -5045,6 +5256,12 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@webgpu/types": { + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", + "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "license": "BSD-3-Clause" + }, "node_modules/acorn": { "version": "8.15.0", "dev": true, @@ -5065,7 +5282,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", + "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": { @@ -5108,11 +5327,49 @@ "node": ">=10" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "dev": true, @@ -5121,6 +5378,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "dev": true, @@ -5162,6 +5428,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -5170,6 +5460,19 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001770", "dev": true, @@ -5247,6 +5550,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, "node_modules/commander": { "version": "8.3.0", "license": "MIT", @@ -5275,9 +5584,26 @@ "url": "https://opencollective.com/express" } }, + "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==", + "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", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5290,7 +5616,6 @@ }, "node_modules/csstype": { "version": "3.2.3", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -5314,6 +5639,24 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "license": "Apache-2.0", @@ -5325,6 +5668,12 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "dev": true, @@ -5585,6 +5934,27 @@ "node": ">=0.10.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -5615,6 +5985,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "dev": true, @@ -5626,6 +6002,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.5.0.tgz", + "integrity": "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/find-up": { "version": "5.0.0", "dev": true, @@ -5658,6 +6046,15 @@ "dev": true, "license": "ISC" }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/framer-motion": { "version": "12.34.0", "license": "MIT", @@ -5712,6 +6109,15 @@ "node": ">=6" } }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "dev": true, @@ -5734,6 +6140,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/graceful-fs": { "version": "4.2.11", "license": "ISC" @@ -5759,6 +6171,32 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -5767,6 +6205,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "dev": true, @@ -5790,6 +6234,18 @@ "node": ">=0.8.19" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -5809,11 +6265,49 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "license": "MIT", @@ -5895,6 +6389,46 @@ "json-buffer": "3.0.1" } }, + "node_modules/leva": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/leva/-/leva-0.10.1.tgz", + "integrity": "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-portal": "^1.1.4", + "@radix-ui/react-tooltip": "^1.1.8", + "@stitches/react": "^1.2.8", + "@use-gesture/react": "^10.2.5", + "colord": "^2.9.2", + "dequal": "^2.0.2", + "merge-value": "^1.0.0", + "react-colorful": "^5.5.1", + "react-dropzone": "^12.0.0", + "v8n": "^1.3.3", + "zustand": "^3.6.9" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/leva/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -5907,6 +6441,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "license": "MPL-2.0", @@ -6174,7 +6717,6 @@ "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -6197,6 +6739,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "license": "MIT", @@ -6204,8 +6756,40 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/merge-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/merge-value/-/merge-value-1.0.0.tgz", + "integrity": "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "is-extendable": "^1.0.0", + "mixin-deep": "^1.2.0", + "set-value": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "license": "MIT" + }, "node_modules/minimatch": { - "version": "3.1.2", + "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": { @@ -6215,6 +6799,19 @@ "node": "*" } }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/motion-dom": { "version": "12.34.0", "license": "MIT", @@ -6260,7 +6857,6 @@ "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6330,7 +6926,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6376,6 +6971,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -6384,10 +6985,19 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -6559,6 +7169,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "19.2.4", "license": "MIT", @@ -6569,10 +7189,26 @@ "react": "^19.2.4" } }, + "node_modules/react-dropzone": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.1.0.tgz", + "integrity": "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.5.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8" + } + }, "node_modules/react-is": { "version": "16.13.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-katex": { "version": "3.1.0", @@ -6690,6 +7326,30 @@ } } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -6699,7 +7359,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -6712,31 +7374,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@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" } }, @@ -6756,9 +7418,32 @@ "version": "2.7.2", "license": "MIT" }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6769,7 +7454,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6782,6 +7466,57 @@ "node": ">=0.10.0" } }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -6804,6 +7539,15 @@ "node": ">=8" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/tailwind-merge": { "version": "3.4.1", "license": "MIT", @@ -6827,6 +7571,44 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.183.2", + "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", + "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "license": "MIT", @@ -6841,6 +7623,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.4.0", "dev": true, @@ -6856,6 +7668,43 @@ "version": "2.8.1", "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/tw-animate-css": { "version": "1.4.0", "dev": true, @@ -6997,6 +7846,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/v8n": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/v8n/-/v8n-1.5.1.tgz", + "integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==", + "license": "MIT" + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -7082,9 +7946,19 @@ } } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index bd0496b..cf3ce67 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-table": "^8.21.3", "canvas-confetti": "^1.9.4", @@ -23,6 +25,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.30.0", "katex": "^0.16.28", + "leva": "^0.10.1", "lucide-react": "^0.562.0", "radix-ui": "^1.4.3", "react": "^19.2.0", @@ -31,6 +34,7 @@ "react-router-dom": "^7.12.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", + "three": "^0.183.2", "vaul": "^1.1.2", "zustand": "^5.0.9" }, diff --git a/src/components/Island3D.tsx b/src/components/Island3D.tsx new file mode 100644 index 0000000..dd45e66 --- /dev/null +++ b/src/components/Island3D.tsx @@ -0,0 +1,479 @@ +// Island3D.tsx +import { useRef, useState, useMemo } from "react"; +import { useFrame } from "@react-three/fiber"; +import { Float, Text, Html, Sparkles, Billboard } from "@react-three/drei"; +import * as THREE from "three"; +import type { QuestNode } from "../types/quest"; + +export interface Island3DProps { + node: QuestNode; + position: [number, number, number]; + accent: string; + terrain: { l: string; m: string; d: string; s: string }; + onTap: (node: QuestNode) => void; + onClaim: (node: QuestNode) => void; + index: number; + /** When true the info card drops behind the modal overlay */ + modalOpen?: boolean; +} + +// ─── Seeded RNG (xorshift32) ────────────────────────────────────────────────── +const makeRng = (seed: number) => { + let s = ((seed + 1) * 1664525 + 1013904223) >>> 0; + return () => { + s ^= s << 13; + s ^= s >>> 17; + s ^= s << 5; + return (s >>> 0) / 4294967296; + }; +}; + +// ─── Irregular island Shape for ExtrudeGeometry ─────────────────────────────── +// ExtrudeGeometry computes normals correctly — no manual BufferGeometry needed. +const makeIslandShape = (seed: number, radiusBase = 1.0): THREE.Shape => { + const rng = makeRng(seed); + const sides = 7 + Math.floor(rng() * 5); // 7–11 sides + const pts: THREE.Vector2[] = []; + + for (let i = 0; i < sides; i++) { + const angle = (i / sides) * Math.PI * 2 - Math.PI / 2; + const radius = radiusBase * (0.82 + rng() * 0.42); + pts.push( + new THREE.Vector2(Math.cos(angle) * radius, Math.sin(angle) * radius), + ); + } + + const shape = new THREE.Shape(pts); + return shape; +}; + +// ─── Status palette ─────────────────────────────────────────────────────────── +const PALETTE = { + LOCKED: { top: "#778fa0", side: "#526070", emissive: "#000000", ei: 0 }, + ACTIVE: { top: "", side: "", emissive: "#ffffff", ei: 0 }, + CLAIMABLE: { top: "#f59e0b", side: "#d97706", emissive: "#fde68a", ei: 0.3 }, + COMPLETED: { top: "#22c55e", side: "#16a34a", emissive: "#bbf7d0", ei: 0.1 }, +}; + +// ─── Card scale ─────────────────────────────────────────────────────────────── +// Change this single number to resize the entire info card. +// 1.0 = default. 0.8 = smaller, 1.3 = larger. +const CARD_SCALE = 0.8; + +// Derived sizes — do not edit these directly +const C = { + width: 175 * CARD_SCALE, + borderRadius: 13 * CARD_SCALE, + padV: 10 * CARD_SCALE, + padH: 12 * CARD_SCALE, + padVb: 9 * CARD_SCALE, + nameSize: 12.5 * CARD_SCALE, + xpSize: 11 * CARD_SCALE, + statusSize: 10 * CARD_SCALE, + xpPadV: 2 * CARD_SCALE, + xpPadH: 8 * CARD_SCALE, + xpRadius: 100 * CARD_SCALE, + gapRow: 6 * CARD_SCALE, + nameMb: 7 * CARD_SCALE, + barMt: 8 * CARD_SCALE, + barH: 4 * CARD_SCALE, + btnMt: 9 * CARD_SCALE, + btnPadV: 6 * CARD_SCALE, + btnSize: 11 * CARD_SCALE, + btnRadius: 9 * CARD_SCALE, + // Html position: how far the card sits from the island centre + posX: 2.1 * CARD_SCALE, +}; + +const REQ_EMOJI: Record = { + questions: "❓", + accuracy: "🎯", + streak: "🔥", + sessions: "📚", + topics: "🗺️", + xp: "⚡", + leaderboard: "🏆", +}; + +export const Island3D = ({ + node, + position, + accent, + terrain, + onTap, + onClaim, + index, + modalOpen = false, +}: Island3DProps) => { + const topMeshRef = useRef(null!); + const [hovered, setHovered] = useState(false); + + const status = (node.status ?? "LOCKED") as keyof typeof PALETTE; + const isLocked = status === "LOCKED"; + const isClaimable = status === "CLAIMABLE"; + const isActive = status === "ACTIVE"; + const isCompleted = status === "COMPLETED"; + + // ── Geometry — ExtrudeGeometry gives correct normals, solid faces ────────── + const { topGeo, cliffGeo } = useMemo(() => { + const seed = index * 13 + 7; + + // Top plateau shape (slightly smaller to show cliff peeking out below) + const topShape = makeIslandShape(seed, 1.0); + const topGeo = new THREE.ExtrudeGeometry(topShape, { + depth: 0.3, + bevelEnabled: true, + bevelThickness: 0.1, + bevelSize: 0.07, + bevelSegments: 4, + }); + // ExtrudeGeometry extrudes along Z — rotate to lie flat (XZ plane) + topGeo.rotateX(-Math.PI / 2); + // Shift so top face is at y=0, bottom at y=-0.3 + topGeo.translate(0, 0, 0); + + // Cliff body — same seed so outline matches, just a bit wider & taller + const cliffShape = makeIslandShape(seed, 1.12); + const cliffGeo = new THREE.ExtrudeGeometry(cliffShape, { + depth: 0.55, + bevelEnabled: true, + bevelThickness: 0.05, + bevelSize: 0.04, + bevelSegments: 2, + }); + cliffGeo.rotateX(-Math.PI / 2); + cliffGeo.translate(0, -0.3, 0); // sit directly below top plateau + + return { topGeo, cliffGeo }; + }, [index]); + + // ── Colours ─────────────────────────────────────────────────────────────── + const pal = PALETTE[status]; + const topColor = isActive + ? terrain.l + : isCompleted + ? PALETTE.COMPLETED.top + : pal.top; + const sideColor = isActive + ? terrain.m + : isCompleted + ? PALETTE.COMPLETED.side + : pal.side; + + // ── Hover scale spring ──────────────────────────────────────────────────── + useFrame((_, dt) => { + if (!topMeshRef.current) return; + const target = hovered && !isLocked ? 1.07 : 1.0; + const curr = topMeshRef.current.scale.x; + const next = curr + (target - curr) * (1 - Math.exp(-14 * dt)); + topMeshRef.current.scale.setScalar(next); + }); + + // ── Emoji ──────────────────────────────────────────────────────────────── + const emoji = isLocked + ? "🔒" + : isClaimable + ? "📦" + : isCompleted + ? "✅" + : (REQ_EMOJI[node.req_type] ?? "🏝️"); + + const pct = + node.req_target > 0 + ? Math.min(100, Math.round((node.current_value / node.req_target) * 100)) + : 0; + + const ringColor = isClaimable ? "#fbbf24" : isCompleted ? "#4ade80" : accent; + + return ( + + { + e.stopPropagation(); + if (!isLocked) setHovered(true); + }} + onPointerLeave={(e) => { + e.stopPropagation(); + setHovered(false); + }} + onClick={(e) => { + e.stopPropagation(); + if (!isLocked) onTap(node); + }} + > + {/* ── Top plateau — solid, opaque ────────────────────────────────── */} + + + + + {/* ── Cliff body — solid, darker shade ──────────────────────────── */} + + + + + {/* ── Water-level glow ring ──────────────────────────────────────── */} + {!isLocked && ( + + + + + )} + + {/* ── Claimable sparkles + top ring ─────────────────────────────── */} + {isClaimable && ( + <> + + + + + + + )} + + {/* ── Hover ring ────────────────────────────────────────────────── */} + {hovered && !isLocked && ( + + + + + )} + + {/* ── Billboard emoji above island ───────────────────────────────── */} + + + {emoji} + + + + {/* ── Info card ─────────────────────────────────────────────────── */} + +
{ + e.stopPropagation(); + if (!isLocked) onTap(node); + }} + style={{ + width: C.width, + background: isClaimable + ? "rgba(28,18,4,0.96)" + : isLocked + ? "rgba(12,18,28,0.72)" + : "rgba(5,12,24,0.93)", + border: `1px solid ${ + isClaimable + ? "rgba(251,191,36,0.65)" + : isCompleted + ? "rgba(74,222,128,0.45)" + : isActive + ? `${accent}55` + : "rgba(255,255,255,0.1)" + }`, + borderLeft: `3px solid ${ + isClaimable + ? "#fbbf24" + : isCompleted + ? "#4ade80" + : isActive + ? accent + : "rgba(255,255,255,0.2)" + }`, + borderRadius: C.borderRadius, + padding: `${C.padV}px ${C.padH}px ${C.padVb}px`, + backdropFilter: "blur(16px)", + WebkitBackdropFilter: "blur(16px)", + boxShadow: isClaimable + ? "0 0 24px rgba(251,191,36,0.28), 0 6px 20px rgba(0,0,0,0.6)" + : "0 6px 20px rgba(0,0,0,0.55)", + opacity: isLocked ? 0.5 : 1, + cursor: isLocked ? "default" : "pointer", + userSelect: "none", + fontFamily: "'Nunito', 'Nunito Sans', sans-serif", + }} + > + {/* Name */} +
+ {node.name ?? "—"} +
+ + {/* XP + status */} +
+ + ⚡ {node.reward_xp} XP + + + {isClaimable + ? "✨ Claim!" + : isCompleted + ? "Done" + : isLocked + ? "Locked" + : `${pct}%`} + +
+ + {/* Progress bar */} + {isActive && node.req_target > 0 && ( +
+
+
+ )} + + {/* Claim button */} + {isClaimable && ( + + )} +
+ + + + ); +}; diff --git a/src/pages/student/QuestMap.tsx b/src/pages/student/QuestMap.tsx index f10cee2..53111f1 100644 --- a/src/pages/student/QuestMap.tsx +++ b/src/pages/student/QuestMap.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from "react"; +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import type { QuestArc, QuestNode, @@ -10,45 +10,110 @@ import { api } from "../../utils/api"; import { QuestNodeModal } from "../../components/QuestNodeModal"; import { ChestOpenModal } from "../../components/ChestOpenModal"; import { InfoHeader } from "../../components/InfoHeader"; +import { Canvas, useThree, useFrame } from "@react-three/fiber"; +import { Island3D } from "../../components/Island3D"; +import { Billboard, OrbitControls, Stars, Text } from "@react-three/drei"; +import * as THREE from "three"; // ─── Map geometry ───────────────────────────────────────────────────────────── const VW = 420; -const ROW_GAP = 390; const TOP_PAD = 80; +const ROW_H = 520; // vertical step per island (increased for more separation) -// ─── Island size ────────────────────────────────────────────────────────────── -// Tune this single value. 1.0 = original, 1.5 = 50% bigger, 0.7 = smaller. const ISLAND_SCALE = 1.2; - -// LAND_H is the half-height of the island shape in SVG units (base value × scale). -// Every vertical offset above/below the island derives from this. const LAND_H_BASE = 40; const LAND_H = LAND_H_BASE * ISLAND_SCALE; -const COL_X = [ - Math.round(VW * 0.22), - Math.round(VW * 0.5), - Math.round(VW * 0.78), -]; - -const ARC_COL_SEQS: Record = { - east_blue: [0, 1, 2, 0, 1, 2], - alabasta: [2, 0, 2, 1, 0, 2], - skypiea: [1, 2, 0, 2, 0, 1], -}; -const COL_SEQ_DEFAULT = [0, 1, 2, 0, 1, 2]; - const CARD_W = 130; const CARD_H = 170; const CARD_H_CLAIMABLE = 235; -const islandCX = (i: number, arcId: string) => { - const seq = ARC_COL_SEQS[arcId] ?? COL_SEQ_DEFAULT; - return COL_X[seq[i % seq.length]]; +// ─── Seeded RNG ─────────────────────────────────────────────────────────────── +const mkRng = (seed: number) => { + let s = seed >>> 0; + return () => { + s += 0x6d2b79f5; + let t = Math.imul(s ^ (s >>> 15), 1 | s); + t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +}; + +const strToSeed = (str: string) => { + let h = 5381; + for (let i = 0; i < str.length; i++) + h = (Math.imul(h, 33) ^ str.charCodeAt(i)) >>> 0; + return h; +}; + +// ─── Random island positions ────────────────────────────────────────────────── +// Generates organic-feeling positions that zigzag downward with random offsets. +// Uses a seeded RNG per arc so positions are deterministic per arc but varied. +const MIN_DIST = 600; // minimum px distance between any two islands (increased for more separation) + +const generateIslandPositions = ( + nodeCount: number, + arcId: string, +): { x: number; y: number }[] => { + const rng = mkRng(strToSeed(arcId + "_positions")); + + // X zones: left, centre-left, centre, centre-right, right + const ZONES = [ + [0.12, 0.32], + [0.28, 0.48], + [0.38, 0.62], + [0.52, 0.72], + [0.68, 0.88], + ]; + + const positions: { x: number; y: number }[] = []; + + for (let i = 0; i < nodeCount; i++) { + const baseY = TOP_PAD + i * ROW_H; + // vertical jitter ±55px so islands feel scattered, not gridded + const yJitter = (rng() - 0.5) * 110; + + let best: { x: number; y: number } | null = null; + let attempts = 0; + + while (!best || attempts < 40) { + // Pick a random zone, biased to alternate left/right to keep paths readable + const zoneIdx = + i % 2 === 0 + ? Math.floor(rng() * 3) // odd rows: left half + : 2 + Math.floor(rng() * 3); // even rows: right half (clamped below) + const zone = ZONES[Math.min(zoneIdx, ZONES.length - 1)]; + const candidate = { + x: Math.round((zone[0] + rng() * (zone[1] - zone[0])) * VW), + y: Math.round(baseY + yJitter * (attempts < 20 ? 1 : 0.4)), + }; + + // Enforce minimum spacing + const tooClose = positions.some((p) => { + const dx = p.x - candidate.x; + const dy = p.y - candidate.y; + return Math.sqrt(dx * dx + dy * dy) < MIN_DIST; + }); + + if (!tooClose || attempts >= 39) { + best = candidate; + break; + } + attempts++; + } + + positions.push(best!); + } + + return positions; +}; + +const svgHeight = (positions: { x: number; y: number }[]) => { + if (!positions.length) return 600; + return ( + Math.max(...positions.map((p) => p.y)) + TOP_PAD + CARD_H_CLAIMABLE + LAND_H + ); }; -const islandCY = (i: number) => TOP_PAD + i * ROW_GAP; -const svgHeight = (n: number) => - TOP_PAD + (n - 1) * ROW_GAP + TOP_PAD + CARD_H_CLAIMABLE + LAND_H; // ─── Island shapes ──────────────────────────────────────────────────────────── const SHAPES = [ @@ -73,11 +138,9 @@ const STYLES = ` position: relative; display: flex; flex-direction: column; - /* No overflow:hidden here — it would clip SVG islands that bleed outside bounds */ background: #060e1f; } - /* ══ MOBILE HEADER ══ */ .qm-header { position: relative; z-index: 30; flex-shrink: 0; background: rgba(4,10,24,0.94); @@ -107,7 +170,6 @@ const STYLES = ` } @keyframes qmDotBlink { 0%,100%{ opacity:1; } 50%{ opacity:0.4; } } - /* ══ MOBILE SEA SCROLL ══ */ .qm-sea-scroll { flex:1; overflow-y:auto; overflow-x:visible; position:relative; scrollbar-width:none; -webkit-overflow-scrolling:touch; @@ -142,7 +204,6 @@ const STYLES = ` } @keyframes qmBobble { 0%,100%{transform:translateY(0) scale(1);opacity:0.5;} 50%{transform:translateY(-10px) scale(1.1);opacity:0.9;} } - /* ── Arc banner ── */ .qm-arc-banner { position:relative; z-index:5; border-radius:22px; padding: 1.1rem 1.25rem; margin-bottom: 1.5rem; @@ -179,11 +240,6 @@ const STYLES = ` color:rgba(255,255,255,0.65); white-space:nowrap; } - /* - * ── Map wrapper ── - * Width matches VW exactly so the SVG never clips or stretches on any screen. - * Centred via margin:auto inside any wider desktop container. - */ .qm-map-wrap { width: ${VW}px; max-width: 100%; @@ -193,11 +249,6 @@ const STYLES = ` overflow: visible; } - /* - * ── Map SVG ── - * Must be pinned to VW pixels. NEVER use width:100% — the viewBox coordinate - * system is fixed at VW wide; stretching it breaks all island/card positions. - */ .qm-map-svg { display: block; width: ${VW}px; @@ -205,10 +256,8 @@ const STYLES = ` overflow: visible; position: relative; z-index: 5; - } - /* ── Info card ── */ .qm-info-card { background: rgba(255,255,255,0.055); border:1px solid rgba(255,255,255,0.09); border-radius:16px; padding:0.7rem 0.85rem; @@ -229,7 +278,6 @@ const STYLES = ` .qm-claim-btn:hover { transform:translateY(-1px); box-shadow:0 5px 0 #d97706; } .qm-claim-btn:active { transform:translateY(1px); box-shadow:0 1px 0 #d97706; } - /* ══ FAB ══ */ .qm-fab { position:fixed; bottom:calc(1.25rem + 80px + env(safe-area-inset-bottom)); right:1.25rem; z-index:25; width:52px; height:52px; border-radius:50%; @@ -242,9 +290,6 @@ const STYLES = ` .qm-fab:hover { transform:scale(1.1) rotate(8deg); } @keyframes qmFabFloat { 0%,100%{transform:translateY(0) rotate(-4deg);} 50%{transform:translateY(-7px) rotate(4deg);} } - /* ════════════════════════════════════════════════════════════ - DESKTOP TWO-COLUMN LAYOUT (≥ 1024px) - ════════════════════════════════════════════════════════════ */ @media (min-width: 1024px) { .qm-header { display: none; } .qm-fab { display: none; } @@ -252,27 +297,23 @@ const STYLES = ` .qm-screen { flex-direction: row; - /* overflow:hidden removed — clips SVG islands. Height is bounded by 100vh on children. */ overflow:visible; padding-left: 270px; } .qm-right-sea-scroll .qm-map-wrap { - width: 420px !important; /* Force exact VW */ - max-width: none !important; - margin: 0 auto !important; - min-height: 600px; /* Ensure scrollable height */ - overflow: visible !important; - } - - .qm-right-sea-scroll .qm-map-svg { - width: 420px !important; - height: auto !important; - max-width: none !important; - } + width: 420px !important; + max-width: none !important; + margin: 0 auto !important; + min-height: 600px; + overflow: visible !important; + } - - + .qm-right-sea-scroll .qm-map-svg { + width: 420px !important; + height: auto !important; + max-width: none !important; + } .qm-left-panel { width: 512px; @@ -301,11 +342,9 @@ const STYLES = ` display: flex; flex-direction: column; overflow: visible; - /* overflow:hidden removed — it clips SVG islands. Scroll containment is on .qm-right-sea-scroll */ position: relative; } - /* Floating glass tab bar — sits above the sea scroll */ .qm-desktop-arc-tabs { position: absolute; top: 0; left: 0; right: 0; @@ -357,17 +396,15 @@ const STYLES = ` .qm-right-sea-scroll { flex: 1; overflow-y: auto; - overflow-x: visible; /* must NOT be hidden — SVG islands bleed outside their centre point */ + overflow-x: visible; scrollbar-width: thin; scrollbar-color: rgba(251,191,36,0.12) transparent; } .qm-right-sea-scroll::-webkit-scrollbar { width: 4px; } .qm-right-sea-scroll::-webkit-scrollbar-thumb { background: rgba(251,191,36,0.12); border-radius: 2px; } - /* Push content below the floating tab bar */ .qm-right-sea-scroll .qm-sea { padding: 5.5rem 2rem 5rem; } - /* Desktop font upscaling */ .qm-arc-banner-name { font-size: 1.6rem; } .qm-arc-banner-sub { font-size: 0.88rem; } .qm-arc-banner-count { font-size: 0.82rem; } @@ -377,7 +414,6 @@ const STYLES = ` .qm-claim-btn { font-size: 0.85rem; } } - /* Hide desktop-only elements on mobile */ @media (max-width: 1023px) { .qm-left-panel { display: none !important; } .qm-right-panel { display: none !important; } @@ -385,9 +421,6 @@ const STYLES = ` .qm-right-sea-scroll { display: none !important; } } - /* ══════════════════════════════════════ - LEFT PANEL INNER COMPONENTS - ══════════════════════════════════════ */ .qmlp-page-title { font-family:'Sorts Mill Goudy',serif; font-size:1.55rem; font-weight:900; color:#fbbf24; text-shadow:0 0 28px rgba(251,191,36,0.35),0 0 60px rgba(251,191,36,0.08); letter-spacing:0.04em; display:flex; align-items:center; gap:0.5rem; line-height:1; } .qmlp-page-sub { font-family:'Nunito Sans',sans-serif; font-size:0.68rem; font-weight:700; color:rgba(255,255,255,0.25); letter-spacing:0.14em; text-transform:uppercase; margin-top:0.22rem; } .qmlp-rank-card { border-radius:20px; background:linear-gradient(160deg,#0d1b38 0%,#070f20 100%); border:1px solid rgba(255,255,255,0.07); box-shadow:0 8px 32px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.05); overflow:hidden; padding:1.2rem; position:relative; } @@ -481,21 +514,18 @@ export interface ArcTheme { terrain: { l: string; m: string; d: string; s: string }; decos: [string, string, string]; } -const mkRng = (seed: number) => { - let s = seed >>> 0; - return () => { - s += 0x6d2b79f5; - let t = Math.imul(s ^ (s >>> 15), 1 | s); - t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; -}; -const strToSeed = (str: string) => { - let h = 5381; - for (let i = 0; i < str.length; i++) - h = (Math.imul(h, 33) ^ str.charCodeAt(i)) >>> 0; - return h; -}; + +const DECO_SETS: [string, string, string][] = [ + ["🌴", "🌿", "🌴"], + ["🌵", "🏺", "🌵"], + ["☁️", "✨", "☁️"], + ["🪨", "🌾", "🪨"], + ["🍄", "🌸", "🍄"], + ["🔥", "💀", "🔥"], + ["❄️", "🌨️", "❄️"], + ["🌺", "🦜", "🌺"], +]; + const hslToHex = (h: number, s: number, l: number) => { const a = s * Math.min(l, 1 - l); const f = (n: number) => { @@ -507,63 +537,68 @@ const hslToHex = (h: number, s: number, l: number) => { }; return `#${f(0)}${f(8)}${f(4)}`; }; -const ARC_EMOJIS = [ - "⚓", - "🏴‍☠️", - "🗺️", - "⚔️", - "🌊", - "🔱", - "☠️", - "🧭", - "💎", - "🏝️", - "⛵", - "🌋", -]; -const DECO_SETS: [string, string, string][] = [ - ["🌴", "🌿", "🌴"], - ["🌵", "🏺", "🌵"], - ["☁️", "✨", "☁️"], - ["🪨", "🌾", "🪨"], - ["🍄", "🌸", "🍄"], - ["🔥", "💀", "🔥"], - ["❄️", "🌨️", "❄️"], - ["🌺", "🦜", "🌺"], -]; + export const generateArcTheme = (arc: QuestArc): ArcTheme => { const rng = mkRng(strToSeed(arc.id)); - const hue = rng(), - hueShift = 0.05 + rng() * 0.1; - const satHigh = 0.55 + rng() * 0.35, - satLow = satHigh * (0.5 + rng() * 0.3); - const accent = hslToHex(hue, satHigh, 0.72); - const accentDark = hslToHex(hue, satHigh, 0.3); - const bgFrom = hslToHex(hue, satLow, 0.14); - const bgTo = hslToHex(hue + hueShift, satLow, 0.22); - const tL = hslToHex(hue, satHigh, 0.68); - const tM = hslToHex(hue, satHigh, 0.42); - const tD = hslToHex(hue, satHigh * 0.85, 0.22); - const sd = parseInt(tD.slice(1, 3), 16), - sg = parseInt(tD.slice(3, 5), 16), - sb = parseInt(tD.slice(5, 7), 16); + const anchors = [150, 165, 180, 200, 230, 260]; + const baseHue = + anchors[Math.floor(rng() * anchors.length)] + (rng() - 0.5) * 8; + const satBase = 0.48 + rng() * 0.18; + const satTerrain = Math.min(0.8, satBase + 0.12); + const accentLightL = 0.48 + rng() * 0.12; + const accentDarkL = 0.22 + rng() * 0.06; + const bgFromL = 0.04 + rng() * 0.06; + const bgToL = 0.1 + rng() * 0.06; + const accent = hslToHex(baseHue, satBase, accentLightL); + const accentDark = hslToHex( + baseHue + (rng() * 6 - 3), + Math.max(0.35, satBase - 0.08), + accentDarkL, + ); + const bgFrom = hslToHex( + baseHue + (rng() * 10 - 5), + 0.1 + rng() * 0.06, + bgFromL, + ); + const bgTo = hslToHex(baseHue + (6 + rng() * 12), 0.08 + rng() * 0.06, bgToL); + const tL = hslToHex( + baseHue + 10 + rng() * 6, + Math.min(0.85, satTerrain), + 0.36 + rng() * 0.08, + ); + const tM = hslToHex( + baseHue + (rng() * 6 - 3), + Math.min(0.72, satTerrain - 0.06), + 0.24 + rng() * 0.06, + ); + const tD = hslToHex( + baseHue + (rng() * 8 - 4), + Math.max(0.38, satBase - 0.18), + 0.1 + rng() * 0.04, + ); + const sd = parseInt(tD.slice(1, 3), 16); + const sg = parseInt(tD.slice(3, 5), 16); + const sb = parseInt(tD.slice(5, 7), 16); + const emojis = ["🌿", "🌲", "🌳", "🌺", "🪨", "🍄", "🌵"]; + const emoji = emojis[Math.floor(rng() * emojis.length)]; return { accent, accentDark, bgFrom, bgTo, - emoji: ARC_EMOJIS[Math.floor(rng() * ARC_EMOJIS.length)], + emoji, terrain: { l: tL, m: tM, d: tD, s: `rgba(${sd},${sg},${sb},0.6)` }, decos: DECO_SETS[Math.floor(rng() * DECO_SETS.length)], }; }; + const themeCache = new Map(); const getArcTheme = (arc: QuestArc): ArcTheme => { if (!themeCache.has(arc.id)) themeCache.set(arc.id, generateArcTheme(arc)); return themeCache.get(arc.id)!; }; -const REQ_ICON: Record = { +const REQ_EMOJI: Record = { questions: "❓", accuracy: "🎯", streak: "🔥", @@ -581,686 +616,6 @@ const REQ_LABEL: Record = { xp: "XP earned", leaderboard: "leaderboard rank", }; -const REQ_EMOJI: Record = { - questions: "❓", - accuracy: "🎯", - streak: "🔥", - sessions: "📚", - topics: "🗺️", - xp: "⚡", - leaderboard: "🏆", -}; - -const FOAM = Array.from({ length: 22 }, (_, i) => ({ - id: i, - w: 10 + ((i * 17 + 7) % 24), - top: `${3 + ((i * 13) % 88)}%`, - left: `${(i * 19 + 5) % 96}%`, - dur: `${4 + ((i * 7) % 7)}s`, - delay: `${(i * 3) % 5}s`, -})); -const completedCount = (arc: QuestArc) => - arc.nodes.filter((n) => n.status === "completed").length; -const truncate = (str: string | undefined, max = 14): string => { - if (!str) return ""; - return str.length > max ? str.slice(0, max - 1) + "…" : str; -}; - -// ─── Island Node ────────────────────────────────────────────────────────────── -// const IslandNode = ({ -// node, -// accent, -// terrain, -// decos, -// userXp, -// index, -// cx, -// cy, -// onTap, -// onClaim, -// }: { -// node: QuestNode; -// accent: string; -// terrain: ArcTheme["terrain"]; -// decos: ArcTheme["decos"]; -// userXp: number; -// index: number; -// cx: number; -// cy: number; -// onTap: (n: QuestNode) => void; -// onClaim: (n: QuestNode) => void; -// }) => { -// const status = node.status as "LOCKED" | "ACTIVE" | "CLAIMABLE" | "COMPLETED"; -// const isCompleted = status === "COMPLETED"; -// const isClaimable = status === "CLAIMABLE"; -// const isActive = status === "ACTIVE"; -// const isLocked = status === "LOCKED"; - -// const pct = Math.min( -// 100, -// Math.round((node.current_value / node.req_target) * 100), -// ); - -// const hiC = isLocked ? "#4b5563" : isCompleted ? "#6ee7b7" : terrain.l; -// const midC = isLocked ? "#374151" : isCompleted ? "#10b981" : terrain.m; -// const loC = isLocked ? "#1f2937" : isCompleted ? "#065f46" : terrain.d; -// const shdC = isLocked ? "rgba(0,0,0,0.5)" : terrain.s; - -// const gradId = `grad-${node.node_id}`; -// const shadowId = `shadow-${node.node_id}`; -// const glowId = `glow-${node.node_id}`; - -// // ✅ FIXED: Consistent transform string -// const islandTransform = `translate(${cx}, ${cy}) scale(${ISLAND_SCALE})`; -// const outlineTransform = `translate(${cx}, ${cy}) scale(${1.22 * ISLAND_SCALE})`; - -// const shapeIdx = index % SHAPES.length; -// const cardTop = cy + LAND_H + 18; -// const cardH = isClaimable ? CARD_H_CLAIMABLE : CARD_H; -// const statusCard = isClaimable -// ? "is-claimable" -// : isActive -// ? "is-active" -// : isLocked -// ? "is-locked" -// : "is-completed"; -// const nodeEmoji = REQ_EMOJI[node.req_type] ?? "🏝️"; -// const decoX = 22 * ISLAND_SCALE; - -// return ( -// !isLocked && onTap(node)} -// > -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// {/* Water reflection ellipse (scaled) */} -// -// -// -// - -// {/* ✅ FIXED: Drop shadow blob — translate to island centre, THEN scale */} -// -// -// - -// {/* ✅ FIXED: Active/claimable outline ring (scaled) */} -// {(isActive || isClaimable) && ( -// -// ", -// ` fill="none" stroke="${isClaimable ? "#fbbf24" : accent}" stroke-width="1.8" stroke-dasharray="6 4" opacity="0.6">`, -// ), -// }} -// /> -// -// )} - -// {/* ✅ FIXED: Main island body — translate to island centre, THEN scale */} -// -// ", ` fill="url(#${gradId})">`), -// }} -// /> -// - -// {/* Deco trees (scaled offset, scaled font) */} -// {!isLocked && ( -// <> -// -// {decos[0]} -// -// -// {decos[1]} -// -// -// )} - -// {/* Active flag (scaled translate + scale transform) */} -// {isActive && ( -// -// -// -// -// )} - -// {/* Claimable chest (scaled y position + font) */} -// {isClaimable && ( -// -// 📦 -// -// -// )} - -// {/* Lock icon (scaled font) */} -// {isLocked && ( -// -// 🔒 -// -// )} - -// {/* Node emoji (scaled font) */} -// {!isLocked && ( -// -// {nodeEmoji} -// -// )} - -// {/* Completed checkmark badge (scaled position + size) */} -// {isCompleted && ( -// -// -// -// ✓ -// -// -// )} - -// {/* Island name label (scaled y position + font size) */} -// -// {truncate(node.name).toUpperCase()} -// - -// {/* Info card — anchored below the scaled island */} -// e.stopPropagation()} -// > -//
!isLocked && onTap(node)} -// > -//
-//

{node.name ?? "—"}

-//
-// -// {node.reward_xp} XP -//
-//
-// {(isActive || isClaimable) && ( -// <> -//
-//
-//
-//

-// {REQ_ICON[node.req_type]} {node.current_value}/ -// {node.req_target}  -// {REQ_LABEL[node.req_type] ?? node.req_type} -//

-// -// )} -// {isLocked && ( -//

-// 🔒 {node.req_target} {REQ_LABEL[node.req_type] ?? node.req_type}{" "} -// to unlock -//

-// )} -// {isCompleted && ( -//

-// ✅ Conquered! -//

-// )} -// {isClaimable && ( -// -// )} -//
-// -// -// ); -// }; - -const IslandNode = ({ - node, - accent, - terrain, - decos, - userXp, - index, - cx, - cy, - onTap, - onClaim, -}) => { - const status = node.status as "LOCKED" | "ACTIVE" | "CLAIMABLE" | "COMPLETED"; - const isCompleted = status === "COMPLETED"; - const isClaimable = status === "CLAIMABLE"; - const isActive = status === "ACTIVE"; - const isLocked = status === "LOCKED"; - - const pct = Math.min( - 100, - Math.round((node.current_value / node.req_target) * 100), - ); - const nodeEmoji = REQ_EMOJI[node.req_type] ?? "🏝️"; - const decoX = 22 * ISLAND_SCALE; - const cardTop = cy + LAND_H + 18; - const cardH = isClaimable ? CARD_H_CLAIMABLE : CARD_H; - - // 🎨 PROFESSIONAL COLOR SYSTEM - FIXED - const theme = { - locked: { - main: "#2d3748", - highlight: "#4a5568", - shadow: "#1a202c", - glow: "#718096", - }, - active: { - main: terrain?.l || "#4299e1", - highlight: "#63b3ed", - shadow: "#3182ce", - glow: accent || "#bee3f8", - }, - claimable: { - main: "#d69e2e", - highlight: "#f6ad55", - shadow: "#b7791f", - glow: "#fbbf24", - }, - completed: { - main: "#38a169", - highlight: "#51cf66", - shadow: "#2f855a", - glow: "#9ae6b4", - }, - }; - - const currentTheme = isClaimable - ? theme.claimable - : isCompleted - ? theme.completed - : isActive - ? theme.active - : theme.locked; - - const shadowColor = isLocked ? "rgba(0,0,0,0.6)" : "rgba(0,0,0,0.4)"; - const glowColor = isClaimable ? "#fbbf24" : isActive ? accent : "transparent"; - - return ( - !isLocked && onTap(node)} - > - {/* ✨ Animated water reflection */} - - - - - - {/* 🎨 Multi-layered professional shadow */} - - - {/* 🏝️ Main island - Sophisticated gradient simulation */} - - - {/* ✨ Premium outline ring */} - {(isActive || isClaimable) && ( - - )} - - {/* Rest of component unchanged - trees, status icons, cards... */} - {!isLocked && ( - <> - - {decos[0]} - - - {decos[1]} - - - )} - - {/* Status emoji/text - copy rest from previous working version */} - - {isLocked ? "🔒" : nodeEmoji} - - - {/* Info card - copy from previous */} - e.stopPropagation()} - > -
!isLocked && onTap(node)} - > -
-

{node.name ?? "—"}

-
- - {node.reward_xp} XP -
-
- {/* Add progress bars, buttons from previous version */} -
-
-
- ); -}; - -// ─── Route path ─────────────────────────────────────────────────────────────── -const RoutePath = ({ - x1, - y1, - x2, - y2, - done, - accent, - showShip, -}: { - x1: number; - y1: number; - x2: number; - y2: number; - done: boolean; - accent: string; - showShip: boolean; -}) => { - const mx = (x1 + x2) / 2, - my = (y1 + y2) / 2, - dx = x2 - x1, - dy = y2 - y1, - len = Math.sqrt(dx * dx + dy * dy) || 1, - perp = 55, - side = x1 < x2 ? 1 : -1; - const cpx = mx - (dy / len) * perp * side, - cpy = my + (dx / len) * perp * side, - path = `M ${x1} ${y1} Q ${cpx} ${cpy} ${x2} ${y2}`; - const shipX = 0.25 * x1 + 0.5 * cpx + 0.25 * x2, - shipY = 0.25 * y1 + 0.5 * cpy + 0.25 * y2; - return ( - - - - {[0.25, 0.5, 0.75].map((t, ti) => { - const ex = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * cpx + t * t * x2, - ey = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * cpy + t * t * y2; - return ( - - ); - })} - {showShip && ( - - ⛵ - - - - )} - - ); -}; // ─── Crew ranks ─────────────────────────────────────────────────────────────── const CREW_RANKS = [ @@ -1271,6 +626,7 @@ const CREW_RANKS = [ { id: "emperor", label: "Emperor", emoji: "👑", xpRequired: 6000 }, { id: "pirate_king", label: "Pirate King", emoji: "☠️", xpRequired: 10000 }, ]; + const SEG_W = 72, EDGE_W = 40; const nodeX = (i: number, total: number) => { @@ -1279,6 +635,538 @@ const nodeX = (i: number, total: number) => { return EDGE_W + SEG_W * (i - 1) + SEG_W / 2; }; +// ─── 3D Route Paths ─────────────────────────────────────────────────────────── +// Proper Three.js sea routes — live in world space, move with camera pan/zoom/rotate. +// Each segment is a CatmullRomCurve3 that bows sideways over the water. +// Dashes scroll via LineDashedMaterial dashOffset animated in useFrame. +// Active legs get a glow tube + a ship that physically travels the curve. + +interface RouteSegmentProps { + from: THREE.Vector3; + to: THREE.Vector3; + idx: number; + isDone: boolean; + isActive: boolean; + isNext: boolean; + accent: string; +} + +const RouteSegment = ({ + from, + to, + idx, + isDone, + isActive, + isNext, + accent, +}: RouteSegmentProps) => { + const lineRef = useRef(null!); + const glowRef = useRef(null!); + const shipRef = useRef(null!); + const shipT = useRef(0); + + // CatmullRom curve bowing sideways — alternate direction per segment + const curve = useMemo(() => { + const side = idx % 2 === 0 ? 1 : -1; + const dx = to.x - from.x; + const dz = to.z - from.z; + const mid = new THREE.Vector3( + (from.x + to.x) / 2 + -dz * 0.28 * side, + -0.65, // hover just above water + (from.z + to.z) / 2 + dx * 0.28 * side, + ); + const y = -0.72; + const p0 = new THREE.Vector3(from.x, y, from.z); + const p1 = new THREE.Vector3(to.x, y, to.z); + return new THREE.CatmullRomCurve3([p0, mid, p1], false, "catmullrom", 0.5); + }, [from, to, idx]); + + // Geometry for the dashed line (needs computeLineDistances) + const lineGeo = useMemo(() => { + const pts = curve.getPoints(80); + return new THREE.BufferGeometry().setFromPoints(pts); + }, [curve]); + + // Tube geometry for the soft glow underlayer + const glowGeo = useMemo( + () => new THREE.TubeGeometry(curve, 40, 0.055, 5, false), + [curve], + ); + + const color = new THREE.Color( + isDone || isNext ? accent : isActive ? "#94d8ff" : "#ffffff", + ); + const opacity = isDone || isNext ? 0.8 : isActive ? 0.85 : 0.15; + const glowOpacity = isDone || isNext ? 0.2 : isActive ? 0.25 : 0; + const dashSpeed = isDone || isNext ? 0.55 : isActive ? 1.05 : 0; + + useFrame((_, dt) => { + // Scroll dashes forward along the route + if (lineRef.current) { + const mat = lineRef.current.material as THREE.LineDashedMaterial; + if (dashSpeed > 0) mat.dashOffset -= dt * dashSpeed; + } + // Pulse glow on active segments + if (glowRef.current && (isActive || isNext)) { + const mat = glowRef.current.material as THREE.MeshStandardMaterial; + mat.emissiveIntensity = 0.28 + Math.sin(Date.now() * 0.0018) * 0.12; + } + // Travel ship along active curve + if (shipRef.current && isActive && !isDone) { + shipT.current = (shipT.current + dt * 0.11) % 1; + const pt = curve.getPoint(shipT.current); + const tan = curve.getTangent(shipT.current); + shipRef.current.position.copy(pt); + shipRef.current.position.y = -0.58; // float just above water surface + shipRef.current.rotation.y = Math.atan2(tan.x, tan.z); + } + }); + + return ( + + {/* Glow underlayer tube */} + {(isDone || isNext || isActive) && ( + + + + )} + + {/* Dashed route line */} + self.computeLineDistances()} + > + + + + {/* Travelling ship on the active leg */} + {isActive && !isDone && ( + + + + ⛵ + + + {/* Gold wake glow disk */} + + + + + + )} + + ); +}; + +// ── RoutePaths3D — all route segments, lives inside ───────────────── +interface RoutePaths3DProps { + nodes: QuestNode[]; + positions: { x: number; y: number }[]; + nodeCount: number; + accent: string; +} + +const RoutePaths3D = ({ + nodes, + positions, + nodeCount, + accent, +}: RoutePaths3DProps) => { + // Mirror the exact same 2D→3D coordinate mapping used in IslandScene + const pos3d = useMemo( + () => + positions.map( + (p, i) => + new THREE.Vector3( + (p.x / VW) * 9 - 4.5, + 0, + (i / Math.max(nodeCount - 1, 1)) * (nodeCount * 2.4), + ), + ), + [positions, nodeCount], + ); + + return ( + <> + {nodes.slice(0, -1).map((node, i) => { + const from = pos3d[i]; + const to = pos3d[i + 1]; + if (!from || !to) return null; + const nextNode = nodes[i + 1]; + const isDone = node.status === "completed"; + const isActive = + node.status === "ACTIVE" || node.status === "CLAIMABLE"; + const isNext = + isDone && + (nextNode?.status === "ACTIVE" || nextNode?.status === "CLAIMABLE"); + return ( + + ); + })} + + ); +}; + +// ─── Map geometry ───────────────────────────────────────────────────────────── +// Camera constants +const POLAR_FIXED = Math.PI / 2 - (25 * Math.PI) / 180; +const DIST_DEFAULT = 7; +const DIST_MIN = 3.5; +const DIST_MAX = 14; +const PAN_RADIUS = 8; + +// ── WaterPlane ──────────────────────────────────────────────────────────────── +const WaterPlane = () => { + const meshRef = useRef(null!); + useFrame(({ clock }) => { + const geo = meshRef.current?.geometry as THREE.PlaneGeometry; + if (!geo) return; + const pos = geo.attributes.position; + const t = clock.elapsedTime; + for (let i = 0; i < pos.count; i++) { + const x = pos.getX(i); + const z = pos.getZ(i); + pos.setY( + i, + Math.sin(x * 0.6 + t * 0.7) * 0.04 + Math.cos(z * 0.5 + t * 0.5) * 0.03, + ); + } + pos.needsUpdate = true; + geo.computeVertexNormals(); + }); + return ( + + + + + ); +}; + +// ── PanClamp ────────────────────────────────────────────────────────────────── +const PanClamp = () => { + const controls = useThree((s) => s.controls) as any; + useFrame(() => { + if (!controls?.target) return; + const t = controls.target as THREE.Vector3; + const dx = t.x, + dz = t.z; + const d = Math.sqrt(dx * dx + dz * dz); + if (d > PAN_RADIUS) { + const sc = PAN_RADIUS / d; + t.x *= sc; + t.z *= sc; + } + t.y = THREE.MathUtils.clamp(t.y, -1, 2); + }); + return null; +}; + +// ── CameraDistSync ───────────────────────────────────────────────────────────── +const CameraDistSync = ({ dist }: { dist: number }) => { + const { camera, controls } = useThree() as any; + useEffect(() => { + if (!camera) return; + const target = controls?.target ?? new THREE.Vector3(0, 0, 0); + const dir = camera.position.clone().sub(target).normalize(); + camera.position.copy(target).addScaledVector(dir, dist); + }, [camera, controls, dist]); + return null; +}; + +// ── IslandScene ─────────────────────────────────────────────────────────────── +interface MapContentProps { + arc: QuestArc; + theme: ArcTheme; + positions: { x: number; y: number }[]; + onNodeTap: (node: QuestNode) => void; + onClaim: (node: QuestNode) => void; + initialTarget: [number, number, number]; + modalOpen: boolean; +} + +const IslandScene = ({ + arc, + theme, + positions, + onNodeTap, + onClaim, + initialTarget, + modalOpen, +}: MapContentProps) => { + const nodeCount = arc.nodes.length; + const tropicalTerrain = { + l: "#3ecf6a", + m: "#1fa84a", + d: "#157a36", + s: "#03045e", + }; + const sorted = useMemo( + () => [...arc.nodes].sort((a, b) => a.sequence_order - b.sequence_order), + [arc.nodes], + ); + + return ( + <> + + + + + + + {/* 3D sea routes — live in world space, respect camera */} + + + {sorted.map((node, i) => { + const centre = positions[i] ?? { x: VW / 2, y: TOP_PAD + i * ROW_H }; + const x3 = (centre.x / VW) * 9 - 4.5; + const z3 = (i / Math.max(nodeCount - 1, 1)) * (nodeCount * 2.4); + return ( + + ); + })} + + + + + + + + + + + + ); +}; + +// ── Zoom button ─────────────────────────────────────────────────────────────── +const zoomBtnStyle: React.CSSProperties = { + width: 30, + height: 30, + borderRadius: "50%", + background: "rgba(255,255,255,0.08)", + border: "1px solid rgba(255,255,255,0.18)", + color: "rgba(255,255,255,0.75)", + fontSize: "1.15rem", + lineHeight: "1", + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + fontFamily: "monospace", + transition: "background 0.15s, transform 0.1s", + userSelect: "none", + padding: 0, +}; + +// ── MapContent ──────────────────────────────────────────────────────────────── +const MapContent = ({ + arc, + theme, + positions, + onNodeTap, + onClaim, + initialTarget, + modalOpen, +}: MapContentProps) => { + const nodeCount = arc.nodes.length; + const isMobile = window.innerWidth < 768; + const offset = isMobile ? 150 : 0; + const canvasH = Math.max(window.innerHeight - offset, nodeCount * 160); + const [dist, setDist] = useState(DIST_DEFAULT); + + return ( +
+ {/* Zoom buttons */} +
+ +
+
+
+ +
+ + {}} + > + + + + +
+ ); +}; + // ─── Left Panel ─────────────────────────────────────────────────────────────── const LeftPanel = ({ arcs, @@ -1301,7 +1189,10 @@ const LeftPanel = ({ const earnedXP = user?.total_xp ?? 0, streak = user?.streak ?? user?.current_streak ?? 0, level = user?.current_level ?? 1; - const totalDone = arcs.reduce((s, a) => s + completedCount(a), 0); + const totalDone = arcs.reduce( + (s, a) => s + a.nodes.filter((n) => n.status === "completed").length, + 0, + ); const totalNodes = arcs.reduce((s, a) => s + a.nodes.length, 0); const claimable = arcs.reduce( (s, a) => s + a.nodes.filter((n) => n.status === "CLAIMABLE").length, @@ -1345,12 +1236,14 @@ const LeftPanel = ({ const totalW = EDGE_W + SEG_W * (N - 2) + EDGE_W; const [animated, setAnimated] = useState(false); const ladderRef = useRef(null); + useEffect(() => { const id = requestAnimationFrame(() => requestAnimationFrame(() => setAnimated(true)), ); return () => cancelAnimationFrame(id); }, []); + useEffect(() => { if (!ladderRef.current) return; ladderRef.current.scrollTo({ @@ -1358,6 +1251,7 @@ const LeftPanel = ({ behavior: "smooth", }); }, [shipXPos]); + const rankPct = Math.round(progToNext * 100); return ( @@ -1519,7 +1413,7 @@ const LeftPanel = ({
{sortedArcs.map((a) => { const t = getArcTheme(a); - const done = completedCount(a); + const done = a.nodes.filter((n) => n.status === "completed").length; const pct = Math.round((done / Math.max(a.nodes.length, 1)) * 100); const hasClaim = a.nodes.some((n) => n.status === "CLAIMABLE"); return ( @@ -1551,176 +1445,6 @@ const LeftPanel = ({ ); }; -// ─── Map content ────────────────────────────────────────────────────────────── -const MapContent = ({ - arc, - theme, - - sorted, - totalSvgH, - userXp, - onTap, - onClaim, -}: { - arc: QuestArc; - theme: ArcTheme; - - sorted: QuestNode[]; - totalSvgH: number; - userXp: number; - onTap: (n: QuestNode) => void; - onClaim: (n: QuestNode) => void; -}) => { - const done = completedCount(arc); - const pct = Math.round((done / arc.nodes.length) * 100); - const centres = arc.nodes.map((node, i) => { - console.log(`Node ${i}:`, { - id: node.node_id, - cx: islandCX(i, arc.id), - cy: islandCY(i), - }); - return { - x: islandCX(i, arc.id), - y: islandCY(i), - }; - }); - return ( -
-
- {FOAM.map((b) => ( -
- ))} -
-
-
{theme.emoji}
-

{arc.name}

-

{arc.description}

-
-
-
-
- - {done}/{arc.nodes.length} islands - -
-
- - {/* - SVG: explicit pixel width = VW, explicit height = totalSvgH. - The viewBox matches exactly. CSS class pins it to VW px. - Never allow CSS to override the width to something different. - */} - - {sorted.map((node, i) => { - if (i >= sorted.length - 1) return null; - const c1 = centres[i], - c2 = centres[i + 1]; - const ship = - node.status === "completed" && sorted[i + 1]?.status === "active"; - return ( - - ); - })} - {sorted.map((node, i) => ( - - ))} - {done === sorted.length && ( - - - - - ARC - - - COMPLETE - - - ⚓ - - - )} - -
-
- ); -}; - // ─── Main ───────────────────────────────────────────────────────────────────── export const QuestMap = () => { const arcs = useQuestStore((s) => s.arcs); @@ -1730,7 +1454,6 @@ export const QuestMap = () => { const syncFromAPI = useQuestStore((s) => s.syncFromAPI); const user = useAuthStore((s) => s.user); const token = useAuthStore((s) => s.token); - const userXp = user?.total_xp ?? 0; const [loading, setLoading] = useState(true); const [fetchError, setFetchError] = useState(null); @@ -1741,12 +1464,13 @@ export const QuestMap = () => { const [claimLoading, setClaimLoading] = useState(false); const [claimError, setClaimError] = useState(null); const [selectedNode, setSelectedNode] = useState(null); + const mobileScrollRef = useRef(null); const desktopScrollRef = useRef(null); useEffect(() => { - if (arcs.length && !activeArcId) setActiveArc(arcs[0].id); - }, [arcs]); + if (arcs.length > 0 && !activeArcId) setActiveArc(arcs[0].id); + }, [arcs, activeArcId, setActiveArc]); useEffect(() => { if (!token) return; @@ -1770,10 +1494,6 @@ export const QuestMap = () => { cancelled = true; }; }, [token, syncFromAPI]); - console.log(arcs, activeArcId); - const arc = arcs.find((a) => a.id === activeArcId) ?? arcs[0]; - - const theme = arc ? getArcTheme(arc) : null; const handleClaim = useCallback( async (node: QuestNode) => { @@ -1794,8 +1514,14 @@ export const QuestMap = () => { [token], ); + const handleNodeTap = useCallback((node: QuestNode) => { + setSelectedNode(node); + }, []); + + const arc = arcs.find((a) => a.id === activeArcId) ?? arcs[0]; + const handleChestClose = useCallback(() => { - if (!claimingNode) return; + if (!claimingNode || !arc) return; const titles = Array.isArray(claimResult?.title_unlocked) ? claimResult!.title_unlocked : claimResult?.title_unlocked @@ -1805,14 +1531,15 @@ export const QuestMap = () => { arc.id, claimingNode.node_id, claimResult?.xp_awarded ?? 0, - titles.map((t) => t.name), + titles.map((t: any) => t.name), ); setClaimingNode(null); setClaimResult(null); setClaimError(null); }, [claimingNode, claimResult, arc, claimNode]); - if (loading) + // ── Early returns ───────────────────────────────────────────────────────── + if (loading) { return (
{
); + } - if (fetchError || !arc) + if (fetchError || !arc) { return (
{
); + } + const theme = getArcTheme(arc); const sorted = [...arc.nodes].sort( (a, b) => a.sequence_order - b.sequence_order, ); - const centres = sorted.map((_, i) => ({ - x: islandCX(i, arc.id), - y: islandCY(i), - })); - const totalSvgH = svgHeight(sorted.length); + + // ── Generate random positions (deterministic per arc) ───────────────────── + const positions = generateIslandPositions(sorted.length, arc.id); + + const sortedArcs = [...arcs].sort( + (a, b) => a.sequence_order - b.sequence_order, + ); + + // Focus camera on first active/claimable island + const focusIdx = (() => { + const i = sorted.findIndex( + (n) => n.status === "ACTIVE" || n.status === "CLAIMABLE", + ); + return i >= 0 ? i : 0; + })(); + const nodeCount = sorted.length; + const focusX3 = ((positions[focusIdx]?.x ?? VW / 2) / VW) * 9 - 4.5; + const focusZ3 = (focusIdx / Math.max(nodeCount - 1, 1)) * (nodeCount * 2.4); + const initialTarget: [number, number, number] = [focusX3, 0, focusZ3]; + + const modalOpen = !!(selectedNode || claimingNode); return (
@@ -1915,44 +1661,43 @@ export const QuestMap = () => {
- {[...arcs] - .sort((a, b) => a.sequence_order - b.sequence_order) - .map((a) => { - const t = getArcTheme(a); - return ( - - ); - })} + {sortedArcs.map((a) => { + const t = getArcTheme(a); + return ( + + ); + })}
+
+
@@ -1973,41 +1718,38 @@ export const QuestMap = () => { />
- {[...arcs] - .sort((a, b) => a.sequence_order - b.sequence_order) - .map((a) => { - const t = getArcTheme(a); - return ( - - ); - })} + {sortedArcs.map((a) => { + const t = getArcTheme(a); + return ( + + ); + })}
@@ -2016,8 +1758,8 @@ export const QuestMap = () => { setSelectedNode(null)} @@ -2027,6 +1769,7 @@ export const QuestMap = () => { }} /> )} + {claimingNode && ( { onClose={handleChestClose} /> )} + {claimError && (