@crowdedkingdoms/crowdyjs 18.10.0 → 18.11.0-dev.1

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 (63) hide show
  1. package/MIGRATION.md +43 -0
  2. package/README.md +79 -30
  3. package/dist/crowdy-client.d.ts +11 -3
  4. package/dist/crowdy-client.d.ts.map +1 -1
  5. package/dist/crowdy-client.js +25 -4
  6. package/dist/default-origin.d.ts +6 -6
  7. package/dist/default-origin.d.ts.map +1 -1
  8. package/dist/default-origin.js +6 -6
  9. package/dist/domains/exec.d.ts +6 -0
  10. package/dist/domains/exec.d.ts.map +1 -1
  11. package/dist/domains/exec.js +28 -2
  12. package/dist/generated/graphql-schema.d.ts +1 -1
  13. package/dist/generated/graphql-schema.d.ts.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -3
  17. package/dist/kit/index.d.ts +13 -2
  18. package/dist/kit/index.d.ts.map +1 -1
  19. package/dist/kit/index.js +13 -2
  20. package/dist/kit/kit.d.ts +83 -7
  21. package/dist/kit/kit.d.ts.map +1 -1
  22. package/dist/kit/kit.js +82 -4
  23. package/dist/kit/mirror.d.ts +111 -0
  24. package/dist/kit/mirror.d.ts.map +1 -0
  25. package/dist/kit/mirror.js +295 -0
  26. package/dist/kit/modules/matchmaking.d.ts +107 -0
  27. package/dist/kit/modules/matchmaking.d.ts.map +1 -0
  28. package/dist/kit/modules/matchmaking.js +109 -0
  29. package/dist/kit/modules/npcs.d.ts +80 -0
  30. package/dist/kit/modules/npcs.d.ts.map +1 -0
  31. package/dist/kit/modules/npcs.js +102 -0
  32. package/dist/kit/modules/presence.d.ts +81 -0
  33. package/dist/kit/modules/presence.d.ts.map +1 -0
  34. package/dist/kit/modules/presence.js +99 -0
  35. package/dist/kit/modules/sessions.d.ts +91 -0
  36. package/dist/kit/modules/sessions.d.ts.map +1 -0
  37. package/dist/kit/modules/sessions.js +84 -0
  38. package/dist/kit/modules/world.d.ts +69 -0
  39. package/dist/kit/modules/world.d.ts.map +1 -0
  40. package/dist/kit/modules/world.js +35 -0
  41. package/dist/kit/protocol.d.ts +128 -0
  42. package/dist/kit/protocol.d.ts.map +1 -0
  43. package/dist/kit/protocol.js +171 -0
  44. package/dist/kit/realtime.d.ts +72 -0
  45. package/dist/kit/realtime.d.ts.map +1 -0
  46. package/dist/kit/realtime.js +108 -0
  47. package/dist/kit/runtime.d.ts +90 -0
  48. package/dist/kit/runtime.d.ts.map +1 -0
  49. package/dist/kit/runtime.js +194 -0
  50. package/dist/kit/store.d.ts +20 -0
  51. package/dist/kit/store.d.ts.map +1 -0
  52. package/dist/kit/store.js +25 -0
  53. package/dist/kit/topology.d.ts +69 -0
  54. package/dist/kit/topology.d.ts.map +1 -0
  55. package/dist/kit/topology.js +156 -0
  56. package/dist/kit/wire.d.ts +45 -5
  57. package/dist/kit/wire.d.ts.map +1 -1
  58. package/dist/kit/wire.js +51 -5
  59. package/dist/live-coding/assets/browser-authoring-index.json +1 -1
  60. package/dist/live-coding/assets/manifest.json +1 -1
  61. package/dist/live-coding/browser-authoring-index.generated.d.ts.map +1 -1
  62. package/dist/live-coding/browser-authoring-index.generated.js +1935 -45
  63. package/package.json +1 -1
@@ -0,0 +1,295 @@
1
+ import { KIT_SNAPSHOT_METHOD, KitError, splitKitMessage, } from './protocol.js';
2
+ /**
3
+ * A hub topic mirrored on the client: it subscribes, asks the hub for a snapshot, then applies
4
+ * what arrives after the snapshot's `seq`. It asks again when the publishing instance's
5
+ * `epoch` changes, when a delta topic skips a `seq`, and after the connection reconnects.
6
+ */
7
+ export class TopicMirror {
8
+ constructor(runtime, nodeType, key, topic) {
9
+ this.runtime = runtime;
10
+ this.nodeType = nodeType;
11
+ this.key = key;
12
+ this.topic = topic;
13
+ this.epoch = -1;
14
+ this.seq = 0;
15
+ this.buffer = null;
16
+ this.syncing = null;
17
+ this.stopSub = null;
18
+ this.offReconnect = null;
19
+ this.listeners = new Set();
20
+ this.eventListeners = new Set();
21
+ this.changedListeners = new Set();
22
+ this.watching = null;
23
+ }
24
+ /** Subscribes and takes the first snapshot; resolves with the state. Idempotent. */
25
+ start() {
26
+ if (!this.watching) {
27
+ this.watching = (async () => {
28
+ this.offReconnect = this.runtime.onReconnect(() => void this.resync().catch(() => undefined));
29
+ this.stopSub = await this.runtime.subscribe(this.nodeType, this.key, this.topic, (p) => this.onPush(p));
30
+ return this.resync();
31
+ })().catch((e) => {
32
+ this.watching = null;
33
+ throw e;
34
+ });
35
+ }
36
+ return this.watching;
37
+ }
38
+ /** Stops the subscription; the last state stays readable. */
39
+ async stop() {
40
+ this.offReconnect?.();
41
+ this.offReconnect = null;
42
+ const stop = this.stopSub;
43
+ this.stopSub = null;
44
+ this.watching = null;
45
+ await stop?.();
46
+ }
47
+ /** Takes a fresh snapshot (shared by concurrent callers). */
48
+ resync() {
49
+ if (!this.syncing) {
50
+ this.buffer = [];
51
+ this.syncing = (async () => {
52
+ try {
53
+ const reply = await this.runtime.call(this.nodeType, this.key, KIT_SNAPSHOT_METHOD, { topic: this.topic });
54
+ const msg = splitKitMessage(reply);
55
+ if (!msg)
56
+ throw new KitError('bad_request', `${this.nodeType} answered snapshot with something other than a kit message`);
57
+ this.epoch = msg.header.epoch;
58
+ this.seq = msg.header.seq;
59
+ this.state = this.fromSnapshot(msg.body);
60
+ const pending = this.buffer ?? [];
61
+ this.buffer = null;
62
+ this.syncing = null;
63
+ for (const m of pending.sort((a, b) => a.header.seq - b.header.seq))
64
+ this.apply(m);
65
+ this.emit();
66
+ return this.state;
67
+ }
68
+ catch (e) {
69
+ this.buffer = null;
70
+ this.syncing = null;
71
+ throw e;
72
+ }
73
+ })();
74
+ }
75
+ return this.syncing;
76
+ }
77
+ get() {
78
+ return this.state;
79
+ }
80
+ subscribe(listener) {
81
+ this.listeners.add(listener);
82
+ return () => this.listeners.delete(listener);
83
+ }
84
+ /** `event` messages on the topic. */
85
+ onEvent(listener) {
86
+ this.eventListeners.add(listener);
87
+ return () => this.eventListeners.delete(listener);
88
+ }
89
+ /** `changed` messages on the topic: someone's private state to read again. */
90
+ onChanged(listener) {
91
+ this.changedListeners.add(listener);
92
+ return () => this.changedListeners.delete(listener);
93
+ }
94
+ /** The epoch and seq the mirror is at; `-1` before its first snapshot. */
95
+ get position() {
96
+ return { epoch: this.epoch, seq: this.seq };
97
+ }
98
+ emit() {
99
+ for (const l of [...this.listeners]) {
100
+ try {
101
+ l(this.state);
102
+ }
103
+ catch {
104
+ /* a listener's exception is its own */
105
+ }
106
+ }
107
+ }
108
+ onPush(push) {
109
+ const msg = splitKitMessage(push.value);
110
+ if (!msg)
111
+ return;
112
+ if (this.buffer) {
113
+ this.buffer.push(msg);
114
+ return;
115
+ }
116
+ this.apply(msg);
117
+ }
118
+ apply({ header, body }) {
119
+ if (header.epoch !== this.epoch) {
120
+ if (header.kind === 'snapshot') {
121
+ this.epoch = header.epoch;
122
+ this.seq = header.seq;
123
+ this.state = this.fromSnapshot(body);
124
+ this.emit();
125
+ }
126
+ else {
127
+ void this.resync().catch(() => undefined);
128
+ }
129
+ return;
130
+ }
131
+ if (header.seq <= this.seq)
132
+ return;
133
+ const gap = header.seq !== this.seq + 1;
134
+ this.seq = header.seq;
135
+ switch (header.kind) {
136
+ case 'snapshot':
137
+ this.state = this.fromSnapshot(body);
138
+ this.emit();
139
+ return;
140
+ case 'delta': {
141
+ const next = gap || this.state === undefined ? null : this.applyDelta(this.state, body);
142
+ if (next === null) {
143
+ void this.resync().catch(() => undefined);
144
+ return;
145
+ }
146
+ this.state = next;
147
+ this.emit();
148
+ return;
149
+ }
150
+ case 'changed':
151
+ for (const l of [...this.changedListeners])
152
+ l(body, header);
153
+ break;
154
+ case 'event':
155
+ for (const l of [...this.eventListeners])
156
+ l(body, header);
157
+ break;
158
+ }
159
+ if (gap && this.gapsLoseState)
160
+ void this.resync().catch(() => undefined);
161
+ }
162
+ }
163
+ /** A topic whose messages are its hub's whole public state (`snapshot` messages). */
164
+ export class HubState extends TopicMirror {
165
+ constructor() {
166
+ super(...arguments);
167
+ this.gapsLoseState = false;
168
+ }
169
+ fromSnapshot(body) {
170
+ return body;
171
+ }
172
+ applyDelta() {
173
+ return null;
174
+ }
175
+ }
176
+ /**
177
+ * A topic carrying a collection (`delta` per member; the snapshot's `containers`): the
178
+ * starters' model topics, such as an area's mobs, NPCs and pets.
179
+ */
180
+ export class CollectionMirror extends TopicMirror {
181
+ constructor() {
182
+ super(...arguments);
183
+ this.gapsLoseState = true;
184
+ }
185
+ fromSnapshot(body) {
186
+ const out = new Map();
187
+ for (const c of body.containers ?? []) {
188
+ if (!c.removed)
189
+ out.set(c.id, { id: c.id, type_name: c.type_name ?? '', props: c.props ?? {} });
190
+ }
191
+ return out;
192
+ }
193
+ applyDelta(state, d) {
194
+ if (typeof d.id !== 'number')
195
+ return null;
196
+ const next = new Map(state);
197
+ if (d.removed) {
198
+ next.delete(d.id);
199
+ }
200
+ else {
201
+ next.set(d.id, { id: d.id, type_name: d.type_name ?? state.get(d.id)?.type_name ?? '', props: d.props ?? {} });
202
+ }
203
+ return next;
204
+ }
205
+ /** The members, oldest first, of one type or all. */
206
+ list(typeName) {
207
+ const all = [...(this.get()?.values() ?? [])].sort((a, b) => a.id - b.id);
208
+ return typeName ? all.filter((c) => c.type_name === typeName) : all;
209
+ }
210
+ /** One member by id. */
211
+ member(id) {
212
+ return this.get()?.get(id);
213
+ }
214
+ }
215
+ /**
216
+ * State only its player may read (a hand of cards, an inventory, a pet's bond): read with a
217
+ * caller-scoped call, read again when the hub's `changed` message names this player, and after
218
+ * a reconnect. Nothing private is ever on a topic.
219
+ */
220
+ export class PrivateState {
221
+ constructor(runtime, source) {
222
+ this.runtime = runtime;
223
+ this.source = source;
224
+ this.reading = null;
225
+ this.again = false;
226
+ this.stopSub = null;
227
+ this.offReconnect = null;
228
+ this.listeners = new Set();
229
+ this.watching = null;
230
+ this.whats = new Set(typeof source.what === 'string' ? [source.what] : source.what);
231
+ }
232
+ /** Subscribes and reads the first value. Idempotent. */
233
+ start() {
234
+ if (!this.watching) {
235
+ this.watching = (async () => {
236
+ this.offReconnect = this.runtime.onReconnect(() => void this.refresh().catch(() => undefined));
237
+ this.stopSub = await this.runtime.subscribe(this.source.nodeType, this.source.key, this.source.topic, (p) => this.onPush(p));
238
+ return this.refresh();
239
+ })().catch((e) => {
240
+ this.watching = null;
241
+ throw e;
242
+ });
243
+ }
244
+ return this.watching;
245
+ }
246
+ async stop() {
247
+ this.offReconnect?.();
248
+ this.offReconnect = null;
249
+ const stop = this.stopSub;
250
+ this.stopSub = null;
251
+ this.watching = null;
252
+ await stop?.();
253
+ }
254
+ /** Reads the value again (one read at a time; a request during one runs after it). */
255
+ refresh() {
256
+ if (this.reading) {
257
+ this.again = true;
258
+ return this.reading;
259
+ }
260
+ this.reading = (async () => {
261
+ try {
262
+ let v;
263
+ do {
264
+ this.again = false;
265
+ v = await this.source.read();
266
+ } while (this.again);
267
+ this.value = v;
268
+ for (const l of [...this.listeners])
269
+ l(v);
270
+ return v;
271
+ }
272
+ finally {
273
+ this.reading = null;
274
+ }
275
+ })();
276
+ return this.reading;
277
+ }
278
+ get() {
279
+ return this.value;
280
+ }
281
+ subscribe(listener) {
282
+ this.listeners.add(listener);
283
+ return () => this.listeners.delete(listener);
284
+ }
285
+ onPush(push) {
286
+ const msg = splitKitMessage(push.value);
287
+ if (!msg || msg.header.kind !== 'changed' || !this.whats.has(msg.body.what))
288
+ return;
289
+ const who = Array.isArray(msg.body.who) ? msg.body.who.map(String) : [];
290
+ void this.runtime.playerId().then((me) => {
291
+ if (me === null || who.includes(me))
292
+ void this.refresh().catch(() => undefined);
293
+ });
294
+ }
295
+ }
@@ -0,0 +1,107 @@
1
+ import { HubState } from '../mirror.js';
2
+ import type { KitRuntime } from '../runtime.js';
3
+ import { type KitStore } from '../store.js';
4
+ /** A player's place in a queue, as the matchmaker's `status` (and every change) answers it. */
5
+ export type MatchPlace = {
6
+ state: 'idle';
7
+ } | {
8
+ state: 'party';
9
+ leader: number;
10
+ members: number[];
11
+ confirmed: number[];
12
+ expires_in_ms: number;
13
+ } | {
14
+ state: 'queued';
15
+ rating: number;
16
+ waited_ms: number;
17
+ window: number;
18
+ } | {
19
+ state: 'proposed';
20
+ players: number[];
21
+ accepted: number[];
22
+ expires_in_ms: number;
23
+ } | {
24
+ state: 'matched';
25
+ session: string;
26
+ };
27
+ /** What the `queue` topic carries: how many, never who. */
28
+ export interface MatchQueueCounts {
29
+ waiting: number;
30
+ proposals: number;
31
+ forming: number;
32
+ }
33
+ /** A queue's settings (developers). */
34
+ export interface MatchmakerConfig {
35
+ players_per_match?: number;
36
+ /** The rating difference a player accepts at first, and how it widens each second. */
37
+ window?: number;
38
+ widen_per_sec?: number;
39
+ max_window?: number;
40
+ accept_ms?: number;
41
+ /** How long a party has for its members to confirm. */
42
+ party_ms?: number;
43
+ }
44
+ /** Joining a queue: alone, as a party's leader (`party`: the others), or confirming (`leader`). */
45
+ export interface MatchJoin {
46
+ rating: number;
47
+ party?: ReadonlyArray<number | string>;
48
+ leader?: number | string;
49
+ }
50
+ /** Where the matchmaker hubs are: type `matchmaker`, one per queue name, in the starter pack. */
51
+ export interface MatchmakingKitOptions {
52
+ nodeType?: string;
53
+ }
54
+ /** Matchmaking queues (the `matchmaker` starter): {@link queue} by name. */
55
+ export declare class MatchmakingKit {
56
+ private readonly runtime;
57
+ private readonly queues;
58
+ private readonly nodeType;
59
+ constructor(runtime: KitRuntime, options?: MatchmakingKitOptions);
60
+ /** The queue named `name` (`ranked`, `casual`, ...), one per name. */
61
+ queue(name: string): MatchQueue;
62
+ }
63
+ /**
64
+ * One queue. Every call's answer updates {@link place}; {@link watch} keeps it current while
65
+ * the player waits (the queue never names its players on its topic, so a waiting client asks)
66
+ * and fires {@link onMatched} with the session key once a match is made.
67
+ */
68
+ export declare class MatchQueue {
69
+ private readonly runtime;
70
+ readonly nodeType: string;
71
+ readonly name: string;
72
+ /** The `queue` topic: how many are waiting. */
73
+ readonly counts: HubState<MatchQueueCounts>;
74
+ private readonly placeStore;
75
+ private readonly matchedListeners;
76
+ private lastMatched;
77
+ private timer;
78
+ constructor(runtime: KitRuntime, nodeType: string, name: string);
79
+ /** The player's place, as last answered. */
80
+ get place(): KitStore<MatchPlace>;
81
+ private settle;
82
+ /**
83
+ * Joins the queue (or updates the rating). With `party`, the caller leads a party of them;
84
+ * each member then joins with `leader`, and the party queues once all have.
85
+ * @throws {KitError} `conflict` (a match to accept), `limit` (party size), `forbidden`, `full`.
86
+ */
87
+ join(req: MatchJoin): Promise<MatchPlace>;
88
+ /** Leaves the queue, a forming party, or a proposed match (the party leaves with you). */
89
+ leave(): Promise<boolean>;
90
+ /** Accepts the proposed match; the answer is `matched` once everyone has. */
91
+ accept(): Promise<MatchPlace>;
92
+ /** Declines the proposed match; the others go back to the queue. */
93
+ decline(): Promise<boolean>;
94
+ /** Asks for the player's place. */
95
+ status(): Promise<MatchPlace>;
96
+ /** Sets the queue's config. Developers only (a developer kit). */
97
+ configure(config: MatchmakerConfig): Promise<boolean>;
98
+ /**
99
+ * Asks for the player's place every `intervalMs` (default 2 000) while they wait in a party,
100
+ * the queue or a proposal. Returns the function that stops it.
101
+ */
102
+ watch(intervalMs?: number): () => void;
103
+ unwatch(): void;
104
+ /** Called with the session key when a match is made for the player. */
105
+ onMatched(listener: (session: string) => void): () => void;
106
+ }
107
+ //# sourceMappingURL=matchmaking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchmaking.d.ts","sourceRoot":"","sources":["../../../src/kit/modules/matchmaking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE9D,+FAA+F;AAC/F,MAAM,MAAM,UAAU,GAClB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjB;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACjG;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACnF;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,2DAA2D;AAC3D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED,iGAAiG;AACjG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,4EAA4E;AAC5E,qBAAa,cAAc;IAKvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAGf,OAAO,EAAE,UAAU,EACpC,OAAO,GAAE,qBAA0B;IAKrC,sEAAsE;IACtE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU;CAQhC;AAID;;;;GAIG;AACH,qBAAa,UAAU;IASnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAVvB,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuD;IAClF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAwC;IACzE,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,KAAK,CAA+C;gBAGzC,OAAO,EAAE,UAAU,EAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM;IAKvB,4CAA4C;IAC5C,IAAI,KAAK,IAAI,QAAQ,CAAC,UAAU,CAAC,CAEhC;IAED,OAAO,CAAC,MAAM;IASd;;;;OAIG;IACG,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAO/C,0FAA0F;IACpF,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAM/B,6EAA6E;IACvE,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC;IAInC,oEAAoE;IAC9D,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAMjC,mCAAmC;IAC7B,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC;IAInC,kEAAkE;IAClE,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrD;;;OAGG;IACH,KAAK,CAAC,UAAU,SAAQ,GAAG,MAAM,IAAI;IAQrC,OAAO,IAAI,IAAI;IAKf,uEAAuE;IACvE,SAAS,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;CAI3D"}
@@ -0,0 +1,109 @@
1
+ import { HubState } from '../mirror.js';
2
+ import { WritableKitStore } from '../store.js';
3
+ /** Matchmaking queues (the `matchmaker` starter): {@link queue} by name. */
4
+ export class MatchmakingKit {
5
+ constructor(runtime, options = {}) {
6
+ this.runtime = runtime;
7
+ this.queues = new Map();
8
+ this.nodeType = options.nodeType ?? 'matchmaker';
9
+ }
10
+ /** The queue named `name` (`ranked`, `casual`, ...), one per name. */
11
+ queue(name) {
12
+ let q = this.queues.get(name);
13
+ if (!q) {
14
+ q = new MatchQueue(this.runtime, this.nodeType, name);
15
+ this.queues.set(name, q);
16
+ }
17
+ return q;
18
+ }
19
+ }
20
+ const ACTIVE = new Set(['party', 'queued', 'proposed']);
21
+ /**
22
+ * One queue. Every call's answer updates {@link place}; {@link watch} keeps it current while
23
+ * the player waits (the queue never names its players on its topic, so a waiting client asks)
24
+ * and fires {@link onMatched} with the session key once a match is made.
25
+ */
26
+ export class MatchQueue {
27
+ constructor(runtime, nodeType, name) {
28
+ this.runtime = runtime;
29
+ this.nodeType = nodeType;
30
+ this.name = name;
31
+ this.placeStore = new WritableKitStore({ state: 'idle' });
32
+ this.matchedListeners = new Set();
33
+ this.lastMatched = null;
34
+ this.timer = null;
35
+ this.counts = new HubState(runtime, nodeType, name, 'queue');
36
+ }
37
+ /** The player's place, as last answered. */
38
+ get place() {
39
+ return this.placeStore;
40
+ }
41
+ settle(p) {
42
+ this.placeStore.set(p);
43
+ if (p.state === 'matched' && p.session !== this.lastMatched) {
44
+ this.lastMatched = p.session;
45
+ for (const l of [...this.matchedListeners])
46
+ l(p.session);
47
+ }
48
+ return p;
49
+ }
50
+ /**
51
+ * Joins the queue (or updates the rating). With `party`, the caller leads a party of them;
52
+ * each member then joins with `leader`, and the party queues once all have.
53
+ * @throws {KitError} `conflict` (a match to accept), `limit` (party size), `forbidden`, `full`.
54
+ */
55
+ async join(req) {
56
+ const args = { rating: req.rating };
57
+ if (req.party?.length)
58
+ args.party = req.party.map(Number);
59
+ if (req.leader !== undefined)
60
+ args.leader = Number(req.leader);
61
+ return this.settle(await this.runtime.call(this.nodeType, this.name, 'join', args, { action: true }));
62
+ }
63
+ /** Leaves the queue, a forming party, or a proposed match (the party leaves with you). */
64
+ async leave() {
65
+ const left = await this.runtime.call(this.nodeType, this.name, 'leave', null, { action: true });
66
+ this.settle({ state: 'idle' });
67
+ return left;
68
+ }
69
+ /** Accepts the proposed match; the answer is `matched` once everyone has. */
70
+ async accept() {
71
+ return this.settle(await this.runtime.call(this.nodeType, this.name, 'accept', null, { action: true }));
72
+ }
73
+ /** Declines the proposed match; the others go back to the queue. */
74
+ async decline() {
75
+ const done = await this.runtime.call(this.nodeType, this.name, 'decline', null, { action: true });
76
+ this.settle({ state: 'idle' });
77
+ return done;
78
+ }
79
+ /** Asks for the player's place. */
80
+ async status() {
81
+ return this.settle(await this.runtime.call(this.nodeType, this.name, 'status'));
82
+ }
83
+ /** Sets the queue's config. Developers only (a developer kit). */
84
+ configure(config) {
85
+ return this.runtime.call(this.nodeType, this.name, 'configure', config, { action: true });
86
+ }
87
+ /**
88
+ * Asks for the player's place every `intervalMs` (default 2 000) while they wait in a party,
89
+ * the queue or a proposal. Returns the function that stops it.
90
+ */
91
+ watch(intervalMs = 2000) {
92
+ this.unwatch();
93
+ this.timer = setInterval(() => {
94
+ if (ACTIVE.has(this.placeStore.get().state))
95
+ void this.status().catch(() => undefined);
96
+ }, Math.max(500, intervalMs));
97
+ return () => this.unwatch();
98
+ }
99
+ unwatch() {
100
+ if (this.timer)
101
+ clearInterval(this.timer);
102
+ this.timer = null;
103
+ }
104
+ /** Called with the session key when a match is made for the player. */
105
+ onMatched(listener) {
106
+ this.matchedListeners.add(listener);
107
+ return () => this.matchedListeners.delete(listener);
108
+ }
109
+ }
@@ -0,0 +1,80 @@
1
+ import { CollectionMirror } from '../mirror.js';
2
+ import { type KitContainer } from '../protocol.js';
3
+ import type { KitRuntime } from '../runtime.js';
4
+ /** A pet as its owner reads it: the public view and its bond. */
5
+ export interface KitPetView {
6
+ id: number;
7
+ species: string;
8
+ name: string;
9
+ x: number;
10
+ z: number;
11
+ /** `follow` while summoned, else `stay`. */
12
+ mode: string;
13
+ bond: number;
14
+ }
15
+ /** Where the area hubs are: type `npcs`, keyed by area centre chunk, in the starter pack. */
16
+ export interface NpcsKitOptions {
17
+ nodeType?: string;
18
+ /** An area's size in chunks, for {@link NpcsKit.areaAt}. Default 8. */
19
+ areaSize?: number;
20
+ }
21
+ /**
22
+ * The world's areas of mobs, NPCs and pets (the `npc-mobs` starter): one hub per area,
23
+ * keyed by its centre chunk, so a client finds the hub for where it stands
24
+ * ({@link areaAt}) without asking.
25
+ */
26
+ export declare class NpcsKit {
27
+ private readonly runtime;
28
+ private readonly areas;
29
+ private readonly nodeType;
30
+ private readonly size;
31
+ constructor(runtime: KitRuntime, options?: NpcsKitOptions);
32
+ /** The key of the area holding chunk `(chunkX, chunkZ)`. */
33
+ keyAt(chunkX: number, chunkZ: number): string;
34
+ /** The area holding chunk `(chunkX, chunkZ)`. */
35
+ areaAt(chunkX: number, chunkZ: number): NpcArea;
36
+ /** The area with key `key` (`"<x>,<z>"`, its centre chunk). */
37
+ area(key: string): NpcArea;
38
+ }
39
+ /**
40
+ * One area: its mobs, NPCs and pets mirrored from the `area` topic ({@link members}), hits
41
+ * on mobs (the hub decides reach, cooldown and damage), NPC talk, and the player's pets.
42
+ * The hub also shows its actors through the realtime tier (`kitActorCodec`).
43
+ */
44
+ export declare class NpcArea {
45
+ private readonly runtime;
46
+ readonly nodeType: string;
47
+ readonly key: string;
48
+ /** The `area` topic: every member's public properties, by id. */
49
+ readonly members: CollectionMirror;
50
+ constructor(runtime: KitRuntime, nodeType: string, key: string);
51
+ /** Mirrors the area. */
52
+ watch(): Promise<void>;
53
+ unwatch(): Promise<void>;
54
+ mobs(): KitContainer[];
55
+ npcs(): KitContainer[];
56
+ pets(): KitContainer[];
57
+ /**
58
+ * Hits mob `mob`; resolves with its hp after the hit.
59
+ * @throws {KitError} `not_in_area`, `out_of_reach`, `cooldown`, `down`, `not_found`.
60
+ */
61
+ attack(mob: number): Promise<number>;
62
+ /** What NPC `npc` says, with its public properties. */
63
+ talk(npc: number): Promise<Record<string, unknown>>;
64
+ /**
65
+ * Adopts a pet where the player stands; resolves with its id.
66
+ * @throws {KitError} `not_in_area`, `limit` (pets a player), `full`, `bad_request`.
67
+ */
68
+ adopt(species: string, name: string): Promise<number>;
69
+ rename(pet: number, name: string): Promise<boolean>;
70
+ /** The pet follows its owner (and its bond grows). */
71
+ summon(pet: number): Promise<boolean>;
72
+ /** The pet stays where it is. */
73
+ dismiss(pet: number): Promise<boolean>;
74
+ /** The pet is gone. */
75
+ release(pet: number): Promise<boolean>;
76
+ /** The player's pets in this area, with their bond. */
77
+ myPets(): Promise<KitPetView[]>;
78
+ private pet;
79
+ }
80
+ //# sourceMappingURL=npcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npcs.d.ts","sourceRoot":"","sources":["../../../src/kit/modules/npcs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,iEAAiE;AACjE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,OAAO;IAMhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAGX,OAAO,EAAE,UAAU,EACpC,OAAO,GAAE,cAAmB;IAM9B,4DAA4D;IAC5D,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAI7C,iDAAiD;IACjD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAI/C,+DAA+D;IAC/D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAQ3B;AAED;;;;GAIG;AACH,qBAAa,OAAO;IAKhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM;IANtB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAGhB,OAAO,EAAE,UAAU,EAC3B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM;IAKtB,wBAAwB;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxB,IAAI,IAAI,YAAY,EAAE;IAItB,IAAI,IAAI,YAAY,EAAE;IAItB,IAAI,IAAI,YAAY,EAAE;IAItB;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpC,uDAAuD;IACvD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAInD;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAInD,sDAAsD;IACtD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrC,iCAAiC;IACjC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItC,uBAAuB;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItC,uDAAuD;IACvD,MAAM,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAI/B,OAAO,CAAC,GAAG;CAIZ"}