@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.
- package/README.md +21 -3
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +5 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/kit/blueprints/combat.d.ts +75 -0
- package/dist/kit/blueprints/combat.d.ts.map +1 -0
- package/dist/kit/blueprints/combat.js +351 -0
- package/dist/kit/blueprints/core.d.ts +222 -0
- package/dist/kit/blueprints/core.d.ts.map +1 -0
- package/dist/kit/blueprints/core.js +185 -0
- package/dist/kit/blueprints/decks.d.ts +50 -0
- package/dist/kit/blueprints/decks.d.ts.map +1 -0
- package/dist/kit/blueprints/decks.js +163 -0
- package/dist/kit/blueprints/economy.d.ts +88 -0
- package/dist/kit/blueprints/economy.d.ts.map +1 -0
- package/dist/kit/blueprints/economy.js +533 -0
- package/dist/kit/blueprints/guild.d.ts +51 -0
- package/dist/kit/blueprints/guild.d.ts.map +1 -0
- package/dist/kit/blueprints/guild.js +50 -0
- package/dist/kit/blueprints/index.d.ts +21 -0
- package/dist/kit/blueprints/index.d.ts.map +1 -0
- package/dist/kit/blueprints/index.js +20 -0
- package/dist/kit/blueprints/inventory.d.ts +38 -0
- package/dist/kit/blueprints/inventory.d.ts.map +1 -0
- package/dist/kit/blueprints/inventory.js +157 -0
- package/dist/kit/blueprints/leaderboards.d.ts +49 -0
- package/dist/kit/blueprints/leaderboards.d.ts.map +1 -0
- package/dist/kit/blueprints/leaderboards.js +126 -0
- package/dist/kit/blueprints/locks.d.ts +82 -0
- package/dist/kit/blueprints/locks.d.ts.map +1 -0
- package/dist/kit/blueprints/locks.js +155 -0
- package/dist/kit/blueprints/loot.d.ts +84 -0
- package/dist/kit/blueprints/loot.d.ts.map +1 -0
- package/dist/kit/blueprints/loot.js +228 -0
- package/dist/kit/blueprints/matches.d.ts +59 -0
- package/dist/kit/blueprints/matches.d.ts.map +1 -0
- package/dist/kit/blueprints/matches.js +246 -0
- package/dist/kit/blueprints/npcs.d.ts +71 -0
- package/dist/kit/blueprints/npcs.d.ts.map +1 -0
- package/dist/kit/blueprints/npcs.js +122 -0
- package/dist/kit/blueprints/plots.d.ts +54 -0
- package/dist/kit/blueprints/plots.d.ts.map +1 -0
- package/dist/kit/blueprints/plots.js +146 -0
- package/dist/kit/blueprints/progression.d.ts +68 -0
- package/dist/kit/blueprints/progression.d.ts.map +1 -0
- package/dist/kit/blueprints/progression.js +324 -0
- package/dist/kit/blueprints/quests.d.ts +65 -0
- package/dist/kit/blueprints/quests.d.ts.map +1 -0
- package/dist/kit/blueprints/quests.js +284 -0
- package/dist/kit/blueprints/worldsim.d.ts +84 -0
- package/dist/kit/blueprints/worldsim.d.ts.map +1 -0
- package/dist/kit/blueprints/worldsim.js +461 -0
- package/dist/kit/combat.d.ts +103 -0
- package/dist/kit/combat.d.ts.map +1 -0
- package/dist/kit/combat.js +151 -0
- package/dist/kit/decks.d.ts +123 -0
- package/dist/kit/decks.d.ts.map +1 -0
- package/dist/kit/decks.js +138 -0
- package/dist/kit/economy.d.ts +244 -0
- package/dist/kit/economy.d.ts.map +1 -0
- package/dist/kit/economy.js +365 -0
- package/dist/kit/features.d.ts +60 -0
- package/dist/kit/features.d.ts.map +1 -0
- package/dist/kit/features.js +65 -0
- package/dist/kit/index.d.ts +13 -2
- package/dist/kit/index.d.ts.map +1 -1
- package/dist/kit/index.js +12 -1
- package/dist/kit/inventory.d.ts +5 -0
- package/dist/kit/inventory.d.ts.map +1 -1
- package/dist/kit/inventory.js +14 -1
- package/dist/kit/kit.d.ts +58 -2
- package/dist/kit/kit.d.ts.map +1 -1
- package/dist/kit/kit.js +24 -2
- package/dist/kit/leaderboards.d.ts +66 -0
- package/dist/kit/leaderboards.d.ts.map +1 -0
- package/dist/kit/leaderboards.js +94 -0
- package/dist/kit/loot.d.ts +106 -0
- package/dist/kit/loot.d.ts.map +1 -0
- package/dist/kit/loot.js +120 -0
- package/dist/kit/matches.d.ts +140 -0
- package/dist/kit/matches.d.ts.map +1 -0
- package/dist/kit/matches.js +253 -0
- package/dist/kit/objects.js +1 -1
- package/dist/kit/plots.js +1 -1
- package/dist/kit/progression.d.ts +181 -0
- package/dist/kit/progression.d.ts.map +1 -0
- package/dist/kit/progression.js +265 -0
- package/dist/kit/quests.d.ts +110 -0
- package/dist/kit/quests.d.ts.map +1 -0
- package/dist/kit/quests.js +158 -0
- package/dist/kit/social.d.ts +267 -0
- package/dist/kit/social.d.ts.map +1 -0
- package/dist/kit/social.js +230 -0
- package/dist/kit/worldsim.d.ts +225 -0
- package/dist/kit/worldsim.d.ts.map +1 -0
- package/dist/kit/worldsim.js +231 -0
- package/package.json +1 -1
- package/dist/kit/blueprints.d.ts +0 -370
- package/dist/kit/blueprints.d.ts.map +0 -1
- package/dist/kit/blueprints.js +0 -643
package/dist/kit/blueprints.js
DELETED
|
@@ -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
|
-
}
|