commit b403672264aada25561718e6fb3b7138cec26de5 Author: Rocky Date: Thu Nov 27 18:37:31 2025 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d71c8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +.env + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..46358ce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,407 @@ +{ + "name": "WebCity", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@react-three/postprocessing": "^3.0.4", + "postprocessing": "^6.38.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@react-three/fiber": { + "version": "9.4.0", + "resolved": "https://registry.npmmirror.com/@react-three/fiber/-/fiber-9.4.0.tgz", + "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.32.0", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.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.0.0", + "react-dom": "^19.0.0", + "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/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/react": { + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.6.tgz", + "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.32.3", + "resolved": "https://registry.npmmirror.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz", + "integrity": "sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/three": { + "version": "0.181.0", + "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.181.0.tgz", + "integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT", + "peer": true + }, + "node_modules/@webgpu/types": { + "version": "0.1.66", + "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.66.tgz", + "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/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", + "peer": true + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/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", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT", + "peer": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/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", + "peer": true + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmmirror.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT", + "peer": true + }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/postprocessing": { + "version": "6.38.0", + "resolved": "https://registry.npmmirror.com/postprocessing/-/postprocessing-6.38.0.tgz", + "integrity": "sha512-tisx8XN/PWTL3uXz2mt8bjlMS1wiOUSCK3ixi4zjwUCFmP8XW8hNhXwrxwd2zf2VmCyCQ3GUaLm7GLnkkBbDsQ==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.157.0 < 0.182.0" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT", + "peer": true + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/three": { + "version": "0.181.2", + "resolved": "https://registry.npmmirror.com/three/-/three-0.181.2.tgz", + "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "license": "MIT", + "peer": true + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9bcb021 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@react-three/postprocessing": "^3.0.4", + "postprocessing": "^6.38.0" + } +} diff --git a/voxel-tactics-horizon/.gitignore b/voxel-tactics-horizon/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/voxel-tactics-horizon/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/voxel-tactics-horizon/README.md b/voxel-tactics-horizon/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/voxel-tactics-horizon/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/voxel-tactics-horizon/eslint.config.js b/voxel-tactics-horizon/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/voxel-tactics-horizon/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/voxel-tactics-horizon/index.html b/voxel-tactics-horizon/index.html new file mode 100644 index 0000000..daa7231 --- /dev/null +++ b/voxel-tactics-horizon/index.html @@ -0,0 +1,13 @@ + + + + + + + voxel-tactics-horizon + + +
+ + + diff --git a/voxel-tactics-horizon/package-lock.json b/voxel-tactics-horizon/package-lock.json new file mode 100644 index 0000000..b414371 --- /dev/null +++ b/voxel-tactics-horizon/package-lock.json @@ -0,0 +1,5183 @@ +{ + "name": "voxel-tactics-horizon", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "voxel-tactics-horizon", + "version": "0.0.0", + "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.4.0", + "@react-three/postprocessing": "^3.0.4", + "@types/three": "^0.181.0", + "@types/uuid": "^10.0.0", + "clsx": "^2.1.1", + "postprocessing": "^6.38.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "simplex-noise": "^4.0.3", + "tailwind-merge": "^3.4.0", + "three": "^0.181.2", + "uuid": "^13.0.0", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmmirror.com/@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.2.0", + "resolved": "https://registry.npmmirror.com/@monogrid/gainmap-js/-/gainmap-js-3.2.0.tgz", + "integrity": "sha512-E/DVmj5tbVEXUlnUWJ+k4BK/dEtimZC4RhxnIDkyJgJsrHkaXSSb9FKtEuvCciTY6Rr8weaCS/Suv3UVa2mFAA==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmmirror.com/@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.4.0", + "resolved": "https://registry.npmmirror.com/@react-three/fiber/-/fiber-9.4.0.tgz", + "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.32.0", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.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.0.0", + "react-dom": "^19.0.0", + "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/@react-three/fiber/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmmirror.com/@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", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmmirror.com/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmmirror.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.6", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.6.tgz", + "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.32.3", + "resolved": "https://registry.npmmirror.com/@types/react-reconciler/-/react-reconciler-0.32.3.tgz", + "integrity": "sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.181.0", + "resolved": "https://registry.npmmirror.com/@types/three/-/three-0.181.0.tgz", + "integrity": "sha512-MLF1ks8yRM2k71D7RprFpDb9DOX0p22DbdPqT/uAkc6AtQXjxWCVDjCy23G9t1o8HcQPk7woD2NIyiaWcWPYmA==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.47.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.66", + "resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.66.tgz", + "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/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.8.29", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", + "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/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.30001756", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmmirror.com/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.258", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz", + "integrity": "sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmmirror.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmmirror.com/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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/n8ao": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/n8ao/-/n8ao-1.10.1.tgz", + "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postprocessing": { + "version": "6.38.0", + "resolved": "https://registry.npmmirror.com/postprocessing/-/postprocessing-6.38.0.tgz", + "integrity": "sha512-tisx8XN/PWTL3uXz2mt8bjlMS1wiOUSCK3ixi4zjwUCFmP8XW8hNhXwrxwd2zf2VmCyCQ3GUaLm7GLnkkBbDsQ==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.157.0 < 0.182.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/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/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "19.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/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/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/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": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simplex-noise": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/simplex-noise/-/simplex-noise-4.0.3.tgz", + "integrity": "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/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.0", + "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/three": { + "version": "0.181.2", + "resolved": "https://registry.npmmirror.com/three/-/three-0.181.2.tgz", + "integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.1.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.47.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.47.0.tgz", + "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.2.4", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.2.4.tgz", + "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/voxel-tactics-horizon/package.json b/voxel-tactics-horizon/package.json new file mode 100644 index 0000000..c3f5558 --- /dev/null +++ b/voxel-tactics-horizon/package.json @@ -0,0 +1,45 @@ +{ + "name": "voxel-tactics-horizon", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.4.0", + "@react-three/postprocessing": "^3.0.4", + "@types/three": "^0.181.0", + "@types/uuid": "^10.0.0", + "clsx": "^2.1.1", + "postprocessing": "^6.38.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "simplex-noise": "^4.0.3", + "tailwind-merge": "^3.4.0", + "three": "^0.181.2", + "uuid": "^13.0.0", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/voxel-tactics-horizon/postcss.config.js b/voxel-tactics-horizon/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/voxel-tactics-horizon/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/voxel-tactics-horizon/public/vite.svg b/voxel-tactics-horizon/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/voxel-tactics-horizon/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/voxel-tactics-horizon/src/App.css b/voxel-tactics-horizon/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/voxel-tactics-horizon/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/voxel-tactics-horizon/src/App.tsx b/voxel-tactics-horizon/src/App.tsx new file mode 100644 index 0000000..51789ce --- /dev/null +++ b/voxel-tactics-horizon/src/App.tsx @@ -0,0 +1,280 @@ +import { useEffect, Suspense, useState, useMemo, useRef } from 'react'; +import { Canvas } from '@react-three/fiber'; +import { ACESFilmicToneMapping, PCFShadowMap } from 'three'; +// Post-processing removed for now to debug and simplify +// import { EffectComposer, TiltShift2, Vignette } from '@react-three/postprocessing'; +import { ChunkRenderer } from './features/Map/components/ChunkRenderer'; +import { CameraRig } from './core/CameraRig'; +import { MinecraftSky, getLightingConfig, type SkyPreset } from './core/MinecraftSky'; +import { useMapStore } from './features/Map/store'; +import { MAP_SIZES, TERRAIN_VERSION } from './features/Map/logic/terrain'; +import { getAllSceneTypes, type SceneType, SCENE_CONFIGS } from './features/Map/logic/scenes'; +// 临时注释:专注场景设计和地形生成算法优化 +// import { UnitRenderer } from './features/Units/components/UnitRenderer'; +// import { SelectionCursor } from './features/Map/components/Cursor'; +// import { MoveOverlay } from './features/Map/components/MoveOverlay'; +import { TerrainGenerationProgress } from './features/Map/components/TerrainGenerationProgress'; +// import { useUnitStore } from './features/Units/store'; +// import { useBattleStore } from './features/Battle/store'; + +function GameScene({ skyPreset }: { skyPreset: SkyPreset }) { + const generateMap = useMapStore((state) => state.generateMap); + const terrainVersion = useMapStore((state) => state.terrainVersion); + // 临时注释:战斗系统相关 + // const addUnit = useUnitStore((state) => state.addUnit); + // const startBattle = useBattleStore((state) => state.startBattle); + // const moveActiveUnit = useBattleStore((state) => state.moveActiveUnit); + // const phase = useBattleStore((state) => state.phase); + + const lighting = useMemo(() => getLightingConfig(skyPreset), [skyPreset]); + + // 使用 ref 跟踪上一次的版本号 + const prevVersionRef = useRef(TERRAIN_VERSION); + + // 初始化地图 - 固定为沙漠场景方便调试 + useEffect(() => { + generateMap('small', undefined, 'desert'); + + // 临时注释:角色生成和战斗初始化 + // setTimeout(() => { + // const map = useMapStore.getState(); + // addUnit({ + // name: 'Ramza', + // job: 'Squire', + // team: 'player', + // position: { x: 5, y: map.getHeightAt(5,5) - 1, z: 5 }, + // facing: 2, + // maxHp: 100, + // stats: { move: 4, jump: 2, speed: 10, attack: 8, defense: 6 } + // }); + // addUnit({ + // name: 'Goblin', + // job: 'Archer', + // team: 'enemy', + // position: { x: 8, y: map.getHeightAt(8,8) - 1, z: 8 }, + // facing: 3, + // maxHp: 50, + // stats: { move: 3, jump: 3, speed: 11, attack: 9, defense: 4 } + // }); + // startBattle(); + // }, 500); + }, [generateMap]); + + // 监听地形版本变化并自动重新生成 + useEffect(() => { + if (prevVersionRef.current !== TERRAIN_VERSION && terrainVersion !== TERRAIN_VERSION) { + console.log(`[HMR] Terrain version changed: ${prevVersionRef.current} -> ${TERRAIN_VERSION}, regenerating map...`); + prevVersionRef.current = TERRAIN_VERSION; + generateMap('small'); + } + }, [TERRAIN_VERSION, terrainVersion, generateMap]); + + // 临时注释:体素点击和移动逻辑 + // const onVoxelClick = (x: number, z: number) => { + // if (phase === 'PLAYER_TURN') { + // moveActiveUnit({ x, z }); + // } + // }; + + return ( + <> + {/* 天空盒 - 必须在最前面渲染 */} + + + {/* 补充光照系统 */} + + + + + + {/* 补充光源:从另一侧添加柔和的光照 */} + + + {/* 游戏对象 */} + + {/* 临时注释:战斗和角色渲染 */} + {/* */} + {/* */} + {/* */} + + {/* 相机控制 */} + + + ); +} + +function UIOverlay({ skyPreset, setSkyPreset }: { + skyPreset: SkyPreset; + setSkyPreset: (preset: SkyPreset) => void; +}) { + const generateMap = useMapStore(s => s.generateMap); + const currentScene = useMapStore(s => s.currentScene); + + // 临时注释:战斗系统UI状态 + // const phase = useBattleStore(s => s.phase); + // const activeUnitId = useBattleStore(s => s.activeUnitId); + // const endTurn = useBattleStore(s => s.endTurn); + // const activeUnit = useUnitStore(s => s.units.find(u => u.id === activeUnitId)); + + // 地图生成控制状态 + const [mapSize, setMapSize] = useState('small'); + const [selectedScene, setSelectedScene] = useState(currentScene); + const allScenes = useMemo(() => getAllSceneTypes(), []); + + const handleGenerateMap = () => { + generateMap(mapSize, undefined, selectedScene); + }; + + return ( +
+
+

Voxel Tactics: Horizon

+ + {/* 地图生成控制 */} +
+ + + {/* 地图大小选择 */} +
+ + +
+ + {/* 场景选择 */} +
+ + +
+ + {/* 生成按钮 */} + +
+ + {/* 天气选择器 */} +
+ + +
+ + {/* 临时注释:战斗UI面板 */} + {/*
+
+ PHASE: {phase} + Turn Order +
+ + {activeUnit ? ( +
+
{activeUnit.name} ({activeUnit.job})
+
+
+
+
+
MOV: {activeUnit.stats.move}
+
JMP: {activeUnit.stats.jump}
+
SPD: {activeUnit.stats.speed}
+
ATK: {activeUnit.stats.attack}
+
+ + {phase === 'PLAYER_TURN' && ( +
+ + +
+ )} +
+ ) : ( +
Calculating CT...
+ )} +
*/} + +
+

Controls:

+

WASD / Left Drag: Move Camera

+

Arrow Up/Down: Zoom

+

Arrow Left/Right: Adjust Pitch Angle

+

Space: Reset Camera

+

Q/E: Rotate 90°

+
+
+
+ ); +} + +export default function App() { + const [skyPreset, setSkyPreset] = useState('day'); + + return ( +
+ + + + + + + +
+ ); +} diff --git a/voxel-tactics-horizon/src/assets/react.svg b/voxel-tactics-horizon/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/voxel-tactics-horizon/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/voxel-tactics-horizon/src/components/LoadingProgress.tsx b/voxel-tactics-horizon/src/components/LoadingProgress.tsx new file mode 100644 index 0000000..08765b5 --- /dev/null +++ b/voxel-tactics-horizon/src/components/LoadingProgress.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; + +interface LoadingProgressProps { + isLoading: boolean; + progress: number; + message?: string; +} + +export const LoadingProgress: React.FC = ({ + isLoading, + progress, + message = 'Generating terrain...' +}) => { + const [displayProgress, setDisplayProgress] = useState(0); + + useEffect(() => { + // 平滑进度条动画 + const interval = setInterval(() => { + setDisplayProgress(prev => { + if (prev < progress) { + return Math.min(prev + 2, progress); + } + return prev; + }); + }, 16); // ~60fps + + return () => clearInterval(interval); + }, [progress]); + + if (!isLoading && displayProgress >= 100) return null; + + return ( +
+
+

+ 🌍 {message} +

+ +
+
+
+
+
+ +
+ {Math.round(displayProgress)}% Complete +
+ +
+ Generating voxels, placing decorations, optimizing... +
+
+
+ ); +}; + diff --git a/voxel-tactics-horizon/src/core/CameraRig.tsx b/voxel-tactics-horizon/src/core/CameraRig.tsx new file mode 100644 index 0000000..5a2b826 --- /dev/null +++ b/voxel-tactics-horizon/src/core/CameraRig.tsx @@ -0,0 +1,215 @@ +import { useEffect, useRef } from 'react'; +import { useThree, useFrame } from '@react-three/fiber'; +import { Vector3, MathUtils } from 'three'; +import { useMapStore } from '../features/Map/store'; + +const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val)); +const lerpScalar = (current: number, target: number, delta: number, speed = 6) => + MathUtils.lerp(current, target, Math.min(1, delta * speed)); + +export const CameraRig: React.FC = () => { + const { camera, gl } = useThree(); + const mapSize = useMapStore((state) => state.size); + + const baseAngle = Math.PI / 4; + const angleIndexRef = useRef(0); + + // 俯仰角控制(pitch angle) + const pitchAngleRef = useRef(0.6); // 默认俯仰角(弧度) + const desiredPitchAngleRef = useRef(0.6); + + const targetRef = useRef(new Vector3(mapSize / 2, 0, mapSize / 2)); + const desiredTargetRef = useRef(targetRef.current.clone()); + + const zoomRef = useRef(22); + const desiredZoomRef = useRef(22); + + const isDragging = useRef(false); + const lastMousePos = useRef({ x: 0, y: 0 }); + + const movementRef = useRef({ + forward: false, + back: false, + left: false, + right: false, + }); + + useEffect(() => { + const canvas = gl.domElement; + + // 确保 canvas 获得焦点 + const ensureFocus = () => { + if (document.activeElement !== canvas) { + canvas.focus(); + } + }; + + // 页面加载时立即获取焦点 + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + desiredZoomRef.current = clamp( + desiredZoomRef.current + e.deltaY * 0.01, + 8, + 45 + ); + }; + + const handleMouseDown = (e: MouseEvent) => { + if (e.button !== 0) return; + ensureFocus(); // 点击时确保获得焦点 + isDragging.current = true; + lastMousePos.current = { x: e.clientX, y: e.clientY }; + }; + + const handleMouseUp = () => { + isDragging.current = false; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return; + const dx = e.clientX - lastMousePos.current.x; + const dy = e.clientY - lastMousePos.current.y; + lastMousePos.current = { x: e.clientX, y: e.clientY }; + + const angle = baseAngle + angleIndexRef.current * (Math.PI / 2); + const right = new Vector3(1, 0, 0).applyAxisAngle(new Vector3(0, 1, 0), angle); + const forward = new Vector3(0, 0, -1).applyAxisAngle(new Vector3(0, 1, 0), angle); + const dragSpeed = 0.025; + + // Drag canvas: move target opposite to mouse movement on screen + desiredTargetRef.current.addScaledVector(right, -dx * dragSpeed); + desiredTargetRef.current.addScaledVector(forward, dy * dragSpeed); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + switch (e.key.toLowerCase()) { + case 'w': + movementRef.current.forward = true; + break; + case 's': + movementRef.current.back = true; + break; + case 'a': + movementRef.current.left = true; + break; + case 'd': + movementRef.current.right = true; + break; + case 'arrowup': + e.preventDefault(); + desiredZoomRef.current = clamp(desiredZoomRef.current - 1, 8, 45); + break; + case 'arrowdown': + e.preventDefault(); + desiredZoomRef.current = clamp(desiredZoomRef.current + 1, 8, 45); + break; + case 'arrowleft': + // 左箭头:降低俯仰角(镜头向下看) + e.preventDefault(); + desiredPitchAngleRef.current = clamp(desiredPitchAngleRef.current - 0.15, 0.2, 1.2); + break; + case 'arrowright': + // 右箭头:增加俯仰角(镜头向上看) + e.preventDefault(); + desiredPitchAngleRef.current = clamp(desiredPitchAngleRef.current + 0.15, 0.2, 1.2); + break; + case 'q': + angleIndexRef.current = (angleIndexRef.current + 1) % 4; + break; + case 'e': + angleIndexRef.current = (angleIndexRef.current + 3) % 4; + break; + case ' ': + desiredTargetRef.current.set(mapSize / 2, 0, mapSize / 2); + targetRef.current.copy(desiredTargetRef.current); + desiredZoomRef.current = 22; + zoomRef.current = 22; + angleIndexRef.current = 0; + desiredPitchAngleRef.current = 0.6; // 重置俯仰角 + pitchAngleRef.current = 0.6; + break; + } + }; + + const handleKeyUp = (e: KeyboardEvent) => { + switch (e.key.toLowerCase()) { + case 'w': + movementRef.current.forward = false; + break; + case 's': + movementRef.current.back = false; + break; + case 'a': + movementRef.current.left = false; + break; + case 'd': + movementRef.current.right = false; + break; + } + }; + + canvas.addEventListener('wheel', handleWheel, { passive: false }); + window.addEventListener('mousedown', handleMouseDown); + window.addEventListener('mouseup', handleMouseUp); + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + + return () => { + canvas.removeEventListener('wheel', handleWheel); + window.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + }; + }, [gl.domElement, mapSize]); + + useFrame((_, delta) => { + // Keyboard translation + const moveSpeed = 8; + const angle = baseAngle + angleIndexRef.current * (Math.PI / 2); + const right = new Vector3(1, 0, 0).applyAxisAngle(new Vector3(0, 1, 0), angle); + const forward = new Vector3(0, 0, -1).applyAxisAngle(new Vector3(0, 1, 0), angle); + + const moveDir = new Vector3(); + if (movementRef.current.forward) moveDir.add(forward); + if (movementRef.current.back) moveDir.addScaledVector(forward, -1); + if (movementRef.current.left) moveDir.addScaledVector(right, -1); + if (movementRef.current.right) moveDir.add(right); + if (moveDir.lengthSq() > 0) { + moveDir.normalize(); + desiredTargetRef.current.addScaledVector(moveDir, moveSpeed * delta); + } + + const alpha = Math.min(1, delta * 6); + targetRef.current.lerp(desiredTargetRef.current, alpha); + zoomRef.current = lerpScalar(zoomRef.current, desiredZoomRef.current, delta, 4); + pitchAngleRef.current = lerpScalar(pitchAngleRef.current, desiredPitchAngleRef.current, delta, 4); + + const zoom = zoomRef.current; + const pitchAngle = pitchAngleRef.current; + + // 在浏览器标题中显示俯仰角(调试用) + document.title = `Pitch: ${pitchAngle.toFixed(2)} rad`; + + // 根据俯仰角计算相机位置 + // pitchAngle: 0.2 (俯视) 到 1.2 (较平视) + const horizontalRadius = zoom * Math.cos(pitchAngle); + const y = zoom * Math.sin(pitchAngle) + 2; + + const camPos = new Vector3( + targetRef.current.x + Math.sin(angle) * horizontalRadius, + y, + targetRef.current.z + Math.cos(angle) * horizontalRadius + ); + + camera.position.lerp(camPos, Math.min(1, delta * 6)); + camera.lookAt(targetRef.current.x, 0, targetRef.current.z); + }); + + return null; +}; diff --git a/voxel-tactics-horizon/src/core/EnhancedSky.tsx b/voxel-tactics-horizon/src/core/EnhancedSky.tsx new file mode 100644 index 0000000..efa0ee7 --- /dev/null +++ b/voxel-tactics-horizon/src/core/EnhancedSky.tsx @@ -0,0 +1,133 @@ +import { useMemo } from 'react'; +import { Sky } from '@react-three/drei'; +import { Vector3 } from 'three'; + +export type SkyPreset = 'sunrise' | 'day' | 'sunset' | 'night' | 'overcast' | 'stormy'; + +interface EnhancedSkyProps { + preset?: SkyPreset; +} + +const SKY_PRESETS: Record = { + sunrise: { + sunPosition: [100, 5, 0], // 地平线附近,更低 + turbidity: 10, + rayleigh: 3, // 增加散射,更明显的渐变 + mieCoefficient: 0.005, + mieDirectionalG: 0.82, + }, + day: { + sunPosition: [0, 60, 0], // 高但不在正上方,更好的渐变 + turbidity: 3, + rayleigh: 1.5, // 增加蓝色渐变 + mieCoefficient: 0.005, + mieDirectionalG: 0.7, + }, + sunset: { + sunPosition: [-100, 5, 0], // 地平线附近,更低 + turbidity: 10, + rayleigh: 3, // 增加散射 + mieCoefficient: 0.005, + mieDirectionalG: 0.82, + }, + night: { + sunPosition: [0, -20, 0], // 地平线以下更深 + turbidity: 1, + rayleigh: 0.1, + mieCoefficient: 0.001, + mieDirectionalG: 0.5, + }, + overcast: { + sunPosition: [0, 50, 0], + turbidity: 40, // 更浓厚 + rayleigh: 0.3, + mieCoefficient: 0.02, + mieDirectionalG: 0.6, + }, + stormy: { + sunPosition: [-50, 20, -30], + turbidity: 60, // 极浓厚 + rayleigh: 0.5, + mieCoefficient: 0.03, + mieDirectionalG: 0.7, + }, +}; + +export const EnhancedSky: React.FC = ({ preset = 'day' }) => { + const config = useMemo(() => SKY_PRESETS[preset], [preset]); + const sunPosition = useMemo(() => new Vector3(...config.sunPosition), [preset]); + + return ( + + ); +}; + +// 根据天空预设返回对应的光照配置 +export const getLightingConfig = (preset: SkyPreset) => { + const configs: Record = { + sunrise: { + ambientColor: '#ffecd2', + ambientIntensity: 0.4, + directionalColor: '#ffb347', + directionalIntensity: 1.2, + directionalPosition: [-100, 20, -50], + }, + day: { + ambientColor: '#ffffff', + ambientIntensity: 0.8, // 显著增加环境光 + directionalColor: '#ffffff', + directionalIntensity: 2.0, // 增加方向光 + directionalPosition: [-80, 120, 30], + }, + sunset: { + ambientColor: '#ffe4b5', + ambientIntensity: 0.45, + directionalColor: '#ff8c42', + directionalIntensity: 1.3, + directionalPosition: [-120, 40, 20], + }, + night: { + ambientColor: '#1a1a2e', + ambientIntensity: 0.2, + directionalColor: '#4a5568', + directionalIntensity: 0.3, + directionalPosition: [0, -50, 0], + }, + overcast: { + ambientColor: '#e0e0e0', + ambientIntensity: 0.6, + directionalColor: '#c0c0c0', + directionalIntensity: 0.8, + directionalPosition: [0, 80, 0], + }, + stormy: { + ambientColor: '#6b7280', + ambientIntensity: 0.35, + directionalColor: '#9ca3af', + directionalIntensity: 0.7, + directionalPosition: [-50, 60, -30], + }, + }; + + return configs[preset]; +}; + diff --git a/voxel-tactics-horizon/src/core/GameControls.tsx b/voxel-tactics-horizon/src/core/GameControls.tsx new file mode 100644 index 0000000..735cd19 --- /dev/null +++ b/voxel-tactics-horizon/src/core/GameControls.tsx @@ -0,0 +1,105 @@ +import React, { useEffect, useRef } from 'react'; +import { useThree, useFrame } from '@react-three/fiber'; +import { Vector3, MathUtils, MOUSE } from 'three'; +import { MapControls } from '@react-three/drei'; + +export const GameControls: React.FC = () => { + const { camera, gl } = useThree(); + const controlsRef = useRef(null); + + // Target Zoom Level + const zoomRef = useRef(20); // Initial distance + const targetZoom = useRef(20); + + // Reset camera function + const resetCamera = () => { + if (controlsRef.current) { + controlsRef.current.target.set(0, 0, 0); + controlsRef.current.object.position.set(20, 20, 20); + targetZoom.current = 20; + zoomRef.current = 20; + controlsRef.current.update(); + } + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const step = 1; + if (!controlsRef.current) return; + + // Arrow Keys for Panning (Manual offset if needed, but MapControls handles dragging) + // We can inject movement into target + const target = controlsRef.current.target; + + // Pan logic via keys + if (e.key === 'ArrowUp') target.z -= step; + if (e.key === 'ArrowDown') target.z += step; + if (e.key === 'ArrowLeft') target.x -= step; + if (e.key === 'ArrowRight') target.x += step; + + // Zoom logic via W/S + if (e.key === 'w' || e.key === 'W') { + targetZoom.current = Math.max(5, targetZoom.current - 5); + } + if (e.key === 's' || e.key === 'S') { + targetZoom.current = Math.min(50, targetZoom.current + 5); + } + + // Reset + if (e.code === 'Space') { + resetCamera(); + } + }; + + const handleWheel = (e: WheelEvent) => { + // Override default zoom slightly or just track it + // MapControls handles wheel zoom natively, but we want to sync our "targetZoom" state + // so WS keys don't jump. + // Actually, let MapControls handle wheel, we handle WS by moving camera along forward vector. + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + useFrame(({ clock }, delta) => { + if (!controlsRef.current) return; + + // Smooth Zoom for W/S keys + // We manually interpolate the camera distance if W/S changed targetZoom + // Note: This might fight with OrbitControls internal zoom if not careful. + // Strategy: Check distance, lerp to targetZoom. + + const currentDist = camera.position.distanceTo(controlsRef.current.target); + if (Math.abs(currentDist - targetZoom.current) > 0.1) { + // Move camera towards target to reach distance + const dir = new Vector3().subVectors(camera.position, controlsRef.current.target).normalize(); + const newPos = controlsRef.current.target.clone().add(dir.multiplyScalar(MathUtils.lerp(currentDist, targetZoom.current, delta * 5))); + camera.position.copy(newPos); + } + }); + + return ( + + ); +}; + diff --git a/voxel-tactics-horizon/src/core/MinecraftSky.tsx b/voxel-tactics-horizon/src/core/MinecraftSky.tsx new file mode 100644 index 0000000..83bf32d --- /dev/null +++ b/voxel-tactics-horizon/src/core/MinecraftSky.tsx @@ -0,0 +1,201 @@ +import { useMemo } from 'react'; +import { shaderMaterial } from '@react-three/drei'; +import { extend, type ReactThreeFiber } from '@react-three/fiber'; +import { Color, Vector3, BackSide } from 'three'; + +export type SkyPreset = 'sunrise' | 'day' | 'sunset' | 'night' | 'overcast' | 'stormy'; + +interface MinecraftSkyProps { + preset: SkyPreset; +} + +const SKY_PRESETS: Record< + SkyPreset, + { + topColor: string; + horizonColor: string; + bottomColor: string; + sunDirection: [number, number, number]; + sunColor: string; + } +> = { + sunrise: { + topColor: '#fdd1a5', + horizonColor: '#ff9b58', + bottomColor: '#ffd7b8', + sunDirection: [0.2, 0.4, -0.3], + sunColor: '#ffd9a0', + }, + day: { + topColor: '#8fd3ff', + horizonColor: '#c3ecff', + bottomColor: '#f6fcff', + sunDirection: [-0.35, 0.55, 0.15], + sunColor: '#ffe6c5', + }, + sunset: { + topColor: '#ff8f70', + horizonColor: '#ffad53', + bottomColor: '#ffd9c0', + sunDirection: [-0.3, 0.35, 0.25], + sunColor: '#ffc26d', + }, + night: { + topColor: '#112045', + horizonColor: '#1a2f59', + bottomColor: '#1f2b40', + sunDirection: [0.1, -0.4, 0.3], + sunColor: '#6f7cff', + }, + overcast: { + topColor: '#b7c0ce', + horizonColor: '#cdd3df', + bottomColor: '#e2e6ef', + sunDirection: [0.15, 0.45, -0.2], + sunColor: '#f0f0f0', + }, + stormy: { + topColor: '#5a6a82', + horizonColor: '#76849b', + bottomColor: '#8d9ab1', + sunDirection: [-0.1, 0.35, -0.4], + sunColor: '#dfe4f5', + }, +}; + +const MinecraftSkyMaterial = shaderMaterial( + { + topColor: new Color('#6fc0ff'), + horizonColor: new Color('#9ed9ff'), + bottomColor: new Color('#d8f2ff'), + sunDirection: new Vector3(0, 1, 0), + sunColor: new Color('#fff0b8'), + }, + ` + varying vec3 vWorldPosition; + void main() { + vec4 worldPosition = modelMatrix * vec4(position, 1.0); + vWorldPosition = normalize(worldPosition.xyz); + gl_Position = projectionMatrix * viewMatrix * worldPosition; + } + `, + ` + varying vec3 vWorldPosition; + uniform vec3 topColor; + uniform vec3 horizonColor; + uniform vec3 bottomColor; + uniform vec3 sunDirection; + uniform vec3 sunColor; + + void main() { + float height = clamp(vWorldPosition.y * 0.5 + 0.5, 0.0, 1.0); + vec3 baseColor = mix(bottomColor, horizonColor, smoothstep(0.0, 0.4, height)); + baseColor = mix(baseColor, topColor, smoothstep(0.3, 1.0, height)); + + float sunFactor = max(0.0, dot(normalize(vWorldPosition), normalize(sunDirection))); + float sunGlow = pow(sunFactor, 400.0); + vec3 color = baseColor + sunColor * sunGlow; + + gl_FragColor = vec4(color, 1.0); + } + ` +); + +extend({ MinecraftSkyMaterial }); + +declare module '@react-three/fiber' { + interface ThreeElements { + minecraftSkyMaterial: ReactThreeFiber.ElementProps & { + attach?: string; + }; + } +} + +export const MinecraftSky: React.FC = ({ preset }) => { + const config = SKY_PRESETS[preset]; + + const uniforms = useMemo( + () => ({ + topColor: new Color(config.topColor), + horizonColor: new Color(config.horizonColor), + bottomColor: new Color(config.bottomColor), + sunDirection: new Vector3(...config.sunDirection).normalize(), + sunColor: new Color(config.sunColor), + }), + [preset, config.bottomColor, config.horizonColor, config.sunColor, config.topColor, config.sunDirection] + ); + + return ( + + + + + ); +}; + +export const getLightingConfig = (preset: SkyPreset) => { + const configs: Record< + SkyPreset, + { + ambientColor: string; + ambientIntensity: number; + directionalColor: string; + directionalIntensity: number; + directionalPosition: [number, number, number]; + } + > = { + sunrise: { + ambientColor: '#ffe8d1', + ambientIntensity: 0.7, // 从0.45增加到0.7 + directionalColor: '#ffb870', + directionalIntensity: 1.6, // 从1.1增加到1.6 + directionalPosition: [50, 45, -30], + }, + day: { + ambientColor: '#ffffff', + ambientIntensity: 1.0, // 从0.75增加到1.0 + directionalColor: '#fff3d1', + directionalIntensity: 2.5, // 从2.0增加到2.5 + directionalPosition: [-80, 120, 30], + }, + sunset: { + ambientColor: '#ffd7b2', + ambientIntensity: 0.75, // 从0.5增加到0.75 + directionalColor: '#ff9f5f', + directionalIntensity: 1.8, // 从1.3增加到1.8 + directionalPosition: [-80, 55, 30], + }, + night: { + ambientColor: '#1b2238', + ambientIntensity: 0.3, // 从0.18增加到0.3 + directionalColor: '#7087ff', + directionalIntensity: 0.4, // 从0.25增加到0.4 + directionalPosition: [40, -50, -20], + }, + overcast: { + ambientColor: '#cfd5df', + ambientIntensity: 0.85, // 从0.65增加到0.85 + directionalColor: '#c6ccd9', + directionalIntensity: 1.2, // 从0.9增加到1.2 + directionalPosition: [0, 90, 0], + }, + stormy: { + ambientColor: '#8d96a9', + ambientIntensity: 0.6, // 从0.4增加到0.6 + directionalColor: '#b0b9cc', + directionalIntensity: 0.9, // 从0.6增加到0.9 + directionalPosition: [-40, 60, -50], + }, + }; + + return configs[preset]; +}; + diff --git a/voxel-tactics-horizon/src/features/Battle/store.ts b/voxel-tactics-horizon/src/features/Battle/store.ts new file mode 100644 index 0000000..e40ec0d --- /dev/null +++ b/voxel-tactics-horizon/src/features/Battle/store.ts @@ -0,0 +1,120 @@ +import { create } from 'zustand'; +import { useUnitStore } from '../Units/store'; +import { getReachableTiles } from '../../utils/pathfinding'; +import { useMapStore } from '../Map/store'; + +export type BattlePhase = 'IDLE' | 'PLAYER_TURN' | 'AI_TURN' | 'VICTORY' | 'DEFEAT'; + +interface BattleState { + phase: BattlePhase; + activeUnitId: string | null; + reachableTiles: { x: number; y: number; z: number }[]; // For visualization + + startBattle: () => void; + nextTurn: () => void; + endTurn: () => void; + moveActiveUnit: (to: { x: number; z: number }) => void; +} + +export const useBattleStore = create((set, get) => ({ + phase: 'IDLE', + activeUnitId: null, + reachableTiles: [], + + startBattle: () => { + get().nextTurn(); + }, + + nextTurn: () => { + const unitStore = useUnitStore.getState(); + const mapStore = useMapStore.getState(); + const units = unitStore.units; + + if (units.length === 0) return; + + let pendingUnits = [...units]; + let readyUnit = pendingUnits.find(u => u.ct >= 100); + + let loops = 0; + while (!readyUnit && loops < 1000) { + let minTicks = Infinity; + pendingUnits.forEach(u => { + const ticks = Math.max(0, (100 - u.ct) / u.stats.speed); + if (ticks < minTicks) minTicks = ticks; + }); + minTicks = Math.ceil(minTicks) || 1; + + const newUnits = pendingUnits.map(u => ({ + ...u, + ct: u.ct + u.stats.speed * minTicks + })); + pendingUnits = newUnits; + readyUnit = pendingUnits.find(u => u.ct >= 100); + loops++; + useUnitStore.setState({ units: pendingUnits }); + } + + if (!readyUnit) return; + + pendingUnits.sort((a, b) => b.ct - a.ct); + const active = pendingUnits[0]; + + set({ + activeUnitId: active.id, + phase: active.team === 'player' ? 'PLAYER_TURN' : 'AI_TURN' + }); + unitStore.selectUnit(active.id); + + // If Player Turn, calculate movement range + if (active.team === 'player') { + const tiles = getReachableTiles( + active.position, + active.stats.move, + active.stats.jump, + mapStore.getHeightAt, + mapStore.size + ); + set({ reachableTiles: tiles }); + } else { + set({ reachableTiles: [] }); + // Simple AI: Wait 1s then end turn + setTimeout(() => { + get().endTurn(); + }, 1000); + } + }, + + moveActiveUnit: (to) => { + const state = get(); + if (state.phase !== 'PLAYER_TURN' || !state.activeUnitId) return; + + // Check if valid move + const valid = state.reachableTiles.some(t => t.x === to.x && t.z === to.z); + if (!valid) return; + + const unitStore = useUnitStore.getState(); + const mapStore = useMapStore.getState(); + const y = mapStore.getHeightAt(to.x, to.z) - 1; + + unitStore.moveUnit(state.activeUnitId, { x: to.x, y, z: to.z }); + set({ reachableTiles: [] }); // Clear highlights + // In a real game, we'd switch to ACTION_SELECT phase here + }, + + endTurn: () => { + const state = get(); + const unitStore = useUnitStore.getState(); + + if (!state.activeUnitId) return; + + const unitIndex = unitStore.units.findIndex(u => u.id === state.activeUnitId); + if (unitIndex >= 0) { + const units = [...unitStore.units]; + units[unitIndex] = { ...units[unitIndex], ct: units[unitIndex].ct - 100 }; + useUnitStore.setState({ units }); + } + + set({ activeUnitId: null, phase: 'IDLE', reachableTiles: [] }); + setTimeout(() => get().nextTurn(), 100); + } +})); diff --git a/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx new file mode 100644 index 0000000..e2ee78d --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/components/ChunkRenderer.tsx @@ -0,0 +1,311 @@ +import React, { useLayoutEffect, useMemo, useRef } from 'react'; +import { InstancedMesh, Object3D, Color, BufferGeometry, Float32BufferAttribute } from 'three'; +import { useMapStore } from '../store'; +import { useUnitStore } from '../../Units/store'; +import { VOXEL_SIZE, TILE_SIZE, TREE_VOXEL_SIZE, type VoxelType } from '../logic/terrain'; +import type { ThreeEvent } from '@react-three/fiber'; + +const tempObject = new Object3D(); +const tempColor = new Color(); + +interface VoxelLayerProps { + // Update to include ix, iy, iz which are present in actual VoxelData + data: { x: number; y: number; z: number; color: string; heightScale?: number; isHighRes?: boolean; ix: number; iy: number; iz: number }[]; + isHighRes: boolean; + type: VoxelType; + onClick?: (x: number, z: number) => void; +} + +/** + * 专门用于水体的网格渲染器 + * 使用 Greedy Meshing / Face Culling 思想,去除相邻水体之间的内部面 + * 解决半透明材质叠加导致的颗粒感和反射问题 + */ +const WaterFlowMesh: React.FC = ({ data, isHighRes, type, onClick }) => { + const meshRef = useRef(null); + + const geometry = useMemo(() => { + if (!data.length) return new BufferGeometry(); + + // 1. 建立查找表 (Set of "ix|iy|iz") + const lookup = new Set(); + data.forEach(v => lookup.add(`${v.ix}|${v.iy}|${v.iz}`)); + + const positions: number[] = []; + const normals: number[] = []; + const colors: number[] = []; + // const indices: number[] = []; // Not strictly needed if not sharing vertices + + const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE; + const halfSize = voxelSize / 2; + + // 6个方向的邻居偏移和法线 + const dirs = [ + { name: 'right', off: [1, 0, 0], normal: [1, 0, 0] }, + { name: 'left', off: [-1, 0, 0], normal: [-1, 0, 0] }, + { name: 'top', off: [0, 1, 0], normal: [0, 1, 0] }, + { name: 'bottom', off: [0, -1, 0], normal: [0, -1, 0] }, + { name: 'front', off: [0, 0, 1], normal: [0, 0, 1] }, + { name: 'back', off: [0, 0, -1], normal: [0, 0, -1] }, + ]; + + data.forEach(v => { + const { x, y, z, ix, iy, iz, color, heightScale = 1 } = v; + const col = new Color(color); + + // 只有当 heightScale 为 1 时才能完美拼接 + // 如果有缩放,简单起见仍然按照满格判定邻居,或者忽略拼接优化(水通常是满格) + const isFullBlock = Math.abs(heightScale - 1) < 0.01; + + dirs.forEach(dir => { + // 检查该方向是否有同类邻居 + const neighborKey = `${ix + dir.off[0]}|${iy + dir.off[1]}|${iz + dir.off[2]}`; + const hasNeighbor = isFullBlock && lookup.has(neighborKey); + + // 如果没有邻居,渲染该面 + if (!hasNeighbor) { + // 生成面的4个顶点 + // 中心点 (x, y, z) 是体素的中心吗? + // InstancedMesh logic: y + (hScale - 1) * (size/2). If hScale=1, y is center. + // VoxelData.y is world coordinate. + // Assuming voxel.y is the center Y coordinate of the block if scale is 1. + + // Calculate actual center based on heightScale logic from VoxelLayer + const centerY = y + (heightScale - 1) * (voxelSize / 2); + const scaleY = voxelSize * heightScale; + const halfY = scaleY / 2; + + // Base vertex offsets for a unit cube centered at 0,0,0 + // Right: (+h, -h, +h), (+h, -h, -h), (+h, +h, -h), (+h, +h, +h) + // Need to be careful with coordinate system + // THREE.js: Y up. Right(x+), Left(x-), Top(y+), Bottom(y-), Front(z+), Back(z-) + + // Vertices relative to center + let v1, v2, v3, v4; + + const hs = halfSize; + const hy = halfY; + + // Using dir.normal to determine face + const [nx, ny, nz] = dir.normal; + + // Logic to generate quad vertices + if (nx === 1) { // Right + v1 = [hs, -hy, hs]; v2 = [hs, -hy, -hs]; v3 = [hs, hy, -hs]; v4 = [hs, hy, hs]; + } else if (nx === -1) { // Left + v1 = [-hs, -hy, -hs]; v2 = [-hs, -hy, hs]; v3 = [-hs, hy, hs]; v4 = [-hs, hy, -hs]; + } else if (ny === 1) { // Top + v1 = [-hs, hy, hs]; v2 = [ hs, hy, hs]; v3 = [ hs, hy, -hs]; v4 = [-hs, hy, -hs]; + } else if (ny === -1) { // Bottom + v1 = [ hs, -hy, hs]; v2 = [-hs, -hy, hs]; v3 = [-hs, -hy, -hs]; v4 = [ hs, -hy, -hs]; + } else if (nz === 1) { // Front (Z+) + v1 = [-hs, -hy, hs]; v2 = [ hs, -hy, hs]; v3 = [ hs, hy, hs]; v4 = [-hs, hy, hs]; + } else if (nz === -1) { // Back (Z-) + v1 = [ hs, -hy, -hs]; v2 = [-hs, -hy, -hs]; v3 = [-hs, hy, -hs]; v4 = [ hs, hy, -hs]; + } else { + // Should not happen + v1=[0,0,0]; v2=[0,0,0]; v3=[0,0,0]; v4=[0,0,0]; + } + + // Translate to world pos + const applyPos = (v: number[]) => [v[0] + x, v[1] + centerY, v[2] + z]; + const p1 = applyPos(v1); + const p2 = applyPos(v2); + const p3 = applyPos(v3); + const p4 = applyPos(v4); + + // Push 2 triangles (CCW winding) + // Tri 1: p1, p2, p3 + positions.push(...p1, ...p2, ...p3); + // Tri 2: p1, p3, p4 + positions.push(...p1, ...p3, ...p4); + + // Normals (same for all 6 verts) + for(let k=0; k<6; k++) normals.push(nx, ny, nz); + + // Colors (same for all 6 verts) + for(let k=0; k<6; k++) colors.push(col.r, col.g, col.b); + } + }); + }); + + const bufGeom = new BufferGeometry(); + bufGeom.setAttribute('position', new Float32BufferAttribute(positions, 3)); + bufGeom.setAttribute('normal', new Float32BufferAttribute(normals, 3)); + bufGeom.setAttribute('color', new Float32BufferAttribute(colors, 3)); + + return bufGeom; + }, [data, isHighRes]); + + return ( + + + + ); +} + +const VoxelLayer: React.FC = ({ data, isHighRes, type, onClick }) => { + const meshRef = useRef | null>(null); + const setHoveredTile = useUnitStore(state => state.setHoveredTile); + + useLayoutEffect(() => { + const mesh = meshRef.current; + if (!mesh || data.length === 0) return; + + // 明确使用配置的尺寸 + const voxelSize = isHighRes ? TREE_VOXEL_SIZE : VOXEL_SIZE; + + data.forEach((voxel, i) => { + const hScale = voxel.heightScale ?? 1; + const scaleY = voxelSize * hScale; + tempObject.position.set( + voxel.x, + voxel.y + (hScale - 1) * (voxelSize / 2), + voxel.z + ); + tempObject.scale.set(voxelSize, scaleY, voxelSize); + tempObject.updateMatrix(); + mesh.setMatrixAt(i, tempObject.matrix); + + tempColor.set(voxel.color); + mesh.setColorAt(i, tempColor); + }); + + mesh.instanceMatrix.needsUpdate = true; + + }, [data, isHighRes]); + + const handlePointerMove = (e: ThreeEvent) => { + e.stopPropagation(); + if (e.instanceId === undefined) return; + + const voxel = data[e.instanceId]; + if (voxel) { + const lx = Math.floor(voxel.x / TILE_SIZE); + const lz = Math.floor(voxel.z / TILE_SIZE); + setHoveredTile({ x: lx, z: lz }); + } + }; + + const handleClick = (e: ThreeEvent) => { + e.stopPropagation(); + if (e.instanceId === undefined || !onClick) return; + const voxel = data[e.instanceId]; + if (voxel) { + const lx = Math.floor(voxel.x / TILE_SIZE); + const lz = Math.floor(voxel.z / TILE_SIZE); + onClick(lx, lz); + } + } + + const materialProps = useMemo(() => { + // Water is now handled by WaterFlowMesh, but keeping this for fallback or other transparent blocks + if (type === 'water') { + return { + roughness: 0.1, + metalness: 0.2, + transparent: true, + opacity: 0.75 + }; + } else if (type === 'ice') { + return { + roughness: 0.1, + metalness: 0.1, + transparent: true, + opacity: 0.8 + }; + } + return { + roughness: 0.9, + metalness: 0.2, + transparent: false, + opacity: 1.0 + }; + }, [type]); + + return ( + + + + + ); +}; + +interface ChunkRendererProps { + onVoxelClick?: (x: number, z: number) => void; +} + +export const ChunkRenderer: React.FC = ({ onVoxelClick }) => { + const voxels = useMapStore((state) => state.voxels); + + const groupedVoxels = useMemo(() => { + const groups: Record = {}; + voxels.forEach((v) => { + const key = `${v.type}|${v.isHighRes ? 'true' : 'false'}`; + if (!groups[key]) groups[key] = []; + groups[key].push(v); + }); + return groups; + }, [voxels]); + + return ( + + {Object.keys(groupedVoxels).map((key) => { + const [type, isHighResStr] = key.split('|'); + const isHighRes = isHighResStr === 'true'; + + // 特殊处理水体:使用 Face Culling 网格来消除内部面 + if (type === 'water') { + return ( + + ); + } + + return ( + + ); + })} + + ); +}; diff --git a/voxel-tactics-horizon/src/features/Map/components/Cursor.tsx b/voxel-tactics-horizon/src/features/Map/components/Cursor.tsx new file mode 100644 index 0000000..339ae11 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/components/Cursor.tsx @@ -0,0 +1,41 @@ +import React, { useRef } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { Mesh, BoxGeometry } from 'three'; +import { useUnitStore } from '../../Units/store'; +import { useMapStore } from '../store'; +import { TILE_SIZE } from '../logic/terrain'; + +export const SelectionCursor: React.FC = () => { + const hoveredTile = useUnitStore((state) => state.hoveredTile); + const getHeightAt = useMapStore((state) => state.getHeightAt); + const meshRef = useRef(null); + + useFrame(({ clock }) => { + if (!hoveredTile || !meshRef.current) { + if (meshRef.current) meshRef.current.visible = false; + return; + } + + meshRef.current.visible = true; + const worldY = getHeightAt(hoveredTile.x, hoveredTile.z); + // Returns Unit Y (Top of block). + // Cursor should be at Block Y (Top - VOXEL_SIZE/2?) or just slightly above block. + + // Logic Coords -> World Center + const cx = hoveredTile.x * TILE_SIZE + TILE_SIZE / 2; + const cz = hoveredTile.z * TILE_SIZE + TILE_SIZE / 2; + + meshRef.current.position.set(cx, worldY - 0.1, cz); // Slightly below unit feet + + // Pulse + const s = (TILE_SIZE * 1.05) + Math.sin(clock.elapsedTime * 10) * 0.05; + meshRef.current.scale.set(s, s, s); + }); + + return ( + + {/* Flattened box */} + + + ); +}; diff --git a/voxel-tactics-horizon/src/features/Map/components/MoveOverlay.tsx b/voxel-tactics-horizon/src/features/Map/components/MoveOverlay.tsx new file mode 100644 index 0000000..16c8153 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/components/MoveOverlay.tsx @@ -0,0 +1,55 @@ +import React, { useLayoutEffect, useRef } from 'react'; +import { InstancedMesh, Object3D } from 'three'; +import { useBattleStore } from '../../Battle/store'; +import { TILE_SIZE } from '../logic/terrain'; + +const tempObject = new Object3D(); + +export const MoveOverlay: React.FC = () => { + const reachableTiles = useBattleStore(state => state.reachableTiles); + const meshRef = useRef(null); + + useLayoutEffect(() => { + if (!meshRef.current) return; + + reachableTiles.forEach((tile, i) => { + // Tile x,z are Logic Coords. + // We need to convert to World Coords (Center of tile). + // Tile goes from x*SIZE to (x+1)*SIZE. Center is x*SIZE + SIZE/2. + const cx = tile.x * TILE_SIZE + TILE_SIZE / 2; + const cz = tile.z * TILE_SIZE + TILE_SIZE / 2; + + // Render slightly above the ground + // tile.y is World Y + tempObject.position.set(cx - 0.5 * TILE_SIZE, tile.y + 0.1, cz - 0.5 * TILE_SIZE); // Offset correction + // Wait, logic tile 5. Center is 5.5? + // Let's check terrain generation. + // voxels.x = lx * MICRO + mx. Scaled by VOXEL_SIZE. + // lx * TILE_SIZE <= voxel.x < (lx+1)*TILE_SIZE. + // So yes, logic tile covers [lx, lx+1]. + // MoveOverlay should cover this area. + + tempObject.position.set(tile.x * TILE_SIZE + TILE_SIZE/2, tile.y + 0.2, tile.z * TILE_SIZE + TILE_SIZE/2); + tempObject.scale.set(TILE_SIZE * 0.9, 1, TILE_SIZE * 0.9); // Almost fill the tile + + tempObject.updateMatrix(); + meshRef.current!.setMatrixAt(i, tempObject.matrix); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + meshRef.current.count = reachableTiles.length; + + }, [reachableTiles]); + + if (reachableTiles.length === 0) return null; + + return ( + + + + + ); +}; diff --git a/voxel-tactics-horizon/src/features/Map/components/TerrainGenerationProgress.tsx b/voxel-tactics-horizon/src/features/Map/components/TerrainGenerationProgress.tsx new file mode 100644 index 0000000..6f1b716 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/components/TerrainGenerationProgress.tsx @@ -0,0 +1,144 @@ +/** + * 地形生成进度条组件 + * 显示地形生成的各个阶段和进度 + * 优化版本:简洁精致的设计 + */ + +import { useMapStore } from '../store'; + +export const TerrainGenerationProgress = () => { + const { isGenerating, generationProgress } = useMapStore(); + + if (!isGenerating || !generationProgress) { + return null; + } + + const { progress, detail } = generationProgress; + + return ( +
+
+ + {/* 进度百分比 - 居中突出显示 */} +
+
+ {progress}% +
+
+ + {/* 阶段指示器 - 步骤圆圈 */} +
+ {['basic', 'features', 'postprocessing', 'vegetation'].map((stage, index) => { + const stageLabels = { + basic: '基础地形', + features: '特征地形', + postprocessing: '后处理', + vegetation: '植被' + }; + const isCompleted = getStageIndex(generationProgress.stage) > index; + const isCurrent = generationProgress.stage === stage; + + return ( +
+
+ {isCompleted ? '✓' : index + 1} +
+ + {stageLabels[stage as keyof typeof stageLabels]} + +
+ ); + })} +
+ + {/* 精致进度条 */} +
+
+ {/* 精致光效层 1: 流动光泽 */} +
+ + {/* 精致光效层 2: 顶部高光 */} +
+
+
+ + {/* 细节信息 - 低调显示 */} + {detail && ( +
+

+ {detail} +

+
+ )} +
+ + {/* 精致CSS动画 */} + +
+ ); +}; + +// 辅助函数:获取阶段索引 +function getStageIndex(stage: string): number { + const stages = ['basic', 'features', 'postprocessing', 'vegetation', 'complete']; + return stages.indexOf(stage); +} + diff --git a/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts new file mode 100644 index 0000000..a9519d3 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/desertFeatures.ts @@ -0,0 +1,605 @@ +/** + * 戈壁特征地形系统:生成戈壁山丘、溪流、巨石等特殊地形 + * - 戈壁山丘生成(普通山丘和拱形山丘) + * - 戈壁溪流生成 + * - 戈壁岩石结构(巨石、石台、土台) + * - 戈壁分层逻辑 + */ + +import type { VoxelType } from './voxelStyles'; + +// MICRO_SCALE 常量:每个逻辑格子包含的微型体素数量 +// const MICRO_SCALE = 8; // Unused + +// ============= 常量定义 ============= + +export const DESERT_MAX_EXTRA_HEIGHT = 4; // 额外高度不超过 4 个逻辑层 + +// ============= 类型定义 ============= + +export interface DesertContext { + gobiHeight: number[][]; + gobiVariant: number[][]; + gobiMinHeight: number[][]; // 记录戈壁起始高度(用于拱门效果) + stoneHeight: number[][]; + stoneVariant: number[][]; + streamDepthMap: Map; // Added isCenter +} + +// ============= 戈壁岩石结构生成 ============= +// (Legacy structures removed) + +// ============= 通用形态学修正 ============= +const applyMorphologicalSmoothing = ( + heightMap: number[][], + startX: number, + startZ: number, + endX: number, + endZ: number, + mapSize: number +) => { + const changes: Array<{x: number, z: number, h: number}> = []; + const sx = Math.max(0, startX - 1); + const sz = Math.max(0, startZ - 1); + const ex = Math.min(mapSize - 1, endX + 1); + const ez = Math.min(mapSize - 1, endZ + 1); + + for (let x = sx; x <= ex; x++) { + for (let z = sz; z <= ez; z++) { + const currentH = heightMap[x][z]; + const n = (z > 0) ? heightMap[x][z-1] : 0; + const s = (z < mapSize - 1) ? heightMap[x][z+1] : 0; + const w = (x > 0) ? heightMap[x-1][z] : 0; + const e = (x < mapSize - 1) ? heightMap[x+1][z] : 0; + + if (currentH === 0) { + const maxNeighborH = Math.max(n, s, w, e); + if (maxNeighborH > 0) { + let shouldFill = false; + if (n > 0 && w > 0) shouldFill = true; + if (n > 0 && e > 0) shouldFill = true; + if (s > 0 && w > 0) shouldFill = true; + if (s > 0 && e > 0) shouldFill = true; + if (n > 0 && s > 0) shouldFill = true; + if (w > 0 && e > 0) shouldFill = true; + if (shouldFill) { + changes.push({x, z, h: maxNeighborH}); + } + } + } + } + } + + for (const change of changes) { + if (heightMap[change.x][change.z] < change.h) { + heightMap[change.x][change.z] = change.h; + } + } +}; + +const spawnMesa = ( + centerX: number, + centerZ: number, + radiusH: number, + radiusV: number, + height: number, + mapSize: number, + gobiHeight: number[][], + gobiVariant: number[][], + gobiMinHeight: number[][], + rand: () => number, + checkLimit?: () => boolean +): number => { + let cellsPlaced = 0; + const topFlatRatio = 0.6 + rand() * 0.2; + const hasRamp = true; + const rampAngle = rand() * Math.PI * 2; + const rampWidth = Math.PI / 2.5; + const rampLengthFactor = 1.4 + rand() * 0.4; + const hasArch = height >= 4 && rand() < 0.3; + let archDirection = 0; + const isUShape = !hasArch && radiusH > 3 && rand() < 0.25; + const uShapeAngle = rand() * Math.PI * 2; + const uShapeWidth = Math.PI / 2.5; + + if (hasArch) { + archDirection = rand() < 0.5 ? 0 : 1; + } + + const scanRadiusH = Math.ceil(radiusH * rampLengthFactor) + 2; + const scanRadiusV = Math.ceil(radiusV * rampLengthFactor) + 2; + const noiseOffset = rand() * 100; + const archCenterOffset = (rand() - 0.5) * 0.6 * (archDirection === 0 ? radiusH : radiusV); + const hasTier = rand() < 0.6; + const tierHeight = hasTier ? 1 + Math.floor(rand() * 2) : 0; + const tierWidthAdded = 1 + Math.floor(rand() * 2); + const scanRadiusH_Tier = scanRadiusH + tierWidthAdded + 1; + const scanRadiusV_Tier = scanRadiusV + tierWidthAdded + 1; + + for (let dx = -scanRadiusH_Tier; dx <= scanRadiusH_Tier; dx++) { + for (let dz = -scanRadiusV_Tier; dz <= scanRadiusV_Tier; dz++) { + if (checkLimit && checkLimit()) return cellsPlaced; + + const lx = centerX + dx; + const lz = centerZ + dz; + + if (lx >= 0 && lx < mapSize && lz >= 0 && lz < mapSize) { + const angle = Math.atan2(dz, dx); + const noise = (Math.sin(angle * 3 + noiseOffset) * 0.5 + Math.cos(angle * 5) * 0.25) * 0.1; + const n = 2.5 + rand() * 1.0; + const rawDist = Math.pow(Math.pow(Math.abs(dx) / radiusH, n) + Math.pow(Math.abs(dz) / radiusV, n), 1/n); + let dist = rawDist + noise; + const rawDistTier = Math.pow(Math.pow(Math.abs(dx) / (radiusH + tierWidthAdded), n) + Math.pow(Math.abs(dz) / (radiusV + tierWidthAdded), n), 1/n); + let distTier = rawDistTier + noise; + + let angleDiff = Math.abs(angle - rampAngle); + if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff; + if (hasRamp && angleDiff < rampWidth / 2) { + const rampFactor = rampLengthFactor; + const t = angleDiff / (rampWidth / 2); + const rampInfluence = (1 + Math.cos(t * Math.PI)) * 0.5; + dist = dist / (1 + (rampFactor - 1) * rampInfluence); + distTier = distTier / (1 + (rampFactor - 1) * rampInfluence); + } + + if (isUShape) { + let uAngleDiff = Math.abs(angle - uShapeAngle); + if (uAngleDiff > Math.PI) uAngleDiff = 2 * Math.PI - uAngleDiff; + if (uAngleDiff < uShapeWidth / 2) { + const cutInfluence = Math.cos((uAngleDiff / (uShapeWidth / 2)) * (Math.PI / 2)); + const centerDist = Math.sqrt(dx*dx + dz*dz) / Math.max(radiusH, radiusV); + if (centerDist < 0.8) { + dist += cutInfluence * 2.0; + distTier += cutInfluence * 2.0; + } + } + } + + let h = 0; + if (dist < 1.0) { + if (dist < topFlatRatio) { + h = height; + } else { + const edgeProgress = (dist - topFlatRatio) / (1 - topFlatRatio); + const t = Math.max(0, Math.min(1, edgeProgress)); + const smoothT = t * t * (3 - 2 * t); + h = Math.round(height * (1 - smoothT)); + } + } else if (hasTier && distTier < 1.0) { + const tierFlatRatio = topFlatRatio * 0.9; + if (distTier < tierFlatRatio) { + h = tierHeight; + } else { + const edgeProgress = (distTier - tierFlatRatio) / (1 - tierFlatRatio); + const t = Math.max(0, Math.min(1, edgeProgress)); + const smoothT = t * t * (3 - 2 * t); + h = Math.round(tierHeight * (1 - smoothT)); + } + } + + if (h > 0) { + let minLayerHeight = 0; + if (hasArch && h > tierHeight) { + const u = archDirection === 0 ? dx : dz; + const v = archDirection === 0 ? dz : dx; + const archSpan = (archDirection === 0 ? radiusH : radiusV) * 0.85; + const archThickness = (archDirection === 0 ? radiusV : radiusH) * 0.6; + if (Math.abs(v) < archThickness) { + const localU = u - archCenterOffset; + if (Math.abs(localU) < archSpan) { + const archHeightRatio = 1 - Math.pow(localU / archSpan, 2); + if (archHeightRatio > 0) { + const minCeiling = Math.max(3, Math.floor(h * 0.3)); + const maxHoleHeight = Math.max(0, h - minCeiling); + let holeHeight = Math.floor(maxHoleHeight * (0.4 + 0.6 * archHeightRatio)); + const asymmetry = 1 + (localU / archSpan) * 0.5; + holeHeight = Math.floor(holeHeight * asymmetry); + holeHeight = Math.max(0, Math.min(maxHoleHeight, holeHeight)); + if (holeHeight > 0) { + minLayerHeight = holeHeight; + } + } + } + } + } + + if (h > gobiHeight[lx][lz]) { + if (gobiHeight[lx][lz] === 0) cellsPlaced++; + gobiHeight[lx][lz] = h; + if (h !== gobiHeight[lx][lz]) { + gobiVariant[lx][lz] = Math.floor(rand() * 3); + } + gobiMinHeight[lx][lz] = minLayerHeight; + } + } + } + } + } + applyMorphologicalSmoothing( + gobiHeight, + centerX - scanRadiusH_Tier, + centerZ - scanRadiusV_Tier, + centerX + scanRadiusH_Tier, + centerZ + scanRadiusV_Tier, + mapSize + ); + return cellsPlaced; +}; + +const spawnMiniMesa = ( + centerX: number, + centerZ: number, + radius: number, + height: number, + mapSize: number, + gobiHeight: number[][], + gobiVariant: number[][], + gobiMinHeight: number[][], + rand: () => number, + checkLimit?: () => boolean +): number => { + let cellsPlaced = 0; + const topFlatRatio = 0.8; + const hasRamp = rand() < 0.3; + const rampAngle = rand() * Math.PI * 2; + const rampWidth = Math.PI / 2; + const scanRadius = Math.ceil(radius) + 2; + + for (let dx = -scanRadius; dx <= scanRadius; dx++) { + for (let dz = -scanRadius; dz <= scanRadius; dz++) { + if (checkLimit && checkLimit()) return cellsPlaced; + const lx = centerX + dx; + const lz = centerZ + dz; + if (lx >= 0 && lx < mapSize && lz >= 0 && lz < mapSize) { + if (gobiHeight[lx][lz] >= height) continue; + const angle = Math.atan2(dz, dx); + const noise = (Math.sin(angle * 3)) * 0.05; + let dist = Math.sqrt(dx*dx + dz*dz) / radius + noise; + if (hasRamp) { + let angleDiff = Math.abs(angle - rampAngle); + if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff; + if (angleDiff < rampWidth / 2) { + const rampFactor = 1.4; + const rampInfluence = Math.cos((angleDiff / (rampWidth / 2)) * (Math.PI / 2)); + dist = dist / (1 + (rampFactor - 1) * rampInfluence); + } + } + if (dist < 1.0) { + let h = 0; + if (dist < topFlatRatio) { + h = height; + } else { + h = Math.round(height * 0.6); + } + if (h > 0 && h > gobiHeight[lx][lz]) { + if (gobiHeight[lx][lz] === 0) cellsPlaced++; + gobiHeight[lx][lz] = h; + gobiVariant[lx][lz] = Math.floor(rand() * 3); + gobiMinHeight[lx][lz] = 0; + } + } + } + } + } + return cellsPlaced; +}; + +const layoutStrategicDesert = ( + mapSize: number, + gobiHeight: number[][], + gobiVariant: number[][], + gobiMinHeight: number[][], + rand: () => number +) => { + console.log('[DesertGen] Using Strategic Segmentation Layout (Optimized)'); + + const getZoneWeight = (x: number, z: number): number => { + const distFromCenter = Math.sqrt((x - mapSize/2)**2 + (z - mapSize/2)**2); + const normalizedDist = distFromCenter / (mapSize / 2); + if (normalizedDist > 0.8) return 0.9; + if (normalizedDist < 0.5) return 0.05; // Expanded center clear zone (was 0.3 / 0.1) + return 0.4; + }; + + const reservedPaths = new Set(); + const pathCount = 4 + Math.floor(rand() * 2); // Increased paths + + for (let i = 0; i < pathCount; i++) { + const startSide = Math.floor(rand() * 4); + const endSide = (startSide + 2) % 4; + const getEdgePoint = (side: number) => { + if (side === 0) return { x: Math.floor(rand() * mapSize), z: 0 }; + if (side === 1) return { x: mapSize - 1, z: Math.floor(rand() * mapSize) }; + if (side === 2) return { x: Math.floor(rand() * mapSize), z: mapSize - 1 }; + return { x: 0, z: Math.floor(rand() * mapSize) }; + }; + let curr = getEdgePoint(startSide); + const target = getEdgePoint(endSide); + const pathWidth = 5 + Math.floor(rand() * 3); // Wider paths + let safety = 0; + while (Math.abs(curr.x - target.x) + Math.abs(curr.z - target.z) > 4 && safety++ < mapSize * 3) { + const dx = Math.sign(target.x - curr.x); + const dz = Math.sign(target.z - curr.z); + if (rand() > 0.2) { + if (Math.abs(target.x - curr.x) > Math.abs(target.z - curr.z)) curr.x += dx; + else curr.z += dz; + } else { + if (rand() > 0.5) curr.x += (rand() > 0.5 ? 1 : -1); + else curr.z += (rand() > 0.5 ? 1 : -1); + } + curr.x = Math.max(0, Math.min(mapSize - 1, curr.x)); + curr.z = Math.max(0, Math.min(mapSize - 1, curr.z)); + for (let px = -Math.floor(pathWidth/2); px <= Math.floor(pathWidth/2); px++) { + for (let pz = -Math.floor(pathWidth/2); pz <= Math.floor(pathWidth/2); pz++) { + const rx = curr.x + px; + const rz = curr.z + pz; + if (rx >= 0 && rx < mapSize && rz >= 0 && rz < mapSize) { + reservedPaths.add(`${rx}|${rz}`); + } + } + } + } + } + + let totalGobiCells = 0; + const maxGobiCells = Math.floor(mapSize * mapSize * 0.22); // Reduced from 30% + const checkLimit = () => totalGobiCells >= maxGobiCells; + + // Rule 1: Minimum 2 mesas (guarantee diversity even on small maps) + const targetMesas = Math.max(2, Math.floor(mapSize / 5)); + + let placedMesas = 0; + let attempts = 0; + + // Helper to try placing a mesa + const tryPlaceMesa = (cx: number, cz: number, forced: boolean = false): boolean => { + if (reservedPaths.has(`${cx}|${cz}`) && !forced) return false; + + // Weight check + if (!forced) { + const weight = getZoneWeight(cx, cz); + if (rand() > weight) return false; + } + + const distFromCenter = Math.sqrt((cx - mapSize/2)**2 + (cz - mapSize/2)**2); + const normalizedDist = distFromCenter / (mapSize / 2); + const isEdge = normalizedDist > 0.7; + + const maxSingleRadius = mapSize * 0.10; + let radius = isEdge ? 2.5 + rand() * 2.5 : 1.5 + rand() * 1.5; + + // Rule 2: Smaller mesas for small maps (<=16) + if (mapSize <= 16) { + radius = 1.5 + rand() * 1.0; // Max 2.5 + } + + radius = Math.min(radius, maxSingleRadius); + const height = isEdge ? 4 + Math.floor(rand() * 2) : 3 + Math.floor(rand() * 2); + const safetyRadius = Math.ceil(radius) + 3; + + let areaOccupied = false; + for (let dx = -safetyRadius; dx <= safetyRadius; dx++) { + for (let dz = -safetyRadius; dz <= safetyRadius; dz++) { + const nx = cx + dx; + const nz = cz + dz; + if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { + if (gobiHeight[nx][nz] > 0) { + areaOccupied = true; + break; + } + } + } + if (areaOccupied) break; + } + if (areaOccupied) return false; + + const added = spawnMesa(cx, cz, radius, radius, height, mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand, checkLimit); + totalGobiCells += added; + return added > 0; + }; + + while (placedMesas < targetMesas && attempts < 200 && !checkLimit()) { + attempts++; + let cx = Math.floor(rand() * mapSize); + let cz = Math.floor(rand() * mapSize); + + // Rule 2 (Cont.): Edge Bias logic (stronger for small maps) + if (mapSize <= 16) { + // 80% chance to pick from outer ring if map is small + if (rand() < 0.8) { + const side = Math.floor(rand() * 4); + const depth = Math.floor(rand() * 3); // 0-2 from edge + if (side === 0) { cx = Math.floor(rand() * mapSize); cz = depth; } + else if (side === 1) { cx = mapSize - 1 - depth; cz = Math.floor(rand() * mapSize); } + else if (side === 2) { cx = Math.floor(rand() * mapSize); cz = mapSize - 1 - depth; } + else { cx = depth; cz = Math.floor(rand() * mapSize); } + } + } + + if (tryPlaceMesa(cx, cz)) { + placedMesas++; + } + } + + // Rule 3: Fallback / Guarantee Loop + // If we haven't met the minimum (2), force placement by scanning edges + let fallbackAttempts = 0; + while (placedMesas < 2 && fallbackAttempts < 100 && !checkLimit()) { + fallbackAttempts++; + // Try random edge positions explicitly ignoring weight + const side = Math.floor(rand() * 4); + const depth = Math.floor(rand() * 3); // 0-2 from edge + let cx, cz; + + if (side === 0) { cx = Math.floor(rand() * mapSize); cz = depth; } + else if (side === 1) { cx = mapSize - 1 - depth; cz = Math.floor(rand() * mapSize); } + else if (side === 2) { cx = Math.floor(rand() * mapSize); cz = mapSize - 1 - depth; } + else { cx = depth; cz = Math.floor(rand() * mapSize); } + + // Force logic: ignore zone weight, rely only on physical space check + if (tryPlaceMesa(cx, cz, true)) { + placedMesas++; + } + } + + const miniTarget = Math.floor(mapSize / 5); + let placedMinis = 0; + attempts = 0; + while (placedMinis < miniTarget && attempts < 50 && !checkLimit()) { + attempts++; + const cx = Math.floor(rand() * mapSize); + const cz = Math.floor(rand() * mapSize); + if (reservedPaths.has(`${cx}|${cz}`)) continue; + if (gobiHeight[cx][cz] === 0) { + let tooClose = false; + for(let dx=-3; dx<=3; dx++) { + for(let dz=-3; dz<=3; dz++) { + const nx = cx+dx; + const nz = cz+dz; + if(nx>=0 && nx=0 && nz0) { + tooClose=true; break; + } + } + if(tooClose) break; + } + if(tooClose) continue; + const added = spawnMiniMesa(cx, cz, 1.5 + rand(), 2, mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand, checkLimit); + totalGobiCells += added; + if (added > 0) placedMinis++; + } + } +}; + +export const createDesertContext = ( + mapSize: number, + rand: () => number +): DesertContext => { + const gobiHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + const gobiVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + const gobiMinHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + const stoneHeight = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + const stoneVariant = Array.from({ length: mapSize }, () => Array(mapSize).fill(0)); + + layoutStrategicDesert(mapSize, gobiHeight, gobiVariant, gobiMinHeight, rand); + + // Adjusted density: Ensure presence on 24/32 maps, occasional on 16 + // 32x32 (1024) / 200 = ~5 stones + // 24x24 (576) / 200 = ~2-3 stones + // 16x16 (256) / 200 = ~1.2 -> handled specifically below + let targetStones = Math.floor((mapSize * mapSize) / 200); + + if (mapSize <= 16) { + // For 16x16: 30% chance to spawn 1 stone, otherwise 0 + targetStones = rand() > 0.7 ? 1 : 0; + } + + let placedStones = 0; + let attempts = 0; + // Give plenty of attempts because the strict avoidance check will reject many positions + const maxAttempts = Math.max(20, targetStones * 50); + + while (placedStones < targetStones && attempts < maxAttempts) { + attempts++; + const startX = Math.floor(rand() * mapSize); + const startZ = Math.floor(rand() * mapSize); + + // Constraint: Max 3 blocks (1-3) + const targetSize = 1 + Math.floor(rand() * 3); + + // Algorithm: Random Walker + const rockCells: Array<{x: number, z: number}> = [{x: startX, z: startZ}]; + + let safety = 0; + while (rockCells.length < targetSize && safety++ < 10) { + const seed = rockCells[Math.floor(rand() * rockCells.length)]; + const dir = Math.floor(rand() * 4); + const dx = dir === 0 ? 1 : dir === 1 ? -1 : 0; + const dz = dir === 2 ? 1 : dir === 3 ? -1 : 0; + const nx = seed.x + dx; + const nz = seed.z + dz; + + if (nx >= 0 && nx < mapSize && nz >= 0 && nz < mapSize) { + if (!rockCells.some(c => c.x === nx && c.z === nz)) { + rockCells.push({x: nx, z: nz}); + } + } + } + + let isValid = true; + const checkDist = 2; + + for (const cell of rockCells) { + for (let dx = -checkDist; dx <= checkDist; dx++) { + for (let dz = -checkDist; dz <= checkDist; dz++) { + const cx = cell.x + dx; + const cz = cell.z + dz; + + if (cx >= 0 && cx < mapSize && cz >= 0 && cz < mapSize) { + if (gobiHeight[cx][cz] > 0) { + isValid = false; + break; + } + if (stoneHeight[cx][cz] > 0) { + isValid = false; + break; + } + } + } + if (!isValid) break; + } + if (!isValid) break; + } + + if (isValid) { + const h = (targetSize > 1 && rand() > 0.85) ? 2 : 1; + const variant = Math.floor(rand() * 2); + + for (const cell of rockCells) { + stoneHeight[cell.x][cell.z] = h; + stoneVariant[cell.x][cell.z] = variant; + } + placedStones++; + } + } + + const streamDepthMap = generateDesertStreamMap(mapSize, rand, gobiHeight, stoneHeight); + return { + gobiHeight, + gobiVariant, + gobiMinHeight, + stoneHeight, + stoneVariant, + streamDepthMap, + }; +}; + +import { generateDesertRiver } from './waterSystem'; + +export const generateDesertStreamMap = ( + mapSize: number, + rand: () => number, + gobiHeight: number[][], + stoneHeight: number[][] +) => { + // 使用新的基于微体素 A* 算法的河流生成系统 + // 默认 MICRO_SCALE 为 8 (由 waterSystem 内部默认值处理) + return generateDesertRiver(mapSize, gobiHeight, stoneHeight, rand); +}; + +export const getGobiLayerType = (absoluteHeight: number, _stackHeight: number, _variant: number): VoxelType => { + if (absoluteHeight >= 5) { + return 'gobi_peak'; + } else if (absoluteHeight === 4) { + return 'gobi_top'; + } else if (absoluteHeight === 3) { + return 'gobi_upper'; + } else if (absoluteHeight === 2) { + return 'gobi_lower'; + } else { + return 'gobi_base'; + } +}; + +export const getStoneLayerType = (_depthFromTop: number, variant: number): VoxelType => { + return variant % 2 === 0 ? 'stone' : 'dark_stone'; +}; diff --git a/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts new file mode 100644 index 0000000..4402cda --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/newVegetation.ts @@ -0,0 +1,678 @@ +import type { VoxelData } from './terrain'; +import { rgbToHex } from './voxelStyles'; + +/** + * 新的植被生成系统 + * 整合了多个生物群系的植被生成逻辑 (Desert, Forest, Ice, Swamp, Beach, Boreal) + * 分辨率: 32x32 (TREE_MICRO_SCALE) + */ + +export interface VegetationGenerationContext { + mapSize: number; + voxels: VoxelData[]; + occupancy: Set; + treePositions: Array<{ x: number, z: number }>; + seed: number; + desertContext?: any; + isDesertScene: boolean; + sceneType?: string; + heightMap: Map; +} + +// ========================================= +// --- 1. 统一颜色调色板 --- +// ========================================= +const C: Record = { + // Desert (Balanced greens) + gDarkest: '#24382A', gDark: '#3E5E48', gMid: '#628A52', gLight: '#8FB371', + gGrey: '#7A8F82', gLime: '#B5D178', gTeal: '#587878', + woodD: '#5C4A35', woodL: '#9E8C68', + sand: '#D6CFA3', red: '#D94141', pink: '#D97998', yellow: '#EBC944', + orange: '#F29F05', white: '#F0F0F0', rock: '#8A867D', + crystal: '#66CCFF', bone: '#E3E1CD', water: '#4FA4F4', + + // Temperate Forest (Balanced greens) + leafDeep: '#3E6B42', leafMid: '#4E7A45', leafLight: '#6A9655', + pineD: '#385E4B', pineL: '#567D65', + willow: '#7E9E64', poplar: '#68994D', + woodOak: '#6B503B', woodBirch: '#E8E8E0', spot: '#3B3B3B', + dirt: '#755E49', grass: '#6FA648', stone: '#888C8D', + + // Ice/Tundra (Balanced greens) + iceBase: '#E0FFFF', iceDeep: '#AEEEEE', iceSpike: '#F0FFFF', + snow: '#FFFFFF', + lichenOrange: '#D98E32', lichenRed: '#A64B4B', lichenGreen: '#6B855E', + frozenGreen: '#92C4B5', frozenStem: '#56757A', + crystalBlue: '#7FFFD4', crystalWhite: '#EFFFFF', + coralBlue: '#55DDFF', + pineDeepBoreal: '#305240', pineMidBoreal: '#456E56', pineLightBoreal: '#608A6E', + larchYellow: '#D9A036', frozenBlue: '#A3C9D9', + birchWhite: '#F0F0F2', birchSpot: '#2A2A2E', woodDark: '#4D3D30', woodRed: '#6B4E40', + rockGrey: '#6E7578', rockMoss: '#657A42', + mossLichen: '#9DAF7D', berryRed: '#C93636', berryBlue: '#364C85', + cottonWhite: '#FDFDFD', grassDry: '#8C8060', + + // Swamp (Balanced greens) + leafDarkSwamp: '#354D40', leafMurky: '#4C6B58', leafLightSwamp: '#6D8C6E', + moss: '#6A8252', spanishMoss: '#95A399', + woodWet: '#4A4238', woodRot: '#63584C', + mud: '#3B3029', mudDry: '#54463A', + reedGreen: '#63783E', reedBrown: '#543D33', + lilypad: '#577A30', flowerLotus: '#D9AAB7', + mushroomCap: '#8A4B3A', mushroomGill: '#D6C29E', + fungusPurple: '#5D456B', + + // Beach (Balanced greens) + palmGreen: '#68B028', palmDark: '#45851E', + bananaLeaf: '#7FB539', bananaStem: '#A3B35C', + trunkPalm: '#966838', trunkGrey: '#A89C8F', trunkDark: '#664A3C', + rootRed: '#96501E', + cactusGreen: '#62944B', cactusLight: '#86B56A', cactusFlower: '#FF7777', + flowerBird: '#FF8800', flowerPurple: '#9933CC', + cycadTrunk: '#54463A', cycadLeaf: '#3C9663', + coconut: '#664A3C' +}; + +const darkenHex = (hex: string, factor: number): string => { + if (!hex) return '#000000'; + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return rgbToHex(r * factor, g * factor, b * factor); +}; + +// ========================================= +// --- 2. Voxel Builder & Helpers --- +// ========================================= + +type AddBlockFn = (dx: number, dy: number, dz: number, color: string) => void; + +class VoxelBuilder { + private addBlock: AddBlockFn; + + constructor(addBlock: AddBlockFn) { + this.addBlock = addBlock; + } + + add(x: number, y: number, z: number, hexColor: string) { + let darkFactor = 1.0; + // Simple AO / shading + if (y < 1) darkFactor = 0.85; + if (y < 0.5) darkFactor = 0.7; + + const finalColor = darkFactor < 1.0 ? darkenHex(hexColor, darkFactor) : hexColor; + this.addBlock(Math.round(x), Math.round(y), Math.round(z), finalColor); + } +} + +// Basic Shapes +const drawStem = (builder: VoxelBuilder, x: number, y: number, z: number, h: number, r: number, cBase: string, cHigh: string, isRoot: boolean = false) => { + let r2 = r * r; + let inR2 = (r - 0.9) ** 2; + for (let yi = 0; yi < h; yi++) { + let isCap = (yi === Math.floor(h) - 1); + for (let xi = -Math.ceil(r); xi <= Math.ceil(r); xi++) { + for (let zi = -Math.ceil(r); zi <= Math.ceil(r); zi++) { + let d2 = xi * xi + zi * zi; + if (d2 <= r2) { + if (!isCap && d2 < inR2) continue; + let col = (xi + zi) % 2 === 0 ? cHigh : cBase; + if (isRoot && yi < 2) col = C.woodL; + builder.add(x + xi, y + yi, z + zi, col); + } + } + } + } +}; + +const drawSphere = (builder: VoxelBuilder, ox: number, oy: number, oz: number, r: number, cBase: string, cVar: string, stretchY: number = 1, noise: number = 0, coreRatio: number = 0.0) => { + let r2 = r * r; + // Use coreRatio to allow solid cores (from qiulin.html) + // If coreRatio > 0, we fill the center. + // Default existing logic was hollow-ish or random. + + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let y = -Math.ceil(r * stretchY); y <= Math.ceil(r * stretchY); y++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + let d2 = x * x + (y / stretchY) ** 2 + z * z; + let isCore = coreRatio > 0 && d2 < r2 * coreRatio; + let isEdge = d2 <= r2 + (Math.random() - 0.5) * noise * r2; + + if (isCore || isEdge) { + builder.add(ox + x, oy + y, oz + z, Math.random() > 0.8 ? cVar : cBase); + } + } + } + } +}; + +const drawCylinder = (builder: VoxelBuilder, ox: number, oy: number, oz: number, h: number, r: number, col: string, isBirch: boolean = false) => { + let r2 = r * r; + for (let y = 0; y < h; y++) { + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r2) { + let c = col; + if (isBirch && (x * x + z * z) > (r2 * 0.6) && Math.random() > 0.85) c = C.birchSpot; + builder.add(ox + x, oy + y, oz + z, c); + } + } + } + } +}; + +// Advanced Shapes (from imports) +const drawCloudCluster = (builder: VoxelBuilder, ox: number, oy: number, oz: number, mainR: number, count: number, subRMin: number, subRMax: number, cBase: string, cVar: string) => { + // Center sphere + drawSphere(builder, ox, oy, oz, mainR, cBase, cVar, 0.9, 0.1, 0.9); + // Sub spheres + for (let i = 0; i < count; i++) { + let angPhi = Math.random() * Math.PI * 2; + let angTheta = Math.random() * Math.PI; + let dist = mainR * (0.5 + Math.random() * 0.4); + + let cx = ox + Math.sin(angTheta) * Math.cos(angPhi) * dist; + let cy = oy + Math.cos(angTheta) * dist * 0.8; + let cz = oz + Math.sin(angTheta) * Math.sin(angPhi) * dist; + + let r = subRMin + Math.random() * (subRMax - subRMin); + drawSphere(builder, Math.round(cx), Math.round(cy), Math.round(cz), r, cBase, cVar, 1, 0.3, 0.8); + } +}; + +const createMudIsland = (builder: VoxelBuilder, ox: number, oz: number, r: number) => { + let h = 2; + for (let y = 0; y < h; y++) { + let currentR = r * (1 - y / (h + 1)); + for (let x = -Math.ceil(currentR); x <= Math.ceil(currentR); x++) { + for (let z = -Math.ceil(currentR); z <= Math.ceil(currentR); z++) { + if (x * x + z * z <= currentR * currentR) { + builder.add(ox + x, y, oz + z, Math.random() > 0.7 ? C.mudDry : C.mud); + } + } + } + } +}; + +// ========================================= +// --- 3. Generators by Biome --- +// ========================================= + +// --- DESERT (Original + Expanded) --- +const createJointedSaguaroBase = (builder: VoxelBuilder, ox: number, oz: number, config: any) => { + const { h, r, armCount, armStyle } = config; + drawStem(builder, ox, 0, oz, h, r, C.gDark, C.gMid, true); + for (let xi = -Math.ceil(r); xi <= Math.ceil(r); xi++) { + for (let zi = -Math.ceil(r); zi <= Math.ceil(r); zi++) { + if (xi * xi + zi * zi <= r * r) builder.add(ox + xi, h, oz + zi, C.gDarkest); + } + } + for (let i = 0; i < armCount; i++) { + let angle = (Math.PI * 2 * i) / armCount + (armStyle === 'wild' ? Math.random() : 0); + let startY = h * (armStyle === 'low' ? 0.3 : armStyle === 'high' ? 0.7 : 0.5) + (Math.random() - 0.5) * 4; + let elbowLen = (armStyle === 'wild' ? 4 + Math.random() * 4 : 3 + Math.random() * 2); + let armH = (armStyle === 'goliath' ? 6 : h * 0.4 + Math.random() * 4); + let armR = r * (armStyle === 'goliath' ? 0.6 : 0.7); + let cos = Math.cos(angle), sin = Math.sin(angle); + for (let d = r - 0.5; d <= r + elbowLen; d += 0.5) { + let ex = ox + cos * d, ez = oz + sin * d; + drawStem(builder, ex, startY, ez, 1.5, armR * 0.9, C.woodL, C.woodL, false); + } + let tipX = ox + cos * (r + elbowLen), tipZ = oz + sin * (r + elbowLen); + drawStem(builder, tipX, startY, tipZ, armH, armR, C.gDark, C.gMid, false); + for (let xi = -Math.ceil(armR); xi <= Math.ceil(armR); xi++) { + for (let zi = -Math.ceil(armR); zi <= Math.ceil(armR); zi++) { + if (xi * xi + zi * zi <= armR * armR) builder.add(tipX + xi, startY + armH, tipZ + zi, C.gDarkest); + } + } + } +}; +const createJointedClassic = (b: VoxelBuilder, x: number, z: number) => createJointedSaguaroBase(b, x, z, { h: 24, r: 2.2, armCount: 2, armStyle: 'balanced' }); +const createJointedCandelabra = (b: VoxelBuilder, x: number, z: number) => createJointedSaguaroBase(b, x, z, { h: 28, r: 3, armCount: 4, armStyle: 'low' }); +const createSaguaro = (b: VoxelBuilder, ox: number, oz: number) => { + drawStem(b, ox, 0, oz, 24, 2, C.gDark, C.gMid, true); + drawSphere(b, ox, 24, oz, 2, C.gDark, C.gMid, 0.6); +}; +// (Omitting some minor desert variations for brevity, keeping the impressive ones) + +// --- TEMPERATE FOREST (Qiulin) --- +const createLushOak = (builder: VoxelBuilder, ox: number, oz: number) => { + drawCylinder(builder, ox, 0, oz, 7, 2, C.woodOak); + drawSphere(builder, ox, 0, oz, 4, C.dirt, C.grass, 0.4); + drawCloudCluster(builder, ox, 8, oz, 4.5, 12, 2.5, 4.0, C.leafDeep, C.leafLight); +}; +const createFullBirch = (builder: VoxelBuilder, ox: number, oz: number) => { + let h = 14; + let lean = (Math.random() - 0.5) * 2; + for (let y = 0; y < h; y++) { + let dx = Math.round(lean * (y / h)); + builder.add(ox + dx, y, oz, Math.random() > 0.85 ? C.spot : C.woodBirch); + if (y > 1 && y < 4) { + builder.add(ox + dx + 1, y, oz, C.woodBirch); + builder.add(ox + dx, y, oz + 1, C.woodBirch); + } + } + drawSphere(builder, ox + Math.round(lean), h - 1, oz, 4.5, C.poplar, C.leafLight, 1.2, 0.2, 0.9); +}; +const createDensePine = (builder: VoxelBuilder, ox: number, oz: number) => { + drawCylinder(builder, ox, 0, oz, 4, 1.5, C.woodOak); + let startY = 3; + let layers = 4; + for (let i = 0; i < layers; i++) { + let y = startY + i * 3.5; + let bottomR = 5.5 - i * 1.2; + let topR = 3.5 - i * 0.8; + let height = 4; + for (let ly = 0; ly < height; ly++) { + let r = bottomR + (topR - bottomR) * (ly / height); + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r * r) { + if (x * x + z * z < r * r * 0.8 || Math.random() > 0.3) + builder.add(ox + x, Math.round(y + ly), oz + z, C.pineD); + } + } + } + } + } + drawCylinder(builder, ox, startY + layers * 3.5, oz, 3, 1, C.pineD); + builder.add(ox, startY + layers * 3.5 + 3, oz, C.pineL); +}; +const createCurtainWillow = (builder: VoxelBuilder, ox: number, oz: number) => { + drawCylinder(builder, ox, 0, oz, 5, 1.5, C.woodOak); + drawSphere(builder, ox, 6, oz, 5, C.willow, C.leafLight, 0.7, 0.1, 0.95); + for (let i = 0; i < 20; i++) { + let ang = (i / 20) * Math.PI * 2; + let dist = 3.5 + Math.random(); + let sx = ox + Math.cos(ang) * dist; + let sz = oz + Math.sin(ang) * dist; + let len = 4 + Math.random() * 5; + for (let y = 0; y < len; y++) { + let px = Math.round(sx + (Math.random() - 0.5) * 0.5); + let pz = Math.round(sz + (Math.random() - 0.5) * 0.5); + let py = 6 - y; + if (py < 0) continue; + builder.add(px, py, pz, C.willow); + if (Math.random() > 0.6) builder.add(px, py, pz + 1, C.willow); + } + } +}; +const createFatPoplar = (builder: VoxelBuilder, ox: number, oz: number) => { + drawCylinder(builder, ox, 0, oz, 3, 1, C.woodOak); + let h = 16; + for (let y = 0; y < h; y++) { + let r = 2.2 * Math.sin((y / h) * Math.PI); + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r * r) { + builder.add(ox + x, y + 2, oz + z, C.poplar); + } + } + } + } +}; + +// --- SWAMP (Zaoze) --- +const createCypress = (builder: VoxelBuilder, ox: number, oz: number) => { + createMudIsland(builder, ox, oz, 4.5); + let h = 16; + for (let y = 0; y < 8; y++) { + let r = 4.5 * Math.pow(0.7, y); + if (r < 1.5) r = 1.5; + drawCylinder(builder, ox, y + 1, oz, 1, r, C.woodWet); + } + drawCylinder(builder, ox, 9, oz, 8, 1.5, C.woodWet); + let crownY = 16; + drawSphere(builder, ox, crownY, oz, 5, C.leafDarkSwamp, C.leafMurky, 0.6, 0.3, 0.7); + for (let i = 0; i < 12; i++) { + let ang = Math.random() * Math.PI * 2; + let dist = 3 + Math.random() * 2; + let x = Math.round(ox + Math.cos(ang) * dist); + let z = Math.round(oz + Math.sin(ang) * dist); + let len = 4 + Math.random() * 5; + for (let y = 0; y < len; y++) { + builder.add(x, crownY - 1 - y, z, C.spanishMoss); + } + } + // Knees + for (let i = 0; i < 5; i++) { + let ang = Math.random() * Math.PI * 2; + let dist = 5 + Math.random() * 3; + let kx = Math.round(ox + Math.cos(ang) * dist); + let kz = Math.round(oz + Math.sin(ang) * dist); + let kh = 1 + Math.random() * 2; + builder.add(kx, 0, kz, C.woodWet); + for (let ky = 0; ky < kh; ky++) builder.add(kx, 1 + ky, kz, C.woodWet); + } +}; +const createSwampWillow = (builder: VoxelBuilder, ox: number, oz: number) => { + createMudIsland(builder, ox, oz, 3); + drawCylinder(builder, ox, 1, oz, 6, 1.5, C.woodRot); + drawSphere(builder, ox, 7, oz, 4, C.leafMurky, C.spanishMoss, 0.8, 0.2, 0.7); + for (let i = 0; i < 20; i++) { + let ang = (i / 20) * Math.PI * 2; + let dist = 3.5; + let x = Math.round(ox + Math.cos(ang) * dist); + let z = Math.round(oz + Math.sin(ang) * dist); + for (let y = 7; y >= 1; y--) { + if (Math.random() > 0.2) builder.add(x, y, z, C.leafDarkSwamp); + } + } +}; +const createGiantMushroom = (builder: VoxelBuilder, ox: number, oz: number) => { + createMudIsland(builder, ox, oz, 1.5); + for (let y = 1; y < 6; y++) builder.add(ox, y, oz, C.woodRot); + let r = 3; + for (let x = -r; x <= r; x++) { + for (let z = -r; z <= r; z++) { + if (x * x + z * z <= r * r) { + builder.add(ox + x, 6, oz + z, C.mushroomGill); + builder.add(ox + x, 7, oz + z, C.mushroomCap); + if (Math.random() > 0.8) builder.add(ox + x, 8, oz + z, C.mushroomCap); + } + } + } +}; + +// --- BOREAL / TUNDRA (Handai) --- +const createSpruceGeneral = (builder: VoxelBuilder, ox: number, oz: number, scale: number, snowFactor: number) => { + let rootH = Math.max(2, Math.floor(4 * scale)); + drawCylinder(builder, ox, 0, oz, rootH, 1.5 * scale, C.woodDark); + let layers = Math.max(3, Math.floor(7 * scale)); + let currentY = rootH - 1; + let radius = 5 * scale; + for (let i = 0; i < layers; i++) { + let layerH = Math.max(2, Math.floor(3 * scale)); + for (let y = 0; y < layerH; y++) { + let r = radius * (1 - y / (layerH + 0.5)); + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r * r) { + let col = Math.random() > 0.7 ? C.pineMidBoreal : C.pineDeepBoreal; + if (snowFactor > 0) { + if (y == layerH - 1 || (x * x + z * z > r * r * 0.7)) if (Math.random() < snowFactor) col = C.snow; + } + builder.add(ox + x, currentY + y, oz + z, col); + } + } + } + } + currentY += (layerH - 1); + radius -= (0.6 * scale); + } + let topCol = snowFactor > 0.5 ? C.snow : C.pineLightBoreal; + builder.add(ox, currentY, oz, topCol); + builder.add(ox, currentY + 1, oz, topCol); +}; +const createSpruceTallSnowy = (b: VoxelBuilder, x: number, z: number) => createSpruceGeneral(b, x, z, 1.0, 0.7); +const createSpruceMediumClear = (b: VoxelBuilder, x: number, z: number) => createSpruceGeneral(b, x, z, 0.7, 0.0); +const createLarch = (builder: VoxelBuilder, ox: number, oz: number) => { + drawCylinder(builder, ox, 0, oz, 18, 1.2, C.woodRed); + let sy = 6; + for (let i = 0; i < 20; i++) { + let y = sy + Math.random() * 12, a = Math.random() * 6.28, l = (18 - y) * 0.3 + 1; + let ex = ox + Math.cos(a) * l, ez = oz + Math.sin(a) * l; + builder.add(Math.round(ex), Math.round(y), Math.round(ez), C.larchYellow); + builder.add(Math.round(ex), Math.round(y) + 1, Math.round(ez), C.snow); + } + for (let y = 0; y < 3; y++) builder.add(ox, 18 + y, oz, C.larchYellow); +}; + +// --- ICEBERG (Iceland) --- +const createCoralTree = (builder: VoxelBuilder, ox: number, oz: number) => { + const branch = (sx: number, sy: number, sz: number, len: number, dirX: number, dirZ: number, width: number) => { + if (len < 2) return; + let steps = Math.floor(len); + let cx = sx, cy = sy, cz = sz; + for (let i = 0; i < steps; i++) { + let w = Math.ceil(width); + for (let wx = -Math.floor(w / 2); wx < Math.ceil(w / 2); wx++) { + for (let wz = -Math.floor(w / 2); wz < Math.ceil(w / 2); wz++) { + let col = cy < 6 ? C.coralBlue : C.iceSpike; + if (Math.random() > 0.9) col = C.crystalWhite; + builder.add(Math.round(cx + wx), Math.round(cy), Math.round(cz + wz), col); + } + } + cy += 1; cx += dirX * 0.5; cz += dirZ * 0.5; + } + if (width > 0.5) { + branch(cx, cy, cz, len * 0.7, dirX + 0.5, dirZ + 0.5, width * 0.6); + branch(cx, cy, cz, len * 0.7, dirX - 0.5, dirZ - 0.5, width * 0.6); + } + }; + builder.add(ox, 0, oz, C.coralBlue); + branch(ox, 0, oz, 8, 0, 0, 2); +}; +const createIceSpike = (builder: VoxelBuilder, ox: number, oz: number) => { + let h = 4 + Math.random() * 4; + for (let y = 0; y < h; y++) { + let r = (h - y) * 0.4; + for (let x = -Math.ceil(r); x <= Math.ceil(r); x++) { + for (let z = -Math.ceil(r); z <= Math.ceil(r); z++) { + if (x * x + z * z <= r * r) builder.add(ox + x, y, oz + z, C.iceSpike); + } + } + } +}; + +// --- BEACH (Tropical) --- +const createPalm = (builder: VoxelBuilder, ox: number, oz: number, height: number, leanType: 'straight' | 'slight') => { + let leanX = 0, leanZ = 0; + if (leanType === 'straight') { + leanX = (Math.random() - 0.5) * 2; leanZ = (Math.random() - 0.5) * 2; + } else { + let ang = Math.random() * 6.28; leanX = Math.cos(ang) * 5; leanZ = Math.sin(ang) * 5; + } + let cx = ox, cz = oz; + for (let y = 0; y < height; y++) { + cx += leanX / height; cz += leanZ / height; + let col = (y % 2 == 0) ? C.trunkPalm : C.trunkDark; + builder.add(Math.round(cx), y, Math.round(cz), col); + if (y < height - 2) { + builder.add(Math.round(cx) + 1, y, Math.round(cz), col); + builder.add(Math.round(cx), y, Math.round(cz) + 1, col); + } + } + // Coconuts + for (let i = 0; i < 4; i++) builder.add(Math.round(cx + (Math.random() - 0.5) * 2), height - 1, Math.round(cz + (Math.random() - 0.5) * 2), C.coconut); + // Fronds + let fronds = 12; + for (let i = 0; i < fronds; i++) { + let a = i / fronds * 6.28; + let len = 8 + Math.random() * 4; + for (let d = 0; d < len; d++) { + let fx = cx + Math.cos(a) * d; + let fz = cz + Math.sin(a) * d; + let fy = height + 2 - d * d * 0.06; + if (d > 1) { + builder.add(Math.round(fx), Math.round(fy), Math.round(fz), C.palmGreen); + builder.add(Math.round(fx), Math.round(fy), Math.round(fz) + 1, C.palmGreen); + } + } + } +}; +const createTwinPalms = (b: VoxelBuilder, x: number, z: number) => { + drawCylinder(b, x, 0, z, 2, 2.5, C.trunkPalm); + createPalm(b, x - 1, z, 14, 'slight'); + createPalm(b, x + 1, z, 22, 'slight'); +}; +const createMangrove = (builder: VoxelBuilder, ox: number, oz: number) => { + let rootH = 6; + for (let i = 0; i < 8; i++) { + let ang = (i / 8) * Math.PI * 2; + let dist = 3.5; + for (let h = 0; h < rootH; h++) { + let t = h / rootH; + let lx = Math.round(ox + Math.cos(ang) * dist * (1 - t)); + let lz = Math.round(oz + Math.sin(ang) * dist * (1 - t)); + builder.add(lx, h, lz, C.rootRed); + builder.add(lx + 1, h, lz, C.rootRed); + builder.add(lx, h, lz + 1, C.rootRed); + } + } + drawCylinder(builder, ox, rootH, oz, 4, 2, C.rootRed); + drawSphere(builder, ox, rootH + 4, oz, 5.5, C.palmDark, C.palmGreen, 0.8, 0.3, 0.8); +}; +const createBanana = (builder: VoxelBuilder, ox: number, oz: number) => { + let h = 8; + drawCylinder(builder, ox, 0, oz, h, 1, C.bananaStem); + let leaves = 5; + for (let i = 0; i < leaves; i++) { + let ang = i / leaves * 6.28; + let len = 7; + for (let d = 0; d < len; d++) { + let lx = ox + Math.cos(ang) * d; + let lz = oz + Math.sin(ang) * d; + let ly = h + d * 0.5; + if (d > len / 2) ly = h + len * 0.5 - (d - len / 2); + let w = 3; + for (let k = -Math.floor(w / 2); k <= Math.floor(w / 2); k++) { + let px = lx - Math.sin(ang) * k; + let pz = lz + Math.cos(ang) * k; + builder.add(Math.round(px), Math.round(ly), Math.round(pz), C.bananaLeaf); + } + } + } +}; + +// ========================================= +// --- 4. Generator Registration --- +// ========================================= + +const DESERT_GENERATORS = [createJointedClassic, createJointedCandelabra, createSaguaro]; +const FOREST_GENERATORS = [createLushOak, createFullBirch, createDensePine, createCurtainWillow, createFatPoplar]; +const SWAMP_GENERATORS = [createCypress, createSwampWillow, createGiantMushroom]; +const TUNDRA_GENERATORS = [createSpruceTallSnowy, createSpruceMediumClear, createLarch]; +const ICE_GENERATORS = [createCoralTree, createIceSpike]; +const BEACH_GENERATORS = [createTwinPalms, createMangrove, createBanana, (b: any, x: any, z: any) => createPalm(b, x, z, 20, 'straight')]; + +// ========================================= +// --- 5. Main Generation Logic --- +// ========================================= + +export const generateVegetation = async ( + context: VegetationGenerationContext +): Promise => { + const { mapSize, occupancy, sceneType, desertContext, heightMap } = context; + const newVoxels: VoxelData[] = []; + + const TILE_SIZE = 1; + const TREE_VOXEL_SIZE = TILE_SIZE / 32; + + const addVoxel = (ix: number, iy: number, iz: number, color: string) => { + const key = `tree_${ix}|${iy}|${iz}`; + if (occupancy.has(key)) return; + newVoxels.push({ + x: ix * TREE_VOXEL_SIZE, + y: iy * TREE_VOXEL_SIZE, + z: iz * TREE_VOXEL_SIZE, + type: 'leaves', // Generic type, color dictates look + color: color, + ix, iy, iz, + heightScale: 1, + isHighRes: true + }); + occupancy.add(key); + }; + + const builder = new VoxelBuilder(addVoxel); + let generators: any[] = []; + let density = 0.05; + + // Determine generators and density based on scene + // Map legacy names if necessary + switch (sceneType) { + case 'desert': + generators = DESERT_GENERATORS; + density = 0.05; // Sparse + break; + case 'mountain': // Mapping Qiulin/Temperate to Mountain/Plains + case 'plains': + generators = FOREST_GENERATORS; + density = 0.15; // Dense forest + break; + case 'swamp': + generators = SWAMP_GENERATORS; + density = 0.2; // Very dense + break; + case 'tundra': + case 'snowy_mountain': + generators = [...TUNDRA_GENERATORS, ...ICE_GENERATORS]; + density = 0.1; + break; + case 'beach': + case 'riverside': + generators = BEACH_GENERATORS; + density = 0.12; + break; + default: + // Fallback to forest for unknown types, or empty + generators = FOREST_GENERATORS; + density = 0.1; + break; + } + + console.log(`Generating vegetation for scene: ${sceneType}, density: ${density}`); + + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + // Check valid placement + // For desert: check stone/gobi/stream + // For others: check if surface is appropriate (water/stone checks) + + // Retrieve context data + const stoneHeight = desertContext?.stoneHeight[lx]?.[lz] ?? 0; + const gobiHeight = desertContext?.gobiHeight[lx]?.[lz] ?? 0; + const centerMicroX = lx * 8 + 4; + const centerMicroZ = lz * 8 + 4; + const streamInfo = desertContext?.streamDepthMap.get(`${centerMicroX}|${centerMicroZ}`); + const isStream = (streamInfo?.depth ?? 0) > 0; + + // General exclusion rules + if (stoneHeight > 0 && sceneType !== 'tundra' && sceneType !== 'snowy_mountain') continue; // Allow trees on rocks in tundra sometimes + if (isStream && sceneType !== 'swamp') continue; // Swamp trees grow in water + + // Desert specific exclusion + if (sceneType === 'desert' && gobiHeight > 0) continue; // Avoid gobi tops + + // Get surface height + const terrainX = lx * 4 + 2; // Rough center in 32-grid (8*4=32) + const terrainZ = lz * 4 + 2; + // Map terrain 8x8 to logic... wait heightMap is 8x8 keys? + // heightMap keys are "ix|iz" where ix is 0..mapSize*8 + // We need to find the surface Y at this tile center + const hKey = `${lx*8+4}|${lz*8+4}`; + const terrainY = heightMap.get(hKey); + + if (terrainY === undefined) continue; + + // Convert to tree-grid height (32x32) + // 1 terrain unit (8 micro) = 4 tree units (32 micro) + // surfaceY in tree grid + let surfaceY = (terrainY + 1) * 4; + + // Swamp special: trees can sink a bit or grow on mud + if (sceneType === 'swamp') surfaceY -= 2; + + // Micro coordinates for tree grid (center of tile) + const microX = lx * 32 + 16; + const microZ = lz * 32 + 16; + + if (Math.random() < density) { + const gen = generators[Math.floor(Math.random() * generators.length)]; + + // Inject offset into builder + const originalAdd = builder['addBlock']; + builder['addBlock'] = (dx, dy, dz, color) => { + addVoxel(microX + dx, surfaceY + dy, microZ + dz, color); + }; + + gen(builder, 0, 0); + + builder['addBlock'] = originalAdd; + } + } + } + + return newVoxels; +}; diff --git a/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts new file mode 100644 index 0000000..01e3b2f --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/postprocessing.ts @@ -0,0 +1,790 @@ +/** + * 地形后处理函数 + * 包含各种对生成的地形进行后处理的算法 + */ + +// ==================== 类型定义 ==================== + +/** + * VoxelType 类型定义(从terrain.ts复制,保持一致) + */ +export type VoxelType = + | 'water' + | 'sand' + | 'grass' + | 'stone' + | 'snow' + | 'dirt' + | 'wood' + | 'leaves' + | 'dark_stone' + | 'bedrock' + | 'grass_dirt_blend' + | 'dark_grass' + | 'cactus' + | 'deep_dirt' + | 'dark_grass_ground' + | 'medium_dirt' + | 'flower' + | 'reed' + | 'lava' + | 'volcanic_rock' + | 'obsidian' + | 'ash' + | 'magma_stone' + | 'mud' + | 'murky_water' + | 'swamp_grass' + | 'moss' + | 'lily_pad' + | 'ice' + | 'packed_ice' + | 'frozen_stone' + | 'icicle' + | 'permafrost' + | 'gobi_base' + | 'gobi_lower' + | 'gobi_upper' + | 'gobi_top' + | 'gobi_peak' + | 'etched_sand'; + +// ==================== 工具函数 ==================== + +/** + * 伪随机数生成器(从terrain.ts复制) + */ +function pseudoRandom(seed: number): number { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); +} + +/** + * clamp 函数(从terrain.ts复制) + */ +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +interface DesertContext { + gobiHeight: number[][]; + gobiVariant: number[][]; + gobiMinHeight: number[][]; + stoneHeight: number[][]; + stoneVariant: number[][]; + streamDepthMap: Map; +} + +// ==================== 戈壁风化算法(基于迭代侵蚀)==================== + +/** + * 戈壁风化预处理结果 + * 包含应该被移除的体素集合 + */ +export interface GobiWeatheringResult { + // 使用 Set 存储应该被移除的体素坐标 "ix|iy|iz" + removedVoxels: Set; +} + +/** + * 戈壁和巨石风化预处理参数 + */ +export interface GobiWeatheringPreprocessParams { + desertContext: DesertContext; // 沙漠上下文 + mapSize: number; // 地图大小(逻辑方块数) + MICRO_SCALE: number; // 微观缩放比例 + gobiMaxIterations: number; // 戈壁最大风蚀层数,默认 3 + gobiHeightBias: number; // 戈壁高度保护阈值,默认 0.5 + stoneMaxIterations: number; // 巨石最大风蚀层数,默认 2 + stoneHeightBias: number; // 巨石高度保护阈值,默认 0.6 + stoneMinExposedFaces: number; // 巨石最少暴露面数,默认 3 + MIN_WORLD_Y: number; // 最小世界Y坐标 +} + +/** + * 戈壁风化预处理:生成整个地图的戈壁风化结果 + * 基于迭代侵蚀算法,识别暴露的边缘并逐层移除 + * + * @param params 风化预处理参数 + * @returns 风化结果,包含应该被移除的体素集合 + */ +export function preprocessGobiWeathering(params: GobiWeatheringPreprocessParams): GobiWeatheringResult { + const { + desertContext, + mapSize, + MICRO_SCALE, + gobiMaxIterations, + gobiHeightBias, + stoneMaxIterations, + stoneHeightBias, + stoneMinExposedFaces, + MIN_WORLD_Y, + } = params; + + // 计算微观网格大小 + const microMapSize = mapSize * MICRO_SCALE; + + // 预估最大世界Y坐标(关键修复:基于真实的 worldY 计算) + let maxWorldY = MIN_WORLD_Y; + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE; + const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE; + const logicHeight = 2; // 沙漠场景的基础高度 + // 计算该位置的最大可能 worldY(表面高度) + const surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight; + maxWorldY = Math.max(maxWorldY, surfaceY); + } + } + + // 基于 maxWorldY 计算网格高度(相对于 MIN_WORLD_Y) + const gridHeight = Math.ceil(maxWorldY - MIN_WORLD_Y + MICRO_SCALE * 2); + + // 创建3D网格:包含戈壁和巨石体素 + // 使用 Uint8Array 节省内存:0 = 空,1 = 戈壁体素,2 = 巨石体素 + const voxelGrid = new Uint8Array(microMapSize * gridHeight * microMapSize); + + // 获取网格索引的辅助函数 + const getIdx = (ix: number, iy: number, iz: number): number => { + return ix + iy * microMapSize + iz * microMapSize * gridHeight; + }; + + // 检查坐标是否在范围内 + const inBounds = (ix: number, iy: number, iz: number): boolean => { + return ix >= 0 && ix < microMapSize && + iy >= 0 && iy < gridHeight && + iz >= 0 && iz < microMapSize; + }; + + // 第一步:填充戈壁体素到网格中 + // 关键:我们需要为每个 (ix, iz) 计算真实的 surfaceY,然后用真实的世界Y坐标 + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const gobiLogicHeight = desertContext.gobiHeight[lx][lz]; + const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE; + const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE; + const minLayer = desertContext.gobiMinHeight[lx][lz] || 0; + + // 只处理有戈壁的区域 + if (gobiMicroHeight === 0) continue; + + // 计算这个逻辑方块的基础高度(需要复制 terrain.ts 的逻辑) + // 注意:这里假设基础地形高度是固定的(沙漠场景通常是平坦的) + // 对于沙漠场景,logicHeight 通常是固定值 + const logicHeight = 2; // 沙漠场景的基础高度(从 terrain.ts 看出) + + // 遍历该逻辑方块对应的所有微观方块 + for (let mx = 0; mx < MICRO_SCALE; mx++) { + for (let mz = 0; mz < MICRO_SCALE; mz++) { + const ix = lx * MICRO_SCALE + mx; + const iz = lz * MICRO_SCALE + mz; + + // 计算这个微观位置的 surfaceY(复制 terrain.ts 的逻辑) + let surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight; + + // 遍历高度,填充戈壁体素 + for (let h = 0; h < gobiMicroHeight; h++) { + const microDepthFromBottom = h; + const layerFromBottom = Math.floor(microDepthFromBottom / MICRO_SCALE) + 1; + + // 检查拱门空洞:如果当前层级低于最小高度限制,跳过 + if (layerFromBottom <= minLayer) { + continue; + } + + // 计算这个体素的真实世界Y坐标 + // 戈壁体素从 surfaceY - stoneMicroHeight - gobiMicroHeight 开始向上堆叠 + // depth = stoneMicroHeight + (gobiMicroHeight - 1 - h) + const depth = stoneMicroHeight + (gobiMicroHeight - 1 - microDepthFromBottom); + const worldY = surfaceY - depth; + + // 转换为网格坐标(相对于 MIN_WORLD_Y) + const gridY = worldY - MIN_WORLD_Y; + + if (inBounds(ix, gridY, iz)) { + const idx = getIdx(ix, gridY, iz); + voxelGrid[idx] = 1; // 标记为戈壁体素 + } + } + } + } + } + } + + // 第一步(续):填充巨石体素到网格中 + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const stoneMicroHeight = desertContext.stoneHeight[lx][lz] * MICRO_SCALE; + + // 只处理有巨石的区域 + if (stoneMicroHeight === 0) continue; + + // 计算基础高度 + const logicHeight = 2; // 沙漠场景的基础高度 + + // 遍历该逻辑方块对应的所有微观方块 + for (let mx = 0; mx < MICRO_SCALE; mx++) { + for (let mz = 0; mz < MICRO_SCALE; mz++) { + const ix = lx * MICRO_SCALE + mx; + const iz = lz * MICRO_SCALE + mz; + + // 获取当前位置的戈壁高度(可能为0) + const gobiMicroHeight = desertContext.gobiHeight[lx][lz] * MICRO_SCALE; + + // 计算这个微观位置的 surfaceY + let surfaceY = logicHeight * MICRO_SCALE + stoneMicroHeight + gobiMicroHeight; + + // 遍历高度,填充巨石体素 + for (let h = 0; h < stoneMicroHeight; h++) { + // 计算这个体素的真实世界Y坐标 + // 修复:巨石位于最顶层 (surfaceY),向下延伸 + // h 从 0 (底部) 到 stoneMicroHeight-1 (顶部) + // worldY 范围应为 [surfaceY - stoneMicroHeight + 1, surfaceY] + const worldY = surfaceY - stoneMicroHeight + 1 + h; + + // 转换为网格坐标(相对于 MIN_WORLD_Y) + const gridY = worldY - MIN_WORLD_Y; + + if (inBounds(ix, gridY, iz)) { + const idx = getIdx(ix, gridY, iz); + voxelGrid[idx] = 2; // 标记为巨石体素 + } + } + } + } + } + } + + // 第二步:应用迭代风化算法 + let currentGrid = new Uint8Array(voxelGrid); + const nextGrid = new Uint8Array(voxelGrid.length); + + // 使用戈壁和巨石的最大迭代次数中的较大者 + const maxIterations = Math.max(gobiMaxIterations, stoneMaxIterations); + + for (let iter = 0; iter < maxIterations; iter++) { + // 复制当前网格到下一个网格(保留未被侵蚀的体素) + nextGrid.set(currentGrid); + + // 遍历所有体素,检测并移除边缘 + // 扩大到整个地图范围(包括边界),让边缘体素也能被风化 + for (let ix = 0; ix < microMapSize; ix++) { + for (let gridY = 1; gridY < gridHeight; gridY++) { // 包含最顶层 + for (let iz = 0; iz < microMapSize; iz++) { + const idx = getIdx(ix, gridY, iz); + + // 只处理存在的体素 + if (currentGrid[idx] === 0) continue; + + // 获取体素类型:1 = 戈壁,2 = 巨石 + const voxelType = currentGrid[idx]; + const isGobi = voxelType === 1; + const isStone = voxelType === 2; + + // 根据体素类型判断是否应该在当前迭代处理 + // 戈壁:处理前 gobiMaxIterations 次迭代 + // 巨石:处理前 stoneMaxIterations 次迭代 + if (isGobi && iter >= gobiMaxIterations) continue; + if (isStone && iter >= stoneMaxIterations) continue; + + // 计算暴露面数量(6个方向的邻居) + // 边界外视为空(暴露) + let exposedFaces = 0; + // X+ 方向 + if (ix + 1 >= microMapSize || currentGrid[getIdx(ix + 1, gridY, iz)] === 0) exposedFaces++; + // X- 方向 + if (ix - 1 < 0 || currentGrid[getIdx(ix - 1, gridY, iz)] === 0) exposedFaces++; + // Y+ 方向(顶部) + if (gridY + 1 >= gridHeight || currentGrid[getIdx(ix, gridY + 1, iz)] === 0) exposedFaces++; + // Y- 方向(底部) + if (gridY - 1 < 0 || currentGrid[getIdx(ix, gridY - 1, iz)] === 0) exposedFaces++; + // Z+ 方向 + if (iz + 1 >= microMapSize || currentGrid[getIdx(ix, gridY, iz + 1)] === 0) exposedFaces++; + // Z- 方向 + if (iz - 1 < 0 || currentGrid[getIdx(ix, gridY, iz - 1)] === 0) exposedFaces++; + + // 根据体素类型定义"直角边"的暴露面要求 + // 戈壁:2个或以上暴露面(棱或角) + // 巨石:3个或以上暴露面(更严格,只有更尖锐的角才被磨圆) + const minExposedFaces = isStone ? stoneMinExposedFaces : 2; + const isSharpEdge = exposedFaces >= minExposedFaces; + + if (!isSharpEdge) continue; + + // 计算相对高度(0.0 ~ 1.0) + // 将 gridY 转换为真实世界高度 + const worldY = MIN_WORLD_Y + gridY; + const hRatio = worldY / Math.max(1, maxWorldY); + + // 额外保护:计算该体素下方是否有支撑 + // 如果下方没有体素,不移除(避免破坏地基) + const hasBottomSupport = (gridY === 0) || (gridY > 0 && currentGrid[getIdx(ix, gridY - 1, iz)] > 0); + + // [关键修复] 拱门和悬挑结构保护 + // 检查上方是否有体素(说明自己是支撑柱的一部分) + // 如果上方有体素,且下方是空的(或者自己是底层),则说明自己是关键支撑点,绝对不能移除 + const hasTopLoad = gridY + 1 < gridHeight && currentGrid[getIdx(ix, gridY + 1, iz)] > 0; + const isCriticalSupport = hasTopLoad && (!hasBottomSupport || gridY === 0); + + if (isCriticalSupport) continue; // 绝对保护关键支撑点 + + // 根据体素类型选择高度保护阈值 + const heightBias = isStone ? stoneHeightBias : gobiHeightBias; + + let shouldRemove = false; + + if (iter === 0) { + // [规则 1] 第一刀:保护底部,避免破坏支撑结构 + // 底部40%高度强制保护,确保山丘有稳固的底座 + if (hRatio > 0.4 && hasBottomSupport) { + shouldRemove = true; + } + } else { + // [规则 2] 后续刀:看高度阈值 + // 高度超过阈值,且依然是直角边,继续移除 + // 保护底部30%,避免过度侵蚀 + if (hRatio > Math.max(0.3, heightBias) && hasBottomSupport) { + shouldRemove = true; + } + } + + if (shouldRemove) { + nextGrid[idx] = 0; // 移除该体素 + } + } + } + } + + // 同步状态 + currentGrid.set(nextGrid); + } + + // 第三步:移除悬空体素(使用连通性检查) + // 找到所有与山丘主体连接的体素,移除孤立的体素 + const connected = new Uint8Array(currentGrid.length); // 0 = 未访问, 1 = 已连接 + + // BFS 队列 + const queue: Array<{ ix: number; gridY: number; iz: number }> = []; + + // 关键修复:找到所有"底部暴露"的体素作为BFS起点 + // 底部暴露 = 该体素存在且下方为空(接触地面或空气) + for (let ix = 0; ix < microMapSize; ix++) { + for (let gridY = 0; gridY < gridHeight; gridY++) { + for (let iz = 0; iz < microMapSize; iz++) { + const idx = getIdx(ix, gridY, iz); + + // 如果该体素存在(戈壁或巨石) + if (currentGrid[idx] > 0) { + // 检查下方是否为空(底部暴露) + let isBottomExposed = false; + if (gridY === 0) { + // 最底层,视为暴露 + isBottomExposed = true; + } else { + const belowIdx = getIdx(ix, gridY - 1, iz); + if (currentGrid[belowIdx] === 0) { + // 下方为空,底部暴露 + isBottomExposed = true; + } + } + + // 如果是底部暴露的体素,加入BFS起点 + if (isBottomExposed) { + connected[idx] = 1; + queue.push({ ix, gridY, iz }); + } + } + } + } + } + + // BFS:从底部暴露的体素向上和周围传播,标记所有连接的体素 + while (queue.length > 0) { + const { ix, gridY, iz } = queue.shift()!; + + // 检查 6 个相邻方向 + const neighbors = [ + { dx: 1, dy: 0, dz: 0 }, // X+ + { dx: -1, dy: 0, dz: 0 }, // X- + { dx: 0, dy: 1, dz: 0 }, // Y+ (向上) + { dx: 0, dy: -1, dz: 0 }, // Y- (向下) + { dx: 0, dy: 0, dz: 1 }, // Z+ + { dx: 0, dy: 0, dz: -1 }, // Z- + ]; + + for (const { dx, dy, dz } of neighbors) { + const nx = ix + dx; + const ny = gridY + dy; + const nz = iz + dz; + + // 检查是否在范围内 + if (nx >= 0 && nx < microMapSize && + ny >= 0 && ny < gridHeight && + nz >= 0 && nz < microMapSize) { + const nIdx = getIdx(nx, ny, nz); + + // 如果邻居存在(戈壁或巨石)且未访问过 + if (currentGrid[nIdx] > 0 && connected[nIdx] === 0) { + connected[nIdx] = 1; + queue.push({ ix: nx, gridY: ny, iz: nz }); + } + } + } + } + + // 移除所有未连接的体素(悬空体素) + for (let idx = 0; idx < currentGrid.length; idx++) { + if (currentGrid[idx] > 0 && connected[idx] === 0) { + currentGrid[idx] = 0; // 移除悬空体素 + } + } + + // 第四步:收集被移除的体素(包括戈壁和巨石) + const removedVoxels = new Set(); + + for (let ix = 0; ix < microMapSize; ix++) { + for (let gridY = 0; gridY < gridHeight; gridY++) { + for (let iz = 0; iz < microMapSize; iz++) { + const idx = getIdx(ix, gridY, iz); + + // 原本存在但现在被移除的体素(戈壁或巨石) + if (voxelGrid[idx] > 0 && currentGrid[idx] === 0) { + // gridY 是相对于 MIN_WORLD_Y 的坐标 + // 转换为真实世界坐标 + const worldY = MIN_WORLD_Y + gridY; + removedVoxels.add(`${ix}|${worldY}|${iz}`); + } + } + } + } + + return { removedVoxels }; +} + +/** + * 检查某个体素是否应该因为风化而被移除 + * + * @param ix 微观x坐标 + * @param iy 世界y坐标 + * @param iz 微观z坐标 + * @param weatheringResult 风化预处理结果 + * @returns 是否应该移除该体素 + */ +export function shouldRemoveByWeathering( + ix: number, + iy: number, + iz: number, + weatheringResult: GobiWeatheringResult | null +): boolean { + if (!weatheringResult) return false; + return weatheringResult.removedVoxels.has(`${ix}|${iy}|${iz}`); +} + +// ==================== 溪流蚀刻效果 ==================== + +/** + * 检查是否应该应用溪流蚀刻效果 + * 在溪流深度范围内的方块会被蚀刻 + * + * @param params 溪流蚀刻参数 + * @returns 是否在溪流蚀刻范围内 + */ +export function shouldApplyStreamEtching(params: { + streamDepthMicro: number; + depth: number; +}): boolean { + const { streamDepthMicro, depth } = params; + return streamDepthMicro > 0 && depth < streamDepthMicro; +} + +// ==================== 通用颜色渗透算法 ==================== + +interface VoxelPenetrationParams { + currentType: VoxelType; + targetType: VoxelType; + noiseValue: number; + threshold: number; // 渗透阈值 (0-1),噪声值大于此阈值则渗透 + condition?: boolean; // 可选的额外条件 +} + +/** + * 通用体素渗透逻辑 + * 如果条件满足且噪声值超过阈值,则返回目标类型,否则返回当前类型 + */ +function applyVoxelPenetration(params: VoxelPenetrationParams): VoxelType { + const { currentType, targetType, noiseValue, threshold, condition = true } = params; + if (!condition) return currentType; + return noiseValue > threshold ? targetType : currentType; +} + +// ==================== 戈壁层颜色渗透 ==================== + +/** + * 获取戈壁层类型的函数类型 + */ +type GetGobiLayerType = (absoluteHeight: number, stackHeight: number, variant: number) => VoxelType; + +interface GobiColorBlendingParams { + currentType: VoxelType; // 当前方块类型 + microDepthFromBottom: number; // 从戈壁底部算起的微观深度 + gobiLogicHeight: number; // 戈壁逻辑高度 + gobiMicroHeight: number; // 戈壁微观高度 + gobiVariantIdx: number; // 戈壁变体索引 + ix: number; // 微观x坐标 + iz: number; // 微观z坐标 + y: number; // 世界y坐标 + MICRO_SCALE: number; // 微观缩放比例 + getGobiLayerType: GetGobiLayerType; // 获取戈壁层类型的函数 +} + +/** + * 应用戈壁层之间的颜色渗透效果 + * 在层的顶部和底部创建像素化渗透,模仿泥土方块的自然过渡 + */ +export function applyGobiColorBlending(params: GobiColorBlendingParams): VoxelType { + const { + currentType, + microDepthFromBottom, + gobiLogicHeight, + gobiMicroHeight, + gobiVariantIdx, + ix, + iz, + y, + MICRO_SCALE, + getGobiLayerType, + } = params; + + let type = currentType; + + // 计算在层内的微观位置 (0, 1, 2) + const subH = microDepthFromBottom % MICRO_SCALE; + + // 计算当前层级 + const layerFromBottom = Math.floor(microDepthFromBottom / MICRO_SCALE) + 1; + const currentLayer = Math.max(1, Math.min(gobiLogicHeight, layerFromBottom)); + + // 使用位置噪声进行抖动 + const pNoise = pseudoRandom(ix * 0.43 + iz * 0.37 + y * 0.41); + + // 向上渗透:如果是层的顶部 (subH == MICRO_SCALE - 1),渗透下一层颜色 + if (subH === MICRO_SCALE - 1 && currentLayer < gobiLogicHeight) { + const targetType = getGobiLayerType(currentLayer + 1, gobiMicroHeight, gobiVariantIdx); + type = applyVoxelPenetration({ + currentType: type, + targetType: targetType, + noiseValue: pNoise, + threshold: 0.45 // 55% 渗透概率 + }); + } + // 向下渗透:如果是层的底部 (subH == 0),渗透上一层颜色 + else if (subH === 0 && currentLayer > 1) { + const targetType = getGobiLayerType(currentLayer - 1, gobiMicroHeight, gobiVariantIdx); + // 注意:这里 pNoise < 0.55 等价于 1-pNoise > 0.45,为了复用函数,我们反转逻辑 + // 或者直接使用 applyVoxelPenetration 的逻辑: noise > threshold => target + // 这里原逻辑是 pNoise < 0.55 => target + // 所以 threshold 设为 0.45 (1-0.55),并传入 1-pNoise + type = applyVoxelPenetration({ + currentType: type, + targetType: targetType, + noiseValue: 1 - pNoise, + threshold: 0.45 + }); + } + + return type; +} + +// ==================== 巨石颜色渗透 ==================== + +interface StoneColorBlendingParams { + currentType: VoxelType; + depth: number; // 从顶部算起的深度 + stoneVariantIdx: number; + ix: number; + iz: number; + y: number; +} + +/** + * 应用巨石颜色渗透 + * 让深色石头和浅色石头在交界处互相渗透,增加自然感 + */ +export function applyStoneColorBlending(params: StoneColorBlendingParams): VoxelType { + const { currentType, depth, ix, iz, y } = params; + // stoneVariantIdx 暂时保留在接口中以备将来使用 + + // 巨石只有两种:stone (浅) 和 dark_stone (深) + // 目前的生成逻辑是:variant % 2 === 0 ? 'stone' : 'dark_stone' + // 也就是整个巨石是纯色的。 + // 我们引入一种模式:基于深度或噪声的混合 + + // 1. 基础渗透:使用 3D 噪声让表面产生斑驳 + const noise = pseudoRandom(ix * 0.23 + iz * 0.27 + y * 0.31); + + // 定义另一种石头类型 + const altType: VoxelType = currentType === 'stone' ? 'dark_stone' : 'stone'; + + // 2. 边缘/风化渗透:在石头边缘或某些层级增加杂色 + // 15% 的概率出现杂色斑点 + // [优化] 使用 depth 参数:越接近顶部 (depth 小),风化越严重 (渗透概率越高) + // 顶部 3 层增加额外的 10% 渗透概率 + let threshold = 0.85; + if (depth < 3) threshold = 0.75; + + return applyVoxelPenetration({ + currentType, + targetType: altType, + noiseValue: noise, + threshold: threshold + }); +} + +// ==================== 泥土分层渐变 ==================== + +interface DirtLayerBlendingParams { + currentType: VoxelType; // 当前方块类型 + y: number; // 世界y坐标 + ix: number; // 微观x坐标 + iz: number; // 微观z坐标 + MIN_WORLD_Y: number; // 最小世界Y坐标 + noiseInfluence: number; // 噪声影响值 + isDesertScene: boolean; // 是否沙漠场景 + isInGobiRange: boolean; // 是否在戈壁范围 + isInStoneRange: boolean; // 是否在巨石范围 +} + +/** + * 应用泥土分层渐变效果 + * 四层平滑渐变(deep_dirt -> medium_dirt -> dirt),带边界渗透效果 + */ +export function applyDirtLayerBlending(params: DirtLayerBlendingParams): VoxelType { + const { + currentType, + y, + ix, + iz, + MIN_WORLD_Y, + noiseInfluence, + isDesertScene, + isInGobiRange, + isInStoneRange, + } = params; + + let type = currentType; + + // 只处理泥土类型,且沙漠场景中不处理戈壁/巨石区域 + if (type !== 'dirt' || (isDesertScene && (isInGobiRange || isInStoneRange))) { + return type; + } + + // ========== 第一步:基础分层 ========== + // ... (保持原有分层逻辑不变,这部分不是渗透,是区域定义) ... + // 深层泥土核心区 + if (y > MIN_WORLD_Y + 1 && y <= MIN_WORLD_Y + 5) type = 'deep_dirt'; + // 深层到中层过渡区 + else if (y > MIN_WORLD_Y + 5 && y <= MIN_WORLD_Y + 10) { + const transitionDepth = (y - MIN_WORLD_Y - 5) / 5; + const smoothT = transitionDepth * transitionDepth * (3 - 2 * transitionDepth); + const noiseEffect = noiseInfluence * 0.2; + const finalT = clamp(smoothT + noiseEffect, 0, 1); + type = finalT > 0.5 ? 'medium_dirt' : 'deep_dirt'; + } + // 中层泥土核心区 + else if (y > MIN_WORLD_Y + 10 && y <= MIN_WORLD_Y + 13) type = 'medium_dirt'; + // 中层到浅层过渡区 + else if (y > MIN_WORLD_Y + 13 && y <= MIN_WORLD_Y + 18) { + const transitionDepth = (y - MIN_WORLD_Y - 13) / 5; + const smoothT = transitionDepth * transitionDepth * (3 - 2 * transitionDepth); + const noiseEffect = noiseInfluence * 0.2; + const finalT = clamp(smoothT + noiseEffect, 0, 1); + type = finalT > 0.5 ? 'dirt' : 'medium_dirt'; + } + + // ========== 第二步:边界渗透效果 (使用通用函数重构) ========== + + // 预计算噪声 + const positionNoise1 = pseudoRandom(ix * 0.17 + iz * 0.23 + y * 0.31); + const positionNoise2 = pseudoRandom(ix * 0.19 + iz * 0.27 + y * 0.33); + const positionNoise3 = pseudoRandom(ix * 0.21 + iz * 0.29 + y * 0.35); + const combinedNoise = (positionNoise1 + positionNoise2 + positionNoise3) / 3; + + // 辅助:应用特定区域的渗透 + const applyZonePenetration = ( + targetYMin: number, targetYMax: number, + boundaryProb: number, + penetrationProb: number, // 0-1, 越高越容易渗透 + typeA: VoxelType, typeB: VoxelType // A <-> B 相互渗透 + ) => { + if (y > targetYMin && y <= targetYMax) { + const boundaryNoise = pseudoRandom(ix * 0.13 + iz * 0.19 + y * 0.27); + if (boundaryNoise > (1 - boundaryProb)) { + // 决定目标类型 + let target = type; + if (combinedNoise < penetrationProb * 0.5) target = typeA; // 倾向A + else if (combinedNoise < penetrationProb) target = typeB; // 倾向B + + if (target !== type) return target; + } + } + return type; + }; + + // 深层 -> 中层 过渡区渗透 + type = applyZonePenetration( + MIN_WORLD_Y + 5, MIN_WORLD_Y + 10, + 0.4, 0.5, + 'deep_dirt', 'medium_dirt' + ); + + // 中层 -> 浅层 过渡区渗透 + type = applyZonePenetration( + MIN_WORLD_Y + 13, MIN_WORLD_Y + 18, + 0.4, 0.5, + 'medium_dirt', 'dirt' + ); + + // 核心区域边界渗透 (重构为更简洁的形式) + // 这里原本的逻辑比较复杂,为了保持视觉效果一致,我们保留原有的核心逻辑结构, + // 但使用 applyVoxelPenetration 来执行最终的类型切换 + + if ( + (y >= MIN_WORLD_Y + 3 && y <= MIN_WORLD_Y + 7) || + (y >= MIN_WORLD_Y + 8 && y <= MIN_WORLD_Y + 12) || + (y >= MIN_WORLD_Y + 11 && y <= MIN_WORLD_Y + 15) || + (y >= MIN_WORLD_Y + 16 && y <= MIN_WORLD_Y + 20) + ) { + const edgeNoise = pseudoRandom(ix * 0.19 + iz * 0.23 + y * 0.37); + // 渗透阈值 (原逻辑 edgeNoise > 0.7 => 30% 概率) + if (edgeNoise > 0.7) { + let prob = 0.25; + if ([5, 10, 13, 18].includes(y - MIN_WORLD_Y)) prob = 0.4; // 边界处概率更高 + + // 这里的 combinedNoise < prob 相当于 noise > (1-prob) + // 我们根据区域决定目标类型 + let target: VoxelType = type; + + // 简化判断逻辑 + const pick = (t1: VoxelType, t2: VoxelType) => combinedNoise < prob * 0.5 ? t1 : t2; + const pick3 = (t1: VoxelType, t2: VoxelType, t3: VoxelType) => combinedNoise < prob * 0.33 ? t1 : (combinedNoise < prob * 0.66 ? t2 : t3); + + if (y >= MIN_WORLD_Y + 3 && y <= MIN_WORLD_Y + 7) target = pick('medium_dirt', 'deep_dirt'); + else if (y >= MIN_WORLD_Y + 8 && y <= MIN_WORLD_Y + 12) target = pick3('deep_dirt', 'dirt', 'medium_dirt'); + else if (y >= MIN_WORLD_Y + 11 && y <= MIN_WORLD_Y + 15) target = pick3('dirt', 'deep_dirt', 'medium_dirt'); + else if (y >= MIN_WORLD_Y + 16 && y <= MIN_WORLD_Y + 20) target = pick('medium_dirt', 'dirt'); + + // 使用通用函数执行切换 (condition已在上面判断,所以 threshold=0) + type = applyVoxelPenetration({ + currentType: type, + targetType: target, + noiseValue: 1, // 强制触发 + threshold: 0, + condition: combinedNoise < prob // 保持原有的概率检查 + }); + } + } + + return type; +} + + diff --git a/voxel-tactics-horizon/src/features/Map/logic/scenes.ts b/voxel-tactics-horizon/src/features/Map/logic/scenes.ts new file mode 100644 index 0000000..7d7f8d9 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/scenes.ts @@ -0,0 +1,296 @@ +import type { VoxelType } from './terrain'; + +// 场景配置接口 +export interface SceneConfig { + name: string; + displayName: string; + + // 地形高度参数 + heightScale: number; // 高度缩放因子 + heightBase: number; // 基础高度偏移 + heightRoughness: number; // 地形粗糙度(噪声频率) + heightAmplitude: number; // 高度变化幅度 + + // 水位配置 + waterLevel: number; + hasWater: boolean; + waterType: VoxelType; // 水的类型(water或murky_water) + + // 表面方块配置 + surfaceBlock: VoxelType; // 表层方块 + subSurfaceBlock: VoxelType; // 次表层方块 + deepBlock: VoxelType; // 深层方块 + + // 植被配置 + vegetationDensity: number; // 植被密度 (0-1) + vegetationTypes: Array<{ + type: VoxelType; + probability: number; + minHeight?: number; + maxHeight?: number; + }>; + + // 特殊地物配置 + specialFeatures: Array<{ + type: 'lava_pool' | 'ice_spike' | 'swamp_tree' | 'cactus' | 'volcanic_vent'; + probability: number; + }>; + + // 生物群系权重(用于多样化) + biomeWeights: { + hot: number; // 炎热区域权重 + cold: number; // 寒冷区域权重 + wet: number; // 潮湿区域权重 + dry: number; // 干燥区域权重 + }; + + // 颜色调整 + colorMultipliers?: { + r: number; + g: number; + b: number; + }; +} + +// 场景类型定义 +export type SceneType = + | 'desert' + | 'mountain' + | 'snowy_mountain' + | 'riverside' + | 'beach' + | 'plains' + | 'volcano' + | 'swamp' + | 'tundra'; + +// 场景配置集合 +export const SCENE_CONFIGS: Record = { + // 1. 沙漠场景 + desert: { + name: 'desert', + displayName: 'Desert', + heightScale: 0.15, // 极低,创建平坦基础 + heightBase: 2, + heightRoughness: 0.008, // 极低粗糙度,几乎平坦 + heightAmplitude: 1, // 极小的高度变化 + waterLevel: -5, + hasWater: false, + waterType: 'water', + surfaceBlock: 'sand', + subSurfaceBlock: 'sand', + deepBlock: 'stone', + vegetationDensity: 0.05, + vegetationTypes: [ + { type: 'cactus', probability: 0.8, minHeight: 0, maxHeight: 10 } + ], + specialFeatures: [ + { type: 'cactus', probability: 0.08 } + ], + biomeWeights: { hot: 1.0, cold: 0.0, wet: 0.1, dry: 1.0 } + }, + + // 2. 山区场景 + mountain: { + name: 'mountain', + displayName: 'Mountain', + heightScale: 1.8, + heightBase: 4, + heightRoughness: 0.04, + heightAmplitude: 8, + waterLevel: 1, + hasWater: true, + waterType: 'water', + surfaceBlock: 'grass', + subSurfaceBlock: 'dirt', + deepBlock: 'stone', + vegetationDensity: 0.25, + vegetationTypes: [ + { type: 'wood', probability: 0.7, minHeight: 0, maxHeight: 6 }, + { type: 'flower', probability: 0.3, minHeight: 0, maxHeight: 4 } + ], + specialFeatures: [], + biomeWeights: { hot: 0.3, cold: 0.5, wet: 0.6, dry: 0.4 } + }, + + // 3. 雪山场景 + snowy_mountain: { + name: 'snowy_mountain', + displayName: 'Snowy Mountain', + heightScale: 2.2, + heightBase: 5, + heightRoughness: 0.05, + heightAmplitude: 12, + waterLevel: -2, + hasWater: true, + waterType: 'ice', + surfaceBlock: 'snow', + subSurfaceBlock: 'packed_ice', + deepBlock: 'frozen_stone', + vegetationDensity: 0.08, + vegetationTypes: [ + { type: 'wood', probability: 0.6, minHeight: 0, maxHeight: 4 } + ], + specialFeatures: [ + { type: 'ice_spike', probability: 0.04 } + ], + biomeWeights: { hot: 0.0, cold: 1.0, wet: 0.4, dry: 0.6 } + }, + + // 4. 河岸场景 + riverside: { + name: 'riverside', + displayName: 'Riverside', + heightScale: 0.6, + heightBase: 3, + heightRoughness: 0.025, + heightAmplitude: 4, + waterLevel: 1.5, + hasWater: true, + waterType: 'water', + surfaceBlock: 'grass', + subSurfaceBlock: 'dirt', + deepBlock: 'stone', + vegetationDensity: 0.35, + vegetationTypes: [ + { type: 'wood', probability: 0.5, minHeight: 0, maxHeight: 5 }, + { type: 'reed', probability: 0.4, minHeight: 0, maxHeight: 2 }, + { type: 'flower', probability: 0.1, minHeight: 0, maxHeight: 3 } + ], + specialFeatures: [], + biomeWeights: { hot: 0.5, cold: 0.3, wet: 1.0, dry: 0.2 } + }, + + // 5. 海滩场景 + beach: { + name: 'beach', + displayName: 'Beach', + heightScale: 0.25, + heightBase: 1.5, + heightRoughness: 0.02, + heightAmplitude: 2, + waterLevel: 1.8, + hasWater: true, + waterType: 'water', + surfaceBlock: 'sand', + subSurfaceBlock: 'sand', + deepBlock: 'stone', + vegetationDensity: 0.12, + vegetationTypes: [ + { type: 'wood', probability: 0.8, minHeight: 2, maxHeight: 10 } + ], + specialFeatures: [], + biomeWeights: { hot: 0.8, cold: 0.2, wet: 0.7, dry: 0.6 } + }, + + // 6. 平原场景 + plains: { + name: 'plains', + displayName: 'Plains', + heightScale: 0.4, + heightBase: 2.5, + heightRoughness: 0.02, + heightAmplitude: 3, + waterLevel: 1, + hasWater: true, + waterType: 'water', + surfaceBlock: 'grass', + subSurfaceBlock: 'dirt', + deepBlock: 'stone', + vegetationDensity: 0.2, + vegetationTypes: [ + { type: 'wood', probability: 0.5, minHeight: 0, maxHeight: 8 }, + { type: 'flower', probability: 0.5, minHeight: 0, maxHeight: 5 } + ], + specialFeatures: [], + biomeWeights: { hot: 0.6, cold: 0.4, wet: 0.5, dry: 0.5 } + }, + + // 7. 火山场景 + volcano: { + name: 'volcano', + displayName: 'Volcano', + heightScale: 2.0, + heightBase: 4, + heightRoughness: 0.06, + heightAmplitude: 10, + waterLevel: -5, + hasWater: false, + waterType: 'lava', + surfaceBlock: 'volcanic_rock', + subSurfaceBlock: 'magma_stone', + deepBlock: 'obsidian', + vegetationDensity: 0.02, + vegetationTypes: [], + specialFeatures: [ + { type: 'lava_pool', probability: 0.15 }, + { type: 'volcanic_vent', probability: 0.05 } + ], + biomeWeights: { hot: 1.0, cold: 0.0, wet: 0.1, dry: 1.0 } + }, + + // 8. 沼泽场景 + swamp: { + name: 'swamp', + displayName: 'Swamp', + heightScale: 0.2, + heightBase: 1.8, + heightRoughness: 0.018, + heightAmplitude: 2, + waterLevel: 1.6, + hasWater: true, + waterType: 'murky_water', + surfaceBlock: 'swamp_grass', + subSurfaceBlock: 'mud', + deepBlock: 'mud', + vegetationDensity: 0.4, + vegetationTypes: [ + { type: 'wood', probability: 0.6, minHeight: 0, maxHeight: 4 }, + { type: 'moss', probability: 0.3, minHeight: 0, maxHeight: 3 }, + { type: 'lily_pad', probability: 0.1, minHeight: 0, maxHeight: 1 } + ], + specialFeatures: [ + { type: 'swamp_tree', probability: 0.12 } + ], + biomeWeights: { hot: 0.7, cold: 0.2, wet: 1.0, dry: 0.1 } + }, + + // 9. 冰原场景 + tundra: { + name: 'tundra', + displayName: 'Tundra', + heightScale: 0.3, + heightBase: 2, + heightRoughness: 0.022, + heightAmplitude: 3, + waterLevel: -2, + hasWater: true, + waterType: 'ice', + surfaceBlock: 'snow', + subSurfaceBlock: 'permafrost', + deepBlock: 'frozen_stone', + vegetationDensity: 0.03, + vegetationTypes: [], + specialFeatures: [ + { type: 'ice_spike', probability: 0.06 } + ], + biomeWeights: { hot: 0.0, cold: 1.0, wet: 0.3, dry: 0.7 } + } +}; + +// 获取所有场景类型 +export const getAllSceneTypes = (): SceneType[] => { + return Object.keys(SCENE_CONFIGS) as SceneType[]; +}; + +// 获取随机场景类型 +export const getRandomSceneType = (): SceneType => { + const types = getAllSceneTypes(); + return types[Math.floor(Math.random() * types.length)]; +}; + +// 获取场景配置 +export const getSceneConfig = (sceneType: SceneType): SceneConfig => { + return SCENE_CONFIGS[sceneType]; +}; + diff --git a/voxel-tactics-horizon/src/features/Map/logic/terrain.ts b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts new file mode 100644 index 0000000..518b907 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/terrain.ts @@ -0,0 +1,929 @@ +/** + * 地形生成主入口:协调所有地形生成模块 + * - 整合方块样式系统 (voxelStyles) + * - 整合戈壁特征地形 (desertFeatures) + * - 整合植被生成 (vegetation) + * - 整合后处理 (postprocessing) + */ + +import { createNoise2D } from 'simplex-noise'; +import { getSceneConfig, type SceneType } from './scenes'; + +// 方块样式和色板 +import { + type VoxelType, + pseudoRandom, + createDeterministicRandom, + varyColor, +} from './voxelStyles'; + +// 戈壁特征地形 +import { + createDesertContext, + getGobiLayerType, + getStoneLayerType, + // DESERT_ROCK_STRUCTS, + // STONE_PLATFORM_STRUCTS, + // MESA_PLATFORM_STRUCTS, +} from './desertFeatures'; + +// 植被生成 (已移除旧系统) +// import { +// type AddTreeVoxelFn, +// pickRandom, +// LUSH_TREES, +// CONIFER_TREES, +// GRASS_DECO, +// FLOWER_STRUCTS, +// REED_STRUCTS, +// CACTUS_STRUCTS, +// placeStructure, +// placeGobiVegetation, +// } from './vegetation'; + +// 新的植被系统 +import { generateVegetation, type VegetationGenerationContext } from './newVegetation'; + +// 后处理 +import { + shouldApplyStreamEtching, + applyGobiColorBlending, + applyStoneColorBlending, // 引入新函数 + applyDirtLayerBlending, + preprocessGobiWeathering, + shouldRemoveByWeathering, + type GobiWeatheringResult, +} from './postprocessing'; + +// ============= 导出类型 ============= +export type { VoxelType }; + +// ============= 进度回调类型 ============= +export interface TerrainGenerationProgress { + stage: 'basic' | 'features' | 'postprocessing' | 'vegetation' | 'complete'; + stageLabel: string; + progress: number; // 0-100 + detail?: string; +} + +export type ProgressCallback = (progress: TerrainGenerationProgress) => void; + +// ============= 地形生成配置 ============= + +// 地形生成版本号 - 每次修改时改变这个数字,触发地图重新生成 +export const TERRAIN_VERSION = 83; + +// 逻辑层面的网格大小(战棋移动格子) +export const TILE_SIZE = 1; +// 每个逻辑格子包含的微型体素数量 (8x8) - 用于地形 +export const MICRO_SCALE = 8; +// 树木专用的高分辨率微体素数量 (32x32) - 用于更精细的树木细节 +export const TREE_MICRO_SCALE = 32; +// 微型体素的物理尺寸 +export const VOXEL_SIZE = TILE_SIZE / MICRO_SCALE; +// 树木微体素的物理尺寸(更小,更精细) +export const TREE_VOXEL_SIZE = TILE_SIZE / TREE_MICRO_SCALE; + +export interface VoxelData { + x: number; + y: number; + z: number; + type: VoxelType; + color: string; + ix: number; + iy: number; + iz: number; + heightScale: number; + isHighRes?: boolean; // 标记是否为高分辨率(16x16)体素 +} + +export const MAP_SIZES = { + small: 16, + medium: 24, + large: 32, +}; + +// 地形生成选项 +export interface TerrainGenerationOptions { + // 戈壁风化参数 + weatheringMaxIterations?: number; // 戈壁风化迭代次数,默认 3 + weatheringHeightBias?: number; // 戈壁高度保护阈值,默认 0.5 + // 巨石风化参数 + stoneWeatheringMaxIterations?: number; // 巨石风化迭代次数,默认 3 + stoneWeatheringHeightBias?: number; // 巨石高度保护阈值,默认 0.5 + stoneWeatheringMinExposedFaces?: number; // 巨石最少暴露面数,默认 2 +} + +// MagicaVoxel Vibrant Palette (More Saturation, Less Grey) +export const generateTerrain = async ( + mapSize: number, + seed: string = 'default', + sceneType?: SceneType, + onProgress?: ProgressCallback, + options?: TerrainGenerationOptions +): Promise => { + // 进度报告辅助函数 - 直接同步调用,确保状态立即更新 + const reportProgress = (stage: TerrainGenerationProgress['stage'], progress: number, detail?: string) => { + if (!onProgress) return; + const stageLabels = { + basic: '生成基础地形', + features: '生成特征地形', + postprocessing: '地貌后处理', + vegetation: '生成植被', + complete: '完成' + }; + const progressData = { + stage, + stageLabel: stageLabels[stage], + progress, + detail + }; + // 直接同步调用,确保状态立即更新 + onProgress(progressData); + }; + let seedVal = 0; + for (let i = 0; i < seed.length; i++) seedVal += seed.charCodeAt(i); + const randomSeedBase = seedVal || 1; + const seededRandom = createDeterministicRandom(randomSeedBase); + + // 获取场景配置 + const sceneConfig = sceneType ? getSceneConfig(sceneType) : undefined; + + const noise2D = createNoise2D(() => { + seedVal = Math.imul(seedVal ^ 0xdeadbeef, 2654435761); + return ((seedVal >>> 0) / 4294967296); + }); + + const moistureNoise = createNoise2D(() => Math.random()); + const detailNoise = createNoise2D(() => Math.random()); + + // 树木聚集噪声:用于控制树木成组生成 (已移除) + // const forestClusterNoise = createNoise2D(() => Math.random()); + + + const desertContext = sceneConfig?.name === 'desert' + ? createDesertContext(mapSize, seededRandom) + : null; + const isDesertScene = sceneConfig?.name === 'desert'; + + // 地形类型决策:如果有场景配置,使用场景配置;否则根据 seed 生成 + let terrainType: string; + let heightBase: number; + let heightRoughness: number; + let waterLevel: number; + let surfaceBlock: VoxelType; + let subSurfaceBlock: VoxelType; + let deepBlock: VoxelType; + + if (sceneConfig) { + terrainType = sceneConfig.name; + heightBase = sceneConfig.heightBase; + heightRoughness = sceneConfig.heightRoughness; + waterLevel = sceneConfig.waterLevel; + surfaceBlock = sceneConfig.surfaceBlock; + subSurfaceBlock = sceneConfig.subSurfaceBlock; + deepBlock = sceneConfig.deepBlock; + } else { + // 旧的随机地形类型逻辑(向后兼容) + const terrainTypeHash = seedVal % 4; + terrainType = ['normal', 'mountain', 'desert', 'canyon'][terrainTypeHash]; + heightBase = 3; + heightRoughness = 0.03; + waterLevel = 1; + surfaceBlock = 'grass'; + subSurfaceBlock = 'dirt'; + deepBlock = 'stone'; + } + + const MIN_WORLD_Y = -6; // ensure solid base + + // 开始生成:基础地形 + reportProgress('basic', 0, '初始化地形生成'); + + // ===== 戈壁和巨石风化预处理 ===== + // 在沙漠场景中,预先计算风化效果 + let gobiWeatheringResult: GobiWeatheringResult | null = null; + if (isDesertScene && desertContext) { + reportProgress('basic', 5, '预处理戈壁和巨石风化效果'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 从options中获取参数,如果没有则使用默认值 + const gobiMaxIterations = options?.weatheringMaxIterations ?? 3; + const gobiHeightBias = options?.weatheringHeightBias ?? 0.5; + const stoneMaxIterations = options?.stoneWeatheringMaxIterations ?? 3; + const stoneHeightBias = options?.stoneWeatheringHeightBias ?? 0.5; + const stoneMinExposedFaces = options?.stoneWeatheringMinExposedFaces ?? 2; + + gobiWeatheringResult = preprocessGobiWeathering({ + desertContext, + mapSize, + MICRO_SCALE, + gobiMaxIterations, + gobiHeightBias, + stoneMaxIterations, + stoneHeightBias, + stoneMinExposedFaces, + MIN_WORLD_Y, + }); + + reportProgress('basic', 10, `风化预处理完成,移除 ${gobiWeatheringResult.removedVoxels.size} 个体素`); + await new Promise(resolve => setTimeout(resolve, 0)); + } + + const voxels: VoxelData[] = []; + const occupancy = new Set(); + const keyOf = (x: number, y: number, z: number) => `${x}|${y}|${z}`; + const topColumns = new Map(); + + // 记录树木位置 + const treePositions: Array<{ x: number, z: number }> = []; + + // 定义添加体素的函数类型 + type AddVoxelFn = ( + ix: number, + iy: number, + iz: number, + type: VoxelType, + color: string, + heightScale?: number + ) => void; + + const addVoxel: AddVoxelFn = (ix, iy, iz, type, color, heightScale = 1) => { + const voxel: VoxelData = { + x: ix * VOXEL_SIZE, + y: iy * VOXEL_SIZE, + z: iz * VOXEL_SIZE, + type, + color, + ix, + iy, + iz, + heightScale, + }; + voxels.push(voxel); + occupancy.add(keyOf(ix, iy, iz)); + }; + + // 专门用于添加树木体素的函数 (已移除) + // const addTreeVoxel... + + for (let lx = 0; lx < mapSize; lx++) { + // 报告基础地形生成进度 + 异步让出控制权 + if (lx % Math.max(1, Math.floor(mapSize / 10)) === 0) { + reportProgress('basic', Math.floor((lx / mapSize) * 100), `生成地形列 ${lx}/${mapSize}`); + // 关键:让浏览器有机会渲染UI + await new Promise(resolve => setTimeout(resolve, 0)); + } + + for (let lz = 0; lz < mapSize; lz++) { + let n, logicHeight, logicType: VoxelType; + const moisture = moistureNoise(lx * 0.15, lz * 0.15); + const gobiLogicHeight = isDesertScene && desertContext ? desertContext.gobiHeight[lx][lz] : 0; + const gobiMicroHeight = gobiLogicHeight * MICRO_SCALE; + const gobiVariantIdx = isDesertScene && desertContext ? desertContext.gobiVariant[lx][lz] : 0; + const stoneLogicHeight = isDesertScene && desertContext ? desertContext.stoneHeight[lx][lz] : 0; + const stoneMicroHeight = stoneLogicHeight * MICRO_SCALE; + const stoneVariantIdx = isDesertScene && desertContext ? desertContext.stoneVariant[lx][lz] : 0; + + // Pre-fetch neighbor heights for Gobi smoothing (not used in current erosion algorithm) + // Kept for potential future use + // let gobiH_L = 0, gobiH_R = 0, gobiH_U = 0, gobiH_D = 0; + + // if (isDesertScene && desertContext && gobiLogicHeight > 0) { + // const gh = desertContext.gobiHeight; + // gobiH_L = gh[lx-1]?.[lz] ?? 0; + // gobiH_R = gh[lx+1]?.[lz] ?? 0; + // gobiH_U = gh[lx]?.[lz-1] ?? 0; + // gobiH_D = gh[lx]?.[lz+1] ?? 0; + // } + + // 使用场景配置生成地形 + if (sceneConfig) { + if (terrainType === 'desert') { + logicHeight = Math.max(1, Math.floor(heightBase)); + logicType = 'sand'; + } else { + const scale = heightRoughness; + n = noise2D(lx * scale, lz * scale); + n += 0.5 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.5; + + logicHeight = Math.floor((n + 1) * sceneConfig.heightAmplitude * 0.5 + heightBase); + if (logicHeight < 0) logicHeight = 0; + + if (sceneConfig.hasWater && logicHeight <= waterLevel) { + logicType = sceneConfig.waterType; + } else { + const relativeHeight = logicHeight - waterLevel; + + if (terrainType === 'volcano') { + if (logicHeight < heightBase - 1) { + logicType = 'lava'; + } else if (logicHeight < heightBase + 2) { + logicType = moisture > 0.3 ? 'ash' : 'volcanic_rock'; + } else if (logicHeight < heightBase + 6) { + logicType = 'volcanic_rock'; + } else { + logicType = moisture > 0.5 ? 'magma_stone' : 'obsidian'; + } + } else if (terrainType === 'swamp') { + if (relativeHeight <= 0.5) { + logicType = 'mud'; + } else { + logicType = moisture > 0.3 ? 'swamp_grass' : 'mud'; + } + } else if (terrainType === 'tundra' || terrainType === 'snowy_mountain') { + if (relativeHeight < 1) { + logicType = 'permafrost'; + } else if (relativeHeight < 3) { + logicType = 'snow'; + } else { + logicType = moisture > 0.4 ? 'packed_ice' : 'frozen_stone'; + } + } else { + logicType = surfaceBlock; + } + } + } + } else { + // 旧逻辑(向后兼容) + // 根据地形类型生成不同的地貌 + if (terrainType === 'mountain') { + // ===== 高山地形:高耸的山峰,无水系 ===== + const scale = 0.06; + n = noise2D(lx * scale, lz * scale); + n += 0.7 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.7; + + logicHeight = Math.floor((n + 1) * 5); // 更高的高度范围 (0-10) + if (logicHeight < 2) logicHeight = 2; // 无水体 + + if (logicHeight < 4) { + logicType = 'grass'; + } else if (logicHeight < 7) { + logicType = 'stone'; + } else { + logicType = 'snow'; + } + + } else if (terrainType === 'desert') { + // ===== 戈壁沙漠:沙地和岩石组成,无水系 ===== + const scale = 0.1; + n = noise2D(lx * scale + 300, lz * scale + 300); + n += 0.3 * noise2D(lx * scale * 3, lz * scale * 3); + n /= 1.3; + + logicHeight = Math.floor((n + 1) * 2.5); // 较平坦 (0-5) + if (logicHeight < 2) logicHeight = 2; // 无水体 + + if (logicHeight < 4) { + logicType = moisture > -0.2 ? 'sand' : 'stone'; // 大部分是沙,少量岩石 + } else { + logicType = 'stone'; // 戈壁岩石 + } + + } else if (terrainType === 'canyon') { + // ===== 峡谷地形:高度差大,中央有河流 ===== + const scale = 0.08; + n = noise2D(lx * scale + 600, lz * scale + 600); + n += 0.6 * noise2D(lx * scale * 2, lz * scale * 2); + n /= 1.6; + + // 河流:地图中央从上到下的一条带 + const distToRiver = Math.abs(lx - mapSize / 2); + const riverWidth = mapSize * 0.08; // 河流宽度约为地图的 8% + + if (distToRiver < riverWidth) { + // 河流区域:深度根据距离河流中心的距离 + const depthFactor = 1 - (distToRiver / riverWidth); + logicHeight = Math.max(0, Math.floor(2 - depthFactor * 2)); // 河流深度 0-2 + logicType = 'water'; + } else { + // 峡谷两侧:高度剧烈变化 + const sideEffect = Math.sin(lz * 0.15) * 0.3; + logicHeight = Math.floor((n + sideEffect + 1) * 4.5); // 高度范围 (0-9) + + if (logicHeight <= 2) { + logicType = 'grass'; + } else if (logicHeight < 6) { + logicType = 'stone'; + } else { + logicType = 'snow'; + } + } + + } else { + // ===== 普通地形:原有逻辑 ===== + const scale = 0.08; + n = noise2D(lx * scale + 100, lz * scale + 100); + n += 0.5 * noise2D(lx * scale * 2 + 200, lz * scale * 2 + 200); + n /= 1.5; + + logicHeight = Math.floor((n + 1) * 3.2); + if (logicHeight < 1) logicHeight = 1; + + // 创建大型湖泊(地图中心偏左下区域) + const lakeCenterX = mapSize * 0.35; + const lakeCenterZ = mapSize * 0.4; + const distToLake = Math.sqrt( + Math.pow(lx - lakeCenterX, 2) + Math.pow(lz - lakeCenterZ, 2) + ); + const lakeRadius = mapSize * 0.12; + + if (distToLake < lakeRadius) { + const depthFactor = 1 - (distToLake / lakeRadius); + logicHeight = Math.max(0, Math.floor(1 - depthFactor * 0.5)); + logicType = 'water'; + } else if (logicHeight <= 1) { + logicType = 'water'; + } else if (logicHeight === 2) { + logicType = moisture > 0 ? 'grass' : 'sand'; + } else if (logicHeight < 5) { + logicType = 'grass'; + if (n > 0.7) logicType = 'stone'; + } else { + logicType = 'stone'; + if (logicHeight > 7) logicType = 'snow'; + } + } + } + + for (let mx = 0; mx < MICRO_SCALE; mx++) { + for (let mz = 0; mz < MICRO_SCALE; mz++) { + const ix = lx * MICRO_SCALE + mx; + const iz = lz * MICRO_SCALE + mz; + + // Much subtler detail noise + // Use lower frequency for broader, gentler slopes + const detailVal = detailNoise(ix * 0.08, iz * 0.08); + + let worldY = logicHeight * MICRO_SCALE; + if (isDesertScene) { + worldY += stoneMicroHeight + gobiMicroHeight; + } + + // SMOOTHING LOGIC: + // Only apply significant noise to Stone/Snow + // Keep Grass/Sand relatively flat (just +/- 1 voxel occasionally) + if (logicType === 'grass' || logicType === 'sand' || logicType === 'swamp_grass') { + if (Math.abs(detailVal) > 0.85) { + worldY += Math.sign(detailVal); + } + } else if (logicType === 'stone' || logicType === 'snow' || logicType === 'volcanic_rock' || logicType === 'frozen_stone') { + worldY += Math.floor(detailVal * 2); + } else if (logicType === 'water' || logicType === 'murky_water' || logicType === 'lava' || logicType === 'ice') { + worldY = Math.floor(1.3 * MICRO_SCALE); + } else if (logicType === 'ash' || logicType === 'mud') { + if (Math.abs(detailVal) > 0.75) { + worldY += Math.sign(detailVal); + } + } + + // 4. Sand Mounds at the base (External Sand Blending) - Pre-calculation + let sandMoundHeight = 0; + + const streamInfo = isDesertScene && desertContext + ? desertContext.streamDepthMap.get(`${ix}|${iz}`) + : undefined; + const streamDepthMicro = streamInfo?.depth ?? 0; + + // 只有非河流区域才生成沙丘 + if (streamDepthMicro === 0 && isDesertScene && logicType === 'sand' && gobiLogicHeight === 0 && stoneLogicHeight === 0) { + // Check if we are near a Gobi hill + const neighborGobiH_L = desertContext?.gobiHeight[lx-1]?.[lz] ?? 0; + const neighborGobiH_R = desertContext?.gobiHeight[lx+1]?.[lz] ?? 0; + const neighborGobiH_U = desertContext?.gobiHeight[lx]?.[lz-1] ?? 0; + const neighborGobiH_D = desertContext?.gobiHeight[lx]?.[lz+1] ?? 0; + + // 方案A补充:检查周围是否有巨石,如果有则禁用沙丘 + // 检查2格范围内是否有巨石 + let hasNearbyStone = false; + for (let checkDx = -2; checkDx <= 2; checkDx++) { + for (let checkDz = -2; checkDz <= 2; checkDz++) { + const checkLx = lx + checkDx; + const checkLz = lz + checkDz; + if (checkLx >= 0 && checkLx < mapSize && checkLz >= 0 && checkLz < mapSize) { + if ((desertContext?.stoneHeight[checkLx]?.[checkLz] ?? 0) > 0) { + hasNearbyStone = true; + break; + } + } + } + if (hasNearbyStone) break; + } + + // 只有在附近没有巨石且相邻戈壁时才生成沙丘 + if (!hasNearbyStone && (neighborGobiH_L > 0 || neighborGobiH_R > 0 || neighborGobiH_U > 0 || neighborGobiH_D > 0)) { + // We are adjacent to a Gobi hill and far from stones + // Calculate distance to the hill in micro-voxels + let distToHill = Infinity; + if (neighborGobiH_L > 0) distToHill = Math.min(distToHill, mx); + if (neighborGobiH_R > 0) distToHill = Math.min(distToHill, (MICRO_SCALE - 1) - mx); + if (neighborGobiH_U > 0) distToHill = Math.min(distToHill, mz); + if (neighborGobiH_D > 0) distToHill = Math.min(distToHill, (MICRO_SCALE - 1) - mz); + + // Generate mounds close to the hill + if (distToHill < 4) { + const moundNoise = pseudoRandom(ix * 0.47 + iz * 0.47 + logicHeight * 0.47); + + if (distToHill <= 1) { + if (moundNoise > 0.3) sandMoundHeight = 1; + if (moundNoise > 0.8) sandMoundHeight = 2; + } else if (distToHill <= 2) { + if (moundNoise > 0.6) sandMoundHeight = 1; + } + } + } + } + + // Apply sand mound height to worldY + worldY += sandMoundHeight; + + // 河流生成:强制拉平水面 + // 1. 消除地形微噪声的影响(对于河流区域) + // 2. 减去河道深度 + if (streamDepthMicro > 0) { + // 还原到基础平面高度,去除 noise/smoothing 的影响 + const baseWorldY = logicHeight * MICRO_SCALE; + + // 如果当前已经受 Gobi/Stone 影响抬高了,也要考虑还原(但通常河流避开了它们) + // 这里我们假设河流区域应当是平坦的沙地基础 + + worldY = baseWorldY; // 重置为绝对平坦 + worldY -= streamDepthMicro; // 挖坑 + } + + const surfaceY = worldY; + + for (let y = surfaceY; y >= MIN_WORLD_Y; y--) { + let type: VoxelType = logicType; + const depth = surfaceY - y; + + // 沙漠场景:优先处理戈壁和巨石,确保它们保持纯色 + const isInGobiRange = gobiMicroHeight > 0 && depth >= stoneMicroHeight && depth < stoneMicroHeight + gobiMicroHeight; + const isInStoneRange = stoneMicroHeight > 0 && depth < stoneMicroHeight; + + // 应用溪流蚀刻效果 + if (shouldApplyStreamEtching({ streamDepthMicro, depth })) { + type = 'etched_sand'; + // CENTER OF STREAM = DARK STONE / GRAVEL + if (streamInfo?.isCenter) { + // Simple noise for gravel patchiness + if (pseudoRandom(ix * 0.3 + iz * 0.3) > 0.4) { + type = 'dark_stone'; + } + } + } else if (isInStoneRange) { + // 巨石风化检查:如果该体素被风化移除,跳过生成 + if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) { + continue; // 跳过生成,风化效果 + } + // 3. 确定基础颜色 + type = getStoneLayerType(depth, stoneVariantIdx); + + // 4. 应用巨石颜色渗透效果 + type = applyStoneColorBlending({ + currentType: type, + depth, + stoneVariantIdx, + ix, + iz, + y + }); + } else if (isInGobiRange) { + const relativeDepth = depth - stoneMicroHeight; + const microDepthFromBottom = gobiMicroHeight - relativeDepth - 1; + + // 1. 拱门空洞检查 + // 如果存在最小高度限制(用于拱门),且当前层级低于该限制,则跳过生成(形成空洞) + const minLayer = desertContext?.gobiMinHeight[lx][lz] || 0; + const layerFromBottom = Math.floor(microDepthFromBottom / MICRO_SCALE) + 1; + + if (layerFromBottom <= minLayer) { + continue; // 跳过生成,形成悬空效果 + } + + // 2. 风化检查:如果该体素被风化移除,跳过生成 + if (shouldRemoveByWeathering(ix, y, iz, gobiWeatheringResult)) { + continue; // 跳过生成,风化效果 + } + + // 3. 确定基础颜色 + const currentLayer = Math.max(1, Math.min(gobiLogicHeight, layerFromBottom)); + type = getGobiLayerType(currentLayer, gobiMicroHeight, gobiVariantIdx); + + // Note: Internal sand blending removed. External sand mounds will be handled in the main loop. + + // 4. 应用戈壁层颜色渗透效果 + type = applyGobiColorBlending({ + currentType: type, + microDepthFromBottom, + gobiLogicHeight, + gobiMicroHeight, + gobiVariantIdx, + ix, + iz, + y, + MICRO_SCALE, + getGobiLayerType, + }); + } + + // 4. Sand Mounds at the base (External Sand Blending) + // handled via worldY adjustment earlier + + // 根据场景配置或逻辑类型进行分层 + // 沙漠场景中,如果当前位置有戈壁或巨石,跳过分层逻辑,保持纯色 + if (sceneConfig && !isDesertScene) { + // 使用场景配置的分层逻辑 + if (logicType === 'grass' || logicType === 'swamp_grass') { + if (depth > 1) { + type = subSurfaceBlock; + } + } else if (logicType === 'sand' || logicType === 'ash') { + if (depth > 2 && type === logicType) type = subSurfaceBlock; + } else if (logicType === 'snow') { + if (depth > 1 && type === logicType) type = subSurfaceBlock; + if (depth > 3 && type === logicType) type = deepBlock; + } else if (logicType === 'volcanic_rock' || logicType === 'magma_stone') { + if (depth > 2 && type === logicType) type = 'obsidian'; + } else if (logicType === 'mud') { + if (depth > 3 && type === logicType) type = deepBlock; + } + } else { + // 更自然的地形分层逻辑(旧逻辑,向后兼容) + // 沙漠场景中,如果当前位置有戈壁或巨石,跳过分层逻辑 + if (isDesertScene && (isInGobiRange || isInStoneRange)) { + // 保持戈壁/巨石类型不变,不进行分层 + } else if (logicType === 'grass') { + // 草地:临时禁用深色草地变化,统一使用普通草地 + // if (depth === 0 && Math.random() > 0.7) { + // type = 'dark_grass_ground'; + // } else + if (depth > 1) { + type = 'dirt'; + } + } else if (logicType === 'sand') { + // 沙地:沙 -> 泥土 -> 基岩 + if (depth > 2) type = 'dirt'; + } + } + + // 应用泥土分层渐变效果 + const noiseInfluence = detailVal; // 使用现有的噪声值 + type = applyDirtLayerBlending({ + currentType: type, + y, + ix, + iz, + MIN_WORLD_Y, + noiseInfluence, + isDesertScene, + isInGobiRange, + isInStoneRange, + }); + + // 最底层统一为基岩 + if (y <= MIN_WORLD_Y + 1) type = 'bedrock'; + + const isSurfaceLayer = y === surfaceY && type !== 'bedrock'; + const decorationColorShift = isSurfaceLayer ? Math.random() * 0.2 : 0; + + // 先使用默认颜色添加体素,稍后处理水体颜色 + const color = varyColor(type, detailVal + y * 0.02 + decorationColorShift, 0); + + addVoxel(ix, y, iz, type, color, 1); + + if (isSurfaceLayer) { + topColumns.set(`${ix}|${iz}`, { index: voxels.length - 1, baseType: logicType }); + } + } + + if (streamDepthMicro > 0) { + // Rule 1: Enable water filling logic + if (streamInfo?.waterHeight && streamInfo.waterHeight > 0) { + for (let h = 1; h <= streamInfo.waterHeight; h++) { + // Ensure water doesn't exceed surface (should not happen with waterH calculation, but safe) + if (surfaceY + h > surfaceY + streamDepthMicro) break; + + addVoxel( + ix, + surfaceY + h, + iz, + 'water', + varyColor('water', seededRandom()), + 1 + ); + } + } + } + + // Sparse half-height tufts on surface for additional detail + if ( + logicType === 'grass' && + Math.random() > 0.985 + ) { + addVoxel( + ix, + surfaceY + 1, + iz, + 'grass', + varyColor('grass', Math.random()), + 0.5 + ); + } + + // 沙漠平面不添加半高沙块,保持平坦 + // 所有起伏通过装饰物(石头、石台)实现 + + // Decorations - 树木成组生成,草丛围绕树木 + if (mx === 4 && mz === 4) { + // 旧植被生成逻辑已移除 + } + + } + } + } + } + + // 基础地形生成完成 + reportProgress('basic', 100, '基础地形生成完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 开始特征地形阶段(在基础地形循环中已经包含了戈壁、石头等特征) + reportProgress('features', 100, '特征地形生成完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 开始后处理阶段 + reportProgress('postprocessing', 0, '开始地貌后处理'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // ===== 水体颜色后处理:基于离陆地距离 ===== + // 1. 构建逻辑地形类型映射(基于 topColumns 的 baseType) + const logicalTerrainMap = new Map(); + topColumns.forEach(({ baseType }, key) => { + logicalTerrainMap.set(key, baseType); + }); + + // 2. 为每个水体方块计算到最近陆地的距离 + voxels.forEach(voxel => { + if (voxel.type === 'water') { + // 转换到逻辑坐标 + const logicX = Math.floor(voxel.ix / TILE_SIZE); + const logicZ = Math.floor(voxel.iz / TILE_SIZE); + + // BFS 查找最近的陆地(使用逻辑坐标) + let minDistToLand = Infinity; + + // 搜索半径(逻辑坐标)- 增加到 12 格 + const searchRadius = 12; + for (let dx = -searchRadius; dx <= searchRadius; dx++) { + for (let dz = -searchRadius; dz <= searchRadius; dz++) { + const checkX = logicX + dx; + const checkZ = logicZ + dz; + const checkType = logicalTerrainMap.get(`${checkX}|${checkZ}`); + + // 如果是陆地(非水体) + if (checkType && checkType !== 'water') { + const dist = Math.sqrt(dx * dx + dz * dz); + if (dist < minDistToLand) { + minDistToLand = dist; + } + } + } + } + + // 根据距离重新计算颜色 + // 使用平滑的平方根函数:让浅色区域更长,深色只在远处出现 + // sqrt(x) 函数增长缓慢,适合创建平滑渐变 + const normalizedDist = Math.min(minDistToLand / 12, 1); // 归一化到 0-1 + const effectiveDepth = Math.sqrt(normalizedDist) * 10; // 平方根映射到 0-10 + + // 降低颜色噪声,让渐变更平滑 + voxel.color = varyColor('water', Math.random() * 0.1, effectiveDepth); + } + }); + + reportProgress('postprocessing', 30, '水体颜色处理完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // ===== 泥土边界渗透后处理 ===== + // (已移除:逻辑已合并至 postprocessing.ts 的 applyDirtLayerBlending 函数中,在生成阶段直接处理) + + reportProgress('postprocessing', 60, '泥土渗透处理完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 后处理完成,开始剔除隐藏体素 + reportProgress('postprocessing', 70, '开始剔除隐藏体素'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // Cull hidden voxels first + const neighborOffsets = [ + [1, 0, 0], + [-1, 0, 0], + [0, 1, 0], + [0, -1, 0], + [0, 0, 1], + [0, 0, -1], + ]; + + const culledVoxels = voxels.filter((voxel) => { + // Always keep bedrock bottom layer to avoid gaps + if (voxel.iy <= MIN_WORLD_Y + 1) return true; + + for (const [dx, dy, dz] of neighborOffsets) { + let neighborKey = keyOf(voxel.ix + dx, voxel.iy + dy, voxel.iz + dz); + // 如果是树木体素,检查树木专用的key空间 + if (voxel.isHighRes) { + neighborKey = `tree_${voxel.ix + dx}|${voxel.iy + dy}|${voxel.iz + dz}`; + } + + if (!occupancy.has(neighborKey)) { + return true; + } + } + return false; + }); + + reportProgress('postprocessing', 90, '隐藏体素剔除完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 开始植被生成阶段(当前版本植被已在基础地形中生成) + reportProgress('vegetation', 0, '开始生成植被'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 新植被系统集成 + // 构建简化的 heightMap + const heightMap = new Map(); + // topColumns key 是 "ix|iz",value 是 { index, baseType } + // voxels[index].iy 即为最高点的 Y 坐标 + topColumns.forEach((data, key) => { + heightMap.set(key, voxels[data.index].iy); + }); + + const vegContext: VegetationGenerationContext = { + mapSize, + voxels, + occupancy, + treePositions, + seed: seedVal, + desertContext, + isDesertScene, + sceneType: sceneConfig?.name, + heightMap + }; + + const newPlantVoxels = await generateVegetation(vegContext); + // 关键修复:将植被添加到 culledVoxels (最终返回的数组),而不是 voxels (原始数组) + // 植被体素不需要再次进行剔除检查,直接作为表面装饰添加 + culledVoxels.push(...newPlantVoxels); + + // Rebuild topColumns map after culling using coordinates + const topColumnsAfterCull = new Map(); + culledVoxels.forEach((voxel) => { + const colKey = `${voxel.ix}|${voxel.iz}`; + const existing = topColumnsAfterCull.get(colKey); + if (!existing || voxel.iy > existing.voxel.iy) { + const original = topColumns.get(colKey); + if (original) { + topColumnsAfterCull.set(colKey, { voxel, baseType: original.baseType }); + } + } + }); + + // Apply surface blend logic based on neighbors AFTER culling + // 临时禁用边界混合逻辑,用于调试 + // const neighbor2D = [ + // [1, 0], + // [-1, 0], + // [0, 1], + // [0, -1], + // ]; + + // topColumnsAfterCull.forEach(({ voxel, baseType }) => { + // const rules = BLEND_RULES.filter((r) => r.base === baseType); + // if (!rules.length) return; + + // for (const rule of rules) { + // const hasNeighbor = neighbor2D.some(([dx, dz]) => { + // const neighbor = topColumnsAfterCull.get(`${voxel.ix + dx}|${voxel.iz + dz}`); + // return neighbor && neighbor.baseType === rule.neighbor; + // }); + // if (hasNeighbor) { + // voxel.type = rule.result as VoxelType; + // voxel.color = varyColor(rule.result as VoxelType, Math.random() * 0.2); + // break; + // } + // } + // }); + + // 植被生成完成 + reportProgress('vegetation', 100, '植被生成完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // 最终完成 + reportProgress('complete', 100, '地形生成完成'); + await new Promise(resolve => setTimeout(resolve, 0)); + + return culledVoxels; +}; diff --git a/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts b/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts new file mode 100644 index 0000000..d566146 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/vegetation.ts @@ -0,0 +1,2420 @@ +/** + * 植物系统:包含所有植物相关的结构和生成函数 + * - 树木(阔叶树、针叶树、果树) + * - 草类(草丛、高草、蕨类、芦苇) + * - 花朵(小花、中花、大花、花丛) + * - 仙人掌(柱状、L形分枝、仙人球等) + */ + +import type { VoxelType } from './voxelStyles'; + +// ============= 工具函数 ============= + +// 伪随机函数 +export function pseudoRandom(seed: number): number { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); +} + +// RGB转十六进制 +export const rgbToHex = (r: number, g: number, b: number): string => { + return '#' + [r, g, b].map(x => { + const hex = Math.round(x).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); +}; + +// 十六进制转RGB +export const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +// 随机选择 +export const pickRandom = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; + +// ============= 类型定义 ============= + +export interface StructureTemplate { + name: string; + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }>; +} + +export type AddTreeVoxelFn = ( + fineX: number, + fineY: number, + fineZ: number, + type: VoxelType, + color: string, + heightScale?: number +) => void; + + +// Helpers for procedural tree generation +const createTrunk = (height: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let y = 0; y < height; y++) { + blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); + blocks.push({ dx: 1, dy: y, dz: 0, type: 'wood' }); + blocks.push({ dx: 0, dy: y, dz: 1, type: 'wood' }); + blocks.push({ dx: 1, dy: y, dz: 1, type: 'wood' }); + } + return blocks; +}; + +const createSphereCrown = (radius: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 改进的蘑菇伞形树冠 + // 使用更大的高度比例,让顶部更圆润 + const crownHeight = radius * 1.8; // 高度是半径的1.8倍,更高更圆润 + const centerY = yStart + crownHeight * 0.45; // 球心位置稍微靠下,让顶部更圆 + + const rSq = radius * radius; + const range = Math.ceil(radius); + + // 生成球形树冠 + for (let y = -range; y <= Math.ceil(crownHeight); y++) { + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dy = y; + const dz = z - centerZ; + const distSq = dx*dx + dy*dy + dz*dz; + + if (distSq <= rSq) { + const actualY = Math.round(centerY + y); + + // 避免在树干位置生成(底部中心区域) + if (actualY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; + + // 底部稍微收窄,形成蘑菇伞的效果 + const heightFromCenter = y; + if (heightFromCenter < -radius * 0.5) { + // 底部区域,需要收窄 + const bottomShrink = 0.7; // 底部收窄到70% + const adjustedDistSq = (dx*dx + dz*dz) / (bottomShrink * bottomShrink) + dy*dy; + if (adjustedDistSq > rSq) continue; + } + + blocks.push({ dx: x, dy: actualY, dz: z, type: 'leaves' }); + } + } + } + } + + return blocks; +}; + +const createConeCrown = (radiusBase: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + for (let y = 0; y < height; y++) { + const progress = y / height; + + // 平滑曲线,顶部1格 + const radiusFactor = Math.pow(1 - progress, 0.75); + + // 顶部强制1格 + const minRadius = progress > 0.92 ? 0.3 : 1; // 0.3确保只有1格 + const currentRadius = Math.max(minRadius, radiusBase * radiusFactor); + const rSq = currentRadius * currentRadius; + + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + // 顶部只保留中心1格 + if (progress > 0.92) { + if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } else { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + } + } + return blocks; +}; + +// 创建双层锥形树冠:圣诞树风格,两层紧密相连 +const createDoubleConeCrown = (radiusBase: number, totalHeight: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 两层紧密相连:下层50%,上层50%,无间隙 + const lowerHeight = Math.floor(totalHeight * 0.5); + const upperHeight = totalHeight - lowerHeight; + + // === 下层:大锥形 === + for (let y = 0; y < lowerHeight; y++) { + const progress = y / lowerHeight; + const radiusFactor = Math.pow(1 - progress, 0.7); + const currentRadius = Math.max(1, radiusBase * radiusFactor); + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + } + + // === 上层:小锥形(底部稍大,顶部1格)=== + const upperBaseRadius = radiusBase * 0.6; // 上层底部稍大(从0.45改为0.5) + for (let y = 0; y < upperHeight; y++) { + const progress = y / upperHeight; + const radiusFactor = Math.pow(1 - progress, 0.8); + + // 顶部强制1格 + const minRadius = progress > 0.92 ? 0.3 : 1; // 0.3确保只有1格 + const currentRadius = Math.max(minRadius, upperBaseRadius * radiusFactor); + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + // 顶部只保留中心1格 + if (progress > 0.92) { + if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { + blocks.push({ dx: x, dy: yStart + lowerHeight + y, dz: z, type: 'leaves' }); + } + } else { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: yStart + lowerHeight + y, dz: z, type: 'leaves' }); + } + } + } + } + } + } + + return blocks; +}; + +// 创建圆润树冠:顶部自然圆润,不再是平的 +const createRoundedCrown = (radius: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 蘑菇伞形树冠:底部收窄,顶部圆润 + for (let y = 0; y < height; y++) { + const progress = y / height; + + let layerR: number; + if (progress < 0.3) { + // 底部30%:从小开始,蘑菇柄效果 + layerR = radius * (0.3 + progress * 0.4); // 从半径的30%增长到42% + } else if (progress < 0.7) { + // 中部40%:快速扩大到最大,蘑菇伞的外沿 + const midProgress = (progress - 0.3) / 0.4; + layerR = radius * (0.42 + midProgress * 0.58); // 从42%增长到100% + } else { + // 顶部30%:圆润的球形顶部 + const topProgress = (progress - 0.7) / 0.3; + const sphereFactor = Math.sqrt(1 - topProgress * topProgress); // 球形曲线 + layerR = radius * sphereFactor; + } + + const rSq = layerR * layerR; + const range = Math.ceil(layerR); + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + if (dx*dx + dz*dz <= rSq) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + return blocks; +}; + +// 创建带果实的球形树冠(蘑菇伞形) +const createFruitSphereCrown = (radius: number, yStart: number, fruitColor: 'red' | 'yellow' | 'orange') => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 改进的蘑菇伞形树冠 + const crownHeight = radius * 1.8; + const centerY = yStart + crownHeight * 0.45; + + const rSq = radius * radius; + const range = Math.ceil(radius); + + // 用于存储所有叶子的位置 + const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; + + // 生成球形树冠 + for (let y = -range; y <= Math.ceil(crownHeight); y++) { + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dy = y; + const dz = z - centerZ; + const distSq = dx*dx + dy*dy + dz*dz; + + if (distSq <= rSq) { + const actualY = Math.round(centerY + y); + + if (actualY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; + + // 底部收窄 + const heightFromCenter = y; + if (heightFromCenter < -radius * 0.5) { + const bottomShrink = 0.7; + const adjustedDistSq = (dx*dx + dz*dz) / (bottomShrink * bottomShrink) + dy*dy; + if (adjustedDistSq > rSq) continue; + } + + blocks.push({ dx: x, dy: actualY, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: actualY, dz: z }); + } + } + } + } + + // 果实颜色 + const fruitRGB = fruitColor === 'red' ? { r: 220, g: 50, b: 50 } + : fruitColor === 'yellow' ? { r: 255, g: 220, b: 50 } + : { r: 255, g: 165, b: 50 }; + const fruitColorHex = rgbToHex(fruitRGB.r, fruitRGB.g, fruitRGB.b); + + // 从叶子中筛选出适合变成果实的位置: + // 1. 在树冠外围(距离中心轴较远) + // 2. 高度在20%-65%之间(避免顶部和底部) + const candidateLeaves = leafPositions.filter(leaf => { + const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); + const relativeHeight = (leaf.dy - yStart) / (crownHeight + 5); + return distFromCenter > radius * 0.6 && relativeHeight >= 0.2 && relativeHeight <= 0.65; + }); + + // 定义8个方向 + const directions = [ + { angle: 0 }, + { angle: Math.PI / 4 }, + { angle: Math.PI / 2 }, + { angle: Math.PI * 3/4 }, + { angle: Math.PI }, + { angle: Math.PI * 5/4 }, + { angle: Math.PI * 3/2 }, + { angle: Math.PI * 7/4 } + ]; + + // 在每个方向选择1-2个叶子变成果实 + const selectedFruits: Array<{dx: number, dy: number, dz: number}> = []; + + directions.forEach(dir => { + const fruitCount = 1 + Math.floor(pseudoRandom(dir.angle * 100) * 2); // 1-2个 + + for (let attempt = 0; attempt < fruitCount; attempt++) { + // 找到该方向附近的候选叶子 + const directionLeaves = candidateLeaves.filter(leaf => { + const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); + let angleDiff = Math.abs(leafAngle - dir.angle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + return angleDiff < Math.PI / 6; // 30度范围内 + }); + + if (directionLeaves.length === 0) continue; + + // 随机选择一个 + const randomIndex = Math.floor(pseudoRandom(dir.angle * 200 + attempt * 50) * directionLeaves.length); + const selectedLeaf = directionLeaves[randomIndex]; + + // 检查与已选果实的距离 + const tooClose = selectedFruits.some(fruit => { + const dx = fruit.dx - selectedLeaf.dx; + const dy = fruit.dy - selectedLeaf.dy; + const dz = fruit.dz - selectedLeaf.dz; + return dx*dx + dy*dy + dz*dz < 9; // 距离至少3格 + }); + + if (!tooClose) { + selectedFruits.push(selectedLeaf); + } + } + }); + + // 将选中的叶子位置改为果实 + selectedFruits.forEach(fruit => { + // 找到对应的叶子块并修改颜色 + const leafBlock = blocks.find(b => b.dx === fruit.dx && b.dy === fruit.dy && b.dz === fruit.dz); + if (leafBlock) { + leafBlock.type = 'flower'; + leafBlock.color = fruitColorHex; + } + }); + + return blocks; +}; + +// 创建带果实的圆润树冠(蘑菇伞形) +const createFruitRoundedCrown = (radius: number, height: number, yStart: number, fruitColor: 'red' | 'yellow' | 'orange') => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 用于存储所有叶子的位置 + const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; + + // 蘑菇伞形树冠:底部收窄,顶部圆润 + for (let y = 0; y < height; y++) { + const progress = y / height; + + let layerR: number; + if (progress < 0.3) { + layerR = radius * (0.3 + progress * 0.4); + } else if (progress < 0.7) { + const midProgress = (progress - 0.3) / 0.4; + layerR = radius * (0.42 + midProgress * 0.58); + } else { + const topProgress = (progress - 0.7) / 0.3; + const sphereFactor = Math.sqrt(1 - topProgress * topProgress); + layerR = radius * sphereFactor; + } + + const rSq = layerR * layerR; + const range = Math.ceil(layerR); + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + if (dx*dx + dz*dz <= rSq) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: yStart + y, dz: z }); + } + } + } + } + + // 果实颜色 + const fruitRGB = fruitColor === 'red' ? { r: 220, g: 50, b: 50 } + : fruitColor === 'yellow' ? { r: 255, g: 220, b: 50 } + : { r: 255, g: 165, b: 50 }; + const fruitColorHex = rgbToHex(fruitRGB.r, fruitRGB.g, fruitRGB.b); + + // 从叶子中筛选出适合变成果实的位置 + const candidateLeaves = leafPositions.filter(leaf => { + const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); + const relativeHeight = (leaf.dy - yStart) / height; + return distFromCenter > radius * 0.6 && relativeHeight >= 0.25 && relativeHeight <= 0.65; + }); + + // 定义8个方向 + const directions = [ + { angle: 0 }, + { angle: Math.PI / 4 }, + { angle: Math.PI / 2 }, + { angle: Math.PI * 3/4 }, + { angle: Math.PI }, + { angle: Math.PI * 5/4 }, + { angle: Math.PI * 3/2 }, + { angle: Math.PI * 7/4 } + ]; + + // 在每个方向选择1-2个叶子变成果实 + const selectedFruits: Array<{dx: number, dy: number, dz: number}> = []; + + directions.forEach(dir => { + const fruitCount = 1 + Math.floor(pseudoRandom(dir.angle * 100) * 2); + + for (let attempt = 0; attempt < fruitCount; attempt++) { + const directionLeaves = candidateLeaves.filter(leaf => { + const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); + let angleDiff = Math.abs(leafAngle - dir.angle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + return angleDiff < Math.PI / 6; + }); + + if (directionLeaves.length === 0) continue; + + const randomIndex = Math.floor(pseudoRandom(dir.angle * 200 + attempt * 50) * directionLeaves.length); + const selectedLeaf = directionLeaves[randomIndex]; + + const tooClose = selectedFruits.some(fruit => { + const dx = fruit.dx - selectedLeaf.dx; + const dy = fruit.dy - selectedLeaf.dy; + const dz = fruit.dz - selectedLeaf.dz; + return dx*dx + dy*dy + dz*dz < 9; + }); + + if (!tooClose) { + selectedFruits.push(selectedLeaf); + } + } + }); + + // 将选中的叶子改为果实 + selectedFruits.forEach(fruit => { + const leafBlock = blocks.find(b => b.dx === fruit.dx && b.dy === fruit.dy && b.dz === fruit.dz); + if (leafBlock) { + leafBlock.type = 'flower'; + leafBlock.color = fruitColorHex; + } + }); + + return blocks; +}; + +// 创建带果实的锥形树冠(针叶树 - 褐色松果) +const createFruitConeCrown = (radiusBase: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 用于存储所有叶子位置 + const leafPositions: Array<{dx: number, dy: number, dz: number}> = []; + + // 首先生成树冠 + for (let y = 0; y < height; y++) { + const progress = y / height; + const radiusFactor = Math.pow(1 - progress, 0.75); + const minRadius = progress > 0.92 ? 0.3 : 1; + const currentRadius = Math.max(minRadius, radiusBase * radiusFactor); + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + if (progress > 0.92) { + if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: yStart + y, dz: z }); + } + } else { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: yStart + y, dz: z }); + } + } + } + } + } + } + + // 褐色松果颜色 + const brownColor = rgbToHex(139, 90, 43); + + // 从叶子中筛选出适合变成松果的位置 + const candidateLeaves = leafPositions.filter(leaf => { + const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); + const relativeHeight = (leaf.dy - yStart) / height; + // 计算该高度的树冠半径 + const progress = relativeHeight; + const radiusFactor = Math.pow(1 - progress, 0.75); + const currentRadius = Math.max(1, radiusBase * radiusFactor); + return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.25 && relativeHeight <= 0.65; + }); + + // 定义8个方向 + const directions = [ + { angle: 0 }, + { angle: Math.PI / 4 }, + { angle: Math.PI / 2 }, + { angle: Math.PI * 3/4 }, + { angle: Math.PI }, + { angle: Math.PI * 5/4 }, + { angle: Math.PI * 3/2 }, + { angle: Math.PI * 7/4 } + ]; + + // 在每个方向选择1个叶子变成松果 + const selectedCones: Array<{dx: number, dy: number, dz: number}> = []; + + directions.forEach(dir => { + // 找到该方向附近的候选叶子 + const directionLeaves = candidateLeaves.filter(leaf => { + const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); + let angleDiff = Math.abs(leafAngle - dir.angle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + return angleDiff < Math.PI / 6; // 30度范围内 + }); + + if (directionLeaves.length === 0) return; + + // 随机选择一个 + const randomIndex = Math.floor(pseudoRandom(dir.angle * 200) * directionLeaves.length); + const selectedLeaf = directionLeaves[randomIndex]; + + // 检查与已选松果的距离 + const tooClose = selectedCones.some(cone => { + const dx = cone.dx - selectedLeaf.dx; + const dy = cone.dy - selectedLeaf.dy; + const dz = cone.dz - selectedLeaf.dz; + return dx*dx + dy*dy + dz*dz < 9; // 距离至少3格 + }); + + if (!tooClose) { + selectedCones.push(selectedLeaf); + } + }); + + // 将选中的叶子改为松果 + selectedCones.forEach(cone => { + const leafBlock = blocks.find(b => b.dx === cone.dx && b.dy === cone.dy && b.dz === cone.dz); + if (leafBlock) { + leafBlock.type = 'flower'; + leafBlock.color = brownColor; + } + }); + + return blocks; +}; + +// 创建带果实的双层锥形树冠(针叶树 - 褐色松果) +const createFruitDoubleConeCrown = (radiusBase: number, totalHeight: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + const lowerHeight = Math.floor(totalHeight * 0.5); + const upperHeight = totalHeight - lowerHeight; + + // 用于存储所有叶子位置 + const leafPositions: Array<{dx: number, dy: number, dz: number, layer: 'lower' | 'upper'}> = []; + + // === 下层:大锥形 === + for (let y = 0; y < lowerHeight; y++) { + const progress = y / lowerHeight; + const radiusFactor = Math.pow(1 - progress, 0.7); + const currentRadius = Math.max(1, radiusBase * radiusFactor); + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: yStart + y, dz: z, layer: 'lower' }); + } + } + } + } + } + + // === 上层:小锥形 === + const upperStartY = yStart + lowerHeight; + const upperRadiusBase = radiusBase * 0.65; + + for (let y = 0; y < upperHeight; y++) { + const progress = y / upperHeight; + const radiusFactor = Math.pow(1 - progress, 0.8); + const minRadius = progress > 0.9 ? 0.3 : 1; + const currentRadius = Math.max(minRadius, upperRadiusBase * radiusFactor); + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + if (progress > 0.9) { + if (Math.abs(dx) < 0.3 && Math.abs(dz) < 0.3) { + blocks.push({ dx: x, dy: upperStartY + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: upperStartY + y, dz: z, layer: 'upper' }); + } + } else { + const edgeFactor = distSq / rSq; + const shouldPlace = edgeFactor < 0.85 || Math.random() > 0.3; + + if (shouldPlace) { + blocks.push({ dx: x, dy: upperStartY + y, dz: z, type: 'leaves' }); + leafPositions.push({ dx: x, dy: upperStartY + y, dz: z, layer: 'upper' }); + } + } + } + } + } + } + + // 褐色松果颜色 + const brownColor = rgbToHex(139, 90, 43); + + // 从下层叶子中筛选候选位置 + const lowerLeaves = leafPositions.filter(leaf => leaf.layer === 'lower'); + const lowerCandidates = lowerLeaves.filter(leaf => { + const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); + const relativeHeight = (leaf.dy - yStart) / lowerHeight; + const progress = relativeHeight; + const radiusFactor = Math.pow(1 - progress, 0.7); + const currentRadius = Math.max(1, radiusBase * radiusFactor); + return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.3 && relativeHeight <= 0.65; + }); + + // 从上层叶子中筛选候选位置 + const upperLeaves = leafPositions.filter(leaf => leaf.layer === 'upper'); + const upperCandidates = upperLeaves.filter(leaf => { + const distFromCenter = Math.sqrt((leaf.dx - 0.5)**2 + (leaf.dz - 0.5)**2); + const relativeHeight = (leaf.dy - upperStartY) / upperHeight; + const progress = relativeHeight; + const radiusFactor = Math.pow(1 - progress, 0.8); + const currentRadius = Math.max(1, upperRadiusBase * radiusFactor); + return distFromCenter > currentRadius * 0.6 && relativeHeight >= 0.3 && relativeHeight <= 0.65; + }); + + // 定义8个方向 + const directions = [ + { angle: 0 }, + { angle: Math.PI / 4 }, + { angle: Math.PI / 2 }, + { angle: Math.PI * 3/4 }, + { angle: Math.PI }, + { angle: Math.PI * 5/4 }, + { angle: Math.PI * 3/2 }, + { angle: Math.PI * 7/4 } + ]; + + // 在每个方向选择1个叶子变成松果(下层和上层各一个) + const selectedCones: Array<{dx: number, dy: number, dz: number}> = []; + + // 下层松果 + directions.forEach(dir => { + const directionLeaves = lowerCandidates.filter(leaf => { + const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); + let angleDiff = Math.abs(leafAngle - dir.angle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + return angleDiff < Math.PI / 6; + }); + + if (directionLeaves.length === 0) return; + + const randomIndex = Math.floor(pseudoRandom(dir.angle * 200) * directionLeaves.length); + const selectedLeaf = directionLeaves[randomIndex]; + + const tooClose = selectedCones.some(cone => { + const dx = cone.dx - selectedLeaf.dx; + const dy = cone.dy - selectedLeaf.dy; + const dz = cone.dz - selectedLeaf.dz; + return dx*dx + dy*dy + dz*dz < 9; + }); + + if (!tooClose) { + selectedCones.push({dx: selectedLeaf.dx, dy: selectedLeaf.dy, dz: selectedLeaf.dz}); + } + }); + + // 上层松果 + directions.forEach(dir => { + const directionLeaves = upperCandidates.filter(leaf => { + const leafAngle = Math.atan2(leaf.dz - 0.5, leaf.dx - 0.5); + let angleDiff = Math.abs(leafAngle - dir.angle); + if (angleDiff > Math.PI) angleDiff = Math.PI * 2 - angleDiff; + return angleDiff < Math.PI / 6; + }); + + if (directionLeaves.length === 0) return; + + const randomIndex = Math.floor(pseudoRandom(dir.angle * 250) * directionLeaves.length); + const selectedLeaf = directionLeaves[randomIndex]; + + const tooClose = selectedCones.some(cone => { + const dx = cone.dx - selectedLeaf.dx; + const dy = cone.dy - selectedLeaf.dy; + const dz = cone.dz - selectedLeaf.dz; + return dx*dx + dy*dy + dz*dz < 9; + }); + + if (!tooClose) { + selectedCones.push({dx: selectedLeaf.dx, dy: selectedLeaf.dy, dz: selectedLeaf.dz}); + } + }); + + // 将选中的叶子改为松果 + selectedCones.forEach(cone => { + const leafBlock = blocks.find(b => b.dx === cone.dx && b.dy === cone.dy && b.dz === cone.dz); + if (leafBlock) { + leafBlock.type = 'flower'; + leafBlock.color = brownColor; + } + }); + + return blocks; +}; + +// 创建细树干(1x1,用于椰树等) +const createThinTrunk = (height: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let y = 0; y < height; y++) { + blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); + } + return blocks; +}; + +// 创建双细树干(并排,用于椰树) +const createDoubleThinTrunk = (height: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let y = 0; y < height; y++) { + // 左侧树干 + blocks.push({ dx: 0, dy: y, dz: 0, type: 'wood' }); + // 右侧树干(并排) + blocks.push({ dx: 1, dy: y, dz: 0, type: 'wood' }); + } + return blocks; +}; + +// 创建椰树树冠:顶部有多个扇形叶片(双树干版本,中心在两个树干之间) +const createCoconutPalmCrown = (yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 1.0; // 两个树干之间:(0,0) 和 (1,0) 的中心 + const centerZ = 0.5; + + // 创建6-8个扇形叶片,从中心向外辐射 + const frondCount = 6 + Math.floor(Math.random() * 3); // 6-8片 + const baseY = yStart; + + for (let i = 0; i < frondCount; i++) { + const angle = (i / frondCount) * Math.PI * 2; + const frondLength = 5 + Math.random() * 2; // 5-7格长度 + + // 每个叶片是一个扇形,从中心向外延伸 + for (let dist = 0; dist < frondLength; dist++) { + const width = Math.max(1, Math.floor(3 - dist * 0.3)); // 宽度随距离递减 + + // 在垂直于叶片方向创建宽度 + for (let w = -width; w <= width; w++) { + const perpAngle = angle + Math.PI / 2; + const dx = Math.round(centerX + dist * Math.cos(angle) + w * Math.cos(perpAngle) * 0.3); + const dz = Math.round(centerZ + dist * Math.sin(angle) + w * Math.sin(perpAngle) * 0.3); + + // 叶片有轻微的高度变化 + const heightOffset = Math.floor(Math.abs(Math.sin(dist * 0.5)) * 2); + blocks.push({ dx, dy: baseY + heightOffset, dz, type: 'leaves' }); + } + } + } + + // 中心区域填充一些叶子 + for (let x = -1; x <= 1; x++) { + for (let z = -1; z <= 1; z++) { + if (Math.abs(x) + Math.abs(z) <= 1) { + blocks.push({ dx: Math.round(centerX + x), dy: baseY, dz: Math.round(centerZ + z), type: 'leaves' }); + } + } + } + + return blocks; +}; + +// 创建小球形树冠:紧凑的小球 +const createSmallSphereCrown = (radius: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const rSq = radius * radius; + const centerX = 0.5; + const centerZ = 0.5; + const centerY = yStart + radius * 0.6; + + const range = Math.ceil(radius); + for (let y = -range; y <= range; y++) { + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dy = y; + const dz = z - centerZ; + if (dx*dx + dy*dy + dz*dz <= rSq) { + if (y + centerY < yStart && Math.abs(x-0.5)<1 && Math.abs(z-0.5)<1) continue; + blocks.push({ dx: x, dy: Math.round(centerY + y), dz: z, type: 'leaves' }); + } + } + } + } + return blocks; +}; + +// 创建分散的松树树冠:稀疏的针叶,可以看到树干 +const createSparsePineCrown = (radiusBase: number, height: number, yStart: number, density: number = 0.4) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + for (let y = 0; y < height; y++) { + const progress = y / height; + const currentRadius = Math.max(0, radiusBase * (1 - progress * 0.8)); + const rSq = currentRadius * currentRadius; + + const range = Math.ceil(currentRadius); + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + // 根据密度随机决定是否放置叶子 + // 中心区域密度更高,边缘更稀疏 + const distFromCenter = Math.sqrt(distSq); + const centerDensity = distFromCenter < currentRadius * 0.3 ? density * 1.5 : density; + + if (Math.random() < centerDensity) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + } + return blocks; +}; + +// 创建伞形树冠:顶部大而圆润(不再是平的) +const createUmbrellaCrown = (radius: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 从树干到顶部的过渡,顶部使用球形曲线 + for (let y = 0; y < height; y++) { + const progress = y / height; + + // 底部到中部:快速扩大 + let currentRadius: number; + if (progress < 0.7) { + // 底部到中部:二次增长 + currentRadius = 1 + (radius - 1) * progress * progress / 0.49; + } else { + // 顶部:使用球形曲线,让顶部圆润而不是平的 + const topProgress = (progress - 0.7) / 0.3; // 0-1 + const sphereFactor = Math.sqrt(1 - topProgress * topProgress); // 球形曲线 + currentRadius = radius * sphereFactor; + } + + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + if (dx*dx + dz*dz <= rSq && dx*dx + dz*dz > 1) { // 排除树干区域 + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + + return blocks; +}; + +// 创建圆柱形树冠:垂直的圆柱体,顶部圆润 +const createCylindricalCrown = (radius: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + for (let y = 0; y < height; y++) { + const progress = y / height; + + // 顶部使用球形曲线,让顶部圆润 + let currentRadius: number; + if (progress < 0.8) { + // 底部到中部:保持圆柱形 + currentRadius = radius; + } else { + // 顶部:使用球形曲线 + const topProgress = (progress - 0.8) / 0.2; // 0-1 + const sphereFactor = Math.sqrt(1 - topProgress * topProgress); + currentRadius = radius * sphereFactor; + } + + const rSq = currentRadius * currentRadius; + const range = Math.ceil(currentRadius); + + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + if (dx*dx + dz*dz <= rSq) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + return blocks; +}; + +// 创建多层球形树冠:多个球形堆叠 +const createMultiSphereCrown = (baseRadius: number, sphereCount: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + let currentY = yStart; + + for (let i = 0; i < sphereCount; i++) { + const radius = baseRadius * (1 - i * 0.15); // 每层稍微小一点 + const rSq = radius * radius; + const centerY = currentY + radius * 0.7; + + const range = Math.ceil(radius); + for (let y = -range; y <= range; y++) { + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dy = y; + const dz = z - centerZ; + if (dx*dx + dy*dy + dz*dz <= rSq) { + const finalY = Math.round(centerY + y); + if (finalY >= currentY) { // 避免与下层重叠太多 + blocks.push({ dx: x, dy: finalY, dz: z, type: 'leaves' }); + } + } + } + } + } + + currentY += radius * 1.2; // 移动到下一层 + } + + return blocks; +}; + +// 创建不规则树冠:随机形状,更自然 +const createIrregularCrown = (baseRadius: number, height: number, yStart: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 使用噪声创建不规则形状 + const noiseSeed = Math.random() * 1000; + const noise = (x: number, y: number, z: number) => { + const val = Math.sin(x * 0.5 + noiseSeed) * Math.cos(y * 0.3) * Math.sin(z * 0.5 + noiseSeed); + return (val + 1) * 0.5; // 归一化到 0-1 + }; + + for (let y = 0; y < height; y++) { + const progress = y / height; + const baseR = baseRadius * (1 - progress * 0.6); + + const range = Math.ceil(baseR * 1.5); + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const dist = Math.sqrt(dx*dx + dz*dz); + + if (dist <= baseR * 1.2) { + // 使用噪声决定是否放置叶子 + const noiseVal = noise(x, y, z); + const threshold = 0.3 + progress * 0.3; // 底部更密集 + + if (noiseVal > threshold && dist < baseR * (0.7 + noiseVal * 0.5)) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'leaves' }); + } + } + } + } + } + + return blocks; +}; + +// 创建草丛:统一的草类生成函数 +const createGrassTuft = (count: number) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let i = 0; i < count; i++) { + const r = Math.random() * 2.5; + const angle = Math.random() * Math.PI * 2; + const dx = Math.round(r * Math.cos(angle)); + const dz = Math.round(r * Math.sin(angle)); + + const height = Math.floor(Math.random() * 3) + 2; + + for (let y = 0; y < height; y++) { + blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); + } + } + return blocks; +}; + +// 创建高草丛:细长的矩形块,高度不同 +const createTallGrass = (count: number = 8) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let i = 0; i < count; i++) { + const r = Math.random() * 2.0; + const angle = Math.random() * Math.PI * 2; + const dx = Math.round(r * Math.cos(angle)); + const dz = Math.round(r * Math.sin(angle)); + + // 高度变化:3-8 个体素 + const height = Math.floor(Math.random() * 5) + 3; + const width = Math.random() < 0.5 ? 1 : 0; // 细长型,偶尔有宽度 + + for (let y = 0; y < height; y++) { + if (width === 0) { + // 单个体素宽 + blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); + } else { + // 偶尔有2个体素宽 + blocks.push({ dx, dy: y, dz, type: 'dark_grass' }); + if (Math.random() < 0.3) { + const offset = Math.random() < 0.5 ? 1 : -1; + blocks.push({ dx: dx + offset, dy: y, dz, type: 'dark_grass' }); + } + } + } + } + return blocks; +}; + +// 创建蕨类植物:中央茎 + 横向分支的叶子 +const createFern = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0; + const centerZ = 0; + + // 中央茎:高度 2-4 + const stemHeight = Math.floor(Math.random() * 3) + 2; + for (let y = 0; y < stemHeight; y++) { + blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'dark_grass' }); + } + + // 横向分支:从不同高度向两侧延伸 + const branchCount = Math.floor(Math.random() * 3) + 2; // 2-4 个分支 + for (let i = 0; i < branchCount; i++) { + const branchY = Math.floor(Math.random() * stemHeight) + 1; + const direction = Math.random() * Math.PI * 2; + const branchLength = Math.floor(Math.random() * 2) + 1; // 1-2 个体素长 + + for (let j = 0; j < branchLength; j++) { + const offsetX = Math.round(Math.cos(direction) * j); + const offsetZ = Math.round(Math.sin(direction) * j); + blocks.push({ dx: centerX + offsetX, dy: branchY, dz: centerZ + offsetZ, type: 'dark_grass' }); + } + } + + return blocks; +}; + +// ========== 重写的花朵系统:严格对称 ========== + +// 1. 小花:4片花瓣十字形围绕1格花蕊,无茎干 +const createSmallFlower = (petalColor: string, centerColor: string) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; + const centerX = 0; + const centerZ = 0; + const y = 0; // 直接贴地 + + // 中心花蕊(1格) + blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: centerColor }); + + // 四片花瓣:上下左右四个方向(十字形) + blocks.push({ dx: centerX + 1, dy: y, dz: centerZ, type: 'flower', color: petalColor }); // 右 + blocks.push({ dx: centerX - 1, dy: y, dz: centerZ, type: 'flower', color: petalColor }); // 左 + blocks.push({ dx: centerX, dy: y, dz: centerZ + 1, type: 'flower', color: petalColor }); // 前 + blocks.push({ dx: centerX, dy: y, dz: centerZ - 1, type: 'flower', color: petalColor }); // 后 + + return blocks; +}; + +// 2. 中花:8片花瓣围绕1格花蕊(9格正方形),可选2格高度茎干 +const createMediumFlower = (petalColor: string, centerColor: string, hasStem: boolean = true) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; + const centerX = 0; + const centerZ = 0; + let flowerY = 0; + + // 可选茎干(2格高度) + if (hasStem) { + const stemHeight = 2; + for (let y = 0; y < stemHeight; y++) { + blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 + } + flowerY = stemHeight; + } + + // 中心花蕊(1格) + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ, type: 'flower', color: centerColor }); + + // 8片花瓣:上下左右 + 四个对角线方向(正方形) + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ, type: 'flower', color: petalColor }); // 右 + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ, type: 'flower', color: petalColor }); // 左 + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 前 + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 后 + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 右前 + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 左后 + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: petalColor }); // 右后 + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: petalColor }); // 左前 + + return blocks; +}; + +// 3. 小花丛:由两个小花组成,其中一个带3格高度茎干 +const createSmallFlowerCluster = (petalColor1: string, centerColor1: string, petalColor2: string, centerColor2: string) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; + + // 第一朵花:带3格高度茎干 + const stemHeight = 3; + for (let y = 0; y < stemHeight; y++) { + blocks.push({ dx: 0, dy: y, dz: 0, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 + } + // 花蕊 + blocks.push({ dx: 0, dy: stemHeight, dz: 0, type: 'flower', color: centerColor1 }); + // 花瓣 + blocks.push({ dx: 1, dy: stemHeight, dz: 0, type: 'flower', color: petalColor1 }); + blocks.push({ dx: -1, dy: stemHeight, dz: 0, type: 'flower', color: petalColor1 }); + blocks.push({ dx: 0, dy: stemHeight, dz: 1, type: 'flower', color: petalColor1 }); + blocks.push({ dx: 0, dy: stemHeight, dz: -1, type: 'flower', color: petalColor1 }); + + // 第二朵花:无茎干,贴地,位置偏移 + const offset = 2; + blocks.push({ dx: offset, dy: 0, dz: 0, type: 'flower', color: centerColor2 }); + blocks.push({ dx: offset + 1, dy: 0, dz: 0, type: 'flower', color: petalColor2 }); + blocks.push({ dx: offset - 1, dy: 0, dz: 0, type: 'flower', color: petalColor2 }); + blocks.push({ dx: offset, dy: 0, dz: 1, type: 'flower', color: petalColor2 }); + blocks.push({ dx: offset, dy: 0, dz: -1, type: 'flower', color: petalColor2 }); + + return blocks; +}; + +// 4. 大花:4格高度茎干,多层花瓣 +const createLargeFlower = (innerPetalColor: string, outerPetalColor: string, centerColor: string) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color: string }> = []; + const centerX = 0; + const centerZ = 0; + + // 茎干(4格高度) + const stemHeight = 4; + for (let y = 0; y < stemHeight; y++) { + blocks.push({ dx: centerX, dy: y, dz: centerZ, type: 'flower', color: rgbToHex(100, 200, 100) }); // 恢复原来的亮绿色茎 + } + + const flowerY = stemHeight; + + // 中心花蕊(1格) + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ, type: 'flower', color: centerColor }); + + // 内层花瓣(4片,十字形) + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ, type: 'flower', color: innerPetalColor }); + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ, type: 'flower', color: innerPetalColor }); + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 1, type: 'flower', color: innerPetalColor }); + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 1, type: 'flower', color: innerPetalColor }); + + // 外层花瓣(8片,包围内层) + blocks.push({ dx: centerX + 2, dy: flowerY, dz: centerZ, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX - 2, dy: flowerY, dz: centerZ, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ + 2, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX, dy: flowerY, dz: centerZ - 2, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX + 1, dy: flowerY, dz: centerZ - 1, type: 'flower', color: outerPetalColor }); + blocks.push({ dx: centerX - 1, dy: flowerY, dz: centerZ + 1, type: 'flower', color: outerPetalColor }); + + return blocks; +}; + +// 创建芦苇:金黄色细长矩形块 +const createReed = (count: number = 8) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + for (let i = 0; i < count; i++) { + const r = Math.random() * 2.0; + const angle = Math.random() * Math.PI * 2; + const dx = Math.round(r * Math.cos(angle)); + const dz = Math.round(r * Math.sin(angle)); + + // 高度变化:3-8 个体素,细长型 + const height = Math.floor(Math.random() * 5) + 3; + + for (let y = 0; y < height; y++) { + blocks.push({ dx, dy: y, dz, type: 'reed' }); + } + } + return blocks; +}; + +// 辅助函数:添加圆柱形节段(带肋状结构) +const addCylindricalSegment = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + centerX: number, + centerZ: number, + yStart: number, + height: number, + radius: number, + ribCount: number = 8 +) => { + const rSq = radius * radius; + + for (let y = 0; y < height; y++) { + const range = Math.ceil(radius); + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dz = z - centerZ; + const distSq = dx*dx + dz*dz; + + if (distSq <= rSq) { + // 添加肋状结构:在特定角度创建凹陷 + const angle = Math.atan2(dz, dx); + const ribAngle = (Math.PI * 2) / ribCount; + const ribIndex = Math.floor((angle + Math.PI) / ribAngle); + const ribCenterAngle = ribIndex * ribAngle - Math.PI; + const angleDiff = Math.abs(angle - ribCenterAngle); + + // 肋的凹陷效果:在肋的中心位置稍微缩小半径 + const ribEffect = Math.cos(angleDiff * ribCount / 2) * 0.15; + const effectiveRadius = radius * (1 - ribEffect); + + if (distSq <= effectiveRadius * effectiveRadius) { + blocks.push({ dx: x, dy: yStart + y, dz: z, type: 'cactus' }); + } + } + } + } + } +}; + +// 辅助函数:添加分支(从主干的特定位置) +const addBranch = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + startX: number, + startZ: number, + startY: number, + direction: number, // 角度(弧度) + length: number, + radius: number, + ribCount: number = 6 +) => { + for (let i = 0; i < length; i++) { + const progress = i / length; + const currentY = startY + i; + const currentX = startX + Math.cos(direction) * progress * 0.3; + const currentZ = startZ + Math.sin(direction) * progress * 0.3; + const currentRadius = radius * (1 - progress * 0.1); // 逐渐变细 + + addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); + } +}; + +// 辅助函数:添加L形分枝(先水平延伸,然后向上弯曲) +const addLBranch = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + startX: number, + startZ: number, + startY: number, + horizontalDirection: number, // 水平方向角度(弧度) + horizontalLength: number, // 水平段长度 + verticalLength: number, // 垂直段长度 + radius: number, + ribCount: number = 6 +) => { + // 水平段:从主干向外水平延伸 + for (let i = 0; i < horizontalLength; i++) { + const progress = i / horizontalLength; + const currentX = startX + Math.cos(horizontalDirection) * progress * 0.5; + const currentZ = startZ + Math.sin(horizontalDirection) * progress * 0.5; + const currentY = startY; + const currentRadius = radius * (1 - progress * 0.05); + + addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); + } + + // 垂直段(向上弯曲):从水平段末端开始向上 + const verticalStartX = startX + Math.cos(horizontalDirection) * horizontalLength * 0.5; + const verticalStartZ = startZ + Math.sin(horizontalDirection) * horizontalLength * 0.5; + + for (let i = 0; i < verticalLength; i++) { + const progress = i / verticalLength; + // 向上弯曲:使用正弦函数创建平滑的向上弯曲效果 + const curveOffset = Math.sin(progress * Math.PI * 0.5) * 0.4; // 向上弯曲 + const currentY = startY + i + Math.round(curveOffset); + const currentX = verticalStartX; + const currentZ = verticalStartZ; + const currentRadius = radius * (1 - progress * 0.1); + + addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, currentRadius, ribCount); + } +}; + +// 辅助函数:添加倾斜的圆柱形节段 +const addTiltedCylindricalSegment = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + startX: number, + startZ: number, + startY: number, + height: number, + radius: number, + tiltAngle: number, // 倾斜角度(弧度) + tiltDirection: number, // 倾斜方向(弧度) + ribCount: number = 8 +) => { + for (let i = 0; i < height; i++) { + const progress = i / height; + const tiltOffset = Math.sin(progress * Math.PI) * tiltAngle; + const currentX = startX + Math.cos(tiltDirection) * tiltOffset; + const currentZ = startZ + Math.sin(tiltDirection) * tiltOffset; + const currentY = startY + i; + + addCylindricalSegment(blocks, currentX, currentZ, currentY, 1, radius, ribCount); + } +}; + +// 辅助函数:添加不规则的圆柱形节段(半径变化) +const addIrregularCylindricalSegment = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + centerX: number, + centerZ: number, + yStart: number, + height: number, + baseRadius: number, + variation: number = 0.2, // 半径变化幅度 + ribCount: number = 8 +) => { + for (let y = 0; y < height; y++) { + const progress = y / height; + // 使用正弦波创建不规则的半径变化 + const radiusVariation = Math.sin(progress * Math.PI * 3) * variation; + const currentRadius = baseRadius + radiusVariation; + + addCylindricalSegment(blocks, centerX, centerZ, yStart + y, 1, currentRadius, ribCount); + } +}; + +// 辅助函数:添加球形节段 +const addSphericalSegment = ( + blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }>, + centerX: number, + centerZ: number, + centerY: number, + radius: number, + ribCount: number = 8 +) => { + const rSq = radius * radius; + const range = Math.ceil(radius); + + for (let y = -range; y <= range; y++) { + for (let x = -range; x <= range + 1; x++) { + for (let z = -range; z <= range + 1; z++) { + const dx = x - centerX; + const dy = y; + const dz = z - centerZ; + const distSq = dx*dx + dy*dy + dz*dz; + + if (distSq <= rSq) { + // 添加垂直肋状结构 + const angle = Math.atan2(dz, dx); + const ribAngle = (Math.PI * 2) / ribCount; + const ribIndex = Math.floor((angle + Math.PI) / ribAngle); + const ribCenterAngle = ribIndex * ribAngle - Math.PI; + const angleDiff = Math.abs(angle - ribCenterAngle); + + const ribEffect = Math.cos(angleDiff * ribCount / 2) * 0.12; + const effectiveRadius = radius * (1 - ribEffect); + + if (distSq <= effectiveRadius * effectiveRadius) { + blocks.push({ dx: x, dy: Math.round(centerY + y), dz: z, type: 'cactus' }); + } + } + } + } + } +}; + +// 创建小型仙人掌:简单的柱状,有节段和肋 +const createSmallCactus = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 参考图:左侧小型仙人掌,高度约3-5格,1-2个L形分枝 + const mainHeight = 3 + Math.floor(Math.random() * 3); // 3-5格 + const branchCount = Math.random() < 0.5 ? 1 : 2; // 1-2个分枝 + + // 主干:简单的柱状,略微变细 + for (let i = 0; i < mainHeight; i++) { + const progress = i / mainHeight; + const currentRadius = 1.0 - progress * 0.1; // 轻微变细 + addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 8); + } + + // 添加L形分枝 + if (branchCount >= 1) { + // 第一个分枝:从主干中部偏下位置 + const branch1Height = Math.floor(mainHeight * 0.4); + const branch1Direction = Math.random() * Math.PI * 2; + const branch1Length = 2 + Math.floor(Math.random() * 2); // 2-3格 + addLBranch(blocks, centerX, centerZ, branch1Height, branch1Direction, 2, branch1Length, 0.8, 6); + } + + if (branchCount >= 2) { + // 第二个分枝:从主干中部偏上位置,方向与第一个相反 + const branch2Height = Math.floor(mainHeight * 0.6); + const branch2Direction = Math.random() * Math.PI * 2; + const branch2Length = 2 + Math.floor(Math.random() * 2); // 2-3格 + addLBranch(blocks, centerX, centerZ, branch2Height, branch2Direction, 2, branch2Length, 0.8, 6); + } + + return blocks; +}; + +// 创建中型仙人掌:参考图中间,高度约6-8格,2-3个分枝 +const createMediumCactus = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + const mainHeight = 6 + Math.floor(Math.random() * 3); // 6-8格 + const branchCount = 2 + Math.floor(Math.random() * 2); // 2-3个分枝 + + // 主干:分段,逐渐变细 + for (let i = 0; i < mainHeight; i++) { + const progress = i / mainHeight; + const currentRadius = 1.2 - progress * 0.2; + addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 8); + } + + // 添加L形分枝,位置错落有致 + const branchHeights = []; + for (let i = 0; i < branchCount; i++) { + const heightProgress = (i + 1) / (branchCount + 1); // 均匀分布 + branchHeights.push(Math.floor(mainHeight * heightProgress)); + } + + branchHeights.forEach((height, index) => { + const direction = (index * Math.PI * 2) / branchCount + Math.random() * 0.5; + const horizontalLength = 2 + Math.floor(Math.random() * 2); + const verticalLength = 3 + Math.floor(Math.random() * 2); // 3-4格 + addLBranch(blocks, centerX, centerZ, height, direction, horizontalLength, verticalLength, 0.9, 6); + }); + + return blocks; +}; + +// 创建大型仙人掌:参考图右侧,高度约10-12格,3-4个分枝 +const createLargeCactus = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + const mainHeight = 10 + Math.floor(Math.random() * 3); // 10-12格 + const branchCount = 3 + Math.floor(Math.random() * 2); // 3-4个分枝 + + // 主干:多段,逐渐变细 + for (let i = 0; i < mainHeight; i++) { + const progress = i / mainHeight; + const currentRadius = 1.4 - progress * 0.3; + addCylindricalSegment(blocks, centerX, centerZ, i, 1, currentRadius, 10); + } + + // 添加L形分枝,位置、长度变化更大 + const branchHeights = []; + for (let i = 0; i < branchCount; i++) { + // 分枝主要集中在上半部分 + const heightProgress = 0.4 + (i / branchCount) * 0.5; + branchHeights.push(Math.floor(mainHeight * heightProgress)); + } + + branchHeights.forEach((height, index) => { + const direction = (index * Math.PI * 2) / branchCount + Math.random() * 0.5; + const horizontalLength = 2 + Math.floor(Math.random() * 2); + const verticalLength = 4 + Math.floor(Math.random() * 3); // 4-6格,长度变化更大 + addLBranch(blocks, centerX, centerZ, height, direction, horizontalLength, verticalLength, 1.0, 8); + }); + + return blocks; +}; + +// ========== 新的仙人掌类型 ========== + +// 创建L形分枝仙人掌:一个主干 + 1-3个L形分枝 +const createLBranchCactus = (branchCount: 1 | 2 | 3 = 2) => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 根据分支数量决定主干大小 + const trunkHeight = branchCount === 1 ? 8 : branchCount === 2 ? 10 : 12; + const trunkRadius = branchCount === 1 ? 1.2 : branchCount === 2 ? 1.3 : 1.4; + + // 主干:多个节段 + const segmentHeight = Math.floor(trunkHeight / 3); + addCylindricalSegment(blocks, centerX, centerZ, 0, segmentHeight, trunkRadius, 8); + addCylindricalSegment(blocks, centerX, centerZ, segmentHeight, segmentHeight, trunkRadius * 0.95, 8); + addCylindricalSegment(blocks, centerX, centerZ, segmentHeight * 2, trunkHeight - segmentHeight * 2, trunkRadius * 0.9, 8); + + // 添加L形分枝 + const branchStartY = Math.floor(trunkHeight * 0.4); // 从主干40%高度开始 + const branchSpacing = Math.floor(trunkHeight * 0.25); // 分枝之间的间距 + + const directions = [ + Math.PI, // 左 + 0, // 右 + Math.PI / 2, // 前 + -Math.PI / 2, // 后 + ]; + + for (let i = 0; i < branchCount; i++) { + const branchY = branchStartY + (i * branchSpacing); + const direction = directions[i % directions.length]; + const horizontalLength = 3 + Math.floor(Math.random() * 2); // 3-4 + const verticalLength = 4 + Math.floor(Math.random() * 2); // 4-5 + const branchRadius = trunkRadius * 0.7; + + addLBranch(blocks, centerX, centerZ, branchY, direction, horizontalLength, verticalLength, branchRadius, 6); + } + + return blocks; +}; + +// 创建双主干仙人掌:两根主干,粗细和高度不同 +const createDualTrunkCactus = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 第一根主干:较粗较高 + const trunk1Height = 10 + Math.floor(Math.random() * 3); // 10-12 + const trunk1Radius = 1.3; + const trunk1OffsetX = -0.3; + + addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, 0, Math.floor(trunk1Height / 3), trunk1Radius, 8); + addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, Math.floor(trunk1Height / 3), Math.floor(trunk1Height / 3), trunk1Radius * 0.95, 8); + addCylindricalSegment(blocks, centerX + trunk1OffsetX, centerZ, Math.floor(trunk1Height / 3) * 2, trunk1Height - Math.floor(trunk1Height / 3) * 2, trunk1Radius * 0.9, 8); + + // 第二根主干:较细较矮 + const trunk2Height = 7 + Math.floor(Math.random() * 3); // 7-9 + const trunk2Radius = 1.0; + const trunk2OffsetX = 0.3; + + addCylindricalSegment(blocks, centerX + trunk2OffsetX, centerZ, 0, Math.floor(trunk2Height / 2), trunk2Radius, 6); + addCylindricalSegment(blocks, centerX + trunk2OffsetX, centerZ, Math.floor(trunk2Height / 2), trunk2Height - Math.floor(trunk2Height / 2), trunk2Radius * 0.9, 6); + + return blocks; +}; + +// 改进的仙人球:根据大小在顶部添加装饰 +const createBarrelCactus = (size: 'small' | 'medium' | 'large' = 'medium') => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + + // 根据大小决定直径(必须是偶数:4、6、8) + let diameter: number; + let height: number; + let ribCount: number; // 垂直棱的数量 + + if (size === 'small') { + diameter = 4; // 直径4格 + height = 4; // 高度4格(球形) + ribCount = 8; // 8条棱 + } else if (size === 'medium') { + diameter = 6; // 直径6格 + height = 6; // 高度6格(球形) + ribCount = 12; // 12条棱 + } else { + diameter = 8; // 直径8格 + height = 8; // 高度8格(球形) + ribCount = 16; // 16条棱 + } + + const radius = diameter / 2; + const centerX = 0; + const centerZ = 0; + + // 为每条棱分配一个角度 + const ribAngles: number[] = []; + for (let i = 0; i < ribCount; i++) { + ribAngles.push((i / ribCount) * Math.PI * 2); + } + + // 逐层生成球形仙人球 + for (let y = 0; y < height; y++) { + // 计算当前层的半径(使用球形公式) + const centerY = height / 2; // 球心在中间 + const dy = y - centerY + 0.5; // 到球心的垂直距离 + const layerRadiusSq = radius * radius - dy * dy; + + if (layerRadiusSq <= 0) continue; // 超出球体范围 + + const layerRadius = Math.sqrt(layerRadiusSq); + + // 生成当前层的所有体素 + for (let x = -Math.ceil(layerRadius); x <= Math.ceil(layerRadius); x++) { + for (let z = -Math.ceil(layerRadius); z <= Math.ceil(layerRadius); z++) { + const distSq = x * x + z * z; + + if (distSq <= layerRadius * layerRadius) { + // 计算该点的角度 + const angle = Math.atan2(z, x); + + // 找到最近的棱 + let minAngleDiff = Math.PI * 2; + for (const ribAngle of ribAngles) { + let angleDiff = Math.abs(angle - ribAngle); + // 处理角度环绕(-π 到 π) + if (angleDiff > Math.PI) { + angleDiff = Math.PI * 2 - angleDiff; + } + minAngleDiff = Math.min(minAngleDiff, angleDiff); + } + + // 根据到最近棱的距离决定是否生成体素 + // 棱附近的概率更高,形成凸起的棱 + const ribThreshold = (Math.PI / ribCount) * 0.6; // 棱的宽度 + const isOnRib = minAngleDiff < ribThreshold; + + // 边缘收缩效果 + const distRatio = Math.sqrt(distSq) / layerRadius; + const shouldPlace = distRatio < 0.95 || (distRatio < 1.0 && isOnRib); + + if (shouldPlace) { + let color: string | undefined; + + // 中型和大型仙人球添加棱的色彩效果 + if (size !== 'small' && isOnRib) { + // 棱上使用稍微深一点的绿色 + color = rgbToHex(90, 140, 70); // 深绿色的棱 + } + + blocks.push({ + dx: centerX + x, + dy: y, + dz: centerZ + z, + type: 'cactus', + ...(color && { color }) + }); + } + } + } + } + } + + // 在顶部中心添加装饰 + const topY = height - 1; + const decorationType = Math.random(); + + if (decorationType < 0.33) { + // 小白点 + blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 255, 255) }); + } else if (decorationType < 0.66) { + // 小红点 + blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 80, 80) }); + } else { + // 小花(对称的4瓣花) + blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 220, 0) }); // 花心 + // 4个对称花瓣 + blocks.push({ dx: centerX + 1, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 192, 203) }); + blocks.push({ dx: centerX - 1, dy: topY + 1, dz: centerZ, type: 'flower', color: rgbToHex(255, 192, 203) }); + blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ + 1, type: 'flower', color: rgbToHex(255, 192, 203) }); + blocks.push({ dx: centerX, dy: topY + 1, dz: centerZ - 1, type: 'flower', color: rgbToHex(255, 192, 203) }); + } + + return blocks; +}; + +// 创建不规则矮胖仙人球:不对称形状,红色装饰块嵌入表面 +const createIrregularBarrelCactus = () => { + const blocks: Array<{ dx: number; dy: number; dz: number; type: VoxelType; color?: string }> = []; + const centerX = 0.5; + const centerZ = 0.5; + + // 创建不规则形状:宽度大于高度,不对称 + // 使用多个层创建不规则的矮胖形状 + const layers = [ + { y: 0, radiusX: 2.5, radiusZ: 2.5, offsetX: 0, offsetZ: 0 }, + { y: 1, radiusX: 2.8, radiusZ: 2.6, offsetX: 0.2, offsetZ: -0.1 }, + { y: 2, radiusX: 2.6, radiusZ: 2.7, offsetX: -0.1, offsetZ: 0.15 }, + { y: 3, radiusX: 2.4, radiusZ: 2.5, offsetX: 0.1, offsetZ: -0.1 }, + { y: 4, radiusX: 2.0, radiusZ: 2.2, offsetX: -0.15, offsetZ: 0.1 }, + { y: 5, radiusX: 1.5, radiusZ: 1.8, offsetX: 0.1, offsetZ: 0 }, + ]; + + const cactusPositions = new Set(); + + layers.forEach(layer => { + const rXSq = layer.radiusX * layer.radiusX; + const rZSq = layer.radiusZ * layer.radiusZ; + const rangeX = Math.ceil(layer.radiusX); + const rangeZ = Math.ceil(layer.radiusZ); + + for (let x = -rangeX; x <= rangeX; x++) { + for (let z = -rangeZ; z <= rangeZ; z++) { + const dx = x - centerX - layer.offsetX; + const dz = z - centerZ - layer.offsetZ; + const ellipse = (dx*dx) / rXSq + (dz*dz) / rZSq; + + // 添加一些随机性,使边缘更不规则 + if (ellipse <= 1 && (ellipse < 0.7 || Math.random() > 0.15)) { + const blockX = Math.round(centerX + x); + const blockZ = Math.round(centerZ + z); + blocks.push({ dx: blockX, dy: layer.y, dz: blockZ, type: 'cactus' }); + cactusPositions.add(`${blockX}|${layer.y}|${blockZ}`); + } + } + } + }); + + // 在表面不同位置嵌入红色装饰块(2-3个) + const redBlockCount = 2 + Math.floor(Math.random() * 2); // 2-3个 + const surfacePositions: Array<{ dx: number; dy: number; dz: number }> = []; + + // 收集表面位置 + cactusPositions.forEach(pos => { + const [dx, dy, dz] = pos.split('|').map(Number); + const neighbors = [ + { dx: dx + 1, dy, dz }, + { dx: dx - 1, dy, dz }, + { dx, dy: dy + 1, dz }, + { dx, dy: dy - 1, dz }, + { dx, dy, dz: dz + 1 }, + { dx, dy, dz: dz - 1 }, + ]; + + // 检查是否是表面位置 + let isSurface = false; + for (const neighbor of neighbors) { + if (!cactusPositions.has(`${neighbor.dx}|${neighbor.dy}|${neighbor.dz}`)) { + isSurface = true; + break; + } + } + + if (isSurface) { + surfacePositions.push({ dx, dy, dz }); + } + }); + + // 随机选择表面位置添加红色块 + const selectedPositions = new Set(); + const positionsToReplace: Array<{ dx: number; dy: number; dz: number }> = []; + + for (let i = 0; i < redBlockCount && i < surfacePositions.length; i++) { + let attempts = 0; + let pos; + do { + pos = surfacePositions[Math.floor(Math.random() * surfacePositions.length)]; + attempts++; + } while (selectedPositions.has(`${pos.dx}|${pos.dy}|${pos.dz}`) && attempts < 50); + + if (pos) { + selectedPositions.add(`${pos.dx}|${pos.dy}|${pos.dz}`); + positionsToReplace.push(pos); + } + } + + // 替换选中的位置为红色装饰块 + positionsToReplace.forEach(pos => { + for (let i = 0; i < blocks.length; i++) { + if (blocks[i].dx === pos.dx && blocks[i].dy === pos.dy && blocks[i].dz === pos.dz && blocks[i].type === 'cactus') { + blocks[i] = { dx: pos.dx, dy: pos.dy, dz: pos.dz, type: 'flower', color: rgbToHex(255, 0, 0) }; + break; + } + } + }); + + return blocks; +}; + +const TREE_VARIANTS: StructureTemplate[] = [ + // 1. 灌木 (Size 2x) + { + name: 'shrub', + blocks: [ + ...createTrunk(3), + ...createSphereCrown(3, 2) + ], + }, + // 2. 球形橡树 (Size 2x) + { + name: 'spherical_oak', + blocks: [ + ...createTrunk(8), + ...createSphereCrown(5.5, 6) + ], + }, + // 3. 矮针叶树 (Size 2x) - 单锥形,底部收窄,使用1x1细树干 + { + name: 'short_pine', + blocks: [ + ...createThinTrunk(4), + ...createConeCrown(2.5, 8, 3) // 底部半径从3改为2.5(收窄),顶部1格 + ], + }, + // 4. 高大针叶树 (Size 2x) - 圣诞树风格双层,协调美观 + { + name: 'tall_pine', + blocks: [ + ...createTrunk(10), // 树干10格 + ...createDoubleConeCrown(7, 16, 6) // 底部半径6,树冠16格高,从第6格开始 → 总高22 + ], + }, + // 5. 桦木 (Size 2x) + { + name: 'birch', + blocks: [ + ...createTrunk(12), + ...createRoundedCrown(5, 7, 9) + ], + }, + // 6. 阔叶大树 (Size 2x) + { + name: 'wide_canopy_short', + blocks: [ + ...createTrunk(5), + ...createSphereCrown(7, 4) + ], + }, + // 7. 针叶大树 (Size 2x) - 双层锥形树冠 + { + name: 'wide_conifer_short', + blocks: [ + ...createTrunk(6), + ...createDoubleConeCrown(5, 12, 3) + ], + }, + // 8. 深色草丛装饰 (Size 2x) + { + name: 'grass_tuft', + blocks: createGrassTuft(12), + }, + // 8.1. 高草丛 + { + name: 'tall_grass', + blocks: createTallGrass(8), + }, + // 8.2. 蕨类植物 + { + name: 'fern', + blocks: createFern(), + }, + // 8.3. 芦苇 + { + name: 'reed', + blocks: createReed(8), + }, + // 8.4-8.7. 新的花朵类型:严格对称 + // 小花:红色花瓣 + 黄色花蕊 + { + name: 'small_flower_red_yellow', + blocks: createSmallFlower(rgbToHex(255, 0, 0), rgbToHex(255, 220, 0)), + }, + // 小花:白色花瓣 + 黄色花蕊 + { + name: 'small_flower_white_yellow', + blocks: createSmallFlower(rgbToHex(255, 255, 255), rgbToHex(255, 220, 0)), + }, + // 小花:粉色花瓣 + 白色花蕊 + { + name: 'small_flower_pink_white', + blocks: createSmallFlower(rgbToHex(255, 192, 203), rgbToHex(255, 255, 255)), + }, + // 小花:紫色花瓣 + 黄色花蕊 + { + name: 'small_flower_purple_yellow', + blocks: createSmallFlower(rgbToHex(180, 100, 200), rgbToHex(255, 220, 0)), + }, + // 中花(带茎干):粉色花瓣 + 黄色花蕊 + { + name: 'medium_flower_pink', + blocks: createMediumFlower(rgbToHex(255, 192, 203), rgbToHex(255, 220, 0), true), + }, + // 中花(带茎干):红色花瓣 + 黄色花蕊 + { + name: 'medium_flower_red', + blocks: createMediumFlower(rgbToHex(255, 0, 0), rgbToHex(255, 220, 0), true), + }, + // 中花(带茎干):蓝色花瓣 + 白色花蕊 + { + name: 'medium_flower_blue', + blocks: createMediumFlower(rgbToHex(100, 150, 255), rgbToHex(255, 255, 255), true), + }, + // 中花(带茎干):橙色花瓣 + 黄色花蕊 + { + name: 'medium_flower_orange', + blocks: createMediumFlower(rgbToHex(255, 165, 0), rgbToHex(255, 220, 0), true), + }, + // 小花丛:红+白组合 + { + name: 'flower_cluster_red_white', + blocks: createSmallFlowerCluster( + rgbToHex(255, 0, 0), rgbToHex(255, 220, 0), + rgbToHex(255, 255, 255), rgbToHex(255, 220, 0) + ), + }, + // 小花丛:粉+紫组合 + { + name: 'flower_cluster_pink_purple', + blocks: createSmallFlowerCluster( + rgbToHex(255, 192, 203), rgbToHex(255, 220, 0), + rgbToHex(180, 100, 200), rgbToHex(255, 255, 255) + ), + }, + // 大花:蓝色外层 + 白色内层 + 红色花蕊 + { + name: 'large_flower_blue_white_red', + blocks: createLargeFlower(rgbToHex(255, 255, 255), rgbToHex(100, 150, 255), rgbToHex(255, 0, 0)), + }, + // 大花:粉色外层 + 白色内层 + 黄色花蕊 + { + name: 'large_flower_pink_white_yellow', + blocks: createLargeFlower(rgbToHex(255, 255, 255), rgbToHex(255, 192, 203), rgbToHex(255, 220, 0)), + }, + // 9. 小型仙人掌 + { + name: 'small_cactus', + blocks: createSmallCactus(), + }, + // 10. 中型仙人掌 + { + name: 'medium_cactus', + blocks: createMediumCactus(), + }, + // 11. 大型仙人掌 + { + name: 'large_cactus', + blocks: createLargeCactus(), + }, + // 12. L形分枝仙人掌(1个分支) + { + name: 'l_branch_cactus_1', + blocks: createLBranchCactus(1), + }, + // 13. L形分枝仙人掌(2个分支) + { + name: 'l_branch_cactus_2', + blocks: createLBranchCactus(2), + }, + // 14. L形分枝仙人掌(3个分支) + { + name: 'l_branch_cactus_3', + blocks: createLBranchCactus(3), + }, + // 15. 双主干仙人掌 + { + name: 'dual_trunk_cactus', + blocks: createDualTrunkCactus(), + }, + // 16. 小型仙人球 + { + name: 'small_barrel_cactus', + blocks: createBarrelCactus('small'), + }, + // 17. 中型仙人球 + { + name: 'medium_barrel_cactus', + blocks: createBarrelCactus('medium'), + }, + // 18. 大型仙人球 + { + name: 'large_barrel_cactus', + blocks: createBarrelCactus('large'), + }, + // 19. 不规则矮胖仙人球 + { + name: 'irregular_barrel_cactus', + blocks: createIrregularBarrelCactus(), + }, + // 20. 椰树:双细树干,顶部有扇形叶片 + { + name: 'coconut_palm', + blocks: [ + ...createDoubleThinTrunk(14), + ...createCoconutPalmCrown(12) + ], + }, + // 21. 苹果树:球形树冠,红色果实 + { + name: 'apple_tree', + blocks: [ + ...createTrunk(6), + ...createFruitSphereCrown(5, 5, 'red') + ], + }, + // 22. 橙树:圆润树冠,橙色果实 + { + name: 'orange_tree', + blocks: [ + ...createTrunk(7), + ...createFruitRoundedCrown(4.5, 8, 6, 'orange') + ], + }, + // 23. 柠檬树:球形树冠,黄色果实 + { + name: 'lemon_tree', + blocks: [ + ...createTrunk(5), + ...createFruitSphereCrown(4.5, 4, 'yellow') + ], + }, + // 24. 樱桃树:圆润树冠,红色果实 + { + name: 'cherry_tree', + blocks: [ + ...createTrunk(8), + ...createFruitRoundedCrown(5.5, 7, 7, 'red') + ], + }, + // 25. 桃树:球形树冠,橙色果实 + { + name: 'peach_tree', + blocks: [ + ...createTrunk(6), + ...createFruitSphereCrown(5, 5, 'orange') + ], + }, + // 26. 松果树(矮):单锥形,褐色松果,1x1细树干 + { + name: 'pine_cone_tree_short', + blocks: [ + ...createThinTrunk(6), + ...createFruitConeCrown(3, 10, 5) + ], + }, + // 27. 松果树(高):双层锥形,褐色松果 + { + name: 'pine_cone_tree_tall', + blocks: [ + ...createTrunk(12), + ...createFruitDoubleConeCrown(6, 18, 10) + ], + }, + // 28. 小球形树:紧凑的小球树冠 + { + name: 'small_spherical', + blocks: [ + ...createTrunk(6), + ...createSmallSphereCrown(3.5, 5) + ], + }, + // 27. 分散的松树:稀疏的针叶,可以看到树干 + { + name: 'sparse_pine', + blocks: [ + ...createTrunk(12), + ...createSparsePineCrown(5, 10, 10, 0.35) + ], + }, + // 28. 伞形树:顶部大而扁平 + { + name: 'umbrella_tree', + blocks: [ + ...createTrunk(8), + ...createUmbrellaCrown(6, 5, 6) + ], + }, + // 29. 圆柱形树:垂直的圆柱体树冠 + { + name: 'cylindrical_tree', + blocks: [ + ...createTrunk(10), + ...createCylindricalCrown(4, 8, 8) + ], + }, + // 30. 多层球形树:多个球形堆叠 + { + name: 'multi_sphere_tree', + blocks: [ + ...createTrunk(9), + ...createMultiSphereCrown(4.5, 3, 7) + ], + }, + // 31. 不规则树:随机形状,更自然 + { + name: 'irregular_tree', + blocks: [ + ...createTrunk(7), + ...createIrregularCrown(5, 9, 5) + ], + }, +]; + +// 专门用于添加树木体素的函数,使用16x16高分辨率网格 +// type AddTreeVoxelFn = ... (Removed duplicate definition) + +const placeStructure = ( + template: StructureTemplate, + addTreeVoxel: AddTreeVoxelFn, + baseX: number, + baseY: number, + baseZ: number +) => { + // baseX/baseZ 是地形的微体素坐标 (8x8 grid) + // 转换为高分辨率坐标 (16x16 grid) + // 一个地形微体素 = 2x2 高分辨率微体素 + // 我们将树木生成的原点对齐到这个地形微体素的左下角(0,0) + // 模板内的偏移量 (dx, dy, dz) 现在直接代表高分辨率网格的单位 + const fineBaseX = baseX * 2; + const fineBaseY = baseY * 2; + const fineBaseZ = baseZ * 2; + + template.blocks.forEach(({ dx, dy, dz, type, color: customColor }) => { + const fineX = fineBaseX + dx; + const fineY = fineBaseY + dy; + const fineZ = fineBaseZ + dz; + + // 如果提供了自定义颜色(如花朵),使用自定义颜色;否则使用默认颜色变化 + const color = customColor || '#00FF00'; // 默认绿色,实际颜色会在addTreeVoxel中通过varyColor处理 + + addTreeVoxel(fineX, fineY, fineZ, type, color); + }); +}; + +// ============= 植物分类数组 ============= + +// 阔叶树:包括各种形状的阔叶树和果树 +const LUSH_TREES = TREE_VARIANTS.filter((t) => [ + 'spherical_oak', + 'birch', + 'wide_canopy_short', + 'small_spherical', + 'umbrella_tree', + 'cylindrical_tree', + 'multi_sphere_tree', + 'irregular_tree', + 'coconut_palm', // 椰树 + 'apple_tree', // 苹果树(红色果实) + 'orange_tree', // 橙树(橙色果实) + 'lemon_tree', // 柠檬树(黄色果实) + 'cherry_tree', // 樱桃树(红色果实) + 'peach_tree' // 桃树(橙色果实) +].includes(t.name)); + +// 针叶树:包括各种松树和针叶树及松果树 +const CONIFER_TREES = TREE_VARIANTS.filter((t) => [ + 'short_pine', + 'tall_pine', + 'wide_conifer_short', + 'sparse_pine', // 分散的松树 + 'pine_cone_tree_short', // 矮松果树(1x1树干) + 'pine_cone_tree_tall' // 高松果树 +].includes(t.name)); + +// 草类植物:包括草丛、高草丛、蕨类 +const GRASS_DECO = TREE_VARIANTS.filter((t) => [ + 'grass_tuft', + 'tall_grass', + 'fern' +].includes(t.name)); + +// 花朵植物:包括所有新的严格对称花朵类型 +const FLOWER_STRUCTS = TREE_VARIANTS.filter((t) => [ + 'small_flower_red_yellow', + 'small_flower_white_yellow', + 'small_flower_pink_white', + 'small_flower_purple_yellow', + 'medium_flower_pink', + 'medium_flower_red', + 'medium_flower_blue', + 'medium_flower_orange', + 'flower_cluster_red_white', + 'flower_cluster_pink_purple', + 'large_flower_blue_white_red', + 'large_flower_pink_white_yellow' +].includes(t.name)); + +// 芦苇植物 +const REED_STRUCTS = TREE_VARIANTS.filter((t) => t.name === 'reed'); + +// 仙人掌:包括所有仙人掌变体和带花的仙人掌(移除掌形和球形仙人掌) +const CACTUS_STRUCTS = TREE_VARIANTS.filter((t) => [ + // 核心柱状仙人掌(3种尺寸,带L形分支) + 'small_cactus', // 小型仙人掌 + 'medium_cactus', // 中型仙人掌 + 'large_cactus', // 大型仙人掌 + + // L形分枝仙人掌(经典造型) + 'l_branch_cactus_1', // 1个L形分支 + 'l_branch_cactus_2', // 2个L形分支 + 'l_branch_cactus_3', // 3个L形分支 + + // 双主干(变化造型) + 'dual_trunk_cactus', // 两根不同高度的主干 + + // 仙人球系列(3种尺寸) + 'small_barrel_cactus', // 小型仙人球(带白点或小花) + 'medium_barrel_cactus', // 中型仙人球 + 'large_barrel_cactus', // 大型仙人球 + + // 特殊造型 + 'irregular_barrel_cactus' // 不规则矮胖仙人球(红色装饰) +].includes(t.name)); + + +/** + * 在戈壁地形上生成植被 + * 根据与水源的距离决定植被分布 + * + * Zone 1: 河岸区域 (Riparian Zone) - 靠近河流 (d < 3) + * - 密度:较高 + * - 类型:草丛 (GrassClump) 和 芦苇 (Reed) + * + * Zone 2: 干旱戈壁 (Arid Gobi) - 远离水源 + * - 密度:稀疏 + * - 类型:仙人掌 (Cactus) 和 仙人球 (BarrelCactus) + */ +const placeGobiVegetation = ( + addTreeVoxel: AddTreeVoxelFn, + ix: number, + iy: number, + iz: number, + streamDepthMap: Map | undefined, + densityRoll: number +) => { + // 1. 检测是否靠近河流 (Zone 1) + let isNearRiver = false; + + if (streamDepthMap) { + // 扩大检测范围,确保河岸带更宽,形成明显的绿带 + const checkRange = 7; // 约1个Tile的宽度 + // 检查中心及周边 + for (let dx = -checkRange; dx <= checkRange; dx++) { + for (let dz = -checkRange; dz <= checkRange; dz++) { + // 使用欧几里得距离判断,使分布更自然 (圆形而不是方形) + if (dx*dx + dz*dz <= checkRange*checkRange) { + if (streamDepthMap.has(`${ix + dx}|${iz + dz}`)) { + isNearRiver = true; + break; + } + } + } + if (isNearRiver) break; + } + } + + // 使用坐标生成确定的随机数,用于选择植被类型 + const typeSeed = ix * 12.9898 + iz * 78.233 + iy * 37.719; + const typeRoll = pseudoRandom(typeSeed); + + if (isNearRiver) { + // Zone 1: 河岸区域 (Riparian) + // 密度:高 (30% 概率) - 略微降低密度避免过于拥挤,但保持高密度特征 + if (densityRoll > 0.70) { + // 生成类型:草丛 (60%) 或 芦苇 (40%) + if (typeRoll < 0.6) { + placeStructure(pickRandom(GRASS_DECO), addTreeVoxel, ix, iy, iz); + } else { + placeStructure(pickRandom(REED_STRUCTS), addTreeVoxel, ix, iy, iz); + } + } + } else { + // Zone 2: 干旱戈壁 (Arid) + // 密度:低 (3.5% 概率) - 大幅降低密度以突显荒漠感,与河岸形成鲜明对比 + if (densityRoll > 0.965) { + // 生成类型:主要是仙人掌和仙人球 + placeStructure(pickRandom(CACTUS_STRUCTS), addTreeVoxel, ix, iy, iz); + } + } +}; + +// ============= 导出所有植物相关内容 ============= + +export { + // 树木创建函数 + createTrunk, + createSphereCrown, + createConeCrown, + createDoubleConeCrown, + createRoundedCrown, + createFruitSphereCrown, + createFruitRoundedCrown, + createFruitConeCrown, + createFruitDoubleConeCrown, + createThinTrunk, + createDoubleThinTrunk, + createCoconutPalmCrown, + createSmallSphereCrown, + createSparsePineCrown, + createUmbrellaCrown, + createCylindricalCrown, + createMultiSphereCrown, + createIrregularCrown, + + // 草类函数 + createGrassTuft, + createTallGrass, + createFern, + createReed, + + // 花朵函数 + createSmallFlower, + createMediumFlower, + createSmallFlowerCluster, + createLargeFlower, + + // 仙人掌函数 + addCylindricalSegment, + addBranch, + addLBranch, + addTiltedCylindricalSegment, + addIrregularCylindricalSegment, + addSphericalSegment, + createSmallCactus, + createMediumCactus, + createLargeCactus, + createLBranchCactus, + createDualTrunkCactus, + createBarrelCactus, + createIrregularBarrelCactus, + + // 模板和分类 + TREE_VARIANTS, + LUSH_TREES, + CONIFER_TREES, + GRASS_DECO, + FLOWER_STRUCTS, + REED_STRUCTS, + CACTUS_STRUCTS, + + // 放置函数 + placeStructure, + placeGobiVegetation, +}; + diff --git a/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts b/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts new file mode 100644 index 0000000..612fde9 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/voxelStyles.ts @@ -0,0 +1,1100 @@ +/** + * 方块样式系统:定义所有方块类型的颜色、色板和噪声变化 + * - 基础方块调色板 (PALETTE) + * - 各种方块的色板变体 (SWATCHES) + * - 颜色变化和噪声函数 + */ + +// ============= 类型定义 ============= + +export type VoxelType = + | 'water' + | 'sand' + | 'grass' + | 'stone' + | 'snow' + | 'dirt' + | 'wood' + | 'leaves' + | 'dark_grass' + | 'deep_dirt' + | 'dark_stone' + | 'cactus' + | 'flower' + | 'reed' + | 'bedrock' + | 'grass_dirt_blend' + | 'medium_dirt' + | 'dark_grass_ground' + | 'lava' + | 'volcanic_rock' + | 'obsidian' + | 'ash' + | 'magma_stone' + | 'mud' + | 'murky_water' + | 'swamp_grass' + | 'moss' + | 'lily_pad' + | 'ice' + | 'packed_ice' + | 'frozen_stone' + | 'icicle' + | 'permafrost' + | 'gobi_base' + | 'gobi_lower' + | 'gobi_upper' + | 'gobi_top' + | 'gobi_peak' + | 'etched_sand'; + +// ============= 辅助函数 ============= + +export const rgbToHex = (r: number, g: number, b: number, a?: number): string => { + const toHex = (c: number) => { + const hex = Math.floor(Math.max(0, Math.min(255, c))).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +}; + +export const lerp = (a: number, b: number, t: number) => a + (b - a) * t; +export const clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val)); + +export const pseudoRandom = (seed: number) => { + const s = Math.sin(seed) * 43758.5453123; + return s - Math.floor(s); +}; + +export const createDeterministicRandom = (seed: number) => { + let state = seed >>> 0; + return () => { + state = (state * 1664525 + 1013904223) >>> 0; + return state / 4294967296; + }; +}; + +/** + * 抑制十字形/规则形状的通用函数 + * 通过惩罚轴向(0, 90, 180, 270度)的距离值,使形状更圆润或更自然,避免出现人工十字 + * @param x 相对中心 X 坐标 + * @param z 相对中心 Z 坐标 + * @param dist 原始距离值 (通常 0-1 范围内) + * @param strength 抑制强度 (0-1),越高轴向收缩越明显,建议 0.2-0.5 + */ +export const suppressCrossShape = (x: number, z: number, dist: number, strength: number = 0.3): number => { + if (x === 0 && z === 0) return dist; + const angle = Math.atan2(z, x); + // cos(4 * angle) 在 0, 90, 180, 270 度时为 1 (轴向) + // 在 45, 135... 度时为 -1 (对角线) + // 我们希望轴向的距离变大 (更容易被裁剪),对角线距离不变或变小 (更容易保留) + // normalize to [0, 1]: (cos(4*angle) + 1) / 2 -> 1 at axis, 0 at diagonal + const axisBias = (Math.cos(4 * angle) + 1) * 0.5; + + return dist * (1 + axisBias * strength); +}; + +// ============= 方块基础调色板 ============= + +export const PALETTE: Record = { + water: [79, 164, 244], // Updated to requested #4FA4F4 + sand: [240, 220, 150], + grass: [140, 215, 105], // 恢复原来的亮绿色草地 + dark_grass: [84, 141, 81], // 恢复原来的深绿草地 + stone: [168, 175, 189], + dark_stone: [150, 157, 171], // 再次提亮,大幅减少对比度 (原: 125, 132, 153) + snow: [255, 255, 255], + wood: [120, 80, 50], // 真实的树皮褐色 (原: 70, 45, 25 太深) + leaves: [55, 95, 55], // 稍亮的墨绿树叶(原: 45, 80, 45) + bedrock: [74, 69, 83], + grass_dirt_blend: [182, 128, 86], + cactus: [75, 135, 75], // 提亮的仙人掌绿 (原: 45, 80, 45 太深) + dirt: [182, 128, 86], + medium_dirt: [160, 100, 65], + deep_dirt: [145, 85, 55], + dark_grass_ground: [160, 210, 155], // 恢复原来的亮绿色草地面 + flower: [255, 255, 255], + reed: [220, 180, 80], + // 火山场景 + lava: [255, 100, 0], + volcanic_rock: [65, 55, 58], + obsidian: [25, 18, 30], + ash: [140, 140, 145], + magma_stone: [180, 60, 40], + // 沼泽场景 + mud: [85, 70, 50], + murky_water: [50, 80, 65], + swamp_grass: [95, 125, 75], // 恢复原来的沼泽草颜色 + moss: [70, 110, 60], // 恢复原来的苔藓颜色 + lily_pad: [85, 150, 95], // 恢复原来的荷叶颜色 + // 冰原场景 + ice: [200, 235, 255], + packed_ice: [165, 210, 240], + frozen_stone: [145, 165, 185], + icicle: [230, 250, 255], + permafrost: [120, 140, 155], + // 戈壁与沙漠 + gobi_base: [210, 165, 130], + gobi_lower: [195, 135, 100], + gobi_upper: [180, 110, 75], + gobi_top: [165, 85, 55], + gobi_peak: [150, 70, 45], + etched_sand: [236, 212, 170], +}; + +const GRASS_SWATCHES = [ + { r: -12, g: -10, b: -10 }, // 深绿块(减少R和B更多,G减少较少,保持绿色调) + { r: -8, g: -7, b: -7 }, // 中深绿(保持绿色调) + { r: -5, g: -4, b: -5 }, // 略深(保持绿色调) + { r: -2, g: -1, b: -2 }, // 稍深(保持绿色调) + { r: 0, g: 0, b: 0 }, // 基础(高饱和度黄绿色) + { r: 4, g: 9, b: 0 }, // 略亮(高饱和度,更亮) + { r: 5, g: 11, b: 1 }, // 中亮(高饱和度,更亮) + { r: 6, g: 13, b: 1 }, // 亮绿(高饱和度,更亮) + { r: 7, g: 15, b: 2 }, // 很亮(高饱和度黄绿色高光,更亮) +]; + +const DARK_GRASS_SWATCHES = [ + { r: -5, g: -3, b: -3 }, // 深绿块 + { r: -3, g: -2, b: -2 }, // 中深绿 + { r: -2, g: -1, b: -1 }, // 略深 + { r: -1, g: 0, b: -1 }, // 基础 + { r: 0, g: 1, b: -1 }, // 标准 + { r: 1, g: 2, b: 0 }, // 略亮 + { r: 2, g: 3, b: 0 }, // 中亮 + { r: 3, g: 4, b: 0 }, // 亮绿 +]; + +const SAND_SWATCHES = [ + { r: -10, g: -12, b: -10 }, // 深沙块(减少G更多,保持黄色调) + { r: -6, g: -8, b: -6 }, // 中深沙(保持黄色调) + { r: -4, g: -6, b: -4 }, // 略深(保持黄色调) + { r: -2, g: -3, b: -2 }, // 稍深(保持黄色调) + { r: 0, g: 0, b: 0 }, // 基础(黄色) + { r: 4, g: 2, b: 1 }, // 略亮(更黄,R增加更多) + { r: 6, g: 3, b: 2 }, // 中亮(更黄) + { r: 8, g: 4, b: 2 }, // 亮沙(更黄) + { r: 10, g: 5, b: 3 }, // 很亮(黄色高光) +]; + +const STONE_SWATCHES = [ + { r: -12, g: -12, b: -10 }, // 深灰块(保持灰蓝色调) + { r: -8, g: -8, b: -7 }, // 中深灰(保持灰蓝色调) + { r: -5, g: -5, b: -4 }, // 略深(保持灰蓝色调) + { r: -2, g: -2, b: -2 }, // 稍深(保持灰蓝色调) + { r: 0, g: 0, b: 0 }, // 基础(灰蓝色) + { r: 3, g: 3, b: 4 }, // 略亮(保持灰蓝色调,B略多) + { r: 5, g: 5, b: 6 }, // 中亮(保持灰蓝色调) + { r: 7, g: 7, b: 8 }, // 亮灰(保持灰蓝色调) + { r: 9, g: 9, b: 10 }, // 很亮(灰蓝色高光) +]; + +const CACTUS_SWATCHES = [ + { r: -10, g: -8, b: -6 }, // 深绿块(保持绿色调) + { r: -7, g: -5, b: -4 }, // 中深绿 + { r: -4, g: -3, b: -2 }, // 略深 + { r: -2, g: -1, b: -1 }, // 稍深 + { r: 0, g: 0, b: 0 }, // 基础(仙人掌绿) + { r: 3, g: 4, b: 2 }, // 略亮(更绿) + { r: 5, g: 7, b: 3 }, // 中亮(鲜绿) + { r: 7, g: 10, b: 4 }, // 亮绿(高光绿) + { r: 9, g: 13, b: 5 }, // 很亮(亮绿高光) +]; + + +const LEAF_SWATCHES = [ + { r: -8, g: -6, b: -5 }, + { r: -5, g: -3, b: -2 }, + { r: -2, g: 2, b: 0 }, + { r: 1, g: 5, b: 1 }, + { r: 4, g: 8, b: 2 }, + { r: 7, g: 10, b: 3 }, +]; + +// 花朵颜色变体:白色、黄色、粉色等 +const FLOWER_SWATCHES = [ + // 白色雏菊 + { r: 255, g: 255, b: 255 }, // 纯白花瓣 + { r: 250, g: 250, b: 250 }, // 略暗白 + { r: 245, g: 245, b: 245 }, // 更暗白 + // 黄色花心 + { r: 255, g: 220, b: 0 }, // 亮黄 + { r: 255, g: 200, b: 0 }, // 中黄 + { r: 240, g: 180, b: 0 }, // 深黄 + // 粉色花朵 + { r: 255, g: 192, b: 203 }, // 粉红 + { r: 255, g: 182, b: 193 }, // 浅粉 + { r: 255, g: 160, b: 180 }, // 深粉 + // 绿色茎(恢复原来的亮绿色) + { r: 100, g: 200, b: 100 }, // 亮绿茎 + { r: 80, g: 180, b: 80 }, // 中绿茎 + { r: 60, g: 160, b: 60 }, // 深绿茎 +]; + +// 芦苇颜色变体:金黄色系 +const REED_SWATCHES = [ + { r: -15, g: -12, b: -8 }, // 深金黄 + { r: -10, g: -8, b: -5 }, // 中深金黄 + { r: -5, g: -4, b: -2 }, // 略深金黄 + { r: 0, g: 0, b: 0 }, // 基础金黄 + { r: 5, g: 4, b: 2 }, // 略亮金黄 + { r: 10, g: 8, b: 4 }, // 中亮金黄 + { r: 15, g: 12, b: 6 }, // 亮金黄 + { r: 20, g: 16, b: 8 }, // 很亮金黄 +]; + +// 火山岩浆颜色变体:从暗红到亮橙 +const LAVA_SWATCHES = [ + { r: -50, g: -30, b: 0 }, // 暗红岩浆 + { r: -30, g: -20, b: 0 }, // 深红岩浆 + { r: -10, g: -10, b: 0 }, // 中红岩浆 + { r: 0, g: 0, b: 0 }, // 基础橙红 + { r: 20, g: 30, b: 10 }, // 亮橙岩浆 + { r: 40, g: 50, b: 20 }, // 明亮橙黄 + { r: 60, g: 80, b: 40 }, // 炽热黄色 + { r: 80, g: 100, b: 60 }, // 极亮白黄 +]; + +// 火山岩颜色变体:深灰黑色系 +const VOLCANIC_ROCK_SWATCHES = [ + { r: -10, g: -8, b: -10 }, // 深黑 + { r: -5, g: -4, b: -5 }, // 中深黑 + { r: -2, g: -2, b: -2 }, // 略深 + { r: 0, g: 0, b: 0 }, // 基础灰黑 + { r: 3, g: 3, b: 3 }, // 略亮 + { r: 5, g: 5, b: 5 }, // 中亮灰 + { r: 8, g: 8, b: 8 }, // 亮灰 +]; + +// 火山灰颜色变体:灰白色系 +const ASH_SWATCHES = [ + { r: -15, g: -15, b: -15 }, // 深灰 + { r: -10, g: -10, b: -10 }, // 中深灰 + { r: -5, g: -5, b: -5 }, // 略深灰 + { r: 0, g: 0, b: 0 }, // 基础灰白 + { r: 5, g: 5, b: 5 }, // 略亮 + { r: 10, g: 10, b: 10 }, // 亮白灰 + { r: 15, g: 15, b: 15 }, // 很亮白 +]; + +// 沼泽泥土颜色变体:深棕色系 +const MUD_SWATCHES = [ + { r: -15, g: -12, b: -10 }, // 深泥棕 + { r: -10, g: -8, b: -6 }, // 中深棕 + { r: -5, g: -4, b: -3 }, // 略深棕 + { r: 0, g: 0, b: 0 }, // 基础泥棕 + { r: 5, g: 4, b: 3 }, // 略亮棕 + { r: 8, g: 6, b: 4 }, // 中亮棕 + { r: 12, g: 9, b: 6 }, // 亮棕 +]; + +// 沼泽草颜色变体:暗绿色系 +const SWAMP_GRASS_SWATCHES = [ + { r: -10, g: -8, b: -6 }, // 深暗绿 + { r: -6, g: -5, b: -4 }, // 中深绿 + { r: -3, g: -2, b: -2 }, // 略深绿 + { r: 0, g: 0, b: 0 }, // 基础暗绿 + { r: 3, g: 5, b: 2 }, // 略亮绿 + { r: 5, g: 8, b: 4 }, // 中亮绿 + { r: 8, g: 12, b: 6 }, // 亮绿 +]; + +// 苔藓颜色变体:深绿色系 +const MOSS_SWATCHES = [ + { r: -8, g: -10, b: -6 }, // 深苔绿 + { r: -5, g: -6, b: -4 }, // 中深绿 + { r: -2, g: -3, b: -2 }, // 略深绿 + { r: 0, g: 0, b: 0 }, // 基础苔绿 + { r: 3, g: 4, b: 2 }, // 略亮 + { r: 5, g: 7, b: 4 }, // 中亮绿 + { r: 8, g: 10, b: 6 }, // 亮苔绿 +]; + +// 冰块颜色变体:浅蓝色系 +const ICE_SWATCHES = [ + { r: -20, g: -15, b: -10 }, // 深蓝冰 + { r: -15, g: -10, b: -5 }, // 中深蓝 + { r: -10, g: -5, b: -3 }, // 略深蓝 + { r: 0, g: 0, b: 0 }, // 基础浅蓝冰 + { r: 10, g: 5, b: 0 }, // 略亮 + { r: 15, g: 8, b: 0 }, // 中亮冰蓝 + { r: 20, g: 10, b: 0 }, // 亮冰蓝 + { r: 25, g: 12, b: 0 }, // 极亮冰蓝 +]; + +// 压缩冰颜色变体:深蓝色系 +const PACKED_ICE_SWATCHES = [ + { r: -15, g: -12, b: -10 }, // 深压缩冰 + { r: -10, g: -8, b: -6 }, // 中深蓝 + { r: -5, g: -4, b: -3 }, // 略深蓝 + { r: 0, g: 0, b: 0 }, // 基础深蓝 + { r: 5, g: 4, b: 3 }, // 略亮 + { r: 8, g: 6, b: 5 }, // 中亮蓝 + { r: 12, g: 9, b: 7 }, // 亮蓝 +]; + +// 永冻土颜色变体:灰蓝色系 +const PERMAFROST_SWATCHES = [ + { r: -15, g: -12, b: -10 }, // 深灰蓝 + { r: -10, g: -8, b: -6 }, // 中深灰蓝 + { r: -5, g: -4, b: -3 }, // 略深 + { r: 0, g: 0, b: 0 }, // 基础灰蓝 + { r: 5, g: 4, b: 3 }, // 略亮 + { r: 8, g: 7, b: 5 }, // 中亮 + { r: 12, g: 10, b: 8 }, // 亮灰蓝 +]; + +const varyColor = (type: VoxelType, noiseVal: number, depth: number = 0): string => { + const [r, g, b] = PALETTE[type]; + + let rOut = r; + let gOut = g; + let bOut = b; + + // 默认 Alpha 为 1.0 (不透明) + let alpha: number | undefined = undefined; + + // 特殊处理水体:根据深度变化颜色(平滑渐变) + if (type === 'water') { + // 注意:临时禁用 Alpha 通道,因为 8 位 HEX 颜色代码可能导致渲染器不显示方块 + // alpha = 0.6; + + const shallowBase = { r: 100, g: 200, b: 255 }; // 变浅一点以模拟清澈 + const midBase = { r: 79, g: 164, b: 244 }; + const deepBase = { r: 20, g: 100, b: 255 }; + + const baseRatio = clamp(depth / 6, 0, 1); + const noisyRatio = clamp(baseRatio * 0.45 + 0.2 + (noiseVal - 0.5) * 0.08, 0, 1); + + if (noisyRatio < 0.4) { + const t = noisyRatio / 0.4; + rOut = lerp(shallowBase.r, midBase.r, t); + gOut = lerp(shallowBase.g, midBase.g, t); + bOut = lerp(shallowBase.b, midBase.b, t); + } else { + const t = (noisyRatio - 0.4) / 0.6; + rOut = lerp(midBase.r, deepBase.r, t); + gOut = lerp(midBase.g, deepBase.g, t); + bOut = lerp(midBase.b, deepBase.b, t); + } + + const highlight = Math.max(0, 0.6 - Math.abs(noiseVal - 0.5)) * 10; + rOut += highlight * (1 - noisyRatio * 0.55); + gOut += highlight * (1 - noisyRatio * 0.35); + bOut += highlight * 0.2; + } else if (type === 'leaves') { + // 树叶:多层深浅绿 + const palettePos = pseudoRandom(noiseVal * 12.17 + depth * 0.13) * (LEAF_SWATCHES.length - 1); + const lowIndex = Math.floor(palettePos); + const highIndex = Math.min(LEAF_SWATCHES.length - 1, lowIndex + 1); + const lerpT = palettePos - lowIndex; + const baseVariant = { + r: lerp(LEAF_SWATCHES[lowIndex].r, LEAF_SWATCHES[highIndex].r, lerpT), + g: lerp(LEAF_SWATCHES[lowIndex].g, LEAF_SWATCHES[highIndex].g, lerpT), + b: lerp(LEAF_SWATCHES[lowIndex].b, LEAF_SWATCHES[highIndex].b, lerpT), + }; + rOut += baseVariant.r; + gOut += baseVariant.g; + bOut += baseVariant.b; + + const microNoise = pseudoRandom(noiseVal * 58.23 + depth * 0.23); + if (microNoise > 0.92) { + const highlight = 3 + Math.random() * 3; + rOut += highlight * 0.2; + gOut += highlight * 0.6; + bOut += highlight * 0.2; + } else if (microNoise < 0.12) { + const shadow = 6 + Math.random() * 4; + rOut -= shadow * 0.5; + gOut -= shadow * 0.9; + bOut -= shadow * 0.5; + } + } else if (type === 'dark_grass') { + const gentleShift = (noiseVal - 0.5) * 6; + rOut += gentleShift * 0.3; + gOut += gentleShift * 0.8; + bOut += gentleShift * 0.3; + + if (noiseVal > 0.93) { + const highlight = 2 + Math.random() * 2; + rOut += highlight * 0.2; + gOut += highlight * 0.5; + bOut += highlight * 0.2; + } else if (noiseVal < 0.08) { + const shadow = 4 + Math.random() * 4; + rOut -= shadow * 0.4; + gOut -= shadow * 0.8; + bOut -= shadow * 0.4; + } + } else if (type === 'cactus') { + // 仙人掌:Minecraft 风格,丰富的颜色变化 + const normalizedNoise = (noiseVal + 1) * 0.5; + const variantPalette = CACTUS_SWATCHES; + + // 使用多个随机种子创建更丰富的色块变化 + const variantSeed1 = pseudoRandom(normalizedNoise * 12.9898 + 3); + const variantSeed2 = pseudoRandom(normalizedNoise * 37.456 + 0.77); + const combinedSeed = (variantSeed1 + variantSeed2 * 0.5) / 1.5; + + const variantPos = combinedSeed * (variantPalette.length - 1); + const lowerIndex = Math.floor(variantPos); + const upperIndex = Math.min(variantPalette.length - 1, lowerIndex + 1); + const lerpT = variantPos - lowerIndex; + + const lower = variantPalette[lowerIndex]; + const upper = variantPalette[upperIndex]; + const variant = { + r: lerp(lower.r, upper.r, lerpT), + g: lerp(lower.g, upper.g, lerpT), + b: lerp(lower.b, upper.b, lerpT), + }; + + rOut += variant.r; + gOut += variant.g; + bOut += variant.b; + + // 像素化噪声:创建Minecraft风格的明暗变化 + const pixelNoise = pseudoRandom(normalizedNoise * 89.123 + 0.67); + if (pixelNoise > 0.88) { + // 亮块:鲜绿色高光 + const bright = 3.5 + (pixelNoise - 0.88) * 4.5; + rOut += bright * 0.15; + gOut += bright * 0.5; // 绿色增加更多 + bOut += bright * 0.2; + } else if (pixelNoise < 0.15) { + // 暗块:深绿色阴影 + const dark = 6 + (0.15 - pixelNoise) * 9; + rOut -= dark * 0.3; + gOut -= dark * 0.7; // 绿色减少更多,保持绿色调 + bOut -= dark * 0.3; + } + + // 散布的小亮点:模拟仙人掌表面的高光 + const highlightNoise = pseudoRandom(normalizedNoise * 234.567 + 0.89); + if (highlightNoise > 0.94) { + const highlightBright = 5 + (highlightNoise - 0.94) * 7; + // 偏亮绿色:增加G更多 + rOut += highlightBright * 0.12; + gOut += highlightBright * 0.4; + bOut += highlightBright * 0.15; + } + + // 老化/干燥部分:偶尔出现黄色/棕色斑点(5%概率) + const agedNoise = pseudoRandom(normalizedNoise * 456.789 + 0.33); + if (agedNoise > 0.95) { + const agedAmount = 6 + (agedNoise - 0.95) * 8; + // 偏黄棕色:增加R,减少G和B + rOut += agedAmount * 0.4; + gOut += agedAmount * 0.1; + bOut -= agedAmount * 0.2; + } + + // 额外的细微变化:模拟仙人掌表面的随机性 + const microVariation = (pseudoRandom(normalizedNoise * 156.789 + 0.95) - 0.5) * 1.5; + if (microVariation < 0) { + // 负值(变暗):减少G更多,保持绿色调 + rOut += microVariation * 0.2; + gOut += microVariation * 0.4; + bOut += microVariation * 0.2; + } else { + // 正值(变亮):增加G更多 + rOut += microVariation * 0.15; + gOut += microVariation * 0.45; + bOut += microVariation * 0.2; + } + + // 防止仙人掌颜色过亮或过暗(放宽限制,允许更亮的绿色) + const maxCactusR = 120; // 上限提高 (原: 65) + const maxCactusG = 180; // 上限提高 (原: 110) + const maxCactusB = 120; // 上限提高 (原: 65) + const minCactusR = 40; // 下限 (原: 30) + const minCactusG = 80; // 下限 (原: 50) + const minCactusB = 40; // 下限 (原: 30) + rOut = Math.max(minCactusR, Math.min(rOut, maxCactusR)); + gOut = Math.max(minCactusG, Math.min(gOut, maxCactusG)); + bOut = Math.max(minCactusB, Math.min(bOut, maxCactusB)); + + // 保持绿色调:确保G值始终是最大的 + if (gOut < rOut) { + rOut = Math.min(rOut, gOut * 0.9); + } + if (gOut < bOut) { + bOut = Math.min(bOut, gOut * 0.8); + } + + // 确保G值不会太低(保持绿色调) + const baseG = PALETTE[type][1]; + const minG = baseG * 0.5; + if (gOut < minG) { + gOut = minG; + if (rOut > 0) rOut = Math.min(rOut, gOut * 0.7); + if (bOut > 0) bOut = Math.min(bOut, gOut * 0.6); + } + } else if (type === 'flower') { + // 花朵:多种颜色(白色、黄色、粉色等) + // 花朵颜色在生成时通过 metadata 传递,这里只做轻微变化 + const normalizedNoise = (noiseVal + 1) * 0.5; + const variation = (pseudoRandom(normalizedNoise * 123.456) - 0.5) * 8; + rOut = Math.max(0, Math.min(255, rOut + variation)); + gOut = Math.max(0, Math.min(255, gOut + variation)); + bOut = Math.max(0, Math.min(255, bOut + variation)); + } else if (type === 'reed') { + // 芦苇:金黄色系 + const normalizedNoise = (noiseVal + 1) * 0.5; + const variantPalette = REED_SWATCHES; + + const variantSeed1 = pseudoRandom(normalizedNoise * 12.9898 + 4); + const variantSeed2 = pseudoRandom(normalizedNoise * 37.456 + 0.88); + const combinedSeed = (variantSeed1 + variantSeed2 * 0.5) / 1.5; + + const variantPos = combinedSeed * (variantPalette.length - 1); + const lowerIndex = Math.floor(variantPos); + const upperIndex = Math.min(variantPalette.length - 1, lowerIndex + 1); + const lerpT = variantPos - lowerIndex; + + const lower = variantPalette[lowerIndex]; + const upper = variantPalette[upperIndex]; + const variant = { + r: lerp(lower.r, upper.r, lerpT), + g: lerp(lower.g, upper.g, lerpT), + b: lerp(lower.b, upper.b, lerpT), + }; + + rOut += variant.r; + gOut += variant.g; + bOut += variant.b; + + // 保持金黄色调:R > G > B + const maxReedR = 240; + const maxReedG = 200; + const maxReedB = 120; + const minReedR = 180; + const minReedG = 140; + const minReedB = 50; + rOut = Math.max(minReedR, Math.min(rOut, maxReedR)); + gOut = Math.max(minReedG, Math.min(gOut, maxReedG)); + bOut = Math.max(minReedB, Math.min(bOut, maxReedB)); + } else if (type === 'gobi_base' || type === 'gobi_lower' || type === 'gobi_upper' || type === 'gobi_top' || type === 'gobi_peak') { + // 戈壁分层:根据类型决定基础偏移(降低饱和度,颜色更柔和) + const baseShift = + type === 'gobi_base' ? { r: -3, g: -2, b: -1 } : // 底层最浅 + type === 'gobi_lower' ? { r: -1, g: -0.5, b: 0 } : // 中下层 + type === 'gobi_upper' ? { r: 1, g: 0.5, b: 0 } : // 中上层 + type === 'gobi_top' ? { r: 2, g: 1, b: 0.5 } : // 顶层 + { r: 3, g: 1.5, b: 1 }; // 峰顶最深 + rOut += baseShift.r; + gOut += baseShift.g; + bOut += baseShift.b; + + // 增强戈壁质感:使用离散的像素化噪声,模拟碎石和盐碱地效果 + const normalizedNoise = (noiseVal + 1) * 0.5; + const pixelNoise = pseudoRandom(normalizedNoise * 97.13 + depth * 0.19); + + // 1. 沉积纹理 (Sediment Stratification) + // 使用 depth 生成正弦波纹理,模拟水平地质层 + // 频率较低的波浪,幅度适中 + const stratNoise = Math.sin(depth * 0.8 + normalizedNoise * 2) * 0.5 + + Math.sin(depth * 3.5) * 0.2; + + // 根据层理调整颜色:深色条纹和浅色条纹 + if (stratNoise > 0.4) { + // 深色沉积带 (富含铁矿) + rOut -= 4; + gOut -= 6; + bOut -= 5; + } else if (stratNoise < -0.4) { + // 浅色沉积带 (钙质沉积) + rOut += 5; + gOut += 4; + bOut += 3; + } + + // 2. 风化微裂纹 (Weathering Cracks) + // 高频噪声,极低概率生成深色细线 + const crackNoise = pseudoRandom(normalizedNoise * 153.7 + depth * 12.3); + if (crackNoise > 0.96) { + const crackDepth = 15 + Math.random() * 10; + rOut -= crackDepth; + gOut -= crackDepth; + bOut -= crackDepth; + } + + // 3. 边缘混合 (Edge Blending for Base) + // 如果是底层 (gobi_base),混入一点沙地颜色,消除悬浮感 + if (type === 'gobi_base') { + // 沙地颜色偏黄 (r: 240, g: 220, b: 150) + // 戈壁基色 (r: 210, g: 165, b: 130) + // 混合一点黄色调 + rOut += 5; + gOut += 8; + bOut += 2; + } + + if (pixelNoise > 0.92) { + // 深色碎石/风化岩(高对比度):模拟散落在戈壁上的深色岩石碎片 + const stoneDark = 15 + Math.random() * 10; + rOut -= stoneDark; + gOut -= stoneDark * 0.9; // 红色减少多一点,偏冷暗 + bOut -= stoneDark * 0.8; + } else if (pixelNoise > 0.85) { + // 较浅的粗砾石:中等深度的斑点 + const gravelDark = 8 + Math.random() * 6; + rOut -= gravelDark; + gOut -= gravelDark * 0.9; + bOut -= gravelDark * 0.8; + } else if (pixelNoise < 0.12) { + // 盐碱/风化白斑(干燥感):模拟干旱地区的盐碱析出或强烈风化 + const saltLight = 8 + Math.random() * 8; + rOut += saltLight; + gOut += saltLight * 0.95; // 保持接近白色但微暖 + bOut += saltLight * 0.9; + } else if (pixelNoise < 0.25) { + // 干燥的尘土斑点:比周围略亮 + const dustLight = 4 + Math.random() * 5; + rOut += dustLight; + gOut += dustLight * 0.9; + bOut += dustLight * 0.7; // 蓝色增加少,保持土色 + } else { + // 基础粗糙度:细微的颗粒感,避免颜色太平滑 + const roughness = (pixelNoise - 0.5) * 8; + rOut += roughness; + gOut += roughness * 0.9; + bOut += roughness * 0.8; + } + } else if (type === 'etched_sand') { + // 被溪流蚀刻的沙面:略带凉色调 + const normalizedNoise = (noiseVal + 1) * 0.5; + const depthTint = clamp(depth * 0.2, 0, 4); + rOut -= depthTint; + gOut -= depthTint * 0.6; + bOut += depthTint * 0.4; + const ripple = (pseudoRandom(normalizedNoise * 91.3 + depth * 0.07) - 0.5) * 6; + rOut += ripple * 0.4; + gOut += ripple * 0.5; + bOut += ripple * 0.8; + } else if (type === 'sand') { + // 沙地:Minecraft 风格,明显的像素化斑驳纹理(类似草地) + const normalizedNoise = (noiseVal + 1) * 0.5; + const variantPalette = SAND_SWATCHES; + + // 使用多个随机种子创建更丰富的色块变化 + const variantSeed1 = pseudoRandom(normalizedNoise * 12.9898 + 2); + const variantSeed2 = pseudoRandom(normalizedNoise * 37.456 + 0.45); + const combinedSeed = (variantSeed1 + variantSeed2 * 0.5) / 1.5; + + const variantPos = combinedSeed * (variantPalette.length - 1); + const lowerIndex = Math.floor(variantPos); + const upperIndex = Math.min(variantPalette.length - 1, lowerIndex + 1); + const lerpT = variantPos - lowerIndex; + + const lower = variantPalette[lowerIndex]; + const upper = variantPalette[upperIndex]; + const variant = { + r: lerp(lower.r, upper.r, lerpT), + g: lerp(lower.g, upper.g, lerpT), + b: lerp(lower.b, upper.b, lerpT), + }; + + rOut += variant.r; + gOut += variant.g; + bOut += variant.b; + + // 像素化噪声:创建Minecraft风格的明暗变化 + const pixelNoise = pseudoRandom(normalizedNoise * 89.123 + 0.67); + if (pixelNoise > 0.88) { + // 亮块:更亮的黄色 + const bright = 3.2 + (pixelNoise - 0.88) * 4.2; + rOut += bright * 0.18; // 增加红色更多,更黄 + gOut += bright * 0.12; // 增加绿色较少 + bOut += bright * 0.05; // 略微增加蓝色 + } else if (pixelNoise < 0.15) { + // 暗块:深黄色(确保是深黄色而不是灰色) + const dark = 5 + (0.15 - pixelNoise) * 8; + rOut -= dark * 0.18; // 减少红色较少,保持黄色调 + gOut -= dark * 0.25; // 减少绿色更多,避免灰色 + bOut -= dark * 0.2; // 减少蓝色更多,保持黄色调 + } + + // 散布的小亮点:模拟参考图中的亮沙小块 + const tuftNoise = pseudoRandom(normalizedNoise * 234.567 + 0.89); + if (tuftNoise > 0.94) { + const tuftBright = 4.5 + (tuftNoise - 0.94) * 6; + // 偏黄色:R较高,G和B较低,保持黄色调 + rOut += tuftBright * 0.2; // 增加红色更多 + gOut += tuftBright * 0.12; // 增加绿色较少 + bOut += tuftBright * 0.08; // 略微增加蓝色 + } + + // 额外的细微变化:模拟沙粒的随机性 + const microVariation = (pseudoRandom(normalizedNoise * 156.789 + 0.93) - 0.5) * 1.2; + if (microVariation < 0) { + // 负值(变暗):减少G和B更多,保持黄色调 + rOut += microVariation * 0.1; // 减少红色较少,保持黄色调 + gOut += microVariation * 0.15; // 减少绿色更多 + bOut += microVariation * 0.12; // 减少蓝色更多 + } else { + // 正值(变亮):正常增加,更黄 + rOut += microVariation * 0.12; // 增加红色更多 + gOut += microVariation * 0.08; // 增加绿色较少 + bOut += microVariation * 0.04; // 略微变化蓝色 + } + + // 防止沙子颜色过亮:确保最终颜色不会发白 + const maxSandR = 250; // 提高红色上限 + const maxSandG = 245; // 提高绿色上限 + const maxSandB = 200; // 提高蓝色上限 + rOut = Math.min(rOut, maxSandR); + gOut = Math.min(gOut, maxSandG); + bOut = Math.min(bOut, maxSandB); + + // 关键保护:确保最终颜色保持黄色调,避免灰色 + // 确保R值略大于G值,保持黄色调(R/G比例约1.05-1.1) + if (rOut < gOut * 1.05) { + // 如果R小于G的1.05倍,调整R使其略大于G + rOut = gOut * 1.05; + } + // 如果R和G差距太大(超过20),调整使其接近但R略大 + if (rOut - gOut > 20) { + rOut = gOut + 15; + } + + // 确保R和G值不会太低(保持黄色调) + const baseR = PALETTE[type][0]; // 基色R值 + const baseG = PALETTE[type][1]; // 基色G值 + const minR = baseR * 0.5; // R值最小不低于基色的50% + const minG = baseG * 0.5; // G值最小不低于基色的50% + if (rOut < minR) { + rOut = minR; + } + if (gOut < minG) { + gOut = minG; + // 确保R略大于G + if (rOut < gOut * 1.05) { + rOut = gOut * 1.05; + } + } + } else if (type === 'grass' || type === 'dark_grass_ground') { + // 草地:Minecraft 风格,明显的像素化斑驳纹理 + const normalizedNoise = (noiseVal + 1) * 0.5; + const variantPalette = type === 'grass' ? GRASS_SWATCHES : DARK_GRASS_SWATCHES; + + // 使用多个随机种子创建更丰富的色块变化 + const variantSeed1 = pseudoRandom(normalizedNoise * 12.9898 + (type === 'grass' ? 0 : 1)); + const variantSeed2 = pseudoRandom(normalizedNoise * 37.456 + 0.23); + const combinedSeed = (variantSeed1 + variantSeed2 * 0.5) / 1.5; + + const variantPos = combinedSeed * (variantPalette.length - 1); + const lowerIndex = Math.floor(variantPos); + const upperIndex = Math.min(variantPalette.length - 1, lowerIndex + 1); + const lerpT = variantPos - lowerIndex; + + const lower = variantPalette[lowerIndex]; + const upper = variantPalette[upperIndex]; + const variant = { + r: lerp(lower.r, upper.r, lerpT), + g: lerp(lower.g, upper.g, lerpT), + b: lerp(lower.b, upper.b, lerpT), + }; + + rOut += variant.r; + gOut += variant.g; + bOut += variant.b; + + // 像素化噪声:创建Minecraft风格的明暗变化(参考图的斑驳效果) + const pixelNoise = pseudoRandom(normalizedNoise * 89.123 + 0.47); + if (pixelNoise > 0.88) { + // 亮块:高饱和度的黄绿色(更亮) + const bright = 3.2 + (pixelNoise - 0.88) * 4.2; + rOut += bright * 0.12; // 适度增加红色 + gOut += bright * 0.32; // 大幅增加绿色,更亮 + bOut += bright * 0.02; // 略微增加蓝色 + } else if (pixelNoise < 0.15) { + // 暗块:深绿色(确保是深绿色而不是灰色) + const dark = 5 + (0.15 - pixelNoise) * 8; + rOut -= dark * 0.25; // 减少红色更多,避免灰色 + gOut -= dark * 0.3; // 减少绿色较少,保持绿色调 + bOut -= dark * 0.25; // 减少蓝色更多,避免灰色 + } + + // 散布的小亮点:模拟参考图中的黄绿色小块(更亮) + const tuftNoise = pseudoRandom(normalizedNoise * 234.567 + 0.67); + if (tuftNoise > 0.94) { + const tuftBright = 4.5 + (tuftNoise - 0.94) * 6; + // 偏黄绿色:增加G,减少B,提高饱和度,更亮 + rOut += tuftBright * 0.16; // 适度增加红色 + gOut += tuftBright * 0.18; // 大幅增加绿色,更亮 + bOut += tuftBright * 0.02; // 略微增加蓝色 + } + + // 额外的细微变化:模拟草叶的随机性(更亮,确保深色时保持绿色) + const microVariation = (pseudoRandom(normalizedNoise * 156.789 + 0.91) - 0.5) * 1.2; + if (microVariation < 0) { + // 负值(变暗):减少R和B更多,保持绿色调 + rOut += microVariation * 0.12; // 减少红色更多 + gOut += microVariation * 0.18; // 减少绿色较少,保持绿色调 + bOut += microVariation * 0.12; // 减少蓝色更多 + } else { + // 正值(变亮):正常增加 + rOut += microVariation * 0.08; // 适度变化红色 + gOut += microVariation * 0.23; // 大幅变化绿色,更亮 + bOut += microVariation * 0.03; // 略微变化蓝色 + } + + // 防止草地颜色过亮:确保最终颜色不会发白(恢复原来的亮绿色上限) + const maxGrassR = 160; // 红色上限 + const maxGrassG = 235; // 绿色上限 + const maxGrassB = 115; // 蓝色上限 + rOut = Math.min(rOut, maxGrassR); + gOut = Math.min(gOut, maxGrassG); + bOut = Math.min(rOut, maxGrassB); + + // 关键保护:确保最终颜色保持绿色调,避免灰色 + // 确保G值始终是最大的,保持绿色调 + if (gOut < rOut) { + // 如果R大于G,调整R使其小于G + rOut = Math.min(rOut, gOut * 0.85); + } + if (gOut < bOut) { + // 如果B大于G,调整B使其小于G + bOut = Math.min(bOut, gOut * 0.5); + } + + // 确保G值不会太低(保持绿色调) + const baseG = PALETTE[type][1]; // 基色G值 + const minG = baseG * 0.4; // G值最小不低于基色的40% + if (gOut < minG) { + // 如果G值太低,提高G值,同时按比例调整R和B + gOut = minG; + // 保持R和B相对于G的比例 + if (rOut > 0) rOut = Math.min(rOut, gOut * 0.75); + if (bOut > 0) bOut = Math.min(bOut, gOut * 0.5); + } + } else if (type === 'stone') { + // 石头:Minecraft 风格,明显的像素化斑驳纹理 + const normalizedNoise = (noiseVal + 1) * 0.5; + const variantPalette = STONE_SWATCHES; + + // 使用多个随机种子创建更丰富的色块变化 + const variantSeed1 = pseudoRandom(normalizedNoise * 12.9898 + 2); + const variantSeed2 = pseudoRandom(normalizedNoise * 37.456 + 0.55); + const combinedSeed = (variantSeed1 + variantSeed2 * 0.5) / 1.5; + + const variantPos = combinedSeed * (variantPalette.length - 1); + const lowerIndex = Math.floor(variantPos); + const upperIndex = Math.min(variantPalette.length - 1, lowerIndex + 1); + const lerpT = variantPos - lowerIndex; + + const lower = variantPalette[lowerIndex]; + const upper = variantPalette[upperIndex]; + const variant = { + r: lerp(lower.r, upper.r, lerpT), + g: lerp(lower.g, upper.g, lerpT), + b: lerp(lower.b, upper.b, lerpT), + }; + + rOut += variant.r; + gOut += variant.g; + bOut += variant.b; + + // 像素化噪声:创建Minecraft风格的明暗变化 + const pixelNoise = pseudoRandom(normalizedNoise * 89.123 + 0.57); + if (pixelNoise > 0.88) { + // 亮块:保持灰蓝色调 + const bright = 3.2 + (pixelNoise - 0.88) * 4.2; + rOut += bright * 0.3; // 均匀增加RGB + gOut += bright * 0.3; + bOut += bright * 0.35; // B略多,保持冷色调 + } else if (pixelNoise < 0.15) { + // 暗块:深灰色(保持灰蓝色调) + const dark = 5 + (0.15 - pixelNoise) * 8; + rOut -= dark * 0.3; // 均匀减少RGB + gOut -= dark * 0.3; + bOut -= dark * 0.25; // B减少较少,保持冷色调 + } + + // 散布的小亮点:模拟石头表面的高光 + const highlightNoise = pseudoRandom(normalizedNoise * 234.567 + 0.77); + if (highlightNoise > 0.94) { + const highlightBright = 4.5 + (highlightNoise - 0.94) * 6; + // 保持灰蓝色调,B略多 + rOut += highlightBright * 0.25; + gOut += highlightBright * 0.25; + bOut += highlightBright * 0.3; + } + + // 额外的细微变化:模拟石头表面的随机性 + const microVariation = (pseudoRandom(normalizedNoise * 156.789 + 0.93) - 0.5) * 1.2; + if (microVariation < 0) { + // 负值(变暗):均匀减少,B减少较少 + rOut += microVariation * 0.3; + gOut += microVariation * 0.3; + bOut += microVariation * 0.25; + } else { + // 正值(变亮):均匀增加,B增加略多 + rOut += microVariation * 0.25; + gOut += microVariation * 0.25; + bOut += microVariation * 0.3; + } + + // 防止石头颜色过亮或过暗 + const maxStoneR = 200; + const maxStoneG = 210; + const maxStoneB = 220; + const minStoneR = 100; + const minStoneG = 105; + const minStoneB = 115; + rOut = Math.max(minStoneR, Math.min(rOut, maxStoneR)); + gOut = Math.max(minStoneG, Math.min(gOut, maxStoneG)); + bOut = Math.max(minStoneB, Math.min(bOut, maxStoneB)); + + // 保持灰蓝色调:确保RGB值接近,但B略高 + const avg = (rOut + gOut + bOut) / 3; + if (Math.abs(rOut - gOut) > 15) { + rOut = avg + (rOut - avg) * 0.5; + gOut = avg + (gOut - avg) * 0.5; + } + // 确保B值略高于R和G,保持冷色调 + if (bOut < Math.max(rOut, gOut) + 3) { + bOut = Math.max(rOut, gOut) + 3; + } + } else if (type === 'dark_stone') { + // 深色石头:保持简单的暗色斑块效果 + if (noiseVal > 0.8) { + const darkAmount = 20 + Math.random() * 15; + rOut -= darkAmount; + gOut -= darkAmount; + bOut -= darkAmount; + } else { + // 保持基色,微小波动 + const shift = (noiseVal * 8) - 4; + rOut += shift; + gOut += shift; + bOut += (shift + 2); // 保持一点点冷色调倾向 + } + } else if (type === 'deep_dirt') { + // 深层泥土:多层次材质(深色斑点 + 浅色颗粒 + 基础波动) + if (noiseVal > 0.85) { + const darkAmount = 8 + Math.random() * 12; + rOut -= darkAmount; + gOut -= darkAmount * 0.9; + bOut -= darkAmount * 0.7; + } else if (noiseVal > 0.70 && noiseVal <= 0.75) { + const mediumDark = 4 + Math.random() * 6; + rOut -= mediumDark; + gOut -= mediumDark; + bOut -= mediumDark; + } else if (noiseVal < 0.08) { + const lightAmount = 6 + Math.random() * 6; + rOut += lightAmount; + gOut += lightAmount * 0.85; + bOut += lightAmount * 0.65; + } else { + const shift = (noiseVal * 8) - 4; + rOut += shift; + gOut += shift * 0.85; + bOut += shift * 0.7; + } + } else if (type === 'medium_dirt') { + // 中层泥土:平衡的深浅材质 + if (noiseVal > 0.87) { + const darkAmount = 7 + Math.random() * 10; + rOut -= darkAmount; + gOut -= darkAmount * 0.9; + bOut -= darkAmount * 0.75; + } else if (noiseVal > 0.72 && noiseVal <= 0.78) { + const mediumDark = 4 + Math.random() * 5; + rOut -= mediumDark; + gOut -= mediumDark; + bOut -= mediumDark; + } else if (noiseVal < 0.10) { + const lightAmount = 6 + Math.random() * 6; + rOut += lightAmount; + gOut += lightAmount * 0.85; + bOut += lightAmount * 0.65; + } else { + const shift = (noiseVal * 8) - 4; + rOut += shift; + gOut += shift * 0.88; + bOut += shift * 0.75; + } + } else if (type === 'dirt') { + // 泥土:Minecraft 风格,包含小石子和细节 + const normalizedNoise = (noiseVal + 1) * 0.5; + const stoneNoise = pseudoRandom(normalizedNoise * 67.891 + 0.13); + + // 小石子:深棕色或灰色像素(约 8% 概率) + if (stoneNoise > 0.92) { + const stoneDark = 12 + Math.random() * 10; + rOut -= stoneDark * 0.7; + gOut -= stoneDark * 0.8; + bOut -= stoneDark * 0.9; + } else if (stoneNoise > 0.88 && stoneNoise <= 0.92) { + // 小灰石:更偏灰色 + const grayStone = 8 + Math.random() * 6; + rOut -= grayStone * 0.5; + gOut -= grayStone * 0.5; + bOut -= grayStone * 0.4; + } else if (noiseVal > 0.88) { + // 深色泥土块 + const darkAmount = 6 + Math.random() * 8; + rOut -= darkAmount; + gOut -= darkAmount * 0.85; + bOut -= darkAmount * 0.7; + } else if (noiseVal > 0.74 && noiseVal <= 0.8) { + // 中等深色 + const mediumDark = 3 + Math.random() * 4; + rOut -= mediumDark; + gOut -= mediumDark; + bOut -= mediumDark; + } else if (noiseVal < 0.16) { + // 浅色泥土 + const lightAmount = 5 + Math.random() * 5; + rOut += lightAmount; + gOut += lightAmount * 0.9; + bOut += lightAmount * 0.7; + } else if (noiseVal >= 0.16 && noiseVal < 0.21) { + // 很浅的亮点 + const veryLight = 6 + Math.random() * 6; + rOut += veryLight; + gOut += veryLight * 0.95; + bOut += veryLight * 0.8; + } else { + // 正常微小噪点 + const shift = (noiseVal * 6) - 3; + rOut += shift; + gOut += shift * 0.9; + bOut += shift * 0.75; + } + } + + // 确保颜色值有效 + rOut = Math.max(0, Math.min(255, rOut)); + gOut = Math.max(0, Math.min(255, gOut)); + bOut = Math.max(0, Math.min(255, bOut)); + + return rgbToHex(rOut, gOut, bOut, alpha); +}; + +// ============= 导出所有样式相关内容 ============= + +export { + GRASS_SWATCHES, + DARK_GRASS_SWATCHES, + SAND_SWATCHES, + STONE_SWATCHES, + CACTUS_SWATCHES, + LEAF_SWATCHES, + FLOWER_SWATCHES, + REED_SWATCHES, + LAVA_SWATCHES, + VOLCANIC_ROCK_SWATCHES, + ASH_SWATCHES, + MUD_SWATCHES, + SWAMP_GRASS_SWATCHES, + MOSS_SWATCHES, + ICE_SWATCHES, + PACKED_ICE_SWATCHES, + PERMAFROST_SWATCHES, + varyColor, +}; diff --git a/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts new file mode 100644 index 0000000..c03b61c --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/logic/waterSystem.ts @@ -0,0 +1,320 @@ +import { createNoise2D } from 'simplex-noise'; + +// 定义微型体素的缩放比例(需与 terrain.ts 保持一致,但为避免循环依赖在此定义或作为参数传入) +const DEFAULT_MICRO_SCALE = 8; + +export interface StreamVoxel { + depth: number; + waterHeight: number; + isCenter: boolean; +} + +interface Point { + x: number; + y: number; // represents Z in 3D space +} + +interface Node { + x: number; + y: number; + f: number; + g: number; + h: number; + parent: Node | null; +} + +/** + * 简单的优先队列实现,用于 A* 寻路 + */ +class PriorityQueue { + private items: Node[] = []; + + enqueue(element: Node) { + let contain = false; + for (let i = 0; i < this.items.length; i++) { + if (this.items[i].f > element.f) { + this.items.splice(i, 0, element); + contain = true; + break; + } + } + if (!contain) { + this.items.push(element); + } + } + + dequeue(): Node | undefined { + return this.items.shift(); + } + + isEmpty(): boolean { + return this.items.length === 0; + } +} + +/** + * 生成沙漠溪流系统 + * 使用微型体素网格进行 A* 寻路,避开障碍物并引入自然蜿蜒 + * + * 更新 V2: + * 1. 智能绕过障碍物(加强版寻路) + * 2. 支持从地图任意边缘流向另一边缘,提高出现率 + * 3. 保持“宽河床 + 深水道”的立体结构 + * + * @param mapSize 逻辑地图大小 (e.g. 16, 32) + * @param gobiHeight 戈壁高度图 (逻辑坐标) + * @param stoneHeight 巨石高度图 (逻辑坐标) + * @param rng 随机数生成器 + * @param microScale 微缩比例 (默认 8) + */ +export const generateDesertRiver = ( + mapSize: number, + gobiHeight: number[][], + stoneHeight: number[][], + rng: () => number, + microScale: number = DEFAULT_MICRO_SCALE +): Map => { + const width = mapSize * microScale; + const height = mapSize * microScale; // height here corresponds to Z axis + + // 1. 初始化噪声生成器 + let seedVal = rng() * 10000; + const noise2D = createNoise2D(() => { + seedVal = (seedVal * 9301 + 49297) % 233280; + return seedVal / 233280; + }); + + // 2. 构建障碍物网格 (Collision Grid) + // 0: 空闲, 1: 缓冲区(高代价), 2: 障碍物(不可通行) + const grid = new Uint8Array(width * height).fill(0); + const setGrid = (x: number, z: number, val: number) => { + if (x >= 0 && x < width && z >= 0 && z < height) { + grid[z * width + x] = Math.max(grid[z * width + x], val); + } + }; + const getGrid = (x: number, z: number): number => { + if (x >= 0 && x < width && z >= 0 && z < height) { + return grid[z * width + x]; + } + return 2; // 越界视为障碍 + }; + + // 填充障碍物数据 + for (let lx = 0; lx < mapSize; lx++) { + for (let lz = 0; lz < mapSize; lz++) { + const hasGobi = gobiHeight[lx][lz] > 0; + const hasStone = stoneHeight[lx][lz] > 0; + + if (hasGobi || hasStone) { + const startX = lx * microScale; + const startZ = lz * microScale; + + // 标记实体障碍 + for (let mx = 0; mx < microScale; mx++) { + for (let mz = 0; mz < microScale; mz++) { + setGrid(startX + mx, startZ + mz, 2); + } + } + + // 标记缓冲区 (Buffer Zone),设为高代价区域,鼓励河流绕行 + // 增加缓冲区范围,确保河流不会贴着山脚 + const bufferSize = 8; + for (let bx = -bufferSize; bx < microScale + bufferSize; bx++) { + for (let bz = -bufferSize; bz < microScale + bufferSize; bz++) { + const gx = startX + bx; + const gz = startZ + bz; + if (getGrid(gx, gz) !== 2) { + setGrid(gx, gz, 1); + } + } + } + } + } + } + + // 3. A* 寻路尝试循环 + // 尝试多次寻找有效路径,直到成功 + const maxAttempts = 10; + let bestPath: Node[] | null = null; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + if (bestPath) break; + + // 随机选择起点和终点边缘 (0: Top, 1: Right, 2: Bottom, 3: Left) + const startEdge = Math.floor(rng() * 4); + let endEdge = Math.floor(rng() * 4); + // 确保终点不在同侧,且尽量不在相邻侧的太近位置 + while (endEdge === startEdge) { + endEdge = Math.floor(rng() * 4); + } + + const getEdgePoint = (edge: number): Point => { + const margin = 10; // 稍微避开角落 + const span = (edge % 2 === 0 ? width : height) - 2 * margin; + const pos = margin + Math.floor(rng() * span); + + switch(edge) { + case 0: return { x: pos, y: 0 }; // Top + case 1: return { x: width - 1, y: pos }; // Right + case 2: return { x: pos, y: height - 1 }; // Bottom + case 3: return { x: 0, y: pos }; // Left + default: return { x: 0, y: 0 }; + } + }; + + const startPoint = getEdgePoint(startEdge); + const endPoint = getEdgePoint(endEdge); + + // 检查起点终点是否在障碍物内 (虽然边缘通常无障碍,但以防万一) + if (getGrid(startPoint.x, startPoint.y) === 2 || getGrid(endPoint.x, endPoint.y) === 2) { + continue; + } + + // 执行 A* + const startNode: Node = { ...startPoint, f: 0, g: 0, h: 0, parent: null }; + const openList = new PriorityQueue(); + openList.enqueue(startNode); + const closedSet = new Set(); + const nodeKey = (x: number, y: number) => `${x}|${y}`; + + let finalNode: Node | null = null; + let iterations = 0; + // 增加迭代上限,允许更长的绕路 + const maxIterations = width * height * 4; + + const heuristic = (x: number, y: number) => Math.abs(endPoint.x - x) + Math.abs(endPoint.y - y); + + while (!openList.isEmpty() && iterations < maxIterations) { + iterations++; + const currentNode = openList.dequeue(); + if (!currentNode) break; + + // 到达终点附近 + if (Math.abs(currentNode.x - endPoint.x) < 4 && Math.abs(currentNode.y - endPoint.y) < 4) { + finalNode = currentNode; + break; + } + + const key = nodeKey(currentNode.x, currentNode.y); + if (closedSet.has(key)) continue; + closedSet.add(key); + + // 8个方向 + const dirs = [ + { x: 1, y: 0, cost: 1.0 }, { x: -1, y: 0, cost: 1.0 }, + { x: 0, y: 1, cost: 1.0 }, { x: 0, y: -1, cost: 1.0 }, + { x: 1, y: 1, cost: 1.414 }, { x: 1, y: -1, cost: 1.414 }, + { x: -1, y: 1, cost: 1.414 }, { x: -1, y: -1, cost: 1.414 }, + ]; + + for (const dir of dirs) { + const nx = currentNode.x + dir.x; + const ny = currentNode.y + dir.y; + + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + if (closedSet.has(nodeKey(nx, ny))) continue; + + const cellType = getGrid(nx, ny); + if (cellType === 2) continue; // 绝对障碍 + + let newG = currentNode.g + dir.cost; + + // 代价权重调整 + if (cellType === 1) { + newG += 50; // 强力避开缓冲区 + } + + // 噪声代价:引导河流走更自然的路径 (Perlin Noise Flow Field) + // 使用噪声值作为地形阻力,河流倾向于流经 "低阻力" (低噪声值) 区域 + const noiseVal = noise2D(nx * 0.03, ny * 0.03); + newG += (noiseVal + 1) * 5; // 增加一些随机地形阻力 + newG += rng() * 1.5; // 微小随机抖动 + + const h = heuristic(nx, ny); + openList.enqueue({ + x: nx, y: ny, f: newG + h, g: newG, h: h, parent: currentNode + }); + } + } + + if (finalNode) { + // 回溯路径 + const path: Node[] = []; + let curr: Node | null = finalNode; + while (curr) { + path.push(curr); + curr = curr.parent; + } + bestPath = path; + } + } + + const streamMap = new Map(); + + if (bestPath) { + // 4. 渲染宽河床和深水道 + // 渲染参数 (保持用户喜欢的立体感) + const channelRadius = 2.0; // 内圈深水道半径 (~4 宽) + const bankRadius = 4.5; // 外圈浅河床半径 (~9 宽),加宽一点 + + const channelDepth = 3; // 深水道总深度 + const bankDepth = 1; // 浅河床深度 + const waterFillHeight = 2; // 水深 + + // 使用 Set 防止重复计算 + const processedPoints = new Set(); + + for (const node of bestPath) { + const cx = node.x; + const cy = node.y; // cy is Z + + const scanRad = Math.ceil(bankRadius + 2); + for (let dx = -scanRad; dx <= scanRad; dx++) { + for (let dy = -scanRad; dy <= scanRad; dy++) { + const tx = cx + dx; + const ty = cy + dy; + + if (tx < 0 || tx >= width || ty < 0 || ty >= height) continue; + + // 绝对避障 + if (getGrid(tx, ty) === 2) continue; + + const key = `${tx}|${ty}`; + const dist = Math.sqrt(dx*dx + dy*dy); + + // 边缘噪声,使河岸不规则 - 降低频率,减少破碎感 + const noise = noise2D(tx * 0.08, ty * 0.08) * 0.8; // 0.15 -> 0.08 + const noisyChannelRad = channelRadius + noise * 0.5; // 0.6 -> 0.5 + const noisyBankRad = bankRadius + noise * 1.0; // 1.2 -> 1.0 + + let newDepth = 0; + let isCenter = false; + + if (dist < noisyChannelRad) { + newDepth = channelDepth; + isCenter = true; + } else if (dist < noisyBankRad) { + newDepth = bankDepth; + } + + if (newDepth > 0) { + const existing = streamMap.get(key); + if (!existing || newDepth > existing.depth) { + streamMap.set(key, { + depth: newDepth, + // 只有深水道才放水,且水位设置为2 (离地表差1格) + waterHeight: newDepth === channelDepth ? waterFillHeight : 0, + isCenter + }); + } + } + } + } + } + console.log(`River generated successfully with length ${bestPath.length}`); + } else { + console.warn("Failed to generate river path after all attempts."); + } + + return streamMap; +}; diff --git a/voxel-tactics-horizon/src/features/Map/store.ts b/voxel-tactics-horizon/src/features/Map/store.ts new file mode 100644 index 0000000..5445d87 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Map/store.ts @@ -0,0 +1,123 @@ +import { create } from 'zustand'; +import { generateTerrain, MAP_SIZES, VOXEL_SIZE, TILE_SIZE, TERRAIN_VERSION, type TerrainGenerationProgress } from './logic/terrain'; +import { getRandomSceneType, type SceneType } from './logic/scenes'; +import type { VoxelData } from './logic/terrain'; + +interface MapState { + size: number; + voxels: VoxelData[]; + logicalHeightMap: Record; // Store logical height for gameplay + terrainVersion: number; // 跟踪地形版本 + currentScene: SceneType; // 当前场景类型 + isGenerating: boolean; // 是否正在生成地形 + generationProgress: TerrainGenerationProgress | null; // 生成进度 + // 戈壁风化参数 + weatheringMaxIterations: number; // 最大风蚀层数 (1-8) + weatheringHeightBias: number; // 高度保护阈值 (0.0-1.0) + generateMap: (sizeKey: keyof typeof MAP_SIZES, seed?: string, sceneType?: SceneType) => void; + getHeightAt: (x: number, z: number) => number; + setScene: (sceneType: SceneType) => void; + setWeatheringMaxIterations: (value: number) => void; + setWeatheringHeightBias: (value: number) => void; +} + +export const useMapStore = create((set, get) => ({ + size: MAP_SIZES.small, + voxels: [], + logicalHeightMap: {}, + terrainVersion: 0, + currentScene: 'desert', // 默认场景:沙漠戈壁(固定用于调试) + isGenerating: false, + generationProgress: null, + // 戈壁风化参数默认值 + weatheringMaxIterations: 3, + weatheringHeightBias: 0.5, + generateMap: async (sizeKey, seed, sceneType) => { + // 标记开始生成 - 初始化进度为0% + set({ + isGenerating: true, + generationProgress: { + stage: 'basic', + stageLabel: '准备生成地形', + progress: 0, + detail: '正在初始化...' + } + }); + + const size = MAP_SIZES[sizeKey]; + // 如果没有指定场景,使用随机场景 + const selectedScene = sceneType || getRandomSceneType(); + const runtimeSeed = + seed ?? + `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + + // 获取当前的风化参数 + const { weatheringMaxIterations, weatheringHeightBias } = get(); + + // 使用 setTimeout 让 React 有时间渲染进度条UI + setTimeout(async () => { + try { + // 使用 await 等待异步地形生成完成,并传递风化参数 + const voxels = await generateTerrain( + size, + runtimeSeed, + selectedScene, + (progress) => { + set({ generationProgress: progress }); + }, + { + weatheringMaxIterations, + weatheringHeightBias + } + ); + + const logicalHeightMap: Record = {}; + voxels.forEach((v) => { + const lx = Math.floor(v.x / TILE_SIZE); + const lz = Math.floor(v.z / TILE_SIZE); + const key = `${lx},${lz}`; + if (logicalHeightMap[key] === undefined || v.y > logicalHeightMap[key]) { + logicalHeightMap[key] = v.y; + } + }); + + set({ + size, + voxels, + logicalHeightMap, + terrainVersion: TERRAIN_VERSION, + currentScene: selectedScene, + isGenerating: false, + generationProgress: null + }); + } catch (error) { + console.error('地形生成失败:', error); + set({ + isGenerating: false, + generationProgress: null + }); + } + }, 50); // 给UI 50ms的时间来渲染进度条 + }, + getHeightAt: (x, z) => { + const key = `${x},${z}`; + const worldY = get().logicalHeightMap[key] || 0; + // Return Logical Y units? No, UnitRenderer expects World Y. + // Our units move in TILE_SIZE steps. + // But their vertical position should be World Y. + + // Unit Logic Y: + // If ground is at World Y = 2.5. Unit stands at 2.5 + UnitHeight/2. + // So we return World Y. + return worldY + VOXEL_SIZE; // +1 voxel size to be ON TOP + }, + setScene: (sceneType) => { + set({ currentScene: sceneType }); + }, + setWeatheringMaxIterations: (value) => { + set({ weatheringMaxIterations: value }); + }, + setWeatheringHeightBias: (value) => { + set({ weatheringHeightBias: value }); + } +})); diff --git a/voxel-tactics-horizon/src/features/Units/components/UnitRenderer.tsx b/voxel-tactics-horizon/src/features/Units/components/UnitRenderer.tsx new file mode 100644 index 0000000..a5c9f04 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Units/components/UnitRenderer.tsx @@ -0,0 +1,117 @@ +import React, { useRef, useMemo, useLayoutEffect } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { Group, Vector3, Color, InstancedMesh, Object3D } from 'three'; +import { Html } from '@react-three/drei'; +import type { Unit, JobType } from '../types'; +import { useUnitStore } from '../store'; +import { TILE_SIZE, VOXEL_SIZE } from '../../Map/logic/terrain'; + +const JOB_COLORS: Record = { + Squire: '#8B4513', + Chemist: '#ADD8E6', + Knight: '#C0C0C0', + Archer: '#228B22', + Wizard: '#800080', +}; + +const generateCharacterVoxels = (job: JobType) => { + const voxels = []; + + for(let y=0; y<4; y++) { + voxels.push({x: -2, y, z: 0, color: '#333'}); + voxels.push({x: 2, y, z: 0, color: '#333'}); + } + const bodyColor = JOB_COLORS[job]; + for(let y=4; y<10; y++) { + for(let x=-2; x<=2; x++) { + voxels.push({x, y, z: 0, color: bodyColor}); + voxels.push({x, y, z: -1, color: bodyColor}); + } + } + for(let y=10; y<14; y++) { + for(let x=-2; x<=2; x++) { + voxels.push({x, y, z: 0, color: '#ffe0bd'}); + } + } + return voxels; +}; + +const HighResUnitModel: React.FC<{ unit: Unit; isSelected: boolean }> = ({ unit, isSelected }) => { + const groupRef = useRef(null); + + useFrame(() => { + if (groupRef.current) { + const cx = unit.position.x * TILE_SIZE + TILE_SIZE / 2; + const cz = unit.position.z * TILE_SIZE + TILE_SIZE / 2; + const targetPos = new Vector3(cx, unit.position.y, cz); + + groupRef.current.position.lerp(targetPos, 0.1); + + const targetRotY = unit.facing * (Math.PI / 2); + groupRef.current.rotation.y = targetRotY; + } + }); + + const charVoxels = useMemo(() => generateCharacterVoxels(unit.job), [unit.job]); + const meshRef = useRef(null); + const tempObj = new Object3D(); + const tempCol = new Color(); + + useLayoutEffect(() => { + if(!meshRef.current) return; + const s = 0.06; + charVoxels.forEach((v, i) => { + tempObj.position.set(v.x * s, v.y * s, v.z * s); + tempObj.scale.set(s, s, s); + tempObj.updateMatrix(); + meshRef.current!.setMatrixAt(i, tempObj.matrix); + tempCol.set(v.color); + meshRef.current!.setColorAt(i, tempCol); + }); + meshRef.current.instanceMatrix.needsUpdate = true; + if (meshRef.current.instanceColor) meshRef.current.instanceColor.needsUpdate = true; + }, [charVoxels]); + + return ( + + + + {/* REMOVED vertexColors={true} here as well */} + + + + {isSelected && ( + + + + + )} + + +
+
+
+ + + ); +}; + +export const UnitRenderer: React.FC = () => { + const units = useUnitStore((state) => state.units); + const selectedUnitId = useUnitStore((state) => state.selectedUnitId); + + return ( + + {units.map((unit) => ( + + ))} + + ); +}; diff --git a/voxel-tactics-horizon/src/features/Units/store.ts b/voxel-tactics-horizon/src/features/Units/store.ts new file mode 100644 index 0000000..e148fb3 --- /dev/null +++ b/voxel-tactics-horizon/src/features/Units/store.ts @@ -0,0 +1,38 @@ +import { create } from 'zustand'; +import { BASE_STATS } from './types'; +import type { Unit, Direction } from './types'; +import { v4 as uuidv4 } from 'uuid'; + +interface UnitState { + units: Unit[]; + selectedUnitId: string | null; + hoveredTile: { x: number; z: number } | null; + + addUnit: (unit: Omit) => void; + selectUnit: (id: string | null) => void; + moveUnit: (id: string, to: { x: number; y: number; z: number }) => void; + setHoveredTile: (tile: { x: number; z: number } | null) => void; +} + +export const useUnitStore = create((set) => ({ + units: [], + selectedUnitId: null, + hoveredTile: null, + + addUnit: (unitData) => set((state) => ({ + units: [...state.units, { + ...unitData, + id: uuidv4(), + currentHp: unitData.maxHp, + ct: 0, + }] + })), + + selectUnit: (id) => set({ selectedUnitId: id }), + + moveUnit: (id, to) => set((state) => ({ + units: state.units.map(u => u.id === id ? { ...u, position: to } : u) + })), + + setHoveredTile: (tile) => set({ hoveredTile: tile }), +})); diff --git a/voxel-tactics-horizon/src/features/Units/types.ts b/voxel-tactics-horizon/src/features/Units/types.ts new file mode 100644 index 0000000..1b783db --- /dev/null +++ b/voxel-tactics-horizon/src/features/Units/types.ts @@ -0,0 +1,33 @@ +export type JobType = 'Squire' | 'Chemist' | 'Knight' | 'Archer' | 'Wizard'; + +export type Direction = 0 | 1 | 2 | 3; // 0: North (-Z), 1: East (+X), 2: South (+Z), 3: West (-X) + +export interface Stats { + move: number; + jump: number; + speed: number; + attack: number; + defense: number; +} + +export interface Unit { + id: string; + name: string; + job: JobType; + team: 'player' | 'enemy'; + position: { x: number; y: number; z: number }; + facing: Direction; + stats: Stats; + currentHp: number; + maxHp: number; + ct: number; // Charge Time (0-100) +} + +export const BASE_STATS: Record = { + Squire: { move: 4, jump: 2, speed: 10, attack: 8, defense: 6 }, + Chemist: { move: 3, jump: 2, speed: 9, attack: 6, defense: 5 }, + Knight: { move: 3, jump: 2, speed: 8, attack: 12, defense: 10 }, + Archer: { move: 4, jump: 3, speed: 11, attack: 9, defense: 4 }, + Wizard: { move: 3, jump: 2, speed: 9, attack: 14, defense: 3 }, +}; + diff --git a/voxel-tactics-horizon/src/index.css b/voxel-tactics-horizon/src/index.css new file mode 100644 index 0000000..4e96186 --- /dev/null +++ b/voxel-tactics-horizon/src/index.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html, body, #root { + @apply w-full h-full m-0 p-0 overflow-hidden bg-[#1a1a1a]; + } +} diff --git a/voxel-tactics-horizon/src/main.tsx b/voxel-tactics-horizon/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/voxel-tactics-horizon/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/voxel-tactics-horizon/src/utils/pathfinding.ts b/voxel-tactics-horizon/src/utils/pathfinding.ts new file mode 100644 index 0000000..fa7c0a7 --- /dev/null +++ b/voxel-tactics-horizon/src/utils/pathfinding.ts @@ -0,0 +1,68 @@ +import type { VoxelData } from '../features/Map/logic/terrain'; + +interface Point { x: number; y: number; z: number } + +export const getReachableTiles = ( + start: Point, + moveRange: number, + jumpHeight: number, + getHeightAt: (x: number, z: number) => number, + mapSize: number +): Point[] => { + const visited = new Set(); + const reachable: Point[] = []; + const queue: { pos: Point; dist: number }[] = [{ pos: start, dist: 0 }]; + + visited.add(`${start.x},${start.z}`); + reachable.push(start); + + while (queue.length > 0) { + const { pos, dist } = queue.shift()!; + + if (dist >= moveRange) continue; + + const neighbors = [ + { x: pos.x + 1, z: pos.z }, + { x: pos.x - 1, z: pos.z }, + { x: pos.x, z: pos.z + 1 }, + { x: pos.x, z: pos.z - 1 }, + ]; + + for (const n of neighbors) { + // Bounds check + if (n.x < 0 || n.z < 0 || n.x >= mapSize || n.z >= mapSize) continue; + + const key = `${n.x},${n.z}`; + if (visited.has(key)) continue; + + const h = getHeightAt(n.x, n.z); + // const currentH = pos.y + 1; // Logic y is floor. + // const targetH = h; // getHeightAt returns top of block + 1 (unit y). + + // Actually, getHeightAt returns (voxel.y + 1). + // Our unit logic stores y as (voxel.y). + // Let's standarize: logic coordinates use "Top of Block Y". + // start.y is Unit Y. getHeightAt returns Unit Y. + + // const heightDiff = Math.abs(targetH - start.y); // Compare to START height? No, step by step. + // const stepDiff = Math.abs(targetH - (pos.y || start.y)); // Needs previous step height. + + // For simple BFS, we need to know height of current tile 'pos'. + // We didn't store it in queue. Let's fix. + // Actually getHeightAt is cheap O(1). + + const myHeight = getHeightAt(pos.x, pos.z); + const nextHeight = getHeightAt(n.x, n.z); + + if (Math.abs(nextHeight - myHeight) <= jumpHeight) { + // Valid move (assuming no unit blockage for now) + visited.add(key); + const nextPos = { x: n.x, y: nextHeight - 1, z: n.z }; // Store unit Y (block top) + reachable.push(nextPos); + queue.push({ pos: nextPos, dist: dist + 1 }); + } + } + } + + return reachable; +}; diff --git a/voxel-tactics-horizon/tailwind.config.js b/voxel-tactics-horizon/tailwind.config.js new file mode 100644 index 0000000..d37737f --- /dev/null +++ b/voxel-tactics-horizon/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/voxel-tactics-horizon/tsconfig.app.json b/voxel-tactics-horizon/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/voxel-tactics-horizon/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/voxel-tactics-horizon/tsconfig.json b/voxel-tactics-horizon/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/voxel-tactics-horizon/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/voxel-tactics-horizon/tsconfig.node.json b/voxel-tactics-horizon/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/voxel-tactics-horizon/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/voxel-tactics-horizon/vite.config.ts b/voxel-tactics-horizon/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/voxel-tactics-horizon/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})