diff --git a/.gitignore b/.gitignore index 4733b025..8f22aa69 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,15 @@ dist /data/assets/ +# Maintainer-only game-data exports for the node importer (FModel +# .umap dumps, persistent-level JSON, etc.). These can be hundreds +# of MB and are regenerated from the game. +/data/FactoryGame/ +/data/Persistent_Level.json + +# Generated map tile pyramid (uploaded to DigitalOcean Spaces, not shipped in build) +/dist-map-tiles/ + # intellij idea .idea diff --git a/CLAUDE.md b/CLAUDE.md index f9e03c8f..9d17dbb1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,25 @@ Savegame files (`.sav`) are parsed using `@etothepii/satisfactory-file-parser` i ## Code Conventions +### Definition of Done (for agents and humans) + +Before declaring any code change complete (and before reporting "done" to the user), run **both** of these on the touched files / project: + +```bash +npm run lint # Biome (formatting + linting) +npm run check-types # tsc --noEmit +``` + +If either fails: + +1. Fix the issues (use `npm run format` or `npx biome check --write ` for autofixable formatting). +2. Re-run both commands until clean. +3. Only then summarise the change to the user. + +For a focused check on just the files you touched, scope biome explicitly: `npx biome check --write `. Do **not** rely solely on a focused check — a final `npm run lint && npm run check-types` is the contract. + +If tests cover the touched area, also run `npm test -- --run`. + ### Writing Style - **Do not use em dashes (`—`)** in code comments, UI copy, notification messages, commit messages, or any text that ships to users. Prefer commas, parentheses, colons, or separate sentences. Applies to both source code and generated text. diff --git a/README.md b/README.md index a2b12348..7546046f 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,34 @@ npm run parse-docs - Copy the exported `FactoryGame` folder to `data/assets/` (FactoryGame should be a subfolder of `data/assets/`) - Run the `npm run parse-docs -- --with-images` command to generate the images +### Resource Node Data (Map) + +The map view in `src/map/` ships with a curated copy of every resource node, deposit, fracking core/satellite, and geyser placement at `src/recipes/WorldResourceNodes.json`. The list is rebuilt from the game's persistent level whenever the game updates. + +To regenerate after a Satisfactory update: + +1. Load the game in [FModel](https://fmodel.app) (same setup as Image Generation above). +2. Navigate to `FactoryGame/Content/FactoryGame/Map/GameLevel01/Persistent_Level.umap`. +3. Right-click → **Save Properties (.json)**. +4. Move the resulting `Persistent_Level.json` (~100MB) to `data/Persistent_Level.json` in this repo. The file is gitignored so it stays out of commits. +5. Run: + + ```bash + npm run extract-world-nodes + ``` + + The script does a two-pass walk over the export: first to collect every `BP_ResourceNode_C` / `BP_ResourceDeposit_C` / `BP_FrackingCore_C` / `BP_FrackingSatellite_C` / `BP_ResourceNodeGeyser_C` actor with its resource and purity, then to bind each one's `RootComponent` transform for world coordinates. The output is sorted deterministically and a diff vs. the previous bundled file is printed. + + Useful flags: + + - `--dry-run` — parse and report without writing. + - `--input ` / `--output ` — override defaults. + - `--verbose` — log every emitted node (debug). + +6. Review the diff in the script's summary output, then commit `src/recipes/WorldResourceNodes.json`. + +> **Heads up:** the parser holds the entire 100MB JSON in memory. If you see out-of-memory errors, prefix the command with `NODE_OPTIONS="--max-old-space-size=4096"`. + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/package-lock.json b/package-lock.json index 6069d7bf..002f6e9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,21 @@ { "name": "satisfactory-logistics", - "version": "0.11.0", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "satisfactory-logistics", - "version": "0.11.0", + "version": "0.12.0", "dependencies": { "@caldwell619/react-kanban": "^0.0.11", "@clickbar/dot-diver": "^1.0.7", "@dagrejs/dagre": "^1.1.4", - "@etothepii/satisfactory-file-parser": "^3.3.1", + "@etothepii/satisfactory-file-parser": "^4.0.1", "@hello-pangea/dnd": "^18.0.0", "@mantine/colors-generator": "^9.0.0", "@mantine/core": "^9.0.0", + "@mantine/dropzone": "^9.0.2", "@mantine/hooks": "^9.0.0", "@mantine/modals": "^9.0.0", "@mantine/notifications": "^9.0.0", @@ -22,6 +23,8 @@ "@nivo/sankey": "^0.99.0", "@sentry/react": "^8.37.1", "@sentry/vite-plugin": "^5.2.0", + "@streamparser/json": "^0.0.22", + "@streamparser/json-whatwg": "^0.0.22", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", "@supabase/sentry-js-integration": "^0.3.0", @@ -50,6 +53,7 @@ "idb-keyval": "^6.2.2", "immer": "^10.2.0", "js-base64": "^3.7.8", + "leaflet": "^1.9.4", "lodash": "^4.18.1", "loglevel": "^1.9.2", "loglevel-plugin-prefix": "^0.8.4", @@ -59,6 +63,7 @@ "pako": "^2.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-leaflet": "^5.0.0", "react-rnd": "^10.5.3", "react-router-dom": "^6.28.0", "short-uuid": "^5.2.0", @@ -71,6 +76,7 @@ "@types/base-64": "^1.0.2", "@types/canvas-confetti": "^1.9.0", "@types/chroma-js": "^2.4.4", + "@types/leaflet": "^1.9.21", "@types/lodash": "^4.17.24", "@types/node": "^25.5.0", "@types/pako": "^2.0.4", @@ -2300,9 +2306,9 @@ } }, "node_modules/@etothepii/satisfactory-file-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@etothepii/satisfactory-file-parser/-/satisfactory-file-parser-3.3.1.tgz", - "integrity": "sha512-U4msn1zAaVWuxAElBOpNwvSS+PpiKyAGgQaBVkSnras2YJFzBN76nioeStlcCS95QiZMpC5S/t6spB/uSX9ICg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@etothepii/satisfactory-file-parser/-/satisfactory-file-parser-4.0.1.tgz", + "integrity": "sha512-8UN5ZKcGOxNPrnR50i5xi2n/zDVUrmT8jkrcAM6qzWwSQ2I9gRYoaQUXDKDU9jm8ZhxaiprUL1q4Py3sXYQ+bg==", "license": "MIT", "dependencies": { "pako": "^2.1.0" @@ -2851,6 +2857,21 @@ "react-dom": "^19.2.0" } }, + "node_modules/@mantine/dropzone": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-9.0.2.tgz", + "integrity": "sha512-Hf7TmI/RgtnwxztiPRI4xO+rVH7cb4eV1U9nnA+9Z209pEATEhZKasutsqZzfWZxSY5m+Dv+I4VZLtmLkXIl2A==", + "license": "MIT", + "dependencies": { + "react-dropzone": "15.0.0" + }, + "peerDependencies": { + "@mantine/core": "9.0.2", + "@mantine/hooks": "9.0.2", + "react": "^19.2.0", + "react-dom": "^19.2.0" + } + }, "node_modules/@mantine/hooks": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.0.2.tgz", @@ -3815,6 +3836,17 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@react-spring/animated": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz", @@ -4687,6 +4719,21 @@ "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", "license": "MIT" }, + "node_modules/@streamparser/json": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.22.tgz", + "integrity": "sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==", + "license": "MIT" + }, + "node_modules/@streamparser/json-whatwg": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@streamparser/json-whatwg/-/json-whatwg-0.0.22.tgz", + "integrity": "sha512-qRYYbaytZYXfGMFNtPAUrbK2CJ0KmGk2l+uLxW1hKVLOt3aBaWGsskiUCdGoaoSTeJBHswNqi59wSppg+IVWKQ==", + "license": "MIT", + "dependencies": { + "@streamparser/json": "^0.0.22" + } + }, "node_modules/@supabase/auth-js": { "version": "2.103.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.103.0.tgz", @@ -5537,6 +5584,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -6018,6 +6082,15 @@ "node": ">= 4.0.0" } }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -7283,6 +7356,18 @@ ], "license": "BSD-3-Clause" }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -8483,6 +8568,12 @@ "node": ">=0.10.0" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/less": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz", @@ -9980,12 +10071,43 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-dropzone": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", + "integrity": "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-number-format": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz", diff --git a/package.json b/package.json index 17c95f0d..51bbb861 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "satisfactory-logistics", "private": true, - "version": "0.11.0", + "version": "0.12.0", "type": "module", "scripts": { "dev": "vite", @@ -13,16 +13,20 @@ "preview": "vite preview", "supabase:types": "npx supabase gen types typescript --project-id nymrtujjmzbhxcimjsci > ./src/core/database.types.ts", "parse-docs": "tsx scripts/parseDocs.ts", - "generate-pwa-icons": "tsx scripts/generatePwaIcons.ts" + "extract-world-nodes": "tsx scripts/extractWorldNodes.ts", + "generate-pwa-icons": "tsx scripts/generatePwaIcons.ts", + "generate-map-tiles": "tsx scripts/generateMapTiles.ts", + "dump-save": "tsx scripts/dumpSavegame.ts" }, "dependencies": { "@caldwell619/react-kanban": "^0.0.11", "@clickbar/dot-diver": "^1.0.7", "@dagrejs/dagre": "^1.1.4", - "@etothepii/satisfactory-file-parser": "^3.3.1", + "@etothepii/satisfactory-file-parser": "^4.0.1", "@hello-pangea/dnd": "^18.0.0", "@mantine/colors-generator": "^9.0.0", "@mantine/core": "^9.0.0", + "@mantine/dropzone": "^9.0.2", "@mantine/hooks": "^9.0.0", "@mantine/modals": "^9.0.0", "@mantine/notifications": "^9.0.0", @@ -30,6 +34,8 @@ "@nivo/sankey": "^0.99.0", "@sentry/react": "^8.37.1", "@sentry/vite-plugin": "^5.2.0", + "@streamparser/json": "^0.0.22", + "@streamparser/json-whatwg": "^0.0.22", "@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-shared": "^0.1.8", "@supabase/sentry-js-integration": "^0.3.0", @@ -58,6 +64,7 @@ "idb-keyval": "^6.2.2", "immer": "^10.2.0", "js-base64": "^3.7.8", + "leaflet": "^1.9.4", "lodash": "^4.18.1", "loglevel": "^1.9.2", "loglevel-plugin-prefix": "^0.8.4", @@ -67,6 +74,7 @@ "pako": "^2.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-leaflet": "^5.0.0", "react-rnd": "^10.5.3", "react-router-dom": "^6.28.0", "short-uuid": "^5.2.0", @@ -79,6 +87,7 @@ "@types/base-64": "^1.0.2", "@types/canvas-confetti": "^1.9.0", "@types/chroma-js": "^2.4.4", + "@types/leaflet": "^1.9.21", "@types/lodash": "^4.17.24", "@types/node": "^25.5.0", "@types/pako": "^2.0.4", diff --git a/public/images/game/alien-power-augmenter_256.png b/public/images/game/alien-power-augmenter_256.png new file mode 100644 index 00000000..bbcfccc7 Binary files /dev/null and b/public/images/game/alien-power-augmenter_256.png differ diff --git a/public/images/game/alien-power-augmenter_64.png b/public/images/game/alien-power-augmenter_64.png new file mode 100644 index 00000000..48aeea39 Binary files /dev/null and b/public/images/game/alien-power-augmenter_64.png differ diff --git a/public/images/game/block-signal_256.png b/public/images/game/block-signal_256.png new file mode 100644 index 00000000..5a59ceac Binary files /dev/null and b/public/images/game/block-signal_256.png differ diff --git a/public/images/game/block-signal_64.png b/public/images/game/block-signal_64.png new file mode 100644 index 00000000..fc12a58e Binary files /dev/null and b/public/images/game/block-signal_64.png differ diff --git a/public/images/game/blueprint-designer-mk-2_256.png b/public/images/game/blueprint-designer-mk-2_256.png new file mode 100644 index 00000000..43afdfa6 Binary files /dev/null and b/public/images/game/blueprint-designer-mk-2_256.png differ diff --git a/public/images/game/blueprint-designer-mk-2_64.png b/public/images/game/blueprint-designer-mk-2_64.png new file mode 100644 index 00000000..0073fe7a Binary files /dev/null and b/public/images/game/blueprint-designer-mk-2_64.png differ diff --git a/public/images/game/blueprint-designer-mk-3_256.png b/public/images/game/blueprint-designer-mk-3_256.png new file mode 100644 index 00000000..f1919bc2 Binary files /dev/null and b/public/images/game/blueprint-designer-mk-3_256.png differ diff --git a/public/images/game/blueprint-designer-mk-3_64.png b/public/images/game/blueprint-designer-mk-3_64.png new file mode 100644 index 00000000..f2d9075a Binary files /dev/null and b/public/images/game/blueprint-designer-mk-3_64.png differ diff --git a/public/images/game/blueprint-designer_256.png b/public/images/game/blueprint-designer_256.png new file mode 100644 index 00000000..ef90d381 Binary files /dev/null and b/public/images/game/blueprint-designer_256.png differ diff --git a/public/images/game/blueprint-designer_64.png b/public/images/game/blueprint-designer_64.png new file mode 100644 index 00000000..d0cfc86f Binary files /dev/null and b/public/images/game/blueprint-designer_64.png differ diff --git a/public/images/game/buffer-stop_256.png b/public/images/game/buffer-stop_256.png new file mode 100644 index 00000000..79ed84b3 Binary files /dev/null and b/public/images/game/buffer-stop_256.png differ diff --git a/public/images/game/buffer-stop_64.png b/public/images/game/buffer-stop_64.png new file mode 100644 index 00000000..ce9608a8 Binary files /dev/null and b/public/images/game/buffer-stop_64.png differ diff --git a/public/images/game/cane-equipment_256.png b/public/images/game/cane-equipment_256.png new file mode 100644 index 00000000..9ddc6ad2 Binary files /dev/null and b/public/images/game/cane-equipment_256.png differ diff --git a/public/images/game/cane-equipment_64.png b/public/images/game/cane-equipment_64.png new file mode 100644 index 00000000..f1e82a0a Binary files /dev/null and b/public/images/game/cane-equipment_64.png differ diff --git a/public/images/game/chainsaw_256.png b/public/images/game/chainsaw_256.png new file mode 100644 index 00000000..8b9035a1 Binary files /dev/null and b/public/images/game/chainsaw_256.png differ diff --git a/public/images/game/chainsaw_64.png b/public/images/game/chainsaw_64.png new file mode 100644 index 00000000..ea41cd2d Binary files /dev/null and b/public/images/game/chainsaw_64.png differ diff --git a/public/images/game/christmas-tree_256.png b/public/images/game/christmas-tree_256.png new file mode 100644 index 00000000..520fcbd5 Binary files /dev/null and b/public/images/game/christmas-tree_256.png differ diff --git a/public/images/game/christmas-tree_64.png b/public/images/game/christmas-tree_64.png new file mode 100644 index 00000000..651a6f12 Binary files /dev/null and b/public/images/game/christmas-tree_64.png differ diff --git a/public/images/game/conveyor-lift-mk-1_256.png b/public/images/game/conveyor-lift-mk-1_256.png new file mode 100644 index 00000000..3a2fcc25 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-1_256.png differ diff --git a/public/images/game/conveyor-lift-mk-1_64.png b/public/images/game/conveyor-lift-mk-1_64.png new file mode 100644 index 00000000..0de516a4 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-1_64.png differ diff --git a/public/images/game/conveyor-lift-mk-2_256.png b/public/images/game/conveyor-lift-mk-2_256.png new file mode 100644 index 00000000..54111f16 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-2_256.png differ diff --git a/public/images/game/conveyor-lift-mk-2_64.png b/public/images/game/conveyor-lift-mk-2_64.png new file mode 100644 index 00000000..c1d0810f Binary files /dev/null and b/public/images/game/conveyor-lift-mk-2_64.png differ diff --git a/public/images/game/conveyor-lift-mk-3_256.png b/public/images/game/conveyor-lift-mk-3_256.png new file mode 100644 index 00000000..3ea1b27f Binary files /dev/null and b/public/images/game/conveyor-lift-mk-3_256.png differ diff --git a/public/images/game/conveyor-lift-mk-3_64.png b/public/images/game/conveyor-lift-mk-3_64.png new file mode 100644 index 00000000..4da6b6a7 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-3_64.png differ diff --git a/public/images/game/conveyor-lift-mk-4_256.png b/public/images/game/conveyor-lift-mk-4_256.png new file mode 100644 index 00000000..7ed8f5af Binary files /dev/null and b/public/images/game/conveyor-lift-mk-4_256.png differ diff --git a/public/images/game/conveyor-lift-mk-4_64.png b/public/images/game/conveyor-lift-mk-4_64.png new file mode 100644 index 00000000..08e42bc5 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-4_64.png differ diff --git a/public/images/game/conveyor-lift-mk-5_256.png b/public/images/game/conveyor-lift-mk-5_256.png new file mode 100644 index 00000000..59695357 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-5_256.png differ diff --git a/public/images/game/conveyor-lift-mk-5_64.png b/public/images/game/conveyor-lift-mk-5_64.png new file mode 100644 index 00000000..58262014 Binary files /dev/null and b/public/images/game/conveyor-lift-mk-5_64.png differ diff --git a/public/images/game/conveyor-lift-mk-6_256.png b/public/images/game/conveyor-lift-mk-6_256.png new file mode 100644 index 00000000..747b73fb Binary files /dev/null and b/public/images/game/conveyor-lift-mk-6_256.png differ diff --git a/public/images/game/conveyor-lift-mk-6_64.png b/public/images/game/conveyor-lift-mk-6_64.png new file mode 100644 index 00000000..f7cf885c Binary files /dev/null and b/public/images/game/conveyor-lift-mk-6_64.png differ diff --git a/public/images/game/conveyor-merger_256.png b/public/images/game/conveyor-merger_256.png new file mode 100644 index 00000000..4a674547 Binary files /dev/null and b/public/images/game/conveyor-merger_256.png differ diff --git a/public/images/game/conveyor-merger_64.png b/public/images/game/conveyor-merger_64.png new file mode 100644 index 00000000..b5e5e6d9 Binary files /dev/null and b/public/images/game/conveyor-merger_64.png differ diff --git a/public/images/game/conveyor-pole-multi_256.png b/public/images/game/conveyor-pole-multi_256.png new file mode 100644 index 00000000..8990616e Binary files /dev/null and b/public/images/game/conveyor-pole-multi_256.png differ diff --git a/public/images/game/conveyor-pole-multi_64.png b/public/images/game/conveyor-pole-multi_64.png new file mode 100644 index 00000000..340c3b3f Binary files /dev/null and b/public/images/game/conveyor-pole-multi_64.png differ diff --git a/public/images/game/conveyor-splitter_256.png b/public/images/game/conveyor-splitter_256.png new file mode 100644 index 00000000..776a33f3 Binary files /dev/null and b/public/images/game/conveyor-splitter_256.png differ diff --git a/public/images/game/conveyor-splitter_64.png b/public/images/game/conveyor-splitter_64.png new file mode 100644 index 00000000..0ef37e7c Binary files /dev/null and b/public/images/game/conveyor-splitter_64.png differ diff --git a/public/images/game/detonator_256.png b/public/images/game/detonator_256.png new file mode 100644 index 00000000..92a8b197 Binary files /dev/null and b/public/images/game/detonator_256.png differ diff --git a/public/images/game/detonator_64.png b/public/images/game/detonator_64.png new file mode 100644 index 00000000..41eeae80 Binary files /dev/null and b/public/images/game/detonator_64.png differ diff --git a/public/images/game/docking-station_256.png b/public/images/game/docking-station_256.png new file mode 100644 index 00000000..b18d42a9 Binary files /dev/null and b/public/images/game/docking-station_256.png differ diff --git a/public/images/game/docking-station_64.png b/public/images/game/docking-station_64.png new file mode 100644 index 00000000..a4fbf4ff Binary files /dev/null and b/public/images/game/docking-station_64.png differ diff --git a/public/images/game/double-wall-outlet-mk-2_256.png b/public/images/game/double-wall-outlet-mk-2_256.png new file mode 100644 index 00000000..1dad4631 Binary files /dev/null and b/public/images/game/double-wall-outlet-mk-2_256.png differ diff --git a/public/images/game/double-wall-outlet-mk-2_64.png b/public/images/game/double-wall-outlet-mk-2_64.png new file mode 100644 index 00000000..0c4a2099 Binary files /dev/null and b/public/images/game/double-wall-outlet-mk-2_64.png differ diff --git a/public/images/game/double-wall-outlet-mk-3_256.png b/public/images/game/double-wall-outlet-mk-3_256.png new file mode 100644 index 00000000..e7592036 Binary files /dev/null and b/public/images/game/double-wall-outlet-mk-3_256.png differ diff --git a/public/images/game/double-wall-outlet-mk-3_64.png b/public/images/game/double-wall-outlet-mk-3_64.png new file mode 100644 index 00000000..de3a03b4 Binary files /dev/null and b/public/images/game/double-wall-outlet-mk-3_64.png differ diff --git a/public/images/game/drone-port_256.png b/public/images/game/drone-port_256.png new file mode 100644 index 00000000..72b2f670 Binary files /dev/null and b/public/images/game/drone-port_256.png differ diff --git a/public/images/game/drone-port_64.png b/public/images/game/drone-port_64.png new file mode 100644 index 00000000..a79c5f8e Binary files /dev/null and b/public/images/game/drone-port_64.png differ diff --git a/public/images/game/empty-platform_256.png b/public/images/game/empty-platform_256.png new file mode 100644 index 00000000..919a10d1 Binary files /dev/null and b/public/images/game/empty-platform_256.png differ diff --git a/public/images/game/empty-platform_64.png b/public/images/game/empty-platform_64.png new file mode 100644 index 00000000..7eab1ae9 Binary files /dev/null and b/public/images/game/empty-platform_64.png differ diff --git a/public/images/game/ficsit-foundation-1-m_256.png b/public/images/game/ficsit-foundation-1-m_256.png new file mode 100644 index 00000000..f643ef68 Binary files /dev/null and b/public/images/game/ficsit-foundation-1-m_256.png differ diff --git a/public/images/game/ficsit-foundation-1-m_64.png b/public/images/game/ficsit-foundation-1-m_64.png new file mode 100644 index 00000000..61323618 Binary files /dev/null and b/public/images/game/ficsit-foundation-1-m_64.png differ diff --git a/public/images/game/ficsit-foundation-2-m_256.png b/public/images/game/ficsit-foundation-2-m_256.png new file mode 100644 index 00000000..20ce38be Binary files /dev/null and b/public/images/game/ficsit-foundation-2-m_256.png differ diff --git a/public/images/game/ficsit-foundation-2-m_64.png b/public/images/game/ficsit-foundation-2-m_64.png new file mode 100644 index 00000000..5efc697c Binary files /dev/null and b/public/images/game/ficsit-foundation-2-m_64.png differ diff --git a/public/images/game/ficsit-foundation-4-m_256.png b/public/images/game/ficsit-foundation-4-m_256.png new file mode 100644 index 00000000..0a8b5cbc Binary files /dev/null and b/public/images/game/ficsit-foundation-4-m_256.png differ diff --git a/public/images/game/ficsit-foundation-4-m_64.png b/public/images/game/ficsit-foundation-4-m_64.png new file mode 100644 index 00000000..1f090367 Binary files /dev/null and b/public/images/game/ficsit-foundation-4-m_64.png differ diff --git a/public/images/game/ficsit-ramp-1-m_256.png b/public/images/game/ficsit-ramp-1-m_256.png new file mode 100644 index 00000000..cdd675be Binary files /dev/null and b/public/images/game/ficsit-ramp-1-m_256.png differ diff --git a/public/images/game/ficsit-ramp-1-m_64.png b/public/images/game/ficsit-ramp-1-m_64.png new file mode 100644 index 00000000..cb92c0f8 Binary files /dev/null and b/public/images/game/ficsit-ramp-1-m_64.png differ diff --git a/public/images/game/ficsit-ramp-2-m_256.png b/public/images/game/ficsit-ramp-2-m_256.png new file mode 100644 index 00000000..5163f3a8 Binary files /dev/null and b/public/images/game/ficsit-ramp-2-m_256.png differ diff --git a/public/images/game/ficsit-ramp-2-m_64.png b/public/images/game/ficsit-ramp-2-m_64.png new file mode 100644 index 00000000..f832f163 Binary files /dev/null and b/public/images/game/ficsit-ramp-2-m_64.png differ diff --git a/public/images/game/ficsit-ramp-4-m_256.png b/public/images/game/ficsit-ramp-4-m_256.png new file mode 100644 index 00000000..0df78fb0 Binary files /dev/null and b/public/images/game/ficsit-ramp-4-m_256.png differ diff --git a/public/images/game/ficsit-ramp-4-m_64.png b/public/images/game/ficsit-ramp-4-m_64.png new file mode 100644 index 00000000..d75ae6b2 Binary files /dev/null and b/public/images/game/ficsit-ramp-4-m_64.png differ diff --git a/public/images/game/ficsit-wall-8-x-1_256.png b/public/images/game/ficsit-wall-8-x-1_256.png new file mode 100644 index 00000000..e26ebe43 Binary files /dev/null and b/public/images/game/ficsit-wall-8-x-1_256.png differ diff --git a/public/images/game/ficsit-wall-8-x-1_64.png b/public/images/game/ficsit-wall-8-x-1_64.png new file mode 100644 index 00000000..53df2245 Binary files /dev/null and b/public/images/game/ficsit-wall-8-x-1_64.png differ diff --git a/public/images/game/ficsit-wall-8-x-4_256.png b/public/images/game/ficsit-wall-8-x-4_256.png new file mode 100644 index 00000000..b270a526 Binary files /dev/null and b/public/images/game/ficsit-wall-8-x-4_256.png differ diff --git a/public/images/game/ficsit-wall-8-x-4_64.png b/public/images/game/ficsit-wall-8-x-4_64.png new file mode 100644 index 00000000..32a28b2a Binary files /dev/null and b/public/images/game/ficsit-wall-8-x-4_64.png differ diff --git a/public/images/game/fluid-storage-industrial_256.png b/public/images/game/fluid-storage-industrial_256.png new file mode 100644 index 00000000..01f4cd4d Binary files /dev/null and b/public/images/game/fluid-storage-industrial_256.png differ diff --git a/public/images/game/fluid-storage-industrial_64.png b/public/images/game/fluid-storage-industrial_64.png new file mode 100644 index 00000000..87637999 Binary files /dev/null and b/public/images/game/fluid-storage-industrial_64.png differ diff --git a/public/images/game/fluid-storage_256.png b/public/images/game/fluid-storage_256.png new file mode 100644 index 00000000..fcdbe2af Binary files /dev/null and b/public/images/game/fluid-storage_256.png differ diff --git a/public/images/game/fluid-storage_64.png b/public/images/game/fluid-storage_64.png new file mode 100644 index 00000000..14c554e2 Binary files /dev/null and b/public/images/game/fluid-storage_64.png differ diff --git a/public/images/game/gas-mask_256.png b/public/images/game/gas-mask_256.png new file mode 100644 index 00000000..72794c47 Binary files /dev/null and b/public/images/game/gas-mask_256.png differ diff --git a/public/images/game/gas-mask_64.png b/public/images/game/gas-mask_64.png new file mode 100644 index 00000000..789d5dc3 Binary files /dev/null and b/public/images/game/gas-mask_64.png differ diff --git a/public/images/game/golf-cart-gold_256.png b/public/images/game/golf-cart-gold_256.png new file mode 100644 index 00000000..b7a822c1 Binary files /dev/null and b/public/images/game/golf-cart-gold_256.png differ diff --git a/public/images/game/golf-cart-gold_64.png b/public/images/game/golf-cart-gold_64.png new file mode 100644 index 00000000..9dbc9c53 Binary files /dev/null and b/public/images/game/golf-cart-gold_64.png differ diff --git a/public/images/game/golf-cart_256.png b/public/images/game/golf-cart_256.png new file mode 100644 index 00000000..e282080e Binary files /dev/null and b/public/images/game/golf-cart_256.png differ diff --git a/public/images/game/golf-cart_64.png b/public/images/game/golf-cart_64.png new file mode 100644 index 00000000..d7573554 Binary files /dev/null and b/public/images/game/golf-cart_64.png differ diff --git a/public/images/game/hazmat-suit_256.png b/public/images/game/hazmat-suit_256.png new file mode 100644 index 00000000..0305cd1c Binary files /dev/null and b/public/images/game/hazmat-suit_256.png differ diff --git a/public/images/game/hazmat-suit_64.png b/public/images/game/hazmat-suit_64.png new file mode 100644 index 00000000..a6acf893 Binary files /dev/null and b/public/images/game/hazmat-suit_64.png differ diff --git a/public/images/game/hoverpack_256.png b/public/images/game/hoverpack_256.png new file mode 100644 index 00000000..7e9b7986 Binary files /dev/null and b/public/images/game/hoverpack_256.png differ diff --git a/public/images/game/hoverpack_64.png b/public/images/game/hoverpack_64.png new file mode 100644 index 00000000..d0838dab Binary files /dev/null and b/public/images/game/hoverpack_64.png differ diff --git a/public/images/game/hub_256.png b/public/images/game/hub_256.png new file mode 100644 index 00000000..a52d6305 Binary files /dev/null and b/public/images/game/hub_256.png differ diff --git a/public/images/game/hub_64.png b/public/images/game/hub_64.png new file mode 100644 index 00000000..828ade41 Binary files /dev/null and b/public/images/game/hub_64.png differ diff --git a/public/images/game/hyper-tube-pole_256.png b/public/images/game/hyper-tube-pole_256.png new file mode 100644 index 00000000..2d3efc92 Binary files /dev/null and b/public/images/game/hyper-tube-pole_256.png differ diff --git a/public/images/game/hyper-tube-pole_64.png b/public/images/game/hyper-tube-pole_64.png new file mode 100644 index 00000000..869171b4 Binary files /dev/null and b/public/images/game/hyper-tube-pole_64.png differ diff --git a/public/images/game/hyper-tube-stackable_256.png b/public/images/game/hyper-tube-stackable_256.png new file mode 100644 index 00000000..358c5dfd Binary files /dev/null and b/public/images/game/hyper-tube-stackable_256.png differ diff --git a/public/images/game/hyper-tube-stackable_64.png b/public/images/game/hyper-tube-stackable_64.png new file mode 100644 index 00000000..0347adbf Binary files /dev/null and b/public/images/game/hyper-tube-stackable_64.png differ diff --git a/public/images/game/hyper-tube-start_256.png b/public/images/game/hyper-tube-start_256.png new file mode 100644 index 00000000..dae2f672 Binary files /dev/null and b/public/images/game/hyper-tube-start_256.png differ diff --git a/public/images/game/hyper-tube-start_64.png b/public/images/game/hyper-tube-start_64.png new file mode 100644 index 00000000..bd4cc1fc Binary files /dev/null and b/public/images/game/hyper-tube-start_64.png differ diff --git a/public/images/game/hyper-tube_256.png b/public/images/game/hyper-tube_256.png new file mode 100644 index 00000000..14a94b52 Binary files /dev/null and b/public/images/game/hyper-tube_256.png differ diff --git a/public/images/game/hyper-tube_64.png b/public/images/game/hyper-tube_64.png new file mode 100644 index 00000000..4e3d128b Binary files /dev/null and b/public/images/game/hyper-tube_64.png differ diff --git a/public/images/game/hypertube-branch_256.png b/public/images/game/hypertube-branch_256.png new file mode 100644 index 00000000..1e4a5de5 Binary files /dev/null and b/public/images/game/hypertube-branch_256.png differ diff --git a/public/images/game/hypertube-branch_64.png b/public/images/game/hypertube-branch_64.png new file mode 100644 index 00000000..8b69f72d Binary files /dev/null and b/public/images/game/hypertube-branch_64.png differ diff --git a/public/images/game/hypertube-junction_256.png b/public/images/game/hypertube-junction_256.png new file mode 100644 index 00000000..7688c6ff Binary files /dev/null and b/public/images/game/hypertube-junction_256.png differ diff --git a/public/images/game/hypertube-junction_64.png b/public/images/game/hypertube-junction_64.png new file mode 100644 index 00000000..2ec1aa62 Binary files /dev/null and b/public/images/game/hypertube-junction_64.png differ diff --git a/public/images/game/hypertube-support_256.png b/public/images/game/hypertube-support_256.png deleted file mode 100644 index 3ce8d6ee..00000000 Binary files a/public/images/game/hypertube-support_256.png and /dev/null differ diff --git a/public/images/game/hypertube-support_64.png b/public/images/game/hypertube-support_64.png deleted file mode 100644 index 8b434687..00000000 Binary files a/public/images/game/hypertube-support_64.png and /dev/null differ diff --git a/public/images/game/jetpack_256.png b/public/images/game/jetpack_256.png new file mode 100644 index 00000000..091acce5 Binary files /dev/null and b/public/images/game/jetpack_256.png differ diff --git a/public/images/game/jetpack_64.png b/public/images/game/jetpack_64.png new file mode 100644 index 00000000..8f61991c Binary files /dev/null and b/public/images/game/jetpack_64.png differ diff --git a/public/images/game/jump-pad_256.png b/public/images/game/jump-pad_256.png new file mode 100644 index 00000000..8be8f9a3 Binary files /dev/null and b/public/images/game/jump-pad_256.png differ diff --git a/public/images/game/jump-pad_64.png b/public/images/game/jump-pad_64.png new file mode 100644 index 00000000..ce2c929f Binary files /dev/null and b/public/images/game/jump-pad_64.png differ diff --git a/public/images/game/landing-pad_256.png b/public/images/game/landing-pad_256.png new file mode 100644 index 00000000..d738faee Binary files /dev/null and b/public/images/game/landing-pad_256.png differ diff --git a/public/images/game/landing-pad_64.png b/public/images/game/landing-pad_64.png new file mode 100644 index 00000000..e2e00537 Binary files /dev/null and b/public/images/game/landing-pad_64.png differ diff --git a/public/images/game/look-out-tower_256.png b/public/images/game/look-out-tower_256.png new file mode 100644 index 00000000..80ae0bc5 Binary files /dev/null and b/public/images/game/look-out-tower_256.png differ diff --git a/public/images/game/look-out-tower_64.png b/public/images/game/look-out-tower_64.png new file mode 100644 index 00000000..55959ab9 Binary files /dev/null and b/public/images/game/look-out-tower_64.png differ diff --git a/public/images/game/mam_256.png b/public/images/game/mam_256.png new file mode 100644 index 00000000..70a7ba00 Binary files /dev/null and b/public/images/game/mam_256.png differ diff --git a/public/images/game/mam_64.png b/public/images/game/mam_64.png new file mode 100644 index 00000000..1d2b50c2 Binary files /dev/null and b/public/images/game/mam_64.png differ diff --git a/public/images/game/object-scanner_256.png b/public/images/game/object-scanner_256.png new file mode 100644 index 00000000..784c4380 Binary files /dev/null and b/public/images/game/object-scanner_256.png differ diff --git a/public/images/game/object-scanner_64.png b/public/images/game/object-scanner_64.png new file mode 100644 index 00000000..0df1bc5a Binary files /dev/null and b/public/images/game/object-scanner_64.png differ diff --git a/public/images/game/path-signal_256.png b/public/images/game/path-signal_256.png new file mode 100644 index 00000000..fca9a6fa Binary files /dev/null and b/public/images/game/path-signal_256.png differ diff --git a/public/images/game/path-signal_64.png b/public/images/game/path-signal_64.png new file mode 100644 index 00000000..364b7374 Binary files /dev/null and b/public/images/game/path-signal_64.png differ diff --git a/public/images/game/pipe-pole-stackable_256.png b/public/images/game/pipe-pole-stackable_256.png new file mode 100644 index 00000000..e0e1d1a4 Binary files /dev/null and b/public/images/game/pipe-pole-stackable_256.png differ diff --git a/public/images/game/pipe-pole-stackable_64.png b/public/images/game/pipe-pole-stackable_64.png new file mode 100644 index 00000000..88b3ba23 Binary files /dev/null and b/public/images/game/pipe-pole-stackable_64.png differ diff --git a/public/images/game/pipe-pole_256.png b/public/images/game/pipe-pole_256.png new file mode 100644 index 00000000..7803d4d0 Binary files /dev/null and b/public/images/game/pipe-pole_256.png differ diff --git a/public/images/game/pipe-pole_64.png b/public/images/game/pipe-pole_64.png new file mode 100644 index 00000000..14726eb8 Binary files /dev/null and b/public/images/game/pipe-pole_64.png differ diff --git a/public/images/game/pipeline-support_256.png b/public/images/game/pipeline-support_256.png deleted file mode 100644 index 000c9788..00000000 Binary files a/public/images/game/pipeline-support_256.png and /dev/null differ diff --git a/public/images/game/pipeline-support_64.png b/public/images/game/pipeline-support_64.png deleted file mode 100644 index b9f1ec1d..00000000 Binary files a/public/images/game/pipeline-support_64.png and /dev/null differ diff --git a/public/images/game/platform-catwalk_256.png b/public/images/game/platform-catwalk_256.png new file mode 100644 index 00000000..7186319f Binary files /dev/null and b/public/images/game/platform-catwalk_256.png differ diff --git a/public/images/game/platform-catwalk_64.png b/public/images/game/platform-catwalk_64.png new file mode 100644 index 00000000..30487509 Binary files /dev/null and b/public/images/game/platform-catwalk_64.png differ diff --git a/public/images/game/player-storage_256.png b/public/images/game/player-storage_256.png new file mode 100644 index 00000000..83cbc6c8 Binary files /dev/null and b/public/images/game/player-storage_256.png differ diff --git a/public/images/game/player-storage_64.png b/public/images/game/player-storage_64.png new file mode 100644 index 00000000..4e7009f6 Binary files /dev/null and b/public/images/game/player-storage_64.png differ diff --git a/public/images/game/portal-satellite_256.png b/public/images/game/portal-satellite_256.png new file mode 100644 index 00000000..5558c91a Binary files /dev/null and b/public/images/game/portal-satellite_256.png differ diff --git a/public/images/game/portal-satellite_64.png b/public/images/game/portal-satellite_64.png new file mode 100644 index 00000000..2981d8c4 Binary files /dev/null and b/public/images/game/portal-satellite_64.png differ diff --git a/public/images/game/portal_256.png b/public/images/game/portal_256.png new file mode 100644 index 00000000..a1c3444f Binary files /dev/null and b/public/images/game/portal_256.png differ diff --git a/public/images/game/portal_64.png b/public/images/game/portal_64.png new file mode 100644 index 00000000..4b43b938 Binary files /dev/null and b/public/images/game/portal_64.png differ diff --git a/public/images/game/power-pole-mk-1_256.png b/public/images/game/power-pole-mk-1_256.png new file mode 100644 index 00000000..9bfa3ee7 Binary files /dev/null and b/public/images/game/power-pole-mk-1_256.png differ diff --git a/public/images/game/power-pole-mk-1_64.png b/public/images/game/power-pole-mk-1_64.png new file mode 100644 index 00000000..992618e1 Binary files /dev/null and b/public/images/game/power-pole-mk-1_64.png differ diff --git a/public/images/game/power-pole-mk-2_256.png b/public/images/game/power-pole-mk-2_256.png new file mode 100644 index 00000000..5228509b Binary files /dev/null and b/public/images/game/power-pole-mk-2_256.png differ diff --git a/public/images/game/power-pole-mk-2_64.png b/public/images/game/power-pole-mk-2_64.png new file mode 100644 index 00000000..22ecd12b Binary files /dev/null and b/public/images/game/power-pole-mk-2_64.png differ diff --git a/public/images/game/power-pole-mk-3_256.png b/public/images/game/power-pole-mk-3_256.png new file mode 100644 index 00000000..5388558d Binary files /dev/null and b/public/images/game/power-pole-mk-3_256.png differ diff --git a/public/images/game/power-pole-mk-3_64.png b/public/images/game/power-pole-mk-3_64.png new file mode 100644 index 00000000..2bfb6941 Binary files /dev/null and b/public/images/game/power-pole-mk-3_64.png differ diff --git a/public/images/game/power-pole-wall-double-mk-1_256.png b/public/images/game/power-pole-wall-double-mk-1_256.png new file mode 100644 index 00000000..3c6aee14 Binary files /dev/null and b/public/images/game/power-pole-wall-double-mk-1_256.png differ diff --git a/public/images/game/power-pole-wall-double-mk-1_64.png b/public/images/game/power-pole-wall-double-mk-1_64.png new file mode 100644 index 00000000..997fe355 Binary files /dev/null and b/public/images/game/power-pole-wall-double-mk-1_64.png differ diff --git a/public/images/game/power-pole-wall-mk-1_256.png b/public/images/game/power-pole-wall-mk-1_256.png new file mode 100644 index 00000000..d1d08743 Binary files /dev/null and b/public/images/game/power-pole-wall-mk-1_256.png differ diff --git a/public/images/game/power-pole-wall-mk-1_64.png b/public/images/game/power-pole-wall-mk-1_64.png new file mode 100644 index 00000000..6c5d16a4 Binary files /dev/null and b/public/images/game/power-pole-wall-mk-1_64.png differ diff --git a/public/images/game/power-storage_256.png b/public/images/game/power-storage_256.png new file mode 100644 index 00000000..79b797ed Binary files /dev/null and b/public/images/game/power-storage_256.png differ diff --git a/public/images/game/power-storage_64.png b/public/images/game/power-storage_64.png new file mode 100644 index 00000000..5650e394 Binary files /dev/null and b/public/images/game/power-storage_64.png differ diff --git a/public/images/game/power-switch_256.png b/public/images/game/power-switch_256.png new file mode 100644 index 00000000..854e18bf Binary files /dev/null and b/public/images/game/power-switch_256.png differ diff --git a/public/images/game/power-switch_64.png b/public/images/game/power-switch_64.png new file mode 100644 index 00000000..70af8e7b Binary files /dev/null and b/public/images/game/power-switch_64.png differ diff --git a/public/images/game/power-tower-platform_256.png b/public/images/game/power-tower-platform_256.png new file mode 100644 index 00000000..d52b88e7 Binary files /dev/null and b/public/images/game/power-tower-platform_256.png differ diff --git a/public/images/game/power-tower-platform_64.png b/public/images/game/power-tower-platform_64.png new file mode 100644 index 00000000..e70d14d6 Binary files /dev/null and b/public/images/game/power-tower-platform_64.png differ diff --git a/public/images/game/power-tower_256.png b/public/images/game/power-tower_256.png new file mode 100644 index 00000000..a5c8d97c Binary files /dev/null and b/public/images/game/power-tower_256.png differ diff --git a/public/images/game/power-tower_64.png b/public/images/game/power-tower_64.png new file mode 100644 index 00000000..bbbe0dba Binary files /dev/null and b/public/images/game/power-tower_64.png differ diff --git a/public/images/game/priority-merger_256.png b/public/images/game/priority-merger_256.png new file mode 100644 index 00000000..ce7fcf30 Binary files /dev/null and b/public/images/game/priority-merger_256.png differ diff --git a/public/images/game/priority-merger_64.png b/public/images/game/priority-merger_64.png new file mode 100644 index 00000000..7bc6f349 Binary files /dev/null and b/public/images/game/priority-merger_64.png differ diff --git a/public/images/game/programmable-splitter_256.png b/public/images/game/programmable-splitter_256.png new file mode 100644 index 00000000..e420e486 Binary files /dev/null and b/public/images/game/programmable-splitter_256.png differ diff --git a/public/images/game/programmable-splitter_64.png b/public/images/game/programmable-splitter_64.png new file mode 100644 index 00000000..5efd09f5 Binary files /dev/null and b/public/images/game/programmable-splitter_64.png differ diff --git a/public/images/game/radar-tower_256.png b/public/images/game/radar-tower_256.png new file mode 100644 index 00000000..aeb5d209 Binary files /dev/null and b/public/images/game/radar-tower_256.png differ diff --git a/public/images/game/radar-tower_64.png b/public/images/game/radar-tower_64.png new file mode 100644 index 00000000..df222e7e Binary files /dev/null and b/public/images/game/radar-tower_64.png differ diff --git a/public/images/game/rebar-gun_256.png b/public/images/game/rebar-gun_256.png new file mode 100644 index 00000000..32b51404 Binary files /dev/null and b/public/images/game/rebar-gun_256.png differ diff --git a/public/images/game/rebar-gun_64.png b/public/images/game/rebar-gun_64.png new file mode 100644 index 00000000..be377515 Binary files /dev/null and b/public/images/game/rebar-gun_64.png differ diff --git a/public/images/game/resource-sink-shop_256.png b/public/images/game/resource-sink-shop_256.png new file mode 100644 index 00000000..49911c02 Binary files /dev/null and b/public/images/game/resource-sink-shop_256.png differ diff --git a/public/images/game/resource-sink-shop_64.png b/public/images/game/resource-sink-shop_64.png new file mode 100644 index 00000000..f9c0eacf Binary files /dev/null and b/public/images/game/resource-sink-shop_64.png differ diff --git a/public/images/game/resource-sink_256.png b/public/images/game/resource-sink_256.png new file mode 100644 index 00000000..50effee4 Binary files /dev/null and b/public/images/game/resource-sink_256.png differ diff --git a/public/images/game/resource-sink_64.png b/public/images/game/resource-sink_64.png new file mode 100644 index 00000000..5206bd28 Binary files /dev/null and b/public/images/game/resource-sink_64.png differ diff --git a/public/images/game/rifle-mk-1_256.png b/public/images/game/rifle-mk-1_256.png new file mode 100644 index 00000000..ce6d6b76 Binary files /dev/null and b/public/images/game/rifle-mk-1_256.png differ diff --git a/public/images/game/rifle-mk-1_64.png b/public/images/game/rifle-mk-1_64.png new file mode 100644 index 00000000..2340fb89 Binary files /dev/null and b/public/images/game/rifle-mk-1_64.png differ diff --git a/public/images/game/smart-splitter_256.png b/public/images/game/smart-splitter_256.png new file mode 100644 index 00000000..985ab533 Binary files /dev/null and b/public/images/game/smart-splitter_256.png differ diff --git a/public/images/game/smart-splitter_64.png b/public/images/game/smart-splitter_64.png new file mode 100644 index 00000000..3308f0d5 Binary files /dev/null and b/public/images/game/smart-splitter_64.png differ diff --git a/public/images/game/smart-switch_256.png b/public/images/game/smart-switch_256.png new file mode 100644 index 00000000..df0fe00a Binary files /dev/null and b/public/images/game/smart-switch_256.png differ diff --git a/public/images/game/smart-switch_64.png b/public/images/game/smart-switch_64.png new file mode 100644 index 00000000..2ea1ae24 Binary files /dev/null and b/public/images/game/smart-switch_64.png differ diff --git a/public/images/game/smasher_256.png b/public/images/game/smasher_256.png new file mode 100644 index 00000000..6bc510a9 Binary files /dev/null and b/public/images/game/smasher_256.png differ diff --git a/public/images/game/smasher_64.png b/public/images/game/smasher_64.png new file mode 100644 index 00000000..974e9a29 Binary files /dev/null and b/public/images/game/smasher_64.png differ diff --git a/public/images/game/space-elevator_256.png b/public/images/game/space-elevator_256.png new file mode 100644 index 00000000..6a45eb43 Binary files /dev/null and b/public/images/game/space-elevator_256.png differ diff --git a/public/images/game/space-elevator_64.png b/public/images/game/space-elevator_64.png new file mode 100644 index 00000000..ecb7db18 Binary files /dev/null and b/public/images/game/space-elevator_64.png differ diff --git a/public/images/game/sprinting-stilts_256.png b/public/images/game/sprinting-stilts_256.png new file mode 100644 index 00000000..dbae2348 Binary files /dev/null and b/public/images/game/sprinting-stilts_256.png differ diff --git a/public/images/game/sprinting-stilts_64.png b/public/images/game/sprinting-stilts_64.png new file mode 100644 index 00000000..1fa2b40d Binary files /dev/null and b/public/images/game/sprinting-stilts_64.png differ diff --git a/public/images/game/stackable-hypertube-support_256.png b/public/images/game/stackable-hypertube-support_256.png deleted file mode 100644 index 8ca621f6..00000000 Binary files a/public/images/game/stackable-hypertube-support_256.png and /dev/null differ diff --git a/public/images/game/stackable-hypertube-support_64.png b/public/images/game/stackable-hypertube-support_64.png deleted file mode 100644 index b163ffd0..00000000 Binary files a/public/images/game/stackable-hypertube-support_64.png and /dev/null differ diff --git a/public/images/game/stackable-pipeline-support_256.png b/public/images/game/stackable-pipeline-support_256.png deleted file mode 100644 index b42e9992..00000000 Binary files a/public/images/game/stackable-pipeline-support_256.png and /dev/null differ diff --git a/public/images/game/stackable-pipeline-support_64.png b/public/images/game/stackable-pipeline-support_64.png deleted file mode 100644 index dcae9dc5..00000000 Binary files a/public/images/game/stackable-pipeline-support_64.png and /dev/null differ diff --git a/public/images/game/storage-container-mk-2_256.png b/public/images/game/storage-container-mk-2_256.png new file mode 100644 index 00000000..d3ba45d8 Binary files /dev/null and b/public/images/game/storage-container-mk-2_256.png differ diff --git a/public/images/game/storage-container-mk-2_64.png b/public/images/game/storage-container-mk-2_64.png new file mode 100644 index 00000000..ed66a052 Binary files /dev/null and b/public/images/game/storage-container-mk-2_64.png differ diff --git a/public/images/game/storage-container_256.png b/public/images/game/storage-container_256.png new file mode 100644 index 00000000..dde069ed Binary files /dev/null and b/public/images/game/storage-container_256.png differ diff --git a/public/images/game/storage-container_64.png b/public/images/game/storage-container_64.png new file mode 100644 index 00000000..e74ef5c4 Binary files /dev/null and b/public/images/game/storage-container_64.png differ diff --git a/public/images/game/storage-hazard_256.png b/public/images/game/storage-hazard_256.png new file mode 100644 index 00000000..9f62af41 Binary files /dev/null and b/public/images/game/storage-hazard_256.png differ diff --git a/public/images/game/storage-hazard_64.png b/public/images/game/storage-hazard_64.png new file mode 100644 index 00000000..9d22b8c8 Binary files /dev/null and b/public/images/game/storage-hazard_64.png differ diff --git a/public/images/game/storage-medkit_256.png b/public/images/game/storage-medkit_256.png new file mode 100644 index 00000000..52c13b4f Binary files /dev/null and b/public/images/game/storage-medkit_256.png differ diff --git a/public/images/game/storage-medkit_64.png b/public/images/game/storage-medkit_64.png new file mode 100644 index 00000000..10f69850 Binary files /dev/null and b/public/images/game/storage-medkit_64.png differ diff --git a/public/images/game/track_256.png b/public/images/game/track_256.png new file mode 100644 index 00000000..61c8f566 Binary files /dev/null and b/public/images/game/track_256.png differ diff --git a/public/images/game/track_64.png b/public/images/game/track_64.png new file mode 100644 index 00000000..28f1e2a4 Binary files /dev/null and b/public/images/game/track_64.png differ diff --git a/public/images/game/train-docking-fluid_256.png b/public/images/game/train-docking-fluid_256.png new file mode 100644 index 00000000..ca2d48da Binary files /dev/null and b/public/images/game/train-docking-fluid_256.png differ diff --git a/public/images/game/train-docking-fluid_64.png b/public/images/game/train-docking-fluid_64.png new file mode 100644 index 00000000..d479406c Binary files /dev/null and b/public/images/game/train-docking-fluid_64.png differ diff --git a/public/images/game/train-station_256.png b/public/images/game/train-station_256.png new file mode 100644 index 00000000..c3850912 Binary files /dev/null and b/public/images/game/train-station_256.png differ diff --git a/public/images/game/train-station_64.png b/public/images/game/train-station_64.png new file mode 100644 index 00000000..95616843 Binary files /dev/null and b/public/images/game/train-station_64.png differ diff --git a/public/images/game/truck-station_256.png b/public/images/game/truck-station_256.png new file mode 100644 index 00000000..3cf43313 Binary files /dev/null and b/public/images/game/truck-station_256.png differ diff --git a/public/images/game/truck-station_64.png b/public/images/game/truck-station_64.png new file mode 100644 index 00000000..0d7619f2 Binary files /dev/null and b/public/images/game/truck-station_64.png differ diff --git a/public/images/game/wall-outlet-mk-2_256.png b/public/images/game/wall-outlet-mk-2_256.png new file mode 100644 index 00000000..df85e56d Binary files /dev/null and b/public/images/game/wall-outlet-mk-2_256.png differ diff --git a/public/images/game/wall-outlet-mk-2_64.png b/public/images/game/wall-outlet-mk-2_64.png new file mode 100644 index 00000000..ce5293a4 Binary files /dev/null and b/public/images/game/wall-outlet-mk-2_64.png differ diff --git a/public/images/game/wall-outlet-mk-3_256.png b/public/images/game/wall-outlet-mk-3_256.png new file mode 100644 index 00000000..b6f96bb5 Binary files /dev/null and b/public/images/game/wall-outlet-mk-3_256.png differ diff --git a/public/images/game/wall-outlet-mk-3_64.png b/public/images/game/wall-outlet-mk-3_64.png new file mode 100644 index 00000000..9348b5b8 Binary files /dev/null and b/public/images/game/wall-outlet-mk-3_64.png differ diff --git a/public/images/game/xeno-basher_256.png b/public/images/game/xeno-basher_256.png new file mode 100644 index 00000000..02baeb16 Binary files /dev/null and b/public/images/game/xeno-basher_256.png differ diff --git a/public/images/game/xeno-basher_64.png b/public/images/game/xeno-basher_64.png new file mode 100644 index 00000000..dac2f780 Binary files /dev/null and b/public/images/game/xeno-basher_64.png differ diff --git a/public/images/game/xeno-zapper_256.png b/public/images/game/xeno-zapper_256.png new file mode 100644 index 00000000..d4464187 Binary files /dev/null and b/public/images/game/xeno-zapper_256.png differ diff --git a/public/images/game/xeno-zapper_64.png b/public/images/game/xeno-zapper_64.png new file mode 100644 index 00000000..a3ae5f5f Binary files /dev/null and b/public/images/game/xeno-zapper_64.png differ diff --git a/public/images/game/zipline_256.png b/public/images/game/zipline_256.png new file mode 100644 index 00000000..a50d7165 Binary files /dev/null and b/public/images/game/zipline_256.png differ diff --git a/public/images/game/zipline_64.png b/public/images/game/zipline_64.png new file mode 100644 index 00000000..25c7a6e3 Binary files /dev/null and b/public/images/game/zipline_64.png differ diff --git a/public/images/map/README.md b/public/images/map/README.md new file mode 100644 index 00000000..c8e18278 --- /dev/null +++ b/public/images/map/README.md @@ -0,0 +1,248 @@ +# World map asset + +The `Map` page (`src/map/`) renders a Satisfactory world map. Starting +from this revision the backdrop is no longer a single static image, but a +**WebP tile pyramid** served from a CDN. `public/images/map/` keeps only +static fallbacks and documentation; the tile pyramid itself lives on +DigitalOcean Spaces (see [below](#tile-pyramid--cdn)). + +## Map image: source and license + +The tile pyramid is derived from an in-game extraction of the +MASSAGE-2 (AB)b world map (8192x8192 PNG), upscaled 4x with AI to +32768x32768. The map artwork is the intellectual property of Coffee Stain +Studios and is reproduced here for reference under fair use, consistent +with how community Satisfactory tools (e.g. +[satisfactory-calculator.com](https://satisfactory-calculator.com)) use +the same asset. + +The wiki text content is published under +[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/); +the map render itself is a Coffee Stain Studios asset. + +## Resource node data: source and license + +The bundled resource-node coordinates in +[`src/recipes/WorldResourceNodes.json`](../../../src/recipes/WorldResourceNodes.json) +are derived from +[`Hirashi3630/satisfactory_node_heatmap`](https://github.com/Hirashi3630/satisfactory_node_heatmap) +(`resources/nodes_vanilla.json`), which extracts vanilla 1.0 node data +via the Ficsit Networking mod. That project is MIT-licensed; we +preserve its underlying values and only reformat the records into our +schema (`{ id, resource, purity, x, y, z }`). + +## Coordinate calibration + +The whole pipeline (game coords → Leaflet LatLng → screen pixels → +tile indices) is driven by a small number of constants in +[`src/map/coords.ts`](../../../src/map/coords.ts). The ASCII diagram +below summarises how the three spaces line up. + +``` +Game world (cm) Leaflet CRS.Simple Pixel / tile space +(Unreal axes) (LatLng, y-up) (Leaflet zoom 0) + + +Y = north (top) lat = 0 top pixel_y = 0 tile y = 0 + │ │ │ + ├── ─┤ ─┤ + │ │ │ + -Y = south (bottom) lat = -256 bottom pixel_y = 256 tile y = 0 + + -X = west (left) lng = 0 left pixel_x = 0 tile x = 0 + +X = east (right) lng = 256 right pixel_x = 256 tile x = 0 +``` + +At Leaflet zoom 0 the entire map fits in a single 256x256 tile (pyramid +level 0). `IMAGE_SIZE` is therefore `256`: it equals the tile size so +that one Leaflet zoom step doubles the displayed pixels and lines up +directly with one pyramid level. + +### Game → Leaflet LatLng + +The **playable area** is a 750,000 cm square in Unreal units: + +- `WORLD_X_MIN = -324,700`, `WORLD_X_MAX = 425,300` (west → east) +- `WORLD_Y_MIN = -375,000`, `WORLD_Y_MAX = 375,000` (south → north) + +(Constants derived from +[`Hirashi3630/satisfactory_node_heatmap`](https://github.com/Hirashi3630/satisfactory_node_heatmap) +and verified against known node counts, e.g. 127 Iron Ore, 62 Coal, +94 Limestone.) + +`gameToLatLng(gameX, gameY)` linearly maps that rectangle onto the +Leaflet image bounds `[[-IMAGE_SIZE, 0], [0, IMAGE_SIZE]]`: + +- `lng = ((gameX - WORLD_X_MIN) / X_RANGE) * IMAGE_SIZE` + → `0` at west, `IMAGE_SIZE` at east. +- `lat = ((gameY - WORLD_Y_MIN) / Y_RANGE) * IMAGE_SIZE - IMAGE_SIZE` + → `-IMAGE_SIZE` at south, `0` at north. + +### Why `lat` goes negative southward + +Leaflet `CRS.Simple` uses a **y-up** convention (see the +[official CRS.Simple example](https://leafletjs.com/examples/crs-simple/crs-simple.html)), +while XYZ tile pyramids (produced by `gdal2tiles.py --xyz`) use the +**y-down** convention with `y = 0` at the top. + +If the map's top edge sat at positive lat, CRS.Simple would project it +to a *negative* pixel-y (because its default transformation is +`(1, 0, -1, 0)`), which in turn would make `TileLayer` compute +*negative* tile y indices (requests like `/3/5/-4.webp`, all 404). Put +north at `lat = 0` and south at `lat = -IMAGE_SIZE` and pixel-y stays +in `[0, IMAGE_SIZE]` over the image → tile y indices land in +`[0, IMAGE_SIZE / 256)`, exactly what the pyramid on disk serves. + +### Leaflet zoom ↔ tile pyramid zoom + +`IMAGE_SIZE = 256` (= tile size) makes Leaflet zoom line up with the +pyramid zoom 1:1. No offset needed: Leaflet zoom `N` asks the tile +layer for pyramid zoom `N`, and the image is displayed at +`IMAGE_SIZE * 2^N = 256 * 2^N` px. + +| Leaflet zoom | Image displayed (px) | Tile URL zoom | Tiles per side | +|---|---|---|---| +| 0 (MIN_ZOOM) | 256 | 0 | 1 | +| 2 (DEFAULT) | 1024 | 2 | 4 | +| 4 | 4096 | 4 | 16 | +| 6 | 16384 | 6 | 64 | +| 7 (MAX_ZOOM) | 32768 | 7 | 128 | + +### Swapping the source image + +If you replace the source with a render that has a **different +framing** (different crop of the world), re-tune the `WORLD_*` bounds +so markers land on the right biomes. If you change the **resolution** +(e.g. a new 64k upscale), extend the pyramid by one level and bump +`MAX_ZOOM`; `IMAGE_SIZE` and the `WORLD_*` bounds stay the same. +The current 32k upscale preserves the original framing of +`world-map-5k.png`, so no re-tuning was needed. + +## Tile pyramid + CDN + +The map is served as an 8-level WebP tile pyramid (zoom 0 to 7, 256x256 +tiles, XYZ numbering) hosted on **DigitalOcean Spaces**: + +- Space: `satisfactory-logistics-maps` (region `fra1`). +- CDN edge base URL: `https://satisfactory-logistics-maps.fra1.cdn.digitaloceanspaces.com` +- Published path (current version): `/map/v2/{z}/{x}/{y}.webp` +- Full URL example: `https://satisfactory-logistics-maps.fra1.cdn.digitaloceanspaces.com/map/v2/0/0/0.webp` + +This is the default base URL baked into +[`WorldMapView.tsx`](../../../src/map/WorldMapView.tsx); it can be +overridden via the `VITE_MAP_TILES_BASE_URL` env var (see +[Environment configuration](#environment-configuration)). + +### 1. Regenerate the tile pyramid + +Prerequisites: `gdal` on PATH (`brew install gdal`, tested with GDAL +3.12.3). The source PNG must be square (the current source is +32768x32768). + +``` +npm run generate-map-tiles -- /path/to/source.png --max-zoom=7 +``` + +The path argument is required. `--max-zoom` defaults to 6 (for a 16k +source); set it to `log2(width / 256)` for other sizes (7 for 32k, 8 +for 64k). The script wipes and re-creates `dist-map-tiles/` at the +repo root (gitignored) and invokes `gdal2tiles.py` with +`--profile=raster --xyz --tiledriver=WEBP --webp-quality=80 +--resampling=lanczos`. Expected output for a 32k source: 21845 WebP +tiles, ~50 MB on disk. + +### 2. Upload to DigitalOcean Spaces (rclone) + +The upload is done with `rclone`. A remote named +`do-satisfactory-logistics-maps` is already configured in +`~/.config/rclone/rclone.conf` (see [rclone S3 docs](https://rclone.org/s3/#digitalocean-spaces) +for recreating it on a new machine). Verify the layout: + +``` +rclone lsd do-satisfactory-logistics-maps: +``` + +Then upload the generated pyramid under a fresh version prefix +(current production is `v2`; bump to `v3` on the next regeneration): + +``` +rclone copy \ + dist-map-tiles/ \ + do-satisfactory-logistics-maps:satisfactory-logistics-maps/map/v2/ \ + --header-upload "Cache-Control: public, max-age=31536000, immutable" \ + --s3-acl public-read \ + --transfers 16 \ + --checkers 32 \ + --progress +``` + +If the rclone remote is configured to already point at the bucket, drop +the bucket segment: +`do-satisfactory-logistics-maps:map/v2/`. + +Notes: + +- `--s3-acl public-read` makes each object world-readable (the CDN + returns HTTP 403 otherwise). +- `Content-Type: image/webp` is auto-detected by rclone from the + extension. +- `--transfers 16 --checkers 32` speeds up the upload across 20000+ + small files (32k source). + +### 3. Verify + +``` +curl -I https://satisfactory-logistics-maps.fra1.cdn.digitaloceanspaces.com/map/v2/0/0/0.webp +curl -I -H 'Origin: https://satisfactory-logistics.xyz' \ + https://satisfactory-logistics-maps.fra1.cdn.digitaloceanspaces.com/map/v2/0/0/0.webp +``` + +Expected: + +- `HTTP/2 200` +- `content-type: image/webp` +- `cache-control: public, max-age=31536000, immutable` +- `access-control-allow-origin: https://satisfactory-logistics.xyz` + +Spot-check visually by opening a few tile URLs in a browser: + +- `…/map/v2/0/0/0.webp` shows the entire map shrunk to 256x256. +- `…/map/v2/7/64/64.webp` shows a native-resolution tile near the + center. + +### 4. Versioning + +Tiles are published under a versioned prefix (`/map/v1/`, `/map/v2/`, +…). On every regeneration, publish to the next version and update +`VITE_MAP_TILES_BASE_URL` to match. This avoids any cache invalidation: +old clients keep pointing at the previous version until they reload, +new builds use the new path. + +### One-time DO Spaces setup + +If the Space is being provisioned from scratch: + +1. Create a Space named `satisfactory-logistics-maps` in region `fra1`. +2. Enable the CDN (default TTL is fine, e.g. 3600s). +3. Configure CORS (Dashboard → Spaces → Settings → CORS Configuration): + - Origin: `https://satisfactory-logistics.xyz` and `http://localhost:5173` + - Allowed Methods: `GET`, `HEAD` + - Allowed Headers: `*` + - Max Age: `3600` +4. Create a Spaces access key (Dashboard → API → Spaces access keys) + and configure the `do-satisfactory-logistics-maps` remote in rclone + with those credentials. + +## Environment configuration + +The production CDN URL is the default, so no env var is required. To +point the app at a different CDN (dev bucket, new `/v2/` rollout, +mirror, etc.) set `VITE_MAP_TILES_BASE_URL` at build-time: + +- **Local dev**: add to `.env` (gitignored), then restart Vite. Example: + ``` + VITE_MAP_TILES_BASE_URL=https:///map/v2 + ``` +- **Production (Render.com)**: set the key in the static site's + Environment settings. Because it is consumed at build-time + (`import.meta.env.*`), it must be set **before** the build that + ships the change. diff --git a/scripts/dumpSavegame.ts b/scripts/dumpSavegame.ts new file mode 100644 index 00000000..79b02d36 --- /dev/null +++ b/scripts/dumpSavegame.ts @@ -0,0 +1,260 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { + Parser, + type SatisfactorySave, +} from '@etothepii/satisfactory-file-parser'; + +type IncludeSection = + | 'header' + | 'levels' + | 'objects' + | 'properties' + | 'specialProperties'; + +const ALL_SECTIONS: IncludeSection[] = [ + 'header', + 'levels', + 'objects', + 'properties', + 'specialProperties', +]; + +interface CliArgs { + input: string; + out: string | null; + typeRegex: RegExp | null; + include: Set; + limit: number | null; + pretty: boolean; +} + +function printUsageAndExit(code: number): never { + const lines = [ + 'Usage: tsx scripts/dumpSavegame.ts [options]', + '', + 'Options:', + ' --out= Output file (default .dump.json, "-" for stdout)', + ' --type= Filter entities by typePath regex', + ' --include= Comma list. Choices:', + ' header,levels,objects,properties,specialProperties', + ' Default: all', + ' --limit= Max entities per typePath bucket', + ' --pretty Indent output (default)', + ' --no-pretty Compact output', + ' -h, --help Show this help', + '', + 'Example: dump up to 3 pipeline entities for inspection:', + ' tsx scripts/dumpSavegame.ts ~/save.sav \\', + " --type='Build_(Pipeline|PipelineHyper)' --limit=3", + ]; + process.stderr.write(`${lines.join('\n')}\n`); + process.exit(code); +} + +function parseArgs(argv: string[]): CliArgs { + const positional: string[] = []; + const flags: Record = {}; + + for (const arg of argv) { + if (arg === '-h' || arg === '--help') { + printUsageAndExit(0); + } + if (arg.startsWith('--')) { + const eq = arg.indexOf('='); + if (eq === -1) { + flags[arg.slice(2)] = true; + } else { + flags[arg.slice(2, eq)] = arg.slice(eq + 1); + } + } else { + positional.push(arg); + } + } + + if (positional.length !== 1) { + process.stderr.write('Error: expected exactly one input file path.\n\n'); + printUsageAndExit(1); + } + + const include = new Set(ALL_SECTIONS); + if (typeof flags.include === 'string') { + include.clear(); + for (const raw of flags.include.split(',')) { + const s = raw.trim() as IncludeSection; + if (!ALL_SECTIONS.includes(s)) { + process.stderr.write(`Error: unknown --include section "${s}".\n\n`); + printUsageAndExit(1); + } + include.add(s); + } + } + + let typeRegex: RegExp | null = null; + if (typeof flags.type === 'string') { + try { + typeRegex = new RegExp(flags.type); + } catch (e) { + process.stderr.write( + `Error: invalid --type regex: ${(e as Error).message}\n\n`, + ); + printUsageAndExit(1); + } + } + + let limit: number | null = null; + if (typeof flags.limit === 'string') { + const n = Number.parseInt(flags.limit, 10); + if (!Number.isFinite(n) || n <= 0) { + process.stderr.write('Error: --limit must be a positive integer.\n\n'); + printUsageAndExit(1); + } + limit = n; + } + + const pretty = flags['no-pretty'] !== true; + const out = + typeof flags.out === 'string' && flags.out.length > 0 ? flags.out : null; + + return { + input: positional[0], + out, + typeRegex, + include, + limit, + pretty, + }; +} + +interface DumpStats { + totalEntities: number; + keptEntities: number; + perType: Map; +} + +function buildOutput(save: SatisfactorySave, args: CliArgs) { + const stats: DumpStats = { + totalEntities: 0, + keptEntities: 0, + perType: new Map(), + }; + + const out: Record = { name: save.name }; + if (args.include.has('header')) { + out.header = save.header; + } + + if (args.include.has('levels')) { + const levels: Record = {}; + for (const [levelName, level] of Object.entries(save.levels)) { + const entry: Record = { name: level.name }; + + if (args.include.has('objects')) { + const filtered: unknown[] = []; + for (const obj of level.objects) { + stats.totalEntities++; + const tp = (obj as { typePath?: unknown }).typePath; + if (typeof tp !== 'string') continue; + if (args.typeRegex && !args.typeRegex.test(tp)) continue; + if (args.limit != null) { + const n = stats.perType.get(tp) ?? 0; + if (n >= args.limit) continue; + } + + const cloned = { ...(obj as unknown as Record) }; + if (!args.include.has('properties')) { + delete cloned.properties; + } + if (!args.include.has('specialProperties')) { + delete cloned.specialProperties; + } + filtered.push(cloned); + stats.keptEntities++; + stats.perType.set(tp, (stats.perType.get(tp) ?? 0) + 1); + } + entry.objects = filtered; + } + + levels[levelName] = entry; + } + out.levels = levels; + } + + return { payload: out, stats }; +} + +function jsonReplacer(_key: string, value: unknown): unknown { + if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'number' && value === 0 && 1 / value < 0) return '-0'; + if (value instanceof Uint8Array) { + return { _binary: true, length: value.byteLength }; + } + if (value instanceof ArrayBuffer) { + return { _binary: true, length: value.byteLength }; + } + return value; +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; + return `${(bytes / 1024 / 1024).toFixed(2)} MB`; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (!fs.existsSync(args.input)) { + process.stderr.write(`Error: file not found: ${args.input}\n`); + process.exit(1); + } + + const buf = fs.readFileSync(args.input); + const ab = buf.buffer.slice( + buf.byteOffset, + buf.byteOffset + buf.byteLength, + ) as ArrayBuffer; + const name = path.basename(args.input, path.extname(args.input)); + + let lastLog = 0; + const save = Parser.ParseSave(name, ab, { + onProgressCallback: (progress: number, msg?: string) => { + const now = Date.now(); + if (now - lastLog < 100 && progress < 1) return; + lastLog = now; + const pct = Math.round(progress * 100); + const line = `Parsing... ${String(pct).padStart(3)}% ${msg ?? ''}`; + process.stderr.write(`\r${line.padEnd(80)}`); + }, + }); + process.stderr.write('\n'); + + const { payload, stats } = buildOutput(save, args); + const json = JSON.stringify(payload, jsonReplacer, args.pretty ? 2 : 0); + + const outPath = args.out ?? `${args.input}.dump.json`; + if (outPath === '-') { + process.stdout.write(json); + } else { + fs.writeFileSync(outPath, json); + process.stderr.write(`Wrote ${formatBytes(json.length)} to ${outPath}\n`); + } + + process.stderr.write( + `Entities: ${stats.keptEntities}/${stats.totalEntities} kept`, + ); + if (stats.perType.size > 0 && stats.perType.size <= 20) { + process.stderr.write(' (per typePath:'); + for (const [tp, n] of stats.perType) { + process.stderr.write(`\n ${n.toString().padStart(6)} ${tp}`); + } + process.stderr.write('\n)\n'); + } else { + process.stderr.write(`, ${stats.perType.size} distinct typePaths\n`); + } +} + +main().catch(err => { + process.stderr.write(`\n${err instanceof Error ? err.stack : String(err)}\n`); + process.exit(1); +}); diff --git a/scripts/extractWorldNodes.ts b/scripts/extractWorldNodes.ts new file mode 100644 index 00000000..36dea3fb --- /dev/null +++ b/scripts/extractWorldNodes.ts @@ -0,0 +1,248 @@ +import path from 'node:path'; +import chalk from 'chalk'; +import { parseWorldCollectibles } from './parsers/parseWorldCollectibles'; +import { parseWorldNodes } from './parsers/parseWorldNodes'; + +/** + * Entry point for `npm run extract-world-nodes`. + * + * Runs two parsers back-to-back over the same FModel dump: + * + * 1. {@link parseWorldNodes} — resource nodes, satellites, geysers. + * Reads only `Persistent_Level.json` (vanilla nodes happen to all + * live in the persistent level). + * 2. {@link parseWorldCollectibles} — power slugs, somersloops, + * mercer spheres, hard-drive drop pods, audio tapes, customization + * unlocks. Reads `Persistent_Level.json` + the `_Generated_/` + * external-actor folder (most pickups live in UE5 World Partition + * external actor packages, not in the persistent level itself). + * + * See `scripts/parsers/*.ts` and the README's "Resource Node Data" + * section for the maintainer workflow (how to obtain the FModel dumps). + */ + +interface CliArgs { + input: string; + externalActorsDir: string; + output: string; + collectiblesOutput: string; + dryRun: boolean; + verbose: boolean; + /** Skip the collectibles pass (only emit nodes). */ + nodesOnly: boolean; + /** Skip the nodes pass (only emit collectibles). */ + collectiblesOnly: boolean; +} + +function parseArgs(argv: string[]): CliArgs { + const args: CliArgs = { + input: 'data/Persistent_Level.json', + externalActorsDir: 'data/Persistent_Level/_Generated_', + output: 'src/recipes/WorldResourceNodes.json', + collectiblesOutput: 'src/recipes/WorldCollectibles.json', + dryRun: false, + verbose: false, + nodesOnly: false, + collectiblesOnly: false, + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + switch (arg) { + case '--input': + case '-i': + args.input = argv[++i]; + break; + case '--external-actors': + case '-e': + args.externalActorsDir = argv[++i]; + break; + case '--output': + case '-o': + args.output = argv[++i]; + break; + case '--collectibles-output': + args.collectiblesOutput = argv[++i]; + break; + case '--dry-run': + case '-n': + args.dryRun = true; + break; + case '--verbose': + case '-v': + args.verbose = true; + break; + case '--nodes-only': + args.nodesOnly = true; + break; + case '--collectibles-only': + args.collectiblesOnly = true; + break; + case '--help': + case '-h': + printHelp(); + process.exit(0); + break; + default: + console.error(chalk.red(`Unknown argument: ${arg}`)); + printHelp(); + process.exit(1); + } + } + return args; +} + +function printHelp(): void { + console.log( + `Usage: npm run extract-world-nodes -- [options] + +Options: + -i, --input Persistent_Level JSON dump + (default: data/Persistent_Level.json) + -e, --external-actors External-actors folder for collectibles + (default: data/Persistent_Level/_Generated_) + -o, --output Resource nodes output + (default: src/recipes/WorldResourceNodes.json) + --collectibles-output

Collectibles output + (default: src/recipes/WorldCollectibles.json) + --nodes-only Skip the collectibles pass + --collectibles-only Skip the nodes pass + -n, --dry-run Parse and report, but do not write + -v, --verbose Log every emitted entity (debug) + -h, --help Show this help`, + ); +} + +function main(): void { + const args = parseArgs(process.argv.slice(2)); + + if (!args.collectiblesOnly) { + runNodesPass(args); + } + if (!args.nodesOnly) { + if (!args.collectiblesOnly) console.log(''); + runCollectiblesPass(args); + } +} + +function runNodesPass(args: CliArgs): void { + console.log(chalk.cyan('Extracting world resource nodes…')); + console.log(` input: ${path.resolve(args.input)}`); + console.log(` output: ${path.resolve(args.output)}`); + if (args.dryRun) console.log(chalk.yellow(' (dry run — no files written)')); + console.log(''); + + const result = parseWorldNodes({ + inputPath: args.input, + outputPath: args.output, + dryRun: args.dryRun, + verbose: args.verbose, + }); + + console.log(''); + console.log(chalk.cyan('--- Nodes summary -----------------------')); + console.log(`Emitted: ${chalk.bold(result.emitted.toLocaleString())} nodes`); + console.log('By type:'); + for (const [type, count] of Object.entries(result.byType)) { + console.log(` ${type.padEnd(20)} ${String(count).padStart(5)}`); + } + console.log('By resource:'); + const sortedResources = Object.entries(result.byResource).sort( + (a, b) => b[1] - a[1], + ); + for (const [resource, count] of sortedResources) { + console.log(` ${resource.padEnd(28)} ${String(count).padStart(5)}`); + } + + if (result.skipped.length > 0) { + console.log(''); + console.log( + chalk.yellow(`Skipped ${result.skipped.length} candidate node actor(s):`), + ); + const reasonCounts = result.skipped.reduce>( + (acc, item) => { + acc[item.reason] = (acc[item.reason] ?? 0) + 1; + return acc; + }, + {}, + ); + for (const [reason, count] of Object.entries(reasonCounts)) { + console.log(` ${reason.padEnd(20)} ${String(count).padStart(5)}`); + } + } + + printDiff(result.added, result.removed, 'bundled nodes file'); +} + +function runCollectiblesPass(args: CliArgs): void { + console.log(chalk.cyan('Extracting world collectibles…')); + console.log(` input: ${path.resolve(args.input)}`); + console.log(` +ext: ${path.resolve(args.externalActorsDir)}`); + console.log(` output: ${path.resolve(args.collectiblesOutput)}`); + if (args.dryRun) console.log(chalk.yellow(' (dry run — no files written)')); + console.log(''); + + const result = parseWorldCollectibles({ + persistentLevelPath: args.input, + externalActorsDir: args.externalActorsDir, + outputPath: args.collectiblesOutput, + dryRun: args.dryRun, + verbose: args.verbose, + }); + + console.log(''); + console.log(chalk.cyan('--- Collectibles summary ----------------')); + console.log( + `Emitted: ${chalk.bold(result.emitted.toLocaleString())} collectibles ` + + `(scanned ${result.externalActorFiles.toLocaleString()} external-actor files)`, + ); + console.log('By type:'); + for (const [type, count] of Object.entries(result.byType)) { + console.log(` ${type.padEnd(22)} ${String(count).padStart(5)}`); + } + + if (result.skipped.length > 0) { + console.log(''); + console.log( + chalk.yellow( + `Skipped ${result.skipped.length} candidate collectible actor(s):`, + ), + ); + const reasonCounts = result.skipped.reduce>( + (acc, item) => { + acc[item.reason] = (acc[item.reason] ?? 0) + 1; + return acc; + }, + {}, + ); + for (const [reason, count] of Object.entries(reasonCounts)) { + console.log(` ${reason.padEnd(20)} ${String(count).padStart(5)}`); + } + } + + printDiff(result.added, result.removed, 'bundled collectibles file'); +} + +function printDiff(added: string[], removed: string[], label: string): void { + if (added.length === 0 && removed.length === 0) { + console.log(''); + console.log(chalk.green(`No id-level changes vs current ${label}.`)); + return; + } + console.log(''); + console.log(chalk.cyan(`Diff vs current ${label}:`)); + if (added.length > 0) { + console.log( + chalk.green(` +${added.length} added`) + + (added.length <= 10 ? `: ${added.slice(0, 10).join(', ')}` : ''), + ); + } + if (removed.length > 0) { + console.log( + chalk.red(` -${removed.length} removed`) + + (removed.length <= 10 ? `: ${removed.slice(0, 10).join(', ')}` : ''), + ); + } +} + +main(); diff --git a/scripts/generateMapTiles.ts b/scripts/generateMapTiles.ts new file mode 100644 index 00000000..74c690a6 --- /dev/null +++ b/scripts/generateMapTiles.ts @@ -0,0 +1,184 @@ +import { spawn } from 'node:child_process'; +import { mkdir, readdir, rm, stat } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { join, resolve } from 'node:path'; + +const ROOT = resolve(import.meta.dirname, '..'); +const OUT_DIR = resolve(ROOT, 'dist-map-tiles'); +const MIN_ZOOM = 0; +const DEFAULT_MAX_ZOOM = 6; +const DEFAULT_WEBP_QUALITY = 80; +const DEFAULT_RESAMPLING = 'lanczos'; +const VALID_RESAMPLING = [ + 'average', + 'near', + 'bilinear', + 'cubic', + 'cubicspline', + 'lanczos', + 'antialias', + 'mode', +] as const; + +function expectedTiles(minZoom: number, maxZoom: number): number { + return (Math.pow(4, maxZoom + 1) - Math.pow(4, minZoom)) / 3; +} + +function parseArgs(argv: string[]): { + source: string; + maxZoom: number; + webpQuality: number; + webpLossless: boolean; + resampling: string; +} { + let source: string | undefined; + let maxZoom = DEFAULT_MAX_ZOOM; + let webpQuality = DEFAULT_WEBP_QUALITY; + let webpLossless = false; + let resampling: string = DEFAULT_RESAMPLING; + for (const arg of argv.slice(2)) { + const zoomMatch = arg.match(/^--max-zoom=(\d+)$/); + const qualityMatch = arg.match(/^--webp-quality=(\d+)$/); + const resamplingMatch = arg.match(/^--resampling=(\w+)$/); + if (zoomMatch) { + maxZoom = Number.parseInt(zoomMatch[1], 10); + } else if (qualityMatch) { + webpQuality = Number.parseInt(qualityMatch[1], 10); + } else if (arg === '--webp-lossless') { + webpLossless = true; + } else if (resamplingMatch) { + const value = resamplingMatch[1]; + if (!VALID_RESAMPLING.includes(value as (typeof VALID_RESAMPLING)[number])) { + throw new Error( + `Invalid --resampling=${value}. Valid: ${VALID_RESAMPLING.join(', ')}`, + ); + } + resampling = value; + } else if (arg.startsWith('--')) { + throw new Error(`Unknown flag: ${arg}`); + } else if (source === undefined) { + source = arg; + } else { + throw new Error(`Unexpected extra argument: ${arg}`); + } + } + if (!source) { + throw new Error( + 'Usage: npm run generate-map-tiles -- [--max-zoom=N] [--webp-quality=N] [--webp-lossless] [--resampling=NAME]', + ); + } + return { source, maxZoom, webpQuality, webpLossless, resampling }; +} + +function expandTilde(p: string): string { + if (p === '~') return homedir(); + if (p.startsWith('~/')) return resolve(homedir(), p.slice(2)); + return p; +} + +function run(cmd: string, args: string[]): Promise { + return new Promise((done, fail) => { + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('error', fail); + child.on('exit', code => { + if (code === 0) done(); + else fail(new Error(`${cmd} exited with code ${code}`)); + }); + }); +} + +async function countWebpFiles( + dir: string, +): Promise<{ count: number; bytes: number }> { + let count = 0; + let bytes = 0; + const walk = async (d: string) => { + const entries = await readdir(d, { withFileTypes: true }); + for (const entry of entries) { + const p = join(d, entry.name); + if (entry.isDirectory()) { + await walk(p); + } else if (entry.isFile() && entry.name.endsWith('.webp')) { + count++; + const s = await stat(p); + bytes += s.size; + } + } + }; + await walk(dir); + return { count, bytes }; +} + +function formatBytes(n: number): string { + if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`; + if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(2)} MB`; + if (n >= 1024) return `${(n / 1024).toFixed(2)} KB`; + return `${n} B`; +} + +async function main() { + const { + source: rawSource, + maxZoom, + webpQuality, + webpLossless, + resampling, + } = parseArgs(process.argv); + const source = expandTilde(rawSource); + const expected = expectedTiles(MIN_ZOOM, maxZoom); + + console.log(`Source: ${source}`); + console.log(`Output: ${OUT_DIR}`); + console.log(`Zoom: ${MIN_ZOOM}-${maxZoom} (${expected} tiles expected)`); + console.log( + `Quality: ${webpLossless ? 'WebP lossless' : `WebP q${webpQuality}`}`, + ); + console.log(`Resampling: ${resampling}`); + console.log(''); + + const sourceStat = await stat(source).catch(() => null); + if (!sourceStat?.isFile()) { + throw new Error(`Source file not found or not a file: ${source}`); + } + + // Sanity: ensure GDAL tooling is on PATH. + await run('gdalinfo', ['--version']); + + await rm(OUT_DIR, { recursive: true, force: true }); + await mkdir(OUT_DIR, { recursive: true }); + + await run('gdal2tiles.py', [ + '--profile=raster', + '--xyz', + '-z', + `${MIN_ZOOM}-${maxZoom}`, + '--tiledriver=WEBP', + ...(webpLossless + ? ['--webp-lossless'] + : [`--webp-quality=${webpQuality}`]), + '--processes=8', + `--resampling=${resampling}`, + '-w', + 'none', + source, + OUT_DIR, + ]); + + const { count, bytes } = await countWebpFiles(OUT_DIR); + + console.log(''); + console.log( + `Done: ${count} WebP tiles, ${formatBytes(bytes)} total, in ${OUT_DIR}`, + ); + + if (count !== expected) { + console.warn( + `Warning: expected ${expected} tiles for zoom ${MIN_ZOOM}-${maxZoom}, got ${count}`, + ); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/parseDocs.ts b/scripts/parseDocs.ts index a7d0f43e..f022fe39 100644 --- a/scripts/parseDocs.ts +++ b/scripts/parseDocs.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import { convertDocsImagesToPublic } from './parsers/images/convertDocsImagesToPublic'; import { parseBuildings } from './parsers/parseBuildings'; import { parseItems } from './parsers/parseItems'; +import { parseMilestoneUnlocks } from './parsers/parseMilestoneUnlocks'; import { parseRecipes } from './parsers/parseRecipes'; import { parseSchematics } from './parsers/parseSchematic'; @@ -24,6 +25,9 @@ async function parseDocs() { // Schematics parseSchematics(docsJson); + // Milestone-only unlocks (equipment items the regular pipeline drops) + parseMilestoneUnlocks(docsJson); + // Images if (args.some(a => a === '--with-images')) { console.log('Parsing images...'); diff --git a/scripts/parsers/parseBuildings.ts b/scripts/parsers/parseBuildings.ts index 58c407a0..798d164f 100644 --- a/scripts/parsers/parseBuildings.ts +++ b/scripts/parsers/parseBuildings.ts @@ -1,26 +1,133 @@ import fs from 'fs'; +import kebabCase from 'lodash/kebabCase'; import sortBy from 'lodash/sortBy'; import voca from 'voca'; import { convertImageName } from './images/convertImageName'; import { parseClearanceData } from './parseClearanceData'; import { ParsingContext } from './ParsingContext'; +// Native classes whose buildings should appear in the codex. Manufacturers, +// generators, and the original logistics types were always here. The rest +// were added so milestone "unlocks" entries (railway signalling, drones, +// blueprint designer, hypertubes, conveyor lifts, splitters/mergers, etc.) +// resolve to real building cards instead of falling back to "Other unlocks". +// Pure decoration / structural primitives (foundations, ramps, walls, beams, +// signs, lights, doors) are intentionally omitted, the codex is for +// production planning not construction kit cataloguing. +const INCLUDED_BUILDABLE_NATIVE_CLASSES = [ + // Production + 'FGBuildableManufacturer', + 'FGBuildableManufacturerVariablePower', + 'FGBuildableFactorySimpleProducer', + 'FGBuildableGenerator', + 'FGBuildableGeneratorFuel', + 'FGBuildableGeneratorNuclear', + 'FGBuildableGeneratorGeoThermal', + // Extraction + 'FGBuildableWaterPump', + 'FGBuildableResourceExtractor', + 'FGBuildableFrackingExtractor', + 'FGBuildableFrackingActivator', + // Pipes + 'FGBuildablePipeline', + 'FGBuildablePolePipe', + 'FGBuildablePoleStackable', + 'FGBuildablePipelineJunction', + 'FGBuildablePipelinePump', + 'FGBuildablePipeReservoir', + // Hypertubes + 'FGBuildablePipeHyper', + 'FGBuildablePipeHyperJunction', + // Conveyors and storage + 'FGBuildableConveyorBelt', + 'FGBuildableConveyorLift', + 'FGBuildableStorage', + 'FGBuildableAttachmentSplitter', + 'FGBuildableAttachmentMerger', + 'FGBuildableSplitterSmart', + 'FGBuildableMergerPriority', + // Power + 'FGBuildablePowerStorage', + 'FGBuildablePowerPole', + 'FGBuildablePowerBooster', + 'FGBuildableCircuitSwitch', + 'FGBuildablePriorityPowerSwitch', + // Trains + 'FGBuildableRailroadTrack', + 'FGBuildableRailroadStation', + 'FGBuildableRailroadSignal', + 'FGBuildableRailroadAttachment', + 'FGBuildableTrainPlatformCargo', + 'FGBuildableTrainPlatformEmpty', + // Vehicles + 'FGBuildableDockingStation', + 'FGBuildableDroneStation', + // Misc gameplay structures + 'FGBuildableTradingPost', + 'FGBuildableMAM', + 'FGBuildableRadarTower', + 'FGBuildableJumppad', + 'FGBuildablePortal', + 'FGBuildablePortalSatellite', + 'FGBuildableBlueprintDesigner', + 'FGBuildableResourceSink', + 'FGBuildableResourceSinkShop', + 'FGBuildableSpaceElevator', + // Two-entry groups it's fine to take wholesale. + 'FGBuildableFactory', // Old Jump Pad, Old Tilted Jump Pad, U-Jelly Landing Pad + 'FGBuildablePolePipe', // Pipeline Support, Hypertube Support +]; + +// Specific buildings to include even though their native class isn't in the +// allowlist above. These are gameplay-relevant entries that live in catch-all +// or near-empty native classes we don't want to import wholesale. +// - `Build_LookoutTower_C` lives under the generic `FGBuildable` class +// alongside many decorative / structural items. +// - `Build_PipeHyperStart_C` is the lone entry in `FGPipeHyperStart`. +// - The eight foundation / ramp / wall variants below are referenced by +// Tier 1 "Base Building" milestone unlocks. Their native classes +// (FGBuildableFoundationLightweight / RampLightweight / WallLightweight) +// contain hundreds of decorative variants we don't want to import +// wholesale, so we cherry-pick the milestone-relevant ones. +const INCLUDED_BUILDING_CLASS_NAMES = new Set([ + 'Build_LookoutTower_C', + 'Build_PipeHyperStart_C', + 'Build_Foundation_8x1_01_C', + 'Build_Foundation_8x2_01_C', + 'Build_Foundation_8x4_01_C', + 'Build_Ramp_8x1_01_C', + 'Build_Ramp_8x2_01_C', + 'Build_Ramp_8x4_01_C', + 'Build_Wall_8x4_01_C', + 'Build_Wall_Orange_8x1_C', +]); + +// `nativeClass.NativeClass` is a quoted UE path like +// `/Script/CoreUObject.Class'/Script/FactoryGame.FGBuildableManufacturer'`. +// Pull out just the trailing class name so substring checks don't bleed +// across e.g. `FGBuildableManufacturer` and `FGBuildableManufacturerVariablePower`. +const NativeClassNameRegex = /FactoryGame\.([A-Za-z]+)'/; +function getNativeClassName(nativeClassPath: string | undefined): string | null { + if (!nativeClassPath) return null; + const match = nativeClassPath.match(NativeClassNameRegex); + return match ? match[1] : null; +} + +const INCLUDED_BUILDABLE_NATIVE_CLASS_SET = new Set( + INCLUDED_BUILDABLE_NATIVE_CLASSES, +); + export function parseBuildings(docsJson: any) { const rawBuildings = docsJson.flatMap(nativeClass => { - if ( - nativeClass.NativeClass?.includes('FGBuildableManufacturer') || - nativeClass.NativeClass?.includes('FGBuildableGenerator') || - nativeClass.NativeClass?.includes('FGBuildableWaterPump') || - nativeClass.NativeClass?.includes('FGBuildableResourceExtractor') || - nativeClass.NativeClass?.includes('FGBuildableFrackingExtractor') || - nativeClass.NativeClass?.includes('FGBuildablePipeline') || - nativeClass.NativeClass?.includes('FGBuildableConveyorBelt') - ) - return nativeClass.Classes.map(c => ({ - ...c, - NativeClass: nativeClass.NativeClass, - })); - return []; + const className = getNativeClassName(nativeClass.NativeClass); + if (!className) return []; + const allowWholeClass = INCLUDED_BUILDABLE_NATIVE_CLASS_SET.has(className); + return nativeClass.Classes.flatMap(c => { + if (!allowWholeClass && !INCLUDED_BUILDING_CLASS_NAMES.has(c.ClassName)) { + return []; + } + return [{ ...c, NativeClass: nativeClass.NativeClass }]; + }); }); const buildingDescriptorsImages = docsJson @@ -109,6 +216,33 @@ function parseBuildCosts(docsJson: any) { return costMap; } +/** + * Resolve the public image path for a building. Prefer the descriptor image + * (`buildingDescriptorsImages[Desc__C]`) since that gives us the canonical + * filename `convertImageName` already records elsewhere. A handful of building + * variants (e.g. `Build_BlueprintDesigner_Mk3_C`, the Mk.2/Mk.3 wall outlets) + * have no matching `FGBuildingDescriptor` entry in the docs, so fall back to + * a kebab-cased path derived from the display name so every building still + * has a stable target path under `public/images/game/`. + */ +function resolveBuildingImagePath( + building: any, + buildingDescriptorsImages: Record, +): string | null { + const descriptorName = convertImageName( + buildingDescriptorsImages[ + building.ClassName.replace('Build_', 'Desc_') + ], + ); + if (descriptorName) { + return '/images/game/' + descriptorName; + } + if (!building.mDisplayName) return null; + const slug = kebabCase(building.mDisplayName); + if (!slug) return null; + return `/images/game/${slug}_256.png`; +} + function parseBuilding(building, index, buildingDescriptorsImages, buildCostMap) { console.log(`Importing -> `, building.ClassName); @@ -142,51 +276,64 @@ function parseBuilding(building, index, buildingDescriptorsImages, buildCostMap) ? 1.0 : parseFloat(building.mProductionShardSlotSize), clearance: parseClearanceData(building.mClearanceData), - imagePath: - '/images/game/' + - convertImageName( - buildingDescriptorsImages[ - building.ClassName.replace('Build_', 'Desc_') - ], - ), + imagePath: resolveBuildingImagePath(building, buildingDescriptorsImages), conveyor: parseBuildingBelt(building), pipeline: parseBuildingsPipeline(building), extractor: parseBuildingExtractor(building), buildCost: buildCostMap[building.ClassName] ?? [], - powerGenerator: building.mFuel - ? { - fuels: building.mFuel.map(fuel => { - return { - resource: fuel.mFuelClass, - supplementalResource: fuel.mSupplementalFuelClass, - byproductResource: fuel.mByproductClass, - byproductAmount: fuel.mByproductAmount - ? parseFloat(fuel.mByproductAmount) - : undefined, - }; - }), - powerProduction: parseFloat(building.mPowerProduction), - supplementalLoadAmount: parseFloat(building.mSupplementalLoadAmount), - fuelLoadAmount: parseFloat(building.mFuelLoadAmount), - requiresSupplementalResource: - building.mRequiresSupplementalResource === 'True', - } + powerGenerator: parsePowerGenerator(building), + }; +} + +function parsePowerGenerator(building) { + if (!building.NativeClass?.includes('FGBuildableGenerator')) return undefined; + + const fuels = (building.mFuel ?? []).map(fuel => ({ + resource: fuel.mFuelClass, + supplementalResource: fuel.mSupplementalFuelClass, + byproductResource: fuel.mByproductClass, + byproductAmount: fuel.mByproductAmount + ? parseFloat(fuel.mByproductAmount) : undefined, + })); + + const basePowerProduction = parseFloat(building.mPowerProduction); + const variableFactor = building.mVariablePowerProductionFactor + ? parseFloat(building.mVariablePowerProductionFactor) + : 0; + // Fuel-less generators (e.g. Geothermal) report mPowerProduction=0 and + // model output via mVariablePowerProductionFactor (the cycle average). + const powerProduction = + basePowerProduction > 0 ? basePowerProduction : variableFactor; + + return { + fuels, + powerProduction, + supplementalLoadAmount: parseFloat( + building.mSupplementalLoadAmount ?? '0', + ), + fuelLoadAmount: parseFloat(building.mFuelLoadAmount ?? '0'), + requiresSupplementalResource: + building.mRequiresSupplementalResource === 'True', }; } function parseBuildingBelt(building) { - if (!building.NativeClass.includes('FGBuildableConveyorBelt')) return null; + if (getNativeClassName(building.NativeClass) !== 'FGBuildableConveyorBelt') { + return null; + } return { - isBelt: building.NativeClass.includes('FGBuildableConveyorBelt'), + isBelt: true, speed: parseFloat(building.mSpeed) / 2.0, // Don't know why, but the speed is doubled }; } function parseBuildingsPipeline(building) { - if (!building.NativeClass.includes('FGBuildablePipeline')) return null; + if (getNativeClassName(building.NativeClass) !== 'FGBuildablePipeline') { + return null; + } return { - isPipeline: building.NativeClass.includes('FGBuildablePipeline'), + isPipeline: true, flowRate: parseFloat(building.mFlowLimit) * 60, }; } diff --git a/scripts/parsers/parseMilestoneUnlocks.ts b/scripts/parsers/parseMilestoneUnlocks.ts new file mode 100644 index 00000000..4a6ac17b --- /dev/null +++ b/scripts/parsers/parseMilestoneUnlocks.ts @@ -0,0 +1,182 @@ +/** + * Captures milestone unlocks (`schematic.unlocks` -> `Recipe_*_C` scripts) + * that the regular pipeline drops. Specifically, the equipment-tier items + * (Object Scanner, Chainsaw, Xeno-Basher, Hazmat Suit, JetPack, Hoverpack, + * U-Jelly Landing Pad, etc.) are produced via build-gun / workbench recipes + * which `parseRecipes.ts` filters out, and their descriptors live under + * `FGEquipmentDescriptor` which `parseItems.ts` also filters out. + * + * They're still real, named, icon-bearing things the player unlocks, so the + * codex tier views need to show them. This parser walks the docs once and + * writes a small JSON keyed by recipe-script id with display name, + * description, and (when extractable) imagePath. The codex resolver in + * `tierUnlocks.ts` consults this map after recipes/buildings/items. + */ + +import fs from 'node:fs'; +import _ from 'lodash'; + +interface RawClass { + ClassName: string; + mDisplayName?: string; + mDescription?: string; + mPersistentBigIcon?: string; + mSmallIcon?: string; + mProduct?: string; + mProducedIn?: string; + NativeClass?: string; +} + +interface RawNativeClass { + NativeClass: string; + Classes: RawClass[]; +} + +export interface MilestoneOnlyUnlock { + /** e.g. `Recipe_Chainsaw_C` */ + script: string; + /** Display name (`mDisplayName` of the recipe). */ + name: string; + /** Free-form description, may be empty. */ + description: string; + /** + * Path under `public/images/game/...` for the icon. Falls back to a + * kebab-cased path derived from the display name when the docs don't + * carry a `mPersistentBigIcon` for the underlying descriptor, which + * gives the wiki image importer a target to write to. + */ + imagePath: string | null; +} + +const ProductClassRegex = /\.([^']+_C)'/; + +function descriptorOf(productString: string | undefined): string | null { + if (!productString) return null; + const match = productString.match(ProductClassRegex); + return match ? match[1] : null; +} + +function buildImagePath( + descriptorClass: string | null, + descriptorIcons: Record, + fallbackName: string, +): string | null { + if (descriptorClass) { + const iconResource = descriptorIcons[descriptorClass]; + if (iconResource) { + const split = iconResource.split('.'); + const fileBase = (split[1] ?? split[0]) + .replace('IconDesc_', '') + .replace(/_(256|512)$/, ''); + const slug = _.kebabCase(fileBase); + if (slug) return `/images/game/${slug}_256.png`; + } + } + if (!fallbackName) return null; + const slug = _.kebabCase(fallbackName); + return slug ? `/images/game/${slug}_256.png` : null; +} + +export function parseMilestoneUnlocks(docsJson: RawNativeClass[]) { + const recipes = new Map(); + const equipmentDescriptors = new Set(); + const descriptorIcons: Record = {}; + const schematicScriptsByMilestone = new Set(); + + for (const nc of docsJson) { + if (!nc.NativeClass) continue; + const isRecipe = nc.NativeClass.includes('FGRecipe'); + const isEquipmentDesc = nc.NativeClass.includes('FGEquipmentDescriptor'); + const isAnyDesc = + nc.NativeClass.includes('Descriptor') || + nc.NativeClass.includes('FGAmmoType'); + const isSchematic = nc.NativeClass.includes('Schematic'); + + for (const c of nc.Classes ?? []) { + if (!c.ClassName) continue; + if (isRecipe) recipes.set(c.ClassName, c); + if (isEquipmentDesc) equipmentDescriptors.add(c.ClassName); + if (isAnyDesc && c.mPersistentBigIcon) { + descriptorIcons[c.ClassName] = c.mPersistentBigIcon; + } + if (isSchematic) { + // Pull every Recipe script unlocked by every milestone, regardless of + // whether it's already covered by FactoryRecipes/Items/Buildings. + const unlocks = (c as any).mUnlocks as Array | undefined; + if (!unlocks) continue; + for (const u of unlocks) { + if (!u?.Class?.includes('Recipe')) continue; + for (const m of (u.mRecipes ?? '').matchAll( + /"\/Script[^,]*\.([^']+)/g, + )) { + schematicScriptsByMilestone.add(m[1]); + } + } + } + } + } + + // Existing exports we want to *not* duplicate. Recipes/items/buildings + // already known to the rest of the app are out of scope here. + const knownRecipeIds = new Set( + JSON.parse( + fs.readFileSync('./src/recipes/FactoryRecipes.json', 'utf8'), + ).map((r: { id: string }) => r.id), + ); + const knownItemIds = new Set( + JSON.parse(fs.readFileSync('./src/recipes/FactoryItems.json', 'utf8')).map( + (i: { id: string }) => i.id, + ), + ); + const knownBuildingIds = new Set( + JSON.parse( + fs.readFileSync('./src/recipes/FactoryBuildings.json', 'utf8'), + ).map((b: { id: string }) => b.id), + ); + + const result: MilestoneOnlyUnlock[] = []; + + for (const script of schematicScriptsByMilestone) { + if (knownRecipeIds.has(script)) continue; + + const recipe = recipes.get(script); + if (!recipe) continue; // Without a recipe we have no name; skip silently. + + const productClass = descriptorOf(recipe.mProduct); + const stem = script.replace(/^Recipe_/, '').replace(/_C$/, ''); + + // Skip if the product is already a known building or item; those are + // surfaced via the existing resolver paths in tierUnlocks.ts. + if (productClass && knownBuildingIds.has(productClass)) continue; + if (productClass && knownItemIds.has(productClass)) continue; + // Some recipes resolve to Build__C via the heuristic in tierUnlocks + // even when their product class is something else, so also skip those. + if (knownBuildingIds.has(`Build_${stem}_C`)) continue; + // Only ship things that are actually equipment, otherwise the codex would + // start listing every BuildGun recipe (foundations, walls, ramps, ...). + if (!productClass || !equipmentDescriptors.has(productClass)) continue; + + const name = + recipe.mDisplayName?.trim() || + // recipes always have a display name in practice, but keep a safety net + stem.replace(/([a-z])([A-Z])/g, '$1 $2').trim(); + + result.push({ + script, + name, + description: recipe.mDescription?.trim() ?? '', + imagePath: buildImagePath(productClass, descriptorIcons, name), + }); + } + + result.sort((a, b) => a.name.localeCompare(b.name)); + + fs.writeFileSync( + './src/recipes/FactoryMilestoneOnlyUnlocks.json', + JSON.stringify(result, null, 2), + ); + + console.log( + `Wrote ${result.length} milestone-only unlocks to FactoryMilestoneOnlyUnlocks.json`, + ); +} diff --git a/scripts/parsers/parseWorldCollectibles.ts b/scripts/parsers/parseWorldCollectibles.ts new file mode 100644 index 00000000..f1a632ab --- /dev/null +++ b/scripts/parsers/parseWorldCollectibles.ts @@ -0,0 +1,453 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import sortBy from 'lodash/sortBy'; +import type { + CollectibleType, + DropPodUnlockCost, +} from '../../src/recipes/WorldCollectibles'; + +/** + * Extracts pickup-style world entities (power slugs, somersloops, + * mercer spheres, hard-drive drop pods, audio tapes, customization + * unlocks) from a FModel JSON dump of `Persistent_Level.umap` together + * with its `__ExternalActors__` companion folder. + * + * Where the data lives: + * + * - `data/Persistent_Level.json` is the main level dump. In Satisfactory + * 1.0 (UE5 World Partition), only a small subset of pickups was + * authored in the persistent level itself: most notably the 29 + * pre-World-Partition `BP_DropPod_C` actors and 3 `BP_TapePickup_C` + * actors. + * - `data/Persistent_Level/_Generated_/*.json` is the per-actor "external + * actor" dump (one file per actor, ~4,200 files for the vanilla map) + * exported separately from FModel under + * `Persistent_Level/__ExternalActors__/`. Power slugs, somersloops, + * mercer spheres, the bulk of drop pods, and the lone customization + * unlock all live here. + * + * This parser walks both sources, applies the same actor-shape → + * collectible-type mapping, and emits one combined dataset. The matching + * algorithm (actor → its `RootComponent`'s `RelativeLocation`) is the + * same two-pass technique used by `parseWorldNodes.ts`, just applied + * file-by-file because each external-actor file is self-contained + * (component and actor live in the same JSON). + * + * Run via `npm run extract-world-nodes` (entry point in + * `scripts/extractWorldNodes.ts`). + */ + +interface ObjectRef { + ObjectName?: string; + ObjectPath?: string; +} + +interface UnrealVector { + X?: number; + Y?: number; + Z?: number; +} + +interface UnrealRotator { + Pitch?: number; + Yaw?: number; + Roll?: number; +} + +interface DropPodCost { + CostType?: string; + ItemCost?: { + ItemClass?: ObjectRef; + Amount?: number; + }; +} + +interface UnrealEntry { + Type?: string; + Name?: string; + Class?: string; + Outer?: ObjectRef; + Properties?: { + RootComponent?: ObjectRef; + RelativeLocation?: UnrealVector; + RelativeRotation?: UnrealRotator; + mItemPickupGuid?: string; + mDropPodGuid?: string; + mUnlockCost?: DropPodCost; + mSchematic?: ObjectRef; + [key: string]: unknown; + }; +} + +interface RawCollectibleOutput { + id: string; + type: CollectibleType; + classPath?: string; + pickupGuid?: string; + x: number; + y: number; + z: number; + rotation?: number; + unlockCost?: DropPodUnlockCost[]; + schematicId?: string; +} + +/** + * Maps actor `Type` (e.g. `BP_Crystal_C`) to our internal + * {@link CollectibleType} discriminator. Only types listed here are + * extracted; everything else (resource nodes, plant life, debris, + * spawners, water…) is silently ignored. + * + * `BP_Crystal_C` is the Mk1 (blue) slug — the variant suffix is empty + * to match the actor's class name. + */ +const COLLECTIBLE_ACTOR_TYPES: Record = { + BP_Crystal_C: 'slugMk1', + BP_Crystal_mk2_C: 'slugMk2', + BP_Crystal_mk3_C: 'slugMk3', + BP_WAT1_C: 'somersloop', + BP_WAT2_C: 'mercerSphere', + BP_DropPod_C: 'hardDrive', + BP_TapePickup_C: 'audioTape', + BP_UnlockPickup_Customization_C: 'customizationUnlock', +}; + +export interface ParseWorldCollectiblesOptions { + /** `data/Persistent_Level.json` (main dump). */ + persistentLevelPath: string; + /** + * `data/Persistent_Level/_Generated_/` (FModel `__ExternalActors__` + * export, one file per actor). Optional: when missing the parser + * still emits the few collectibles authored in the persistent level + * itself (29 drop pods + 3 audio tapes for vanilla 1.0). + */ + externalActorsDir?: string; + /** Output bundled JSON path. */ + outputPath: string; + dryRun?: boolean; + /** Print one line per emitted collectible (very chatty, debug only). */ + verbose?: boolean; +} + +export interface ParseWorldCollectiblesResult { + emitted: number; + byType: Record; + skipped: { reason: string; id: string }[]; + added: string[]; + removed: string[]; + /** + * How many external-actor files were inspected. Useful for sanity- + * checking that the maintainer pointed `--collectibles-dir` at the + * right folder. + */ + externalActorFiles: number; +} + +export function parseWorldCollectibles( + options: ParseWorldCollectiblesOptions, +): ParseWorldCollectiblesResult { + const { + persistentLevelPath, + externalActorsDir, + outputPath, + dryRun = false, + verbose = false, + } = options; + + const byType: Record = { + slugMk1: 0, + slugMk2: 0, + slugMk3: 0, + somersloop: 0, + mercerSphere: 0, + hardDrive: 0, + audioTape: 0, + customizationUnlock: 0, + }; + const skipped: { reason: string; id: string }[] = []; + const collectibles: RawCollectibleOutput[] = []; + + // --- Source 1: Persistent_Level.json (the few pre-WP pickups) ---------- + if (fs.existsSync(persistentLevelPath)) { + log(`Reading ${persistentLevelPath}…`); + const raw = fs.readFileSync(persistentLevelPath, 'utf8'); + log(`Parsing ${(raw.length / 1_000_000).toFixed(1)}MB of JSON…`); + const entries = JSON.parse(raw) as UnrealEntry[]; + log(` -> ${entries.length.toLocaleString()} entries`); + extractFromEntries(entries, collectibles, skipped, verbose); + } else { + log(`(no ${persistentLevelPath} — skipping main level dump)`); + } + + // --- Source 2: per-actor external-actor files -------------------------- + let externalActorFiles = 0; + if (externalActorsDir && fs.existsSync(externalActorsDir)) { + log(`Scanning external actors in ${externalActorsDir}…`); + const files = fs + .readdirSync(externalActorsDir) + .filter(name => name.endsWith('.json')) + .map(name => path.join(externalActorsDir, name)); + externalActorFiles = files.length; + log(` -> ${files.length.toLocaleString()} external-actor files`); + + let processed = 0; + for (const file of files) { + try { + const raw = fs.readFileSync(file, 'utf8'); + const entries = JSON.parse(raw) as UnrealEntry[]; + extractFromEntries(entries, collectibles, skipped, verbose); + } catch (err) { + skipped.push({ + reason: 'parse-error', + id: `${path.basename(file)}: ${(err as Error).message}`, + }); + } + processed++; + if (processed % 500 === 0) { + log(` …${processed.toLocaleString()} / ${files.length.toLocaleString()}`); + } + } + } else if (externalActorsDir) { + log( + `(${externalActorsDir} not found; skipping external-actor scan — ` + + 'expect only the ~32 pre-WP collectibles)', + ); + } + + // --- Tally + sort + diff ---------------------------------------------- + for (const c of collectibles) byType[c.type]++; + + const sorted = sortBy(collectibles, ['type', 'id']); + + const previousIds = readPreviousIds(outputPath); + const currentIds = new Set(sorted.map(c => c.id)); + const added = [...currentIds].filter(id => !previousIds.has(id)); + const removed = [...previousIds].filter(id => !currentIds.has(id)); + + // --- Write ------------------------------------------------------------- + if (!dryRun) { + fs.writeFileSync(outputPath, formatJson(sorted)); + log(`Wrote ${sorted.length.toLocaleString()} collectibles to ${outputPath}`); + } else { + log('(dry-run) skipping write'); + } + + return { + emitted: sorted.length, + byType, + skipped, + added, + removed, + externalActorFiles, + }; +} + +/** + * Two-pass match within a single entries array (either the whole + * persistent level or one external-actor file). Pass 1 collects + * collectible-shaped actors keyed by their UE reference; pass 2 walks + * the components and copies each actor's `RootComponent` transform. + * + * Each external-actor file is small and self-contained, so iterating + * twice is fine (and keeps the code symmetric with how + * `parseWorldNodes.ts` handles the persistent level). + */ +function extractFromEntries( + entries: UnrealEntry[], + out: RawCollectibleOutput[], + skipped: { reason: string; id: string }[], + verbose: boolean, +): void { + interface ActorMeta { + name: string; + type: CollectibleType; + classPath: string; + rootComponentRef?: string; + pickupGuid?: string; + unlockCost?: DropPodUnlockCost[]; + schematicId?: string; + } + + const actorsByRef = new Map(); + + for (const entry of entries) { + const type = entry.Type; + if (!type || !(type in COLLECTIBLE_ACTOR_TYPES)) continue; + const name = entry.Name; + if (!name) { + skipped.push({ reason: 'missing-name', id: type }); + continue; + } + + const collectibleType = COLLECTIBLE_ACTOR_TYPES[type]; + const props = entry.Properties ?? {}; + const classPath = parseClassFromActor(entry) ?? type; + + // The `Outer` ref points at the level itself; child components + // identify their owner with `':PersistentLevel.'`. + // Pre-build the same key for the actor lookup. + const ownerScope = parseLevelScope(entry.Outer?.ObjectName) ?? 'PersistentLevel'; + const actorRef = `${type}'Persistent_Level:${ownerScope}.${name}'`; + + actorsByRef.set(actorRef, { + name, + type: collectibleType, + classPath, + rootComponentRef: props.RootComponent?.ObjectName, + pickupGuid: pickGuid(props), + unlockCost: parseUnlockCost(props.mUnlockCost), + schematicId: parseObjectRefId(props.mSchematic), + }); + } + + if (actorsByRef.size === 0) return; + + // Pass 2: bind each actor to its RootComponent transform. + interface Transform { + x: number; + y: number; + z: number; + yaw?: number; + } + const transformsByActor = new Map(); + + for (const entry of entries) { + const type = entry.Type; + if ( + type !== 'BoxComponent' && + type !== 'SceneComponent' && + type !== 'StaticMeshComponent' && + type !== 'SphereComponent' + ) { + continue; + } + const ownerRef = entry.Outer?.ObjectName; + if (!ownerRef) continue; + const actor = actorsByRef.get(ownerRef); + if (!actor) continue; + + if (actor.rootComponentRef) { + const expectedSuffix = actor.rootComponentRef + .split('.') + .pop() + ?.replace(/'$/, ''); + if (expectedSuffix && entry.Name !== expectedSuffix) continue; + } + + const loc = entry.Properties?.RelativeLocation; + if (!loc || loc.X == null || loc.Y == null) continue; + + transformsByActor.set(ownerRef, { + x: Math.round(loc.X), + y: Math.round(loc.Y), + z: Math.round(loc.Z ?? 0), + yaw: + entry.Properties?.RelativeRotation?.Yaw != null + ? round2(entry.Properties.RelativeRotation.Yaw) + : undefined, + }); + } + + for (const [actorRef, actor] of actorsByRef) { + const xform = transformsByActor.get(actorRef); + if (!xform) { + skipped.push({ reason: 'missing-transform', id: actor.name }); + continue; + } + + out.push({ + id: actor.name, + type: actor.type, + classPath: actor.classPath, + pickupGuid: actor.pickupGuid, + x: xform.x, + y: xform.y, + z: xform.z, + rotation: xform.yaw, + unlockCost: actor.unlockCost?.length ? actor.unlockCost : undefined, + schematicId: actor.schematicId, + }); + + if (verbose) { + log( + ` + ${actor.type.padEnd(20)} ${actor.name} ` + + `(${xform.x}, ${xform.y}, ${xform.z})`, + ); + } + } +} + +/* ---------------- helpers ---------------- */ + +function parseObjectRefId(ref: ObjectRef | undefined): string | undefined { + // `BlueprintGeneratedClass'Schematic_Huntdown_C'` -> `Schematic_Huntdown_C` + const name = ref?.ObjectName; + if (!name) return undefined; + const m = name.match(/'([^']+)'/); + return m?.[1]; +} + +function parseClassFromActor(entry: UnrealEntry): string | undefined { + const cls = entry.Class; + if (!cls) return undefined; + const m = cls.match(/\.([^.']+)'$/); + return m?.[1] ?? entry.Type; +} + +/** + * Pulls the `Level` segment out of an Outer ref. Most actors live in + * `PersistentLevel`, but external-actor files may use a different + * sublevel name — e.g. + * `Level'Persistent_Level:PersistentLevel'`. We extract the bit + * between `:` and the trailing quote so the actor reference we + * synthesize matches what the components store in their `Outer`. + */ +function parseLevelScope(name: string | undefined): string | undefined { + if (!name) return undefined; + const m = name.match(/:([^']+)'/); + return m?.[1]; +} + +function pickGuid( + props: NonNullable, +): string | undefined { + // Pickups carry `mItemPickupGuid`; drop pods carry `mDropPodGuid`. + // Either is a stable cross-run identity (UE persists them in the + // savefile). + return props.mItemPickupGuid ?? props.mDropPodGuid; +} + +function parseUnlockCost( + cost: DropPodCost | undefined, +): DropPodUnlockCost[] | undefined { + if (!cost?.ItemCost) return undefined; + const item = parseObjectRefId(cost.ItemCost.ItemClass); + const amount = cost.ItemCost.Amount; + if (!item || amount == null) return undefined; + return [{ item, amount }]; +} + +function readPreviousIds(outputPath: string): Set { + if (!fs.existsSync(outputPath)) return new Set(); + try { + const raw = JSON.parse(fs.readFileSync(outputPath, 'utf8')) as { + id: string; + }[]; + return new Set(raw.map(c => c.id)); + } catch (err) { + log(`WARN: could not read existing ${outputPath}: ${(err as Error).message}`); + return new Set(); + } +} + +function formatJson(value: unknown): string { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function round2(n: number): number { + return Math.round(n * 100) / 100; +} + +function log(msg: string): void { + console.log(msg); +} diff --git a/scripts/parsers/parseWorldNodes.ts b/scripts/parsers/parseWorldNodes.ts new file mode 100644 index 00000000..f91ca220 --- /dev/null +++ b/scripts/parsers/parseWorldNodes.ts @@ -0,0 +1,438 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import sortBy from 'lodash/sortBy'; +import type { + Purity, + WorldResourceNodeType, +} from '../../src/recipes/WorldResourceNodes'; + +/** + * Extracts world resource node placements from a FModel JSON dump of + * `Persistent_Level.umap`. + * + * The dump is a flat array of every actor and component packed into the + * map's persistent level. Resource nodes themselves carry the resource + * class and purity, but their world position lives on a child component + * (the `RootComponent`), so this parser does a two-pass match: first + * collect every node-shaped actor by its full UE reference, then walk + * the components and copy each actor's transform from the matching + * `RootComponent`. + * + * Run via `npm run extract-world-nodes` (entry point in + * `scripts/extractWorldNodes.ts`). + */ + +interface ObjectRef { + ObjectName?: string; + ObjectPath?: string; +} + +interface UnrealVector { + X?: number; + Y?: number; + Z?: number; +} + +interface UnrealRotator { + Pitch?: number; + Yaw?: number; + Roll?: number; +} + +interface UnrealEntry { + Type?: string; + Name?: string; + Outer?: ObjectRef; + Properties?: { + mResourceClass?: ObjectRef; + mOverrideResourceClass?: ObjectRef; + mPurity?: string; + mResourcesLeft?: number; + mCore?: ObjectRef; + RootComponent?: ObjectRef; + RelativeLocation?: UnrealVector; + RelativeRotation?: UnrealRotator; + [key: string]: unknown; + }; +} + +interface RawNodeOutput { + id: string; + resource: string; + purity: Purity; + classPath: string; + nodeType: WorldResourceNodeType; + displayName?: string; + x: number; + y: number; + z: number; + rotation?: number; +} + +const NODE_ACTOR_TYPES: Record = { + BP_ResourceNode_C: 'node', + BP_ResourceDeposit_C: 'deposit', + BP_FrackingCore_C: 'frackingCore', + BP_FrackingSatellite_C: 'frackingSatellite', + BP_ResourceNodeGeyser_C: 'geyser', +}; + +/** + * Geysers spawn no `mResourceClass` because the actor *is* the + * resource — they're geothermal generator anchors. The current item DB + * doesn't expose geothermal energy as a `Desc_*_C`, so we tag them with + * a synthetic id; the renderer can decide whether to show them. + */ +const GEYSER_RESOURCE_ID = 'Desc_GeothermalEnergy_C'; + +/** + * Map FModel's UE `EResourcePurity` enum to our app's lowercase token. + * Note `RP_Inpure` — that's the in-game spelling and not a typo on our + * side. UE serialises unset enum-by-value properties by omitting them + * entirely, and the in-game default for resource nodes is `Normal`, so + * a missing `mPurity` means a normal-purity node. + */ +const PURITY_MAP: Record = { + RP_Inpure: 'impure', + RP_Impure: 'impure', + RP_Normal: 'normal', + RP_Pure: 'pure', +}; + +interface FactoryItem { + id: string; + displayName?: string; + name?: string; +} + +export interface ParseWorldNodesOptions { + inputPath: string; + outputPath: string; + itemsPath?: string; + dryRun?: boolean; + /** Print one line per emitted node (very chatty, debug only). */ + verbose?: boolean; +} + +export interface ParseWorldNodesResult { + emitted: number; + skipped: { reason: string; id: string }[]; + byType: Record; + byResource: Record; + added: string[]; + removed: string[]; +} + +export function parseWorldNodes( + options: ParseWorldNodesOptions, +): ParseWorldNodesResult { + const { + inputPath, + outputPath, + itemsPath = path.resolve('src/recipes/FactoryItems.json'), + dryRun = false, + verbose = false, + } = options; + + const absInput = path.resolve(inputPath); + if (!fs.existsSync(absInput)) { + throw new Error(`Persistent_Level export not found at ${absInput}`); + } + + log(`Reading ${absInput}…`); + const raw = fs.readFileSync(absInput, 'utf8'); + log(`Parsing ${(raw.length / 1_000_000).toFixed(1)}MB of JSON…`); + const entries = JSON.parse(raw) as UnrealEntry[]; + log(` -> ${entries.length.toLocaleString()} entries`); + + const items = loadItemIndex(itemsPath); + + // ---- Pass 1: collect node-shaped actors -------------------------------- + // Keyed by the actor's typed UE reference (e.g. + // `BP_ResourceNode_C'Persistent_Level:PersistentLevel.BP_ResourceNode100'`) + // because that's exactly what child components store in their + // `Outer.ObjectName`. + interface ActorMeta { + name: string; + nodeType: WorldResourceNodeType; + resource: string; + purity: Purity; + classPath: string; + rootComponentRef?: string; + skipReason?: string; + } + + const actorsByRef = new Map(); + let nodeActorTotal = 0; + const skipped: { reason: string; id: string }[] = []; + const unknownResources = new Set(); + + for (const entry of entries) { + const type = entry.Type; + if (!type || !(type in NODE_ACTOR_TYPES)) continue; + nodeActorTotal++; + + const name = entry.Name; + if (!name) { + skipped.push({ reason: 'missing-name', id: type }); + continue; + } + + const nodeType = NODE_ACTOR_TYPES[type]; + const props = entry.Properties ?? {}; + + let resource: string | undefined; + if (nodeType === 'geyser') { + resource = GEYSER_RESOURCE_ID; + } else { + const resRef = + nodeType === 'deposit' + ? props.mOverrideResourceClass + : props.mResourceClass; + resource = parseObjectRefId(resRef); + } + + if (!resource) { + skipped.push({ reason: 'missing-resource', id: name }); + continue; + } + + if ( + nodeType !== 'geyser' && + items.size > 0 && + !items.has(resource) + ) { + // Don't skip — modded resources may appear here legitimately. + // Just warn so the maintainer can decide whether to extend the + // item DB before committing. + unknownResources.add(resource); + } + + const purity = parsePurity(props.mPurity, nodeType); + const classPath = parseClassFromActor(entry) ?? `${type}`; + const rootComponentRef = props.RootComponent?.ObjectName; + + const actorRef = `${type}'Persistent_Level:PersistentLevel.${name}'`; + actorsByRef.set(actorRef, { + name, + nodeType, + resource, + purity, + classPath, + rootComponentRef, + }); + } + + log( + `Pass 1: ${nodeActorTotal.toLocaleString()} candidate node actors -> ` + + `${actorsByRef.size.toLocaleString()} kept (${skipped.length} skipped)`, + ); + + // ---- Pass 2: bind each actor to its RootComponent transform ------------ + // Components may appear *before* their owning actor in the dump (the + // file isn't ordered), so a single linear pass after pass 1 is safe. + interface Transform { + x: number; + y: number; + z: number; + yaw?: number; + } + + const transformsByActor = new Map(); + let componentMatches = 0; + + for (const entry of entries) { + const type = entry.Type; + if ( + type !== 'BoxComponent' && + type !== 'SceneComponent' && + type !== 'StaticMeshComponent' + ) { + continue; + } + const ownerRef = entry.Outer?.ObjectName; + if (!ownerRef) continue; + const actor = actorsByRef.get(ownerRef); + if (!actor) continue; + + // Only the component the actor calls its `RootComponent` carries + // the world transform we want; ignore decals / particle systems / + // other auxiliary children. + if (actor.rootComponentRef) { + const componentName = entry.Name; + const expected = actor.rootComponentRef; + // expected looks like `BoxComponent'…BP_ResourceNode100.BoxComponent_0'` + // — match on the suffix after the last dot. + const expectedSuffix = expected.split('.').pop()?.replace(/'$/, ''); + if (expectedSuffix && componentName !== expectedSuffix) continue; + } + + const loc = entry.Properties?.RelativeLocation; + if (!loc || loc.X == null || loc.Y == null) continue; + + transformsByActor.set(ownerRef, { + x: Math.round(loc.X), + y: Math.round(loc.Y), + z: Math.round(loc.Z ?? 0), + yaw: + entry.Properties?.RelativeRotation?.Yaw != null + ? round2(entry.Properties.RelativeRotation.Yaw) + : undefined, + }); + componentMatches++; + } + + log(`Pass 2: ${componentMatches.toLocaleString()} component matches`); + + // ---- Build output ------------------------------------------------------ + const byType: Record = { + node: 0, + deposit: 0, + frackingCore: 0, + frackingSatellite: 0, + geyser: 0, + }; + const byResource: Record = {}; + const output: RawNodeOutput[] = []; + + for (const [actorRef, actor] of actorsByRef) { + const xform = transformsByActor.get(actorRef); + if (!xform) { + skipped.push({ reason: 'missing-transform', id: actor.name }); + continue; + } + + const displayName = items.get(actor.resource)?.displayName; + output.push({ + id: actor.name, + resource: actor.resource, + purity: actor.purity, + classPath: actor.classPath, + nodeType: actor.nodeType, + displayName, + x: xform.x, + y: xform.y, + z: xform.z, + rotation: xform.yaw, + }); + byType[actor.nodeType]++; + byResource[actor.resource] = (byResource[actor.resource] ?? 0) + 1; + + if (verbose) { + log( + ` + ${actor.name} ${actor.resource} ${actor.purity} ` + + `(${xform.x}, ${xform.y}, ${xform.z})`, + ); + } + } + + const sorted = sortBy(output, ['nodeType', 'resource', 'id']); + + // ---- Diff vs current bundled file -------------------------------------- + const previousIds = readPreviousIds(outputPath); + const currentIds = new Set(sorted.map(n => n.id)); + const added = [...currentIds].filter(id => !previousIds.has(id)); + const removed = [...previousIds].filter(id => !currentIds.has(id)); + + // ---- Write ------------------------------------------------------------- + if (!dryRun) { + fs.writeFileSync(outputPath, formatJson(sorted)); + log(`Wrote ${sorted.length.toLocaleString()} nodes to ${outputPath}`); + } else { + log('(dry-run) skipping write'); + } + + if (unknownResources.size > 0) { + log( + `WARN: ${unknownResources.size} resource class(es) not in FactoryItems.json: ` + + [...unknownResources].sort().join(', '), + ); + } + + return { + emitted: sorted.length, + skipped, + byType, + byResource, + added, + removed, + }; +} + +/* ---------------- helpers ---------------- */ + +function parseObjectRefId(ref: ObjectRef | undefined): string | undefined { + // `BlueprintGeneratedClass'Desc_OreIron_C'` -> `Desc_OreIron_C` + const name = ref?.ObjectName; + if (!name) return undefined; + const m = name.match(/'([^']+)'/); + return m?.[1]; +} + +function parseClassFromActor(entry: UnrealEntry): string | undefined { + // The actor entry has a top-level `Class` like + // `BlueprintGeneratedClass'/Game/FactoryGame/Resource/BP_ResourceNode.BP_ResourceNode_C'`. + // We expose the suffix (`BP_ResourceNode_C`) since that's what + // savegame-side parsers also see. + const cls = (entry as unknown as { Class?: string }).Class; + if (!cls) return undefined; + const m = cls.match(/\.([^.']+)'$/); + return m?.[1] ?? entry.Type; +} + +function parsePurity( + raw: string | undefined, + nodeType: WorldResourceNodeType, +): Purity { + if (!raw) { + // Deposits don't have purity in-game; treat as normal so the + // enum-driven UI doesn't choke. + return 'normal'; + } + const stripped = raw.replace(/^EResourcePurity::/, ''); + const mapped = PURITY_MAP[stripped]; + if (!mapped) { + log( + `WARN: unknown purity "${raw}" for ${nodeType}, defaulting to normal`, + ); + return 'normal'; + } + return mapped; +} + +function loadItemIndex(itemsPath: string): Map { + if (!fs.existsSync(itemsPath)) { + log(`(no FactoryItems.json at ${itemsPath}; skipping resource validation)`); + return new Map(); + } + const items = JSON.parse(fs.readFileSync(itemsPath, 'utf8')) as FactoryItem[]; + const map = new Map(); + for (const item of items) map.set(item.id, item); + return map; +} + +function readPreviousIds(outputPath: string): Set { + if (!fs.existsSync(outputPath)) return new Set(); + try { + const raw = JSON.parse(fs.readFileSync(outputPath, 'utf8')) as { + id: string; + }[]; + return new Set(raw.map(n => n.id)); + } catch (err) { + log(`WARN: could not read existing ${outputPath}: ${(err as Error).message}`); + return new Set(); + } +} + +function formatJson(value: unknown): string { + // Match the existing file's 2-space, trailing-newline style. + return `${JSON.stringify(value, null, 2)}\n`; +} + +function round2(n: number): number { + return Math.round(n * 100) / 100; +} + +function log(msg: string): void { + console.log(msg); +} diff --git a/src/App.tsx b/src/App.tsx index 661caee0..185c5d0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import { v8CssVariablesResolver, } from '@mantine/core'; import '@mantine/core/styles.css'; +import '@mantine/dropzone/styles.css'; import { Notifications } from '@mantine/notifications'; import '@mantine/notifications/styles.css'; @@ -40,6 +41,7 @@ import { GamesRoutes } from './games/page/GamesRoutes'; import { AppLayout } from './layout/AppLayout'; import { PWAUpdatePrompt } from './pwa/PWAUpdatePrompt'; import { FactoryRoutes } from './routes/FactoriesRoutes'; +import { MapRoutes } from './routes/MapRoutes'; import { theme } from './theme'; import { ToolsRoutes } from './tools/page/ToolsRoutes'; @@ -106,6 +108,13 @@ const router = createBrowserRouter( throw useRouteError(); }, }, + { + path: '/map/*', + element: , + ErrorBoundary: () => { + throw useRouteError(); + }, + }, { path: '*', element: , diff --git a/src/codex/CodexPage.tsx b/src/codex/CodexPage.tsx index 89de893b..c4ab77cb 100644 --- a/src/codex/CodexPage.tsx +++ b/src/codex/CodexPage.tsx @@ -10,12 +10,14 @@ import { import { IconBox, IconBuildingFactory2, + IconStairsUp, IconToolsKitchen2, } from '@tabler/icons-react'; import { Link } from 'react-router-dom'; import { AllFactoryBuildings } from '@/recipes/FactoryBuilding'; import { AllFactoryItems } from '@/recipes/FactoryItem'; import { AllFactoryRecipes } from '@/recipes/FactoryRecipe'; +import { TierTotals } from './tiers/tierUnlocks'; const categories = [ { @@ -47,6 +49,16 @@ const categories = [ 'View all recipes including default, alternate, and MAM research recipes.', count: AllFactoryRecipes.length, }, + { + id: 'tiers', + to: '/codex/tiers', + icon: IconStairsUp, + color: 'grape', + title: 'Tiers', + description: + 'Walk the HUB progression and see what each tier and milestone unlocks.', + count: TierTotals.tiers, + }, ]; export function CodexPage() { @@ -59,7 +71,7 @@ export function CodexPage() { Satisfactory. - + {categories.map(cat => ( } /> } /> } /> + } /> + } /> ); } diff --git a/src/codex/buildings/CodexBuildingDetail.tsx b/src/codex/buildings/CodexBuildingDetail.tsx index 4fb0507b..0d4d06d4 100644 --- a/src/codex/buildings/CodexBuildingDetail.tsx +++ b/src/codex/buildings/CodexBuildingDetail.tsx @@ -32,6 +32,10 @@ import { AllFactoryRecipes } from '@/recipes/FactoryRecipe'; import { isDefaultRecipe, isMAMRecipe } from '@/recipes/graph/SchematicGraph'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; import { SectionCard, StatCard } from '../components/StatCard'; +import { + getEarliestTierForBuilding, + getMilestonesForBuilding, +} from '../tiers/tierUnlocks'; function calcOverclockedPower( basePower: number, @@ -53,6 +57,9 @@ export function CodexBuildingDetail() { if (!building) return ; + const earliestTier = getEarliestTierForBuilding(building.id); + const buildingMilestones = getMilestonesForBuilding(building.id); + const isProduction = !building.conveyor && !building.pipeline && @@ -112,6 +119,17 @@ export function CodexBuildingDetail() { Production )} + {earliestTier != null && ( + + Unlocked at Tier {earliestTier} + + )} {building.description && ( @@ -220,7 +238,11 @@ export function CodexBuildingDetail() { > - + {item?.displayName ?? cost.resource} @@ -238,6 +260,52 @@ export function CodexBuildingDetail() { )} + {buildingMilestones.length > 0 && ( + + + {buildingMilestones.map(milestone => { + const color = + milestone.type === 'Milestone' + ? 'blue' + : milestone.type === 'MAM' + ? 'violet' + : milestone.type === 'Alternate' + ? 'orange' + : 'gray'; + const label = + milestone.tier != null + ? `T${milestone.tier} · ${milestone.name}` + : milestone.name; + if (milestone.type === 'Milestone' && milestone.tier != null) { + return ( + + {label} + + ); + } + return ( + + {label} + + ); + })} + + + )} + {hasOverclock && ( @@ -318,7 +386,11 @@ export function CodexBuildingDetail() { > - + {item?.displayName ?? fuel.resource} @@ -369,6 +441,7 @@ export function CodexBuildingDetail() { key={ing.resource} id={ing.resource} size={20} + withTooltip /> ))} @@ -380,6 +453,7 @@ export function CodexBuildingDetail() { key={prod.resource} id={prod.resource} size={20} + withTooltip /> ))} diff --git a/src/codex/items/CodexItemDetail.tsx b/src/codex/items/CodexItemDetail.tsx index d3d44b62..25ac9b51 100644 --- a/src/codex/items/CodexItemDetail.tsx +++ b/src/codex/items/CodexItemDetail.tsx @@ -26,6 +26,7 @@ import { AllFactoryRecipes, type FactoryRecipe } from '@/recipes/FactoryRecipe'; import { isDefaultRecipe, isMAMRecipe } from '@/recipes/graph/SchematicGraph'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; import { SectionCard, StatCard } from '../components/StatCard'; +import { getEarliestTierForItem } from '../tiers/tierUnlocks'; function getRecipeTypeBadge(recipe: FactoryRecipe) { if (isDefaultRecipe(recipe.id)) return { label: 'Default', color: 'teal' }; @@ -51,6 +52,8 @@ export function CodexItemDetail() { if (!item) return ; + const earliestTier = getEarliestTierForItem(item.id); + return ( @@ -63,13 +66,24 @@ export function CodexItemDetail() { - + {item.displayName} {item.form} + {earliestTier != null && ( + + Unlocked at Tier {earliestTier} + + )} {item.isFicsmas && ( FICSMAS @@ -204,6 +218,7 @@ function RecipeTable({ key={ing.resource} id={ing.resource} size={20} + withTooltip /> ))} @@ -215,6 +230,7 @@ function RecipeTable({ key={prod.resource} id={prod.resource} size={20} + withTooltip /> ))} diff --git a/src/codex/items/CodexItemsPage.tsx b/src/codex/items/CodexItemsPage.tsx index f89af7bc..899aed0d 100644 --- a/src/codex/items/CodexItemsPage.tsx +++ b/src/codex/items/CodexItemsPage.tsx @@ -80,7 +80,7 @@ export function CodexItemsPage() { style={{ cursor: 'pointer' }} > - + {item.displayName} diff --git a/src/codex/recipes/CodexRecipeDetail.tsx b/src/codex/recipes/CodexRecipeDetail.tsx index 9037f196..980cceb4 100644 --- a/src/codex/recipes/CodexRecipeDetail.tsx +++ b/src/codex/recipes/CodexRecipeDetail.tsx @@ -10,6 +10,7 @@ import { Stack, Text, Title, + Tooltip, } from '@mantine/core'; import { IconArrowLeft, @@ -139,7 +140,11 @@ export function CodexRecipeDetail() { > - + {item?.displayName ?? ing.resource} @@ -162,14 +167,17 @@ export function CodexRecipeDetail() { color="var(--mantine-color-dimmed)" /> {building && ( - - - + + + {building.name} + + )} {recipe.time}s @@ -189,7 +197,11 @@ export function CodexRecipeDetail() { > - + {item?.displayName ?? prod.resource} @@ -213,22 +225,42 @@ export function CodexRecipeDetail() { {unlockedBy.map(unlock => { const schematic = AllFactorySchematicsMap[unlock.id]; + const color = + unlock.type === 'Milestone' + ? 'blue' + : unlock.type === 'MAM' + ? 'violet' + : unlock.type === 'Alternate' + ? 'orange' + : 'gray'; + const label = schematic?.name ?? unlock.id; + const tier = schematic?.tier ?? null; + const display = tier != null ? `T${tier} · ${label}` : label; + + if (unlock.type === 'Milestone' && tier != null) { + return ( + + {display} + + ); + } + return ( - {schematic?.name ?? unlock.id} + {display} ); })} diff --git a/src/codex/recipes/CodexRecipesPage.tsx b/src/codex/recipes/CodexRecipesPage.tsx index ff230234..512cd3a4 100644 --- a/src/codex/recipes/CodexRecipesPage.tsx +++ b/src/codex/recipes/CodexRecipesPage.tsx @@ -5,6 +5,7 @@ import { Group, Image, SegmentedControl, + Select, Stack, Table, Text, @@ -19,6 +20,7 @@ import { AllFactoryBuildingsMap } from '@/recipes/FactoryBuilding'; import { AllFactoryRecipes, type FactoryRecipe } from '@/recipes/FactoryRecipe'; import { isDefaultRecipe, isMAMRecipe } from '@/recipes/graph/SchematicGraph'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { AllTierNumbers, RecipeTierMap } from '../tiers/tierUnlocks'; type RecipeFilter = 'All' | 'Default' | 'Alternate' | 'MAM'; @@ -34,13 +36,23 @@ function getRecipeTypeBadge(type: string) { return { label: 'Alternate', color: 'orange' }; } +const tierFilterOptions = [ + { value: 'all', label: 'All tiers' }, + ...AllTierNumbers.map(t => ({ value: String(t), label: `Tier ${t}` })), +]; + export function CodexRecipesPage() { const [search, setSearch] = useState(''); const [filter, setFilter] = useState('All'); + const [tierFilter, setTierFilter] = useState('all'); const filtered = useMemo(() => { return AllFactoryRecipes.filter(recipe => { if (filter !== 'All' && getRecipeType(recipe) !== filter) return false; + if (tierFilter !== 'all') { + const tier = RecipeTierMap[recipe.id]; + if (tier == null || String(tier) !== tierFilter) return false; + } if (search) { const q = search.toLowerCase(); if ( @@ -51,7 +63,7 @@ export function CodexRecipesPage() { } return true; }); - }, [search, filter]); + }, [search, filter, tierFilter]); return ( @@ -71,6 +83,13 @@ export function CodexRecipesPage() { onChange={v => setFilter(v as RecipeFilter)} data={['All', 'Default', 'Alternate', 'MAM']} /> + { + if (value == null) return; + onChange('showOutputFactoriesNodes')(value); + }} + /> ); diff --git a/src/games/settings/showOutputFactoriesNodesOptions.ts b/src/games/settings/showOutputFactoriesNodesOptions.ts new file mode 100644 index 00000000..68bb121d --- /dev/null +++ b/src/games/settings/showOutputFactoriesNodesOptions.ts @@ -0,0 +1,35 @@ +import type { ShowOutputFactoriesNodesMode } from '@/games/Game'; + +/** + * Single source of truth for the labels and short descriptions used by + * the "Show Output Factories Nodes" setting across the game settings + * modal and the in-graph node popover dropdowns. Keeping it shared + * means the dropdown a user sees in either place reads exactly the + * same so the choice they make matches the wording they remember. + */ +export interface ShowOutputFactoriesNodesOption { + value: ShowOutputFactoriesNodesMode; + label: string; + description: string; +} + +export const SHOW_OUTPUT_FACTORIES_NODES_OPTIONS: ShowOutputFactoriesNodesOption[] = + [ + { + value: 'none', + label: 'None', + description: "Don't display nodes for downstream consumer factories.", + }, + { + value: 'allocated', + label: 'Only allocated', + description: + "Show one node per consumer factory that's pulling from this factory's outputs.", + }, + { + value: 'all', + label: 'Allocated and unallocated', + description: + 'Also show a node for any production capacity that no consumer factory has claimed.', + }, + ]; diff --git a/src/games/store/gameFactoriesActions.ts b/src/games/store/gameFactoriesActions.ts index bd6d91a4..80506d61 100644 --- a/src/games/store/gameFactoriesActions.ts +++ b/src/games/store/gameFactoriesActions.ts @@ -4,10 +4,56 @@ import { v4 } from 'uuid'; import { useStore } from '@/core/zustand'; import { createActions } from '@/core/zustand-helpers/actions'; import type { Factory } from '@/factories/Factory'; +import { generateFactoryName } from '@/factories/factoryNameGenerator'; import type { Game } from '@/games/Game'; import { allowedToBlockedBuildings } from '@/solver/store/allowedToBlockedBuildings'; import type { SolverInstance } from '@/solver/store/Solver'; +export const SERIALIZED_FACTORY_SCHEMA_VERSION = 1; + +export type SerializedFactory = { + schemaVersion: typeof SERIALIZED_FACTORY_SCHEMA_VERSION; + kind: 'factory'; + factory: Factory; + solver?: SolverInstance; + exportedAt: string; + /** Informational hint, shown in UI but not used for validation. */ + gameName?: string; +}; + +export function isSerializedFactory( + value: unknown, +): value is SerializedFactory { + if (!value || typeof value !== 'object') return false; + const v = value as Partial; + return ( + v.kind === 'factory' && + v.schemaVersion === SERIALIZED_FACTORY_SCHEMA_VERSION && + typeof v.factory === 'object' && + v.factory != null + ); +} + +/** + * Returns a name that doesn't collide with any of `existingNames`. If the + * incoming name is already free, returns it unchanged. Otherwise appends + * " (Copy)", then " (Copy 2)", " (Copy 3)" and so on. + */ +function disambiguateFactoryName( + baseName: string | null | undefined, + existingNames: Set, +): string | null | undefined { + if (baseName == null || baseName === '') return baseName; + if (!existingNames.has(baseName)) return baseName; + let attempt = `${baseName} (Copy)`; + let counter = 2; + while (existingNames.has(attempt)) { + attempt = `${baseName} (Copy ${counter})`; + counter += 1; + } + return attempt; +} + export const gameFactoriesActions = createActions({ initGame: (game: Partial) => state => { const gameId = v4(); @@ -15,7 +61,7 @@ export const gameFactoriesActions = createActions({ state.games.selected = gameId; state.factories.factories[factoryId] = { id: factoryId, - // name: 'New Factory', + name: generateFactoryName(), inputs: [], outputs: [{ resource: null, amount: null }], progress: 'draft', @@ -44,7 +90,12 @@ export const gameFactoriesActions = createActions({ throw new Error('No game selected'); } - get().createFactory(factoryId, factory); + const factoryWithName: Partial> = { + ...factory, + name: factory?.name ?? generateFactoryName(), + }; + + get().createFactory(factoryId, factoryWithName); get().addFactoryIdToGame(targetId, factoryId); }, cloneGameFactory: (factoryId: string) => state => { @@ -68,6 +119,57 @@ export const gameFactoriesActions = createActions({ state.games.games[state.games.selected!].factoriesIds.push(newFactoryId); }, + /** + * Imports a `SerializedFactory` payload into the currently-selected game. + * The caller mints `newFactoryId` (so it can navigate to the new factory + * after dispatch). Sync metadata on the solver is stripped: the imported + * factory is local, not a remote-shared copy. + * + * If a factory with the same name already exists in the target game, the + * imported factory is renamed by appending " (Copy)" / " (Copy 2)" / ... + * so the user can tell the two apart in the list. + */ + importSerializedFactoryIntoCurrentGame: + (newFactoryId: string, payload: SerializedFactory) => state => { + const targetGameId = state.games.selected; + if (!targetGameId || !state.games.games[targetGameId]) { + throw new Error('No game selected'); + } + + const targetGame = state.games.games[targetGameId]; + const existingNames = new Set( + targetGame.factoriesIds + .map(id => state.factories.factories[id]?.name ?? null) + .filter((name): name is string => typeof name === 'string'), + ); + + const importedFactory: Factory = { + ...cloneDeep(payload.factory), + id: newFactoryId, + name: disambiguateFactoryName(payload.factory.name, existingNames), + // Cross-factory references don't exist in the target game, so drop + // them to avoid orphan links pointing at unrelated factories. + inputs: (payload.factory.inputs ?? []).map(input => ({ + ...input, + factoryId: null, + })), + }; + state.factories.factories[newFactoryId] = importedFactory; + + if (payload.solver) { + const importedSolver: SolverInstance = { + ...cloneDeep(payload.solver), + id: newFactoryId, + sharedId: undefined, + remoteSharedId: undefined, + isOwner: true, + isFactory: true, + }; + state.solvers.instances[newFactoryId] = importedSolver; + } + + state.games.games[targetGameId].factoriesIds.push(newFactoryId); + }, // TODO For now only for selected game removeGameFactory: (factoryId: string) => state => { const index = @@ -150,3 +252,47 @@ export function serializeGame( .filter(Boolean) as SolverInstance[], }; } + +/** + * Builds a standalone `SerializedFactory` payload for the given factory id. + * Strips share-sync metadata from the solver and clears cross-factory input + * links (those refer to factories that won't exist in the target game). + */ +export function serializeFactory(factoryId: string): SerializedFactory { + const state = useStore.getState(); + const factory = state.factories.factories[factoryId]; + if (!factory) { + throw new Error('Factory not found'); + } + const solver = state.solvers.instances[factoryId]; + const ownerGame = Object.values(state.games.games).find(g => + g.factoriesIds.includes(factoryId), + ); + + const cleanedFactory: Factory = { + ...cloneDeep(factory), + inputs: (factory.inputs ?? []).map(input => ({ + ...input, + factoryId: null, + })), + }; + + const cleanedSolver: SolverInstance | undefined = solver + ? { + ...cloneDeep(solver), + sharedId: undefined, + remoteSharedId: undefined, + isOwner: undefined, + isFactory: undefined, + } + : undefined; + + return { + schemaVersion: SERIALIZED_FACTORY_SCHEMA_VERSION, + kind: 'factory', + factory: cleanedFactory, + solver: cleanedSolver, + exportedAt: dayjs().toISOString(), + gameName: ownerGame?.name, + }; +} diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index c3b3d5df..efbe08e7 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -9,7 +9,9 @@ export function AppLayout() { const { pathname } = useLocation(); const compactFooter = - pathname.includes('/charts') || pathname.includes('/calculator'); + pathname.includes('/charts') || + pathname.includes('/calculator') || + pathname.startsWith('/map'); return ( diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 3ecf2cd2..4cd06ad8 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -13,6 +13,7 @@ import { IconBuildingFactory, IconCalculator, IconChartBar, + IconMap2, IconPackages, IconSearch, IconTools, @@ -32,12 +33,20 @@ import { HotkeyKbd } from '@/utils/HotkeyKbd'; import classes from './Header.module.css'; import { HeaderMobileDrawer } from './HeaderMobileDrawer'; -const TABS = ['factories', 'charts', 'calculator', 'tools', 'codex'] as const; +const TABS = [ + 'factories', + 'map', + 'charts', + 'calculator', + 'tools', + 'codex', +] as const; type HeaderTab = (typeof TABS)[number]; export const TAB_ROUTES: Record = { factories: '/factories', + map: '/map', charts: '/factories/charts', calculator: '/factories/calculator', tools: '/tools', @@ -46,6 +55,7 @@ export const TAB_ROUTES: Record = { export const TAB_ICONS: Record = { factories: , + map: , charts: , calculator: , tools: , @@ -53,6 +63,7 @@ export const TAB_ICONS: Record = { }; export function resolveActiveTab(pathname: string): HeaderTab | null { + if (pathname.startsWith('/map')) return 'map'; if (pathname.startsWith('/tools')) return 'tools'; if (pathname.startsWith('/codex')) return 'codex'; if (pathname.startsWith('/factories/charts')) return 'charts'; diff --git a/src/layout/HeaderMobileDrawer.tsx b/src/layout/HeaderMobileDrawer.tsx index d05df488..7e80fb85 100644 --- a/src/layout/HeaderMobileDrawer.tsx +++ b/src/layout/HeaderMobileDrawer.tsx @@ -50,17 +50,24 @@ export function HeaderMobileDrawer({ Navigation - {(['factories', 'charts', 'calculator', 'tools', 'codex'] as const).map( - tab => ( - navigateTo(tab)} - /> - ), - )} + {( + [ + 'factories', + 'map', + 'charts', + 'calculator', + 'tools', + 'codex', + ] as const + ).map(tab => ( + navigateTo(tab)} + /> + ))} diff --git a/src/map/CollectibleMarkersLayer.tsx b/src/map/CollectibleMarkersLayer.tsx new file mode 100644 index 00000000..bfdf25e5 --- /dev/null +++ b/src/map/CollectibleMarkersLayer.tsx @@ -0,0 +1,241 @@ +import L from 'leaflet'; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import { useStore } from '@/core/zustand'; +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import { + COLLECTIBLE_TYPE_META, + type WorldCollectible, +} from '@/recipes/WorldCollectibles'; +import { gameToLatLng } from './coords'; +import { getCollectibleMarkerIcon } from './markerIcons'; + +export interface CollectibleMarkersLayerProps { + collectibles: WorldCollectible[]; + /** + * Ids of collectibles the player has marked as collected in the + * current game. + */ + collectedIds: Set; + /** + * Currently selected game id, forwarded to `toggleGameCollectedItem` + * so collected-state is scoped per game. + */ + gameId: string | null; +} + +const HTML_ESCAPES: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; + +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, ch => HTML_ESCAPES[ch] ?? ch); +} + +function formatNumber(value: number): string { + return value.toLocaleString('en-US'); +} + +/** + * Builds the popup HTML for a single collectible. Same delegated- + * click pattern as `ResourceMarkersLayer` so toggling "collected" + * survives Leaflet's `setPopupContent` DOM swap. + */ +function buildPopupHtml( + collectible: WorldCollectible, + isCollected: boolean, +): string { + const meta = COLLECTIBLE_TYPE_META[collectible.type]; + const name = escapeHtml(meta.displayName); + const description = escapeHtml(meta.description); + const x = formatNumber(collectible.x); + const y = formatNumber(collectible.y); + const altitude = + collectible.z != null + ? `${formatNumber(Math.round(collectible.z / 100))} m` + : '—'; + + const iconHtml = meta.iconImagePath + ? `` + : ''; + + // Drop-pod-only: render the unlock cost as a row of icon + amount + // chips. Lookups in `AllFactoryItemsMap` give us the displayName + + // image so the player can recognize what they need at a glance. + const unlockCost = collectible.unlockCost ?? []; + const unlockHtml = + unlockCost.length > 0 + ? ` +

+ Unlock cost +
+ ${unlockCost + .map(({ item, amount }) => { + const itemMeta = AllFactoryItemsMap[item]; + const itemName = escapeHtml(itemMeta?.displayName ?? item); + const imagePath = + itemMeta?.imagePath?.replace('_256', '_64') ?? ''; + const imageHtml = imagePath + ? `` + : ''; + return ` + + ${imageHtml} + ${formatNumber(amount)}× ${itemName} + + `; + }) + .join('')} +
+
+ ` + : ''; + + // Audio-tape-only: the schematic id reads as a tracklist hint + // ("Schematic_Huntdown_C" -> "Huntdown"). We strip the prefix + + // suffix for display since there's no localized title in the dump. + const schematicHtml = + collectible.schematicId != null + ? (() => { + const trimmed = collectible.schematicId + .replace(/^Schematic_/, '') + .replace(/_C$/, ''); + return `
Tape
${escapeHtml(trimmed)}
`; + })() + : ''; + + const collectedBadge = isCollected + ? 'Collected' + : ''; + const collectedLabel = isCollected + ? 'Mark as not collected' + : 'Mark as collected'; + const collectedModifier = isCollected + ? ' map-marker-popup__action--used' + : ''; + const idAttr = escapeHtml(collectible.id); + + return ` +
+
+ ${iconHtml} +
+
${name}${ + collectedBadge ? ` ${collectedBadge}` : '' + }
+
${escapeHtml(meta.shortName)}
+
+
+

${description}

+
+
Coordinates
${x} / ${y}
+
Altitude
${altitude}
+ ${schematicHtml} +
+ ${unlockHtml} +
+ +
+
+ `; +} + +/** + * Imperative leaflet layer that renders the given collectibles as + * individual markers. Mirrors `ResourceMarkersLayer` but for the + * collectible track: + * + * - No purity, no extractor — markers are themed by `CollectibleType`. + * - No sum mode — collectibles aren't part of the per-rate aggregation. + * - "Collected" replaces "used" semantically; we reuse the same + * visual variant on the marker (dim + checkmark) since it reads + * identically on the map. + * + * Click handling uses a single delegated listener on the map + * container, scoped to `[data-action][data-collectible-id]` so it + * doesn't conflict with the resource layer's + * `[data-action][data-node-id]` listener. + */ +export function CollectibleMarkersLayer({ + collectibles, + collectedIds, + gameId, +}: CollectibleMarkersLayerProps) { + const map = useMap(); + + useEffect(() => { + const layer = L.layerGroup(); + const markersById = new Map(); + const collectiblesById = new Map(); + + const repaintMarker = (id: string) => { + const marker = markersById.get(id); + const collectible = collectiblesById.get(id); + if (!marker || !collectible) return; + const isCollected = collectedIds.has(id); + marker.setIcon( + getCollectibleMarkerIcon(collectible.type, { collected: isCollected }), + ); + if (marker.getPopup()) { + marker.setPopupContent(buildPopupHtml(collectible, isCollected)); + } + }; + + for (const collectible of collectibles) { + collectiblesById.set(collectible.id, collectible); + const isCollected = collectedIds.has(collectible.id); + const marker = L.marker(gameToLatLng(collectible.x, collectible.y), { + icon: getCollectibleMarkerIcon(collectible.type, { + collected: isCollected, + }), + }); + marker.bindPopup(buildPopupHtml(collectible, isCollected), { + closeButton: true, + offset: [0, -4], + maxWidth: 320, + }); + markersById.set(collectible.id, marker); + layer.addLayer(marker); + } + + layer.addTo(map); + + const container = map.getContainer(); + const onDelegatedClick = (event: MouseEvent) => { + const target = event.target as HTMLElement | null; + if (!target) return; + const button = target.closest( + 'button[data-action][data-collectible-id]', + ); + if (!button) return; + + const id = button.dataset.collectibleId; + const action = button.dataset.action; + if (!id || action !== 'toggle-collected') return; + + event.preventDefault(); + event.stopPropagation(); + + useStore.getState().toggleGameCollectedItem(gameId, id); + repaintMarker(id); + }; + + container.addEventListener('click', onDelegatedClick); + + return () => { + container.removeEventListener('click', onDelegatedClick); + layer.removeFrom(map); + }; + }, [map, collectibles, collectedIds, gameId]); + + return null; +} diff --git a/src/map/MapFiltersPanel.module.css b/src/map/MapFiltersPanel.module.css new file mode 100644 index 00000000..32176a15 --- /dev/null +++ b/src/map/MapFiltersPanel.module.css @@ -0,0 +1,450 @@ +.panel { + width: 280px; + flex-shrink: 0; + background-color: var(--mantine-color-dark-7); + border-radius: var(--mantine-radius-md); + overflow-y: auto; +} + +.panelTitle { + letter-spacing: 0.01em; +} + +/* Sum section — intentionally preserved from the previous design. + The soft violet wash anchors it as the "primary" action in the + panel so the rest of the sidebar recedes by comparison. */ + +.sumSection { + padding: 10px 12px; + border-radius: var(--mantine-radius-sm); + background: linear-gradient( + 135deg, + rgba(109, 40, 217, 0.12), + rgba(139, 92, 246, 0.06) + ); + border: 1px solid rgba(139, 92, 246, 0.25); +} + +.sumHint { + line-height: 1.3; +} + +/* Section headers. Uppercase + dimmed; inline counts sit next to them + as plain dimmed text rather than Mantine badges so the header stays + quiet until its content is interacted with. */ + +.sectionTitle { + letter-spacing: 0.04em; +} + +.sectionCount { + font-variant-numeric: tabular-nums; +} + +.inlineAction { + align-self: flex-start; + padding-left: 4px; + padding-right: 4px; +} + +/* Mini actions in section headers (used for "All" / "None"). These + replace the old dedicated "Bulk" section with its chunky button + groups. They sit to the right of the "Resources" header so all + bulk controls are inline with the title they affect. */ + +.miniAction { + height: 22px; + padding: 0 8px; + border: 1px solid var(--mantine-color-dark-4); + border-radius: var(--mantine-radius-sm); + background: transparent; + color: var(--mantine-color-gray-4); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: + background-color 120ms ease, + color 120ms ease, + border-color 120ms ease; +} + +.miniAction:hover { + border-color: var(--mantine-color-gray-6); + color: var(--mantine-color-gray-1); + background-color: var(--mantine-color-dark-6); +} + +/* "Only" quick-filter row — one word label followed by three colored + chips. Each chip filters the whole map to a single purity. Kept + separate from the per-resource rows so they can scan independently. */ + +.purityOnlyRow { + display: flex; + align-items: center; + gap: 8px; +} + +.purityOnlyLabel { + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.purityOnlyChip { + --chip-color: var(--mantine-color-gray-5); + + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px 2px 6px; + height: 22px; + border: 1px solid var(--mantine-color-dark-4); + border-radius: 999px; + background: transparent; + color: var(--mantine-color-gray-3); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: + background-color 120ms ease, + color 120ms ease, + border-color 120ms ease; +} + +.purityOnlyChip:hover { + border-color: var(--chip-color); + color: var(--mantine-color-gray-0); + background-color: color-mix(in srgb, var(--chip-color) 14%, transparent); +} + +.purityOnlyDot { + width: 7px; + height: 7px; + border-radius: 50%; + background-color: var(--chip-color); + flex-shrink: 0; +} + +/* Per-resource row: left = icon + name; right = segmented purity + control. The row itself dims when no purities are active so the + eye can quickly scan for what's shown. */ + +.resourceRow { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 6px; + border-radius: var(--mantine-radius-sm); + transition: + background-color 120ms ease, + opacity 120ms ease; +} + +.resourceRow:hover { + background-color: var(--mantine-color-dark-6); +} + +.resourceRowDim { + opacity: 0.4; +} + +.resourceRowDim:hover { + opacity: 0.75; +} + +.resourceLabel { + flex: 1 1 auto; + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 2px 0; + border: 0; + background: transparent; + color: var(--mantine-color-gray-1); + text-align: left; + cursor: pointer; +} + +.resourceName { + flex: 1 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Segmented purity control. Three adjoined buttons share one rounded + outline so the trio reads as a single control, not three loose + chips. Each segment shows I/N/P in its purity color plus a count. */ + +.puritySegments { + display: inline-flex; + flex: 0 0 auto; + border: 1px solid var(--mantine-color-dark-4); + border-radius: var(--mantine-radius-sm); + overflow: hidden; + background-color: var(--mantine-color-dark-8); +} + +.puritySegment { + --chip-color: var(--mantine-color-gray-5); + + display: inline-flex; + align-items: center; + gap: 3px; + min-width: 34px; + padding: 2px 6px; + border: 0; + border-right: 1px solid var(--mantine-color-dark-4); + background: transparent; + color: var(--mantine-color-gray-5); + font-size: 10px; + font-weight: 500; + line-height: 1.2; + cursor: pointer; + transition: + background-color 120ms ease, + color 120ms ease; + font-variant-numeric: tabular-nums; + justify-content: center; +} + +.puritySegment:last-child { + border-right: 0; +} + +.puritySegment:hover:not(:disabled) { + background-color: color-mix(in srgb, var(--chip-color) 14%, transparent); + color: var(--mantine-color-gray-1); +} + +.puritySegmentActive { + background-color: color-mix(in srgb, var(--chip-color) 22%, transparent); + color: var(--mantine-color-gray-0); +} + +.puritySegmentActive:hover { + background-color: color-mix(in srgb, var(--chip-color) 30%, transparent); +} + +.puritySegmentEmpty { + opacity: 0.3; + cursor: not-allowed; +} + +.puritySegmentLetter { + font-weight: 700; + color: var(--chip-color); +} + +.puritySegmentActive .puritySegmentLetter { + color: var(--chip-color); +} + +.puritySegmentCount { + color: inherit; +} + +/* + * Collectibles list. Each row is a single tap target — toggles + * visibility for that category — with a colored swatch on the left + * (the category's themed color from COLLECTIBLE_TYPE_META) and a + * "collected / total" count on the right. Mirrors the resource + * rows visually so the eye reads the panel as one continuous list. + */ + +.collectibleRow { + --chip-color: var(--mantine-color-gray-5); + + display: flex; + align-items: center; + padding: 3px 6px; + border-radius: var(--mantine-radius-sm); + transition: + background-color 120ms ease, + opacity 120ms ease; +} + +.collectibleRow:hover { + background-color: var(--mantine-color-dark-6); +} + +.collectibleRowDim { + opacity: 0.4; +} + +.collectibleRowDim:hover { + opacity: 0.75; +} + +.collectibleRowEmpty { + opacity: 0.25; + pointer-events: none; +} + +.collectibleLabel { + flex: 1 1 auto; + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 2px 0; + border: 0; + background: transparent; + color: var(--mantine-color-gray-1); + text-align: left; + cursor: pointer; +} + +.collectibleLabel:disabled { + cursor: not-allowed; +} + +.collectibleSwatch { + flex: 0 0 22px; + width: 22px; + height: 22px; + border-radius: 50%; + border: 1.5px solid var(--chip-color); + background-color: var(--mantine-color-dark-8); + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--chip-color); + overflow: hidden; +} + +.collectibleSwatch img { + width: 14px; + height: 14px; +} + +.collectibleName { + flex: 1 1 auto; + min-width: 0; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.collectibleProgress { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--mantine-color-gray-4); +} + +.collectibleCheck { + color: var(--mantine-color-teal-4); +} + +/* + * Built-infrastructure toggles. A 2-column grid of compact chips + * (icon + label + count) so an end-game save with 8 categories and 5 + * spline kinds doesn't push the rest of the panel below the fold. + * The chip glows in its category color when active and washes out + * to dim grey when toggled off — mirrors the resource segmented + * control's "active = tinted" idiom. + */ + +.infraGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.infraChip { + --chip-color: var(--mantine-color-gray-5); + + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + height: 28px; + padding: 0 8px; + border: 1px solid var(--mantine-color-dark-4); + border-radius: var(--mantine-radius-sm); + background-color: var(--mantine-color-dark-7); + color: var(--mantine-color-gray-3); + font-size: 12px; + font-weight: 500; + text-align: left; + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease, + opacity 120ms ease; +} + +.infraChip:hover { + border-color: var(--chip-color); + color: var(--mantine-color-gray-0); + background-color: color-mix(in srgb, var(--chip-color) 14%, transparent); +} + +.infraChipActive { + background-color: color-mix(in srgb, var(--chip-color) 22%, transparent); + border-color: color-mix(in srgb, var(--chip-color) 50%, transparent); + color: var(--mantine-color-gray-0); +} + +.infraChipActive:hover { + background-color: color-mix(in srgb, var(--chip-color) 30%, transparent); +} + +.infraChipDim { + opacity: 0.4; +} + +.infraChipDim:hover { + opacity: 0.85; +} + +.infraChipIcon { + flex: 0 0 auto; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--chip-color); +} + +.infraChipLabel { + flex: 1 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.infraChipCount { + flex: 0 0 auto; + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--mantine-color-gray-4); +} + +.infraChipActive .infraChipCount { + color: var(--mantine-color-gray-2); +} + +.infraGroupLabel { + font-size: 10px; + letter-spacing: 0.04em; + color: var(--mantine-color-dark-2); + text-transform: uppercase; + padding: 4px 2px 0; +} + +@media (max-width: 768px) { + .panel { + width: 100%; + max-height: 320px; + } +} diff --git a/src/map/MapFiltersPanel.tsx b/src/map/MapFiltersPanel.tsx new file mode 100644 index 00000000..df9b46c0 --- /dev/null +++ b/src/map/MapFiltersPanel.tsx @@ -0,0 +1,880 @@ +import { + Badge, + Button, + FileButton, + Group, + Progress, + Stack, + Switch, + Text, + Title, + Tooltip, +} from '@mantine/core'; +import { + IconBolt, + IconBox, + IconBrush, + IconBuildingFactory2, + IconCheck, + IconCloudUpload, + IconCrosshair, + IconDeviceAudioTape, + IconDroplet, + IconLayoutGrid, + IconPackage, + IconRefresh, + IconRoute, + IconShape, + IconSum, + IconTrain, + IconTransferIn, + IconTruck, +} from '@tabler/icons-react'; +import clsx from 'clsx'; +import { type CSSProperties, type ReactNode, useMemo } from 'react'; +import { useShallowStore, useStore } from '@/core/zustand'; +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import { + INFRASTRUCTURE_CATEGORIES, + type InfrastructureCategory, + SPLINE_KINDS, + type SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; +import { useSavegameImport } from '@/recipes/savegame/useSavegameImport'; +import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { + COLLECTIBLE_TYPE_META, + COLLECTIBLE_TYPES, + type CollectibleType, + getWorldCollectibles, +} from '@/recipes/WorldCollectibles'; +import { + getWorldResourceNodes, + PURITIES, + type Purity, +} from '@/recipes/WorldResourceNodes'; +import { WorldResourcesList } from '@/recipes/WorldResources'; +import { + CategoryColor, + CategoryLabel, + SplineLabel, + splineColor, +} from './infrastructure/infrastructureCategories'; +import classes from './MapFiltersPanel.module.css'; +import { getPurityColor, getPurityLabel } from './markerIcons'; + +const PURITY_SHORT: Record = { + impure: 'I', + normal: 'N', + pure: 'P', +}; + +/** + * Shared empty array so `useShallowStore` selectors can return a + * stable fallback when a game has no used-node marks yet. Returning a + * fresh `[]` on every call would make the shallow-equal diff always + * report the slice as changed and trigger pointless re-renders. + */ +const EMPTY_USED_NODES: readonly string[] = []; +/** Stable fallback for `resourceFilters` when the slice hasn't rehydrated yet. */ +const EMPTY_RESOURCE_FILTERS: Record = {}; +/** Stable fallback for collectible visibility before rehydrate finishes. */ +const EMPTY_COLLECTIBLE_VISIBILITY: Record = (() => { + const visibility = {} as Record; + for (const type of COLLECTIBLE_TYPES) visibility[type] = true; + return visibility; +})(); +/** Empty list mirror of {@link EMPTY_USED_NODES} for collectibles. */ +const EMPTY_COLLECTED_LIST: readonly string[] = []; + +/** + * Bundled-asset icons for collectible categories that have in-game + * art (slugs, somersloops, mercer spheres). Tabler-icon fallbacks + * cover hard drives, audio tapes, and customization unlocks where + * the game ships no representative icon we can reuse. Keys are + * `iconName` from {@link COLLECTIBLE_TYPE_META} so we can swap + * implementations without changing the meta data. + */ +const TABLER_ICON_BY_NAME: Record = { + IconPackage: , + IconDeviceAudioTape: , + IconBrush: , +}; + +/** Icons rendered inside built-infrastructure chips. */ +const INFRA_CATEGORY_ICONS: Record = { + production: , + logistics: , + power: , + storage: , + transport: , + foundation: , + decor: , + other: , +}; + +const INFRA_SPLINE_ICONS: Record = { + belt: , + pipe: , + hyper: , + rail: , + power: , +}; + +export interface MapFiltersPanelProps { + gameId?: string | null; +} + +export function MapFiltersPanel({ gameId }: MapFiltersPanelProps) { + const { + resourceFilters, + hideUsedNodes, + usedNodesForGame, + sumMode, + selectedCount, + collectibleVisibility, + hideCollectedCollectibles, + collectedForGame, + infrastructureMaster, + infrastructureCategoryVisibility, + infrastructureSplineVisibility, + activeInfrastructure, + } = useShallowStore(state => { + const mapState = state.map; + const game = gameId ? state.games.games[gameId] : null; + const infraSlice = state.mapInfrastructure; + const infraOwnedByActiveGame = + infraSlice?.gameId != null && infraSlice.gameId === state.games.selected; + return { + resourceFilters: mapState?.resourceFilters ?? EMPTY_RESOURCE_FILTERS, + hideUsedNodes: mapState?.hideUsedNodes ?? false, + usedNodesForGame: game?.usedNodes ?? EMPTY_USED_NODES, + sumMode: state.mapSelection?.sumMode ?? false, + selectedCount: state.mapSelection?.selectedNodeIds.length ?? 0, + collectibleVisibility: + mapState?.collectibleVisibility ?? EMPTY_COLLECTIBLE_VISIBILITY, + hideCollectedCollectibles: mapState?.hideCollectedCollectibles ?? false, + collectedForGame: game?.collectedItems ?? EMPTY_COLLECTED_LIST, + infrastructureMaster: mapState?.infrastructureMaster ?? true, + infrastructureCategoryVisibility: + mapState?.infrastructureCategoryVisibility, + infrastructureSplineVisibility: mapState?.infrastructureSplineVisibility, + activeInfrastructure: infraOwnedByActiveGame + ? infraSlice.infrastructure + : null, + }; + }); + const toggleResourcePurity = useStore(state => state.toggleResourcePurity); + const setResourcePurities = useStore(state => state.setResourcePurities); + const setAllResourcesEnabled = useStore( + state => state.setAllResourcesEnabled, + ); + const setOnlyPurity = useStore(state => state.setOnlyPurity); + const setHideUsedNodes = useStore(state => state.setHideUsedNodes); + const clearGameUsedNodes = useStore(state => state.clearGameUsedNodes); + const resetMapFilters = useStore(state => state.resetMapFilters); + const setSumMode = useStore(state => state.setSumMode); + const clearSelection = useStore(state => state.clearSelection); + const toggleCollectibleType = useStore(state => state.toggleCollectibleType); + const setAllCollectiblesVisible = useStore( + state => state.setAllCollectiblesVisible, + ); + const setHideCollectedCollectibles = useStore( + state => state.setHideCollectedCollectibles, + ); + const clearGameCollectedItems = useStore( + state => state.clearGameCollectedItems, + ); + const setInfrastructureMaster = useStore( + state => state.setInfrastructureMaster, + ); + const toggleInfrastructureCategory = useStore( + state => state.toggleInfrastructureCategory, + ); + const setAllInfrastructureCategoriesVisible = useStore( + state => state.setAllInfrastructureCategoriesVisible, + ); + const toggleInfrastructureSplineKind = useStore( + state => state.toggleInfrastructureSplineKind, + ); + const clearInfrastructure = useStore(state => state.clearInfrastructure); + const requestInfrastructureFit = useStore( + state => state.requestInfrastructureFit, + ); + + const { importing, progress, importAndApplyToGame } = useSavegameImport(); + + const handleSavegameImport = (file: File | null) => { + if (!file) return; + // Same surface as the map drop-zone: pull recipes, used nodes, and + // built infrastructure in one shot. The button label is "Import + // from save" so users expect everything to come along. + importAndApplyToGame(file, gameId, { + defaultRecipes: true, + usedNodes: true, + infrastructure: true, + }).catch(() => { + // Notification surfaced by the hook; nothing else to do here. + }); + }; + + const allNodes = useMemo(() => getWorldResourceNodes(gameId), [gameId]); + const allCollectibles = useMemo(() => getWorldCollectibles(), []); + + /** + * Per-type counts of all collectibles in the world. Computed once + * since the source data is static. Used both for the per-row + * "(N)" total and the overall "X of Y" header. + */ + const collectibleTotalsByType = useMemo(() => { + const totals = {} as Record; + for (const type of COLLECTIBLE_TYPES) totals[type] = 0; + for (const c of allCollectibles) totals[c.type] += 1; + return totals; + }, [allCollectibles]); + + /** + * How many of each collectible type the player has marked + * collected in the current game. Recomputed only when the + * collected list shape changes, since the underlying static + * collectibles never change at runtime. + */ + const collectedCountsByType = useMemo(() => { + const counts = {} as Record; + for (const type of COLLECTIBLE_TYPES) counts[type] = 0; + if (collectedForGame.length === 0) return counts; + const collectedSet = new Set(collectedForGame); + for (const c of allCollectibles) { + if (collectedSet.has(c.id)) counts[c.type] += 1; + } + return counts; + }, [allCollectibles, collectedForGame]); + + const totalCollectibles = allCollectibles.length; + const totalCollected = collectedForGame.length; + + /** + * Counts nodes broken down as `counts[resource][purity]`. Computed + * once per node list swap so toggling filters stays cheap. + */ + const countsByResourcePurity = useMemo(() => { + const counts: Record> = {}; + for (const node of allNodes) { + let row = counts[node.resource]; + if (!row) { + row = { impure: 0, normal: 0, pure: 0 }; + counts[node.resource] = row; + } + row[node.purity] += 1; + } + return counts; + }, [allNodes]); + + const usedNodesCount = usedNodesForGame.length; + + const infrastructureBuildingCount = + activeInfrastructure?.buildings.count ?? 0; + const infrastructureSplineCount = useMemo( + () => + activeInfrastructure?.splines.reduce((sum, s) => sum + s.count, 0) ?? 0, + [activeInfrastructure], + ); + + return ( + + + + Filters + + + + + + + + + + Sum nodes + + {selectedCount > 0 ? ( + + {selectedCount} + + ) : null} + + + + {sumMode + ? 'Tap nodes on the map to add or remove them.' + : 'Total extraction rates across several nodes.'} + + {selectedCount > 0 ? ( + + ) : null} + + + + + + + Used Nodes + + + {usedNodesCount} + + + setHideUsedNodes(event.currentTarget.checked)} + aria-label="Hide used nodes on the map" + label="Hide" + labelPosition="left" + styles={{ + label: { fontSize: 11, color: 'var(--mantine-color-dimmed)' }, + }} + /> + + {usedNodesCount > 0 ? ( + + ) : null} + + + {fileButtonProps => ( + + )} + + + {importing ? ( + <> + + {progress.message ? ( + + {progress.message} + + ) : null} + + ) : null} + + + + + + Resources + + + + + + + + + + + +
+ + Only + + + {PURITIES.map(purity => ( + + + + ))} + +
+ + + {WorldResourcesList.map(resource => { + const item = AllFactoryItemsMap[resource]; + const counts = countsByResourcePurity[resource] ?? { + impure: 0, + normal: 0, + pure: 0, + }; + const selectedPurities = resourceFilters[resource] ?? []; + const allSelected = selectedPurities.length === PURITIES.length; + const noneSelected = selectedPurities.length === 0; + + return ( +
+ + + +
+ {PURITIES.map(purity => { + const count = counts[purity]; + const active = selectedPurities.includes(purity); + const disabled = count === 0; + return ( + + + + ); + })} +
+
+ ); + })} +
+
+ + + + + Collectibles + + + + + + + + + + + + + + + {totalCollected} / {totalCollectibles} collected + + {totalCollected > 0 ? ( + + ) : null} + + {totalCollected > 0 ? ( + + setHideCollectedCollectibles(event.currentTarget.checked) + } + aria-label="Hide collected collectibles on the map" + label="Hide" + labelPosition="left" + styles={{ + label: { fontSize: 11, color: 'var(--mantine-color-dimmed)' }, + }} + /> + ) : null} + + + + {COLLECTIBLE_TYPES.map(type => { + const meta = COLLECTIBLE_TYPE_META[type]; + const total = collectibleTotalsByType[type]; + const collected = collectedCountsByType[type]; + const visible = collectibleVisibility[type]; + const empty = total === 0; + + return ( +
+ + + +
+ ); + })} +
+
+ + + + + Infrastructure + + + setInfrastructureMaster(event.currentTarget.checked) + } + aria-label="Show built infrastructure on the map" + label="Show" + labelPosition="left" + data-tutorial-id="map-infrastructure-toggle" + styles={{ + label: { fontSize: 11, color: 'var(--mantine-color-dimmed)' }, + }} + /> + + + {!activeInfrastructure ? ( + + Drop a Satisfactory .sav onto the map to load the + buildings, belts, pipes, and rails the player has built. + + ) : ( + <> + + + {infrastructureBuildingCount} build + {infrastructureBuildingCount === 1 ? '' : 's'} + {infrastructureSplineCount > 0 + ? `, ${infrastructureSplineCount} line${infrastructureSplineCount === 1 ? '' : 's'}` + : ''} + + + + + + + + + + + + + + + + + + Buildings +
+ {INFRASTRUCTURE_CATEGORIES.map(category => { + const count = activeInfrastructure.counts[category] ?? 0; + if (count === 0) return null; + const visible = + infrastructureCategoryVisibility?.[category] ?? true; + return ( + + ); + })} +
+ + Networks +
+ {SPLINE_KINDS.map(kind => { + const count = activeInfrastructure.splineCounts[kind] ?? 0; + if (count === 0) return null; + const visible = infrastructureSplineVisibility?.[kind] ?? true; + return ( + + ); + })} +
+ + )} +
+
+ ); +} diff --git a/src/map/MapPage.module.css b/src/map/MapPage.module.css new file mode 100644 index 00000000..d994955b --- /dev/null +++ b/src/map/MapPage.module.css @@ -0,0 +1,22 @@ +.layout { + display: flex; + flex-direction: row; + gap: 12px; + padding: 12px; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.mapArea { + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} + +@media (max-width: 768px) { + .layout { + flex-direction: column; + padding: 8px; + } +} diff --git a/src/map/MapPage.tsx b/src/map/MapPage.tsx new file mode 100644 index 00000000..8120d7d9 --- /dev/null +++ b/src/map/MapPage.tsx @@ -0,0 +1,20 @@ +import { useStore } from '@/core/zustand'; +import { FullHeightContainer } from '@/layout/FullHeightContainer'; +import { MapFiltersPanel } from './MapFiltersPanel'; +import classes from './MapPage.module.css'; +import { WorldMapView } from './WorldMapView'; + +export function MapPage() { + const gameId = useStore(state => state.games.selected ?? null); + + return ( + +
+ +
+ +
+
+
+ ); +} diff --git a/src/map/MapSelectionSummary.module.css b/src/map/MapSelectionSummary.module.css new file mode 100644 index 00000000..e3ac1ed1 --- /dev/null +++ b/src/map/MapSelectionSummary.module.css @@ -0,0 +1,138 @@ +.panel { + position: absolute; + left: 12px; + right: 12px; + bottom: 12px; + z-index: 500; + padding: 10px 12px; + border-radius: var(--mantine-radius-md); + background-color: rgba(20, 21, 23, 0.94); + backdrop-filter: blur(6px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + color: var(--mantine-color-gray-1); + font-size: 13px; + line-height: 1.4; + max-height: min(55%, 420px); + display: flex; + flex-direction: column; + gap: 8px; + overflow: hidden; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + padding: 0 8px; + border-radius: 999px; + background-color: var(--mantine-color-violet-6); + color: white; + font-size: 12px; + font-weight: 700; +} + +.controls { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.body { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 8px; + overflow-y: auto; + padding-right: 4px; +} + +.row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: var(--mantine-radius-sm); + background-color: var(--mantine-color-dark-6); + border: 1px solid var(--mantine-color-dark-5); +} + +.rowIcon { + width: 24px; + height: 24px; + flex: 0 0 24px; + object-fit: contain; +} + +.rowText { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1 1 auto; +} + +.rowName { + font-weight: 500; + font-size: 12px; + color: var(--mantine-color-gray-1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.rowMeta { + font-size: 11px; + color: var(--mantine-color-gray-5); + font-variant-numeric: tabular-nums; +} + +.rowTotal { + font-weight: 700; + font-size: 14px; + color: var(--mantine-color-gray-0); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.rowUnit { + font-size: 10px; + color: var(--mantine-color-gray-5); + margin-left: 2px; + font-weight: 500; +} + +.empty { + color: var(--mantine-color-gray-4); + font-size: 12px; + font-style: italic; + padding: 2px 0; +} + +.collapsed { + max-height: none; + padding: 6px 10px; +} + +.collapsedSummary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} diff --git a/src/map/MapSelectionSummary.tsx b/src/map/MapSelectionSummary.tsx new file mode 100644 index 00000000..394ca7a7 --- /dev/null +++ b/src/map/MapSelectionSummary.tsx @@ -0,0 +1,160 @@ +import { Button, Group, SegmentedControl, Select, Text } from '@mantine/core'; +import { IconX } from '@tabler/icons-react'; +import { useMemo } from 'react'; +import { useShallowStore, useStore } from '@/core/zustand'; +import { AllFactoryBuildings } from '@/recipes/FactoryBuilding'; +import { getWorldResourceNodes } from '@/recipes/WorldResourceNodes'; +import { OVERCLOCK_STEPS, type OverclockStep } from './extraction'; +import classes from './MapSelectionSummary.module.css'; +import { getSelectionAggregates, SOLID_MINER_CHOICES } from './selectionMath'; + +export interface MapSelectionSummaryProps { + gameId: string | null; +} + +const EMPTY_SELECTED_NODE_IDS: readonly string[] = []; + +/** + * Floating panel at the bottom of the map that sums the selected + * nodes' extraction rates per resource at a chosen miner tier and + * overclock. Hidden entirely when the selection is empty so it + * doesn't occlude the map. + */ +export function MapSelectionSummary({ gameId }: MapSelectionSummaryProps) { + const { selectedNodeIds, selectedMinerId, selectedOverclock, sumMode } = + useShallowStore(state => ({ + selectedNodeIds: + state.mapSelection?.selectedNodeIds ?? EMPTY_SELECTED_NODE_IDS, + selectedMinerId: + state.mapSelection?.selectedMinerId ?? 'Build_MinerMk3_C', + selectedOverclock: (state.mapSelection?.selectedOverclock ?? + 100) as OverclockStep, + sumMode: state.mapSelection?.sumMode ?? false, + })); + + const selectedNodes = useMemo(() => { + const selectedSet = new Set(selectedNodeIds); + return getWorldResourceNodes(gameId).filter(n => selectedSet.has(n.id)); + }, [gameId, selectedNodeIds]); + + const aggregates = useMemo( + () => + getSelectionAggregates(selectedNodes, selectedMinerId, selectedOverclock), + [selectedNodes, selectedMinerId, selectedOverclock], + ); + + // Solid-miner { + if (value) useStore.getState().setSelectedMinerId(value); + }} + data={minerOptions} + w={140} + allowDeselect={false} + comboboxProps={{ withinPortal: true }} + aria-label="Solid-miner tier" + /> + + useStore + .getState() + .setSelectedOverclock(Number(value) as OverclockStep) + } + data={OVERCLOCK_STEPS.map(step => ({ + value: String(step), + label: `${step}%`, + }))} + aria-label="Overclock percentage" + /> + + + + + {aggregates.length === 0 ? ( + + No extractable resources in selection. + + ) : ( +
+ {aggregates.map(a => ( +
+
+
{a.displayName}
+
+ {a.nodeCount} node{a.nodeCount === 1 ? '' : 's'} + {a.purityCounts.pure > 0 ? ` · ${a.purityCounts.pure}P` : ''} + {a.purityCounts.normal > 0 + ? ` · ${a.purityCounts.normal}N` + : ''} + {a.purityCounts.impure > 0 + ? ` · ${a.purityCounts.impure}I` + : ''} + {' · '} + {a.extractorName} +
+
+
+ {a.totalRate.toLocaleString('en-US')} + {a.unit} +
+
+ ))} +
+ )} + + + + Totals assume all nodes extracted simultaneously at the chosen + settings. + + + + ); +} diff --git a/src/map/PlayerMarkerLayer.tsx b/src/map/PlayerMarkerLayer.tsx new file mode 100644 index 00000000..ea40ad4a --- /dev/null +++ b/src/map/PlayerMarkerLayer.tsx @@ -0,0 +1,59 @@ +import L from 'leaflet'; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import { useStore } from '@/core/zustand'; +import { gameToLatLng } from './coords'; + +const PLAYER_ICON_HTML = ` +
+ + +
+`; + +const PLAYER_ICON: L.DivIcon = L.divIcon({ + html: PLAYER_ICON_HTML, + className: 'map-player-marker-wrapper', + iconSize: [28, 28], + iconAnchor: [14, 14], +}); + +/** + * Renders one Leaflet marker per `Char_Player_C` actor extracted from + * the most recent savegame import. The marker is a violet pulsing dot + * to make the player position stand out against the resource and + * collectible pins. Hidden when the loaded payload belongs to a + * different game than the active selection (mirrors the gating used by + * the infrastructure canvas). + */ +export function PlayerMarkerLayer() { + const map = useMap(); + const players = useStore(s => s.mapInfrastructure.players); + const ownerGameId = useStore(s => s.mapInfrastructure.gameId); + const selectedGameId = useStore(s => s.games.selected); + + const isActive = + players.length > 0 && ownerGameId != null && ownerGameId === selectedGameId; + + useEffect(() => { + if (!isActive) return; + const layer = L.layerGroup(); + for (const p of players) { + const marker = L.marker(gameToLatLng(p.x, p.y), { + icon: PLAYER_ICON, + // Keep the player marker above resource / collectible pins. + zIndexOffset: 1000, + title: 'Player', + interactive: false, + keyboard: false, + }); + layer.addLayer(marker); + } + layer.addTo(map); + return () => { + layer.removeFrom(map); + }; + }, [map, players, isActive]); + + return null; +} diff --git a/src/map/ResourceMarkersLayer.tsx b/src/map/ResourceMarkersLayer.tsx new file mode 100644 index 00000000..459debfe --- /dev/null +++ b/src/map/ResourceMarkersLayer.tsx @@ -0,0 +1,319 @@ +import L from 'leaflet'; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import { useStore } from '@/core/zustand'; +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import type { WorldResourceNode } from '@/recipes/WorldResourceNodes'; +import { gameToLatLng } from './coords'; +import { + getExtractionMethodLabel, + getExtractionRate, + getExtractionUnit, + getExtractorsForNode, + OVERCLOCK_STEPS, +} from './extraction'; +import { + getPurityColor, + getPurityLabel, + getResourceMarkerIcon, +} from './markerIcons'; + +export interface ResourceMarkersLayerProps { + nodes: WorldResourceNode[]; + /** Ids of nodes the player has marked as "used" in the current game. */ + usedNodes: Set; + /** + * Currently selected game id, forwarded to the `toggleGameUsedNode` + * action so used-state is scoped per game. + */ + gameId: string | null; + /** + * When true, clicking a marker toggles its selection instead of + * opening the popup. Forwarded from `mapSelection.sumMode`. + */ + sumMode: boolean; +} + +const HTML_ESCAPES: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; + +function escapeHtml(value: string): string { + return value.replace(/[&<>"']/g, ch => HTML_ESCAPES[ch] ?? ch); +} + +function formatNumber(value: number): string { + return value.toLocaleString('en-US'); +} + +/** + * Builds the popup HTML for a single node. Buttons use `data-action` + * + `data-node-id` so a single delegated click listener on the map + * container can wire every popup's actions in one place (and survive + * Leaflet's DOM swaps when `setPopupContent` runs). + */ +function buildPopupHtml( + node: WorldResourceNode, + isUsed: boolean, + isSelected: boolean, +): string { + const item = AllFactoryItemsMap[node.resource]; + const name = escapeHtml(item?.displayName ?? node.resource); + const imagePath = item?.imagePath?.replace('_256', '_64') ?? ''; + const purityLabel = getPurityLabel(node.purity); + const purityColor = getPurityColor(node.purity); + const x = formatNumber(node.x); + const y = formatNumber(node.y); + const altitude = + node.z != null ? `${formatNumber(Math.round(node.z / 100))} m` : '—'; + + const extractors = getExtractorsForNode(node); + const unit = getExtractionUnit(node.resource); + const methodLabel = getExtractionMethodLabel(node.nodeType); + + const tableRows = extractors + .map(building => { + const cells = OVERCLOCK_STEPS.map(step => { + const rate = getExtractionRate(building, node.purity, step); + return `${formatNumber(rate)} ${escapeHtml(unit)}`; + }).join(''); + return `${escapeHtml(building.name)}${cells}`; + }) + .join(''); + + const headerCells = OVERCLOCK_STEPS.map(step => `${step}%`).join(''); + + const tableHtml = + extractors.length > 0 + ? ` + + + ${headerCells} + + ${tableRows} +
+ ` + : ''; + + const methodHtml = methodLabel + ? `

${escapeHtml(methodLabel)}

` + : ''; + + const usedLabel = isUsed ? 'Mark as unused' : 'Mark as used'; + const usedModifier = isUsed ? ' map-marker-popup__action--used' : ''; + const selectLabel = isSelected ? 'Remove from selection' : 'Add to selection'; + const selectModifier = isSelected + ? ' map-marker-popup__action--selected' + : ''; + const nodeIdAttr = escapeHtml(node.id); + const pills = [ + isUsed + ? 'Used' + : '', + isSelected + ? 'Selected' + : '', + ] + .filter(Boolean) + .join(' '); + + return ` +
+
+ ${imagePath ? `` : ''} +
+
${name}${pills ? ` ${pills}` : ''}
+
+ + ${escapeHtml(purityLabel)} purity +
+
+
+
+
Coordinates
${x} / ${y}
+
Altitude
${altitude}
+
+ ${methodHtml} + ${tableHtml} +
+ + +
+
+ `; +} + +/** + * Imperative leaflet layer that renders the given resource nodes as + * individual (non-clustered) markers. We bypass react-leaflet's + * component model so all markers can share a single render pass + * without per-marker React reconciliation cost. + * + * Used-node and selected-node state are applied both as visual + * variants on the marker icon and as toggleable actions in the + * popup. Click handling uses a single delegated listener on the map + * container so it keeps working across Leaflet's `setPopupContent` + * DOM swaps. + * + * Selected-node state is intentionally not in the effect deps — we + * imperatively repaint only the affected marker when selection + * changes, rather than tearing the whole layer down on every toggle. + */ +export function ResourceMarkersLayer({ + nodes, + usedNodes, + gameId, + sumMode, +}: ResourceMarkersLayerProps) { + const map = useMap(); + + useEffect(() => { + const layer = L.layerGroup(); + const markersByNodeId = new Map(); + const nodesByNodeId = new Map(); + const getCurrentSelection = () => + new Set(useStore.getState().mapSelection.selectedNodeIds); + + /** + * Repaints a single marker's icon and popup content from current + * store state. Called after any action that might have changed + * its used or selected status. + */ + const repaintMarker = (nodeId: string) => { + const marker = markersByNodeId.get(nodeId); + const node = nodesByNodeId.get(nodeId); + if (!marker || !node) return; + const isUsed = usedNodes.has(nodeId); + const isSelected = getCurrentSelection().has(nodeId); + marker.setIcon( + getResourceMarkerIcon(node.resource, node.purity, { + used: isUsed, + selected: isSelected, + }), + ); + // `getPopup()` is only non-null in normal mode, since compare + // mode skips `bindPopup` entirely. + if (marker.getPopup()) { + marker.setPopupContent(buildPopupHtml(node, isUsed, isSelected)); + } + }; + + const initialSelection = getCurrentSelection(); + + for (const node of nodes) { + nodesByNodeId.set(node.id, node); + const isUsed = usedNodes.has(node.id); + const isSelected = initialSelection.has(node.id); + const marker = L.marker(gameToLatLng(node.x, node.y), { + icon: getResourceMarkerIcon(node.resource, node.purity, { + used: isUsed, + selected: isSelected, + }), + }); + + if (sumMode) { + // Sum mode: no popup. Click toggles selection directly. + marker.on('click', event => { + L.DomEvent.stopPropagation(event); + useStore.getState().toggleNodeSelected(node.id); + repaintMarker(node.id); + }); + } else { + // Normal mode: click opens popup. Selection is managed from + // the popup's "Add to selection" action. `maxWidth` is high + // enough to fit the widest content we produce — two + // extractors × 5 overclock columns × `m³/min` units — while + // still letting Leaflet auto-size narrower popups naturally. + marker.bindPopup(buildPopupHtml(node, isUsed, isSelected), { + closeButton: true, + offset: [0, -4], + maxWidth: 720, + }); + } + + markersByNodeId.set(node.id, marker); + layer.addLayer(marker); + } + + layer.addTo(map); + + // Delegated click listener for popup action buttons — one per + // effect run, attached to the map's root element. + const container = map.getContainer(); + const onDelegatedClick = (event: MouseEvent) => { + const target = event.target as HTMLElement | null; + if (!target) return; + const button = target.closest( + 'button[data-action][data-node-id]', + ); + if (!button) return; + + const nodeId = button.dataset.nodeId; + const action = button.dataset.action; + if (!nodeId || !action) return; + + event.preventDefault(); + event.stopPropagation(); + + if (action === 'toggle-used') { + useStore.getState().toggleGameUsedNode(gameId, nodeId); + } else if (action === 'toggle-selected') { + useStore.getState().toggleNodeSelected(nodeId); + } else { + return; + } + + repaintMarker(nodeId); + }; + + container.addEventListener('click', onDelegatedClick); + + // Keep every marker's "selected" visual in sync with store + // changes that didn't originate from a click on this marker + // (e.g. clearing the selection from the summary panel). We + // diff the new vs old selection and repaint only what changed. + let lastSelection = initialSelection; + const unsubscribeSelection = useStore.subscribe(state => { + const next = new Set(state.mapSelection.selectedNodeIds); + if (next.size === lastSelection.size) { + let identical = true; + for (const id of next) { + if (!lastSelection.has(id)) { + identical = false; + break; + } + } + if (identical) return; + } + // Repaint every id that flipped in either direction. + for (const id of next) if (!lastSelection.has(id)) repaintMarker(id); + for (const id of lastSelection) if (!next.has(id)) repaintMarker(id); + lastSelection = next; + }); + + return () => { + unsubscribeSelection(); + container.removeEventListener('click', onDelegatedClick); + layer.removeFrom(map); + }; + // `sumMode` is in the deps so we fully rebind markers whenever + // the mode flips — compact and unambiguous behaviour vs. trying + // to detach/attach click handlers on the fly. + }, [map, nodes, usedNodes, gameId, sumMode]); + + return null; +} diff --git a/src/map/ShareUrlSync.tsx b/src/map/ShareUrlSync.tsx new file mode 100644 index 00000000..68be7cb7 --- /dev/null +++ b/src/map/ShareUrlSync.tsx @@ -0,0 +1,188 @@ +import { useEffect, useRef } from 'react'; +import { useMap } from 'react-leaflet'; +import { useStore } from '@/core/zustand'; +import type { CollectibleType } from '@/recipes/WorldCollectibles'; +import type { Purity } from '@/recipes/WorldResourceNodes'; +import { + decodeShareUrl, + encodeShareUrl, + type ShareableMapState, + shareUrlsEqual, +} from './shareUrlState'; + +/** + * Live-syncs the map's view + filter state with `location.hash`. + * + * - On first mount, parses the incoming hash (if any) and writes + * that snapshot into the zustand store + Leaflet viewport. This + * lets a shared link override the recipient's persisted state. + * - After mount, watches the store and the Leaflet map and writes + * the current state back to the hash on a short debounce, using + * `history.replaceState` so the browser's back/forward stack + * doesn't fill up with one entry per pan tick. + * + * Intentionally lives as a child of `` so it can use + * `useMap()` — `MapContainer` itself can't read its own viewport + * imperatively from outside. + */ +export function ShareUrlSync() { + const map = useMap(); + + /** True until we've finished the one-shot URL→state apply. */ + const initialApplyDoneRef = useRef(false); + /** Last hash we wrote, so we can ignore the resulting hashchange. */ + const lastWrittenHashRef = useRef(null); + + // --------------------------------------------------------------- + // 1) On mount, hydrate from the URL hash (if it looks like ours) + // --------------------------------------------------------------- + useEffect(() => { + if (!initialApplyDoneRef.current) { + const incoming = decodeShareUrl(window.location.hash); + if (incoming) { + applyToStore(incoming); + applyToMap(map, incoming); + } + initialApplyDoneRef.current = true; + } + // Also listen for back/forward navigation so the map updates + // when the user moves through history. Same parser, no + // re-application of unrelated hashes. + const onPopState = () => { + const next = decodeShareUrl(window.location.hash); + if (!next) return; + applyToStore(next); + applyToMap(map, next); + }; + window.addEventListener('popstate', onPopState); + return () => { + window.removeEventListener('popstate', onPopState); + }; + }, [map]); + + // --------------------------------------------------------------- + // 2) After mount, write store + viewport changes back to the URL + // --------------------------------------------------------------- + useEffect(() => { + let timer: number | null = null; + + /** + * Recomputes the share URL from the current store + viewport + * and writes it via `replaceState`. Skips the initial mount + * (the hydrator above already settled state) and skips writes + * that wouldn't change the encoded hash, so a slow pan that + * fires `move` 60 times a second only rewrites once after it + * settles. + */ + const sync = () => { + if (!initialApplyDoneRef.current) return; + const state = readStateFromStoreAndMap(map); + if (!state) return; + const params = encodeShareUrl(state); + const nextHash = `#${params.toString()}`; + if (lastWrittenHashRef.current === nextHash) return; + // Only bypass the equality check when the parsed forms match; + // covers the edge case where another writer rewrote the hash + // without going through this hook. + const currentParsed = new URLSearchParams( + window.location.hash.replace(/^#/, ''), + ); + if (shareUrlsEqual(currentParsed, params)) { + lastWrittenHashRef.current = nextHash; + return; + } + lastWrittenHashRef.current = nextHash; + const url = `${window.location.pathname}${window.location.search}${nextHash}`; + window.history.replaceState(window.history.state, '', url); + }; + + /** Debounced wrapper. 250ms feels live without thrashing. */ + const scheduleSync = () => { + if (timer != null) window.clearTimeout(timer); + timer = window.setTimeout(sync, 250); + }; + + // Map viewport changes — `moveend` covers both pan and zoom on + // commit; we deliberately don't use `move` to avoid spam. + map.on('moveend', scheduleSync); + map.on('zoomend', scheduleSync); + + // Store changes — subscribe to the entire `map` slice and the + // viewport-irrelevant fields are filtered out by the encoder. + const unsub = useStore.subscribe(scheduleSync); + + // Trigger one initial sync so a fresh visit (no hash) gets a + // shareable URL right away. + scheduleSync(); + + return () => { + if (timer != null) window.clearTimeout(timer); + map.off('moveend', scheduleSync); + map.off('zoomend', scheduleSync); + unsub(); + }; + }, [map]); + + return null; +} + +/** + * Pulls the current shareable state out of the zustand store + + * Leaflet map. Returns `null` when the store hasn't rehydrated yet + * (the slice can be undefined for a frame after first paint). + */ +function readStateFromStoreAndMap(map: L.Map): ShareableMapState | null { + const state = useStore.getState(); + const mapSlice = state.map; + if (!mapSlice) return null; + const center = map.getCenter(); + return { + zoom: map.getZoom(), + center: [center.lat, center.lng], + resourceFilters: mapSlice.resourceFilters, + collectibleVisibility: mapSlice.collectibleVisibility, + hideUsedNodes: mapSlice.hideUsedNodes, + hideCollectedCollectibles: mapSlice.hideCollectedCollectibles, + }; +} + +/** + * Applies the decoded URL state to the zustand store. Each field + * is independently optional — the URL might omit `cv` if the link + * was created on a build that didn't have collectibles yet, and + * we want that to leave the recipient's collectible visibility + * alone instead of force-hiding everything. + */ +function applyToStore(state: Partial): void { + const store = useStore.getState(); + if (state.resourceFilters) { + store.setResourceFilters(state.resourceFilters as Record); + } + if (state.collectibleVisibility) { + store.setCollectibleVisibility( + state.collectibleVisibility as Record, + ); + } + if (typeof state.hideUsedNodes === 'boolean') { + store.setHideUsedNodes(state.hideUsedNodes); + } + if (typeof state.hideCollectedCollectibles === 'boolean') { + store.setHideCollectedCollectibles(state.hideCollectedCollectibles); + } +} + +/** + * Drives the Leaflet viewport from a decoded URL state. Uses + * `setView` (no animation) since this only fires on initial load + * or back/forward navigation; `flyTo` would feel sluggish in those + * contexts. + */ +function applyToMap(map: L.Map, state: Partial): void { + if (state.center && typeof state.zoom === 'number') { + map.setView(state.center, state.zoom, { animate: false }); + } else if (state.center) { + map.setView(state.center, map.getZoom(), { animate: false }); + } else if (typeof state.zoom === 'number') { + map.setZoom(state.zoom, { animate: false }); + } +} diff --git a/src/map/WorldMapView.module.css b/src/map/WorldMapView.module.css new file mode 100644 index 00000000..0c0ae277 --- /dev/null +++ b/src/map/WorldMapView.module.css @@ -0,0 +1,603 @@ +/* + * Root of the Mantine Dropzone wrapping the map. Mantine's default + * Dropzone styling paints a dashed border, md padding, and a dark-6 + * fill; the drop target here should be transparent chrome around + * the existing `mapShell`, which supplies the rounded corners, + * clipping, and background. This rule strips the defaults while + * keeping the 100% fill behaviour so the map still occupies the + * full panel. + */ +.dropzoneRoot { + position: relative; + width: 100%; + height: 100%; + padding: 0; + border: none; + background: transparent; + border-radius: var(--mantine-radius-md); + cursor: default; +} + +/* Neutralize the dark-6 background tint Mantine paints on the + Dropzone root when files are being dragged over it. Our own + dropOverlay inside `mapShell` handles the visual feedback. */ +.dropzoneRoot:where([data-accept]), +.dropzoneRoot:where([data-reject]) { + background-color: transparent; + border-color: transparent; +} + +/* + * Mantine renders children inside an `inner` wrapper div (the class + * Dropzone.inner) which by default has no `height`. Without this + * rule, `mapShell`'s `height: 100%` has no definite parent to + * resolve against and the map collapses to 0px. Setting the inner + * to 100%/100% lets the existing height chain flow through + * untouched. + */ +.dropzoneInner { + width: 100%; + height: 100%; +} + +.mapShell { + position: relative; + width: 100%; + height: 100%; + border-radius: var(--mantine-radius-md); + overflow: hidden; + background-color: var(--mantine-color-dark-8); + isolation: isolate; +} + +.map { + width: 100%; + height: 100%; + background-color: var(--mantine-color-dark-5); +} + +.nodeCount { + position: absolute; + bottom: 12px; + left: 12px; + z-index: 500; + padding: 4px 10px; + border-radius: var(--mantine-radius-sm); + background-color: rgba(20, 21, 23, 0.85); + color: var(--mantine-color-gray-3); + font-size: 12px; + pointer-events: none; +} + +.sumBanner { + position: absolute; + top: 12px; + left: 50%; + transform: translateX(-50%); + z-index: 500; + padding: 6px 14px; + border-radius: var(--mantine-radius-sm); + background-color: rgba(109, 40, 217, 0.92); + color: white; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.02em; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); + pointer-events: none; +} + +/* Progress toast shown during a savegame import. Rendered as a + sibling of `mapShell` (inside the Dropzone root) rather than a + child, so it shares the Dropzone's stacking context with + Mantine's LoadingOverlay (default z-index: 400). The 410 value + keeps it just above the dimmed backdrop so the user can read the + parser status. */ +.importProgress { + position: absolute; + bottom: 46px; + left: 12px; + z-index: 410; + min-width: 220px; + padding: 8px 12px; + border-radius: var(--mantine-radius-sm); + background-color: rgba(20, 21, 23, 0.92); + color: var(--mantine-color-gray-1); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); + pointer-events: none; +} + +/* Full-area overlay shown while the user is dragging a file over the + map. The Dropzone.Accept/Reject slots control when this overlay is + rendered at all, so it only ever paints during an active drag. */ +.dropOverlay { + position: absolute; + inset: 0; + z-index: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + border-radius: var(--mantine-radius-md); + background-color: rgba(20, 21, 23, 0.6); + color: var(--mantine-color-gray-0); + border: 2px dashed var(--mantine-color-teal-5); + pointer-events: none; + text-align: center; +} + +.dropOverlayReject { + border-color: var(--mantine-color-red-5); + color: var(--mantine-color-red-1); + background-color: rgba(60, 10, 10, 0.55); +} + +/* Marker styling (rendered as raw HTML by Leaflet via divIcon) */ + +:global(.map-marker-wrapper) { + background: transparent; + border: none; +} + +:global(.map-marker) { + position: relative; + width: 32px; + height: 32px; + /* Tip sits at (30.14, 30.14) of a 32-square viewBox (~94%). Scaling from + the tip keeps the pin point anchored to the map coordinate regardless + of zoom scale. */ + transform-origin: 94% 94%; + transform: scale(var(--marker-zoom-scale, 1)); + /* `filter` is intentionally not in the transition list: it forces a per- + frame raster of every marker, and at hundreds of nodes the cost shows + up as visible lag. Used/selected variant changes are click-driven and + read fine without a tween. */ + transition: + transform 120ms ease-out, + opacity 120ms ease-out; +} + +:global(.map-marker:hover) { + transform: scale(calc(var(--marker-zoom-scale, 1) * 1.1)); +} + +:global(.map-marker__shape) { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; + overflow: visible; +} + +:global(.map-marker__shape-path) { + /* Background = ring color heavily tinted toward white so the game icons + stay readable, with a strong dark-ring border doing the purity + signalling instead of the fill. */ + fill: color-mix(in srgb, var(--ring, #f1c40f) 35%, white 65%); + /* Border = darker shade of the ring, painted as a native stroke. We + initially used 4 stacked drop-shadows (the "fake stroke" trick) for + a softer halo look, but at hundreds of markers the filter cost + lagged the map. Stroke is GPU-fast and visually close. */ + stroke: color-mix(in srgb, var(--ring, #f1c40f) 70%, black 30%); + stroke-width: 1.25; + stroke-linejoin: round; + stroke-linecap: round; +} + +/* Collectibles share the pin silhouette but sit on a plain white fill so + the themed glyph + ring color do all the communication. */ +:global(.map-marker--collectible) :global(.map-marker__shape-path) { + fill: white; + stroke: var(--ring, #f1c40f); +} + +:global(.map-marker--used) { + opacity: 0.5; + filter: grayscale(0.6); +} + +:global(.map-marker--used:hover) { + opacity: 0.9; + filter: grayscale(0); +} + +:global(.map-marker__used-badge) { + position: absolute; + top: -4px; + right: -4px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: var(--mantine-color-teal-6); + color: white; + font-size: 10px; + line-height: 14px; + text-align: center; + font-weight: 700; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + pointer-events: none; + z-index: 2; +} + +:global(.map-marker--selected) { + opacity: 1; + z-index: 1000; +} + +/* Selected variant repaints the pin border with a thicker violet stroke + instead of a drop-shadow halo, again to stay off the filter pipeline. */ +:global(.map-marker--selected) :global(.map-marker__shape-path) { + stroke: var(--mantine-color-violet-5); + stroke-width: 2.5; +} + +:global(.map-marker--used.map-marker--selected) { + opacity: 0.85; + filter: grayscale(0.3); +} + +:global(.map-marker__selected-badge) { + position: absolute; + bottom: -4px; + left: -4px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: var(--mantine-color-violet-6); + color: white; + font-size: 10px; + line-height: 14px; + text-align: center; + font-weight: 700; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + pointer-events: none; + z-index: 2; +} + +/* Player marker (savegame import): violet pulsing dot, distinct from + resource / collectible pins so the user can spot themselves at a + glance. Rendered via L.divIcon, so all selectors are :global. */ + +:global(.map-player-marker-wrapper) { + background: transparent; + border: none; + pointer-events: none; +} + +:global(.map-player-marker) { + position: relative; + width: 28px; + height: 28px; +} + +:global(.map-player-marker__pulse) { + position: absolute; + inset: 0; + border-radius: 50%; + background: var(--mantine-color-violet-5); + opacity: 0.55; + animation: map-player-pulse 1.6s ease-out infinite; + pointer-events: none; +} + +:global(.map-player-marker__core) { + position: absolute; + inset: 7px; + border-radius: 50%; + background: var(--mantine-color-violet-5); + border: 2px solid white; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55); +} + +@keyframes map-player-pulse { + 0% { + transform: scale(0.55); + opacity: 0.65; + } + 100% { + transform: scale(1.9); + opacity: 0; + } +} + +/* Popup styling */ + +:global(.map-marker-popup) { + font-family: var(--mantine-font-family); + color: var(--mantine-color-gray-1); +} + +:global(.map-marker-popup__header) { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--mantine-color-dark-5); +} + +:global(.map-marker-popup__icon) { + width: 32px; + height: 32px; + flex: 0 0 32px; +} + +:global(.map-marker-popup__title) { + display: flex; + flex-direction: column; + min-width: 0; +} + +:global(.map-marker-popup__name) { + font-weight: 600; + font-size: 14px; + line-height: 1.2; + color: var(--mantine-color-gray-0); +} + +:global(.map-marker-popup__meta) { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + line-height: 1.3; + color: var(--mantine-color-gray-4); +} + +:global(.map-marker-popup__purity-dot) { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35); + flex: 0 0 10px; +} + +:global(.map-marker-popup__stats) { + display: grid; + grid-template-columns: auto 1fr; + gap: 2px 10px; + margin: 0 0 10px; + font-size: 12px; + line-height: 1.4; +} + +:global(.map-marker-popup__stats dt) { + color: var(--mantine-color-gray-5); + font-weight: 500; +} + +:global(.map-marker-popup__stats dd) { + margin: 0; + color: var(--mantine-color-gray-2); + font-variant-numeric: tabular-nums; +} + +:global(.map-marker-popup__method) { + margin: 0 0 10px; + padding: 6px 8px; + border-radius: 4px; + background-color: var(--mantine-color-dark-6); + border-left: 2px solid var(--mantine-color-gray-6); + font-size: 11px; + line-height: 1.4; + color: var(--mantine-color-gray-3); +} + +:global(.map-marker-popup__rates) { + width: 100%; + border-collapse: collapse; + font-size: 12px; + line-height: 1.3; + font-variant-numeric: tabular-nums; + color: var(--mantine-color-gray-1); +} + +:global(.map-marker-popup__rates) th, +:global(.map-marker-popup__rates) td { + padding: 4px 8px; + text-align: right; + border-bottom: 1px solid var(--mantine-color-dark-5); + white-space: nowrap; +} + +:global(.map-marker-popup__rates) thead th { + font-weight: 600; + color: var(--mantine-color-gray-4); + background-color: var(--mantine-color-dark-6); +} + +:global(.map-marker-popup__rates-unit) { + font-size: 10px; + color: var(--mantine-color-gray-5); + margin-left: 2px; +} + +:global(.map-marker-popup__rates) tbody th { + text-align: left; + font-weight: 500; + color: var(--mantine-color-gray-2); + background-color: var(--mantine-color-dark-6); +} + +:global(.map-marker-popup__rates) tbody tr:last-child th, +:global(.map-marker-popup__rates) tbody tr:last-child td { + border-bottom: none; +} + +:global(.map-marker-popup__pill) { + display: inline-block; + margin-left: 6px; + padding: 1px 6px; + border-radius: 999px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + vertical-align: 2px; +} + +:global(.map-marker-popup__pill--used) { + background-color: var(--mantine-color-teal-9); + color: var(--mantine-color-teal-2); +} + +:global(.map-marker-popup__pill--selected) { + background-color: var(--mantine-color-violet-9); + color: var(--mantine-color-violet-2); +} + +/* + * Drop pod cost chips. Rendered as a labeled row of icon + amount + * pills so the player can scan it like a recipe card without us + * needing a full table. + */ +:global(.map-marker-popup__cost) { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0 0 10px; + font-size: 12px; + line-height: 1.3; +} + +:global(.map-marker-popup__cost-label) { + color: var(--mantine-color-gray-5); + font-weight: 500; +} + +:global(.map-marker-popup__cost-items) { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +:global(.map-marker-popup__cost-chip) { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 6px 2px 4px; + border-radius: 999px; + background-color: var(--mantine-color-dark-6); + color: var(--mantine-color-gray-1); + font-variant-numeric: tabular-nums; +} + +:global(.map-marker-popup__cost-chip img) { + width: 18px; + height: 18px; + flex: 0 0 18px; +} + +:global(.map-marker-popup__actions) { + display: flex; + justify-content: flex-end; + gap: 6px; + margin-top: 10px; + flex-wrap: wrap; +} + +:global(.map-marker-popup__action) { + appearance: none; + border: 1px solid var(--mantine-color-dark-4); + background-color: var(--mantine-color-dark-6); + color: var(--mantine-color-gray-1); + padding: 4px 10px; + border-radius: var(--mantine-radius-sm); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +:global(.map-marker-popup__action:hover) { + background-color: var(--mantine-color-dark-5); + border-color: var(--mantine-color-dark-3); + color: var(--mantine-color-gray-0); +} + +:global(.map-marker-popup__action--used) { + border-color: var(--mantine-color-teal-7); + color: var(--mantine-color-teal-2); +} + +:global(.map-marker-popup__action--used:hover) { + background-color: var(--mantine-color-dark-5); + color: var(--mantine-color-teal-0); +} + +:global(.map-marker-popup__action--selected) { + border-color: var(--mantine-color-violet-7); + color: var(--mantine-color-violet-2); +} + +:global(.map-marker-popup__action--selected:hover) { + background-color: var(--mantine-color-dark-5); + color: var(--mantine-color-violet-0); +} + +/* Dark-theme polish for default Leaflet chrome */ + +:global(.leaflet-container) { + font-family: var(--mantine-font-family); +} + +:global(.leaflet-popup-content-wrapper), +:global(.leaflet-popup-tip) { + background-color: var(--mantine-color-dark-7); + color: var(--mantine-color-gray-1); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); +} + +:global(.leaflet-popup-content) { + margin: 12px; +} + +:global(.leaflet-container) :global(a.leaflet-popup-close-button) { + top: 8px; + right: 8px; + width: 28px; + height: 28px; + padding: 0; + box-sizing: border-box; + display: block; + text-align: center; + border-radius: var(--mantine-radius-sm); + color: var(--mantine-color-gray-3); + font: 700 16px / 28px var(--mantine-font-family); + text-decoration: none; + transition: + background-color 120ms ease, + color 120ms ease; +} + +:global(.leaflet-container) :global(a.leaflet-popup-close-button:hover) { + background-color: var(--mantine-color-dark-5); + color: var(--mantine-color-gray-0); +} + +:global(.leaflet-control-zoom a) { + background-color: var(--mantine-color-dark-7); + color: var(--mantine-color-gray-2); + border-color: var(--mantine-color-dark-5); +} + +:global(.leaflet-control-zoom a:hover) { + background-color: var(--mantine-color-dark-6); + color: var(--mantine-color-gray-0); +} + +/* Leaflet adds `leaflet-disabled` at min/max zoom. Its default rule paints + the button with a light-gray background that clashes with our dark chrome, + so repaint it in the same dark palette with dimmed text. */ +:global(.leaflet-control-zoom a.leaflet-disabled), +:global(.leaflet-control-zoom a.leaflet-disabled:hover) { + background-color: var(--mantine-color-dark-8); + color: var(--mantine-color-dark-3); + border-color: var(--mantine-color-dark-5); + cursor: default; +} diff --git a/src/map/WorldMapView.tsx b/src/map/WorldMapView.tsx new file mode 100644 index 00000000..dca54e93 --- /dev/null +++ b/src/map/WorldMapView.tsx @@ -0,0 +1,325 @@ +import { Progress, Text } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { notifications } from '@mantine/notifications'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import { useEffect, useMemo } from 'react'; +import { MapContainer, TileLayer, useMap } from 'react-leaflet'; +import { useShallowStore } from '@/core/zustand'; +import { useSavegameImport } from '@/recipes/savegame/useSavegameImport'; +import { + COLLECTIBLE_TYPES, + type CollectibleType, + getWorldCollectibles, +} from '@/recipes/WorldCollectibles'; +import { + getWorldResourceNodes, + type Purity, +} from '@/recipes/WorldResourceNodes'; +import { CollectibleMarkersLayer } from './CollectibleMarkersLayer'; +import { + DEFAULT_CENTER, + DEFAULT_ZOOM, + IMAGE_BOUNDS, + MAX_ZOOM, + MIN_ZOOM, +} from './coords'; +import { InfrastructureCanvasLayer } from './infrastructure/InfrastructureCanvasLayer'; +import { MapSelectionSummary } from './MapSelectionSummary'; +import { PlayerMarkerLayer } from './PlayerMarkerLayer'; +import { ResourceMarkersLayer } from './ResourceMarkersLayer'; +import { ShareUrlSync } from './ShareUrlSync'; +import classes from './WorldMapView.module.css'; + +const DEFAULT_TILES_BASE_URL = + 'https://satisfactory-logistics-maps.fra1.cdn.digitaloceanspaces.com/map/v2'; + +/** + * Deepest displayed zoom that still maps 1:1 to a real tile in the + * pyramid. `detectRetina` on the TileLayer bumps zoomOffset by 1 (it + * pulls tiles from one pyramid level above the displayed zoom) and + * decrements its own internal maxZoom by 1, so on retina the highest + * URL-backed displayed zoom is `MAX_ZOOM - 1` rather than `MAX_ZOOM`. + */ +const MAX_NATIVE_DISPLAYED_ZOOM = L.Browser.retina ? MAX_ZOOM - 1 : MAX_ZOOM; + +/** + * Extra zoom steps allowed past the native pyramid. Leaflet stretches + * the deepest tiles via CSS for these levels (no extra tile fetches), + * so two steps give ~4x more zoom for inspecting individual machines + * and splines without the upscaled imagery turning into a blur. + */ +const OVERZOOM_LEVELS = 2; +const MAX_DISPLAYED_ZOOM = MAX_NATIVE_DISPLAYED_ZOOM + OVERZOOM_LEVELS; + +/** + * `detectRetina` decrements the TileLayer's own maxZoom by 1, and + * GridLayer refuses to render any tile zoom strictly greater than that + * effective maxZoom (the "grey screen" failure mode). Compensate by + * passing a TileLayer maxZoom that is one above the map's maxZoom on + * retina, so the post-detectRetina effective value lands on + * `MAX_DISPLAYED_ZOOM`. + */ +const TILE_LAYER_MAX_ZOOM = L.Browser.retina + ? MAX_DISPLAYED_ZOOM + 1 + : MAX_DISPLAYED_ZOOM; + +const TILES_BASE_URL = + import.meta.env.VITE_MAP_TILES_BASE_URL ?? DEFAULT_TILES_BASE_URL; + +export interface WorldMapViewProps { + gameId?: string | null; +} + +/** + * Shared empty array so the `usedNodesList` selector returns a stable + * reference when the current game has no used-node marks. Prevents + * `useShallow` from treating the slice as "changed" every render. + */ +const EMPTY_USED_NODES: readonly string[] = []; +/** Stable fallback for `resourceFilters` when the slice hasn't rehydrated yet. */ +const EMPTY_RESOURCE_FILTERS: Record = {}; +/** Stable fallback for collectible visibility before rehydrate finishes. */ +const EMPTY_COLLECTIBLE_VISIBILITY: Record = (() => { + const visibility = {} as Record; + for (const type of COLLECTIBLE_TYPES) visibility[type] = true; + return visibility; +})(); +/** Stable empty list mirror of `EMPTY_USED_NODES` for collectibles. */ +const EMPTY_COLLECTED_LIST: readonly string[] = []; + +/** Marker scale at the min and max zoom levels; linearly interpolated between. */ +const MARKER_SCALE_AT_MIN_ZOOM = 0.75; +const MARKER_SCALE_AT_MAX_ZOOM = 1.75; + +/** + * Writes `--marker-zoom-scale` on the Leaflet container so `.map-marker` + * can scale its transform with the current zoom level — markers grow as + * you zoom in, shrink as you zoom out. + */ +function MarkerZoomScaleController() { + const map = useMap(); + useEffect(() => { + const container = map.getContainer(); + const apply = () => { + const zoomRange = MAX_ZOOM - MIN_ZOOM || 1; + const t = (map.getZoom() - MIN_ZOOM) / zoomRange; + const scale = + MARKER_SCALE_AT_MIN_ZOOM + + t * (MARKER_SCALE_AT_MAX_ZOOM - MARKER_SCALE_AT_MIN_ZOOM); + container.style.setProperty('--marker-zoom-scale', scale.toFixed(3)); + }; + apply(); + map.on('zoomend', apply); + return () => { + map.off('zoomend', apply); + container.style.removeProperty('--marker-zoom-scale'); + }; + }, [map]); + return null; +} + +/** + * Mantine forwards an `Accept` object straight to react-dropzone's + * `accept` option. Passing the bare `['.sav']` array form would + * make react-dropzone validate `.sav` as a MIME type and spam + * `Skipped ".sav" because it is not a valid MIME type` warnings on + * every drag. Using the object form keys by a real MIME + * (`application/octet-stream`, the generic binary type browsers + * already report for `.sav`) and lists the extension as the value + * — this is the canonical shape and silences the warning while + * keeping extension-based matching. + */ +const SAVEGAME_ACCEPT = { + 'application/octet-stream': ['.sav'], +}; + +export function WorldMapView({ gameId }: WorldMapViewProps) { + const { + resourceFilters, + hideUsedNodes, + usedNodesList, + sumMode, + collectibleVisibility, + hideCollectedCollectibles, + collectedList, + } = useShallowStore(state => { + const mapState = state.map; + const game = gameId ? state.games.games[gameId] : null; + return { + resourceFilters: mapState?.resourceFilters ?? EMPTY_RESOURCE_FILTERS, + hideUsedNodes: mapState?.hideUsedNodes ?? false, + usedNodesList: game?.usedNodes ?? EMPTY_USED_NODES, + sumMode: state.mapSelection?.sumMode ?? false, + collectibleVisibility: + mapState?.collectibleVisibility ?? EMPTY_COLLECTIBLE_VISIBILITY, + hideCollectedCollectibles: mapState?.hideCollectedCollectibles ?? false, + collectedList: game?.collectedItems ?? EMPTY_COLLECTED_LIST, + }; + }); + + const usedNodes = useMemo(() => new Set(usedNodesList), [usedNodesList]); + const collectedIds = useMemo(() => new Set(collectedList), [collectedList]); + + const filteredNodes = useMemo(() => { + return getWorldResourceNodes(gameId).filter(node => { + if (!resourceFilters[node.resource]?.includes(node.purity)) return false; + if (hideUsedNodes && usedNodes.has(node.id)) return false; + return true; + }); + }, [gameId, resourceFilters, hideUsedNodes, usedNodes]); + + const filteredCollectibles = useMemo(() => { + return getWorldCollectibles().filter(collectible => { + if (!collectibleVisibility[collectible.type]) return false; + if (hideCollectedCollectibles && collectedIds.has(collectible.id)) + return false; + return true; + }); + }, [collectibleVisibility, hideCollectedCollectibles, collectedIds]); + + const { importing, progress, importAndApplyToGame } = useSavegameImport(); + + const handleDroppedSavegame = (files: File[]) => { + const file = files[0]; + if (!file) return; + importAndApplyToGame(file, gameId, { + defaultRecipes: true, + usedNodes: true, + infrastructure: true, + }).catch(() => { + // Notification surfaced by the hook; nothing else to do here. + }); + }; + + const handleRejectedSavegame = () => { + notifications.show({ + title: 'Unsupported file', + message: 'Drop a single Satisfactory .sav save file to import it.', + color: 'red', + }); + }; + + return ( + +
+ + + + + + + + + +
+ {filteredNodes.length} node{filteredNodes.length === 1 ? '' : 's'} + {' · '} + {filteredCollectibles.length} collectible + {filteredCollectibles.length === 1 ? '' : 's'} +
+ {sumMode ? ( +
+ Sum mode — tap nodes to add or remove them from the total +
+ ) : null} + + +
+ + Drop save to import recipes, used nodes, and built infrastructure + +
+
+ +
+ + Only .sav files are supported + +
+
+
+ {/* + Sibling of `mapShell` (and of Mantine's internal LoadingOverlay) + rather than a child, so it shares the dropzone root's stacking + context. `mapShell` uses `isolation: isolate` which would trap + children inside its own stacking context — at that point + `z-index` could not lift this banner above the LoadingOverlay + backdrop (default `z-index: 400`). Rendered here with + `z-index: 401` it paints just above the backdrop while the + Mantine spinner sits next to it. + */} + {importing ? ( +
+ + {progress.message ?? 'Parsing savegame…'} + + +
+ ) : null} +
+ ); +} diff --git a/src/map/coords.ts b/src/map/coords.ts new file mode 100644 index 00000000..721a32e7 --- /dev/null +++ b/src/map/coords.ts @@ -0,0 +1,77 @@ +import L from 'leaflet'; + +/** + * Projects game world coordinates (cm, Unreal default unit) into + * Leaflet `CRS.Simple` LatLng space. Backdrop is a WebP tile pyramid + * (levels 0-6) on DigitalOcean Spaces; see the map README for details + * on calibration, tile generation, and the coord mapping. + */ + +/** Logical coord-space size. Equals the tile size so zoom 0 == 1 tile. */ +export const IMAGE_SIZE = 256; + +/** Game-space bounds of the playable area, in centimeters. */ +export const WORLD_X_MIN = -324_700; +export const WORLD_X_MAX = 425_300; +export const WORLD_Y_MIN = -375_000; +export const WORLD_Y_MAX = 375_000; + +/** + * Bounds for the world map image in Leaflet `CRS.Simple` coordinates. + * + * `CRS.Simple` has y increasing upward (northing convention, see + * https://leafletjs.com/examples/crs-simple/crs-simple.html), so the + * top edge of the image sits at `lat = 0` and the bottom edge at + * `lat = -IMAGE_SIZE`. This orientation is required for XYZ tile + * layers: pixel-y = -lat is then non-negative over the image, and + * tile indices (pixel-y / tileSize) stay in [0, imageHeight/tileSize), + * matching the `gdal2tiles.py --xyz` pyramid on disk. + */ +export const IMAGE_BOUNDS: L.LatLngBoundsExpression = [ + [-IMAGE_SIZE, 0], + [0, IMAGE_SIZE], +]; + +const X_RANGE = WORLD_X_MAX - WORLD_X_MIN; +const Y_RANGE = WORLD_Y_MAX - WORLD_Y_MIN; + +/** + * Converts a `(gameX, gameY)` point in centimeters to a Leaflet + * `LatLng` for `CRS.Simple`. Game `+x` maps right. The current tile + * pyramid is rendered with game `+y` (north on the in-game compass) at + * the *bottom* of the image, so `+Y` maps to `lat = -IMAGE_SIZE` and + * `-Y` maps to `lat = 0`. + */ +export function gameToLatLng(gameX: number, gameY: number): L.LatLng { + const px = ((gameX - WORLD_X_MIN) / X_RANGE) * IMAGE_SIZE; + const py = -((gameY - WORLD_Y_MIN) / Y_RANGE) * IMAGE_SIZE; + return L.latLng(py, px); +} + +/** + * Inverse of {@link gameToLatLng}: turns a Leaflet `LatLng` (CRS.Simple) + * back into a `(gameX, gameY)` pair in cm. Used by the infrastructure + * tile layer to map a tile's pixel corners to a worldspace bbox so the + * spatial index can be queried for entities that fall inside. + */ +export function latLngToGame(latLng: L.LatLng): { x: number; y: number } { + const x = (latLng.lng / IMAGE_SIZE) * X_RANGE + WORLD_X_MIN; + const y = (-latLng.lat / IMAGE_SIZE) * Y_RANGE + WORLD_Y_MIN; + return { x, y }; +} + +/** Convenient default center (geometric center of the playable area). */ +export const DEFAULT_CENTER: L.LatLngExpression = [ + -IMAGE_SIZE / 2, + IMAGE_SIZE / 2, +]; + +/** + * Leaflet zoom range. Matches the tile pyramid 1:1 (no offset): 0 shows + * the whole map in one 256x256 tile, 7 is the native 32768x32768 + * resolution of the source PNG. `DEFAULT_ZOOM` shows the map at about + * 1024 px (readable on typical viewports). + */ +export const MIN_ZOOM = 0; +export const MAX_ZOOM = 7; +export const DEFAULT_ZOOM = 2; diff --git a/src/map/extraction.ts b/src/map/extraction.ts new file mode 100644 index 00000000..4877f92a --- /dev/null +++ b/src/map/extraction.ts @@ -0,0 +1,149 @@ +import { + AllFactoryBuildings, + type FactoryBuilding, +} from '@/recipes/FactoryBuilding'; +import { AllFactoryItemsMap, FactoryItemForm } from '@/recipes/FactoryItem'; +import type { + Purity, + WorldResourceNode, + WorldResourceNodeType, +} from '@/recipes/WorldResourceNodes'; + +/** + * Purity multiplier applied to an extractor's base `itemsPerMinute`. + * Matches the in-game scaling: Impure = 0.5x, Normal = 1x, Pure = 2x. + */ +export const PURITY_MULTIPLIER: Record = { + impure: 0.5, + normal: 1, + pure: 2, +}; + +/** + * Overclock percentages we surface in the per-node yield table. Mirrors + * the satisfactory-calculator.com node popover (50% / 100% / 150% / + * 200% / 250%). + */ +export const OVERCLOCK_STEPS = [50, 100, 150, 200, 250] as const; +export type OverclockStep = (typeof OVERCLOCK_STEPS)[number]; + +const SOLID_MINER_IDS = [ + 'Build_MinerMk1_C', + 'Build_MinerMk2_C', + 'Build_MinerMk3_C', +] as const; + +const RESOURCE_WELL_EXTRACTOR_ID = 'Build_FrackingExtractor_C'; + +/** + * Standalone (non-fracking) fluid pumps per resource id. These apply + * to `BP_ResourceNode_C` actors only — fracking satellites use the + * resource well extractor regardless of resource. + */ +const STANDALONE_FLUID_EXTRACTOR_IDS: Record = { + Desc_LiquidOil_C: 'Build_OilPump_C', + Desc_Water_C: 'Build_WaterPump_C', +}; + +function findBuildings(ids: readonly string[]): FactoryBuilding[] { + const buildings: FactoryBuilding[] = []; + for (const id of ids) { + const building = AllFactoryBuildings.find(b => b.id === id); + if (building) buildings.push(building); + } + return buildings; +} + +/** + * Returns the list of extractors that can pull this specific node out + * of the ground, in display order. The choice depends on the node's + * **actor type**, not just its resource — a fracking satellite of oil + * is extracted only by the Resource Well Extractor, *not* an Oil Pump, + * even though both produce crude oil. + * + * - `node` (standalone): Mk1/Mk2/Mk3 miners for solids, or the + * appropriate fluid pump for liquids (e.g. Oil Pump for crude oil). + * - `frackingSatellite`: Resource Well Extractor only. + * - `frackingCore`: empty — the core itself isn't extracted; players + * place a Resource Well Pressurizer on it to activate the + * surrounding satellites. + * - `geyser`: empty — geysers feed the Geothermal Generator (power + * only, not a resource extractor). + * - `deposit`: empty — breakable rocks are harvested with the portable + * miner, which isn't a placeable factory building. + */ +export function getExtractorsForNode( + node: Pick, +): FactoryBuilding[] { + switch (node.nodeType) { + case 'frackingSatellite': + return findBuildings([RESOURCE_WELL_EXTRACTOR_ID]); + case 'frackingCore': + case 'geyser': + case 'deposit': + return []; + case 'node': { + const item = AllFactoryItemsMap[node.resource]; + if (!item) return []; + if (item.form === FactoryItemForm.Solid) { + return findBuildings(SOLID_MINER_IDS); + } + const pumpId = STANDALONE_FLUID_EXTRACTOR_IDS[node.resource]; + return pumpId ? findBuildings([pumpId]) : []; + } + default: + return []; + } +} + +/** + * Short human-friendly summary of how this node is harvested in-game. + * Surfaced in the popover as a hint above (or instead of) the rates + * table, so players who don't know the well/geyser mechanics can still + * make sense of the markers. + */ +export function getExtractionMethodLabel( + nodeType: WorldResourceNodeType, +): string | undefined { + switch (nodeType) { + case 'frackingSatellite': + case 'frackingCore': + return undefined; + case 'geyser': + return 'Powers the Geothermal Generator (no resource extraction).'; + case 'deposit': + return 'Mine with the Portable Miner.'; + default: + return undefined; + } +} + +/** + * Computes the rounded items-per-minute (or m³/min) for the given + * extractor on a node of `purity`, at the given overclock percentage. + */ +export function getExtractionRate( + building: FactoryBuilding, + purity: Purity, + overclock: OverclockStep, +): number { + const base = building.extractor?.itemsPerMinute ?? 0; + const rate = base * PURITY_MULTIPLIER[purity] * (overclock / 100); + return Math.round(rate); +} + +/** + * Unit string used in the yield table — `m³/min` for liquids and + * gases, `/min` for solid items (matches the project's existing + * convention in the building codex). + */ +export function getExtractionUnit(resource: string): string { + const item = AllFactoryItemsMap[resource]; + if ( + item?.form === FactoryItemForm.Liquid || + item?.form === FactoryItemForm.Gas + ) { + return 'm³/min'; + } + return '/min'; +} diff --git a/src/map/infrastructure/InfrastructureCanvasLayer.tsx b/src/map/infrastructure/InfrastructureCanvasLayer.tsx new file mode 100644 index 00000000..248f83f6 --- /dev/null +++ b/src/map/infrastructure/InfrastructureCanvasLayer.tsx @@ -0,0 +1,1071 @@ +import L from 'leaflet'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { useMap } from 'react-leaflet'; +import { loglev } from '@/core/logger/log'; +import { useStore } from '@/core/zustand'; +import { + INFRASTRUCTURE_CATEGORIES, + type InfrastructureCategory, + type ParsedInfrastructure, + SPLINE_KINDS, + type SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; +import { gameToLatLng, latLngToGame } from '../coords'; +import { + type Hit, + hitTestBuildings, + hitTestSplines, + splinePolylineLengthCm, +} from './hitTest'; +import { InfrastructureHoverPopover } from './InfrastructureHoverPopover'; +import { CategoryColor, splineColor } from './infrastructureCategories'; + +const logger = loglev.getLogger('map:infrastructure-layer'); + +interface RenderState { + master: boolean; + infrastructure: ParsedInfrastructure | null; + /** May be missing keys / undefined entirely if a save predates the + * v10 store migration; treat absent as visible everywhere. */ + categoryVisibility: Partial> | null; + splineVisibility: Partial> | null; +} + +interface AffineGameToContainer { + ox: number; + oy: number; + /** Pixel per game cm along the world's X axis. */ + ax: number; + /** Pixel per game cm along the world's Y axis. */ + by: number; +} + +const HIDDEN_CATEGORIES_BY_ZOOM: Array<[number, InfrastructureCategory[]]> = [ + // Foundations + decor are usually >70% of the entity count on + // endgame saves and at low zoom collapse into illegible blobs; + // hiding them keeps the factory cores readable and dramatically + // reduces per-tile draw work. + [3, ['foundation', 'decor']], + [2, ['storage']], +]; + +const BIN_DEDUP_PX = 4; + +function splineStepForZoom(zoom: number): number { + if (zoom < 2) return 8; + if (zoom < 3) return 4; + if (zoom < 4) return 2; + return 1; +} + +function categoryHiddenByLOD( + zoom: number, + category: InfrastructureCategory, +): boolean { + for (const [zMax, hidden] of HIDDEN_CATEGORIES_BY_ZOOM) { + if (zoom < zMax && hidden.includes(category)) return true; + } + return false; +} + +const AFFINE_PROBE_CM = 100_000; + +function computeAffine(map: L.Map): AffineGameToContainer { + const origin = map.latLngToContainerPoint(gameToLatLng(0, 0)); + const xUnit = map.latLngToContainerPoint(gameToLatLng(AFFINE_PROBE_CM, 0)); + const yUnit = map.latLngToContainerPoint(gameToLatLng(0, AFFINE_PROBE_CM)); + return { + ox: origin.x, + oy: origin.y, + ax: (xUnit.x - origin.x) / AFFINE_PROBE_CM, + by: (yUnit.y - origin.y) / AFFINE_PROBE_CM, + }; +} + +// ---- Spatial index --------------------------------------------------------- + +const SPATIAL_CELL_CM = 10_000; + +/** + * Fixed-grid spatial index over a `ParsedInfrastructure`. Bucketing by + * a coarse worldspace grid (default 100 m cells) lets each tile fetch + * just the buildings + spline polylines it actually paints in O(cells) + * — the dominant per-tile cost ends up being the rasterisation, not + * the lookup. + * + * Building bucketing keys by the entity's centre (no half-extent + * inflation): the consumer is expected to query with a small + * worldspace pad (~50 m, the largest hand-tuned clearance) so a + * building straddling the tile edge still gets picked up. + * + * Spline polylines are inserted into every cell their AABB intersects, + * and `querySplines` dedups via a `Set` so a polyline that runs across + * multiple cells in the query range is returned once. + */ +class SpatialIndex { + readonly cellSizeCm: number; + private readonly buildingsByCell: Map; + private readonly splinesByCell: Map; + + constructor(infra: ParsedInfrastructure, cellSizeCm = SPATIAL_CELL_CM) { + this.cellSizeCm = cellSizeCm; + + const buildingTmp = new Map(); + const { positionsXY, count } = infra.buildings; + for (let i = 0; i < count; i++) { + const x = positionsXY[i * 2]; + const y = positionsXY[i * 2 + 1]; + const key = SpatialIndex.cellKey( + Math.floor(x / cellSizeCm), + Math.floor(y / cellSizeCm), + ); + let arr = buildingTmp.get(key); + if (!arr) { + arr = []; + buildingTmp.set(key, arr); + } + arr.push(i); + } + this.buildingsByCell = new Map(); + for (const [k, arr] of buildingTmp) { + this.buildingsByCell.set(k, Uint32Array.from(arr)); + } + + const splineTmp = new Map(); + for (let blockIdx = 0; blockIdx < infra.splines.length; blockIdx++) { + const block = infra.splines[blockIdx]; + const { polylineBounds } = block; + for (let polyIdx = 0; polyIdx < block.count; polyIdx++) { + const minX = polylineBounds[polyIdx * 4]; + const minY = polylineBounds[polyIdx * 4 + 1]; + const maxX = polylineBounds[polyIdx * 4 + 2]; + const maxY = polylineBounds[polyIdx * 4 + 3]; + const cMinX = Math.floor(minX / cellSizeCm); + const cMinY = Math.floor(minY / cellSizeCm); + const cMaxX = Math.floor(maxX / cellSizeCm); + const cMaxY = Math.floor(maxY / cellSizeCm); + // Pack `(blockIdx, polyIdx)` into one 32-bit ref so a single + // Uint32Array per cell carries the lookup payload. + const ref = (blockIdx << 20) | (polyIdx & 0xfffff); + for (let cx = cMinX; cx <= cMaxX; cx++) { + for (let cy = cMinY; cy <= cMaxY; cy++) { + const key = SpatialIndex.cellKey(cx, cy); + let arr = splineTmp.get(key); + if (!arr) { + arr = []; + splineTmp.set(key, arr); + } + arr.push(ref); + } + } + } + } + this.splinesByCell = new Map(); + for (const [k, arr] of splineTmp) { + this.splinesByCell.set(k, Uint32Array.from(arr)); + } + } + + /** Returns building indices whose centre falls in any cell intersecting + * the query bbox. Caller pads the bbox to compensate for the centre- + * only insertion. */ + queryBuildings( + minX: number, + minY: number, + maxX: number, + maxY: number, + ): number[] { + const cMinX = Math.floor(minX / this.cellSizeCm); + const cMinY = Math.floor(minY / this.cellSizeCm); + const cMaxX = Math.floor(maxX / this.cellSizeCm); + const cMaxY = Math.floor(maxY / this.cellSizeCm); + const out: number[] = []; + for (let cx = cMinX; cx <= cMaxX; cx++) { + for (let cy = cMinY; cy <= cMaxY; cy++) { + const arr = this.buildingsByCell.get(SpatialIndex.cellKey(cx, cy)); + if (!arr) continue; + for (let i = 0; i < arr.length; i++) out.push(arr[i]); + } + } + return out; + } + + /** Returns packed `(blockIdx<<20 | polyIdx)` refs deduped across cells. */ + querySplines( + minX: number, + minY: number, + maxX: number, + maxY: number, + ): number[] { + const cMinX = Math.floor(minX / this.cellSizeCm); + const cMinY = Math.floor(minY / this.cellSizeCm); + const cMaxX = Math.floor(maxX / this.cellSizeCm); + const cMaxY = Math.floor(maxY / this.cellSizeCm); + const seen = new Set(); + for (let cx = cMinX; cx <= cMaxX; cx++) { + for (let cy = cMinY; cy <= cMaxY; cy++) { + const arr = this.splinesByCell.get(SpatialIndex.cellKey(cx, cy)); + if (!arr) continue; + for (let i = 0; i < arr.length; i++) seen.add(arr[i]); + } + } + return [...seen]; + } + + private static cellKey(cx: number, cy: number): number { + // `(cx + 32768) * 65536 + (cy + 32768)` keeps the key positive and + // collision-free for any worldspace within ±32768 cells (= ±3.3·10^8 cm + // at 10 000 cm cells, i.e. ±3300 km — many orders of magnitude + // larger than Satisfactory's playable area). + return (cx + 32768) * 65536 + (cy + 32768); + } +} + +// ---- Tile painters --------------------------------------------------------- + +/** Worldspace pad (cm) used when querying the spatial index for a tile, + * so a building or spline whose centre / bbox sits just outside the + * tile but whose footprint extends into it still gets drawn. ~50 m + * covers the largest hand-tuned clearances. */ +const TILE_QUERY_PAD_CM = 5000; + +function paintTileBuildings( + ctx: CanvasRenderingContext2D, + infra: ParsedInfrastructure, + indices: number[], + a: AffineGameToContainer, + zoom: number, + state: RenderState, +): void { + if (indices.length === 0) return; + const { categories, positionsXY, yaw, sizeWL, typePaths } = infra.buildings; + + const visibleByCat: boolean[] = INFRASTRUCTURE_CATEGORIES.map( + cat => + (state.categoryVisibility?.[cat] ?? true) && + !categoryHiddenByLOD(zoom, cat), + ); + + const paths: (Path2D | null)[] = INFRASTRUCTURE_CATEGORIES.map(() => null); + const dotCells: (Set | null)[] = INFRASTRUCTURE_CATEGORIES.map( + () => null, + ); + + for (let k = 0; k < indices.length; k++) { + const i = indices[k]; + const catIdx = categories[i]; + if (!visibleByCat[catIdx]) continue; + + const gx = positionsXY[i * 2]; + const gy = positionsXY[i * 2 + 1]; + const widthCm = sizeWL[i * 2]; + const lengthCm = sizeWL[i * 2 + 1]; + + const cx = a.ox + gx * a.ax; + const cy = a.oy + gy * a.by; + const halfW = (widthCm / 2) * Math.abs(a.ax); + const halfL = (lengthCm / 2) * Math.abs(a.by); + + if (halfW < BIN_DEDUP_PX && halfL < BIN_DEDUP_PX) { + // The bin is only the *dedup key* — two buildings of the same + // category landing in the same bin paint just one dot. The dot + // itself is drawn at the building's real footprint size (with a + // 0.5 px floor so a sub-pixel building still shows up as a 1 px + // dot). Snapping the rect to the bin would make small buildings + // look 4-8x bigger than they are at low zoom (e.g. a beam + // connector showing up as a chunky 4x4 blob). + const binX = Math.floor(cx / BIN_DEDUP_PX); + const binY = Math.floor(cy / BIN_DEDUP_PX); + const key = ((binX + 32768) << 16) | ((binY + 32768) & 0xffff); + let cellSet = dotCells[catIdx]; + if (!cellSet) { + cellSet = new Set(); + dotCells[catIdx] = cellSet; + } + if (cellSet.has(key)) continue; + cellSet.add(key); + let path = paths[catIdx]; + if (!path) { + path = new Path2D(); + paths[catIdx] = path; + } + const drawHalfW = Math.max(halfW, 0.5); + const drawHalfL = Math.max(halfL, 0.5); + path.rect(cx - drawHalfW, cy - drawHalfL, drawHalfW * 2, drawHalfL * 2); + continue; + } + + const angle = yaw[i]; + const cosA = Math.cos(angle); + const sinA = Math.sin(angle); + const dx = halfW * cosA; + const dy = halfW * sinA; + const ex = -halfL * sinA; + const ey = halfL * cosA; + let path = paths[catIdx]; + if (!path) { + path = new Path2D(); + paths[catIdx] = path; + } + path.moveTo(cx - dx - ex, cy - dy - ey); + path.lineTo(cx + dx - ex, cy + dy - ey); + path.lineTo(cx + dx + ex, cy + dy + ey); + path.lineTo(cx - dx + ex, cy - dy + ey); + path.closePath(); + } + + for (let c = 0; c < INFRASTRUCTURE_CATEGORIES.length; c++) { + const path = paths[c]; + if (!path) continue; + const cat = INFRASTRUCTURE_CATEGORIES[c]; + const color = CategoryColor[cat]; + // `other` joins foundation/decor in the quiet bucket: it's a + // catch-all that mostly contains tiny connector / accessory + // entities (beam connectors, supports, indicators) which would + // otherwise dominate the map at low zoom. + const quiet = cat === 'foundation' || cat === 'decor' || cat === 'other'; + ctx.fillStyle = color; + ctx.globalAlpha = quiet ? 0.12 : 0.35; + ctx.fill(path); + ctx.globalAlpha = quiet ? 0.6 : 1; + ctx.strokeStyle = color; + ctx.lineWidth = quiet ? 0.75 : 1.5; + ctx.stroke(path); + } + ctx.globalAlpha = 1; +} + +function paintTileSplines( + ctx: CanvasRenderingContext2D, + infra: ParsedInfrastructure, + splineRefs: number[], + a: AffineGameToContainer, + zoom: number, + state: RenderState, +): void { + if (zoom < 1 || splineRefs.length === 0) return; + + const baseLineWidth = zoom >= 6 ? 3 : zoom >= 4 ? 2 : zoom >= 2 ? 1.25 : 1; + const lineWidthMultiplier: Record = { + belt: 1, + pipe: 1, + hyper: 1, + rail: 1.8, + power: 0.75, + }; + + const step = splineStepForZoom(zoom); + // One Path2D per (kind, tier) block so each colour/lineWidth pair + // rasterises in a single `stroke` call. + const pathsByBlock = new Map(); + + for (let k = 0; k < splineRefs.length; k++) { + const ref = splineRefs[k]; + const blockIdx = ref >>> 20; + const polyIdx = ref & 0xfffff; + const block = infra.splines[blockIdx]; + if (!block) continue; + if (!SPLINE_KINDS.includes(block.kind)) continue; + if ((state.splineVisibility?.[block.kind] ?? true) === false) continue; + + const start = block.offsets[polyIdx]; + const end = block.offsets[polyIdx + 1]; + if (end - start < 2) continue; + + let path = pathsByBlock.get(blockIdx); + if (!path) { + path = new Path2D(); + pathsByBlock.set(blockIdx, path); + } + + const { pointsXY, tangentsXY } = block; + const useBezier = step === 1 && tangentsXY != null; + const sx = a.ox + pointsXY[start * 2] * a.ax; + const sy = a.oy + pointsXY[start * 2 + 1] * a.by; + path.moveTo(sx, sy); + + if (useBezier) { + const t = tangentsXY; + let p0x = sx; + let p0y = sy; + for (let j = start + 1; j < end; j++) { + const p1x = a.ox + pointsXY[j * 2] * a.ax; + const p1y = a.oy + pointsXY[j * 2 + 1] * a.by; + const leaveX = (t[(j - 1) * 4 + 2] * a.ax) / 3; + const leaveY = (t[(j - 1) * 4 + 3] * a.by) / 3; + const arriveX = (t[j * 4] * a.ax) / 3; + const arriveY = (t[j * 4 + 1] * a.by) / 3; + path.bezierCurveTo( + p0x + leaveX, + p0y + leaveY, + p1x - arriveX, + p1y - arriveY, + p1x, + p1y, + ); + p0x = p1x; + p0y = p1y; + } + continue; + } + + let lastDX = sx; + let lastDY = sy; + const lastIdx = end - 1; + for (let j = start + step; j < lastIdx; j += step) { + const px = a.ox + pointsXY[j * 2] * a.ax; + const py = a.oy + pointsXY[j * 2 + 1] * a.by; + const dx = px - lastDX; + const dy = py - lastDY; + if (dx * dx + dy * dy < 1) continue; + path.lineTo(px, py); + lastDX = px; + lastDY = py; + } + const lx = a.ox + pointsXY[lastIdx * 2] * a.ax; + const ly = a.oy + pointsXY[lastIdx * 2 + 1] * a.by; + const ldx = lx - lastDX; + const ldy = ly - lastDY; + if (ldx * ldx + ldy * ldy >= 1 || end - start === 2) { + path.lineTo(lx, ly); + } + } + + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + for (const [blockIdx, path] of pathsByBlock) { + const block = infra.splines[blockIdx]; + ctx.strokeStyle = splineColor(block.kind, block.tier); + ctx.lineWidth = baseLineWidth * lineWidthMultiplier[block.kind]; + ctx.stroke(path); + } +} + +// ---- Highlight (hover) ----------------------------------------------------- + +function drawHighlight( + ctx: CanvasRenderingContext2D, + infra: ParsedInfrastructure, + a: AffineGameToContainer, + hit: Hit, +): void { + ctx.save(); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + if (hit.kind === 'building') { + const cx = a.ox + hit.positionGame.x * a.ax; + const cy = a.oy + hit.positionGame.y * a.by; + const halfW = (hit.size.width / 2) * Math.abs(a.ax); + const halfL = (hit.size.length / 2) * Math.abs(a.by); + const angle = hit.yaw; + const cosA = Math.cos(angle); + const sinA = Math.sin(angle); + const dx = halfW * cosA; + const dy = halfW * sinA; + const ex = -halfL * sinA; + const ey = halfL * cosA; + + const path = new Path2D(); + path.moveTo(cx - dx - ex, cy - dy - ey); + path.lineTo(cx + dx - ex, cy + dy - ey); + path.lineTo(cx + dx + ex, cy + dy + ey); + path.lineTo(cx - dx + ex, cy - dy + ey); + path.closePath(); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)'; + ctx.lineWidth = 4; + ctx.stroke(path); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1.5; + ctx.stroke(path); + ctx.restore(); + return; + } + + const block = infra.splines[hit.blockIndex]; + if (!block) { + ctx.restore(); + return; + } + const start = block.offsets[hit.polylineIndex]; + const end = block.offsets[hit.polylineIndex + 1]; + if (end - start < 2) { + ctx.restore(); + return; + } + const path = new Path2D(); + const sx = a.ox + block.pointsXY[start * 2] * a.ax; + const sy = a.oy + block.pointsXY[start * 2 + 1] * a.by; + path.moveTo(sx, sy); + for (let j = start + 1; j < end; j++) { + const px = a.ox + block.pointsXY[j * 2] * a.ax; + const py = a.oy + block.pointsXY[j * 2 + 1] * a.by; + path.lineTo(px, py); + } + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)'; + ctx.lineWidth = 6; + ctx.stroke(path); + ctx.strokeStyle = splineColor(block.kind, block.tier); + ctx.lineWidth = 3; + ctx.stroke(path); + + ctx.restore(); +} + +type HoverPayload = { + hit: Hit | null; + mousePx: { x: number; y: number } | null; +}; + +// ---- GridLayer (tile-based main renderer) --------------------------------- + +/** + * Tile-based renderer for the imported infrastructure. By extending + * `L.GridLayer` we get the entire production-grade pipeline that + * Leaflet uses for its base map: viewport-only tile creation, CSS + * scale during zoom transitions (so pinch / scroll-zoom stay 60 fps + * even on an endgame save), automatic tile recycling, and culling of + * tiles outside the current bounds. Per-tile cost is bounded by the + * spatial index, so the heaviest single piece of work is the tile + * paint itself (small, fits well inside one frame) rather than a + * monolithic full-canvas redraw. + */ +const InfrastructureGridLayer = L.GridLayer.extend({ + options: { + tileSize: 256, + // No `minZoom` / `maxZoom`: the layer should track the live map at + // any zoom level, including past the basemap pyramid's + // `MAX_ZOOM` of 7 (Leaflet still allows scrolling further in). + // Spatial-index lookups stay cheap even at high zoom because each + // tile covers a tiny worldspace bbox. + // Keep the default keepBuffer (2): one extra row/col of tiles + // around the viewport so a small pan never shows a blank rim + // before new tiles render. + }, + + initialize(this: GridLayerInternals, options?: L.GridLayerOptions) { + // `L.GridLayer.prototype.initialize` exists at runtime (it's how + // every Leaflet class boots) but isn't declared in the `@types` + // because the `extend()` factory normally hides it from authors. + ( + L.GridLayer.prototype as unknown as { + initialize?: (opts?: L.GridLayerOptions) => void; + } + ).initialize?.call(this, options); + this._spatialIndex = null; + this._renderState = null; + }, + + setRenderState(this: GridLayerInternals, state: RenderState) { + const previous = this._renderState; + this._renderState = state; + const previousInfra = previous?.infrastructure ?? null; + const nowInfra = state.infrastructure; + if (previousInfra !== nowInfra) { + this._spatialIndex = nowInfra ? new SpatialIndex(nowInfra) : null; + if (nowInfra) { + logger.info( + 'spatial index built', + `${nowInfra.buildings.count} buildings,`, + `${nowInfra.splines.reduce((s, b) => s + b.count, 0)} polylines`, + ); + } + } + // Anything that affects what a tile paints requires re-rendering + // every visible tile. `redraw()` (a method on L.GridLayer) drops + // the existing tile cache and re-creates them on demand. + if ( + !previous || + previous.master !== state.master || + previous.infrastructure !== state.infrastructure || + previous.categoryVisibility !== state.categoryVisibility || + previous.splineVisibility !== state.splineVisibility + ) { + this.redraw(); + } + }, + + createTile( + this: GridLayerInternals, + coords: L.Coords, + done: (error: Error | undefined, tile: HTMLElement) => void, + ): HTMLCanvasElement { + const tile = L.DomUtil.create('canvas') as HTMLCanvasElement; + const tileSize = this.getTileSize(); + const dpr = window.devicePixelRatio || 1; + tile.width = tileSize.x * dpr; + tile.height = tileSize.y * dpr; + tile.style.width = `${tileSize.x}px`; + tile.style.height = `${tileSize.y}px`; + + // Defer the actual rasterisation by one rAF so a burst of new + // tiles (e.g. on zoom-in) doesn't all run inside the same frame. + // Leaflet shows the previous-zoom tiles scaled-up until `done` + // fires, so the user sees a smooth zoom rather than a blank rim. + requestAnimationFrame(() => { + try { + this._renderTile(tile, coords); + done(undefined, tile); + } catch (e) { + done(e as Error, tile); + } + }); + return tile; + }, + + _renderTile( + this: GridLayerInternals, + canvas: HTMLCanvasElement, + coords: L.Coords, + ): void { + const state = this._renderState; + if (!state?.master || !state.infrastructure) return; + const idx = this._spatialIndex; + if (!idx) return; + + const tileSize = this.getTileSize(); + const dpr = window.devicePixelRatio || 1; + const zoom = coords.z; + + const map = (this as unknown as { _map: L.Map | null })._map; + if (!map) return; + + // Compute this tile's worldspace bbox by unprojecting its corners. + // `coords.scaleBy(tileSize)` returns the NW pixel of the tile in + // the layer's global pixel space at zoom `coords.z`. + const nwPoint = coords.scaleBy(tileSize); + const sePoint = nwPoint.add(tileSize); + const nwLatLng = map.unproject(nwPoint, zoom); + const seLatLng = map.unproject(sePoint, zoom); + const nwGame = latLngToGame(nwLatLng); + const seGame = latLngToGame(seLatLng); + const tileMinX = Math.min(nwGame.x, seGame.x); + const tileMaxX = Math.max(nwGame.x, seGame.x); + const tileMinY = Math.min(nwGame.y, seGame.y); + const tileMaxY = Math.max(nwGame.y, seGame.y); + + if (tileMaxX - tileMinX <= 0 || tileMaxY - tileMinY <= 0) return; + + // Affine: world cm → tile pixel (CSS units, the 2D ctx is scaled + // by dpr below). Tile (0, 0) is the corner with smaller game x + // and smaller game y in our `gameToLatLng` orientation. + const ax = tileSize.x / (tileMaxX - tileMinX); + const by = tileSize.y / (tileMaxY - tileMinY); + const tileAffine: AffineGameToContainer = { + ax, + by, + ox: -tileMinX * ax, + oy: -tileMinY * by, + }; + + const queryMinX = tileMinX - TILE_QUERY_PAD_CM; + const queryMaxX = tileMaxX + TILE_QUERY_PAD_CM; + const queryMinY = tileMinY - TILE_QUERY_PAD_CM; + const queryMaxY = tileMaxY + TILE_QUERY_PAD_CM; + const buildingIndices = idx.queryBuildings( + queryMinX, + queryMinY, + queryMaxX, + queryMaxY, + ); + const splineRefs = idx.querySplines( + queryMinX, + queryMinY, + queryMaxX, + queryMaxY, + ); + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, tileSize.x, tileSize.y); + + paintTileSplines( + ctx, + state.infrastructure, + splineRefs, + tileAffine, + zoom, + state, + ); + paintTileBuildings( + ctx, + state.infrastructure, + buildingIndices, + tileAffine, + zoom, + state, + ); + }, +}) as unknown as new ( + options?: L.GridLayerOptions, +) => InfrastructureGridLayerType; + +interface GridLayerInternals extends L.GridLayer { + _spatialIndex: SpatialIndex | null; + _renderState: RenderState | null; + _renderTile: (canvas: HTMLCanvasElement, coords: L.Coords) => void; + setRenderState: (state: RenderState) => void; +} + +interface InfrastructureGridLayerType extends L.GridLayer { + setRenderState(state: RenderState): void; + spatialIndex(): SpatialIndex | null; +} + +// ---- Highlight overlay layer (hover hit-test + accent paint) -------------- + +/** + * Single-canvas overlay that sits above the GridLayer. It owns the + * mousemove hit test and paints the accent stroke on the hovered + * entity. Decoupling it from the main renderer lets us redraw only + * the tiny highlight on every mousemove without touching the tile + * cache. + */ +class InfrastructureHighlightLayer extends L.Layer { + private canvas: HTMLCanvasElement | null = null; + private state: RenderState | null = null; + private affine: AffineGameToContainer | null = null; + private currentZoom = 0; + private hovered: Hit | null = null; + private hoverMousePx: { x: number; y: number } | null = null; + private hoverCallback: ((payload: HoverPayload) => void) | null = null; + private rafId: number | null = null; + private isInteracting = false; + + onAdd(map: L.Map): this { + const canvas = L.DomUtil.create( + 'canvas', + 'leaflet-infrastructure-highlight', + ) as HTMLCanvasElement; + canvas.style.position = 'absolute'; + canvas.style.pointerEvents = 'none'; + canvas.style.willChange = 'transform'; + map.getPanes().overlayPane.appendChild(canvas); + this.canvas = canvas; + + map.on('viewreset moveend zoomend resize', this.scheduleRedraw, this); + map.on('movestart zoomstart', this.onInteractionStart, this); + map.on('mousemove', this.handleMouseMove, this); + map.on('mouseout', this.handleMouseOut, this); + this.scheduleRedraw(); + return this; + } + + onRemove(map: L.Map): this { + if (this.rafId != null) { + cancelAnimationFrame(this.rafId); + this.rafId = null; + } + if (this.canvas) { + this.canvas.remove(); + this.canvas = null; + } + map.off('viewreset moveend zoomend resize', this.scheduleRedraw, this); + map.off('movestart zoomstart', this.onInteractionStart, this); + map.off('mousemove', this.handleMouseMove, this); + map.off('mouseout', this.handleMouseOut, this); + return this; + } + + setState(state: RenderState): void { + this.state = state; + if (this.hovered) { + this.hovered = null; + this.hoverMousePx = null; + this.hoverCallback?.({ hit: null, mousePx: null }); + } + this.scheduleRedraw(); + } + + setHoverCallback(cb: ((payload: HoverPayload) => void) | null): void { + this.hoverCallback = cb; + } + + private onInteractionStart = (): void => { + this.isInteracting = true; + if (this.hovered) { + this.hovered = null; + this.hoverMousePx = null; + this.hoverCallback?.({ hit: null, mousePx: null }); + this.scheduleRedraw(); + } + }; + + private scheduleRedraw = (): void => { + this.isInteracting = false; + if (this.rafId != null) return; + this.rafId = requestAnimationFrame(() => { + this.rafId = null; + this.redraw(); + }); + }; + + private handleMouseMove = (event: L.LeafletMouseEvent): void => { + if (this.isInteracting) return; + if (!this.state?.master || !this.state.infrastructure || !this.affine) { + this.updateHover(null, null); + return; + } + const infra = this.state.infrastructure; + const a = this.affine; + if (a.ax === 0 || a.by === 0) return; + + const cp = event.containerPoint; + const worldX = (cp.x - a.ox) / a.ax; + const worldY = (cp.y - a.oy) / a.by; + + const visibleByCat = INFRASTRUCTURE_CATEGORIES.map( + cat => + (this.state?.categoryVisibility?.[cat] ?? true) && + !categoryHiddenByLOD(this.currentZoom, cat), + ); + const buildingHit = hitTestBuildings(infra, visibleByCat, worldX, worldY); + + let hit: Hit | null = buildingHit; + if (!hit) { + const splineThresholdGameCm = 6 / Math.max(Math.abs(a.ax), 1e-9); + hit = hitTestSplines( + infra, + kind => this.state?.splineVisibility?.[kind] ?? true, + worldX, + worldY, + splineThresholdGameCm, + ); + } + + this.updateHover(hit, { x: cp.x, y: cp.y }); + }; + + private handleMouseOut = (): void => { + this.updateHover(null, null); + }; + + private updateHover( + hit: Hit | null, + mousePx: { x: number; y: number } | null, + ): void { + const sameHit = + hit === this.hovered || + (hit?.kind === 'building' && + this.hovered?.kind === 'building' && + hit.index === this.hovered.index) || + (hit?.kind === 'spline' && + this.hovered?.kind === 'spline' && + hit.blockIndex === this.hovered.blockIndex && + hit.polylineIndex === this.hovered.polylineIndex); + const samePx = + this.hoverMousePx?.x === mousePx?.x && + this.hoverMousePx?.y === mousePx?.y; + + this.hovered = hit; + this.hoverMousePx = mousePx; + + if (!sameHit) this.scheduleRedraw(); + if (!sameHit || !samePx) { + this.hoverCallback?.({ hit, mousePx }); + } + this.updateCursor(); + } + + private updateCursor(): void { + const map = this._map; + if (!map) return; + const container = map.getContainer(); + if (this.hovered) { + container.style.cursor = 'pointer'; + } else if (container.style.cursor === 'pointer') { + container.style.cursor = ''; + } + } + + private redraw(): void { + const canvas = this.canvas; + const map = this._map; + if (!canvas || !map) return; + + const size = map.getSize(); + const topLeft = map.containerPointToLayerPoint([0, 0]); + L.DomUtil.setPosition(canvas, topLeft); + + const dpr = window.devicePixelRatio || 1; + const targetW = Math.round(size.x * dpr); + const targetH = Math.round(size.y * dpr); + if (canvas.width !== targetW) canvas.width = targetW; + if (canvas.height !== targetH) canvas.height = targetH; + canvas.style.width = `${size.x}px`; + canvas.style.height = `${size.y}px`; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, size.x, size.y); + + const affine = computeAffine(map); + const zoom = map.getZoom(); + this.affine = affine; + this.currentZoom = zoom; + + if (this.hovered && this.state?.master && this.state.infrastructure) { + drawHighlight(ctx, this.state.infrastructure, affine, this.hovered); + } + } +} + +// ---- Misc helpers --------------------------------------------------------- + +function infrastructureLatLngBounds( + infra: ParsedInfrastructure, +): L.LatLngBounds | null { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + const { buildings, splines } = infra; + for (let i = 0; i < buildings.count; i++) { + const x = buildings.positionsXY[i * 2]; + const y = buildings.positionsXY[i * 2 + 1]; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + for (const block of splines) { + const pts = block.pointsXY; + for (let i = 0; i < pts.length; i += 2) { + const x = pts[i]; + const y = pts[i + 1]; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null; + + return L.latLngBounds(gameToLatLng(minX, minY), gameToLatLng(maxX, maxY)); +} + +// ---- React wrapper -------------------------------------------------------- + +/** + * React wrapper around the tile-based grid layer + a thin highlight + * overlay. Reads the in-memory `mapInfrastructure` slice plus the + * persisted visibility flags from `mapSlice`, and pushes them into + * the imperative layers whenever they change. The layers are mounted + * once per `` and torn down with the host component. + */ +export function InfrastructureCanvasLayer() { + const map = useMap(); + + const infrastructure = useStore(s => s.mapInfrastructure.infrastructure); + const players = useStore(s => s.mapInfrastructure.players); + const ownerGameId = useStore(s => s.mapInfrastructure.gameId); + const requestedFitAt = useStore(s => s.mapInfrastructure.requestedFitAt); + const selectedGameId = useStore(s => s.games.selected); + const master = useStore(s => s.map.infrastructureMaster); + const categoryVisibility = useStore( + s => s.map.infrastructureCategoryVisibility, + ); + const splineVisibility = useStore(s => s.map.infrastructureSplineVisibility); + + const activeInfrastructure = + ownerGameId != null && ownerGameId === selectedGameId + ? infrastructure + : null; + + const gridRef = useRef(null); + const highlightRef = useRef(null); + + const [hoverHit, setHoverHit] = useState(null); + const [hoverMousePx, setHoverMousePx] = useState<{ + x: number; + y: number; + } | null>(null); + + useEffect(() => { + const grid = new InfrastructureGridLayer(); + grid.addTo(map); + const highlight = new InfrastructureHighlightLayer(); + highlight.addTo(map); + highlight.setHoverCallback(({ hit, mousePx }) => { + setHoverHit(hit); + setHoverMousePx(mousePx); + }); + gridRef.current = grid; + highlightRef.current = highlight; + logger.info('infrastructure layers mounted'); + return () => { + highlight.setHoverCallback(null); + highlight.remove(); + grid.remove(); + gridRef.current = null; + highlightRef.current = null; + }; + }, [map]); + + useEffect(() => { + const state: RenderState = { + master, + infrastructure: activeInfrastructure, + categoryVisibility, + splineVisibility, + }; + gridRef.current?.setRenderState(state); + highlightRef.current?.setState(state); + }, [master, activeInfrastructure, categoryVisibility, splineVisibility]); + + // Frame the camera when the slice asks for it (after every fresh + // import, plus on-demand from the filter panel's "Locate" button). + // Priority: player position (host pawn) → infrastructure bounds → + // noop. The player path always wins when available so users land + // on themselves rather than on a foundation in the corner. + useEffect(() => { + if (requestedFitAt == null) return; + if (ownerGameId != null && ownerGameId !== selectedGameId) return; + + if (players.length > 0) { + const p = players[0]; + map.setView(gameToLatLng(p.x, p.y), 5, { animate: true }); + logger.info('framed camera on player'); + return; + } + + if (!activeInfrastructure) return; + const bounds = infrastructureLatLngBounds(activeInfrastructure); + if (!bounds?.isValid()) return; + map.fitBounds(bounds, { padding: [40, 40], maxZoom: 6 }); + logger.info('framed camera on infrastructure'); + }, [ + requestedFitAt, + players, + activeInfrastructure, + ownerGameId, + selectedGameId, + map, + ]); + + const splineLengthCm = useMemo(() => { + if (!hoverHit || hoverHit.kind !== 'spline' || !activeInfrastructure) { + return undefined; + } + return splinePolylineLengthCm( + activeInfrastructure, + hoverHit.blockIndex, + hoverHit.polylineIndex, + ); + }, [hoverHit, activeInfrastructure]); + + return createPortal( + , + map.getContainer(), + ); +} diff --git a/src/map/infrastructure/InfrastructureHoverPopover.module.css b/src/map/infrastructure/InfrastructureHoverPopover.module.css new file mode 100644 index 00000000..05026760 --- /dev/null +++ b/src/map/infrastructure/InfrastructureHoverPopover.module.css @@ -0,0 +1,34 @@ +.popover { + position: absolute; + z-index: 600; + pointer-events: none; + max-width: 240px; + padding: 8px 10px; + border-radius: var(--mantine-radius-sm); + background-color: rgba(20, 22, 26, 0.92); + border: 1px solid var(--mantine-color-dark-4); + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.4), + 0 4px 12px rgba(0, 0, 0, 0.4); + color: var(--mantine-color-gray-1); + backdrop-filter: blur(2px); +} + +.title { + flex: 1 1 auto; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.swatch { + flex: 0 0 auto; + width: 10px; + height: 10px; + border-radius: 2px; +} + +.coords { + font-variant-numeric: tabular-nums; +} diff --git a/src/map/infrastructure/InfrastructureHoverPopover.tsx b/src/map/infrastructure/InfrastructureHoverPopover.tsx new file mode 100644 index 00000000..a62693af --- /dev/null +++ b/src/map/infrastructure/InfrastructureHoverPopover.tsx @@ -0,0 +1,213 @@ +import { Group, Image, Stack, Text } from '@mantine/core'; +import { AllFactoryBuildingsMap } from '@/recipes/FactoryBuilding'; +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import { AllFactoryRecipesMap } from '@/recipes/FactoryRecipe'; +import type { + InfrastructureCategory, + SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; +import { INFRASTRUCTURE_CATEGORIES } from '@/recipes/savegame/ParseSavegameMessages'; +import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { StaticWorldResourceNodesById } from '@/recipes/WorldResourceNodes'; +import type { Hit } from './hitTest'; +import classes from './InfrastructureHoverPopover.module.css'; +import { + buildingIdFromTypePath, + CategoryColor, + CategoryLabel, + SplineLabel, + splineColor, +} from './infrastructureCategories'; + +export interface InfrastructureHoverPopoverProps { + hit: Hit | null; + /** Container coords (px relative to the map container's top-left). */ + mousePx: { x: number; y: number } | null; + /** Optional length pre-computed by the layer for spline hits. */ + splineLengthCm?: number; +} + +/** + * Floating tooltip that shadows the cursor while it sits over an + * imported building or spline. Positioned absolutely over the map + * container so the rest of the page (including Leaflet's controls) + * stays untouched. Pointer events disabled — hovering the popover + * itself never re-triggers a hit-test loop. + */ +export function InfrastructureHoverPopover({ + hit, + mousePx, + splineLengthCm, +}: InfrastructureHoverPopoverProps) { + if (!hit || !mousePx) return null; + + if (hit.kind === 'building') { + const id = buildingIdFromTypePath(hit.typePath); + const known = id ? AllFactoryBuildingsMap[id] : null; + const displayName = known?.name ?? id ?? 'Unknown building'; + const buildingImage = known?.imagePath || null; + const category = INFRASTRUCTURE_CATEGORIES[ + hit.categoryIndex + ] as InfrastructureCategory; + const color = CategoryColor[category]; + const widthM = hit.size.width / 100; + const lengthM = hit.size.length / 100; + + // Recipe display name + icon. The save stores the recipe ref's + // last path segment, which matches the `id` field we ship in + // FactoryRecipes.json — so a successful lookup yields the in-game + // "Iron Plate" / "Reinforced Iron Plate" name; an unknown id + // (mods, deprecated recipes) falls back to the raw id and skips + // the icon. The recipe icon is the first product's resource image, + // which matches what the in-game machine UI shows. + const recipe = hit.recipe ? AllFactoryRecipesMap[hit.recipe] : null; + const recipeName = hit.recipe ? (recipe?.name ?? hit.recipe) : null; + const recipeIconId = recipe?.products?.[0]?.resource ?? null; + + // Show overclock only when it's meaningfully off 100% and finite — + // a foundation reports `NaN`, an untouched machine reports 1.0, + // both should stay quiet in the popover. + const showOverclock = + Number.isFinite(hit.overclock) && Math.abs(hit.overclock - 1) > 0.001; + const overclockPct = showOverclock ? Math.round(hit.overclock * 100) : null; + const showSomersloop = hit.somersloop > 0; + + // Extracted resource (miners / oil pumps / fracking / water pumps). + // Water pumps point at `FGWaterVolume_*` ids that aren't in the + // static node dataset, so we infer the resource from the typePath + // for those. Miners + pumps + fracking resolve via the bundled + // node lookup, falling back to whatever the raw resource id is + // (mods, modded nodes) when the dataset doesn't know the entry. + let extractedResourceId: string | null = null; + let extractedResourceName: string | null = null; + let extractedPurity: string | null = null; + if (hit.typePath.includes('Build_WaterPump')) { + extractedResourceId = 'Desc_Water_C'; + extractedResourceName = + AllFactoryItemsMap[extractedResourceId]?.displayName ?? 'Water'; + } else if (hit.extractedNode) { + const node = StaticWorldResourceNodesById[hit.extractedNode]; + if (node) { + extractedResourceId = node.resource; + extractedResourceName = + node.displayName ?? + AllFactoryItemsMap[node.resource]?.displayName ?? + node.resource; + extractedPurity = node.purity; + } + } + + return ( + + + + {buildingImage ? ( + {displayName} + ) : ( + + {recipeName ? ( + + {recipeIconId ? ( + + ) : null} + {recipeName} + + ) : null} + {extractedResourceId ? ( + + + + {extractedResourceName ?? extractedResourceId} + {extractedPurity ? ` · ${extractedPurity}` : ''} + + + ) : null} + {showOverclock || showSomersloop ? ( + + {showOverclock ? `Overclock: ${overclockPct}%` : null} + {showOverclock && showSomersloop ? ' · ' : null} + {showSomersloop ? `Somersloop ×${hit.somersloop}` : null} + + ) : null} + + {CategoryLabel[category]} · {widthM.toFixed(1)}m ×{' '} + {lengthM.toFixed(1)}m × {(hit.height / 100).toFixed(1)}m + + + ({Math.round(hit.positionGame.x / 100)}m,{' '} + {Math.round(hit.positionGame.y / 100)}m, z={' '} + {Math.round(hit.z / 100)}m) + + + + ); + } + + // Spline hit + const kind = hit.splineKind as SplineKind; + const tier = hit.splineTier; + const color = splineColor(kind, tier); + const tierSuffix = tier > 0 ? ` Mk${tier}` : ''; + const lengthM = + splineLengthCm != null ? `${Math.round(splineLengthCm / 100)}m` : null; + return ( + + + + + {lengthM ? ( + + Length: {lengthM} + + ) : null} + + + ); +} + +function PopoverShell({ + mousePx, + children, +}: { + mousePx: { x: number; y: number }; + children: React.ReactNode; +}) { + // Offset the box from the cursor so it never sits *under* the + // pointer (which would obscure the entity it describes). Flips to + // the left/top side near the right/bottom edges via CSS clamping. + return ( +
+ {children} +
+ ); +} diff --git a/src/map/infrastructure/hitTest.ts b/src/map/infrastructure/hitTest.ts new file mode 100644 index 00000000..01592dfd --- /dev/null +++ b/src/map/infrastructure/hitTest.ts @@ -0,0 +1,245 @@ +import type { + ParsedInfrastructure, + SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; + +export interface BuildingHit { + kind: 'building'; + index: number; + /** game cm */ + positionGame: { x: number; y: number }; + /** game cm, base elevation (transform.translation.z). */ + z: number; + /** game cm */ + size: { width: number; length: number }; + /** game cm, height of the building. */ + height: number; + yaw: number; + typePath: string; + categoryIndex: number; + /** Overclock multiplier (1.0 = 100%). `NaN` for entities without + * `mCurrentPotential` (foundations, decor, lightweight instances). */ + overclock: number; + /** Somersloop / production-shard count. 0 when not applicable. */ + somersloop: number; + /** Selected recipe id (last segment of `mCurrentRecipeRef.pathName`). + * Empty string when none. */ + recipe: string; + /** Resource node id this entity extracts from (last segment of + * `mExtractableResource.value.pathName`). Empty string when the + * entity isn't an extractor. Water pumps point at `FGWaterVolume_*` + * which won't resolve in the static node dataset; the popover + * falls back on the building's typePath in that case. */ + extractedNode: string; +} + +export interface SplineHit { + kind: 'spline'; + /** Block within `infrastructure.splines`. */ + blockIndex: number; + /** Polyline within the block. */ + polylineIndex: number; + splineKind: SplineKind; + splineTier: number; +} + +export type Hit = BuildingHit | SplineHit; + +/** + * Returns the topmost building (greatest `z + height`) whose oriented + * rectangle contains the given world point. Picking by physical + * elevation (rather than footprint area) lets a constructor stacked + * on a mid-rise foundation win the hit over the foundation, and keeps + * an upper-floor machine on top of a building below it. Ties on + * elevation fall back to the smaller footprint so a machine still + * wins over a same-height neighbour. + */ +export function hitTestBuildings( + infra: ParsedInfrastructure, + visibleByCat: boolean[], + worldX: number, + worldY: number, +): BuildingHit | null { + const { + count, + categories, + positionsXY, + positionsZ, + yaw, + sizeWL, + heights, + typePaths, + overclocks, + somersloops, + recipes, + extractedNodes, + } = infra.buildings; + let bestTopZ = -Infinity; + let bestArea = Infinity; + let bestIdx = -1; + + for (let i = 0; i < count; i++) { + const catIdx = categories[i]; + if (!visibleByCat[catIdx]) continue; + + const cx = positionsXY[i * 2]; + const cy = positionsXY[i * 2 + 1]; + const halfW = sizeWL[i * 2] / 2; + const halfL = sizeWL[i * 2 + 1] / 2; + + // Inverse rotation: project the world point into the building's + // local frame, then check against an axis-aligned half-extents + // box. `width` extends along the forward axis (local +X) and + // `length` extends along the side axis (local +Y), matching the + // mapping used by drawBuildings in InfrastructureCanvasLayer. + const a = -yaw[i]; + const cosA = Math.cos(a); + const sinA = Math.sin(a); + const dx = worldX - cx; + const dy = worldY - cy; + const localX = dx * cosA - dy * sinA; + const localY = dx * sinA + dy * cosA; + if (Math.abs(localX) > halfW || Math.abs(localY) > halfL) continue; + + const topZ = positionsZ[i] + heights[i]; + const area = halfW * halfL; + if (topZ > bestTopZ || (topZ === bestTopZ && area < bestArea)) { + bestTopZ = topZ; + bestArea = area; + bestIdx = i; + } + } + + if (bestIdx === -1) return null; + return { + kind: 'building', + index: bestIdx, + positionGame: { + x: positionsXY[bestIdx * 2], + y: positionsXY[bestIdx * 2 + 1], + }, + z: positionsZ[bestIdx], + size: { + width: sizeWL[bestIdx * 2], + length: sizeWL[bestIdx * 2 + 1], + }, + height: heights[bestIdx], + yaw: yaw[bestIdx], + typePath: typePaths[bestIdx], + categoryIndex: categories[bestIdx], + overclock: overclocks[bestIdx], + somersloop: somersloops[bestIdx], + recipe: recipes[bestIdx], + extractedNode: extractedNodes[bestIdx], + }; +} + +function pointToSegmentDistanceSq( + px: number, + py: number, + ax: number, + ay: number, + bx: number, + by: number, +): number { + const abx = bx - ax; + const aby = by - ay; + const lenSq = abx * abx + aby * aby; + if (lenSq === 0) { + const dx = px - ax; + const dy = py - ay; + return dx * dx + dy * dy; + } + const t = Math.max( + 0, + Math.min(1, ((px - ax) * abx + (py - ay) * aby) / lenSq), + ); + const cx = ax + t * abx; + const cy = ay + t * aby; + const dx = px - cx; + const dy = py - cy; + return dx * dx + dy * dy; +} + +/** + * Closest spline polyline within `thresholdGameCm` of the world point. + * Returns null if nothing is within reach. Skipped spline kinds whose + * visibility is off so hovering a hidden network doesn't pop tooltips. + */ +export function hitTestSplines( + infra: ParsedInfrastructure, + splineVisible: (kind: SplineKind) => boolean, + worldX: number, + worldY: number, + thresholdGameCm: number, +): SplineHit | null { + const thresholdSq = thresholdGameCm * thresholdGameCm; + let bestDistSq = thresholdSq; + let bestBlock = -1; + let bestPolyline = -1; + + for (let b = 0; b < infra.splines.length; b++) { + const block = infra.splines[b]; + if (!splineVisible(block.kind)) continue; + if (block.count === 0) continue; + const { offsets, pointsXY } = block; + for (let i = 0; i < block.count; i++) { + const start = offsets[i]; + const end = offsets[i + 1]; + if (end - start < 2) continue; + let prevX = pointsXY[start * 2]; + let prevY = pointsXY[start * 2 + 1]; + for (let j = start + 1; j < end; j++) { + const nx = pointsXY[j * 2]; + const ny = pointsXY[j * 2 + 1]; + const d2 = pointToSegmentDistanceSq( + worldX, + worldY, + prevX, + prevY, + nx, + ny, + ); + if (d2 < bestDistSq) { + bestDistSq = d2; + bestBlock = b; + bestPolyline = i; + } + prevX = nx; + prevY = ny; + } + } + } + + if (bestBlock === -1) return null; + const block = infra.splines[bestBlock]; + return { + kind: 'spline', + blockIndex: bestBlock, + polylineIndex: bestPolyline, + splineKind: block.kind, + splineTier: block.tier, + }; +} + +/** + * Total length of a spline polyline in game cm. Used in the hover + * popover so the player can see a "how much belt did this stretch + * eat" number without leaving the map. + */ +export function splinePolylineLengthCm( + infra: ParsedInfrastructure, + blockIndex: number, + polylineIndex: number, +): number { + const block = infra.splines[blockIndex]; + const start = block.offsets[polylineIndex]; + const end = block.offsets[polylineIndex + 1]; + let total = 0; + for (let j = start + 1; j < end; j++) { + const dx = block.pointsXY[j * 2] - block.pointsXY[(j - 1) * 2]; + const dy = block.pointsXY[j * 2 + 1] - block.pointsXY[(j - 1) * 2 + 1]; + total += Math.sqrt(dx * dx + dy * dy); + } + return total; +} diff --git a/src/map/infrastructure/infrastructureCategories.ts b/src/map/infrastructure/infrastructureCategories.ts new file mode 100644 index 00000000..24b662b9 --- /dev/null +++ b/src/map/infrastructure/infrastructureCategories.ts @@ -0,0 +1,159 @@ +import { + AllFactoryBuildingsMap, + type FactoryBuilding, +} from '@/recipes/FactoryBuilding'; +import type { + InfrastructureCategory, + SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; + +/** + * Last segment of the typePath after the final `.`. For + * `/Game/.../Build_AssemblerMk1.Build_AssemblerMk1_C` returns + * `Build_AssemblerMk1_C`. Returns `null` if the input is empty. + */ +export function buildingIdFromTypePath(typePath: string): string | null { + const last = typePath.split('.').pop(); + return last && last.length > 0 ? last : null; +} + +function categoryFromKnownBuilding( + building: FactoryBuilding, +): InfrastructureCategory { + if (building.powerGenerator) return 'power'; + if (building.conveyor || building.pipeline) return 'logistics'; + // Extractors (miners, oil pumps, fracking) are production for our + // purposes: they are the head of a production chain, not a logistics + // connector, and the user thinks of them as factory machines. + return 'production'; +} + +const FOUNDATION_BUILDING_PREFIXES = [ + '/Buildable/Building/Foundation', + '/Buildable/Building/Wall', + '/Buildable/Building/Stair', + '/Buildable/Building/Beam', + '/Buildable/Building/Ramp', + '/Buildable/Building/Roof', + '/Buildable/Building/Floor', + '/Buildable/Building/Catwalk', + '/Buildable/Building/Pillar', + '/Buildable/Building/Window', + '/Buildable/Building/Door', + '/Buildable/Building/Fence', +]; + +/** + * Maps an entity's `typePath` to a category for rendering. Path-based + * checks come *before* the `AllFactoryBuildingsMap` lookup because the + * map's per-building category derivation defaults to `production` for + * anything without an extractor / conveyor / pipeline / generator + * marker — and that misclassifies foundations, walls, train stations, + * etc. that *do* have entries (and recipes) in the static catalog. + * The catalog still serves as a precise fallback for the things that + * aren't matched by any path prefix. + */ +export function categoryFor(typePath: string): InfrastructureCategory { + if (typePath.includes('/Buildable/Vehicle/')) return 'transport'; + if (typePath.includes('/Buildable/Factory/Train/')) return 'transport'; + + if (typePath.includes('/Buildable/Factory/PowerLine')) return 'power'; + if (typePath.includes('/Buildable/Factory/PowerPole')) return 'power'; + if (typePath.includes('/Buildable/Factory/PowerTower')) return 'power'; + if (typePath.includes('/Buildable/Factory/PowerStorage')) return 'power'; + if (typePath.includes('/Buildable/Factory/Generator')) return 'power'; + + if (typePath.includes('/Buildable/Factory/Storage')) return 'storage'; + + if (typePath.includes('/Buildable/Factory/Pipeline')) return 'logistics'; + if (typePath.includes('/Buildable/Factory/PipeJunction')) return 'logistics'; + if (typePath.includes('/Buildable/Factory/Conveyor')) return 'logistics'; + if (typePath.includes('/Buildable/Factory/CA_')) return 'logistics'; + + if (typePath.includes('/Buildable/Factory/Sign')) return 'decor'; + if (typePath.includes('/Buildable/Factory/Light')) return 'decor'; + if (typePath.includes('/Buildable/Factory/Potty')) return 'decor'; + if (typePath.includes('/Buildable/Factory/Jumppad')) return 'decor'; + + for (const prefix of FOUNDATION_BUILDING_PREFIXES) { + if (typePath.includes(prefix)) return 'foundation'; + } + if (typePath.includes('/Buildable/Building/')) return 'foundation'; + + // Catalog lookup: precise classification for the remaining factory + // machines (assemblers, miners, refineries, ...). + const id = buildingIdFromTypePath(typePath); + if (id) { + const known = AllFactoryBuildingsMap[id]; + if (known) return categoryFromKnownBuilding(known); + } + + if (typePath.includes('/Buildable/Factory/')) return 'production'; + + return 'other'; +} + +export const CategoryColor: Record = { + production: '#3b82f6', + logistics: '#f59e0b', + power: '#a855f7', + storage: '#10b981', + transport: '#06b6d4', + foundation: '#6b7280', + decor: '#9ca3af', + other: '#78716c', +}; + +export const CategoryLabel: Record = { + production: 'Production', + logistics: 'Logistics', + power: 'Power', + storage: 'Storage', + transport: 'Transport', + foundation: 'Foundation', + decor: 'Decoration', + other: 'Other', +}; + +/** + * Per-tier stroke color for the spline networks. `tier === 0` is the + * fallback for kinds without tiering (rail, power). + */ +export const SplineColor: Record> = { + belt: { + 0: '#9ca3af', + 1: '#9ca3af', + 2: '#fbbf24', + 3: '#f97316', + 4: '#dc2626', + 5: '#a21caf', + 6: '#7c3aed', + }, + pipe: { + 0: '#3b82f6', + 1: '#3b82f6', + 2: '#0ea5e9', + }, + hyper: { + 0: '#a855f7', + }, + rail: { + 0: '#78350f', + }, + power: { + 0: '#fde68a', + }, +}; + +export const SplineLabel: Record = { + belt: 'Conveyor belts', + pipe: 'Pipes', + hyper: 'Hyper tubes', + rail: 'Railroads', + power: 'Power lines', +}; + +export function splineColor(kind: SplineKind, tier: number): string { + const palette = SplineColor[kind]; + return palette[tier] ?? palette[0]; +} diff --git a/src/map/infrastructure/quaternion.ts b/src/map/infrastructure/quaternion.ts new file mode 100644 index 00000000..b3a254e7 --- /dev/null +++ b/src/map/infrastructure/quaternion.ts @@ -0,0 +1,21 @@ +/** + * Yaw (rotation around the world's vertical Z axis) extracted from a + * Unreal quaternion stored as `{x, y, z, w}`. Used to orient building + * footprints on the top-down map; pitch and roll are irrelevant for + * 2D rendering. + * + * The result is in radians in the world frame. `gameToLatLng` in + * `src/map/coords.ts` maps game (X, Y) → canvas (X, Y) preserving + * direction on both axes (game +Y and canvas +Y both increase + * downward), so the world-frame yaw is also the canvas-frame angle: + * apply it directly without negation. + */ +export function quaternionToYaw(q: { + x: number; + y: number; + z: number; + w: number; +}): number { + const { x, y, z, w } = q; + return Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)); +} diff --git a/src/map/markerIcons.ts b/src/map/markerIcons.ts new file mode 100644 index 00000000..fd4a07d4 --- /dev/null +++ b/src/map/markerIcons.ts @@ -0,0 +1,264 @@ +import L from 'leaflet'; +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import { + COLLECTIBLE_TYPE_META, + type CollectibleType, +} from '@/recipes/WorldCollectibles'; +import type { Purity } from '@/recipes/WorldResourceNodes'; + +// Open Color shade 5 (also Mantine defaults): red-5 / yellow-5 / green-5. +// See https://yeun.github.io/open-color/ +const PURITY_RING: Record = { + impure: '#fa5252', + normal: '#ffd43b', + pure: '#51cf66', +}; + +const PURITY_LABEL: Record = { + impure: 'Impure', + normal: 'Normal', + pure: 'Pure', +}; + +export function getPurityColor(purity: Purity): string { + return PURITY_RING[purity]; +} + +export function getPurityLabel(purity: Purity): string { + return PURITY_LABEL[purity]; +} + +// Pin silhouette: head circle (center (16,16), radius 10·√2 ≈ 14.14) with +// a tip at (30.14, 30.14). The two straight sides run from the tip to the +// head's rightmost tangent point (30.14, 16) and to its bottommost tangent +// point (16, 30.14); they meet at the tip at exactly 90° and the arc-to- +// line transitions are smooth (tangent = no seam). The pin natively leans +// 45° toward its upper-left, so the glyph inside the head and the corner +// badges stay upright with no CSS rotation. +const PIN_VIEW_SIZE = 32; +const PIN_HEAD_CENTER = 16; +const PIN_HEAD_RADIUS = 14.1421; +const PIN_TIP = 30.1421; +const PIN_PATH = `M ${PIN_TIP} ${PIN_TIP} L ${PIN_TIP} ${PIN_HEAD_CENTER} A ${PIN_HEAD_RADIUS} ${PIN_HEAD_RADIUS} 0 1 0 ${PIN_HEAD_CENTER} ${PIN_TIP} Z`; + +interface PinIconParams { + /** Outer SVG width in pixels; height is scaled from the pin viewBox. */ + width: number; + /** Square icon edge (in viewBox units) painted inside the pin head. */ + innerSize: number; + /** Path to the raster icon, when not rendering an inline glyph. */ + iconHref?: string; + /** Inline SVG body to embed inside the pin head instead of an image. */ + inlineSvg?: string; + ringColor: string; + classes: string[]; + badgesHtml: string; +} + +/** + * Centralizes the pin SVG markup so resources and collectibles share + * the exact same silhouette (just at different sizes). The icon is + * rendered as an `` (or nested ``) inside the same SVG as + * the path, so it is guaranteed to stack above the pin without + * fighting CSS stacking contexts. + */ +function buildPinIcon({ + width, + innerSize, + iconHref, + inlineSvg, + ringColor, + classes, + badgesHtml, +}: PinIconParams): L.DivIcon { + const scale = width / PIN_VIEW_SIZE; + const size = Math.round(PIN_VIEW_SIZE * scale); + const tipOffset = Math.round(PIN_TIP * scale); + // Center the icon on the pin head circle (whose center is at + // (PIN_HEAD_CENTER, PIN_HEAD_CENTER) in viewBox units). + const iconOffset = PIN_HEAD_CENTER - innerSize / 2; + + let iconNode = ''; + if (iconHref) { + iconNode = ``; + } else if (inlineSvg) { + // Tabler "outline" defaults: 24x24 viewBox, 2px stroke, no fill, + // round caps + joins. `color` flows through to any path that uses + // `currentColor` (the audio-tape reels, for example). + iconNode = `${inlineSvg}`; + } + + const html = ` +
+ + ${badgesHtml} +
+ `; + + return L.divIcon({ + html, + className: 'map-marker-wrapper', + iconSize: [size, size], + iconAnchor: [tipOffset, tipOffset], + // Place the popup tail above the pin's visual center (midpoint between + // head center and tip), not the tip itself. Anchoring on the tip pushes + // the wide popup body to the right of the pin since the tip is the + // pin's rightmost point; centering balances it. + popupAnchor: [ + Math.round(((PIN_HEAD_CENTER - PIN_TIP) / 2) * scale), + -Math.round((PIN_TIP - (PIN_HEAD_CENTER - PIN_HEAD_RADIUS)) * scale), + ], + }); +} + +const RESOURCE_MARKER_WIDTH = 28; +// The pin head is a circle (center 18,18 r≈14.14), whose inscribed square +// is ~20 units. We size the icon just under that so the corners stay clear +// of the curved edge and the stroke has a sliver of room. +const RESOURCE_INNER_SIZE = 18; + +export interface ResourceMarkerIconOptions { + /** + * When true, the marker is rendered in a dimmed "already-used" + * variant with a checkmark badge in the top-right corner. + */ + used?: boolean; + /** + * When true, the marker gets a violet "selected for comparison" + * halo, distinct from both plain and used variants. Used + selected + * combine (dim + halo + check). + */ + selected?: boolean; +} + +/** + * Builds a Leaflet `divIcon` for a resource node. The marker is a + * pin-shaped SVG silhouette (gray fill, purity-tinted stroke) with the + * resource's in-game icon centered in its head. + */ +export function getResourceMarkerIcon( + resource: string, + purity: Purity, + options: ResourceMarkerIconOptions = {}, +): L.DivIcon { + const item = AllFactoryItemsMap[resource]; + const imagePath = item?.imagePath?.replace('_256', '_64') ?? ''; + const ringColor = PURITY_RING[purity]; + const classes = ['map-marker']; + if (options.used) classes.push('map-marker--used'); + if (options.selected) classes.push('map-marker--selected'); + const usedBadge = options.used + ? '' + : ''; + const selectedBadge = options.selected + ? '' + : ''; + return buildPinIcon({ + width: RESOURCE_MARKER_WIDTH, + innerSize: RESOURCE_INNER_SIZE, + iconHref: imagePath, + ringColor, + classes, + badgesHtml: `${usedBadge}${selectedBadge}`, + }); +} + +// Slightly smaller markers for collectibles, so several collectibles +// clustered around the same biome don't crowd out the resource nodes, +// but big enough that the (mostly small) Tabler glyphs stay legible. +const COLLECTIBLE_MARKER_WIDTH = 24; +const COLLECTIBLE_INNER_SIZE = 18; + +/** + * Inline SVG fallbacks for collectible types whose game art isn't + * bundled (drop pods, audio tapes, customization unlocks). Keyed by + * the Tabler icon name from {@link COLLECTIBLE_TYPE_META}.iconName. + * + * Source: `@tabler/icons` (MIT). We inline the path data directly so + * the imperative marker DOM doesn't need React's renderToString and + * the bundle stays small. + */ +const TABLER_ICON_PATHS: Record = { + IconPackage: [ + 'M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5', + 'M12 12l8 -4.5', + 'M12 12l0 9', + 'M12 12l-8 -4.5', + 'M16 5.25l-8 4.5', + ] + .map(d => ``) + .join(''), + IconDeviceAudioTape: [ + [ + 'M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10', + null, + ], + ['M3 17l4 -3h10l4 3', null], + ['M7 9.5a.5 .5 0 1 0 1 0a.5 .5 0 1 0 -1 0', 'currentColor'], + ['M16 9.5a.5 .5 0 1 0 1 0a.5 .5 0 1 0 -1 0', 'currentColor'], + ] + .map(([d, fill]) => + fill ? `` : ``, + ) + .join(''), + IconBrush: [ + 'M3 21v-4a4 4 0 1 1 4 4h-4', + 'M21 3a16 16 0 0 0 -12.8 10.2', + 'M21 3a16 16 0 0 1 -10.2 12.8', + 'M10.6 9a9 9 0 0 1 4.4 4.4', + ] + .map(d => ``) + .join(''), +}; + +export interface CollectibleMarkerIconOptions { + /** Render the marker dimmed + checkmarked (already collected). */ + collected?: boolean; +} + +/** + * Builds a Leaflet `divIcon` for a collectible. Mirrors + * {@link getResourceMarkerIcon} so the visual language stays + * consistent (pin silhouette + icon), but uses the collectible's + * themed color instead of a purity color and falls back to an inline + * Tabler glyph when the collectible has no bundled game art. + * + * The "selected" / sum-mode variant is intentionally absent: + * collectibles aren't part of the sum-mode flow (they have no + * extraction rate to sum). + */ +export function getCollectibleMarkerIcon( + type: CollectibleType, + options: CollectibleMarkerIconOptions = {}, +): L.DivIcon { + const meta = COLLECTIBLE_TYPE_META[type]; + const ringColor = meta.color; + const classes = ['map-marker', 'map-marker--collectible']; + if (options.collected) classes.push('map-marker--used'); + const collectedBadge = options.collected + ? '' + : ''; + + if (meta.iconImagePath) { + return buildPinIcon({ + width: COLLECTIBLE_MARKER_WIDTH, + innerSize: COLLECTIBLE_INNER_SIZE, + iconHref: meta.iconImagePath, + ringColor, + classes, + badgesHtml: collectedBadge, + }); + } + + return buildPinIcon({ + width: COLLECTIBLE_MARKER_WIDTH, + innerSize: COLLECTIBLE_INNER_SIZE, + inlineSvg: TABLER_ICON_PATHS[meta.iconName ?? ''] ?? '', + ringColor, + classes, + badgesHtml: collectedBadge, + }); +} diff --git a/src/map/selectionMath.ts b/src/map/selectionMath.ts new file mode 100644 index 00000000..b7b328de --- /dev/null +++ b/src/map/selectionMath.ts @@ -0,0 +1,145 @@ +import { + AllFactoryBuildings, + type FactoryBuilding, +} from '@/recipes/FactoryBuilding'; +import { AllFactoryItemsMap, FactoryItemForm } from '@/recipes/FactoryItem'; +import type { Purity, WorldResourceNode } from '@/recipes/WorldResourceNodes'; +import { + getExtractionRate, + getExtractionUnit, + type OverclockStep, +} from './extraction'; + +/** + * Solid-ore miners, ordered from worst to best. The aggregate panel + * lets the player pick one and applies it to every solid resource in + * the selection. + */ +export const SOLID_MINER_CHOICES = [ + 'Build_MinerMk1_C', + 'Build_MinerMk2_C', + 'Build_MinerMk3_C', +] as const; + +/** + * Standalone fluid pump per resource id, used only for non-fracking + * (`BP_ResourceNode_C`) nodes. Fracking satellites always use the + * Resource Well Extractor regardless of resource. + */ +const STANDALONE_FLUID_EXTRACTOR: Record = { + Desc_LiquidOil_C: 'Build_OilPump_C', + Desc_Water_C: 'Build_WaterPump_C', +}; + +const RESOURCE_WELL_EXTRACTOR_ID = 'Build_FrackingExtractor_C'; + +function findBuilding(id: string): FactoryBuilding | undefined { + return AllFactoryBuildings.find(b => b.id === id); +} + +/** + * Picks the extractor building used when summing a node's yield in + * the aggregate panel. The decision is gated by the node's actor type + * so a fracking satellite of crude oil is summed using the Resource + * Well Extractor's rate, *not* the Oil Pump's, even though both + * produce the same resource. + * + * Returns `undefined` when the node has no factory-buildable extractor + * (cores, geysers, deposits) so the sum panel skips it cleanly. + */ +function getExtractorForNode( + node: WorldResourceNode, + selectedMinerId: string, +): FactoryBuilding | undefined { + switch (node.nodeType) { + case 'frackingSatellite': + return findBuilding(RESOURCE_WELL_EXTRACTOR_ID); + case 'frackingCore': + case 'geyser': + case 'deposit': + return undefined; + case 'node': { + const item = AllFactoryItemsMap[node.resource]; + if (!item) return undefined; + if (item.form === FactoryItemForm.Solid) { + return findBuilding(selectedMinerId); + } + const pumpId = STANDALONE_FLUID_EXTRACTOR[node.resource]; + return pumpId ? findBuilding(pumpId) : undefined; + } + default: + return undefined; + } +} + +export interface ResourceAggregate { + /** + * Stable React key. Combines resource and extractor so a selection + * containing both an oil node *and* an oil fracking satellite shows + * up as two honest rows (Oil Pump vs Resource Well Extractor) rather + * than one mis-summed row. + */ + key: string; + resource: string; + /** Display name for the resource, for convenience in the UI. */ + displayName: string; + /** Extractor picked for this row (differs per solid vs fluid vs well). */ + extractorName: string; + unit: string; + /** Sum of yields across every selected node of this resource. */ + totalRate: number; + /** Count of nodes contributing, broken down by purity. */ + purityCounts: Record; + /** Total node count for this row (sum of {@link purityCounts}). */ + nodeCount: number; +} + +/** + * Computes per-resource totals for the given set of nodes at the + * chosen miner tier and overclock. Fluids/gases ignore `minerId` and + * use a default extractor. Resources with no known extractor (cores, + * geysers, deposits) are skipped. Rows are split per `(resource, + * extractor)` pair so a selection that mixes standalone oil nodes with + * oil fracking satellites yields two honest sub-totals. + */ +export function getSelectionAggregates( + nodes: readonly WorldResourceNode[], + minerId: string, + overclock: OverclockStep, +): ResourceAggregate[] { + const byKey = new Map(); + for (const node of nodes) { + const extractor = getExtractorForNode(node, minerId); + if (!extractor) continue; + + const key = `${node.resource}::${extractor.id}`; + let aggregate = byKey.get(key); + if (!aggregate) { + const item = AllFactoryItemsMap[node.resource]; + aggregate = { + key, + resource: node.resource, + displayName: item?.displayName ?? node.resource, + extractorName: extractor.name, + unit: getExtractionUnit(node.resource), + totalRate: 0, + purityCounts: { impure: 0, normal: 0, pure: 0 }, + nodeCount: 0, + }; + byKey.set(key, aggregate); + } + + aggregate.totalRate += getExtractionRate(extractor, node.purity, overclock); + aggregate.purityCounts[node.purity] += 1; + aggregate.nodeCount += 1; + } + + // Stable display order: alphabetical by display name first, then + // extractor name to keep matching resources adjacent. + return [...byKey.values()].sort((a, b) => { + const byName = a.displayName.localeCompare(b.displayName); + return byName !== 0 + ? byName + : a.extractorName.localeCompare(b.extractorName); + }); +} diff --git a/src/map/shareUrlState.ts b/src/map/shareUrlState.ts new file mode 100644 index 00000000..6f637088 --- /dev/null +++ b/src/map/shareUrlState.ts @@ -0,0 +1,288 @@ +import { + COLLECTIBLE_TYPES, + type CollectibleType, +} from '@/recipes/WorldCollectibles'; +import { PURITIES, type Purity } from '@/recipes/WorldResourceNodes'; +import { WorldResourcesList } from '@/recipes/WorldResources'; + +/** + * Compact, sharable representation of the map view + filter state. + * Encoded into the URL hash so refreshes restore the view and other + * players can open someone's link to the exact same map. This is + * intentionally a *snapshot* of view-style state — personal marks + * (used nodes, collected collectibles) are persisted only in the + * recipient's own indexedDB store and never travel through the URL. + */ +export interface ShareableMapState { + /** Leaflet zoom level. */ + zoom: number; + /** Leaflet `LatLng` center as `[lat, lng]`. */ + center: [number, number]; + /** + * Per-resource purity selection. Same shape as + * `MapSlice.resourceFilters`: a missing key hides the resource + * entirely; an empty array hides all purities for it. + */ + resourceFilters: Record; + /** Per-collectible-type visibility. */ + collectibleVisibility: Record; + /** Hide already-used nodes from the render. */ + hideUsedNodes: boolean; + /** Hide already-collected collectibles from the render. */ + hideCollectedCollectibles: boolean; +} + +/** + * Bumped if the encoding format changes in a backwards-incompatible + * way (e.g. resources renamed). Older URLs without `v` are treated + * as v1 for forward-safety. + */ +const SCHEMA_VERSION = 1; + +/** Single character per purity, in {@link PURITIES} order. */ +const PURITY_CHAR: Record = { + impure: 'I', + normal: 'N', + pure: 'P', +}; +const PURITY_FROM_CHAR: Record = { + I: 'impure', + N: 'normal', + P: 'pure', +}; + +/** + * Strip the verbose `Desc_…_C` wrapper for compactness in the URL. + * `Desc_OreIron_C` → `OreIron`. We re-wrap on parse, and validate + * against `WorldResourcesList` so unknown ids are dropped instead + * of poisoning the filter state. + */ +function shortenResourceId(id: string): string { + return id.replace(/^Desc_/, '').replace(/_C$/, ''); +} +function expandResourceId(short: string): string | null { + const candidate = `Desc_${short}_C`; + return WorldResourcesList.includes(candidate) ? candidate : null; +} + +/** + * Two-character codes for collectible types. Hand-picked so they + * stay stable when we add types later (don't reuse codes). + */ +const COLLECTIBLE_CODE: Record = { + slugMk1: 's1', + slugMk2: 's2', + slugMk3: 's3', + somersloop: 'sl', + mercerSphere: 'ms', + hardDrive: 'hd', + audioTape: 'at', + customizationUnlock: 'cu', +}; +const COLLECTIBLE_FROM_CODE: Record = (() => { + const m: Record = {}; + for (const t of COLLECTIBLE_TYPES) m[COLLECTIBLE_CODE[t]] = t; + return m; +})(); + +function defaultResourceFilters(): Record { + const filters: Record = {}; + for (const r of WorldResourcesList) filters[r] = [...PURITIES]; + return filters; +} + +function defaultCollectibleVisibility(): Record { + const v = {} as Record; + for (const t of COLLECTIBLE_TYPES) v[t] = false; + return v; +} + +/** + * Compares the given filter map to the "everything visible" default. + * When equal, we omit `rf` from the URL entirely so default-state + * links stay short. + */ +function isDefaultResourceFilters(filters: Record): boolean { + if (Object.keys(filters).length !== WorldResourcesList.length) return false; + for (const r of WorldResourcesList) { + const purities = filters[r]; + if (!purities || purities.length !== PURITIES.length) return false; + for (const p of PURITIES) if (!purities.includes(p)) return false; + } + return true; +} + +/** + * Compares to the "all collectibles hidden" default. When equal we + * omit `cv` from the URL. + */ +function isDefaultCollectibleVisibility( + visibility: Record, +): boolean { + for (const t of COLLECTIBLE_TYPES) if (visibility[t]) return false; + return true; +} + +function encodeResourceFilters(filters: Record): string { + const parts: string[] = []; + for (const resource of WorldResourcesList) { + const purities = filters[resource]; + if (!purities || purities.length === 0) continue; + const letters = PURITIES.filter(p => purities.includes(p)) + .map(p => PURITY_CHAR[p]) + .join(''); + parts.push(`${shortenResourceId(resource)}:${letters}`); + } + return parts.join(','); +} + +function decodeResourceFilters( + raw: string | null, +): Record | null { + if (raw == null) return null; + // An explicit empty `rf=` string means "all hidden", distinct from + // the absence of the key (which means "use default = all visible"). + if (raw.length === 0) return {}; + const out: Record = {}; + for (const segment of raw.split(',')) { + const [shortId, lettersRaw = ''] = segment.split(':'); + const fullId = expandResourceId(shortId); + if (!fullId) continue; + const purities: Purity[] = []; + for (const ch of lettersRaw.toUpperCase()) { + const p = PURITY_FROM_CHAR[ch]; + if (p && !purities.includes(p)) purities.push(p); + } + if (purities.length > 0) out[fullId] = purities; + } + return out; +} + +function encodeCollectibleVisibility( + visibility: Record, +): string { + return COLLECTIBLE_TYPES.filter(t => visibility[t]) + .map(t => COLLECTIBLE_CODE[t]) + .join(','); +} + +function decodeCollectibleVisibility( + raw: string | null, +): Record | null { + if (raw == null) return null; + const v = defaultCollectibleVisibility(); + if (raw.length === 0) return v; + for (const code of raw.split(',')) { + const type = COLLECTIBLE_FROM_CODE[code.trim()]; + if (type) v[type] = true; + } + return v; +} + +/** + * Serializes the given state into a URLSearchParams instance ready + * to be stuffed into `location.hash`. Keys present in the result: + * + * - `v` (schema version) — always present so we can identify our + * own URLs vs. unrelated query strings on the same path. + * - `z` (zoom) + * - `c` (center, `lat,lng`) + * - `rf` (resource filters) — omitted when equal to "show everything". + * - `cv` (collectible visibility) — omitted when no types are visible. + * - `hu` / `hc` (hide flags) — omitted when false. + * + * Numeric values are rounded to keep URLs short while still being + * accurate enough that the recipient sees roughly the same framing. + */ +export function encodeShareUrl(state: ShareableMapState): URLSearchParams { + const params = new URLSearchParams(); + params.set('v', String(SCHEMA_VERSION)); + params.set('z', roundFloat(state.zoom, 2)); + params.set( + 'c', + `${roundFloat(state.center[0], 2)},${roundFloat(state.center[1], 2)}`, + ); + if (!isDefaultResourceFilters(state.resourceFilters)) { + params.set('rf', encodeResourceFilters(state.resourceFilters)); + } + if (!isDefaultCollectibleVisibility(state.collectibleVisibility)) { + params.set('cv', encodeCollectibleVisibility(state.collectibleVisibility)); + } + if (state.hideUsedNodes) params.set('hu', '1'); + if (state.hideCollectedCollectibles) params.set('hc', '1'); + return params; +} + +/** + * Parses a hash string into a partial {@link ShareableMapState}. + * Returns `null` when the hash doesn't look like one of ours + * (missing schema version) so unrelated hashes — e.g. a `#section` + * link — don't blow away the user's local state. + * + * Each field is independently optional: an old URL without `cv` + * leaves the recipient's collectible visibility untouched. + */ +export function decodeShareUrl( + hash: string, +): Partial | null { + const trimmed = hash.startsWith('#') ? hash.slice(1) : hash; + if (trimmed.length === 0) return null; + const params = new URLSearchParams(trimmed); + if (!params.has('v')) return null; + + const out: Partial = {}; + + const zoomRaw = params.get('z'); + if (zoomRaw != null) { + const z = Number(zoomRaw); + if (Number.isFinite(z)) out.zoom = z; + } + + const centerRaw = params.get('c'); + if (centerRaw != null) { + const [latRaw, lngRaw] = centerRaw.split(','); + const lat = Number(latRaw); + const lng = Number(lngRaw); + if (Number.isFinite(lat) && Number.isFinite(lng)) out.center = [lat, lng]; + } + + const rf = decodeResourceFilters(params.get('rf')); + if (rf != null) out.resourceFilters = rf; + + const cv = decodeCollectibleVisibility(params.get('cv')); + if (cv != null) out.collectibleVisibility = cv; + + if (params.has('hu')) out.hideUsedNodes = params.get('hu') === '1'; + if (params.has('hc')) + out.hideCollectedCollectibles = params.get('hc') === '1'; + + return out; +} + +function roundFloat(value: number, decimals: number): string { + const factor = 10 ** decimals; + return String(Math.round(value * factor) / factor); +} + +/** + * Compares two {@link ShareableMapState} instances for value + * equality. Used by the live-sync writer to skip redundant URL + * rewrites when nothing has actually changed (panning often fires + * `moveend` even when the center didn't shift after rounding). + */ +export function shareUrlsEqual( + a: URLSearchParams, + b: URLSearchParams, +): boolean { + if (a.toString() === b.toString()) return true; + // Order-insensitive equality fallback in case the browser + // re-serializes our hash in a different order on round-trip. + const aKeys = Array.from(a.keys()).sort(); + const bKeys = Array.from(b.keys()).sort(); + if (aKeys.length !== bKeys.length) return false; + for (let i = 0; i < aKeys.length; i++) { + if (aKeys[i] !== bKeys[i]) return false; + if (a.get(aKeys[i]) !== b.get(bKeys[i])) return false; + } + return true; +} diff --git a/src/map/store/mapInfrastructureSlice.ts b/src/map/store/mapInfrastructureSlice.ts new file mode 100644 index 00000000..04c8888c --- /dev/null +++ b/src/map/store/mapInfrastructureSlice.ts @@ -0,0 +1,87 @@ +import { createSlice } from '@/core/zustand-helpers/slices'; +import type { + ParsedInfrastructure, + ParsedPlayerPosition, +} from '@/recipes/savegame/ParseSavegameMessages'; + +/** + * In-memory only: the parsed user-built infrastructure for the + * currently active game. Excluded from persistence in the root store's + * `partialize`, so it disappears on reload — by design. The data set is + * a few MB of typed arrays; persisting it would gum up cloud sync and + * IndexedDB without giving the user much benefit (re-parsing the + * `.sav` is fast and explicit). + * + * The `gameId` is kept alongside the payload so a reader can tell + * whether the in-memory data is for the currently selected game and + * fall back to "no infrastructure" if the user has switched games. + */ +export interface MapInfrastructureSlice { + /** Parsed payload from the worker, or `null` if nothing is loaded. */ + infrastructure: ParsedInfrastructure | null; + /** + * World-cm positions of every `Char_Player_C` actor extracted from + * the most recent savegame import. Empty array when nothing is + * loaded or the save had no spawned player. The first entry is + * treated as the host and used as the centering target on import / + * "Locate". + */ + players: ParsedPlayerPosition[]; + /** Game id the payload was imported for. */ + gameId: string | null; + /** When the payload was set, in ms since epoch. Used for cache keys. */ + loadedAt: number | null; + /** + * Bumped to a new timestamp whenever the user asks the map to re-frame + * itself around the loaded infrastructure (e.g. clicking "Locate"). The + * canvas layer owns the camera and listens for changes here so the + * filter panel doesn't need a direct handle on the Leaflet `Map`. + */ + requestedFitAt: number | null; +} + +export const initialMapInfrastructureState = (): MapInfrastructureSlice => ({ + infrastructure: null, + players: [], + gameId: null, + loadedAt: null, + requestedFitAt: null, +}); + +export const mapInfrastructureSlice = createSlice({ + name: 'mapInfrastructure', + value: initialMapInfrastructureState() as MapInfrastructureSlice, + actions: { + setInfrastructure: + (gameId: string, infrastructure: ParsedInfrastructure) => state => { + state.infrastructure = infrastructure; + state.gameId = gameId; + state.loadedAt = Date.now(); + // Pan/zoom to the freshly imported geometry the first time it + // arrives, otherwise it lives invisibly off-viewport for users + // who haven't already centered the map on their factory. + state.requestedFitAt = state.loadedAt; + }, + setPlayers: (gameId: string, players: ParsedPlayerPosition[]) => state => { + state.players = players; + if (state.gameId !== gameId) { + state.gameId = gameId; + state.loadedAt = Date.now(); + } + // Trigger re-framing so the camera centers on the player even + // for recipes-only imports (where `setInfrastructure` is not + // dispatched and would otherwise leave the view untouched). + state.requestedFitAt = Date.now(); + }, + clearInfrastructure: () => state => { + state.infrastructure = null; + state.players = []; + state.gameId = null; + state.loadedAt = null; + state.requestedFitAt = null; + }, + requestInfrastructureFit: () => state => { + state.requestedFitAt = Date.now(); + }, + }, +}); diff --git a/src/map/store/mapSelectionSlice.ts b/src/map/store/mapSelectionSlice.ts new file mode 100644 index 00000000..0ead63b6 --- /dev/null +++ b/src/map/store/mapSelectionSlice.ts @@ -0,0 +1,76 @@ +import { createSlice } from '@/core/zustand-helpers/slices'; +import type { OverclockStep } from '../extraction'; + +/** + * Ephemeral, session-only state backing the "sum nodes" experience. + * Intentionally excluded from persistence via `partialize` in the + * root store so it resets on every reload — matches the mental model + * of a transient aggregate rather than a saved shopping list. + */ +export interface MapSelectionSlice { + /** + * Whether the map is in "sum" mode. In sum mode, clicking a marker + * toggles it in/out of the selection instead of opening its popup. + * Tapping the same marker again deselects. + */ + sumMode: boolean; + /** Node ids currently in the selection (order preserved). */ + selectedNodeIds: string[]; + /** + * Extractor id used when summing solid-ore yields in the aggregate + * panel. Defaults to Miner Mk3 since that's the end-game target + * most players are planning around. + */ + selectedMinerId: string; + /** Overclock percentage applied uniformly across the aggregate. */ + selectedOverclock: OverclockStep; +} + +export const initialMapSelectionState = (): MapSelectionSlice => ({ + sumMode: false, + selectedNodeIds: [], + selectedMinerId: 'Build_MinerMk3_C', + selectedOverclock: 100, +}); + +export const mapSelectionSlice = createSlice({ + name: 'mapSelection', + value: initialMapSelectionState() as MapSelectionSlice, + actions: { + setSumMode: (enabled: boolean) => state => { + state.sumMode = enabled; + // Intentionally keep selectedNodeIds when leaving sum mode so + // the user's aggregates don't vanish the moment they step out + // of the multi-select workflow to inspect a popup. + }, + toggleSumMode: () => state => { + state.sumMode = !state.sumMode; + }, + toggleNodeSelected: (nodeId: string) => state => { + const idx = state.selectedNodeIds.indexOf(nodeId); + if (idx === -1) { + state.selectedNodeIds.push(nodeId); + } else { + state.selectedNodeIds.splice(idx, 1); + } + }, + addNodeToSelection: (nodeId: string) => state => { + if (!state.selectedNodeIds.includes(nodeId)) { + state.selectedNodeIds.push(nodeId); + } + }, + removeNodeFromSelection: (nodeId: string) => state => { + const idx = state.selectedNodeIds.indexOf(nodeId); + if (idx !== -1) state.selectedNodeIds.splice(idx, 1); + }, + clearSelection: () => state => { + state.selectedNodeIds = []; + }, + setSelectedMinerId: (minerId: string) => state => { + state.selectedMinerId = minerId; + }, + setSelectedOverclock: (overclock: OverclockStep) => state => { + state.selectedOverclock = overclock; + }, + }, +}); diff --git a/src/map/store/mapSlice.ts b/src/map/store/mapSlice.ts new file mode 100644 index 00000000..f5787e06 --- /dev/null +++ b/src/map/store/mapSlice.ts @@ -0,0 +1,322 @@ +import { createSlice } from '@/core/zustand-helpers/slices'; +import { + INFRASTRUCTURE_CATEGORIES, + type InfrastructureCategory, + SPLINE_KINDS, + type SplineKind, +} from '@/recipes/savegame/ParseSavegameMessages'; +import { + COLLECTIBLE_TYPES, + type CollectibleType, +} from '@/recipes/WorldCollectibles'; +import { PURITIES, type Purity } from '@/recipes/WorldResourceNodes'; +import { WorldResourcesList } from '@/recipes/WorldResources'; + +export interface MapSlice { + /** + * Per-resource purity filter. The set of visible nodes is the union + * of `(resource, purity)` pairs in this map. Absent keys hide the + * resource entirely; an empty array hides all purities for it. + * Defaults to "every world resource visible at every purity" so the + * map shows everything on first load. + */ + resourceFilters: Record; + /** + * When true, used nodes are dropped from the map render entirely. + * When false, they're rendered with a faded/checkmark variant. + * Persists across games — it's a view preference, not a per-game + * value (the marks themselves live on each {@link Game}). + */ + hideUsedNodes: boolean; + /** + * Per-collectible-type visibility. `true` = render markers on the + * map; `false` = hide entirely. Defaults to "everything visible". + * Kept independent of `resourceFilters` because collectibles have + * different semantics (no purity, no extractor) and the player will + * want to toggle them as a separate group. + */ + collectibleVisibility: Record; + /** + * When true, already-collected collectibles are dropped from the + * render entirely. Same view-preference rationale as + * {@link hideUsedNodes}. + */ + hideCollectedCollectibles: boolean; + /** + * Master toggle for the built-infrastructure canvas layer. When + * false the layer is hidden regardless of the per-category / + * per-spline-kind toggles below. Persists as a view preference. + */ + infrastructureMaster: boolean; + /** + * Per-category visibility for the infrastructure canvas layer + * (production / logistics / power / ...). Categories not in the + * record default to visible at rehydrate via + * {@link ensureMapSliceShape}. + */ + infrastructureCategoryVisibility: Record; + /** + * Per-kind visibility for the spline networks rendered by the + * infrastructure layer (belts / pipes / hyper / rail / power). + */ + infrastructureSplineVisibility: Record; +} + +function defaultResourceFilters(): Record { + const filters: Record = {}; + for (const resource of WorldResourcesList) { + filters[resource] = [...PURITIES]; + } + return filters; +} + +/** + * Collectibles ship hidden by default. There are ~1.7k of them so + * showing every category at once on first load drowns out the + * resource layer; the filter panel makes opting in cheap and the + * "0 / N collected" counter advertises the toggles are there. + */ +function defaultCollectibleVisibility(): Record { + const visibility = {} as Record; + for (const type of COLLECTIBLE_TYPES) visibility[type] = false; + return visibility; +} + +function defaultInfrastructureCategoryVisibility(): Record< + InfrastructureCategory, + boolean +> { + const visibility = {} as Record; + for (const cat of INFRASTRUCTURE_CATEGORIES) visibility[cat] = true; + return visibility; +} + +function defaultInfrastructureSplineVisibility(): Record { + const visibility = {} as Record; + for (const kind of SPLINE_KINDS) visibility[kind] = true; + return visibility; +} + +/** + * Factory for a fresh {@link MapSlice}. Exported so the persist + * migration can replace the old slice shape wholesale — zustand's + * default shallow merge would otherwise leave the rehydrated state + * missing the new keys. + */ +export const initialMapSliceState = (): MapSlice => ({ + resourceFilters: defaultResourceFilters(), + hideUsedNodes: false, + collectibleVisibility: defaultCollectibleVisibility(), + hideCollectedCollectibles: false, + infrastructureMaster: true, + infrastructureCategoryVisibility: defaultInfrastructureCategoryVisibility(), + infrastructureSplineVisibility: defaultInfrastructureSplineVisibility(), +}); + +/** + * Ensures the slice has every expected field before an action mutates + * it. Older persisted stores from this feature's development cycles + * had only a subset of keys and the zustand persist middleware's + * shallow merge leaves the slice in that stale shape on rehydrate. + * Backfilling here keeps actions safe regardless of where the slice + * came from. + */ +function ensureMapSliceShape(state: MapSlice): void { + if (!state.resourceFilters) state.resourceFilters = defaultResourceFilters(); + if (typeof state.hideUsedNodes !== 'boolean') state.hideUsedNodes = false; + if (!state.collectibleVisibility) { + state.collectibleVisibility = defaultCollectibleVisibility(); + } else { + // Backfill any newly-introduced collectible types. We default + // them off to match {@link defaultCollectibleVisibility} — the + // player can opt in from the filter panel. + for (const type of COLLECTIBLE_TYPES) { + if (typeof state.collectibleVisibility[type] !== 'boolean') { + state.collectibleVisibility[type] = false; + } + } + } + if (typeof state.hideCollectedCollectibles !== 'boolean') { + state.hideCollectedCollectibles = false; + } +} + +export const mapSlice = createSlice({ + name: 'map', + value: initialMapSliceState() as MapSlice, + actions: { + /** + * Toggles a single (resource, purity) pair. When the resource + * isn't tracked yet, it's added with just this purity selected. + */ + toggleResourcePurity: (resource: string, purity: Purity) => state => { + ensureMapSliceShape(state); + const current = state.resourceFilters[resource]; + if (!current) { + state.resourceFilters[resource] = [purity]; + return; + } + const idx = current.indexOf(purity); + if (idx === -1) { + current.push(purity); + } else { + current.splice(idx, 1); + } + }, + /** Sets every selected purity for a single resource at once. */ + setResourcePurities: (resource: string, purities: Purity[]) => state => { + ensureMapSliceShape(state); + if (purities.length === 0) { + delete state.resourceFilters[resource]; + } else { + state.resourceFilters[resource] = [...purities]; + } + }, + /** + * Bulk action: enable or disable every world resource at every + * purity (Reset / Clear all). + */ + setAllResourcesEnabled: (enabled: boolean) => state => { + ensureMapSliceShape(state); + if (!enabled) { + state.resourceFilters = {}; + return; + } + state.resourceFilters = defaultResourceFilters(); + }, + /** + * Bulk action: across every world resource, force the given + * purity on or off (e.g. "show only Pure", "hide all Impure"). + */ + setAllResourcesPurity: (purity: Purity, enabled: boolean) => state => { + ensureMapSliceShape(state); + for (const resource of WorldResourcesList) { + const current = state.resourceFilters[resource] ?? []; + const idx = current.indexOf(purity); + if (enabled && idx === -1) { + state.resourceFilters[resource] = [...current, purity]; + } else if (!enabled && idx !== -1) { + const next = [...current]; + next.splice(idx, 1); + if (next.length === 0) { + delete state.resourceFilters[resource]; + } else { + state.resourceFilters[resource] = next; + } + } + } + }, + /** + * Convenience for "show only the given purity, on every + * resource". Replaces any existing per-resource selection. + */ + setOnlyPurity: (purity: Purity) => state => { + ensureMapSliceShape(state); + const next: Record = {}; + for (const resource of WorldResourcesList) { + next[resource] = [purity]; + } + state.resourceFilters = next; + }, + /** + * Replaces the entire resource filter map atomically. Used by + * the share-URL loader so applying an incoming link doesn't + * fire 13 individual mutations and trigger 13 re-renders. + */ + setResourceFilters: (filters: Record) => state => { + ensureMapSliceShape(state); + const next: Record = {}; + for (const [resource, purities] of Object.entries(filters)) { + if (!WorldResourcesList.includes(resource)) continue; + const cleaned = purities.filter(p => PURITIES.includes(p)); + if (cleaned.length > 0) next[resource] = cleaned; + } + state.resourceFilters = next; + }, + setHideUsedNodes: (hide: boolean) => state => { + ensureMapSliceShape(state); + state.hideUsedNodes = hide; + }, + /** Toggles visibility of a single collectible type on the map. */ + toggleCollectibleType: (type: CollectibleType) => state => { + ensureMapSliceShape(state); + state.collectibleVisibility[type] = !state.collectibleVisibility[type]; + }, + /** Bulk action: show / hide every collectible type at once. */ + setAllCollectiblesVisible: (visible: boolean) => state => { + ensureMapSliceShape(state); + for (const type of COLLECTIBLE_TYPES) { + state.collectibleVisibility[type] = visible; + } + }, + /** + * Replaces the collectible visibility map atomically. Mirrors + * {@link setResourceFilters} for the share-URL loader path. + * Unknown keys are dropped; missing keys default to hidden so + * older URLs (created before a new collectible type existed) + * keep that type off, preserving the recipient's "explicit + * opt-in" semantics for collectibles. + */ + setCollectibleVisibility: + (visibility: Record) => state => { + ensureMapSliceShape(state); + for (const type of COLLECTIBLE_TYPES) { + state.collectibleVisibility[type] = !!visibility[type]; + } + }, + setHideCollectedCollectibles: (hide: boolean) => state => { + ensureMapSliceShape(state); + state.hideCollectedCollectibles = hide; + }, + setInfrastructureMaster: (visible: boolean) => state => { + ensureMapSliceShape(state); + state.infrastructureMaster = visible; + }, + toggleInfrastructureCategory: + (category: InfrastructureCategory) => state => { + ensureMapSliceShape(state); + state.infrastructureCategoryVisibility[category] = + !state.infrastructureCategoryVisibility[category]; + }, + setInfrastructureCategoryVisibility: + (category: InfrastructureCategory, visible: boolean) => state => { + ensureMapSliceShape(state); + state.infrastructureCategoryVisibility[category] = visible; + }, + setAllInfrastructureCategoriesVisible: (visible: boolean) => state => { + ensureMapSliceShape(state); + for (const cat of INFRASTRUCTURE_CATEGORIES) { + state.infrastructureCategoryVisibility[cat] = visible; + } + }, + toggleInfrastructureSplineKind: (kind: SplineKind) => state => { + ensureMapSliceShape(state); + state.infrastructureSplineVisibility[kind] = + !state.infrastructureSplineVisibility[kind]; + }, + setInfrastructureSplineVisibility: + (kind: SplineKind, visible: boolean) => state => { + ensureMapSliceShape(state); + state.infrastructureSplineVisibility[kind] = visible; + }, + /** + * Resets only the visibility filters back to "show everything". + * Used-node marks and collected collectibles live on each + * {@link Game} now and are intentionally untouched here — those + * represent real player choices, not display preferences. + */ + resetMapFilters: () => state => { + ensureMapSliceShape(state); + const reset = initialMapSliceState(); + state.resourceFilters = reset.resourceFilters; + state.hideUsedNodes = reset.hideUsedNodes; + state.collectibleVisibility = reset.collectibleVisibility; + state.hideCollectedCollectibles = reset.hideCollectedCollectibles; + state.infrastructureMaster = reset.infrastructureMaster; + state.infrastructureCategoryVisibility = + reset.infrastructureCategoryVisibility; + state.infrastructureSplineVisibility = + reset.infrastructureSplineVisibility; + }, + }, +}); diff --git a/src/recipes/FactoryBuildings.json b/src/recipes/FactoryBuildings.json index ef925d67..41e2c3b0 100644 --- a/src/recipes/FactoryBuildings.json +++ b/src/recipes/FactoryBuildings.json @@ -18,10 +18,7 @@ }, "imagePath": "/images/game/pipe-pump-mk-2_256.png", "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, + "pipeline": null, "extractor": null, "buildCost": [ { @@ -42,7 +39,7 @@ "id": "Build_PipelinePump_C", "name": "Pipeline Pump Mk.1", "index": 1, - "description": "Attaches to a Pipeline to apply Head Lift.\r\nMaximum Head Lift: 20 m\r\n(Allows fluids to be transported 20 m upwards.)\r\n\r\nNOTE: Arrows and holograms when building indicate Head Lift direction.\r\nNOTE: Head Lift does not stack, so space between Pumps is recommended.", + "description": "Can be attached to a Pipeline to apply Head Lift.\r\nMaximum Head Lift: 20 m\r\n(Allows fluids to be transported 20 meters upwards.)\r\n\r\nNOTE: Arrows and holograms when building indicate Head Lift direction.\r\nNOTE: Head Lift does not stack, so space between Pumps is recommended.", "minimumPowerConsumption": null, "maximumPowerConsumption": null, "averagePowerConsumption": 4, @@ -57,10 +54,7 @@ }, "imagePath": "/images/game/pipe-pump_256.png", "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, + "pipeline": null, "extractor": null, "buildCost": [ { @@ -92,10 +86,7 @@ }, "imagePath": "/images/game/valve_256.png", "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, + "pipeline": null, "extractor": null, "buildCost": [ { @@ -480,154 +471,6 @@ "requiresSupplementalResource": false } }, - { - "id": "Build_PipelineSupport_C", - "name": "Pipeline Support", - "index": 12, - "description": "Connects Pipeline segments. Support height can be adjusted.\r\nUseful for routing Pipelines more precisely and across long distances.", - "minimumPowerConsumption": null, - "maximumPowerConsumption": null, - "averagePowerConsumption": null, - "powerConsumption": null, - "powerConsumptionExponent": null, - "somersloopPowerConsumptionExponent": null, - "somersloopSlots": null, - "clearance": { - "width": null, - "length": null, - "height": null - }, - "imagePath": "/images/game/pipeline-support_256.png", - "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, - "extractor": null, - "buildCost": [ - { - "resource": "Desc_IronPlate_C", - "amount": 2 - }, - { - "resource": "Desc_Cement_C", - "amount": 2 - } - ] - }, - { - "id": "Build_PipeSupportStackable_C", - "name": "Stackable Pipeline Support", - "index": 13, - "description": "Supports Pipelines. Can be stacked on other stackable supports.", - "minimumPowerConsumption": null, - "maximumPowerConsumption": null, - "averagePowerConsumption": null, - "powerConsumption": null, - "powerConsumptionExponent": null, - "somersloopPowerConsumptionExponent": null, - "somersloopSlots": null, - "clearance": { - "width": 1, - "length": 2.4, - "height": 2 - }, - "imagePath": "/images/game/stackable-pipeline-support_256.png", - "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, - "extractor": null, - "buildCost": [ - { - "resource": "Desc_IronPlate_C", - "amount": 4 - }, - { - "resource": "Desc_IronRod_C", - "amount": 2 - }, - { - "resource": "Desc_Cement_C", - "amount": 2 - } - ] - }, - { - "id": "Build_HyperPoleStackable_C", - "name": "Stackable Hypertube Support", - "index": 14, - "description": "Supports Hypertubes. Can be stacked on other stackable supports.", - "minimumPowerConsumption": null, - "maximumPowerConsumption": null, - "averagePowerConsumption": null, - "powerConsumption": null, - "powerConsumptionExponent": null, - "somersloopPowerConsumptionExponent": null, - "somersloopSlots": null, - "clearance": { - "width": 1, - "length": 2.4, - "height": 2 - }, - "imagePath": "/images/game/stackable-hypertube-support_256.png", - "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, - "extractor": null, - "buildCost": [ - { - "resource": "Desc_IronPlate_C", - "amount": 4 - }, - { - "resource": "Desc_IronRod_C", - "amount": 2 - }, - { - "resource": "Desc_Cement_C", - "amount": 2 - } - ] - }, - { - "id": "Build_PipeHyperSupport_C", - "name": "Hypertube Support", - "index": 15, - "description": "Supports Hypertubes, allowing them to stretch over longer distances.", - "minimumPowerConsumption": null, - "maximumPowerConsumption": null, - "averagePowerConsumption": null, - "powerConsumption": null, - "powerConsumptionExponent": null, - "somersloopPowerConsumptionExponent": null, - "somersloopSlots": null, - "clearance": { - "width": null, - "length": null, - "height": null - }, - "imagePath": "/images/game/hypertube-support_256.png", - "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, - "extractor": null, - "buildCost": [ - { - "resource": "Desc_IronPlate_C", - "amount": 2 - }, - { - "resource": "Desc_Cement_C", - "amount": 2 - } - ] - }, { "id": "Build_Pipeline_C", "name": "Pipeline Mk.1", @@ -774,15 +617,12 @@ "somersloopSlots": 0, "clearance": { "width": 2.4, - "length": 1.5, + "length": 2.4, "height": 1.5 }, "imagePath": "/images/game/pipeline-junction_256.png", "conveyor": null, - "pipeline": { - "isPipeline": true, - "flowRate": null - }, + "pipeline": null, "extractor": null, "buildCost": [ { @@ -1164,7 +1004,7 @@ "id": "Build_OilRefinery_C", "name": "Refinery", "index": 29, - "description": "Refines fluid and/or solid parts into other parts.\r\nHead Lift: 10 m\r\n(Allows fluids to be transported 10 meters upwards.)\r\n\r\nContains both Conveyor Belt and Pipeline input and output ports so that a wide range of recipes can be automated.", + "description": "Refines fluid and/or solid parts into other parts.\r\nHead Lift: 10 m\r\n(Allows fluids to be transported 10 meters upwards.)\r\n\r\nHas both Conveyor Belt and Pipeline input and output ports so that a wide range of recipes can be automated.", "minimumPowerConsumption": null, "maximumPowerConsumption": null, "averagePowerConsumption": 30, @@ -1240,7 +1080,7 @@ "id": "Build_Packager_C", "name": "Packager", "index": 31, - "description": "Packages and unpackages fluids.\r\nHead Lift: 10 m\r\n(Allows fluids to be transported 10 meters upwards.)\r\n\r\nContains both Conveyor Belt and Pipeline input and output ports so that a wide range of recipes can be automated.", + "description": "Packages and unpackages fluids.\r\nHead Lift: 10 m\r\n(Allows fluids to be transported 10 meters upwards.)\r\n\r\nHas both Conveyor Belt and Pipeline input and output ports so that a wide range of recipes can be automated.", "minimumPowerConsumption": null, "maximumPowerConsumption": null, "averagePowerConsumption": 10, @@ -1420,6 +1260,38 @@ } ] }, + { + "id": "Build_PipelineSupport_C", + "name": "Pipeline Support", + "index": 36, + "description": "Connects Pipeline segments. Support height can be adjusted.\r\nUseful for routing Pipelines more precisely and across long distances.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/pipe-pole_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, { "id": "Build_ConstructorMk1_C", "name": "Constructor", @@ -1452,6 +1324,38 @@ } ] }, + { + "id": "Build_PipeHyperSupport_C", + "name": "Hypertube Support", + "index": 37, + "description": "Supports Hypertubes, allowing them to stretch over longer distances.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/hyper-tube-pole_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, { "id": "Build_GeneratorNuclear_C", "name": "Nuclear Power Plant", @@ -1562,47 +1466,2646 @@ ] }, { - "id": "Build_GeneratorGeoThermal_C", - "name": "Geothermal Generator", - "index": 39, - "description": "Harnesses geothermal energy to generate power. Must be built on a Geyser.\r\n\r\nCaution: Power production fluctuates.\r\n\r\nPower Production:\r\nImpure Geyser: 50-150 MW (100 MW average)\r\nNormal Geyser: 100-300 MW (200 MW average)\r\nPure Geyser: 200-600 MW (400 MW average)", + "id": "Build_PipeSupportStackable_C", + "name": "Stackable Pipeline Support", + "index": 38, + "description": "Supports Pipelines. Can be stacked on other stackable supports.", "minimumPowerConsumption": null, "maximumPowerConsumption": null, - "averagePowerConsumption": 0, - "powerConsumption": 0, - "powerConsumptionExponent": 1.6, - "somersloopPowerConsumptionExponent": 2, - "somersloopSlots": 0, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, "clearance": { - "width": 20, - "length": 19, - "height": 32 + "width": null, + "length": null, + "height": null }, - "imagePath": "/images/game/geo-thermal-power-generator_256.png", + "imagePath": "/images/game/pipe-pole-stackable_256.png", "conveyor": null, "pipeline": null, "extractor": null, "buildCost": [ { - "resource": "Desc_Motor_C", - "amount": 10 - }, - { - "resource": "Desc_ModularFrame_C", - "amount": 25 - }, - { - "resource": "Desc_HighSpeedConnector_C", - "amount": 25 + "resource": "Desc_IronPlate_C", + "amount": 4 }, { - "resource": "Desc_CopperSheet_C", - "amount": 50 + "resource": "Desc_IronRod_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_HyperPoleStackable_C", + "name": "Stackable Hypertube Support", + "index": 39, + "description": "Supports Hypertubes. Can be stacked on other stackable supports.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/hyper-tube-stackable_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 4 + }, + { + "resource": "Desc_IronRod_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_GeneratorGeoThermal_C", + "name": "Geothermal Generator", + "index": 39, + "description": "Harnesses geothermal energy to generate power. Must be built on a Geyser.\r\n\r\nCaution: Power production fluctuates.\r\n\r\nPower Production:\r\nImpure Geyser: 50-150 MW (100 MW average)\r\nNormal Geyser: 100-300 MW (200 MW average)\r\nPure Geyser: 200-600 MW (400 MW average)", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 20, + "length": 19, + "height": 32 + }, + "imagePath": "/images/game/geo-thermal-power-generator_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Motor_C", + "amount": 10 + }, + { + "resource": "Desc_ModularFrame_C", + "amount": 25 + }, + { + "resource": "Desc_HighSpeedConnector_C", + "amount": 25 + }, + { + "resource": "Desc_CopperSheet_C", + "amount": 50 }, { "resource": "Desc_Wire_C", "amount": 250 } + ], + "powerGenerator": { + "fuels": [], + "powerProduction": 200, + "supplementalLoadAmount": 0, + "fuelLoadAmount": 0, + "requiresSupplementalResource": false + } + }, + { + "id": "Build_ConveyorPoleStackable_C", + "name": "Stackable Conveyor Pole", + "index": 40, + "description": "Supports Conveyor Belts. Can be stacked on other stackable supports.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-pole-multi_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronRod_C", + "amount": 2 + }, + { + "resource": "Desc_IronPlate_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_RailroadTrack_C", + "name": "Railway", + "index": 41, + "description": "Carries trains reliably and quickly.\r\nHas a wide turn angle, so make sure to plan it out properly.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/track_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPipe_C", + "amount": 1 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 1 + } + ] + }, + { + "id": "Build_TradingPost_C", + "name": "The HUB", + "index": 42, + "description": "The heart of your factory. This is where you complete FICSIT milestones to unlock additional schematics for buildings, vehicles, parts, equipment, etc.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 14, + "length": 26, + "height": 8.5 + }, + "imagePath": "/images/game/hub_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_OreIron_C", + "amount": 20 + } + ] + }, + { + "id": "Build_PowerPoleMk1_C", + "name": "Power Pole Mk.1", + "index": 43, + "description": "Allows for up to 4 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators, and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.8, + "length": 0.8, + "height": 7.6 + }, + "imagePath": "/images/game/power-pole-mk-1_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Wire_C", + "amount": 3 + }, + { + "resource": "Desc_IronRod_C", + "amount": 1 + }, + { + "resource": "Desc_Cement_C", + "amount": 1 + } + ] + }, + { + "id": "Build_PowerPoleWall_C", + "name": "Wall Outlet Mk.1", + "index": 44, + "description": "Functions like a Power Pole, but attaches to a wall.\r\n\r\nAllows for up to 4 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.89640068, + "length": 0.56551498, + "height": 0.56551582 + }, + "imagePath": "/images/game/power-pole-wall-mk-1_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Wire_C", + "amount": 4 + }, + { + "resource": "Desc_IronRod_C", + "amount": 1 + } + ] + }, + { + "id": "Build_PowerTower_C", + "name": "Power Tower", + "index": 45, + "description": "Helps span Power Lines across greater distances.\r\nThere is an additional power connector at the bottom of the Power Tower to connect it to other buildings, such as Power Poles.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 12, + "length": 14, + "height": 7 + }, + "imagePath": "/images/game/power-tower_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 10 + }, + { + "resource": "Desc_Wire_C", + "amount": 20 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 20 + } + ] + }, + { + "id": "Build_PowerTowerPlatform_C", + "name": "Power Tower Platform", + "index": 46, + "description": "Helps span Power Lines across greater distances.\r\nThere is an additional power connector at the bottom of the Power Tower to connect it to other buildings, such as Power Poles.\r\n\r\nNote: This Power Tower variant includes a ladder and platform for improved utility.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 12, + "length": 14, + "height": 7 + }, + "imagePath": "/images/game/power-tower-platform_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 10 + }, + { + "resource": "Desc_Wire_C", + "amount": 20 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 20 + } + ] + }, + { + "id": "Build_PowerPoleMk3_C", + "name": "Power Pole Mk.3", + "index": 47, + "description": "Allows for up to 10 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators, and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.8, + "length": 0.8, + "height": 8.7 + }, + "imagePath": "/images/game/power-pole-mk-3_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_HighSpeedConnector_C", + "amount": 2 + }, + { + "resource": "Desc_SteelPipe_C", + "amount": 2 + }, + { + "resource": "Desc_Rubber_C", + "amount": 3 + } + ] + }, + { + "id": "Build_PowerPoleMk2_C", + "name": "Power Pole Mk.2", + "index": 48, + "description": "Allows for up to 7 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators, and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.8, + "length": 0.8, + "height": 7.4 + }, + "imagePath": "/images/game/power-pole-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_HighSpeedWire_C", + "amount": 6 + }, + { + "resource": "Desc_IronRod_C", + "amount": 2 + }, + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_PowerPoleWallDouble_C", + "name": "Double Wall Outlet Mk.1", + "index": 49, + "description": "Functions like a Power Pole, but attaches to a wall. Has one connector on each side of the wall.\r\n\r\nAllows for up to 4 Power Line connections per side.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 2.0031363, + "length": 0.56551498, + "height": 0.56551582 + }, + "imagePath": "/images/game/power-pole-wall-double-mk-1_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Wire_C", + "amount": 8 + }, + { + "resource": "Desc_IronRod_C", + "amount": 2 + } + ] + }, + { + "id": "Build_PowerPoleWallDouble_Mk2_C", + "name": "Double Wall Outlet Mk.2", + "index": 50, + "description": "Functions like a Power Pole, but attaches to a wall. Has one connector on each side of the wall.\r\n\r\nAllows for up to 7 Power Line connections per side.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 2.0031363, + "length": 0.69121078, + "height": 0.69121064 + }, + "imagePath": "/images/game/double-wall-outlet-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_PowerPoleWall_Mk2_C", + "name": "Wall Outlet Mk.2", + "index": 51, + "description": "Functions like a Power Pole, but attaches to a wall.\r\n\r\nAllows for up to 7 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.89640068, + "length": 0.69121078, + "height": 0.69121064 + }, + "imagePath": "/images/game/wall-outlet-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_PowerPoleWallDouble_Mk3_C", + "name": "Double Wall Outlet Mk.3", + "index": 52, + "description": "Functions like a Power Pole, but attaches to a wall. Has one connector on each side of the wall.\r\n\r\nAllows for up to 10 Power Line connections per side.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 2.0031363, + "length": 0.8651617500000001, + "height": 0.8651621199999999 + }, + "imagePath": "/images/game/double-wall-outlet-mk-3_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_PowerPoleWall_Mk3_C", + "name": "Wall Outlet Mk.3", + "index": 53, + "description": "Functions like a Power Pole, but attaches to a wall.\r\n\r\nAllows for up to 10 Power Line connections.\r\n\r\nConnect Power Poles, Power Generators and factory buildings with Power Lines to create a power grid. The power grid supplies all connected buildings with power.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.89640068, + "length": 0.8651617500000001, + "height": 0.8651621199999999 + }, + "imagePath": "/images/game/wall-outlet-mk-3_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_PipeStorageTank_C", + "name": "Fluid Buffer", + "index": 54, + "description": "Holds up to 400 m³ of fluid.\r\nHas Pipeline input and output ports.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 6, + "length": 6, + "height": 8 + }, + "imagePath": "/images/game/fluid-storage_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_CopperSheet_C", + "amount": 10 + }, + { + "resource": "Desc_ModularFrame_C", + "amount": 5 + } + ] + }, + { + "id": "Build_IndustrialTank_C", + "name": "Industrial Fluid Buffer", + "index": 55, + "description": "Holds up to 2400 m³ of fluid.\r\nHas Pipeline input and output ports.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 14, + "length": 14, + "height": 12 + }, + "imagePath": "/images/game/fluid-storage-industrial_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 5 + }, + { + "resource": "Desc_CopperSheet_C", + "amount": 10 + }, + { + "resource": "Desc_Plastic_C", + "amount": 25 + } + ] + }, + { + "id": "Build_ResourceSink_C", + "name": "AWESOME Sink", + "index": 56, + "description": "Got excess resources? Fear not, for FICSIT does not waste! The newly developed AWESOME Sink turns any and all useful parts into research data just as fast as you can supply them! \r\nParticipating pioneers will be compensated with Coupons that can be spent at the AWESOME Shop.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 30, + "powerConsumption": 30, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 14, + "height": 9.5 + }, + "imagePath": "/images/game/resource-sink_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlateReinforced_C", + "amount": 15 + }, + { + "resource": "Desc_Cable_C", + "amount": 30 + }, + { + "resource": "Desc_Cement_C", + "amount": 45 + } + ] + }, + { + "id": "Build_ResourceSinkShop_C", + "name": "AWESOME Shop", + "index": 57, + "description": "Redeem your FICSIT Coupons here! \r\nFor those employees going the extra kilometer we have set aside special bonus milestones and rewards! Get your Coupons in the AWESOME Sink program now!\r\n\r\n*No refunds possible.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 4, + "length": 6, + "height": 5 + }, + "imagePath": "/images/game/resource-sink-shop_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronScrew_C", + "amount": 200 + }, + { + "resource": "Desc_IronPlate_C", + "amount": 10 + }, + { + "resource": "Desc_Cable_C", + "amount": 10 + } + ] + }, + { + "id": "Build_FrackingSmasher_C", + "name": "Resource Well Pressurizer", + "index": 58, + "description": "Activates a Resource Well by pressurizing the underground resource. Must be placed on a Resource Well.\r\nOnce activated, Resource Well Extractors can be placed on the surrounding sub-nodes to extract the resource.\r\nRequires power. Overclocking increases the output potential of the entire Resource Well.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 150, + "powerConsumption": 150, + "powerConsumptionExponent": 1.321929, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 15, + "length": 15, + "height": 22.5 + }, + "imagePath": "/images/game/smasher_256.png", + "conveyor": null, + "pipeline": null, + "extractor": { + "type": "None", + "allowedForms": [ + "Liquid", + "Gas", + "Heat" + ], + "allowedResources": [ + "Desc_LiquidOil_C", + "Desc_NitrogenGas_C", + "Desc_Water_C" + ], + "itemsPerCycle": null, + "cycleTime": null, + "itemsPerMinute": null + }, + "buildCost": [ + { + "resource": "Desc_ModularFrameLightweight_C", + "amount": 10 + }, + { + "resource": "Desc_ModularFrameHeavy_C", + "amount": 25 + }, + { + "resource": "Desc_Motor_C", + "amount": 50 + }, + { + "resource": "Desc_AluminumPlate_C", + "amount": 50 + }, + { + "resource": "Desc_Rubber_C", + "amount": 100 + } + ] + }, + { + "id": "Build_DroneStation_C", + "name": "Drone Port", + "index": 59, + "description": "Functions as home Port to a single Drone, which transports available input back and forth between its home Port and destination Port.\r\nDrone Ports can have one other Port assigned as their transport destination.\r\n\r\nThe Drone Port interface provides delivery details and allows management of Port connections.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 100, + "powerConsumption": 100, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 24, + "length": 24, + "height": 3.5 + }, + "imagePath": "/images/game/drone-port_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrameHeavy_C", + "amount": 20 + }, + { + "resource": "Desc_HighSpeedConnector_C", + "amount": 20 + }, + { + "resource": "Desc_AluminumPlate_C", + "amount": 50 + }, + { + "resource": "Desc_AluminumCasing_C", + "amount": 50 + }, + { + "resource": "Desc_ModularFrameLightweight_C", + "amount": 10 + } + ] + }, + { + "id": "Build_ConveyorLiftMk1_C", + "name": "Conveyor Lift Mk.1", + "index": 60, + "description": "Transports up to 60 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-1_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorLiftMk5_C", + "name": "Conveyor Lift Mk.5", + "index": 61, + "description": "Transports up to 780 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-5_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_AluminumPlate_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorLiftMk6_C", + "name": "Conveyor Lift Mk.6", + "index": 62, + "description": "Transports up to 1200 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-6_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_FicsiteMesh_C", + "amount": 2 + }, + { + "resource": "Desc_TimeCrystal_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorLiftMk4_C", + "name": "Conveyor Lift Mk.4", + "index": 63, + "description": "Transports up to 480 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-4_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorLiftMk3_C", + "name": "Conveyor Lift Mk.3", + "index": 64, + "description": "Transports up to 270 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-3_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlate_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorLiftMk2_C", + "name": "Conveyor Lift Mk.2", + "index": 65, + "description": "Transports up to 120 resources per minute. Used to move resources between floors.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/conveyor-lift-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlateReinforced_C", + "amount": 2 + } + ] + }, + { + "id": "Build_Portal_C", + "name": "Main Portal", + "index": 66, + "description": "Enables pioneer teleportation between two linked Portals.\r\n\r\nEach Portal can only have a single link. \r\nA link can only be made between a Main and Satellite Portal.\r\n\r\nSingularity Cells need to be supplied to the Main Portal and will be consumed to establish and maintain the Portal link.\r\n\r\nWARNINGS:\r\n- Massive power draw will occur during start-up and usage.\r\n- Power draw on use increases with travel distance.\r\n- Link start-up will take time. Letting the connection expire is not recommended.\r\n- Failure to deliver sufficient Singularity Cells will cause the link to expire.\r\n- FICSIT does not condone the use of wormhole technology. Any usage of wormhole technology is at the user's own risk.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 250, + "powerConsumption": 250, + "powerConsumptionExponent": 1.321929, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 8, + "length": 8, + "height": 12 + }, + "imagePath": "/images/game/portal_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_MotorLightweight_C", + "amount": 5 + }, + { + "resource": "Desc_ModularFrameLightweight_C", + "amount": 10 + }, + { + "resource": "Desc_QuantumOscillator_C", + "amount": 15 + }, + { + "resource": "Desc_SAMFluctuator_C", + "amount": 25 + }, + { + "resource": "Desc_FicsiteMesh_C", + "amount": 50 + } + ] + }, + { + "id": "Build_PortalSatellite_C", + "name": "Satellite Portal", + "index": 67, + "description": "Enables pioneer teleportation between two linked Portals.\r\n\r\nEach Portal can only have a single link. \r\nA link can only be made between a Main and Satellite Portal.\r\n\r\nSingularity Cells need to be supplied to the Main Portal and will be consumed to establish and maintain the Portal link.\r\n\r\nWARNINGS:\r\n- Massive power draw will occur during start-up and usage.\r\n- Power draw on use increases with travel distance.\r\n- Link start-up will take time. Letting the connection expire is not recommended.\r\n- Failure to deliver sufficient Singularity Cells will cause the link to expire.\r\n- FICSIT does not condone the use of wormhole technology. Any usage of wormhole technology is at the user's own risk.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 250, + "powerConsumption": 250, + "powerConsumptionExponent": 1.321929, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 8, + "length": 8, + "height": 10 + }, + "imagePath": "/images/game/portal-satellite_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrameLightweight_C", + "amount": 5 + }, + { + "resource": "Desc_QuantumOscillator_C", + "amount": 10 + }, + { + "resource": "Desc_SAMFluctuator_C", + "amount": 25 + }, + { + "resource": "Desc_FicsiteMesh_C", + "amount": 25 + } + ] + }, + { + "id": "Build_BlueprintDesigner_Mk3_C", + "name": "Blueprint Designer Mk.3", + "index": 68, + "description": "The Blueprint Designer is used to create custom factory designs and save them as Blueprints.\r\nBlueprints can be accessed from the Blueprint tab of the Build Menu.\r\n\r\nNote that buildings can only be placed in the Blueprint Designer if they are fully within the boundary frame.\r\n\r\nDimensions: 48 m x 48 m x 48 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/blueprint-designer-mk-3_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_BlueprintDesigner_MK2_C", + "name": "Blueprint Designer Mk.2", + "index": 69, + "description": "The Blueprint Designer is used to create custom factory designs and save them as Blueprints.\r\nBlueprints can be accessed from the Blueprint tab of the Build Menu.\r\n\r\nNote that buildings can only be placed in the Blueprint Designer if they are fully within the boundary frame.\r\n\r\nDimensions: 40 m x 40 m x 40 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/blueprint-designer-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrameHeavy_C", + "amount": 10 + }, + { + "resource": "Desc_Computer_C", + "amount": 20 + }, + { + "resource": "Desc_Rubber_C", + "amount": 100 + }, + { + "resource": "Desc_Cement_C", + "amount": 100 + } + ] + }, + { + "id": "Build_BlueprintDesigner_C", + "name": "Blueprint Designer Mk.1", + "index": 70, + "description": "The Blueprint Designer is used to create custom factory designs and save them as Blueprints.\r\nBlueprints can be accessed from the Blueprint tab of the Build Menu.\r\n\r\nNote that buildings can only be placed in the Blueprint Designer if they are fully within the boundary frame.\r\n\r\nDimensions: 32 m x 32 m x 32 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/blueprint-designer_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrame_C", + "amount": 15 + }, + { + "resource": "Desc_Cable_C", + "amount": 25 + }, + { + "resource": "Desc_Cement_C", + "amount": 100 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 100 + } + ] + }, + { + "id": "Build_RailroadBlockSignal_C", + "name": "Block Signal", + "index": 71, + "description": "Directs the movement of trains to avoid collisions and bottlenecks.\r\n\r\nBlock Signals can be placed on Railways to create 'Blocks' between them. When a train is occupying one of these Blocks, other trains will be unable to enter it.\r\n\r\nCaution: Signals are directional! Trains are unable to move against this direction, so be sure to set up Signals in both directions for bi-directional Railways.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/block-signal_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPipe_C", + "amount": 2 + }, + { + "resource": "Desc_Computer_C", + "amount": 1 + } + ] + }, + { + "id": "Build_RailroadPathSignal_C", + "name": "Path Signal", + "index": 72, + "description": "Directs the movement of trains to avoid collisions and bottlenecks.\r\n\r\nPath Signals are advanced signals that are especially useful for bi-directional Railways and complex intersections. They function similarly to Block Signals, but rather than occupying the entire Block, trains can reserve a specific path through it and will only enter the Block if their path allows them to fully pass through.\r\n\r\nCaution: Signals are directional! Trains are unable to move against this direction, so be sure to set up Signals in both directions for bi-directional Railways.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/path-signal_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPipe_C", + "amount": 2 + }, + { + "resource": "Desc_Computer_C", + "amount": 1 + } + ] + }, + { + "id": "Build_RailroadEndStop_C", + "name": "Buffer Stop", + "index": 73, + "description": "Prevents trains from derailing at the end of a railway.\r\n\r\nWhile automated driving systems have made Buffer Stops mostly obsolete, their use is still recommended to mitigate potential human error.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 6, + "length": 6, + "height": 2.5 + }, + "imagePath": "/images/game/buffer-stop_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPipe_C", + "amount": 2 + }, + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 5 + }, + { + "resource": "Desc_Cement_C", + "amount": 20 + } + ] + }, + { + "id": "Build_TrainDockingStation_C", + "name": "Freight Platform", + "index": 74, + "description": "Loads and unloads Freight Cars that stop at the Freight Platform.\r\nLoading and unloading options can be set by configuring the building.\r\nSnaps to other Platforms and Stations.\r\nNeeds to be connected to a powered Railway to function.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 50, + "powerConsumption": 50, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 34, + "height": 20 + }, + "imagePath": "/images/game/docking-station_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Motor_C", + "amount": 5 + }, + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 10 + }, + { + "resource": "Desc_Plastic_C", + "amount": 25 + }, + { + "resource": "Desc_Cement_C", + "amount": 50 + }, + { + "resource": "Desc_Wire_C", + "amount": 100 + } + ] + }, + { + "id": "Build_TrainDockingStationLiquid_C", + "name": "Fluid Freight Platform", + "index": 75, + "description": "Loads and unloads Freight Cars that stop at the Freight Platform.\r\nLoading and unloading options can be set by configuring the building.\r\nSnaps to other Platforms and Stations.\r\nNeeds to be connected to a powered Railway to function.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 50, + "powerConsumption": 50, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 34, + "height": 20 + }, + "imagePath": "/images/game/train-docking-fluid_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Motor_C", + "amount": 5 + }, + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 10 + }, + { + "resource": "Desc_Plastic_C", + "amount": 25 + }, + { + "resource": "Desc_Cement_C", + "amount": 50 + }, + { + "resource": "Desc_Wire_C", + "amount": 100 + } + ] + }, + { + "id": "Build_TrainPlatformEmpty_C", + "name": "Empty Platform", + "index": 76, + "description": "Creates empty space where necessary.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 34, + "height": 8 + }, + "imagePath": "/images/game/empty-platform_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 10 + }, + { + "resource": "Desc_Cement_C", + "amount": 50 + } + ] + }, + { + "id": "Build_TrainPlatformEmpty_02_C", + "name": "Empty Platform With Catwalk", + "index": 77, + "description": "Creates empty space where necessary.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 34, + "height": 9 + }, + "imagePath": "/images/game/platform-catwalk_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 10 + }, + { + "resource": "Desc_Cement_C", + "amount": 50 + } + ] + }, + { + "id": "Build_TrainStation_C", + "name": "Train Station", + "index": 78, + "description": "Serves as a hub for Locomotives, which can be set to navigate to and stop at a Train Station.\r\nYou can connect power to a Train Station to power up the trains on the Railway as well as feed power to other stations.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 50, + "powerConsumption": 50, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 16, + "length": 34, + "height": 21 + }, + "imagePath": "/images/game/train-station_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 10 + }, + { + "resource": "Desc_Plastic_C", + "amount": 50 + }, + { + "resource": "Desc_Cement_C", + "amount": 50 + }, + { + "resource": "Desc_Wire_C", + "amount": 200 + } + ] + }, + { + "id": "Build_StorageContainerMk2_C", + "name": "Industrial Storage Container", + "index": 79, + "description": "Contains 48 slots for storing large amounts of items.\r\nHas 2 Conveyor Belt input ports and 2 Conveyor Belt output ports.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 5, + "length": 11, + "height": 8 + }, + "imagePath": "/images/game/storage-container-mk-2_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlate_C", + "amount": 20 + }, + { + "resource": "Desc_SteelPipe_C", + "amount": 20 + } + ] + }, + { + "id": "Build_StoragePlayer_C", + "name": "Personal Storage Box", + "index": 80, + "description": "Contains 25 slots for storing large amounts of items.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 1, + "length": 1.6, + "height": 0.8 + }, + "imagePath": "/images/game/player-storage_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 6 + }, + { + "resource": "Desc_IronRod_C", + "amount": 6 + } + ] + }, + { + "id": "Build_StorageMedkit_C", + "name": "Medical Storage Box", + "index": 81, + "description": "Contains 25 slots for storing large amounts of items.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 1, + "length": 1.6, + "height": 0.8 + }, + "imagePath": "/images/game/storage-medkit_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 6 + }, + { + "resource": "Desc_IronRod_C", + "amount": 6 + } + ] + }, + { + "id": "Build_StorageHazard_C", + "name": "Hazard Storage Box", + "index": 82, + "description": "Contains 25 slots for storing large amounts of items.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 1, + "length": 1.6, + "height": 0.8 + }, + "imagePath": "/images/game/storage-hazard_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 6 + }, + { + "resource": "Desc_IronRod_C", + "amount": 6 + } + ] + }, + { + "id": "Build_StorageContainerMk1_C", + "name": "Storage Container", + "index": 83, + "description": "Contains 24 slots for storing large amounts of items.\r\nHas 1 Conveyor Belt input port and 1 Conveyor Belt output port.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 5, + "length": 11, + "height": 4 + }, + "imagePath": "/images/game/storage-container_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 10 + }, + { + "resource": "Desc_IronRod_C", + "amount": 10 + } + ] + }, + { + "id": "Build_HyperTubeJunction_C", + "name": "Hypertube Junction", + "index": 84, + "description": "A three-way Hypertube Junction that allows travel to and from any of its directions.\r\n\r\nCan be built on its own or attached to a Hypertube Support or Junction.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 2, + "length": 3.5, + "height": 2 + }, + "imagePath": "/images/game/hypertube-junction_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 2 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 2 + }, + { + "resource": "Desc_CopperSheet_C", + "amount": 5 + } + ] + }, + { + "id": "Build_HypertubeTJunction_C", + "name": "Hypertube Branch", + "index": 85, + "description": "A one-way Hypertube Branch that allows another Hypertube to split off from an existing path. The branching Hypertube can only be accessed when entering from the longer side.\r\n\r\nCan be built on its own, onto an existing Hypertube, or attached to a Hypertube Support or Junction.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 5, + "length": 2, + "height": 2 + }, + "imagePath": "/images/game/hypertube-branch_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 2 + }, + { + "resource": "Desc_SteelPipe_C", + "amount": 2 + }, + { + "resource": "Desc_CopperSheet_C", + "amount": 5 + } + ] + }, + { + "id": "Build_PipeHyper_C", + "name": "Hypertube", + "index": 86, + "description": "Transports FICSIT employees.\r\nA Hypertube system cannot be powered up or used until a Hypertube Entrance is attached.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/hyper-tube_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_CopperSheet_C", + "amount": 1 + }, + { + "resource": "Desc_SteelPipe_C", + "amount": 1 + } + ] + }, + { + "id": "Build_PowerStorageMk1_C", + "name": "Power Storage", + "index": 87, + "description": "Connects to a power grid to store excess power produced. The stored power can be harnessed if power grid consumption exceeds production.\r\n\r\nStorage Capacity: 100 MWh (100 MW for 1 hour)\r\nMaximum Charge Rate: 100 MW\r\nMaximum Discharge Rate: Unlimited", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 6, + "length": 6, + "height": 12 + }, + "imagePath": "/images/game/power-storage_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 5 + }, + { + "resource": "Desc_ModularFrame_C", + "amount": 10 + }, + { + "resource": "Desc_Wire_C", + "amount": 100 + } + ] + }, + { + "id": "Build_TruckStation_C", + "name": "Truck Station", + "index": 88, + "description": "Sends or receives resources to/from vehicles.\r\n\r\nHas 48 inventory slots.\r\n\r\nTransfers up to 120 stacks per minute to/from docked vehicles. \r\nAlways refuels vehicles if it has access to a matching fuel type.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 20, + "powerConsumption": 20, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 17, + "length": 9, + "height": 6.2 + }, + "imagePath": "/images/game/truck-station_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrame_C", + "amount": 15 + }, + { + "resource": "Desc_Rotor_C", + "amount": 20 + }, + { + "resource": "Desc_Cable_C", + "amount": 50 + } + ] + }, + { + "id": "Build_JumpPadAdjustable_C", + "name": "Jump Pad", + "index": 89, + "description": "Launches pioneers for quick, vertical traversal.\r\nThe launch angle can be adjusted while building.\r\nCaution: Be sure to land safely!", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 5, + "powerConsumption": 5, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 6, + "length": 6, + "height": 3 + }, + "imagePath": "/images/game/jump-pad_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Rotor_C", + "amount": 2 + }, + { + "resource": "Desc_IronPlate_C", + "amount": 15 + }, + { + "resource": "Desc_Cable_C", + "amount": 10 + } + ] + }, + { + "id": "Build_Mam_C", + "name": "MAM", + "index": 90, + "description": "The Molecular Analysis Machine is used to analyze new and exotic materials found on alien planets.\r\nThrough the MAM, R&D will assist pioneers in turning any valuable data into usable research options and new technologies.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 6, + "length": 9, + "height": 5 + }, + "imagePath": "/images/game/mam_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlateReinforced_C", + "amount": 5 + }, + { + "resource": "Desc_Cable_C", + "amount": 15 + }, + { + "resource": "Desc_Wire_C", + "amount": 45 + } + ] + }, + { + "id": "Build_ConveyorAttachmentMerger_C", + "name": "Conveyor Merger", + "index": 91, + "description": "Merges up to three Conveyor Belts into one.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 4, + "length": 4, + "height": 2.6 + }, + "imagePath": "/images/game/conveyor-merger_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 2 + }, + { + "resource": "Desc_IronRod_C", + "amount": 2 + } + ] + }, + { + "id": "Build_ConveyorAttachmentSplitter_C", + "name": "Conveyor Splitter", + "index": 92, + "description": "Splits one Conveyor Belt into two or three. \r\nUseful for diverting parts and resources away from backlogged Conveyor Belts.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 4, + "length": 4, + "height": 2.6 + }, + "imagePath": "/images/game/conveyor-splitter_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 2 + }, + { + "resource": "Desc_Cable_C", + "amount": 2 + } + ] + }, + { + "id": "Build_AlienPowerBuilding_C", + "name": "Alien Power Augmenter", + "index": 93, + "description": "Generates power based on the total amount of power on the attached power grid.\r\n\r\nThis experimental technology is somehow able to extract power from the Somersloop by blasting it with energy.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 30, + "length": 30, + "height": 25 + }, + "imagePath": "/images/game/alien-power-augmenter_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_WAT1_C", + "amount": 10 + }, + { + "resource": "Desc_SAMFluctuator_C", + "amount": 50 + }, + { + "resource": "Desc_Cable_C", + "amount": 100 + }, + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 50 + }, + { + "resource": "Desc_Motor_C", + "amount": 25 + }, + { + "resource": "Desc_Computer_C", + "amount": 10 + } + ] + }, + { + "id": "Build_ConveyorAttachmentSplitterProgrammable_C", + "name": "Programmable Splitter", + "index": 94, + "description": "Splits one Conveyor Belt into two or three. \r\nMultiple filters can be set for each output to allow specific parts to pass through.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 4, + "length": 4, + "height": 2.6 + }, + "imagePath": "/images/game/programmable-splitter_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_ModularFrameHeavy_C", + "amount": 1 + }, + { + "resource": "Desc_Computer_C", + "amount": 2 + }, + { + "resource": "Desc_CircuitBoardHighSpeed_C", + "amount": 5 + } + ] + }, + { + "id": "Build_ConveyorAttachmentSplitterSmart_C", + "name": "Smart Splitter", + "index": 95, + "description": "Splits one Conveyor Belt into two or three.\r\nA filter can be set for each output to allow a specific part to pass through.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 4, + "length": 4, + "height": 2.6 + }, + "imagePath": "/images/game/smart-splitter_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlateReinforced_C", + "amount": 2 + }, + { + "resource": "Desc_Rotor_C", + "amount": 2 + }, + { + "resource": "Desc_CircuitBoardHighSpeed_C", + "amount": 1 + } + ] + }, + { + "id": "Build_PriorityPowerSwitch_C", + "name": "Priority Power Switch", + "index": 96, + "description": "Priority Power Switches can be ranked by priority. When power production is too low, Switches will start turning off automatically until the power stabilizes, starting with Priority Group 8.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 2, + "length": 1, + "height": 3 + }, + "imagePath": "/images/game/smart-switch_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_HighSpeedConnector_C", + "amount": 2 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 6 + }, + { + "resource": "Desc_HighSpeedWire_C", + "amount": 50 + } + ] + }, + { + "id": "Build_PowerSwitch_C", + "name": "Power Switch", + "index": 97, + "description": "Enables/disables the connection between 2 power grids when switched ON/OFF.\r\n\r\nNote the A and B connector labels.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 2, + "length": 1, + "height": 3 + }, + "imagePath": "/images/game/power-switch_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_HighSpeedWire_C", + "amount": 20 + }, + { + "resource": "Desc_SteelPlate_C", + "amount": 4 + }, + { + "resource": "Desc_CircuitBoardHighSpeed_C", + "amount": 1 + } + ] + }, + { + "id": "Build_ConveyorAttachmentMergerPriority_C", + "name": "Priority Merger", + "index": 98, + "description": "Merges up to three Conveyor Belts into one.\r\nInputs can be prioritized to always be merged before others.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 4, + "length": 4, + "height": 2.6 + }, + "imagePath": "/images/game/priority-merger_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlateReinforced_C", + "amount": 2 + }, + { + "resource": "Desc_ModularFrame_C", + "amount": 1 + }, + { + "resource": "Desc_CrystalOscillator_C", + "amount": 1 + } + ] + }, + { + "id": "Build_RadarTower_C", + "name": "Radar Tower", + "index": 99, + "description": "Scans the surrounding area to display additional information on the Map.\r\n\r\nInformation revealed on the Map includes:\r\n- Resource node locations\r\n- Terrain data\r\n- Flora & fauna information\r\n- Notable signal readings", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 30, + "powerConsumption": 30, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 10, + "length": 10, + "height": 118 + }, + "imagePath": "/images/game/radar-tower_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Computer_C", + "amount": 10 + }, + { + "resource": "Desc_ModularFrameHeavy_C", + "amount": 20 + }, + { + "resource": "Desc_CrystalOscillator_C", + "amount": 25 + }, + { + "resource": "Desc_Cable_C", + "amount": 100 + } + ] + }, + { + "id": "Build_TreeGiftProducer_C", + "name": "FICSMAS Gift Tree", + "index": 100, + "description": "A festively disguised production building. Reminder: Nothing is ever truly free.\r\nProduces 15 Gifts per minute.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 8, + "length": 8, + "height": 17 + }, + "imagePath": "/images/game/christmas-tree_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Gift_C", + "amount": 50 + }, + { + "resource": "Desc_XmasBranch_C", + "amount": 100 + }, + { + "resource": "Desc_XmasBall1_C", + "amount": 30 + }, + { + "resource": "Desc_XmasBall2_C", + "amount": 30 + } + ] + }, + { + "id": "Build_SpaceElevator_C", + "name": "Space Elevator", + "index": 101, + "description": "Requires deliveries of special Project Parts to complete Project Assembly Phases.\r\nCompleting these Phases will unlock new Tiers in the HUB Terminal.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 0, + "powerConsumption": 0, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 17, + "length": 48, + "height": 35 + }, + "imagePath": "/images/game/space-elevator_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 500 + }, + { + "resource": "Desc_IronPlate_C", + "amount": 250 + }, + { + "resource": "Desc_IronRod_C", + "amount": 400 + }, + { + "resource": "Desc_Wire_C", + "amount": 1500 + } + ] + }, + { + "id": "Build_LookoutTower_C", + "name": "Lookout Tower", + "index": 102, + "description": "Provides a good vantage point to facilitate factory construction.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 1.5 + }, + "imagePath": "/images/game/look-out-tower_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_IronPlate_C", + "amount": 5 + }, + { + "resource": "Desc_IronRod_C", + "amount": 5 + } + ] + }, + { + "id": "Build_PipeHyperStart_C", + "name": "Hypertube Entrance", + "index": 103, + "description": "Powers up a Hypertube system and allows it to be entered.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 10, + "powerConsumption": 10, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": null, + "length": null, + "height": null + }, + "imagePath": "/images/game/hyper-tube-start_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_SteelPlateReinforced_C", + "amount": 4 + }, + { + "resource": "Desc_Rotor_C", + "amount": 4 + }, + { + "resource": "Desc_SteelPipe_C", + "amount": 10 + } + ] + }, + { + "id": "Build_Wall_8x4_01_C", + "name": "Basic Wall (4 m)", + "index": 104, + "description": "Snaps to Foundations and other Walls.\r\nUseful for building multi-floor structures.\r\n\r\nSize: 8 m x 4 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.5, + "length": 8, + "height": 4 + }, + "imagePath": "/images/game/ficsit-wall-8-x-4_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_Wall_Orange_8x1_C", + "name": "Basic Wall (1 m)", + "index": 105, + "description": "Snaps to Foundations and other Walls.\r\nUseful for building multi-floor structures.\r\n\r\nSize: 8 m x 1 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 0.5, + "length": 8, + "height": 1 + }, + "imagePath": "/images/game/ficsit-wall-8-x-1_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 2 + } + ] + }, + { + "id": "Build_JumpPad_C", + "name": "Old Jump Pad", + "index": 106, + "description": "Propels you upwards through the air.\r\nMake sure you land softly.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 2, + "powerConsumption": 2, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 6, + "length": 6, + "height": 6 + }, + "imagePath": "/images/game/jump-pad_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_JumpPadTilted_C", + "name": "Old Tilted Jump Pad", + "index": 107, + "description": "Propels you forwards through the air.\r\nMake sure you land softly.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 2, + "powerConsumption": 2, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 6, + "length": 6, + "height": 6 + }, + "imagePath": "/images/game/jump-pad_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [] + }, + { + "id": "Build_LandingPad_C", + "name": "U-Jelly Landing Pad", + "index": 108, + "description": "Generates a speed-dampening jelly.\r\nGuarantees a safe landing.", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": 5, + "powerConsumption": 5, + "powerConsumptionExponent": 1.6, + "somersloopPowerConsumptionExponent": 2, + "somersloopSlots": 0, + "clearance": { + "width": 10, + "length": 10, + "height": 5 + }, + "imagePath": "/images/game/landing-pad_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Rotor_C", + "amount": 2 + }, + { + "resource": "Desc_Cable_C", + "amount": 20 + }, + { + "resource": "Desc_GenericBiomass_C", + "amount": 200 + } + ] + }, + { + "id": "Build_Foundation_8x1_01_C", + "name": "Foundation (1 m)", + "index": 109, + "description": "Provides a flat floor to build your factory on.\r\n\r\nBuildings on top of the Foundation snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 1 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 1 + }, + "imagePath": "/images/game/ficsit-foundation-1-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } + ] + }, + { + "id": "Build_Foundation_8x2_01_C", + "name": "Foundation (2 m)", + "index": 110, + "description": "Provides a flat floor to build your factory on.\r\n\r\nBuildings on top of the Foundation snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 2 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 2 + }, + "imagePath": "/images/game/ficsit-foundation-2-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } + ] + }, + { + "id": "Build_Foundation_8x4_01_C", + "name": "Foundation (4 m)", + "index": 111, + "description": "Provides a flat floor to build your factory on.\r\n\r\nBuildings on top of the Foundation snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 4 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 4 + }, + "imagePath": "/images/game/ficsit-foundation-4-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } + ] + }, + { + "id": "Build_Ramp_8x1_01_C", + "name": "Ramp (1 m)", + "index": 112, + "description": "Snaps to Foundations and makes it easier to get onto them.\r\n\r\nBuildings on top of the Ramp snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 1 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 1 + }, + "imagePath": "/images/game/ficsit-ramp-1-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } + ] + }, + { + "id": "Build_Ramp_8x2_01_C", + "name": "Ramp (2 m)", + "index": 113, + "description": "Snaps to Foundations and makes it easier to get onto them.\r\n\r\nBuildings on top of the Ramp snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 2 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 2 + }, + "imagePath": "/images/game/ficsit-ramp-2-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } + ] + }, + { + "id": "Build_Ramp_8x4_01_C", + "name": "Ramp (4 m)", + "index": 114, + "description": "Snaps to Foundations and makes it easier to get onto them.\r\n\r\nBuildings on top of the Ramp snap to a grid, making it easier to line them up with each other.\r\n\r\nSize: 8 m x 4 m", + "minimumPowerConsumption": null, + "maximumPowerConsumption": null, + "averagePowerConsumption": null, + "powerConsumption": null, + "powerConsumptionExponent": null, + "somersloopPowerConsumptionExponent": null, + "somersloopSlots": null, + "clearance": { + "width": 8, + "length": 8, + "height": 4 + }, + "imagePath": "/images/game/ficsit-ramp-4-m_256.png", + "conveyor": null, + "pipeline": null, + "extractor": null, + "buildCost": [ + { + "resource": "Desc_Cement_C", + "amount": 5 + } ] } ] \ No newline at end of file diff --git a/src/recipes/FactoryMilestoneOnlyUnlocks.json b/src/recipes/FactoryMilestoneOnlyUnlocks.json new file mode 100644 index 00000000..25e5d947 --- /dev/null +++ b/src/recipes/FactoryMilestoneOnlyUnlocks.json @@ -0,0 +1,98 @@ +[ + { + "script": "Recipe_BladeRunners_C", + "name": "Blade Runners", + "description": "", + "imagePath": "/images/game/sprinting-stilts_256.png" + }, + { + "script": "Recipe_CandyCaneBasher_C", + "name": "Candy Cane Basher", + "description": "", + "imagePath": "/images/game/cane-equipment_256.png" + }, + { + "script": "Recipe_Chainsaw_C", + "name": "Chainsaw", + "description": "", + "imagePath": "/images/game/chainsaw_256.png" + }, + { + "script": "Recipe_FactoryCart_C", + "name": "Factory Cart™", + "description": "", + "imagePath": "/images/game/golf-cart_256.png" + }, + { + "script": "Recipe_Gasmask_C", + "name": "Gas Mask", + "description": "", + "imagePath": "/images/game/gas-mask_256.png" + }, + { + "script": "Recipe_GoldenCart_C", + "name": "Golden Factory Cart™", + "description": "", + "imagePath": "/images/game/golf-cart-gold_256.png" + }, + { + "script": "Recipe_HazmatSuit_C", + "name": "Hazmat Suit", + "description": "", + "imagePath": "/images/game/hazmat-suit_256.png" + }, + { + "script": "Recipe_Hoverpack_C", + "name": "Hoverpack", + "description": "", + "imagePath": "/images/game/hoverpack_256.png" + }, + { + "script": "Recipe_JetPack_C", + "name": "Jetpack", + "description": "", + "imagePath": "/images/game/jetpack_256.png" + }, + { + "script": "Recipe_NobeliskDetonator_C", + "name": "Nobelisk Detonator", + "description": "", + "imagePath": "/images/game/detonator_256.png" + }, + { + "script": "Recipe_ObjectScanner_C", + "name": "Object Scanner", + "description": "", + "imagePath": "/images/game/object-scanner_256.png" + }, + { + "script": "Recipe_RebarGun_C", + "name": "Rebar Gun", + "description": "", + "imagePath": "/images/game/rebar-gun_256.png" + }, + { + "script": "Recipe_SpaceRifleMk1_C", + "name": "Rifle", + "description": "", + "imagePath": "/images/game/rifle-mk-1_256.png" + }, + { + "script": "Recipe_XenoBasher_C", + "name": "Xeno-Basher", + "description": "", + "imagePath": "/images/game/xeno-basher_256.png" + }, + { + "script": "Recipe_XenoZapper_C", + "name": "Xeno-Zapper", + "description": "", + "imagePath": "/images/game/xeno-zapper_256.png" + }, + { + "script": "Recipe_ZipLine_C", + "name": "Zipline", + "description": "", + "imagePath": "/images/game/zipline_256.png" + } +] \ No newline at end of file diff --git a/src/recipes/FactoryRecipe.ts b/src/recipes/FactoryRecipe.ts index 8b0553f3..56b71cbb 100644 --- a/src/recipes/FactoryRecipe.ts +++ b/src/recipes/FactoryRecipe.ts @@ -71,6 +71,12 @@ export function getAllRecipesForItem(item: string) { return recipes; } +export function getAllRecipesForIngredient(item: string) { + return AllFactoryRecipes.filter(r => + r.ingredients.some(i => i.resource === item), + ); +} + export function getRecipeProductPerBuilding( recipe: FactoryRecipe, productId: string, diff --git a/src/recipes/FactoryRecipeId.ts b/src/recipes/FactoryRecipeId.ts index 03a08444..3248e772 100644 --- a/src/recipes/FactoryRecipeId.ts +++ b/src/recipes/FactoryRecipeId.ts @@ -1,2 +1,2 @@ /** Automatically generated */ -export type FactoryRecipeId = 'Recipe_IronPlate_C' | 'Recipe_IronRod_C' | 'Recipe_IngotIron_C' | 'Recipe_Alternate_RocketFuel_Nitro_C' | 'Recipe_RocketFuel_C' | 'Recipe_PackagedRocketFuel_C' | 'Recipe_UnpackageRocketFuel_C' | 'Recipe_Alternate_IonizedFuel_Dark_C' | 'Recipe_DarkEnergy_C' | 'Recipe_QuantumEnergy_C' | 'Recipe_DarkMatter_C' | 'Recipe_SuperpositionOscillator_C' | 'Recipe_TemporalProcessor_C' | 'Recipe_SpaceElevatorPart_12_C' | 'Recipe_IonizedFuel_C' | 'Recipe_PackagedIonizedFuel_C' | 'Recipe_UnpackageIonizedFuel_C' | 'Recipe_Alternate_Diamond_Turbo_C' | 'Recipe_SAMFluctuator_C' | 'Recipe_FicsiteMesh_C' | 'Recipe_FicsiteIngot_Iron_C' | 'Recipe_TimeCrystal_C' | 'Recipe_Diamond_C' | 'Recipe_IngotSAM_C' | 'Recipe_SpaceElevatorPart_10_C' | 'Recipe_FicsiteIngot_AL_C' | 'Recipe_FicsiteIngot_CAT_C' | 'Recipe_Bauxite_Caterium_C' | 'Recipe_Bauxite_Copper_C' | 'Recipe_Caterium_Copper_C' | 'Recipe_Caterium_Quartz_C' | 'Recipe_Coal_Iron_C' | 'Recipe_Coal_Limestone_C' | 'Recipe_Copper_Quartz_C' | 'Recipe_Copper_Sulfur_C' | 'Recipe_Iron_Limestone_C' | 'Recipe_Limestone_Sulfur_C' | 'Recipe_Nitrogen_Bauxite_C' | 'Recipe_Nitrogen_Caterium_C' | 'Recipe_Quartz_Bauxite_C' | 'Recipe_Quartz_Coal_C' | 'Recipe_Sulfur_Coal_C' | 'Recipe_Sulfur_Iron_C' | 'Recipe_Uranium_Bauxite_C' | 'Recipe_Alternate_Turbofuel_C' | 'Recipe_PackagedTurboFuel_C' | 'Recipe_UnpackageTurboFuel_C' | 'Recipe_Alternate_Coal_1_C' | 'Recipe_Alternate_Coal_2_C' | 'Recipe_Alternate_EnrichedCoal_C' | 'Recipe_CircuitBoard_C' | 'Recipe_LiquidFuel_C' | 'Recipe_PetroleumCoke_C' | 'Recipe_Plastic_C' | 'Recipe_Rubber_C' | 'Recipe_ResidualFuel_C' | 'Recipe_ResidualPlastic_C' | 'Recipe_ResidualRubber_C' | 'Recipe_Alternate_Diamond_Pink_C' | 'Recipe_Alternate_Diamond_Petroleum_C' | 'Recipe_Alternate_Diamond_OilBased_C' | 'Recipe_Alternate_Diamond_Cloudy_C' | 'Recipe_Alternate_DarkMatter_Trap_C' | 'Recipe_Alternate_DarkMatter_Crystallization_C' | 'Recipe_Alternate_WetConcrete_C' | 'Recipe_Alternate_TurboHeavyFuel_C' | 'Recipe_Alternate_SteelRod_C' | 'Recipe_SteelBeam_C' | 'Recipe_SteelPipe_C' | 'Recipe_IngotSteel_C' | 'Recipe_SpaceElevatorPart_2_C' | 'Recipe_Alternate_SteelCanister_C' | 'Recipe_FluidCanister_C' | 'Recipe_Fuel_C' | 'Recipe_LiquidBiofuel_C' | 'Recipe_PackagedBiofuel_C' | 'Recipe_PackagedCrudeOil_C' | 'Recipe_PackagedOilResidue_C' | 'Recipe_PackagedWater_C' | 'Recipe_UnpackageBioFuel_C' | 'Recipe_UnpackageFuel_C' | 'Recipe_UnpackageOil_C' | 'Recipe_UnpackageOilResidue_C' | 'Recipe_UnpackageWater_C' | 'Recipe_Alternate_SteamedCopperSheet_C' | 'Recipe_Alternate_RubberConcrete_C' | 'Recipe_Alternate_RecycledRubber_C' | 'Recipe_Alternate_PureQuartzCrystal_C' | 'Recipe_QuartzCrystal_C' | 'Recipe_Alternate_PureIronIngot_C' | 'Recipe_Alternate_PureCopperIngot_C' | 'Recipe_Alternate_PureCateriumIngot_C' | 'Recipe_PureAluminumIngot_C' | 'Recipe_AluminumCasing_C' | 'Recipe_AluminumSheet_C' | 'Recipe_AluminaSolution_C' | 'Recipe_AluminumScrap_C' | 'Recipe_PackagedAlumina_C' | 'Recipe_IngotAluminum_C' | 'Recipe_Silica_C' | 'Recipe_CrystalOscillator_C' | 'Recipe_UnpackageAlumina_C' | 'Recipe_Alternate_PolymerResin_C' | 'Recipe_Alternate_PlasticSmartPlating_C' | 'Recipe_Alternate_HighSpeedWiring_C' | 'Recipe_EncasedIndustrialBeam_C' | 'Recipe_Motor_C' | 'Recipe_Stator_C' | 'Recipe_SpaceElevatorPart_3_C' | 'Recipe_AILimiter_C' | 'Recipe_Alternate_HeavyOilResidue_C' | 'Recipe_Alternate_HeavyFlexibleFrame_C' | 'Recipe_Computer_C' | 'Recipe_ModularFrameHeavy_C' | 'Recipe_SpaceElevatorPart_4_C' | 'Recipe_SpaceElevatorPart_5_C' | 'Recipe_Alternate_FusedWire_C' | 'Recipe_Alternate_FlexibleFramework_C' | 'Recipe_Alternate_ElectrodeCircuitBoard_C' | 'Recipe_Alternate_ElectroAluminumScrap_C' | 'Recipe_Alternate_DilutedPackagedFuel_C' | 'Recipe_Alternate_CopperRotor_C' | 'Recipe_ModularFrame_C' | 'Recipe_Rotor_C' | 'Recipe_CopperSheet_C' | 'Recipe_SpaceElevatorPart_1_C' | 'Recipe_Alternate_CopperAlloyIngot_C' | 'Recipe_Alternate_CokeSteelIngot_C' | 'Recipe_Alternate_CoatedIronPlate_C' | 'Recipe_Alternate_CoatedIronCanister_C' | 'Recipe_Alternate_CoatedCable_C' | 'Recipe_Alternate_BoltedFrame_C' | 'Recipe_Alternate_AdheredIronPlate_C' | 'Recipe_Alternate_TurboPressureMotor_C' | 'Recipe_PlutoniumCell_C' | 'Recipe_PressureConversionCube_C' | 'Recipe_NitricAcid_C' | 'Recipe_NonFissileUranium_C' | 'Recipe_CopperDust_C' | 'Recipe_Plutonium_C' | 'Recipe_PlutoniumFuelRod_C' | 'Recipe_PackagedNitricAcid_C' | 'Recipe_SpaceElevatorPart_9_C' | 'Recipe_UnpackageNitricAcid_C' | 'Recipe_Alternate_TurboBlendFuel_C' | 'Recipe_UraniumCell_C' | 'Recipe_CoolingSystem_C' | 'Recipe_Battery_C' | 'Recipe_ComputerSuper_C' | 'Recipe_RadioControlUnit_C' | 'Recipe_SulfuricAcid_C' | 'Recipe_PackagedSulfuricAcid_C' | 'Recipe_SpaceElevatorPart_7_C' | 'Recipe_HighSpeedConnector_C' | 'Recipe_UnpackageSulfuricAcid_C' | 'Recipe_Alternate_SuperStateComputer_C' | 'Recipe_ElectromagneticControlRod_C' | 'Recipe_NuclearFuelRod_C' | 'Recipe_SpaceElevatorPart_6_C' | 'Recipe_Alternate_SloppyAlumina_C' | 'Recipe_Alternate_RadioControlSystem_C' | 'Recipe_Alternate_PlutoniumFuelUnit_C' | 'Recipe_Alternate_OCSupercomputer_C' | 'Recipe_HeatSink_C' | 'Recipe_FusedModularFrame_C' | 'Recipe_GasTank_C' | 'Recipe_PackagedNitrogen_C' | 'Recipe_UnpackageNitrogen_C' | 'Recipe_Alternate_InstantScrap_C' | 'Recipe_Alternate_InstantPlutoniumCell_C' | 'Recipe_Alternate_HeatFusedFrame_C' | 'Recipe_Alternate_FertileUranium_C' | 'Recipe_Alternate_ElectricMotor_C' | 'Recipe_Alternate_DilutedFuel_C' | 'Recipe_Alternate_CoolingDevice_C' | 'Recipe_Alternate_ClassicBattery_C' | 'Recipe_Alternate_AutomatedMiner_C' | 'Recipe_Alternate_AlcladCasing_C' | 'Recipe_Alternate_SteelPipe_Molded_C' | 'Recipe_Alternate_SteelPipe_Iron_C' | 'Recipe_Alternate_SteelCastedPlate_C' | 'Recipe_Alternate_SteelBeam_Molded_C' | 'Recipe_Alternate_SteelBeam_Aluminum_C' | 'Recipe_Alternate_AluminumRod_C' | 'Recipe_Alternate_AILimiter_Plastic_C' | 'Recipe_Alternate_Silica_Distilled_C' | 'Recipe_Alternate_Quartz_Purified_C' | 'Recipe_Alternate_Quartz_Fused_C' | 'Recipe_Alternate_IronIngot_Leached_C' | 'Recipe_Alternate_IronIngot_Basic_C' | 'Recipe_Alternate_CopperIngot_Tempered_C' | 'Recipe_Alternate_CopperIngot_Leached_C' | 'Recipe_Alternate_CateriumIngot_Tempered_C' | 'Recipe_Alternate_CateriumIngot_Leached_C' | 'Recipe_Alternate_Wire_2_C' | 'Recipe_Alternate_Wire_1_C' | 'Recipe_Alternate_UraniumCell_1_C' | 'Recipe_IngotCaterium_C' | 'Recipe_Alternate_TurboMotor_1_C' | 'Recipe_MotorTurbo_C' | 'Recipe_SpaceElevatorPart_8_C' | 'Recipe_Alternate_Stator_C' | 'Recipe_Alternate_Silica_C' | 'Recipe_Alternate_Screw_2_C' | 'Recipe_Alternate_Screw_C' | 'Recipe_Alternate_Rotor_C' | 'Recipe_Alternate_EncasedIndustrialBeam_C' | 'Recipe_Alternate_ReinforcedIronPlate_2_C' | 'Recipe_Alternate_ReinforcedIronPlate_1_C' | 'Recipe_Alternate_RadioControlUnit_1_C' | 'Recipe_Alternate_Quickwire_C' | 'Recipe_Alternate_Plastic_1_C' | 'Recipe_Alternate_NuclearFuelRod_1_C' | 'Recipe_Alternate_Motor_1_C' | 'Recipe_Alternate_ModularFrame_C' | 'Recipe_Alternate_IngotSteel_2_C' | 'Recipe_Alternate_IngotSteel_1_C' | 'Recipe_Alternate_IngotIron_C' | 'Recipe_Alternate_HighSpeedConnector_C' | 'Recipe_Alternate_ModularFrameHeavy_C' | 'Recipe_Alternate_HeatSink_1_C' | 'Recipe_Alternate_Gunpowder_1_C' | 'Recipe_Alternate_ElectromagneticControlRod_1_C' | 'Recipe_Alternate_CrystalOscillator_C' | 'Recipe_Alternate_Concrete_C' | 'Recipe_Alternate_Computer_2_C' | 'Recipe_Alternate_Computer_1_C' | 'Recipe_Alternate_CircuitBoard_2_C' | 'Recipe_Alternate_CircuitBoard_1_C' | 'Recipe_Alternate_Cable_2_C' | 'Recipe_Alternate_Cable_1_C' | 'Recipe_Ficsonium_C' | 'Recipe_FicsoniumFuelRod_C' | 'Recipe_SingularityCell_C' | 'Recipe_SpaceElevatorPart_11_C' | 'Recipe_FilterHazmat_C' | 'Recipe_Quickwire_C' | 'Recipe_Biofuel_C' | 'Recipe_Protein_Hog_C' | 'Recipe_Protein_Spitter_C' | 'Recipe_Biomass_Mycelia_C' | 'Recipe_PowerCrystalShard_1_C' | 'Recipe_Gunpowder_C' | 'Recipe_Protein_Stinger_C' | 'Recipe_Protein_Crab_C' | 'Recipe_AlienDNACapsule_C' | 'Recipe_Biomass_AlienProtein_C' | 'Recipe_SpikedRebar_C' | 'Recipe_AlienPowerFuel_C' | 'Recipe_CartridgeSmart_C' | 'Recipe_Rebar_Stunshot_C' | 'Recipe_FilterGasMask_C' | 'Recipe_NobeliskGas_C' | 'Recipe_Alternate_PolyesterFabric_C' | 'Recipe_Fabric_C' | 'Recipe_SyntheticPowerShard_C' | 'Recipe_PowerCrystalShard_3_C' | 'Recipe_PowerCrystalShard_2_C' | 'Recipe_NobeliskShockwave_C' | 'Recipe_Rebar_Spreadshot_C' | 'Recipe_CartridgeChaos_Packaged_C' | 'Recipe_CartridgeChaos_C' | 'Recipe_NobeliskNuke_C' | 'Recipe_Cartridge_C' | 'Recipe_Rebar_Explosive_C' | 'Recipe_NobeliskCluster_C' | 'Recipe_Nobelisk_C' | 'Recipe_GunpowderMK2_C' | 'Recipe_XmasWreath_C' | 'Recipe_Snowball_C' | 'Recipe_XmasStar_C' | 'Recipe_XmasBall3_C' | 'Recipe_XmasBall4_C' | 'Recipe_XmasBallCluster_C' | 'Recipe_XmasBall1_C' | 'Recipe_XmasBall2_C' | 'Recipe_Snow_C' | 'Recipe_XmasBranch_C' | 'Recipe_XmasBow_C' | 'Recipe_CandyCane_C' | 'Recipe_Biomass_Leaves_C' | 'Recipe_Biomass_Wood_C' | 'Recipe_IronPlateReinforced_C' | 'Recipe_Concrete_C' | 'Recipe_Screw_C' | 'Recipe_Cable_C' | 'Recipe_Wire_C' | 'Recipe_IngotCopper_C' | 'Recipe_Fireworks_01_C' | 'Recipe_Fireworks_02_C' | 'Recipe_Fireworks_03_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_Coal_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_CompactedCoal_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_PetroleumCoke_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidTurboFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidBiofuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_RocketFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_IonizedFuel_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Leaves_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Wood_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Mycelia_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_GenericBiomass_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Biofuel_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_PackagedBiofuel_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_NuclearFuelRod_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_PlutoniumFuelRod_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_FicsoniumFuelRod_C'; +export type FactoryRecipeId = 'Recipe_IronPlate_C' | 'Recipe_IronRod_C' | 'Recipe_IngotIron_C' | 'Recipe_Alternate_RocketFuel_Nitro_C' | 'Recipe_RocketFuel_C' | 'Recipe_PackagedRocketFuel_C' | 'Recipe_UnpackageRocketFuel_C' | 'Recipe_Alternate_IonizedFuel_Dark_C' | 'Recipe_DarkEnergy_C' | 'Recipe_QuantumEnergy_C' | 'Recipe_DarkMatter_C' | 'Recipe_SuperpositionOscillator_C' | 'Recipe_TemporalProcessor_C' | 'Recipe_SpaceElevatorPart_12_C' | 'Recipe_IonizedFuel_C' | 'Recipe_PackagedIonizedFuel_C' | 'Recipe_UnpackageIonizedFuel_C' | 'Recipe_Alternate_Diamond_Turbo_C' | 'Recipe_SAMFluctuator_C' | 'Recipe_FicsiteMesh_C' | 'Recipe_FicsiteIngot_Iron_C' | 'Recipe_TimeCrystal_C' | 'Recipe_Diamond_C' | 'Recipe_IngotSAM_C' | 'Recipe_SpaceElevatorPart_10_C' | 'Recipe_FicsiteIngot_AL_C' | 'Recipe_FicsiteIngot_CAT_C' | 'Recipe_Bauxite_Caterium_C' | 'Recipe_Bauxite_Copper_C' | 'Recipe_Caterium_Copper_C' | 'Recipe_Caterium_Quartz_C' | 'Recipe_Coal_Iron_C' | 'Recipe_Coal_Limestone_C' | 'Recipe_Copper_Quartz_C' | 'Recipe_Copper_Sulfur_C' | 'Recipe_Iron_Limestone_C' | 'Recipe_Limestone_Sulfur_C' | 'Recipe_Nitrogen_Bauxite_C' | 'Recipe_Nitrogen_Caterium_C' | 'Recipe_Quartz_Bauxite_C' | 'Recipe_Quartz_Coal_C' | 'Recipe_Sulfur_Coal_C' | 'Recipe_Sulfur_Iron_C' | 'Recipe_Uranium_Bauxite_C' | 'Recipe_Alternate_Turbofuel_C' | 'Recipe_PackagedTurboFuel_C' | 'Recipe_UnpackageTurboFuel_C' | 'Recipe_Alternate_Coal_1_C' | 'Recipe_Alternate_Coal_2_C' | 'Recipe_Alternate_EnrichedCoal_C' | 'Recipe_CircuitBoard_C' | 'Recipe_LiquidFuel_C' | 'Recipe_PetroleumCoke_C' | 'Recipe_Plastic_C' | 'Recipe_Rubber_C' | 'Recipe_ResidualFuel_C' | 'Recipe_ResidualPlastic_C' | 'Recipe_ResidualRubber_C' | 'Recipe_Alternate_Diamond_Pink_C' | 'Recipe_Alternate_Diamond_Petroleum_C' | 'Recipe_Alternate_Diamond_OilBased_C' | 'Recipe_Alternate_Diamond_Cloudy_C' | 'Recipe_Alternate_DarkMatter_Trap_C' | 'Recipe_Alternate_DarkMatter_Crystallization_C' | 'Recipe_Alternate_WetConcrete_C' | 'Recipe_Alternate_TurboHeavyFuel_C' | 'Recipe_Alternate_SteelRod_C' | 'Recipe_SteelBeam_C' | 'Recipe_SteelPipe_C' | 'Recipe_IngotSteel_C' | 'Recipe_SpaceElevatorPart_2_C' | 'Recipe_Alternate_SteelCanister_C' | 'Recipe_FluidCanister_C' | 'Recipe_Fuel_C' | 'Recipe_LiquidBiofuel_C' | 'Recipe_PackagedBiofuel_C' | 'Recipe_PackagedCrudeOil_C' | 'Recipe_PackagedOilResidue_C' | 'Recipe_PackagedWater_C' | 'Recipe_UnpackageBioFuel_C' | 'Recipe_UnpackageFuel_C' | 'Recipe_UnpackageOil_C' | 'Recipe_UnpackageOilResidue_C' | 'Recipe_UnpackageWater_C' | 'Recipe_Alternate_SteamedCopperSheet_C' | 'Recipe_Alternate_RubberConcrete_C' | 'Recipe_Alternate_RecycledRubber_C' | 'Recipe_Alternate_PureQuartzCrystal_C' | 'Recipe_QuartzCrystal_C' | 'Recipe_Alternate_PureIronIngot_C' | 'Recipe_Alternate_PureCopperIngot_C' | 'Recipe_Alternate_PureCateriumIngot_C' | 'Recipe_PureAluminumIngot_C' | 'Recipe_AluminumCasing_C' | 'Recipe_AluminumSheet_C' | 'Recipe_AluminaSolution_C' | 'Recipe_AluminumScrap_C' | 'Recipe_PackagedAlumina_C' | 'Recipe_IngotAluminum_C' | 'Recipe_Silica_C' | 'Recipe_CrystalOscillator_C' | 'Recipe_UnpackageAlumina_C' | 'Recipe_Alternate_PolymerResin_C' | 'Recipe_Alternate_PlasticSmartPlating_C' | 'Recipe_Alternate_HighSpeedWiring_C' | 'Recipe_EncasedIndustrialBeam_C' | 'Recipe_Motor_C' | 'Recipe_Stator_C' | 'Recipe_SpaceElevatorPart_3_C' | 'Recipe_AILimiter_C' | 'Recipe_Alternate_HeavyOilResidue_C' | 'Recipe_Alternate_HeavyFlexibleFrame_C' | 'Recipe_Computer_C' | 'Recipe_ModularFrameHeavy_C' | 'Recipe_SpaceElevatorPart_4_C' | 'Recipe_SpaceElevatorPart_5_C' | 'Recipe_Alternate_FusedWire_C' | 'Recipe_Alternate_FlexibleFramework_C' | 'Recipe_Alternate_ElectrodeCircuitBoard_C' | 'Recipe_Alternate_ElectroAluminumScrap_C' | 'Recipe_Alternate_DilutedPackagedFuel_C' | 'Recipe_Alternate_CopperRotor_C' | 'Recipe_ModularFrame_C' | 'Recipe_Rotor_C' | 'Recipe_CopperSheet_C' | 'Recipe_SpaceElevatorPart_1_C' | 'Recipe_Alternate_CopperAlloyIngot_C' | 'Recipe_Alternate_CokeSteelIngot_C' | 'Recipe_Alternate_CoatedIronPlate_C' | 'Recipe_Alternate_CoatedIronCanister_C' | 'Recipe_Alternate_CoatedCable_C' | 'Recipe_Alternate_BoltedFrame_C' | 'Recipe_Alternate_AdheredIronPlate_C' | 'Recipe_Alternate_TurboPressureMotor_C' | 'Recipe_PlutoniumCell_C' | 'Recipe_PressureConversionCube_C' | 'Recipe_NitricAcid_C' | 'Recipe_NonFissileUranium_C' | 'Recipe_CopperDust_C' | 'Recipe_Plutonium_C' | 'Recipe_PlutoniumFuelRod_C' | 'Recipe_PackagedNitricAcid_C' | 'Recipe_SpaceElevatorPart_9_C' | 'Recipe_UnpackageNitricAcid_C' | 'Recipe_Alternate_TurboBlendFuel_C' | 'Recipe_UraniumCell_C' | 'Recipe_CoolingSystem_C' | 'Recipe_Battery_C' | 'Recipe_ComputerSuper_C' | 'Recipe_RadioControlUnit_C' | 'Recipe_SulfuricAcid_C' | 'Recipe_PackagedSulfuricAcid_C' | 'Recipe_SpaceElevatorPart_7_C' | 'Recipe_HighSpeedConnector_C' | 'Recipe_UnpackageSulfuricAcid_C' | 'Recipe_Alternate_SuperStateComputer_C' | 'Recipe_ElectromagneticControlRod_C' | 'Recipe_NuclearFuelRod_C' | 'Recipe_SpaceElevatorPart_6_C' | 'Recipe_Alternate_SloppyAlumina_C' | 'Recipe_Alternate_RadioControlSystem_C' | 'Recipe_Alternate_PlutoniumFuelUnit_C' | 'Recipe_Alternate_OCSupercomputer_C' | 'Recipe_HeatSink_C' | 'Recipe_FusedModularFrame_C' | 'Recipe_GasTank_C' | 'Recipe_PackagedNitrogen_C' | 'Recipe_UnpackageNitrogen_C' | 'Recipe_Alternate_InstantScrap_C' | 'Recipe_Alternate_InstantPlutoniumCell_C' | 'Recipe_Alternate_HeatFusedFrame_C' | 'Recipe_Alternate_FertileUranium_C' | 'Recipe_Alternate_ElectricMotor_C' | 'Recipe_Alternate_DilutedFuel_C' | 'Recipe_Alternate_CoolingDevice_C' | 'Recipe_Alternate_ClassicBattery_C' | 'Recipe_Alternate_AutomatedMiner_C' | 'Recipe_Alternate_AlcladCasing_C' | 'Recipe_Alternate_SteelPipe_Molded_C' | 'Recipe_Alternate_SteelPipe_Iron_C' | 'Recipe_Alternate_SteelCastedPlate_C' | 'Recipe_Alternate_SteelBeam_Molded_C' | 'Recipe_Alternate_SteelBeam_Aluminum_C' | 'Recipe_Alternate_AluminumRod_C' | 'Recipe_Alternate_AILimiter_Plastic_C' | 'Recipe_Alternate_Silica_Distilled_C' | 'Recipe_Alternate_Quartz_Purified_C' | 'Recipe_Alternate_Quartz_Fused_C' | 'Recipe_Alternate_IronIngot_Leached_C' | 'Recipe_Alternate_IronIngot_Basic_C' | 'Recipe_Alternate_CopperIngot_Tempered_C' | 'Recipe_Alternate_CopperIngot_Leached_C' | 'Recipe_Alternate_CateriumIngot_Tempered_C' | 'Recipe_Alternate_CateriumIngot_Leached_C' | 'Recipe_Alternate_Wire_2_C' | 'Recipe_Alternate_Wire_1_C' | 'Recipe_Alternate_UraniumCell_1_C' | 'Recipe_IngotCaterium_C' | 'Recipe_Alternate_TurboMotor_1_C' | 'Recipe_MotorTurbo_C' | 'Recipe_SpaceElevatorPart_8_C' | 'Recipe_Alternate_Stator_C' | 'Recipe_Alternate_Silica_C' | 'Recipe_Alternate_Screw_2_C' | 'Recipe_Alternate_Screw_C' | 'Recipe_Alternate_Rotor_C' | 'Recipe_Alternate_EncasedIndustrialBeam_C' | 'Recipe_Alternate_ReinforcedIronPlate_2_C' | 'Recipe_Alternate_ReinforcedIronPlate_1_C' | 'Recipe_Alternate_RadioControlUnit_1_C' | 'Recipe_Alternate_Quickwire_C' | 'Recipe_Alternate_Plastic_1_C' | 'Recipe_Alternate_NuclearFuelRod_1_C' | 'Recipe_Alternate_Motor_1_C' | 'Recipe_Alternate_ModularFrame_C' | 'Recipe_Alternate_IngotSteel_2_C' | 'Recipe_Alternate_IngotSteel_1_C' | 'Recipe_Alternate_IngotIron_C' | 'Recipe_Alternate_HighSpeedConnector_C' | 'Recipe_Alternate_ModularFrameHeavy_C' | 'Recipe_Alternate_HeatSink_1_C' | 'Recipe_Alternate_Gunpowder_1_C' | 'Recipe_Alternate_ElectromagneticControlRod_1_C' | 'Recipe_Alternate_CrystalOscillator_C' | 'Recipe_Alternate_Concrete_C' | 'Recipe_Alternate_Computer_2_C' | 'Recipe_Alternate_Computer_1_C' | 'Recipe_Alternate_CircuitBoard_2_C' | 'Recipe_Alternate_CircuitBoard_1_C' | 'Recipe_Alternate_Cable_2_C' | 'Recipe_Alternate_Cable_1_C' | 'Recipe_Ficsonium_C' | 'Recipe_FicsoniumFuelRod_C' | 'Recipe_SingularityCell_C' | 'Recipe_SpaceElevatorPart_11_C' | 'Recipe_FilterHazmat_C' | 'Recipe_Quickwire_C' | 'Recipe_Biofuel_C' | 'Recipe_Protein_Hog_C' | 'Recipe_Protein_Spitter_C' | 'Recipe_Biomass_Mycelia_C' | 'Recipe_PowerCrystalShard_1_C' | 'Recipe_Gunpowder_C' | 'Recipe_Protein_Stinger_C' | 'Recipe_Protein_Crab_C' | 'Recipe_AlienDNACapsule_C' | 'Recipe_Biomass_AlienProtein_C' | 'Recipe_SpikedRebar_C' | 'Recipe_AlienPowerFuel_C' | 'Recipe_CartridgeSmart_C' | 'Recipe_Rebar_Stunshot_C' | 'Recipe_FilterGasMask_C' | 'Recipe_NobeliskGas_C' | 'Recipe_Alternate_PolyesterFabric_C' | 'Recipe_Fabric_C' | 'Recipe_SyntheticPowerShard_C' | 'Recipe_PowerCrystalShard_3_C' | 'Recipe_PowerCrystalShard_2_C' | 'Recipe_NobeliskShockwave_C' | 'Recipe_Rebar_Spreadshot_C' | 'Recipe_CartridgeChaos_Packaged_C' | 'Recipe_CartridgeChaos_C' | 'Recipe_NobeliskNuke_C' | 'Recipe_Cartridge_C' | 'Recipe_Rebar_Explosive_C' | 'Recipe_NobeliskCluster_C' | 'Recipe_Nobelisk_C' | 'Recipe_GunpowderMK2_C' | 'Recipe_XmasWreath_C' | 'Recipe_Snowball_C' | 'Recipe_XmasStar_C' | 'Recipe_XmasBall3_C' | 'Recipe_XmasBall4_C' | 'Recipe_XmasBallCluster_C' | 'Recipe_XmasBall1_C' | 'Recipe_XmasBall2_C' | 'Recipe_Snow_C' | 'Recipe_XmasBranch_C' | 'Recipe_XmasBow_C' | 'Recipe_CandyCane_C' | 'Recipe_Biomass_Leaves_C' | 'Recipe_Biomass_Wood_C' | 'Recipe_IronPlateReinforced_C' | 'Recipe_Concrete_C' | 'Recipe_Screw_C' | 'Recipe_Cable_C' | 'Recipe_Wire_C' | 'Recipe_IngotCopper_C' | 'Recipe_Fireworks_01_C' | 'Recipe_Fireworks_02_C' | 'Recipe_Fireworks_03_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_Coal_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_CompactedCoal_C' | 'RecipeCustom_Build_GeneratorCoal_C_Desc_PetroleumCoke_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidTurboFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_LiquidBiofuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_RocketFuel_C' | 'RecipeCustom_Build_GeneratorFuel_C_Desc_IonizedFuel_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Leaves_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Wood_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Mycelia_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_GenericBiomass_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_Biofuel_C' | 'RecipeCustom_Build_GeneratorBiomass_Automated_C_Desc_PackagedBiofuel_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_NuclearFuelRod_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_PlutoniumFuelRod_C' | 'RecipeCustom_Build_GeneratorNuclear_C_Desc_FicsoniumFuelRod_C'; \ No newline at end of file diff --git a/src/recipes/WorldCollectibles.json b/src/recipes/WorldCollectibles.json new file mode 100644 index 00000000..cf9a5bdb --- /dev/null +++ b/src/recipes/WorldCollectibles.json @@ -0,0 +1,17980 @@ +[ + { + "id": "BP_TapePickup1", + "type": "audioTape", + "classPath": "BP_TapePickup_C", + "x": 194667, + "y": -187625, + "z": 26092, + "rotation": 170, + "schematicId": "Schematic_Huntdown_C" + }, + { + "id": "BP_TapePickup2_11", + "type": "audioTape", + "classPath": "BP_TapePickup_C", + "x": -37230, + "y": -60710, + "z": 15850, + "schematicId": "Schematic_SongsOfConquest_C" + }, + { + "id": "BP_TapePickup3", + "type": "audioTape", + "classPath": "BP_TapePickup_C", + "x": -125140, + "y": 168530, + "z": -6120, + "rotation": -100, + "schematicId": "Schematic_DeepRockGalactic_C" + }, + { + "id": "BP_UnlockPickup_Customization_C_UAID_40B076DF2F796CF701_1075566000", + "type": "customizationUnlock", + "classPath": "BP_UnlockPickup_Customization_C", + "x": -160356, + "y": 66416, + "z": 3980, + "rotation": 113.85, + "schematicId": "Schematic_Helmet_Beta_C" + }, + { + "id": "BP_DropPod1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "F56B6167-42EA4384-D4A06CA6-408C81C0", + "x": 87740, + "y": -62975, + "z": 13444, + "rotation": 168.33 + }, + { + "id": "BP_DropPod10", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A32748D3-4DE139EC-915B0EAA-E8AF78D6", + "x": 232138, + "y": 27191, + "z": -1629, + "rotation": -160.47, + "unlockCost": [ + { + "item": "Desc_QuartzCrystal_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod11_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "0A292B39-43E65E30-FA22BF9F-994F6910", + "x": -238334, + "y": 17322, + "z": 19741, + "rotation": -117.73, + "unlockCost": [ + { + "item": "Desc_QuartzCrystal_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod12", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "C30F1959-45EAB754-C282E5A6-8FABAB59", + "x": -4707, + "y": -76302, + "z": 13619, + "rotation": -0.29, + "unlockCost": [ + { + "item": "Desc_Gunpowder_C", + "amount": 2 + } + ] + }, + { + "id": "BP_DropPod13", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "11025427-4F16D236-29E381B7-D71ED6DB", + "x": 55248, + "y": -51316, + "z": 14363, + "rotation": 39.1, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod14_389", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B0C84A6E-4EEE42C9-0DA51D88-A74CFC87", + "x": -43144, + "y": 145820, + "z": 7472, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod15_821", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "0CF9A6C4-43915886-2B02B782-A7F2E507", + "x": -189258, + "y": 116331, + "z": -1764, + "rotation": -116.26 + }, + { + "id": "BP_DropPod16_4595", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E1E2FB56-420E7683-9FFEDBA3-1B712545", + "x": 30384, + "y": 266976, + "z": -987, + "rotation": -149.14 + }, + { + "id": "BP_DropPod17_7892", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "31B1956B-49EDCEF9-88FB7FB5-F56C9996", + "x": -108813, + "y": 214051, + "z": 3201, + "rotation": -113.7, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod18", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "DD489163-4A38309B-C97C5E98-16F75F4A", + "x": -259577, + "y": 105049, + "z": -1548, + "rotation": -87.68 + }, + { + "id": "BP_DropPod19", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9A2C2EAF-4C5E8009-25937F90-9144AED3", + "x": 8680, + "y": -41778, + "z": 13053, + "rotation": 175.24, + "unlockCost": [ + { + "item": "Desc_Rubber_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod1_0", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "650DD44A-49947CE5-8776EBA0-5015500E", + "x": -89284, + "y": -50630, + "z": 16019, + "rotation": -17.73 + }, + { + "id": "BP_DropPod1_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B0B13242-4155BB00-895D86A8-D738208B", + "x": 293299, + "y": 52, + "z": 523, + "rotation": -140.42, + "unlockCost": [ + { + "item": "Desc_CrystalOscillator_C", + "amount": 8 + } + ] + }, + { + "id": "BP_DropPod1_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A9244946-4B7AC789-D7E7B990-16087593", + "x": 157250, + "y": -40206, + "z": 13695, + "rotation": 162.3, + "unlockCost": [ + { + "item": "Desc_ModularFrameHeavy_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod1_22", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "5D3E0A8A-4F9F6CF0-EE37239F-619607C2", + "x": -95228, + "y": 6971, + "z": 25143, + "rotation": -69.24, + "unlockCost": [ + { + "item": "Desc_ModularFrameHeavy_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod1_3", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "5CC60660-48D7C9D0-AE1658A9-4381440E", + "x": 46952, + "y": 221859, + "z": 5917, + "rotation": -3.58 + }, + { + "id": "BP_DropPod1_4", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8850E0D4-445301E8-79B624BB-BF8173B9", + "x": 303169, + "y": -246169, + "z": 5488, + "rotation": -102.23, + "unlockCost": [ + { + "item": "Desc_SteelPlate_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod1_8", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "0F2CD4EE-4DFDF623-5D6183A0-CDB4F43D", + "x": 176423, + "y": 243274, + "z": -9780, + "rotation": -45, + "unlockCost": [ + { + "item": "Desc_Rubber_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod1_9", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "172C1690-4C4E2B8E-258C4D8F-F8074E82", + "x": -118481, + "y": 74930, + "z": 16995, + "rotation": 56.44 + }, + { + "id": "BP_DropPod2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8C7AD0A7-49E75403-224E7CBD-691DB5E7", + "x": -26327, + "y": -129047, + "z": 7781, + "rotation": 95.64, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod20", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8A7C1E18-48386B7C-F407328C-B58D29C2", + "x": 66652, + "y": -13642, + "z": 13421, + "rotation": -107.48, + "unlockCost": [ + { + "item": "Desc_SteelPlateReinforced_C", + "amount": 3 + } + ] + }, + { + "id": "BP_DropPod21_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "FE601A10-4B7DBE51-3675CFA0-C9E23E38", + "x": 295167, + "y": -173140, + "z": 8083, + "rotation": -134.55, + "unlockCost": [ + { + "item": "Desc_IronRod_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod22", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "BF8340C7-4E8872E9-781EA3A4-4257B46B", + "x": 366637, + "y": -303549, + "z": -7289, + "rotation": -17.61 + }, + { + "id": "BP_DropPod23", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "1FC0C031-48239EB3-B4DA209A-542ACC42", + "x": 360285, + "y": -217559, + "z": 3901, + "rotation": -98.06 + }, + { + "id": "BP_DropPod24_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "02B3C4A8-4EDFAFA3-4B9A0696-45601C1F", + "x": 174948, + "y": -276436, + "z": 21151, + "rotation": -10.66, + "unlockCost": [ + { + "item": "Desc_ElectromagneticControlRod_C", + "amount": 43 + } + ] + }, + { + "id": "BP_DropPod25", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "16577A50-4AE0341C-8644E394-14872335", + "x": 159088, + "y": -145116, + "z": 23165, + "rotation": 169.3, + "unlockCost": [ + { + "item": "Desc_ModularFrameFused_C", + "amount": 12 + } + ] + }, + { + "id": "BP_DropPod26", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "52DBF625-4DEF9466-5E54A2B8-CBE67BA5", + "x": -55111, + "y": -204858, + "z": 7845, + "rotation": 169.59, + "unlockCost": [ + { + "item": "Desc_Rubber_C", + "amount": 35 + } + ] + }, + { + "id": "BP_DropPod27_20823", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "7FAE180A-47EC3350-960F8091-546877B5", + "x": 163999, + "y": 61333, + "z": 21481, + "rotation": -176.51, + "unlockCost": [ + { + "item": "Desc_CrystalOscillator_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod28_23787", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "DD53BCCD-4B40D4DC-1E76D2B2-19BC0D3E", + "x": 232516, + "y": -20519, + "z": 8980, + "rotation": -136.76, + "unlockCost": [ + { + "item": "Desc_AluminumPlateReinforced_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod29_27444", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "74D0C88C-447112CD-EF28D9A9-C1F0FFC9", + "x": 94267, + "y": 47237, + "z": 9435, + "rotation": 0, + "unlockCost": [ + { + "item": "Desc_ElectromagneticControlRod_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod2_0", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "3AB79B1E-4AF1A675-64D8009B-27930F61", + "x": 47767, + "y": 195703, + "z": 2891, + "rotation": 66.63, + "unlockCost": [ + { + "item": "Desc_SteelPlateReinforced_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod2_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "452B9987-4E4771FC-11125F85-4C94AC9A", + "x": 209272, + "y": -66917, + "z": 14011, + "rotation": 14.64, + "unlockCost": [ + { + "item": "Desc_Plastic_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod2_10", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "87621B01-40563CC9-D9038496-3887425E", + "x": -78236, + "y": 90857, + "z": 20306, + "rotation": 24.41, + "unlockCost": [ + { + "item": "Desc_ModularFrameLightweight_C", + "amount": 6 + } + ] + }, + { + "id": "BP_DropPod2_11", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "097E143F-4EFF51E7-66669990-33C5A0F3", + "x": 187056, + "y": 223657, + "z": -3215, + "rotation": -57.26, + "unlockCost": [ + { + "item": "Desc_ModularFrameHeavy_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod2_1325", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "7B497BC2-4F05AE12-69650A94-04B0F788", + "x": -195756, + "y": -59210, + "z": -84, + "rotation": 130.77, + "unlockCost": [ + { + "item": "Desc_IronScrew_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod2_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "1462EC70-453D4A25-7114389A-C0945DA3", + "x": 232255, + "y": 79926, + "z": -1276, + "rotation": -79.39, + "unlockCost": [ + { + "item": "Desc_MotorLightweight_C", + "amount": 2 + } + ] + }, + { + "id": "BP_DropPod2_23", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "999483B8-4FD7205E-852C6FA6-A36587AE", + "x": 12250, + "y": 114177, + "z": 26722, + "rotation": 91.25, + "unlockCost": [ + { + "item": "Desc_ElectromagneticControlRod_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod2_5", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B6238255-4371B1E1-D2A04D91-B7B9F7F7", + "x": 46048, + "y": 141933, + "z": 13065, + "rotation": -18.14 + }, + { + "id": "BP_DropPod2_6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "00ED4E88-4C8F8D1A-AA0040A3-CDB7E42A", + "x": 360115, + "y": -106614, + "z": 11815, + "rotation": -176.17, + "unlockCost": [ + { + "item": "Desc_Computer_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod3", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "553543F1-4666C9C6-15637B99-D27BAF7B", + "x": -174495, + "y": -197135, + "z": -1538, + "rotation": -64.06, + "unlockCost": [ + { + "item": "Desc_CopperSheet_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod30_6998", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "7F506E40-46587F50-2CB8E298-B4A262A3", + "x": 170058, + "y": -10580, + "z": 18824, + "rotation": -98.68, + "unlockCost": [ + { + "item": "Desc_MotorLightweight_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod31", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "4540D285-450FC6F3-B3E3D989-69F31299", + "x": 200510, + "y": 131912, + "z": 6341, + "rotation": -138.45, + "unlockCost": [ + { + "item": "Desc_Motor_C", + "amount": 30 + } + ] + }, + { + "id": "BP_DropPod32_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "4D7DF1B6-4861A532-06232BAA-AA0B4E93", + "x": 90314, + "y": 129584, + "z": 9113, + "rotation": -6.31, + "unlockCost": [ + { + "item": "Desc_CoolingSystem_C", + "amount": 30 + } + ] + }, + { + "id": "BP_DropPod33", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E41BFCCB-444DADCA-66B814A6-10EE400D", + "x": -78884, + "y": 292640, + "z": -4767, + "unlockCost": [ + { + "item": "Desc_CopperSheet_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod34", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "BC384D99-4965FC0A-65DC11A4-9E941489", + "x": -152279, + "y": 229520, + "z": 1052, + "rotation": 150.89, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod35", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "C30462C5-4781083B-A023E19E-0E3B4C96", + "x": 5302, + "y": -187091, + "z": -1608, + "rotation": 23.41, + "unlockCost": [ + { + "item": "Desc_Computer_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod36", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "6782E352-4E8D3C71-3BE883AB-E2B8B8BE", + "x": -136, + "y": -237257, + "z": -1761, + "rotation": 97.44, + "unlockCost": [ + { + "item": "Desc_SteelPlateReinforced_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod36_UAID_40B076DF2F79496001_1121515405", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "573404B7-48D36178-96EC9D9A-87E80EDD", + "x": -50656, + "y": -259272, + "z": -1668, + "rotation": -139.48 + }, + { + "id": "BP_DropPod37", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "323CF29B-4E5DCD3D-EAE44DBF-81808A93", + "x": 44579, + "y": -244344, + "z": -875, + "rotation": -4.43 + }, + { + "id": "BP_DropPod38", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "24F2DCFB-40A6D61F-82BFFE9A-8B1FFBC5", + "x": 84256, + "y": -171123, + "z": -291, + "rotation": 161.43, + "unlockCost": [ + { + "item": "Desc_HighSpeedWire_C", + "amount": 38 + } + ] + }, + { + "id": "BP_DropPod39", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B6DFB341-478A8409-44357B93-FC522D61", + "x": 119339, + "y": -165711, + "z": -171, + "rotation": 23.52 + }, + { + "id": "BP_DropPod39_UAID_40B076DF2F79F35F01_1355966267", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9A6CB2B1-48F6531E-B96EB78B-DFE64660", + "x": 52687, + "y": -163605, + "z": 15414, + "rotation": 133.82 + }, + { + "id": "BP_DropPod3_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "3F1A351A-497AB1DD-162F0FA4-0A696385", + "x": 17808, + "y": -136923, + "z": 13622, + "rotation": 152.82, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod3_11", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "3756A46A-4889FB29-53B19EAA-5E56D8DF", + "x": -40194, + "y": 62957, + "z": 26262, + "rotation": 33.56, + "unlockCost": [ + { + "item": "Desc_AluminumCasing_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod3_12", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8136FA91-46930F96-DF308FAE-F43B6FDC", + "x": 190459, + "y": 176105, + "z": 11479, + "rotation": 60.2, + "unlockCost": [ + { + "item": "Desc_Plastic_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod3_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "D2461D3C-4322AA22-4779ED96-9CC239AB", + "x": -56144, + "y": -72864, + "z": 27668, + "rotation": 3.79, + "unlockCost": [ + { + "item": "Desc_QuartzCrystal_C", + "amount": 2 + } + ] + }, + { + "id": "BP_DropPod3_24", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "EC670FBE-400A6B5C-8DDB6F88-5A235755", + "x": -3512, + "y": 62315, + "z": 22109, + "rotation": -167.49, + "unlockCost": [ + { + "item": "Desc_MotorLightweight_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod3_3", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "F413642D-4DB3C352-4C0DD089-E426DA72", + "x": 291822, + "y": 74782, + "z": -1575, + "rotation": 91.79, + "unlockCost": [ + { + "item": "Desc_ModularFrameHeavy_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod3_4", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "465EA8FC-45BC31FC-AE44109E-D4BC8BF7", + "x": 94940, + "y": 105483, + "z": 9860, + "rotation": -0.44, + "unlockCost": [ + { + "item": "Desc_ModularFrameHeavy_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod3_5", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "310EA96B-43C2B889-59817C9E-2EA1F1F6", + "x": 28696, + "y": 193441, + "z": 17460, + "rotation": 81.99, + "unlockCost": [ + { + "item": "Desc_SteelPlate_C", + "amount": 130 + } + ] + }, + { + "id": "BP_DropPod3_7", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E6C5CE7F-4E2D1837-16896EB0-278F559E", + "x": 275070, + "y": -52585, + "z": 5980, + "rotation": 148.4 + }, + { + "id": "BP_DropPod3_8", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "558FC461-424324FE-EE726EB5-235A8AF1", + "x": -56988, + "y": -144928, + "z": 2202, + "rotation": -4.72, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod3_8348", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A2865F36-456634CD-2AA9FD90-9AF3A349", + "x": -232498, + "y": -51432, + "z": -386, + "rotation": 0.61, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 21 + } + ] + }, + { + "id": "BP_DropPod4", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "021CE733-46D7D524-9EF927A0-998DC4A7", + "x": 111213, + "y": -113041, + "z": 12036, + "rotation": 123.2, + "unlockCost": [ + { + "item": "Desc_IronScrew_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod40_1163", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "6748D465-45BFE002-63FBF1AF-1444D636", + "x": 21373, + "y": 132336, + "z": 2511, + "rotation": 48.04, + "unlockCost": [ + { + "item": "Desc_Motor_C", + "amount": 2 + } + ] + }, + { + "id": "BP_DropPod41_8375", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "F8E03597-42AD97DE-A7F7A1B0-53DDCEE7", + "x": -9988, + "y": 227626, + "z": -1017, + "rotation": -9.56, + "unlockCost": [ + { + "item": "Desc_IronScrew_C", + "amount": 12 + } + ] + }, + { + "id": "BP_DropPod42", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "AF0CA13A-4E93A412-B6C60CB3-2540A076", + "x": 221070, + "y": 185915, + "z": 11560 + }, + { + "id": "BP_DropPod42_5", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "D4192556-4B1DA012-857FA7A9-EF4A1574", + "x": 190785, + "y": -236810, + "z": 23130, + "rotation": 39.38, + "unlockCost": [ + { + "item": "Desc_HighSpeedConnector_C", + "amount": 35 + } + ] + }, + { + "id": "BP_DropPod43_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9CABF60E-45E48365-7D22089E-1503AFB0", + "x": 176245, + "y": -243985, + "z": 7165 + }, + { + "id": "BP_DropPod44_10", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9561A2B3-48F284E9-BD5FF7A4-4A414224", + "x": 109775, + "y": -162730, + "z": 15570 + }, + { + "id": "BP_DropPod45_6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "AA320DC2-4F4373C7-C9764EA1-528B00D4", + "x": 190865, + "y": -127670, + "z": 16020, + "unlockCost": [ + { + "item": "Desc_AluminumPlate_C", + "amount": 57 + } + ] + }, + { + "id": "BP_DropPod4_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "DD94DC1D-44790F82-93FD50AF-58F21618", + "x": 143672, + "y": 92573, + "z": 24990, + "rotation": 119.28, + "unlockCost": [ + { + "item": "Desc_CrystalOscillator_C", + "amount": 2 + } + ] + }, + { + "id": "BP_DropPod4_12", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E34B6AEA-419C99C5-88F1EDB2-22EAA37D", + "x": -157078, + "y": -6313, + "z": 25129, + "rotation": -39.66, + "unlockCost": [ + { + "item": "Desc_ElectromagneticControlRod_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod4_13058", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "79EBC591-4EC58EF4-6E57D192-BD3543DA", + "x": -200203, + "y": -17767, + "z": 12193, + "rotation": 148.46, + "unlockCost": [ + { + "item": "Desc_Biofuel_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod4_25", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "0607A873-4E40D116-C7216E91-2EDB8C07", + "x": -33340, + "y": 5176, + "z": 23519, + "rotation": 170.14, + "unlockCost": [ + { + "item": "Desc_CoolingSystem_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod4_4", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "99B95FBD-4A321387-F1155281-DA4F4129", + "x": 191366, + "y": 37694, + "z": 5677, + "rotation": 83.84, + "unlockCost": [ + { + "item": "Desc_Computer_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod4_6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "575B441E-436C1E34-9921039E-9536B17F", + "x": 88324, + "y": 188914, + "z": 1420, + "rotation": -140.17 + }, + { + "id": "BP_DropPod4_7", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "3BF51392-4C77B79B-01FD7DB9-F3AA8BB6", + "x": 98306, + "y": -149782, + "z": 2552, + "rotation": -157.36 + }, + { + "id": "BP_DropPod4_8033", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "F2CBDED9-4AA3B00D-7452EB92-8660586C", + "x": 144457, + "y": 36294, + "z": 17301, + "rotation": -174.36, + "unlockCost": [ + { + "item": "Desc_AluminumPlateReinforced_C", + "amount": 17 + } + ] + }, + { + "id": "BP_DropPod4_9", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "DC9B56C9-4094AD97-6582FC9C-DD010090", + "x": 118350, + "y": 221905, + "z": -7063, + "rotation": 39.84, + "unlockCost": [ + { + "item": "Desc_CircuitBoard_C", + "amount": 15 + } + ] + }, + { + "id": "BP_DropPod5", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "AD9CCCC1-4D17D9C2-8238BFA8-11267C53", + "x": 226418, + "y": 98110, + "z": 7339, + "rotation": -70.07, + "unlockCost": [ + { + "item": "Desc_Computer_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod5_10", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "3841C37D-45C2C392-B81B06BE-FFA8E536", + "x": 156570, + "y": 191768, + "z": -9312, + "rotation": -64.02, + "unlockCost": [ + { + "item": "Desc_Rubber_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod5_13", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "7232305B-4D899F37-42D54E83-3F93195F", + "x": -152677, + "y": 33865, + "z": 19283, + "rotation": 49.52 + }, + { + "id": "BP_DropPod5_26", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "6BFD8371-474F736B-8B929498-853108DA", + "x": 35096, + "y": 16492, + "z": 22705, + "rotation": -268.26, + "unlockCost": [ + { + "item": "Desc_ComputerSuper_C", + "amount": 7 + } + ] + }, + { + "id": "BP_DropPod5_6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A290EEE2-49F5D887-025CAB92-CFF4EFA9", + "x": 127216, + "y": -116867, + "z": -1397, + "rotation": 126.53, + "unlockCost": [ + { + "item": "Desc_Rubber_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod5_740", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "60FDF366-44C90762-CFC0AAA6-BFF61A3B", + "x": -151842, + "y": 72468, + "z": 9945, + "unlockCost": [ + { + "item": "Desc_Stator_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod5_9", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "1BEDA476-4BEC36CF-5F187E9B-0EC67644", + "x": 349295, + "y": -38832, + "z": -1485, + "rotation": -47.63, + "unlockCost": [ + { + "item": "Desc_IronPlateReinforced_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "027C418F-46C542E8-C04088B5-EE7ADBFF", + "x": 272715, + "y": 28088, + "z": -1586, + "rotation": -50.28, + "unlockCost": [ + { + "item": "Desc_Silica_C", + "amount": 60 + } + ] + }, + { + "id": "BP_DropPod6_2102", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E9381D5C-40FC87F1-115E2889-9E6EA68E", + "x": -108774, + "y": 107811, + "z": 10154, + "rotation": -5.22, + "unlockCost": [ + { + "item": "Desc_CopperSheet_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod6_27", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "95164A56-4509597F-3BB67BB6-818A356D", + "x": -35360, + "y": 116594, + "z": 21828, + "rotation": 173.8, + "unlockCost": [ + { + "item": "Desc_MotorLightweight_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod7", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B50A9B52-44AD7D48-E84FB3B9-0922A7C6", + "x": 216097, + "y": -268, + "z": -1593, + "rotation": -61.26, + "unlockCost": [ + { + "item": "Desc_QuantumOscillator_C", + "amount": 1 + } + ] + }, + { + "id": "BP_DropPod7_5615", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "2E293624-479EA2FC-0C1328B6-B6C8D60C", + "x": -121994, + "y": 166916, + "z": -49, + "rotation": -93.65, + "unlockCost": [ + { + "item": "Desc_SteelPlate_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod8", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "1FF55973-4B58509B-4E2D3F9B-C5F0B281", + "x": 261798, + "y": 124617, + "z": -2597, + "rotation": 23.27, + "unlockCost": [ + { + "item": "Desc_CoolingSystem_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod9", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "D3D5C9CC-4C59CA7B-6B7214B6-9BABA057", + "x": 249920, + "y": 59534, + "z": 2430, + "rotation": -81.61, + "unlockCost": [ + { + "item": "Desc_MotorLightweight_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod9_1568", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "2FDF842A-495F2A8B-1BB6EDB4-D8D61BFF", + "x": -129115, + "y": 60165, + "z": 4800, + "rotation": -7.52 + }, + { + "id": "BP_DropPod_1145", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A7D03505-445DECE6-8DE345A8-425DA4D5", + "x": -157080, + "y": -67028, + "z": 11766, + "rotation": -36.7, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod_C_0", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "324A72BD-4AC94205-37F7E6AF-B7675FB3", + "x": -123677, + "y": -167107, + "z": 29711, + "rotation": 46.86, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod_C_1", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8F837D98-464649DF-66E90FAE-52C0FCA0", + "x": 80980, + "y": -44100, + "z": 8303, + "rotation": 68.98, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 3 + } + ] + }, + { + "id": "BP_DropPod_C_10", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A7939D29-41AF0B18-BDD3CDB4-FD257A42", + "x": -142000, + "y": 23970, + "z": 32660, + "rotation": 110.5 + }, + { + "id": "BP_DropPod_C_11", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "DFE72D58-4B8C83D4-1D9681A9-5BF412D8", + "x": 219146, + "y": -199880, + "z": 6504, + "rotation": -10.66, + "unlockCost": [ + { + "item": "Desc_IronScrew_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod_C_12", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "42F66618-4E7F9C00-87902387-5124CF45", + "x": 236508, + "y": -312236, + "z": 9971, + "rotation": -10.66 + }, + { + "id": "BP_DropPod_C_2", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "C29B0A0C-47E0CE72-49FA7D85-E6BB809E", + "x": 150634, + "y": 146699, + "z": 7728, + "rotation": -6.31, + "unlockCost": [ + { + "item": "Desc_HighSpeedWire_C", + "amount": 25 + } + ] + }, + { + "id": "BP_DropPod_C_3", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B263233F-483AAD28-410848AB-22E48604", + "x": -146044, + "y": -137047, + "z": 2358, + "rotation": 148.21, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod_C_4", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "F309C8BF-4B5C51A7-C43CF0B9-71D35726", + "x": -29069, + "y": -22640, + "z": 17384, + "rotation": -147.14, + "unlockCost": [ + { + "item": "Desc_Plastic_C", + "amount": 20 + } + ] + }, + { + "id": "BP_DropPod_C_5", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "ED6074DA-448054E6-981A1FA0-8BC91AA3", + "x": 111479, + "y": -54515, + "z": 17081, + "rotation": -172.39, + "unlockCost": [ + { + "item": "Desc_CopperSheet_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod_C_6", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "CFDCF316-46084583-49C2688B-917009CF", + "x": 125497, + "y": -34949, + "z": 8221, + "rotation": -29.53 + }, + { + "id": "BP_DropPod_C_7", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "6D05B721-4681AEA4-A8D2EDAC-13C1232B", + "x": -247304, + "y": -142348, + "z": 4524, + "rotation": -100.39, + "unlockCost": [ + { + "item": "Desc_Rotor_C", + "amount": 4 + } + ] + }, + { + "id": "BP_DropPod_C_8", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "4EA2B83F-4000B97A-E2885D9A-6C6F9780", + "x": 64697, + "y": 156039, + "z": 14067, + "rotation": 0, + "unlockCost": [ + { + "item": "Desc_ModularFrame_C", + "amount": 6 + } + ] + }, + { + "id": "BP_DropPod_C_9", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "A80B6A54-4E995DA2-FF81EABA-3F1A52B2", + "x": -94709, + "y": 40338, + "z": 19832, + "rotation": 107.02, + "unlockCost": [ + { + "item": "Desc_AluminumPlateReinforced_C", + "amount": 12 + } + ] + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F01FA401_2123550800", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9AF83E54-4743D0E6-1706E399-C063E690", + "x": -190866, + "y": 66921, + "z": 23751, + "rotation": 0, + "unlockCost": [ + { + "item": "Desc_Silica_C", + "amount": 10 + } + ] + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F020A401_1123860977", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "EE7DEE4E-4BBD5357-B972278B-7FC3CC70", + "x": -164591, + "y": 155611, + "z": 12383, + "unlockCost": [ + { + "item": "Desc_CrystalOscillator_C", + "amount": 5 + } + ] + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F020A401_1502230978", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "305A7D5B-4497637A-2D68F280-3E1F6069", + "x": -142173, + "y": 97937, + "z": 18517 + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F03B7C01_1559404536", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9F451320-4A0510C0-525A8F8C-43FDB65B", + "x": 115978, + "y": 21424, + "z": 15519, + "rotation": 0 + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F03B7C01_1712034537", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "8FFFF2EE-40F44245-DDBF1A8F-C389C141", + "x": 198418, + "y": -41186, + "z": 13786 + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F03B7C01_1807913538", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "9226B94E-441EF095-A8392095-210C1C5B", + "x": 121061, + "y": 45324, + "z": 17373, + "unlockCost": [ + { + "item": "Desc_AluminumPlateReinforced_C", + "amount": 7 + } + ] + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F03C7C01_1131248715", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "B3BABAE9-409DBD28-59E193AA-D2973001", + "x": 188304, + "y": 17059, + "z": 12949, + "rotation": -17.86 + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F0486401_1144991453", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "E64BBF51-4721975C-3BCD61B8-692E4E7D", + "x": 241532, + "y": 131343, + "z": 17157, + "rotation": 50.54 + }, + { + "id": "BP_DropPod_C_UAID_04421A9713F0FF6301_1123988602", + "type": "hardDrive", + "classPath": "BP_DropPod_C", + "pickupGuid": "452BA69B-4650065C-27AEFD9F-CD4463E7", + "x": 182825, + "y": 93880, + "z": 17755, + "unlockCost": [ + { + "item": "Desc_AluminumCasing_C", + "amount": 10 + } + ] + }, + { + "id": "BP_WAT102", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E104C029-4DC5E444-CCCBCB9D-8BBD88E7", + "x": 846, + "y": -168496, + "z": 1844 + }, + { + "id": "BP_WAT103", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0B149395-49ED0524-17B3E284-499BE499", + "x": 37172, + "y": -192272, + "z": 1100 + }, + { + "id": "BP_WAT104", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BE3E221E-43FEEC3B-F4B7BA97-533BC712", + "x": 67777, + "y": -192082, + "z": 3292 + }, + { + "id": "BP_WAT105", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5B418E5B-43CFB7FE-8E1227B4-3619BBDF", + "x": 79199, + "y": -159556, + "z": 15953 + }, + { + "id": "BP_WAT106", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E3F70056-484D2167-A5D9B299-141DC072", + "x": 152032, + "y": -204217, + "z": 8686 + }, + { + "id": "BP_WAT106_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "741398E4-4C2FF254-09F819B3-049F5ED1", + "x": 163570, + "y": -294545, + "z": 23785 + }, + { + "id": "BP_WAT108_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4B45174D-478CE875-DE5304A6-C904EB75", + "x": 164459, + "y": -271549, + "z": 17583 + }, + { + "id": "BP_WAT109", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4757A50C-4246EDAE-570044AA-DBA14B99", + "x": 129690, + "y": -183159, + "z": -579 + }, + { + "id": "BP_WAT109_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4A5002A8-44F8319B-AD05AF80-33CBA88B", + "x": 200644, + "y": -247527, + "z": 26623 + }, + { + "id": "BP_WAT10_5", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D228B7BB-4535D8B5-AB3E9DA1-E7A985E9", + "x": 169226, + "y": -32286, + "z": 11407, + "rotation": -101.18 + }, + { + "id": "BP_WAT11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EDFA8E41-491A517A-5EE79C8C-E8712366", + "x": -113192, + "y": -53236, + "z": 21683, + "rotation": 30.12 + }, + { + "id": "BP_WAT111", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5C99EC52-45180B7A-60A8FEA4-B1B5F6DF", + "x": 139915, + "y": -157899, + "z": 559 + }, + { + "id": "BP_WAT112_14", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1CC63F00-4591F020-6ADCD890-71D0A3FB", + "x": 186820, + "y": -204185, + "z": 25648 + }, + { + "id": "BP_WAT113", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1A92E420-413CD7CD-39C44F85-58B93296", + "x": 154805, + "y": -190795, + "z": 2575 + }, + { + "id": "BP_WAT113_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F163567B-4888BDE2-24056F87-1FC1ACE7", + "x": 140820, + "y": -206425, + "z": 2629 + }, + { + "id": "BP_WAT118", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9A42719B-461EECBB-BFD3D5A2-C64340E0", + "x": 116955, + "y": -196713, + "z": 8911 + }, + { + "id": "BP_WAT119", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F004CC91-478FA8B7-AF8F908C-0C19046A", + "x": 161725, + "y": -170475, + "z": 17202 + }, + { + "id": "BP_WAT119_8", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F80334B1-42A1C097-62F2B89C-17A7CFB0", + "x": 180577, + "y": -242488, + "z": 15323 + }, + { + "id": "BP_WAT11_15", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "67ADAC91-476B4EA0-AF0C68B3-AEF968CC", + "x": 59821, + "y": 75987, + "z": 21210 + }, + { + "id": "BP_WAT11_21", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A6270F08-4FB6A60F-1BDD62B6-4D0053F0", + "x": -273893, + "y": -149957, + "z": -1510 + }, + { + "id": "BP_WAT12", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E9F72650-43FB144D-76215893-829C30DA", + "x": -218992, + "y": -117543, + "z": 11052, + "rotation": 84.05 + }, + { + "id": "BP_WAT120", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F32C9FB4-422FAFD1-610A33AD-02CC40E9", + "x": 118715, + "y": -150470, + "z": 5030 + }, + { + "id": "BP_WAT122_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F940EB1C-4E252A08-BA1FB19E-A603FE4C", + "x": 213200, + "y": -193520, + "z": 24567 + }, + { + "id": "BP_WAT124", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D551A02A-46F11BD7-8F7A6E90-CFBA6032", + "x": -221844, + "y": -88194, + "z": -1602, + "rotation": -146.6 + }, + { + "id": "BP_WAT125", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4D0E1BB9-4789628B-3629459C-EAE3A922", + "x": -260014, + "y": -4718, + "z": -127, + "rotation": -149.86 + }, + { + "id": "BP_WAT126", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5BA579A8-47D80164-95C5A9B3-AE144FDA", + "x": -138825, + "y": -38238, + "z": 800 + }, + { + "id": "BP_WAT127", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C27FD487-4026B737-DA68D181-15576271", + "x": -133099, + "y": -93390, + "z": 486, + "rotation": 53.92 + }, + { + "id": "BP_WAT12_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "67F7C33B-4AB56EF7-10CCA686-0F81F3DA", + "x": -32922, + "y": -48290, + "z": 14668, + "rotation": -28.92 + }, + { + "id": "BP_WAT12_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3070417A-4C1E456E-48BB6CBF-233153C8", + "x": -101976, + "y": 278426, + "z": -1620, + "rotation": -154.56 + }, + { + "id": "BP_WAT12_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0B6BC206-4514DD47-88F3CE8B-07AFAA2D", + "x": -5508, + "y": 102348, + "z": 23653, + "rotation": 44.94 + }, + { + "id": "BP_WAT130", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8E8BB782-482AAAAC-6D0E80AE-81225002", + "x": -199017, + "y": -41618, + "z": 764, + "rotation": -142.23 + }, + { + "id": "BP_WAT135", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "30CED776-48806BC2-BCDF06AD-B3ABC92D", + "x": -255588, + "y": -61460, + "z": 180, + "rotation": 93.2 + }, + { + "id": "BP_WAT139", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "AB2D0709-42BB845D-869EEF85-9AA48076", + "x": -33565, + "y": 121714, + "z": 3027, + "rotation": 12.45 + }, + { + "id": "BP_WAT13_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "CE7CDB55-45928283-4F55488A-506FAC0A", + "x": -36931, + "y": -76082, + "z": 11640, + "rotation": 33.43 + }, + { + "id": "BP_WAT13_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "805932F9-4A3909C6-1A03EB88-ACA12D81", + "x": -44173, + "y": -259058, + "z": 1142 + }, + { + "id": "BP_WAT13_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "432318B6-47FF38C3-DC8EBD9F-36E63248", + "x": 187165, + "y": 165654, + "z": 3372, + "rotation": -53.62 + }, + { + "id": "BP_WAT14", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "AE6A0B2F-44FDECBC-B32240A3-DE29FD75", + "x": -118080, + "y": -186090, + "z": 14189, + "rotation": -74.23 + }, + { + "id": "BP_WAT140", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "63F4EBE5-41CD2220-FA7AE8BB-D2004782", + "x": -106845, + "y": 124972, + "z": 6294, + "rotation": -0.41 + }, + { + "id": "BP_WAT141", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1916E055-4417D557-1E2FDBA9-77B6C4EE", + "x": -57170, + "y": 140413, + "z": -4968, + "rotation": 172.05 + }, + { + "id": "BP_WAT142", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1772BDA2-4F9BB1A4-5D1855B1-A0703DFF", + "x": -124535, + "y": 154337, + "z": 623, + "rotation": 170.86 + }, + { + "id": "BP_WAT143_4633", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5C261A25-489CAB23-9B1AEA9A-769ADBC6", + "x": -145474, + "y": 89816, + "z": 9324 + }, + { + "id": "BP_WAT144", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "ADE4C314-47B8BAB8-4A00EB93-2621A5BD", + "x": -110467, + "y": 92197, + "z": 5077, + "rotation": -32.72 + }, + { + "id": "BP_WAT145", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "CA9333FD-4073821B-D7D8A5B0-13720C36", + "x": -168746, + "y": 57825, + "z": 12554, + "rotation": 112.1 + }, + { + "id": "BP_WAT148", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "90E29E8A-410DECB3-6BF308BB-B4219A74", + "x": -205219, + "y": 22857, + "z": -1181, + "rotation": -149.86 + }, + { + "id": "BP_WAT149", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4A3E9F84-446528A2-97EDC4AB-06AF9204", + "x": -278041, + "y": -89803, + "z": -863, + "rotation": -180 + }, + { + "id": "BP_WAT14_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0D963FA1-43FA1FA8-A35872AB-DCB9DC9B", + "x": 239741, + "y": 40268, + "z": -1682, + "rotation": 0 + }, + { + "id": "BP_WAT15_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0A663A8E-460A2E2E-76D31FBE-E826A20A", + "x": 67996, + "y": -162824, + "z": -1760, + "rotation": 0 + }, + { + "id": "BP_WAT15_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "77E2C3D6-438BF5DA-2572E5BD-1A1E780F", + "x": -64626, + "y": -65620, + "z": 12511, + "rotation": -44.01 + }, + { + "id": "BP_WAT16_10", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E0AB836A-43506476-96C4F8AF-577F7444", + "x": 14064, + "y": -10585, + "z": 25644, + "rotation": -69.63 + }, + { + "id": "BP_WAT16_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E9207178-4E3E40A6-B0C11CB0-1B5CA581", + "x": -58160, + "y": -197611, + "z": 13112 + }, + { + "id": "BP_WAT17", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "555CF0C5-431F224C-E803DF9B-9FCE823A", + "x": -168902, + "y": -173016, + "z": 7728, + "rotation": 22.95 + }, + { + "id": "BP_WAT17_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "85A0058F-46304413-7F854DB6-EFBCF1E8", + "x": 20787, + "y": 23944, + "z": 23861, + "rotation": 144.34 + }, + { + "id": "BP_WAT18_12", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9ED02ECD-47D6434E-9E1F63BE-3EE1CA03", + "x": 46245, + "y": 14149, + "z": 23995, + "rotation": -36.21 + }, + { + "id": "BP_WAT18_9", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A85366F0-4807869B-2CC33289-09919422", + "x": 142194, + "y": 241283, + "z": -2773, + "rotation": 52.93 + }, + { + "id": "BP_WAT19_10", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A92957FC-4D65A95C-3BC531B7-31A41DF6", + "x": 169480, + "y": 173413, + "z": -5234, + "rotation": -4.46 + }, + { + "id": "BP_WAT1_9", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BEAB21FD-4DC6E209-339906A5-6E4E450F", + "x": 49064, + "y": -94568, + "z": 17693 + }, + { + "id": "BP_WAT1_C_13", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "176AE1DC-4A18CB93-05BC16A0-C2226C8C", + "x": 206883, + "y": 112912, + "z": 79, + "rotation": -91.13 + }, + { + "id": "BP_WAT1_C_14", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "FA24530A-462B3B8A-F05A5388-0C4132C4", + "x": 247454, + "y": 119664, + "z": -951, + "rotation": 13.75 + }, + { + "id": "BP_WAT1_C_17", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4601644C-4D82153F-F1833C82-E10B3548", + "x": 223173, + "y": 90154, + "z": 1338 + }, + { + "id": "BP_WAT1_C_19", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1026E12F-4A218038-F88E28B0-1600A8B3", + "x": 270276, + "y": -37611, + "z": 5306, + "rotation": 68.98 + }, + { + "id": "BP_WAT2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "61E335A0-499A85F1-CA7160BD-720AC7FA", + "x": -13329, + "y": -130433, + "z": 9323 + }, + { + "id": "BP_WAT20", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "968CF1E9-4A21DCB3-6956549A-316A0EF7", + "x": 74273, + "y": -155774, + "z": 21542 + }, + { + "id": "BP_WAT20_9811", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9F0DF45D-407E736B-6B375690-4F50E597", + "x": 184956, + "y": 27671, + "z": 10782, + "rotation": 0 + }, + { + "id": "BP_WAT210", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C85A7650-4789CE84-A5A5C1B2-EFD3D4A9", + "x": -21211, + "y": 263473, + "z": -4610, + "rotation": 176.82 + }, + { + "id": "BP_WAT210_42", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "99B5524B-41481155-3047BD89-0D4F222B", + "x": -32033, + "y": 51592, + "z": 21866 + }, + { + "id": "BP_WAT211", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "05AF0B44-4FC31C5E-F6A09AAC-680687BB", + "x": 14370, + "y": 198309, + "z": -11031, + "rotation": 175.35 + }, + { + "id": "BP_WAT211_UAID_40B076DF2F79007A01_1867371992", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A1E1E2B7-4D088969-E0912391-EB6C1B28", + "x": 62878, + "y": 135613, + "z": -8358, + "rotation": 172.05 + }, + { + "id": "BP_WAT211_UAID_40B076DF2F79007A01_1988795993", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6BD11E15-4DE67C01-12ED8185-95B3B498", + "x": 23533, + "y": 157514, + "z": -16557, + "rotation": 172.05 + }, + { + "id": "BP_WAT211_UAID_40B076DF2F79017A01_1322649170", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A72C191B-4634ADF9-03E1179C-3F88D010", + "x": 48435, + "y": 152060, + "z": 6222, + "rotation": 172.05 + }, + { + "id": "BP_WAT212", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8ADE4DA2-4165B14C-99581D98-3BB3C9E6", + "x": -263894, + "y": -111561, + "z": -1508, + "rotation": 0 + }, + { + "id": "BP_WAT21_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "45AB7F7A-48E7B779-0BF108A5-584C7DA0", + "x": -90210, + "y": -72819, + "z": 15898 + }, + { + "id": "BP_WAT21_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "09D04BA6-4AD71DE7-915302A7-B651C131", + "x": -126042, + "y": 215385, + "z": 2780 + }, + { + "id": "BP_WAT21_10", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "08CC0684-43CBB195-E4D7E083-31F04452", + "x": 33760, + "y": 65595, + "z": 23524, + "rotation": 10.83 + }, + { + "id": "BP_WAT21_10184", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A846E1FE-4726C654-0DC7F89F-B9E4B6F3", + "x": 124817, + "y": 11860, + "z": 18560 + }, + { + "id": "BP_WAT21_15", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "161B172C-4601153A-FD64179A-C7FBE829", + "x": 64471, + "y": 102288, + "z": 11766 + }, + { + "id": "BP_WAT21_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "7DAF3AF3-4E1CD330-76C98BBB-7C5898F9", + "x": 178776, + "y": 189126, + "z": 771 + }, + { + "id": "BP_WAT21_24", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "57F312B1-4D3D68E7-8F3E6BB4-976C3E53", + "x": -98837, + "y": 85512, + "z": 17574 + }, + { + "id": "BP_WAT21_25", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1025E2C6-448CFAEE-22C775A4-C2DB90A7", + "x": 255759, + "y": 115581, + "z": 8746 + }, + { + "id": "BP_WAT21_356", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "36D48C59-44F95B16-DD362E9E-2C161746", + "x": -74871, + "y": 104948, + "z": 11242 + }, + { + "id": "BP_WAT21_UAID_40B076DF2F79CA7C01_1851394664", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "67EDDB35-40996128-A7D1F3B6-6DE9B88A", + "x": -98170, + "y": 231611, + "z": -663, + "rotation": 0 + }, + { + "id": "BP_WAT22", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "59063034-4E8FE199-2727408E-3E9F58F7", + "x": -267599, + "y": -180681, + "z": 462 + }, + { + "id": "BP_WAT22_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3CB74209-439C1DD7-013732B3-2EC5B2B2", + "x": -34386, + "y": 96725, + "z": 23435 + }, + { + "id": "BP_WAT22_16", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "95CE47F7-4DD0B251-073947A5-502ACBDF", + "x": 110329, + "y": 109191, + "z": 9321 + }, + { + "id": "BP_WAT22_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8CA93086-427BA2C3-09681280-E23279D6", + "x": -29342, + "y": 245731, + "z": 2511, + "rotation": 0 + }, + { + "id": "BP_WAT22_25", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1526C4BE-469DC2D4-ED7FF396-5B7A4097", + "x": -67362, + "y": 29824, + "z": 19643 + }, + { + "id": "BP_WAT22_26", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1A923C57-4802E8AF-47E57E81-2F91A7B5", + "x": 283018, + "y": 52947, + "z": -898 + }, + { + "id": "BP_WAT22_277", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A5881062-4751D3CF-205014AF-A451B45C", + "x": -159288, + "y": 132746, + "z": 3833, + "rotation": -37.52 + }, + { + "id": "BP_WAT22_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D8175EA5-4FC91F45-A660469B-5C7AA928", + "x": 153149, + "y": 175179, + "z": -4478 + }, + { + "id": "BP_WAT22_7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1CC3D223-477185E1-6BC4F3BA-5DACB769", + "x": -238995, + "y": 56239, + "z": -1461 + }, + { + "id": "BP_WAT23", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EAC72E3C-43ABBC5B-4EDE88B2-82EECCB6", + "x": 95689, + "y": -212116, + "z": 10343 + }, + { + "id": "BP_WAT23_1410", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2DA0864C-4907FE5F-4C2BECA2-5A5F1FE4", + "x": -154230, + "y": 191086, + "z": 1619, + "rotation": 42.66 + }, + { + "id": "BP_WAT23_15", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "67BBA756-43528E99-A2BAB290-8F536A80", + "x": 24240, + "y": 88167, + "z": 35421 + }, + { + "id": "BP_WAT23_17", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "67BCEC10-410E6D55-A04123A5-C0E915C1", + "x": 140716, + "y": 121162, + "z": 12593 + }, + { + "id": "BP_WAT23_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A5D6C398-4FE6F8F1-0B5D1784-2421A31A", + "x": -51484, + "y": -24457, + "z": 18533, + "rotation": 0 + }, + { + "id": "BP_WAT23_26", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9187B627-48B82F14-623204A1-73370228", + "x": -66454, + "y": 60150, + "z": 22381, + "rotation": 9.26 + }, + { + "id": "BP_WAT23_27", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "40DD90AE-4D9214B0-EF9CE88A-26820CA3", + "x": 238670, + "y": 111306, + "z": -4900 + }, + { + "id": "BP_WAT23_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "26BF9825-4FAC714D-23204CB8-63EB066D", + "x": -77522, + "y": 254843, + "z": -4589, + "rotation": 0 + }, + { + "id": "BP_WAT23_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EB6F3F70-433EC930-185B129E-98614968", + "x": 196020, + "y": 223705, + "z": 3841, + "rotation": -87.96 + }, + { + "id": "BP_WAT24_18", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2868C121-481DFB04-F800048C-EDE5C2E2", + "x": 137950, + "y": 75593, + "z": 13022, + "rotation": 45.3 + }, + { + "id": "BP_WAT24_19", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1974D3AC-458D2C6F-56509CB1-9463DD41", + "x": -77005, + "y": 16959, + "z": 23138 + }, + { + "id": "BP_WAT24_27", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "226D7FF7-4ED172AE-139CF7AF-B01B0D51", + "x": -129782, + "y": 15841, + "z": 19524 + }, + { + "id": "BP_WAT24_278", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F070D07F-47F3A2C2-9B5CC9B8-6CF93F2E", + "x": -188845, + "y": 98522, + "z": 5641 + }, + { + "id": "BP_WAT24_28", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E764FCCD-4A6423C0-291787AB-4483CCB0", + "x": 256311, + "y": -24294, + "z": 9150, + "rotation": 25.99 + }, + { + "id": "BP_WAT24_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "45FE2D4C-4D3D4592-DE8EDF9A-141D5AEB", + "x": -22061, + "y": -44071, + "z": 20305 + }, + { + "id": "BP_WAT24_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "66330EC1-4174AAAC-F1CCFBB2-D0991F38", + "x": -10456, + "y": 177575, + "z": 1102, + "rotation": 83.56 + }, + { + "id": "BP_WAT24_5", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BA454675-4164D644-AE95F7A9-CE384747", + "x": 139038, + "y": 216779, + "z": -8892, + "rotation": -89.53 + }, + { + "id": "BP_WAT25", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "18D33BFC-4882713C-B84D83BE-114D00A0", + "x": -253370, + "y": -152041, + "z": 15231 + }, + { + "id": "BP_WAT25_19", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C1C0714A-4A92809D-C90B4986-CD063354", + "x": 38448, + "y": 40905, + "z": 18103 + }, + { + "id": "BP_WAT25_23", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A7EA326F-41AB7663-70F3DB99-44C760E6", + "x": -42592, + "y": 12442, + "z": 24000 + }, + { + "id": "BP_WAT25_28", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "35390AFF-40FB9937-C88F819A-0EE4A534", + "x": -88096, + "y": 52526, + "z": 24590 + }, + { + "id": "BP_WAT25_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4A2F5DC6-43EE266E-DF2F6F8F-224FE0C5", + "x": -40593, + "y": -59641, + "z": 14472, + "rotation": 16.88 + }, + { + "id": "BP_WAT25_5", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2039FFD5-41C1FE48-5CDB76A5-3BA491D8", + "x": 7656, + "y": 294058, + "z": -2276, + "rotation": -42.36 + }, + { + "id": "BP_WAT25_6", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "29F78297-4A1B6D5C-3497439D-FA878256", + "x": 163534, + "y": 207428, + "z": -9177, + "rotation": 27.13 + }, + { + "id": "BP_WAT26", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D9D38EE3-4EF84830-F8180BBA-C6CF0B37", + "x": -199602, + "y": -176962, + "z": -1531 + }, + { + "id": "BP_WAT26_27", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "543201CA-4C37935D-81080496-19C6D6E8", + "x": -13388, + "y": 68720, + "z": 23096, + "rotation": 125.54 + }, + { + "id": "BP_WAT26_29", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D0D6956E-48C511AA-392A0785-0EF78E56", + "x": -79992, + "y": 39740, + "z": 19784 + }, + { + "id": "BP_WAT26_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "B9A7ED82-44ACE0C2-35B11582-F46D8F05", + "x": -211902, + "y": -130283, + "z": 6633 + }, + { + "id": "BP_WAT26_6", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "14825BE5-4617392B-371E3CB9-265B4A5F", + "x": -12550, + "y": 195573, + "z": -1380, + "rotation": 0 + }, + { + "id": "BP_WAT26_7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "720F1B4E-47CB23D6-EF6FFD80-916E24F3", + "x": 128959, + "y": 252404, + "z": -1773 + }, + { + "id": "BP_WAT27", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "23595FBE-48E52036-2B23EDA9-9C0EAB8D", + "x": -176140, + "y": -145909, + "z": 5994 + }, + { + "id": "BP_WAT27_30", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A9CA3054-460A4F82-08A420B6-47596B7F", + "x": -183084, + "y": 13871, + "z": 21768 + }, + { + "id": "BP_WAT27_31", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2FAA90CA-45F5DD8F-6D793498-F04586D0", + "x": -9284, + "y": -14729, + "z": 32839 + }, + { + "id": "BP_WAT27_7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "777DA238-4A09D57F-42276CBA-154900E9", + "x": -49121, + "y": 267537, + "z": -3112, + "rotation": 18.24 + }, + { + "id": "BP_WAT27_716", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "331F491A-480FE923-DA0C46B8-418B1185", + "x": -170069, + "y": 131715, + "z": 9363, + "rotation": -167.06 + }, + { + "id": "BP_WAT27_8", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "837F0A0A-4D1097AA-C8ECB2A7-A30EACF6", + "x": 101585, + "y": 205493, + "z": -3128 + }, + { + "id": "BP_WAT28", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "DE63A367-437418EA-11B2C1AF-65BCFB86", + "x": -33627, + "y": 198750, + "z": 1278 + }, + { + "id": "BP_WAT28_34", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3ED1CBB0-46794227-A03D5CAE-392F3A3A", + "x": -41319, + "y": 69777, + "z": 21100 + }, + { + "id": "BP_WAT28_38", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A024569D-42CA5F3C-2C21319A-42DCD251", + "x": 21822, + "y": 8591, + "z": 23372, + "rotation": -138.4 + }, + { + "id": "BP_WAT28_8", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "95E81796-4CE206BC-886D009D-A230B64A", + "x": 14752, + "y": -136715, + "z": 1232 + }, + { + "id": "BP_WAT28_9", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "14ABCCC8-4113FC64-F24D05A7-92E34B84", + "x": 90260, + "y": 163147, + "z": -4523, + "rotation": -86.63 + }, + { + "id": "BP_WAT29", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "69C5579A-4C491EBE-41FC2BA7-0CA21854", + "x": 22878, + "y": 285311, + "z": -493, + "rotation": 130.27 + }, + { + "id": "BP_WAT29_10", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "38D2D0BB-40A5E0B1-3F25E68F-0AEB65C5", + "x": 146849, + "y": 279756, + "z": -5032, + "rotation": -84.24 + }, + { + "id": "BP_WAT29_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "769D27CF-42F34702-FC86249B-9046040A", + "x": 143163, + "y": -48605, + "z": 978 + }, + { + "id": "BP_WAT29_38", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "28F0167C-4AA2B677-936AB8A3-34875719", + "x": -137796, + "y": 47355, + "z": 23704 + }, + { + "id": "BP_WAT29_42", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "71B2697A-4B489581-C04B53A4-16E8C3DA", + "x": 9808, + "y": 41677, + "z": 22512 + }, + { + "id": "BP_WAT2_C_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C621184D-4603A490-DC0456A9-5B6BDAB8", + "x": -28435, + "y": -180056, + "z": 11814 + }, + { + "id": "BP_WAT2_C_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2B90D0B8-4E35E986-5DDB66A3-F2CE450A", + "x": 66314, + "y": -61352, + "z": 7539, + "rotation": -11.16 + }, + { + "id": "BP_WAT2_C_10", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "095188E9-43214560-6FB642BA-D1666397", + "x": 176979, + "y": 115977, + "z": 17057 + }, + { + "id": "BP_WAT2_C_11", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C246A675-400A899D-BE11F893-28A7D5A5", + "x": 174067, + "y": 118773, + "z": 10465 + }, + { + "id": "BP_WAT2_C_12", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "19D0D25D-4CAAE3D4-BCFD2AB3-F46A8A29", + "x": 192570, + "y": 86145, + "z": 1122 + }, + { + "id": "BP_WAT2_C_13", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2D2FA73A-4B576D07-40159698-DBF7E97B", + "x": 167448, + "y": 86817, + "z": 6072 + }, + { + "id": "BP_WAT2_C_14", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "78213983-46E91763-15CE7BBB-85846EEB", + "x": 112217, + "y": -73476, + "z": 12662 + }, + { + "id": "BP_WAT2_C_15", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D95F94BD-4818D972-D6E8B1A9-E8D13331", + "x": 109404, + "y": -36637, + "z": 8860 + }, + { + "id": "BP_WAT2_C_16", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F2DFA8B0-422C8052-98524D9E-FD66FF96", + "x": 124816, + "y": -10483, + "z": 14744 + }, + { + "id": "BP_WAT2_C_17", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EE555AEB-49002B6B-8C329597-43BAB4E6", + "x": 118479, + "y": -177280, + "z": 1280 + }, + { + "id": "BP_WAT2_C_18", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "31DDC006-4A312957-2C07BEAB-4929F45B", + "x": 188950, + "y": -168060, + "z": 23738 + }, + { + "id": "BP_WAT2_C_19", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BC75B3B9-412B623F-71329ABE-EDCFBF10", + "x": 193305, + "y": -149223, + "z": 23070 + }, + { + "id": "BP_WAT2_C_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D5E58F74-45E27991-D605778F-CEE3940F", + "x": 77135, + "y": -4647, + "z": 13480 + }, + { + "id": "BP_WAT2_C_20", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D5E69685-4AC50334-81A9349F-747FF0C6", + "x": 182590, + "y": -179605, + "z": 27411, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_21", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BC1CC93A-45A31406-EC9AA3AF-E9B6F1EA", + "x": 281413, + "y": 121409, + "z": 156, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_23", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "CD8C1606-453C478F-E31CD493-895A7548", + "x": 215830, + "y": 139040, + "z": -3035 + }, + { + "id": "BP_WAT2_C_25", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "FC33311C-4E5D155F-118202A4-2AFD22DD", + "x": 259073, + "y": 91984, + "z": -1514 + }, + { + "id": "BP_WAT2_C_26", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "37BCBF6C-4140F8C6-9B67C5A3-B963DD27", + "x": 263650, + "y": 12571, + "z": 1694 + }, + { + "id": "BP_WAT2_C_27", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "75CB5D56-4CEB2FD3-C12A4F91-4FE019BE", + "x": 219767, + "y": 24492, + "z": -1715, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_28", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "644E3D88-4D1718D4-A81D5BB1-54428172", + "x": 205782, + "y": 59101, + "z": -1537, + "rotation": -178.74 + }, + { + "id": "BP_WAT2_C_29", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "09F456E4-48806528-F63F97B8-FDAAF643", + "x": 295279, + "y": -293696, + "z": 6722 + }, + { + "id": "BP_WAT2_C_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D875F9C5-48CF3AF9-96C7C5BF-20EB6D14", + "x": 63992, + "y": -4939, + "z": 19352 + }, + { + "id": "BP_WAT2_C_30", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "7BA86298-4D5E57AF-D026EB98-97947FAB", + "x": 242430, + "y": -261110, + "z": 9879 + }, + { + "id": "BP_WAT2_C_31", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9DA69BB5-46569917-92570BB6-C14D3886", + "x": 218915, + "y": -247705, + "z": 8273 + }, + { + "id": "BP_WAT2_C_32", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E81EA80A-4089F558-E147D3A7-F4B706F5", + "x": 323970, + "y": -20408, + "z": -1447 + }, + { + "id": "BP_WAT2_C_33", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EC63CCD8-4CDA7AEA-4A6C1798-B379D241", + "x": 321860, + "y": -72077, + "z": 5669 + }, + { + "id": "BP_WAT2_C_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D4823087-4E31FA57-CB9B8A96-F521F575", + "x": 11210, + "y": -24138, + "z": 13450 + }, + { + "id": "BP_WAT2_C_5", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5E4E2ECE-4504F689-5051A9AB-C5B0E9FA", + "x": 50824, + "y": -32522, + "z": 13389 + }, + { + "id": "BP_WAT2_C_6", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "51705508-4244FF30-05E336A7-AD48DC10", + "x": 36281, + "y": -169656, + "z": 11787 + }, + { + "id": "BP_WAT2_C_7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8157E70F-4547BBD3-259CA192-EA3981C7", + "x": 13825, + "y": -166920, + "z": 12230 + }, + { + "id": "BP_WAT2_C_8", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F0347AE2-41FD384E-ED9C76BB-FFA524DC", + "x": 81278, + "y": -176513, + "z": 6840 + }, + { + "id": "BP_WAT2_C_9", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "60373F7F-41FBAC3A-F76C1785-E1B203D1", + "x": 74841, + "y": -200048, + "z": 14885 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0006401_1631706786", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "92FC1520-433039EC-2566679C-3ED974FE", + "x": 153515, + "y": 106900, + "z": 14153 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0156301_1631721457", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "98434CDB-4383A29A-7692E982-56AA04C1", + "x": 168160, + "y": 87610, + "z": 11633 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F01D6201_1939266754", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C939E840-4E6F4036-9ED58F8C-6C96AB68", + "x": 213635, + "y": -239505, + "z": 12831, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F01D6201_1995497755", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "57788A3A-48F6175D-C3A2FC82-9D8C8987", + "x": 233708, + "y": -215382, + "z": 4296 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F01E6201_1415384932", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "39AA5F5D-4A72D3DA-53E66C93-CEC48387", + "x": 263585, + "y": -232761, + "z": 1102 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0226201_2044450637", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1C5E230B-48E55627-E2057388-393326B8", + "x": 224784, + "y": -157918, + "z": 4518 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F025A401_1185385862", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "148B1A89-4CD19E19-788D50A0-5611E918", + "x": -121712, + "y": 127393, + "z": 15396 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F025A401_1318668863", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5C83ECAD-48F64C2A-AFD3D1B0-0F8E0F88", + "x": -115572, + "y": 147016, + "z": 13947 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F02AA401_1366971746", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "198CBBC7-4AB0CB21-33F2FDAE-2E991F37", + "x": -175535, + "y": 81270, + "z": 23923 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F02AA401_1546189749", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "03DEB96C-47B173F3-75886C96-0FF231A9", + "x": -161957, + "y": 63047, + "z": 18816, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F02AA401_1741245750", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "797B1010-4818E9EA-DE37A98D-6009161F", + "x": -173554, + "y": 113686, + "z": 19567 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F02AA401_1814961751", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1CF1BE37-4885FCF1-9AE821B1-B0FBCAC0", + "x": -137627, + "y": 116054, + "z": 15174 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0307C01_1078840569", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "CCA47C12-45F475E3-1C960EA8-260E9272", + "x": 112534, + "y": 67833, + "z": 16212 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0307C01_1582547570", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D56E1F54-4D54933F-FBE492A9-C52BB38C", + "x": 119667, + "y": 52084, + "z": 14186 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0317C01_1516355747", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "297B500D-435A46BA-CB00679E-68DE680B", + "x": 69901, + "y": 54545, + "z": 9747 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0317C01_2116555749", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "19687D59-42AD9818-87F0B0A2-0F226E8C", + "x": 84989, + "y": 34269, + "z": 11723 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0327C01_1340668927", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C63F7BAE-49DE8E86-6333FCB0-25259759", + "x": 125465, + "y": -288, + "z": 13751 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0327C01_1394521928", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E2E517CA-4F361105-5ABF3789-C1C714B8", + "x": 146425, + "y": -12458, + "z": 16369 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0327C01_1605927929", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C02EAA4E-41B11464-74116188-22A6C065", + "x": 198204, + "y": -157, + "z": 5407 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0337C01_1494320107", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5DC1A5FD-43EF2BB0-EF9D399A-52993D92", + "x": 207318, + "y": -30730, + "z": 13392 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0337C01_1544698108", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "740A0626-47DDC5DA-E326408C-5C7A8D88", + "x": 201926, + "y": 4550, + "z": 12999 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0337C01_1632970109", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9CD95B86-4752FE8E-3953C8A8-DF0A2B83", + "x": 187921, + "y": -20166, + "z": 11033 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0357C01_1629276462", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EDB7FE4C-464FEBE5-815B7C8F-D4164D5D", + "x": 160376, + "y": 7724, + "z": 16381 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0357C01_1830097463", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D8AB37FD-417573D2-67C23BB5-07C29F7F", + "x": 142195, + "y": 9509, + "z": 13395 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0367C01_1997582645", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3E0714E6-45FDED3B-400BB9AC-9CBDFF4A", + "x": 148721, + "y": 24496, + "z": 11566 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0397C01_1667868175", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D04EEA45-4B0588AE-66A0F8A0-F08BC23D", + "x": 214286, + "y": -10641, + "z": 7599 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0397C01_2086373177", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E283780B-475C4337-B9D92DA4-8CFABD2B", + "x": 157982, + "y": 50049, + "z": 11031 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F03A7C01_1499181354", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1978994D-49099521-7B6182B1-BDDD8C81", + "x": 137971, + "y": 67038, + "z": 12668 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F03A7C01_1798728356", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "29C0821B-46F9DD69-FC780BAB-4EBDFA70", + "x": 109133, + "y": 34192, + "z": 10379 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0426401_1291680381", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9DDE6B5C-4ACD8AF1-46A5AD96-2ACE10BC", + "x": 219541, + "y": 124266, + "z": 12483, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0447B01_1978893022", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5C51E098-4E9E3D6F-8EA609A4-2270C609", + "x": 83739, + "y": 37811, + "z": 9868, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0466401_1164905096", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0D79B734-4F76DA14-E9D0F4AF-99C4B6EB", + "x": 206068, + "y": 162596, + "z": 7363 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F05E6901_1906631590", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6EA63F0A-44FF6C0B-B6B1D7A4-4FEFFD27", + "x": -211160, + "y": 149809, + "z": -1733, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0607801_1378431781", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3754E55B-4B58674F-8EE9099C-1D98FDEF", + "x": 257367, + "y": -18485, + "z": 12447 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0737801_1258887144", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "54CD99EF-46C798AA-5AD0378A-12D1DDE2", + "x": 186351, + "y": -30838, + "z": 10333, + "rotation": 33.14 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F087BE01_1694514560", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "426C76A3-4B65B861-D080D9AC-94AAE1C5", + "x": -138788, + "y": 157630, + "z": 17468, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F087BE01_1872685562", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F2135F26-40744A09-60A5528D-69B2FB2C", + "x": -165854, + "y": 110202, + "z": 11182 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0A96F01_1534709120", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "DD787F2C-4640ABE3-6439408C-567E9254", + "x": -82211, + "y": 104580, + "z": 21784 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0A96F01_1573952121", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EA518AB7-44930BA5-909109B2-D48D9BCB", + "x": -75376, + "y": 54532, + "z": 22701 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0A96F01_1627684122", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D435276E-425A891D-62D425A3-D252FC01", + "x": -111264, + "y": 51997, + "z": 23991 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0A96F01_1658558123", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "B2CEFCFE-4BA63CF7-8CB5379A-DB5D4CA8", + "x": -54339, + "y": 81544, + "z": 20035, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0A96F01_1680839124", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9433F347-4FDB2355-6D513DBE-7C49485E", + "x": -188676, + "y": 39373, + "z": 16911 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0B36201_2026104153", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EB19F4E8-4BFDA8FC-E189B5AB-1A5CD2E7", + "x": 334931, + "y": -68308, + "z": 6118 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0B37301_1874710112", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "789B38FE-4871116D-9DF0E49B-FBD60E52", + "x": 110839, + "y": 48953, + "z": 21602 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0B46201_1162892331", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C903C909-478FA389-313FA89E-6136B4D0", + "x": 358514, + "y": -208960, + "z": 4184 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0B46201_1220705332", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "EA3D9C64-4470AF73-FC3B77A5-DECFEBBE", + "x": 369114, + "y": -229286, + "z": 4198 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C06801_1834655836", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "039C60F8-47567395-0E02CD99-7C5F4862", + "x": -271571, + "y": 104970, + "z": -1602 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C26101_1093316736", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "069E12E5-41DCAAEA-608E9D82-3F957987", + "x": -218216, + "y": -1453, + "z": 2008 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C46101_1131406096", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "701BD0E6-4F09D812-4CDEFBB8-31AA4A9C", + "x": -118884, + "y": -25804, + "z": 10832, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C46101_1178535097", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "46269AD1-410A2BC1-BB5405AD-7998666C", + "x": -172466, + "y": -3625, + "z": 12872, + "rotation": 46.82 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C46101_1232106098", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "31A8A816-4AEE6A0A-0DBC6296-5587B7C5", + "x": -180297, + "y": -22474, + "z": 3242 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0C86101_1887823809", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A71E31F3-44D3DC60-D931BB8D-13255814", + "x": -182103, + "y": -90640, + "z": 1903 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0EE7A01_1386093905", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A2154C4D-4CFC2C40-35C38088-F077279F", + "x": 100696, + "y": 17546, + "z": 19038 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F07A01_1459225259", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "02252F54-444BD7E0-E0EBF888-62445462", + "x": 127207, + "y": 16821, + "z": 13038, + "rotation": -0.07 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F17A01_1899547441", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5CD98409-47B53943-BB76F38F-C97630F8", + "x": 141000, + "y": 18583, + "z": 10929 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F17A01_2106088442", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "53439F7A-44288DAC-ACF7FA8A-20B67C27", + "x": 237050, + "y": -36906, + "z": 17298 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F27A01_1632980625", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2104CE58-438A5479-A9BC09AF-E3A78498", + "x": 131878, + "y": 30168, + "z": 10341 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F47A01_2000362987", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3B530996-4C24E4D0-51E80E97-490CBF50", + "x": 163226, + "y": -26023, + "z": 19627 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0FA6801_1163136984", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "B9D197EB-409EA313-B701A9BC-06DA5082", + "x": -171165, + "y": 159617, + "z": 247, + "rotation": -0.36 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0FD6301_1551743241", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "24F2428F-4C4A9B24-701F1CB5-387D469D", + "x": 176140, + "y": 83550, + "z": 15118 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0FD6301_1747414245", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2E6EED32-499AF2D5-CEBA05B7-B93F6A0B", + "x": 188755, + "y": 137135, + "z": 12000 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F79117901_1226789931", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "E4706093-466788C5-4860559A-C3C4DF5C", + "x": 381622, + "y": -67653, + "z": -315 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F793DA801_1139638307", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9490C2F2-4A5607DA-2BAC6AA4-65D144BA", + "x": 211420, + "y": -83564, + "z": 11761, + "rotation": -9.13 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F794FA001_1689147024", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C0C5E218-4DED7713-EA443EAC-AEA84A0C", + "x": 289400, + "y": -150062, + "z": 3508, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F79A4A101_1223072041", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "319E260B-451DF23B-DED942B1-63C60E25", + "x": 239346, + "y": -109822, + "z": 3526, + "rotation": -86.36 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F79AD9B01_1500683292", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "236937AC-48019E53-71C9BA8C-00E79C4F", + "x": 348549, + "y": -81514, + "z": 2425, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_40B076DF2F79D68D01_1178565720", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6AFC916C-4B4513E7-06A4AABD-13AA631A", + "x": 106404, + "y": -7677, + "z": 15484, + "rotation": 2.24 + }, + { + "id": "BP_WAT2_UAID_40B076DF2F796A7301_1664532256", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0B862D43-4CC56678-2B5669B3-27D38AC4", + "x": -6984, + "y": -130403, + "z": 21074 + }, + { + "id": "BP_WAT30_14", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4711820D-426825CF-5BD6C29D-EF814BFF", + "x": 184820, + "y": -49218, + "z": 5642 + }, + { + "id": "BP_WAT31_17", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "15DD7658-492D8763-1FEB8EAF-064829A2", + "x": 153167, + "y": -110705, + "z": -1196 + }, + { + "id": "BP_WAT31_UAID_40B076DF2F7932A801_1176269369", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BAED8500-4A86196C-13F73D99-C7CE295E", + "x": 193778, + "y": -100454, + "z": 2417 + }, + { + "id": "BP_WAT31_UAID_40B076DF2F7932A801_1267200370", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0F36C8FA-462A0B87-4264F1A8-22FC777B", + "x": 123039, + "y": -124467, + "z": 3789 + }, + { + "id": "BP_WAT32_20", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2A2918A4-4C0E1BE6-3E55C5BC-C8192CDA", + "x": 162692, + "y": -73458, + "z": -23 + }, + { + "id": "BP_WAT33_18", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "08C39190-427E23A7-3BFD05B1-531E1232", + "x": 200555, + "y": -303560, + "z": 22415 + }, + { + "id": "BP_WAT38_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2DBD26A9-4D8CA0A2-8B591EA4-A679E3FD", + "x": -127076, + "y": -148618, + "z": 3998, + "rotation": 31.61 + }, + { + "id": "BP_WAT39_3", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "71FBE48D-42A1A013-91130D80-067BE492", + "x": 229726, + "y": -189200, + "z": 6126 + }, + { + "id": "BP_WAT3_4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5FE8C05A-4F0EF638-AB374D89-A1DC5F3E", + "x": 25599, + "y": -118720, + "z": 12504 + }, + { + "id": "BP_WAT3_UAID_40B076DF2F795E7801_1675473424", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "7788B4E0-40385436-5F2F7591-41E57FDA", + "x": 67057, + "y": -77630, + "z": 7999 + }, + { + "id": "BP_WAT3_UAID_40B076DF2F795E7801_2071161425", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "06A7B7FF-49E8ABFC-B30B42AB-FC015118", + "x": 40287, + "y": -96469, + "z": 9343 + }, + { + "id": "BP_WAT3_UAID_40B076DF2F796A7301_1820015257", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "BB8A8F41-45819B03-E18C5F8B-81F98BC1", + "x": 42209, + "y": -118679, + "z": 11370 + }, + { + "id": "BP_WAT4", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3AFE054A-46DA2B26-4280979E-4A84FCD8", + "x": 37400, + "y": -112562, + "z": 10781 + }, + { + "id": "BP_WAT40", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "AD944DE3-41178E96-1944CE97-44FE3A57", + "x": 360182, + "y": -240032, + "z": 3097, + "rotation": 0 + }, + { + "id": "BP_WAT40_UAID_04421A9713F0D2E401_2079149903", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "91403B5F-40AB8101-E6142BB7-85FE70D2", + "x": 373689, + "y": -250924, + "z": -1004 + }, + { + "id": "BP_WAT41", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "82C29773-4D7C1DF1-2B3840B2-214CEC72", + "x": 322307, + "y": -107934, + "z": 12733 + }, + { + "id": "BP_WAT42", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3100FC09-4A94700A-7727F398-EA36A950", + "x": 386922, + "y": -221023, + "z": 5182 + }, + { + "id": "BP_WAT43", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2465BBEC-4BC2E76B-204B5C82-809202A1", + "x": 286959, + "y": -235733, + "z": 3060, + "rotation": 85.41 + }, + { + "id": "BP_WAT44", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "AE9D5CC3-46215937-134C3D88-986AC4FB", + "x": 318099, + "y": -283229, + "z": -1757 + }, + { + "id": "BP_WAT45_7909", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F6C2C51F-45CEAA58-A4D88B82-22F52B46", + "x": 160626, + "y": 43688, + "z": 18793, + "rotation": -61.85 + }, + { + "id": "BP_WAT48", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "51F5DE55-447CF903-F4273288-306BE788", + "x": 387614, + "y": -117122, + "z": 8561 + }, + { + "id": "BP_WAT51", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8A08E094-42B8487B-53B292AC-FCE54A7D", + "x": 234389, + "y": -224003, + "z": 13623 + }, + { + "id": "BP_WAT53", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "DCF3F56E-466B5030-FD87FB9B-C78BB537", + "x": 240622, + "y": -287487, + "z": 18486 + }, + { + "id": "BP_WAT54", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "64DAD7C2-48397306-D2A73E92-667A1F84", + "x": 362305, + "y": -95286, + "z": 8769, + "rotation": 0 + }, + { + "id": "BP_WAT54_UAID_40B076DF2F793C8101_1652897954", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A727C09C-47FC2F11-15F77FA8-CA587EE2", + "x": 385675, + "y": -137041, + "z": 7326 + }, + { + "id": "BP_WAT5_8", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "9DE777FD-4F451C95-3A3F29A3-8C927B73", + "x": -24001, + "y": -91327, + "z": 9658, + "rotation": 0 + }, + { + "id": "BP_WAT60", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C93CA35F-49005D1B-A650F4BC-FD960CB1", + "x": 291518, + "y": -46168, + "z": 6850, + "rotation": 0 + }, + { + "id": "BP_WAT61", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1D1561A5-42225776-0A134589-9C9CBE69", + "x": 268603, + "y": -69678, + "z": 9574 + }, + { + "id": "BP_WAT65_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "81A7F1CE-4B06F616-C2C2EA92-8032D058", + "x": 272302, + "y": -296610, + "z": 6152 + }, + { + "id": "BP_WAT69", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "40955E7B-4A58693F-62D1DEB8-12144969", + "x": 293822, + "y": -78078, + "z": 5731 + }, + { + "id": "BP_WAT6_7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6C7EFC16-494ED934-CB11EF8C-032B6302", + "x": 4719, + "y": -96922, + "z": 14819 + }, + { + "id": "BP_WAT7", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8A73AC64-4956BDFB-4E268B99-D945562E", + "x": 77418, + "y": -119983, + "z": 14084 + }, + { + "id": "BP_WAT71", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6A756BBC-42FAD05B-917094A8-0776FBF1", + "x": 231785, + "y": -245638, + "z": 1262 + }, + { + "id": "BP_WAT73", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "81042D84-428C3F35-181EE0A1-48BC67BB", + "x": -72019, + "y": -224771, + "z": 5750 + }, + { + "id": "BP_WAT73_2", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "D8FB1AD5-48542500-FC66639D-9A017DCD", + "x": 164330, + "y": -222745, + "z": 8907 + }, + { + "id": "BP_WAT75", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2636D40A-4D17D9C8-84CCA2B2-DAFCF7AE", + "x": 261248, + "y": -256629, + "z": 6537, + "rotation": 43.98 + }, + { + "id": "BP_WAT76", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C11F0A33-4F530145-910C24B4-5DA8F580", + "x": 225044, + "y": -215833, + "z": 1284 + }, + { + "id": "BP_WAT77", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "B72466F2-44097A69-51451492-2A1FEA45", + "x": -42083, + "y": -210767, + "z": 2649 + }, + { + "id": "BP_WAT78", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8D8B983F-4E7312B9-C695F8AF-161D9E26", + "x": 372417, + "y": -186063, + "z": 3947, + "rotation": 0 + }, + { + "id": "BP_WAT7_UAID_40B076DF2F795F7801_1562250602", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "0FA9FEAB-4756E4C8-9E06DBA6-B5CF66E0", + "x": 122159, + "y": -103309, + "z": 17467 + }, + { + "id": "BP_WAT80", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "5EA0E224-4E3E90EC-1F0719B5-8C2038D2", + "x": -61293, + "y": -180147, + "z": -160 + }, + { + "id": "BP_WAT81", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "75D30984-41CCEE90-59C8258A-EAABA5EE", + "x": 233629, + "y": -299693, + "z": 1314 + }, + { + "id": "BP_WAT82", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "C7DDB052-45B3A0F7-2DB82E8F-228E1606", + "x": 253770, + "y": -284738, + "z": 2899 + }, + { + "id": "BP_WAT84", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1A6A2B47-44BD88C3-46F725BD-D9AC48E1", + "x": -4750, + "y": -201852, + "z": -956 + }, + { + "id": "BP_WAT85", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3D8E5216-4DF10713-5E1B2DA5-EE1F0BAF", + "x": -8991, + "y": -246390, + "z": -737 + }, + { + "id": "BP_WAT86_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2A732B7B-45893E91-3008AA9E-4DEFA7B9", + "x": 203300, + "y": -221365, + "z": 24626 + }, + { + "id": "BP_WAT87", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "2913D06F-45BB18EF-FCCD1CBA-84E9A706", + "x": 118885, + "y": 41788, + "z": 9846 + }, + { + "id": "BP_WAT87_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "61193565-4DD40769-37D5FF93-13CCCC8F", + "x": 214480, + "y": -278185, + "z": 20571 + }, + { + "id": "BP_WAT88", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "8DA17889-4B8876B9-CFB87B8D-C0909DD7", + "x": 26512, + "y": -233953, + "z": 2582, + "rotation": 0 + }, + { + "id": "BP_WAT89", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "43B1BA72-4DFF81DB-89092B8A-3585192E", + "x": 57796, + "y": -250997, + "z": 1570, + "rotation": 0 + }, + { + "id": "BP_WAT8_5", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "56501348-466906B5-76D9F1B9-EB059F8B", + "x": -72810, + "y": -87490, + "z": -55, + "rotation": 18.54 + }, + { + "id": "BP_WAT9", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "61C2C918-45997A31-86F043BE-A5513645", + "x": 182016, + "y": 93031, + "z": 6673 + }, + { + "id": "BP_WAT90", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "291EE456-47D85E6B-05866BA3-5D0767DD", + "x": 76962, + "y": -216831, + "z": 2721 + }, + { + "id": "BP_WAT91_0", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "1428706C-49CD2A42-2CBBE294-023E07CA", + "x": 146780, + "y": -136900, + "z": 16336 + }, + { + "id": "BP_WAT91_340", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "028EFFD2-4AA7A7C4-772B6681-34AA94BB", + "x": 3345, + "y": 220744, + "z": -1998, + "rotation": 19.47 + }, + { + "id": "BP_WAT93", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "F0C830A0-4D676C89-DCA6C0B0-B8D9CD42", + "x": 60222, + "y": -214157, + "z": -1077 + }, + { + "id": "BP_WAT94_1", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "4EAA84A9-4025A186-864EC58D-7FE958E4", + "x": 123320, + "y": -152385, + "z": 19742 + }, + { + "id": "BP_WAT94_781", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "3F5D006A-4EBD06FB-D20F88A7-5B44E83E", + "x": 67301, + "y": 243297, + "z": 1172 + }, + { + "id": "BP_WAT95_2253", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "A74E2750-44E88FFC-AB3CAF85-320391FD", + "x": 48269, + "y": 216631, + "z": -6017 + }, + { + "id": "BP_WAT96_3047", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "6D3D8267-43926749-27FF08BA-A420051A", + "x": 35887, + "y": 239512, + "z": -5054 + }, + { + "id": "BP_WAT97_3991", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "B63D5B8C-4D00D91C-390FA194-386D9361", + "x": 86134, + "y": 162054, + "z": 1756 + }, + { + "id": "BP_WAT98_5463", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "791746A1-4C60C7BF-B8CEE083-99CD084F", + "x": 75108, + "y": 202370, + "z": 3426, + "rotation": -180 + }, + { + "id": "BP_WAT99_2052", + "type": "mercerSphere", + "classPath": "BP_WAT2_C", + "pickupGuid": "36B95C44-4B8635ED-8908E08A-110F70B6", + "x": 51762, + "y": 202143, + "z": -1561, + "rotation": -30.87 + }, + { + "id": "BP_Crystal1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "934EE090-42204C4D-C3A74D9D-FC3549E6", + "x": -211813, + "y": -140593, + "z": 3107, + "rotation": 1.45 + }, + { + "id": "BP_Crystal10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DCC88194-4F809EEE-EAFB6F86-508863D3", + "x": -260892, + "y": -152811, + "z": 992, + "rotation": 132.36 + }, + { + "id": "BP_Crystal102", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "787ABF70-41A0E8B8-68C3DFA7-52D0F50B", + "x": 141380, + "y": -159269, + "z": 5767, + "rotation": 4.99 + }, + { + "id": "BP_Crystal10_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A33E0DB4-4615A9FA-0572AAB7-04568F35", + "x": 11565, + "y": 275010, + "z": -5486, + "rotation": 64.93 + }, + { + "id": "BP_Crystal10_10171", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9A315BDB-478C2E2B-A26510A6-153E54A0", + "x": -4715, + "y": -42869, + "z": 16427, + "rotation": 36.62 + }, + { + "id": "BP_Crystal10_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3A4FBDCB-4A79BE17-EBA33EA6-9E898635", + "x": 133091, + "y": -81206, + "z": 12308, + "rotation": 124.95 + }, + { + "id": "BP_Crystal10_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6045E03E-4316AE7F-854A6EB5-2DFA3C0B", + "x": -195259, + "y": 12077, + "z": 11902, + "rotation": -47.18 + }, + { + "id": "BP_Crystal10_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BF973157-45919CBB-AF5002BF-34BFB0A9", + "x": 153655, + "y": 257705, + "z": -926, + "rotation": 1.39 + }, + { + "id": "BP_Crystal10_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "90339A04-43F9DD3B-FC1A79B7-0D50400E", + "x": -2630, + "y": -141809, + "z": 15899, + "rotation": -86.77 + }, + { + "id": "BP_Crystal10_79", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9D6227C2-4A975545-AF4A2AB6-29422AD3", + "x": 227730, + "y": 60731, + "z": -1641, + "rotation": -178.22 + }, + { + "id": "BP_Crystal11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E67DA037-49FE2BFE-9B843D93-5E6B175B", + "x": -190230, + "y": -196400, + "z": 5527, + "rotation": 106.05 + }, + { + "id": "BP_Crystal111_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "72E38BAE-40E50771-E8C6EA98-400C9468", + "x": 129030, + "y": -138605, + "z": 12648, + "rotation": 24.97 + }, + { + "id": "BP_Crystal113", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3D9DA597-4E241461-E6B61EB9-C4154021", + "x": 281911, + "y": -152776, + "z": 3937, + "rotation": -39.25 + }, + { + "id": "BP_Crystal114", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "59D2B960-4CE511B0-5500AEA6-4F82EE37", + "x": 277271, + "y": -238507, + "z": 4069, + "rotation": 25.48 + }, + { + "id": "BP_Crystal115", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E8300EC3-49BEF71A-40B7FB90-72A97260", + "x": 236718, + "y": -237214, + "z": 3417, + "rotation": -59.53 + }, + { + "id": "BP_Crystal117", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "527BAD2A-4014BE20-470ACABA-D3983AC1", + "x": 300855, + "y": -94145, + "z": 8791, + "rotation": 160.26 + }, + { + "id": "BP_Crystal117_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "70DFB103-4D84B607-550D1A96-1AAF8D3A", + "x": 188356, + "y": -152668, + "z": 23561, + "rotation": -142.03 + }, + { + "id": "BP_Crystal118", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A72BEDED-4BA9E56E-AA28F8AB-DBBABC06", + "x": 287542, + "y": -87886, + "z": 8996, + "rotation": 8.49 + }, + { + "id": "BP_Crystal119", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "46B7912F-4462E9E0-86B2028E-80E1C714", + "x": 244057, + "y": -240028, + "z": 2539, + "rotation": -168.38 + }, + { + "id": "BP_Crystal11_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E7492FE8-4B43F8D7-C432B6A3-A1FF86B6", + "x": 14477, + "y": 230072, + "z": 7250, + "rotation": -161.96 + }, + { + "id": "BP_Crystal11_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6DBF4777-425D31BB-BF4167A8-5893489A", + "x": -53806, + "y": 300084, + "z": -6723, + "rotation": -90.66 + }, + { + "id": "BP_Crystal11_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BFF5153D-4F63FC13-76E2179C-72E0FCC1", + "x": 135312, + "y": -58728, + "z": 10235, + "rotation": 48.97 + }, + { + "id": "BP_Crystal11_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C19D6FB0-4499E0FD-BCAD30A5-2EF2A32A", + "x": -151934, + "y": -40291, + "z": 2864, + "rotation": 137.98 + }, + { + "id": "BP_Crystal11_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C45E0821-465E8186-52A9ABB8-8FF6ACCB", + "x": -2097, + "y": -110265, + "z": 11561, + "rotation": 107.24 + }, + { + "id": "BP_Crystal11_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0684FF78-45A70047-790D908E-4F548ADE", + "x": 115017, + "y": 178031, + "z": -5253, + "rotation": 15.22 + }, + { + "id": "BP_Crystal11_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D742D23E-4EF8EC30-F8EBD188-8CAF8CB7", + "x": 126617, + "y": 155745, + "z": 6756, + "rotation": -50.6 + }, + { + "id": "BP_Crystal11_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0F2727CC-43F6078E-5ABC39B3-39622F42", + "x": 80426, + "y": -19573, + "z": 13645, + "rotation": -152.5 + }, + { + "id": "BP_Crystal11_80", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1BF08A33-47B9E664-6C3EDAA2-4DB15FD4", + "x": 246886, + "y": 69222, + "z": -1708, + "rotation": -23.83 + }, + { + "id": "BP_Crystal12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0AE23600-4189DF19-90F183B5-63705004", + "x": -159879, + "y": -184973, + "z": 18190, + "rotation": 144.8 + }, + { + "id": "BP_Crystal121_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D09BA73D-45B2DD64-EBC85884-A26C3275", + "x": 168015, + "y": -241040, + "z": 5530, + "rotation": 165.73 + }, + { + "id": "BP_Crystal122_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0474C59C-446145A2-0EEF4BB3-7999F924", + "x": 150560, + "y": -223335, + "z": -569, + "rotation": -18.2 + }, + { + "id": "BP_Crystal125", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "238C76FF-40D24400-9824FD9F-1F2695F4", + "x": 128250, + "y": -166055, + "z": 4005, + "rotation": -36.08 + }, + { + "id": "BP_Crystal125_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "32260DF3-4302BD71-286FE0B3-74423F17", + "x": 205824, + "y": -156941, + "z": 22062, + "rotation": -18.84 + }, + { + "id": "BP_Crystal126", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CF6C08D3-47C1E45C-E04C7E89-57B305D2", + "x": 144750, + "y": -200675, + "z": 2340, + "rotation": -48.57 + }, + { + "id": "BP_Crystal127", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9869F19E-4DDD9DD6-6DB05B90-57F95B47", + "x": 311190, + "y": -109510, + "z": 10724, + "rotation": -176.09 + }, + { + "id": "BP_Crystal129", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A96BB88A-4539181C-065D35AE-9EE07EF9", + "x": 241707, + "y": -310697, + "z": 6774, + "rotation": 1.67 + }, + { + "id": "BP_Crystal12_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1E01E9B6-48DF07CC-BA42269B-E29FB4DE", + "x": 114001, + "y": -14368, + "z": 16178, + "rotation": -0.47 + }, + { + "id": "BP_Crystal12_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DAAB75A2-45BA9384-02987A99-FB19D7E5", + "x": 19407, + "y": 247199, + "z": 3313, + "rotation": 148.52 + }, + { + "id": "BP_Crystal12_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D9A1977A-410F63A9-F25B3CB5-0367589B", + "x": -39480, + "y": 276042, + "z": -2511, + "rotation": 165.62 + }, + { + "id": "BP_Crystal12_12002", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4EDA3021-44BB9E38-57F7B7BA-419AE6FA", + "x": -63923, + "y": -73396, + "z": 17319, + "rotation": 50.69 + }, + { + "id": "BP_Crystal12_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "44DC01E6-43643BF4-15984BBE-0A0BCB09", + "x": -169211, + "y": 75979, + "z": 2372, + "rotation": 156.34 + }, + { + "id": "BP_Crystal12_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1B081912-47C2D145-03C84BA7-4510EE9A", + "x": 179796, + "y": -53462, + "z": 10520, + "rotation": -8.61 + }, + { + "id": "BP_Crystal12_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "88493FF2-4D924D8C-F0F64496-8449C38E", + "x": -177366, + "y": -44370, + "z": -1752, + "rotation": 1.25 + }, + { + "id": "BP_Crystal12_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0741A0C5-41CC6A7E-B120BABF-F07363EE", + "x": 132181, + "y": 175558, + "z": -2331, + "rotation": 53.44 + }, + { + "id": "BP_Crystal12_43", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CC47B1B7-45311E6E-2A5227A5-78116E84", + "x": 46672, + "y": 126416, + "z": 13982, + "rotation": 29.36 + }, + { + "id": "BP_Crystal12_81", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3841DA6C-41BA1E87-2A6131B9-E8055D61", + "x": 219926, + "y": 85262, + "z": -558, + "rotation": 73.38 + }, + { + "id": "BP_Crystal13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1B86F621-487C0068-C259C2AB-DA5BFE27", + "x": -136606, + "y": -184491, + "z": 15781, + "rotation": -95.82 + }, + { + "id": "BP_Crystal132", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D7FA0CD9-4E012C5B-AEAEEA88-4E988CD7", + "x": 286874, + "y": -288896, + "z": 4090, + "rotation": -118.83 + }, + { + "id": "BP_Crystal133_1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A5239852-422F0A78-7B51518F-28E84940", + "x": 314685, + "y": -102611, + "z": 7744, + "rotation": 32.2 + }, + { + "id": "BP_Crystal135", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "17C6AC4A-440C626C-A0E6B0BF-3374946C", + "x": 324586, + "y": -53434, + "z": 7148, + "rotation": -150.91 + }, + { + "id": "BP_Crystal13_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C69E5A7F-4924205F-B96B5382-92BCB497", + "x": 130111, + "y": -26602, + "z": 9200, + "rotation": -0.76 + }, + { + "id": "BP_Crystal13_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2FDCE908-4FED4A6F-F79056B1-361BC133", + "x": 43384, + "y": 252867, + "z": -117, + "rotation": 94.79 + }, + { + "id": "BP_Crystal13_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "73BCC18D-4BB92B2A-F1F64D98-6FF66BF8", + "x": -18183, + "y": 263639, + "z": 1988, + "rotation": -167.24 + }, + { + "id": "BP_Crystal13_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E62CE891-4A619AD2-E2279CAD-41AF994F", + "x": 182198, + "y": -74181, + "z": 4147, + "rotation": -24.2 + }, + { + "id": "BP_Crystal13_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "29A49261-4372BBB6-7E7159A4-65241764", + "x": -164419, + "y": -14324, + "z": 6185, + "rotation": -26.81 + }, + { + "id": "BP_Crystal13_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E63BE936-4CF8F53F-F46B71A6-EEB1876C", + "x": -93864, + "y": -22672, + "z": 17598, + "rotation": -83.85 + }, + { + "id": "BP_Crystal13_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "262C96EF-44F517BA-CAEDC3B8-8BB41E23", + "x": 127141, + "y": 163917, + "z": 2337, + "rotation": 127.43 + }, + { + "id": "BP_Crystal13_44", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B9462000-43244CC4-1333CC9C-70A46B71", + "x": 53796, + "y": 108947, + "z": 11439, + "rotation": 8.53 + }, + { + "id": "BP_Crystal13_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0BB27937-436CFF22-A7B8BCAB-0875DC0C", + "x": 341345, + "y": -65035, + "z": 991, + "rotation": 79.33 + }, + { + "id": "BP_Crystal13_82", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1EED22B4-42903186-C4CE6D9A-DB82F936", + "x": 223798, + "y": 99283, + "z": 1474, + "rotation": -14.58 + }, + { + "id": "BP_Crystal13_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3845EF8B-4515A6C8-821649B3-4EAF1E71", + "x": 3859, + "y": -64057, + "z": 15100, + "rotation": -148.79 + }, + { + "id": "BP_Crystal13_UAID_40B076DF2F79AC9B01_1616270112", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "03134124-4B634C6F-E45EB39A-B34694EE", + "x": 358645, + "y": -86415, + "z": 2671, + "rotation": -69.14 + }, + { + "id": "BP_Crystal13_UAID_40B076DF2F79AD9B01_1733787293", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B507878A-4F2B4143-D92B90AB-3E7A1EFE", + "x": 332495, + "y": -60520, + "z": 1561, + "rotation": 50.48 + }, + { + "id": "BP_Crystal14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "10E8F5EE-4FB54847-2377D3A5-24833859", + "x": -136481, + "y": -168497, + "z": 21129, + "rotation": -113.08 + }, + { + "id": "BP_Crystal141", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "08605562-45B4A69A-7D6E56BD-2B7E6CB5", + "x": 241463, + "y": -226396, + "z": 1931, + "rotation": -113.21 + }, + { + "id": "BP_Crystal146", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4BDD2446-481BD496-205C4591-35483DBB", + "x": 373734, + "y": -126247, + "z": 5814, + "rotation": 10.23 + }, + { + "id": "BP_Crystal147", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9D46E507-422067A4-01AF4ABD-D4624271", + "x": 362441, + "y": -91302, + "z": 6713, + "rotation": 69.15 + }, + { + "id": "BP_Crystal148", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6BF02D7C-4491A03D-0DF1949E-BCF31772", + "x": 365617, + "y": -140136, + "z": 4930, + "rotation": -175.45 + }, + { + "id": "BP_Crystal149", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EAD8CC1A-4A4673A2-D41E6FA4-2B6F5688", + "x": 350118, + "y": -96867, + "z": 8058, + "rotation": -161.67 + }, + { + "id": "BP_Crystal14_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "35E048DF-46901B1E-B84DC1A1-4DE2FA5A", + "x": 39873, + "y": 1630, + "z": 17326, + "rotation": -94.2 + }, + { + "id": "BP_Crystal14_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "81419EA4-4B858C72-AD5C6CA3-DEEE500F", + "x": 96438, + "y": -39584, + "z": 10002, + "rotation": 102.25 + }, + { + "id": "BP_Crystal14_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FD635F5B-4BD39E23-1BB98184-D7393A60", + "x": 45527, + "y": 226860, + "z": 1279, + "rotation": -165.92 + }, + { + "id": "BP_Crystal14_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7CDE8F6E-45BC2D4C-F987E3B4-6F7900B7", + "x": -18094, + "y": 272198, + "z": -5811, + "rotation": -30.03 + }, + { + "id": "BP_Crystal14_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "99D83268-4E51C98C-7003A8A3-B18EDA58", + "x": -176793, + "y": 79808, + "z": 12266, + "rotation": -180 + }, + { + "id": "BP_Crystal14_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C4029336-4A16E2D4-A5F3C48B-516DB6F3", + "x": 158493, + "y": -56806, + "z": 4905, + "rotation": 125.48 + }, + { + "id": "BP_Crystal14_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E3F2878E-4E07F159-5122B9A6-AB3650A9", + "x": -137332, + "y": -51820, + "z": 8183, + "rotation": 84.77 + }, + { + "id": "BP_Crystal14_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "057BA1D5-41AFB449-065867AA-E800D51D", + "x": 175704, + "y": 225212, + "z": -3270, + "rotation": -108.39 + }, + { + "id": "BP_Crystal14_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "68E52DF1-4E00FAAE-4C1A9A8F-DCC28D8E", + "x": 311602, + "y": -26396, + "z": 411, + "rotation": 175.99 + }, + { + "id": "BP_Crystal14_83", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B07A59EA-48EBECF9-87CA3DAA-43C5664B", + "x": 213708, + "y": 85508, + "z": 391, + "rotation": 20.26 + }, + { + "id": "BP_Crystal15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "07DE27FD-49A91FCC-53B2A79F-76CBF7E9", + "x": -208852, + "y": -151187, + "z": 10258, + "rotation": 75.63 + }, + { + "id": "BP_Crystal150", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "809E006E-494B10AB-4FBDB484-08F90BC5", + "x": 362063, + "y": -120812, + "z": 8665, + "rotation": -168.88 + }, + { + "id": "BP_Crystal151", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "342B499E-402C7E58-E50046A6-508FF12D", + "x": 384480, + "y": -133837, + "z": 6067, + "rotation": 149.04 + }, + { + "id": "BP_Crystal152", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B7E7EC19-43DB010B-A84B91B4-EEBB0229", + "x": 398590, + "y": -134014, + "z": 5891, + "rotation": -168.34 + }, + { + "id": "BP_Crystal153", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E68BD637-4C0FD29C-4F7BBD83-6D197CFE", + "x": 401055, + "y": -144113, + "z": 4978, + "rotation": 5.35 + }, + { + "id": "BP_Crystal154", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FB1D50AC-41DE9110-C732B2A2-6982E4AA", + "x": 406175, + "y": -148724, + "z": 7051, + "rotation": -149.22 + }, + { + "id": "BP_Crystal155", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "687F3088-451590A2-1E79ACBC-02BF569F", + "x": 395150, + "y": -118250, + "z": 5637, + "rotation": -93.11 + }, + { + "id": "BP_Crystal156", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "221C3F58-411380C3-8C14D4B9-143E9B53", + "x": 363097, + "y": -112041, + "z": 6612, + "rotation": 0.36 + }, + { + "id": "BP_Crystal15_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "255CFB14-4C5AB19E-7C7778BA-EB13CF2D", + "x": 335963, + "y": -21417, + "z": -1221, + "rotation": -157.86 + }, + { + "id": "BP_Crystal15_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "065B8F08-48BC5F43-EEC3678F-DBD76A36", + "x": 55917, + "y": -16436, + "z": 13119, + "rotation": 112.96 + }, + { + "id": "BP_Crystal15_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8EBA01E8-4FA0B480-67EA1BAD-3A1FA730", + "x": 114624, + "y": -46728, + "z": 8322, + "rotation": 70.99 + }, + { + "id": "BP_Crystal15_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "54905EB8-49454814-79098B82-4038015A", + "x": -4366, + "y": 290254, + "z": 2358, + "rotation": -75.2 + }, + { + "id": "BP_Crystal15_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1E3948DA-4AE5356F-19D8D6B9-62B51EFE", + "x": -165979, + "y": 186656, + "z": 2318, + "rotation": 48.53 + }, + { + "id": "BP_Crystal15_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6D66B76D-42E0A589-B9C0ADBD-67F981A4", + "x": 212048, + "y": -59078, + "z": 19267, + "rotation": -95.27 + }, + { + "id": "BP_Crystal15_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1E66543C-4A7F9173-8487508B-E9D877FC", + "x": 165282, + "y": 181677, + "z": -2581, + "rotation": -0.87 + }, + { + "id": "BP_Crystal15_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EC805180-462EAE19-44FFCDAC-C3D16654", + "x": -12630, + "y": -77361, + "z": 11183, + "rotation": 127.53 + }, + { + "id": "BP_Crystal15_84", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "17BD2A02-485646C8-B370F4A6-A00A7730", + "x": 238686, + "y": 96477, + "z": 893, + "rotation": -15.12 + }, + { + "id": "BP_Crystal16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5FE67E05-4CFAAA51-3FB6D488-E63B0641", + "x": 240041, + "y": 80233, + "z": -475, + "rotation": 157.46 + }, + { + "id": "BP_Crystal16_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "97022111-43FCFD3C-72FEAB8D-739EF099", + "x": -43197, + "y": -102533, + "z": 2430, + "rotation": -23.2 + }, + { + "id": "BP_Crystal16_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "47328AAD-4C9CBBA8-0A18E18B-4859D890", + "x": -10340, + "y": -73590, + "z": 11090, + "rotation": -134.56 + }, + { + "id": "BP_Crystal16_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3121A279-4B160959-C57F3797-6AAEFC66", + "x": -44645, + "y": 229140, + "z": 278, + "rotation": 52.49 + }, + { + "id": "BP_Crystal16_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "810049E3-4BF9AF74-37EC2C8D-E562376C", + "x": -127715, + "y": -83531, + "z": 2465, + "rotation": -79.19 + }, + { + "id": "BP_Crystal16_22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0C47ECC1-4012FECD-D639D0AD-3969B8C9", + "x": 166727, + "y": 203701, + "z": -3606, + "rotation": 133.84 + }, + { + "id": "BP_Crystal17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A6559C8E-47CEA068-41B4B496-93E2D515", + "x": 47130, + "y": -198733, + "z": 914, + "rotation": 132.44 + }, + { + "id": "BP_Crystal17_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C6F784F1-49646E0E-0C6423BB-3674A0DB", + "x": -8220, + "y": -69410, + "z": 10830, + "rotation": 13.71 + }, + { + "id": "BP_Crystal17_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "429D7658-4F77BB0E-1C3D41A8-620960C8", + "x": 180560, + "y": 189919, + "z": -2922, + "rotation": 46.31 + }, + { + "id": "BP_Crystal17_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "632A7B79-43C9A35E-45C55DA8-3C52E30A", + "x": -97705, + "y": 200305, + "z": 2836, + "rotation": 4.14 + }, + { + "id": "BP_Crystal17_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "088C07AF-492A270A-0ACCE097-3AA8F412", + "x": 21591, + "y": -84423, + "z": 18500, + "rotation": -15.55 + }, + { + "id": "BP_Crystal17_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4444267C-4426F32D-B16153A4-F9055462", + "x": -124509, + "y": -71728, + "z": 1736, + "rotation": -180 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79175C01_2025354360", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7FA2FB21-490418B8-D9876AAA-7FD6987A", + "x": 92480, + "y": -163999, + "z": 2322, + "rotation": -148.21 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79755B01_1333072849", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3E42CDF4-46726221-214976B1-649ADAFD", + "x": 33857, + "y": -178775, + "z": 973, + "rotation": -59.01 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79755B01_1353668850", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DC6392B3-495D299B-E08055B3-8E42B3BE", + "x": 21496, + "y": -184696, + "z": 2483, + "rotation": -137.02 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79845B01_1828836491", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BA0CD317-4D9938E4-8363C292-ECD96835", + "x": 45852, + "y": -169787, + "z": 1674, + "rotation": -153.98 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79875B01_2021256020", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F7336922-43EA379E-29D86ABA-75ECBBA5", + "x": 54723, + "y": -163373, + "z": 2697, + "rotation": -178.4 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79F76301_1463908182", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "22E69A19-4B8935B1-A84EF8BF-E90ACDD6", + "x": 62992, + "y": -213664, + "z": 1913, + "rotation": 1.26 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79F76301_1548250183", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B7936B94-4300F106-6DCDC7A7-4A8414CD", + "x": 54354, + "y": -226060, + "z": -1363, + "rotation": -178.81 + }, + { + "id": "BP_Crystal18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "770C7160-4B2CDA43-BC5930A4-A6D5E849", + "x": 229666, + "y": 160136, + "z": 7624, + "rotation": -174.05 + }, + { + "id": "BP_Crystal18_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1E4D8FF9-427B0008-C302BFBB-6C211FF3", + "x": 100490, + "y": -112100, + "z": 18312, + "rotation": -171.24 + }, + { + "id": "BP_Crystal18_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5DF0C266-4AB03311-B50A729B-860064CC", + "x": -56673, + "y": 191129, + "z": 402, + "rotation": -174.97 + }, + { + "id": "BP_Crystal18_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3EA57A1A-46CE9025-0993D88E-EFB491BB", + "x": 208619, + "y": -58330, + "z": 20899, + "rotation": 93.13 + }, + { + "id": "BP_Crystal18_22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6607E9F1-475D26F6-F5701399-0284F637", + "x": -229149, + "y": -21168, + "z": 4776, + "rotation": 108.85 + }, + { + "id": "BP_Crystal19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A6924316-41EBFBB7-D4187BA9-43CCB3B2", + "x": -116191, + "y": -160532, + "z": 897, + "rotation": -126.7 + }, + { + "id": "BP_Crystal19_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4B53504B-446FB76F-B97962BE-A4D15092", + "x": 183056, + "y": 180708, + "z": -4265, + "rotation": -7.17 + }, + { + "id": "BP_Crystal19_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "950F721B-43D503D0-02951292-80B5A0CC", + "x": -25938, + "y": 147257, + "z": 296, + "rotation": -108.34 + }, + { + "id": "BP_Crystal19_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3DF531F2-4367DE4A-CA123498-7B8A85AF", + "x": 209178, + "y": -56345, + "z": 19245, + "rotation": 74.2 + }, + { + "id": "BP_Crystal19_23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C7A0AEC7-4FF64188-0ACA7CBB-14BA7097", + "x": -285159, + "y": -61168, + "z": -1267, + "rotation": -138.22 + }, + { + "id": "BP_Crystal19_28", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0338B3D9-48ADECFA-2AEEDCB2-595393BA", + "x": -109871, + "y": 69604, + "z": 13389, + "rotation": 19.35 + }, + { + "id": "BP_Crystal19_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "92B52D1F-43C0A5D2-7ECF559F-BF6B1053", + "x": 40960, + "y": -61444, + "z": 11987, + "rotation": -23.72 + }, + { + "id": "BP_Crystal1_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "70A8A775-4E213C58-A4D3B9A6-2AA6EF53", + "x": -56826, + "y": 234985, + "z": -2648, + "rotation": -87.32 + }, + { + "id": "BP_Crystal1_1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C5F54517-4E3E7AD8-EC20928E-D1D83714", + "x": -155398, + "y": 210147, + "z": -1544, + "rotation": 54.87 + }, + { + "id": "BP_Crystal1_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9F3D3E8B-459BDFD9-0248BC9C-132C69C8", + "x": -72107, + "y": -47798, + "z": 16773, + "rotation": 175.92 + }, + { + "id": "BP_Crystal1_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "83799337-4D5FCC0D-AF7EED99-01BD2E24", + "x": -225302, + "y": 55321, + "z": 1662, + "rotation": -57.13 + }, + { + "id": "BP_Crystal1_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9BB8B4F5-4E5AA2A8-A5884CBD-D6C16D0F", + "x": -210682, + "y": 43084, + "z": 10395, + "rotation": -3.74 + }, + { + "id": "BP_Crystal1_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EDDF28E7-4D442D15-2FB316AF-B3D248E3", + "x": 70482, + "y": 77916, + "z": 13008, + "rotation": -47.14 + }, + { + "id": "BP_Crystal1_227", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9BD7DF18-48247C33-3005EBBD-B1A0C3FD", + "x": 64308, + "y": 49429, + "z": 31374, + "rotation": -67.15 + }, + { + "id": "BP_Crystal1_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5C0BD264-46830280-5784FBA8-84E7D2DF", + "x": 138340, + "y": -45752, + "z": 7939, + "rotation": 24.46 + }, + { + "id": "BP_Crystal1_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "93ABD5EE-46DB50BA-EE20FF8E-045AA2C1", + "x": 112477, + "y": 200650, + "z": -5055, + "rotation": -81.04 + }, + { + "id": "BP_Crystal1_87", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "44BA11F1-44A6EE73-F365C0BF-C17A7E68", + "x": 229589, + "y": 69717, + "z": -1675, + "rotation": 4.76 + }, + { + "id": "BP_Crystal2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "35ABC720-4A1ACC19-3F5937AE-08B833E6", + "x": -261667, + "y": -191990, + "z": -890, + "rotation": -83.05 + }, + { + "id": "BP_Crystal20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F26ADD4D-4D5A1C9D-F65133AE-F066E00A", + "x": -103165, + "y": -120773, + "z": 253, + "rotation": -171.23 + }, + { + "id": "BP_Crystal20_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "36F3CB63-4A6FC64F-DE4C15B7-89A39CFF", + "x": 178417, + "y": 186708, + "z": -4237, + "rotation": 49.4 + }, + { + "id": "BP_Crystal20_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6E034BAE-478275ED-C7FBB2BC-BD292FF6", + "x": 37453, + "y": 144081, + "z": -7061, + "rotation": -133.28 + }, + { + "id": "BP_Crystal20_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BF8F6499-465BAB3F-8EFC91B0-4B1354DE", + "x": -273273, + "y": -64069, + "z": 3103, + "rotation": -52.14 + }, + { + "id": "BP_Crystal20_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8F49F552-4DC90412-33CD00A7-70698ACF", + "x": 75580, + "y": -126975, + "z": 7007, + "rotation": -22.93 + }, + { + "id": "BP_Crystal20_29", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F3F22B71-49F17FBE-34D3EE8D-DAD43CC5", + "x": -114214, + "y": 72590, + "z": 14877, + "rotation": 21.01 + }, + { + "id": "BP_Crystal21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "80F3D072-458E087F-6CA1AD91-D5D4E52F", + "x": -140602, + "y": -170184, + "z": 9075, + "rotation": -28.12 + }, + { + "id": "BP_Crystal21_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "42E1AED5-46554C40-6EED438E-4D3ABB87", + "x": 178836, + "y": 182414, + "z": -1657, + "rotation": -39.58 + }, + { + "id": "BP_Crystal21_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A211857D-4AEDBF82-33C672BF-E6211DBF", + "x": 22571, + "y": 274565, + "z": 2792, + "rotation": -178.1 + }, + { + "id": "BP_Crystal21_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "818986E1-49119CB2-14F028B4-228DA77D", + "x": -236733, + "y": -38309, + "z": -1622, + "rotation": -126.11 + }, + { + "id": "BP_Crystal21_26", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "89658EFA-43942759-4E2B8492-EA5EB9D6", + "x": -116513, + "y": 69435, + "z": 14658, + "rotation": -172.02 + }, + { + "id": "BP_Crystal21_86", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "00F65973-42CBD5F3-B4F25191-B776D6CC", + "x": 207371, + "y": 92, + "z": -1131, + "rotation": -77.53 + }, + { + "id": "BP_Crystal22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F3D94AB4-4C8368B5-7390F29C-EA23633A", + "x": 96905, + "y": 230776, + "z": -3913, + "rotation": 13.54 + }, + { + "id": "BP_Crystal22_22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C44A6BB9-4A246BD9-BDA01EB0-E0FCA7C8", + "x": -110797, + "y": 262232, + "z": -6225, + "rotation": 164.6 + }, + { + "id": "BP_Crystal22_25", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F36B5880-4738D556-5B2F2CA6-C5E3B37C", + "x": -263224, + "y": -16429, + "z": 512, + "rotation": -105.01 + }, + { + "id": "BP_Crystal22_27", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "87602D4E-4E44123A-54CD959F-C1BF05F9", + "x": -113224, + "y": 66524, + "z": 13003, + "rotation": -159.57 + }, + { + "id": "BP_Crystal22_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F8462FC0-41F5DC57-67EF5F93-CABBC2BE", + "x": 47501, + "y": -126014, + "z": 13866, + "rotation": 149.6 + }, + { + "id": "BP_Crystal23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DCCD86E8-4AC233A8-2D142D87-6967A542", + "x": -104759, + "y": -186989, + "z": -1760, + "rotation": 151.78 + }, + { + "id": "BP_Crystal23_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F63AE7C1-4904A678-15E0E6A7-EFA032F3", + "x": 140682, + "y": 238885, + "z": -11460, + "rotation": 85.45 + }, + { + "id": "BP_Crystal23_23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F40CB391-42DDE15B-8AAD98B2-BA95F6DC", + "x": -111112, + "y": 223563, + "z": 736, + "rotation": 141.41 + }, + { + "id": "BP_Crystal23_26", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EEC862EC-46FC5EF3-02DC1097-81AB4F86", + "x": -298838, + "y": -37511, + "z": 711, + "rotation": 92.45 + }, + { + "id": "BP_Crystal24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "41DC3B33-4C6D18C0-692EEFBB-8B1F92AE", + "x": -147412, + "y": -175894, + "z": 10487, + "rotation": 86.45 + }, + { + "id": "BP_Crystal24_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D651F4D5-49B82899-9CDA0CB0-699FE31C", + "x": -183688, + "y": 16112, + "z": 5583, + "rotation": -160.61 + }, + { + "id": "BP_Crystal24_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1FA5F5AA-496E9562-4AA2649F-63D3868B", + "x": 125414, + "y": 237617, + "z": -10841, + "rotation": 132.74 + }, + { + "id": "BP_Crystal24_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "00544FAF-49CCE998-E8BAF483-C6D937D8", + "x": 3925, + "y": 191567, + "z": -5231, + "rotation": -163.7 + }, + { + "id": "BP_Crystal24_25", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D8D79A24-4DE1FD53-2521B6AE-80439F50", + "x": -117465, + "y": 65317, + "z": 13400, + "rotation": 65.95 + }, + { + "id": "BP_Crystal25", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6F2D62C3-4EA92203-6C3AEFAF-DF2CA31B", + "x": -112208, + "y": -148939, + "z": 3780, + "rotation": 106.84 + }, + { + "id": "BP_Crystal25_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E84B9574-487B412F-CE04DABD-1A0DDE52", + "x": 135815, + "y": 248732, + "z": -16827, + "rotation": -22.86 + }, + { + "id": "BP_Crystal25_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8A4CBDC8-4990C5CC-F5AAFAAB-0CEE6F89", + "x": -113645, + "y": 148049, + "z": 6219, + "rotation": -16.46 + }, + { + "id": "BP_Crystal25_25", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4F830242-49917EEA-ADB25785-43B36E50", + "x": -35282, + "y": 167057, + "z": 2546, + "rotation": -43.3 + }, + { + "id": "BP_Crystal26", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FD908837-4127DFD6-BBE8F2B7-F2D4C237", + "x": 132610, + "y": 232404, + "z": -14229, + "rotation": 161.03 + }, + { + "id": "BP_Crystal26_26", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "935B8AEE-4091B71B-F4134789-FBC433B8", + "x": -70297, + "y": 292339, + "z": -6332, + "rotation": -148.88 + }, + { + "id": "BP_Crystal26_32", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "560B347E-49F90914-C392ACAB-37B71D62", + "x": -139447, + "y": 130393, + "z": 9833, + "rotation": 148.79 + }, + { + "id": "BP_Crystal27", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C14D68E3-4A271663-2CAA0A83-98279773", + "x": -160089, + "y": -163808, + "z": 14627, + "rotation": -9.77 + }, + { + "id": "BP_Crystal27_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "600B58B0-4A68D4CC-B696A8AC-053D54E1", + "x": 132078, + "y": 248281, + "z": -10341, + "rotation": -130.78 + }, + { + "id": "BP_Crystal27_22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B8B91B75-4BB04B59-311D70BE-38BED0BD", + "x": -97121, + "y": 97881, + "z": 12219, + "rotation": 115.62 + }, + { + "id": "BP_Crystal27_27", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E3628BC3-430B5200-49624CA7-3AD9C5E4", + "x": -54543, + "y": 270830, + "z": -1452, + "rotation": 142.9 + }, + { + "id": "BP_Crystal28", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0BA75CBB-47BC98D4-773AC5AD-E4425E77", + "x": -206359, + "y": -195657, + "z": -1391, + "rotation": -135.76 + }, + { + "id": "BP_Crystal28_23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6D4C970E-42BCB5F9-675009A8-3EA137BA", + "x": -124470, + "y": 84573, + "z": 7208, + "rotation": -169.69 + }, + { + "id": "BP_Crystal28_28", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6232D49F-418401ED-37E36BB4-237C24BA", + "x": -78244, + "y": 270755, + "z": -3966, + "rotation": -138.77 + }, + { + "id": "BP_Crystal29", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "237BB7A3-43FD8E0E-57C80ABB-919A1B83", + "x": -192704, + "y": -147394, + "z": 3606, + "rotation": 31.71 + }, + { + "id": "BP_Crystal29_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "088AE241-47549549-81BF5387-1C9E80C2", + "x": -135682, + "y": 72159, + "z": 8200, + "rotation": 69.01 + }, + { + "id": "BP_Crystal29_29", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BF479A3D-4B63667A-868A8B82-159FB987", + "x": -24540, + "y": 214616, + "z": -3446, + "rotation": 138.11 + }, + { + "id": "BP_Crystal2_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8C963996-44289B32-FF56FFA1-3D8562AC", + "x": 128184, + "y": -94790, + "z": 4200, + "rotation": -62.06 + }, + { + "id": "BP_Crystal2_1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F8E69F15-427238A8-840B208B-99F60D01", + "x": 39006, + "y": 84525, + "z": 14359, + "rotation": 156.99 + }, + { + "id": "BP_Crystal2_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D23B67D0-45362929-37ABA6AE-A10E29D3", + "x": 6633, + "y": 292644, + "z": 1161, + "rotation": 61.02 + }, + { + "id": "BP_Crystal2_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8233629E-44BD90D7-AF8CCFAB-79B28B9D", + "x": -210904, + "y": 138995, + "z": -579, + "rotation": 55.73 + }, + { + "id": "BP_Crystal2_228", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D04CFF91-41A01853-08E34DBB-7BFBFDE9", + "x": 47248, + "y": 68396, + "z": 31465, + "rotation": 27.86 + }, + { + "id": "BP_Crystal2_2780", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "204D3DF7-4E9C2CBE-BCFD6EB6-E40C76FA", + "x": -101136, + "y": -37459, + "z": 15237, + "rotation": 0.03 + }, + { + "id": "BP_Crystal2_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "31FA682A-4E107D55-C6E56F9A-E991C7A7", + "x": -149929, + "y": 171000, + "z": 3607, + "rotation": -173.19 + }, + { + "id": "BP_Crystal2_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4438421E-4B9C188F-9CA3AAAA-F01CAF93", + "x": -273618, + "y": -93544, + "z": -841, + "rotation": -88.12 + }, + { + "id": "BP_Crystal2_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1C5C0EDC-47C1C6CA-5FBD0796-C0016770", + "x": 189389, + "y": 200749, + "z": 3491, + "rotation": 72.01 + }, + { + "id": "BP_Crystal3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F993C00B-463AC0ED-C30C8286-6BB94CFE", + "x": -227222, + "y": -144440, + "z": 4640, + "rotation": -158.46 + }, + { + "id": "BP_Crystal30", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "16BA490F-4B54881A-4468FD97-04AB0985", + "x": -195442, + "y": -139537, + "z": 2678, + "rotation": 161.49 + }, + { + "id": "BP_Crystal30_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C8BE3ADA-4B7EFD7C-F66E14B6-E0860CDF", + "x": -11948, + "y": 302673, + "z": -1881, + "rotation": 119.34 + }, + { + "id": "BP_Crystal30_30", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AE16F68D-42533424-28208080-4EE1033C", + "x": -105265, + "y": 83191, + "z": 13173, + "rotation": 103.93 + }, + { + "id": "BP_Crystal31", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9E364903-4BF11F17-2566ADBD-9C2FA38D", + "x": -161215, + "y": 93265, + "z": 9840 + }, + { + "id": "BP_Crystal31_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4167A771-48689E43-9FE2A4A7-C3011408", + "x": -165206, + "y": -147210, + "z": 3964, + "rotation": 132.34 + }, + { + "id": "BP_Crystal31_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B8463F34-4E68655B-5EBEF5B2-5276CC37", + "x": -12908, + "y": 291934, + "z": -1779, + "rotation": 59.79 + }, + { + "id": "BP_Crystal32", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CB73C3DE-41A3942A-F7B38E9F-57F30AA5", + "x": -68347, + "y": 170999, + "z": 787, + "rotation": -62.48 + }, + { + "id": "BP_Crystal32_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F431DEF7-427667A3-FFD4D5AC-67344A6E", + "x": -54766, + "y": 283465, + "z": -1427, + "rotation": 153.33 + }, + { + "id": "BP_Crystal32_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EADBED92-4F8F9E0F-6BEB5398-40574B15", + "x": -182318, + "y": -150281, + "z": 2007, + "rotation": 22.34 + }, + { + "id": "BP_Crystal33", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D065FD39-475D80E0-88F157A6-C4884218", + "x": -176828, + "y": -143190, + "z": 5313, + "rotation": -99.61 + }, + { + "id": "BP_Crystal33_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CBF52234-404B0E43-DB81DDBD-B37C0736", + "x": -115731, + "y": 238884, + "z": -4679, + "rotation": -101.79 + }, + { + "id": "BP_Crystal34", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DD417E5A-47CAFF36-FAB2A4A8-18912E6F", + "x": -87052, + "y": 276875, + "z": -3049, + "rotation": 60.86 + }, + { + "id": "BP_Crystal34_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C6D18509-48AB6A82-599E2583-D7FB49FA", + "x": -160825, + "y": 75738, + "z": 5015, + "rotation": -11.17 + }, + { + "id": "BP_Crystal34_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "42669586-42FC4A1E-AF3339B8-33960729", + "x": -252342, + "y": -129726, + "z": 8114, + "rotation": 136.49 + }, + { + "id": "BP_Crystal35", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9972D14E-439F8C0A-99E45E99-E6AEFC6E", + "x": -91820, + "y": 249822, + "z": -3181, + "rotation": 38.35 + }, + { + "id": "BP_Crystal35_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "078F49C7-4A561B05-750BCA99-F6E88566", + "x": -181216, + "y": 76730, + "z": 17784, + "rotation": -12.92 + }, + { + "id": "BP_Crystal36", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5C6643AA-427E6EB7-09B37580-1F8CF8FE", + "x": -15141, + "y": 218879, + "z": 233, + "rotation": 106.37 + }, + { + "id": "BP_Crystal36_23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E7BBF4C9-4AECB2C0-050736B2-96D3AA78", + "x": -110938, + "y": -141243, + "z": -529, + "rotation": -173.16 + }, + { + "id": "BP_Crystal37", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "755C22BD-4D34BCFB-C75EF697-A75091C8", + "x": -39678, + "y": 213271, + "z": -426, + "rotation": 53.53 + }, + { + "id": "BP_Crystal37_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FFAC3B7D-41DE28F2-71EC2298-CAEDFCD1", + "x": -197509, + "y": -145896, + "z": 1994, + "rotation": -24.24 + }, + { + "id": "BP_Crystal38", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C2F01AE7-454D6D81-E67ACEB6-6A72D1B2", + "x": -187640, + "y": -143410, + "z": -1236, + "rotation": 126.62 + }, + { + "id": "BP_Crystal38_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "860836A4-493035EA-29BD70A0-3B68DBDA", + "x": 167795, + "y": 97575, + "z": 6007, + "rotation": 79.34 + }, + { + "id": "BP_Crystal39", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "40474CE8-4F5D9951-FAE33389-403DC845", + "x": -191990, + "y": -165497, + "z": -2998, + "rotation": -0.81 + }, + { + "id": "BP_Crystal3_1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8AE65190-4C683374-791DC8AA-6C4E4FEA", + "x": 8996, + "y": -144952, + "z": 505, + "rotation": 71.59 + }, + { + "id": "BP_Crystal3_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0B66DD28-4619EE8E-5577BB98-A3334927", + "x": -42610, + "y": -37552, + "z": 18914, + "rotation": 27.18 + }, + { + "id": "BP_Crystal3_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1D6DAE89-4D470EDC-0D30BD9A-6603274B", + "x": 101580, + "y": 124437, + "z": 10567, + "rotation": 129.54 + }, + { + "id": "BP_Crystal3_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8BFC7C3D-486DD347-C63A03BB-957C3969", + "x": 61848, + "y": 216669, + "z": -1129, + "rotation": 15.79 + }, + { + "id": "BP_Crystal3_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "77BE41E3-42A8E617-B77D3499-C98D2202", + "x": -216109, + "y": 112517, + "z": -689, + "rotation": 178.82 + }, + { + "id": "BP_Crystal3_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F6E04599-4756BD1C-43E16D8E-4723FE41", + "x": 53704, + "y": -80022, + "z": 12783, + "rotation": -46.55 + }, + { + "id": "BP_Crystal3_234", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9D07C0E0-479B6951-187FCC9C-A234F3BD", + "x": -590, + "y": 118438, + "z": 23006, + "rotation": 39.2 + }, + { + "id": "BP_Crystal3_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8F8D7B87-44745480-691125BD-0111D807", + "x": -64959, + "y": 268214, + "z": -3017, + "rotation": 25.73 + }, + { + "id": "BP_Crystal3_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1E1AF66A-467E507C-F0F14CAD-A2DBBFF5", + "x": -178837, + "y": 131313, + "z": 3782, + "rotation": 176.72 + }, + { + "id": "BP_Crystal3_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BBE8B1D5-43F87F16-C5C6D096-9D6B162B", + "x": -107122, + "y": -43626, + "z": 3303, + "rotation": 109.41 + }, + { + "id": "BP_Crystal3_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3BBFC343-44AD71B9-B58EF6AD-A47299EF", + "x": 207260, + "y": 203801, + "z": 8445, + "rotation": 66.83 + }, + { + "id": "BP_Crystal4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5251AF05-400AD591-0B99258C-D98A7477", + "x": -216557, + "y": -164256, + "z": -1792, + "rotation": 106.17 + }, + { + "id": "BP_Crystal40", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E93229F5-4593ADAC-B4EE50A4-863D742C", + "x": -148952, + "y": -104097, + "z": 1976, + "rotation": 66.99 + }, + { + "id": "BP_Crystal41", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A4E66FC0-4033C7F2-C2FF9DB2-2AB79CF4", + "x": -267651, + "y": -140338, + "z": 6370, + "rotation": -32.03 + }, + { + "id": "BP_Crystal42", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D9E3F693-4ED06000-E745F395-DF99CF98", + "x": -180197, + "y": -135956, + "z": -1723, + "rotation": 7.45 + }, + { + "id": "BP_Crystal43", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EF9FB6FD-44A9A7FA-9DD3FEB7-00163C8D", + "x": -149455, + "y": -145817, + "z": 7391, + "rotation": -165.69 + }, + { + "id": "BP_Crystal44", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B2473C6A-44A046C3-9B9717AA-06335E2C", + "x": 29163, + "y": 110840, + "z": -8773, + "rotation": 1.27 + }, + { + "id": "BP_Crystal45", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B529D9D8-40264F4D-068A1FBD-235CD109", + "x": -116580, + "y": -174837, + "z": 5765, + "rotation": -156.63 + }, + { + "id": "BP_Crystal46", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A2A5A61B-431CE76C-7E57009D-47AAEFAF", + "x": -152342, + "y": -172704, + "z": 18054, + "rotation": 103.08 + }, + { + "id": "BP_Crystal47", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ED3BF4A7-4C4EF4E2-7E00C085-50D19AA9", + "x": -136412, + "y": -184601, + "z": 25884, + "rotation": 35.43 + }, + { + "id": "BP_Crystal48", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4654110F-4238B9D3-BA355EA4-7CD606E0", + "x": 252111, + "y": -204394, + "z": 1602, + "rotation": -91.04 + }, + { + "id": "BP_Crystal49", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "57B8F6DF-49142D71-953A3F8B-8EB42CA7", + "x": 340377, + "y": -190603, + "z": 4548, + "rotation": -139.02 + }, + { + "id": "BP_Crystal4_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "43EC86D4-4EF985C0-13F22BAB-D910BD08", + "x": -3712, + "y": 78149, + "z": 21871, + "rotation": -134.28 + }, + { + "id": "BP_Crystal4_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2EA2A7F0-423F4820-FA4A909F-00337A2D", + "x": 88077, + "y": 137503, + "z": 10776, + "rotation": -70.27 + }, + { + "id": "BP_Crystal4_3771", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CC4E524C-4EED326B-8F496EB1-53F984B8", + "x": -72766, + "y": -66350, + "z": 15571, + "rotation": -21.48 + }, + { + "id": "BP_Crystal4_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "905A95DF-48C54781-1F15C69B-701EB536", + "x": 10973, + "y": 292829, + "z": 3545, + "rotation": -131.12 + }, + { + "id": "BP_Crystal4_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1046690D-45A92B19-F074B5AE-79B26C67", + "x": -180207, + "y": 108189, + "z": 11441, + "rotation": -170.55 + }, + { + "id": "BP_Crystal4_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "810F4ECE-4DD91E84-163D6E97-E69A6A31", + "x": 103336, + "y": -125396, + "z": -686, + "rotation": -41.71 + }, + { + "id": "BP_Crystal4_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0FF90DC7-45B67A26-71546997-CA042808", + "x": -213661, + "y": -68965, + "z": 78, + "rotation": 47.64 + }, + { + "id": "BP_Crystal4_73", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5AFEB273-46A96E8A-19979796-6C08C9F8", + "x": 212703, + "y": 68578, + "z": -1665, + "rotation": 30.37 + }, + { + "id": "BP_Crystal4_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "09630864-4363C862-70A3D0AD-609CA871", + "x": 194376, + "y": 193847, + "z": -1886, + "rotation": 1.31 + }, + { + "id": "BP_Crystal5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0C340D6F-4D28AB77-E86312BF-C3B4E5ED", + "x": -193556, + "y": -96514, + "z": -291, + "rotation": 139.41 + }, + { + "id": "BP_Crystal50", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0802A376-4D248B96-2E973981-18F422E1", + "x": 351189, + "y": -205183, + "z": 4453, + "rotation": -73.79 + }, + { + "id": "BP_Crystal51", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2E6B09B4-4FC4357B-D33267B8-FB595D7B", + "x": -2433, + "y": -89074, + "z": 16376, + "rotation": 94.54 + }, + { + "id": "BP_Crystal52", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EA4DA304-49A3970C-43F53CA6-BD0BCB11", + "x": 381564, + "y": -208871, + "z": 5197, + "rotation": 4.35 + }, + { + "id": "BP_Crystal53", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5F9FCBB8-48DA6B18-618BE480-501E99A6", + "x": 364228, + "y": -210001, + "z": 4051, + "rotation": -0.48 + }, + { + "id": "BP_Crystal54", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ED8BC545-452A4B20-14823689-C36AC23B", + "x": 377439, + "y": -186364, + "z": 5812, + "rotation": -0.9 + }, + { + "id": "BP_Crystal55", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7256AFDE-4FF757AE-647E08AE-C0C7E7B0", + "x": 354541, + "y": -215741, + "z": 4586, + "rotation": 27.69 + }, + { + "id": "BP_Crystal56_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5F56ACBA-4DC26E5F-B8DAD3B6-7DC86E0B", + "x": 163254, + "y": -198418, + "z": 14214, + "rotation": 42.71 + }, + { + "id": "BP_Crystal57_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A811AA6D-4A69C4A6-0F5F2C96-FA0487AC", + "x": -126519, + "y": 136940, + "z": 5168, + "rotation": 150.56 + }, + { + "id": "BP_Crystal58", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0B205A77-4E721F04-A34EA4A8-F4704A18", + "x": 356876, + "y": -129668, + "z": 4867, + "rotation": -93.24 + }, + { + "id": "BP_Crystal59", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ACD8DBE1-4D15057F-6A6ACC8D-D9C2CDB8", + "x": 311590, + "y": -112250, + "z": 6358, + "rotation": 73.69 + }, + { + "id": "BP_Crystal5_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5D0CC0A2-4B5E37CF-2C91F89F-2A8EFF76", + "x": 137129, + "y": 162100, + "z": -4671, + "rotation": 38.31 + }, + { + "id": "BP_Crystal5_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5E77A797-435A95CE-D68DD996-D9EF4A70", + "x": 124924, + "y": 117905, + "z": 10145, + "rotation": 87.76 + }, + { + "id": "BP_Crystal5_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D1B45230-49A40703-EF6F979C-F9C5F05E", + "x": 53967, + "y": -43188, + "z": 15949, + "rotation": 104.51 + }, + { + "id": "BP_Crystal5_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "86BA3CE6-4E28C0F9-D4374292-7B01BFE3", + "x": 14158, + "y": 277845, + "z": -5276, + "rotation": 27.18 + }, + { + "id": "BP_Crystal5_5126", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0F057D76-448E9ACD-54FE739D-A54CC217", + "x": -59719, + "y": -21522, + "z": 18940, + "rotation": 7.18 + }, + { + "id": "BP_Crystal5_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "05DD5336-4B7DF27F-7A86E7B4-8BC460D0", + "x": -90367, + "y": 54630, + "z": 3794, + "rotation": 131.51 + }, + { + "id": "BP_Crystal5_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3994E918-4302B61A-DF8E1198-B5AE8104", + "x": -263272, + "y": -74856, + "z": -1639, + "rotation": 47.17 + }, + { + "id": "BP_Crystal5_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AE373BEF-40B7E164-406D4983-C6FD4007", + "x": 26660, + "y": 40204, + "z": 22601, + "rotation": -3.75 + }, + { + "id": "BP_Crystal6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E41120A0-45A426B9-D520DDAD-F46106A6", + "x": -136276, + "y": -113776, + "z": 2835, + "rotation": -1.13 + }, + { + "id": "BP_Crystal60", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FED26901-47392035-180EE4AC-345577A3", + "x": 121986, + "y": -60568, + "z": 8124, + "rotation": -118.08 + }, + { + "id": "BP_Crystal61", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E64DAB4A-41116D67-85AB699A-C53A1352", + "x": 374722, + "y": -217659, + "z": 4357, + "rotation": -54.77 + }, + { + "id": "BP_Crystal62", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A696AC47-4CC8D90B-7D1653B2-92B3439B", + "x": 7206, + "y": -84186, + "z": 10442, + "rotation": 177.49 + }, + { + "id": "BP_Crystal63", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E1121022-4E0CB2C6-4F90C6A9-DD14DC62", + "x": -136888, + "y": 162495, + "z": 722, + "rotation": -14.19 + }, + { + "id": "BP_Crystal65", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "360BFF47-4BF77DA5-669932BB-993ED590", + "x": 43201, + "y": 57779, + "z": 13030, + "rotation": -57.87 + }, + { + "id": "BP_Crystal66_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E6832208-432D8030-5108159B-AEE602D7", + "x": 37112, + "y": 30624, + "z": 8911, + "rotation": -177.9 + }, + { + "id": "BP_Crystal67", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6B0F59E6-49ECB8B5-A9C2C484-C3383001", + "x": 71638, + "y": -47006, + "z": 14106, + "rotation": -136.48 + }, + { + "id": "BP_Crystal68", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ECC897B3-4A8E224F-D00D0AA6-CF2B5CDA", + "x": 219580, + "y": -176570, + "z": 4904, + "rotation": 120.56 + }, + { + "id": "BP_Crystal6_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "07BE3AD7-42EFAA3E-76BC7A86-1C77F0AB", + "x": 169976, + "y": 194630, + "z": -7904, + "rotation": 14.86 + }, + { + "id": "BP_Crystal6_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "986B3BAC-4E95FE49-20520986-95F35F52", + "x": -18250, + "y": -48836, + "z": 15742, + "rotation": -99.72 + }, + { + "id": "BP_Crystal6_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4582ED78-43AD797A-29EF468C-90341FB5", + "x": -35295, + "y": -141966, + "z": 10443, + "rotation": 150.27 + }, + { + "id": "BP_Crystal6_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "589CF0CD-488A975F-A30965B7-5A986C44", + "x": 94681, + "y": -24578, + "z": 5884, + "rotation": -41.83 + }, + { + "id": "BP_Crystal6_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "096C75B5-4B13E696-8045E580-293A2F36", + "x": 11639, + "y": 278218, + "z": -5135, + "rotation": -65.8 + }, + { + "id": "BP_Crystal6_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "69B12FDD-420A85F3-427634AF-B278AA86", + "x": -155579, + "y": 99241, + "z": 13744, + "rotation": -84.74 + }, + { + "id": "BP_Crystal6_76", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A4399192-4E44C8EF-A63088AE-9AA38BE9", + "x": 245766, + "y": 48680, + "z": -1350, + "rotation": -111.34 + }, + { + "id": "BP_Crystal6_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7D3D4081-4CA5F210-75897C94-126EE928", + "x": -229638, + "y": -20539, + "z": -1909, + "rotation": 142.08 + }, + { + "id": "BP_Crystal7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5DB7A101-4D8F460C-DDDE82A8-86A11B2E", + "x": -220729, + "y": -129010, + "z": 4907, + "rotation": -55 + }, + { + "id": "BP_Crystal70", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "42EA647B-44ADFE80-F1F46EA5-78D79D53", + "x": 14540, + "y": -82989, + "z": 12499, + "rotation": 143 + }, + { + "id": "BP_Crystal72", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "15E2E4DE-42E9DE4E-F1626BB5-3D485E33", + "x": 77700, + "y": -91565, + "z": 15887, + "rotation": -56.47 + }, + { + "id": "BP_Crystal73", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8FD3D0D8-457723AA-E1B77E86-ECAA2B07", + "x": 283019, + "y": -44041, + "z": 6246, + "rotation": -94.49 + }, + { + "id": "BP_Crystal74", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "05537890-4167F33D-000672B0-74840525", + "x": 313923, + "y": -79801, + "z": 5641, + "rotation": 118.18 + }, + { + "id": "BP_Crystal75", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9F33A2A1-44374919-4BA623B3-B1E9999F", + "x": 95409, + "y": -17844, + "z": 15507, + "rotation": -76.09 + }, + { + "id": "BP_Crystal76", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "64B78198-46D0C014-01628EBC-F4059AC9", + "x": 283152, + "y": -125203, + "z": 2753, + "rotation": -103.87 + }, + { + "id": "BP_Crystal77", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "48A269B8-44B32A0C-91935988-0F4DDF2F", + "x": 218984, + "y": 74573, + "z": -1288, + "rotation": -158.79 + }, + { + "id": "BP_Crystal78", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "359FBB0F-4300F5DE-B9D8499B-509A29D7", + "x": 325469, + "y": -81766, + "z": 5739, + "rotation": 40.83 + }, + { + "id": "BP_Crystal79", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D48655DF-46DE648D-FB072A8E-E1AA4138", + "x": 272740, + "y": -164523, + "z": 4245, + "rotation": 76.27 + }, + { + "id": "BP_Crystal7_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3837D17B-40AEFCFC-9F4C8B8B-0B4D063C", + "x": -205602, + "y": -27121, + "z": 12255, + "rotation": 131.65 + }, + { + "id": "BP_Crystal7_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "599E205B-498CF4B8-C801F5B2-9FD1DAA4", + "x": -6017, + "y": 14171, + "z": 29141, + "rotation": 89.32 + }, + { + "id": "BP_Crystal7_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F4AE5705-45C4A00B-BF49C7B0-95918865", + "x": 136433, + "y": 205189, + "z": -8365, + "rotation": -88.34 + }, + { + "id": "BP_Crystal7_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "35899109-46A559D5-4A9C3FA9-7A956AA0", + "x": -33545, + "y": -79359, + "z": 784, + "rotation": -43.15 + }, + { + "id": "BP_Crystal7_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C25162A6-4D5C311B-34C0E999-165C7DFA", + "x": 71777, + "y": -32929, + "z": 19808, + "rotation": 56.52 + }, + { + "id": "BP_Crystal7_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B80B23CF-4731F22F-E67914A8-1266229C", + "x": 99867, + "y": -64276, + "z": 15056, + "rotation": -162.73 + }, + { + "id": "BP_Crystal7_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4BDF326F-4A5615E5-DFF842B3-BDBBEF30", + "x": 10053, + "y": 276180, + "z": -5658, + "rotation": 33.07 + }, + { + "id": "BP_Crystal7_75", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1F176701-4BC9E1B5-8B782DB1-464AE555", + "x": 246631, + "y": 35136, + "z": -1285, + "rotation": 169.89 + }, + { + "id": "BP_Crystal7_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "86988B51-4BDC7EB7-CA9462A8-46C211AD", + "x": -128700, + "y": 117704, + "z": 7810, + "rotation": 87.07 + }, + { + "id": "BP_Crystal8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7C5A86CA-48FFB286-855D56A6-9722012E", + "x": -237526, + "y": -154961, + "z": 13809, + "rotation": 144.04 + }, + { + "id": "BP_Crystal80", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "11192C4C-47E299D7-6F2EEEA3-FE90FBEF", + "x": 336706, + "y": -95976, + "z": 5910, + "rotation": 41.72 + }, + { + "id": "BP_Crystal81", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4898A004-406BF56A-306ECCA4-FC6DECC8", + "x": 299931, + "y": -183533, + "z": 3583, + "rotation": 67.62 + }, + { + "id": "BP_Crystal82", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "78D2376B-4C63B05D-4E3FADAC-375CC015", + "x": 346188, + "y": -209790, + "z": 4462, + "rotation": 28.45 + }, + { + "id": "BP_Crystal83", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "168F7A81-47BE5475-5D6A6184-6D4E6875", + "x": 225007, + "y": 77164, + "z": -705, + "rotation": 135.41 + }, + { + "id": "BP_Crystal84", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "13AEF980-487DBD1E-5B561688-2EA9B512", + "x": 266140, + "y": -146164, + "z": 4397, + "rotation": -134.19 + }, + { + "id": "BP_Crystal86", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "449F3344-46B68EA6-ABD5B695-D67C5BD4", + "x": 4581, + "y": 197113, + "z": 907, + "rotation": -116.18 + }, + { + "id": "BP_Crystal87", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5C78044E-4DDB908B-30EA2C83-F8BBEFF8", + "x": 360869, + "y": -233574, + "z": 4619, + "rotation": -149.11 + }, + { + "id": "BP_Crystal88", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "76F286CF-4E4849B0-B118FBA3-EF56B084", + "x": 235815, + "y": -187736, + "z": 1208, + "rotation": 168.3 + }, + { + "id": "BP_Crystal89", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1F9445AD-47AC710C-4BB01C9C-A247D440", + "x": 234297, + "y": -200231, + "z": 1536, + "rotation": -18.82 + }, + { + "id": "BP_Crystal8_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D147BB9E-426251F4-8C7EDFA8-BE12C29E", + "x": 63292, + "y": -154308, + "z": 9754, + "rotation": -157.87 + }, + { + "id": "BP_Crystal8_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7B2CB0D7-4D3BCC5D-15C1D2A8-50F824CA", + "x": -173585, + "y": -78948, + "z": 4679, + "rotation": 9.9 + }, + { + "id": "BP_Crystal8_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "729DA4C4-4D1C5A7F-3A58FA86-89D54FEA", + "x": -38396, + "y": -117359, + "z": 7935, + "rotation": -127.52 + }, + { + "id": "BP_Crystal8_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6D958FAC-449C543C-DF5AC4AE-3FEE55B3", + "x": 152138, + "y": 165806, + "z": 237, + "rotation": 1.39 + }, + { + "id": "BP_Crystal8_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E53588E7-447F4FEA-786E038A-D995D955", + "x": -47813, + "y": -79783, + "z": 18904, + "rotation": -25.96 + }, + { + "id": "BP_Crystal8_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "95865422-482E4B8A-07E0CD9F-B49B0A23", + "x": 48054, + "y": 50744, + "z": 24676, + "rotation": 112.58 + }, + { + "id": "BP_Crystal8_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6BCDADE6-49D680F7-E5DECF97-0EDAFDD6", + "x": 117109, + "y": -28657, + "z": 11723, + "rotation": -72.54 + }, + { + "id": "BP_Crystal8_77", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "033537CE-4089952B-9705B5B4-4B74F3F3", + "x": 234864, + "y": 50020, + "z": -1669, + "rotation": -143.02 + }, + { + "id": "BP_Crystal8_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B79123C5-4BB516AE-15F5F5A6-7C18E1C5", + "x": 11885, + "y": 276437, + "z": -5037, + "rotation": -0.68 + }, + { + "id": "BP_Crystal8_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5B63D348-486FB1BB-0AB127AC-6D905E01", + "x": -86890, + "y": 45067, + "z": 3570, + "rotation": -52.51 + }, + { + "id": "BP_Crystal9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9CE80E29-413FE6C7-7A06FF8E-DCF35F8C", + "x": -237200, + "y": -186284, + "z": -137, + "rotation": 151.46 + }, + { + "id": "BP_Crystal90", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8E7E7673-4291CF50-3338E2A7-58AB39E2", + "x": 318095, + "y": -230797, + "z": 3621, + "rotation": -68.87 + }, + { + "id": "BP_Crystal91", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FEDD5F25-47125D88-B043D5B3-539EFCA6", + "x": 335850, + "y": -157789, + "z": 4133, + "rotation": -107.95 + }, + { + "id": "BP_Crystal93", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "79965788-41EBA82B-B34DFA9F-227E1241", + "x": 127045, + "y": -183015, + "z": 5565, + "rotation": 58.78 + }, + { + "id": "BP_Crystal93_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "39E09F75-4FBD6814-383D9EA2-4D0F1AD7", + "x": 170655, + "y": -202645, + "z": 17622, + "rotation": -1.25 + }, + { + "id": "BP_Crystal94_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DD502E46-494DB30A-BD33DFAA-FD2BEDF2", + "x": 260948, + "y": -82121, + "z": 7029, + "rotation": -87.23 + }, + { + "id": "BP_Crystal96", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F903F4E4-468696A3-50BB95BF-EEE96AF6", + "x": 157593, + "y": -172494, + "z": 8384, + "rotation": -29.43 + }, + { + "id": "BP_Crystal97", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3A6FE612-47108C15-41FB2C86-EB9E389B", + "x": 364325, + "y": -160359, + "z": 7240, + "rotation": 49.1 + }, + { + "id": "BP_Crystal98", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "82A49A4D-458872CC-3FDCACB9-62B3C95B", + "x": 51977, + "y": 34517, + "z": 21533, + "rotation": 19.65 + }, + { + "id": "BP_Crystal98_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "954F7465-473900E3-1F495492-FBA254C6", + "x": 222225, + "y": -192077, + "z": 1373, + "rotation": 107.18 + }, + { + "id": "BP_Crystal99", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2F0F5F41-42602266-99BD9BAF-DD5B0979", + "x": 51493, + "y": 23386, + "z": 19467, + "rotation": 27.44 + }, + { + "id": "BP_Crystal9_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B42E2AC0-4D5288CB-6291A0AA-0C42E494", + "x": -96647, + "y": 51528, + "z": 2484, + "rotation": 15 + }, + { + "id": "BP_Crystal9_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "496EC327-49C0E1D1-287CD0B4-9B5C75B0", + "x": 40512, + "y": -156265, + "z": 6093, + "rotation": 11.37 + }, + { + "id": "BP_Crystal9_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8A62CCD7-4D02C88B-B70D12A7-712DAA3B", + "x": -268982, + "y": 9245, + "z": 26, + "rotation": 15.64 + }, + { + "id": "BP_Crystal9_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "794B6981-4CC0ACE4-37C1BFAB-4CBE1AAB", + "x": 105235, + "y": 233314, + "z": -2224, + "rotation": 63.97 + }, + { + "id": "BP_Crystal9_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A42A5BCB-4BB98973-F34EC595-11DEDE6E", + "x": 54318, + "y": 56208, + "z": 20088, + "rotation": -84.91 + }, + { + "id": "BP_Crystal9_78", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D3E1E908-4AC7A44C-E4C371B0-81BB3385", + "x": 238902, + "y": 69954, + "z": -1655, + "rotation": 155.88 + }, + { + "id": "BP_Crystal9_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "65883A11-428BD7CE-9D31B7B3-0926C641", + "x": 128885, + "y": -17680, + "z": 14242 + }, + { + "id": "BP_Crystal9_8928", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8DD7C8F4-4B869284-3B54B4BE-C2FA138D", + "x": -24256, + "y": -52936, + "z": 15623, + "rotation": -133.15 + }, + { + "id": "BP_Crystal9_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AF8F4919-433FA217-A3DB2680-3598E332", + "x": 13038, + "y": 275406, + "z": -5644, + "rotation": 138.3 + }, + { + "id": "BP_Crystal_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "78DE0F06-4C2344CE-4F6417A6-442142C9", + "x": -46998, + "y": -175082, + "z": 2662, + "rotation": -119.54 + }, + { + "id": "BP_Crystal_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5DF2B928-46930928-234246A5-97895A00", + "x": -11880, + "y": -75670, + "z": 9999, + "rotation": -79.96 + }, + { + "id": "BP_Crystal_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "14583038-48C1D189-2112F3B6-A2DBEE5F", + "x": 221544, + "y": 131718, + "z": -2001, + "rotation": -2.1 + }, + { + "id": "BP_Crystal_C_0", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F754E491-4ECC8E2C-B9B15789-9F5EE5C3", + "x": -7210, + "y": -72590, + "z": 9390, + "rotation": -43.49 + }, + { + "id": "BP_Crystal_C_1", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A141ED2B-4C5DC5C1-2F6427B9-D229257C", + "x": -21927, + "y": -123522, + "z": 11088, + "rotation": 28.08 + }, + { + "id": "BP_Crystal_C_10", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B00A85EF-44F24FE7-0AC786A1-7B244963", + "x": 77393, + "y": 189828, + "z": 2239, + "rotation": 95.27 + }, + { + "id": "BP_Crystal_C_100", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A474620C-4DF12968-4F2C4C83-73091803", + "x": 332899, + "y": -246819, + "z": 6806, + "rotation": -70.6 + }, + { + "id": "BP_Crystal_C_101", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "59D1049C-418A06E0-B2706B95-5D8B859C", + "x": 380984, + "y": -283543, + "z": 2017, + "rotation": 43.55 + }, + { + "id": "BP_Crystal_C_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2D52DEC2-4F77FC9F-A743F1A6-C19C57D8", + "x": 58804, + "y": 190304, + "z": 3288, + "rotation": 86.96 + }, + { + "id": "BP_Crystal_C_12", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D09F9D52-449B2AC6-ACC8A3A3-AF30C280", + "x": 43542, + "y": 161857, + "z": -1568, + "rotation": 9.43 + }, + { + "id": "BP_Crystal_C_13", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EE477DF5-463FB01E-7D45DFB8-0CE1D9A5", + "x": 68007, + "y": 6851, + "z": 13419, + "rotation": -45.33 + }, + { + "id": "BP_Crystal_C_14", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A469BD8D-41E3B2B6-F5A9B285-0F961348", + "x": 14301, + "y": 1997, + "z": 23354, + "rotation": -0.11 + }, + { + "id": "BP_Crystal_C_15", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7D21DAD6-4A617F2E-6CCB039A-90D46379", + "x": 68996, + "y": -73351, + "z": 12585, + "rotation": -65.17 + }, + { + "id": "BP_Crystal_C_16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "71A65DA8-4E3A8585-DBBCA5AD-BDA5616F", + "x": 27496, + "y": -99944, + "z": 12243, + "rotation": -39.64 + }, + { + "id": "BP_Crystal_C_17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "702164EC-4703CF8D-4C8BF08E-A858A343", + "x": 13100, + "y": -41117, + "z": 13984, + "rotation": -64.57 + }, + { + "id": "BP_Crystal_C_18", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "38997834-43AADA58-E62B438B-A1213302", + "x": 36058, + "y": -55722, + "z": 15537, + "rotation": -13.25 + }, + { + "id": "BP_Crystal_C_19", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6C48DB9E-4A3F566D-4EEC7E92-F79DA89E", + "x": 33209, + "y": -62277, + "z": 6328, + "rotation": 0.94 + }, + { + "id": "BP_Crystal_C_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5D7DBF7F-46990BE7-742796AD-6D31F07B", + "x": -17754, + "y": -166057, + "z": 721, + "rotation": -47.49 + }, + { + "id": "BP_Crystal_C_20", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0F2CA646-49A65FFF-BCA67681-EF3D0B82", + "x": 54186, + "y": -65394, + "z": 6283, + "rotation": 136.79 + }, + { + "id": "BP_Crystal_C_21", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B6E1D1FC-4F6CC312-4D7B44A9-E02E7D13", + "x": 52441, + "y": -51877, + "z": 9319, + "rotation": -150.02 + }, + { + "id": "BP_Crystal_C_22", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5FD95AB9-473CDB65-77A7719B-B88C24E6", + "x": 54282, + "y": -60322, + "z": 9872, + "rotation": 122.43 + }, + { + "id": "BP_Crystal_C_23", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EB6D0C3A-4C8B3068-06D106B9-CF3B1AB5", + "x": 37706, + "y": -39335, + "z": 13522, + "rotation": 25.08 + }, + { + "id": "BP_Crystal_C_24", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3580CA61-4CD1A50E-5F7DF985-445248EA", + "x": 100096, + "y": -231, + "z": 19208, + "rotation": -28.59 + }, + { + "id": "BP_Crystal_C_25", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A8FD46A5-4D4D8E54-FE7C8BBD-427AC73E", + "x": 89989, + "y": -1540, + "z": 17381, + "rotation": 67.45 + }, + { + "id": "BP_Crystal_C_26", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4FC1C025-4101CA10-39547D83-10608707", + "x": 16118, + "y": -112630, + "z": 16250, + "rotation": -101.44 + }, + { + "id": "BP_Crystal_C_27", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0782889B-4641C9A9-9E0C53B0-8EFBD45F", + "x": 7229, + "y": -131491, + "z": 13971, + "rotation": 42.57 + }, + { + "id": "BP_Crystal_C_28", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "314E0E65-456E3365-4D9D2DB9-749BCF13", + "x": 68513, + "y": -106230, + "z": 10610, + "rotation": 113.69 + }, + { + "id": "BP_Crystal_C_29", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "96C65D4D-423ACA7F-BDBB36A1-157132A0", + "x": 47911, + "y": -182549, + "z": 813, + "rotation": -100.65 + }, + { + "id": "BP_Crystal_C_3", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AF3DE4BD-46376396-FA691DBB-A78C683F", + "x": -27514, + "y": -213251, + "z": 3130, + "rotation": -109.26 + }, + { + "id": "BP_Crystal_C_30", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F17DE885-47AA3886-58497D82-657F6B05", + "x": 31549, + "y": -168083, + "z": -1164, + "rotation": -101.11 + }, + { + "id": "BP_Crystal_C_31", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C4D28368-4EDC829B-BE3CB89E-602C58D5", + "x": 33275, + "y": -162994, + "z": 1599, + "rotation": -126.27 + }, + { + "id": "BP_Crystal_C_32", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2574EC62-432F24C2-596EEC81-6F6D2722", + "x": 63726, + "y": -157832, + "z": 4033, + "rotation": 167.52 + }, + { + "id": "BP_Crystal_C_33", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B4E40B18-46C54946-311AF782-A6B7CB89", + "x": 26534, + "y": -227792, + "z": 1685, + "rotation": -176.14 + }, + { + "id": "BP_Crystal_C_34", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D018B059-401BB0FE-1DE46290-D9E8F2D9", + "x": 123343, + "y": 136007, + "z": 11009, + "rotation": -6.33 + }, + { + "id": "BP_Crystal_C_35", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A298B608-458E19E7-238377BC-8BA96696", + "x": 184978, + "y": 184737, + "z": -2015, + "rotation": 137.4 + }, + { + "id": "BP_Crystal_C_38", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1A2EA40F-4679E26B-48140F9F-24F84342", + "x": 180344, + "y": 126586, + "z": 11567, + "rotation": 1.14 + }, + { + "id": "BP_Crystal_C_39", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A77DDE3C-43CD02A3-473510AC-5575D5F2", + "x": 185849, + "y": 111911, + "z": 14296, + "rotation": 27.64 + }, + { + "id": "BP_Crystal_C_4", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "44C046A7-4A97211B-65A6DCAB-76A52116", + "x": 8526, + "y": -199634, + "z": 799, + "rotation": 25.64 + }, + { + "id": "BP_Crystal_C_41", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E77F1F13-4B9043F1-4E4AC0B5-9B19A8B0", + "x": 166036, + "y": 116547, + "z": 9841, + "rotation": 21.28 + }, + { + "id": "BP_Crystal_C_43", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "92A0885A-4CC10E37-08B5B996-A44A974F", + "x": 165838, + "y": 141362, + "z": 9845, + "rotation": -0.57 + }, + { + "id": "BP_Crystal_C_47", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A4910790-41ABF69B-672B8C97-ECD413B4", + "x": 128172, + "y": 91506, + "z": 14047, + "rotation": 56.59 + }, + { + "id": "BP_Crystal_C_5", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F45F85F0-470FC956-DAC7A3BB-129E9AE5", + "x": 25539, + "y": 216946, + "z": -2491, + "rotation": -133.15 + }, + { + "id": "BP_Crystal_C_51", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BB5EE260-40B72F9B-CB614A9F-B9A6F9FD", + "x": 181599, + "y": 85971, + "z": 9224, + "rotation": 33.49 + }, + { + "id": "BP_Crystal_C_52", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C4D79020-47B96552-08519182-B5DB728C", + "x": 147771, + "y": 80510, + "z": 13465, + "rotation": 70.5 + }, + { + "id": "BP_Crystal_C_53", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7DC11833-4DCEE9AA-2898D29E-276AB41B", + "x": 179371, + "y": 73733, + "z": 4290, + "rotation": 77.79 + }, + { + "id": "BP_Crystal_C_54", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B4A416EB-4B73B5DF-F55D2EAA-56A643BD", + "x": 109485, + "y": -22975, + "z": 10757, + "rotation": 68.9 + }, + { + "id": "BP_Crystal_C_55", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B0358176-492A91A1-1A39FE94-A2E185E8", + "x": 101963, + "y": -88116, + "z": 14033, + "rotation": -137.8 + }, + { + "id": "BP_Crystal_C_56", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A359819E-4E684D4B-A74B74B4-17143225", + "x": 101701, + "y": -52767, + "z": 9024, + "rotation": 55.41 + }, + { + "id": "BP_Crystal_C_57", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8601FEB7-477F72A9-D99DDBBB-7BCB0F2D", + "x": 202543, + "y": -44483, + "z": 4890, + "rotation": 1.96 + }, + { + "id": "BP_Crystal_C_58", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D3AC609D-4ECEA85D-F2FD9AA7-366E23DE", + "x": 138690, + "y": -181345, + "z": 3505, + "rotation": -16.66 + }, + { + "id": "BP_Crystal_C_59", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7CF741BF-44828EDD-620622AC-8D8EFB74", + "x": 112283, + "y": -197085, + "z": -1322, + "rotation": 1.74 + }, + { + "id": "BP_Crystal_C_6", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "911CDC32-49EC155A-DF5A1397-68093504", + "x": 34668, + "y": 215761, + "z": -5370, + "rotation": -102.8 + }, + { + "id": "BP_Crystal_C_60", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AA83F7C1-47809214-F6ADAC8E-26F267C7", + "x": 28430, + "y": -212600, + "z": -1756, + "rotation": -176.14 + }, + { + "id": "BP_Crystal_C_61", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FF7E1471-48E3A686-5F241596-B0EAC040", + "x": 137738, + "y": -146279, + "z": 2755, + "rotation": 12.4 + }, + { + "id": "BP_Crystal_C_63", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "09533DEC-4D06CD7F-8A256099-F04E6AAF", + "x": 189073, + "y": -286000, + "z": 17916, + "rotation": 146.47 + }, + { + "id": "BP_Crystal_C_64", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "22EC7BCC-442C5308-AFC734A1-2A515A2D", + "x": 162965, + "y": -269125, + "z": 12110, + "rotation": 73.15 + }, + { + "id": "BP_Crystal_C_68", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7ED0AD2B-4A95D789-DAF4E384-34E94DD6", + "x": 212810, + "y": 192285, + "z": 12650, + "rotation": -159.21 + }, + { + "id": "BP_Crystal_C_7", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "98C2380B-4D92CB03-08660CBA-4E6F56A7", + "x": 76368, + "y": 227611, + "z": 4364, + "rotation": 125.1 + }, + { + "id": "BP_Crystal_C_74", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "83F8BB9E-41204D14-88281C97-F65F63A0", + "x": 220790, + "y": 129840, + "z": 9040, + "rotation": -132.99 + }, + { + "id": "BP_Crystal_C_75", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E54F34C4-45476CCF-11389CA6-D0F25BD4", + "x": 231636, + "y": 94325, + "z": 3983, + "rotation": -44.74 + }, + { + "id": "BP_Crystal_C_76", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E5BA82E2-47DD27F3-03BA8892-8E5B4564", + "x": 218619, + "y": 82222, + "z": -595, + "rotation": -43.88 + }, + { + "id": "BP_Crystal_C_77", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E0CBA7AF-46488E47-66A7C485-2F413F53", + "x": 227423, + "y": 87137, + "z": -927, + "rotation": 37.04 + }, + { + "id": "BP_Crystal_C_78", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "40D5DDF7-42B7FDAE-76A624B3-613FB29C", + "x": 206735, + "y": 79106, + "z": -889, + "rotation": 15.39 + }, + { + "id": "BP_Crystal_C_79", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C0FF7A18-49619EC0-DF25E6BC-7E3FF901", + "x": 237021, + "y": 60782, + "z": -1667, + "rotation": -167.68 + }, + { + "id": "BP_Crystal_C_8", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D46A1926-4FEB5E76-53297A91-764859DA", + "x": 88404, + "y": 202261, + "z": 717, + "rotation": 79.7 + }, + { + "id": "BP_Crystal_C_80", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1743955F-49A935A6-5102C991-DC4B818B", + "x": 211428, + "y": -56340, + "z": 19434, + "rotation": 65.55 + }, + { + "id": "BP_Crystal_C_81", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "82A2280B-40BA707C-8B681EB5-ADB2658D", + "x": 213452, + "y": -60482, + "z": 20010, + "rotation": -6.28 + }, + { + "id": "BP_Crystal_C_82", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "52A26C36-4E0F4460-8DCA488F-C5A7F147", + "x": 210324, + "y": -26246, + "z": 7599, + "rotation": -6.96 + }, + { + "id": "BP_Crystal_C_83", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "976301A3-43DF1108-6420B8BD-09034757", + "x": 207553, + "y": -30936, + "z": 8331, + "rotation": -14.31 + }, + { + "id": "BP_Crystal_C_84", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C1FC8B38-4B6BE705-78CC30AC-A6B414E1", + "x": 205059, + "y": -41001, + "z": 2517, + "rotation": 81.53 + }, + { + "id": "BP_Crystal_C_85", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9005E95B-4846D7BD-6216C39A-5DA0836F", + "x": 207376, + "y": -20684, + "z": 2469, + "rotation": 7.22 + }, + { + "id": "BP_Crystal_C_86", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DE2B380E-424FF21B-6C27038F-935DA870", + "x": 210337, + "y": -28393, + "z": 1371, + "rotation": 79.29 + }, + { + "id": "BP_Crystal_C_87", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "07D77363-4465CCCA-714B319F-20362229", + "x": 211441, + "y": -20923, + "z": 6362, + "rotation": 142.05 + }, + { + "id": "BP_Crystal_C_88", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2A3FC56D-4F6C63AD-E6540EBA-D67FD983", + "x": 204080, + "y": -24318, + "z": 55, + "rotation": -11.97 + }, + { + "id": "BP_Crystal_C_89", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4E0922A7-4EF3A7C5-429E4DB6-7968D6A7", + "x": 206078, + "y": -12988, + "z": 4165, + "rotation": -84.65 + }, + { + "id": "BP_Crystal_C_9", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FD173BAA-46E2C99C-3EDA59BD-E7354433", + "x": 94291, + "y": 191764, + "z": 6766, + "rotation": 93.69 + }, + { + "id": "BP_Crystal_C_90", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "29357679-418B178A-80DCA8B8-7355C47A", + "x": 258399, + "y": -109980, + "z": 5798, + "rotation": -75.44 + }, + { + "id": "BP_Crystal_C_91", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6AC9DAF6-4E8C9B79-0804FDBC-3AE1DC79", + "x": 258013, + "y": -253609, + "z": 7162, + "rotation": -145.55 + }, + { + "id": "BP_Crystal_C_92", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C3655015-430A9BEA-D5CFA7A0-F10A4A66", + "x": 248769, + "y": -252499, + "z": 1279, + "rotation": 132.8 + }, + { + "id": "BP_Crystal_C_93", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9AB2C343-477F9D90-D5A9B8BB-D3C7D12C", + "x": 275135, + "y": -276673, + "z": 8504, + "rotation": -106.07 + }, + { + "id": "BP_Crystal_C_94", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "24F37FA4-47F907A8-24884582-0E8BE70D", + "x": 216565, + "y": -301215, + "z": 16625, + "rotation": 39.56 + }, + { + "id": "BP_Crystal_C_96", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "95729EA6-40F8DBF6-567DEB94-B9F4167B", + "x": 306024, + "y": -47557, + "z": 5786, + "rotation": 90.92 + }, + { + "id": "BP_Crystal_C_97", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9334E5FD-4F30A475-2C93CAB7-44F2BB1A", + "x": 378909, + "y": -141011, + "z": 4787, + "rotation": -139.71 + }, + { + "id": "BP_Crystal_C_98", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B999E89C-422D9256-3BE9C7B3-A7C05D94", + "x": 329779, + "y": -117572, + "z": 7559, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_99", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "51DA5375-47DFA5F8-42685A83-421B3DF4", + "x": 318922, + "y": -188064, + "z": 4238, + "rotation": 130.78 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F006B101_2082315136", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2399E657-437FD517-8DC84AB2-04C7535C", + "x": 213060, + "y": -107089, + "z": 8812, + "rotation": -133.5 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F007B101_1247933313", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CE953085-433090E1-DF4235B4-C869495F", + "x": 223585, + "y": -92053, + "z": 1440 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F007B101_1510242320", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8702394C-4E71F669-CA71B385-76083D73", + "x": 243257, + "y": -63569, + "z": 6945, + "rotation": -56.94 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0217201_1462011415", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "39E21309-49F30027-9C93CFBA-42EFA99C", + "x": -95903, + "y": 8243, + "z": 22013, + "rotation": -153.79 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0217201_2134786423", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "631A7815-415A5366-FCCA7180-D4EE87C2", + "x": -94414, + "y": 8679, + "z": 21705, + "rotation": 50.52 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0245C01_1792102668", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "58F99883-4D3B5643-E9D819AE-1B11682B", + "x": 196708, + "y": -261893, + "z": 947, + "rotation": 10.51 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0277201_2091967520", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FDEAF2FD-45583E65-432C17BF-EA5EAC5B", + "x": -92948, + "y": 7469, + "z": 23110, + "rotation": -36.64 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F02B7201_1774057258", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "438964B0-4AE3923C-E49F8380-6E57A0A5", + "x": -93807, + "y": 6182, + "z": 20619, + "rotation": 156.11 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F02C6001_1338841280", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F062FAB9-4D4ADDB0-A38C29AB-660C81D0", + "x": 207905, + "y": -167378, + "z": 16879, + "rotation": 72.17 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F02C6001_1557408281", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EBCCA944-4980CB99-A2DE7782-96B4120B", + "x": 221659, + "y": -204572, + "z": 14370, + "rotation": -1.11 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F02C6001_1622416282", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "552299FE-46956139-41322E96-2169F85D", + "x": 209102, + "y": -231180, + "z": 16946, + "rotation": -51.55 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0316001_1179996182", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "10CD193B-4AF1384D-1622D3A0-19B534A7", + "x": 160918, + "y": -129496, + "z": 15386, + "rotation": 0.59 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0355B01_2005222588", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4B56D545-4E9FC549-D5F548A9-39B55ACF", + "x": 204021, + "y": -240579, + "z": 24584, + "rotation": 5.63 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0375B01_1529908943", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "79608763-4E305F71-CE62879D-D3836F08", + "x": 201898, + "y": -266576, + "z": 6173, + "rotation": -22.78 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0395B01_1557158296", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2BBC4B61-4A9B6297-92E2DD91-2713356D", + "x": 192732, + "y": -173452, + "z": 23641, + "rotation": -122.42 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0395B01_1742920298", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EC9727DE-42CE3EDC-B4CA85A5-5291EE6E", + "x": 183494, + "y": -194858, + "z": 23854, + "rotation": 60.28 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F03A5B01_1333253475", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CA07AF00-4929F6F4-75C527A5-0BAA52E5", + "x": 194457, + "y": -213998, + "z": 23474, + "rotation": 107.82 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F03B5B01_1299814658", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1EE1ADC0-4A8E2D11-3B27C3A6-F8472733", + "x": 206170, + "y": -198008, + "z": 23403, + "rotation": 87.22 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F03B5B01_1622325660", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8724E14A-489AD83A-13921FA4-6BA69878", + "x": 148068, + "y": -187028, + "z": 10405, + "rotation": -79.53 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0439B01_1646991632", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F51667A4-4B42924E-DB3B33A4-01A53E5F", + "x": -193643, + "y": -78730, + "z": 1088, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F04C5E01_2099362813", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "400371A0-4FBEDF16-49BC87A2-85D7BD70", + "x": 127405, + "y": -154675, + "z": 16610, + "rotation": 19.73 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05A7301_2035634448", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "68064B79-4A97CB6A-334F78B9-0D8A854E", + "x": -93358, + "y": 9641, + "z": 22301, + "rotation": -83.5 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05D5D01_2007016764", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1B139FA4-4CCAEC3A-38CDD4A1-2F8A1A65", + "x": 228632, + "y": 106588, + "z": 5556, + "rotation": -73.27 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05E5D01_1095164941", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0D321D4C-4FE54B70-EA434C80-BB55B5CD", + "x": 228323, + "y": 109351, + "z": 8098, + "rotation": 28.27 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05F5301_1878669572", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D3C8518A-4C2A49FF-81CE259E-8BADB1FB", + "x": 159805, + "y": -283795, + "z": 16441, + "rotation": -117.79 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0605301_1273983749", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3B3FB816-477AF54D-78A19681-F0377558", + "x": 158746, + "y": -261100, + "z": 1394, + "rotation": -4.01 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0615301_1117010932", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CA57F81B-466F99FB-CAA65EBC-2A2BA051", + "x": 169548, + "y": -181645, + "z": 20626, + "rotation": 88.43 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0655D01_1109507179", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DA6F4E1F-494AF49C-E106E9B3-1EF5BEF7", + "x": 160647, + "y": 143586, + "z": 8072, + "rotation": -107.81 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0675D01_1725830534", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "837C7EA1-4A5FDDD8-31F0D4BA-43BA2697", + "x": 214651, + "y": 113982, + "z": 10739, + "rotation": -44.34 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F06C5C01_1356614339", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A1B8A925-40D25897-DB95FEAB-E97A931B", + "x": 219571, + "y": 111317, + "z": 5147, + "rotation": -73.71 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F06E5D01_1646682771", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D01649A0-46BCC470-4DE6F7A9-8EB8D8A7", + "x": 181293, + "y": 149901, + "z": 8525, + "rotation": 159.97 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0766701_1589843752", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5E565CCA-4A6CAEE3-60A350B9-1254F337", + "x": -14306, + "y": -21528, + "z": 16210, + "rotation": -4.79 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0766701_1663177753", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "202CB1A6-4EA908AB-8F283EA8-56213530", + "x": -13871, + "y": -23374, + "z": 15569, + "rotation": 13.91 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0766701_1707460754", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2BD119A7-4B7F3AFE-733B2EBD-BA4C8633", + "x": -14725, + "y": -21597, + "z": 14980, + "rotation": -89.07 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0766701_1729573755", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E64B4459-4197B7B4-292762AF-D184D65F", + "x": -15165, + "y": -23014, + "z": 14497, + "rotation": 36.23 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0766701_1842097756", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "41B35190-4A3382A2-45C575BF-798CCDD7", + "x": -16440, + "y": -21683, + "z": 16020, + "rotation": 11.1 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0775B01_1420184201", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "78914DD1-4DB6715A-9DC1F99A-C97497A0", + "x": 156985, + "y": -188437, + "z": 7352, + "rotation": 5.1 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0775B01_1580171202", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DB2F9C9E-4C5E17BA-8F4C7FA8-94F05D31", + "x": 110997, + "y": -169301, + "z": 521, + "rotation": 49 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0785B01_1425075382", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "23BFB686-44051296-36BEDE90-6797523F", + "x": 141507, + "y": -162513, + "z": 2480, + "rotation": -95.28 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0795B01_1115579560", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "59E4A750-4BCD3014-AE62A2B5-A1202309", + "x": 124997, + "y": -143352, + "z": 2062, + "rotation": 170.26 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0795B01_1706418562", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8387C8A9-427C6FA5-795C0A93-7650B1BA", + "x": 110606, + "y": -154488, + "z": 4514, + "rotation": -77.84 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F079AF01_2086299378", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8BE5A809-4342C0F2-0CD4149A-B564DE8E", + "x": 17087, + "y": -151959, + "z": 3057, + "rotation": 72.96 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F07B6001_2100568184", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E7D6ACED-4E88F92E-A77F1893-072D4AF3", + "x": 180478, + "y": -125915, + "z": 17418, + "rotation": -5.62 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0805B01_1369679802", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AC128495-41D0D01E-7940C3BD-87DF87D0", + "x": 123754, + "y": -182009, + "z": 1902, + "rotation": -49.52 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0815B01_1784184985", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "259AAADB-4E8AEEE1-C92FE0AE-E1542A91", + "x": 133485, + "y": -147639, + "z": 6667, + "rotation": -91.26 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0825B01_1553698166", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "10CA9F8B-48E4E344-0BEC65A5-8468CB25", + "x": 115145, + "y": -165203, + "z": 3668, + "rotation": 180 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0825B01_1706644167", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "745D5DD8-4867F7B0-E94FD9AE-B8D59622", + "x": 145351, + "y": -186760, + "z": 1906, + "rotation": -177.41 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F09E6901_1553651856", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6431E642-408F39CF-EC4D029D-17A7BEF1", + "x": -246584, + "y": 50205, + "z": 977, + "rotation": 15.2 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A05F01_1896752643", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "09EC0BAF-433029D5-013B3AA8-258018E2", + "x": 205340, + "y": -135650, + "z": 18455, + "rotation": -93.04 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A57401_1817017698", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "24F10705-4F1741A0-F45D089A-61895EA7", + "x": 188670, + "y": -9447, + "z": 16640, + "rotation": 134.2 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A57401_2054736699", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E7A67F98-49546B50-7A0F5FB2-8D7604AF", + "x": 190307, + "y": -7889, + "z": 16305, + "rotation": -98.89 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A57401_2106808700", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D175AFF1-4AD95B90-2119149F-3296AFDF", + "x": 191676, + "y": -10331, + "z": 16377, + "rotation": 125.77 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A67401_1113792877", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0224D50F-4D26C9F2-CCC2DA9C-0B0B3E11", + "x": 190268, + "y": -12429, + "z": 17360, + "rotation": -109.81 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A67401_1901138878", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7514EE7D-4AA2753B-9C1D0792-1BD698BF", + "x": 100061, + "y": 60654, + "z": 22600, + "rotation": 22.39 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A67401_1962658879", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "07328687-4FF31B17-C703DEAB-051466CC", + "x": 96212, + "y": 63600, + "z": 22345, + "rotation": 139.53 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A67401_2008955880", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5DB726A2-4721A58B-858FCC99-D26EE182", + "x": 95950, + "y": 60899, + "z": 22448, + "rotation": -138.36 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A67401_2137612882", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ECC0DA60-453DF6C3-84B14B83-358DD97E", + "x": 97846, + "y": 56929, + "z": 22733, + "rotation": -100.76 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A77401_1114044059", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7AC89803-462D3E09-A2366E8B-00654A7A", + "x": 99450, + "y": 58688, + "z": 21949, + "rotation": 130.8 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A77401_2134624060", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E90821C9-4CF05D72-49214B8A-AFFBB7EB", + "x": 139741, + "y": -9295, + "z": 21217, + "rotation": -15.98 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A87401_1178078237", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "375A1A0E-419CDC09-7E536397-3A5D8CAB", + "x": 136605, + "y": -8650, + "z": 21781, + "rotation": -104.87 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A87401_1211555238", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "166A791F-45B5E150-051B2FB7-5222E5DC", + "x": 136496, + "y": -6285, + "z": 21844, + "rotation": 8.9 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A87401_1304279239", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "28347C2F-42341F5E-96829895-436E36D6", + "x": 140384, + "y": -7876, + "z": 21091, + "rotation": 22.81 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0A87401_1376559241", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "7C99E845-4CCD0474-A98A4EB1-C4A29B13", + "x": 139727, + "y": -7445, + "z": 21396, + "rotation": -66.25 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AA5201_1287975681", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4203CE48-4CFFFB32-04D6BFAA-B8649DB2", + "x": 194737, + "y": -186699, + "z": 23570, + "rotation": -20.49 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AA7401_1909401594", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D7BF6335-4B1146C2-9BFB33A8-B9EF189C", + "x": 184587, + "y": 37045, + "z": 23154, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AA7401_1919713595", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "25D0ACE4-46098C2B-D0D499BC-EDF7AF8B", + "x": 181321, + "y": 38823, + "z": 21110, + "rotation": 19.15 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AA7401_2076003597", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3C15836A-4112ABBB-8FE0DB8C-20FA3260", + "x": 184000, + "y": 37967, + "z": 20988, + "rotation": -60.09 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AB5201_1104409861", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FC123178-422BB308-A43B54BB-E14DEDE8", + "x": 195348, + "y": -187881, + "z": 24109 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AC5201_1696194044", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F63B4379-42C09CCD-3B938FA0-A013D51E", + "x": 203915, + "y": -284267, + "z": 10054, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AE5D01_1835696000", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BA568C16-4F77C125-6D5D668F-FE01BF22", + "x": 183085, + "y": 164584, + "z": 9245, + "rotation": -127.44 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AE7301_1502060226", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "902DAA0E-4896F4F2-6D3657AD-B41D02EA", + "x": 191620, + "y": -11873, + "z": 16837, + "rotation": -46.89 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AF7401_1381809479", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1C3D93A1-4FCDFE0A-9AEAD7AA-823CE4A4", + "x": 182270, + "y": 36358, + "z": 20725, + "rotation": 46.81 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AF7401_1394094480", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "183171E6-4ED30E9A-4793B49C-5FAD9650", + "x": 182291, + "y": 40591, + "z": 21012, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B3AF01_1789903464", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E2B59111-4346C95A-B6CA59A2-8F2C13D6", + "x": 66153, + "y": -134287, + "z": 679 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0BF5D01_1402621031", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B3767824-4A17AC47-A4CE01A7-F18294E2", + "x": 128003, + "y": -144472, + "z": 15763, + "rotation": 39.35 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0C7AF01_1736399985", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "68DEBFB7-4D19F318-B346FCBB-D896C8CA", + "x": -65336, + "y": -154298, + "z": -195, + "rotation": -42.37 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0D45B01_1751130579", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C8985061-4741283D-E61CA2A2-EED22FA5", + "x": 109665, + "y": -180967, + "z": 4012, + "rotation": 27.87 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0DD5B01_1562001181", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EA03D124-4F911B89-01A2BAAE-75F5F3A4", + "x": 148511, + "y": -172218, + "z": 3923, + "rotation": -55.73 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0E85F01_1421899313", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4726BCCC-4414AC9C-FC9A6F9D-9E051DEE", + "x": 200225, + "y": -183930, + "z": 26734, + "rotation": 22.55 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0E85F01_1933315318", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C8E4198D-40635F24-C16C7281-5558A11C", + "x": 227180, + "y": -278348, + "z": 24722, + "rotation": 139.68 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0E95F01_1231921499", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3325B7EB-4981A2D3-37848DB5-AE4C8DCA", + "x": 211232, + "y": -283751, + "z": 30679, + "rotation": 19.37 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0EA5F01_1564806683", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "25070572-45B0A44A-2B859C99-602C9E0A", + "x": 157574, + "y": -125909, + "z": 9353, + "rotation": 33.58 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0EC5F01_1293213040", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A7120550-4F6A81A7-04D1C99B-065A56E7", + "x": 175746, + "y": -124768, + "z": 10608, + "rotation": -164.56 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0F25F01_1725349098", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "9C92A67C-4B64CF0C-9FA6BB94-6079650C", + "x": 204175, + "y": -225007, + "z": 23533, + "rotation": -116.02 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0F35F01_1247028277", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2E42C9B5-4907E2D3-A7876293-4E347655", + "x": 204012, + "y": -249790, + "z": 16103, + "rotation": 50.5 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0F45F01_1549163457", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E0E7D065-480FBC3A-C4BE7E94-FF60E7E1", + "x": 145980, + "y": -150527, + "z": 18422, + "rotation": -14.23 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79066401_1370998836", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1703E908-441FC992-8558D399-0E93923D", + "x": -68178, + "y": -188867, + "z": -1139, + "rotation": -150.08 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F790A6401_1480408547", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "656A27E6-43964551-62B43D8D-813040F0", + "x": -39356, + "y": -189929, + "z": 2915, + "rotation": -53.87 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79226201_1519973634", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3B458F8B-41837AF3-7D2B0AB4-CC21DE3F", + "x": 131292, + "y": -145159, + "z": 2045, + "rotation": -19.43 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79265B01_1210582944", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EFCF85AC-48E23FDD-EB91E990-52AE2226", + "x": 32861, + "y": -239913, + "z": 1057, + "rotation": 70.9 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79275B01_1177099121", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F5292432-4E6DD31D-BA0614AE-17818C04", + "x": 20890, + "y": -248994, + "z": 1811, + "rotation": 153.1 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79285B01_1944304298", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6AF8C62A-44034EED-CA8D84A3-B8895FDE", + "x": 74252, + "y": -230383, + "z": -135, + "rotation": 32.94 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792A8701_1662356113", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "018C3EAC-46EF57A7-E7E3ED88-D67FD72F", + "x": -59465, + "y": -109540, + "z": 3360, + "rotation": 0 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792D6001_1825806456", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F10738BA-435C0EC3-4EBB27A2-2778008D", + "x": 47033, + "y": -153055, + "z": 14990, + "rotation": -110.78 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792E6001_1313258633", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "53EE18ED-447A4C75-8E7B3A90-9C8585A9", + "x": 35933, + "y": -162478, + "z": 18702, + "rotation": 175.56 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792E6001_2033315634", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "49B21B7A-44BE2D06-E74C8DBF-58BF3A6A", + "x": 47189, + "y": -169433, + "z": 21354, + "rotation": -43.46 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792F6001_1132971811", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BDF489E2-4062C79A-516177A1-2FF619A1", + "x": 27934, + "y": -162468, + "z": 13921, + "rotation": -105.92 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F792F6001_1856291813", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E35DE3B3-4AFA4A26-82381BA9-0523F8C4", + "x": 92804, + "y": -173763, + "z": 18656, + "rotation": 90.95 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79306001_1695738994", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "45A967D7-487A9F67-6220978E-5CCCC8B6", + "x": 96360, + "y": -157740, + "z": 15840, + "rotation": -47.21 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79355B01_1612365590", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "55C7CB65-48D484B0-E2C19796-9F210846", + "x": 91912, + "y": -210865, + "z": 1054, + "rotation": 17.29 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79386001_2015526406", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2F612BD4-4EE06FBE-4350F4B7-482EEA2B", + "x": -20892, + "y": -199472, + "z": 14133, + "rotation": -31.09 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F793A8701_1412502928", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "CBDDDC6E-48CB392D-55D5DFB7-AED24644", + "x": 93840, + "y": -34320, + "z": 7100, + "rotation": 2.12 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79486001_1490105224", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F7C79951-40D3823C-3953F08A-E8EEFBAC", + "x": -12888, + "y": -226797, + "z": 1636, + "rotation": 81.25 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79486001_1737515225", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "522F251F-4B8997D7-BAE0FCB8-60866DBD", + "x": -63860, + "y": -217729, + "z": 3088, + "rotation": 90.65 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79486001_1870504226", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C37F927C-45627DF6-02055B83-FF719AD1", + "x": -52582, + "y": -215584, + "z": 2879, + "rotation": -44.94 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79486401_1444694438", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "33E3001A-4523058B-91E54F99-D8A6DDD1", + "x": 89953, + "y": -151456, + "z": 3778, + "rotation": -91.05 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79496001_1203214406", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C5731E5B-4D0260F3-6C6C5B84-3CAFB3A3", + "x": -61759, + "y": -208869, + "z": -1240, + "rotation": -113.81 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79496001_1424029408", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FF1235DB-488AAEC0-F17C8CAA-2BD04BA1", + "x": -65376, + "y": -196565, + "z": 173, + "rotation": 38.69 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79625301_1884959066", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EFA2899F-4A9A6F53-153F09AE-9CF65424", + "x": -35999, + "y": -242816, + "z": 190, + "rotation": -85.43 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79685201_1807114065", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5526D463-4F3CDA2E-9A19868F-0C602BBF", + "x": 22905, + "y": -168541, + "z": -1050, + "rotation": -57.16 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F796C8301_1303426504", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2DDFF23F-41090918-4439DA8C-C1D6BB3E", + "x": 21985, + "y": 169795, + "z": -11810, + "rotation": -112.85 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F796D5201_1366229946", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0A0248E8-46FC70C3-EF2F0093-9BBA9352", + "x": 28912, + "y": -179325, + "z": -1070, + "rotation": -12.11 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79738301_1767542746", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B4CE597B-43A70799-A544A285-3A48A3D8", + "x": -4060, + "y": 147800, + "z": -7030, + "rotation": -96.73 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F798B5F01_1501465945", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "85424437-4756987C-E61FCDAD-B03A16E1", + "x": -48094, + "y": -246385, + "z": -400, + "rotation": -83.91 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F798B5F01_1548531946", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3E2D2B1F-4F738371-5F257582-33EABC8B", + "x": -57983, + "y": -202457, + "z": 357, + "rotation": 27.12 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79955F01_1408102760", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "E7AC4CBC-44176C10-952EAA89-6E8CC757", + "x": -39207, + "y": -198516, + "z": 8225, + "rotation": -118.19 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F799E5F01_1400742291", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2E2556BD-4FC59A47-1D85DBBD-9495C9BA", + "x": 83988, + "y": -199980, + "z": -595, + "rotation": 60.18 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F799E5F01_1682598293", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "82A45A61-4F607263-D77EE784-D2F3EB3A", + "x": 96126, + "y": -208506, + "z": 6984, + "rotation": -122.34 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79A65901_1120267361", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "75090694-4A86D09B-EEBE239E-6D99B106", + "x": 19529, + "y": -219606, + "z": -1787, + "rotation": -83.71 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79A75901_1341271538", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "923725BD-444BF8AE-A1D40EAA-BAF35DAC", + "x": 34921, + "y": -220349, + "z": -1701, + "rotation": 58.47 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79B36201_1122808154", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "4B0274A6-487BF42F-8AA4CEBB-7A01BE63", + "x": 55250, + "y": -168119, + "z": -539, + "rotation": 165.86 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79B36201_1823459155", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B4E37C3C-44413C4C-0AF5CEB3-0ACA9936", + "x": 40619, + "y": -181290, + "z": -1191, + "rotation": -89.28 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79B46201_1396705333", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "50BD1189-462AF5B7-51D08FB7-5BDD70A9", + "x": 33965, + "y": -230994, + "z": -785, + "rotation": -167.24 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79BB6201_1894786568", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "48EEE132-4FE15BAC-714DA1B0-02CF63E2", + "x": 15760, + "y": -233958, + "z": -1129, + "rotation": 160.54 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C66201_1113882509", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FFEA9D43-4022281E-F4E50BA9-44E942B0", + "x": 14700, + "y": -176180, + "z": -1198, + "rotation": -134.95 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C66201_1961069510", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C00E90AE-48E40D71-0C761C86-07F9F7EB", + "x": 16653, + "y": -190924, + "z": -986, + "rotation": -19.26 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C96201_2035056042", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0F73A8C8-44625648-0CA2EC91-EDF6566D", + "x": -13970, + "y": -180298, + "z": -1368, + "rotation": 35.36 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CB6201_1168596395", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "8F501251-4B867711-B16B4DBA-41729E8B", + "x": -14340, + "y": -199165, + "z": -1422, + "rotation": 41.38 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CC6201_1413653574", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "93A51FB3-42CFCB82-9B05AC8F-9376135E", + "x": -37161, + "y": -172970, + "z": 1609, + "rotation": -131.67 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CC6201_1521305575", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F57ECEE9-410D7BC1-A46039BA-79B2BC08", + "x": -41055, + "y": -202378, + "z": 2637, + "rotation": -88.15 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79D38101_2011951550", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "65A110B0-4A50887C-A51339A4-6C66C9CD", + "x": 322367, + "y": -292843, + "z": -878, + "rotation": 87.47 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F25F01_1763105083", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "387F1295-41DF0543-0B8F7680-6C226852", + "x": 63183, + "y": -162727, + "z": 19365, + "rotation": 25.85 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F25F01_2029785085", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "C9FBD2A6-433B4265-ED5DB99E-BF6EA288", + "x": 61387, + "y": -157028, + "z": 16611, + "rotation": 43.53 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F35F01_1129500264", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "79ECDF4C-461171D2-5803B98F-D4D12AD9", + "x": 59145, + "y": -164158, + "z": 18594, + "rotation": 175.79 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F35F01_1275724266", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0A0116FE-41CD020F-82B635AB-62EA9C3B", + "x": 82842, + "y": -160989, + "z": 20260, + "rotation": 165.91 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F35F01_1502667268", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "B585CF46-45D11D10-EF775BBD-922C3E48", + "x": 92401, + "y": -151363, + "z": 21180, + "rotation": 4.66 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F76301_1288406181", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "F528DD03-4620F4AE-E248DF88-32FB4BE4", + "x": 9780, + "y": -220133, + "z": -908, + "rotation": -48.1 + }, + { + "id": "BP_Crystal_C_UAID_4CEDFB3E2F7F8B9201_1239800812", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "05D5F5E4-46667C78-0705F49C-CCC639C6", + "x": 168432, + "y": 78660, + "z": 1260, + "rotation": 19.01 + }, + { + "id": "BP_Crystal_C_UAID_4CEDFB3E2F7F8B9201_1242129813", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5A37A579-4CA2A9F2-9A17C6B2-FBF62D8B", + "x": 160960, + "y": 79690, + "z": 1760, + "rotation": -25.42 + }, + { + "id": "BP_Crystal_C_UAID_4CEDFB3E2F7F8B9201_1243912814", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AC609C5F-499573C5-DFB936A9-65C43580", + "x": 174293, + "y": 76534, + "z": -1156, + "rotation": -94.41 + }, + { + "id": "BP_Crystal_UAID_40B076DF2F79065901_1178046200", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6F03363A-4D8D84CF-511FCFA8-1E1842EA", + "x": -42186, + "y": -248434, + "z": 1621, + "rotation": 81.61 + }, + { + "id": "BP_Crystal_UAID_40B076DF2F79425901_1727560760", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6F2CAE94-46FABD75-40D8729C-540281F6", + "x": -45923, + "y": -232263, + "z": 388, + "rotation": -155.77 + }, + { + "id": "BP_Crystal_UAID_40B076DF2F79BF5D01_1163546993", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "87A34BFD-463EB90E-50F30384-D43B6FD2", + "x": -27169, + "y": -238412, + "z": -1518, + "rotation": -68.17 + }, + { + "id": "BP_Crystal_UAID_40B076DF2F79FD6301_2028775242", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "2456F5DF-465313C0-C0A14285-C2169302", + "x": -16074, + "y": -240293, + "z": -1476, + "rotation": -167.4 + }, + { + "id": "BP_Crystal_mk16", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "1110C7D6-44B80A36-D4E57EAF-455EB10A", + "x": 84699, + "y": -225473, + "z": 1744, + "rotation": -0.24 + }, + { + "id": "BP_Crystal_mk17", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "ECFAC46A-4B48D481-3C9B95B2-175729E4", + "x": 88785, + "y": -211269, + "z": -1330, + "rotation": -86.46 + }, + { + "id": "BP_Crystal_mk17_UAID_40B076DF2F79036301_1202643250", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A268104A-437D7F4B-6DAEFCB5-AF973D8F", + "x": 79880, + "y": -210493, + "z": -1213, + "rotation": 58.03 + }, + { + "id": "BP_Crystal_mk24_11", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "06C4D3F0-4FFCB74D-715E9BBA-C6F3CEEE", + "x": 226306, + "y": -223188, + "z": 5126, + "rotation": 168.2 + }, + { + "id": "BP_Crystal_mk2_C_2", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3AA54FAF-4F16A223-A94DDD8E-307287A0", + "x": -4190, + "y": -210862, + "z": -1383, + "rotation": -71.55 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F007B101_1436486318", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "27C52A3B-4620093B-2FB4B487-61CCE186", + "x": 221677, + "y": -77772, + "z": 10489, + "rotation": 98.44 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0958101_1177712657", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "D35E5874-45201A16-19802797-FAFEBA1A", + "x": -97215, + "y": 18665, + "z": 20330, + "rotation": 171.47 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79006401_1101274773", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "FABD168A-4F05B9A3-0F08AAB3-24B704BC", + "x": -7853, + "y": -228178, + "z": -1773, + "rotation": -30.31 + }, + { + "id": "BP_Crystal_mk33_31", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "663F634A-4840E39A-E5FC4A88-BEDD793A", + "x": 209182, + "y": -33152, + "z": 4346, + "rotation": 53.99 + }, + { + "id": "BP_Crystal_mk45", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "6366AE60-42AD82F9-5CFE32BF-E2C54EFC", + "x": 115470, + "y": -145759, + "z": 1625, + "rotation": -29.83 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F790D6301_1205856025", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DD838DE2-483BD8EC-668335A8-1B2FFC41", + "x": 102686, + "y": -149908, + "z": -1428, + "rotation": -97.5 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79106201_1437870469", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "15CF0D9C-4D7FD71C-1A714D91-0FEDCB32", + "x": 125449, + "y": -149826, + "z": 499, + "rotation": -152.09 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79106201_1550891470", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "5F4CA185-4870B9E2-FAA6D480-47D8B0CD", + "x": 127085, + "y": -158390, + "z": 136, + "rotation": -171.54 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F791E6201_1165229929", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0DB87C4C-48603CCE-36134D90-DC98FCF8", + "x": 133541, + "y": -171204, + "z": 340, + "rotation": -42.08 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79236201_1250559816", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "DCAAF9AE-41E0960E-D2693595-707D7BE0", + "x": 142763, + "y": -194313, + "z": -1554, + "rotation": -155.4 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79626201_1797282896", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BDD3CCB9-4E06A55C-820C30B0-29489F56", + "x": 151858, + "y": -205861, + "z": -1132, + "rotation": -143.94 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79646201_1982914249", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3EAE7C8E-45857374-03F081B2-740C5324", + "x": 126266, + "y": -193749, + "z": 676, + "rotation": -36.94 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F798F6001_1203275705", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0A0ABD64-41A3D3FF-DE9D23A5-B2EB7312", + "x": 126704, + "y": -140831, + "z": 398, + "rotation": -111.87 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79916001_1775397058", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "BF869BC0-448267E7-A41D4DA2-6A9916F5", + "x": 91625, + "y": -157717, + "z": 70, + "rotation": 162.95 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79946001_1613712588", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "A718D309-478C22FE-426542BE-D5489F0B", + "x": 79236, + "y": -171572, + "z": 16, + "rotation": 13.73 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79956001_1285061765", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "0427BEA3-4F651D07-74F047AC-F9348F9A", + "x": 93307, + "y": -176808, + "z": -798, + "rotation": -150.64 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79B26201_1656659976", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "AA7DBC5F-4D62C442-46E95282-E11688EB", + "x": 66848, + "y": -178461, + "z": -1388, + "rotation": 161.24 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79B26201_1806151977", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "17059FAB-44E2C842-EF5C3094-B104BA0C", + "x": 77799, + "y": -185467, + "z": -1284, + "rotation": -59.46 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79BB6101_1629973504", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "EEAE5AB1-490F21A2-76104E84-A089A7E0", + "x": 108720, + "y": -179155, + "z": -964, + "rotation": 49.01 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79C06101_1984578386", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "245A6063-492B9D98-F94827AB-171FBE17", + "x": 119553, + "y": -228340, + "z": -942, + "rotation": -0.19 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79C26101_1487303739", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "3FA8C54B-4A3A2DD0-073E7BB1-AB4F1411", + "x": 100085, + "y": -208456, + "z": -1633, + "rotation": -79.56 + }, + { + "id": "BP_Crystal_mk46", + "type": "slugMk1", + "classPath": "BP_Crystal_C", + "pickupGuid": "903A0CAD-45BB8F6D-942F20BE-78E356ED", + "x": 109061, + "y": -159161, + "z": -340, + "rotation": 0 + }, + { + "id": "BP_Crystal138", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "31B7FA55-424AFAAE-F47B48AD-972E07E6", + "x": 228520, + "y": -222137, + "z": 9364, + "rotation": -68.77 + }, + { + "id": "BP_Crystal17_UAID_40B076DF2F79245C01_1529847652", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "42F61AE7-41406BD9-DB47048B-8245C040", + "x": 85925, + "y": -182500, + "z": -487, + "rotation": 58.1 + }, + { + "id": "BP_Crystal8_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "371B3423-4D785CF5-A7393698-47E3F50F", + "x": 143827, + "y": 125949, + "z": 16240, + "rotation": 115.7 + }, + { + "id": "BP_Crystal_C_40", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C4D1E579-42253938-0468EF85-64890CAA", + "x": 183859, + "y": 104898, + "z": 5404, + "rotation": -11.46 + }, + { + "id": "BP_Crystal_C_42", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "634E8743-41465A71-79B18080-24B1E102", + "x": 153421, + "y": 113802, + "z": 7941, + "rotation": 71.21 + }, + { + "id": "BP_Crystal_C_48", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9B986305-4F34E9B4-F29027B6-F59FFEDB", + "x": 168304, + "y": 62749, + "z": 12327, + "rotation": -0.14 + }, + { + "id": "BP_Crystal_C_49", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A1D54011-4C5A867C-E7085BBA-C75B029C", + "x": 157117, + "y": 78593, + "z": 12539, + "rotation": -12.46 + }, + { + "id": "BP_Crystal_C_50", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BBE212F3-49237E50-1BD5F5A6-C55CC0E8", + "x": 164906, + "y": 94881, + "z": 8701, + "rotation": -150.17 + }, + { + "id": "BP_Crystal_C_62", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BF984E60-4DCC0B30-BF80D1B2-6520C616", + "x": 149462, + "y": -176538, + "z": -915, + "rotation": -47.9 + }, + { + "id": "BP_Crystal_C_66", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7A3CAB38-462D5096-EA9EFDBB-D93ABDAA", + "x": 246888, + "y": 150580, + "z": 9420, + "rotation": 105.76 + }, + { + "id": "BP_Crystal_C_67", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F548C438-474DC192-DE544D97-540C8CD2", + "x": 209309, + "y": 154859, + "z": 50, + "rotation": -55.56 + }, + { + "id": "BP_Crystal_C_69", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8439B055-4C45CBDA-1ECF3797-4C5E1591", + "x": 201315, + "y": 169308, + "z": 11448, + "rotation": -33.31 + }, + { + "id": "BP_Crystal_C_70", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3C1546B1-438ED7E7-1DDC8DA7-10BCDA3E", + "x": 220420, + "y": 122285, + "z": 4015, + "rotation": 35.88 + }, + { + "id": "BP_Crystal_C_72", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0E5B23F6-42E0A329-76BD8193-3C264148", + "x": 210046, + "y": 134583, + "z": -81, + "rotation": 61.02 + }, + { + "id": "BP_Crystal_C_73", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1233DEBA-473E0587-A32E3A8B-D3B8BC06", + "x": 224929, + "y": 131825, + "z": 4723, + "rotation": -49.98 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05F7801_1738752601", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "19622083-4392A76B-2CE024BD-51DC55F8", + "x": 245919, + "y": -12133, + "z": 9374, + "rotation": 60.39 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0607801_1667283784", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "02019F78-4921FAF8-9E8053B2-32FDA9E0", + "x": 248612, + "y": -35291, + "z": 14055, + "rotation": 78.32 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0607801_2027765785", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C7CAB791-4F5170C9-2B1508BA-35403F07", + "x": 227632, + "y": -37474, + "z": 17205, + "rotation": -84.83 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0767801_1471881678", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "36F3D98A-4E3D6257-EFB80BB9-6BD08360", + "x": 128192, + "y": -4493, + "z": 17861, + "rotation": -24.24 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F09E7401_1282285465", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "29ACC6FB-4604C032-A896998D-D657B556", + "x": 151596, + "y": -11018, + "z": 18890, + "rotation": 38.76 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AE7301_1236769225", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "04491E65-412BC6C7-D8225FB2-6DA6C241", + "x": 199623, + "y": -23749, + "z": 12429, + "rotation": 20.49 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0AE7301_1916140227", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B384BEE6-48ECAF02-C4BAA390-8085297B", + "x": 216223, + "y": -29985, + "z": 18055, + "rotation": 29.82 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B37301_2109802113", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FCC858FD-416A96DD-2D4706AC-725451E1", + "x": 101656, + "y": 45831, + "z": 22441, + "rotation": 1.09 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B47301_1731763291", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9CDB9934-445ACE7E-50912FAA-A323E180", + "x": 74776, + "y": 28178, + "z": 12092, + "rotation": -172.7 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B47301_1859830293", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "86F916BF-4C4BC567-22F74BB6-C6A75FB7", + "x": 89182, + "y": 14541, + "z": 17865, + "rotation": 84.67 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B57301_1300199470", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B80388A6-45F5363A-BDB48B8A-496D2905", + "x": 115680, + "y": 39405, + "z": 12359, + "rotation": 163.91 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79016301_2047367890", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F3176902-492EDE13-E18EC1A1-70F1A5FD", + "x": 71017, + "y": -203899, + "z": -1174, + "rotation": 108.77 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F790F6201_1248400288", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A14C9919-419A3594-829ED185-8D4E962A", + "x": 121522, + "y": -206978, + "z": 266, + "rotation": 83.33 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79486001_2099963228", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CF997F82-4A3AF2B8-C1FCC080-C22B5F73", + "x": -57685, + "y": -232489, + "z": 1480, + "rotation": -82.58 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F798E5F01_1514525492", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6ABDDECA-4D1111EB-99C60BA4-2D35E98C", + "x": 90408, + "y": -195365, + "z": -886, + "rotation": -140.82 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F798E5F01_1854991497", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "30A47DE2-4BB828D1-7852928B-65CF9026", + "x": 36929, + "y": -201651, + "z": -789, + "rotation": 49.57 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79A35901_1559939832", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "814995C1-41DA0713-9934D5A2-4D3330A3", + "x": 25605, + "y": -220837, + "z": -1745, + "rotation": -83.03 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79B56201_1247281510", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6F9897F2-413CFF74-3B3BD093-0E1EBF1F", + "x": 29879, + "y": -234711, + "z": 1798, + "rotation": -163.77 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C56201_1257600332", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6CCA000F-460B5F45-80F277BD-2DD2A810", + "x": 28862, + "y": -204036, + "z": -956, + "rotation": 15.09 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C66201_2074565511", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "20212B6E-4F08BFDD-C5F85B88-01FD2610", + "x": 17912, + "y": -207272, + "z": -1197, + "rotation": -127.24 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79C96201_1094933041", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9F55A518-477A9AE5-2870A0B5-3835E6A0", + "x": 9445, + "y": -203096, + "z": -1151, + "rotation": -62.7 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CB6201_1211861396", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "59D45F16-40870C1A-90F5D8BA-180D2DC3", + "x": -20884, + "y": -218321, + "z": -1205, + "rotation": -22.83 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CB6201_2127484397", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9A4EA82E-40676965-F89C1ABE-65BF5E36", + "x": -34837, + "y": -222461, + "z": -1389, + "rotation": -115.76 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79CC6201_1781367576", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B2156A3B-43A937AF-75FB0BBA-C08AA51A", + "x": -39167, + "y": -252318, + "z": -1329, + "rotation": -95.19 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F35F01_1207047265", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FA57C603-452B8820-BC145EBB-BD432B07", + "x": 70976, + "y": -160164, + "z": 17495, + "rotation": -145.29 + }, + { + "id": "BP_Crystal_UAID_40B076DF2F79FD6301_1735716241", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8B617E9B-437574CF-55C855A2-78F7082B", + "x": -30462, + "y": -256501, + "z": -106, + "rotation": -50.6 + }, + { + "id": "BP_Crystal_mk10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3703F816-47344EE6-647742AC-C68AC647", + "x": 8908, + "y": -220732, + "z": 2203, + "rotation": 6.66 + }, + { + "id": "BP_Crystal_mk14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7E2AA4A4-4110559E-25DD3CA4-BAD436D0", + "x": 47454, + "y": -239545, + "z": -518, + "rotation": 176.32 + }, + { + "id": "BP_Crystal_mk19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8FA77726-4DF5E0BB-C03E4688-57CBF4D1", + "x": -23774, + "y": -186056, + "z": 2571 + }, + { + "id": "BP_Crystal_mk20_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C4F975C9-433F9E41-B77A088A-8A2C4F2E", + "x": 163980, + "y": -206176, + "z": 20755, + "rotation": -11.16 + }, + { + "id": "BP_Crystal_mk21", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AEE86E22-42518DCB-AED982A4-29838148", + "x": -126517, + "y": 169345, + "z": -5713, + "rotation": 86.46 + }, + { + "id": "BP_Crystal_mk210", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BFA2394F-4D2928DC-CE48ECA4-37350036", + "x": -54094, + "y": 172148, + "z": 1833, + "rotation": 72.54 + }, + { + "id": "BP_Crystal_mk210_0", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CE67B75A-4360E707-AC526AA3-BF6B6836", + "x": -41315, + "y": 82767, + "z": 27301, + "rotation": -91.24 + }, + { + "id": "BP_Crystal_mk210_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CB7C1DCA-428BA53A-CD91E598-4C83BFF4", + "x": -89793, + "y": 19022, + "z": 20985, + "rotation": 46.16 + }, + { + "id": "BP_Crystal_mk210_16", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D4D45425-479F5073-B67A5BB2-D746CD48", + "x": 125263, + "y": 244040, + "z": -7112, + "rotation": -39.43 + }, + { + "id": "BP_Crystal_mk210_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E7E5C32F-4B3FF114-CCF453B3-CD5E4000", + "x": -200043, + "y": -123746, + "z": 1763, + "rotation": 127.59 + }, + { + "id": "BP_Crystal_mk210_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "625245A9-4D7F547D-647336A3-DC64DAD0", + "x": -142519, + "y": 185884, + "z": 2271, + "rotation": -87.08 + }, + { + "id": "BP_Crystal_mk210_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "10B679B8-4CC7DC32-12A2F1B3-80F8A321", + "x": 250724, + "y": -270294, + "z": 1862, + "rotation": 6.18 + }, + { + "id": "BP_Crystal_mk210_47", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9C4C4770-437438BE-59F657A7-75B042C1", + "x": 194853, + "y": -69260, + "z": 4454, + "rotation": -81.09 + }, + { + "id": "BP_Crystal_mk211", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0A3690BE-484BB747-E4CE29A4-0F0D30ED", + "x": -12195, + "y": 283251, + "z": 3348, + "rotation": 130.88 + }, + { + "id": "BP_Crystal_mk211_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "16CE97A6-4FC9CE7B-ED410895-9E67D772", + "x": 18247, + "y": 88332, + "z": 24814, + "rotation": -11.07 + }, + { + "id": "BP_Crystal_mk211_17", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "53C64EC6-4D6F65EA-D8D384B3-9BE2FACC", + "x": 124452, + "y": 237040, + "z": -2512, + "rotation": -15.34 + }, + { + "id": "BP_Crystal_mk211_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "394AF810-46FFD2A7-FC810B8E-E3DAA9A3", + "x": -137217, + "y": -196704, + "z": 6528, + "rotation": -97.49 + }, + { + "id": "BP_Crystal_mk211_24", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E9EC9AD5-4E113D31-839D8CB3-F04DC47C", + "x": 237319, + "y": -271874, + "z": 6302, + "rotation": -152.29 + }, + { + "id": "BP_Crystal_mk211_48", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C6737E19-4F820A77-DA0C9287-0F6A8D99", + "x": 45581, + "y": -125435, + "z": 2308, + "rotation": 25.59 + }, + { + "id": "BP_Crystal_mk211_85", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8FB03427-4B55E645-6CD55E95-9B7D01F9", + "x": -156426, + "y": 16550, + "z": 18194, + "rotation": 21.81 + }, + { + "id": "BP_Crystal_mk211_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4D397378-4A47F7A5-0C6DA88E-A51BD7E9", + "x": -163138, + "y": 91518, + "z": -207, + "rotation": 29.37 + }, + { + "id": "BP_Crystal_mk212", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "64B7FB41-4B7E4517-D10D05BF-2FE55113", + "x": 192813, + "y": -41491, + "z": 5780, + "rotation": 168.11 + }, + { + "id": "BP_Crystal_mk212_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EB9B8F77-4B22E369-BD897FA2-59D91659", + "x": -90474, + "y": 119845, + "z": 6337, + "rotation": 118.93 + }, + { + "id": "BP_Crystal_mk212_16", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BEE42040-48528BCC-FC9F8D84-2476FAF7", + "x": -18871, + "y": 42347, + "z": 26923, + "rotation": -149.49 + }, + { + "id": "BP_Crystal_mk212_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7D50F463-416E9FB9-C6B99394-146C19BE", + "x": 142239, + "y": 161305, + "z": 5205, + "rotation": 145.66 + }, + { + "id": "BP_Crystal_mk212_25", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9F73B705-42CCF1EB-8EE0FA98-E27F5452", + "x": 268754, + "y": -263821, + "z": 4230, + "rotation": -143.04 + }, + { + "id": "BP_Crystal_mk212_86", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2B0ECB1D-45418E0C-0577E3B9-0149517B", + "x": -169826, + "y": 206, + "z": 20487, + "rotation": 174.54 + }, + { + "id": "BP_Crystal_mk212_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "172A12EB-4F9FB755-B0F226BA-2712BDBE", + "x": 3337, + "y": 121870, + "z": 2106, + "rotation": 2.08 + }, + { + "id": "BP_Crystal_mk213", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "97F4CCBA-48BE6959-910D02BE-8D62F09D", + "x": -143655, + "y": 145514, + "z": 4375, + "rotation": -130.83 + }, + { + "id": "BP_Crystal_mk213_17", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "51E02615-40A736C8-8C0C42B3-DF1988DC", + "x": 4961, + "y": -4576, + "z": 23476, + "rotation": -137.48 + }, + { + "id": "BP_Crystal_mk213_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "898A78F1-4493500D-3B885C9F-7B0B4F6D", + "x": 152769, + "y": 232177, + "z": -9410, + "rotation": 50.79 + }, + { + "id": "BP_Crystal_mk213_22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D54A82C1-44FA7F38-A275B7B3-2DA221A7", + "x": -137229, + "y": -171261, + "z": 37362, + "rotation": -119.01 + }, + { + "id": "BP_Crystal_mk213_26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CCA4A8ED-4A3216C6-E64F63AE-13B225EE", + "x": 192491, + "y": -314104, + "z": 4072, + "rotation": 66.77 + }, + { + "id": "BP_Crystal_mk213_52", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "72A71935-491C453A-9D8F7DAE-00A05D96", + "x": 162443, + "y": -59650, + "z": 10362, + "rotation": -145.99 + }, + { + "id": "BP_Crystal_mk213_87", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "183CF4A2-40160B42-442A0F84-68DE5B46", + "x": -99992, + "y": 26448, + "z": 21435, + "rotation": 115.16 + }, + { + "id": "BP_Crystal_mk214", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FF7B6D07-400E1073-283818A1-317F43AD", + "x": 85802, + "y": -135144, + "z": 7883, + "rotation": -50.33 + }, + { + "id": "BP_Crystal_mk214_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "48F79BEE-4794FDA1-29C8F78F-843058A3", + "x": 8023, + "y": 16307, + "z": 23336, + "rotation": 102.56 + }, + { + "id": "BP_Crystal_mk214_88", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6A7AC564-48C9DE18-51C7D68D-7ECC96AA", + "x": -132430, + "y": -4225, + "z": 22880, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk214_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FDD78A4C-4914AFB1-32152B90-D4A41BCC", + "x": -132713, + "y": -133456, + "z": 6457, + "rotation": 92.79 + }, + { + "id": "BP_Crystal_mk215", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EAA08FB4-47D7FD0B-B3B1A38B-5D66661C", + "x": -206689, + "y": -191820, + "z": 7609, + "rotation": -1.67 + }, + { + "id": "BP_Crystal_mk215_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "82099AE5-42B5B0A3-F11A3C94-A0B786D4", + "x": 8120, + "y": 80104, + "z": 21411, + "rotation": -85.6 + }, + { + "id": "BP_Crystal_mk215_95", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "948E71A3-4DD41FF4-EA9F36AC-F906A121", + "x": -86214, + "y": 73390, + "z": 20805, + "rotation": -48.86 + }, + { + "id": "BP_Crystal_mk216", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2E696766-40D9A66B-F9D7E9A5-52DF3988", + "x": -286304, + "y": -127242, + "z": 2346, + "rotation": -38.62 + }, + { + "id": "BP_Crystal_mk216_106", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DCF6EB54-45A12E1F-893350AB-C9E9D83F", + "x": -59983, + "y": 42597, + "z": 22058, + "rotation": 61.07 + }, + { + "id": "BP_Crystal_mk216_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CB59BDC6-4ECC33E2-6449E79D-93E9BBFA", + "x": 29626, + "y": -4633, + "z": 22391, + "rotation": 47.57 + }, + { + "id": "BP_Crystal_mk217", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F6C2024B-4312AFC0-49A31C8A-1CC3DA92", + "x": 29385, + "y": 55906, + "z": 3854, + "rotation": -177.97 + }, + { + "id": "BP_Crystal_mk217_91", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AE71BCA5-4B02AAA8-CF4E26BE-650B8CEF", + "x": -76831, + "y": 61150, + "z": 20636, + "rotation": -135.82 + }, + { + "id": "BP_Crystal_mk218", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D6CD7D45-4AA407A8-4CDB55AF-41773E76", + "x": -51526, + "y": 55751, + "z": 24633, + "rotation": 51.52 + }, + { + "id": "BP_Crystal_mk219", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "33AB6B2E-44FC0C01-67088693-B72892D7", + "x": -122308, + "y": 28473, + "z": 19845, + "rotation": 56.9 + }, + { + "id": "BP_Crystal_mk21_0", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5E84BFE8-4B5DEAF6-6C5D3D93-04FDA138", + "x": 10539, + "y": 267942, + "z": -3765, + "rotation": 34.72 + }, + { + "id": "BP_Crystal_mk21_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BD038942-411EAFD8-979E079F-2BE3A2F0", + "x": 373948, + "y": -241997, + "z": 4106, + "rotation": -12.47 + }, + { + "id": "BP_Crystal_mk21_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "586C3583-4E79ABA2-720D5E83-B62B4529", + "x": -82983, + "y": -3903, + "z": 24093, + "rotation": -108.26 + }, + { + "id": "BP_Crystal_mk21_12", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6EF6D0E5-4447F59D-3927AF83-149278E4", + "x": 324894, + "y": -32673, + "z": 557, + "rotation": -19.46 + }, + { + "id": "BP_Crystal_mk21_13", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CF548BA9-4F6A72E8-B1F5F188-17C9BD9E", + "x": 252289, + "y": -275567, + "z": 9398, + "rotation": 94.09 + }, + { + "id": "BP_Crystal_mk21_21", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "56FECDD6-4529A935-B9C030A5-A4C455B7", + "x": -167204, + "y": 45892, + "z": 21913, + "rotation": -12.13 + }, + { + "id": "BP_Crystal_mk21_22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BA247A97-431B12E8-DC43B2AC-FBC5F8E2", + "x": 25995, + "y": -47247, + "z": 18669, + "rotation": -176.91 + }, + { + "id": "BP_Crystal_mk21_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0C56F1CC-4DB90E31-6977C586-1A850383", + "x": 113855, + "y": -46988, + "z": 12305, + "rotation": -59.48 + }, + { + "id": "BP_Crystal_mk21_27", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E3B4DE42-4CCCDC90-311A9998-32F5FE6E", + "x": 70194, + "y": 67573, + "z": 17283, + "rotation": -159.85 + }, + { + "id": "BP_Crystal_mk21_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E8FCA5D7-479A1E4A-279DB28D-8FF125D1", + "x": -264745, + "y": 74082, + "z": -910, + "rotation": -7.74 + }, + { + "id": "BP_Crystal_mk21_33", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A1B5B345-4CAF5E13-C776BDAB-C6E36FE1", + "x": 143693, + "y": -43679, + "z": 4641, + "rotation": -35.89 + }, + { + "id": "BP_Crystal_mk21_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AA585E82-440AD02F-B8A392B5-F6E20596", + "x": 359153, + "y": -112871, + "z": 9360, + "rotation": 154.14 + }, + { + "id": "BP_Crystal_mk21_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1F72C91E-49FEBDFD-176D649E-067E80E7", + "x": -248684, + "y": -168365, + "z": 5078, + "rotation": 140.04 + }, + { + "id": "BP_Crystal_mk21_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "504CE99B-4910FA17-89E5E483-BA81C581", + "x": 157444, + "y": 165267, + "z": -3296, + "rotation": 29.7 + }, + { + "id": "BP_Crystal_mk21_8", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1289B8FA-418BAED3-4F449887-5AB1DFD7", + "x": 244265, + "y": -215284, + "z": 5474, + "rotation": 46.78 + }, + { + "id": "BP_Crystal_mk22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E8414103-47A37D68-6D50A2AE-D4FCAEBE", + "x": -144088, + "y": -182547, + "z": -977, + "rotation": -113.77 + }, + { + "id": "BP_Crystal_mk220", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "596B9F60-4C8F3413-7C9E6B89-99992C9C", + "x": -62254, + "y": 74170, + "z": 20370, + "rotation": -179.56 + }, + { + "id": "BP_Crystal_mk221", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2B200061-49890C54-FE81ADB8-4B6F12BF", + "x": 251112, + "y": 79026, + "z": -1671, + "rotation": -166.66 + }, + { + "id": "BP_Crystal_mk222_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "78ADAD0C-4949DBF8-0A8B168D-4FAE8BC8", + "x": 226165, + "y": 12414, + "z": -1655, + "rotation": -19.53 + }, + { + "id": "BP_Crystal_mk223_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D06FCB42-4E1C6ADF-4C5C7890-1709AB6C", + "x": 309131, + "y": 15106, + "z": 3468, + "rotation": -161.92 + }, + { + "id": "BP_Crystal_mk224", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "23F2554B-4F607F3C-1672D398-26C21B31", + "x": 291502, + "y": 31379, + "z": -931, + "rotation": -9.39 + }, + { + "id": "BP_Crystal_mk225", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2962D4C8-4F88A8DB-454F7EBE-D0DC3696", + "x": 249765, + "y": 110459, + "z": -1958, + "rotation": -103.03 + }, + { + "id": "BP_Crystal_mk226", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7E0FB8A9-4B3CDB7C-90498A8C-D71C3FE0", + "x": 250130, + "y": 11353, + "z": -1205, + "rotation": 97.43 + }, + { + "id": "BP_Crystal_mk227", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DB3962A9-4B152DE8-727616BE-A716B549", + "x": 257277, + "y": -14575, + "z": 5681, + "rotation": -88.6 + }, + { + "id": "BP_Crystal_mk228", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AB29E994-46D8B9D3-9C323B9E-F3E045D7", + "x": 287375, + "y": 38388, + "z": -1474, + "rotation": 114.14 + }, + { + "id": "BP_Crystal_mk229", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "738C4573-4D4766FA-B0F161AA-F63DC4E7", + "x": 220847, + "y": 65711, + "z": -1193, + "rotation": -140.57 + }, + { + "id": "BP_Crystal_mk22_0", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "379C6CC2-4B841454-A90E61B3-9994BBCE", + "x": -174330, + "y": 180824, + "z": 6287, + "rotation": 37.65 + }, + { + "id": "BP_Crystal_mk22_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "45F28E09-45D31D43-3F9678A3-23C2525B", + "x": -24915, + "y": 184968, + "z": -922, + "rotation": 106.45 + }, + { + "id": "BP_Crystal_mk22_10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "186BFCC1-476D13B1-DB0AB7A0-36033637", + "x": -256160, + "y": 115332, + "z": -634, + "rotation": -34.44 + }, + { + "id": "BP_Crystal_mk22_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AE943BAB-488B55C3-F13142B2-ED668B42", + "x": 286954, + "y": -288060, + "z": 22985, + "rotation": 5.22 + }, + { + "id": "BP_Crystal_mk22_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7D556616-4B0D69A1-6A7D7784-BCD7C819", + "x": 343442, + "y": -76969, + "z": 5861, + "rotation": 137.7 + }, + { + "id": "BP_Crystal_mk22_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0C151856-461C916B-EB167B90-F355AB96", + "x": 296551, + "y": -288371, + "z": 18058, + "rotation": 120.44 + }, + { + "id": "BP_Crystal_mk22_22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8939E66D-46EDCEEE-15D68788-544836CA", + "x": -69961, + "y": -14796, + "z": 23641, + "rotation": -32.69 + }, + { + "id": "BP_Crystal_mk22_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "96E9A98A-4A6878B8-B56D7C8E-C4B94817", + "x": 30727, + "y": -20890, + "z": 13456, + "rotation": -2.01 + }, + { + "id": "BP_Crystal_mk22_24", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F65BA4C9-464BFD73-19B41780-8711CE3B", + "x": -53446, + "y": -486, + "z": 23039, + "rotation": -57.86 + }, + { + "id": "BP_Crystal_mk22_28", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FE12949E-409C98D4-3C89E0B3-26A57545", + "x": 60744, + "y": 98718, + "z": 12722, + "rotation": -157.8 + }, + { + "id": "BP_Crystal_mk22_34", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5F8ECA4A-48A864B5-063DB2B7-24DFAF4E", + "x": 160026, + "y": -34908, + "z": 9359, + "rotation": -1.74 + }, + { + "id": "BP_Crystal_mk22_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D08AC529-4FBD1750-352369BF-3DA5F306", + "x": 396174, + "y": -181216, + "z": 3861, + "rotation": -18.92 + }, + { + "id": "BP_Crystal_mk22_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1AC4C57A-4E441876-F35BB886-B9FD772A", + "x": -15463, + "y": -47131, + "z": 14203, + "rotation": -106.22 + }, + { + "id": "BP_Crystal_mk22_8", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8816DE66-47EC6C51-2F4EB3AF-0B217211", + "x": 175011, + "y": 161669, + "z": 13101, + "rotation": 35.61 + }, + { + "id": "BP_Crystal_mk22_88", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F8AB1FA7-4D359A2D-D6656A90-DA3C980B", + "x": 278445, + "y": -11379, + "z": 10741, + "rotation": 140.62 + }, + { + "id": "BP_Crystal_mk23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F48C1C83-4CD582A4-ADCE1C98-E9331A2F", + "x": -13806, + "y": -187350, + "z": 4194, + "rotation": -65.61 + }, + { + "id": "BP_Crystal_mk230", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8F5C7695-45F39FCB-2D4839A3-39DF0968", + "x": 271675, + "y": -267406, + "z": 12223, + "rotation": 17.2 + }, + { + "id": "BP_Crystal_mk23_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A0B79FE8-4C78907E-4649D281-5BF29D23", + "x": -186682, + "y": 55910, + "z": 10871, + "rotation": -18.11 + }, + { + "id": "BP_Crystal_mk23_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1858FCBB-4D33B97A-6DA24D99-E3B51DDF", + "x": -216627, + "y": 127790, + "z": -779, + "rotation": -34.31 + }, + { + "id": "BP_Crystal_mk23_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1033779E-43FBCF2B-BBEE3585-2A2C1835", + "x": -46871, + "y": -57634, + "z": 16076, + "rotation": -175.29 + }, + { + "id": "BP_Crystal_mk23_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "098AB7C7-4CFD5F82-EC8614B4-13C5B0A7", + "x": 280601, + "y": -286689, + "z": 1632, + "rotation": 50.67 + }, + { + "id": "BP_Crystal_mk23_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "84FB5463-4BFA7047-F8C7B7B7-F0C5CE5F", + "x": 22335, + "y": 182534, + "z": -7747, + "rotation": -148.42 + }, + { + "id": "BP_Crystal_mk23_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7D73D66C-43A94D8D-C1E63099-ADC3ED1F", + "x": -80489, + "y": 47701, + "z": 21570, + "rotation": -134.86 + }, + { + "id": "BP_Crystal_mk23_21", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AA326FD4-4B064238-F7EA4AB1-FEEAB049", + "x": 399632, + "y": -98188, + "z": -421, + "rotation": -53.59 + }, + { + "id": "BP_Crystal_mk23_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "22A0A716-48600BC1-72C9088B-CE970A81", + "x": 254685, + "y": 135377, + "z": -3415, + "rotation": -86.48 + }, + { + "id": "BP_Crystal_mk23_24", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B33680B6-4176BF8F-985690A0-CCFA1128", + "x": -4838, + "y": -25353, + "z": 19436, + "rotation": 123.53 + }, + { + "id": "BP_Crystal_mk23_25", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F9A50984-4AC4D97C-0CFFB78D-5A754CC3", + "x": -55193, + "y": 23146, + "z": 21706, + "rotation": 21.47 + }, + { + "id": "BP_Crystal_mk23_29", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8D62B48F-487E44B8-884229B1-4F4C9082", + "x": 131772, + "y": 133791, + "z": 12903, + "rotation": 148.34 + }, + { + "id": "BP_Crystal_mk23_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EBC89DE9-4098BB01-71DC5DA4-ED0EB3E2", + "x": 134639, + "y": 218683, + "z": 876, + "rotation": 64.23 + }, + { + "id": "BP_Crystal_mk24", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CD8CBB29-41C13463-0ECC219E-29063669", + "x": 91594, + "y": -55203, + "z": 8920, + "rotation": -171.23 + }, + { + "id": "BP_Crystal_mk24_0", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8ED2CCA1-40C7C9C5-2AD17188-B643EA46", + "x": -11343, + "y": -149365, + "z": 12213, + "rotation": 135.35 + }, + { + "id": "BP_Crystal_mk24_10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "58DAB331-4B6180B6-09B027BD-24DAD273", + "x": 148346, + "y": 263033, + "z": 9910, + "rotation": 6.54 + }, + { + "id": "BP_Crystal_mk24_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2976C8FA-4960237F-6609D4B9-1B619A2D", + "x": -101168, + "y": -68268, + "z": 13817, + "rotation": 96.8 + }, + { + "id": "BP_Crystal_mk24_16", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FAF01238-4D7BD8E8-99CAC28C-101D3499", + "x": 298037, + "y": -59179, + "z": 7096, + "rotation": 24.55 + }, + { + "id": "BP_Crystal_mk24_17", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A9A72D41-40EE03B3-EC4B91B6-84CCAA41", + "x": 241758, + "y": -302467, + "z": 2506, + "rotation": 52.93 + }, + { + "id": "BP_Crystal_mk24_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "84730934-49471F1C-C3B9A79C-EBCEE36F", + "x": -140202, + "y": 62486, + "z": 11797, + "rotation": 45.99 + }, + { + "id": "BP_Crystal_mk24_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4D9C6041-46E72BFD-02CF95A1-0F7D1AA1", + "x": -104847, + "y": 63694, + "z": 21512, + "rotation": -133.29 + }, + { + "id": "BP_Crystal_mk24_25", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D7F4C083-44716385-FC00B79F-10CB06CB", + "x": 54817, + "y": 14339, + "z": 13498, + "rotation": 122.75 + }, + { + "id": "BP_Crystal_mk24_26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7F38FC01-43378C46-7B1EC59D-B2562472", + "x": -20426, + "y": 7257, + "z": 25365, + "rotation": 29.58 + }, + { + "id": "BP_Crystal_mk24_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "49FF05CC-45CE1741-46702385-E88F979C", + "x": -5944, + "y": 266008, + "z": 677, + "rotation": 0.44 + }, + { + "id": "BP_Crystal_mk24_30", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1957735B-4DBCF626-1D0DD0A9-D39D36F2", + "x": 147807, + "y": 86563, + "z": 17688, + "rotation": 3.34 + }, + { + "id": "BP_Crystal_mk24_89", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1339E28D-4759B6BE-7B4161AE-41709161", + "x": 289937, + "y": 13139, + "z": 5262, + "rotation": 129.76 + }, + { + "id": "BP_Crystal_mk25", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AA490B21-4BED8ECC-91A105BF-BEC09845", + "x": 137138, + "y": -36545, + "z": 14123, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk25_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4A4A1DE1-42D71D70-F117F0B5-D2193B99", + "x": -64714, + "y": -129057, + "z": 3804, + "rotation": -69.18 + }, + { + "id": "BP_Crystal_mk25_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5907DB18-48B78896-34332689-2237614E", + "x": 185231, + "y": 209199, + "z": 4808, + "rotation": -162.33 + }, + { + "id": "BP_Crystal_mk25_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "BB93FCCD-4839CA29-9514B5A8-A14F3FA8", + "x": 23069, + "y": 56043, + "z": 21881, + "rotation": 24.6 + }, + { + "id": "BP_Crystal_mk25_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6B4EEF3A-4582A34E-3DC06DA6-27CCBE0C", + "x": 264768, + "y": -307157, + "z": 1218, + "rotation": 91.08 + }, + { + "id": "BP_Crystal_mk25_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "422233F7-4FD0F936-DA352AA1-D22E4B99", + "x": -20905, + "y": -9428, + "z": 21602, + "rotation": -67.57 + }, + { + "id": "BP_Crystal_mk25_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0CD0B4F6-4C7D0131-D5E9A9BC-96C69FDA", + "x": -145229, + "y": -153750, + "z": 19247, + "rotation": 121.58 + }, + { + "id": "BP_Crystal_mk25_26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "250BAE80-40B669BA-36959CAB-0A7EC404", + "x": 65935, + "y": -1704, + "z": 13496, + "rotation": 135.68 + }, + { + "id": "BP_Crystal_mk25_27", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FC3D4CD3-411CF034-00AB249C-2E4559B3", + "x": -154659, + "y": 49170, + "z": 24059, + "rotation": -29.55 + }, + { + "id": "BP_Crystal_mk25_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0AADA9B0-404C42EC-E9B2F28F-5197C1C8", + "x": -64067, + "y": 123969, + "z": 14867, + "rotation": 86.08 + }, + { + "id": "BP_Crystal_mk25_31", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FBFD1D79-4101A8B5-0371A9A6-36F62648", + "x": 91826, + "y": 76581, + "z": 13587, + "rotation": 70.29 + }, + { + "id": "BP_Crystal_mk25_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A439CF74-4F81C50C-7B9A8290-FB63CFEB", + "x": -31863, + "y": 304196, + "z": -1471, + "rotation": 112.04 + }, + { + "id": "BP_Crystal_mk25_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "357D677B-438D425E-CBFB19A2-8381B6D4", + "x": 297231, + "y": -289118, + "z": 1615, + "rotation": -48.41 + }, + { + "id": "BP_Crystal_mk25_90", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AE2CD030-49D7E832-BAF910B9-1C0B800D", + "x": 241825, + "y": 121074, + "z": 5735, + "rotation": 41.26 + }, + { + "id": "BP_Crystal_mk26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A9A05053-4E263558-5963F4B8-6181D1C9", + "x": -18206, + "y": -116745, + "z": 9020, + "rotation": 26.07 + }, + { + "id": "BP_Crystal_mk26_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "471EEB62-45CD82FF-7B37E586-7F3F092C", + "x": 219260, + "y": -267056, + "z": 16643, + "rotation": 57.19 + }, + { + "id": "BP_Crystal_mk26_26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5E185A09-481C910A-95F2AB85-CCF05269", + "x": 100407, + "y": -1180, + "z": 10628, + "rotation": 4.35 + }, + { + "id": "BP_Crystal_mk26_27", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F4D96C1F-4F840D58-E7B4CBAA-F57ACC44", + "x": 97342, + "y": -9033, + "z": 12607, + "rotation": 31.09 + }, + { + "id": "BP_Crystal_mk26_36", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AD0D6AC7-44B0FEF7-A88210B4-72CA7233", + "x": 92689, + "y": 112726, + "z": 10873, + "rotation": 147.63 + }, + { + "id": "BP_Crystal_mk26_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "590B209C-454B2DE1-144FA1A9-B99A64D3", + "x": -191441, + "y": 82122, + "z": 122, + "rotation": -99.64 + }, + { + "id": "BP_Crystal_mk26_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F4AB9376-4992DE6D-7BB238A7-2A1A9A71", + "x": -71179, + "y": 282573, + "z": -3690, + "rotation": -8.74 + }, + { + "id": "BP_Crystal_mk26_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "156664ED-4D4EE4CD-D899E484-21121802", + "x": -48139, + "y": 93947, + "z": 22369, + "rotation": -80.76 + }, + { + "id": "BP_Crystal_mk26_81", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F14C89E8-4D69B01E-DF829B8E-398BF5B6", + "x": -64403, + "y": 51226, + "z": 20749, + "rotation": 1.62 + }, + { + "id": "BP_Crystal_mk26_91", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A3FC4BD6-4858BEB0-810091AA-60E9887B", + "x": 205127, + "y": 74401, + "z": 4686, + "rotation": 48.34 + }, + { + "id": "BP_Crystal_mk27", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "892B36A1-4C37179A-6FE048BF-7664EDDF", + "x": -861, + "y": -192515, + "z": -1116, + "rotation": 58.28 + }, + { + "id": "BP_Crystal_mk27_13", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "56EF1D4B-4905747B-DE566296-1310AB62", + "x": 203141, + "y": 209019, + "z": 13254, + "rotation": -52.78 + }, + { + "id": "BP_Crystal_mk27_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "56925BDD-4F0E84C3-00182DBA-D6B1BA37", + "x": 221440, + "y": -246935, + "z": 11623, + "rotation": -32.96 + }, + { + "id": "BP_Crystal_mk27_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "083883B6-43D0A299-29D77FAC-413BA36A", + "x": 238538, + "y": -279524, + "z": 2787, + "rotation": 163.76 + }, + { + "id": "BP_Crystal_mk27_28", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "682A6243-4D80B906-9493F199-F59FD596", + "x": 11101, + "y": -53429, + "z": 14891, + "rotation": 12.38 + }, + { + "id": "BP_Crystal_mk27_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B890148D-49EC4875-F30455B2-627E9B85", + "x": 31363, + "y": 103124, + "z": 25912, + "rotation": 91.55 + }, + { + "id": "BP_Crystal_mk27_33", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "711CA8C3-44EB9D72-1EF0ADBC-30E0D5F7", + "x": 34405, + "y": 109332, + "z": 18144, + "rotation": -134.76 + }, + { + "id": "BP_Crystal_mk27_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "67401924-40CC5779-1AC632B3-4885DE5C", + "x": -80854, + "y": -97340, + "z": 1521, + "rotation": 0.14 + }, + { + "id": "BP_Crystal_mk27_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D1C8E47D-4ACD997A-AF1B9F83-4BBB6409", + "x": -153175, + "y": 108962, + "z": 7960, + "rotation": 16.98 + }, + { + "id": "BP_Crystal_mk27_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5D4DB638-468DE8A9-D5A238BE-91374A80", + "x": -91286, + "y": 271310, + "z": -6500, + "rotation": -164.72 + }, + { + "id": "BP_Crystal_mk27_82", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "57213B4E-431C624D-2436B9B1-85A5F3DA", + "x": -202123, + "y": -1080, + "z": 20376, + "rotation": 64.72 + }, + { + "id": "BP_Crystal_mk27_92", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "85DD6105-4744D878-FA8080AD-102E3A11", + "x": 222151, + "y": 40182, + "z": 443, + "rotation": -54.25 + }, + { + "id": "BP_Crystal_mk28", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F6E2F976-45976BFB-DF6460B1-8F9CFC67", + "x": -271757, + "y": -192357, + "z": 2767, + "rotation": -141.03 + }, + { + "id": "BP_Crystal_mk28_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D7EFC1E9-44F01FC7-982DA797-FDF9D266", + "x": 118271, + "y": 184715, + "z": -2800, + "rotation": -74.19 + }, + { + "id": "BP_Crystal_mk28_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1E439938-49D5627A-B9BACFB1-C6E97E49", + "x": 25469, + "y": -72204, + "z": 10157, + "rotation": 15.83 + }, + { + "id": "BP_Crystal_mk28_21", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8CFD7958-4184F35B-D9F40B84-E975113F", + "x": 263901, + "y": -270161, + "z": 6723, + "rotation": 37.55 + }, + { + "id": "BP_Crystal_mk28_34", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C8E13B56-4AE851E7-6A294E8F-F9D296ED", + "x": 134471, + "y": 103707, + "z": 13407, + "rotation": -109.36 + }, + { + "id": "BP_Crystal_mk28_35", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D5C057DF-41F44B25-616922AB-55C26D7E", + "x": 208648, + "y": -81614, + "z": 3290, + "rotation": 153.56 + }, + { + "id": "BP_Crystal_mk28_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "23F4CF62-40585F8F-3B88B4AC-EC0AB6CD", + "x": -122140, + "y": 98783, + "z": 7165, + "rotation": -100.84 + }, + { + "id": "BP_Crystal_mk28_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7CB7DF1D-4CB22EF1-9DE7F3B3-20BD9F88", + "x": -121973, + "y": 206941, + "z": -303, + "rotation": 40.63 + }, + { + "id": "BP_Crystal_mk28_83", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E45B6613-40A38D8D-461DE09C-4CFB6E9F", + "x": -184098, + "y": 31015, + "z": 22716, + "rotation": 131.24 + }, + { + "id": "BP_Crystal_mk29", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0F532BDE-4F989F2A-7489208F-069BE19C", + "x": -55705, + "y": 250304, + "z": -1964, + "rotation": 104.57 + }, + { + "id": "BP_Crystal_mk29_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4ECD7506-4963ED98-7593F692-9DFFA0D9", + "x": -17159, + "y": 89012, + "z": 20889, + "rotation": -69.65 + }, + { + "id": "BP_Crystal_mk29_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8AE41912-4073A309-9E7DF197-87F82A82", + "x": 141830, + "y": 190145, + "z": -5793, + "rotation": 2.22 + }, + { + "id": "BP_Crystal_mk29_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AB2B998A-44776E21-CAC9638B-A4E4DBE9", + "x": -188783, + "y": -170113, + "z": -506, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk29_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0C386A13-4A337379-D7388996-8E1CB762", + "x": -169658, + "y": 65659, + "z": 17682, + "rotation": -1.5 + }, + { + "id": "BP_Crystal_mk29_22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F0ECE088-43A44206-8F08C3B2-4E5E6160", + "x": 283210, + "y": -76534, + "z": 7313, + "rotation": 158.58 + }, + { + "id": "BP_Crystal_mk29_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7692BD3A-45D12260-D95D30AA-186341B4", + "x": 145332, + "y": -238260, + "z": 5983, + "rotation": -110.32 + }, + { + "id": "BP_Crystal_mk29_37", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5F583DBB-43F4E452-4416CE95-F17C7804", + "x": 178969, + "y": -91003, + "z": 9177, + "rotation": 168.62 + }, + { + "id": "BP_Crystal_mk29_84", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2DB44908-42E94940-2D3F5682-65BC85B7", + "x": -141907, + "y": 47702, + "z": 19468, + "rotation": 40.47 + }, + { + "id": "BP_Crystal_mk2_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "955F4D10-45D9EB66-C0A35683-AE269970", + "x": -280227, + "y": -29336, + "z": 2944, + "rotation": -148.42 + }, + { + "id": "BP_Crystal_mk2_10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D04607E7-408E7AEE-118A3B92-1F6B60E8", + "x": 354279, + "y": -176863, + "z": 8424, + "rotation": -20.37 + }, + { + "id": "BP_Crystal_mk2_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A86FB717-4DF67E88-7CCFE08A-EC9E04D5", + "x": 405280, + "y": -100423, + "z": 5891, + "rotation": -104.86 + }, + { + "id": "BP_Crystal_mk2_12", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9B7891EE-45B6D585-F1E3D0BF-9930A02B", + "x": 328078, + "y": -39446, + "z": 6001, + "rotation": -56.23 + }, + { + "id": "BP_Crystal_mk2_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B495A11B-48777807-26209E96-925DE6AE", + "x": -114662, + "y": -57562, + "z": 13728, + "rotation": -90.54 + }, + { + "id": "BP_Crystal_mk2_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7B4C00AB-4235BE14-F6427C80-BC8F25E5", + "x": -211792, + "y": -48537, + "z": 6656, + "rotation": 77.87 + }, + { + "id": "BP_Crystal_mk2_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FCD854B8-43AAB5EF-C3B5C88A-B4769D4A", + "x": -232567, + "y": -73522, + "z": 1110, + "rotation": 93.47 + }, + { + "id": "BP_Crystal_mk2_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B3048CF6-489EBA6C-14234DA7-05DBD067", + "x": -140119, + "y": -15384, + "z": 10427, + "rotation": 150.63 + }, + { + "id": "BP_Crystal_mk2_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "72C0695B-47F8E66C-865ABBB6-2FE8D636", + "x": -176508, + "y": -69715, + "z": 1101, + "rotation": 6.2 + }, + { + "id": "BP_Crystal_mk2_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E08D7153-49E6E203-D0359DAC-02C156CF", + "x": -237853, + "y": 20709, + "z": -815, + "rotation": -93.49 + }, + { + "id": "BP_Crystal_mk2_8", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "45AD3D9B-42609A2C-1409CABF-1967AC51", + "x": -154156, + "y": -62050, + "z": 4753, + "rotation": -24.7 + }, + { + "id": "BP_Crystal_mk2_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "370571E7-446DF177-A191B1B1-F691195D", + "x": -155161, + "y": 15771, + "z": 4124, + "rotation": -75.22 + }, + { + "id": "BP_Crystal_mk2_C_0", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2DFB8E95-4D4789D4-F2292E98-899EB7AE", + "x": -75500, + "y": -203822, + "z": 1109, + "rotation": 8.92 + }, + { + "id": "BP_Crystal_mk2_C_1", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B0290437-46B3D9A1-53D3589D-9BEA59DD", + "x": -34746, + "y": -207730, + "z": 3255, + "rotation": -23.02 + }, + { + "id": "BP_Crystal_mk2_C_10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "01FB7DBA-4C4686C1-DDA4F29C-1FA064A8", + "x": 41566, + "y": -68761, + "z": 9450, + "rotation": 93.63 + }, + { + "id": "BP_Crystal_mk2_C_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F0CFCAF8-4849F0B3-03122982-1430CE05", + "x": 13884, + "y": -67553, + "z": 6531, + "rotation": 50.58 + }, + { + "id": "BP_Crystal_mk2_C_12", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EF1653F9-427297FC-A616A590-A47EE73F", + "x": 86781, + "y": -111233, + "z": 9698, + "rotation": -18.14 + }, + { + "id": "BP_Crystal_mk2_C_13", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D9533A8E-45744447-C73BD98A-2A4A59F1", + "x": 62245, + "y": -126948, + "z": 10574, + "rotation": 133.1 + }, + { + "id": "BP_Crystal_mk2_C_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B9F325A6-4797F345-B773E19E-0E56AAA3", + "x": 25366, + "y": -170206, + "z": 2813, + "rotation": 116.8 + }, + { + "id": "BP_Crystal_mk2_C_15", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1DC0B631-46DAA4D3-5E02E790-F6A1158F", + "x": 70980, + "y": -167514, + "z": 5003, + "rotation": 19.06 + }, + { + "id": "BP_Crystal_mk2_C_18", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "58E08AEB-4F9215A9-1BFDF695-445DC072", + "x": 37475, + "y": -237485, + "z": -118, + "rotation": -62.79 + }, + { + "id": "BP_Crystal_mk2_C_19", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3C46E6B2-4B5BF186-762A2B8B-4829871D", + "x": 50327, + "y": -255417, + "z": 1703, + "rotation": 16.55 + }, + { + "id": "BP_Crystal_mk2_C_20", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CACBA33F-4DDC9426-3324859F-BD9B72B4", + "x": 44398, + "y": -215375, + "z": -1751, + "rotation": 16.7 + }, + { + "id": "BP_Crystal_mk2_C_21", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "791455D8-4C9848E2-7063C992-4A442010", + "x": 68939, + "y": -214632, + "z": 1371, + "rotation": 92.65 + }, + { + "id": "BP_Crystal_mk2_C_22", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "37B056B4-48A329F5-5135DF87-68A2FD3E", + "x": 104301, + "y": 152734, + "z": 6634, + "rotation": 76.74 + }, + { + "id": "BP_Crystal_mk2_C_23", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "14094F5C-4BD826AE-0D7095B6-A0139CE4", + "x": 192314, + "y": 25728, + "z": -1451, + "rotation": -97.63 + }, + { + "id": "BP_Crystal_mk2_C_24", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FD4CE560-4F744BA5-A8E7C484-2C2458BC", + "x": 177589, + "y": 66278, + "z": 16009, + "rotation": -179.9 + }, + { + "id": "BP_Crystal_mk2_C_25", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4A0062C0-4558FE33-65EF4B90-1FD296C1", + "x": 174300, + "y": 94266, + "z": 8096, + "rotation": -125.22 + }, + { + "id": "BP_Crystal_mk2_C_26", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9580503F-43F04D8C-0F0CCFB7-AE235055", + "x": 181625, + "y": 67903, + "z": 9571, + "rotation": -14.33 + }, + { + "id": "BP_Crystal_mk2_C_27", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4D425EB9-42CB5E58-485234BD-1186D9C2", + "x": 104883, + "y": -32946, + "z": 14088, + "rotation": 127.15 + }, + { + "id": "BP_Crystal_mk2_C_28", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5DCAE0FA-4F540FD6-14917888-3A363DFE", + "x": 139884, + "y": -70281, + "z": 3154, + "rotation": 0.19 + }, + { + "id": "BP_Crystal_mk2_C_29", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "33E5444E-4CDBC749-44E59F9C-CB346C1E", + "x": 190227, + "y": -57143, + "z": 8016, + "rotation": -177.2 + }, + { + "id": "BP_Crystal_mk2_C_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4D4414A2-4B28087D-6DD48185-A5523B99", + "x": 30579, + "y": 242052, + "z": -3493, + "rotation": 67.5 + }, + { + "id": "BP_Crystal_mk2_C_30", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "29006613-490FD856-0F399F8A-D91CD22F", + "x": 142565, + "y": -135839, + "z": 14001, + "rotation": 1.83 + }, + { + "id": "BP_Crystal_mk2_C_31", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "65B8C268-44AAAE72-AD324FAF-3D034F26", + "x": 198829, + "y": -163207, + "z": 27278, + "rotation": -62.1 + }, + { + "id": "BP_Crystal_mk2_C_33", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "00BF4E2C-494ABD32-452BF082-5FDBC4F4", + "x": 184075, + "y": -163335, + "z": 25400, + "rotation": 5.67 + }, + { + "id": "BP_Crystal_mk2_C_34", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7BAA8E59-4428AE8E-5089D298-5A1EBA17", + "x": 120030, + "y": -142740, + "z": 18490, + "rotation": 102.64 + }, + { + "id": "BP_Crystal_mk2_C_36", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EA75FF1D-45D435D7-8A815896-A59EBDC4", + "x": 280505, + "y": 105488, + "z": -135, + "rotation": 78.1 + }, + { + "id": "BP_Crystal_mk2_C_37", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F328E4B9-4D4345B8-B4CADDBC-A10A26D5", + "x": 209910, + "y": 173870, + "z": 16615, + "rotation": 117.53 + }, + { + "id": "BP_Crystal_mk2_C_39", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "12FB223A-4A5C8FBB-DDAEA0A5-ABF9F4E1", + "x": 214148, + "y": 11961, + "z": 31, + "rotation": -38.75 + }, + { + "id": "BP_Crystal_mk2_C_40", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FB95DDBC-4C8A0FDC-E51898AC-6A47F416", + "x": 213676, + "y": 100009, + "z": -587, + "rotation": 73.05 + }, + { + "id": "BP_Crystal_mk2_C_41", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E64ABCE7-4B69B647-689521A9-97FB2778", + "x": 237416, + "y": 85466, + "z": 2369, + "rotation": 32.47 + }, + { + "id": "BP_Crystal_mk2_C_42", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0BA652BB-4827949F-FC5F69AC-4A5A7FA8", + "x": 209116, + "y": 59416, + "z": 1499, + "rotation": -126.92 + }, + { + "id": "BP_Crystal_mk2_C_43", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E72ABEDF-45C7940F-B8BFCB8B-15C6BDBF", + "x": 207989, + "y": 27720, + "z": 308, + "rotation": 173.04 + }, + { + "id": "BP_Crystal_mk2_C_45", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9D508C69-456D589B-A8D3EABB-24BC38F1", + "x": 214895, + "y": -184615, + "z": 9165, + "rotation": -53.44 + }, + { + "id": "BP_Crystal_mk2_C_46", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "096DDE1E-43CDB1B6-BBE2F2A4-FAA13AA6", + "x": 283788, + "y": -278678, + "z": 1692, + "rotation": -15.31 + }, + { + "id": "BP_Crystal_mk2_C_47", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "06FB3CA3-48723944-A2932BB4-64DA53EA", + "x": 344491, + "y": -143897, + "z": 4535, + "rotation": -90.42 + }, + { + "id": "BP_Crystal_mk2_C_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "198329D7-408A5D9C-E31CF69D-0CD1FB5B", + "x": 20302, + "y": 192237, + "z": 18412, + "rotation": -5.47 + }, + { + "id": "BP_Crystal_mk2_C_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "96D2E0B3-45B0BCF6-4C5FD48C-0393B6F5", + "x": 68067, + "y": 171001, + "z": 12604, + "rotation": 98.34 + }, + { + "id": "BP_Crystal_mk2_C_7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "140FA9E4-4D0BE078-E163CE9C-8EF8EA4F", + "x": 52709, + "y": 170328, + "z": 9669, + "rotation": 157.62 + }, + { + "id": "BP_Crystal_mk2_C_8", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DB8C7792-4FAFB8D0-9B42CB81-60678217", + "x": 63529, + "y": 135699, + "z": 6860, + "rotation": 8.16 + }, + { + "id": "BP_Crystal_mk2_C_9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "573EBCFF-4367B38A-2E294881-70E23817", + "x": 71037, + "y": -52130, + "z": 9506, + "rotation": -147.33 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0156301_1720971459", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "26EE48BC-4B790AAE-8021CA9D-1DD33888", + "x": 178365, + "y": 117380, + "z": 12980, + "rotation": 82.57 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0245C01_1697523667", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "ED76039B-4DEA5FE2-05772ABB-0B7B5492", + "x": 203030, + "y": -257567, + "z": 9368, + "rotation": -0.41 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F02A8701_1121902119", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1A7992FF-4BC73E1E-C3818195-73C1EC87", + "x": -85404, + "y": -143607, + "z": -888, + "rotation": -105.45 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0327B01_1576760848", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "424B6F10-48C794BA-ABB44094-21F97958", + "x": 116233, + "y": 19336, + "z": 16708 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0327B01_2054731851", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5F280D0B-45080DE5-C98B6E83-000706B6", + "x": 129829, + "y": 27414, + "z": 15565, + "rotation": 34.33 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0357B01_1376513381", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9BB8B487-42BBF9A4-912AF9B7-FB493737", + "x": 221511, + "y": -52436, + "z": 16165, + "rotation": -71.17 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0397B01_1952176084", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "53BC03E4-4C1EE839-C4E3038F-B5DF5915", + "x": 143192, + "y": 7531, + "z": 17435, + "rotation": 58.69 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F03A5B01_1502427478", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "16678B36-43075603-7991B6A3-69F53D65", + "x": 185962, + "y": -136393, + "z": 21262, + "rotation": 170.4 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F03E6501_1921080728", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D2379D8A-473ADC69-72A1CAA4-415C9FFF", + "x": -111593, + "y": -36885, + "z": 24204, + "rotation": -76.93 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F03F7B01_1398535141", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AD63F7AB-43EDF1DA-7726C8A6-F59EC7E2", + "x": 134757, + "y": 14609, + "z": 17075, + "rotation": -157.88 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F049B201_2036002976", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "850E45D1-4C88FA43-02F7918E-A6BFF5F2", + "x": 63119, + "y": 210396, + "z": 1668, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0575301_1826590140", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B675C234-472FF506-421D779A-38B1AD36", + "x": 167921, + "y": -264110, + "z": 3951, + "rotation": -40.19 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0595301_1302912499", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EF4E81D4-4AFE16EE-E6BE5D8B-02B8F8E8", + "x": 152014, + "y": -283555, + "z": 2976, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0595D01_1702343033", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1B586BB0-4C91A2C0-4D543AAB-428117AC", + "x": 228255, + "y": 109553, + "z": 3414, + "rotation": -82.99 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F05B5D01_1459628393", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DE9E3878-496305BF-256D21AC-2E799EE6", + "x": 228451, + "y": 115713, + "z": 7122, + "rotation": 172.28 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F062A401_1920904598", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7F72826A-45CAF334-3955CFAD-2397564C", + "x": -147146, + "y": 90692, + "z": 19152, + "rotation": 70.81 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0635D01_1402584822", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "906AA79A-4E3A3399-177954A1-9DF010B1", + "x": 222466, + "y": 112035, + "z": 10484, + "rotation": 167 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F063A401_1791316779", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "44B79797-421D5900-2728C9B0-83856832", + "x": -163552, + "y": 134766, + "z": 12793, + "rotation": -64.32 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F06D5201_1177233968", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A4504ECE-4B0822F2-5FD3F19F-27670BD2", + "x": 153693, + "y": -151003, + "z": 25630, + "rotation": 25.33 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F06FA401_2035551894", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EC543AF6-4523D72B-B4E7CBBB-5095BE7B", + "x": -144324, + "y": 156856, + "z": 14338, + "rotation": -6.25 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0706801_1432001712", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7C04E1B9-4D113410-001757AF-6C7DF8F2", + "x": 172977, + "y": 113564, + "z": 10456, + "rotation": -154.76 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F071A401_1739232248", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "75CAB273-41899CE2-2C7875A0-7EAA1FCE", + "x": -178742, + "y": 93006, + "z": 20888, + "rotation": 115.78 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F071A401_1842127249", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "476BE054-4B5F7140-D4A4AC9B-73CDB1C6", + "x": -163321, + "y": 106057, + "z": 20025, + "rotation": -9.03 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0775B01_1268218200", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B4AF5105-4CA6A8D1-35F3C79B-4D5D1A60", + "x": 197167, + "y": -273736, + "z": 12056, + "rotation": 124.57 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0785B01_1533531383", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "46C4857E-4ADE01A2-BF14A989-262D25C7", + "x": 141595, + "y": -168654, + "z": 2592, + "rotation": 5.32 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0795B01_1357525561", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3CBF2AEA-4A9E27AB-9E2E3A8E-1686E230", + "x": 124696, + "y": -147164, + "z": 7521, + "rotation": 9.96 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0795B01_2008124563", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "233041C7-443F0683-C2CC45A4-B4F50CE9", + "x": 107782, + "y": -161384, + "z": 4122, + "rotation": 6.51 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F07F5B01_2043697625", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2B2D0F6D-40DA495D-6367E580-F95E4906", + "x": 116687, + "y": -172844, + "z": 5954, + "rotation": -41.91 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0805B01_1612786803", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B254332C-473970DB-50054992-9152971D", + "x": 136978, + "y": -185959, + "z": 2388 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0815B01_1488038982", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DFB29C69-4AB0F4AD-8B9387B4-3A95B94B", + "x": 125681, + "y": -156174, + "z": 5259, + "rotation": 49.68 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0815B01_1988173986", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "57059615-4C03CBE4-3ACE3F98-DF20AEEA", + "x": 133553, + "y": -142260, + "z": 8475, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0825B01_1335318165", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "9801CD13-4DE4834E-FAE3F4BB-B39DDD0D", + "x": 114992, + "y": -164153, + "z": 7442, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0855B01_1254483667", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D46AC76B-4BA74530-C200A884-7877FF84", + "x": 161292, + "y": -213405, + "z": 2017, + "rotation": 13.24 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0855B01_1620780674", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DC0D6315-491A5156-8A84A0B7-F2778510", + "x": 162272, + "y": -234132, + "z": 810, + "rotation": 39.04 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0865B01_1135287853", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1B6CAE6F-4181A727-08295EBD-5CE252E3", + "x": 173571, + "y": -233679, + "z": 11494, + "rotation": 73.03 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0AA5201_1255154679", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6C912ABE-4F9B4C1C-5B6535AF-EDE5397A", + "x": 193282, + "y": -189305, + "z": 24604, + "rotation": 53.44 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0AA5201_1338657684", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6093A9D1-4B8424AB-6D008A80-0E531499", + "x": 192538, + "y": -187176, + "z": 26134, + "rotation": 155.47 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0AE5D01_1615859992", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "73E7C02E-4761023D-464AA698-F34D4648", + "x": 196566, + "y": 153128, + "z": 8669, + "rotation": -5.62 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0B65201_2137568813", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7F394E24-43AB4BBC-69E86197-1C8D53F9", + "x": 170142, + "y": -130203, + "z": 19620, + "rotation": 35.89 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0BD5201_1079947071", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3482AC63-4194F051-317585B0-2200AEB8", + "x": 136325, + "y": -181035, + "z": 8958, + "rotation": 52.22 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0D25B01_1831343223", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C299005C-43D90FA8-C20DCF82-E9223804", + "x": 139246, + "y": -146226, + "z": 15697, + "rotation": -120.03 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0D45B01_1937028581", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8B3F70FF-4E35A951-47183F8E-7C3FAF60", + "x": 111032, + "y": -166069, + "z": 9345, + "rotation": -58.49 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E57A01_1469842296", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "C895360A-414D8C32-65179BA2-F051FA58", + "x": 155132, + "y": 27074, + "z": 19268, + "rotation": -58.01 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E57A01_1590485297", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "89572F83-437AD3FA-FC2BEA9F-115BDDA1", + "x": 180696, + "y": 14461, + "z": 18596, + "rotation": 135.48 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E67A01_1084770479", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8B1E46BE-4EA95FC6-BD933B8C-01C68CC1", + "x": 156882, + "y": 17776, + "z": 18279, + "rotation": 5.68 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E85F01_1340931312", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "165FDDE7-4061CF4E-92F05F87-51E2E9E3", + "x": 194610, + "y": -194975, + "z": 26250, + "rotation": -86.11 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E85F01_1744709315", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A255F6F7-4FE68393-7D81BA9F-F0F3356F", + "x": 214632, + "y": -219424, + "z": 29564, + "rotation": -3.25 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E87A01_1147893840", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "A0337547-46D9595C-666B958F-16F37137", + "x": 63094, + "y": 11931, + "z": 19057, + "rotation": -103.34 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E87A01_1547094844", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6BF59B4C-43BE0DEE-2D8ECF97-D1D55047", + "x": 76289, + "y": 41302, + "z": 19581, + "rotation": 6.17 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E95F01_1115335496", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5D31E22D-426B3B09-452318A1-C5C814F3", + "x": 243082, + "y": -272859, + "z": 26753, + "rotation": -83.68 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0EE7A01_1935774906", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E4F60B8F-467F8CDE-906515B9-21165CD2", + "x": 110069, + "y": 12351, + "z": 21050, + "rotation": -99.3 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F35F01_1114488275", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "143342D6-4C15E8F6-A47A04A4-007378A8", + "x": 222910, + "y": -235200, + "z": 26282, + "rotation": -165.96 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F37A01_1199794805", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CD819C0D-4F8703F3-7FAC6880-69DE53FF", + "x": 137014, + "y": 28209, + "z": 11957, + "rotation": -19.52 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F37A01_1513905808", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4EA10ED4-410E030A-7E8524A6-F7E023B2", + "x": 142769, + "y": 45435, + "z": 17570, + "rotation": -147.99 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F67A01_1684184353", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5A88A48E-44F1B13D-300BF7BC-5DDCFC4C", + "x": 166424, + "y": 38892, + "z": 11812, + "rotation": 33.19 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F67A01_1837729354", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E2A9BCAB-411F490E-6BA8519F-A9CBD95E", + "x": 209097, + "y": -2989, + "z": 7236, + "rotation": -57.11 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F67A01_1953261355", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "41B5FBD7-435BDA67-ACC69FA4-CAB93705", + "x": 195317, + "y": 15085, + "z": 10326, + "rotation": -82.76 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F77A01_2016858540", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "68CE9B2A-4E47CC80-D5EBF8B6-475444C6", + "x": 206543, + "y": -43030, + "z": 15264, + "rotation": 19.07 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0F87A01_1248874721", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F3E41B2A-4E00477F-016816AA-3DCFCA59", + "x": 187002, + "y": -26896, + "z": 13648, + "rotation": -5.62 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0FC5201_1997354134", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8182F0B7-43D13351-1C755DAF-D87710BC", + "x": 130920, + "y": -199555, + "z": 13002, + "rotation": 70.11 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79245C01_1676679653", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "DB179778-44FF3939-CE42D9BE-F116E4BC", + "x": 87154, + "y": -182011, + "z": 1946, + "rotation": 81.87 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79738301_1516430745", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "628FB599-44734D19-48180B8A-AEE73451", + "x": -27600, + "y": 139690, + "z": -6960, + "rotation": 58.05 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79745B01_1428147672", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FCA63E2C-4BAC0CDF-8F3A83B0-7F101593", + "x": 52932, + "y": -177475, + "z": 5512, + "rotation": 116.04 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F799E5F01_1558324292", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "23E683FB-48BCD99E-61A872A6-6EB1EDEA", + "x": 85950, + "y": -214841, + "z": 4173, + "rotation": 102.29 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79AB5201_1083372854", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CDF79ECD-4F6C4D3F-4A0563BD-584B0257", + "x": 82917, + "y": -232288, + "z": 438, + "rotation": -24.05 + }, + { + "id": "BP_Crystal_mk2_C_UAID_40B076DF2F79BC5D01_1825052456", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "87A51F3F-43BD5F9D-A09DA49E-2D2860A3", + "x": -29419, + "y": -174117, + "z": 5977, + "rotation": -164.43 + }, + { + "id": "BP_Crystal_mk2_C_UAID_4CEDFB3E2F7F8B9201_1245751815", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "91B26761-4DDF17A7-567417AF-E19387B7", + "x": 167040, + "y": 83620, + "z": 1920, + "rotation": -91.38 + }, + { + "id": "BP_Crystal_mk30_12", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2F6BAA2C-44C18DC2-E3A6DEAD-1C243BA8", + "x": 139980, + "y": -213485, + "z": 5350, + "rotation": 4.82 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0747801_1178299322", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B4B971DD-45AE94ED-A73E36B3-A215C224", + "x": 164676, + "y": -3915, + "z": 21256, + "rotation": 155.29 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0EE7A01_1112899902", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2B9E6E6B-47DEB989-79AC0581-68025B86", + "x": 82757, + "y": 22623, + "z": 21809, + "rotation": 5.52 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0F77A01_1321147532", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5FB19A14-46725A11-0098ADAC-720543D0", + "x": 180444, + "y": 26740, + "z": 2049, + "rotation": 92.69 + }, + { + "id": "BP_Crystal_mk43_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "00BBE718-416D7E3A-153433A5-181B6C03", + "x": 190853, + "y": -222990, + "z": 23933, + "rotation": -165.37 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F790F6201_1607312289", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E322D4B3-45DDF598-5AF1DDAA-28937554", + "x": 122984, + "y": -215475, + "z": -1216, + "rotation": 38.99 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79926001_1740797235", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E000A2EE-43C7D37B-EC8D5EB6-B216AB5D", + "x": 90580, + "y": -164221, + "z": 246, + "rotation": 37.32 + }, + { + "id": "BP_Crystal_mk45_UAID_40B076DF2F79BF6101_1113212209", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6553513C-429BBCE2-DA7B5F96-AC4238D4", + "x": 100698, + "y": -198219, + "z": -1071, + "rotation": 179.32 + }, + { + "id": "BP_Crystal_mk47", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "D8DD244C-46100E8B-5C890583-4B2A6F0F", + "x": 31696, + "y": -214888, + "z": -1764, + "rotation": 16.84 + }, + { + "id": "BP_Crystal_mk48_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AC714195-4932D516-BD779FA8-05B6FBDB", + "x": -55461, + "y": -168285, + "z": 6042, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk49", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "01D24A8A-4F4821EA-B42B7891-662326E7", + "x": -66490, + "y": -175652, + "z": 10320, + "rotation": 0.15 + }, + { + "id": "BP_Crystal_mk49_UAID_40B076DF2F79955F01_1622838761", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7E7CA60C-428EF38A-8363748C-B1D0337B", + "x": -59970, + "y": -190331, + "z": 8660, + "rotation": -61.12 + }, + { + "id": "BP_Crystal_mk50", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1F4F13EF-468DFF2D-ACFBE5B9-BB745ECA", + "x": -45530, + "y": -177778, + "z": 9388, + "rotation": -88.89 + }, + { + "id": "BP_Crystal_mk51", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1741314E-4DDE6E73-961E389C-FCBBB34F", + "x": 132230, + "y": -200725, + "z": 6230, + "rotation": -14.06 + }, + { + "id": "BP_Crystal_mk51_28", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E6439AAD-47B80F23-9D3316AB-211A7468", + "x": 124495, + "y": -202820, + "z": 5405, + "rotation": 6.72 + }, + { + "id": "BP_Crystal_mk52", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "70476BAA-46F48127-80688EA1-4572836C", + "x": -54880, + "y": -214424, + "z": 5914, + "rotation": 3.79 + }, + { + "id": "BP_Crystal_mk53", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "EA1D50E0-4FAA50B5-868AF4B6-E5FC8944", + "x": -49351, + "y": -228613, + "z": 4474, + "rotation": 70.16 + }, + { + "id": "BP_Crystal_mk53_UAID_40B076DF2F79105301_1728144638", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FC23FC3E-454E28CC-6BF276A0-A362D956", + "x": -51261, + "y": -250327, + "z": 3341, + "rotation": 180 + }, + { + "id": "BP_Crystal_mk53_UAID_40B076DF2F79615301_1537373889", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5F051306-4764CF7B-A9F23BAC-6A403E5D", + "x": -27596, + "y": -249982, + "z": 624, + "rotation": 122.84 + }, + { + "id": "BP_Crystal_mk53_UAID_40B076DF2F79625301_2075930067", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F1950BA4-41CAE8CC-F867D28E-41D8F286", + "x": 64634, + "y": -186579, + "z": 616, + "rotation": -112.04 + }, + { + "id": "BP_Crystal_mk53_UAID_40B076DF2F79F55801_1917538208", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "B46E36E5-461895FA-5347CCA6-14DB85F9", + "x": -51747, + "y": -208021, + "z": 2432, + "rotation": -156.01 + }, + { + "id": "BP_Crystal_mk54", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "84593D7C-42B5AC17-13EEEDA2-8C984E5A", + "x": -28658, + "y": -173785, + "z": 9723, + "rotation": 23.03 + }, + { + "id": "BP_Crystal_mk55", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "14A3815A-407009FE-434DA5A8-1CF22C1A", + "x": -9032, + "y": -165462, + "z": 6012, + "rotation": -144.86 + }, + { + "id": "BP_Crystal_mk56", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "73CB9ED7-4AFD5D60-52D44880-2966E24C", + "x": 9093, + "y": -169112, + "z": 4681, + "rotation": 0.38 + }, + { + "id": "BP_Crystal_mk57", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "94643D9C-48A9CD50-BAD39AA7-30821C35", + "x": 7926, + "y": -178552, + "z": 4952, + "rotation": -0.35 + }, + { + "id": "BP_Crystal_mk58", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "247CCF5C-4F4E0381-3E3422AD-34653D43", + "x": 21661, + "y": -176291, + "z": 11012, + "rotation": -98.79 + }, + { + "id": "BP_Crystal_mk59", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "511FB631-47989459-6906F5AB-C068EAA5", + "x": 17664, + "y": -154946, + "z": 12264, + "rotation": 113.25 + }, + { + "id": "BP_Crystal_mk6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5D77A8C5-4605ADDE-9A5D088E-70C8A986", + "x": -55053, + "y": -185332, + "z": 6443, + "rotation": -78.69 + }, + { + "id": "BP_Crystal_mk60", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "F947B0F3-4F3A675A-16F802BE-7371D539", + "x": 41030, + "y": -164983, + "z": 20134, + "rotation": -97.26 + }, + { + "id": "BP_Crystal_mk60_UAID_40B076DF2F794D6401_1849768319", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2BE465CA-4E2B824C-34F30181-44FA2C8B", + "x": 19170, + "y": -170847, + "z": 4531, + "rotation": -43.88 + }, + { + "id": "BP_Crystal_mk60_UAID_40B076DF2F79F35F01_1959731276", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "FBF4AB60-45E54B4B-3942EB8A-C0469C43", + "x": 19377, + "y": -163773, + "z": 14261, + "rotation": 33.59 + }, + { + "id": "BP_Crystal_mk61", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "95077C1A-4CB9A478-11781CB3-5B2505BD", + "x": 123850, + "y": -176825, + "z": 8070, + "rotation": 40.85 + }, + { + "id": "BP_Crystal_mk63", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "287DFC6E-4C9C11E7-3AEC048C-3889088C", + "x": 127605, + "y": -168050, + "z": 7660, + "rotation": 180 + }, + { + "id": "BP_Crystal_mk63_10", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "26C88C32-4D74E19C-8C3146B5-23586E10", + "x": 203544, + "y": -286437, + "z": 22147, + "rotation": 46.22 + }, + { + "id": "BP_Crystal_mk66", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "57C1F450-4BC32954-013864AB-52065B4E", + "x": 41348, + "y": -173688, + "z": 9259, + "rotation": 55.34 + }, + { + "id": "BP_Crystal_mk67", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "29F50129-4BC9D084-8542268C-7A2B82D3", + "x": 137690, + "y": -167725, + "z": 7075, + "rotation": 40.9 + }, + { + "id": "BP_Crystal_mk6_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5A8EB7CB-4B6BE0E8-E1DCCEBA-114FD5A4", + "x": 190339, + "y": -263888, + "z": 4334, + "rotation": -30.72 + }, + { + "id": "BP_Crystal_mk7", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "13927DCE-48A7D51C-E481769E-325B2359", + "x": -17612, + "y": -203213, + "z": 1834, + "rotation": 81.96 + }, + { + "id": "BP_Crystal_mk70_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "98F0F3DB-4795CFEB-8FE3E2BC-493D63F1", + "x": 215380, + "y": -213390, + "z": 14660, + "rotation": 81.13 + }, + { + "id": "BP_Crystal_mk72_11", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0EA4D101-464C3E6F-4A9A65B5-AD729B42", + "x": 238314, + "y": 133385, + "z": 12325, + "rotation": 92.79 + }, + { + "id": "BP_Crystal_mk72_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "7F49BDEB-4CB6EF0D-5BA1A0A3-3375803D", + "x": 143930, + "y": -165650, + "z": 8215, + "rotation": 87.6 + }, + { + "id": "BP_Crystal_mk73_3", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "8AE9C003-49290213-25E653B9-75F06B2A", + "x": 148000, + "y": -185700, + "z": 6440, + "rotation": 90 + }, + { + "id": "BP_Crystal_mk74_4", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "57A7C5FE-40375359-D8E17AA3-B27BD6F7", + "x": 156251, + "y": -178558, + "z": 23, + "rotation": 89.6 + }, + { + "id": "BP_Crystal_mk75_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E82A229E-4F5BB808-AA88F2B4-6A1C5345", + "x": 228660, + "y": 133683, + "z": -5741, + "rotation": -8.61 + }, + { + "id": "BP_Crystal_mk75_5", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "CBABB886-449E5718-BF262EB7-A59B2FD8", + "x": 158490, + "y": -165477, + "z": 15800, + "rotation": -90.29 + }, + { + "id": "BP_Crystal_mk76_2", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "2DB88249-411ABE73-E78D9A8D-05A7DB75", + "x": 208982, + "y": 115908, + "z": 11668, + "rotation": -39.3 + }, + { + "id": "BP_Crystal_mk76_6", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "08307927-4262821E-19B85594-5AADAA95", + "x": 114835, + "y": -196315, + "z": 3160, + "rotation": 64.69 + }, + { + "id": "BP_Crystal_mk79", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "0A677D14-43596F93-ED9B1DA3-EE8170D7", + "x": 66356, + "y": -160959, + "z": 7164, + "rotation": 11.78 + }, + { + "id": "BP_Crystal_mk80", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "17FA8A4C-41D1208D-2108F6A3-52A75CA3", + "x": 85815, + "y": -156663, + "z": 19816, + "rotation": 64.54 + }, + { + "id": "BP_Crystal_mk81", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "25948A1F-4952C4E1-99B1CE8E-FC79323A", + "x": 90873, + "y": -148056, + "z": 10085, + "rotation": 148.44 + }, + { + "id": "BP_Crystal_mk82", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "48524E83-425228D5-678AA6A1-5C2471E9", + "x": 98481, + "y": -158049, + "z": 7812, + "rotation": -33.48 + }, + { + "id": "BP_Crystal_mk83", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "5D6F4F17-4E393B54-D0AFCBB0-B5C8963B", + "x": 88413, + "y": -165020, + "z": 7299, + "rotation": 9.61 + }, + { + "id": "BP_Crystal_mk84", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "1FE4DE53-40D599C2-3069FFBC-89B9BECD", + "x": 87303, + "y": -172522, + "z": 8874, + "rotation": -154.17 + }, + { + "id": "BP_Crystal_mk88", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "E806D1EC-44AC98D7-1CE6C498-E3574F51", + "x": 174657, + "y": 129903, + "z": 16334, + "rotation": 131.16 + }, + { + "id": "BP_Crystal_mk9", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "6A3DD0D0-47A96729-F92574B8-D88B6C46", + "x": 14673, + "y": -242518, + "z": -944, + "rotation": -21.13 + }, + { + "id": "BP_Crystal_mk93", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "3E0D12AC-484910A7-A05F1BB7-3598DD3F", + "x": 175015, + "y": 106610, + "z": 11552, + "rotation": 112.06 + }, + { + "id": "BP_Crystal_mk94", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "4EE3BAFB-4D7926BF-239CBF84-CA519B50", + "x": 184321, + "y": 131052, + "z": 15902, + "rotation": 5.3 + }, + { + "id": "BP_Crystal_mk9_14", + "type": "slugMk2", + "classPath": "BP_Crystal_mk2_C", + "pickupGuid": "AEA37FD9-40248A5F-AE4B6991-D9E7F6FC", + "x": 181935, + "y": -253645, + "z": 14235, + "rotation": -146.99 + }, + { + "id": "BP_Crystal7_17", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "10CBDE8A-42EC2D10-41196081-CD08F0A2", + "x": 125763, + "y": 81108, + "z": 12794, + "rotation": 155.16 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05D5D01_1813536763", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B6E5BC8A-4BFECC73-6788F39F-2835935E", + "x": 232352, + "y": 109088, + "z": 4992, + "rotation": 69.28 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F05F7801_1673526600", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8E5CF64F-4B1AF023-901F7AB3-55DE8878", + "x": 233479, + "y": -7977, + "z": 7615, + "rotation": 17.36 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0757801_1889185499", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "87A006DA-407C88A9-DA1101AA-C0B93CDF", + "x": 149148, + "y": 7319, + "z": 20663, + "rotation": 66.12 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0757801_2136946500", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "848F4308-41A776C6-CBE4CA88-044767AF", + "x": 119255, + "y": -2582, + "z": 17628, + "rotation": 136.15 + }, + { + "id": "BP_Crystal_C_UAID_04421A9713F0B37301_2124852114", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "98F7EAF8-419BEEDE-A92DFC80-AC1D35C9", + "x": 86205, + "y": 46730, + "z": 23200, + "rotation": 42.04 + }, + { + "id": "BP_Crystal_C_UAID_40B076DF2F79F25F01_1677709082", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C82B6ECF-4D3BF659-C9AF809D-7A8C39D4", + "x": 53396, + "y": -163213, + "z": 20922, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk10_UAID_40B076DF2F79486001_1273441223", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C601DD4B-4C66570A-0E03DF80-D1F88195", + "x": 9055, + "y": -221878, + "z": 4925, + "rotation": 25.39 + }, + { + "id": "BP_Crystal_mk11", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "232FD9B0-479B0B99-8D857195-09B1E3A0", + "x": 82049, + "y": -158687, + "z": 5139, + "rotation": -151.62 + }, + { + "id": "BP_Crystal_mk12", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5DD31EB6-4035EB26-A150088D-DFCB9845", + "x": 22010, + "y": -238736, + "z": 1506, + "rotation": -72.49 + }, + { + "id": "BP_Crystal_mk13", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E1115C49-4713AA47-61C4A084-D5C554CE", + "x": 36093, + "y": -252512, + "z": 3403, + "rotation": -2.36 + }, + { + "id": "BP_Crystal_mk13_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E83A3976-4258091B-8279A280-7F8E28A9", + "x": 187825, + "y": -257335, + "z": -1780, + "rotation": 90 + }, + { + "id": "BP_Crystal_mk14_5", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1F3150E6-42B212E0-8864A2B5-5CAF3463", + "x": 183780, + "y": -296705, + "z": 21390, + "rotation": -28.59 + }, + { + "id": "BP_Crystal_mk15", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "59C0B796-413EE73B-A8096C95-C4D6391F", + "x": 54723, + "y": -229467, + "z": 4340, + "rotation": 0.18 + }, + { + "id": "BP_Crystal_mk16_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D5F926F8-43EDBA4D-6797D2B9-B4C05CA7", + "x": 177155, + "y": -206000, + "z": 24184, + "rotation": -81.3 + }, + { + "id": "BP_Crystal_mk18", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "52FFBEB6-442C7A49-6F9780B7-4CA9ECD5", + "x": 85514, + "y": -195345, + "z": 5124, + "rotation": -15.92 + }, + { + "id": "BP_Crystal_mk21_6", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2BB99499-42F0620F-8EEA20A7-095DBC1B", + "x": -55085, + "y": -43846, + "z": 16938 + }, + { + "id": "BP_Crystal_mk24_14", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2A8C7086-47C381F1-587E1CB2-FC5825CD", + "x": 121164, + "y": -214017, + "z": 3010, + "rotation": 132.5 + }, + { + "id": "BP_Crystal_mk2_C_16", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "644D9850-48E9A6A5-0EF60BBD-3C7037AF", + "x": 95747, + "y": -198310, + "z": 5836, + "rotation": -108.71 + }, + { + "id": "BP_Crystal_mk2_C_38", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D0C8EEE8-4D8BDFAB-C77EB89C-0C9BFD80", + "x": 203508, + "y": 145635, + "z": 6584, + "rotation": -95.64 + }, + { + "id": "BP_Crystal_mk2_C_UAID_04421A9713F0E77A01_1394636661", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "421E41E0-40F11478-6A3053B6-76BE485E", + "x": 131035, + "y": 43109, + "z": 18044, + "rotation": 16.82 + }, + { + "id": "BP_Crystal_mk30", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8A11606E-4FE9002F-38658285-81B7C4FB", + "x": 236675, + "y": -208777, + "z": 17216, + "rotation": -18.16 + }, + { + "id": "BP_Crystal_mk30_10", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B3D4302B-4106D186-295412A6-82E13ED6", + "x": 394732, + "y": -151166, + "z": 9860, + "rotation": 35.22 + }, + { + "id": "BP_Crystal_mk30_7", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DCD66EB8-4F8B9043-6A937D87-9BA9861E", + "x": 385175, + "y": -207938, + "z": 9585, + "rotation": -38.11 + }, + { + "id": "BP_Crystal_mk31", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "191169FA-483F3D4C-9E4A7482-FD09DEEA", + "x": -175604, + "y": 44187, + "z": 18990, + "rotation": -1.13 + }, + { + "id": "BP_Crystal_mk310", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F95F06C5-47739800-F89DFAA3-F12D37ED", + "x": -182085, + "y": 28721, + "z": 10801, + "rotation": 23.94 + }, + { + "id": "BP_Crystal_mk310_42", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8DD5DBC3-47632B3A-A31A1CA8-A96BF2D7", + "x": -21824, + "y": 53806, + "z": 21953, + "rotation": 79.92 + }, + { + "id": "BP_Crystal_mk310_92", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "238C629B-4FE84542-5A3919BF-4739AB1E", + "x": -56255, + "y": 58994, + "z": 20969, + "rotation": -177.68 + }, + { + "id": "BP_Crystal_mk311", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "670465EA-4CE1F723-D44D3883-1894D58C", + "x": 27555, + "y": 73708, + "z": -754, + "rotation": -8.81 + }, + { + "id": "BP_Crystal_mk311_48", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "98243606-4939CA9E-84838A83-DE597F39", + "x": -37436, + "y": -6213, + "z": 30029, + "rotation": 100.89 + }, + { + "id": "BP_Crystal_mk311_93", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C9021C3D-49E7A2B1-6F762397-CD45B9D6", + "x": -91579, + "y": 51276, + "z": 21546, + "rotation": 65.13 + }, + { + "id": "BP_Crystal_mk312", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "FD57A6D1-417EA4CD-2A124EAF-900FBD79", + "x": -125152, + "y": 49267, + "z": 34042, + "rotation": -88.04 + }, + { + "id": "BP_Crystal_mk312_52", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "BF911366-493A4FCF-49AB0E8F-22432D65", + "x": -28150, + "y": 19758, + "z": 22616, + "rotation": 22.91 + }, + { + "id": "BP_Crystal_mk313", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5C265DE7-409F4756-614D9480-0BE0D1E5", + "x": -151141, + "y": 22066, + "z": 26175, + "rotation": 22.82 + }, + { + "id": "BP_Crystal_mk313_59", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2B4A5A14-47EBC06A-7ABC5F91-9A6CA130", + "x": -1594, + "y": 29752, + "z": 24393, + "rotation": 66.04 + }, + { + "id": "BP_Crystal_mk315", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "05FF37A1-440D34F7-6CBF76AF-1D04E650", + "x": -106598, + "y": 34219, + "z": 23600, + "rotation": -86.76 + }, + { + "id": "BP_Crystal_mk316", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F2451746-46ED458F-E68F9994-D0C631AA", + "x": 46136, + "y": -190558, + "z": 7632, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk317", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "278049C4-47E3B527-7F8D1587-82D9DD4E", + "x": -80753, + "y": 34086, + "z": 19814, + "rotation": 33.29 + }, + { + "id": "BP_Crystal_mk318", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "62658796-4896AEAD-D427D2AC-31929CC3", + "x": -127059, + "y": 43459, + "z": 19965, + "rotation": 31.2 + }, + { + "id": "BP_Crystal_mk319", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "A2E6AE43-4C3E4A2B-D8CD22B5-06C4051E", + "x": -129127, + "y": 20714, + "z": 28690, + "rotation": -3.8 + }, + { + "id": "BP_Crystal_mk31_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B6A0C8D9-4642704E-F5B26185-1AED7351", + "x": 33124, + "y": 176853, + "z": -6538, + "rotation": 108.7 + }, + { + "id": "BP_Crystal_mk31_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "574164F2-41724C8E-A4C3EAAF-9D9C8928", + "x": -95974, + "y": 44011, + "z": 5092, + "rotation": -51.88 + }, + { + "id": "BP_Crystal_mk31_10", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CA6ACDF7-40CC1733-1A009281-6432A70E", + "x": -51694, + "y": -78723, + "z": 5734, + "rotation": 107.69 + }, + { + "id": "BP_Crystal_mk31_12", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "4B605173-42431B86-62640EAC-E39B0180", + "x": 58424, + "y": -27553, + "z": 24846, + "rotation": -77.82 + }, + { + "id": "BP_Crystal_mk31_13", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B7890DC9-4AE63DA1-2149BD81-463CB813", + "x": 285727, + "y": 128184, + "z": 11354, + "rotation": 19.94 + }, + { + "id": "BP_Crystal_mk31_15", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CDBEABFD-4D1D940E-A9DB5393-5EC229F8", + "x": 37876, + "y": 36310, + "z": 23414, + "rotation": -148.68 + }, + { + "id": "BP_Crystal_mk31_16", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EEF26FE5-463E5990-6FF0B296-0D3B59BE", + "x": 65682, + "y": 64887, + "z": 35873, + "rotation": 45.61 + }, + { + "id": "BP_Crystal_mk31_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5F927D61-4BFA8658-3D9ED1BB-6B4BE995", + "x": 226649, + "y": -295590, + "z": 1647, + "rotation": 5.25 + }, + { + "id": "BP_Crystal_mk31_3", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B256322F-4946FE03-2AD3768D-8433EF70", + "x": -268369, + "y": 87126, + "z": -1743, + "rotation": -50.02 + }, + { + "id": "BP_Crystal_mk31_4", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "92B6CE3A-48F39E12-00FFD080-829A01DF", + "x": 237541, + "y": -206787, + "z": 8013, + "rotation": 146.97 + }, + { + "id": "BP_Crystal_mk31_7", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6924F5A3-453D83F0-E4C3E7B7-A048A7A3", + "x": 133466, + "y": 240929, + "z": -8872, + "rotation": -47.08 + }, + { + "id": "BP_Crystal_mk31_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7C8F1BC4-4E8FAF3A-89BDC283-48D252E7", + "x": -132419, + "y": -181783, + "z": 46942, + "rotation": 170.62 + }, + { + "id": "BP_Crystal_mk31_9", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EC2469DB-474B9678-62CB7884-AD1F238E", + "x": 52026, + "y": -80074, + "z": 5870, + "rotation": 141.4 + }, + { + "id": "BP_Crystal_mk32", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "39938BEA-4A33EFA5-61AC0580-7D633496", + "x": -176864, + "y": 88805, + "z": 11533, + "rotation": -22.92 + }, + { + "id": "BP_Crystal_mk320", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F46C7710-48BA364A-98C975B1-809A2662", + "x": -30875, + "y": 68456, + "z": 21706, + "rotation": 59.16 + }, + { + "id": "BP_Crystal_mk321", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E4DF8AA8-448A959E-85E5C596-AB6D6125", + "x": -42889, + "y": 40321, + "z": 21540, + "rotation": -40.03 + }, + { + "id": "BP_Crystal_mk322", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D78AB88A-44765658-AED47899-77D6D6DE", + "x": -31906, + "y": 31787, + "z": 20665, + "rotation": -99.55 + }, + { + "id": "BP_Crystal_mk323", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2C2B9BAE-48371C8E-00D035AD-399C7C8C", + "x": -87771, + "y": 62349, + "z": 25336, + "rotation": -30.04 + }, + { + "id": "BP_Crystal_mk324", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F617E115-4CCDCB4F-86E52497-869E0A6D", + "x": -78122, + "y": 21531, + "z": 21166, + "rotation": 120.58 + }, + { + "id": "BP_Crystal_mk325", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "53BF0E50-47F14DD3-55783DB2-E9E5AE74", + "x": -59225, + "y": -180011, + "z": 15343, + "rotation": -89.15 + }, + { + "id": "BP_Crystal_mk327", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1C117DDB-45DBDF06-9622F097-FFAE89D3", + "x": 51468, + "y": -2179, + "z": 13377, + "rotation": -73.67 + }, + { + "id": "BP_Crystal_mk32_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "05C29DBB-4390194D-7C0FE1A0-E6F3C2BD", + "x": -129640, + "y": -124075, + "z": -3827, + "rotation": -134.77 + }, + { + "id": "BP_Crystal_mk32_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3C9E1330-453F8474-32254996-9882CDA8", + "x": -19617, + "y": 163536, + "z": 7997, + "rotation": 149.28 + }, + { + "id": "BP_Crystal_mk32_16", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9B5B55A1-401998F9-BA36EEBD-9EB94F3F", + "x": -19907, + "y": 78261, + "z": 38859, + "rotation": 13.1 + }, + { + "id": "BP_Crystal_mk32_17", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2E3F7C3B-4941BE9D-76216C94-A722DB8C", + "x": 53245, + "y": 73958, + "z": 10890, + "rotation": 148.26 + }, + { + "id": "BP_Crystal_mk32_3", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0F305A07-4FDFA54D-4B32AA90-FF8BE7A8", + "x": 253884, + "y": -292994, + "z": 5424, + "rotation": -99.14 + }, + { + "id": "BP_Crystal_mk32_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EE71296A-479E1693-3F0D72A0-2E407E61", + "x": 176027, + "y": 250642, + "z": -899, + "rotation": 36.75 + }, + { + "id": "BP_Crystal_mk32_84", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "43C339D4-49299A1C-CD430787-B0CA56E7", + "x": -171998, + "y": 25223, + "z": 15695, + "rotation": -96.34 + }, + { + "id": "BP_Crystal_mk33", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F90E6AAE-42D82C02-B11AA3AB-235F051F", + "x": -164073, + "y": 92803, + "z": 7268, + "rotation": 82 + }, + { + "id": "BP_Crystal_mk33_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "770FEA44-48C6B7DE-6182E688-149F6DC7", + "x": -225540, + "y": -151572, + "z": -5124, + "rotation": -77.67 + }, + { + "id": "BP_Crystal_mk33_11", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0C951C08-472C99C6-DDCB4C8F-EBFF356C", + "x": 100493, + "y": -91392, + "z": 5325, + "rotation": 93.32 + }, + { + "id": "BP_Crystal_mk33_12", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D827DE31-4C9C3A1B-8F2E1CB0-85441C07", + "x": -25619, + "y": -65690, + "z": 23114, + "rotation": 23.83 + }, + { + "id": "BP_Crystal_mk33_14", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B25385E3-487783BC-47543B8E-5EF928B4", + "x": -27367, + "y": -76017, + "z": 11632, + "rotation": -135.24 + }, + { + "id": "BP_Crystal_mk33_17", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "FBEBF4C1-4B5F52FC-570CCCAF-A614BC3E", + "x": -16104, + "y": 114043, + "z": 21485, + "rotation": 42.81 + }, + { + "id": "BP_Crystal_mk33_18", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6FF42D11-455748AE-4A8ACCB4-BEC8C3C1", + "x": 114957, + "y": 78926, + "z": 18275, + "rotation": 133.88 + }, + { + "id": "BP_Crystal_mk33_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2B944B7E-4376D4AD-5713B68A-391EEF7F", + "x": -19009, + "y": 272696, + "z": 10535, + "rotation": -140.25 + }, + { + "id": "BP_Crystal_mk33_4", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "99C8EF69-485549BE-90DB4F99-52614B8A", + "x": 299170, + "y": -296890, + "z": 9882, + "rotation": -175.03 + }, + { + "id": "BP_Crystal_mk33_85", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "AAABB367-4CB7ECB8-829F46B3-07B2BF92", + "x": -44925, + "y": 55495, + "z": 21221, + "rotation": -66.64 + }, + { + "id": "BP_Crystal_mk33_9", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "A84AD0F5-4270CCD1-DD1B7BAF-7F6C7FDD", + "x": 190325, + "y": 230743, + "z": -11162, + "rotation": -48.29 + }, + { + "id": "BP_Crystal_mk34", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "36BAA9DC-406A6C4C-E6C596B0-4EDE7E3A", + "x": -92511, + "y": 57168, + "z": 9571, + "rotation": 36 + }, + { + "id": "BP_Crystal_mk34_10", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "FE67F0C5-4E8D447A-8B2C18B5-61B70F92", + "x": 171376, + "y": 174413, + "z": 4755, + "rotation": 126.87 + }, + { + "id": "BP_Crystal_mk34_15", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6EAEC7C6-4D5FC470-F89E3B83-86FA5011", + "x": 14424, + "y": 2752, + "z": 18402, + "rotation": -56.75 + }, + { + "id": "BP_Crystal_mk34_18", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "86172467-4145872C-634212A5-6836303E", + "x": -10103, + "y": -28939, + "z": 22220, + "rotation": 129.51 + }, + { + "id": "BP_Crystal_mk34_19", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "4985E90A-46F97191-CC8F27A5-F2C68031", + "x": 160560, + "y": 133222, + "z": 11080, + "rotation": -110.45 + }, + { + "id": "BP_Crystal_mk34_20", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "FF369DB7-498A7F51-5A7F9298-6DC65335", + "x": 60888, + "y": -130688, + "z": 2713, + "rotation": -20.46 + }, + { + "id": "BP_Crystal_mk34_3", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7B96B0B2-4EEE1997-79387EB4-B7495FC0", + "x": -136864, + "y": 241804, + "z": -11804, + "rotation": 149.14 + }, + { + "id": "BP_Crystal_mk34_31", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "41ACCBFA-4EC7D55D-4446E48C-C1AABE12", + "x": 244585, + "y": 27220, + "z": -1671, + "rotation": -6.66 + }, + { + "id": "BP_Crystal_mk34_6", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F7596CC1-48E5035C-1FD108B2-37B7E4A0", + "x": -286815, + "y": -160492, + "z": 6792, + "rotation": -1.92 + }, + { + "id": "BP_Crystal_mk34_86", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "49EFCD2A-418C92E2-E9681F89-D4B5AEF4", + "x": -96334, + "y": 68858, + "z": 12271, + "rotation": -60.43 + }, + { + "id": "BP_Crystal_mk35", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0654306F-48B1AA81-FAB8508F-84274314", + "x": -62877, + "y": -212286, + "z": 8378, + "rotation": -96.28 + }, + { + "id": "BP_Crystal_mk35_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "BB697AE6-425BD93E-20E04C99-BBE34983", + "x": -48774, + "y": 131219, + "z": 24038, + "rotation": -132.66 + }, + { + "id": "BP_Crystal_mk35_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C117DF5E-419A592D-055977A7-BBED5362", + "x": 374477, + "y": -114467, + "z": 5892, + "rotation": -48.51 + }, + { + "id": "BP_Crystal_mk35_11", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3E11C2CB-4AA02820-C8AE309B-38C58CB0", + "x": 198117, + "y": 180766, + "z": 17957, + "rotation": -111.1 + }, + { + "id": "BP_Crystal_mk35_21", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "042D7611-46243403-20849DB6-FA8F9FF7", + "x": 150971, + "y": -88202, + "z": 12951, + "rotation": 39.54 + }, + { + "id": "BP_Crystal_mk35_22", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CC5D8417-46614DF4-E9619E99-4AB2083C", + "x": 17851, + "y": 125646, + "z": 27380, + "rotation": 24.79 + }, + { + "id": "BP_Crystal_mk35_6", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3D3C7465-42B2F013-F1E277A0-292D717F", + "x": -135991, + "y": -182952, + "z": 18125, + "rotation": 42.94 + }, + { + "id": "BP_Crystal_mk35_87", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7FD340CF-4FF47E08-F4736D97-A438E7D3", + "x": -76092, + "y": 70611, + "z": 27346, + "rotation": 90.41 + }, + { + "id": "BP_Crystal_mk36", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "FA76AEA6-4F9E4C89-5C4E33A3-17F71680", + "x": -143599, + "y": 127389, + "z": 13045, + "rotation": -143.6 + }, + { + "id": "BP_Crystal_mk36_12", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6169C52F-4E3BC094-C2B800B7-ED08EC2A", + "x": 140604, + "y": 251774, + "z": -5815, + "rotation": -21.24 + }, + { + "id": "BP_Crystal_mk36_22", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "33C4422F-401051E5-8029EF97-BF540398", + "x": 201331, + "y": -90733, + "z": 10952, + "rotation": -66.17 + }, + { + "id": "BP_Crystal_mk36_26", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "33443FB9-41E3FC78-9E3464B0-59B142AE", + "x": -61493, + "y": 10492, + "z": 23073, + "rotation": -51.05 + }, + { + "id": "BP_Crystal_mk36_50", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5D3C9116-44819CE5-61E2D5A3-72E62EB3", + "x": 258787, + "y": 67454, + "z": -1043, + "rotation": -51.12 + }, + { + "id": "BP_Crystal_mk36_88", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "617F65FE-46B4C722-42BD44A6-5E9298B9", + "x": -51403, + "y": 66521, + "z": 24877, + "rotation": -31.53 + }, + { + "id": "BP_Crystal_mk37", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DF278BDE-409661F3-E1ED2285-EC37B8A0", + "x": -36416, + "y": -255209, + "z": 4687, + "rotation": -53.41 + }, + { + "id": "BP_Crystal_mk37_14", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CF0A153C-44300415-C3D94DA3-FBA89B05", + "x": 174830, + "y": -222015, + "z": 13420, + "rotation": 35.32 + }, + { + "id": "BP_Crystal_mk37_30", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F34FCD02-4E7C85B5-C225A0A9-858D2934", + "x": 9355, + "y": 57885, + "z": 20895, + "rotation": 6.05 + }, + { + "id": "BP_Crystal_mk37_51", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9EA4033B-494A66AB-7E001986-2957446E", + "x": 187595, + "y": 55850, + "z": -1683, + "rotation": 68.37 + }, + { + "id": "BP_Crystal_mk37_89", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E1DC779E-4B3E65E8-51EBF995-C6DBB66B", + "x": -56555, + "y": 110985, + "z": 23875, + "rotation": -20.85 + }, + { + "id": "BP_Crystal_mk38", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "51F06776-4DD7A062-DBC10AAB-665D59A4", + "x": -24413, + "y": -198056, + "z": 19271, + "rotation": 90.22 + }, + { + "id": "BP_Crystal_mk38_14", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DB4D5066-4B99B79D-EB6CBEA4-E6416091", + "x": 131389, + "y": 164666, + "z": -3922, + "rotation": 52.46 + }, + { + "id": "BP_Crystal_mk38_34", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5D2295DE-4411B55A-B778E98E-509F2BE4", + "x": 40556, + "y": 77732, + "z": 26895, + "rotation": -0.06 + }, + { + "id": "BP_Crystal_mk38_52", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "60D9D868-457283C7-8DA632BD-888AB95B", + "x": 308293, + "y": -9705, + "z": 1764, + "rotation": -156.53 + }, + { + "id": "BP_Crystal_mk38_90", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8D856840-44EC5DA5-8DE762B0-FADE4252", + "x": -109141, + "y": 101011, + "z": 16308, + "rotation": 112.88 + }, + { + "id": "BP_Crystal_mk39", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "AEA45DBE-483E95EA-24A9BF8E-8D41AB64", + "x": -15336, + "y": -192824, + "z": 7399, + "rotation": 42.32 + }, + { + "id": "BP_Crystal_mk39_41", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C5A89D7C-47E2B6D0-369C93A4-FD30F24C", + "x": 9162, + "y": 100359, + "z": 24430, + "rotation": 62.85 + }, + { + "id": "BP_Crystal_mk39_91", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C70731E2-4458D74A-728D5C93-BB9F374B", + "x": -73275, + "y": 48792, + "z": 25491, + "rotation": -119 + }, + { + "id": "BP_Crystal_mk3_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "15087482-41878B66-581DB096-F7E2D789", + "x": 255772, + "y": -290706, + "z": 32566, + "rotation": -147.22 + }, + { + "id": "BP_Crystal_mk3_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0F515D5E-4CDE6B0A-F076BCB6-02A4EBF2", + "x": -264290, + "y": -33953, + "z": 9152, + "rotation": -120.25 + }, + { + "id": "BP_Crystal_mk3_13", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B8E35E99-4D3A84B1-9C8C2C9E-C16019B5", + "x": 66408, + "y": 203372, + "z": 11019, + "rotation": 87.55 + }, + { + "id": "BP_Crystal_mk3_16", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "49B09E30-4E0C7DE8-53FD7994-181A847B", + "x": 78298, + "y": 173893, + "z": -3989, + "rotation": 20.69 + }, + { + "id": "BP_Crystal_mk3_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D9FB094B-4AAE1753-2C04F4B4-35028B18", + "x": -198374, + "y": -10592, + "z": 3816, + "rotation": 8.82 + }, + { + "id": "BP_Crystal_mk3_3", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D9DE6996-408DBEB8-845F7287-6F397813", + "x": -157871, + "y": -89591, + "z": -4185, + "rotation": 165.22 + }, + { + "id": "BP_Crystal_mk3_4", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6FA16E73-48446BB4-76A67F88-1E3669C6", + "x": -218427, + "y": 24200, + "z": 8755, + "rotation": 83.6 + }, + { + "id": "BP_Crystal_mk3_5", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "57C6758C-4515A778-70ED4899-195E1249", + "x": 91263, + "y": 232000, + "z": 7409, + "rotation": -1.98 + }, + { + "id": "BP_Crystal_mk3_9", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0CD2EAD6-4D149FB0-FC826896-15BB11F9", + "x": 16288, + "y": 214765, + "z": 13773, + "rotation": -92.61 + }, + { + "id": "BP_Crystal_mk3_C_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DD9DA315-4086A0AF-EE5BC3AA-CCD81279", + "x": -59148, + "y": -109189, + "z": 1205, + "rotation": -168.36 + }, + { + "id": "BP_Crystal_mk3_C_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "BE19F268-46E7ECF7-A2057AB2-597D2410", + "x": -3404, + "y": -179646, + "z": 6313, + "rotation": 41.55 + }, + { + "id": "BP_Crystal_mk3_C_10", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9BC6C413-4461A6A7-03B3D793-050045FE", + "x": 45875, + "y": -171693, + "z": 5396, + "rotation": -4.75 + }, + { + "id": "BP_Crystal_mk3_C_11", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C059C7DB-49D8D6CA-688A21A7-F9D8DA84", + "x": 25680, + "y": -161574, + "z": 17079, + "rotation": 13.26 + }, + { + "id": "BP_Crystal_mk3_C_12", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EA1B8B2E-4A3681F4-6E04289A-328AD4C5", + "x": 52140, + "y": -179771, + "z": 9201, + "rotation": -37.49 + }, + { + "id": "BP_Crystal_mk3_C_13", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "03A3E7FD-4051972C-536ACF97-FDCC3A0E", + "x": 62256, + "y": -162903, + "z": 23172, + "rotation": 18.05 + }, + { + "id": "BP_Crystal_mk3_C_14", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "32635781-49FAAFE0-5186A697-35D96E1A", + "x": 85671, + "y": -158300, + "z": 9726, + "rotation": 58.12 + }, + { + "id": "BP_Crystal_mk3_C_15", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "808C27CC-4AC5D8B6-B7EFBDA0-6EED2114", + "x": 81300, + "y": -155564, + "z": 28169, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk3_C_16", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9B72988A-4C4A8EEA-C7C1C5AC-49AAFF7B", + "x": 94412, + "y": -170437, + "z": 13388, + "rotation": -175.8 + }, + { + "id": "BP_Crystal_mk3_C_17", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "62DB1ADE-4914FC30-BF502280-C7DFADE4", + "x": 83481, + "y": -195209, + "z": 19227, + "rotation": 72.37 + }, + { + "id": "BP_Crystal_mk3_C_18", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C31E9225-40CAF3A6-A7C231AF-5A7B5E4C", + "x": 99788, + "y": -214924, + "z": 4077, + "rotation": -1.3 + }, + { + "id": "BP_Crystal_mk3_C_19", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0A16773E-459F0224-9D415285-62DDBA36", + "x": 72368, + "y": -207758, + "z": 12638, + "rotation": 93.64 + }, + { + "id": "BP_Crystal_mk3_C_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "25509B4E-4DB14E19-B1CAC5B9-7C3984AC", + "x": 31741, + "y": -175316, + "z": 3247, + "rotation": -156.31 + }, + { + "id": "BP_Crystal_mk3_C_20", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "35F333E0-4663C10A-D230898C-C6C9B29D", + "x": 162495, + "y": 224747, + "z": -7890, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk3_C_22", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B1A2F3BA-423E3BCA-5A9C1094-EDFC2CC7", + "x": 144988, + "y": 101902, + "z": 21318, + "rotation": -173.09 + }, + { + "id": "BP_Crystal_mk3_C_23", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6DF0545E-4F34ED57-C0ED2EA3-8843568E", + "x": 181954, + "y": 92921, + "z": 3222 + }, + { + "id": "BP_Crystal_mk3_C_25", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0674DAC1-4512294C-597B57B5-31DE561F", + "x": 120698, + "y": -126196, + "z": 19370, + "rotation": 2.23 + }, + { + "id": "BP_Crystal_mk3_C_27", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7D113218-4834D797-4100DDBB-235B2D89", + "x": 169905, + "y": -180630, + "z": 9435, + "rotation": 18.64 + }, + { + "id": "BP_Crystal_mk3_C_28", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "716875B9-4E965851-D66C60B1-6BB4382D", + "x": 140413, + "y": -168848, + "z": 23048, + "rotation": -135.33 + }, + { + "id": "BP_Crystal_mk3_C_29", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8A2B66E1-4E66FF82-C43DB3BF-50CBC13E", + "x": 167125, + "y": -161695, + "z": 28410, + "rotation": -42.85 + }, + { + "id": "BP_Crystal_mk3_C_3", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "4A625F6D-41A6260F-265A57A0-F7C04CC2", + "x": -53124, + "y": -228898, + "z": 8128, + "rotation": -21.89 + }, + { + "id": "BP_Crystal_mk3_C_30", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9CA8BFF9-419D8397-81DA1C86-79036EB5", + "x": 134090, + "y": -148245, + "z": 12905, + "rotation": -164.96 + }, + { + "id": "BP_Crystal_mk3_C_31", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3F4C3995-4EAED41E-85D2CC81-B498D17A", + "x": 131140, + "y": -196795, + "z": 9875, + "rotation": 64.07 + }, + { + "id": "BP_Crystal_mk3_C_32", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B3013E48-44D42A37-F89EDD96-248EFEFB", + "x": 121715, + "y": -170190, + "z": 12265, + "rotation": -51.86 + }, + { + "id": "BP_Crystal_mk3_C_33", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E1197866-42A6388A-37DFA1A0-770B3862", + "x": 121695, + "y": -190865, + "z": 7765, + "rotation": 80.75 + }, + { + "id": "BP_Crystal_mk3_C_34", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "76170EE7-4B75603B-5FD9A9AA-6FAB79DD", + "x": 95362, + "y": -182902, + "z": 5831, + "rotation": -49.46 + }, + { + "id": "BP_Crystal_mk3_C_35", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "75640373-4E70B4D4-0F215E9B-3F518D58", + "x": 226569, + "y": 106004, + "z": -1398, + "rotation": -3.22 + }, + { + "id": "BP_Crystal_mk3_C_37", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "056F97B5-46B433C9-D6D00DAA-34B7EAEF", + "x": 228965, + "y": 129795, + "z": 16900, + "rotation": -32.62 + }, + { + "id": "BP_Crystal_mk3_C_38", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "ABD4F6A9-4C31E532-2F06B197-9F29F8C7", + "x": 231574, + "y": 148534, + "z": 10093, + "rotation": 86.48 + }, + { + "id": "BP_Crystal_mk3_C_39", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "85087D8F-4117E648-1BEB39A6-15FDB92C", + "x": 205619, + "y": 124952, + "z": -6371, + "rotation": 6.96 + }, + { + "id": "BP_Crystal_mk3_C_4", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "83F4A03E-43F30D0E-3CF79E91-0E90052B", + "x": 85420, + "y": 125286, + "z": 8913, + "rotation": 93.21 + }, + { + "id": "BP_Crystal_mk3_C_40", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F68D5477-4FCAE488-DFF8629D-AA974524", + "x": 276096, + "y": 50570, + "z": -616, + "rotation": -1.16 + }, + { + "id": "BP_Crystal_mk3_C_41", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5C78B322-414B9A45-808AA8B8-C29E9EA3", + "x": 236033, + "y": 94330, + "z": -896, + "rotation": 179.03 + }, + { + "id": "BP_Crystal_mk3_C_42", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "073D2CEE-45473FB5-EC6BA6A1-9AC9160A", + "x": 278162, + "y": 12682, + "z": 1374, + "rotation": -93.84 + }, + { + "id": "BP_Crystal_mk3_C_43", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "51E9D0C8-4314933A-BB0D1DA7-9A2CA1C0", + "x": 228990, + "y": -265020, + "z": 20885, + "rotation": -43.9 + }, + { + "id": "BP_Crystal_mk3_C_46", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "53BBE2BD-457FED78-09AB99B2-C0FB5856", + "x": 210835, + "y": -290925, + "z": 22885, + "rotation": -76.08 + }, + { + "id": "BP_Crystal_mk3_C_5", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3A11F927-41916585-2028C893-89D749DA", + "x": 44047, + "y": 189345, + "z": 10354, + "rotation": 140.88 + }, + { + "id": "BP_Crystal_mk3_C_6", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7BE4ECDA-406314C5-CCABDC8C-E2EE14B2", + "x": 101425, + "y": -44532, + "z": 17824, + "rotation": -144.98 + }, + { + "id": "BP_Crystal_mk3_C_7", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1CC8E524-47A772A2-5A8E658F-A89DE4C0", + "x": 53902, + "y": -116005, + "z": 24485, + "rotation": -106.62 + }, + { + "id": "BP_Crystal_mk3_C_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "ED5C3063-42BE17C6-DBE9318F-57849C05", + "x": 23240, + "y": -183814, + "z": 5098, + "rotation": -32.62 + }, + { + "id": "BP_Crystal_mk3_C_9", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "2B89571A-444DB405-2742DFBB-64466069", + "x": 56082, + "y": -169283, + "z": 25644, + "rotation": -125.73 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0036301_1297164245", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "44AAB8D8-48B279BA-94CBB2AB-145E5E02", + "x": 238371, + "y": 153571, + "z": 13063, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0056301_2025275612", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "02F0DBA5-450EFAF8-50872D98-0674E95C", + "x": 186090, + "y": 97485, + "z": 12635, + "rotation": -74.77 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0145C01_1961591832", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9D039FA8-4A9D5749-BC2ED392-A50B658B", + "x": 172164, + "y": -216179, + "z": 20199, + "rotation": -37.84 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0277C01_2138469968", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7B2212DD-4184EC76-3E324786-BF33DFB0", + "x": 129432, + "y": 57904, + "z": 10448, + "rotation": -69.07 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0297C01_1197302326", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "BA0A9CDD-45F791E3-4A570AA9-C461C068", + "x": 150205, + "y": 46067, + "z": 12251, + "rotation": 0.14 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0297C01_2084244334", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1F4CC13A-453AB510-A7AB968C-AE2F7174", + "x": 128995, + "y": 64193, + "z": 19440, + "rotation": 38.88 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F02A7C01_1360375511", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3F4D892D-4089ABC6-7D6B01AE-7A4D7881", + "x": 103576, + "y": 75659, + "z": 16428, + "rotation": 81.27 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F02A7C01_1594764512", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "83FD2534-49794BF7-D06E2FBE-826CAB4D", + "x": 82290, + "y": 67192, + "z": 24097, + "rotation": 76.17 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F02AA401_1523751748", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E626AE1D-4345E5C1-0D049BAE-D0E4C53F", + "x": -151575, + "y": 77412, + "z": 21708, + "rotation": 72.82 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0406001_1876322801", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B840FEBB-40E31A45-2927619F-0620C219", + "x": 173899, + "y": -154970, + "z": 26277, + "rotation": -8.36 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0585D01_1891820856", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "45B10C85-40FEB7B1-6D7952B2-432922BE", + "x": 231950, + "y": 125298, + "z": 6984, + "rotation": -126.67 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0585E01_1956229960", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B931BDA6-43CF33DE-5AC8169D-A79660B8", + "x": 124270, + "y": -149770, + "z": 15820, + "rotation": -61.87 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F05A5301_1813979681", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EA21FB15-4789A6D8-3DCF08BF-E8A9C5B8", + "x": 154163, + "y": -271535, + "z": -1336, + "rotation": 63.89 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0627801_2036326140", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8BBA789A-4B132E0D-D496DEA1-2D773D4C", + "x": 208907, + "y": -21631, + "z": 18580, + "rotation": -153.71 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F063A401_1570680776", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F3F898C5-465331E3-D8E5D18D-01C09A1E", + "x": -155221, + "y": 126132, + "z": 15257, + "rotation": 36.61 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F06A5C01_1458580968", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "31E00568-4E60B426-DFABDC8F-774C2CDD", + "x": 223085, + "y": 114951, + "z": 3132, + "rotation": -107.12 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F06B5C01_1971558160", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "099071D0-4224D97E-C8EDAD92-AB9F98B1", + "x": 234537, + "y": 117263, + "z": 11707, + "rotation": 104.28 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F06DAF01_1615607160", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1C3E90F0-49E3415A-8A8538A4-3C146B22", + "x": 170487, + "y": -45485, + "z": 5512 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F06EA401_1809380716", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D4724BF9-40482265-250F8BB9-091AC4A6", + "x": -143905, + "y": 139824, + "z": 13128, + "rotation": 26.03 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F070A401_1749057071", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "ACB19A45-40CCC38A-1A8FB49E-C4E94EEF", + "x": -128483, + "y": 156718, + "z": 13823, + "rotation": 56.25 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0717801_1844491783", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "D265F219-43D63DE1-BD1BC8B9-39B97F73", + "x": 177385, + "y": 849, + "z": 18925, + "rotation": -19.69 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0727801_1634602960", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "56F8AEED-48E13D90-FF16D7A0-FC085875", + "x": 174510, + "y": -24395, + "z": 19107, + "rotation": -36.61 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F072A401_1701014427", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8D9870B1-47DA2855-02936CAF-B984C47C", + "x": -136317, + "y": 176577, + "z": 16319, + "rotation": -130.47 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0775B01_1729283204", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CF421FF7-4263A3E5-DF4B3488-F38B2EFB", + "x": 144026, + "y": -162984, + "z": 2648, + "rotation": 75.49 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F079AF01_1615812377", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "31B2B5DC-42F415EF-3B6E8784-46698F06", + "x": -30334, + "y": -159804, + "z": 16061, + "rotation": -74.51 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F07F6001_2021531889", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "AE07DAF9-46B311BB-AF137AAD-25DB1BF4", + "x": 175113, + "y": -140313, + "z": 23781, + "rotation": -171.56 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0807C01_1687435632", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "4B444742-4AE955AA-40D09FB5-408FCD25", + "x": 157366, + "y": 67974, + "z": -10, + "rotation": 19.69 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0835B01_1215726345", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3BF480FA-4ADFA42B-7D41359D-50FCFD41", + "x": 142477, + "y": -188688, + "z": 7345, + "rotation": -16.34 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0895B01_1662240391", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EDDAC355-4919460F-AE8FC59A-001ACF2C", + "x": 163448, + "y": -221893, + "z": 4746, + "rotation": -10.22 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F08A5B01_2054934580", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0B22E947-484E9D78-448C03BD-9E80C69C", + "x": 175457, + "y": -225145, + "z": 21124, + "rotation": 37.59 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F08B5B01_2043989757", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "46873407-44735A22-F98223A3-471CD516", + "x": 124160, + "y": -156162, + "z": 11828, + "rotation": 2.65 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0AA5201_1333563683", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "27D869B8-4DA61921-FE1A5CA6-C02F8037", + "x": 198073, + "y": -188701, + "z": 25342, + "rotation": 117.63 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0AC5201_1755460045", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5DAD5C56-4548AE58-ACA1628E-6897E9BF", + "x": 205434, + "y": -285492, + "z": 9664, + "rotation": -36.73 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0D45B01_1873455580", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "20EB5C7D-4E9BB766-3FAE92BE-502D337A", + "x": 113623, + "y": -159043, + "z": 9278, + "rotation": 30.44 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0DC5B01_1453235998", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1A83C740-456599D2-84D84FBB-3097DD97", + "x": 160158, + "y": -171897, + "z": 11245, + "rotation": -118.29 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0DD5B01_1099993180", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "420E45A8-4BB74523-7E9619A5-3AF28697", + "x": 171001, + "y": -177233, + "z": 4656, + "rotation": -73.12 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E57A01_1659753299", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "479151F8-4212ADA2-F2B1EA94-BB531735", + "x": 167368, + "y": 15657, + "z": 22839, + "rotation": 107.72 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E57A01_1764014301", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "AEEBF13C-440E0BAE-470950AE-A6184807", + "x": 146608, + "y": 15383, + "z": 18161, + "rotation": 8.58 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E67A01_2112441483", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8111E029-4BA4C860-A534C3AC-F9147A87", + "x": 135567, + "y": 31101, + "z": 18559, + "rotation": -142.09 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E85F01_1517332314", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "CF76E147-406249AF-C92BD0BE-2FD48CBB", + "x": 200672, + "y": -200223, + "z": 34166, + "rotation": -85.55 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E85F01_1852831317", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "10556B5D-44E997CB-99D9A2BC-BCD4FD4B", + "x": 218848, + "y": -255005, + "z": 33313, + "rotation": 116.05 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E87A01_1253298841", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B0CA6267-4AD1645F-A1070D87-98F89411", + "x": 71978, + "y": 19244, + "z": 20486, + "rotation": 64.55 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0E95F01_1176278498", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E35D6496-420CF8F0-F213309D-85BA2217", + "x": 216484, + "y": -286480, + "z": 42936, + "rotation": 15.17 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0EE7A01_1230590903", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "38ABEC04-4E4914E2-01C64B91-06F65E34", + "x": 92555, + "y": 21594, + "z": 19405, + "rotation": 10.84 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0F77A01_1578399538", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "123C46B9-4F43D75C-72F5ADAD-8076D6A3", + "x": 128833, + "y": 12802, + "z": 14071, + "rotation": 38.68 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0F87A01_1173440720", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "0E23E2D3-432B7B57-9DD098A6-EAA41F88", + "x": 182921, + "y": -37060, + "z": 14455, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0F87A01_1467276724", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5DB5B18C-46B943EA-46D7AFB1-487FEA06", + "x": 192834, + "y": -23714, + "z": 15292, + "rotation": 41.59 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0F87A01_1842830725", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "16626587-442C1993-B6B778B4-9A514517", + "x": 258281, + "y": -41580, + "z": 14458, + "rotation": -14.13 + }, + { + "id": "BP_Crystal_mk3_C_UAID_04421A9713F0FB5201_1256959955", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "74ACB878-48DA3EAA-B30BB387-9394A66C", + "x": 160194, + "y": -206341, + "z": 7271, + "rotation": 23.11 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F798B5F01_1138825944", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6140D392-461F752B-22868EB5-4508072D", + "x": 37632, + "y": -166387, + "z": 22369 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F79905F01_1499395864", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3F07069B-43FF8915-326EF8AD-3E02A2EC", + "x": 37881, + "y": -182601, + "z": 3610, + "rotation": 95.72 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F79D5AD01_1325784341", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5828B22F-46211438-33DA4B86-F1A7F48D", + "x": 294583, + "y": -172605, + "z": 3901, + "rotation": -44.75 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F79DBAD01_1358054399", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E0822FD1-4EEBB344-4A24AB93-BB1ECC37", + "x": 261007, + "y": -256439, + "z": 3673, + "rotation": 150.08 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F79DCAD01_1900669576", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6BB75F1F-4C18B2EC-B3324AA9-758623C6", + "x": 380202, + "y": -274445, + "z": 1909, + "rotation": -6.05 + }, + { + "id": "BP_Crystal_mk3_C_UAID_40B076DF2F79F68401_1268529865", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "64A8D2C0-4E376983-3513FFBF-54907732", + "x": -33890, + "y": 113635, + "z": -6565, + "rotation": 160.05 + }, + { + "id": "BP_Crystal_mk4", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "BF9682A7-41D5D2E7-6408D69F-5C4C6A87", + "x": -50686, + "y": -238907, + "z": 3871, + "rotation": -53.3 + }, + { + "id": "BP_Crystal_mk40", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8DD7FF68-40F2BD84-AC67F68A-8E848159", + "x": 141650, + "y": -194650, + "z": 8835, + "rotation": 141.58 + }, + { + "id": "BP_Crystal_mk41", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "21D4F6D5-4AAB9CA6-3210128E-0FC9D348", + "x": 137058, + "y": -201534, + "z": 800, + "rotation": 153.28 + }, + { + "id": "BP_Crystal_mk42_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8D34B364-4219DD23-5E5C91A3-A5A1DCA6", + "x": 180355, + "y": -267555, + "z": 19165, + "rotation": -27.18 + }, + { + "id": "BP_Crystal_mk43", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "F3271681-47DC4431-D5DBE1B9-A298A249", + "x": 141585, + "y": -168570, + "z": 11485, + "rotation": 64.69 + }, + { + "id": "BP_Crystal_mk44", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "B00B45D0-44F84235-F8CFBDA4-94DB9F86", + "x": 100283, + "y": -182879, + "z": 2284, + "rotation": 23.46 + }, + { + "id": "BP_Crystal_mk4_UAID_40B076DF2F79435901_1076437937", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "28B831EB-4FA391D6-27899485-F3787D05", + "x": -49358, + "y": -220524, + "z": 3663, + "rotation": -29.15 + }, + { + "id": "BP_Crystal_mk5", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "68DE6262-41C61DEE-8B3A5782-D06B9E16", + "x": -57751, + "y": -219160, + "z": -1021, + "rotation": 57.99 + }, + { + "id": "BP_Crystal_mk5_5", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5C82E252-43AAA8CD-420BD598-70135E18", + "x": 123779, + "y": -201060, + "z": 9742, + "rotation": -156.91 + }, + { + "id": "BP_Crystal_mk61_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "E63FCCC2-48105EF9-F90C5BBE-79CABA14", + "x": 147242, + "y": -216065, + "z": 6780, + "rotation": -68.45 + }, + { + "id": "BP_Crystal_mk62", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "319B187D-4B648469-AF1A62AB-AC4F8CB4", + "x": 129225, + "y": -167400, + "z": 10770, + "rotation": 70.13 + }, + { + "id": "BP_Crystal_mk64_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7AD04E4D-45E528AA-1A37F989-E092AE70", + "x": 183086, + "y": -244460, + "z": 23505, + "rotation": -72.73 + }, + { + "id": "BP_Crystal_mk65", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5880CEEB-4F1F95C4-01F51FAD-1D1BA017", + "x": 122425, + "y": -183035, + "z": 10600, + "rotation": -2.86 + }, + { + "id": "BP_Crystal_mk67_2", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DB3CAA48-4D0C061B-C5338C94-FD2282CB", + "x": 182224, + "y": -305952, + "z": 20502, + "rotation": -0.02 + }, + { + "id": "BP_Crystal_mk70_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "9B9921D6-4A700084-4780EFA1-16CEDC8A", + "x": 121860, + "y": -153925, + "z": 11895, + "rotation": 171.56 + }, + { + "id": "BP_Crystal_mk71_0", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "5135E25B-4769316F-227B6C8F-71B2E1DE", + "x": 196414, + "y": -169702, + "z": 13456, + "rotation": 3.42 + }, + { + "id": "BP_Crystal_mk71_9", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "75CF523D-415AE257-B8D84D83-F03DA1FA", + "x": 212415, + "y": 126415, + "z": 6325, + "rotation": -66.19 + }, + { + "id": "BP_Crystal_mk72_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "EB4A7724-4E034D62-2BBA1D85-C665AFD5", + "x": 213015, + "y": -186910, + "z": 20425, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk73", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "29470881-41B1E782-8CB0DA8B-3BF8214B", + "x": 237167, + "y": -273298, + "z": 16501, + "rotation": -41.41 + }, + { + "id": "BP_Crystal_mk73_15", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "8DC9A09A-421377D4-1CFF23AC-DBB69939", + "x": 228315, + "y": 139843, + "z": 3658, + "rotation": -177.86 + }, + { + "id": "BP_Crystal_mk73_UAID_04421A9713F03A5B01_1974981481", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "166710EB-43D269A5-FD8EBFBE-7DE8D607", + "x": 212843, + "y": -144939, + "z": 16431, + "rotation": 138.53 + }, + { + "id": "BP_Crystal_mk73_UAID_04421A9713F0B65201_1605781812", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "6E3AF0DD-42583C80-5CD6AD9D-F676404A", + "x": 151123, + "y": -135011, + "z": 21699, + "rotation": -8.48 + }, + { + "id": "BP_Crystal_mk74", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "07D0B03F-48AC9459-F5AE0E9A-8CFF9EB6", + "x": 162130, + "y": 69133, + "z": 14664, + "rotation": -174.04 + }, + { + "id": "BP_Crystal_mk75", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7EF615B8-44D2AFA1-18784293-954C0AD5", + "x": 177248, + "y": 98029, + "z": 7139, + "rotation": -58.01 + }, + { + "id": "BP_Crystal_mk76", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "7FDF5DFB-420A28B3-93FB9C90-18B50681", + "x": 176778, + "y": 92114, + "z": 11410, + "rotation": 27.1 + }, + { + "id": "BP_Crystal_mk77_1", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "1A93F318-437CCFF2-5E0B5D80-A734B6B4", + "x": 121240, + "y": -143071, + "z": 10707, + "rotation": -12.71 + }, + { + "id": "BP_Crystal_mk79_8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "61D72A12-45B85D9A-58DF2C96-DA5E890E", + "x": 232149, + "y": 134644, + "z": -8415, + "rotation": -125.51 + }, + { + "id": "BP_Crystal_mk8", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "DBC2CCFF-436767AB-2231C9B0-13288E66", + "x": -18591, + "y": -229429, + "z": 4296, + "rotation": 61.56 + }, + { + "id": "BP_Crystal_mk85", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "99CE7812-4731435A-DA4BA3B0-0D7821E2", + "x": 177695, + "y": 68263, + "z": 19688, + "rotation": 67.78 + }, + { + "id": "BP_Crystal_mk86", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C972D4E1-414911AA-7B836082-225C9DA2", + "x": 157461, + "y": 71204, + "z": 17878, + "rotation": 155.96 + }, + { + "id": "BP_Crystal_mk87", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "075A5275-4E943A50-7D6316BB-B029FD02", + "x": 155407, + "y": 101946, + "z": 8236, + "rotation": 124.83 + }, + { + "id": "BP_Crystal_mk89", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "3CEB8C81-45FA8D6C-CC2569B4-6F7FF234", + "x": 194244, + "y": 110754, + "z": 15475, + "rotation": 88.76 + }, + { + "id": "BP_Crystal_mk8_UAID_40B076DF2F79915F01_1298320049", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "22465196-4076EAE1-E1EAC98E-A75632DC", + "x": -13821, + "y": -187407, + "z": -727, + "rotation": 0 + }, + { + "id": "BP_Crystal_mk90", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "93EA57D4-4C879824-3CD73482-564998C4", + "x": 162677, + "y": 131663, + "z": 12843, + "rotation": -106.28 + }, + { + "id": "BP_Crystal_mk91", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "76E87ACD-4064794C-7EFBAB96-8FD021A6", + "x": 151141, + "y": 140691, + "z": 13255, + "rotation": 13.3 + }, + { + "id": "BP_Crystal_mk92", + "type": "slugMk3", + "classPath": "BP_Crystal_mk3_C", + "pickupGuid": "C4ADF800-46C41590-1E709F99-0FEF3B0C", + "x": 200497, + "y": 107571, + "z": 8178, + "rotation": 19.04 + }, + { + "id": "BP_Crystal_mk3_C_36", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "FFE5193E-413E6E53-DBF13C8A-A6FB2ACF", + "x": 206595, + "y": 171285, + "z": 22367, + "rotation": 0.82 + }, + { + "id": "BP_WAT10", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "33A3E0C7-4D138637-4C933686-86346940", + "x": 20256, + "y": -250754, + "z": 4922, + "rotation": -170.59 + }, + { + "id": "BP_WAT100_3119", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "615A1A3F-4920671E-A00D6E84-E4A4D107", + "x": 41150, + "y": 173525, + "z": 1546, + "rotation": -22 + }, + { + "id": "BP_WAT10_UAID_40B076DF2F79B75201_1879979970", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "C36C7A0D-4782160E-890ABA8F-2E8A41BB", + "x": 26013, + "y": -232917, + "z": -1783, + "rotation": 51.36 + }, + { + "id": "BP_WAT110", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4720E6F7-48660B82-860303A6-EBFFEC31", + "x": -25119, + "y": 267763, + "z": -11490, + "rotation": -120.23 + }, + { + "id": "BP_WAT110_47", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "9A5FCA50-4EF764A9-B19634BC-01DC2C72", + "x": -44104, + "y": 31122, + "z": 23611, + "rotation": 59.74 + }, + { + "id": "BP_WAT11_0", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4480F02D-4005CA6E-3765EFB4-52777DBD", + "x": 170052, + "y": 232325, + "z": -8567, + "rotation": 80.53 + }, + { + "id": "BP_WAT11_1", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4EC965D1-4CF5361D-30A723BD-EAB546BA", + "x": -50928, + "y": 220009, + "z": -3798, + "rotation": -13.83 + }, + { + "id": "BP_WAT11_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "6210BEC1-42629CCE-07DCA597-9A6E6AB3", + "x": 24129, + "y": 74259, + "z": 22864, + "rotation": -49.1 + }, + { + "id": "BP_WAT11_35", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "B9C33516-4564BD67-D0033280-7A689F32", + "x": -29456, + "y": 81320, + "z": 21328, + "rotation": 29.39 + }, + { + "id": "BP_WAT121", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "486D2B8E-476833A8-CBD2E684-CFF5ABBC", + "x": 210229, + "y": -248867, + "z": 20268, + "rotation": 132.19 + }, + { + "id": "BP_WAT123_1", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "0F3F8C57-465A4779-B3536E95-835732B1", + "x": 209893, + "y": -186445, + "z": 7430, + "rotation": -56.25 + }, + { + "id": "BP_WAT12_1", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "7E8D89EB-49200F83-959E6695-CA5BBBB0", + "x": 178590, + "y": 57809, + "z": 4282, + "rotation": 9.59 + }, + { + "id": "BP_WAT12_16", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "451A69E5-4A2FF152-8124E397-AA58F0C8", + "x": 151406, + "y": 96198, + "z": 25683, + "rotation": 162.84 + }, + { + "id": "BP_WAT12_25", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "3053D9C3-432F06C1-FF6AF2B0-95E2C410", + "x": -164981, + "y": 40623, + "z": 17845, + "rotation": 28.51 + }, + { + "id": "BP_WAT12_3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "6BD2D8B1-4FD81724-9CFE9DA9-062720E3", + "x": 121433, + "y": 192946, + "z": -5001, + "rotation": 45.47 + }, + { + "id": "BP_WAT13", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "2468B123-41524701-497C47BB-3080D880", + "x": -142453, + "y": -154272, + "z": -1740, + "rotation": 35.22 + }, + { + "id": "BP_WAT133", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "F3F880FB-45FA3C45-0D138788-B97442F2", + "x": -263369, + "y": -42598, + "z": 2115, + "rotation": -59.82 + }, + { + "id": "BP_WAT134", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "FF599678-47131879-F187BB82-E57D3A16", + "x": -98597, + "y": -72562, + "z": 7321, + "rotation": 74.23 + }, + { + "id": "BP_WAT137", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "32D12894-48A8A181-DB1583A5-E59230C7", + "x": -128379, + "y": 73567, + "z": 4950, + "rotation": 77.37 + }, + { + "id": "BP_WAT138", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "EC12B38E-4C6C530D-93DC8494-5BF34359", + "x": -112843, + "y": 200646, + "z": 6687, + "rotation": 91.71 + }, + { + "id": "BP_WAT13_17", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "8169E697-40FBC62D-3AD1A285-24457B7B", + "x": 127043, + "y": 81370, + "z": 17597, + "rotation": 18.66 + }, + { + "id": "BP_WAT13_3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "FCFE3558-44D0DE5C-149FDBA4-9707154B", + "x": -62127, + "y": 278743, + "z": -801, + "rotation": -104.6 + }, + { + "id": "BP_WAT13_5", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "15F8720B-4D9FDCBE-2AE6C9B3-60C377F9", + "x": -68107, + "y": 15570, + "z": 22881, + "rotation": -10.23 + }, + { + "id": "BP_WAT147", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "B2DEC466-4C39E90B-1902428D-8C3EAF3C", + "x": -228736, + "y": 48228, + "z": 14435, + "rotation": -90 + }, + { + "id": "BP_WAT14_18", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "47AB8F70-430C32DA-C03EBD84-00A1638E", + "x": 21814, + "y": 126438, + "z": 13876, + "rotation": -19.88 + }, + { + "id": "BP_WAT14_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E4D21EB8-4CDCEB30-3391C28D-C3A43435", + "x": -23497, + "y": -37127, + "z": 12691 + }, + { + "id": "BP_WAT14_26", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "DC64FC7F-458E58E9-45012D93-561D6B9A", + "x": 46216, + "y": 30079, + "z": 21985, + "rotation": 1.55 + }, + { + "id": "BP_WAT14_5", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "A31F31CC-456C54CD-49F1D1B3-0C6C1AD7", + "x": 135358, + "y": 263051, + "z": 5221, + "rotation": -89.52 + }, + { + "id": "BP_WAT14_UAID_04421A9713F0E56401_1503060069", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "56A4413F-41AD2F62-59DDFC9B-44F662F5", + "x": -46914, + "y": -20742, + "z": 18663, + "rotation": -114.35 + }, + { + "id": "BP_WAT15", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E24E2403-4511293D-B1C15984-A72A267E", + "x": -150619, + "y": -105222, + "z": -2841, + "rotation": 39.8 + }, + { + "id": "BP_WAT15_13", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "5C316FAB-454ABD47-FA3770A8-C0D6480D", + "x": -16097, + "y": 29430, + "z": 24678, + "rotation": 55.97 + }, + { + "id": "BP_WAT15_6", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "B3D40004-4F49CE7B-ED4716A0-F94D9FE1", + "x": 194968, + "y": 199950, + "z": -4852, + "rotation": 1.76 + }, + { + "id": "BP_WAT16", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "9C206948-451ED422-E71DF699-263EC9D2", + "x": -232489, + "y": -181844, + "z": 5385, + "rotation": 95.62 + }, + { + "id": "BP_WAT16_7", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "5DF6B210-44E63C24-A0199F8B-20458213", + "x": 204083, + "y": 195093, + "z": 17626, + "rotation": -10.8 + }, + { + "id": "BP_WAT17_30", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "3EDF9880-453114D7-99078894-7DCD89AC", + "x": -164657, + "y": 21770, + "z": 21901, + "rotation": 80.06 + }, + { + "id": "BP_WAT17_6", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4EAE6E6D-43D7E73D-EAC25CB3-B65B3E33", + "x": 314591, + "y": -59778, + "z": 5928, + "rotation": 0 + }, + { + "id": "BP_WAT17_7", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "BEA908F6-48D13213-EC8CA49D-AACBF8AD", + "x": 38425, + "y": 275787, + "z": 186, + "rotation": 9.25 + }, + { + "id": "BP_WAT17_8", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "A22A3BC2-4EE68608-69E42D8E-66211A04", + "x": 161888, + "y": 158080, + "z": 10189, + "rotation": 93.51 + }, + { + "id": "BP_WAT18", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E2CDD69B-457D26F1-47824C85-840E0556", + "x": -95237, + "y": 184738, + "z": 2788, + "rotation": 55.28 + }, + { + "id": "BP_WAT18_1", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "85F898D2-4DC5108A-2E98BA96-1A5D38D0", + "x": 331660, + "y": -222661, + "z": 4584, + "rotation": 163.6 + }, + { + "id": "BP_WAT19", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "2F3618FF-4015C8E5-3892B087-FC3EA468", + "x": -68181, + "y": 304185, + "z": -6019, + "rotation": -127.35 + }, + { + "id": "BP_WAT19_14", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "92EC1999-4E0A3603-153A0E8A-8D8F82B5", + "x": -9339, + "y": 192, + "z": 24137, + "rotation": 26.8 + }, + { + "id": "BP_WAT19_3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "0928CEE1-4C100C77-92DB27B2-756D5173", + "x": 391493, + "y": -241467, + "z": 5424, + "rotation": 90.91 + }, + { + "id": "BP_WAT19_39", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "489B9140-4487CA45-10A9F1A4-29A645C9", + "x": -106650, + "y": -1537, + "z": 24247, + "rotation": 56.87 + }, + { + "id": "BP_WAT1_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "902D2142-4DE00580-5AED6AA9-0F00E563", + "x": 182498, + "y": 107568, + "z": 12254, + "rotation": 79.7 + }, + { + "id": "BP_WAT1_C_0", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CB03692A-42223011-78D53E92-97177AB6", + "x": -341, + "y": -52405, + "z": 13702, + "rotation": -30.8 + }, + { + "id": "BP_WAT1_C_1", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "330DD209-46189555-E638EE94-5D1EB3D6", + "x": 87775, + "y": -186079, + "z": 10491, + "rotation": -40.73 + }, + { + "id": "BP_WAT1_C_10", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "18259C73-4295236C-9936FFBF-7B2E0890", + "x": 127614, + "y": -46788, + "z": 13046, + "rotation": 73.82 + }, + { + "id": "BP_WAT1_C_11", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "F49E6C18-4B838D67-F4BD729A-F12E7763", + "x": 112145, + "y": -211342, + "z": 1613, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_12", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D35D5B65-40856AE6-99AF258E-7956B49D", + "x": 137233, + "y": -156060, + "z": 16118, + "rotation": 61.32 + }, + { + "id": "BP_WAT1_C_15", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "1FDFEC32-437A963B-0DB6A680-9AB41C6F", + "x": 232575, + "y": 135846, + "z": -11331, + "rotation": 0.28 + }, + { + "id": "BP_WAT1_C_16", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "47091C17-45986EEC-5C628696-08C8B7A1", + "x": 273061, + "y": 81579, + "z": -1571 + }, + { + "id": "BP_WAT1_C_18", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "C810B1CC-4AA4AA47-4EEC27BA-C964BC15", + "x": 218362, + "y": 53799, + "z": -1362, + "rotation": -89.63 + }, + { + "id": "BP_WAT1_C_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "545CEFE4-435E8384-0571A8B0-21561274", + "x": -28430, + "y": -226425, + "z": -1718, + "rotation": -147.92 + }, + { + "id": "BP_WAT1_C_20", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D00A17D1-4931FB62-B9409DB1-690411B2", + "x": 202735, + "y": -161515, + "z": 30523 + }, + { + "id": "BP_WAT1_C_21", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "054345BC-4BD7074F-5D8502B6-5CC1E6B5", + "x": 309568, + "y": -8915, + "z": 5573, + "rotation": 73.96 + }, + { + "id": "BP_WAT1_C_3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "DF93C7ED-40BE801B-04B179A1-CDD580CF", + "x": -62622, + "y": -243573, + "z": 1998, + "rotation": -0.5 + }, + { + "id": "BP_WAT1_C_4", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "3AC1CD66-476A9043-09D24882-58043143", + "x": 16985, + "y": -65299, + "z": 10249, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_5", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "BE9469AF-4386F7BA-CD4831B1-D65E979D", + "x": 78249, + "y": -45687, + "z": 6509, + "rotation": -0.27 + }, + { + "id": "BP_WAT1_C_6", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "C2EAC46D-4176134D-0D0D0180-D2BC99F2", + "x": 44936, + "y": -12095, + "z": 23273, + "rotation": 56.41 + }, + { + "id": "BP_WAT1_C_7", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D89F914E-4F4B2FB9-F83C70A9-D656944D", + "x": 94021, + "y": -55894, + "z": 16329, + "rotation": 65 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F022A401_1406932331", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "20484450-4F286935-5C8EB6AB-1C4A66B0", + "x": -139509, + "y": 139430, + "z": 15453, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F023A401_1359406508", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D2CE0771-4F9E626C-4DC20085-268A7217", + "x": -168310, + "y": 63731, + "z": 11053, + "rotation": 66.06 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F024A401_1924847685", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "B6A26AAD-4F180875-35E88791-334D13B7", + "x": -154838, + "y": 91683, + "z": 21293, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0416401_2093804204", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "1227C096-46D6FAB7-B90AA5A2-F6E0E314", + "x": 212100, + "y": 111435, + "z": -5163, + "rotation": -46.7 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0565301_1886270961", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "31EE7313-4D2FADFA-5741F4B5-803017C0", + "x": 171062, + "y": -266773, + "z": 663, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F05F6901_1130070767", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E32928DE-40BDBD0B-BC6BCFB4-D9BC6D97", + "x": -233103, + "y": 102345, + "z": -14 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0B36201_2077013154", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4EED500F-49054889-E2886D8C-2BEAF08F", + "x": 370156, + "y": -211385, + "z": 5163, + "rotation": -80.82 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0C46101_1282102099", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D16FD7DB-4DFA573E-50696F81-F21E2EA3", + "x": -158423, + "y": -55812, + "z": 954 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0E57A01_1732622300", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CE519549-48E4E2BF-7BAE84AC-ED759619", + "x": 143591, + "y": 20011, + "z": 14861, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0E87A01_1449703843", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E2836681-4B263DEB-7ACC3A98-A7BBDF01", + "x": 63364, + "y": 29487, + "z": 22998, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0EE7A01_1306830904", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "6FAF4393-46912620-5FE464B1-1F5F99DF", + "x": 96387, + "y": 30292, + "z": 18324, + "rotation": 0 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0F27A01_1137768619", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "A8EA7BC8-46158EB6-0919D084-E35176BC", + "x": 189071, + "y": 7849, + "z": 19716, + "rotation": -42.19 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0F37A01_1079321804", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "56907DAE-4A4A34FB-5E96B285-6FD36943", + "x": 138397, + "y": 61287, + "z": 20000, + "rotation": 111.55 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0F57A01_1575931164", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "AD3AC3BD-4990A19B-DB3B5DB2-364C9331", + "x": 147900, + "y": 66475, + "z": -212, + "rotation": -78.73 + }, + { + "id": "BP_WAT1_C_UAID_04421A9713F0F57A01_1942877174", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D650453D-4C9BFCC0-246094AA-B094DB28", + "x": 238635, + "y": -21507, + "z": -195 + }, + { + "id": "BP_WAT24", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "E39BF4DC-490C9962-ACF9DCB8-C108A7E4", + "x": -230182, + "y": -113829, + "z": -661 + }, + { + "id": "BP_WAT25_382", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4947A6F7-4B16D856-6922ADAA-D3517188", + "x": -57955, + "y": 128068, + "z": 5070, + "rotation": -105.98 + }, + { + "id": "BP_WAT2_C_22", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "23074F9C-480D738D-106C2AAE-EEAC1568", + "x": 249320, + "y": 148540, + "z": 14872 + }, + { + "id": "BP_WAT2_C_24", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D6422CAE-46B96633-DCB9FA8D-2AF878BC", + "x": 216782, + "y": 145682, + "z": 7984, + "rotation": 0 + }, + { + "id": "BP_WAT2_C_UAID_04421A9713F0F27A01_1759969627", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CC4BFA59-43E17765-E9EBB3B2-994CBBB5", + "x": 148750, + "y": 37026, + "z": 10848 + }, + { + "id": "BP_WAT3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "B8F59ED7-4D5C0E0A-3A7397A0-2E33B10D", + "x": 329081, + "y": -12866, + "z": -1106, + "rotation": -142.27 + }, + { + "id": "BP_WAT35_7", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "63125CC8-4F723AF5-66CE5695-4A21D0FD", + "x": 194099, + "y": -81819, + "z": 1416, + "rotation": -50.23 + }, + { + "id": "BP_WAT37_13", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "3D4FB516-4CB130A0-619313A8-B6BC2AB7", + "x": 151391, + "y": -73482, + "z": 11315, + "rotation": 13.7 + }, + { + "id": "BP_WAT3_5", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "20EEA868-4027AAAA-3220A9BC-E8D0CCD0", + "x": 85523, + "y": -131861, + "z": 14977, + "rotation": -60.04 + }, + { + "id": "BP_WAT41_6362", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "F4D645F2-41E4DEB6-81B08492-40B08543", + "x": 159441, + "y": -10152, + "z": 20170, + "rotation": 109.61 + }, + { + "id": "BP_WAT45", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "A15E4678-4FF23C8E-4ED32EA2-6F62D4D5", + "x": 405743, + "y": -161308, + "z": 5525, + "rotation": -164.85 + }, + { + "id": "BP_WAT49", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "C2D67F16-4BB639C5-4A4CA3A7-BB6A0360", + "x": 378307, + "y": -120583, + "z": 7258, + "rotation": 130.46 + }, + { + "id": "BP_WAT4_8", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "7251C400-457D3F87-043DE6A1-D255B17F", + "x": 28422, + "y": -130004, + "z": 16209, + "rotation": 87.87 + }, + { + "id": "BP_WAT52", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CDF0B9A5-4125D9E5-9B843FB4-7755C527", + "x": 264968, + "y": -259565, + "z": 17590, + "rotation": 141.82 + }, + { + "id": "BP_WAT55", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CCD6135A-4FF6DF1B-5CA10683-20988229", + "x": 280621, + "y": -308275, + "z": 3041, + "rotation": 91.51 + }, + { + "id": "BP_WAT57", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "5C3C9D54-45754A99-7C797C82-11635601", + "x": 261873, + "y": -194228, + "z": 3964, + "rotation": 26.16 + }, + { + "id": "BP_WAT58", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "A41EB577-4573D4AE-C653D196-A933B67D", + "x": 295658, + "y": -237690, + "z": 5420, + "rotation": 6.59 + }, + { + "id": "BP_WAT5_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "4021A884-42BE3139-8C855089-F3CA2FAF", + "x": -78715, + "y": -190868, + "z": 17541, + "rotation": -40.8 + }, + { + "id": "BP_WAT62", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "552A64FF-4570E098-9CB40C8C-2BC32DDC", + "x": 311247, + "y": -89881, + "z": 8315, + "rotation": 40.91 + }, + { + "id": "BP_WAT64", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "9CF0890A-46225D65-0CEFFEAC-8B7140A6", + "x": 242952, + "y": -257677, + "z": -324, + "rotation": 76.93 + }, + { + "id": "BP_WAT66", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "281ED3C4-4C5615B4-C9537D80-0E204D7E", + "x": 293393, + "y": -164435, + "z": 1423, + "rotation": 179.41 + }, + { + "id": "BP_WAT70", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "CA2B8F38-42D64E75-9FB87790-F168A3AC", + "x": 213504, + "y": -45384, + "z": 15881, + "rotation": 74.93 + }, + { + "id": "BP_WAT74", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "BE565293-460CA7A4-90CC1F88-4922A8DB", + "x": 264716, + "y": -288364, + "z": 22833, + "rotation": 5.17 + }, + { + "id": "BP_WAT78_0", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "28D065C4-4D9BDEFC-E14919A1-BEB56F0D", + "x": 211149, + "y": -259597, + "z": -1601, + "rotation": 59.59 + }, + { + "id": "BP_WAT7_3", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "BECF8178-4644A998-694B5A84-AA3DC378", + "x": -75147, + "y": -112863, + "z": 6097, + "rotation": 156.95 + }, + { + "id": "BP_WAT80_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "91DFB1E5-4761AA33-9B86588C-C6D0AA0C", + "x": 190045, + "y": -249792, + "z": 13565, + "rotation": -101.2 + }, + { + "id": "BP_WAT83_2", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "0BE1D42F-4377CCFC-E0335BAC-152E7CF5", + "x": 223238, + "y": -290110, + "z": 15266, + "rotation": -0.09 + }, + { + "id": "BP_WAT86", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "D3974E8D-44FDAFB0-EF439189-2AC942B9", + "x": 195850, + "y": -168882, + "z": 15333, + "rotation": -115.39 + }, + { + "id": "BP_WAT92", + "type": "somersloop", + "classPath": "BP_WAT1_C", + "pickupGuid": "6DE13C96-4EF118F8-D1D75DBC-918C9178", + "x": 60541, + "y": 231749, + "z": -3434, + "rotation": 66.77 + } +] diff --git a/src/recipes/WorldCollectibles.ts b/src/recipes/WorldCollectibles.ts new file mode 100644 index 00000000..3d9c4cc2 --- /dev/null +++ b/src/recipes/WorldCollectibles.ts @@ -0,0 +1,263 @@ +import RawWorldCollectibles from './WorldCollectibles.json'; + +/** + * Pickup-style entities scattered around the world, separate from + * extractable resource nodes. Each type is a distinct collectible + * "track" the player completes once per game (the slugs/sloops/spheres + * stack across the run, but the location itself is one-and-done). + * + * The keys here are stable storage tokens — they're persisted on each + * `Game.collectedItems` list, so don't rename without a migration. + */ +export type CollectibleType = + | 'slugMk1' + | 'slugMk2' + | 'slugMk3' + | 'somersloop' + | 'mercerSphere' + | 'hardDrive' + | 'audioTape' + | 'customizationUnlock'; + +export const COLLECTIBLE_TYPES: CollectibleType[] = [ + 'slugMk1', + 'slugMk2', + 'slugMk3', + 'somersloop', + 'mercerSphere', + 'hardDrive', + 'audioTape', + 'customizationUnlock', +]; + +/** + * Display + visual metadata for each collectible type, used by the + * filter panel rows, marker icons, and popover headers. `iconImagePath` + * points at a bundled `/images/game/*` asset (preserves the resource- + * marker visual language); `iconName` falls back to a Tabler icon for + * collectibles whose game art isn't bundled (drop pods, audio tapes). + */ +export interface CollectibleTypeMeta { + type: CollectibleType; + displayName: string; + shortName: string; + /** + * Path to a bundled image asset (e.g. + * `/images/game/power-slug-green_64.png`). When unset, callers fall + * back to `iconName` and render a Tabler icon instead. + */ + iconImagePath?: string; + /** + * Tabler icon name (e.g. `IconPackage`). Used only when the + * collectible has no bundled game art. + */ + iconName?: string; + /** + * Ring/badge color used for the marker outline and the filter row's + * count chip. CSS color string. Picks lean toward the in-game color + * of the item (slugs especially). + */ + color: string; + /** One-line player-facing description shown in the popover. */ + description: string; +} + +export const COLLECTIBLE_TYPE_META: Record< + CollectibleType, + CollectibleTypeMeta +> = { + slugMk1: { + type: 'slugMk1', + displayName: 'Blue Power Slug', + shortName: 'Mk1 Slug', + iconImagePath: '/images/game/power-slug-green_64.png', + color: '#3498db', + description: 'Refines into 1 Power Shard at a Constructor.', + }, + slugMk2: { + type: 'slugMk2', + displayName: 'Yellow Power Slug', + shortName: 'Mk2 Slug', + iconImagePath: '/images/game/power-slug-yellow_64.png', + color: '#f1c40f', + description: 'Refines into 2 Power Shards at a Constructor.', + }, + slugMk3: { + type: 'slugMk3', + displayName: 'Purple Power Slug', + shortName: 'Mk3 Slug', + iconImagePath: '/images/game/power-slug-purple_64.png', + color: '#9b59b6', + description: 'Refines into 5 Power Shards at a Constructor.', + }, + somersloop: { + type: 'somersloop', + displayName: 'Somersloop', + shortName: 'Sloop', + iconImagePath: '/images/game/wat-1_64.png', + color: '#e91e63', + description: 'Doubles output of any Manufacturer when slotted in.', + }, + mercerSphere: { + type: 'mercerSphere', + displayName: 'Mercer Sphere', + shortName: 'Sphere', + iconImagePath: '/images/game/wat-2_64.png', + color: '#1abc9c', + description: 'Powers the Dimensional Depot at a Mercer Sphere shrine.', + }, + hardDrive: { + type: 'hardDrive', + displayName: 'Hard Drive (Drop Pod)', + shortName: 'HD', + iconName: 'IconPackage', + color: '#e67e22', + description: + 'Crashed drop pod. Open with the listed cost to claim a hard drive for the MAM.', + }, + audioTape: { + type: 'audioTape', + displayName: 'Audio Tape', + shortName: 'Tape', + iconName: 'IconDeviceAudioTape', + color: '#95a5a6', + description: 'Adds a song to your boombox playlist.', + }, + customizationUnlock: { + type: 'customizationUnlock', + displayName: 'Customization Unlock', + shortName: 'Cosmetic', + iconName: 'IconBrush', + color: '#bdc3c7', + description: 'Unlocks a cosmetic / paint job.', + }, +}; + +/** + * Item cost the player has to pay to open a drop pod. Mirrors UE's + * `EFGDropPodUnlockCostType::Item` — power costs aren't represented + * yet (they don't appear in vanilla 1.0 drop pod definitions). + */ +export interface DropPodUnlockCost { + /** Item id matching `AllFactoryItemsMap`, e.g. `Desc_ModularFrame_C`. */ + item: string; + /** Stack amount required to consume. */ + amount: number; +} + +export interface WorldCollectible { + /** + * Satisfactory actor id — the exact string the game uses internally + * for this pickup (e.g. `BP_Crystal_C_UAID_…`). Persists across + * runs of the parser as long as the game's actor stays put, and is + * the key for "collected" marks. + */ + id: string; + /** Discriminator for the marker icon, popover layout, and filters. */ + type: CollectibleType; + /** + * Full blueprint class path of the spawning actor (e.g. + * `BP_Crystal_C`). Preserved so a future savegame parser can + * cross-reference these without translating the id format. + */ + classPath?: string; + /** + * Stable cross-run identity for the pickup, when present. UE's + * `mItemPickupGuid` stays constant across patches, so the savegame + * parser can use it to resolve already-collected pickups even when + * the actor `Name` changes between game versions. + */ + pickupGuid?: string; + /** + * Game-world coordinates in centimeters (Unreal default unit). Same + * convention as `WorldResourceNode`. + */ + x: number; + y: number; + z?: number; + /** Yaw rotation in degrees, when reported by the source dataset. */ + rotation?: number; + /** + * Drop-pod-only: the item cost to open the pod. Empty for + * collectibles that don't use this mechanic. + */ + unlockCost?: DropPodUnlockCost[]; + /** + * Audio-tape-only: the schematic id (e.g. `Schematic_Huntdown_C`) + * the tape unlocks. Surface in the popover so the player can + * identify the song without picking it up. + */ + schematicId?: string; + /** + * Where the data point came from. Mirrors `WorldResourceNode.source` + * to leave room for a future savegame-derived overlay. + */ + source: 'static' | 'savegame'; +} + +interface RawCollectible { + id: string; + type: CollectibleType; + classPath?: string; + pickupGuid?: string; + x: number; + y: number; + z?: number; + rotation?: number; + unlockCost?: DropPodUnlockCost[]; + schematicId?: string; +} + +const StaticWorldCollectibles: WorldCollectible[] = ( + RawWorldCollectibles as RawCollectible[] +).map(c => ({ ...c, source: 'static' as const })); + +/** + * Per-game savegame-derived overrides. Returns an empty list today; + * the same shape as {@link getSavegameOverridesForCollectibles}'s + * eventual savegame parser. Kept stable so consumers don't change + * shape when that lands. + */ +export function getSavegameCollectibleOverrides( + _gameId?: string | null, +): WorldCollectible[] { + return []; +} + +/** + * Returns every collectible to render on the map for the given game. + * Falls back to the bundled static dataset; per-game savegame-derived + * collectibles (when present) supersede static entries with matching + * ids. + */ +export function getWorldCollectibles( + gameId?: string | null, +): WorldCollectible[] { + const overrides = getSavegameCollectibleOverrides(gameId); + if (overrides.length === 0) return StaticWorldCollectibles; + const overrideIds = new Set(overrides.map(c => c.id)); + return [ + ...StaticWorldCollectibles.filter(c => !overrideIds.has(c.id)), + ...overrides, + ]; +} + +/** + * Total counts per type across the static dataset, for the filter + * panel's "X of Y collected" rendering. Computed once at module load. + */ +export const COLLECTIBLE_TOTALS_BY_TYPE: Record = + (() => { + const totals: Record = { + slugMk1: 0, + slugMk2: 0, + slugMk3: 0, + somersloop: 0, + mercerSphere: 0, + hardDrive: 0, + audioTape: 0, + customizationUnlock: 0, + }; + for (const c of StaticWorldCollectibles) totals[c.type] += 1; + return totals; + })(); diff --git a/src/recipes/WorldResourceNodes.json b/src/recipes/WorldResourceNodes.json new file mode 100644 index 00000000..55c4dd2b --- /dev/null +++ b/src/recipes/WorldResourceNodes.json @@ -0,0 +1,7423 @@ +[ + { + "id": "BP_ResourceDeposit1642", + "resource": "Desc_OreUranium_C", + "purity": "normal", + "classPath": "BP_ResourceDeposit_C", + "nodeType": "deposit", + "displayName": "Uranium", + "x": 159055, + "y": -126925, + "z": 9480, + "rotation": 303.63 + }, + { + "id": "BP_FrackingCore6_UAID_40B076DF2F79D3DF01_1961476789", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Crude Oil", + "x": -261572, + "y": 74705, + "z": -1345, + "rotation": -25.29 + }, + { + "id": "BP_FrackingCore7", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Crude Oil", + "x": 268156, + "y": 112798, + "z": -173, + "rotation": -26.01 + }, + { + "id": "BP_FrackingCore9", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Crude Oil", + "x": -40180, + "y": -7912, + "z": 24981, + "rotation": -63.47 + }, + { + "id": "BP_FrackingCore2", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": 215924, + "y": 135918, + "z": -438, + "rotation": 20.83 + }, + { + "id": "BP_FrackingCore3", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": 223618, + "y": -311503, + "z": 7674, + "rotation": -55.39 + }, + { + "id": "BP_FrackingCore4", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": -251395, + "y": -187071, + "z": -1668, + "rotation": -25.2 + }, + { + "id": "BP_FrackingCore5", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": -143896, + "y": 122197, + "z": 15134, + "rotation": -25.13 + }, + { + "id": "BP_FrackingCore6", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": 84979, + "y": 104525, + "z": 10068, + "rotation": -25.29 + }, + { + "id": "BP_FrackingCore_8", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Nitrogen Gas", + "x": 128310, + "y": 256759, + "z": 269, + "rotation": 26.86 + }, + { + "id": "BP_FrackingCore10", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 316663, + "y": -290557, + "z": 1067, + "rotation": -195.91 + }, + { + "id": "BP_FrackingCore11", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 185710, + "y": -57263, + "z": 5396, + "rotation": -227 + }, + { + "id": "BP_FrackingCore12", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 240513, + "y": -69723, + "z": 5520, + "rotation": 132.97 + }, + { + "id": "BP_FrackingCore13", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 99210, + "y": 83713, + "z": 10073, + "rotation": -194.51 + }, + { + "id": "BP_FrackingCore14", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 104339, + "y": 145803, + "z": 5428, + "rotation": -194.39 + }, + { + "id": "BP_FrackingCore15", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": 6932, + "y": 187185, + "z": -10649, + "rotation": -194.43 + }, + { + "id": "BP_FrackingCore17", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": -19414, + "y": 173196, + "z": -198, + "rotation": -194.31 + }, + { + "id": "BP_FrackingCore18", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingCore_C", + "nodeType": "frackingCore", + "displayName": "Water", + "x": -108264, + "y": 73536, + "z": 3002, + "rotation": -194.25 + }, + { + "id": "BP_FrackingSatellite45", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 267123, + "y": 115156, + "z": -101, + "rotation": 54.33 + }, + { + "id": "BP_FrackingSatellite46", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 264052, + "y": 112281, + "z": -66, + "rotation": -26.05 + }, + { + "id": "BP_FrackingSatellite47", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 264231, + "y": 114518, + "z": -78, + "rotation": -1.17 + }, + { + "id": "BP_FrackingSatellite48", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 271425, + "y": 111504, + "z": -103, + "rotation": -24.61 + }, + { + "id": "BP_FrackingSatellite49", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 269500, + "y": 115588, + "z": -83, + "rotation": -25.42 + }, + { + "id": "BP_FrackingSatellite50", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": 267439, + "y": 110349, + "z": -91, + "rotation": -26.06 + }, + { + "id": "BP_FrackingSatellite57", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -40200, + "y": -4304, + "z": 25054, + "rotation": 16.67 + }, + { + "id": "BP_FrackingSatellite59", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -43759, + "y": -8409, + "z": 25139, + "rotation": -63.95 + }, + { + "id": "BP_FrackingSatellite60", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -41776, + "y": -9868, + "z": 25142, + "rotation": -208.54 + }, + { + "id": "BP_FrackingSatellite61", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -38957, + "y": -10363, + "z": 25325, + "rotation": -62.21 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D7DF01_1933830510", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -260626, + "y": 72573, + "z": -1188, + "rotation": -62.21 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D7DF01_2053713511", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -261941, + "y": 70475, + "z": -1159, + "rotation": -62.21 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D8DF01_1587984689", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -264373, + "y": 76059, + "z": -1297, + "rotation": -91.96 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D8DF01_1704280690", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -266195, + "y": 75227, + "z": -1280, + "rotation": -92.06 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D8DF01_1999134691", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -263342, + "y": 72360, + "z": -1354, + "rotation": -102.05 + }, + { + "id": "BP_FrackingSatellite61_UAID_40B076DF2F79D9DF01_1230935868", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -265378, + "y": 72493, + "z": -1342, + "rotation": -102.07 + }, + { + "id": "BP_FrackingSatellite62", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -36959, + "y": -7834, + "z": 25170, + "rotation": -62.99 + }, + { + "id": "BP_FrackingSatellite63", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Crude Oil", + "x": -43239, + "y": -6519, + "z": 25054, + "rotation": -63.16 + }, + { + "id": "BP_FrackingSatellite10", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 212492, + "y": 138087, + "z": -126, + "rotation": 65.91 + }, + { + "id": "BP_FrackingSatellite11", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 216407, + "y": 139500, + "z": -212 + }, + { + "id": "BP_FrackingSatellite12", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 218169, + "y": 137549, + "z": -209 + }, + { + "id": "BP_FrackingSatellite13", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 211686, + "y": 133161, + "z": -247, + "rotation": 20.7 + }, + { + "id": "BP_FrackingSatellite14", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 225242, + "y": -309813, + "z": 7876, + "rotation": 25.16 + }, + { + "id": "BP_FrackingSatellite15", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 221785, + "y": -308346, + "z": 8244, + "rotation": -55.16 + }, + { + "id": "BP_FrackingSatellite16", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 220215, + "y": -310721, + "z": 7880, + "rotation": -53.7 + }, + { + "id": "BP_FrackingSatellite17", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 226488, + "y": -313624, + "z": 8161, + "rotation": -54.1 + }, + { + "id": "BP_FrackingSatellite18_3", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 126644, + "y": 253042, + "z": -372, + "rotation": 0 + }, + { + "id": "BP_FrackingSatellite19", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 221264, + "y": -312257, + "z": 7932, + "rotation": -53.79 + }, + { + "id": "BP_FrackingSatellite2", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 125104, + "y": 256475, + "z": 734, + "rotation": -25.91 + }, + { + "id": "BP_FrackingSatellite20", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 223981, + "y": -314009, + "z": 7867, + "rotation": -53.79 + }, + { + "id": "BP_FrackingSatellite21", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -253679, + "y": -187152, + "z": -1241, + "rotation": 83.99 + }, + { + "id": "BP_FrackingSatellite22", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -254692, + "y": -191073, + "z": -1582, + "rotation": -52.1 + }, + { + "id": "BP_FrackingSatellite23_5", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 226852, + "y": -311423, + "z": 7906, + "rotation": -53.79 + }, + { + "id": "BP_FrackingSatellite24", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -251958, + "y": -191566, + "z": -1741, + "rotation": -23.88 + }, + { + "id": "BP_FrackingSatellite25", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -249899, + "y": -190381, + "z": -1740, + "rotation": -24.37 + }, + { + "id": "BP_FrackingSatellite26", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -249449, + "y": -188216, + "z": -1740, + "rotation": -25.32 + }, + { + "id": "BP_FrackingSatellite27", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -250937, + "y": -184679, + "z": -1457, + "rotation": 85.23 + }, + { + "id": "BP_FrackingSatellite28", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -147629, + "y": 125723, + "z": 15316, + "rotation": 54.91 + }, + { + "id": "BP_FrackingSatellite29", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -144342, + "y": 124610, + "z": 15388, + "rotation": -58.19 + }, + { + "id": "BP_FrackingSatellite3", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 125099, + "y": 254321, + "z": 160, + "rotation": -0.61 + }, + { + "id": "BP_FrackingSatellite30", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -145715, + "y": 119442, + "z": 15182, + "rotation": -23.71 + }, + { + "id": "BP_FrackingSatellite31", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -142779, + "y": 120243, + "z": 15175, + "rotation": -64.85 + }, + { + "id": "BP_FrackingSatellite32", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -141750, + "y": 121998, + "z": 15259, + "rotation": -24.37 + }, + { + "id": "BP_FrackingSatellite33", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -142911, + "y": 118402, + "z": 14995, + "rotation": -25.03 + }, + { + "id": "BP_FrackingSatellite34", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -145565, + "y": 126028, + "z": 15439, + "rotation": -13.84 + }, + { + "id": "BP_FrackingSatellite35", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 87917, + "y": 102045, + "z": 10178, + "rotation": 82.23 + }, + { + "id": "BP_FrackingSatellite36", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 81639, + "y": 103495, + "z": 10184, + "rotation": -77.58 + }, + { + "id": "BP_FrackingSatellite37", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 88734, + "y": 107723, + "z": 9992, + "rotation": 5.34 + }, + { + "id": "BP_FrackingSatellite38", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 83094, + "y": 101821, + "z": 10216, + "rotation": -5.2 + }, + { + "id": "BP_FrackingSatellite39", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 87991, + "y": 105052, + "z": 10209, + "rotation": -24.64 + }, + { + "id": "BP_FrackingSatellite4", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 130516, + "y": 255126, + "z": -294, + "rotation": 0 + }, + { + "id": "BP_FrackingSatellite40", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 86697, + "y": 99524, + "z": 10058, + "rotation": -58.3 + }, + { + "id": "BP_FrackingSatellite41", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 83988, + "y": 107803, + "z": 10104, + "rotation": 55.92 + }, + { + "id": "BP_FrackingSatellite42", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 84661, + "y": 99203, + "z": 10050, + "rotation": 70.42 + }, + { + "id": "BP_FrackingSatellite43", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 81664, + "y": 106668, + "z": 10241, + "rotation": 78.33 + }, + { + "id": "BP_FrackingSatellite44", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 86028, + "y": 109451, + "z": 9962, + "rotation": -23.95 + }, + { + "id": "BP_FrackingSatellite5", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 131025, + "y": 257428, + "z": 238, + "rotation": 0 + }, + { + "id": "BP_FrackingSatellite51_1", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": -253969, + "y": -189214, + "z": -1460, + "rotation": 54.19 + }, + { + "id": "BP_FrackingSatellite6", + "resource": "Desc_NitrogenGas_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 214660, + "y": 137994, + "z": -225, + "rotation": 21.05 + }, + { + "id": "BP_FrackingSatellite7", + "resource": "Desc_NitrogenGas_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 213449, + "y": 132707, + "z": -317, + "rotation": 20.54 + }, + { + "id": "BP_FrackingSatellite8", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 215726, + "y": 132791, + "z": -285, + "rotation": 21.96 + }, + { + "id": "BP_FrackingSatellite9", + "resource": "Desc_NitrogenGas_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 218041, + "y": 134223, + "z": -188 + }, + { + "id": "BP_FrackingSatellite_2", + "resource": "Desc_NitrogenGas_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Nitrogen Gas", + "x": 128216, + "y": 258878, + "z": 890, + "rotation": 116.41 + }, + { + "id": "BP_FrackingSatellite100", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 9269, + "y": 190202, + "z": -10441, + "rotation": -193.96 + }, + { + "id": "BP_FrackingSatellite101", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 3583, + "y": 187082, + "z": -10305, + "rotation": -195.52 + }, + { + "id": "BP_FrackingSatellite102", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 185694, + "y": -61072, + "z": 5432, + "rotation": -225.93 + }, + { + "id": "BP_FrackingSatellite103_8", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 314408, + "y": -292942, + "z": 1253, + "rotation": -195.84 + }, + { + "id": "BP_FrackingSatellite108", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -22451, + "y": 171714, + "z": -28, + "rotation": -113.32 + }, + { + "id": "BP_FrackingSatellite109", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -17701, + "y": 174877, + "z": -71, + "rotation": -191.9 + }, + { + "id": "BP_FrackingSatellite110", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -16087, + "y": 170351, + "z": 351, + "rotation": -195.29 + }, + { + "id": "BP_FrackingSatellite111", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -15612, + "y": 172710, + "z": 239, + "rotation": -142.93 + }, + { + "id": "BP_FrackingSatellite112", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -23407, + "y": 174254, + "z": -438, + "rotation": -203.79 + }, + { + "id": "BP_FrackingSatellite113", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -20451, + "y": 175654, + "z": -100, + "rotation": -194 + }, + { + "id": "BP_FrackingSatellite114_7", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -111801, + "y": 73189, + "z": 3120, + "rotation": -113.31 + }, + { + "id": "BP_FrackingSatellite115", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -111815, + "y": 71121, + "z": 3062, + "rotation": -217.62 + }, + { + "id": "BP_FrackingSatellite116", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -107607, + "y": 71069, + "z": 2912, + "rotation": -195.23 + }, + { + "id": "BP_FrackingSatellite117", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -104692, + "y": 73356, + "z": 3096, + "rotation": -192.79 + }, + { + "id": "BP_FrackingSatellite118", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -110139, + "y": 75375, + "z": 3196, + "rotation": -328.34 + }, + { + "id": "BP_FrackingSatellite119", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -104201, + "y": 70977, + "z": 2859, + "rotation": -193.4 + }, + { + "id": "BP_FrackingSatellite120", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -108478, + "y": 77600, + "z": 3709, + "rotation": -193.49 + }, + { + "id": "BP_FrackingSatellite121", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": -20347, + "y": 170787, + "z": 7, + "rotation": -113.32 + }, + { + "id": "BP_FrackingSatellite58", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 313891, + "y": -287979, + "z": 1212, + "rotation": -114.86 + }, + { + "id": "BP_FrackingSatellite64", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 314278, + "y": -289756, + "z": 1268, + "rotation": -193.67 + }, + { + "id": "BP_FrackingSatellite65", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 319127, + "y": -290063, + "z": 1228, + "rotation": -173.12 + }, + { + "id": "BP_FrackingSatellite66", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 320535, + "y": -291280, + "z": 1230, + "rotation": -225.18 + }, + { + "id": "BP_FrackingSatellite67", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 316513, + "y": -293372, + "z": 1243, + "rotation": -162.52 + }, + { + "id": "BP_FrackingSatellite68", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 316668, + "y": -288090, + "z": 1174, + "rotation": -166.89 + }, + { + "id": "BP_FrackingSatellite69", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 188863, + "y": -57590, + "z": 5566, + "rotation": -146 + }, + { + "id": "BP_FrackingSatellite70", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 183114, + "y": -55533, + "z": 5512, + "rotation": -224.8 + }, + { + "id": "BP_FrackingSatellite71", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 182806, + "y": -58148, + "z": 5536, + "rotation": -226.23 + }, + { + "id": "BP_FrackingSatellite72", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 186318, + "y": -52986, + "z": 5426, + "rotation": -225.06 + }, + { + "id": "BP_FrackingSatellite73", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 183610, + "y": -59944, + "z": 5586, + "rotation": -211.87 + }, + { + "id": "BP_FrackingSatellite74", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 188079, + "y": -54424, + "z": 5519, + "rotation": -226.65 + }, + { + "id": "BP_FrackingSatellite75", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 240892, + "y": -66619, + "z": 6183, + "rotation": -146.31 + }, + { + "id": "BP_FrackingSatellite76", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 238332, + "y": -69283, + "z": 5403, + "rotation": -194.96 + }, + { + "id": "BP_FrackingSatellite77", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 242802, + "y": -71310, + "z": 5419, + "rotation": -200.75 + }, + { + "id": "BP_FrackingSatellite78", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 239763, + "y": -71695, + "z": 5257, + "rotation": -194.38 + }, + { + "id": "BP_FrackingSatellite79", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 245034, + "y": -71846, + "z": 4617, + "rotation": 160.41 + }, + { + "id": "BP_FrackingSatellite80", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 240788, + "y": -74885, + "z": 4802, + "rotation": -226.61 + }, + { + "id": "BP_FrackingSatellite81", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 95982, + "y": 85228, + "z": 10170, + "rotation": -113.3 + }, + { + "id": "BP_FrackingSatellite82", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 101159, + "y": 85561, + "z": 10250, + "rotation": -192.06 + }, + { + "id": "BP_FrackingSatellite83", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 98701, + "y": 86232, + "z": 10246, + "rotation": -193.17 + }, + { + "id": "BP_FrackingSatellite84", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 97653, + "y": 82024, + "z": 10255, + "rotation": -193.03 + }, + { + "id": "BP_FrackingSatellite85", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 100209, + "y": 81508, + "z": 10271, + "rotation": -194.19 + }, + { + "id": "BP_FrackingSatellite86", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 102319, + "y": 82976, + "z": 10270, + "rotation": -220.19 + }, + { + "id": "BP_FrackingSatellite87", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 99744, + "y": 147303, + "z": 6441, + "rotation": -113.19 + }, + { + "id": "BP_FrackingSatellite88", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 106355, + "y": 139679, + "z": 6603, + "rotation": -170.27 + }, + { + "id": "BP_FrackingSatellite89", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 108081, + "y": 142057, + "z": 6632, + "rotation": -139.74 + }, + { + "id": "BP_FrackingSatellite90", + "resource": "Desc_Water_C", + "purity": "impure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 108262, + "y": 145418, + "z": 6486, + "rotation": -192.89 + }, + { + "id": "BP_FrackingSatellite91", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 103180, + "y": 141525, + "z": 6429, + "rotation": -295.3 + }, + { + "id": "BP_FrackingSatellite92", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 104641, + "y": 149305, + "z": 6456, + "rotation": -193.96 + }, + { + "id": "BP_FrackingSatellite93", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 100830, + "y": 143446, + "z": 6461, + "rotation": -195.48 + }, + { + "id": "BP_FrackingSatellite94", + "resource": "Desc_Water_C", + "purity": "normal", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 100717, + "y": 149704, + "z": 6494, + "rotation": -195.33 + }, + { + "id": "BP_FrackingSatellite95", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 4934, + "y": 184744, + "z": -10425, + "rotation": -113.3 + }, + { + "id": "BP_FrackingSatellite96", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 7164, + "y": 183858, + "z": -10525, + "rotation": -191.97 + }, + { + "id": "BP_FrackingSatellite97", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 5973, + "y": 189394, + "z": -10358, + "rotation": -226.21 + }, + { + "id": "BP_FrackingSatellite98", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 9113, + "y": 184870, + "z": -10506, + "rotation": -192.89 + }, + { + "id": "BP_FrackingSatellite99", + "resource": "Desc_Water_C", + "purity": "pure", + "classPath": "BP_FrackingSatellite_C", + "nodeType": "frackingSatellite", + "displayName": "Water", + "x": 10343, + "y": 187521, + "z": -10474, + "rotation": -194.19 + }, + { + "id": "BP_ResourceNodeGeyser10_3650", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 49337, + "y": -54670, + "z": 5773 + }, + { + "id": "BP_ResourceNodeGeyser11_3803", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 70191, + "y": -14100, + "z": 13085 + }, + { + "id": "BP_ResourceNodeGeyser12_3894", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -33373, + "y": -88036, + "z": -87, + "rotation": 53.44 + }, + { + "id": "BP_ResourceNodeGeyser13_3999", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -49789, + "y": -162416, + "z": -1849, + "rotation": 0 + }, + { + "id": "BP_ResourceNodeGeyser14", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 202233, + "y": 45027, + "z": -1317 + }, + { + "id": "BP_ResourceNodeGeyser15", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -142293, + "y": -61383, + "z": 3018 + }, + { + "id": "BP_ResourceNodeGeyser18", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -138870, + "y": -57953, + "z": 3236 + }, + { + "id": "BP_ResourceNodeGeyser19", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 206903, + "y": 45592, + "z": -1367 + }, + { + "id": "BP_ResourceNodeGeyser2_581", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 141833, + "y": -89090, + "z": 1559 + }, + { + "id": "BP_ResourceNodeGeyser4_1615", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 281106, + "y": 8140, + "z": 953 + }, + { + "id": "BP_ResourceNodeGeyser7_2873", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 92620, + "y": -31568, + "z": 5616, + "rotation": 0 + }, + { + "id": "BP_ResourceNodeGeyser8", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 95895, + "y": -30846, + "z": 5590, + "rotation": 64.69 + }, + { + "id": "BP_ResourceNodeGeyser9_3239", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 43763, + "y": -98725, + "z": 10544 + }, + { + "id": "BP_ResourceNodeGeyser_76", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 134478, + "y": -90978, + "z": 1984, + "rotation": 39.72 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F792DE001_1228687627", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 125777, + "y": -147081, + "z": 18842 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F792EE001_1257671809", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -213328, + "y": -160655, + "z": -1621 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F792FE001_1083532997", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -209862, + "y": -159644, + "z": -1769 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F7967E001_1786196831", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -92534, + "y": -67192, + "z": 15498 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796AE001_1661824368", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -173000, + "y": 29532, + "z": 15333 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796AE001_1928280369", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": -169793, + "y": 20466, + "z": 15329, + "rotation": -122.44 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796CE001_1156904726", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 50532, + "y": 44056, + "z": 23962 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796DE001_2106907903", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 46522, + "y": 53125, + "z": 23943 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796EE001_1440606080", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 44326, + "y": 48548, + "z": 23761 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F796FE001_1768243264", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 39037, + "y": 218871, + "z": -5933 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F7974E001_1257931119", + "resource": "Desc_GeothermalEnergy_C", + "purity": "normal", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 40792, + "y": 211028, + "z": -5818 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F7975E001_2124245305", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 152735, + "y": 211451, + "z": -8796 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F797AE001_1940806190", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 282899, + "y": 1745, + "z": 2550 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F797AE001_2137062191", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 281822, + "y": -583, + "z": 2408 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F79ADDD01_1447318011", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 89458, + "y": 223800, + "z": 21 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F79ADDD01_1602669012", + "resource": "Desc_GeothermalEnergy_C", + "purity": "pure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 94642, + "y": 218487, + "z": -154 + }, + { + "id": "BP_ResourceNodeGeyser_C_UAID_40B076DF2F79C7DB01_1750096454", + "resource": "Desc_GeothermalEnergy_C", + "purity": "impure", + "classPath": "BP_ResourceNodeGeyser_C", + "nodeType": "geyser", + "x": 191906, + "y": -242852, + "z": 22813 + }, + { + "id": "BP_ResourceNode122", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -99604, + "y": -146823, + "z": -1631, + "rotation": 87.82 + }, + { + "id": "BP_ResourceNode129", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -113616, + "y": -50450, + "z": 16266, + "rotation": 0 + }, + { + "id": "BP_ResourceNode130", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -107640, + "y": -52376, + "z": 16193, + "rotation": -0.12 + }, + { + "id": "BP_ResourceNode449", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 136290, + "y": -20924, + "z": 9425, + "rotation": -111.32 + }, + { + "id": "BP_ResourceNode451", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 128059, + "y": -24825, + "z": 8264, + "rotation": 155.52 + }, + { + "id": "BP_ResourceNode452", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 122698, + "y": -26966, + "z": 7999, + "rotation": -75.09 + }, + { + "id": "BP_ResourceNode469", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 138805, + "y": -23953, + "z": 9441, + "rotation": -180.21 + }, + { + "id": "BP_ResourceNode498", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 400379, + "y": -262928, + "z": 3427, + "rotation": 87.42 + }, + { + "id": "BP_ResourceNode499", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 155868, + "y": 248394, + "z": -6063, + "rotation": 53.1 + }, + { + "id": "BP_ResourceNode500", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 157195, + "y": 241872, + "z": -6201, + "rotation": 57.05 + }, + { + "id": "BP_ResourceNode501", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 153876, + "y": 252817, + "z": -4610, + "rotation": -50.05 + }, + { + "id": "BP_ResourceNode502", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 150583, + "y": 249924, + "z": -5136, + "rotation": -115.84 + }, + { + "id": "BP_ResourceNode503", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -75977, + "y": 130576, + "z": -3632, + "rotation": -174.19 + }, + { + "id": "BP_ResourceNode504", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -78419, + "y": 129174, + "z": -3649, + "rotation": 52.71 + }, + { + "id": "BP_ResourceNode559", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -89379, + "y": 136763, + "z": -3949, + "rotation": -16.08 + }, + { + "id": "BP_ResourceNode560", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -86788, + "y": 138201, + "z": -3614, + "rotation": 166.06 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F795EE801_1339162695", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 153854, + "y": 84163, + "z": 9506, + "rotation": -36.6 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F796BE101_1963012602", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 206003, + "y": 181237, + "z": 10347, + "rotation": 36.96 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F796FE101_1569477308", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 209927, + "y": 162975, + "z": 5348, + "rotation": 48.9 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7971E101_2069219665", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -144746, + "y": 64419, + "z": 3137, + "rotation": 36.99 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7972E101_1083579843", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -148430, + "y": 56310, + "z": 3128, + "rotation": 14.45 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7973E101_1125437021", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -281034, + "y": -142695, + "z": -1636, + "rotation": 14.43 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7974E101_1439035199", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -271793, + "y": -146229, + "z": -1629, + "rotation": 14.55 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7974E101_1674467201", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -275456, + "y": -165870, + "z": -1622, + "rotation": 14.55 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F797DE001_1807610722", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 151131, + "y": 75311, + "z": 9714, + "rotation": -35.77 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7980E001_1705275252", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 155600, + "y": 113112, + "z": 6026, + "rotation": -15.34 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7981E001_1464253430", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 177478, + "y": 129905, + "z": 10377, + "rotation": 79.55 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7983E001_1088885785", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 214894, + "y": 116756, + "z": 10801, + "rotation": 79.46 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7983E001_1840982787", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 239775, + "y": 148305, + "z": 6682, + "rotation": 79.21 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7984E001_1384492965", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 193869, + "y": 116585, + "z": 10248, + "rotation": 79.48 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F7984E001_1664435967", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 180820, + "y": 141195, + "z": 9394, + "rotation": 36.98 + }, + { + "id": "BP_ResourceNode573_UAID_40B076DF2F79ACE101_1257410031", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -265694, + "y": -162571, + "z": 1526, + "rotation": 14.55 + }, + { + "id": "BP_ResourceNode581", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 276453, + "y": -250331, + "z": 1261, + "rotation": 127.27 + }, + { + "id": "BP_ResourceNode587", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -66077, + "y": 296871, + "z": -6102, + "rotation": 40.19 + }, + { + "id": "BP_ResourceNode590", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 17164, + "y": 280788, + "z": 2768, + "rotation": 50.75 + }, + { + "id": "BP_ResourceNode594", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 404788, + "y": -113961, + "z": 635, + "rotation": -17.78 + }, + { + "id": "BP_ResourceNode599", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -60961, + "y": 96196, + "z": 21234, + "rotation": 52.72 + }, + { + "id": "BP_ResourceNode5_381", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -107739, + "y": -130640, + "z": -1525, + "rotation": 90.44 + }, + { + "id": "BP_ResourceNode600", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -29926, + "y": 39693, + "z": 21308, + "rotation": 52.66 + }, + { + "id": "BP_ResourceNode601", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -64843, + "y": -7738, + "z": 22627, + "rotation": -21.89 + }, + { + "id": "BP_ResourceNode602", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 33025, + "y": 69, + "z": 23076, + "rotation": -126.51 + }, + { + "id": "BP_ResourceNode603", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 57623, + "y": 45463, + "z": 24895, + "rotation": 4.84 + }, + { + "id": "BP_ResourceNode604", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 8348, + "y": 70432, + "z": 23245, + "rotation": 12.08 + }, + { + "id": "BP_ResourceNode605", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -107794, + "y": 31855, + "z": 22428, + "rotation": 41.93 + }, + { + "id": "BP_ResourceNode606", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -141072, + "y": 39662, + "z": 19145, + "rotation": -6.19 + }, + { + "id": "BP_ResourceNode609", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -53782, + "y": 33003, + "z": 20012 + }, + { + "id": "BP_ResourceNode610", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 404471, + "y": -116795, + "z": 1122, + "rotation": 99.01 + }, + { + "id": "BP_ResourceNode611", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 368851, + "y": -76219, + "z": -539, + "rotation": -42.19 + }, + { + "id": "BP_ResourceNode612", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 365436, + "y": -80082, + "z": -63, + "rotation": 132.78 + }, + { + "id": "BP_ResourceNode614", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 330471, + "y": -264658, + "z": 2144, + "rotation": 109.76 + }, + { + "id": "BP_ResourceNode615", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 271514, + "y": -246511, + "z": 1631, + "rotation": 37.79 + }, + { + "id": "BP_ResourceNode616", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 399534, + "y": -255365, + "z": 3823, + "rotation": 135.03 + }, + { + "id": "BP_ResourceNode617", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 362594, + "y": -135770, + "z": 5895, + "rotation": 0 + }, + { + "id": "BP_ResourceNode618", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 368128, + "y": -139151, + "z": 5476, + "rotation": 165.43 + }, + { + "id": "BP_ResourceNode619", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 365854, + "y": -133384, + "z": 5256, + "rotation": 165.34 + }, + { + "id": "BP_ResourceNode620", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 406197, + "y": -252990, + "z": 3940, + "rotation": 135.33 + }, + { + "id": "BP_ResourceNode621", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 218926, + "y": -255339, + "z": 1070, + "rotation": -238.76 + }, + { + "id": "BP_ResourceNode622", + "resource": "Desc_Coal_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 220713, + "y": -250482, + "z": 1257, + "rotation": -142.66 + }, + { + "id": "BP_ResourceNode623", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": 325549, + "y": -264500, + "z": 1674, + "rotation": 109.55 + }, + { + "id": "BP_ResourceNode6_379", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -110109, + "y": -129160, + "z": -1405, + "rotation": -129.46 + }, + { + "id": "BP_ResourceNode7_380", + "resource": "Desc_Coal_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -102630, + "y": -140320, + "z": -1582, + "rotation": 90.43 + }, + { + "id": "BP_ResourceNode_700", + "resource": "Desc_Coal_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Coal", + "x": -113216, + "y": -44145, + "z": 16296, + "rotation": 0 + }, + { + "id": "BP_ResourceNode100", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 178265, + "y": 206096, + "z": -9239, + "rotation": 33.25 + }, + { + "id": "BP_ResourceNode14_609", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 26598, + "y": -193984, + "z": -1684, + "rotation": 125 + }, + { + "id": "BP_ResourceNode15", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -32600, + "y": -192537, + "z": 2280, + "rotation": -33.62 + }, + { + "id": "BP_ResourceNode151", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 185124, + "y": 214442, + "z": -8911, + "rotation": 62.89 + }, + { + "id": "BP_ResourceNode152_995", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 184227, + "y": 217102, + "z": -8911, + "rotation": -70.25 + }, + { + "id": "BP_ResourceNode154", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 182116, + "y": 188397, + "z": -5640, + "rotation": 102.17 + }, + { + "id": "BP_ResourceNode155", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 184783, + "y": 191090, + "z": -5732, + "rotation": -108.1 + }, + { + "id": "BP_ResourceNode16", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -43612, + "y": -190316, + "z": 2291, + "rotation": 42.41 + }, + { + "id": "BP_ResourceNode23_96", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 57144, + "y": -191300, + "z": -1745, + "rotation": 118.36 + }, + { + "id": "BP_ResourceNode24_97", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 15357, + "y": -197672, + "z": -1606, + "rotation": 151.44 + }, + { + "id": "BP_ResourceNode25_98", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 29991, + "y": -186850, + "z": -1689, + "rotation": 29.05 + }, + { + "id": "BP_ResourceNode26_99", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 44496, + "y": -228950, + "z": -1744, + "rotation": 112.1 + }, + { + "id": "BP_ResourceNode27_100", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 12163, + "y": -225348, + "z": -1744, + "rotation": 97.91 + }, + { + "id": "BP_ResourceNode28_101", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -35726, + "y": -209521, + "z": -1709, + "rotation": 165.52 + }, + { + "id": "BP_ResourceNode29_102", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 52471, + "y": -201465, + "z": -1659, + "rotation": 0 + }, + { + "id": "BP_ResourceNode30_103", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 148478, + "y": -193869, + "z": -1603, + "rotation": -21.24 + }, + { + "id": "BP_ResourceNode31_104", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 147769, + "y": -212912, + "z": -1429, + "rotation": 52.1 + }, + { + "id": "BP_ResourceNode32_105", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 146452, + "y": -222638, + "z": -1581, + "rotation": -27.9 + }, + { + "id": "BP_ResourceNode444_0", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 173702, + "y": -91776, + "z": 1051 + }, + { + "id": "BP_ResourceNode446_1", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 167392, + "y": -88954, + "z": 1050 + }, + { + "id": "BP_ResourceNode447", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 176297, + "y": -94197, + "z": 1419 + }, + { + "id": "BP_ResourceNode448", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 172814, + "y": -94608, + "z": 1042 + }, + { + "id": "BP_ResourceNode458", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 49404, + "y": -4744, + "z": 13192, + "rotation": -25 + }, + { + "id": "BP_ResourceNode459", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 52589, + "y": -8859, + "z": 13593 + }, + { + "id": "BP_ResourceNode460", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 49639, + "y": 656, + "z": 13150, + "rotation": -40 + }, + { + "id": "BP_ResourceNode86", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -229054, + "y": 78058, + "z": -1685 + }, + { + "id": "BP_ResourceNode87", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -245049, + "y": 90588, + "z": -1652, + "rotation": -50 + }, + { + "id": "BP_ResourceNode88", + "resource": "Desc_LiquidOil_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -232469, + "y": 62838, + "z": -1676, + "rotation": -69.86 + }, + { + "id": "BP_ResourceNode89", + "resource": "Desc_LiquidOil_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": -252534, + "y": 69813, + "z": -1562, + "rotation": -39.86 + }, + { + "id": "BP_ResourceNode98", + "resource": "Desc_LiquidOil_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Crude Oil", + "x": 169717, + "y": 203386, + "z": -9243, + "rotation": -0.01 + }, + { + "id": "BP_ResourceNode476", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 263148, + "y": 53611, + "z": -1325, + "rotation": 54.61 + }, + { + "id": "BP_ResourceNode477", + "resource": "Desc_OreBauxite_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 261128, + "y": 48275, + "z": -817, + "rotation": 68.32 + }, + { + "id": "BP_ResourceNode479", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 260299, + "y": 56228, + "z": -867, + "rotation": 58.52 + }, + { + "id": "BP_ResourceNode480", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 199007, + "y": 89173, + "z": 1097, + "rotation": 54.61 + }, + { + "id": "BP_ResourceNode481", + "resource": "Desc_OreBauxite_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 198936, + "y": 93354, + "z": 1239, + "rotation": -94.53 + }, + { + "id": "BP_ResourceNode485", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -59322, + "y": 14595, + "z": 23995, + "rotation": -9.52 + }, + { + "id": "BP_ResourceNode486", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 2571, + "y": 9834, + "z": 23972, + "rotation": -9.47 + }, + { + "id": "BP_ResourceNode529", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 39478, + "y": 52120, + "z": 23602, + "rotation": -39.03 + }, + { + "id": "BP_ResourceNode566", + "resource": "Desc_OreBauxite_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -5634, + "y": 44274, + "z": 21106, + "rotation": 6.33 + }, + { + "id": "BP_ResourceNode568", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -5293, + "y": 92075, + "z": 22756, + "rotation": 26.5 + }, + { + "id": "BP_ResourceNode595", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -217052, + "y": 11242, + "z": 17705, + "rotation": 35.71 + }, + { + "id": "BP_ResourceNode596", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -177367, + "y": 44999, + "z": 23766, + "rotation": -28.26 + }, + { + "id": "BP_ResourceNode597", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": -150575, + "y": -23, + "z": 19506, + "rotation": -30.44 + }, + { + "id": "BP_ResourceNode633", + "resource": "Desc_OreBauxite_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 106048, + "y": 50479, + "z": 9765, + "rotation": -39.02 + }, + { + "id": "BP_ResourceNode634", + "resource": "Desc_OreBauxite_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 116878, + "y": 51511, + "z": 17360, + "rotation": -39.02 + }, + { + "id": "BP_ResourceNode635", + "resource": "Desc_OreBauxite_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 103318, + "y": 67499, + "z": 9540, + "rotation": -39.01 + }, + { + "id": "BP_ResourceNode636", + "resource": "Desc_OreBauxite_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Bauxite", + "x": 89593, + "y": 62639, + "z": 10375, + "rotation": -39 + }, + { + "id": "BP_ResourceNode111_3367", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -268264, + "y": -54991, + "z": -344, + "rotation": -166.33 + }, + { + "id": "BP_ResourceNode112", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -266712, + "y": -51318, + "z": -331, + "rotation": 167.61 + }, + { + "id": "BP_ResourceNode117", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -148008, + "y": -30185, + "z": -556, + "rotation": 0 + }, + { + "id": "BP_ResourceNode125_5930", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -96801, + "y": -135276, + "z": 166 + }, + { + "id": "BP_ResourceNode127", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -94265, + "y": 165842, + "z": -3735, + "rotation": -8.89 + }, + { + "id": "BP_ResourceNode131", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -266904, + "y": -133355, + "z": 5974 + }, + { + "id": "BP_ResourceNode141", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 297710, + "y": -107996, + "z": 5475, + "rotation": -99.6 + }, + { + "id": "BP_ResourceNode150", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 108117, + "y": 152284, + "z": 3388, + "rotation": -146.44 + }, + { + "id": "BP_ResourceNode156", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -281346, + "y": -72000, + "z": -1201, + "rotation": -166.33 + }, + { + "id": "BP_ResourceNode159", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -166357, + "y": -129628, + "z": 1600 + }, + { + "id": "BP_ResourceNode162_5199", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -163354, + "y": -192097, + "z": 10761 + }, + { + "id": "BP_ResourceNode179", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 225504, + "y": 55773, + "z": -1304, + "rotation": -389.17 + }, + { + "id": "BP_ResourceNode185", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 252875, + "y": -161693, + "z": 3453, + "rotation": 0 + }, + { + "id": "BP_ResourceNode188", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 256402, + "y": -184355, + "z": 3519, + "rotation": -125.87 + }, + { + "id": "BP_ResourceNode192_0", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 270317, + "y": -211597, + "z": 3051, + "rotation": -40.78 + }, + { + "id": "BP_ResourceNode194", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 297398, + "y": -96437, + "z": 6021, + "rotation": -0.31 + }, + { + "id": "BP_ResourceNode195", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 277591, + "y": -94119, + "z": 5536, + "rotation": 0 + }, + { + "id": "BP_ResourceNode197", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 355998, + "y": -66386, + "z": -396, + "rotation": 68.9 + }, + { + "id": "BP_ResourceNode202", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 233447, + "y": -247935, + "z": 1057, + "rotation": 41.52 + }, + { + "id": "BP_ResourceNode211", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 348939, + "y": -185584, + "z": 4236, + "rotation": 0 + }, + { + "id": "BP_ResourceNode212", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 329410, + "y": -186014, + "z": 3741, + "rotation": -98.24 + }, + { + "id": "BP_ResourceNode213", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 355462, + "y": -149808, + "z": 4215, + "rotation": -20.59 + }, + { + "id": "BP_ResourceNode214", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 326947, + "y": -209586, + "z": 3688, + "rotation": 0 + }, + { + "id": "BP_ResourceNode215", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 287768, + "y": -274886, + "z": 1242, + "rotation": -54.53 + }, + { + "id": "BP_ResourceNode216", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 342806, + "y": -114728, + "z": 6020, + "rotation": 8.66 + }, + { + "id": "BP_ResourceNode235", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 380814, + "y": -169868, + "z": 4187, + "rotation": -20.75 + }, + { + "id": "BP_ResourceNode237", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 381666, + "y": -268290, + "z": 3486, + "rotation": 9.22 + }, + { + "id": "BP_ResourceNode38_902", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 167904, + "y": -116373, + "z": 2427, + "rotation": 10.18 + }, + { + "id": "BP_ResourceNode445", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 56109, + "y": -85970, + "z": 9944, + "rotation": 115.31 + }, + { + "id": "BP_ResourceNode492", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -83962, + "y": 273577, + "z": -6240, + "rotation": 90.64 + }, + { + "id": "BP_ResourceNode493", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -83134, + "y": 275762, + "z": -6264, + "rotation": -88.39 + }, + { + "id": "BP_ResourceNode505", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 7172, + "y": 155232, + "z": -12083, + "rotation": -212.16 + }, + { + "id": "BP_ResourceNode506", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 5251, + "y": 157169, + "z": -12083, + "rotation": -62.58 + }, + { + "id": "BP_ResourceNode507", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 5024, + "y": 159951, + "z": -12083, + "rotation": 179.15 + }, + { + "id": "BP_ResourceNode513", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 152648, + "y": 5227, + "z": 15168, + "rotation": -75.77 + }, + { + "id": "BP_ResourceNode514", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 157738, + "y": 14878, + "z": 15370, + "rotation": 58.7 + }, + { + "id": "BP_ResourceNode515", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 149937, + "y": 4686, + "z": 15468, + "rotation": -88.25 + }, + { + "id": "BP_ResourceNode53_510", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -21758, + "y": -145312, + "z": 10042, + "rotation": 7.23 + }, + { + "id": "BP_ResourceNode540", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -33328, + "y": 231626, + "z": -1394, + "rotation": -105.67 + }, + { + "id": "BP_ResourceNode547", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -21265, + "y": 283148, + "z": -2531, + "rotation": -69.27 + }, + { + "id": "BP_ResourceNode547_UAID_40B076DF2F79ADE101_1836911209", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 80832, + "y": 242478, + "z": -4444, + "rotation": -68.69 + }, + { + "id": "BP_ResourceNode547_UAID_40B076DF2F79AEE101_1979010387", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 58031, + "y": 82305, + "z": 11801, + "rotation": -86.07 + }, + { + "id": "BP_ResourceNode562", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -71673, + "y": 106511, + "z": 8466, + "rotation": 94.56 + }, + { + "id": "BP_ResourceNode593", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -28703, + "y": 198210, + "z": -1997, + "rotation": 144.59 + }, + { + "id": "BP_ResourceNode68_2514", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -36771, + "y": 296779, + "z": -2366, + "rotation": 36.42 + }, + { + "id": "BP_ResourceNode75_6425", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 130551, + "y": 179529, + "z": -5545, + "rotation": 0.06 + }, + { + "id": "BP_ResourceNode83", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -171879, + "y": 185789, + "z": 1077, + "rotation": -40.4 + }, + { + "id": "BP_ResourceNode83_UAID_40B076DF2F79FBE101_1618730935", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -159616, + "y": 112858, + "z": 16254, + "rotation": -38.95 + }, + { + "id": "BP_ResourceNode83_UAID_40B076DF2F79FFE101_1122581639", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -156723, + "y": 109165, + "z": 16113, + "rotation": -93.7 + }, + { + "id": "BP_ResourceNode91_785", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 282284, + "y": 63327, + "z": -1601, + "rotation": -19.35 + }, + { + "id": "BP_ResourceNode92", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 297499, + "y": -7312, + "z": -298, + "rotation": -99.53 + }, + { + "id": "BP_ResourceNode94_406", + "resource": "Desc_OreCopper_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 160086, + "y": 259421, + "z": -1095, + "rotation": 7.9 + }, + { + "id": "BP_ResourceNode_C_UAID_40B076DF2F794DE201_1841969367", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 20144, + "y": -169847, + "z": 15478, + "rotation": -197.5 + }, + { + "id": "BP_ResourceNode_C_UAID_40B076DF2F794FE201_2126930721", + "resource": "Desc_OreCopper_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": 95346, + "y": -168442, + "z": 13262, + "rotation": -197.56 + }, + { + "id": "BP_ResourceNode_C_UAID_A036BCACDEB0A6A601_2086848673", + "resource": "Desc_OreCopper_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Copper Ore", + "x": -26898, + "y": -145469, + "z": 8397, + "rotation": -31.87 + }, + { + "id": "BP_ResourceNode121_4877", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -178491, + "y": -79788, + "z": 3331 + }, + { + "id": "BP_ResourceNode121_UAID_40B076DF2F7938DF01_2097772508", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -135468, + "y": -174682, + "z": 7753 + }, + { + "id": "BP_ResourceNode134_8590", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 109619, + "y": 155798, + "z": 4176, + "rotation": 152.53 + }, + { + "id": "BP_ResourceNode140", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 236609, + "y": -260096, + "z": 11784, + "rotation": 148.85 + }, + { + "id": "BP_ResourceNode142", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 224308, + "y": -128742, + "z": 7241, + "rotation": -51.85 + }, + { + "id": "BP_ResourceNode142_UAID_40B076DF2F79E8DD01_2087440367", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 211843, + "y": -18820, + "z": -29, + "rotation": -36.22 + }, + { + "id": "BP_ResourceNode142_UAID_40B076DF2F79E9DD01_1434900545", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 208140, + "y": -23983, + "z": 594, + "rotation": 294.34 + }, + { + "id": "BP_ResourceNode142_UAID_40B076DF2F79E9DD01_1872254547", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 208007, + "y": -16383, + "z": -55, + "rotation": 136.66 + }, + { + "id": "BP_ResourceNode169_UAID_40B076DF2F7939DE01_2083925623", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 114180, + "y": -17000, + "z": 14021, + "rotation": -0.07 + }, + { + "id": "BP_ResourceNode176_UAID_40B076DF2F793BDF01_1694110039", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -153387, + "y": 82252, + "z": 20330, + "rotation": -29.86 + }, + { + "id": "BP_ResourceNode240", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 406564, + "y": -206810, + "z": -1539, + "rotation": -156.1 + }, + { + "id": "BP_ResourceNode487", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -12815, + "y": -104642, + "z": 10909, + "rotation": -111.8 + }, + { + "id": "BP_ResourceNode487_UAID_40B076DF2F7934DF01_1597642799", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 103846, + "y": -94854, + "z": 12652, + "rotation": -111.91 + }, + { + "id": "BP_ResourceNode570", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 270786, + "y": 120111, + "z": -55, + "rotation": -100.77 + }, + { + "id": "BP_ResourceNode574", + "resource": "Desc_OreGold_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": 90376, + "y": 80714, + "z": 10117, + "rotation": 32.86 + }, + { + "id": "BP_ResourceNode70_3132", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -131574, + "y": 227253, + "z": -787, + "rotation": 0.14 + }, + { + "id": "BP_ResourceNode72_998", + "resource": "Desc_OreGold_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Caterium Ore", + "x": -92343, + "y": 281606, + "z": -4620, + "rotation": 27.78 + }, + { + "id": "BP_ResourceNode105_2463", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -263873, + "y": -30848, + "z": -412 + }, + { + "id": "BP_ResourceNode106", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -260110, + "y": -27674, + "z": -640 + }, + { + "id": "BP_ResourceNode107", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -256244, + "y": -29564, + "z": -668 + }, + { + "id": "BP_ResourceNode108", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -259120, + "y": -44689, + "z": -494, + "rotation": 32.6 + }, + { + "id": "BP_ResourceNode109", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -261358, + "y": -41228, + "z": -327, + "rotation": 13.6 + }, + { + "id": "BP_ResourceNode114", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -152490, + "y": -45787, + "z": 693, + "rotation": 108.34 + }, + { + "id": "BP_ResourceNode115", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -166731, + "y": -26566, + "z": -510, + "rotation": 60.62 + }, + { + "id": "BP_ResourceNode116", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -170448, + "y": -28321, + "z": -596, + "rotation": -163.86 + }, + { + "id": "BP_ResourceNode123_5084", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -110588, + "y": -135153, + "z": 1911 + }, + { + "id": "BP_ResourceNode126_6409", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -96101, + "y": 163753, + "z": -3734, + "rotation": 30.57 + }, + { + "id": "BP_ResourceNode128_5242", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -190787, + "y": -117690, + "z": 706 + }, + { + "id": "BP_ResourceNode12_91", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 248481, + "y": -146160, + "z": 3592, + "rotation": -0.4 + }, + { + "id": "BP_ResourceNode13", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 242692, + "y": -149433, + "z": 2591, + "rotation": -0.72 + }, + { + "id": "BP_ResourceNode146", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 96959, + "y": 157163, + "z": 2110, + "rotation": 7.39 + }, + { + "id": "BP_ResourceNode147", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 96326, + "y": 159762, + "z": 2103, + "rotation": 129.04 + }, + { + "id": "BP_ResourceNode148", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 93730, + "y": 160264, + "z": 2111, + "rotation": 115.24 + }, + { + "id": "BP_ResourceNode149", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 92725, + "y": 163306, + "z": 1882, + "rotation": 96.18 + }, + { + "id": "BP_ResourceNode153", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -258433, + "y": -40561, + "z": -327, + "rotation": 163.15 + }, + { + "id": "BP_ResourceNode160", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -250080, + "y": -126363, + "z": 3838, + "rotation": 92.71 + }, + { + "id": "BP_ResourceNode161", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -252033, + "y": -125001, + "z": 3848, + "rotation": 7.34 + }, + { + "id": "BP_ResourceNode167", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -186902, + "y": -118340, + "z": 706, + "rotation": -156.3 + }, + { + "id": "BP_ResourceNode168", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -187933, + "y": -114624, + "z": 706, + "rotation": -156.3 + }, + { + "id": "BP_ResourceNode173", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 264240, + "y": -162899, + "z": 3332, + "rotation": -175 + }, + { + "id": "BP_ResourceNode174", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -227411, + "y": -118862, + "z": 3932, + "rotation": 92.71 + }, + { + "id": "BP_ResourceNode175", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -230853, + "y": -115124, + "z": 3918, + "rotation": 7.34 + }, + { + "id": "BP_ResourceNode180", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 285861, + "y": -127665, + "z": 3876, + "rotation": 166.64 + }, + { + "id": "BP_ResourceNode184", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 311230, + "y": -128705, + "z": 4669, + "rotation": 6.24 + }, + { + "id": "BP_ResourceNode196", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 255250, + "y": -91660, + "z": 5524, + "rotation": 0.07 + }, + { + "id": "BP_ResourceNode198", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 268524, + "y": -116944, + "z": 4207, + "rotation": -34.94 + }, + { + "id": "BP_ResourceNode199", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 266881, + "y": -120715, + "z": 4084, + "rotation": -40.16 + }, + { + "id": "BP_ResourceNode200", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 283969, + "y": -121598, + "z": 4452, + "rotation": -1.52 + }, + { + "id": "BP_ResourceNode201", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 347228, + "y": -58491, + "z": -749, + "rotation": -3.5 + }, + { + "id": "BP_ResourceNode203", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 319464, + "y": -158098, + "z": 3865, + "rotation": -2.63 + }, + { + "id": "BP_ResourceNode204_0", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 330433, + "y": -142400, + "z": 3966, + "rotation": 0 + }, + { + "id": "BP_ResourceNode205", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 313010, + "y": -133842, + "z": 4862, + "rotation": 307.68 + }, + { + "id": "BP_ResourceNode206", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 339300, + "y": -183354, + "z": 4477, + "rotation": 108.93 + }, + { + "id": "BP_ResourceNode207", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 273730, + "y": -148173, + "z": 4083, + "rotation": -22.26 + }, + { + "id": "BP_ResourceNode208", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 338208, + "y": -177564, + "z": 4959, + "rotation": 26.08 + }, + { + "id": "BP_ResourceNode209", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 372374, + "y": -153421, + "z": 4006, + "rotation": 17.5 + }, + { + "id": "BP_ResourceNode210", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 348526, + "y": -123751, + "z": 4284, + "rotation": 17.49 + }, + { + "id": "BP_ResourceNode217", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 278266, + "y": -210772, + "z": 3222, + "rotation": -27.99 + }, + { + "id": "BP_ResourceNode218", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 298734, + "y": -199293, + "z": 2709, + "rotation": -31.78 + }, + { + "id": "BP_ResourceNode219", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 282689, + "y": -179305, + "z": 4212, + "rotation": 1.3 + }, + { + "id": "BP_ResourceNode220", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 385303, + "y": -189928, + "z": 4281, + "rotation": -1.33 + }, + { + "id": "BP_ResourceNode221", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 311799, + "y": -215343, + "z": 2437, + "rotation": 0 + }, + { + "id": "BP_ResourceNode222", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 314598, + "y": -196773, + "z": 3906, + "rotation": -47.26 + }, + { + "id": "BP_ResourceNode223", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 276674, + "y": -195100, + "z": 2899, + "rotation": 0.22 + }, + { + "id": "BP_ResourceNode230", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 280138, + "y": -261097, + "z": 1860, + "rotation": 0.56 + }, + { + "id": "BP_ResourceNode233", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 374875, + "y": -259903, + "z": 3232, + "rotation": 28.36 + }, + { + "id": "BP_ResourceNode236", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 377738, + "y": -254226, + "z": 3347, + "rotation": 91.38 + }, + { + "id": "BP_ResourceNode238", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 304840, + "y": -172908, + "z": 3801, + "rotation": 0.08 + }, + { + "id": "BP_ResourceNode239", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 297576, + "y": -150186, + "z": 3973, + "rotation": 0.41 + }, + { + "id": "BP_ResourceNode35", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 256612, + "y": -197979, + "z": 3561, + "rotation": 197.37 + }, + { + "id": "BP_ResourceNode36", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 241091, + "y": -239788, + "z": 1048, + "rotation": 197.5 + }, + { + "id": "BP_ResourceNode39", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 181724, + "y": -116280, + "z": 2150, + "rotation": -25.22 + }, + { + "id": "BP_ResourceNode40", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 178062, + "y": -120504, + "z": 2281, + "rotation": 29.83 + }, + { + "id": "BP_ResourceNode426", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 68075, + "y": -81001, + "z": 9944, + "rotation": -25.88 + }, + { + "id": "BP_ResourceNode427", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 70865, + "y": -78126, + "z": 10452, + "rotation": -27.89 + }, + { + "id": "BP_ResourceNode430", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 59113, + "y": -71502, + "z": 10568, + "rotation": -16.23 + }, + { + "id": "BP_ResourceNode431", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 64576, + "y": -71387, + "z": 10641, + "rotation": -0.03 + }, + { + "id": "BP_ResourceNode435_26", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 84199, + "y": -86394, + "z": 9767, + "rotation": 32.26 + }, + { + "id": "BP_ResourceNode437_30", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 83509, + "y": -90507, + "z": 9591, + "rotation": -39.33 + }, + { + "id": "BP_ResourceNode453", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -44869, + "y": -139480, + "z": 7983, + "rotation": 48.75 + }, + { + "id": "BP_ResourceNode454", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -43382, + "y": -136820, + "z": 8046, + "rotation": -3.53 + }, + { + "id": "BP_ResourceNode457", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 116236, + "y": -15552, + "z": 14027, + "rotation": 87.26 + }, + { + "id": "BP_ResourceNode462", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -37614, + "y": -142250, + "z": 7972 + }, + { + "id": "BP_ResourceNode462_UAID_40B076DF2F7902E201_1630060169", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -58996, + "y": -224575, + "z": 9574, + "rotation": 0.02 + }, + { + "id": "BP_ResourceNode462_UAID_40B076DF2F7907E201_1624182051", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -64963, + "y": -201169, + "z": 6925, + "rotation": -0.11 + }, + { + "id": "BP_ResourceNode462_UAID_40B076DF2F790CE201_2008279933", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -52877, + "y": -201762, + "z": 6793, + "rotation": -0.07 + }, + { + "id": "BP_ResourceNode488", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -94905, + "y": 262678, + "z": -4620, + "rotation": -231.63 + }, + { + "id": "BP_ResourceNode489", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -97178, + "y": 264232, + "z": -4626, + "rotation": 126.9 + }, + { + "id": "BP_ResourceNode49", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 265501, + "y": -178206, + "z": 3457, + "rotation": -0.07 + }, + { + "id": "BP_ResourceNode490", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -98579, + "y": 265801, + "z": -4635, + "rotation": -52.65 + }, + { + "id": "BP_ResourceNode491", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -91268, + "y": 253077, + "z": -4514, + "rotation": 29.5 + }, + { + "id": "BP_ResourceNode494", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -36500, + "y": 243894, + "z": -2503, + "rotation": -42.75 + }, + { + "id": "BP_ResourceNode495", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -35041, + "y": 245805, + "z": -2504, + "rotation": 156.98 + }, + { + "id": "BP_ResourceNode496", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -49077, + "y": 231708, + "z": -3846, + "rotation": 126.93 + }, + { + "id": "BP_ResourceNode497_1", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -51645, + "y": 229391, + "z": -3846, + "rotation": 96.25 + }, + { + "id": "BP_ResourceNode517", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 124933, + "y": 5226, + "z": 15574, + "rotation": -106.7 + }, + { + "id": "BP_ResourceNode518", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 128573, + "y": 1795, + "z": 15543, + "rotation": 90.38 + }, + { + "id": "BP_ResourceNode523_UAID_40B076DF2F7987DF01_1117795413", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 245043, + "y": -22318, + "z": 8821, + "rotation": -95.29 + }, + { + "id": "BP_ResourceNode524_UAID_40B076DF2F798ADF01_1172524943", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 237725, + "y": -23006, + "z": 8797, + "rotation": -50.42 + }, + { + "id": "BP_ResourceNode528", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 233142, + "y": -26440, + "z": 8976, + "rotation": -140.57 + }, + { + "id": "BP_ResourceNode530", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -99587, + "y": 267812, + "z": -4661, + "rotation": 31.6 + }, + { + "id": "BP_ResourceNode531", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -93267, + "y": 254146, + "z": -4510, + "rotation": 29.5 + }, + { + "id": "BP_ResourceNode532", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -94007, + "y": 268116, + "z": -3128, + "rotation": -122.07 + }, + { + "id": "BP_ResourceNode533", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -92372, + "y": 266082, + "z": -3125, + "rotation": -7.67 + }, + { + "id": "BP_ResourceNode535", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -65049, + "y": 137464, + "z": 8488, + "rotation": 114.74 + }, + { + "id": "BP_ResourceNode536", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -42273, + "y": 132151, + "z": 8669, + "rotation": -140.68 + }, + { + "id": "BP_ResourceNode537", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -60453, + "y": 141696, + "z": 8417, + "rotation": 17.72 + }, + { + "id": "BP_ResourceNode538", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -41928, + "y": 206908, + "z": -2228, + "rotation": 170.73 + }, + { + "id": "BP_ResourceNode539", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -43940, + "y": 207992, + "z": -2228, + "rotation": -80.1 + }, + { + "id": "BP_ResourceNode541", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -38249, + "y": 127975, + "z": 8811, + "rotation": 76.46 + }, + { + "id": "BP_ResourceNode542", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -41484, + "y": 125794, + "z": 8645, + "rotation": 103.19 + }, + { + "id": "BP_ResourceNode543", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 572, + "y": 283802, + "z": -1140, + "rotation": -133.35 + }, + { + "id": "BP_ResourceNode544", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -1126, + "y": 285301, + "z": -1140, + "rotation": 134.82 + }, + { + "id": "BP_ResourceNode545", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -44922, + "y": 302274, + "z": -2573, + "rotation": -168.74 + }, + { + "id": "BP_ResourceNode546", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -50407, + "y": 302871, + "z": -2583, + "rotation": 40.17 + }, + { + "id": "BP_ResourceNode551", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -54101, + "y": 229417, + "z": -3846, + "rotation": 96.25 + }, + { + "id": "BP_ResourceNode558", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -8997, + "y": 293512, + "z": -2527, + "rotation": -142.54 + }, + { + "id": "BP_ResourceNode55_1215", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -47085, + "y": -141055, + "z": 8074, + "rotation": -44.67 + }, + { + "id": "BP_ResourceNode563", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 271771, + "y": 100432, + "z": -1295, + "rotation": -88.34 + }, + { + "id": "BP_ResourceNode563_UAID_40B076DF2F79B7E101_1869159978", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 225651, + "y": 147648, + "z": -193, + "rotation": -3.41 + }, + { + "id": "BP_ResourceNode563_UAID_40B076DF2F79B8E101_1620414156", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 221481, + "y": 146594, + "z": -200, + "rotation": -3.43 + }, + { + "id": "BP_ResourceNode563_UAID_40B076DF2F79B9E101_1570060334", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 233433, + "y": 145603, + "z": -267, + "rotation": 18.45 + }, + { + "id": "BP_ResourceNode563_UAID_40B076DF2F79BBE101_1434258671", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 233293, + "y": 139880, + "z": -304, + "rotation": 43.72 + }, + { + "id": "BP_ResourceNode565_8", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 242757, + "y": 87933, + "z": -617, + "rotation": -108.41 + }, + { + "id": "BP_ResourceNode567", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 245862, + "y": 4379, + "z": 574, + "rotation": 163.98 + }, + { + "id": "BP_ResourceNode569", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -43327, + "y": 130254, + "z": 8662, + "rotation": -50.55 + }, + { + "id": "BP_ResourceNode577", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -55693, + "y": 194370, + "z": -2557, + "rotation": 144.59 + }, + { + "id": "BP_ResourceNode578", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -57161, + "y": 192773, + "z": -2513, + "rotation": 53.99 + }, + { + "id": "BP_ResourceNode579", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -58919, + "y": 195893, + "z": -2196, + "rotation": 29.11 + }, + { + "id": "BP_ResourceNode580", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -61757, + "y": 194634, + "z": -2042, + "rotation": -146.55 + }, + { + "id": "BP_ResourceNode583_1", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 255908, + "y": -3071, + "z": 3583, + "rotation": 219.35 + }, + { + "id": "BP_ResourceNode585", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -10234, + "y": 291035, + "z": -2476, + "rotation": 121.68 + }, + { + "id": "BP_ResourceNode591", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -47534, + "y": 289607, + "z": -2526, + "rotation": 131.78 + }, + { + "id": "BP_ResourceNode592", + "resource": "Desc_OreIron_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -50102, + "y": 284478, + "z": -2615, + "rotation": 29.5 + }, + { + "id": "BP_ResourceNode65_1865", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 219395, + "y": 37189, + "z": -1292, + "rotation": 6.1 + }, + { + "id": "BP_ResourceNode66", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 245700, + "y": 30065, + "z": -847, + "rotation": -24.17 + }, + { + "id": "BP_ResourceNode73_6071", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 133276, + "y": 197857, + "z": -7379, + "rotation": 0.12 + }, + { + "id": "BP_ResourceNode74", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 131967, + "y": 194729, + "z": -7425, + "rotation": -0.1 + }, + { + "id": "BP_ResourceNode76", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 133637, + "y": 205792, + "z": -7424, + "rotation": -0.18 + }, + { + "id": "BP_ResourceNode80", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -167603, + "y": 179557, + "z": 1590, + "rotation": -25.6 + }, + { + "id": "BP_ResourceNode81", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -164443, + "y": 171772, + "z": 1612, + "rotation": -5.39 + }, + { + "id": "BP_ResourceNode82", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": -162724, + "y": 182453, + "z": 713, + "rotation": 0.11 + }, + { + "id": "BP_ResourceNode90_482", + "resource": "Desc_OreIron_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 252751, + "y": 29140, + "z": -799, + "rotation": 42.41 + }, + { + "id": "BP_ResourceNode95_579", + "resource": "Desc_OreIron_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Iron Ore", + "x": 164518, + "y": 238318, + "z": -8589, + "rotation": 0.03 + }, + { + "id": "BP_ResourceNode484", + "resource": "Desc_OreUranium_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Uranium", + "x": 168713, + "y": 50839, + "z": -9, + "rotation": 35.25 + }, + { + "id": "BP_ResourceNode484_UAID_40B076DF2F79E0DF01_2091429101", + "resource": "Desc_OreUranium_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Uranium", + "x": 180194, + "y": -189199, + "z": 23503, + "rotation": 35.27 + }, + { + "id": "BP_ResourceNode576", + "resource": "Desc_OreUranium_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Uranium", + "x": 38000, + "y": 91736, + "z": -4809, + "rotation": -74.31 + }, + { + "id": "BP_ResourceNode598_0", + "resource": "Desc_OreUranium_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Uranium", + "x": -75893, + "y": 51637, + "z": 19146, + "rotation": 45.69 + }, + { + "id": "BP_ResourceNode632", + "resource": "Desc_OreUranium_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Uranium", + "x": -132772, + "y": -187579, + "z": 46528, + "rotation": 45.62 + }, + { + "id": "BP_ResourceNode136", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 37926, + "y": 120939, + "z": 13370 + }, + { + "id": "BP_ResourceNode136_UAID_40B076DF2F7975DF01_1587576239", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 61654, + "y": 196432, + "z": -3725, + "rotation": 0 + }, + { + "id": "BP_ResourceNode136_UAID_40B076DF2F7975DF01_1617269241", + "resource": "Desc_RawQuartz_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 62111, + "y": 207627, + "z": -4197, + "rotation": 42.82 + }, + { + "id": "BP_ResourceNode136_UAID_40B076DF2F7975DF01_1622351243", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 55253, + "y": 205856, + "z": -5763, + "rotation": 28.3 + }, + { + "id": "BP_ResourceNode137_2248", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 34968, + "y": 118464, + "z": 13370, + "rotation": 117.05 + }, + { + "id": "BP_ResourceNode231", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 305660, + "y": -301306, + "z": 1233, + "rotation": 0.58 + }, + { + "id": "BP_ResourceNode232", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 308598, + "y": -296079, + "z": 1257, + "rotation": 0.52 + }, + { + "id": "BP_ResourceNode474_UAID_40B076DF2F7983DF01_2128950703", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 196668, + "y": -13956, + "z": 9398, + "rotation": 53.29 + }, + { + "id": "BP_ResourceNode474_UAID_40B076DF2F798DDF01_1645035472", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 193980, + "y": -5468, + "z": 10260, + "rotation": 63.89 + }, + { + "id": "BP_ResourceNode474_UAID_40B076DF2F798EDF01_2134364650", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 190098, + "y": -17281, + "z": 9457, + "rotation": 53.28 + }, + { + "id": "BP_ResourceNode520", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": -90474, + "y": 67260, + "z": 2145, + "rotation": 110.19 + }, + { + "id": "BP_ResourceNode522", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": -90370, + "y": 63712, + "z": 2145, + "rotation": -137.14 + }, + { + "id": "BP_ResourceNode552", + "resource": "Desc_RawQuartz_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": -194230, + "y": -140422, + "z": -1242, + "rotation": -67.63 + }, + { + "id": "BP_ResourceNode57_UAID_40B076DF2F7935DF01_1413169977", + "resource": "Desc_RawQuartz_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 55042, + "y": -130641, + "z": 7156, + "rotation": -35.56 + }, + { + "id": "BP_ResourceNode57_UAID_40B076DF2F7991DF01_1459615180", + "resource": "Desc_RawQuartz_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": 62290, + "y": -137424, + "z": 8325, + "rotation": -35.59 + }, + { + "id": "BP_ResourceNode588", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": -167360, + "y": -145683, + "z": -1914, + "rotation": 0.04 + }, + { + "id": "BP_ResourceNode588_UAID_40B076DF2F79CEDF01_1910960903", + "resource": "Desc_RawQuartz_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Raw Quartz", + "x": -171986, + "y": -144146, + "z": -2030, + "rotation": -111.8 + }, + { + "id": "BP_ResourceNode101_1893", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 161929, + "y": 103777, + "z": 5603, + "rotation": 0.01 + }, + { + "id": "BP_ResourceNode101_UAID_40B076DF2F79E6D901_1551800812", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 119149, + "y": 71321, + "z": 18674, + "rotation": 0.09 + }, + { + "id": "BP_ResourceNode101_UAID_40B076DF2F79E7D901_2125168992", + "resource": "Desc_SAM_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 162899, + "y": 65414, + "z": 9782, + "rotation": -0.02 + }, + { + "id": "BP_ResourceNode135", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -182183, + "y": -142367, + "z": -1744, + "rotation": 38.27 + }, + { + "id": "BP_ResourceNode172", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -24066, + "y": 269858, + "z": -13354 + }, + { + "id": "BP_ResourceNode172_UAID_40B076DF2F79DFD901_1471130569", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 80730, + "y": 204812, + "z": 5722, + "rotation": 38.24 + }, + { + "id": "BP_ResourceNode241", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 172559, + "y": -285517, + "z": 21591, + "rotation": -112.5 + }, + { + "id": "BP_ResourceNode241_UAID_40B076DF2F7947D301_1723440520", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 272832, + "y": -261866, + "z": 1088, + "rotation": -144.49 + }, + { + "id": "BP_ResourceNode43", + "resource": "Desc_SAM_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 82559, + "y": -219609, + "z": 8929, + "rotation": 10.18 + }, + { + "id": "BP_ResourceNode43_UAID_40B076DF2F7932D901_1711042113", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -46645, + "y": -252947, + "z": 4931, + "rotation": 10.15 + }, + { + "id": "BP_ResourceNode43_UAID_40B076DF2F7936D401_1733397541", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -80870, + "y": -32419, + "z": 15657, + "rotation": -19.4 + }, + { + "id": "BP_ResourceNode43_UAID_40B076DF2F793ED901_1532454233", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 152962, + "y": -24054, + "z": 4287, + "rotation": 10.02 + }, + { + "id": "BP_ResourceNode43_UAID_40B076DF2F7941D901_1404601764", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 230542, + "y": -33617, + "z": 2185, + "rotation": 10.13 + }, + { + "id": "BP_ResourceNode47_3066", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 15460, + "y": -804, + "z": 14671, + "rotation": -2.89 + }, + { + "id": "BP_ResourceNode519", + "resource": "Desc_SAM_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -48375, + "y": 128937, + "z": 23614, + "rotation": 131.53 + }, + { + "id": "BP_ResourceNode519_UAID_40B076DF2F79D3D901_1586151453", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -181814, + "y": 89077, + "z": 17039, + "rotation": 178.43 + }, + { + "id": "BP_ResourceNode607", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": -143612, + "y": 20728, + "z": 19458, + "rotation": -28.97 + }, + { + "id": "BP_ResourceNode78_1097", + "resource": "Desc_SAM_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 228273, + "y": 116287, + "z": -1582 + }, + { + "id": "BP_ResourceNode99", + "resource": "Desc_SAM_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "SAM", + "x": 132958, + "y": 240893, + "z": -14193, + "rotation": -24.4 + }, + { + "id": "BP_ResourceNode102_2068", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -280837, + "y": -42089, + "z": -1276, + "rotation": 100.57 + }, + { + "id": "BP_ResourceNode103", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -267051, + "y": -24387, + "z": -724, + "rotation": -23.85 + }, + { + "id": "BP_ResourceNode104", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -273321, + "y": -19667, + "z": -1537, + "rotation": 0 + }, + { + "id": "BP_ResourceNode11", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -279020, + "y": -186294, + "z": 269, + "rotation": -9.56 + }, + { + "id": "BP_ResourceNode110", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -178099, + "y": -165242, + "z": 7617, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode113", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -165002, + "y": -133278, + "z": 1611, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode118_4340", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -133445, + "y": -35240, + "z": 3832 + }, + { + "id": "BP_ResourceNode119", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -145781, + "y": -31969, + "z": 3281 + }, + { + "id": "BP_ResourceNode124_5785", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -90458, + "y": -152279, + "z": -1650, + "rotation": 50.98 + }, + { + "id": "BP_ResourceNode132_5908", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -173249, + "y": -112173, + "z": 32, + "rotation": 0 + }, + { + "id": "BP_ResourceNode133_6963", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 109989, + "y": 196759, + "z": -3247, + "rotation": -0.01 + }, + { + "id": "BP_ResourceNode138_590", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 175981, + "y": -40156, + "z": 6612, + "rotation": -85.32 + }, + { + "id": "BP_ResourceNode139_909", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 158314, + "y": -65003, + "z": 2591, + "rotation": -31.49 + }, + { + "id": "BP_ResourceNode143_1543", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -129599, + "y": -96956, + "z": 4210, + "rotation": -0.07 + }, + { + "id": "BP_ResourceNode144_1644", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -231390, + "y": -89530, + "z": 804, + "rotation": 0.19 + }, + { + "id": "BP_ResourceNode145_1749", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -194906, + "y": -25780, + "z": -628 + }, + { + "id": "BP_ResourceNode157", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -268788, + "y": -79095, + "z": -292, + "rotation": -166.33 + }, + { + "id": "BP_ResourceNode158", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -129015, + "y": -39541, + "z": 2898, + "rotation": -105.89 + }, + { + "id": "BP_ResourceNode163", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -221697, + "y": -104736, + "z": 4106, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode164", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -261496, + "y": -116493, + "z": 4303, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode165", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -281761, + "y": -134704, + "z": 1998, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode166", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -206173, + "y": -141689, + "z": 4860, + "rotation": -136.46 + }, + { + "id": "BP_ResourceNode178", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 186164, + "y": 77070, + "z": 1138, + "rotation": -339.37 + }, + { + "id": "BP_ResourceNode181", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 237355, + "y": -164936, + "z": 2636, + "rotation": 59.94 + }, + { + "id": "BP_ResourceNode182", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 244591, + "y": -190903, + "z": 1833, + "rotation": 62.11 + }, + { + "id": "BP_ResourceNode186", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 295896, + "y": -272876, + "z": 1237, + "rotation": 133.29 + }, + { + "id": "BP_ResourceNode187_0", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 225406, + "y": -237057, + "z": 1043, + "rotation": 0.02 + }, + { + "id": "BP_ResourceNode189", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 308652, + "y": -100658, + "z": 6378, + "rotation": 37.78 + }, + { + "id": "BP_ResourceNode190", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 311623, + "y": -69811, + "z": 5620, + "rotation": 77.98 + }, + { + "id": "BP_ResourceNode191", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 299122, + "y": -85512, + "z": 5936 + }, + { + "id": "BP_ResourceNode193", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 383046, + "y": -92549, + "z": 207, + "rotation": -16.47 + }, + { + "id": "BP_ResourceNode20_3137", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -280306, + "y": -181363, + "z": 338, + "rotation": -159.27 + }, + { + "id": "BP_ResourceNode224", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 359885, + "y": -200469, + "z": 4110 + }, + { + "id": "BP_ResourceNode225", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 391118, + "y": -208703, + "z": 4998, + "rotation": -24.48 + }, + { + "id": "BP_ResourceNode226", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 338761, + "y": -205718, + "z": 4225 + }, + { + "id": "BP_ResourceNode227", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 348908, + "y": -163350, + "z": 4238, + "rotation": -6.72 + }, + { + "id": "BP_ResourceNode228", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 370713, + "y": -221971, + "z": 4225 + }, + { + "id": "BP_ResourceNode229", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 385953, + "y": -254365, + "z": 3390, + "rotation": 0 + }, + { + "id": "BP_ResourceNode234", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 355116, + "y": -122970, + "z": 5004, + "rotation": 34.26 + }, + { + "id": "BP_ResourceNode37_178", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 177198, + "y": -86517, + "z": 2459, + "rotation": 4.12 + }, + { + "id": "BP_ResourceNode41_1099", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 107368, + "y": -43239, + "z": 14774, + "rotation": 0.45 + }, + { + "id": "BP_ResourceNode42_1294", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 63823, + "y": 8377, + "z": 14013, + "rotation": -70.75 + }, + { + "id": "BP_ResourceNode439_1", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 79671, + "y": -75852, + "z": 12028, + "rotation": 41.15 + }, + { + "id": "BP_ResourceNode440", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 69552, + "y": -100369, + "z": 9392, + "rotation": 30.69 + }, + { + "id": "BP_ResourceNode441", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 67878, + "y": -95777, + "z": 9352, + "rotation": -3.87 + }, + { + "id": "BP_ResourceNode442", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 60593, + "y": -77891, + "z": 9417, + "rotation": 1.6 + }, + { + "id": "BP_ResourceNode443", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -6449, + "y": -120483, + "z": 10380, + "rotation": -89.22 + }, + { + "id": "BP_ResourceNode463", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -55555, + "y": -129406, + "z": 6952, + "rotation": 11.24 + }, + { + "id": "BP_ResourceNode464", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 50260, + "y": -157153, + "z": -144, + "rotation": -5.42 + }, + { + "id": "BP_ResourceNode464_UAID_40B076DF2F790EE201_1850696287", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 15240, + "y": -158579, + "z": -1387, + "rotation": -5.4 + }, + { + "id": "BP_ResourceNode464_UAID_40B076DF2F790FE201_1577140465", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 66757, + "y": -150677, + "z": -1181, + "rotation": -5.51 + }, + { + "id": "BP_ResourceNode464_UAID_40B076DF2F7914E201_2026233335", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 92495, + "y": -132749, + "z": 671, + "rotation": 32.51 + }, + { + "id": "BP_ResourceNode464_UAID_40B076DF2F7915E201_1334543513", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 40251, + "y": -144691, + "z": 369, + "rotation": 12.04 + }, + { + "id": "BP_ResourceNode465", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 121322, + "y": -88295, + "z": 13669, + "rotation": 0.05 + }, + { + "id": "BP_ResourceNode466", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 168497, + "y": -108030, + "z": 1557, + "rotation": 0 + }, + { + "id": "BP_ResourceNode46_2284", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 343519, + "y": -83470, + "z": 5618 + }, + { + "id": "BP_ResourceNode508", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 120085, + "y": -9957, + "z": 10448, + "rotation": 118.85 + }, + { + "id": "BP_ResourceNode509", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -108120, + "y": 261059, + "z": -4802, + "rotation": 29 + }, + { + "id": "BP_ResourceNode511", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -52587, + "y": 264647, + "z": -3820, + "rotation": 104 + }, + { + "id": "BP_ResourceNode512", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -89046, + "y": 153285, + "z": -3020, + "rotation": -179 + }, + { + "id": "BP_ResourceNode516", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 166452, + "y": 7804, + "z": 15934, + "rotation": 3.72 + }, + { + "id": "BP_ResourceNode521", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -107865, + "y": 60191, + "z": 2653, + "rotation": 21.27 + }, + { + "id": "BP_ResourceNode521_UAID_40B076DF2F79C3E101_1100698081", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -164381, + "y": 64841, + "z": 24380, + "rotation": 31.76 + }, + { + "id": "BP_ResourceNode521_UAID_40B076DF2F79C3E101_1735462083", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -167201, + "y": 61298, + "z": 24001, + "rotation": 42.28 + }, + { + "id": "BP_ResourceNode534", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -55951, + "y": 271565, + "z": -3777, + "rotation": -51 + }, + { + "id": "BP_ResourceNode548", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 3998, + "y": 271121, + "z": -4108, + "rotation": -114.4 + }, + { + "id": "BP_ResourceNode549", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 4128, + "y": 273242, + "z": -4108, + "rotation": 145.57 + }, + { + "id": "BP_ResourceNode54_833", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -41845, + "y": -152412, + "z": 9227, + "rotation": 0 + }, + { + "id": "BP_ResourceNode550", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 16092, + "y": 264153, + "z": -3854, + "rotation": 145.57 + }, + { + "id": "BP_ResourceNode553", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -41301, + "y": 288558, + "z": -2576, + "rotation": 179.81 + }, + { + "id": "BP_ResourceNode554", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -37142, + "y": 201538, + "z": -3066, + "rotation": 170.73 + }, + { + "id": "BP_ResourceNode555", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -62537, + "y": 228043, + "z": -3279, + "rotation": -63.56 + }, + { + "id": "BP_ResourceNode556", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -110522, + "y": 249722, + "z": -5380, + "rotation": 29.5 + }, + { + "id": "BP_ResourceNode557", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -94477, + "y": 228243, + "z": -2399, + "rotation": 52.89 + }, + { + "id": "BP_ResourceNode561", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -55288, + "y": 129003, + "z": 8672, + "rotation": 38.28 + }, + { + "id": "BP_ResourceNode564", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 284986, + "y": 111894, + "z": -1606, + "rotation": -88.48 + }, + { + "id": "BP_ResourceNode571", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -53615, + "y": 160376, + "z": 3356, + "rotation": 38.28 + }, + { + "id": "BP_ResourceNode575", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -80089, + "y": 204747, + "z": 1545, + "rotation": 38.28 + }, + { + "id": "BP_ResourceNode584", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -8701, + "y": 283440, + "z": -1137, + "rotation": -114.56 + }, + { + "id": "BP_ResourceNode586", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -43322, + "y": 239553, + "z": -3851, + "rotation": -130.67 + }, + { + "id": "BP_ResourceNode589", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 8342, + "y": 147572, + "z": -9600, + "rotation": -176.39 + }, + { + "id": "BP_ResourceNode589_UAID_40B076DF2F79B1E101_1767545917", + "resource": "Desc_Stone_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 135005, + "y": 123175, + "z": 12311, + "rotation": -176.37 + }, + { + "id": "BP_ResourceNode589_UAID_40B076DF2F79B2E101_1298360096", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 146861, + "y": 115063, + "z": 12296, + "rotation": -234.67 + }, + { + "id": "BP_ResourceNode59_755", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 20776, + "y": -134970, + "z": 16882, + "rotation": 143.46 + }, + { + "id": "BP_ResourceNode60_984", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 75115, + "y": -123003, + "z": 13630, + "rotation": -28.91 + }, + { + "id": "BP_ResourceNode62", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -227331, + "y": -158278, + "z": 8948, + "rotation": -245.85 + }, + { + "id": "BP_ResourceNode67_2193", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 270772, + "y": 47036, + "z": -1614, + "rotation": 13.89 + }, + { + "id": "BP_ResourceNode69_UAID_A036BCACDEB0A7A601_1261875850", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 225431, + "y": -6949, + "z": -1550, + "rotation": -98.43 + }, + { + "id": "BP_ResourceNode77", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 226320, + "y": -200702, + "z": 1079 + }, + { + "id": "BP_ResourceNode84", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -156729, + "y": 202160, + "z": 961, + "rotation": 3.41 + }, + { + "id": "BP_ResourceNode85", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": -151152, + "y": 184381, + "z": 978, + "rotation": 28.68 + }, + { + "id": "BP_ResourceNode93_5", + "resource": "Desc_Stone_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 219901, + "y": 42217, + "z": -1598, + "rotation": -329.36 + }, + { + "id": "BP_ResourceNode96_886", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 141568, + "y": 230907, + "z": -5835, + "rotation": 1.01 + }, + { + "id": "BP_ResourceNode97_1", + "resource": "Desc_Stone_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Limestone", + "x": 245860, + "y": -218934, + "z": 1312, + "rotation": 56.16 + }, + { + "id": "BP_ResourceNode170_363", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": -41122, + "y": -72631, + "z": 23212, + "rotation": -2.85 + }, + { + "id": "BP_ResourceNode177", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 34655, + "y": 284303, + "z": 905, + "rotation": 0.04 + }, + { + "id": "BP_ResourceNode461", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 128296, + "y": -75942, + "z": 3668, + "rotation": -39.55 + }, + { + "id": "BP_ResourceNode467", + "resource": "Desc_Sulfur_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 92073, + "y": 3164, + "z": 16079, + "rotation": -25 + }, + { + "id": "BP_ResourceNode510", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": -39890, + "y": -93579, + "z": 343, + "rotation": -44.26 + }, + { + "id": "BP_ResourceNode582", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 296500, + "y": 13254, + "z": -882, + "rotation": -96.48 + }, + { + "id": "BP_ResourceNode613", + "resource": "Desc_Sulfur_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 380366, + "y": -109496, + "z": 9237, + "rotation": -226.73 + }, + { + "id": "BP_ResourceNode624", + "resource": "Desc_Sulfur_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 246502, + "y": -277000, + "z": 1499, + "rotation": -179.87 + }, + { + "id": "BP_ResourceNode71_736", + "resource": "Desc_Sulfur_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": -101382, + "y": 91579, + "z": 8665 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F7912DC01_2042985647", + "resource": "Desc_Sulfur_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 188183, + "y": -247468, + "z": 23307, + "rotation": -191.69 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F7923DB01_2085455593", + "resource": "Desc_Sulfur_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 242310, + "y": 152779, + "z": 8888, + "rotation": -0.01 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F7924DB01_1576108771", + "resource": "Desc_Sulfur_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 94847, + "y": 224537, + "z": -86, + "rotation": -161.72 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F7925DB01_1453678949", + "resource": "Desc_Sulfur_C", + "purity": "impure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 91305, + "y": 220217, + "z": -79, + "rotation": -252.5 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F7929DB01_1177072656", + "resource": "Desc_Sulfur_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": 96368, + "y": 220401, + "z": -84, + "rotation": -308.55 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F797CDB01_1695622247", + "resource": "Desc_Sulfur_C", + "purity": "pure", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": -145044, + "y": 165965, + "z": 12699, + "rotation": -25.16 + }, + { + "id": "BP_ResourceNode71_UAID_40B076DF2F79B9DB01_1490254983", + "resource": "Desc_Sulfur_C", + "purity": "normal", + "classPath": "BP_ResourceNode_C", + "nodeType": "node", + "displayName": "Sulfur", + "x": -213023, + "y": -170021, + "z": -1317, + "rotation": -25.29 + } +] diff --git a/src/recipes/WorldResourceNodes.ts b/src/recipes/WorldResourceNodes.ts new file mode 100644 index 00000000..1a079a7b --- /dev/null +++ b/src/recipes/WorldResourceNodes.ts @@ -0,0 +1,159 @@ +import RawWorldResourceNodes from './WorldResourceNodes.json'; + +export type Purity = 'impure' | 'normal' | 'pure'; + +export const PURITIES: Purity[] = ['impure', 'normal', 'pure']; + +/** + * Kinds of extractable deposit the game distinguishes. Matches + * Satisfactory's own actor taxonomy: + * + * - `node` — solid miner-ore nodes (`BP_ResourceNode_C`). + * - `deposit` — breakable rock deposits harvested by the portable + * miner. FicsIt Remote Monitoring exposes these via + * `getResourceDeposit`. + * - `frackingCore` — the central ring of a fracking well that + * powers the surrounding satellites. + * - `frackingSatellite` — the pressurised fluid/gas extraction + * points around a core (`BP_FrackingSatellite_C`). + * - `geyser` — geothermal generator spots + * (`BP_ResourceNodeGeyser_C`). + * + * We don't render geysers today (they're power-only), but the type + * is kept so the importer can round-trip them without information + * loss. + */ +export type WorldResourceNodeType = + | 'node' + | 'deposit' + | 'frackingCore' + | 'frackingSatellite' + | 'geyser'; + +export interface WorldResourceNode { + /** + * Satisfactory actor id — the exact string the game uses internally + * for this deposit (e.g. `BP_ResourceNode573_UAID_40B076...`). Also + * serves as the key for "used" marks so a future savegame parser + * can match up what the player has built on without translation. + */ + id: string; + /** Item id matching `AllFactoryItemsMap`, e.g. `Desc_OreIron_C`. */ + resource: string; + /** + * Full blueprint class path of the spawning actor + * (e.g. `BP_ResourceNode_C`). Preserved for future savegame parsing + * since the same field appears in the `.sav` object header. + */ + classPath?: string; + /** Which extractor family this deposit belongs to. */ + nodeType: WorldResourceNodeType; + /** Display name as shown in-game (e.g. "Iron Ore", "Crude Oil"). */ + displayName?: string; + purity: Purity; + /** Game-world coordinates in centimeters (Unreal default unit). */ + x: number; + y: number; + z?: number; + /** Yaw rotation in degrees, as reported by the source dataset. */ + rotation?: number; + /** + * Where the data point came from: the bundled curated dataset, or + * extracted from a user's savegame in a future iteration. + */ + source: 'static' | 'savegame'; +} + +interface RawNode { + id: string; + resource: string; + purity: Purity; + classPath?: string; + nodeType?: WorldResourceNodeType; + displayName?: string; + x: number; + y: number; + z?: number; + rotation?: number; +} + +const StaticWorldResourceNodes: WorldResourceNode[] = ( + RawWorldResourceNodes as RawNode[] +).map(node => ({ + ...node, + // Older bundled datasets (pre-schema-v2) lacked `nodeType`. Fall + // back to `'node'` so those entries remain renderable. + nodeType: node.nodeType ?? 'node', + source: 'static' as const, +})); + +/** + * Static `id` → node lookup. Used by call sites that have a node id + * from a savegame (e.g. an extractor's `mExtractableResource`) and + * want to resolve it to its resource type / display name without + * scanning the full array. Excludes savegame overrides; consumers + * that need them should layer those on top. + */ +export const StaticWorldResourceNodesById: Record = + StaticWorldResourceNodes.reduce( + (acc, node) => { + acc[node.id] = node; + return acc; + }, + {} as Record, + ); + +/** + * Per-game savegame-derived overrides. Returns an empty list today; the + * savegame parser will populate this in a future iteration. Kept as a + * stable hook so the consumer (`getWorldResourceNodes`) does not change + * shape when that lands. + */ +export function getSavegameOverrides( + _gameId?: string | null, +): WorldResourceNode[] { + return []; +} + +/** + * Node types intentionally hidden from the map UI. Kept out at the + * top-level accessor so the rest of the app (filters, popovers, sum + * mode) never has to special-case them. + * + * - `frackingCore` — the well's anchor point. Players don't extract + * from it directly; they place a Resource Well Pressurizer here to + * activate the surrounding satellites. The satellites themselves + * already mark every extraction location, so cores are noise. + * + * The underlying dataset still includes these (the parser keeps the + * full world picture so future features — e.g. drawing lines from + * satellites to their parent core — can opt back in via a separate + * accessor). + */ +const HIDDEN_NODE_TYPES: ReadonlySet = new Set([ + 'frackingCore', +]); + +/** + * Returns the list of world resource nodes to render on the map for the + * given game. Falls back to the bundled static dataset; per-game + * savegame-derived nodes (when present) supersede static entries with + * matching ids. Node types in {@link HIDDEN_NODE_TYPES} are filtered + * out here so callers get a clean "what to render" list. + */ +export function getWorldResourceNodes( + gameId?: string | null, +): WorldResourceNode[] { + const overrides = getSavegameOverrides(gameId); + const merged = + overrides.length === 0 + ? StaticWorldResourceNodes + : (() => { + const overrideIds = new Set(overrides.map(n => n.id)); + return [ + ...StaticWorldResourceNodes.filter(n => !overrideIds.has(n.id)), + ...overrides, + ]; + })(); + return merged.filter(n => !HIDDEN_NODE_TYPES.has(n.nodeType)); +} diff --git a/src/recipes/savegame/ImportSavegameRecipesModal.tsx b/src/recipes/savegame/ImportSavegameRecipesModal.tsx index 7b053e42..48b1c69a 100644 --- a/src/recipes/savegame/ImportSavegameRecipesModal.tsx +++ b/src/recipes/savegame/ImportSavegameRecipesModal.tsx @@ -1,8 +1,8 @@ import { Button, - Checkbox, Divider, FileButton, + List, Modal, Progress, Stack, @@ -10,56 +10,41 @@ import { Tooltip, } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { notifications } from '@mantine/notifications'; import { IconCloudUpload } from '@tabler/icons-react'; -import { useState } from 'react'; import type { ParsedSatisfactorySave } from './ParseSavegameMessages'; -import { startSavegameParsing } from './startSavegameParsing'; export interface IImportSavegameModalProps { - onImported?: (save: ParsedSatisfactorySave, asDefault: boolean) => void; + /** Whether an import is currently running (drives the loading UI). */ + importing: boolean; + /** Progress fraction (0-1) and optional status message. */ + progress: { value: number; message?: string }; + /** + * Picks the file and runs the import via the parent (which owns + * the {@link useSavegameImport} hook). Resolves with the parsed + * save on success so the modal knows when to close, or `null` if + * the parent aborted (e.g. no game selected). + */ + onImport: (file: File) => Promise; } export function ImportSavegameRecipesModal(props: IImportSavegameModalProps) { const [opened, { toggle, close }] = useDisclosure(); - const [importing, setImporting] = useState(false); - const [asDefault, setAsDefault] = useState(true); - const [progress, setProgress] = useState({ - value: 0, - message: undefined as string | undefined, - }); const handleImport = (file: File) => { - setImporting(true); - setProgress({ value: 0, message: undefined }); - startSavegameParsing(file, (progress, message) => { - setProgress({ value: progress, message }); - }) + props + .onImport(file) .then(save => { - // console.log('Parsed:', save.json); - setImporting(false); - notifications.show({ - title: 'Savegame imported', - message: 'Savegame recipes have been imported successfully', - color: 'green', - }); - props.onImported?.(save, asDefault); - close(); + if (save) close(); }) - .catch(e => { - console.error('Error while parsing:', e.message); - setImporting(false); - notifications.show({ - title: 'Error while parsing savegame', - message: e.message, - color: 'red', - }); + .catch(() => { + // Notification surfaced by the parent's hook; keep the modal + // open so the user can retry with a different file. }); }; return ( <> - + )} - {importing && ( + {props.importing && ( <> - - {progress.message && {progress.message}} + + {props.progress.message && ( + {props.progress.message} + )} )} diff --git a/src/recipes/savegame/ParseSavegameMessages.ts b/src/recipes/savegame/ParseSavegameMessages.ts index 42239933..922bac0d 100644 --- a/src/recipes/savegame/ParseSavegameMessages.ts +++ b/src/recipes/savegame/ParseSavegameMessages.ts @@ -1,13 +1,180 @@ export type IParseSavegameRequest = { type: 'parse'; file: File; + extractInfrastructure?: boolean; }; +export type InfrastructureCategory = + | 'production' + | 'logistics' + | 'power' + | 'storage' + | 'transport' + | 'foundation' + | 'decor' + | 'other'; + +export const INFRASTRUCTURE_CATEGORIES: InfrastructureCategory[] = [ + 'production', + 'logistics', + 'power', + 'storage', + 'transport', + 'foundation', + 'decor', + 'other', +]; + +export type SplineKind = 'belt' | 'pipe' | 'hyper' | 'rail' | 'power'; + +export const SPLINE_KINDS: SplineKind[] = [ + 'belt', + 'pipe', + 'hyper', + 'rail', + 'power', +]; + +/** + * Parallel arrays representing all standalone buildings (anything with a + * footprint that is not a spline-shaped connector). Indexed 0..count-1. + */ +export interface InfrastructureBuildingsBlock { + count: number; + /** Index into INFRASTRUCTURE_CATEGORIES. */ + categories: Uint8Array; + /** count*2, world cm. */ + positionsXY: Float32Array; + /** + * count, world cm. Z translation (the building's base elevation). + * Used together with {@link heights} to pick the topmost building + * under the cursor — a constructor stacked on a foundation should + * win the hit over the foundation it sits on. + */ + positionsZ: Float32Array; + /** count, radians (yaw around vertical axis, world frame). */ + yaw: Float32Array; + /** count*2, width and length in cm (footprint). */ + sizeWL: Float32Array; + /** count, height of the building in cm (clearance.height). */ + heights: Float32Array; + /** + * Per-building typePath (e.g. `Build_AssemblerMk1_C`), used for the + * hover popover's display name. Stored as a parallel string array + * rather than a transferable view because each unique typePath is a + * shared interned string in the worker, so the structured-clone copy + * is cheap (one pointer per building). + */ + typePaths: string[]; + /** + * Per-building overclock multiplier (`mCurrentPotential` in the save: + * 1.0 = 100%, 1.5 = 150% / overclocked, 0.5 = 50% / underclocked). + * `NaN` for entities that don't expose the property (foundations, + * decor, every instance inside the lightweight buildable subsystem). + * The hover popover only renders the value when it's a finite + * number that isn't ~1.0. + */ + overclocks: Float32Array; + /** + * Per-building somersloop / production-shard count + * (`mAddedSomersloops` in the save). 0 for entities without slots + * filled. The popover renders this as `Somersloop ×N` when N > 0. + */ + somersloops: Uint8Array; + /** + * Per-building recipe id selected in the production machine + * (last segment of `mCurrentRecipeRef.pathName`, e.g. + * `Recipe_IronPlate_C`). Empty string when the entity has no + * recipe slot or hasn't been set. + */ + recipes: string[]; + /** + * Per-building resource node id this entity extracts from + * (last segment of `mExtractableResource.value.pathName`, e.g. + * `BP_ResourceDeposit1642`). Empty string for non-extractor + * entities. Water pumps point at `FGWaterVolume_*` entries which + * aren't in the static node dataset; the hover popover special- + * cases the typePath to render "Water" for those instead of + * trying to resolve the id. + */ + extractedNodes: string[]; +} + +/** + * One block per (kind, tier) pair. `offsets` is a CSR-style index: + * polyline `i` occupies pointsXY[offsets[i]*2 .. offsets[i+1]*2]. + */ +export interface InfrastructureSplinesBlock { + kind: SplineKind; + /** 0 when the kind has no tiering (rail, power). */ + tier: number; + count: number; + /** count+1, point indices (NOT byte offsets). */ + offsets: Uint32Array; + /** Total points across all polylines, *2, world cm. */ + pointsXY: Float32Array; + /** + * count*4 — per-polyline axis-aligned bounding box in world cm, + * laid out as `[minX, minY, maxX, maxY]` per polyline. Computed + * once during {@link import('./infrastructure/extractInfrastructure').finalizeInfrastructure} + * so the canvas layer can viewport-cull polylines without + * re-scanning their points every frame. + */ + polylineBounds: Float32Array; + /** + * `null` when the source has no Hermite tangents (power-line wires); + * otherwise `totalPoints*4` floats in world cm per point, laid out + * `[arriveX, arriveY, leaveX, leaveY]`. Lets the canvas layer turn + * each segment into a `bezierCurveTo` so curved track / belt + * sections render as actual curves instead of straight chords + * between control points. + */ + tangentsXY: Float32Array | null; +} + +export interface ParsedInfrastructure { + buildings: InfrastructureBuildingsBlock; + splines: InfrastructureSplinesBlock[]; + counts: Record; + splineCounts: Record; +} + +export interface ParsedPlayerPosition { + x: number; + y: number; + z: number; +} + export interface ParsedSatisfactorySave { /** * Includes all recipes that are available in the savegame, even buildings. */ availableRecipes: string[]; + /** + * Resource node ids (matching `WorldResourceNodes.json`'s `id` field) + * that have a miner / oil pump / fracking extractor placed on them in + * the save. Intended to be written straight into + * `games[gameId].usedNodes` so imported saves light up the map's + * used-node state. Water pumps are intentionally excluded: they sit + * in `FGWaterVolume_*` actors rather than on `BP_ResourceNode_*` so + * their `mExtractableResource` pathName does not map to a node in + * our static dataset. + */ + usedNodeIds: string[]; + /** + * World-cm positions of every `Char_Player_C` actor in the save. + * Empty array when no player has spawned yet (rare). Used to center + * the map view on the host on import and to render a Player marker + * on the canvas. In-memory only, never persisted. + */ + players: ParsedPlayerPosition[]; + /** + * User-built infrastructure (buildings + spline networks) packed into + * typed arrays. Only present when the request had + * `extractInfrastructure: true`. Held in memory for the session only, + * never persisted. + */ + infrastructure?: ParsedInfrastructure; } export type IParseSavegameResponse = @@ -24,3 +191,33 @@ export type IParseSavegameResponse = type: 'error'; message: string; }; + +/** + * Returns every transferable ArrayBuffer inside a `ParsedInfrastructure`, + * to be passed as the second argument of `worker.postMessage` (and the + * `transfer` option of `structuredClone`-style APIs) for zero-copy + * delivery to the main thread. + */ +export function collectInfrastructureTransferables( + infra: ParsedInfrastructure, +): ArrayBuffer[] { + const buffers: ArrayBuffer[] = [ + infra.buildings.categories.buffer as ArrayBuffer, + infra.buildings.positionsXY.buffer as ArrayBuffer, + infra.buildings.positionsZ.buffer as ArrayBuffer, + infra.buildings.yaw.buffer as ArrayBuffer, + infra.buildings.sizeWL.buffer as ArrayBuffer, + infra.buildings.heights.buffer as ArrayBuffer, + infra.buildings.overclocks.buffer as ArrayBuffer, + infra.buildings.somersloops.buffer as ArrayBuffer, + ]; + for (const spline of infra.splines) { + buffers.push(spline.offsets.buffer as ArrayBuffer); + buffers.push(spline.pointsXY.buffer as ArrayBuffer); + buffers.push(spline.polylineBounds.buffer as ArrayBuffer); + if (spline.tangentsXY) { + buffers.push(spline.tangentsXY.buffer as ArrayBuffer); + } + } + return buffers; +} diff --git a/src/recipes/savegame/infrastructure/classifyTypePath.test.ts b/src/recipes/savegame/infrastructure/classifyTypePath.test.ts new file mode 100644 index 00000000..4c537c8a --- /dev/null +++ b/src/recipes/savegame/infrastructure/classifyTypePath.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { classifyTypePath } from './classifyTypePath'; + +describe('classifyTypePath', () => { + it('classifies belts and parses Mk tier', () => { + expect( + classifyTypePath( + '/Game/FactoryGame/Buildable/Factory/ConveyorBeltMk5/Build_ConveyorBeltMk5.Build_ConveyorBeltMk5_C', + ), + ).toEqual({ mode: 'spline', kind: 'belt', tier: 5 }); + }); + + it('defaults belt tier to 1 when the suffix is absent', () => { + expect( + classifyTypePath('/Game/.../Build_ConveyorBelt.Build_ConveyorBelt_C'), + ).toEqual({ mode: 'spline', kind: 'belt', tier: 1 }); + }); + + it('classifies pipes (Mk1 default and Mk2)', () => { + expect( + classifyTypePath('/Game/.../Pipeline/Build_Pipeline.Build_Pipeline_C'), + ).toEqual({ mode: 'spline', kind: 'pipe', tier: 1 }); + expect( + classifyTypePath( + '/Game/.../PipelineMk2/Build_PipelineMK2.Build_PipelineMK2_C', + ), + ).toEqual({ mode: 'spline', kind: 'pipe', tier: 2 }); + expect( + classifyTypePath( + '/Game/.../Pipeline/Build_Pipeline_NoIndicator.Build_Pipeline_NoIndicator_C', + ), + ).toEqual({ mode: 'spline', kind: 'pipe', tier: 1 }); + }); + + it('classifies hyper tubes before falling through to the generic pipe regex', () => { + expect( + classifyTypePath('/Game/.../Build_PipelineHyper.Build_PipelineHyper_C'), + ).toEqual({ mode: 'spline', kind: 'hyper', tier: 0 }); + }); + + it('classifies railroad tracks (regular and integrated)', () => { + expect( + classifyTypePath('/Game/.../Build_RailroadTrack.Build_RailroadTrack_C'), + ).toEqual({ mode: 'spline', kind: 'rail', tier: 0 }); + expect( + classifyTypePath( + '/Game/.../Build_RailroadTrackIntegrated.Build_RailroadTrackIntegrated_C', + ), + ).toEqual({ mode: 'spline', kind: 'rail', tier: 0 }); + }); + + it('classifies power lines', () => { + expect( + classifyTypePath('/Game/.../Build_PowerLine.Build_PowerLine_C'), + ).toEqual({ mode: 'powerline' }); + }); + + it('falls back to building for everything else', () => { + expect( + classifyTypePath( + '/Game/.../AssemblerMk1/Build_AssemblerMk1.Build_AssemblerMk1_C', + ), + ).toEqual({ mode: 'building' }); + expect( + classifyTypePath( + '/Game/.../Foundation/Build_Foundation_8x1_01.Build_Foundation_8x1_01_C', + ), + ).toEqual({ mode: 'building' }); + }); +}); diff --git a/src/recipes/savegame/infrastructure/classifyTypePath.ts b/src/recipes/savegame/infrastructure/classifyTypePath.ts new file mode 100644 index 00000000..6a606cc6 --- /dev/null +++ b/src/recipes/savegame/infrastructure/classifyTypePath.ts @@ -0,0 +1,37 @@ +import type { Classification } from './types'; + +const RE_BELT = /Build_ConveyorBelt(?:Mk(\d+))?_C$/; +const RE_HYPER = /Build_PipelineHyper.*_C$/; +const RE_PIPE = /Build_Pipeline(?:MK(\d+))?(?:_NoIndicator)?_C$/; +const RE_RAIL = /Build_RailroadTrack(?:Integrated)?_C$/; +const RE_POWER_LINE = /Build_PowerLine.*_C$/; + +/** + * Decides how the worker should treat an entity given its `typePath`. + * The order of checks matters: hyper tubes have to be checked before + * the generic pipe regex (`Build_PipelineHyper*` would otherwise + * match `Build_Pipeline*`), and rails before "anything else", because + * the network branches each have their own data layout downstream. + */ +export function classifyTypePath(typePath: string): Classification { + const belt = typePath.match(RE_BELT); + if (belt) { + const tier = belt[1] ? Number.parseInt(belt[1], 10) : 1; + return { mode: 'spline', kind: 'belt', tier }; + } + if (RE_HYPER.test(typePath)) { + return { mode: 'spline', kind: 'hyper', tier: 0 }; + } + const pipe = typePath.match(RE_PIPE); + if (pipe) { + const tier = pipe[1] ? Number.parseInt(pipe[1], 10) : 1; + return { mode: 'spline', kind: 'pipe', tier }; + } + if (RE_RAIL.test(typePath)) { + return { mode: 'spline', kind: 'rail', tier: 0 }; + } + if (RE_POWER_LINE.test(typePath)) { + return { mode: 'powerline' }; + } + return { mode: 'building' }; +} diff --git a/src/recipes/savegame/infrastructure/extractInfrastructure.ts b/src/recipes/savegame/infrastructure/extractInfrastructure.ts new file mode 100644 index 00000000..e5b41ea0 --- /dev/null +++ b/src/recipes/savegame/infrastructure/extractInfrastructure.ts @@ -0,0 +1,453 @@ +import type { SatisfactorySave } from '@etothepii/satisfactory-file-parser'; +import { categoryFor } from '@/map/infrastructure/infrastructureCategories'; +import { quaternionToYaw } from '@/map/infrastructure/quaternion'; +import { + INFRASTRUCTURE_CATEGORIES, + type InfrastructureBuildingsBlock, + type InfrastructureCategory, + type InfrastructureSplinesBlock, + type ParsedInfrastructure, + SPLINE_KINDS, + type SplineKind, +} from '../ParseSavegameMessages'; +import { classifyTypePath } from './classifyTypePath'; +import { getClearance } from './getClearance'; +import { buildAbsolutePolyline, buildRotatedPolyline } from './polyline'; +import { readPowerLineWires, readSplineLocations } from './readSaveProperties'; +import { + type BuildableSubsystemLike, + LIGHTWEIGHT_SUBSYSTEM_TYPEPATH, + type SaveEntityLike, + type Vec4, +} from './types'; + +interface SplineBucket { + kind: SplineKind; + tier: number; + /** Each polyline is a flat [x0,y0,x1,y1,...] number[]. */ + polylines: number[][]; + /** + * Per-polyline Hermite tangents, layout `[aX, aY, lX, lY]` per point. + * `null` when the polyline came from a source without tangents + * (power-line wires, fallback lineTo paths). + */ + tangents: (number[] | null)[]; +} + +/** + * Mutable state passed across {@link ingestEntity} calls during a + * streaming parse. The worker creates one of these via + * {@link createInfrastructureAccumulator}, feeds each `SaveEntity` from + * the JSON stream into {@link ingestEntity}, then converts the final + * state into typed arrays via {@link finalizeInfrastructure}. + */ +export interface InfrastructureAccumulator { + splineBuckets: Map; + counts: Record; + splineCounts: Record; + bCategories: number[]; + bPositionsX: number[]; + bPositionsY: number[]; + bPositionsZ: number[]; + bYaws: number[]; + bSizesW: number[]; + bSizesL: number[]; + bHeights: number[]; + bTypePaths: string[]; + /** Overclock multiplier per building (NaN when not applicable). */ + bOverclocks: number[]; + /** Somersloop count per building (0 when not applicable). */ + bSomersloops: number[]; + /** Selected recipe id per building (empty string when none). */ + bRecipes: string[]; + /** Resource node id this building extracts from (empty string when + * the entity isn't an extractor). */ + bExtractedNodes: string[]; +} + +function emptyCategoryCounts(): Record { + const counts = {} as Record; + for (const cat of INFRASTRUCTURE_CATEGORIES) counts[cat] = 0; + return counts; +} + +function emptySplineCounts(): Record { + const counts = {} as Record; + for (const kind of SPLINE_KINDS) counts[kind] = 0; + return counts; +} + +export function createInfrastructureAccumulator(): InfrastructureAccumulator { + return { + splineBuckets: new Map(), + counts: emptyCategoryCounts(), + splineCounts: emptySplineCounts(), + bCategories: [], + bPositionsX: [], + bPositionsY: [], + bPositionsZ: [], + bYaws: [], + bSizesW: [], + bSizesL: [], + bHeights: [], + bTypePaths: [], + bOverclocks: [], + bSomersloops: [], + bRecipes: [], + bExtractedNodes: [], + }; +} + +function getOrCreateBucket( + acc: InfrastructureAccumulator, + kind: SplineKind, + tier: number, +): SplineBucket { + const key = `${kind}|${tier}`; + let bucket = acc.splineBuckets.get(key); + if (!bucket) { + bucket = { kind, tier, polylines: [], tangents: [] }; + acc.splineBuckets.set(key, bucket); + } + return bucket; +} + +function pushBuilding( + acc: InfrastructureAccumulator, + typePath: string, + tx: number, + ty: number, + tz: number, + yaw: number, + machine: MachineDetails | null = null, +): void { + const category = categoryFor(typePath); + const { width, length, height } = getClearance(typePath); + acc.bCategories.push(INFRASTRUCTURE_CATEGORIES.indexOf(category)); + acc.bPositionsX.push(tx); + acc.bPositionsY.push(ty); + acc.bPositionsZ.push(tz); + acc.bYaws.push(yaw); + acc.bSizesW.push(width); + acc.bSizesL.push(length); + acc.bHeights.push(height); + acc.bTypePaths.push(typePath); + acc.bOverclocks.push(machine?.overclock ?? Number.NaN); + acc.bSomersloops.push(machine?.somersloop ?? 0); + acc.bRecipes.push(machine?.recipe ?? ''); + acc.bExtractedNodes.push(machine?.extractedNode ?? ''); + acc.counts[category]++; +} + +interface MachineDetails { + overclock: number; + somersloop: number; + recipe: string; + extractedNode: string; +} + +/** + * Pulls overclock / somersloop / recipe out of a `SaveEntity`'s + * `properties` bag, when the building is the kind that exposes them. + * Returns null when none of the three are set, so the caller can skip + * the per-building entry. Failures (missing fields, malformed values) + * silently fall back to defaults — every machine in a save has a + * different mix of these properties depending on its tier and on + * whether the player ever touched it. + */ +function readNumberProperty(value: unknown): number | null { + const v = (value as { value?: unknown })?.value; + return typeof v === 'number' && Number.isFinite(v) ? v : null; +} + +function readObjectPathName(value: unknown): string | null { + const v = (value as { value?: { pathName?: unknown } })?.value?.pathName; + return typeof v === 'string' && v.length > 0 ? v : null; +} + +function readMachineDetails( + properties: Record | undefined, +): MachineDetails | null { + if (!properties) return null; + + // Overclock — `mCurrentPotential` is the live value, `mPendingPotential` + // is the slider position the player set; we prefer the live value + // and fall back so a freshly-cranked machine still shows something. + const potential = + readNumberProperty(properties.mCurrentPotential) ?? + readNumberProperty(properties.mPendingPotential); + + // Somersloop / production shard count. The save format has been + // through several renames across U1.0 / U1.1 builds; try them all. + const somersloops = + readNumberProperty(properties.mProductionShardCount) ?? + readNumberProperty(properties.mAddedSomersloops) ?? + readNumberProperty(properties.mNumOccupiedAlienOrgan); + + // Selected recipe — `mCurrentRecipe` is the canonical + // `FGBuildableManufacturer` field, but a few save versions / mods + // expose it as `mCurrentRecipeRef`. + const recipeRef = + readObjectPathName(properties.mCurrentRecipe) ?? + readObjectPathName(properties.mCurrentRecipeRef); + + // Resource node id — `mExtractableResource` is set on miners, oil + // pumps, fracking extractors and water pumps. The popover uses it + // (with the static `WorldResourceNodesById` lookup) to render the + // "extracts: …" line. + const extractRef = readObjectPathName(properties.mExtractableResource); + + const overclock = potential ?? Number.NaN; + const somersloop = somersloops != null ? somersloops | 0 : 0; + let recipe = ''; + if (recipeRef) { + const tail = recipeRef.split('.').pop(); + if (tail) recipe = tail; + } + let extractedNode = ''; + if (extractRef) { + const tail = extractRef.split('.').pop(); + if (tail) extractedNode = tail; + } + + if ( + Number.isNaN(overclock) && + somersloop === 0 && + recipe === '' && + extractedNode === '' + ) { + return null; + } + return { overclock, somersloop, recipe, extractedNode }; +} + +/** + * Processes a single object from the save (a `SaveEntity` or + * `SaveComponent` shape, as produced by the WHATWG JSON streaming + * parse). Updates the accumulator in place. Non-`SaveEntity` objects + * and non-buildable entities are silently skipped, which matches the + * pre-streaming behaviour of {@link extractInfrastructure}. + * + * Drilling into `FGLightweightBuildableSubsystem.specialProperties. + * buildables[].instances[]` happens here too: in U1.0+ saves + * thousands of foundation/wall/decor instances are aggregated into + * a single SaveEntity rather than appearing as standalone entries. + */ +export function ingestEntity( + acc: InfrastructureAccumulator, + rawObj: unknown, +): void { + const obj = rawObj as SaveEntityLike; + if (obj.type !== 'SaveEntity') return; + const typePath = obj.typePath; + if (typeof typePath !== 'string') return; + + if (typePath === LIGHTWEIGHT_SUBSYSTEM_TYPEPATH) { + const sp = obj.specialProperties as BuildableSubsystemLike | undefined; + if (sp?.type === 'BuildableSubsystemSpecialProperties' && sp.buildables) { + for (const group of sp.buildables) { + const groupTypePath = group.typeReference?.pathName; + if (typeof groupTypePath !== 'string' || !group.instances) continue; + for (const inst of group.instances) { + const itr = inst.transform?.translation; + const itx = typeof itr?.x === 'number' ? itr.x : 0; + const ity = typeof itr?.y === 'number' ? itr.y : 0; + const itz = typeof itr?.z === 'number' ? itr.z : 0; + const irot = inst.transform?.rotation; + const iyaw = + irot && + typeof irot.x === 'number' && + typeof irot.y === 'number' && + typeof irot.z === 'number' && + typeof irot.w === 'number' + ? quaternionToYaw(irot as Vec4) + : 0; + pushBuilding(acc, groupTypePath, itx, ity, itz, iyaw); + } + } + } + return; + } + + if (!typePath.includes('/Buildable/')) return; + + const tr = obj.transform?.translation; + const tx = typeof tr?.x === 'number' ? tr.x : 0; + const ty = typeof tr?.y === 'number' ? tr.y : 0; + const tz = typeof tr?.z === 'number' ? tr.z : 0; + + const rot = obj.transform?.rotation; + const yaw = + rot && + typeof rot.x === 'number' && + typeof rot.y === 'number' && + typeof rot.z === 'number' && + typeof rot.w === 'number' + ? quaternionToYaw(rot as Vec4) + : 0; + + const cls = classifyTypePath(typePath); + + if (cls.mode === 'spline') { + const points = readSplineLocations(obj.properties); + if (points) { + const bucket = getOrCreateBucket(acc, cls.kind, cls.tier); + const built = buildRotatedPolyline(tx, ty, yaw, points); + bucket.polylines.push(built.flat); + bucket.tangents.push(built.tangents); + acc.splineCounts[cls.kind]++; + return; + } + // mSplineData missing: fall through to building treatment so + // the entity still appears as a marker on the map. + } else if (cls.mode === 'powerline') { + const wires = readPowerLineWires(obj.properties); + if (wires.length > 0) { + const bucket = getOrCreateBucket(acc, 'power', 0); + for (const wire of wires) { + const built = buildAbsolutePolyline(wire); + bucket.polylines.push(built.flat); + bucket.tangents.push(built.tangents); + acc.splineCounts.power++; + } + return; + } + // No wire data: render as building marker so it isn't lost. + } + + // Production-machine bookkeeping (overclock / somersloop / recipe). + // Lightweight-subsystem instances (foundations, walls, decor, …) are + // bare transforms with no `properties` bag, so they never surface + // these — `readMachineDetails(undefined)` returns null and + // pushBuilding falls back to the defaults. + const machine = readMachineDetails(obj.properties); + pushBuilding(acc, typePath, tx, ty, tz, yaw, machine); +} + +/** + * Converts the JS-array accumulators into the flat typed-array layout + * the canvas layer renders directly. Splits the spline buckets into + * one block per (kind, tier) pair with a CSR offset index. + */ +export function finalizeInfrastructure( + acc: InfrastructureAccumulator, +): ParsedInfrastructure { + const count = acc.bYaws.length; + const positionsXY = new Float32Array(count * 2); + const sizeWL = new Float32Array(count * 2); + for (let i = 0; i < count; i++) { + positionsXY[i * 2] = acc.bPositionsX[i]; + positionsXY[i * 2 + 1] = acc.bPositionsY[i]; + sizeWL[i * 2] = acc.bSizesW[i]; + sizeWL[i * 2 + 1] = acc.bSizesL[i]; + } + const buildings: InfrastructureBuildingsBlock = { + count, + categories: Uint8Array.from(acc.bCategories), + positionsXY, + positionsZ: Float32Array.from(acc.bPositionsZ), + yaw: Float32Array.from(acc.bYaws), + sizeWL, + heights: Float32Array.from(acc.bHeights), + typePaths: acc.bTypePaths, + overclocks: Float32Array.from(acc.bOverclocks), + somersloops: Uint8Array.from(acc.bSomersloops), + recipes: acc.bRecipes, + extractedNodes: acc.bExtractedNodes, + }; + + const splines: InfrastructureSplinesBlock[] = []; + for (const bucket of acc.splineBuckets.values()) { + const polylineCount = bucket.polylines.length; + const offsets = new Uint32Array(polylineCount + 1); + let totalPoints = 0; + for (let i = 0; i < polylineCount; i++) { + offsets[i] = totalPoints; + totalPoints += bucket.polylines[i].length / 2; + } + offsets[polylineCount] = totalPoints; + const pointsXY = new Float32Array(totalPoints * 2); + const polylineBounds = new Float32Array(polylineCount * 4); + // A bucket is homogeneous: all polylines either come with Hermite + // tangents (mSplineData) or none do (power-line wires). Probing the + // first entry is enough. + const hasTangents = polylineCount > 0 && bucket.tangents[0] != null; + const tangentsXY = hasTangents ? new Float32Array(totalPoints * 4) : null; + let cursor = 0; + for (let i = 0; i < polylineCount; i++) { + const flat = bucket.polylines[i]; + pointsXY.set(flat, cursor); + if (tangentsXY) { + const t = bucket.tangents[i]; + if (t) tangentsXY.set(t, cursor * 2); + } + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (let j = 0; j < flat.length; j += 2) { + const x = flat[j]; + const y = flat[j + 1]; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + polylineBounds[i * 4] = minX; + polylineBounds[i * 4 + 1] = minY; + polylineBounds[i * 4 + 2] = maxX; + polylineBounds[i * 4 + 3] = maxY; + cursor += flat.length; + } + splines.push({ + kind: bucket.kind, + tier: bucket.tier, + count: polylineCount, + offsets, + pointsXY, + polylineBounds, + tangentsXY, + }); + } + + return { + buildings, + splines, + counts: acc.counts, + splineCounts: acc.splineCounts, + }; +} + +/** + * Walks every level / object in the save once, classifies each entity, + * and produces a payload of typed arrays the canvas layer can render + * without any further per-entity work. Splits into three branches: + * + * - Spline networks (belts, pipes, hyper tubes, rails): read + * `mSplineData` and rotate/translate the local-frame points into + * world coordinates. + * - Power lines: read `mWireInstances` (already in world space). + * - Everything else: a footprinted building, with per-entity + * position / rotation / size / typePath captured for hit-testing. + * + * Foundations / walls / decor live inside a single + * `FGLightweightBuildableSubsystem` SaveEntity in U1.0+ saves; the + * loop drills into its `BuildableSubsystemSpecialProperties.buildables` + * and treats each instance as a regular building. + * + * Thin wrapper around the streaming-friendly accumulator API + * ({@link createInfrastructureAccumulator}, {@link ingestEntity}, + * {@link finalizeInfrastructure}) used by tests and any callsite that + * already has the full save in memory. + */ +export function extractInfrastructure( + save: SatisfactorySave, +): ParsedInfrastructure { + const acc = createInfrastructureAccumulator(); + for (const level of Object.values(save.levels)) { + for (const obj of level.objects) { + ingestEntity(acc, obj); + } + } + return finalizeInfrastructure(acc); +} diff --git a/src/recipes/savegame/infrastructure/getClearance.test.ts b/src/recipes/savegame/infrastructure/getClearance.test.ts new file mode 100644 index 00000000..80805b77 --- /dev/null +++ b/src/recipes/savegame/infrastructure/getClearance.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { getClearance } from './getClearance'; + +describe('getClearance', () => { + it('returns hand-tuned values for hard-coded connector ids', () => { + expect( + getClearance( + '/Game/.../ConveyorPole/Build_ConveyorPole.Build_ConveyorPole_C', + ), + ).toEqual({ width: 100, length: 100, height: 200 }); + }); + + it('converts catalog values from metres to centimetres', () => { + // AssemblerMk1 catalog entry is 9 x 16 x 3 (metres). + expect( + getClearance( + '/Game/.../AssemblerMk1/Build_AssemblerMk1.Build_AssemblerMk1_C', + ), + ).toEqual({ width: 900, length: 1600, height: 300 }); + }); + + it('falls back to a 1x1m footprint for poles/supports without a catalog entry', () => { + // PipelineSupport_C exists in the catalog with `null` clearance — + // the small-connector regex picks it up. + expect( + getClearance( + '/Game/.../PipelineSupport/Build_PipelineSupport.Build_PipelineSupport_C', + ), + ).toEqual({ width: 100, length: 100, height: 200 }); + }); + + it('falls back to 8x8m for completely unknown buildables', () => { + expect( + getClearance('/Game/.../Build_TotallyMadeUp.Build_TotallyMadeUp_C'), + ).toEqual({ width: 800, length: 800, height: 200 }); + }); +}); diff --git a/src/recipes/savegame/infrastructure/getClearance.ts b/src/recipes/savegame/infrastructure/getClearance.ts new file mode 100644 index 00000000..359927cd --- /dev/null +++ b/src/recipes/savegame/infrastructure/getClearance.ts @@ -0,0 +1,78 @@ +import { buildingIdFromTypePath } from '@/map/infrastructure/infrastructureCategories'; +import { AllFactoryBuildingsMap } from '@/recipes/FactoryBuilding'; + +export interface ClearanceCm { + width: number; + length: number; + height: number; +} + +const FALLBACK_CLEARANCE_CM = 800; +const FALLBACK_HEIGHT_CM = 200; + +/** Clearance fields in `FactoryBuildings.json` are in metres (game UI + * unit), but the savegame transforms are in centimetres (Unreal native + * unit) — multiply to compare apples to apples. */ +const CLEARANCE_M_TO_CM = 100; + +/** + * Hand-tuned clearances (in cm) for connector buildings whose entry in + * `FactoryBuildings.json` is either absent or has a `null` clearance. + * The default 8x8m fallback paints these as factory-sized blobs over + * the conveyors/pipes they actually serve as 1x1m attach points. + */ +const HARDCODED_CLEARANCE_CM: Record = { + Build_ConveyorPole_C: { width: 100, length: 100, height: 200 }, + Build_ConveyorPoleStackable_C: { width: 100, length: 100, height: 400 }, + Build_ConveyorPoleWall_C: { width: 100, length: 100, height: 100 }, + Build_ConveyorCeilingAttachment_C: { width: 100, length: 100, height: 100 }, + Build_PipelineSupport_C: { width: 100, length: 100, height: 200 }, + Build_PipelineSupportWall_C: { width: 100, length: 100, height: 100 }, + Build_PipelineSupportWallHole_C: { width: 100, length: 100, height: 100 }, + Build_PipelineFlowIndicator_C: { width: 100, length: 100, height: 100 }, +}; + +const SMALL_CLEARANCE_PATTERNS = [/Pole/, /Support/, /FlowIndicator/]; + +/** + * Resolves a building's footprint + height in centimetres, used both + * for rendering the rotated rectangle on canvas and for picking the + * topmost building under the cursor (`z + height`). Lookup order: + * + * 1. Hard-coded overrides for connector-style buildables that have + * `null` or missing entries in the catalog. + * 2. `AllFactoryBuildingsMap` entry, with metres → cm conversion. + * 3. A 1x1m "small connector" fallback for any id matching the + * pole/support/flow-indicator regex (their catalog entries + * consistently arrive with `null` clearance). + * 4. The 8x8m factory default, for unknown buildings. + */ +export function getClearance(typePath: string): ClearanceCm { + const id = buildingIdFromTypePath(typePath); + if (id) { + const override = HARDCODED_CLEARANCE_CM[id]; + if (override) return override; + const b = AllFactoryBuildingsMap[id]; + if (b?.clearance) { + const { width, length, height } = b.clearance; + if (width > 0 && length > 0) { + return { + width: width * CLEARANCE_M_TO_CM, + length: length * CLEARANCE_M_TO_CM, + height: + (typeof height === 'number' && height > 0 + ? height + : FALLBACK_HEIGHT_CM / CLEARANCE_M_TO_CM) * CLEARANCE_M_TO_CM, + }; + } + } + if (SMALL_CLEARANCE_PATTERNS.some(re => re.test(id))) { + return { width: 100, length: 100, height: 200 }; + } + } + return { + width: FALLBACK_CLEARANCE_CM, + length: FALLBACK_CLEARANCE_CM, + height: FALLBACK_HEIGHT_CM, + }; +} diff --git a/src/recipes/savegame/infrastructure/polyline.test.ts b/src/recipes/savegame/infrastructure/polyline.test.ts new file mode 100644 index 00000000..82b8a5d6 --- /dev/null +++ b/src/recipes/savegame/infrastructure/polyline.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; +import { buildAbsolutePolyline, buildRotatedPolyline } from './polyline'; +import type { SplinePoint } from './readSaveProperties'; + +const EPSILON = 1e-6; + +function expectClose(actual: number[], expected: number[]) { + expect(actual.length).toBe(expected.length); + for (let i = 0; i < expected.length; i++) { + expect(Math.abs(actual[i] - expected[i])).toBeLessThan(EPSILON); + } +} + +function point( + x: number, + y: number, + partial: Partial> = {}, +): SplinePoint { + return { + x, + y, + z: partial.z ?? 0, + arriveX: partial.arriveX ?? 0, + arriveY: partial.arriveY ?? 0, + leaveX: partial.leaveX ?? 0, + leaveY: partial.leaveY ?? 0, + }; +} + +describe('buildRotatedPolyline', () => { + it('translates points without rotation when yaw is 0', () => { + const result = buildRotatedPolyline(100, 200, 0, [ + point(0, 0), + point(50, 0), + point(50, 30), + ]); + expectClose(result.flat, [100, 200, 150, 200, 150, 230]); + }); + + it('rotates a +X-aligned segment 90° CCW into a +Y segment', () => { + const result = buildRotatedPolyline(0, 0, Math.PI / 2, [ + point(0, 0), + point(100, 0), + ]); + expectClose(result.flat, [0, 0, 0, 100]); + }); + + it('rotates around the translation, not the local origin', () => { + const result = buildRotatedPolyline(1000, 500, Math.PI, [ + point(0, 0), + point(100, 0), + ]); + expectClose(result.flat, [1000, 500, 900, 500]); + }); + + it('rotates the Hermite tangents but does not translate them', () => { + // 90° CCW: a +X tangent becomes a +Y tangent. Translation is + // applied to locations, never to tangent vectors. + const result = buildRotatedPolyline(1000, 500, Math.PI / 2, [ + point(0, 0, { leaveX: 100, leaveY: 0 }), + point(50, 0, { arriveX: 100, arriveY: 0 }), + ]); + expect(result.tangents).not.toBeNull(); + // Layout: [aX0, aY0, lX0, lY0, aX1, aY1, lX1, lY1]. + expectClose(result.tangents as number[], [0, 0, 0, 100, 0, 100, 0, 0]); + }); +}); + +describe('buildAbsolutePolyline', () => { + it('flattens points to interleaved xy with no transform', () => { + const result = buildAbsolutePolyline([ + { x: -73987.74, y: 229807.58, z: -1583 }, + { x: -72172.83, y: 229337.62, z: -1893 }, + ]); + expect(result.flat).toEqual([-73987.74, 229807.58, -72172.83, 229337.62]); + expect(result.tangents).toBeNull(); + }); + + it('returns an empty array on an empty input', () => { + const result = buildAbsolutePolyline([]); + expect(result.flat).toEqual([]); + expect(result.tangents).toBeNull(); + }); +}); diff --git a/src/recipes/savegame/infrastructure/polyline.ts b/src/recipes/savegame/infrastructure/polyline.ts new file mode 100644 index 00000000..f41a777f --- /dev/null +++ b/src/recipes/savegame/infrastructure/polyline.ts @@ -0,0 +1,58 @@ +import type { SplinePoint } from './readSaveProperties'; +import type { Vec3 } from './types'; + +export interface BuiltPolyline { + /** Flat xy-interleaved canvas-frame point coordinates, length = N*2. */ + flat: number[]; + /** + * Flat tangent buffer in canvas frame, length = N*4. Layout per + * point: `[arriveX, arriveY, leaveX, leaveY]`. `null` when the + * source has no Hermite tangents (e.g. power-line wires), in which + * case the consumer should fall back to `lineTo` segments. + */ + tangents: number[] | null; +} + +/** + * Builds a flat (xy interleaved) polyline by rotating each local-frame + * point by `yaw` (radians, around vertical Z) and translating by + * `(tx, ty)`. The Hermite tangents are rotated too (they're vectors, + * so the translation does not apply). Used for spline data extracted + * in the entity's local frame (mSplineData on belts/pipes/rails). + */ +export function buildRotatedPolyline( + tx: number, + ty: number, + yaw: number, + points: SplinePoint[], +): BuiltPolyline { + const cy = Math.cos(yaw); + const sy = Math.sin(yaw); + const flat = new Array(points.length * 2); + const tangents = new Array(points.length * 4); + for (let i = 0; i < points.length; i++) { + const p = points[i]; + flat[i * 2] = tx + p.x * cy - p.y * sy; + flat[i * 2 + 1] = ty + p.x * sy + p.y * cy; + tangents[i * 4] = p.arriveX * cy - p.arriveY * sy; + tangents[i * 4 + 1] = p.arriveX * sy + p.arriveY * cy; + tangents[i * 4 + 2] = p.leaveX * cy - p.leaveY * sy; + tangents[i * 4 + 3] = p.leaveX * sy + p.leaveY * cy; + } + return { flat, tangents }; +} + +/** + * Builds a flat (xy interleaved) polyline directly from absolute world + * positions. Used for power-line wire instances whose `Locations` are + * already in world space and which have no Hermite tangents (the wire + * sag is rendered as a series of straight chords). + */ +export function buildAbsolutePolyline(points: Vec3[]): BuiltPolyline { + const flat = new Array(points.length * 2); + for (let i = 0; i < points.length; i++) { + flat[i * 2] = points[i].x; + flat[i * 2 + 1] = points[i].y; + } + return { flat, tangents: null }; +} diff --git a/src/recipes/savegame/infrastructure/readSaveProperties.test.ts b/src/recipes/savegame/infrastructure/readSaveProperties.test.ts new file mode 100644 index 00000000..25d48b38 --- /dev/null +++ b/src/recipes/savegame/infrastructure/readSaveProperties.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, it } from 'vitest'; +import { readPowerLineWires, readSplineLocations } from './readSaveProperties'; + +function makeSplinePoint( + x: number, + y: number, + z = 0, + tangents: { + arriveX?: number; + arriveY?: number; + leaveX?: number; + leaveY?: number; + } = {}, +) { + return { + properties: { + Location: { value: { x, y, z } }, + ArriveTangent: { + value: { x: tangents.arriveX ?? 0, y: tangents.arriveY ?? 0, z: 0 }, + }, + LeaveTangent: { + value: { x: tangents.leaveX ?? 0, y: tangents.leaveY ?? 0, z: 0 }, + }, + }, + }; +} + +function makeWireValue(x: number, y: number, z = 0) { + return { value: { x, y, z } }; +} + +const ZERO_TANGENTS = { + arriveX: 0, + arriveY: 0, + leaveX: 0, + leaveY: 0, +}; + +describe('readSplineLocations', () => { + it('returns null when mSplineData is missing', () => { + expect(readSplineLocations(undefined)).toBeNull(); + expect(readSplineLocations({})).toBeNull(); + expect(readSplineLocations({ mSplineData: { values: [] } })).toBeNull(); + }); + + it('returns null when fewer than 2 valid points are present', () => { + expect( + readSplineLocations({ + mSplineData: { values: [makeSplinePoint(0, 0)] }, + }), + ).toBeNull(); + }); + + it('parses the Location.value of each spline point in order', () => { + const result = readSplineLocations({ + mSplineData: { + values: [makeSplinePoint(0, 0), makeSplinePoint(100, 200, 5)], + }, + }); + expect(result).toEqual([ + { x: 0, y: 0, z: 0, ...ZERO_TANGENTS }, + { x: 100, y: 200, z: 5, ...ZERO_TANGENTS }, + ]); + }); + + it('parses ArriveTangent and LeaveTangent when present', () => { + const result = readSplineLocations({ + mSplineData: { + values: [ + makeSplinePoint(0, 0, 0, { leaveX: 100, leaveY: 50 }), + makeSplinePoint(200, 0, 0, { arriveX: 100, arriveY: -50 }), + ], + }, + }); + expect(result).toEqual([ + { x: 0, y: 0, z: 0, arriveX: 0, arriveY: 0, leaveX: 100, leaveY: 50 }, + { + x: 200, + y: 0, + z: 0, + arriveX: 100, + arriveY: -50, + leaveX: 0, + leaveY: 0, + }, + ]); + }); + + it('drops malformed entries while keeping the rest', () => { + const result = readSplineLocations({ + mSplineData: { + values: [ + makeSplinePoint(0, 0), + // Non-numeric and Infinity entries should be skipped. + { properties: { Location: { value: { x: 'oops', y: 1 } } } }, + { + properties: { + Location: { value: { x: 1, y: Number.POSITIVE_INFINITY } }, + }, + }, + makeSplinePoint(50, 50), + ], + }, + }); + expect(result).toEqual([ + { x: 0, y: 0, z: 0, ...ZERO_TANGENTS }, + { x: 50, y: 50, z: 0, ...ZERO_TANGENTS }, + ]); + }); +}); + +describe('readPowerLineWires', () => { + it('returns an empty list when mWireInstances is absent', () => { + expect(readPowerLineWires(undefined)).toEqual([]); + expect(readPowerLineWires({})).toEqual([]); + }); + + it('returns one polyline per wire instance with valid endpoints', () => { + const result = readPowerLineWires({ + mWireInstances: { + values: [ + { + properties: { + Locations: [ + makeWireValue(-73987, 229807, -1583), + makeWireValue(-72172, 229337, -1893), + ], + }, + }, + ], + }, + }); + expect(result).toEqual([ + [ + { x: -73987, y: 229807, z: -1583 }, + { x: -72172, y: 229337, z: -1893 }, + ], + ]); + }); + + it('skips wires with fewer than 2 valid points', () => { + const result = readPowerLineWires({ + mWireInstances: { + values: [ + { properties: { Locations: [makeWireValue(0, 0)] } }, + { + properties: { + Locations: [makeWireValue(1, 2), makeWireValue(3, 4)], + }, + }, + ], + }, + }); + expect(result).toEqual([ + [ + { x: 1, y: 2, z: 0 }, + { x: 3, y: 4, z: 0 }, + ], + ]); + }); +}); diff --git a/src/recipes/savegame/infrastructure/readSaveProperties.ts b/src/recipes/savegame/infrastructure/readSaveProperties.ts new file mode 100644 index 00000000..7f893b7f --- /dev/null +++ b/src/recipes/savegame/infrastructure/readSaveProperties.ts @@ -0,0 +1,120 @@ +import type { Vec3 } from './types'; + +/** + * One point of an Unreal `mSplineData` array: the location plus + * `ArriveTangent` / `LeaveTangent` (Hermite tangents in the entity's + * local frame). The renderer turns the tangents into Bezier control + * points so curved track / belt sections actually look curved instead + * of being approximated by straight chords between control points. + * Tangents default to zero on entries that don't expose them, which + * makes the resulting Bezier collapse to a straight `lineTo`. + */ +export interface SplinePoint { + x: number; + y: number; + z: number; + arriveX: number; + arriveY: number; + leaveX: number; + leaveY: number; +} + +/** + * Reads `properties.mSplineData` (an `ArrayProperty` of + * `SplinePointData`) into a flat list of points + tangents relative to + * the entity's transform. The worker is expected to rotate/translate + * them into world space afterwards. Returns null if the field is + * absent or contains fewer than 2 valid points (a single-point + * "polyline" can't be drawn). + */ +export function readSplineLocations( + properties: Record | undefined, +): SplinePoint[] | null { + const sd = ( + properties as { mSplineData?: { values?: unknown[] } } | undefined + )?.mSplineData; + if (!sd || !Array.isArray(sd.values)) return null; + const out: SplinePoint[] = []; + for (const sp of sd.values) { + const props = ( + sp as + | { + properties?: { + Location?: { value?: Partial }; + ArriveTangent?: { value?: Partial }; + LeaveTangent?: { value?: Partial }; + }; + } + | undefined + )?.properties; + const loc = props?.Location?.value; + if ( + !loc || + typeof loc.x !== 'number' || + typeof loc.y !== 'number' || + !Number.isFinite(loc.x) || + !Number.isFinite(loc.y) + ) { + continue; + } + const arrive = props?.ArriveTangent?.value; + const leave = props?.LeaveTangent?.value; + out.push({ + x: loc.x, + y: loc.y, + z: typeof loc.z === 'number' ? loc.z : 0, + arriveX: + typeof arrive?.x === 'number' && Number.isFinite(arrive.x) + ? arrive.x + : 0, + arriveY: + typeof arrive?.y === 'number' && Number.isFinite(arrive.y) + ? arrive.y + : 0, + leaveX: + typeof leave?.x === 'number' && Number.isFinite(leave.x) ? leave.x : 0, + leaveY: + typeof leave?.y === 'number' && Number.isFinite(leave.y) ? leave.y : 0, + }); + } + return out.length >= 2 ? out : null; +} + +/** + * Reads `properties.mWireInstances[*].properties.Locations` into per- + * wire arrays of absolute world positions. `Locations` are absolute + * (unlike `mSplineData` which is relative); `CachedRelativeLocations` + * is the relative variant and is intentionally ignored. Wires with + * fewer than 2 valid points are dropped. + */ +export function readPowerLineWires( + properties: Record | undefined, +): Vec3[][] { + const wires = ( + properties as { mWireInstances?: { values?: unknown[] } } | undefined + )?.mWireInstances; + if (!wires || !Array.isArray(wires.values)) return []; + const out: Vec3[][] = []; + for (const wire of wires.values) { + const locs = ( + wire as { properties?: { Locations?: unknown[] } } | undefined + )?.properties?.Locations; + if (!Array.isArray(locs)) continue; + const points: Vec3[] = []; + for (const item of locs) { + const v = (item as { value?: Partial } | undefined)?.value; + if ( + !v || + typeof v.x !== 'number' || + typeof v.y !== 'number' || + !Number.isFinite(v.x) || + !Number.isFinite(v.y) + ) { + continue; + } + points.push({ x: v.x, y: v.y, z: typeof v.z === 'number' ? v.z : 0 }); + } + if (points.length >= 2) out.push(points); + } + return out; +} diff --git a/src/recipes/savegame/infrastructure/types.ts b/src/recipes/savegame/infrastructure/types.ts new file mode 100644 index 00000000..962f03bf --- /dev/null +++ b/src/recipes/savegame/infrastructure/types.ts @@ -0,0 +1,63 @@ +import type { SplineKind } from '../ParseSavegameMessages'; + +export interface Vec3 { + x: number; + y: number; + z: number; +} + +export interface Vec4 extends Vec3 { + w: number; +} + +/** + * Result of {@link import('./classifyTypePath').classifyTypePath}. + * Drives the per-entity branch in the worker (extract spline points, + * extract power-line wires, or treat as a footprinted building). + */ +export type Classification = + | { mode: 'spline'; kind: SplineKind; tier: number } + | { mode: 'powerline' } + | { mode: 'building' }; + +/** + * Loose shape of a `SaveEntity` from `@etothepii/satisfactory-file-parser`. + * Kept narrow on purpose: the parser exposes a deeply nested tagged + * union that's painful to thread through; we only read a handful of + * fields and prefer crisp local typing over importing the whole world. + */ +export interface SaveEntityLike { + type?: string; + typePath?: unknown; + transform?: { + translation?: Partial; + rotation?: Partial; + }; + properties?: Record; + specialProperties?: { type?: unknown } & Record; +} + +/** Shape of a single instance inside the lightweight buildable subsystem. */ +export interface BuildableInstanceLike { + transform?: { + translation?: Partial; + rotation?: Partial; + }; +} + +/** + * Shape of `BuildableSubsystemSpecialProperties` as exposed by the + * parser. Used to walk the aggregated foundation/wall/decor list that + * Satisfactory 1.0+ stores under `FGLightweightBuildableSubsystem` + * instead of as standalone SaveEntities. + */ +export interface BuildableSubsystemLike { + type?: string; + buildables?: Array<{ + typeReference?: { pathName?: unknown }; + instances?: BuildableInstanceLike[]; + }>; +} + +export const LIGHTWEIGHT_SUBSYSTEM_TYPEPATH = + '/Script/FactoryGame.FGLightweightBuildableSubsystem'; diff --git a/src/recipes/savegame/inspectSavegame.ts b/src/recipes/savegame/inspectSavegame.ts new file mode 100644 index 00000000..9d6fc3c0 --- /dev/null +++ b/src/recipes/savegame/inspectSavegame.ts @@ -0,0 +1,155 @@ +import type { + ObjectArrayProperty, + ObjectProperty, + SatisfactorySave, +} from '@etothepii/satisfactory-file-parser'; +import type { Vec3 } from './infrastructure/types'; + +/** + * Full Unreal typePath strings for every placeable extractor we + * translate into a "used node" mark. Checked against + * `SaveEntity.typePath`. Water pumps are intentionally excluded: + * their `mExtractableResource` points at `FGWaterVolume_*` actors + * rather than a `BP_ResourceNode_*`, so the id does not line up with + * anything in our `WorldResourceNodes.json` dataset. + * + * Miner tiers are matched by regex at the last segment of the + * typePath (see {@link MINER_LAST_SEGMENT_RE}), so a hypothetical + * future Mk4 will be picked up without a code change. The + * non-miner extractors are listed explicitly since their naming is + * one-offs. + */ +const EXPLICIT_EXTRACTOR_TYPE_PATHS = new Set([ + '/Game/FactoryGame/Buildable/Factory/OilPump/Build_OilPump.Build_OilPump_C', + '/Game/FactoryGame/Buildable/Factory/FrackingExtractor/Build_FrackingExtractor.Build_FrackingExtractor_C', +]); + +/** Matches `Build_MinerMk1_C`, `Build_MinerMk2_C`, ... on the last segment. */ +const MINER_LAST_SEGMENT_RE = /^Build_MinerMk\d+_C$/; + +const RECIPE_MANAGER_TYPEPATH = '/Script/FactoryGame.FGRecipeManager'; + +const PLAYER_TYPEPATH = + '/Game/FactoryGame/Character/Player/Char_Player.Char_Player_C'; + +function isExtractorTypePath(typePath: string): boolean { + if (EXPLICIT_EXTRACTOR_TYPE_PATHS.has(typePath)) return true; + const lastSegment = typePath.split('.').pop(); + return lastSegment != null && MINER_LAST_SEGMENT_RE.test(lastSegment); +} + +interface SaveObjectLike { + typePath?: unknown; + properties?: Record; + transform?: { + translation?: Partial; + }; +} + +/** + * Mutable state passed across {@link inspectObject} calls during a + * streaming parse. The worker creates one of these via + * {@link createInspectAccumulator}, feeds each save object from the + * JSON stream into {@link inspectObject}, then turns the final state + * into the flat shape returned by {@link finalizeInspect}. + */ +export interface InspectAccumulator { + availableRecipes: Set; + usedNodeIds: Set; + /** + * World-cm positions of every `Char_Player_C` actor seen in the + * stream. Plain array (not a Set) so the relative order matches the + * save's level objects: the host's pawn is typically first, which + * gives the camera a deterministic centering target. + */ + players: Vec3[]; +} + +export function createInspectAccumulator(): InspectAccumulator { + return { + availableRecipes: new Set(), + usedNodeIds: new Set(), + players: [], + }; +} + +/** + * Pulls the two pieces of game state we want from a single save + * object: the recipe-manager's `mAvailableRecipes` (one object per + * save) and the `mExtractableResource` reference of every miner / + * oil pump / fracking extractor (one per node). Mutates the + * accumulator in place. Safe to call on any object shape; non- + * matching objects are silently skipped. + */ +export function inspectObject(acc: InspectAccumulator, rawObj: unknown): void { + const obj = rawObj as SaveObjectLike; + if (typeof obj.typePath !== 'string') return; + + if (obj.typePath === PLAYER_TYPEPATH) { + const t = obj.transform?.translation; + if ( + t && + typeof t.x === 'number' && + typeof t.y === 'number' && + typeof t.z === 'number' && + Number.isFinite(t.x) && + Number.isFinite(t.y) && + Number.isFinite(t.z) + ) { + acc.players.push({ x: t.x, y: t.y, z: t.z }); + } + return; + } + + if (obj.typePath === RECIPE_MANAGER_TYPEPATH) { + const prop = obj.properties?.mAvailableRecipes as + | ObjectArrayProperty + | undefined; + const values = prop?.values; + if (Array.isArray(values)) { + for (const v of values) { + const id = v?.pathName?.split('.')[1]; + if (id) acc.availableRecipes.add(id); + } + } + return; + } + + if (!isExtractorTypePath(obj.typePath)) return; + const ref = obj.properties?.mExtractableResource as + | ObjectProperty + | undefined; + const pathName = ref?.value?.pathName; + if (!pathName) return; + const nodeId = pathName.split('.').pop(); + if (nodeId) acc.usedNodeIds.add(nodeId); +} + +export interface InspectSummary { + availableRecipes: string[]; + usedNodeIds: string[]; + players: Vec3[]; +} + +export function finalizeInspect(acc: InspectAccumulator): InspectSummary { + return { + availableRecipes: [...acc.availableRecipes], + usedNodeIds: [...acc.usedNodeIds], + players: acc.players.slice(), + }; +} + +/** + * Thin wrapper around the streaming-friendly accumulator API used + * for tests and the eager-parse fallback: walks every object in the + * fully-parsed save and returns the inspected snapshot. + */ +export function inspectSavegame(save: SatisfactorySave): InspectSummary { + const acc = createInspectAccumulator(); + for (const level of Object.values(save.levels)) { + for (const obj of level.objects) { + inspectObject(acc, obj); + } + } + return finalizeInspect(acc); +} diff --git a/src/recipes/savegame/nodeStreamWebShim.ts b/src/recipes/savegame/nodeStreamWebShim.ts new file mode 100644 index 00000000..20946f52 --- /dev/null +++ b/src/recipes/savegame/nodeStreamWebShim.ts @@ -0,0 +1,14 @@ +// Browser shim for Node's `stream/web`. +// +// `@etothepii/satisfactory-file-parser` calls `require('stream/web')` +// optimistically (Node-first). In the browser Vite externalises the +// import to an empty object instead of throwing, so the library's own +// `web-streams-polyfill` / `globalThis.ReadableStream` fallback never +// fires and `streamWeb.ReadableStream` ends up `undefined`. Aliasing +// `stream/web` to this file in `vite.config.ts` makes the WHATWG +// globals available where the library expects them. +export const ReadableStream = globalThis.ReadableStream; +export const WritableStream = globalThis.WritableStream; +export const TransformStream = globalThis.TransformStream; +export const ByteLengthQueuingStrategy = globalThis.ByteLengthQueuingStrategy; +export const CountQueuingStrategy = globalThis.CountQueuingStrategy; diff --git a/src/recipes/savegame/parseSavegameWorker.ts b/src/recipes/savegame/parseSavegameWorker.ts index 6dc553d0..83e03bf8 100644 --- a/src/recipes/savegame/parseSavegameWorker.ts +++ b/src/recipes/savegame/parseSavegameWorker.ts @@ -1,36 +1,143 @@ -import { - type ObjectArrayProperty, - Parser, - type SatisfactorySave, -} from '@etothepii/satisfactory-file-parser'; +import { ReadableStreamParser } from '@etothepii/satisfactory-file-parser'; +import { JSONParser } from '@streamparser/json'; import { loglev } from '@/core/logger/log'; -import type { - IParseSavegameRequest, - IParseSavegameResponse, +import { + createInfrastructureAccumulator, + finalizeInfrastructure, + ingestEntity, +} from './infrastructure/extractInfrastructure'; +import { + createInspectAccumulator, + finalizeInspect, + inspectObject, +} from './inspectSavegame'; +import { + collectInfrastructureTransferables, + type IParseSavegameRequest, + type IParseSavegameResponse, + type ParsedSatisfactorySave, } from './ParseSavegameMessages'; +import { installSatisfactoryParserPatches } from './parserPatches'; + +installSatisfactoryParserPatches(); const logger = loglev.getLogger('parse-savegame'); -async function parseSavegame(file: File) { +// `postMessage` inside a Worker module accepts a `transfer` array, but +// the default DOM lib in tsconfig types it as the window-scoped variant +// (which expects a `targetOrigin` string). Locally re-typed to avoid +// pulling the WebWorker lib into the whole project. +const workerPostMessage = postMessage as ( + message: IParseSavegameResponse, + transfer?: ArrayBuffer[], +) => void; + +/** + * Streaming-friendly parse: pipes the parser library's + * `ReadableStream` of JSON through a SAX-style + * `JSONParser` TransformStream that emits one fully-formed object + * per `levels.*.objects.*` match. Each object is fed into the + * inspect / infrastructure accumulators and then dropped, keeping + * the worker heap bounded (the parser's WHATWG backpressure pauses + * production once the consumer falls behind). This replaces an + * earlier eager `Parser.ParseSave` path that materialised the + * entire save graph in memory and OOMed on endgame saves. + */ +async function parseSavegame( + file: File, + options: { extractInfrastructure?: boolean }, +) { try { - const json = Parser.ParseSave('Save', await file.arrayBuffer(), { - onProgressCallback: (progress: number, msg?: string) => { - postMessage({ - type: 'progress', - progress, - message: msg, - } as IParseSavegameResponse); - }, + const buffer = await file.arrayBuffer(); + const { stream, startStreaming } = + ReadableStreamParser.CreateReadableStreamFromSaveToJson('Save', buffer, { + onProgress: (progress: number, message?: string) => { + postMessage({ + type: 'progress', + progress, + message, + } as IParseSavegameResponse); + }, + }); + + const wantInfrastructure = options.extractInfrastructure === true; + const inspectAcc = createInspectAccumulator(); + const infraAcc = wantInfrastructure + ? createInfrastructureAccumulator() + : null; + + // Consume the parser library's `ReadableStream` directly and + // feed each chunk into a `JSONParser` (the non-WHATWG variant). The + // WHATWG `TransformStream` wrapper from `@streamparser/json-whatwg` + // cannot be used here: its `cloneParsedElementInfo` does + // `JSON.parse(JSON.stringify(parent))` on every emit, and with + // `keepStack: false` the parent array grows monotonically (deleted + // entries leave holes). The clone cost is O(N) per emit, total O(N²) + // — effectively hangs on endgame saves with hundreds of thousands + // of objects. Driving the parser via the callback path skips the + // clone entirely. + const jsonParser = new JSONParser({ + paths: ['$.levels.*.objects.*'], + keepStack: false, }); + jsonParser.onValue = info => { + const obj = info.value; + inspectObject(inspectAcc, obj); + if (infraAcc) ingestEntity(infraAcc, obj); + }; - const { availableRecipes } = inspectSavegame(json); + const reader = (stream as ReadableStream).getReader(); - postMessage({ - type: 'parsed', - save: { - availableRecipes, - }, - } as IParseSavegameResponse); + const streamingDone = startStreaming(); + streamingDone.catch(err => { + // If the parser library throws, surface the error through the + // reader as well so the await loop below exits. + reader.cancel(err).catch(() => {}); + }); + + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + jsonParser.write(value); + } + } finally { + if (!jsonParser.isEnded) jsonParser.end(); + } + // Wait for the parser library's startStreaming to settle. If it + // rejected this re-throws here. + await streamingDone; + + const { availableRecipes, usedNodeIds, players } = + finalizeInspect(inspectAcc); + const save: ParsedSatisfactorySave = { + availableRecipes, + usedNodeIds, + players, + }; + + let transfer: ArrayBuffer[] = []; + if (infraAcc) { + postMessage({ + type: 'progress', + progress: 0.99, + message: 'Finalising infrastructure...', + } as IParseSavegameResponse); + save.infrastructure = finalizeInfrastructure(infraAcc); + transfer = collectInfrastructureTransferables(save.infrastructure); + logger.log( + 'Infrastructure extracted:', + save.infrastructure.buildings.count, + 'buildings,', + save.infrastructure.splines.reduce((sum, s) => sum + s.count, 0), + 'spline polylines', + ); + } + + workerPostMessage( + { type: 'parsed', save } as IParseSavegameResponse, + transfer, + ); } catch (e) { logger.error(`Error while parsing`, e); postMessage({ @@ -40,33 +147,11 @@ async function parseSavegame(file: File) { } } -function inspectSavegame(save: SatisfactorySave) { - // All objects in the savegame - const objects = Object.values(save.levels).flatMap(level => level.objects); - - // Search for the recipe manager - const recipeManager = objects.find( - obj => obj.typePath === '/Script/FactoryGame.FGRecipeManager', - ); - - // Get the available recipes - const availableRecipesProperty = recipeManager?.properties - ?.mAvailableRecipes as ObjectArrayProperty; - const availableRecipesIds = new Set( - availableRecipesProperty?.values.map( - value => value?.pathName.split('.')[1], - ), - ); - - logger.log('Available recipes:', availableRecipesIds); - return { - availableRecipes: [...availableRecipesIds], - }; -} - addEventListener('message', (event: MessageEvent) => { const { data } = event; if (data.type === 'parse') { - parseSavegame(data.file); + parseSavegame(data.file, { + extractInfrastructure: data.extractInfrastructure ?? false, + }); } }); diff --git a/src/recipes/savegame/parserPatches.ts b/src/recipes/savegame/parserPatches.ts new file mode 100644 index 00000000..59d8a592 --- /dev/null +++ b/src/recipes/savegame/parserPatches.ts @@ -0,0 +1,76 @@ +import { ByteReader } from '@etothepii/satisfactory-file-parser'; + +interface ByteReaderInternals { + bufferView: DataView; + currentByte: number; +} + +const ARRAY_FROM_DUMP_THRESHOLD_BYTES = 1024 * 1024; + +let patchesInstalled = false; + +/** + * Monkey-patches two memory hot-spots in + * `@etothepii/satisfactory-file-parser` (v4.0.1) that make endgame + * saves OOM the worker tab: + * + * 1. `ByteReader.prototype.readBytes(N)` is implemented as + * `new Uint8Array(new Array(N).fill(0).map(p => this.readByte()))`. + * For N in the hundreds of MB this peaks at ~17N bytes (two + * N-element JS Arrays plus the final Uint8Array). The replacement + * copies straight from the underlying DataView buffer, peaking at N. + * + * 2. `PropertiesList.ParseSingleProperty` and + * `SaveObject.parseTrailingData` fall back to + * `Array.from(reader.readBytes(N))` when a property type cannot be + * parsed (post-1.0 game updates introducing new property kinds, + * mod-only types, version drift). That converts the entire byte + * blob into a JS Array of N SMI numbers (~8N bytes). On a real + * endgame save we observed this around ~71% through reading + * objects, where a single property's binarySize was big enough to + * push the worker past Chrome's tab memory limit. + * + * `Array.from` is monkey-patched so that calls with a `Uint8Array` + * of more than 1 MB (and no map function) return `[]` instead. + * Our use case (recipes, used nodes, infrastructure) never reads + * `rawBytes` / `trailingData`, so dropping the dump is safe; the + * cursor still advances by N byte (the copy happens inside + * `readBytes` before the wrapped `Array.from` runs), so subsequent + * properties parse from the correct offset. + * + * Idempotent: calling more than once is a no-op. Designed to be invoked + * once at worker module load. + */ +export function installSatisfactoryParserPatches(): void { + if (patchesInstalled) return; + patchesInstalled = true; + + ByteReader.prototype.readBytes = function readBytesPatched( + this: ByteReader, + count: number, + ): Uint8Array { + const internals = this as unknown as ByteReaderInternals; + const sourceBuffer = internals.bufferView.buffer; + const start = internals.bufferView.byteOffset + internals.currentByte; + const out = new Uint8Array(count); + out.set(new Uint8Array(sourceBuffer, start, count)); + internals.currentByte += count; + return out; + }; + + const originalArrayFrom = Array.from.bind(Array); + Array.from = function patchedArrayFrom( + this: unknown, + iterable: Iterable | ArrayLike, + ...rest: unknown[] + ): unknown[] { + if ( + iterable instanceof Uint8Array && + iterable.length > ARRAY_FROM_DUMP_THRESHOLD_BYTES && + rest.length === 0 + ) { + return []; + } + return originalArrayFrom(iterable as Iterable, ...(rest as [])); + } as typeof Array.from; +} diff --git a/src/recipes/savegame/startSavegameParsing.ts b/src/recipes/savegame/startSavegameParsing.ts index 79e82c30..36be03e7 100644 --- a/src/recipes/savegame/startSavegameParsing.ts +++ b/src/recipes/savegame/startSavegameParsing.ts @@ -4,15 +4,31 @@ import type { ParsedSatisfactorySave, } from './ParseSavegameMessages'; +export interface StartSavegameParsingOptions { + /** + * When true, the worker also extracts every user-built buildable + * (machines, belts, pipes, foundations, ...) and returns it in + * `ParsedSatisfactorySave.infrastructure` for the map's + * infrastructure layer. Off by default because it adds a parse pass + * and a few MB of typed-array payload on large saves. + */ + extractInfrastructure?: boolean; +} + export function startSavegameParsing( file: File, onProgress?: (progress: number, message?: string) => void, + options: StartSavegameParsingOptions = {}, ): Promise { const worker = new Worker( new URL('./parseSavegameWorker.ts', import.meta.url), { type: 'module' }, ); - worker.postMessage({ type: 'parse', file } as IParseSavegameRequest); + worker.postMessage({ + type: 'parse', + file, + extractInfrastructure: options.extractInfrastructure, + } as IParseSavegameRequest); return new Promise((resolve, reject) => { worker.onmessage = (event: MessageEvent) => { diff --git a/src/recipes/savegame/useSavegameImport.ts b/src/recipes/savegame/useSavegameImport.ts new file mode 100644 index 00000000..76f64ba3 --- /dev/null +++ b/src/recipes/savegame/useSavegameImport.ts @@ -0,0 +1,217 @@ +import { notifications } from '@mantine/notifications'; +import { useCallback, useState } from 'react'; +import { useStore } from '@/core/zustand'; +import type { ApplySavegameToGameOptions } from '@/games/gamesSlice'; +import type { ParsedSatisfactorySave } from './ParseSavegameMessages'; +import { startSavegameParsing } from './startSavegameParsing'; + +/** + * Progress snapshot surfaced while the web worker chews through a + * `.sav` file. `value` is a 0-1 fraction; `message` is the + * parser's occasional status string (e.g. "Parsing levels..."). + */ +export interface SavegameImportProgress { + value: number; + message?: string; +} + +/** + * Result of a successful `importAndApplyToGame` call. Includes the + * parsed save so callers (e.g. the recipes drawer) can do additional + * non-game-state work on top, like updating the active solver + * instance's allowed-recipe list. + */ +export interface SavegameImportApplied { + save: ParsedSatisfactorySave; + applied: ApplySavegameToGameOptions; + recipeCount: number; + usedNodeCount: number; + /** Sum of buildings + spline polylines extracted, when applicable. */ + infrastructureCount: number; +} + +export interface UseSavegameImportResult { + importing: boolean; + progress: SavegameImportProgress; + /** + * Low-level: kicks off parsing for the given `.sav` file and + * resolves with the parsed save. Most callers should prefer + * {@link UseSavegameImportResult.importAndApplyToGame} so import + * semantics stay consistent across surfaces. + */ + importFile: ( + file: File, + options?: { extractInfrastructure?: boolean }, + ) => Promise; + /** + * High-level: parses the file, applies the requested slices of + * derivable state to the game in a single store patch, and surfaces + * a unified success / failure notification. The promise resolves + * with the parsed save plus a summary of what was applied so + * callers can chain additional updates (e.g. solver-instance + * recipe lists) on the same flow. + * + * If `gameId` is null/undefined a "no game selected" notification + * is shown and the promise resolves to `null` without parsing. + */ + importAndApplyToGame: ( + file: File, + gameId: string | null | undefined, + apply: ApplySavegameToGameOptions, + ) => Promise; + /** + * Resets progress / importing back to idle. Useful when a caller + * unmounts a modal after success so reopening it starts fresh. + */ + reset: () => void; +} + +const IDLE_PROGRESS: SavegameImportProgress = { value: 0, message: undefined }; + +/** + * Wrapper around {@link startSavegameParsing} that tracks `importing` + * + `progress` state for a single active import and centralizes the + * "apply this save to a game" flow. Shared between the recipes drawer, + * map filter panel, and map drop zone so all three surfaces produce + * identical store updates and notifications for the same file. + */ +export function useSavegameImport(): UseSavegameImportResult { + const [importing, setImporting] = useState(false); + const [progress, setProgress] = + useState(IDLE_PROGRESS); + + const importFile = useCallback( + ( + file: File, + options: { extractInfrastructure?: boolean } = {}, + ): Promise => { + setImporting(true); + setProgress(IDLE_PROGRESS); + return startSavegameParsing( + file, + (value, message) => { + setProgress({ value, message }); + }, + { extractInfrastructure: options.extractInfrastructure }, + ) + .then(save => { + setImporting(false); + return save; + }) + .catch(err => { + setImporting(false); + throw err; + }); + }, + [], + ); + + const importAndApplyToGame = useCallback( + async ( + file: File, + gameId: string | null | undefined, + apply: ApplySavegameToGameOptions, + ): Promise => { + if (!gameId) { + notifications.show({ + title: 'No game selected', + message: 'Create or select a game before importing a save.', + color: 'yellow', + }); + return null; + } + + try { + const save = await importFile(file, { + extractInfrastructure: apply.infrastructure, + }); + useStore.getState().updateGameFromSavegame(gameId, save, apply); + + const recipeCount = apply.defaultRecipes + ? save.availableRecipes.length + : 0; + const usedNodeCount = apply.usedNodes ? save.usedNodeIds.length : 0; + + let infrastructureCount = 0; + if (apply.infrastructure && save.infrastructure) { + infrastructureCount = + save.infrastructure.buildings.count + + save.infrastructure.splines.reduce((s, b) => s + b.count, 0); + useStore.getState().setInfrastructure(gameId, save.infrastructure); + } + // Player positions are cheap to extract, so we always surface + // them: the map centers on the player and the marker shows up + // even for recipes-only imports (where infrastructure isn't + // dispatched). + useStore.getState().setPlayers(gameId, save.players); + + notifications.show({ + title: 'Savegame imported', + message: buildSummaryMessage( + apply, + recipeCount, + usedNodeCount, + infrastructureCount, + ), + color: 'green', + }); + + return { + save, + applied: apply, + recipeCount, + usedNodeCount, + infrastructureCount, + }; + } catch (err) { + const message = + err instanceof Error ? err.message : 'Unknown parser error'; + console.error('Error while parsing savegame:', message); + notifications.show({ + title: 'Error while parsing savegame', + message, + color: 'red', + }); + throw err; + } + }, + [importFile], + ); + + const reset = useCallback(() => { + setImporting(false); + setProgress(IDLE_PROGRESS); + }, []); + + return { importing, progress, importFile, importAndApplyToGame, reset }; +} + +function buildSummaryMessage( + apply: ApplySavegameToGameOptions, + recipeCount: number, + usedNodeCount: number, + infrastructureCount: number, +): string { + const parts: string[] = []; + if (apply.defaultRecipes) { + parts.push( + `${recipeCount} recipe${recipeCount === 1 ? '' : 's'} as game default`, + ); + } + if (apply.usedNodes) { + if (usedNodeCount === 0) { + parts.push('cleared used-node marks (no miners in save)'); + } else { + parts.push(`${usedNodeCount} used node${usedNodeCount === 1 ? '' : 's'}`); + } + } + if (apply.infrastructure) { + parts.push( + `${infrastructureCount} built structure${infrastructureCount === 1 ? '' : 's'}`, + ); + } + if (parts.length === 0) { + return 'Save parsed successfully (no game state updated).'; + } + return `Updated: ${parts.join(', ')}.`; +} diff --git a/src/recipes/ui/FactoryItemImage.tsx b/src/recipes/ui/FactoryItemImage.tsx index 100cf04c..a4e1c68a 100644 --- a/src/recipes/ui/FactoryItemImage.tsx +++ b/src/recipes/ui/FactoryItemImage.tsx @@ -1,4 +1,5 @@ -import { Image } from '@mantine/core'; +import { Image, Tooltip } from '@mantine/core'; +import type * as React from 'react'; import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; export interface IFactoryItemImageProps { @@ -6,25 +7,43 @@ export interface IFactoryItemImageProps { size?: number; /** Force high resolution */ highRes?: boolean; + /** + * Wrap the rendered image in a Mantine `Tooltip` showing the item's + * display name on hover. Off by default to keep existing call sites + * unchanged; the codex passes this in to make icons self-describing. + */ + withTooltip?: boolean; } export function FactoryItemImage(props: IFactoryItemImageProps) { - const { size = 42, highRes = false } = props; + const { size = 42, highRes = false, withTooltip = false } = props; if (!props.id) { return null; } const item = AllFactoryItemsMap[props.id]; - if (item.imageComponent) { - return ; + let content: React.ReactNode; + if (item?.imageComponent) { + content = ; + } else { + const baseImagePath = item?.imagePath ?? ''; + const imagePath = + size <= 32 && !highRes + ? baseImagePath.replace('_256', '_64') + : baseImagePath; + content = ( + {item?.displayName} + ); } - const baseImagePath = item?.imagePath ?? ''; - // Lower resolution image for smaller sizes - const imagePath = - size <= 32 && !highRes - ? baseImagePath.replace('_256', '_64') - : baseImagePath; - return {item.displayName}; + if (!withTooltip || !item) { + return content; + } + + return ( + + {content} + + ); } diff --git a/src/routes/MapRoutes.tsx b/src/routes/MapRoutes.tsx new file mode 100644 index 00000000..8f296995 --- /dev/null +++ b/src/routes/MapRoutes.tsx @@ -0,0 +1,10 @@ +import { Route, Routes } from 'react-router-dom'; +import { MapPage } from '@/map/MapPage'; + +export function MapRoutes() { + return ( + + } /> + + ); +} diff --git a/src/solver/algorithm/SolverNode.ts b/src/solver/algorithm/SolverNode.ts index d5c74c8b..7ff853cc 100644 --- a/src/solver/algorithm/SolverNode.ts +++ b/src/solver/algorithm/SolverNode.ts @@ -30,6 +30,25 @@ export type SolverRawInputNode = { inputIndex?: number; }; +/** + * A "raw output" — represents a portion of this factory's output that flows + * to a downstream consumer factory. Mirrors `SolverRawInputNode` but on the + * output side. Created by scanning other factories' inputs that reference + * this factory's id and resource. + */ +export type SolverRawOutputNode = { + type: 'raw_output'; + label: string; + resource: FactoryItem; + variable: string; + output?: FactoryOutput; + outputIndex?: number; + consumerFactoryId: string; + consumerFactoryName?: string | null; + consumerInputIndex: number; + consumerAmount: number; +}; + export type SolverOutputNode = { type: 'output'; label: string; @@ -83,6 +102,7 @@ export type SolverAreaNode = { export type SolverNode = | SolverRawNode | SolverRawInputNode + | SolverRawOutputNode | SolverOutputNode | SolverByproductNode | SolverInputNode diff --git a/src/solver/algorithm/getSolutionNodes.tsx b/src/solver/algorithm/getSolutionNodes.tsx index 4cac9abd..e0c37aec 100644 --- a/src/solver/algorithm/getSolutionNodes.tsx +++ b/src/solver/algorithm/getSolutionNodes.tsx @@ -1,6 +1,7 @@ import type { Node } from '@xyflow/react'; import type { IByproductNodeData } from '@/solver/layout/nodes/byproduct-node/ByproductNode'; import type { IMachineNodeData } from '@/solver/layout/nodes/machine-node/MachineNode'; +import type { IOutputConsumerNodeData } from '@/solver/layout/nodes/output-consumer-node/OutputConsumerNode'; import type { IResourceNodeData } from '@/solver/layout/nodes/resource-node/ResourceNode'; import type { SolutionNode } from './solveProduction'; @@ -21,3 +22,9 @@ export function isByproductNode( ): node is Node { return node.type === 'Byproduct'; } + +export function isOutputConsumerNode( + node: SolutionNode, +): node is Node { + return node.type === 'OutputConsumer'; +} diff --git a/src/solver/algorithm/request/addOutputConsumerNodes.ts b/src/solver/algorithm/request/addOutputConsumerNodes.ts new file mode 100644 index 00000000..2a162715 --- /dev/null +++ b/src/solver/algorithm/request/addOutputConsumerNodes.ts @@ -0,0 +1,55 @@ +import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; +import type { SolverContext } from '@/solver/algorithm/SolverContext'; +import type { FactoryOutputConsumer } from '@/solver/algorithm/solveProduction'; + +/** + * Stable React Flow node id for an "output to consumer factory" entry. + * + * Kept stable across solves so the layout system can preserve positions + * (matches the pattern used for raw / raw_input variables). + */ +export function getOutputConsumerNodeId( + consumer: FactoryOutputConsumer, + consumerIndex: number, +) { + const item = AllFactoryItemsMap[consumer.resource]; + const outputIndexPart = consumer.outputIndex ?? 'x'; + return `ro${item.index}o${outputIndexPart}c${consumerIndex}`; +} + +/** + * Records the consumer in the LP graph as a `raw_output` node so the + * result walker can later discover and emit a React Flow node + edge for + * it. We deliberately do NOT add an LP edge or constraint here: + * + * - These nodes are pure display markers — they show how much of the + * producer's existing output goes to a specific consumer factory. The + * amount is already accounted for by the consumer's own input flow. + * - Adding them as additional consumers of the resource node would + * double-count the demand (once via the existing byproduct/output + * constraint, once via this extra consumer), causing infeasibility + * when the consumer claim overlaps with the producer's declared + * output amount. + */ +export function addOutputConsumerNode( + ctx: SolverContext, + consumer: FactoryOutputConsumer, + consumerIndex: number, +) { + const { resource, output, outputIndex, amount } = consumer; + const resourceItem = AllFactoryItemsMap[resource]; + const nodeId = getOutputConsumerNodeId(consumer, consumerIndex); + + ctx.graph.mergeNode(nodeId, { + type: 'raw_output', + label: resource, + resource: resourceItem, + variable: nodeId, + output, + outputIndex, + consumerFactoryId: consumer.consumerFactoryId, + consumerFactoryName: consumer.consumerFactoryName, + consumerInputIndex: consumer.consumerInputIndex, + consumerAmount: amount, + }); +} diff --git a/src/solver/algorithm/solveProduction.tsx b/src/solver/algorithm/solveProduction.tsx index d607de9d..0058155d 100644 --- a/src/solver/algorithm/solveProduction.tsx +++ b/src/solver/algorithm/solveProduction.tsx @@ -3,15 +3,20 @@ import highloader, { type Highs, type HighsSolution } from 'highs'; import { useEffect, useRef, useState } from 'react'; import { log } from '@/core/logger/log'; import type { FactoryInput, FactoryOutput } from '@/factories/Factory'; +import type { ShowOutputFactoriesNodesMode } from '@/games/Game'; +import type { FactoryItem } from '@/recipes/FactoryItem'; import type { IIngredientEdgeData } from '@/solver/edges/IngredientEdge'; import type { IByproductNodeData } from '@/solver/layout/nodes/byproduct-node/ByproductNode'; import type { IMachineNodeData } from '@/solver/layout/nodes/machine-node/MachineNode'; +import type { IOutputConsumerNodeData } from '@/solver/layout/nodes/output-consumer-node/OutputConsumerNode'; +import type { IUnallocatedOutputNodeData } from '@/solver/layout/nodes/output-consumer-node/UnallocatedOutputNode'; import type { IResourceNodeData } from '@/solver/layout/nodes/resource-node/ResourceNode'; import type { SolverNodeState, SolverRequest } from '@/solver/store/Solver'; import { avoidPackagedFuelIfPossible } from './consolidate/avoidPackagedFuelIfPossible'; import { avoidUnproducibleResources } from './consolidate/avoidUnproducibleResources'; import { consolidateProductionConstraints } from './consolidate/consolidateProductionConstraints'; import { addInputResourceConstraints } from './request/addInputProductionConstraints'; +import { addOutputConsumerNode } from './request/addOutputConsumerNodes'; import { addOutputProductionConstraints } from './request/addOutputProductionConstraints'; import { blockWorldResourcesForInputs } from './request/blockWorldResourcesForInputs'; import { SolverContext } from './SolverContext'; @@ -20,9 +25,34 @@ import { applySolverObjective } from './solve/applySolverObjectives'; const logger = log.getLogger('solver:production'); logger.setLevel('info'); +/** + * Represents a downstream factory that consumes one of the current + * factory's outputs. Derived by scanning every other factory's inputs + * for ones whose `factoryId` references the current factory. + */ +export interface FactoryOutputConsumer { + resource: string; + amount: number; + consumerFactoryId: string; + consumerFactoryName?: string | null; + /** Index of the matched input row on the consumer factory. */ + consumerInputIndex: number; + /** Index of the matched output row on the producing factory, if any. */ + outputIndex?: number; + /** Snapshot of the producing factory's output row, if any. */ + output?: FactoryOutput; +} + export interface SolverProductionRequest extends SolverRequest { inputs: FactoryInput[]; outputs: FactoryOutput[]; + outputConsumers?: FactoryOutputConsumer[]; + /** + * Per-game preference for whether the solver graph displays + * output-consumer / unallocated nodes. Defaults to `'allocated'` if + * the request omits it (matches the migration default). + */ + showOutputFactoriesNodes?: ShowOutputFactoriesNodesMode; nodes?: Record; } @@ -59,7 +89,9 @@ export function useHighs() { export type SolutionNode = | Node | Node - | Node; + | Node + | Node + | Node; /** * Translates a production request into a linear programming problem and solves it, * returning the solution and the corresponding graph. @@ -87,6 +119,22 @@ export function solveProduction( addOutputProductionConstraints(ctx, item, i); } + // 2b. Output consumer nodes (downstream factories that pull from this one). + // These are pure display markers — they live in the graph so we can + // iterate them after the LP solves, but they do not add any LP + // constraints (see addOutputConsumerNode for why). + // The per-game `showOutputFactoriesNodes` setting gates the entire + // feature: 'none' skips emitting both consumer and unallocated nodes. + const showOutputFactoriesNodes = + request.showOutputFactoriesNodes ?? 'allocated'; + const outputConsumers = + showOutputFactoriesNodes === 'none' ? [] : (request.outputConsumers ?? []); + for (let i = 0; i < outputConsumers.length; i++) { + const consumer = outputConsumers[i]; + if (!consumer.resource || consumer.amount == null) continue; + addOutputConsumerNode(ctx, consumer, i); + } + // 3. Consolidate consolidateProductionConstraints(ctx); avoidUnproducibleResources(ctx); @@ -238,13 +286,130 @@ export function solveProduction( // type: 'Floating', target: targetNode.recipeMainProductVariable, data: { - label: `${typeof edge.source === 'string' ? 'Resource' : edge.source.name} -> ${edge.target.name}`, + label: `${typeof edge.source === 'string' ? 'Resource' : edge.source.name} -> ${typeof edge.target === 'string' ? 'Consumer' : edge.target.name}`, value: Number(value.Primal), resource: edge.resource, }, }); } } + + // 5. Output consumer nodes (downstream factories pulling from this one). + // Walk graphology directly — these have no LP variable so they don't + // appear in `result.Columns`. Emit the React Flow node and an edge + // from the matching byproduct node (the producer's "output" sink). + // While doing so, accumulate per-resource consumer claim totals so we + // can render an "Unallocated" node for any leftover production. + const allocatedByResource = new Map< + string, + { item: FactoryItem; total: number } + >(); + ctx.graph.forEachNode((nodeId, node) => { + if (node.type !== 'raw_output') return; + nodes.push({ + id: nodeId, + type: 'OutputConsumer', + data: { + resource: node.resource, + value: node.consumerAmount, + consumerAmount: node.consumerAmount, + consumerFactoryId: node.consumerFactoryId, + consumerFactoryName: node.consumerFactoryName, + consumerInputIndex: node.consumerInputIndex, + output: node.output, + outputIndex: node.outputIndex, + } satisfies IOutputConsumerNodeData, + position: { x: 0, y: 0 }, + }); + + const existing = allocatedByResource.get(node.resource.id); + if (existing) { + existing.total += node.consumerAmount; + } else { + allocatedByResource.set(node.resource.id, { + item: node.resource, + total: node.consumerAmount, + }); + } + + const byproductId = `b${node.resource.index}`; + if (!ctx.graph.hasNode(byproductId)) return; + edges.push({ + id: `e_${byproductId}_${nodeId}`, + source: byproductId, + target: nodeId, + type: 'Ingredient', + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + }, + data: { + label: `${node.resource.name} -> ${node.consumerFactoryName ?? 'consumer'}`, + value: node.consumerAmount, + resource: node.resource, + } as IIngredientEdgeData, + }); + }); + + // 6. Unallocated output nodes — one per resource where production + // exceeds the sum of declared consumer claims. Hidden when zero + // so a fully-allocated factory doesn't get extra clutter, and + // only emitted at all when the user has opted into the 'all' mode. + const skipUnallocated = showOutputFactoriesNodes !== 'all'; + for (const { + item, + total: totalAllocated, + } of allocatedByResource.values()) { + if (skipUnallocated) break; + const byproductId = `b${item.index}`; + const byproductCol = result.Columns[byproductId]; + if (!byproductCol) continue; + const totalProduced = Number(byproductCol.Primal); + const unallocated = totalProduced - totalAllocated; + if (unallocated <= 0.0001) continue; + + const unallocatedId = `u${item.index}`; + const matchingOutputIndex = (request.outputs ?? []).findIndex( + o => o.resource === item.id, + ); + const matchingOutput = + matchingOutputIndex >= 0 + ? request.outputs[matchingOutputIndex] + : undefined; + + nodes.push({ + id: unallocatedId, + type: 'UnallocatedOutput', + data: { + resource: item, + value: unallocated, + totalProduced, + totalAllocated, + output: matchingOutput, + outputIndex: + matchingOutputIndex >= 0 ? matchingOutputIndex : undefined, + } satisfies IUnallocatedOutputNodeData, + position: { x: 0, y: 0 }, + }); + + edges.push({ + id: `e_${byproductId}_${unallocatedId}`, + source: byproductId, + target: unallocatedId, + type: 'Ingredient', + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + }, + data: { + label: `${item.name} (unallocated)`, + value: unallocated, + resource: item, + } as IIngredientEdgeData, + }); + } } return { result, nodes, edges, graph: ctx.graph, context: ctx }; diff --git a/src/solver/edges/FloatingEdge.tsx b/src/solver/edges/FloatingEdge.tsx index 5a9a06ec..3bb1cac3 100644 --- a/src/solver/edges/FloatingEdge.tsx +++ b/src/solver/edges/FloatingEdge.tsx @@ -1,6 +1,7 @@ import { type EdgeProps, useInternalNode } from '@xyflow/react'; import { useGameSetting } from '@/games/gamesSlice'; +import { useSolverHighlightOptional } from '@/solver/layout/highlight/SolverHighlightContext'; import { getConfigurableEdgePath } from './getConfigurableEdgePath'; import { getEdgeParams } from './utils.js'; @@ -16,6 +17,7 @@ export function FloatingEdge({ const orthogonalEdges = useGameSetting('orthogonalEdges') as | boolean | undefined; + const highlight = useSolverHighlightOptional(); if (!sourceNode || !targetNode) { return null; @@ -38,14 +40,24 @@ export function FloatingEdge({ !!orthogonalEdges, ); + const highlightedNodeId = highlight?.highlightedNodeId ?? null; + const isHighlighted = + highlightedNodeId != null && + (highlightedNodeId === source || highlightedNodeId === target); + const isDimmed = highlightedNodeId != null && !isHighlighted; + return ( ); } diff --git a/src/solver/edges/IngredientEdge.tsx b/src/solver/edges/IngredientEdge.tsx index f652187e..28eb62d9 100644 --- a/src/solver/edges/IngredientEdge.tsx +++ b/src/solver/edges/IngredientEdge.tsx @@ -23,6 +23,7 @@ import { type FactoryItem, FactoryItemForm } from '@/recipes/FactoryItem'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; import { getConfigurableEdgePath } from '@/solver/edges/getConfigurableEdgePath'; import { useEdgeAnimationEnabled } from '@/solver/edges/useEdgeAnimationEnabled'; +import { useSolverHighlightOptional } from '@/solver/layout/highlight/SolverHighlightContext'; import { getEdgeParams, getSpecialPath } from './utils'; export interface IIngredientEdgeData { @@ -70,6 +71,7 @@ export const IngredientEdge: FC>> = ({ | boolean | undefined; const animationEnabled = useEdgeAnimationEnabled(); + const highlight = useSolverHighlightOptional(); const isOverMaxBelt = maxBelt && (data?.value ?? 0) > maxBelt.conveyor!.speed; const isOverMaxPipeline = maxPipeline && (data?.value ?? 0) > maxPipeline.pipeline!.flowRate; @@ -119,26 +121,43 @@ export const IngredientEdge: FC>> = ({ const usedLogistic = isLiquidOrGas ? usedPipeline : usedBelt; const usedLogisticMax = isLiquidOrGas ? neededPipelines : neededBelts; + const highlightedNodeId = highlight?.highlightedNodeId ?? null; + const isHighlighted = + highlightedNodeId != null && + (highlightedNodeId === source || highlightedNodeId === target); + const isDimmed = highlightedNodeId != null && !isHighlighted; + const edgeOpacity = isDimmed ? 0.15 : 1; + const labelOpacity = isDimmed ? 0.3 : 1; + return ( <> - - {animationEnabled && ( - - - - )} + > + + {animationEnabled && ( + + + + )} + >> = ({ ), position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, + opacity: labelOpacity, + transition: 'opacity 0.2s', }} className="nodrag" > diff --git a/src/solver/layout/SolverLayout.tsx b/src/solver/layout/SolverLayout.tsx index 5bb8124a..c6487b67 100644 --- a/src/solver/layout/SolverLayout.tsx +++ b/src/solver/layout/SolverLayout.tsx @@ -30,8 +30,11 @@ import { toggleFullscreen } from '@/utils/toggleFullscreen'; import '@xyflow/react/dist/style.css'; import { isEqual } from 'lodash'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSolverHighlight } from './highlight/SolverHighlightContext'; import { ByproductNode } from './nodes/byproduct-node/ByproductNode'; import { MachineNode } from './nodes/machine-node/MachineNode'; +import { OutputConsumerNode } from './nodes/output-consumer-node/OutputConsumerNode'; +import { UnallocatedOutputNode } from './nodes/output-consumer-node/UnallocatedOutputNode'; import { ResourceNode } from './nodes/resource-node/ResourceNode'; import classes from './SolverLayout.module.css'; import { @@ -196,6 +199,8 @@ const nodeTypes = { Machine: MachineNode, Resource: ResourceNode, Byproduct: ByproductNode, + OutputConsumer: OutputConsumerNode, + UnallocatedOutput: UnallocatedOutputNode, }; const edgeTypes = { @@ -206,6 +211,8 @@ const edgeTypes = { export const SolverLayout = (props: SolverLayoutProps) => { const savedLayout = usePathSolverLayout(props.id); const { fitView, getNodes, getEdges } = useReactFlow(); + const { highlightedNodeId, toggleHighlightedNodeId, clearHighlight } = + useSolverHighlight(); const [nodes, setNodes, onNodesChange] = useNodesState(props.nodes); const [edges, setEdges, onEdgesChange] = useEdgesState(props.edges); @@ -259,6 +266,16 @@ export const SolverLayout = (props: SolverLayoutProps) => { // setTimeout(() => {}, 1); }, [props.edges, props.nodes, setEdges, setNodes]); + // Clear highlight if the highlighted node is no longer in the graph + // (e.g. solution recomputed). Avoids holding a dangling id that would + // dim every edge with no visible "selected" node to explain why. + useEffect(() => { + if (highlightedNodeId == null) return; + if (!props.nodes.some(n => n.id === highlightedNodeId)) { + clearHighlight(); + } + }, [props.nodes, highlightedNodeId, clearHighlight]); + const { getCompatiblePreviousLayout, cachePreviousLayout } = usePreviousSolverLayoutStates(); @@ -362,6 +379,22 @@ export const SolverLayout = (props: SolverLayoutProps) => { [cachePreviousLayout, getNodes, onNodesChange, savedLayout, props.id], ); + // Double-click / double-tap a node to highlight its incident edges. + // Single click keeps its existing behavior (selects the node and opens + // the popover) so this gesture is purely additive: on touch devices + // users can inspect connections without committing to the popover. + const handleNodeDoubleClick = useCallback( + (_event: React.MouseEvent, node: Node) => { + toggleHighlightedNodeId(node.id); + }, + [toggleHighlightedNodeId], + ); + + // Tap/click the empty pane to clear the highlight. + const handlePaneClick = useCallback(() => { + clearHighlight(); + }, [clearHighlight]); + // Context menu // const onNodeContextMenu = useCallback( // (event: React.MouseEvent, node: Node) => { @@ -394,6 +427,8 @@ export const SolverLayout = (props: SolverLayoutProps) => { edgeTypes={edgeTypes} onNodesChange={handleNodesChange} onEdgesChange={onEdgesChange} + onNodeDoubleClick={handleNodeDoubleClick} + onPaneClick={handlePaneClick} connectionLineType={ConnectionLineType.SmoothStep} selectNodesOnDrag={false} // onNodeContextMenu={onNodeContextMenu} diff --git a/src/solver/layout/highlight/SolverHighlightContext.tsx b/src/solver/layout/highlight/SolverHighlightContext.tsx new file mode 100644 index 00000000..6f757692 --- /dev/null +++ b/src/solver/layout/highlight/SolverHighlightContext.tsx @@ -0,0 +1,128 @@ +import { useStore } from '@xyflow/react'; +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +export interface SolverHighlightContextValue { + highlightedNodeId: string | null; + setHighlightedNodeId: (id: string | null) => void; + toggleHighlightedNodeId: (id: string) => void; + clearHighlight: () => void; +} + +export const SolverHighlightContext = + createContext(null); + +/** + * Tracks which node (if any) the user has tapped to highlight its + * incident edges. Kept separate from React Flow's built-in `selected` + * state so a tap can highlight edges without opening the node's popover. + * + * The popover continues to follow `props.selected`, which is now opened + * via double-click / double-tap instead of single-click. + */ +export const SolverHighlightProvider: React.FC = ({ + children, +}) => { + const [highlightedNodeId, setHighlightedNodeId] = useState( + null, + ); + + const toggleHighlightedNodeId = useCallback((id: string) => { + setHighlightedNodeId(prev => (prev === id ? null : id)); + }, []); + + const clearHighlight = useCallback(() => { + setHighlightedNodeId(null); + }, []); + + const value = useMemo( + () => ({ + highlightedNodeId, + setHighlightedNodeId, + toggleHighlightedNodeId, + clearHighlight, + }), + [highlightedNodeId, toggleHighlightedNodeId, clearHighlight], + ); + + return ( + + {children} + + ); +}; + +export function useSolverHighlight() { + const context = useContext(SolverHighlightContext); + if (!context) { + throw new Error( + 'useSolverHighlight must be used within a SolverHighlightProvider', + ); + } + return context; +} + +/** + * Same as `useSolverHighlight` but safe to call from components that may + * render outside the provider (e.g. node components used in other graphs). + * Returns `null` if no provider is present. + */ +export function useSolverHighlightOptional() { + return useContext(SolverHighlightContext); +} + +/** + * Set of node ids that should appear "in the highlighted chain" when a + * node is double-tapped: the tapped node itself plus every node it has + * a direct edge to or from. Returns `null` when no node is highlighted + * so callers can short-circuit dim logic. + * + * Recomputes only when the highlighted id or the underlying edges + * change. Reads edges from the React Flow store directly so we always + * see the same edges that the renderer sees. + */ +export function useHighlightedNodeIds(): ReadonlySet | null { + const highlight = useSolverHighlightOptional(); + const highlightedNodeId = highlight?.highlightedNodeId ?? null; + + return useStore(s => { + if (highlightedNodeId == null) return null; + const ids = new Set([highlightedNodeId]); + for (const edge of s.edges) { + if (edge.source === highlightedNodeId) ids.add(edge.target); + else if (edge.target === highlightedNodeId) ids.add(edge.source); + } + return ids; + }, areSetsEqual); +} + +/** + * Per-node convenience hook. Returns true when the given node id should + * be visually emphasised (the tapped node or one of its direct + * neighbors), false when it should be dimmed because some other node is + * highlighted, and null when no node is highlighted at all. + */ +export function useIsNodeHighlighted(nodeId: string): boolean | null { + const set = useHighlightedNodeIds(); + if (set == null) return null; + return set.has(nodeId); +} + +function areSetsEqual( + a: ReadonlySet | null, + b: ReadonlySet | null, +): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.size !== b.size) return false; + for (const value of a) { + if (!b.has(value)) return false; + } + return true; +} diff --git a/src/solver/layout/nodes/byproduct-node/ByproductNode.tsx b/src/solver/layout/nodes/byproduct-node/ByproductNode.tsx index f375f364..4fd8893a 100644 --- a/src/solver/layout/nodes/byproduct-node/ByproductNode.tsx +++ b/src/solver/layout/nodes/byproduct-node/ByproductNode.tsx @@ -6,6 +6,10 @@ import { RepeatingNumber } from '@/core/intl/NumberFormatter'; import type { FactoryOutput } from '@/factories/Factory'; import type { FactoryItem } from '@/recipes/FactoryItem'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { + useIsNodeHighlighted, + useSolverHighlightOptional, +} from '@/solver/layout/highlight/SolverHighlightContext'; import { NodeActionsBox } from '@/solver/layout/nodes/utils/NodeActionsBox'; import { InvisibleHandles } from '@/solver/layout/rendering/InvisibleHandles'; import type { SolverNodeState } from '@/solver/store/Solver'; @@ -34,9 +38,12 @@ export const ByproductNode = memo((props: IByproductNodeProps) => { const [isHovering, { close, open }] = useDisclosure(false); + const highlight = useSolverHighlightOptional(); + const isPrimaryHighlighted = highlight?.highlightedNodeId === props.id; + const isDimmed = useIsNodeHighlighted(props.id) === false; + return ( { { ) : ( - Click on the node to see available actions, like editing - amount. + {isByproduct + ? 'Click on the node to pick a recipe that processes this byproduct further.' + : 'Click on the node to edit this output or pick a recipe to process it further.'} )} diff --git a/src/solver/layout/nodes/byproduct-node/ByproductNodeActions.tsx b/src/solver/layout/nodes/byproduct-node/ByproductNodeActions.tsx index 639906f6..9c960c1c 100644 --- a/src/solver/layout/nodes/byproduct-node/ByproductNodeActions.tsx +++ b/src/solver/layout/nodes/byproduct-node/ByproductNodeActions.tsx @@ -1,6 +1,13 @@ import { setByPath } from '@clickbar/dot-diver'; -import { ActionIcon, Button, Group, Stack, Tooltip } from '@mantine/core'; -import { IconDeviceFloppy, IconTrash } from '@tabler/icons-react'; +import { + ActionIcon, + Button, + Divider, + Group, + Stack, + Tooltip, +} from '@mantine/core'; +import { IconDeviceFloppy, IconPlus, IconTrash } from '@tabler/icons-react'; import { produce, type WritableDraft } from 'immer'; import { isEqual } from 'lodash'; import { useState } from 'react'; @@ -11,6 +18,7 @@ import type { FactoryOutput } from '@/factories/Factory'; import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; import type { IByproductNodeData } from './ByproductNode'; import { ByproductNodeOutputConfig } from './ByproductNodeInputConfig'; +import { ByproductProcessFurtherAction } from './ByproductProcessFurtherAction'; export interface IByproductNodeActionsProps { id: string; @@ -19,8 +27,7 @@ export interface IByproductNodeActionsProps { export function ByproductNodeActions(props: IByproductNodeActionsProps) { const { - id, - data: { value, output, outputIndex }, + data: { value, output, outputIndex, resource }, } = props; const solverId = useFactoryContext(); @@ -49,64 +56,87 @@ export function ByproductNodeActions(props: IByproductNodeActionsProps) { .updateFactoryOutput(solverId!, outputIndex!, temporaryOutput); }; + const handleSaveAsOutput = () => { + useStore.getState().addFactoryOutput(solverId!, { + resource: resource.id, + amount: value, + objective: 'default', + }); + }; + return ( - - - {output && ( - - - useStore - .getState() - .removeFactoryOutput(solverId!, outputIndex!) - } - > - - - - )} + {!output && ( + + + + + + )} + {output && ( + <> + + + + + useStore + .getState() + .removeFactoryOutput(solverId!, outputIndex!) + } + > + + + - {output?.objective === 'max' && ( - - - useStore - .getState() - .updateFactoryOutput(solverId!, outputIndex!, { - amount: value, - }) - } - > - - - - )} - - - + {output.objective === 'max' && ( + + + useStore + .getState() + .updateFactoryOutput(solverId!, outputIndex!, { + amount: value, + }) + } + > + + + + )} + + + - {output && ( - + + + + )} + + ); } diff --git a/src/solver/layout/nodes/byproduct-node/ByproductProcessFurtherAction.tsx b/src/solver/layout/nodes/byproduct-node/ByproductProcessFurtherAction.tsx new file mode 100644 index 00000000..d937ab77 --- /dev/null +++ b/src/solver/layout/nodes/byproduct-node/ByproductProcessFurtherAction.tsx @@ -0,0 +1,118 @@ +import { Button, Group, ScrollArea, Stack, Text } from '@mantine/core'; +import { useMemo } from 'react'; +import { useStore } from '@/core/zustand'; +import { useFactoryContext } from '@/FactoryContext'; +import { + AllFactoryRecipesMap, + getAllRecipesForIngredient, +} from '@/recipes/FactoryRecipe'; +import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { RecipeTooltip } from '@/recipes/ui/RecipeTooltip'; +import { useSolverAllowedRecipes } from '@/solver/store/solverSelectors'; + +export interface IByproductProcessFurtherActionProps { + resourceId: string; + onPicked?: () => void; +} + +export function ByproductProcessFurtherAction( + props: IByproductProcessFurtherActionProps, +) { + const { resourceId, onPicked } = props; + const solverId = useFactoryContext(); + const allowedRecipes = useSolverAllowedRecipes(solverId); + + const recipes = useMemo( + () => getAllRecipesForIngredient(resourceId), + [resourceId], + ); + + const handlePick = (recipeId: string) => { + const recipe = AllFactoryRecipesMap[recipeId]; + if (!recipe) return; + + const newResource = recipe.products[0]?.resource; + if (!newResource) return; + + const factory = useStore.getState().factories.factories[solverId]; + const alreadyExists = factory?.outputs?.some( + o => o.resource === newResource && o.objective === 'max', + ); + + if (!allowedRecipes?.includes(recipeId)) { + useStore.getState().toggleRecipe(solverId, { recipeId, use: true }); + } + + if (!alreadyExists) { + useStore.getState().addFactoryOutput(solverId, { + resource: newResource, + amount: 0, + objective: 'max', + }); + } + + onPicked?.(); + }; + + if (recipes.length === 0) { + return ( + + + Process further + + + No recipes consume this item. + + + ); + } + + return ( + + + Process further + + + Pick a recipe to consume this item. + + + + {recipes.map(r => { + const productId = r.products[0]?.resource; + const isAlternate = r.name.startsWith('Alternate: '); + const displayName = isAlternate + ? r.name.slice('Alternate: '.length) + : r.name; + return ( + + + + ); + })} + + + + ); +} diff --git a/src/solver/layout/nodes/machine-node/MachineNode.tsx b/src/solver/layout/nodes/machine-node/MachineNode.tsx index b3d54df7..8b47485c 100644 --- a/src/solver/layout/nodes/machine-node/MachineNode.tsx +++ b/src/solver/layout/nodes/machine-node/MachineNode.tsx @@ -1,4 +1,5 @@ import { + ActionIcon, alpha, Badge, Box, @@ -12,17 +13,20 @@ import { Table, Text, Title, + Tooltip, useMantineTheme, } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { + IconArrowDown, + IconArrowUp, IconBolt, IconBuildingFactory2, IconCircleCheckFilled, IconClockBolt, } from '@tabler/icons-react'; import { type NodeProps, useReactFlow } from '@xyflow/react'; -import { memo } from 'react'; +import { memo, useState } from 'react'; import { RepeatingNumber } from '@/core/intl/NumberFormatter'; import { PercentageFormatter } from '@/core/intl/PercentageFormatter'; import { useStore } from '@/core/zustand'; @@ -35,11 +39,16 @@ import { getRecipeDisplayName, } from '@/recipes/FactoryRecipe'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { + useIsNodeHighlighted, + useSolverHighlightOptional, +} from '@/solver/layout/highlight/SolverHighlightContext'; import { NodeActionsBox } from '@/solver/layout/nodes/utils/NodeActionsBox'; import { InvisibleHandles } from '@/solver/layout/rendering/InvisibleHandles'; import { MachineNodeActions } from './MachineNodeActions'; import { calculateMachineNodeBuildings } from './postprocess/calculateMachineNodeBuildings'; import { RecipeIngredientRow } from './RecipeIngredientRow'; +import { roundOverclock } from './roundOverclock'; export interface IMachineNodeData { label: string; @@ -68,6 +77,10 @@ export const MachineNode = memo((props: IMachineNodeProps) => { const [isHovering, { close, open }] = useDisclosure(false); + const highlight = useSolverHighlightOptional(); + const isPrimaryHighlighted = highlight?.highlightedNodeId === props.id; + const isDimmed = useIsNodeHighlighted(props.id) === false; + const solverId = useFactoryContext(); const nodeState = useStore( @@ -77,6 +90,41 @@ export const MachineNode = memo((props: IMachineNodeProps) => { const overclock = machineCalc.overclock; const buildingsAmount = machineCalc.buildingsAmount; const amplifiedRate = machineCalc.amplifiedRate; + + const [overclockValue, setOverclockValue] = useState( + nodeState?.overclock as number | string, + ); + + const editedOverclock = + overclockValue != null && overclockValue !== '' + ? Number(overclockValue) + : overclock; + + // Back-solve overclock from a target whole-number building count, holding the + // node's required output rate (value) and amplified rate constant. + // buildingsAmount = value / perBuilding / overclock / amplifiedRate + const overclockForBuildings = (target: number) => + roundOverclock( + value / (machineCalc.perBuilding * machineCalc.amplifiedRate * target), + ); + + const flooredBuildings = Math.floor(buildingsAmount); + const ceiledBuildings = Math.ceil(buildingsAmount); + const buildingsAreFractional = ceiledBuildings !== flooredBuildings; + const overclockIfRoundedDown = + flooredBuildings > 0 ? overclockForBuildings(flooredBuildings) : null; + const overclockIfRoundedUp = + ceiledBuildings > 0 ? overclockForBuildings(ceiledBuildings) : null; + // 2.5 == 250% is the same upper bound enforced by MachineNodeProductionConfig. + const canRoundDown = + buildingsAreFractional && + overclockIfRoundedDown != null && + overclockIfRoundedDown <= 2.5; + const canRoundUp = + buildingsAreFractional && + overclockIfRoundedUp != null && + overclockIfRoundedUp > 0; + return ( { borderRadius: 4, border: props.selected ? '1px solid var(--mantine-color-gray-3)' - : '1px solid transparent', + : isPrimaryHighlighted + ? '1px solid var(--mantine-color-blue-4)' + : '1px solid transparent', + opacity: isDimmed ? 0.25 : 1, + transition: 'border-color 0.2s, opacity 0.2s', }} bg={nodeState?.done ? '#304d3e' : 'dark.4'} onMouseEnter={open} @@ -250,17 +302,75 @@ export const MachineNode = memo((props: IMachineNodeProps) => { x {' '} {building.name} + {props.selected && buildingsAreFractional && ( + + + { + if (overclockIfRoundedDown != null) { + setOverclockValue(overclockIfRoundedDown); + } + }} + > + + + + + { + if (overclockIfRoundedUp != null) { + setOverclockValue(overclockIfRoundedUp); + } + }} + > + + + + + )} - {machineCalc.partialBuildingAmount > 0 && ( - - {machineCalc.fullBuildingsAmount} at{' '} - {PercentageFormatter.format(overclock)} - {' + 1 at '} - {PercentageFormatter.format( - machineCalc.partialBuildingOverclock, - )} - - )} + {machineCalc.partialBuildingAmount > 0 && + // Hide the "X at A% + 1 at B%" split when A and B render + // to the same string (e.g. after the round-up button + // picks an overclock where every building runs at the + // same rate). Comparing formatted output avoids picking + // a fragile numeric tolerance when the values may have + // drifted by float noise but display identically. + PercentageFormatter.format( + machineCalc.partialBuildingOverclock, + ) !== PercentageFormatter.format(overclock) && ( + + {machineCalc.fullBuildingsAmount > 0 && ( + <> + {machineCalc.fullBuildingsAmount} at{' '} + {PercentageFormatter.format(overclock)} + {' + '} + + )} + 1 at{' '} + {PercentageFormatter.format( + machineCalc.partialBuildingOverclock, + )} + + )} {' '} @@ -291,6 +401,11 @@ export const MachineNode = memo((props: IMachineNodeProps) => { {PercentageFormatter.format(overclock)} + {machineCalc.totalPowerShards > 0 && ( + + · {machineCalc.totalPowerShards} + + )} {amplifiedRate > 1 && ( @@ -315,8 +430,10 @@ export const MachineNode = memo((props: IMachineNodeProps) => { ingredient={ingredient} key={ingredient.resource} buildingsAmount={buildingsAmount} - overclock={overclock} + overclock={editedOverclock} amplifiedRate={amplifiedRate} + editable={props.selected} + onOverclockChange={setOverclockValue} /> ))} @@ -334,8 +451,10 @@ export const MachineNode = memo((props: IMachineNodeProps) => { ingredient={product} key={product.resource} buildingsAmount={buildingsAmount} - overclock={overclock} + overclock={editedOverclock} amplifiedRate={amplifiedRate} + editable={props.selected} + onOverclockChange={setOverclockValue} /> ))} @@ -347,6 +466,8 @@ export const MachineNode = memo((props: IMachineNodeProps) => { data={props.data} id={props.id} buildingsAmount={buildingsAmount} + overclockValue={overclockValue} + setOverclockValue={setOverclockValue} /> ) : ( diff --git a/src/solver/layout/nodes/machine-node/MachineNodeActions.tsx b/src/solver/layout/nodes/machine-node/MachineNodeActions.tsx index fe20ade7..b6485c76 100644 --- a/src/solver/layout/nodes/machine-node/MachineNodeActions.tsx +++ b/src/solver/layout/nodes/machine-node/MachineNodeActions.tsx @@ -19,6 +19,8 @@ export interface IMachineNodeActionsProps { data: IMachineNodeData; buildingsAmount: number; + overclockValue: number | string; + setOverclockValue: (value: number | string) => void; } /** @@ -26,7 +28,7 @@ export interface IMachineNodeActionsProps { * These are the ones requiring the "apply" button. */ export function MachineNodeActions(props: IMachineNodeActionsProps) { - const { data, buildingsAmount } = props; + const { data, buildingsAmount, overclockValue, setOverclockValue } = props; const { recipe, value } = data; const solverId = useFactoryContext(); @@ -48,18 +50,17 @@ export function MachineNodeActions(props: IMachineNodeActionsProps) { changed: recipesChanged, } = useRecipeAlternatesInputState(data.recipe.id); - // 2. Somersloops (stored per-machine, displayed per-machine) and overclock - // Clamp stored value to slotsPerBuilding for backward compat with old saves - // where the stored value was a total (0..buildings*slots). + // 2. Somersloops (stored per-machine, displayed per-machine). + // Overclock is lifted into the parent MachineNode so that ingredient rows + // can edit it inline (back-solving overclock from a desired /min rate). + // Clamp stored somersloops to slotsPerBuilding for backward compat with old + // saves where the stored value was a total (0..buildings*slots). const storedPerMachine = nodeState?.somersloops ? Math.min(nodeState.somersloops, slotsPerBuilding) : undefined; const [somersloopsValue, setSomersloopsValue] = useInputState( (storedPerMachine ?? '') as number | string, ); - const [overclockValue, setOverclockValue] = useInputState( - nodeState?.overclock as number | string, - ); const perMachineSomersloops = Number(somersloopsValue) || 0; const totalSomersloops = perMachineSomersloops * roundedBuildings; @@ -158,7 +159,7 @@ export function MachineNodeActions(props: IMachineNodeActionsProps) { color="blue" variant="outline" onClick={() => - useStore.getState().addFactoryInput(solverId!, { + useStore.getState().upsertFactoryInput(solverId!, { resource: recipe.products[0].resource, amount: value, }) diff --git a/src/solver/layout/nodes/machine-node/MachineNodeProductionConfig.tsx b/src/solver/layout/nodes/machine-node/MachineNodeProductionConfig.tsx index a2414bbc..2309d450 100644 --- a/src/solver/layout/nodes/machine-node/MachineNodeProductionConfig.tsx +++ b/src/solver/layout/nodes/machine-node/MachineNodeProductionConfig.tsx @@ -3,6 +3,7 @@ import { AllFactoryBuildingsMap } from '@/recipes/FactoryBuilding'; import type { FactoryItemId } from '@/recipes/FactoryItemId'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; import type { IMachineNodeData } from './MachineNode'; +import { roundOverclock } from './roundOverclock'; export interface IMachineNodeProductionConfigProps { id: string; @@ -89,10 +90,14 @@ export function MachineNodeProductionConfig( value={ overclockValue === '' || overclockValue == null ? '' - : Number(overclockValue) * 100 + : // Round to 4 decimal places on the percent scale so values like + // 2.24 don't render as 224.00000000000003 from float pollution. + Math.round(Number(overclockValue) * 100 * 10000) / 10000 } onValueChange={({ floatValue }) => - setOverclockValue(floatValue == null ? '' : floatValue / 100) + setOverclockValue( + floatValue == null ? '' : roundOverclock(floatValue / 100), + ) } min={0} max={250} diff --git a/src/solver/layout/nodes/machine-node/RecipeIngredientRow.tsx b/src/solver/layout/nodes/machine-node/RecipeIngredientRow.tsx index baf70785..907c98e6 100644 --- a/src/solver/layout/nodes/machine-node/RecipeIngredientRow.tsx +++ b/src/solver/layout/nodes/machine-node/RecipeIngredientRow.tsx @@ -1,8 +1,9 @@ -import { Table, Text } from '@mantine/core'; +import { NumberInput, Table, Text } from '@mantine/core'; import { RepeatingNumber } from '@/core/intl/NumberFormatter'; import { AllFactoryItemsMap } from '@/recipes/FactoryItem'; import type { FactoryRecipe, RecipeIngredient } from '@/recipes/FactoryRecipe'; import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { roundOverclock } from './roundOverclock'; export const RecipeIngredientRow = ({ index, @@ -12,6 +13,8 @@ export const RecipeIngredientRow = ({ buildingsAmount, overclock, amplifiedRate, + editable, + onOverclockChange, }: { index: number; type: 'Ingredients' | 'Products'; @@ -20,10 +23,12 @@ export const RecipeIngredientRow = ({ buildingsAmount: number; overclock: number; amplifiedRate: number; + editable?: boolean; + onOverclockChange?: (overclock: number | string) => void; }) => { const item = AllFactoryItemsMap[ingredient.resource]; - const amountPerMinute = - ((ingredient.displayAmount * 60) / recipe.time) * overclock; + const baseRate = (ingredient.displayAmount * 60) / recipe.time; + const amountPerMinute = baseRate * overclock; return ( @@ -38,10 +43,34 @@ export const RecipeIngredientRow = ({ - - - /min - + {editable && onOverclockChange ? ( + { + if (floatValue == null || floatValue <= 0) return; + const newOverclock = roundOverclock(floatValue / baseRate); + onOverclockChange(newOverclock); + }} + min={0} + allowNegative={false} + decimalScale={3} + w={110} + styles={{ + input: { + fontStyle: 'italic', + fontSize: 'var(--mantine-font-size-sm)', + }, + }} + /> + ) : ( + + + /min + + )} diff --git a/src/solver/layout/nodes/machine-node/postprocess/calculateMachineNodeBuildings.tsx b/src/solver/layout/nodes/machine-node/postprocess/calculateMachineNodeBuildings.tsx index 1fd7d3ed..321c3418 100644 --- a/src/solver/layout/nodes/machine-node/postprocess/calculateMachineNodeBuildings.tsx +++ b/src/solver/layout/nodes/machine-node/postprocess/calculateMachineNodeBuildings.tsx @@ -4,6 +4,10 @@ import { getRecipeProductPerBuilding } from '@/recipes/FactoryRecipe'; import type { IMachineNodeData } from '@/solver/layout/nodes/machine-node/MachineNode'; import type { SolverNodeState } from '@/solver/store/Solver'; +function calculatePowerShards(overclock: number): number { + return overclock > 1 ? Math.ceil((overclock - 1) * 2) : 0; +} + export function calculateMachineNodeBuildings( data: IMachineNodeData, nodeState: SolverNodeState | null | undefined, @@ -55,6 +59,14 @@ export function calculateMachineNodeBuildings( overclock ** building.somersloopPowerConsumptionExponent; const totalPower = normalPower + boostedPower; + const powerShardsPerMachine = calculatePowerShards(overclock); + const partialBuildingPowerShards = calculatePowerShards( + partialBuildingOverclock, + ); + const totalPowerShards = + powerShardsPerMachine * fullBuildingsAmount + + partialBuildingPowerShards * partialBuildingAmount; + return { overclock, somersloops, @@ -71,5 +83,8 @@ export function calculateMachineNodeBuildings( partialBuildingOverclock, totalPower, boostedBuildings, + powerShardsPerMachine, + partialBuildingPowerShards, + totalPowerShards, }; } diff --git a/src/solver/layout/nodes/machine-node/roundOverclock.test.ts b/src/solver/layout/nodes/machine-node/roundOverclock.test.ts new file mode 100644 index 00000000..73d676d5 --- /dev/null +++ b/src/solver/layout/nodes/machine-node/roundOverclock.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from 'vitest'; +import { roundOverclock } from './roundOverclock'; + +// Mirrors the display-side rounding done by MachineNodeProductionConfig so we +// can verify what the user actually sees in the % NumberInput, not just the +// stored multiplier. +const displayPercent = (multiplier: number) => + Math.round(multiplier * 100 * 10000) / 10000; + +describe('roundOverclock', () => { + test('keeps clean 2-decimal-percent values exact (75% -> 0.75)', () => { + expect(roundOverclock(0.75)).toBe(0.75); + expect(displayPercent(roundOverclock(0.75))).toBe(75); + }); + + test('regression: round-up button result that displayed as 224.00000000000003% now displays as 224%', () => { + // What the back-solve produces for a typical "round buildings up" case. + // Note: 2.2400000000000003 is the same float as 2.24, but we assert the + // display side using the same rounding the UI does. + const fromBackSolve = 2.24; + const rounded = roundOverclock(fromBackSolve); + expect(displayPercent(rounded)).toBe(224); + }); + + test('falls back to 4-decimal percent when 2dp would lose detail (2/3 -> 66.6667%)', () => { + const rounded = roundOverclock(2 / 3); + expect(displayPercent(rounded)).toBe(66.6667); + }); + + test('keeps 100% as exactly 1', () => { + expect(roundOverclock(1)).toBe(1); + expect(displayPercent(roundOverclock(1))).toBe(100); + }); + + test('handles min and max overclocks cleanly (1% and 250%)', () => { + expect(displayPercent(roundOverclock(0.01))).toBe(1); + expect(displayPercent(roundOverclock(2.5))).toBe(250); + }); + + test('passes through non-finite values', () => { + expect(roundOverclock(Number.NaN)).toBeNaN(); + expect(roundOverclock(Number.POSITIVE_INFINITY)).toBe( + Number.POSITIVE_INFINITY, + ); + }); +}); diff --git a/src/solver/layout/nodes/machine-node/roundOverclock.ts b/src/solver/layout/nodes/machine-node/roundOverclock.ts new file mode 100644 index 00000000..0c8c8401 --- /dev/null +++ b/src/solver/layout/nodes/machine-node/roundOverclock.ts @@ -0,0 +1,25 @@ +/** + * Round an overclock multiplier to a sensible precision for display & storage. + * + * Strategy: work on the percent scale (multiplier * 100), prefer 2-decimal + * percent precision (e.g. 75.00%), and fall back to 4-decimal percent + * precision when 2dp would lose detail (e.g. 2/3 -> 66.6667% rather than + * 66.67%). Operating on the percent scale avoids the floating-point pollution + * that "/ 10000" would introduce on the multiplier scale (e.g. dividing by + * 100 vs 10000 for terminating decimals). + */ +export function roundOverclock(multiplier: number): number { + if (!Number.isFinite(multiplier)) return multiplier; + + const percent = multiplier * 100; + const percent2dp = Math.round(percent * 100) / 100; + const percent4dp = Math.round(percent * 10000) / 10000; + + // Tolerance is on the percent scale: if 2dp and 4dp agree to within + // ~1e-6 % they're effectively the same value (this also absorbs + // float-arithmetic noise from the multiplier->percent conversion). + const chosenPercent = + Math.abs(percent2dp - percent4dp) < 1e-6 ? percent2dp : percent4dp; + + return chosenPercent / 100; +} diff --git a/src/solver/layout/nodes/output-consumer-node/OutputConsumerNode.tsx b/src/solver/layout/nodes/output-consumer-node/OutputConsumerNode.tsx new file mode 100644 index 00000000..be1529d7 --- /dev/null +++ b/src/solver/layout/nodes/output-consumer-node/OutputConsumerNode.tsx @@ -0,0 +1,196 @@ +import { + Anchor, + Badge, + Box, + Button, + Divider, + Flex, + Group, + Popover, + Stack, + Text, + Title, + Tooltip, +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { IconBuildingFactory2, IconExternalLink } from '@tabler/icons-react'; +import type { NodeProps } from '@xyflow/react'; +import { memo } from 'react'; +import { Link } from 'react-router-dom'; +import { RepeatingNumber } from '@/core/intl/NumberFormatter'; +import { FactoryOutputIcon } from '@/factories/components/peek/icons/OutputInputIcons'; +import type { FactoryOutput } from '@/factories/Factory'; +import type { FactoryItem } from '@/recipes/FactoryItem'; +import { FactoryItemImage } from '@/recipes/ui/FactoryItemImage'; +import { + useIsNodeHighlighted, + useSolverHighlightOptional, +} from '@/solver/layout/highlight/SolverHighlightContext'; +import { NodeActionsBox } from '@/solver/layout/nodes/utils/NodeActionsBox'; +import { InvisibleHandles } from '@/solver/layout/rendering/InvisibleHandles'; +import type { SolverNodeState } from '@/solver/store/Solver'; +import { ShowOutputFactoriesNodesAction } from './ShowOutputFactoriesNodesAction'; + +export type IOutputConsumerNodeData = { + resource: FactoryItem; + /** Solver-computed amount actually flowing to this consumer. */ + value: number; + /** Amount the consumer factory has declared it needs. */ + consumerAmount: number; + consumerFactoryId: string; + consumerFactoryName?: string | null; + consumerInputIndex: number; + output?: FactoryOutput; + outputIndex?: number; + + state?: SolverNodeState; +}; + +export type IOutputConsumerNodeProps = NodeProps & { + data: IOutputConsumerNodeData; + type: 'OutputConsumer'; +}; + +export const OutputConsumerNode = memo((props: IOutputConsumerNodeProps) => { + const { id } = props; + const { + resource, + value, + consumerAmount, + consumerFactoryId, + consumerFactoryName, + } = props.data; + + const [isHovering, { close, open }] = useDisclosure(false); + + const highlight = useSolverHighlightOptional(); + const isPrimaryHighlighted = highlight?.highlightedNodeId === id; + const isDimmed = useIsNodeHighlighted(id) === false; + + const factoryLabel = consumerFactoryName || 'Unknown Factory'; + + return ( + + + + + + + + {factoryLabel}: + {resource.displayName} + + + + + + + + /min + + + + + + + + + + + + + <Group gap={4} component="span" align="center"> + <Badge radius={2} color="cyan.9" variant="filled"> + Output + </Badge> + <IconBuildingFactory2 size={16} /> + <Anchor + component={Link} + to={`/factories/${consumerFactoryId}/calculator`} + c="gray" + > + <Group gap={2}> + {factoryLabel} + <IconExternalLink size={12} /> + </Group> + </Anchor> + </Group> + + + + + + + + /min + + + + {resource.displayName} + + + + Requested: + + + /min + + + + + + + + Edit the link from the consumer factory's inputs. + + + + + + + + + + ); +}); diff --git a/src/solver/layout/nodes/output-consumer-node/ShowOutputFactoriesNodesAction.tsx b/src/solver/layout/nodes/output-consumer-node/ShowOutputFactoriesNodesAction.tsx new file mode 100644 index 00000000..ffb00649 --- /dev/null +++ b/src/solver/layout/nodes/output-consumer-node/ShowOutputFactoriesNodesAction.tsx @@ -0,0 +1,40 @@ +import { Select, Stack, Text } from '@mantine/core'; +import { + setShowOutputFactoriesNodes, + useShowOutputFactoriesNodes, +} from '@/games/gamesSlice'; +import { SHOW_OUTPUT_FACTORIES_NODES_OPTIONS } from '@/games/settings/showOutputFactoriesNodesOptions'; + +const SELECT_DATA = SHOW_OUTPUT_FACTORIES_NODES_OPTIONS.map(o => ({ + value: o.value, + label: o.label, +})); + +/** + * Inline editor for the per-game `showOutputFactoriesNodes` setting, + * embedded in the OutputConsumer / Unallocated node popovers so the + * user can hide the new node category from inside the graph itself + * without hunting for the game-settings modal. + */ +export function ShowOutputFactoriesNodesAction() { + const mode = useShowOutputFactoriesNodes(); + return ( + + + Show output factory nodes + +