@crowdedkingdoms/crowdyjs 8.2.0 → 8.3.0

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 (102) hide show
  1. package/README.md +21 -3
  2. package/dist/crowdy-client.d.ts.map +1 -1
  3. package/dist/crowdy-client.js +5 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/kit/blueprints/combat.d.ts +75 -0
  8. package/dist/kit/blueprints/combat.d.ts.map +1 -0
  9. package/dist/kit/blueprints/combat.js +351 -0
  10. package/dist/kit/blueprints/core.d.ts +222 -0
  11. package/dist/kit/blueprints/core.d.ts.map +1 -0
  12. package/dist/kit/blueprints/core.js +185 -0
  13. package/dist/kit/blueprints/decks.d.ts +50 -0
  14. package/dist/kit/blueprints/decks.d.ts.map +1 -0
  15. package/dist/kit/blueprints/decks.js +163 -0
  16. package/dist/kit/blueprints/economy.d.ts +88 -0
  17. package/dist/kit/blueprints/economy.d.ts.map +1 -0
  18. package/dist/kit/blueprints/economy.js +533 -0
  19. package/dist/kit/blueprints/guild.d.ts +51 -0
  20. package/dist/kit/blueprints/guild.d.ts.map +1 -0
  21. package/dist/kit/blueprints/guild.js +50 -0
  22. package/dist/kit/blueprints/index.d.ts +21 -0
  23. package/dist/kit/blueprints/index.d.ts.map +1 -0
  24. package/dist/kit/blueprints/index.js +20 -0
  25. package/dist/kit/blueprints/inventory.d.ts +38 -0
  26. package/dist/kit/blueprints/inventory.d.ts.map +1 -0
  27. package/dist/kit/blueprints/inventory.js +157 -0
  28. package/dist/kit/blueprints/leaderboards.d.ts +49 -0
  29. package/dist/kit/blueprints/leaderboards.d.ts.map +1 -0
  30. package/dist/kit/blueprints/leaderboards.js +126 -0
  31. package/dist/kit/blueprints/locks.d.ts +82 -0
  32. package/dist/kit/blueprints/locks.d.ts.map +1 -0
  33. package/dist/kit/blueprints/locks.js +155 -0
  34. package/dist/kit/blueprints/loot.d.ts +84 -0
  35. package/dist/kit/blueprints/loot.d.ts.map +1 -0
  36. package/dist/kit/blueprints/loot.js +228 -0
  37. package/dist/kit/blueprints/matches.d.ts +59 -0
  38. package/dist/kit/blueprints/matches.d.ts.map +1 -0
  39. package/dist/kit/blueprints/matches.js +246 -0
  40. package/dist/kit/blueprints/npcs.d.ts +71 -0
  41. package/dist/kit/blueprints/npcs.d.ts.map +1 -0
  42. package/dist/kit/blueprints/npcs.js +122 -0
  43. package/dist/kit/blueprints/plots.d.ts +54 -0
  44. package/dist/kit/blueprints/plots.d.ts.map +1 -0
  45. package/dist/kit/blueprints/plots.js +146 -0
  46. package/dist/kit/blueprints/progression.d.ts +68 -0
  47. package/dist/kit/blueprints/progression.d.ts.map +1 -0
  48. package/dist/kit/blueprints/progression.js +324 -0
  49. package/dist/kit/blueprints/quests.d.ts +65 -0
  50. package/dist/kit/blueprints/quests.d.ts.map +1 -0
  51. package/dist/kit/blueprints/quests.js +284 -0
  52. package/dist/kit/blueprints/worldsim.d.ts +84 -0
  53. package/dist/kit/blueprints/worldsim.d.ts.map +1 -0
  54. package/dist/kit/blueprints/worldsim.js +461 -0
  55. package/dist/kit/combat.d.ts +103 -0
  56. package/dist/kit/combat.d.ts.map +1 -0
  57. package/dist/kit/combat.js +151 -0
  58. package/dist/kit/decks.d.ts +123 -0
  59. package/dist/kit/decks.d.ts.map +1 -0
  60. package/dist/kit/decks.js +138 -0
  61. package/dist/kit/economy.d.ts +244 -0
  62. package/dist/kit/economy.d.ts.map +1 -0
  63. package/dist/kit/economy.js +365 -0
  64. package/dist/kit/features.d.ts +60 -0
  65. package/dist/kit/features.d.ts.map +1 -0
  66. package/dist/kit/features.js +65 -0
  67. package/dist/kit/index.d.ts +13 -2
  68. package/dist/kit/index.d.ts.map +1 -1
  69. package/dist/kit/index.js +12 -1
  70. package/dist/kit/inventory.d.ts +5 -0
  71. package/dist/kit/inventory.d.ts.map +1 -1
  72. package/dist/kit/inventory.js +14 -1
  73. package/dist/kit/kit.d.ts +58 -2
  74. package/dist/kit/kit.d.ts.map +1 -1
  75. package/dist/kit/kit.js +24 -2
  76. package/dist/kit/leaderboards.d.ts +66 -0
  77. package/dist/kit/leaderboards.d.ts.map +1 -0
  78. package/dist/kit/leaderboards.js +94 -0
  79. package/dist/kit/loot.d.ts +106 -0
  80. package/dist/kit/loot.d.ts.map +1 -0
  81. package/dist/kit/loot.js +120 -0
  82. package/dist/kit/matches.d.ts +140 -0
  83. package/dist/kit/matches.d.ts.map +1 -0
  84. package/dist/kit/matches.js +253 -0
  85. package/dist/kit/objects.js +1 -1
  86. package/dist/kit/plots.js +1 -1
  87. package/dist/kit/progression.d.ts +181 -0
  88. package/dist/kit/progression.d.ts.map +1 -0
  89. package/dist/kit/progression.js +265 -0
  90. package/dist/kit/quests.d.ts +110 -0
  91. package/dist/kit/quests.d.ts.map +1 -0
  92. package/dist/kit/quests.js +158 -0
  93. package/dist/kit/social.d.ts +267 -0
  94. package/dist/kit/social.d.ts.map +1 -0
  95. package/dist/kit/social.js +230 -0
  96. package/dist/kit/worldsim.d.ts +225 -0
  97. package/dist/kit/worldsim.d.ts.map +1 -0
  98. package/dist/kit/worldsim.js +231 -0
  99. package/package.json +1 -1
  100. package/dist/kit/blueprints.d.ts +0 -370
  101. package/dist/kit/blueprints.d.ts.map +0 -1
  102. package/dist/kit/blueprints.js +0 -643
