From f76370151ecab7dc10dabdd5380cedbe9d28a4ba Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Mon, 31 Aug 2026 20:54:53 +0300 Subject: [PATCH] initial commit --- .gitignore | 4 + README.md | 43 + index.html | 12 + package-lock.json | 1790 +++++++++++++++++++++++++++++++++ package.json | 24 + src/App.tsx | 153 +++ src/components/GameCanvas.tsx | 97 ++ src/components/NodeCard.tsx | 51 + src/components/NodeLine.tsx | 39 + src/components/Sidebar.tsx | 111 ++ src/components/Topbar.tsx | 30 + src/engine.ts.none | 242 +++++ src/engine/functions/index.ts | 98 ++ src/engine/index.ts | 0 src/engine/saveLoad.ts | 78 ++ src/index.css | 354 +++++++ src/main.tsx | 10 + src/prefabs/game.ts | 71 ++ src/storage.ts | 47 + src/types/index.ts | 79 ++ src/ui/graph.ts | 79 ++ src/ui/node.ts | 0 src/ui/resource.ts | 17 + tsconfig.app.json | 20 + tsconfig.json | 7 + tsconfig.node.json | 11 + vite.config.ts | 6 + 27 files changed, 3473 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/components/GameCanvas.tsx create mode 100644 src/components/NodeCard.tsx create mode 100644 src/components/NodeLine.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/Topbar.tsx create mode 100644 src/engine.ts.none create mode 100644 src/engine/functions/index.ts create mode 100644 src/engine/index.ts create mode 100644 src/engine/saveLoad.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/prefabs/game.ts create mode 100644 src/storage.ts create mode 100644 src/types/index.ts create mode 100644 src/ui/graph.ts create mode 100644 src/ui/node.ts create mode 100644 src/ui/resource.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a1f1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.tsbuildinfo +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd945ae --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Граф ресурсов — тиковая симуляция (Vite + React + TypeScript) + +## Запуск + +```bash +npm install +npm run dev +``` + +Откроется адрес вида `http://localhost:5173`. + +## Сборка + +```bash +npm run build # tsc -b && vite build -> dist/ +npm run preview # локальный просмотр собранного dist/ +``` + +## Структура + +- `src/engine.ts` — ядро симуляции: типы (`SimNode`, `SimEdge`, `Behavior`, + `Topology`, `Buffers`), функция `tick()` (один детерминированный тик) и + `resetBuffers()`. Модуль не зависит от React/DOM — логика переносима на + другой стек (Rust/C#/GDScript) по этой же спецификации. +- `src/storage.ts` — чтение/запись localStorage (`incsim.topology.v1`, + `incsim.buffers.v1`, `incsim.tickcount.v1`). +- `src/App.tsx` — состояние приложения (topology/buffers/tickCount/selected), + все обработчики мутаций, эффекты персистентности. +- `src/components/` — `Topbar`, `WorldCanvas` (холст с узлами и SVG-связями), + `NodeCard` (перетаскиваемая карточка узла), `EdgeLines` (SVG-стрелки), + `Sidebar`, `NodeEditor`, `EdgeEditor`, `RecipeEditor`. + +## Модель + +- **Storage** — пассивный буфер с ёмкостью. +- **Converter** — единственный активный узел: раз в `period` тиков атомарно + списывает `inputs` и начисляет `outputs` в собственном буфере. + `source`/`consumer` — его частные случаи с пустым списком входов/выходов. +- **Edge** — перенос ресурса между буферами узлов; конкуренция при + fan-in/fan-out разрешается по `priority` (выше — раньше), порядок + обработки узлов/рёбер фиксирован — тик полностью детерминирован. + +Reset обнуляет буферы и таймеры конвертеров, настройки узлов/связей не трогает. diff --git a/index.html b/index.html new file mode 100644 index 0000000..01012f4 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Граф ресурсов — тиковая симуляция + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..171ac5b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1790 @@ +{ + "name": "resource-graph-sim", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "resource-graph-sim", + "version": "0.1.0", + "dependencies": { + "ngraph.forcelayout": "^3.3.1", + "ngraph.graph": "^20.1.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.416", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", + "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", + "license": "BSD-3-Clause" + }, + "node_modules/ngraph.forcelayout": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz", + "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.0.0", + "ngraph.merge": "^1.0.0", + "ngraph.random": "^1.0.0" + } + }, + "node_modules/ngraph.graph": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.2.tgz", + "integrity": "sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ngraph.events": "^1.4.0" + } + }, + "node_modules/ngraph.merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz", + "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==", + "license": "MIT" + }, + "node_modules/ngraph.random": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz", + "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==", + "license": "BSD-3-Clause" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "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/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc7dc24 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "resource-graph-sim", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "ngraph.forcelayout": "^3.3.1", + "ngraph.graph": "^20.1.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..80a4f28 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,153 @@ +import { useEffect, useState } from 'react'; +import Topbar from './components/Topbar'; +import { GameCanvas } from './components/GameCanvas'; +import { Game } from './types'; +import { deserialize, load, save } from './engine/saveLoad'; +import { defaultGame } from './prefabs/game'; + +export type Selection = { kind: 'node'; id: string } | null; + +export default function App() { + const [game, setGame] = useState(() => load()); + const [selected, setSelected] = useState(null); + + useEffect(() => save(game), [game]); + // useEffect(() => saveTickCount(tickCount), [tickCount]); + + function handleTick() { + game.tick(1.0 / 60); + // const nextTopology = cloneJSON(game); + // const nextBuffers = cloneJSON(buffers); + // tick(nextTopology, nextBuffers); + // setGame(nextTopology); + // setBuffers(nextBuffers); + // setTickCount((n) => n + 1); + } + + function handleReset() { + const ok = window.confirm( + 'Обнулить все значения и таймеры? Настройки узлов и связей не изменятся.' + ); + if (!ok) + return; + + game.time = 0; + save(game); + } + + function handleInitialize() { + const ok = window.confirm( + 'Удалить все объекты и начать игру с начала?' + ); + if (!ok) + return; + + setGame(deserialize(defaultGame())); + save(game); + } + + // function handleAddNode(kind: NodeKind) { + // const idNum = game.nextId + 1; + // const id = `${kind}-${idNum}`; + // const node: SimNode = { + // id, + // label: kindLabel(kind), + // x: 60 + (game.nodes.length * 25) % 600, + // y: 60 + (game.nodes.length * 47) % 400, + // capacity: kind === 'storage' ? 100 : null, + // behavior: BEHAVIOR_DEFAULTS[kind], + // }; + // setGame((prev) => ({ ...prev, nextId: idNum, nodes: [...prev.nodes, node] })); + // setBuffers((prev) => ({ ...prev, [id]: {} })); + // setSelected({ kind: 'node', id }); + // } + + // function handleRemoveNode(id: string) { + // setGame((prev) => ({ + // ...prev, + // nodes: prev.nodes.filter((n) => n.id !== id), + // edges: prev.edges.filter((e) => e.from !== id && e.to !== id), + // })); + // setBuffers((prev) => { + // const next = { ...prev }; + // delete next[id]; + // return next; + // }); + // setSelected((s) => (s && s.id === id ? null : s)); + // } + + // function handleAddEdge() { + // if (game.nodes.length < 2) return; + // const idNum = game.nextId + 1; + // const id = `e-${idNum}`; + // const edge: SimEdge = { + // id, + // from: game.nodes[0].id, + // to: game.nodes[1].id, + // resource: 'wood', + // rate: 1, + // priority: 0, + // }; + // setGame((prev) => ({ ...prev, nextId: idNum, edges: [...prev.edges, edge] })); + // setSelected({ kind: 'edge', id }); + // } + + // function handleRemoveEdge(id: string) { + // setGame((prev) => ({ ...prev, edges: prev.edges.filter((e) => e.id !== id) })); + // setSelected((s) => (s && s.id === id ? null : s)); + // } + + // function handleUpdateNode(id: string, patch: Partial) { + // setGame((prev) => ({ + // ...prev, + // nodes: prev.nodes.map((n) => (n.id === id ? { ...n, ...patch } : n)), + // })); + // } + + // function handleUpdateNodeBehavior(id: string, behavior: Behavior) { + // handleUpdateNode(id, { behavior }); + // } + + // function handleUpdateEdge(id: string, patch: Partial) { + // setGame((prev) => ({ + // ...prev, + // edges: prev.edges.map((e) => (e.id === id ? { ...e, ...patch } : e)), + // })); + // } + + // function handleDragNode(id: string, x: number, y: number) { + // handleUpdateNode(id, { x, y }); + // } + + return ( +
+ + +
+ setSelected({ kind: 'node', id })} + /> + {/* setSelected({ kind: 'node', id })} + onSelectEdge={(id) => setSelected({ kind: 'edge', id })} + onAddNode={handleAddNode} + onAddEdge={handleAddEdge} + onRemoveNode={handleRemoveNode} + onRemoveEdge={handleRemoveEdge} + onUpdateNode={handleUpdateNode} + onUpdateNodeBehavior={handleUpdateNodeBehavior} + onUpdateEdge={handleUpdateEdge} + /> */} +
+
+ ); +} diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx new file mode 100644 index 0000000..eb1ccdd --- /dev/null +++ b/src/components/GameCanvas.tsx @@ -0,0 +1,97 @@ +import type { Selection } from '../App'; +import NodeCard from './NodeCard'; +import { Game } from '../types'; +import { build2dNodes, Node2D } from '../ui/graph'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { NodeLine } from './NodeLine'; + +interface Props { + game: Game; + selected: Selection; + onSelectNode: (id: string) => void; +} + +export const GameCanvas = function ({ + game, + selected, + onSelectNode, +}: Props) { + + const canvasRef = useRef(null); + const nodeElsRef = useRef>(new Map()); + const [size, setSize] = useState({ width: 0, height: 0 }); + const [centers, setCenters] = useState>({}); + + useEffect(() => { + const el = canvasRef.current; + if (!el) return; + const observer = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + setSize({ width, height }); + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const nodes2d = useMemo( + () => build2dNodes(game.nodes, { width: size.width, height: size.height, padding: 40 }), + [game.nodes, size.width, size.height], + ); + + useLayoutEffect(() => { + const worldEl = canvasRef.current; + if (!worldEl) return; + const worldRect = worldEl.getBoundingClientRect(); + const next: Record = {}; + for (const node2d of nodes2d) { + const el = nodeElsRef.current.get(node2d.id); + if (!el) continue; + const rect = el.getBoundingClientRect(); + next[node2d.id] = { + x: rect.left - worldRect.left + rect.width / 2, + y: rect.top - worldRect.top + rect.height / 2, + }; + } + setCenters(next); + }, [nodes2d]); + + function withCenter(node2d: Node2D): Node2D { + const center = centers[node2d.id]; + return center ? { ...node2d, position: center } : node2d; + } + + return ( +
+
+ { + nodes2d.map((node2d) => + node2d.node.behaviour.getDisplayInfo().links.map((link, idx) => { + const target = nodes2d.find((n2d) => n2d.id === link.target); + return target && ( + + ); + })) + } + { + nodes2d.map((node2d) => ( + { + if (el) nodeElsRef.current.set(node2d.id, el); + else nodeElsRef.current.delete(node2d.id); + }} + game={game} + key={node2d.id} + node2d={node2d} + selected={selected?.kind === 'node' && selected.id === node2d.id} + onSelect={() => onSelectNode(node2d.id)} + /> + ))} +
+
+ ); +} diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx new file mode 100644 index 0000000..2c381c7 --- /dev/null +++ b/src/components/NodeCard.tsx @@ -0,0 +1,51 @@ +import { forwardRef } from 'react'; +import { Game, Resource } from '../types'; +import { Node2D } from '../ui/graph'; +import { ResourceDescriptions } from '../ui/resource'; + +interface Props { + game: Game; + node2d: Node2D; + selected: boolean; + onSelect: () => void; +} + +const NodeCard = forwardRef(function NodeCard({ node2d, selected, onSelect }, ref) { + const node = node2d.node; + function handleClick() { + onSelect(); + } + + const info = node.behaviour.getDisplayInfo(); + + return ( +
+
{node.id}
+
{node.name}
+
{node.behaviour.kind}
+ { + Object.entries(info.storages).map(([name, storage]) => ( +
+
{name}
+ { + Object.entries(storage) + .map(([rt, v]) => `${ResourceDescriptions[rt as Resource].icon}${v}`).join(' ') + } +
+ )) + } +
+ { + info.links.map((link) =>
{link.target}/{link.resource}
) + } +
+
+ ); +}); + +export default NodeCard; diff --git a/src/components/NodeLine.tsx b/src/components/NodeLine.tsx new file mode 100644 index 0000000..01e56b6 --- /dev/null +++ b/src/components/NodeLine.tsx @@ -0,0 +1,39 @@ +import { Game } from "../types"; +import { Node2D } from "../ui/graph"; + +type Props = { + game: Game, + from: Node2D, + to: Node2D, +} + +export const NodeLine = function ({ from, to }: Props) { + const markerId = `node-line-arrow-${from.id}-${to.id}`.replace(/[^a-zA-Z0-9_-]/g, '_'); + + return ( + + + + + + + + + ); +}; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..016ebf0 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,111 @@ +import type { ChangeEvent } from 'react'; +import type { Topology, SimNode, SimEdge, NodeKind, Behavior } from '../engine'; +import type { Selection } from '../App'; +import NodeEditor from './NodeEditor'; +import EdgeEditor from './EdgeEditor'; +import { Game, SerializedNodeBehaviour } from '../types'; + +const BEHAVIOR_DEFAULTS: SerializedNodeBehaviour[] = [ + { + kind: 'converter', + inputs: [], + outputs: [], + }, +]; + +interface Props { + game: Game; + selected: Selection; + onSelectNode: (id: string) => void; + onSelectEdge: (id: string) => void; + onAddNode: (kind: NodeKind) => void; + onAddEdge: () => void; + onRemoveNode: (id: string) => void; + onRemoveEdge: (id: string) => void; + onUpdateNode: (id: string, patch: Partial) => void; + onUpdateNodeBehavior: (id: string, behavior: Behavior) => void; + onUpdateEdge: (id: string, patch: Partial) => void; +} + +export default function Sidebar(props: Props) { + const { game: topology, selected } = props; + const selectedNode = selected?.kind === 'node' ? topology.nodes.find((n) => n.id === selected.id) : undefined; + const selectedEdge = selected?.kind === 'edge' ? topology.edges.find((e) => e.id === selected.id) : undefined; + + function handleAddNodeChange(e: ChangeEvent) { + const kind = e.target.value as NodeKind | ''; + if (kind) props.onAddNode(kind); + e.target.value = ''; + } + + return ( +
+
+

Узлы

+
    + {topology.nodes.map((n) => ( +
  • props.onSelectNode(n.id)} + > + {n.label} ({n.id}) +
  • + ))} +
+ +
+ +
+

Связи

+
    + {topology.edges.map((e) => ( +
  • props.onSelectEdge(e.id)} + > + {e.from} → {e.to} [{e.resource}] +
  • + ))} +
+ +
+ +
+

Редактор

+ +
+
+ ); +} diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx new file mode 100644 index 0000000..6d6b0f7 --- /dev/null +++ b/src/components/Topbar.tsx @@ -0,0 +1,30 @@ +interface Props { + time: number; + onTick: () => void; + onReset: () => void; + onInitialize: () => void; +} + +export default function Topbar({ time, onTick, onReset, onInitialize }: Props) { + return ( +
+

+ Граф ресурсов + детерминированная тиковая симуляция +

+
+ Время {time} +
+
+ + + +
+ ); +} diff --git a/src/engine.ts.none b/src/engine.ts.none new file mode 100644 index 0000000..96406cf --- /dev/null +++ b/src/engine.ts.none @@ -0,0 +1,242 @@ +// for Claude: ignore this file + +/** + * engine.ts — ядро симуляции. + * + * - Storage — пассивный буфер с ёмкостью, сам ничего не делает. + * - Converter — единственный активный узел: раз в `period` тиков атомарно + * списывает `inputs` и начисляет `outputs` в СВОИХ ЖЕ буферах. + * source / consumer — частные случаи converter с пустым списком + * входов или выходов соответственно. + * - Edge (транспорт) — переносит ресурс между буферами двух узлов. + * Порядок обработки рёбер фиксирован (priority desc, id asc) — + * разрешает конкуренцию при fan-in/fan-out детерминированно. + * + * Модуль не содержит DOM/React-зависимостей — чистые данные и функции, + * поэтому логику легко перенести на другой язык/платформу по этой же + * спецификации (см. обсуждение — Rust/C#/GDScript как нативные цели). + */ + +export type ResourceType = 'wood' | 'iron' | 'stone' | 'gold' | 'knowledge'; + +export interface ResourceMeta { + id: ResourceType; + label: string; + color: string; + icon: string; +} + +export const RESOURCES: ResourceMeta[] = [ + { id: 'wood', label: 'Дерево', color: '#b98249', icon: '🌲' }, + { id: 'iron', label: 'Железо', color: '#9aa7b0', icon: '⛓' }, + { id: 'stone', label: 'Камень', color: '#8a8a8a', icon: '🪨' }, + { id: 'gold', label: 'Золото', color: '#d9b64e', icon: '💰' }, + { id: 'knowledge', label: 'Знание', color: '#7d8bf0', icon: '📖' }, +]; + +export type NodeKind = 'source' | 'storage' | 'converter' | 'consumer'; + +export interface RecipeLine { + resource: ResourceType; + amount: number; +} + +export type Behavior = + | { kind: 'storage' } + | { kind: 'source'; resource: ResourceType; rate: number } + | { kind: 'consumer'; resource: ResourceType; rate: number } + | { + kind: 'converter'; + period: number; + timer: number; + inputs: RecipeLine[]; + outputs: RecipeLine[]; + }; + +export interface SimNode { + label: string; + x: number; + y: number; + actor: Actor; +} + +export type Actor = { + id: string; + behavior: Behavior; +} + +export interface SimEdge { + id: string; + from: string; + to: string; + resource: ResourceType; + rate: number; + priority: number; +} + +export interface Topology { + nextId: number; + nodes: SimNode[]; + edges: SimEdge[]; +} + +/** buffers[nodeId][resource] = amount */ +export type Buffers = Record>>; + +export const NODE_W = 150; +export const NODE_H = 74; + +export function kindLabel(kind: NodeKind): string { + return { source: 'Источник', storage: 'Склад', converter: 'Конвертер', consumer: 'Потребитель' }[kind]; +} + +/** Глубокая копия чисто-JSON-данных (topology/buffers сериализуемы, structuredClone не требуется). */ +export function cloneJSON(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function getBuf(buffers: Buffers, nodeId: string, r: ResourceType): number { + return buffers[nodeId]?.[r] ?? 0; +} + +function setBuf(buffers: Buffers, nodeId: string, r: ResourceType, v: number): void { + if (!buffers[nodeId]) buffers[nodeId] = {}; + buffers[nodeId][r] = v; +} + +function capOf(node: SimNode): number { + return node.capacity === null || node.capacity === undefined ? Infinity : node.capacity; +} + +/** + * Выполняет ровно один детерминированный тик (мутирует topology.*.timer и buffers). + * Порядок фаз фиксирован и не зависит от порядка вставки узлов/рёбер в массивах. + */ +export function tick(topology: Topology, buffers: Buffers): void { + const nodesById = new Map(topology.nodes.map((n) => [n.id, n])); + const sortedNodes = [...topology.nodes].sort((a, b) => a.id.localeCompare(b.id)); + + // Фаза 1: источники — генерация "из ничего" в собственный буфер. + for (const node of sortedNodes) { + if (node.behavior.kind === 'source') { + const { resource, rate } = node.behavior; + const cur = getBuf(buffers, node.id, resource); + setBuf(buffers, node.id, resource, Math.min(capOf(node), cur + rate)); + } + } + + // Фаза 2: конвертеры. Blocking: при нехватке сырья таймер не сбрасывается, + // попытка повторяется каждый следующий тик. + for (const node of sortedNodes) { + if (node.behavior.kind === 'converter') { + const b = node.behavior; + b.timer += 1; + if (b.timer >= b.period) { + const canRun = b.inputs.every((line) => getBuf(buffers, node.id, line.resource) >= line.amount); + if (canRun) { + for (const line of b.inputs) { + setBuf(buffers, node.id, line.resource, getBuf(buffers, node.id, line.resource) - line.amount); + } + for (const line of b.outputs) { + const next = getBuf(buffers, node.id, line.resource) + line.amount; + setBuf(buffers, node.id, line.resource, Math.min(capOf(node), next)); + } + b.timer = 0; + } + } + } + } + + // Фаза 3: транспорт по рёбрам, порядок (priority desc, id asc). + const sortedEdges = [...topology.edges].sort( + (a, b) => b.priority - a.priority || a.id.localeCompare(b.id) + ); + for (const edge of sortedEdges) { + const fromNode = nodesById.get(edge.from); + const toNode = nodesById.get(edge.to); + if (!fromNode || !toNode) continue; + const available = getBuf(buffers, edge.from, edge.resource); + const freeSpace = capOf(toNode) - getBuf(buffers, edge.to, edge.resource); + const amount = Math.max(0, Math.min(edge.rate, available, freeSpace)); + if (amount > 0) { + setBuf(buffers, edge.from, edge.resource, available - amount); + setBuf(buffers, edge.to, edge.resource, getBuf(buffers, edge.to, edge.resource) + amount); + } + } + + // Фаза 4: потребители — безвозвратное поглощение. + for (const node of sortedNodes) { + if (node.behavior.kind === 'consumer') { + const { resource, rate } = node.behavior; + const cur = getBuf(buffers, node.id, resource); + const amount = Math.min(cur, rate); + setBuf(buffers, node.id, resource, cur - amount); + } + } +} + +/** Обнуляет все буферы и таймеры конвертеров. Настройки узлов/связей не трогает. */ +export function resetBuffers(topology: Topology): Buffers { + const buffers: Buffers = {}; + for (const node of topology.nodes) { + buffers[node.id] = {}; + if (node.behavior.kind === 'converter') node.behavior.timer = 0; + } + return buffers; +} + +export function defaultTopology(): Topology { + return { + nextId: 100, + nodes: [ + { id: 'src-wood', label: 'Лесопилка', x: 40, y: 40, capacity: null, + behavior: { kind: 'source', resource: 'wood', rate: 5 } }, + { id: 'src-stone', label: 'Каменоломня', x: 40, y: 180, capacity: null, + behavior: { kind: 'source', resource: 'stone', rate: 3 } }, + { id: 'src-iron', label: 'Рудник', x: 40, y: 320, capacity: null, + behavior: { kind: 'source', resource: 'iron', rate: 2 } }, + + { id: 'store-wood', label: 'Склад дерева', x: 300, y: 40, capacity: 100, + behavior: { kind: 'storage' } }, + { id: 'store-stone', label: 'Склад камня', x: 300, y: 180, capacity: 100, + behavior: { kind: 'storage' } }, + { id: 'store-iron', label: 'Склад железа', x: 300, y: 320, capacity: 100, + behavior: { kind: 'storage' } }, + + { id: 'conv-craft', label: 'Мастерская', x: 560, y: 110, capacity: 50, + behavior: { kind: 'converter', period: 2, timer: 0, + inputs: [{ resource: 'wood', amount: 3 }, { resource: 'stone', amount: 2 }], + outputs: [{ resource: 'gold', amount: 1 }] } }, + { id: 'conv-forge', label: 'Кузница', x: 560, y: 320, capacity: 50, + behavior: { kind: 'converter', period: 2, timer: 0, + inputs: [{ resource: 'iron', amount: 2 }, { resource: 'wood', amount: 1 }], + outputs: [{ resource: 'gold', amount: 2 }] } }, + + { id: 'vault', label: 'Хранилище золота', x: 820, y: 210, capacity: 200, + behavior: { kind: 'storage' } }, + + { id: 'conv-research', label: 'Лаборатория', x: 1060, y: 210, capacity: 30, + behavior: { kind: 'converter', period: 3, timer: 0, + inputs: [{ resource: 'gold', amount: 5 }], + outputs: [{ resource: 'knowledge', amount: 1 }] } }, + { id: 'store-knowledge', label: 'Архив знаний', x: 1300, y: 210, capacity: null, + behavior: { kind: 'storage' } }, + ], + edges: [ + { id: 'e1', from: 'src-wood', to: 'store-wood', resource: 'wood', rate: 5, priority: 0 }, + { id: 'e2', from: 'src-stone', to: 'store-stone', resource: 'stone', rate: 3, priority: 0 }, + { id: 'e3', from: 'src-iron', to: 'store-iron', resource: 'iron', rate: 2, priority: 0 }, + + { id: 'e4', from: 'store-wood', to: 'conv-craft', resource: 'wood', rate: 3, priority: 1 }, + { id: 'e5', from: 'store-stone', to: 'conv-craft', resource: 'stone', rate: 2, priority: 1 }, + { id: 'e6', from: 'store-wood', to: 'conv-forge', resource: 'wood', rate: 1, priority: 0 }, + { id: 'e7', from: 'store-iron', to: 'conv-forge', resource: 'iron', rate: 2, priority: 1 }, + + { id: 'e8', from: 'conv-craft', to: 'vault', resource: 'gold', rate: 5, priority: 0 }, + { id: 'e9', from: 'conv-forge', to: 'vault', resource: 'gold', rate: 5, priority: 0 }, + + { id: 'e10', from: 'vault', to: 'conv-research', resource: 'gold', rate: 5, priority: 0 }, + { id: 'e11', from: 'conv-research', to: 'store-knowledge', resource: 'knowledge', rate: 5, priority: 0 }, + ], + }; +} diff --git a/src/engine/functions/index.ts b/src/engine/functions/index.ts new file mode 100644 index 0000000..ca4c4a0 --- /dev/null +++ b/src/engine/functions/index.ts @@ -0,0 +1,98 @@ +import { Resource, Resources, SerializedConverter, SerializedNodeBehaviour as SerializedBehaviour, SerializedNodeBehaviour, SerializedConverterOutput, SerializedConverterInput } from "../../types"; + +export type BehaviourResourcePin = { + resource: Resource; +} + +export type BehaviourLink = { + target: Behaviour['id'], + resource: Resource; +} + +export type BehaviourInput = BehaviourResourcePin & { + link: BehaviourLink; +} + +export type BehaviourInputs = { + pins: BehaviourInput[]; +} + +export type BehaviourOutput = BehaviourResourcePin & { +} + +export type BehaviourOutputs = { + pins: BehaviourOutput[]; +} + +export type BehaviourDisplayInfo = { + storages: Record; + links: BehaviourLink[]; +} + +export abstract class Behaviour { + public readonly id: string; + public readonly kind: string; + public readonly inputs: BehaviourInputs; + public readonly outputs: BehaviourOutputs; + + constructor(source: SerializedBehaviour) { + this.id = source.id; + this.kind = source.kind; + this.outputs = { + pins: source.outputs.pins.map((input) => ({ + resource: input.resource, + })), + } + this.inputs = { + pins: source.inputs.pins.map((input) => ({ + resource: input.resource, + link: input.link, + })), + } + } + + abstract getDisplayInfo(): BehaviourDisplayInfo; + + abstract tick(deltaTime: number): void; +} + +export type ResourceUsageRule = { + resource: Resource; + speed: number; // per tick +} + +export class Converter extends Behaviour { + public outputRules: ResourceUsageRule[] = []; + rules: { inputs: SerializedConverterInput[]; outputs: SerializedConverterOutput[]; }; + + constructor(source: SerializedNodeBehaviour & SerializedConverter) { + if (source.kind !== 'converter') + throw new Error('Invalid source'); + super(source); + + this.rules = { + inputs: source.inputs.pins, + outputs: source.outputs.pins, + }; + } + + public getDisplayInfo(): BehaviourDisplayInfo { + return { + storages: {}, + links: this.inputs.pins.map((input) => input.link), + } + } + + tick(deltaTime: number): void { + + } +} + +// export class Storage extends Behaviour { +// public static type: string = 'storage'; +// } + +export const behaviours = { + 'converter': Converter, + // 'storage': , +} satisfies Record; diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/saveLoad.ts b/src/engine/saveLoad.ts new file mode 100644 index 0000000..9f320b2 --- /dev/null +++ b/src/engine/saveLoad.ts @@ -0,0 +1,78 @@ +import { defaultGame } from "../prefabs/game"; +import { Game, SerializedGame, SerializedNodeBehaviour } from "../types"; +import { Behaviour, behaviours } from "./functions"; + +export function serializeBehaviour(behaviour: Behaviour): SerializedNodeBehaviour { + switch (behaviour.kind) { + case 'converter': + return { + id: behaviour.id, + kind: 'converter', + inputs: { + pins: behaviour.inputs.pins.map((pin) => ({ + ...pin, + rate: 11, + })), + }, + outputs: { + pins: behaviour.outputs.pins.map((pin) => ({ + ...pin, + rate: 11, + })), + }, + }; + default: + throw new Error('Unknown behaviour'); + } +} + +export function deserializeBehaviour(value: SerializedNodeBehaviour): Behaviour { + switch (value.kind) { + case 'converter': + return new (behaviours.converter)(value); + default: + throw new Error('Unknown behaviour'); + } +} + +export function serialize(game: Game): SerializedGame { + return { + time: game.time, + nodes: game.nodes.map((node) => ({ + id: node.id, + name: node.name, + behaviour: serializeBehaviour(node.behaviour), + })), + }; +} + +export function deserialize(value: SerializedGame): Game { + return new Game(value); +} + +export function serializeToString(game: Game): string { + return JSON.stringify(serialize(game)); +} + +export function deserializeFromString(value: string): Game { + return deserialize(JSON.parse(value)); +} + +const KEY_GAME = 'incgane'; + +export function save(game: Game) { + localStorage.setItem(KEY_GAME, serializeToString(game)); +} + +export function load(): Game { + try { + const raw = localStorage.getItem(KEY_GAME); + if (raw) + return deserializeFromString(raw); + } catch (e) { + console.warn('Не удалось прочитать игру из localStorage', e); + } + const fresh = deserialize(defaultGame()); + save(fresh); + return fresh; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..00b7ba7 --- /dev/null +++ b/src/index.css @@ -0,0 +1,354 @@ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap'); + +:root { + --bg: #201f1c; + --panel: #29271f; + --panel-2: #322f26; + --line: #4a4536; + --grid-line: rgba(196, 178, 138, 0.08); + --ink: #e9e2d2; + --ink-dim: #a89f8a; + --accent-source: #7fae6a; + --accent-storage: #a89f8a; + --accent-converter: #d9a441; + --accent-consumer: #c15a4e; + --edge-color: #8a8a8a; + --danger: #c15a4e; + --mono: 'IBM Plex Mono', ui-monospace, monospace; + --display: 'Space Grotesk', ui-sans-serif, sans-serif; +} + +* { box-sizing: border-box; } + +html, body, #root { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--ink); + font-family: var(--mono); +} + +#app { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* ---------------------------------------------------------- top bar --- */ + +.topbar { + display: flex; + align-items: center; + gap: 20px; + padding: 12px 20px; + background: var(--panel); + border-bottom: 1px solid var(--line); + flex-shrink: 0; +} + +.topbar__title { + font-family: var(--display); + font-weight: 700; + font-size: 15px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ink); + margin: 0; + border-right: 1px solid var(--line); + padding-right: 16px; +} + +.topbar__title span { + display: block; + font-family: var(--mono); + font-weight: 400; + font-size: 10px; + letter-spacing: 0.04em; + color: var(--ink-dim); + text-transform: none; + margin-top: 2px; +} + +.tick-counter { + font-family: var(--mono); + font-size: 12px; + color: var(--ink-dim); + background: var(--panel-2); + border: 1px solid var(--line); + padding: 6px 12px; + border-radius: 3px; +} +.tick-counter b { + color: var(--accent-converter); + font-size: 14px; +} + +.spacer { flex: 1; } + +.btn { + font-family: var(--mono); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.03em; + border: 1px solid var(--line); + background: var(--panel-2); + color: var(--ink); + padding: 9px 16px; + border-radius: 3px; + cursor: pointer; + transition: border-color 0.12s, transform 0.05s; +} +.btn:hover { border-color: var(--ink-dim); } +.btn:active { transform: translateY(1px); } +.btn:focus-visible { outline: 2px solid var(--accent-converter); outline-offset: 2px; } + +.btn--primary { + background: var(--accent-converter); + border-color: var(--accent-converter); + color: #201f1c; + font-weight: 600; +} +.btn--primary:hover { border-color: #f0c169; } + +.btn--danger { + background: transparent; + border-color: var(--danger); + color: var(--danger); + margin-top: 14px; + width: 100%; +} +.btn--danger:hover { background: rgba(193, 90, 78, 0.12); } + +.btn--ghost { + background: transparent; + border-style: dashed; + color: var(--ink-dim); + width: 100%; + margin-top: 4px; +} + +.btn--icon { + padding: 4px 8px; + font-size: 11px; +} + +select { + font-family: var(--mono); + font-size: 12px; + background: var(--panel-2); + color: var(--ink); + border: 1px solid var(--line); + border-radius: 3px; + padding: 8px 10px; +} + +/* --------------------------------------------------------------- main --- */ + +.main { + flex: 1; + display: flex; + min-height: 0; +} + +/* ------------------------------------------------------------- world --- */ + +.world-wrap { + flex: 1; + overflow: auto; + position: relative; + background-color: var(--bg); + background-image: + linear-gradient(var(--grid-line) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); + background-size: 24px 24px; +} + +#world { + position: relative; + width: 1600px; + height: 900px; +} + +#world > svg { + position: absolute; + top: 0; left: 0; + width: 1600px; + height: 900px; + pointer-events: none; +} +#world > svg line { pointer-events: stroke; } + +.node { + position: absolute; + background: var(--panel); + border: 1px solid var(--line); + border-left: 4px solid var(--accent-storage); + border-radius: 4px; + padding: 8px 10px; + cursor: grab; + user-select: none; + box-shadow: 0 2px 6px rgba(0,0,0,0.35); + touch-action: none; +} +.node:active { cursor: grabbing; } +.node.is-selected { + border-left-width: 4px; + outline: 1.5px dashed var(--ink); + outline-offset: 3px; +} + +.node--source { border-left-color: var(--accent-source); } +.node--storage { border-left-color: var(--accent-storage); } +.node--converter { border-left-color: var(--accent-converter); } +.node--consumer { border-left-color: var(--accent-consumer); } + +.node__title { + font-family: var(--display); + font-weight: 600; + font-size: 12.5px; + color: var(--ink); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.node__kind { + font-size: 9px; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--ink-dim); + margin: 2px 0 6px; +} + +.node__resources { + font-size: 11px; + background: #171613; + border: 1px solid var(--line); + border-radius: 3px; + padding: 4px 6px; + letter-spacing: 0.02em; + white-space: nowrap; +} + +/* ------------------------------------------------------------ sidebar --- */ + +.sidebar { + width: 340px; + flex-shrink: 0; + background: var(--panel); + border-left: 1px solid var(--line); + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.sidebar__section { + padding: 14px 16px; + border-bottom: 1px solid var(--line); +} +.sidebar__section--grow { + flex: 1; + border-bottom: none; +} + +.sidebar__section h4 { + font-family: var(--display); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-dim); + margin: 0 0 8px; +} + +.sidebar__list { + list-style: none; + margin: 0; + padding: 0; + max-height: 140px; + overflow-y: auto; +} +.sidebar__list li { + padding: 6px 8px; + font-size: 11.5px; + border-radius: 3px; + cursor: pointer; + color: var(--ink-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sidebar__list li:hover { background: var(--panel-2); color: var(--ink); } +.sidebar__list li.is-active { background: var(--panel-2); color: var(--accent-converter); } + +#sidebar-content { + padding: 16px 0 0; + flex: 1; + overflow-y: auto; +} + +#sidebar-content h3 { + font-family: var(--display); + font-size: 13px; + color: var(--ink); + margin: 0 0 14px; + word-break: break-all; +} + +.hint { + color: var(--ink-dim); + font-size: 11.5px; + line-height: 1.5; +} + +.field { + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 12px; + font-size: 11px; + color: var(--ink-dim); +} +.field input, .field select { + font-family: var(--mono); + font-size: 12px; + background: var(--panel-2); + color: var(--ink); + border: 1px solid var(--line); + border-radius: 3px; + padding: 7px 9px; + width: 100%; +} +.field input:focus-visible, .field select:focus-visible { + outline: 2px solid var(--accent-converter); + outline-offset: 1px; +} + +.coord-row { display: flex; gap: 8px; } +.coord-row input { width: 50%; } + +.recipe-box { + border: 1px solid var(--line); + border-radius: 4px; + padding: 10px; + margin-bottom: 12px; + background: var(--panel-2); +} +.recipe-box__title { + font-size: 10.5px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ink-dim); + margin-bottom: 8px; +} +.recipe-row { + display: flex; + gap: 6px; + margin-bottom: 6px; + align-items: center; +} +.recipe-row select { flex: 1.4; } +.recipe-row input { flex: 1; } + +::-webkit-scrollbar { width: 9px; height: 9px; } +::-webkit-scrollbar-track { background: var(--bg); } +::-webkit-scrollbar-thumb { background: var(--line); border-radius: 5px; } diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..a2bf01b --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +); diff --git a/src/prefabs/game.ts b/src/prefabs/game.ts new file mode 100644 index 0000000..77be7c0 --- /dev/null +++ b/src/prefabs/game.ts @@ -0,0 +1,71 @@ +import { SerializedGame } from "../types"; + +export const defaultGame: () => SerializedGame = () => ({ + time: 0, + nodes: [ + { + id: 'forest', + name: 'Forest', + behaviour: { + id: 'forest.1', + kind: 'converter', + inputs: { + pins: [], + }, + outputs: { + pins: [ + { + resource: 'wood', + rate: 1, + }, + ], + }, + }, + }, + { + id: 'sawmill', + name: 'Sawmill', + behaviour: { + id: 'sawmill.1', + kind: 'converter', + inputs: { + pins: [ + { + resource: 'wood', + rate: 0.5, + link: { target: 'forest.1', resource: 'wood' }, + }, + ] + }, + outputs: { + pins: [ + { + resource: 'plank', + rate: 1, + }, + ], + }, + }, + }, + { + id: 'burner', + name: 'Burner', + behaviour: { + id: 'burner.1', + kind: 'converter', + inputs: { + pins: [ + { + resource: 'plank', + rate: 1, + link: { target: 'sawmill.1', resource: 'plank' }, + }, + ], + }, + outputs: { + pins: [], + }, + }, + }, + ], +}); diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..eb7129c --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,47 @@ +import type { Topology, Buffers } from './engine'; +import { defaultTopology, resetBuffers } from './engine'; + +const KEY_TOPOLOGY = 'incsim.topology.v1'; +const KEY_BUFFERS = 'incsim.buffers.v1'; +const KEY_TICKS = 'incsim.tickcount.v1'; + +export function loadTopology(): Topology { + try { + const raw = localStorage.getItem(KEY_TOPOLOGY); + if (raw) return JSON.parse(raw) as Topology; + } catch (e) { + console.warn('Не удалось прочитать игру из localStorage', e); + } + const fresh = defaultTopology(); + saveTopology(fresh); + return fresh; +} + +export function saveTopology(topology: Topology): void { + localStorage.setItem(KEY_TOPOLOGY, JSON.stringify(topology)); +} + +export function loadBuffers(topology: Topology): Buffers { + try { + const raw = localStorage.getItem(KEY_BUFFERS); + if (raw) return JSON.parse(raw) as Buffers; + } catch (e) { + console.warn('Не удалось прочитать буферы из localStorage', e); + } + const fresh = resetBuffers(topology); + saveBuffers(fresh); + return fresh; +} + +export function saveBuffers(buffers: Buffers): void { + localStorage.setItem(KEY_BUFFERS, JSON.stringify(buffers)); +} + +export function loadTickCount(): number { + const raw = localStorage.getItem(KEY_TICKS); + return raw ? Number(raw) || 0 : 0; +} + +export function saveTickCount(n: number): void { + localStorage.setItem(KEY_TICKS, String(n)); +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..44da47c --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,79 @@ +import { Behaviour } from "../engine/functions"; +import { deserializeBehaviour } from "../engine/saveLoad"; + +export type Resource = 'wood' | 'plank'; + +export type Resources = Record; + +export type Node = { + id: string; + name: string; + behaviour: Behaviour; +} + +export class Game { + public time: number; + public readonly nodes: Node[]; + + constructor(source: SerializedGame) { + this.time = source.time; + this.nodes = source.nodes.map((node) => ({ + id: node.id, + name: node.name, + behaviour: deserializeBehaviour(node.behaviour), + })); + } + + public tick(deltaTime: number) { + this.time += deltaTime; + for (const node of this.nodes) + node.behaviour.tick(deltaTime); + } +} + +export type SerializedConverterInput = { + resource: Resource; + rate: number; + link: { + target: SerializedNodeBehaviour['id'], + resource: Resource, + } +} + +export type SerializedConverterOutput = { + resource: Resource; + rate: number; +} + +export type SerializedConverterInputs = { + pins: SerializedConverterInput[]; +} + +export type SerializedConverterOutputs = { + pins: SerializedConverterOutput[]; +} + +export type SerializedConverter = { + kind: 'converter'; +} + +export type SerializedStorage = { + kind: 'storage', +} + +export type SerializedNodeBehaviour = { + id: string; + inputs: SerializedConverterInputs; + outputs: SerializedConverterOutputs; +} & (SerializedConverter | SerializedStorage); + +export type SerializedNode = { + id: Node['id'], + name: Node['name'], + behaviour: SerializedNodeBehaviour, +} + +export type SerializedGame = { + time: number; + nodes: SerializedNode[]; +} \ No newline at end of file diff --git a/src/ui/graph.ts b/src/ui/graph.ts new file mode 100644 index 0000000..d1815b9 --- /dev/null +++ b/src/ui/graph.ts @@ -0,0 +1,79 @@ +import createGraph, { Graph } from "ngraph.graph"; +import createLayout from "ngraph.forcelayout"; +import { Node } from "../types"; + +const ITERATION_COUNT = 10; + +export type Node2D = { + id: Node['id']; + position: { x: number, y: number }; + node: Node; +} + +export function build2dNodes(nodes: Node[], area: { width: number, height: number, padding: number }): Node2D[] { + const graph = createGraph(); + for (const node of nodes) { + graph.addNode(node.behaviour.id, { node }); + } + for (const node of nodes) { + for (const link of node.behaviour.getDisplayInfo().links) + graph.addLink(node.behaviour.id, link.target); + } + return buildGraph2dNodes(graph, area); +} + +function buildGraph2dNodes(graph: Graph, area: { width: number, height: number, padding: number }): Node2D[] { + const layout = createLayout( + graph, + { + springLength: 300, + // springTransform: (link, spring) => { + // const from = graph.getNode(link.fromId).data.radius; + // const to = graph.getNode(link.toId).data.radius; + // spring.length = from + to + 10; // отступ 10px + // }, + }, + ); + + function clampToBounds() { + const minX = area.padding; + const maxX = area.width - area.padding; + const minY = area.padding; + const maxY = area.height - area.padding; + layout.forEachBody((body) => { + if (body.pos.x < minX) { body.pos.x = minX; body.velocity.x = 0; } + if (body.pos.x > maxX) { body.pos.x = maxX; body.velocity.x = 0; } + if (body.pos.y < minY) { body.pos.y = minY; body.velocity.y = 0; } + if (body.pos.y > maxY) { body.pos.y = maxY; body.velocity.y = 0; } + }); + } + + for (var i = 0; i < ITERATION_COUNT; ++i) { + layout.step(); + // clampToBounds(); + } + + // const rect = layout.getGraphRect(); + // const W = area.width, H = 600, PADDING = 40; + + // const scaleX = (area.width - area.padding * 2) / (rect.x2 - rect.x1); + // const scaleY = (area.height - area.padding * 2) / (rect.y2 - rect.y1); + // const scale = Math.min(scaleX, scaleY); // сохраняем пропорции + + // function toCanvas(pos: { x: number; y: number }) { + // return { + // x: area.padding + (pos.x - rect.x1) * scale, + // y: area.padding + (pos.y - rect.y1) * scale, + // }; + // } + + const nodes: Node2D[] = []; + layout.forEachBody((body, nodeId) => { + nodes.push({ + id: nodeId.toString(), + position: { x: body.pos.x + area.width / 2, y: body.pos.y + area.height / 2 }, + node: graph.getNode(nodeId)?.data.node, + }); + }); + return nodes; +} \ No newline at end of file diff --git a/src/ui/node.ts b/src/ui/node.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/resource.ts b/src/ui/resource.ts new file mode 100644 index 0000000..ea8feb4 --- /dev/null +++ b/src/ui/resource.ts @@ -0,0 +1,17 @@ +import { Resource } from "../types"; + +export type ResourceDescription = { + id: Resource; + label: string; + color: string; + icon: string; +} + +export const ResourceDescriptions: Record = { + wood: { id: 'wood', label: 'Дерево', color: '#b98249', icon: '🌲' }, + plank: { id: 'plank', label: 'Доска', color: '#c9c209', icon: 'P' }, + // iron: { id: 'iron', label: 'Железо', color: '#9aa7b0', icon: '⛓' }, + // stone: { id: 'stone', label: 'Камень', color: '#8a8a8a', icon: '🪨' }, + // gold: { id: 'gold', label: 'Золото', color: '#d9b64e', icon: '💰' }, + // knowledge: { id: 'knowledge', label: 'Знание', color: '#7d8bf0', icon: '📖' }, +}; \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..0466183 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +});