@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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { andPolicies, kitPolicyJson, toSnakeCase, } from './core.js';
|
|
2
|
+
/** Compute the type/function names a lock blueprint (and its runtime helper) uses. */
|
|
3
|
+
export function lockNames(objectTypeName = 'Lockable', keyTypeName) {
|
|
4
|
+
const snake = toSnakeCase(objectTypeName);
|
|
5
|
+
return {
|
|
6
|
+
objectType: objectTypeName,
|
|
7
|
+
keyType: keyTypeName ?? `${objectTypeName}Key`,
|
|
8
|
+
openFn: `open_${snake}`,
|
|
9
|
+
closeFn: `close_${snake}`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function lockAuthorityRule(authority) {
|
|
13
|
+
switch (authority.kind) {
|
|
14
|
+
case 'owner':
|
|
15
|
+
return { type: 'owner_of_self' };
|
|
16
|
+
case 'key':
|
|
17
|
+
// Container ownership is not readable from expressions, so the key
|
|
18
|
+
// mirrors its owner into an `owner_user_id` property; the condition
|
|
19
|
+
// verifies both the match and the ownership server-side.
|
|
20
|
+
return {
|
|
21
|
+
type: 'condition',
|
|
22
|
+
expression: 'ref($key_id).key_id == self.required_key_id && ref($key_id).owner_user_id == $caller_user_id',
|
|
23
|
+
};
|
|
24
|
+
case 'gridPermission':
|
|
25
|
+
return {
|
|
26
|
+
type: 'grid_permission',
|
|
27
|
+
key: authority.key,
|
|
28
|
+
...(authority.gridId !== undefined ? { gridId: authority.gridId } : {}),
|
|
29
|
+
};
|
|
30
|
+
case 'chunkPermission':
|
|
31
|
+
// Resolved per-invocation against the grid covering the object's chunk.
|
|
32
|
+
return {
|
|
33
|
+
type: 'condition',
|
|
34
|
+
expression: `has_chunk_permission($caller_user_id, "${authority.key}", self.cx, self.cy, self.cz, "${authority.mode ?? 'first'}")`,
|
|
35
|
+
};
|
|
36
|
+
case 'groupPermission':
|
|
37
|
+
return {
|
|
38
|
+
type: 'group_permission',
|
|
39
|
+
groupId: authority.groupId,
|
|
40
|
+
...(authority.permission !== undefined
|
|
41
|
+
? { permission: authority.permission }
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
case 'custom':
|
|
45
|
+
return authority.rule;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Blueprint for a **lockable game object** (door, chest, gate, switch) whose
|
|
50
|
+
* `open`/`close` functions are gated by a configurable authority source:
|
|
51
|
+
* ownership, a key item the caller must hold, a runtime grid permission, a
|
|
52
|
+
* team/group permission, or any custom policy rule. Multiple authorities are
|
|
53
|
+
* OR'd, so "the owner, or anyone with the right key" is one blueprint.
|
|
54
|
+
*
|
|
55
|
+
* Runtime counterpart: `client.kit(appId).objects`.
|
|
56
|
+
*/
|
|
57
|
+
export function lockBlueprint(options) {
|
|
58
|
+
const authorities = Array.isArray(options.authority)
|
|
59
|
+
? options.authority
|
|
60
|
+
: [options.authority];
|
|
61
|
+
if (authorities.length === 0) {
|
|
62
|
+
throw new Error('lockBlueprint requires at least one authority source');
|
|
63
|
+
}
|
|
64
|
+
const names = lockNames(options.objectTypeName, options.keyTypeName);
|
|
65
|
+
const usesKey = authorities.some((a) => a.kind === 'key');
|
|
66
|
+
const usesChunk = authorities.some((a) => a.kind === 'chunkPermission');
|
|
67
|
+
const rules = authorities.map(lockAuthorityRule);
|
|
68
|
+
const policy = rules.length === 1 ? rules[0] : { type: 'or', rules };
|
|
69
|
+
const policyJson = kitPolicyJson(andPolicies(policy, options.policyExtra));
|
|
70
|
+
const parameters = usesKey
|
|
71
|
+
? [{ name: 'key_id', valueType: 'container_ref', required: true }]
|
|
72
|
+
: [];
|
|
73
|
+
const containerTypes = [
|
|
74
|
+
{
|
|
75
|
+
typeName: names.objectType,
|
|
76
|
+
displayName: names.objectType,
|
|
77
|
+
instantiableBy: 'admin',
|
|
78
|
+
description: 'A lockable world object operated through gated functions.',
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const propertyDefinitions = [
|
|
82
|
+
{
|
|
83
|
+
containerTypeName: names.objectType,
|
|
84
|
+
key: 'is_open',
|
|
85
|
+
valueType: 'bool',
|
|
86
|
+
defaultValueJson: 'false',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
if (usesChunk) {
|
|
90
|
+
// The chunk the object stands in, read by has_chunk_permission().
|
|
91
|
+
for (const axis of ['cx', 'cy', 'cz']) {
|
|
92
|
+
propertyDefinitions.push({
|
|
93
|
+
containerTypeName: names.objectType,
|
|
94
|
+
key: axis,
|
|
95
|
+
valueType: 'int',
|
|
96
|
+
defaultValueJson: '0',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (usesKey) {
|
|
101
|
+
propertyDefinitions.push({
|
|
102
|
+
containerTypeName: names.objectType,
|
|
103
|
+
key: 'required_key_id',
|
|
104
|
+
valueType: 'string',
|
|
105
|
+
});
|
|
106
|
+
containerTypes.push({
|
|
107
|
+
typeName: names.keyType,
|
|
108
|
+
displayName: names.keyType,
|
|
109
|
+
instantiableBy: 'admin',
|
|
110
|
+
description: 'A key item granting access to matching lockable objects.',
|
|
111
|
+
});
|
|
112
|
+
propertyDefinitions.push({ containerTypeName: names.keyType, key: 'key_id', valueType: 'string' }, {
|
|
113
|
+
containerTypeName: names.keyType,
|
|
114
|
+
key: 'owner_user_id',
|
|
115
|
+
valueType: 'int',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const openMutation = {
|
|
119
|
+
target: 'self',
|
|
120
|
+
property: 'is_open',
|
|
121
|
+
expression: 'true',
|
|
122
|
+
};
|
|
123
|
+
const closeMutation = {
|
|
124
|
+
target: 'self',
|
|
125
|
+
property: 'is_open',
|
|
126
|
+
expression: 'false',
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
name: names.objectType,
|
|
130
|
+
containerTypes,
|
|
131
|
+
propertyDefinitions,
|
|
132
|
+
functions: [
|
|
133
|
+
{
|
|
134
|
+
name: names.openFn,
|
|
135
|
+
containerTypeName: names.objectType,
|
|
136
|
+
returnType: 'bool',
|
|
137
|
+
parameters,
|
|
138
|
+
mutations: [openMutation],
|
|
139
|
+
returnExpression: 'self.is_open',
|
|
140
|
+
invokePolicyJson: policyJson,
|
|
141
|
+
description: `Open a ${names.objectType}; the invoke policy decides who may.`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: names.closeFn,
|
|
145
|
+
containerTypeName: names.objectType,
|
|
146
|
+
returnType: 'bool',
|
|
147
|
+
parameters,
|
|
148
|
+
mutations: [closeMutation],
|
|
149
|
+
returnExpression: 'self.is_open',
|
|
150
|
+
invokePolicyJson: policyJson,
|
|
151
|
+
description: `Close a ${names.objectType}; same authority as opening.`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type KitBlueprint, type KitOwnerIdKind, type KitTrustedAuthority } from './core.js';
|
|
2
|
+
/** One weighted entry of a loot table. */
|
|
3
|
+
export interface LootEntrySpec {
|
|
4
|
+
itemId: string;
|
|
5
|
+
/** Relative weight (> 0); probabilities are weights normalized per table. */
|
|
6
|
+
weight: number;
|
|
7
|
+
/** Minimum quantity granted. Defaults to 1. */
|
|
8
|
+
minQty?: number;
|
|
9
|
+
/** Maximum quantity granted. Defaults to `minQty`. */
|
|
10
|
+
maxQty?: number;
|
|
11
|
+
}
|
|
12
|
+
/** One loot table, unrolled into a single expression at blueprint-build time. */
|
|
13
|
+
export interface LootTableSpec {
|
|
14
|
+
/** Stable table identifier; also derives the roll function name. */
|
|
15
|
+
tableId: string;
|
|
16
|
+
/** 1–16 weighted entries (the expression parser caps chain length). */
|
|
17
|
+
entries: LootEntrySpec[];
|
|
18
|
+
}
|
|
19
|
+
/** An event-triggered drop: rolls a pooled LootRoll when a model event fires. */
|
|
20
|
+
export interface LootDropSpec {
|
|
21
|
+
/** Automation name (unique per app), e.g. `'goblin-drop'`. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** The table to roll. */
|
|
24
|
+
tableId: string;
|
|
25
|
+
/** The model event that triggers the drop (e.g. a `mob_died` function). */
|
|
26
|
+
onEvent: 'function_invoked' | 'property_changed' | 'container_created';
|
|
27
|
+
functionName?: string;
|
|
28
|
+
containerTypeName?: string;
|
|
29
|
+
propertyKey?: string;
|
|
30
|
+
debounceMs?: number;
|
|
31
|
+
/** Rolls per event. Defaults to 1. */
|
|
32
|
+
maxTargets?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Options for {@link lootBlueprint}. */
|
|
35
|
+
export interface LootBlueprintOptions {
|
|
36
|
+
/** Prefix for the type/function names. Defaults to none. */
|
|
37
|
+
typePrefix?: string;
|
|
38
|
+
/** The loot tables; entries are unrolled into expressions at build time. */
|
|
39
|
+
tables: LootTableSpec[];
|
|
40
|
+
/**
|
|
41
|
+
* Who may roll. Defaults to `'server'` (app admins / studio backend);
|
|
42
|
+
* automations configured via `drops` may always roll (trusted automations
|
|
43
|
+
* bypass invoke policies, and the roll functions are marked
|
|
44
|
+
* `autonomousInvocable` when drops reference their table).
|
|
45
|
+
*/
|
|
46
|
+
rollAuthority?: KitTrustedAuthority;
|
|
47
|
+
/**
|
|
48
|
+
* Event-triggered drops: each rolls an UNROLLED pooled `LootRoll` container
|
|
49
|
+
* of its table when the event fires (automations mutate — they cannot
|
|
50
|
+
* create containers — so keep a small pool of pre-created rolls per table).
|
|
51
|
+
*/
|
|
52
|
+
drops?: LootDropSpec[];
|
|
53
|
+
/** Owner-mirror typing (see the kit convention). Defaults to `'int'`. */
|
|
54
|
+
ownerIdKind?: KitOwnerIdKind;
|
|
55
|
+
}
|
|
56
|
+
/** Names derived by {@link lootBlueprint} for a given prefix. */
|
|
57
|
+
export interface LootNames {
|
|
58
|
+
rollType: string;
|
|
59
|
+
claimFn: string;
|
|
60
|
+
/** Snake-cased function-name prefix (empty without a `typePrefix`). */
|
|
61
|
+
fnPrefix: string;
|
|
62
|
+
}
|
|
63
|
+
/** Compute the type/function names a loot blueprint (and its runtime helper) uses. */
|
|
64
|
+
export declare function lootNames(typePrefix?: string): LootNames;
|
|
65
|
+
/** The roll function name for one table. */
|
|
66
|
+
export declare function lootRollFn(tableId: string, typePrefix?: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Blueprint for **loot tables & drops**: weighted tables are unrolled into
|
|
69
|
+
* pure expressions at blueprint-build time (the expression language is
|
|
70
|
+
* loop-free), rolled server-side into durable `LootRoll` containers, and
|
|
71
|
+
* claimed atomically into an item stack. Covers chest rolls, mob drops, and
|
|
72
|
+
* gacha pulls.
|
|
73
|
+
*
|
|
74
|
+
* Flow: create a `LootRoll` (member) for a table → `roll_<table>` (trusted:
|
|
75
|
+
* server scope by default, or an event automation via `drops`) stores a
|
|
76
|
+
* single `rand()` seed and resolves item + quantity from it in one
|
|
77
|
+
* transaction → the owner claims with `claim_roll`, which marks the roll
|
|
78
|
+
* claimed AND grants the stack in the same invoke (no double-claim, no
|
|
79
|
+
* client-chosen rewards).
|
|
80
|
+
*
|
|
81
|
+
* Runtime counterpart: `client.kit(appId).loot`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function lootBlueprint(options: LootBlueprintOptions): KitBlueprint;
|
|
84
|
+
//# sourceMappingURL=loot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loot.d.ts","sourceRoot":"","sources":["../../../src/kit/blueprints/loot.ts"],"names":[],"mappings":"AAIA,OAAO,EAQL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,WAAW,CAAC;AAEnB,0CAA0C;AAC1C,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC;;;;OAIG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,yEAAyE;IACzE,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED,iEAAiE;AACjE,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,UAAU,SAAK,GAAG,SAAS,CAOpD;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,MAAM,CAGnE;AAwCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,YAAY,CAuLzE"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { kitPolicyJson, ownerEqualsCaller, ownerMirrorProperty, toSnakeCase, trustedAuthorityFields, } from './core.js';
|
|
2
|
+
/** Compute the type/function names a loot blueprint (and its runtime helper) uses. */
|
|
3
|
+
export function lootNames(typePrefix = '') {
|
|
4
|
+
const fnPrefix = typePrefix ? `${toSnakeCase(typePrefix)}_` : '';
|
|
5
|
+
return {
|
|
6
|
+
rollType: `${typePrefix}LootRoll`,
|
|
7
|
+
claimFn: `${fnPrefix}claim_roll`,
|
|
8
|
+
fnPrefix,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** The roll function name for one table. */
|
|
12
|
+
export function lootRollFn(tableId, typePrefix = '') {
|
|
13
|
+
const fnPrefix = typePrefix ? `${toSnakeCase(typePrefix)}_` : '';
|
|
14
|
+
return `${fnPrefix}roll_${toSnakeCase(tableId)}`;
|
|
15
|
+
}
|
|
16
|
+
/** Format a probability threshold as an expression float literal. */
|
|
17
|
+
function floatLiteral(n) {
|
|
18
|
+
const s = String(n);
|
|
19
|
+
return s.includes('.') || s.includes('e') ? s : `${s}.0`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Unroll a weighted table into a nested-if chain over the stored roll seed:
|
|
23
|
+
* `if(self.seed < t1, "a", if(self.seed < t2, "b", "c"))` with cumulative
|
|
24
|
+
* normalized thresholds. Expressions are loop-free, so the selection chain
|
|
25
|
+
* is generated at BUILD time — one `rand()` is stored first and every branch
|
|
26
|
+
* reads that same seed, keeping the distribution exact.
|
|
27
|
+
*/
|
|
28
|
+
function unrollItemChain(entries) {
|
|
29
|
+
const total = entries.reduce((sum, e) => sum + e.weight, 0);
|
|
30
|
+
let expr = JSON.stringify(entries[entries.length - 1].itemId);
|
|
31
|
+
for (let i = entries.length - 2; i >= 0; i--) {
|
|
32
|
+
// Threshold below which entries[0..i] win; nested back-to-front.
|
|
33
|
+
const cumulative = entries.slice(0, i + 1).reduce((sum, e) => sum + e.weight, 0);
|
|
34
|
+
expr = `if(self.seed < ${floatLiteral(cumulative / total)}, ${JSON.stringify(entries[i].itemId)}, ${expr})`;
|
|
35
|
+
}
|
|
36
|
+
return expr;
|
|
37
|
+
}
|
|
38
|
+
/** Unroll the per-item quantity chain, keyed on the already-rolled item id. */
|
|
39
|
+
function unrollQtyChain(entries) {
|
|
40
|
+
const qtyExpr = (e) => {
|
|
41
|
+
const min = e.minQty ?? 1;
|
|
42
|
+
const max = e.maxQty ?? min;
|
|
43
|
+
return max > min ? `rand_int(${min}, ${max})` : String(min);
|
|
44
|
+
};
|
|
45
|
+
let expr = qtyExpr(entries[entries.length - 1]);
|
|
46
|
+
for (let i = entries.length - 2; i >= 0; i--) {
|
|
47
|
+
expr = `if(self.rolled_item_id == ${JSON.stringify(entries[i].itemId)}, ${qtyExpr(entries[i])}, ${expr})`;
|
|
48
|
+
}
|
|
49
|
+
return expr;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Blueprint for **loot tables & drops**: weighted tables are unrolled into
|
|
53
|
+
* pure expressions at blueprint-build time (the expression language is
|
|
54
|
+
* loop-free), rolled server-side into durable `LootRoll` containers, and
|
|
55
|
+
* claimed atomically into an item stack. Covers chest rolls, mob drops, and
|
|
56
|
+
* gacha pulls.
|
|
57
|
+
*
|
|
58
|
+
* Flow: create a `LootRoll` (member) for a table → `roll_<table>` (trusted:
|
|
59
|
+
* server scope by default, or an event automation via `drops`) stores a
|
|
60
|
+
* single `rand()` seed and resolves item + quantity from it in one
|
|
61
|
+
* transaction → the owner claims with `claim_roll`, which marks the roll
|
|
62
|
+
* claimed AND grants the stack in the same invoke (no double-claim, no
|
|
63
|
+
* client-chosen rewards).
|
|
64
|
+
*
|
|
65
|
+
* Runtime counterpart: `client.kit(appId).loot`.
|
|
66
|
+
*/
|
|
67
|
+
export function lootBlueprint(options) {
|
|
68
|
+
const { typePrefix = '', tables, rollAuthority = 'server', drops = [], ownerIdKind: kind = 'int', } = options;
|
|
69
|
+
if (!tables.length) {
|
|
70
|
+
throw new Error('lootBlueprint requires at least one table');
|
|
71
|
+
}
|
|
72
|
+
for (const table of tables) {
|
|
73
|
+
if (table.entries.length < 1 || table.entries.length > 16) {
|
|
74
|
+
throw new Error(`lootBlueprint table '${table.tableId}' must have 1-16 entries (parser-limit-aware unrolling)`);
|
|
75
|
+
}
|
|
76
|
+
if (table.entries.some((e) => !(e.weight > 0))) {
|
|
77
|
+
throw new Error(`lootBlueprint table '${table.tableId}' entries must have weight > 0`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const tableIds = new Set(tables.map((t) => t.tableId));
|
|
81
|
+
for (const drop of drops) {
|
|
82
|
+
if (!tableIds.has(drop.tableId)) {
|
|
83
|
+
throw new Error(`lootBlueprint drop '${drop.name}' references unknown table '${drop.tableId}'`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const names = lootNames(typePrefix);
|
|
87
|
+
const droppedTables = new Set(drops.map((d) => d.tableId));
|
|
88
|
+
const propertyDefinitions = [
|
|
89
|
+
ownerMirrorProperty(names.rollType, kind),
|
|
90
|
+
{
|
|
91
|
+
containerTypeName: names.rollType,
|
|
92
|
+
key: 'table_id',
|
|
93
|
+
valueType: 'string',
|
|
94
|
+
defaultValueJson: '""',
|
|
95
|
+
description: 'The loot table this roll draws from.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
containerTypeName: names.rollType,
|
|
99
|
+
key: 'seed',
|
|
100
|
+
valueType: 'float',
|
|
101
|
+
defaultValueJson: '0',
|
|
102
|
+
description: 'The stored rand() seed the weighted selection reads (audit-friendly).',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
containerTypeName: names.rollType,
|
|
106
|
+
key: 'rolled_item_id',
|
|
107
|
+
valueType: 'string',
|
|
108
|
+
defaultValueJson: '""',
|
|
109
|
+
description: 'The rolled item (empty until rolled; a roll cannot re-roll).',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
containerTypeName: names.rollType,
|
|
113
|
+
key: 'rolled_qty',
|
|
114
|
+
valueType: 'int',
|
|
115
|
+
defaultValueJson: '0',
|
|
116
|
+
description: 'The rolled quantity.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
containerTypeName: names.rollType,
|
|
120
|
+
key: 'claimed',
|
|
121
|
+
valueType: 'bool',
|
|
122
|
+
defaultValueJson: 'false',
|
|
123
|
+
description: 'True once claimed into a stack (single-claim guard).',
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
const functions = tables.map((table) => {
|
|
127
|
+
const auth = trustedAuthorityFields(rollAuthority, `self.table_id == ${JSON.stringify(table.tableId)} && self.rolled_item_id == ""`);
|
|
128
|
+
return {
|
|
129
|
+
name: lootRollFn(table.tableId, typePrefix),
|
|
130
|
+
containerTypeName: names.rollType,
|
|
131
|
+
returnType: 'string',
|
|
132
|
+
mutations: [
|
|
133
|
+
{ target: 'self', property: 'seed', expression: 'rand()' },
|
|
134
|
+
{
|
|
135
|
+
target: 'self',
|
|
136
|
+
property: 'rolled_item_id',
|
|
137
|
+
expression: unrollItemChain(table.entries),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
target: 'self',
|
|
141
|
+
property: 'rolled_qty',
|
|
142
|
+
expression: unrollQtyChain(table.entries),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
returnExpression: 'self.rolled_item_id',
|
|
146
|
+
...auth,
|
|
147
|
+
...(droppedTables.has(table.tableId) ? { autonomousInvocable: true } : {}),
|
|
148
|
+
description: `Roll the '${table.tableId}' loot table: stores one rand() seed, then resolves item and quantity from it in one transaction (unrolled weighted selection).`,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
functions.push({
|
|
152
|
+
name: names.claimFn,
|
|
153
|
+
containerTypeName: names.rollType,
|
|
154
|
+
returnType: 'int',
|
|
155
|
+
parameters: [
|
|
156
|
+
{
|
|
157
|
+
name: 'to_stack_id',
|
|
158
|
+
valueType: 'container_ref',
|
|
159
|
+
required: true,
|
|
160
|
+
description: "A caller-owned stack of the rolled item that receives the quantity.",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
mutations: [
|
|
164
|
+
{ target: 'self', property: 'claimed', expression: 'true' },
|
|
165
|
+
{
|
|
166
|
+
target: 'ref($to_stack_id)',
|
|
167
|
+
property: 'quantity',
|
|
168
|
+
expression: 'ref($to_stack_id).quantity + self.rolled_qty',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
returnExpression: 'self.rolled_qty',
|
|
172
|
+
invokePolicyJson: kitPolicyJson({
|
|
173
|
+
type: 'condition',
|
|
174
|
+
expression: [
|
|
175
|
+
ownerEqualsCaller('self.owner_user_id', kind),
|
|
176
|
+
'not(self.claimed)',
|
|
177
|
+
'self.rolled_item_id != ""',
|
|
178
|
+
'ref($to_stack_id).item_id == self.rolled_item_id',
|
|
179
|
+
ownerEqualsCaller('ref($to_stack_id).owner_user_id', kind),
|
|
180
|
+
].join(' && '),
|
|
181
|
+
}),
|
|
182
|
+
description: 'Claim a rolled loot into a stack: the claimed flag and the grant commit atomically, so a roll can never be claimed twice.',
|
|
183
|
+
});
|
|
184
|
+
const automations = [];
|
|
185
|
+
const automationTriggers = [];
|
|
186
|
+
for (const drop of drops) {
|
|
187
|
+
automations.push({
|
|
188
|
+
name: drop.name,
|
|
189
|
+
functionName: lootRollFn(drop.tableId, typePrefix),
|
|
190
|
+
targetMode: 'type',
|
|
191
|
+
targetTypeName: names.rollType,
|
|
192
|
+
triggerType: 'event',
|
|
193
|
+
maxTargets: drop.maxTargets ?? 1,
|
|
194
|
+
selectorJson: JSON.stringify({
|
|
195
|
+
selfWhere: [
|
|
196
|
+
{ key: 'table_id', op: '==', value: drop.tableId },
|
|
197
|
+
{ key: 'rolled_item_id', op: '==', value: '' },
|
|
198
|
+
],
|
|
199
|
+
pick: 'random',
|
|
200
|
+
}),
|
|
201
|
+
description: `Event-triggered '${drop.tableId}' drop: rolls a pooled unrolled LootRoll when the event fires.`,
|
|
202
|
+
});
|
|
203
|
+
automationTriggers.push({
|
|
204
|
+
automationName: drop.name,
|
|
205
|
+
onEvent: drop.onEvent,
|
|
206
|
+
...(drop.functionName !== undefined ? { functionName: drop.functionName } : {}),
|
|
207
|
+
...(drop.containerTypeName !== undefined
|
|
208
|
+
? { containerTypeName: drop.containerTypeName }
|
|
209
|
+
: {}),
|
|
210
|
+
...(drop.propertyKey !== undefined ? { propertyKey: drop.propertyKey } : {}),
|
|
211
|
+
...(drop.debounceMs !== undefined ? { debounceMs: drop.debounceMs } : {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name: names.rollType,
|
|
216
|
+
containerTypes: [
|
|
217
|
+
{
|
|
218
|
+
typeName: names.rollType,
|
|
219
|
+
displayName: names.rollType,
|
|
220
|
+
instantiableBy: 'member',
|
|
221
|
+
description: 'One durable loot roll: seeded, resolved, and claimed entirely server-side.',
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
propertyDefinitions,
|
|
225
|
+
functions,
|
|
226
|
+
...(automations.length ? { automations, automationTriggers } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type KitBlueprint, type KitOwnerIdKind, type KitTrustedAuthority } from './core.js';
|
|
2
|
+
/** Options for {@link matchesBlueprint}. */
|
|
3
|
+
export interface MatchesBlueprintOptions {
|
|
4
|
+
/** Prefix for the type/function names. Defaults to none. */
|
|
5
|
+
typePrefix?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Who may submit points to a `Score` row. Defaults to `'host'` (the
|
|
8
|
+
* elected host referees); use `'server'` or `'automation'` for
|
|
9
|
+
* server-refereed modes. Never plain players.
|
|
10
|
+
*/
|
|
11
|
+
scoreAuthority?: KitTrustedAuthority;
|
|
12
|
+
/**
|
|
13
|
+
* When set, adds a `turn_tick` interval automation that increments each
|
|
14
|
+
* active match's `tick_count` — the wall-clock-free timer primitive: store
|
|
15
|
+
* the tick at turn start and treat `tick_count - turn_started_tick >= N`
|
|
16
|
+
* as a timeout (there is no `now()` in expressions; counters replace
|
|
17
|
+
* clocks).
|
|
18
|
+
*/
|
|
19
|
+
turnTick?: {
|
|
20
|
+
intervalMs: number;
|
|
21
|
+
};
|
|
22
|
+
/** Owner-mirror typing (see the kit convention). Defaults to `'int'`. */
|
|
23
|
+
ownerIdKind?: KitOwnerIdKind;
|
|
24
|
+
}
|
|
25
|
+
/** Names derived by {@link matchesBlueprint} for a given prefix. */
|
|
26
|
+
export interface MatchesNames {
|
|
27
|
+
metaType: string;
|
|
28
|
+
scoreType: string;
|
|
29
|
+
startFn: string;
|
|
30
|
+
advanceRoundFn: string;
|
|
31
|
+
scoreFn: string;
|
|
32
|
+
endFn: string;
|
|
33
|
+
turnTickFn: string;
|
|
34
|
+
turnTickAutomation: string;
|
|
35
|
+
}
|
|
36
|
+
/** Compute the type/function names a matches blueprint (and its runtime helper) uses. */
|
|
37
|
+
export declare function matchesNames(typePrefix?: string): MatchesNames;
|
|
38
|
+
/**
|
|
39
|
+
* Blueprint for **matches** (lobbies, rounds, turns, scoring) — the
|
|
40
|
+
* session-layer wrapper every session game needs. Sessions ARE the match
|
|
41
|
+
* primitive (participants + current turn); this adds a session-scoped
|
|
42
|
+
* `MatchMeta` (lobby state, round, winner, notification channel) and
|
|
43
|
+
* per-player `Score` rows.
|
|
44
|
+
*
|
|
45
|
+
* Lifecycle functions (`start_match` / `advance_round` / `end_match`) are
|
|
46
|
+
* creator-or-host gated and declare a **channel notification** — Buddy pings
|
|
47
|
+
* every channel member post-commit with `"match_changed"`, and clients
|
|
48
|
+
* re-pull the meta (the notify-to-pull pattern; `kit.matches.onMatchChanged`
|
|
49
|
+
* wraps the subscription). Turn order itself uses the platform's
|
|
50
|
+
* `gameModelSetSessionTurn` (the runtime helper calls it — turn authority is
|
|
51
|
+
* enforced by the service, not an expression).
|
|
52
|
+
*
|
|
53
|
+
* End-of-match hooks: attach an event automation to `end_match`
|
|
54
|
+
* (`function_invoked`) to submit leaderboard scores or adjust ratings.
|
|
55
|
+
*
|
|
56
|
+
* Runtime counterpart: `client.kit(appId).matches`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function matchesBlueprint(options?: MatchesBlueprintOptions): KitBlueprint;
|
|
59
|
+
//# sourceMappingURL=matches.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matches.d.ts","sourceRoot":"","sources":["../../../src/kit/blueprints/matches.ts"],"names":[],"mappings":"AAKA,OAAO,EAML,KAAK,YAAY,EAEjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,WAAW,CAAC;AAEnB,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,yEAAyE;IACzE,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,UAAU,SAAK,GAAG,YAAY,CAY1D;AAaD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,YAAY,CA0NpF"}
|