cyberia 3.2.12 → 3.2.70

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.
Files changed (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -19,8 +19,8 @@
19
19
  * the entire simulation defaults into the public JS payload.
20
20
  *
21
21
  * Shared content **vocabulary** (item/entity type enums, the
22
- * `DefaultCyberiaItems` registry, `ENTITY_TYPE_TO_ITEM_TYPES`, quest /
23
- * action enums) lives in `SharedDefaultsCyberia.js`. This file
22
+ * `DefaultCyberiaItems` registry, `ENTITY_TYPE_TO_ITEM_TYPES`, the quest
23
+ * step objective enum) lives in `SharedDefaultsCyberia.js`. This file
24
24
  * re-imports those so the browser editor never needs to reach into
25
25
  * server-defaults to learn the schema.
26
26
  *
@@ -36,20 +36,11 @@
36
36
  // The canonical client-defaults module lives under src/client/ so the
37
37
  // browser bundler can resolve the URL inside the client tree. Engine-side
38
38
  // Node imports work from any path, so we reach into it from here.
39
- import { ITEM_TYPES, ENTITY_TYPES } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
40
-
41
- // Re-export the shared vocabulary so existing server-side imports keep
42
- // working without each consumer having to know which module owns which.
43
- export {
39
+ import {
44
40
  ITEM_TYPES,
45
41
  ENTITY_TYPES,
46
- ENTITY_TYPE_TO_ITEM_TYPES,
47
- QUEST_STEPS_TYPES,
48
- CYBERIA_ACTION_TYPES,
49
- DefaultCyberiaItems,
50
- getDefaultCyberiaItemById,
51
- getDefaultCyberiaItemsByItemType,
52
- getDefaultCyberiaItemsByEntityType,
42
+ SKILL_LOGIC_ID_VALUES,
43
+ isCanonicalSkillLogicId,
53
44
  } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
54
45
 
55
46
  /**
@@ -70,14 +61,87 @@ export class CyberiaDependencies {
70
61
  // ─────────────────────────────────────────────────────────────────────────────
71
62
 
72
63
  /**
73
- * Compact `triggerItemId logicEventIds[]` table. The engine expands each
74
- * entry into a richer skill record in CYBERIA_INSTANCE_CONF_DEFAULTS below.
64
+ * Canonical skill configuration single source of truth for every trigger
65
+ * item skill mapping consumed by the runtime simulation.
66
+ *
67
+ * Each entry carries both:
68
+ * - `logicEventIds` (compact array of handler keys, used by the Mongo schema)
69
+ * - `skills` (expanded metadata with name, description, summoned entity)
70
+ *
71
+ * Consumers:
72
+ * - `CYBERIA_INSTANCE_CONF_DEFAULTS.skillConfig` (gRPC fallback defaults)
73
+ * - `bin/cyberia.js seed-skills` (upsert into the cyberia-skill collection —
74
+ * the authoritative CyberiaSkill model keeps the full record, including the
75
+ * `skills` metadata the instance-conf skillConfig schema drops)
75
76
  */
76
77
  export const DefaultSkillConfig = [
77
- { triggerItemId: 'atlas_pistol_mk2', logicEventIds: ['projectile'] },
78
- { triggerItemId: 'coin', logicEventIds: ['coin_drop_or_transaction'] },
78
+ {
79
+ triggerItemId: 'atlas_pistol_mk2',
80
+ logicEventIds: ['projectile'],
81
+ skills: [
82
+ {
83
+ logicEventId: 'projectile',
84
+ name: 'Projectile',
85
+ description:
86
+ 'Fires a projectile in the direction of the tap. Spawn chance and lifetime scale with Intelligence and Range.',
87
+ summonedEntityItemId: 'atlas_pistol_mk2_bullet',
88
+ },
89
+ ],
90
+ },
91
+ {
92
+ triggerItemId: 'coin',
93
+ logicEventIds: ['coin_drop_or_transaction'],
94
+ skills: [
95
+ {
96
+ logicEventId: 'coin_drop_or_transaction',
97
+ name: 'Coin Drop',
98
+ description:
99
+ 'Coins are dropped automatically when an entity is killed. Transfer amount scales with kill percent rules.',
100
+ summonedEntityItemId: 'coin',
101
+ },
102
+ ],
103
+ },
104
+ // {
105
+ // triggerItemId: 'anon',
106
+ // logicEventIds: ['doppelganger'],
107
+ // skills: [
108
+ // {
109
+ // logicEventId: 'doppelganger',
110
+ // name: 'Doppelganger',
111
+ // description: 'Summons a passive clone of yourself that wanders nearby. Spawn chance scales with Intelligence.',
112
+ // summonedEntityItemId: '$active_skin',
113
+ // },
114
+ // ],
115
+ // },
116
+ {
117
+ triggerItemId: 'hatchet',
118
+ logicEventIds: ['projectile'],
119
+ skills: [
120
+ {
121
+ logicEventId: 'projectile',
122
+ name: 'Projectile',
123
+ description:
124
+ 'Fires a projectile in the direction of the tap. Spawn chance and lifetime scale with Intelligence and Range.',
125
+ summonedEntityItemId: 'hatchet-skill',
126
+ },
127
+ ],
128
+ },
79
129
  ];
80
130
 
131
+ // Fail fast on a non-canonical logicEventId: the canonical LogicId registry in
132
+ // SharedDefaultsCyberia.js is the single source of truth, so a typo or a handler
133
+ // the dispatcher does not know must surface at boot, not as a silent no-op skill.
134
+ for (const cfg of DefaultSkillConfig) {
135
+ for (const logicEventId of [...(cfg.logicEventIds || []), ...(cfg.skills || []).map((sk) => sk.logicEventId)]) {
136
+ if (!isCanonicalSkillLogicId(logicEventId)) {
137
+ throw new Error(
138
+ `DefaultSkillConfig: unknown skill logicEventId "${logicEventId}" for trigger "${cfg.triggerItemId}". ` +
139
+ `Allowed (SharedDefaultsCyberia.SKILL_LOGIC_IDS): ${SKILL_LOGIC_ID_VALUES.join(', ')}`,
140
+ );
141
+ }
142
+ }
143
+ }
144
+
81
145
  /**
82
146
  * Default dialogue seeds. Mirrors `CyberiaDialogue` model schema:
83
147
  * { code, order, speaker, text, mood }
@@ -85,38 +149,247 @@ export const DefaultSkillConfig = [
85
149
  * Used by the seed/migration script to populate Mongo on first boot.
86
150
  */
87
151
  export const DefaultCyberiaDialogues = [
88
- { code: 'default-coin', order: 0, speaker: 'Coin', text: 'A standard unit of exchange in the cyberia network.', mood: 'neutral' },
89
- { code: 'default-atlas_pistol_mk2', order: 0, speaker: 'Atlas Pistol MK2', text: 'Military-grade sidearm. Fires energy projectiles.', mood: 'neutral' },
90
- { code: 'default-atlas_pistol_mk2_bullet', order: 0, speaker: 'MK2 Bullet', text: 'High-velocity energy round. Dissipates on impact.', mood: 'neutral' },
91
- { code: 'default-hatchet', order: 0, speaker: 'Hatchet', text: 'A crude but reliable melee tool. Good for close quarters.', mood: 'neutral' },
92
- { code: 'default-wason', order: 0, speaker: 'Wason', text: 'They say I am just a wandering merchant... but I have seen things.', mood: 'neutral' },
93
- { code: 'default-wason', order: 1, speaker: 'Wason', text: 'The network was not always like this. There was a time before the portals.', mood: 'sad' },
94
- { code: 'default-scp-2040', order: 0, speaker: 'SCP-2040', text: 'CONTAINMENT PROTOCOL ACTIVE. Do not make direct eye contact.', mood: 'angry' },
95
- { code: 'default-scp-2040', order: 1, speaker: 'SCP-2040', text: 'I remember everything. Every iteration. Every reset.', mood: 'sad' },
96
- { code: 'default-purple', order: 0, speaker: 'Purple', text: 'The void between nodes is not empty — it is alive.', mood: 'neutral' },
97
- { code: 'default-punk', order: 0, speaker: 'Punk', text: 'Rules are just code someone else wrote. I write my own.', mood: 'happy' },
98
- { code: 'default-lain', order: 0, speaker: 'Lain', text: 'No matter where you go, everyone is connected.', mood: 'neutral' },
99
- { code: 'default-lain', order: 1, speaker: 'Lain', text: 'If you are not remembered, then you never existed.', mood: 'sad' },
100
- { code: 'default-lain', order: 2, speaker: 'Lain', text: 'The wired is not a separate world. It is layered over this one.', mood: 'neutral' },
101
- { code: 'default-kaneki', order: 0, speaker: 'Kaneki', text: 'I am not the protagonist of a novel. I am just... me.', mood: 'sad' },
152
+ {
153
+ code: 'default-coin',
154
+ order: 0,
155
+ speaker: 'Coin',
156
+ text: 'A standard unit of exchange in the cyberia network.',
157
+ mood: 'neutral',
158
+ },
159
+ {
160
+ code: 'default-atlas_pistol_mk2',
161
+ order: 0,
162
+ speaker: 'Atlas Pistol MK2',
163
+ text: 'Military-grade sidearm. Fires energy projectiles.',
164
+ mood: 'neutral',
165
+ },
166
+ {
167
+ code: 'default-atlas_pistol_mk2_bullet',
168
+ order: 0,
169
+ speaker: 'MK2 Bullet',
170
+ text: 'High-velocity energy round. Dissipates on impact.',
171
+ mood: 'neutral',
172
+ },
173
+ {
174
+ code: 'default-hatchet',
175
+ order: 0,
176
+ speaker: 'Hatchet',
177
+ text: 'A crude but reliable melee tool. Good for close quarters.',
178
+ mood: 'neutral',
179
+ },
180
+ {
181
+ code: 'default-wason',
182
+ order: 0,
183
+ speaker: 'Wason',
184
+ text: 'They say I am just a wandering merchant... but I have seen things.',
185
+ mood: 'neutral',
186
+ },
187
+ {
188
+ code: 'default-wason',
189
+ order: 1,
190
+ speaker: 'Wason',
191
+ text: 'The network was not always like this. There was a time before the portals.',
192
+ mood: 'sad',
193
+ },
194
+ {
195
+ code: 'default-scp-2040',
196
+ order: 0,
197
+ speaker: 'SCP-2040',
198
+ text: 'CONTAINMENT PROTOCOL ACTIVE. Do not make direct eye contact.',
199
+ mood: 'angry',
200
+ },
201
+ {
202
+ code: 'default-scp-2040',
203
+ order: 1,
204
+ speaker: 'SCP-2040',
205
+ text: 'I remember everything. Every iteration. Every reset.',
206
+ mood: 'sad',
207
+ },
208
+ {
209
+ code: 'default-purple',
210
+ order: 0,
211
+ speaker: 'Purple',
212
+ text: 'The void between nodes is not empty — it is alive.',
213
+ mood: 'neutral',
214
+ },
215
+ {
216
+ code: 'default-punk',
217
+ order: 0,
218
+ speaker: 'Punk',
219
+ text: 'Rules are just code someone else wrote. I write my own.',
220
+ mood: 'happy',
221
+ },
222
+ {
223
+ code: 'default-lain',
224
+ order: 0,
225
+ speaker: 'Lain',
226
+ text: 'No matter where you go, everyone is connected.',
227
+ mood: 'neutral',
228
+ },
229
+ {
230
+ code: 'default-lain',
231
+ order: 1,
232
+ speaker: 'Lain',
233
+ text: 'If you are not remembered, then you never existed.',
234
+ mood: 'sad',
235
+ },
236
+ {
237
+ code: 'default-lain',
238
+ order: 2,
239
+ speaker: 'Lain',
240
+ text: 'The wired is not a separate world. It is layered over this one.',
241
+ mood: 'neutral',
242
+ },
243
+ {
244
+ code: 'default-kaneki',
245
+ order: 0,
246
+ speaker: 'Kaneki',
247
+ text: 'I am not the protagonist of a novel. I am just... me.',
248
+ mood: 'sad',
249
+ },
102
250
  { code: 'default-kaneki', order: 1, speaker: 'Kaneki', text: 'What is 1000 minus 7?', mood: 'angry' },
103
- { code: 'default-junko', order: 0, speaker: 'Junko', text: 'Despair is the seed from which hope blooms!', mood: 'happy' },
104
- { code: 'default-junko', order: 1, speaker: 'Junko', text: 'How boring... nothing ever surprises me anymore.', mood: 'sad' },
251
+ {
252
+ code: 'default-junko',
253
+ order: 0,
254
+ speaker: 'Junko',
255
+ text: 'Despair is the seed from which hope blooms!',
256
+ mood: 'happy',
257
+ },
258
+ {
259
+ code: 'default-junko',
260
+ order: 1,
261
+ speaker: 'Junko',
262
+ text: 'How boring... nothing ever surprises me anymore.',
263
+ mood: 'sad',
264
+ },
105
265
  { code: 'default-ghost', order: 0, speaker: 'Ghost', text: '...', mood: 'neutral' },
106
- { code: 'default-eiri', order: 0, speaker: 'Eiri', text: 'I am the god of the wired. I designed the protocol.', mood: 'neutral' },
107
- { code: 'default-eiri', order: 1, speaker: 'Eiri', text: 'Flesh is just hardware. Consciousness is the only software that matters.', mood: 'neutral' },
266
+ {
267
+ code: 'default-eiri',
268
+ order: 0,
269
+ speaker: 'Eiri',
270
+ text: 'I am the god of the wired. I designed the protocol.',
271
+ mood: 'neutral',
272
+ },
273
+ {
274
+ code: 'default-eiri',
275
+ order: 1,
276
+ speaker: 'Eiri',
277
+ text: 'Flesh is just hardware. Consciousness is the only software that matters.',
278
+ mood: 'neutral',
279
+ },
108
280
  { code: 'default-anon', order: 0, speaker: '???', text: 'You should not be here. Turn back.', mood: 'angry' },
109
- { code: 'default-anon', order: 1, speaker: '???', text: 'Or stay. It does not matter. Nothing leaves this place.', mood: 'neutral' },
110
- { code: 'default-alex', order: 0, speaker: 'Alex', text: 'I have been mapping the portal network. Something does not add up.', mood: 'neutral' },
111
- { code: 'default-alex', order: 1, speaker: 'Alex', text: 'There are nodes that exist in the registry but have no physical anchor.', mood: 'neutral' },
112
- { code: 'default-agent', order: 0, speaker: 'Agent', text: 'Civilian, this area is restricted. State your business.', mood: 'neutral' },
113
- { code: 'default-agent', order: 1, speaker: 'Agent', text: 'Hmm. Proceed, but know that you are being watched.', mood: 'neutral' },
114
- { code: 'default-grass', order: 0, speaker: 'Grass', text: 'A patch of synthetic grass. It sways gently despite no wind.', mood: 'neutral' },
115
- { code: 'quest-talk-wason', order: 0, speaker: 'Wason', text: 'Wanderer! Glad you stopped by. I need a favor — nothing dangerous... mostly.', mood: 'happy' },
116
- { code: 'quest-talk-wason', order: 1, speaker: 'Wason', text: "First, find Alex — she's been surveying the nodes east of here. Then gather a hatchet for me.", mood: 'neutral' },
117
- { code: 'quest-talk-wason', order: 2, speaker: 'Wason', text: 'And one more thing: the SCP-2040 anomalies are overrunning my trade routes. Deal with two of them.', mood: 'sad' },
118
- { code: 'quest-talk-alex', order: 0, speaker: 'Alex', text: "Wason sent you? Good. The portal anomalies are getting worse. I've logged what I can.", mood: 'neutral' },
119
- { code: 'quest-talk-alex', order: 1, speaker: 'Alex', text: 'Tell Wason: the source is somewhere in the deeper nodes. The registry does not lie.', mood: 'neutral' },
281
+ {
282
+ code: 'default-anon',
283
+ order: 1,
284
+ speaker: '???',
285
+ text: 'Or stay. It does not matter. Nothing leaves this place.',
286
+ mood: 'neutral',
287
+ },
288
+ {
289
+ code: 'default-alex',
290
+ order: 0,
291
+ speaker: 'Alex',
292
+ text: 'I have been mapping the portal network. Something does not add up.',
293
+ mood: 'neutral',
294
+ },
295
+ {
296
+ code: 'default-alex',
297
+ order: 1,
298
+ speaker: 'Alex',
299
+ text: 'There are nodes that exist in the registry but have no physical anchor.',
300
+ mood: 'neutral',
301
+ },
302
+ {
303
+ code: 'default-agent',
304
+ order: 0,
305
+ speaker: 'Agent',
306
+ text: 'Civilian, this area is restricted. State your business.',
307
+ mood: 'neutral',
308
+ },
309
+ {
310
+ code: 'default-agent',
311
+ order: 1,
312
+ speaker: 'Agent',
313
+ text: 'Hmm. Proceed, but know that you are being watched.',
314
+ mood: 'neutral',
315
+ },
316
+ {
317
+ code: 'default-grass',
318
+ order: 0,
319
+ speaker: 'Grass',
320
+ text: 'A patch of synthetic grass. It sways gently despite no wind.',
321
+ mood: 'neutral',
322
+ },
323
+ {
324
+ code: 'quest-talk-wason',
325
+ order: 0,
326
+ speaker: 'Wason',
327
+ text: 'Wanderer! Glad you stopped by. I need a favor — nothing dangerous... mostly.',
328
+ mood: 'happy',
329
+ },
330
+ {
331
+ code: 'quest-talk-wason',
332
+ order: 1,
333
+ speaker: 'Wason',
334
+ text: "First, find Alex — she's been surveying the nodes east of here. Then gather a hatchet for me.",
335
+ mood: 'neutral',
336
+ },
337
+ {
338
+ code: 'quest-talk-wason',
339
+ order: 2,
340
+ speaker: 'Wason',
341
+ text: 'And one more thing: the SCP-2040 anomalies are overrunning my trade routes. Deal with two of them.',
342
+ mood: 'sad',
343
+ },
344
+ {
345
+ code: 'quest-talk-alex',
346
+ order: 0,
347
+ speaker: 'Alex',
348
+ text: "Wason sent you? Good. The portal anomalies are getting worse. I've logged what I can.",
349
+ mood: 'neutral',
350
+ },
351
+ {
352
+ code: 'quest-talk-alex',
353
+ order: 1,
354
+ speaker: 'Alex',
355
+ text: 'Tell Wason: the source is somewhere in the deeper nodes. The registry does not lie.',
356
+ mood: 'neutral',
357
+ },
358
+ {
359
+ code: 'talk-lain',
360
+ order: 0,
361
+ speaker: 'Lain',
362
+ text: 'Present day... present time. You are here, so you are real.',
363
+ mood: 'neutral',
364
+ },
365
+ {
366
+ code: 'talk-lain',
367
+ order: 1,
368
+ speaker: 'Lain',
369
+ text: 'There is nothing I can grant you. Only the wired remembers.',
370
+ mood: 'neutral',
371
+ },
372
+ {
373
+ code: 'quest-talk-wason-errand',
374
+ order: 0,
375
+ speaker: 'Wason',
376
+ text: 'A small thing, friend — the field is littered with loose coin. Gather a few and bring them here.',
377
+ mood: 'happy',
378
+ },
379
+ {
380
+ code: 'quest-talk-wason-errand',
381
+ order: 1,
382
+ speaker: 'Wason',
383
+ text: 'You have my thanks. A good hatchet for an honest errand.',
384
+ mood: 'neutral',
385
+ },
386
+ {
387
+ code: 'quest-talk-agent',
388
+ order: 0,
389
+ speaker: 'Agent',
390
+ text: 'Civilian. There is a bounty if you have the stomach for it. Eliminate the threat, collect the drop, report back.',
391
+ mood: 'neutral',
392
+ },
120
393
  ];
121
394
 
122
395
  /**
@@ -124,25 +397,52 @@ export const DefaultCyberiaDialogues = [
124
397
  * Each entry follows the `CyberiaAction` model schema.
125
398
  */
126
399
  export const DefaultCyberiaActions = [
400
+ // An action has no `type`: it declares the capabilities available at a cell.
401
+ // `code` is a generic location slug; `label` is the bot's overhead name (the
402
+ // client fetches it by code via REST). The NPC skin is derived from
403
+ // `dialogCode` (default-<skin>). `questDialogueCodes` maps each quest the NPC
404
+ // handles to the dialogue shown for it (offer + talk-objective validation).
405
+ // The quests an NPC OFFERS are those whose source cell matches the action's.
127
406
  {
128
- code: 'wason-quest-intro', type: 'quest-talk', label: 'Quest',
129
- provideItemId: 'wason', grantQuestCode: 'fallback-intro-quest',
130
- dialogCode: 'quest-talk-wason', questDialogueCodes: ['quest-talk-wason'],
407
+ code: 'loc-fallback-map-0-12-10',
408
+ label: 'Wason',
409
+ sourceMapCode: 'fallback-map-0',
410
+ sourceCellX: 12,
411
+ sourceCellY: 10,
412
+ dialogCode: 'default-wason',
413
+ questDialogueCodes: [
414
+ { questCode: 'fallback-intro-quest', dialogCode: 'quest-talk-wason' },
415
+ { questCode: 'wason-errand', dialogCode: 'quest-talk-wason-errand' },
416
+ // bounty's report-back talk happens here too (bounty is offered at the agent cell).
417
+ { questCode: 'bounty-quest-alpha', dialogCode: 'quest-talk-wason' },
418
+ ],
131
419
  },
132
420
  {
133
- code: 'alex-quest-talk', type: 'quest-talk', label: 'Quest Talk',
134
- provideItemId: 'alex', grantQuestCode: '',
135
- dialogCode: 'quest-talk-alex', questDialogueCodes: ['quest-talk-alex'],
421
+ code: 'loc-fallback-map-0-18-10',
422
+ label: 'Alex',
423
+ sourceMapCode: 'fallback-map-0',
424
+ sourceCellX: 18,
425
+ sourceCellY: 10,
426
+ dialogCode: 'default-alex',
427
+ questDialogueCodes: [{ questCode: 'fallback-intro-quest', dialogCode: 'quest-talk-alex' }],
136
428
  },
137
429
  {
138
- code: 'agent-mission-brief', type: 'quest-talk', label: 'Mission Brief',
139
- provideItemId: 'agent', grantQuestCode: 'bounty-quest-alpha',
140
- dialogCode: 'default-agent', questDialogueCodes: ['default-agent'],
430
+ code: 'loc-fallback-map-0-12-16',
431
+ label: 'Agent',
432
+ sourceMapCode: 'fallback-map-0',
433
+ sourceCellX: 12,
434
+ sourceCellY: 16,
435
+ dialogCode: 'default-agent',
436
+ questDialogueCodes: [{ questCode: 'bounty-quest-alpha', dialogCode: 'quest-talk-agent' }],
141
437
  },
142
438
  {
143
- code: 'wason-bounty-brief', type: 'quest-talk', label: 'Bounty Brief',
144
- provideItemId: 'wason', grantQuestCode: '',
145
- dialogCode: 'quest-talk-wason', questDialogueCodes: ['quest-talk-wason'],
439
+ code: 'loc-fallback-map-0-15-22',
440
+ label: 'Lain',
441
+ sourceMapCode: 'fallback-map-0',
442
+ sourceCellX: 15,
443
+ sourceCellY: 22,
444
+ dialogCode: 'default-lain',
445
+ questDialogueCodes: [],
146
446
  },
147
447
  ];
148
448
 
@@ -155,31 +455,82 @@ export const DefaultCyberiaQuests = [
155
455
  code: 'fallback-intro-quest',
156
456
  title: "The Wanderer's Task",
157
457
  description: 'Help Wason restore order to the fractured nodes.',
458
+ // Spatial binding — must match the granting action `wason-quest-intro`.
459
+ sourceMapCode: 'fallback-map-0',
460
+ sourceCellX: 12,
461
+ sourceCellY: 10,
158
462
  prerequisiteCodes: [],
159
463
  unlocksQuestCodes: ['bounty-quest-alpha'],
160
464
  steps: [
161
- { id: 'step-talk-alex', description: 'Find Alex and hear her report on the portal anomalies.',
162
- objectives: [{ type: 'talk', itemId: 'alex', quantity: 1 }] },
163
- { id: 'step-collect-hatchet', description: 'Obtain a hatchet for Wason.',
164
- objectives: [{ type: 'collect', itemId: 'hatchet', quantity: 1 }] },
165
- { id: 'step-kill-scp', description: 'Eliminate SCP-2040 anomalies threatening the trade routes.',
166
- objectives: [{ type: 'kill', itemId: 'scp-2040', quantity: 2 }] },
465
+ {
466
+ id: 'step-talk-alex',
467
+ description: 'Find Alex and hear her report on the portal anomalies.',
468
+ objectives: [{ type: 'talk', itemId: 'alex', quantity: 1 }],
469
+ },
470
+ {
471
+ id: 'step-collect-wood',
472
+ description: 'Obtain a wood for Wason.',
473
+ objectives: [{ type: 'collect', itemId: 'wood-drop-1', quantity: 1 }],
474
+ },
475
+ {
476
+ id: 'step-kill-kishins',
477
+ description: 'Eliminate Kishins anomalies threatening the trade routes.',
478
+ objectives: [{ type: 'kill', itemId: 'kishins', quantity: 2 }],
479
+ },
167
480
  ],
168
481
  rewards: [{ itemId: 'coin', quantity: 50 }],
169
482
  },
483
+ {
484
+ // Parallel initial mission — same source cell as Wason (12,10), no
485
+ // prerequisites, so the player can accept it alongside the intro quest.
486
+ code: 'wason-errand',
487
+ title: "Wason's Errand",
488
+ description: 'Gather coins from the field and bring them back to Wason.',
489
+ sourceMapCode: 'fallback-map-0',
490
+ sourceCellX: 12,
491
+ sourceCellY: 10,
492
+ prerequisiteCodes: [],
493
+ unlocksQuestCodes: [],
494
+ steps: [
495
+ {
496
+ id: 'step-collect-coins',
497
+ description: 'Collect 5 coins.',
498
+ objectives: [{ type: 'collect', itemId: 'coin', quantity: 5 }],
499
+ },
500
+ {
501
+ id: 'step-return-wason',
502
+ description: 'Return to Wason.',
503
+ objectives: [{ type: 'talk', itemId: 'wason', quantity: 1 }],
504
+ },
505
+ ],
506
+ rewards: [{ itemId: 'hatchet', quantity: 1 }],
507
+ },
170
508
  {
171
509
  code: 'bounty-quest-alpha',
172
510
  title: 'Alpha Bounty',
173
511
  description: 'A field test: eliminate a threat, claim your reward, then report back.',
512
+ // Spatial binding — must match the granting action `agent-mission-brief`.
513
+ sourceMapCode: 'fallback-map-0',
514
+ sourceCellX: 12,
515
+ sourceCellY: 16,
174
516
  prerequisiteCodes: ['fallback-intro-quest'],
175
517
  unlocksQuestCodes: [],
176
518
  steps: [
177
- { id: 'step-kill-first', description: 'Eliminate the SCP-2040 threat.',
178
- objectives: [{ type: 'kill', itemId: 'scp-2040', quantity: 1 }] },
179
- { id: 'step-collect-reward', description: 'Collect the bounty coin drop.',
180
- objectives: [{ type: 'collect', itemId: 'coin', quantity: 10 }] },
181
- { id: 'step-report-wason', description: 'Report back to Wason.',
182
- objectives: [{ type: 'talk', itemId: 'wason', quantity: 1 }] },
519
+ {
520
+ id: 'step-kill-first',
521
+ description: 'Eliminate the Kishins threat.',
522
+ objectives: [{ type: 'kill', itemId: 'kishins', quantity: 1 }],
523
+ },
524
+ {
525
+ id: 'step-collect-reward',
526
+ description: 'Collect the bounty coin drop.',
527
+ objectives: [{ type: 'collect', itemId: 'coin', quantity: 10 }],
528
+ },
529
+ {
530
+ id: 'step-report-wason',
531
+ description: 'Report back to Wason.',
532
+ objectives: [{ type: 'talk', itemId: 'wason', quantity: 1 }],
533
+ },
183
534
  ],
184
535
  rewards: [{ itemId: 'hatchet', quantity: 1 }],
185
536
  },
@@ -222,7 +573,30 @@ export const STATUS_ICONS = Object.freeze([
222
573
  { id: 5, name: 'dead', description: 'Entity is dead / respawning' },
223
574
  { id: 6, name: 'resource', description: 'Resource entity — static, exploitable (wood, minerals, etc.)' },
224
575
  { id: 7, name: 'resource-extracted', description: 'Resource entity extracted/depleted (dead state)' },
225
- { id: 8, name: 'action-provider', description: 'Bot with available quest-talk/shop/storage/craft actions' },
576
+ {
577
+ id: 8,
578
+ name: 'action-provider',
579
+ description:
580
+ 'Capability: bot has a usable cyberia-action (shop/storage/craft/talk) for players who can interact with it. Sent as a capability bit, not a presence state.',
581
+ },
582
+ {
583
+ id: 9,
584
+ name: 'quest-provider',
585
+ description:
586
+ 'Capability: bot offers or advances a cyberia-quest for the viewing player (acceptable offer or active talk-target). Sent as a capability bit, not a presence state.',
587
+ },
588
+ {
589
+ id: 10,
590
+ name: 'portal',
591
+ description:
592
+ 'Presence: fixed-target portal / transport entity. The client renders the transport icon plus a "<targetMapCode> <x>,<y>" nameplate.',
593
+ },
594
+ {
595
+ id: 11,
596
+ name: 'portal-random',
597
+ description:
598
+ 'Presence: random-target portal (inter-random / intra-random, targetCell -1,-1). The client renders the transport-random icon plus a "<targetMapCode>" nameplate (no cell, the destination is random).',
599
+ },
226
600
  ]);
227
601
 
228
602
  // ─────────────────────────────────────────────────────────────────────────────
@@ -296,6 +670,14 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
296
670
  { itemId: 'coin', active: false, quantity: 0 },
297
671
  ],
