cyberia 3.0.3 → 3.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.env.production → .env.example} +20 -4
- package/.github/workflows/engine-cyberia.cd.yml +43 -10
- package/.github/workflows/engine-cyberia.ci.yml +48 -26
- package/.github/workflows/ghpkg.ci.yml +5 -5
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/hardhat.ci.yml +82 -0
- package/.github/workflows/npmpkg.ci.yml +60 -14
- package/.github/workflows/publish.ci.yml +26 -7
- package/.github/workflows/publish.cyberia.ci.yml +5 -5
- package/.github/workflows/pwa-microservices-template-page.cd.yml +6 -7
- package/.github/workflows/pwa-microservices-template-test.ci.yml +4 -4
- package/.github/workflows/release.cd.yml +14 -8
- package/.vscode/extensions.json +9 -8
- package/.vscode/settings.json +3 -2
- package/CHANGELOG.md +643 -1
- package/CLI-HELP.md +132 -57
- package/Dockerfile +4 -2
- package/README.md +347 -22
- package/WHITE-PAPER.md +1540 -0
- package/bin/build.js +21 -12
- package/bin/cyberia.js +2640 -106
- package/bin/deploy.js +258 -372
- package/bin/file.js +5 -1
- package/bin/index.js +2640 -106
- package/bin/vs.js +3 -3
- package/conf.js +169 -105
- package/deployment.yaml +236 -20
- package/hardhat/.env.example +31 -0
- package/hardhat/README.md +531 -0
- package/hardhat/WHITE-PAPER.md +1540 -0
- package/hardhat/contracts/ObjectLayerToken.sol +391 -0
- package/hardhat/deployments/.gitkeep +0 -0
- package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
- package/hardhat/hardhat.config.js +136 -0
- package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
- package/hardhat/networks/besu-object-layer.network.json +138 -0
- package/hardhat/package-lock.json +4323 -0
- package/hardhat/package.json +36 -0
- package/hardhat/scripts/deployObjectLayerToken.js +98 -0
- package/hardhat/test/ObjectLayerToken.js +592 -0
- package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
- package/hardhat/types/ethers-contracts/common.ts +92 -0
- package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
- package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
- package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
- package/hardhat/types/ethers-contracts/index.ts +6 -0
- package/jsdoc.dd-cyberia.json +68 -0
- package/jsdoc.json +65 -49
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +562 -0
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +297 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +132 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
- package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/pv-pvc-dd.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +60 -48
- package/proxy.yaml +118 -10
- package/pv-pvc.yaml +132 -0
- package/scripts/k3s-node-setup.sh +1 -1
- package/scripts/ports-ls.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +47 -1
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +17 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
- package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
- package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
- package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
- package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
- package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
- package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
- package/src/api/cyberia-map/cyberia-map.model.js +30 -0
- package/src/api/cyberia-map/cyberia-map.router.js +40 -0
- package/src/api/cyberia-map/cyberia-map.service.js +74 -0
- package/src/api/document/document.service.js +1 -1
- package/src/api/file/file.controller.js +3 -1
- package/src/api/file/file.ref.json +18 -0
- package/src/api/file/file.service.js +28 -5
- package/src/api/ipfs/ipfs.controller.js +4 -25
- package/src/api/ipfs/ipfs.model.js +43 -34
- package/src/api/ipfs/ipfs.router.js +8 -13
- package/src/api/ipfs/ipfs.service.js +56 -104
- package/src/api/object-layer/README.md +347 -22
- package/src/api/object-layer/object-layer.controller.js +6 -2
- package/src/api/object-layer/object-layer.model.js +12 -8
- package/src/api/object-layer/object-layer.router.js +698 -42
- package/src/api/object-layer/object-layer.service.js +119 -37
- package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +1 -2
- package/src/api/user/user.router.js +10 -5
- package/src/api/user/user.service.js +15 -14
- package/src/cli/baremetal.js +6 -10
- package/src/cli/cloud-init.js +0 -3
- package/src/cli/cluster.js +7 -7
- package/src/cli/db.js +723 -857
- package/src/cli/deploy.js +215 -105
- package/src/cli/env.js +34 -5
- package/src/cli/fs.js +5 -4
- package/src/cli/image.js +0 -3
- package/src/cli/index.js +83 -15
- package/src/cli/kubectl.js +211 -0
- package/src/cli/monitor.js +5 -6
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +708 -62
- package/src/cli/run.js +371 -151
- package/src/cli/secrets.js +73 -2
- package/src/cli/ssh.js +1 -1
- package/src/cli/test.js +3 -3
- package/src/client/Cryptokoyn.index.js +3 -4
- package/src/client/CyberiaPortal.index.js +3 -4
- package/src/client/Default.index.js +3 -4
- package/src/client/Itemledger.index.js +4 -963
- package/src/client/Underpost.index.js +3 -4
- package/src/client/components/core/AgGrid.js +20 -5
- package/src/client/components/core/Alert.js +2 -2
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/CalendarCore.js +2 -2
- package/src/client/components/core/Content.js +22 -3
- package/src/client/components/core/Docs.js +30 -6
- package/src/client/components/core/DropDown.js +137 -17
- package/src/client/components/core/FileExplorer.js +71 -4
- package/src/client/components/core/Input.js +1 -1
- package/src/client/components/core/Keyboard.js +2 -2
- package/src/client/components/core/LogIn.js +2 -2
- package/src/client/components/core/LogOut.js +2 -2
- package/src/client/components/core/Modal.js +20 -7
- package/src/client/components/core/Panel.js +0 -1
- package/src/client/components/core/PanelForm.js +19 -19
- package/src/client/components/core/RichText.js +1 -2
- package/src/client/components/core/SocketIo.js +82 -29
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
- package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
- package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
- package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
- package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
- package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
- package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
- package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +217 -30
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +44 -2
- package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -4
- package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +104 -9
- package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/LogInDefault.js +3 -3
- package/src/client/components/default/LogOutDefault.js +2 -2
- package/src/client/components/default/MenuDefault.js +5 -5
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
- package/src/client/components/itemledger/LogInItemledger.js +3 -3
- package/src/client/components/itemledger/LogOutItemledger.js +2 -2
- package/src/client/components/itemledger/MenuItemledger.js +3 -3
- package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
- package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
- package/src/client/components/underpost/CssUnderpost.js +59 -0
- package/src/client/components/underpost/LogInUnderpost.js +6 -3
- package/src/client/components/underpost/LogOutUnderpost.js +4 -2
- package/src/client/components/underpost/MenuUnderpost.js +104 -18
- package/src/client/components/underpost/RoutesUnderpost.js +2 -0
- package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
- package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
- package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
- package/src/client/public/cryptokoyn/microdata.json +85 -0
- package/src/client/public/cryptokoyn/site.webmanifest +57 -0
- package/src/client/public/cryptokoyn/sitemap +3 -3
- package/src/client/public/default/sitemap +3 -3
- package/src/client/public/itemledger/browserconfig.xml +2 -2
- package/src/client/public/itemledger/manifest.webmanifest +4 -4
- package/src/client/public/itemledger/microdata.json +71 -0
- package/src/client/public/itemledger/sitemap +3 -3
- package/src/client/public/itemledger/yandex-browser-manifest.json +2 -2
- package/src/client/public/test/sitemap +3 -3
- package/src/client/services/core/core.service.js +20 -8
- package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
- package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
- package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
- package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
- package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
- package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
- package/src/client/services/instance/instance.management.js +2 -2
- package/src/client/services/ipfs/ipfs.service.js +3 -23
- package/src/client/services/object-layer/object-layer.management.js +3 -3
- package/src/client/services/object-layer/object-layer.service.js +21 -0
- package/src/client/services/user/user.management.js +2 -2
- package/src/client/ssr/body/404.js +15 -11
- package/src/client/ssr/body/500.js +15 -11
- package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
- package/src/client/ssr/head/PwaItemledger.js +60 -0
- package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
- package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/pages/Test.js +11 -10
- package/src/client.build.js +0 -3
- package/src/client.dev.js +0 -3
- package/src/db/DataBaseProvider.js +17 -2
- package/src/db/mariadb/MariaDB.js +14 -9
- package/src/db/mongo/MongooseDB.js +17 -1
- package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
- package/src/grpc/cyberia/README.md +326 -0
- package/src/grpc/cyberia/grpc-server.js +530 -0
- package/src/index.js +24 -1
- package/src/proxy.js +0 -3
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/express/Express.js +33 -10
- package/src/runtime/lampp/Dockerfile +13 -2
- package/src/runtime/lampp/Lampp.js +33 -17
- package/src/runtime/wp/Dockerfile +68 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/auth.js +36 -15
- package/src/server/backup.js +39 -12
- package/src/server/besu-genesis-generator.js +1630 -0
- package/src/server/client-build-docs.js +133 -17
- package/src/server/client-build-live.js +9 -18
- package/src/server/client-build.js +229 -101
- package/src/server/client-dev-server.js +14 -13
- package/src/server/client-formatted.js +109 -57
- package/src/server/conf.js +391 -164
- package/src/server/cron.js +27 -24
- package/src/server/dns.js +29 -12
- package/src/server/downloader.js +0 -2
- package/src/server/ipfs-client.js +24 -1
- package/src/server/logger.js +27 -9
- package/src/server/object-layer.js +217 -103
- package/src/server/peer.js +8 -2
- package/src/server/process.js +1 -50
- package/src/server/proxy.js +4 -8
- package/src/server/runtime.js +30 -9
- package/src/server/semantic-layer-generator-floor.js +359 -0
- package/src/server/semantic-layer-generator-skin.js +1294 -0
- package/src/server/semantic-layer-generator.js +116 -555
- package/src/server/ssr.js +0 -3
- package/src/server/start.js +19 -12
- package/src/server/tls.js +0 -2
- package/src/server.js +0 -4
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/.env.development +0 -43
- package/.env.test +0 -43
- package/hardhat/contracts/CryptoKoyn.sol +0 -59
- package/hardhat/contracts/ItemLedger.sol +0 -73
- package/hardhat/contracts/Lock.sol +0 -34
- package/hardhat/hardhat.config.cjs +0 -45
- package/hardhat/ignition/modules/Lock.js +0 -18
- package/hardhat/networks/cryptokoyn-itemledger.network.json +0 -29
- package/hardhat/scripts/deployCryptokoyn.cjs +0 -25
- package/hardhat/scripts/deployItemledger.cjs +0 -25
- package/hardhat/test/Lock.js +0 -126
- package/hardhat/white-paper.md +0 -581
- package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
- package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
- package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/client/components/itemledger/CommonItemledger.js +0 -29
- package/src/client/components/itemledger/ElementsItemledger.js +0 -38
- package/src/client/components/underpost/CommonUnderpost.js +0 -29
- package/src/client/components/underpost/ElementsUnderpost.js +0 -38
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
- package/white-paper.md +0 -581
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { DataBaseProvider } from '../../db/DataBaseProvider.js';
|
|
2
|
+
import { loggerFactory } from '../../server/logger.js';
|
|
3
|
+
import { DataQuery } from '../../server/data-query.js';
|
|
4
|
+
import { connectPortals, generateProceduralEntities } from './cyberia-portal-connector.js';
|
|
5
|
+
import { generateFallbackWorld } from './cyberia-fallback-world.js';
|
|
6
|
+
import { CYBERIA_INSTANCE_CONF_DEFAULTS } from '../cyberia-instance-conf/cyberia-instance-conf.defaults.js';
|
|
7
|
+
|
|
8
|
+
const logger = loggerFactory(import.meta);
|
|
9
|
+
|
|
10
|
+
const CyberiaInstanceService = {
|
|
11
|
+
post: async (req, res, options) => {
|
|
12
|
+
/** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
|
|
13
|
+
const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
|
|
14
|
+
const CyberiaInstanceConf =
|
|
15
|
+
DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstanceConf;
|
|
16
|
+
if (req.auth && req.auth.user) req.body.creator = req.auth.user._id;
|
|
17
|
+
const instance = await new CyberiaInstance(req.body).save();
|
|
18
|
+
|
|
19
|
+
// Auto-upsert a CyberiaInstanceConf for this instance using schema defaults.
|
|
20
|
+
// $setOnInsert ensures existing conf documents are never overwritten.
|
|
21
|
+
if (instance.code && CyberiaInstanceConf) {
|
|
22
|
+
try {
|
|
23
|
+
const conf = await CyberiaInstanceConf.findOneAndUpdate(
|
|
24
|
+
{ instanceCode: instance.code },
|
|
25
|
+
{ $setOnInsert: { instanceCode: instance.code } },
|
|
26
|
+
{ upsert: true, new: true },
|
|
27
|
+
);
|
|
28
|
+
if (conf && !instance.conf) {
|
|
29
|
+
await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: conf._id });
|
|
30
|
+
instance.conf = conf._id;
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
logger.error('auto-upsert CyberiaInstanceConf failed:', e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return instance;
|
|
38
|
+
},
|
|
39
|
+
get: async (req, res, options) => {
|
|
40
|
+
/** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
|
|
41
|
+
const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
|
|
42
|
+
const populateCreator = { path: 'creator', model: 'User', select: '_id username' };
|
|
43
|
+
if (req.params.id) return await CyberiaInstance.findById(req.params.id).populate(populateCreator);
|
|
44
|
+
|
|
45
|
+
// Parse query parameters using DataQuery helper
|
|
46
|
+
const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
|
|
47
|
+
|
|
48
|
+
const [data, total] = await Promise.all([
|
|
49
|
+
CyberiaInstance.find(query).sort(sort).limit(limit).skip(skip).populate(populateCreator),
|
|
50
|
+
CyberiaInstance.countDocuments(query),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const totalPages = Math.ceil(total / limit);
|
|
54
|
+
return { data, total, page, totalPages };
|
|
55
|
+
},
|
|
56
|
+
put: async (req, res, options) => {
|
|
57
|
+
/** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
|
|
58
|
+
const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
|
|
59
|
+
const instance = await CyberiaInstance.findById(req.params.id);
|
|
60
|
+
if (!instance) throw new Error('instance not found');
|
|
61
|
+
if (req.auth.user.role !== 'admin' && String(instance.creator) !== String(req.auth.user._id))
|
|
62
|
+
throw new Error('insufficient permission');
|
|
63
|
+
if (req.body.thumbnail && instance.thumbnail && String(req.body.thumbnail) !== String(instance.thumbnail)) {
|
|
64
|
+
const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
|
|
65
|
+
await File.findByIdAndDelete(instance.thumbnail);
|
|
66
|
+
}
|
|
67
|
+
return await CyberiaInstance.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' });
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Central portal connector endpoint.
|
|
71
|
+
*
|
|
72
|
+
* Delegates topology computation to the pure-function `connectPortals()`
|
|
73
|
+
* from cyberia-portal-connector.js so the same logic can be used by the
|
|
74
|
+
* GUI without a DB dependency.
|
|
75
|
+
*
|
|
76
|
+
* Optionally generates procedural fallback obstacle/foreground entities
|
|
77
|
+
* for maps that have none, controlled by query flags:
|
|
78
|
+
* ?generateEntities=true — append procedural obstacles & foreground
|
|
79
|
+
* ?obstacleCount=N — obstacles per map (default 5)
|
|
80
|
+
* ?foregroundCount=N — foreground per map (default 3)
|
|
81
|
+
* ?persist=true — save generated portals & entities to DB
|
|
82
|
+
*/
|
|
83
|
+
portalConnect: async (req, res, options) => {
|
|
84
|
+
const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
|
|
85
|
+
const CyberiaMap = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaMap;
|
|
86
|
+
|
|
87
|
+
const instance = await CyberiaInstance.findById(req.params.id).lean();
|
|
88
|
+
if (!instance) throw new Error('instance not found');
|
|
89
|
+
|
|
90
|
+
const mapCodes = instance.cyberiaMapCodes || [];
|
|
91
|
+
|
|
92
|
+
// Load maps with the fields needed by the connector.
|
|
93
|
+
const mapDocs = await CyberiaMap.find(
|
|
94
|
+
{ code: { $in: mapCodes } },
|
|
95
|
+
{
|
|
96
|
+
code: 1,
|
|
97
|
+
gridX: 1,
|
|
98
|
+
gridY: 1,
|
|
99
|
+
entities: 1,
|
|
100
|
+
},
|
|
101
|
+
).lean();
|
|
102
|
+
|
|
103
|
+
// ── Portal topology (pure function) ──────────────────────────────────
|
|
104
|
+
const result = connectPortals(mapCodes, mapDocs);
|
|
105
|
+
|
|
106
|
+
// ── Procedural entity generation (optional) ──────────────────────────
|
|
107
|
+
const colors = CYBERIA_INSTANCE_CONF_DEFAULTS.colors;
|
|
108
|
+
const wantEntities = req.query?.generateEntities === 'true';
|
|
109
|
+
const obstacleCount = req.query?.obstacleCount ? parseInt(req.query.obstacleCount, 10) : undefined;
|
|
110
|
+
const foregroundCount = req.query?.foregroundCount ? parseInt(req.query.foregroundCount, 10) : undefined;
|
|
111
|
+
const seed = instance.seed || '';
|
|
112
|
+
|
|
113
|
+
const generatedEntities = {};
|
|
114
|
+
if (wantEntities) {
|
|
115
|
+
for (const doc of mapDocs) {
|
|
116
|
+
const hasObstacles = (doc.entities || []).some((e) => e.entityType === 'obstacle');
|
|
117
|
+
const hasForeground = (doc.entities || []).some((e) => e.entityType === 'foreground');
|
|
118
|
+
if (!hasObstacles || !hasForeground) {
|
|
119
|
+
const mapSeed = seed ? `${seed}:${doc.code}` : doc.code;
|
|
120
|
+
const generated = generateProceduralEntities({ gridX: doc.gridX || 16, gridY: doc.gridY || 16 }, colors, {
|
|
121
|
+
obstacleCount,
|
|
122
|
+
foregroundCount,
|
|
123
|
+
seed: mapSeed,
|
|
124
|
+
});
|
|
125
|
+
generatedEntities[doc.code] = {
|
|
126
|
+
obstacles: hasObstacles ? [] : generated.obstacles,
|
|
127
|
+
foreground: hasForeground ? [] : generated.foreground,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Persist to DB when requested ─────────────────────────────────────
|
|
134
|
+
const persist = req.query?.persist === 'true';
|
|
135
|
+
if (persist) {
|
|
136
|
+
await CyberiaInstance.findByIdAndUpdate(req.params.id, { portals: result.portals });
|
|
137
|
+
for (const [mapCode, ents] of Object.entries(generatedEntities)) {
|
|
138
|
+
const toAdd = [...ents.obstacles, ...ents.foreground];
|
|
139
|
+
if (toAdd.length > 0) {
|
|
140
|
+
await CyberiaMap.findOneAndUpdate({ code: mapCode }, { $push: { entities: { $each: toAdd } } });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...result,
|
|
147
|
+
...(wantEntities ? { generatedEntities } : {}),
|
|
148
|
+
persisted: persist,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
delete: async (req, res, options) => {
|
|
153
|
+
/** @type {import('./cyberia-instance.model.js').CyberiaInstanceModel} */
|
|
154
|
+
const CyberiaInstance = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.CyberiaInstance;
|
|
155
|
+
if (req.params.id) {
|
|
156
|
+
const instance = await CyberiaInstance.findById(req.params.id);
|
|
157
|
+
if (!instance) throw new Error('instance not found');
|
|
158
|
+
if (req.auth.user.role !== 'admin' && String(instance.creator) !== String(req.auth.user._id))
|
|
159
|
+
throw new Error('insufficient permission');
|
|
160
|
+
if (instance.thumbnail) {
|
|
161
|
+
const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
|
|
162
|
+
await File.findByIdAndDelete(instance.thumbnail);
|
|
163
|
+
}
|
|
164
|
+
return await CyberiaInstance.findByIdAndDelete(req.params.id);
|
|
165
|
+
} else return await CyberiaInstance.deleteMany();
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Return an in-memory procedural fallback world.
|
|
170
|
+
*
|
|
171
|
+
* Nothing is persisted to MongoDB. The world is regenerated on every
|
|
172
|
+
* call but stays deterministic for a given seed.
|
|
173
|
+
*
|
|
174
|
+
* Query params:
|
|
175
|
+
* ?mapCount=<number> — maps to generate (default: 4)
|
|
176
|
+
* ?botCount=<number> — bots per map (random 8–16 if omitted)
|
|
177
|
+
* ?obstacleCount=<number> — obstacles per map (random 12–20 if omitted)
|
|
178
|
+
* ?foregroundCount=<number>— foreground per map (random 6–12 if omitted)
|
|
179
|
+
*/
|
|
180
|
+
fallbackWorld: async (req) => {
|
|
181
|
+
const q = req.query || {};
|
|
182
|
+
return generateFallbackWorld({
|
|
183
|
+
mapCount: q.mapCount ? parseInt(q.mapCount, 10) : undefined,
|
|
184
|
+
botCount: q.botCount ? parseInt(q.botCount, 10) : undefined,
|
|
185
|
+
obstacleCount: q.obstacleCount ? parseInt(q.obstacleCount, 10) : undefined,
|
|
186
|
+
foregroundCount: q.foregroundCount ? parseInt(q.foregroundCount, 10) : undefined,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export { CyberiaInstanceService };
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central Portal Connector — pure-function module.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the backend (CyberiaInstanceService) and the GUI (map editor)
|
|
5
|
+
* to build, validate, and procedurally generate portal topology and world
|
|
6
|
+
* entities for a CyberiaInstance.
|
|
7
|
+
*
|
|
8
|
+
* All exported functions are stateless and synchronous — they operate on
|
|
9
|
+
* plain JS objects (lean Mongoose docs or JSON from the API) so the GUI
|
|
10
|
+
* can call them directly without a DB dependency.
|
|
11
|
+
*
|
|
12
|
+
* @module src/api/cyberia-instance/cyberia-portal-connector
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── Color helpers ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert a { r, g, b, a } palette entry to an `rgba(…)` CSS string.
|
|
19
|
+
* @param {{ r: number, g: number, b: number, a: number }} c
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
const colorToRgba = (c) => `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a / 255})`;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Look up a palette entry by key from a colours array.
|
|
26
|
+
* @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors
|
|
27
|
+
* @param {string} key
|
|
28
|
+
* @returns {{ r: number, g: number, b: number, a: number } | undefined}
|
|
29
|
+
*/
|
|
30
|
+
const findColor = (colors, key) => colors.find((c) => c.key === key);
|
|
31
|
+
|
|
32
|
+
// ── Random helpers ───────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return a random integer in [min, max] (inclusive).
|
|
36
|
+
* @param {number} min
|
|
37
|
+
* @param {number} max
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
const randInt = (min, max) => min + Math.floor(Math.random() * (max - min + 1));
|
|
41
|
+
|
|
42
|
+
// ── Occupancy grid ───────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 2D boolean grid that tracks which cells are blocked (obstacle / placed entity).
|
|
46
|
+
* Used to find valid walkable positions when placing portals and bots.
|
|
47
|
+
*/
|
|
48
|
+
class OccupancyGrid {
|
|
49
|
+
/**
|
|
50
|
+
* @param {number} width Grid columns.
|
|
51
|
+
* @param {number} height Grid rows.
|
|
52
|
+
*/
|
|
53
|
+
constructor(width, height) {
|
|
54
|
+
this.width = width;
|
|
55
|
+
this.height = height;
|
|
56
|
+
// false = walkable, true = blocked
|
|
57
|
+
this.cells = Array.from({ length: height }, () => new Array(width).fill(false));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mark a rectangular region as blocked.
|
|
62
|
+
* @param {number} x
|
|
63
|
+
* @param {number} y
|
|
64
|
+
* @param {number} w
|
|
65
|
+
* @param {number} h
|
|
66
|
+
*/
|
|
67
|
+
block(x, y, w, h) {
|
|
68
|
+
for (let row = y; row < y + h && row < this.height; row++) {
|
|
69
|
+
for (let col = x; col < x + w && col < this.width; col++) {
|
|
70
|
+
if (row >= 0 && col >= 0) this.cells[row][col] = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check whether a rectangle fits entirely within walkable (unblocked) cells.
|
|
77
|
+
* @param {number} x
|
|
78
|
+
* @param {number} y
|
|
79
|
+
* @param {number} w
|
|
80
|
+
* @param {number} h
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
fits(x, y, w, h) {
|
|
84
|
+
if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) return false;
|
|
85
|
+
for (let row = y; row < y + h; row++) {
|
|
86
|
+
for (let col = x; col < x + w; col++) {
|
|
87
|
+
if (this.cells[row][col]) return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find a random walkable position for a rectangle of given dimensions.
|
|
95
|
+
* Tries up to `maxAttempts` random positions before giving up.
|
|
96
|
+
* @param {number} w
|
|
97
|
+
* @param {number} h
|
|
98
|
+
* @param {number} [maxAttempts=200]
|
|
99
|
+
* @returns {{ x: number, y: number } | null} Position or null if no fit found.
|
|
100
|
+
*/
|
|
101
|
+
findPosition(w, h, maxAttempts = 200) {
|
|
102
|
+
const maxX = Math.max(0, this.width - w);
|
|
103
|
+
const maxY = Math.max(0, this.height - h);
|
|
104
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
105
|
+
const x = randInt(0, maxX);
|
|
106
|
+
const y = randInt(0, maxY);
|
|
107
|
+
if (this.fits(x, y, w, h)) return { x, y };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Populate the grid from an array of obstacle entities.
|
|
114
|
+
* @param {Array<{ initCellX: number, initCellY: number, dimX: number, dimY: number }>} obstacles
|
|
115
|
+
*/
|
|
116
|
+
addObstacles(obstacles) {
|
|
117
|
+
for (const o of obstacles) {
|
|
118
|
+
this.block(o.initCellX, o.initCellY, o.dimX, o.dimY);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Portal topology builders ─────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Canonical portal mode strings.
|
|
127
|
+
* @enum {string}
|
|
128
|
+
*/
|
|
129
|
+
const PORTAL_MODES = Object.freeze({
|
|
130
|
+
INTER_PORTAL: 'inter-portal', // teleport to a portal on another map
|
|
131
|
+
INTER_RANDOM: 'inter-random', // teleport to a random spot on another map
|
|
132
|
+
INTRA_RANDOM: 'intra-random', // teleport to a random spot on the same map
|
|
133
|
+
INTRA_PORTAL: 'intra-portal', // teleport to a portal on the same map
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* All portal mode values as an array (for random selection).
|
|
138
|
+
* @type {string[]}
|
|
139
|
+
*/
|
|
140
|
+
const PORTAL_MODE_LIST = Object.values(PORTAL_MODES);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Map from portal mode to its palette colour key.
|
|
144
|
+
* @type {Record<string, string>}
|
|
145
|
+
*/
|
|
146
|
+
const PORTAL_MODE_COLOR_KEY = Object.freeze({
|
|
147
|
+
[PORTAL_MODES.INTER_PORTAL]: 'PORTAL_INTER_PORTAL',
|
|
148
|
+
[PORTAL_MODES.INTER_RANDOM]: 'PORTAL_INTER_RANDOM',
|
|
149
|
+
[PORTAL_MODES.INTRA_RANDOM]: 'PORTAL_INTRA_RANDOM',
|
|
150
|
+
[PORTAL_MODES.INTRA_PORTAL]: 'PORTAL_INTRA_PORTAL',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Portal modes available for extra (non-ring) portals.
|
|
155
|
+
* The ring always uses INTER_PORTAL; extras are randomly chosen from these.
|
|
156
|
+
* @type {string[]}
|
|
157
|
+
*/
|
|
158
|
+
const EXTRA_PORTAL_MODES = [PORTAL_MODES.INTRA_PORTAL, PORTAL_MODES.INTRA_RANDOM, PORTAL_MODES.INTER_RANDOM];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Extract all portal-type entities from each map document and build
|
|
162
|
+
* a lookup: `{ [mapCode]: portalEntity[] }`.
|
|
163
|
+
*
|
|
164
|
+
* @param {Array<{ code: string, entities: Array<{ entityType: string, portalSubtype?: string, initCellX: number, initCellY: number }> }>} maps
|
|
165
|
+
* @returns {Record<string, object[]>}
|
|
166
|
+
*/
|
|
167
|
+
function indexPortalEntities(maps) {
|
|
168
|
+
const idx = {};
|
|
169
|
+
for (const map of maps) {
|
|
170
|
+
idx[map.code] = (map.entities || []).filter((e) => e.entityType === 'portal');
|
|
171
|
+
}
|
|
172
|
+
return idx;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build portal edges from a set of maps with portal entities.
|
|
177
|
+
*
|
|
178
|
+
* Phase 1 — **Ring guarantee**: creates an inter-portal ring that
|
|
179
|
+
* connects every map in a circle (0→1→2→…→n-1→0) so that every map
|
|
180
|
+
* is reachable from every other map. One portal entity per map is
|
|
181
|
+
* consumed for the ring.
|
|
182
|
+
*
|
|
183
|
+
* Phase 2 — **Extra edges**: remaining portal entities (those not used
|
|
184
|
+
* in the ring) produce edges according to their `portalSubtype`:
|
|
185
|
+
* inter-portal → portal on a DIFFERENT map
|
|
186
|
+
* inter-random → random pos on a DIFFERENT map
|
|
187
|
+
* intra-random → random pos on the SAME map
|
|
188
|
+
* intra-portal → portal on the SAME map
|
|
189
|
+
*
|
|
190
|
+
* @param {string[]} orderedCodes Map codes in instance order.
|
|
191
|
+
* @param {Record<string, object[]>} portalIndex From `indexPortalEntities`.
|
|
192
|
+
* @returns {{ portals: object[], topology: string }}
|
|
193
|
+
*/
|
|
194
|
+
function buildTopologyFromSubtypes(orderedCodes, portalIndex) {
|
|
195
|
+
const n = orderedCodes.length;
|
|
196
|
+
if (n < 1) return { portals: [], topology: 'none' };
|
|
197
|
+
|
|
198
|
+
const portals = [];
|
|
199
|
+
const usedInRing = new Set();
|
|
200
|
+
|
|
201
|
+
// ── Phase 1: Guaranteed inter-portal ring ───────────────────────────
|
|
202
|
+
// Each map links to the next in a circle: 0→1→2→…→(n-1)→0.
|
|
203
|
+
if (n >= 2) {
|
|
204
|
+
for (let i = 0; i < n; i++) {
|
|
205
|
+
const srcCode = orderedCodes[i];
|
|
206
|
+
const tgtCode = orderedCodes[(i + 1) % n];
|
|
207
|
+
|
|
208
|
+
// Prefer an unused inter-portal entity as source; fall back to any unused, then any
|
|
209
|
+
const srcAll = portalIndex[srcCode] || [];
|
|
210
|
+
const srcInterUnused = srcAll.filter(
|
|
211
|
+
(e) => (e.portalSubtype || PORTAL_MODES.INTER_PORTAL) === PORTAL_MODES.INTER_PORTAL && !usedInRing.has(e),
|
|
212
|
+
);
|
|
213
|
+
const srcAnyUnused = srcAll.filter((e) => !usedInRing.has(e));
|
|
214
|
+
const srcEnt = srcInterUnused[0] || srcAnyUnused[0] || srcAll[0];
|
|
215
|
+
|
|
216
|
+
// Target: pick any portal on the target map for landing coordinates
|
|
217
|
+
const tgtAll = portalIndex[tgtCode] || [];
|
|
218
|
+
const tgtEnt = tgtAll.length > 0 ? tgtAll[Math.floor(Math.random() * tgtAll.length)] : null;
|
|
219
|
+
|
|
220
|
+
if (srcEnt) {
|
|
221
|
+
usedInRing.add(srcEnt);
|
|
222
|
+
portals.push({
|
|
223
|
+
sourceMapCode: srcCode,
|
|
224
|
+
sourceCellX: srcEnt.initCellX ?? 0,
|
|
225
|
+
sourceCellY: srcEnt.initCellY ?? 0,
|
|
226
|
+
targetMapCode: tgtCode,
|
|
227
|
+
targetCellX: tgtEnt?.initCellX ?? 0,
|
|
228
|
+
targetCellY: tgtEnt?.initCellY ?? 0,
|
|
229
|
+
portalMode: PORTAL_MODES.INTER_PORTAL,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Phase 2: Extra edges from remaining portals ─────────────────────
|
|
236
|
+
const otherMap = (srcCode) => {
|
|
237
|
+
if (n < 2) return srcCode;
|
|
238
|
+
let code;
|
|
239
|
+
do {
|
|
240
|
+
code = orderedCodes[Math.floor(Math.random() * n)];
|
|
241
|
+
} while (code === srcCode && n > 1);
|
|
242
|
+
return code;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
for (const srcCode of orderedCodes) {
|
|
246
|
+
const allOnMap = portalIndex[srcCode] || [];
|
|
247
|
+
const remaining = allOnMap.filter((e) => !usedInRing.has(e));
|
|
248
|
+
|
|
249
|
+
for (const srcEnt of remaining) {
|
|
250
|
+
const sub = srcEnt.portalSubtype || PORTAL_MODES.INTER_PORTAL;
|
|
251
|
+
|
|
252
|
+
switch (sub) {
|
|
253
|
+
case PORTAL_MODES.INTER_PORTAL: {
|
|
254
|
+
const tgtCode = otherMap(srcCode);
|
|
255
|
+
const candidates = portalIndex[tgtCode] || [];
|
|
256
|
+
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
|
|
257
|
+
portals.push({
|
|
258
|
+
sourceMapCode: srcCode,
|
|
259
|
+
sourceCellX: srcEnt.initCellX ?? 0,
|
|
260
|
+
sourceCellY: srcEnt.initCellY ?? 0,
|
|
261
|
+
targetMapCode: tgtCode,
|
|
262
|
+
targetCellX: tgtEnt?.initCellX ?? 0,
|
|
263
|
+
targetCellY: tgtEnt?.initCellY ?? 0,
|
|
264
|
+
portalMode: PORTAL_MODES.INTER_PORTAL,
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case PORTAL_MODES.INTER_RANDOM: {
|
|
269
|
+
const tgtCode = otherMap(srcCode);
|
|
270
|
+
portals.push({
|
|
271
|
+
sourceMapCode: srcCode,
|
|
272
|
+
sourceCellX: srcEnt.initCellX ?? 0,
|
|
273
|
+
sourceCellY: srcEnt.initCellY ?? 0,
|
|
274
|
+
targetMapCode: tgtCode,
|
|
275
|
+
targetCellX: -1,
|
|
276
|
+
targetCellY: -1,
|
|
277
|
+
portalMode: PORTAL_MODES.INTER_RANDOM,
|
|
278
|
+
});
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case PORTAL_MODES.INTRA_RANDOM: {
|
|
282
|
+
portals.push({
|
|
283
|
+
sourceMapCode: srcCode,
|
|
284
|
+
sourceCellX: srcEnt.initCellX ?? 0,
|
|
285
|
+
sourceCellY: srcEnt.initCellY ?? 0,
|
|
286
|
+
targetMapCode: srcCode,
|
|
287
|
+
targetCellX: -1,
|
|
288
|
+
targetCellY: -1,
|
|
289
|
+
portalMode: PORTAL_MODES.INTRA_RANDOM,
|
|
290
|
+
});
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case PORTAL_MODES.INTRA_PORTAL: {
|
|
294
|
+
const candidates = allOnMap.filter(
|
|
295
|
+
(e) => e !== srcEnt && (e.initCellX !== srcEnt.initCellX || e.initCellY !== srcEnt.initCellY),
|
|
296
|
+
);
|
|
297
|
+
const tgtEnt = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : null;
|
|
298
|
+
portals.push({
|
|
299
|
+
sourceMapCode: srcCode,
|
|
300
|
+
sourceCellX: srcEnt.initCellX ?? 0,
|
|
301
|
+
sourceCellY: srcEnt.initCellY ?? 0,
|
|
302
|
+
targetMapCode: srcCode,
|
|
303
|
+
targetCellX: tgtEnt?.initCellX ?? 0,
|
|
304
|
+
targetCellY: tgtEnt?.initCellY ?? 0,
|
|
305
|
+
portalMode: PORTAL_MODES.INTRA_PORTAL,
|
|
306
|
+
});
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { portals, topology: n === 1 ? 'intra-only' : 'ring+mixed' };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Central portal-connect pipeline.
|
|
318
|
+
*
|
|
319
|
+
* Given an instance's ordered map codes and the full map documents (with
|
|
320
|
+
* entities), returns the auto-generated portal edge list.
|
|
321
|
+
*
|
|
322
|
+
* @param {string[]} mapCodes Instance's `cyberiaMapCodes` array.
|
|
323
|
+
* @param {Array<{ code: string, entities: object[] }>} maps Map documents (lean or JSON).
|
|
324
|
+
* @returns {{ portals: object[], topology: string, mapCount: number }}
|
|
325
|
+
*/
|
|
326
|
+
function connectPortals(mapCodes, maps) {
|
|
327
|
+
if (!mapCodes || mapCodes.length < 1) {
|
|
328
|
+
return { portals: [], topology: 'none', mapCount: mapCodes?.length ?? 0, message: 'Need at least 1 map.' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const portalIndex = indexPortalEntities(maps);
|
|
332
|
+
|
|
333
|
+
// Filter to codes that actually exist in the fetched maps.
|
|
334
|
+
const knownCodes = new Set(maps.map((m) => m.code));
|
|
335
|
+
const ordered = mapCodes.filter((c) => knownCodes.has(c));
|
|
336
|
+
if (ordered.length < 1) {
|
|
337
|
+
return { portals: [], topology: 'none', mapCount: ordered.length, message: 'Need at least 1 map.' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { portals, topology } = buildTopologyFromSubtypes(ordered, portalIndex);
|
|
341
|
+
return { portals, topology, mapCount: ordered.length };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Procedural entity generators ─────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate procedural obstacle entities for a map.
|
|
348
|
+
*
|
|
349
|
+
* Obstacles use empty `objectLayerItemIds` so they render as a solid colour
|
|
350
|
+
* from the OBSTACLE palette entry. Count, dimensions, and positions are
|
|
351
|
+
* all fully random within the declared ranges.
|
|
352
|
+
*
|
|
353
|
+
* @param {{ gridX: number, gridY: number }} mapDims Map grid dimensions.
|
|
354
|
+
* @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors Palette.
|
|
355
|
+
* @param {object} [opts]
|
|
356
|
+
* @param {number} [opts.count] Override count (ignores range).
|
|
357
|
+
* @param {number} [opts.minDim=1] Minimum obstacle width/height (cells).
|
|
358
|
+
* @param {number} [opts.maxDim=4] Maximum obstacle width/height (cells).
|
|
359
|
+
* @returns {object[]} Array of CyberiaEntity plain objects.
|
|
360
|
+
*/
|
|
361
|
+
function generateObstacles(mapDims, colors, opts = {}) {
|
|
362
|
+
const { minDim = 1, maxDim = 4 } = opts;
|
|
363
|
+
const count = opts.count ?? randInt(OBSTACLE_RANGE[0], OBSTACLE_RANGE[1]);
|
|
364
|
+
const { gridX, gridY } = mapDims;
|
|
365
|
+
|
|
366
|
+
const obstacleColor = findColor(colors, 'OBSTACLE');
|
|
367
|
+
const rgba = obstacleColor ? colorToRgba(obstacleColor) : 'rgba(80, 80, 80, 1)';
|
|
368
|
+
|
|
369
|
+
const entities = [];
|
|
370
|
+
for (let i = 0; i < count; i++) {
|
|
371
|
+
const dimX = randInt(minDim, maxDim);
|
|
372
|
+
const dimY = randInt(minDim, maxDim);
|
|
373
|
+
const maxX = Math.max(0, gridX - dimX);
|
|
374
|
+
const maxY = Math.max(0, gridY - dimY);
|
|
375
|
+
entities.push({
|
|
376
|
+
entityType: 'obstacle',
|
|
377
|
+
initCellX: randInt(0, maxX),
|
|
378
|
+
initCellY: randInt(0, maxY),
|
|
379
|
+
dimX,
|
|
380
|
+
dimY,
|
|
381
|
+
color: rgba,
|
|
382
|
+
objectLayerItemIds: [],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return entities;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Generate procedural foreground entities for a map.
|
|
390
|
+
*
|
|
391
|
+
* Foregrounds use empty `objectLayerItemIds` and a semi-transparent colour
|
|
392
|
+
* from the FOREGROUND palette entry. Count, dimensions, and positions are
|
|
393
|
+
* all fully random within the declared ranges.
|
|
394
|
+
*
|
|
395
|
+
* @param {{ gridX: number, gridY: number }} mapDims Map grid dimensions.
|
|
396
|
+
* @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors Palette.
|
|
397
|
+
* @param {object} [opts]
|
|
398
|
+
* @param {number} [opts.count] Override count (ignores range).
|
|
399
|
+
* @param {number} [opts.minDim=2] Minimum foreground width/height (cells).
|
|
400
|
+
* @param {number} [opts.maxDim=6] Maximum foreground width/height (cells).
|
|
401
|
+
* @returns {object[]} Array of CyberiaEntity plain objects.
|
|
402
|
+
*/
|
|
403
|
+
function generateForeground(mapDims, colors, opts = {}) {
|
|
404
|
+
const { minDim = 2, maxDim = 6 } = opts;
|
|
405
|
+
const count = opts.count ?? randInt(FOREGROUND_RANGE[0], FOREGROUND_RANGE[1]);
|
|
406
|
+
const { gridX, gridY } = mapDims;
|
|
407
|
+
|
|
408
|
+
const fgColor = findColor(colors, 'FOREGROUND');
|
|
409
|
+
const rgba = fgColor ? colorToRgba(fgColor) : 'rgba(200, 200, 200, 0.31)';
|
|
410
|
+
|
|
411
|
+
const entities = [];
|
|
412
|
+
for (let i = 0; i < count; i++) {
|
|
413
|
+
const dimX = randInt(minDim, maxDim);
|
|
414
|
+
const dimY = randInt(minDim, maxDim);
|
|
415
|
+
const maxX = Math.max(0, gridX - dimX);
|
|
416
|
+
const maxY = Math.max(0, gridY - dimY);
|
|
417
|
+
entities.push({
|
|
418
|
+
entityType: 'foreground',
|
|
419
|
+
initCellX: randInt(0, maxX),
|
|
420
|
+
initCellY: randInt(0, maxY),
|
|
421
|
+
dimX,
|
|
422
|
+
dimY,
|
|
423
|
+
color: rgba,
|
|
424
|
+
objectLayerItemIds: [],
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return entities;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generate all procedural fallback entities (obstacles + foreground) for a map.
|
|
432
|
+
*
|
|
433
|
+
* @param {{ gridX: number, gridY: number }} mapDims
|
|
434
|
+
* @param {Array<{ key: string, r: number, g: number, b: number, a: number }>} colors
|
|
435
|
+
* @param {object} [opts]
|
|
436
|
+
* @param {number} [opts.obstacleCount]
|
|
437
|
+
* @param {number} [opts.foregroundCount]
|
|
438
|
+
* @returns {{ obstacles: object[], foreground: object[] }}
|
|
439
|
+
*/
|
|
440
|
+
function generateProceduralEntities(mapDims, colors, opts = {}) {
|
|
441
|
+
return {
|
|
442
|
+
obstacles: generateObstacles(mapDims, colors, { count: opts.obstacleCount }),
|
|
443
|
+
foreground: generateForeground(mapDims, colors, { count: opts.foregroundCount }),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Entity count ranges ──────────────────────────────────────────────────────
|
|
448
|
+
// [min, max] — actual count is random within range on each generation call.
|
|
449
|
+
|
|
450
|
+
const OBSTACLE_RANGE = [20, 35];
|
|
451
|
+
const FOREGROUND_RANGE = [10, 20];
|
|
452
|
+
const BOT_RANGE = [8, 16];
|
|
453
|
+
const BOT_WEAPON_CHANCE = 0.6;
|
|
454
|
+
const PORTAL_DIM_RANGE = [2, 3];
|
|
455
|
+
const PORTAL_COUNT_RANGE = [2, 4];
|
|
456
|
+
|
|
457
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
export {
|
|
460
|
+
// Portal topology
|
|
461
|
+
connectPortals,
|
|
462
|
+
buildTopologyFromSubtypes,
|
|
463
|
+
indexPortalEntities,
|
|
464
|
+
// Portal modes
|
|
465
|
+
PORTAL_MODES,
|
|
466
|
+
PORTAL_MODE_LIST,
|
|
467
|
+
PORTAL_MODE_COLOR_KEY,
|
|
468
|
+
EXTRA_PORTAL_MODES,
|
|
469
|
+
// Procedural entities
|
|
470
|
+
generateObstacles,
|
|
471
|
+
generateForeground,
|
|
472
|
+
generateProceduralEntities,
|
|
473
|
+
// Placement
|
|
474
|
+
OccupancyGrid,
|
|
475
|
+
// Helpers
|
|
476
|
+
colorToRgba,
|
|
477
|
+
findColor,
|
|
478
|
+
randInt,
|
|
479
|
+
// Ranges
|
|
480
|
+
OBSTACLE_RANGE,
|
|
481
|
+
FOREGROUND_RANGE,
|
|
482
|
+
BOT_RANGE,
|
|
483
|
+
BOT_WEAPON_CHANCE,
|
|
484
|
+
PORTAL_DIM_RANGE,
|
|
485
|
+
PORTAL_COUNT_RANGE,
|
|
486
|
+
};
|