@@ -1,643 +0,0 @@
1
- /** Serialize a {@link KitInvokePolicy} tree to the wire `invokePolicyJson`. */
2
- export function kitPolicyJson(policy) {
3
- return JSON.stringify(policy);
4
- }
5
- /** Convert `PascalCase`/`camelCase` to `snake_case` for derived names. */
6
- export function toSnakeCase(name) {
7
- return name
8
- .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
9
- .replace(/[\s-]+/g, '_')
10
- .toLowerCase();
11
- }
12
- /** Compute the type/function names an inventory blueprint (and its runtime helper) uses. */
13
- export function inventoryNames(typePrefix = '') {
14
- const fnPrefix = typePrefix ? `${toSnakeCase(typePrefix)}_` : '';
15
- return {
16
- inventoryType: `${typePrefix}Inventory`,
17
- stackType: `${typePrefix}ItemStack`,
18
- grantFn: `${fnPrefix}grant_stack`,
19
- consumeFn: `${fnPrefix}consume_stack`,
20
- moveFn: `${fnPrefix}move_stack`,
21
- transferFn: `${fnPrefix}transfer_stack`,
22
- containsEdge: 'inventory_contains',
23
- };
24
- }
25
- /**
26
- * Blueprint for a server-authoritative **inventory**: a per-player
27
- * `Inventory` container plus `ItemStack` containers (`item_id`, `quantity`,
28
- * `slot`), and owner-gated functions to grant, consume, move, and transfer
29
- * stacks. Quantity guards live in the invoke policies, so an untrusted client
30
- * can never overdraw or touch another player's items.
31
- *
32
- * Runtime counterpart: `client.kit(appId).inventory`.
33
- */
34
- export function inventoryBlueprint(options = {}) {
35
- const { typePrefix = '', maxSlots = 24, slotCount = 64 } = options;
36
- const names = inventoryNames(typePrefix);
37
- const ownerOnly = kitPolicyJson({ type: 'owner_of_self' });
38
- return {
39
- name: names.inventoryType,
40
- containerTypes: [
41
- {
42
- typeName: names.inventoryType,
43
- displayName: names.inventoryType,
44
- instantiableBy: 'member',
45
- description: 'A bag of item stacks owned by one player.',
46
- },
47
- {
48
- typeName: names.stackType,
49
- displayName: names.stackType,
50
- instantiableBy: 'member',
51
- description: 'One stack of a single item type in an inventory slot.',
52
- },
53
- ],
54
- propertyDefinitions: [
55
- {
56
- containerTypeName: names.inventoryType,
57
- key: 'max_slots',
58
- valueType: 'int',
59
- defaultValueJson: String(maxSlots),
60
- },
61
- { containerTypeName: names.stackType, key: 'item_id', valueType: 'string' },
62
- {
63
- containerTypeName: names.stackType,
64
- key: 'quantity',
65
- valueType: 'int',
66
- defaultValueJson: '0',
67
- },
68
- {
69
- containerTypeName: names.stackType,
70
- key: 'slot',
71
- valueType: 'int',
72
- defaultValueJson: '0',
73
- },
74
- ],
75
- functions: [
76
- {
77
- name: names.grantFn,
78
- containerTypeName: names.stackType,
79
- returnType: 'int',
80
- parameters: [{ name: 'amount', valueType: 'int', required: true }],
81
- mutations: [
82
- {
83
- target: 'self',
84
- property: 'quantity',
85
- expression: 'self.quantity + max(0, $amount)',
86
- },
87
- ],
88
- returnExpression: 'self.quantity',
89
- invokePolicyJson: ownerOnly,
90
- description: 'Add items to a stack the caller owns.',
91
- },
92
- {
93
- name: names.consumeFn,
94
- containerTypeName: names.stackType,
95
- returnType: 'int',
96
- parameters: [{ name: 'amount', valueType: 'int', required: true }],
97
- mutations: [
98
- { target: 'self', property: 'quantity', expression: 'self.quantity - $amount' },
99
- ],
100
- returnExpression: 'self.quantity',
101
- invokePolicyJson: kitPolicyJson({
102
- type: 'and',
103
- rules: [
104
- { type: 'owner_of_self' },
105
- {
106
- type: 'condition',
107
- expression: '$amount > 0 && self.quantity >= $amount',
108
- },
109
- ],
110
- }),
111
- description: 'Spend items from a stack; refuses to overdraw.',
112
- },
113
- {
114
- name: names.moveFn,
115
- containerTypeName: names.stackType,
116
- returnType: 'int',
117
- parameters: [{ name: 'to_slot', valueType: 'int', required: true }],
118
- mutations: [
119
- {
120
- target: 'self',
121
- property: 'slot',
122
- expression: `clamp($to_slot, 0, ${slotCount - 1})`,
123
- },
124
- ],
125
- returnExpression: 'self.slot',
126
- invokePolicyJson: ownerOnly,
127
- description: 'Move a stack to another slot.',
128
- },
129
- {
130
- name: names.transferFn,
131
- containerTypeName: names.stackType,
132
- returnType: 'int',
133
- parameters: [
134
- { name: 'to_id', valueType: 'container_ref', required: true },
135
- { name: 'amount', valueType: 'int', required: true },
136
- ],
137
- mutations: [
138
- { target: 'self', property: 'quantity', expression: 'self.quantity - $amount' },
139
- {
140
- target: 'ref($to_id)',
141
- property: 'quantity',
142
- expression: 'ref($to_id).quantity + $amount',
143
- },
144
- ],
145
- returnExpression: 'self.quantity',
146
- invokePolicyJson: kitPolicyJson({
147
- type: 'and',
148
- rules: [
149
- { type: 'owner_of_self' },
150
- {
151
- type: 'condition',
152
- expression: '$amount > 0 && self.quantity >= $amount && ref($to_id).item_id == self.item_id',
153
- },
154
- ],
155
- }),
156
- description: 'Atomically move items between two stacks of the same item type.',
157
- },
158
- ],
159
- };
160
- }
161
- /** Compute the type/function names a lock blueprint (and its runtime helper) uses. */
162
- export function lockNames(objectTypeName = 'Lockable', keyTypeName) {
163
- const snake = toSnakeCase(objectTypeName);
164
- return {
165
- objectType: objectTypeName,
166
- keyType: keyTypeName ?? `${objectTypeName}Key`,
167
- openFn: `open_${snake}`,
168
- closeFn: `close_${snake}`,
169
- };
170
- }
171
- function lockAuthorityRule(authority) {
172
- switch (authority.kind) {
173
- case 'owner':
174
- return { type: 'owner_of_self' };
175
- case 'key':
176
- // Container ownership is not readable from expressions, so the key
177
- // mirrors its owner into an `owner_user_id` property; the condition
178
- // verifies both the match and the ownership server-side.
179
- return {
180
- type: 'condition',
181
- expression: 'ref($key_id).key_id == self.required_key_id && ref($key_id).owner_user_id == $caller_user_id',
182
- };
183
- case 'gridPermission':
184
- return {
185
- type: 'grid_permission',
186
- key: authority.key,
187
- ...(authority.gridId !== undefined ? { gridId: authority.gridId } : {}),
188
- };
189
- case 'chunkPermission':
190
- // Resolved per-invocation against the grid covering the object's chunk.
191
- return {
192
- type: 'condition',
193
- expression: `has_chunk_permission($caller_user_id, "${authority.key}", self.cx, self.cy, self.cz, "${authority.mode ?? 'first'}")`,
194
- };
195
- case 'groupPermission':
196
- return {
197
- type: 'group_permission',
198
- groupId: authority.groupId,
199
- ...(authority.permission !== undefined
200
- ? { permission: authority.permission }
201
- : {}),
202
- };
203
- case 'custom':
204
- return authority.rule;
205
- }
206
- }
207
- /**
208
- * Blueprint for a **lockable game object** (door, chest, gate, switch) whose
209
- * `open`/`close` functions are gated by a configurable authority source:
210
- * ownership, a key item the caller must hold, a runtime grid permission, a
211
- * team/group permission, or any custom policy rule. Multiple authorities are
212
- * OR'd, so "the owner, or anyone with the right key" is one blueprint.
213
- *
214
- * Runtime counterpart: `client.kit(appId).objects`.
215
- */
216
- export function lockBlueprint(options) {
217
- const authorities = Array.isArray(options.authority)
218
- ? options.authority
219
- : [options.authority];
220
- if (authorities.length === 0) {
221
- throw new Error('lockBlueprint requires at least one authority source');
222
- }
223
- const names = lockNames(options.objectTypeName, options.keyTypeName);
224
- const usesKey = authorities.some((a) => a.kind === 'key');
225
- const usesChunk = authorities.some((a) => a.kind === 'chunkPermission');
226
- const rules = authorities.map(lockAuthorityRule);
227
- const policy = rules.length === 1 ? rules[0] : { type: 'or', rules };
228
- const policyJson = kitPolicyJson(policy);
229
- const parameters = usesKey
230
- ? [{ name: 'key_id', valueType: 'container_ref', required: true }]
231
- : [];
232
- const containerTypes = [
233
- {
234
- typeName: names.objectType,
235
- displayName: names.objectType,
236
- instantiableBy: 'admin',
237
- description: 'A lockable world object operated through gated functions.',
238
- },
239
- ];
240
- const propertyDefinitions = [
241
- {
242
- containerTypeName: names.objectType,
243
- key: 'is_open',
244
- valueType: 'bool',
245
- defaultValueJson: 'false',
246
- },
247
- ];
248
- if (usesChunk) {
249
- // The chunk the object stands in, read by has_chunk_permission().
250
- for (const axis of ['cx', 'cy', 'cz']) {
251
- propertyDefinitions.push({
252
- containerTypeName: names.objectType,
253
- key: axis,
254
- valueType: 'int',
255
- defaultValueJson: '0',
256
- });
257
- }
258
- }
259
- if (usesKey) {
260
- propertyDefinitions.push({
261
- containerTypeName: names.objectType,
262
- key: 'required_key_id',
263
- valueType: 'string',
264
- });
265
- containerTypes.push({
266
- typeName: names.keyType,
267
- displayName: names.keyType,
268
- instantiableBy: 'admin',
269
- description: 'A key item granting access to matching lockable objects.',
270
- });
271
- propertyDefinitions.push({ containerTypeName: names.keyType, key: 'key_id', valueType: 'string' }, {
272
- containerTypeName: names.keyType,
273
- key: 'owner_user_id',
274
- valueType: 'int',
275
- });
276
- }
277
- const openMutation = {
278
- target: 'self',
279
- property: 'is_open',
280
- expression: 'true',
281
- };
282
- const closeMutation = {
283
- target: 'self',
284
- property: 'is_open',
285
- expression: 'false',
286
- };
287
- return {
288
- name: names.objectType,
289
- containerTypes,
290
- propertyDefinitions,
291
- functions: [
292
- {
293
- name: names.openFn,
294
- containerTypeName: names.objectType,
295
- returnType: 'bool',
296
- parameters,
297
- mutations: [openMutation],
298
- returnExpression: 'self.is_open',
299
- invokePolicyJson: policyJson,
300
- description: `Open a ${names.objectType}; the invoke policy decides who may.`,
301
- },
302
- {
303
- name: names.closeFn,
304
- containerTypeName: names.objectType,
305
- returnType: 'bool',
306
- parameters,
307
- mutations: [closeMutation],
308
- returnExpression: 'self.is_open',
309
- invokePolicyJson: policyJson,
310
- description: `Close a ${names.objectType}; same authority as opening.`,
311
- },
312
- ],
313
- };
314
- }
315
- /** Compute the type/function names a plot blueprint (and its runtime helper) uses. */
316
- export function plotNames(typeName = 'Plot') {
317
- const snake = toSnakeCase(typeName);
318
- return {
319
- plotType: typeName,
320
- buyFn: `buy_${snake}`,
321
- rentFn: `rent_${snake}`,
322
- evictFn: `evict_${snake}`,
323
- };
324
- }
325
- /**
326
- * Blueprint for **sellable/rentable land**: a `Plot` container mapping a world
327
- * [grid](../domains/gameApps.js) to a price, whose `buy`/`rent` functions
328
- * consume currency AND grant runtime grid permissions in one transaction (via
329
- * permission effects) — the canonical read+write permission loop. Buying sets
330
- * the plot's `owner_user_id`; renting grants with a TTL; `evict` revokes.
331
- * The grants are enforced by the replication layer on movement/voxel writes
332
- * immediately at commit. Requires game-api v0.13.11+ (effects) — pair with
333
- * chunk-permission locks (v0.13.12+) for doors that honor the purchase.
334
- *
335
- * Runtime counterpart: `client.kit(appId).plots`.
336
- */
337
- export function plotBlueprint(options = {}) {
338
- const names = plotNames(options.typeName);
339
- const keys = options.permissionKeys ?? ['access', 'update_voxel_data'];
340
- const currency = options.currencyProperty ?? 'gold';
341
- const walletOwner = options.walletOwnerProperty ?? 'owner_user_id';
342
- const rentable = options.rentable ?? false;
343
- const walletParam = [
344
- { name: 'wallet_id', valueType: 'container_ref', required: true },
345
- ];
346
- const walletGuard = (priceExpr) => kitPolicyJson({
347
- type: 'condition',
348
- expression: `ref($wallet_id).${walletOwner} == $caller_user_id && ref($wallet_id).${currency} >= ${priceExpr}`,
349
- });
350
- const propertyDefinitions = [
351
- { containerTypeName: names.plotType, key: 'grid_id', valueType: 'int' },
352
- { containerTypeName: names.plotType, key: 'price', valueType: 'int', defaultValueJson: '0' },
353
- {
354
- containerTypeName: names.plotType,
355
- key: 'owner_user_id',
356
- valueType: 'int',
357
- defaultValueJson: '0',
358
- },
359
- ];
360
- if (rentable) {
361
- propertyDefinitions.push({
362
- containerTypeName: names.plotType,
363
- key: 'rent_price',
364
- valueType: 'int',
365
- defaultValueJson: '0',
366
- }, {
367
- containerTypeName: names.plotType,
368
- key: 'rent_ttl_seconds',
369
- valueType: 'int',
370
- defaultValueJson: '86400',
371
- });
372
- }
373
- const functions = [
374
- {
375
- name: names.buyFn,
376
- containerTypeName: names.plotType,
377
- returnType: 'int',
378
- parameters: walletParam,
379
- mutations: [
380
- {
381
- target: 'ref($wallet_id)',
382
- property: currency,
383
- expression: `ref($wallet_id).${currency} - self.price`,
384
- },
385
- { target: 'self', property: 'owner_user_id', expression: '$caller_user_id' },
386
- ],
387
- permissionEffects: [
388
- {
389
- action: 'grant',
390
- permissionKeys: keys,
391
- userExpression: '$caller_user_id',
392
- gridIdExpression: 'self.grid_id',
393
- },
394
- ],
395
- returnExpression: `ref($wallet_id).${currency}`,
396
- invokePolicyJson: walletGuard('self.price'),
397
- description: 'Buy this plot: spend the price AND receive grid permissions atomically.',
398
- },
399
- {
400
- name: names.evictFn,
401
- containerTypeName: names.plotType,
402
- parameters: [{ name: 'target_user_id', valueType: 'int', required: true }],
403
- mutations: [],
404
- permissionEffects: [
405
- {
406
- action: 'revoke',
407
- permissionKeys: keys,
408
- userExpression: '$target_user_id',
409
- gridIdExpression: 'self.grid_id',
410
- },
411
- ],
412
- invokePolicyJson: kitPolicyJson({
413
- type: 'condition',
414
- expression: 'self.owner_user_id == $caller_user_id',
415
- }),
416
- description: "Revoke a user's permissions on this plot (owner-gated; admins bypass).",
417
- },
418
- ];
419
- if (rentable) {
420
- functions.push({
421
- name: names.rentFn,
422
- containerTypeName: names.plotType,
423
- returnType: 'int',
424
- parameters: walletParam,
425
- mutations: [
426
- {
427
- target: 'ref($wallet_id)',
428
- property: currency,
429
- expression: `ref($wallet_id).${currency} - self.rent_price`,
430
- },
431
- ],
432
- permissionEffects: [
433
- {
434
- action: 'grant',
435
- permissionKeys: keys,
436
- userExpression: '$caller_user_id',
437
- gridIdExpression: 'self.grid_id',
438
- ttlSecondsExpression: 'self.rent_ttl_seconds',
439
- },
440
- ],
441
- returnExpression: `ref($wallet_id).${currency}`,
442
- invokePolicyJson: walletGuard('self.rent_price'),
443
- description: 'Rent this plot: spend the rent AND receive an expiring grid grant atomically.',
444
- });
445
- }
446
- return {
447
- name: names.plotType,
448
- containerTypes: [
449
- {
450
- typeName: names.plotType,
451
- displayName: names.plotType,
452
- instantiableBy: 'admin',
453
- description: 'A sellable/rentable plot of land mapped to a world grid.',
454
- },
455
- ],
456
- propertyDefinitions,
457
- functions,
458
- };
459
- }
460
- /** Compute the function/automation names an NPC behavior deploys under. */
461
- export function npcBehaviorFunctionName(behavior) {
462
- return behavior.functionName ?? toSnakeCase(behavior.name);
463
- }
464
- /**
465
- * Blueprint for an **NPC archetype**: an admin-instantiable container type
466
- * holding the NPC's durable state, one `autonomousInvocable` model function
467
- * per behavior (gated `is_automation` so players cannot puppet them), and the
468
- * automations + event triggers that drive those behaviors on the server.
469
- *
470
- * Runtime counterpart: `client.kit(appId).npcs`.
471
- */
472
- export function npcBlueprint(options) {
473
- const typeName = options.typeName ?? 'Npc';
474
- if (options.behaviors.length === 0) {
475
- throw new Error('npcBlueprint requires at least one behavior');
476
- }
477
- const propertyDefinitions = [
478
- {
479
- containerTypeName: typeName,
480
- key: 'role',
481
- valueType: 'string',
482
- defaultValueJson: '""',
483
- },
484
- { containerTypeName: typeName, key: 'x', valueType: 'float', defaultValueJson: '0' },
485
- { containerTypeName: typeName, key: 'y', valueType: 'float', defaultValueJson: '0' },
486
- { containerTypeName: typeName, key: 'z', valueType: 'float', defaultValueJson: '0' },
487
- {
488
- containerTypeName: typeName,
489
- key: 'behavior_state',
490
- valueType: 'string',
491
- defaultValueJson: '"idle"',
492
- },
493
- {
494
- containerTypeName: typeName,
495
- key: 'health',
496
- valueType: 'int',
497
- defaultValueJson: '100',
498
- },
499
- ...(options.extraProperties ?? []).map((p) => ({
500
- ...p,
501
- containerTypeName: typeName,
502
- })),
503
- ];
504
- const functions = [];
505
- const automations = [];
506
- const automationTriggers = [];
507
- for (const behavior of options.behaviors) {
508
- const functionName = npcBehaviorFunctionName(behavior);
509
- functions.push({
510
- name: functionName,
511
- containerTypeName: typeName,
512
- parameters: behavior.parameters,
513
- mutations: behavior.mutations,
514
- invokePolicyJson: kitPolicyJson({ type: 'is_automation' }),
515
- autonomousInvocable: true,
516
- description: `Server-driven NPC behavior for the '${behavior.name}' automation.`,
517
- });
518
- const selector = behavior.selector ??
519
- (behavior.role !== undefined
520
- ? { selfWhere: [{ key: 'role', op: '==', value: behavior.role }] }
521
- : undefined);
522
- const automation = {
523
- name: behavior.name,
524
- functionName,
525
- targetMode: 'type',
526
- targetTypeName: typeName,
527
- maxTargets: behavior.maxTargets ?? 8,
528
- ...(selector ? { selectorJson: JSON.stringify(selector) } : {}),
529
- ...(behavior.params ? { paramsJson: JSON.stringify(behavior.params) } : {}),
530
- ...(behavior.runAsUserId !== undefined
531
- ? { runAsUserId: behavior.runAsUserId }
532
- : {}),
533
- };
534
- if ('intervalMs' in behavior.trigger) {
535
- automation.triggerType = 'schedule';
536
- automation.scheduleKind = 'interval';
537
- automation.intervalMs = behavior.trigger.intervalMs;
538
- }
539
- else if ('cronExpr' in behavior.trigger) {
540
- automation.triggerType = 'schedule';
541
- automation.scheduleKind = 'cron';
542
- automation.cronExpr = behavior.trigger.cronExpr;
543
- }
544
- else {
545
- automation.triggerType = 'event';
546
- automationTriggers.push({
547
- automationName: behavior.name,
548
- onEvent: behavior.trigger.onEvent,
549
- ...(behavior.trigger.functionName !== undefined
550
- ? { functionName: behavior.trigger.functionName }
551
- : {}),
552
- ...(behavior.trigger.containerTypeName !== undefined
553
- ? { containerTypeName: behavior.trigger.containerTypeName }
554
- : {}),
555
- ...(behavior.trigger.propertyKey !== undefined
556
- ? { propertyKey: behavior.trigger.propertyKey }
557
- : {}),
558
- ...(behavior.trigger.debounceMs !== undefined
559
- ? { debounceMs: behavior.trigger.debounceMs }
560
- : {}),
561
- });
562
- }
563
- automations.push(automation);
564
- }
565
- return {
566
- name: typeName,
567
- containerTypes: [
568
- {
569
- typeName,
570
- displayName: typeName,
571
- instantiableBy: 'admin',
572
- description: 'A server-driven non-player character.',
573
- },
574
- ],
575
- propertyDefinitions,
576
- functions,
577
- automations,
578
- automationTriggers,
579
- };
580
- }
581
- /**
582
- * Merge blueprints into a single `gameModelSeed` payload plus the automation
583
- * upserts, rejecting duplicate type, property, function, or automation names
584
- * across blueprints (a duplicate almost always means two blueprints need
585
- * distinct prefixes/type names).
586
- */
587
- export function mergeBlueprints(appId, blueprints, options = {}) {
588
- const containerTypes = [];
589
- const propertyDefinitions = [];
590
- const functions = [];
591
- const containers = [];
592
- const edges = [];
593
- const automations = [];
594
- const automationTriggers = [];
595
- const seenTypes = new Map();
596
- const seenProps = new Map();
597
- const seenFunctions = new Map();
598
- const seenAutomations = new Map();
599
- const seenTempIds = new Map();
600
- const claim = (seen, key, blueprintName, kind) => {
601
- const existing = seen.get(key);
602
- if (existing !== undefined) {
603
- throw new Error(`Blueprint '${blueprintName}' redefines ${kind} '${key}' already defined by blueprint '${existing}'`);
604
- }
605
- seen.set(key, blueprintName);
606
- };
607
- for (const blueprint of blueprints) {
608
- for (const type of blueprint.containerTypes ?? []) {
609
- claim(seenTypes, type.typeName, blueprint.name, 'container type');
610
- containerTypes.push(type);
611
- }
612
- for (const prop of blueprint.propertyDefinitions ?? []) {
613
- claim(seenProps, `${prop.containerTypeName}.${prop.key}`, blueprint.name, 'property');
614
- propertyDefinitions.push(prop);
615
- }
616
- for (const fn of blueprint.functions ?? []) {
617
- claim(seenFunctions, fn.name, blueprint.name, 'function');
618
- functions.push(fn);
619
- }
620
- for (const container of blueprint.containers ?? []) {
621
- claim(seenTempIds, container.tempId, blueprint.name, 'container tempId');
622
- containers.push(container);
623
- }
624
- edges.push(...(blueprint.edges ?? []));
625
- for (const automation of blueprint.automations ?? []) {
626
- claim(seenAutomations, automation.name, blueprint.name, 'automation');
627
- automations.push({ ...automation, appId });
628
- }
629
- for (const trigger of blueprint.automationTriggers ?? []) {
630
- automationTriggers.push({ ...trigger, appId });
631
- }
632
- }
633
- const seedInput = {
634
- appId,
635
- ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
636
- ...(containerTypes.length ? { containerTypes } : {}),
637
- ...(propertyDefinitions.length ? { propertyDefinitions } : {}),
638
- ...(functions.length ? { functions } : {}),
639
- ...(containers.length ? { containers } : {}),
640
- ...(edges.length ? { edges } : {}),
641
- };
642
- return { seedInput, automations, automationTriggers };
643
- }