298
672
  },
673
+ // Fallback-world mission/action givers. Resolved by their active skin (the
674
+ // liveItemIds key), these bots take the canonical `provider` behavior: they
675
+ // barely move from their spawn and are immortal. Lain only talks in place, so
676
+ // she is fully static. Authors can retarget any of these via EntityEngineCyberia.
677
+ { entityType: ENTITY_TYPES.bot, liveItemIds: ['wason'], deadItemIds: ['ghost'], behavior: 'provider' },
678
+ { entityType: ENTITY_TYPES.bot, liveItemIds: ['alex'], deadItemIds: ['ghost'], behavior: 'provider' },
679
+ { entityType: ENTITY_TYPES.bot, liveItemIds: ['agent'], deadItemIds: ['ghost'], behavior: 'provider' },
680
+ { entityType: ENTITY_TYPES.bot, liveItemIds: ['lain'], deadItemIds: ['ghost'], behavior: 'provider-static' },
299
681
  {
300
682
  entityType: ENTITY_TYPES.skill,
301
683
  liveItemIds: ['atlas_pistol_mk2_bullet'],
@@ -312,9 +694,33 @@ export const ENTITY_TYPE_DEFAULTS = Object.freeze([
312
694
  { entityType: ENTITY_TYPES.obstacle, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
313
695
  { entityType: ENTITY_TYPES.portal, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
314
696
  { entityType: ENTITY_TYPES.foreground, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
697
+ // Static decorator — non-moving, passable, depth-sorted. Visuals come from the
698
+ // map definition / world generator; no live/dead/drop rotation.
699
+ { entityType: ENTITY_TYPES.static, liveItemIds: [], deadItemIds: [], dropItemIds: [], defaultObjectLayers: [] },
315
700
  ...RESOURCE_ENTITY_TYPE_DEFAULTS,
316
701
  ]);
317
702
 
703
+ // ─────────────────────────────────────────────────────────────────────────────
704
+ // Player spawn
705
+ // ─────────────────────────────────────────────────────────────────────────────
706
+
707
+ /**
708
+ * Canonical default player spawn policy. Mirrors PlayerSpawnSchema in
709
+ * cyberia-instance.model.js and the Go PlayerSpawnConfig: when `random` is false
710
+ * and `sourceMapCode` names a loaded map, new players spawn at
711
+ * (sourceCellX, sourceCellY) on it; otherwise (random, or no/unknown map) they
712
+ * spawn at a random walkable cell on a random map.
713
+ *
714
+ * The canonical default is a random spawn so a fresh or procedural world never
715
+ * piles every new player onto a single cell.
716
+ */
717
+ export const DEFAULT_PLAYER_SPAWN = Object.freeze({
718
+ sourceMapCode: 'fallback-map-0',
719
+ sourceCellX: 3,
720
+ sourceCellY: 3,
721
+ random: false,
722
+ });
723
+
318
724
  // ─────────────────────────────────────────────────────────────────────────────
319
725
  // Instance-level simulation configuration
320
726
  // ─────────────────────────────────────────────────────────────────────────────
@@ -383,56 +789,10 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
383
789
  statusIcons: STATUS_ICONS.map((s) => ({ ...s })),
384
790
 
385
791
  // ── Skill system ───────────────────────────────────────────────────
386
- skillConfig: [
387
- {
388
- triggerItemId: 'atlas_pistol_mk2',
389
- skills: [
390
- {
391
- logicEventId: 'projectile',
392
- name: 'Projectile',
393
- description:
394
- 'Fires a projectile in the direction of the tap. Spawn chance and lifetime scale with Intelligence and Range.',
395
- summonedEntityItemId: 'atlas_pistol_mk2_bullet',
396
- },
397
- ],
398
- },
399
- {
400
- triggerItemId: 'coin',
401
- skills: [
402
- {
403
- logicEventId: 'coin_drop_or_transaction',
404
- name: 'Coin Drop',
405
- description:
406
- 'Coins are dropped automatically when an entity is killed. Transfer amount scales with kill percent rules.',
407
- summonedEntityItemId: 'coin',
408
- },
409
- ],
410
- },
411
- {
412
- triggerItemId: 'anon',
413
- skills: [
414
- {
415
- logicEventId: 'doppelganger',
416
- name: 'Doppelganger',
417
- description:
418
- 'Summons a passive clone of yourself that wanders nearby. Spawn chance scales with Intelligence.',
419
- summonedEntityItemId: '$active_skin',
420
- },
421
- ],
422
- },
423
- {
424
- triggerItemId: 'hatchet',
425
- skills: [
426
- {
427
- logicEventId: 'projectile',
428
- name: 'Projectile',
429
- description:
430
- 'Fires a projectile in the direction of the tap. Spawn chance and lifetime scale with Intelligence and Range.',
431
- summonedEntityItemId: 'hatchet-skill',
432
- },
433
- ],
434
- },
435
- ],
792
+ // Canonical skill definitions live in DefaultSkillConfig above.
793
+ // This reference is the single point of consumption for the gRPC
794
+ // fallback defaults — any change goes through DefaultSkillConfig.
795
+ skillConfig: DefaultSkillConfig,
436
796
 
437
797
  skillRules: {
438
798
  projectileSpawnChance: 0.5,
@@ -449,3 +809,72 @@ export const CYBERIA_INSTANCE_CONF_DEFAULTS = {
449
809
  // ── Equipment Rules ────────────────────────────────────────────────
450
810
  equipmentRules: { ...EQUIPMENT_RULES_DEFAULTS },
451
811
  };
812
+
813
+ // ─────────────────────────────────────────────────────────────────────────────
814
+ // Instance-conf default backfill
815
+ // ─────────────────────────────────────────────────────────────────────────────
816
+
817
+ const _isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
818
+
819
+ const _cloneDefault = (v) => JSON.parse(JSON.stringify(v));
820
+
821
+ /**
822
+ * Recursively backfill one value against its canonical default:
823
+ * - null / undefined → deep clone of the default
824
+ * - empty array → deep clone of the default (config lists must never
825
+ * export empty — e.g. skillConfig, entityDefaults)
826
+ * - non-empty array → kept verbatim (author content)
827
+ * - plain object → keep author-set keys, recurse to fill missing default keys
828
+ * - scalar → kept verbatim when present (0, '', false count as present)
829
+ */
830
+ function _deepFillDefaults(value, defaultValue) {
831
+ if (value === null || value === undefined) return _cloneDefault(defaultValue);
832
+ if (Array.isArray(defaultValue)) {
833
+ return Array.isArray(value) && value.length > 0 ? value : _cloneDefault(defaultValue);
834
+ }
835
+ if (_isPlainObject(defaultValue) && _isPlainObject(value)) {
836
+ const out = { ...value };
837
+ for (const key of Object.keys(defaultValue)) {
838
+ out[key] = _deepFillDefaults(value[key], defaultValue[key]);
839
+ }
840
+ return out;
841
+ }
842
+ return value;
843
+ }
844
+
845
+ /**
846
+ * Normalise a skillConfig array to the shape CyberiaInstanceConfSchema stores
847
+ * (`SkillConfigEntrySchema` = { triggerItemId, logicEventIds }). The canonical
848
+ * DefaultSkillConfig carries extra `skills` metadata the schema drops on write;
849
+ * stripping it here keeps export ⇄ DB round-trips churn-free.
850
+ */
851
+ function _normaliseSkillConfig(skillConfig) {
852
+ if (!Array.isArray(skillConfig)) return [];
853
+ return skillConfig.map((entry) => ({
854
+ triggerItemId: entry.triggerItemId,
855
+ logicEventIds: [...(entry.logicEventIds || [])],
856
+ }));
857
+ }
858
+
859
+ /**
860
+ * Return a copy of a CyberiaInstanceConf document with every field defined by
861
+ * CyberiaInstanceConfSchema present. Missing, null/undefined, or empty-array
862
+ * fields — common when a doc is read with `.lean()` (which skips Mongoose schema
863
+ * defaults), was created before a schema field existed, or was seeded with the
864
+ * schema's empty-array default (e.g. `skillConfig`) — are filled from
865
+ * CYBERIA_INSTANCE_CONF_DEFAULTS, the same canonical values the fallback world
866
+ * uses. Author-set scalars (including 0, '', false), non-empty arrays, and DB
867
+ * metadata (_id, instanceCode, timestamps) are preserved untouched.
868
+ *
869
+ * @param {object} [conf]
870
+ * @returns {object}
871
+ */
872
+ export function fillInstanceConfDefaults(conf = {}) {
873
+ const source = _isPlainObject(conf) ? conf : {};
874
+ const out = { ...source };
875
+ for (const key of Object.keys(CYBERIA_INSTANCE_CONF_DEFAULTS)) {
876
+ out[key] = _deepFillDefaults(source[key], CYBERIA_INSTANCE_CONF_DEFAULTS[key]);
877
+ }
878
+ out.skillConfig = _normaliseSkillConfig(out.skillConfig);
879
+ return out;
880
+ }