@bct-app/game-engine 0.1.1 → 0.1.4
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/dist/index.d.mts +24 -92
- package/dist/index.d.ts +24 -92
- package/dist/index.js +495 -165
- package/dist/index.mjs +495 -165
- package/package.json +20 -8
package/dist/index.js
CHANGED
|
@@ -35,6 +35,25 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
|
+
// src/effects/handler-types.ts
|
|
39
|
+
var createEffectHandler = (type, handler) => {
|
|
40
|
+
return (context) => {
|
|
41
|
+
if (context.effect.type !== type) {
|
|
42
|
+
console.warn("Invalid effect type for handler:", context.effect.type, "expected:", type);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
handler({
|
|
46
|
+
...context,
|
|
47
|
+
effect: context.effect
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/effects/handlers/ability-change.ts
|
|
53
|
+
var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", () => {
|
|
54
|
+
console.warn("ABILITY_CHANGE effect handling not implemented yet.");
|
|
55
|
+
});
|
|
56
|
+
|
|
38
57
|
// src/guards.ts
|
|
39
58
|
var isNumberOrNumberArray = (value) => {
|
|
40
59
|
if (typeof value === "number") {
|
|
@@ -75,6 +94,7 @@ var isPlayerReminderArray = (value) => {
|
|
|
75
94
|
};
|
|
76
95
|
|
|
77
96
|
// src/player-utils.ts
|
|
97
|
+
var DEFAULT_ARRAY = [];
|
|
78
98
|
var transformEmptyArray = (arr) => {
|
|
79
99
|
return arr.length === 0 ? [] : arr;
|
|
80
100
|
};
|
|
@@ -94,7 +114,7 @@ var adjustValueAsNumberArray = (value) => {
|
|
|
94
114
|
if (isNumberOrNumberArray(value)) {
|
|
95
115
|
return typeof value === "number" ? [value] : value;
|
|
96
116
|
}
|
|
97
|
-
return
|
|
117
|
+
return DEFAULT_ARRAY;
|
|
98
118
|
};
|
|
99
119
|
var adjustValueAsNumber = (value) => {
|
|
100
120
|
if (isNumberOrNumberArray(value)) {
|
|
@@ -120,33 +140,39 @@ var getValueFromPayloads = (payloads, idx) => {
|
|
|
120
140
|
return payloads[idx];
|
|
121
141
|
};
|
|
122
142
|
var getSourceValue = (source, payloads) => {
|
|
123
|
-
if (source
|
|
143
|
+
if (source?.from === "PAYLOAD") {
|
|
124
144
|
if (!isNumberOrNumberArray(source.value)) {
|
|
125
145
|
return void 0;
|
|
126
146
|
}
|
|
127
147
|
return getValueFromPayloads(payloads, source.value);
|
|
128
148
|
}
|
|
129
|
-
if (source
|
|
149
|
+
if (source?.from === "CONSTANT") {
|
|
130
150
|
return source.value;
|
|
131
151
|
}
|
|
132
152
|
return null;
|
|
133
153
|
};
|
|
134
|
-
var resolveSourceValue = (source,
|
|
135
|
-
|
|
154
|
+
var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSeatMap, resolveFromPlayer, isResolvedValue) => {
|
|
155
|
+
const castOrValidate = (value) => {
|
|
156
|
+
if (!isResolvedValue) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
return isResolvedValue(value) ? value : void 0;
|
|
160
|
+
};
|
|
161
|
+
if (source?.from === "PAYLOAD") {
|
|
136
162
|
const payloadValue = getSourceValue(source, payloads);
|
|
137
163
|
const idx = adjustValueAsNumberArray(source.value);
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
164
|
+
const input = writableInputs[idx[0]];
|
|
165
|
+
if (input?.type === "PLAYER") {
|
|
140
166
|
const seat = adjustValueAsNumber(payloadValue);
|
|
141
167
|
const player = typeof seat === "number" ? snapshotSeatMap.get(seat) : void 0;
|
|
142
168
|
return player ? resolveFromPlayer(player) : void 0;
|
|
143
169
|
}
|
|
144
|
-
return payloadValue;
|
|
170
|
+
return castOrValidate(payloadValue);
|
|
145
171
|
}
|
|
146
|
-
if (source
|
|
147
|
-
return source.value;
|
|
172
|
+
if (source?.from === "CONSTANT") {
|
|
173
|
+
return castOrValidate(source.value);
|
|
148
174
|
}
|
|
149
|
-
if (source
|
|
175
|
+
if (source?.from === "EFFECTOR") {
|
|
150
176
|
if (!effector) {
|
|
151
177
|
return void 0;
|
|
152
178
|
}
|
|
@@ -156,7 +182,423 @@ var resolveSourceValue = (source, writableParts, payloads, effector, snapshotSea
|
|
|
156
182
|
return void 0;
|
|
157
183
|
};
|
|
158
184
|
|
|
185
|
+
// src/effects/handlers/alignment-change.ts
|
|
186
|
+
var applyAlignmentChange = createEffectHandler("ALIGNMENT_CHANGE", ({
|
|
187
|
+
effect,
|
|
188
|
+
payloads,
|
|
189
|
+
effector,
|
|
190
|
+
writableInputs,
|
|
191
|
+
getSnapshotSeatMap,
|
|
192
|
+
makePlayersEffect
|
|
193
|
+
}) => {
|
|
194
|
+
const alignment = resolveSourceValue(
|
|
195
|
+
effect.source,
|
|
196
|
+
writableInputs,
|
|
197
|
+
payloads,
|
|
198
|
+
effector,
|
|
199
|
+
getSnapshotSeatMap(),
|
|
200
|
+
(player) => player.alignment,
|
|
201
|
+
(isResolvedValue) => typeof isResolvedValue === "string" && (isResolvedValue === "GOOD" || isResolvedValue === "EVIL")
|
|
202
|
+
);
|
|
203
|
+
if (!alignment) {
|
|
204
|
+
console.warn("Alignment not found for ALIGNMENT_CHANGE effect:", effect.source);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
makePlayersEffect.forEach((player) => {
|
|
208
|
+
player.alignment = alignment;
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/effects/handlers/character-change.ts
|
|
213
|
+
var resolveCharacterId = (value) => {
|
|
214
|
+
if (typeof value === "string") {
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(value) && typeof value[0] === "string") {
|
|
218
|
+
return value[0];
|
|
219
|
+
}
|
|
220
|
+
return void 0;
|
|
221
|
+
};
|
|
222
|
+
var applyCharacterChange = createEffectHandler("CHARACTER_CHANGE", ({
|
|
223
|
+
effect,
|
|
224
|
+
payloads,
|
|
225
|
+
effector,
|
|
226
|
+
writableInputs,
|
|
227
|
+
getSnapshotSeatMap,
|
|
228
|
+
characterMap,
|
|
229
|
+
makePlayersEffect
|
|
230
|
+
}) => {
|
|
231
|
+
const resolvedCharacterId = resolveSourceValue(
|
|
232
|
+
effect.source,
|
|
233
|
+
writableInputs,
|
|
234
|
+
payloads,
|
|
235
|
+
effector,
|
|
236
|
+
getSnapshotSeatMap(),
|
|
237
|
+
(player) => player.characterId
|
|
238
|
+
);
|
|
239
|
+
const characterId = resolveCharacterId(resolvedCharacterId);
|
|
240
|
+
if (!characterId) {
|
|
241
|
+
console.warn("Character ID not found for CHARACTER_CHANGE effect:", effect.source);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const newCharacter = characterMap.get(characterId);
|
|
245
|
+
if (!newCharacter) {
|
|
246
|
+
console.warn("Character not found for CHARACTER_CHANGE effect:", characterId);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const abilityIds = newCharacter.abilities.map((ability) => ability.id);
|
|
250
|
+
makePlayersEffect.forEach((player) => {
|
|
251
|
+
player.characterId = newCharacter.id;
|
|
252
|
+
player.abilities = abilityIds;
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// src/effects/handlers/character-count-change.ts
|
|
257
|
+
var applyCharacterCountChange = createEffectHandler("CHARACTER_COUNT_CHANGE", () => {
|
|
258
|
+
console.warn("CHARACTER_COUNT_CHANGE effect handling not implemented yet.");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/effects/handlers/game-change.ts
|
|
262
|
+
var applyGameChange = createEffectHandler("GAME_CHANGE", () => {
|
|
263
|
+
console.warn("GAME_CHANGE effect handling not implemented yet.");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// src/effects/handlers/perceived-character-change.ts
|
|
267
|
+
var resolveCharacterId2 = (value) => {
|
|
268
|
+
if (typeof value === "string") {
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(value) && typeof value[0] === "string") {
|
|
272
|
+
return value[0];
|
|
273
|
+
}
|
|
274
|
+
return void 0;
|
|
275
|
+
};
|
|
276
|
+
var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHANGE", ({
|
|
277
|
+
effect,
|
|
278
|
+
payloads,
|
|
279
|
+
effector,
|
|
280
|
+
writableInputs,
|
|
281
|
+
getSnapshotSeatMap,
|
|
282
|
+
characterMap,
|
|
283
|
+
makePlayersEffect
|
|
284
|
+
}) => {
|
|
285
|
+
const resolvedCharacterId = resolveSourceValue(
|
|
286
|
+
effect.source,
|
|
287
|
+
writableInputs,
|
|
288
|
+
payloads,
|
|
289
|
+
effector,
|
|
290
|
+
getSnapshotSeatMap(),
|
|
291
|
+
(player) => player.characterId
|
|
292
|
+
);
|
|
293
|
+
const characterId = resolveCharacterId2(resolvedCharacterId);
|
|
294
|
+
if (!characterId) {
|
|
295
|
+
console.warn("Character ID not found for PERCEIVED_CHARACTER_CHANGE effect:", effect.source);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const newCharacter = characterMap.get(characterId);
|
|
299
|
+
if (!newCharacter) {
|
|
300
|
+
console.warn("Character not found for PERCEIVED_CHARACTER_CHANGE effect:", characterId);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
makePlayersEffect.forEach((player) => {
|
|
304
|
+
player.perceivedCharacter = {
|
|
305
|
+
characterId: newCharacter.id,
|
|
306
|
+
abilities: newCharacter.abilities.map((ability) => ability.id),
|
|
307
|
+
asCharacter: effect.followPriority || false
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// src/effects/handlers/reminder-add.ts
|
|
313
|
+
var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
|
|
314
|
+
effect,
|
|
315
|
+
payloads,
|
|
316
|
+
nowTimelineIndex,
|
|
317
|
+
operationInTimelineIdx,
|
|
318
|
+
makePlayersEffect
|
|
319
|
+
}) => {
|
|
320
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
321
|
+
if (!isReminder(maybeReminder)) {
|
|
322
|
+
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
326
|
+
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
makePlayersEffect.forEach((player) => {
|
|
330
|
+
if (!player.reminders) {
|
|
331
|
+
player.reminders = [];
|
|
332
|
+
}
|
|
333
|
+
player.reminders.push(maybeReminder);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// src/effects/handlers/reminder-remove.ts
|
|
338
|
+
var applyReminderRemove = createEffectHandler("REMINDER_REMOVE", ({ effect, payloads, playerSeatMap }) => {
|
|
339
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
340
|
+
if (!isPlayerReminderArray(maybeReminder)) {
|
|
341
|
+
console.warn("Invalid reminder data for REMINDER_REMOVE effect:", maybeReminder);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
maybeReminder.sort((left, right) => right.index - left.index).forEach((reminder) => {
|
|
345
|
+
const player = playerSeatMap.get(reminder.playerSeat);
|
|
346
|
+
if (!player) {
|
|
347
|
+
console.warn("Player not found for REMINDER_REMOVE effect:", reminder.playerSeat);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (!player.reminders) {
|
|
351
|
+
player.reminders = [];
|
|
352
|
+
}
|
|
353
|
+
player.reminders = player.reminders.filter((_value, index) => index !== reminder.index);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// src/effects/handlers/seat-change.ts
|
|
358
|
+
var applySeatChange = createEffectHandler("SEAT_CHANGE", () => {
|
|
359
|
+
console.warn("SEAT_CHANGE effect handling not implemented yet.");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// src/effects/handlers/status-change.ts
|
|
363
|
+
var applyStatusChange = createEffectHandler("STATUS_CHANGE", ({ effect, payloads, makePlayersEffect }) => {
|
|
364
|
+
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
365
|
+
if (maybeStatus === "DEAD") {
|
|
366
|
+
makePlayersEffect.forEach((player) => {
|
|
367
|
+
player.isDead = true;
|
|
368
|
+
});
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (maybeStatus === "ALIVE") {
|
|
372
|
+
makePlayersEffect.forEach((player) => {
|
|
373
|
+
player.isDead = false;
|
|
374
|
+
});
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.warn("Invalid status value for STATUS_CHANGE effect:", maybeStatus);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// src/effects/registry.ts
|
|
381
|
+
var effectHandlers = {
|
|
382
|
+
ABILITY_CHANGE: applyAbilityChange,
|
|
383
|
+
ALIGNMENT_CHANGE: applyAlignmentChange,
|
|
384
|
+
CHARACTER_CHANGE: applyCharacterChange,
|
|
385
|
+
CHARACTER_COUNT_CHANGE: applyCharacterCountChange,
|
|
386
|
+
GAME_CHANGE: applyGameChange,
|
|
387
|
+
PERCEIVED_CHARACTER_CHANGE: applyPerceivedCharacterChange,
|
|
388
|
+
REMINDER_ADD: applyReminderAdd,
|
|
389
|
+
REMINDER_REMOVE: applyReminderRemove,
|
|
390
|
+
SEAT_CHANGE: applySeatChange,
|
|
391
|
+
STATUS_CHANGE: applyStatusChange
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/effects/resolve-targets.ts
|
|
395
|
+
var isPayloadRef = (value) => {
|
|
396
|
+
if (!value || typeof value !== "object") {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
const candidate = value;
|
|
400
|
+
if (candidate.from !== "PAYLOAD") {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return typeof candidate.value === "number" || Array.isArray(candidate.value);
|
|
404
|
+
};
|
|
405
|
+
var resolveDynamicValue = (value, payloads, isExpected) => {
|
|
406
|
+
if (typeof value === "undefined") {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
if (!isPayloadRef(value)) {
|
|
410
|
+
return isExpected(value) ? value : void 0;
|
|
411
|
+
}
|
|
412
|
+
const resolved = getValueFromPayloads(payloads, value.value);
|
|
413
|
+
if (!isExpected(resolved)) {
|
|
414
|
+
return void 0;
|
|
415
|
+
}
|
|
416
|
+
return resolved;
|
|
417
|
+
};
|
|
418
|
+
var getSortedPlayers = (playerSeatMap) => {
|
|
419
|
+
return [...playerSeatMap.values()].sort((left, right) => left.seat - right.seat);
|
|
420
|
+
};
|
|
421
|
+
var getCircularDistance = (anchorIdx, targetIdx, total) => {
|
|
422
|
+
const clockwise = (targetIdx - anchorIdx + total) % total;
|
|
423
|
+
const anticlockwise = (anchorIdx - targetIdx + total) % total;
|
|
424
|
+
return Math.min(clockwise, anticlockwise);
|
|
425
|
+
};
|
|
426
|
+
var getAnchorSeat = (dynamicTarget, payloads, effector) => {
|
|
427
|
+
const anchor = dynamicTarget.anchor;
|
|
428
|
+
if (!anchor || anchor.from === "EFFECTOR") {
|
|
429
|
+
return effector?.seat;
|
|
430
|
+
}
|
|
431
|
+
if (anchor.from === "STATIC") {
|
|
432
|
+
return anchor.value;
|
|
433
|
+
}
|
|
434
|
+
const payloadSeat = getValueFromPayloads(payloads, anchor.value);
|
|
435
|
+
return typeof payloadSeat === "number" ? payloadSeat : void 0;
|
|
436
|
+
};
|
|
437
|
+
var resolveSelectorScope = (dynamicTarget, payloads) => {
|
|
438
|
+
return resolveDynamicValue(
|
|
439
|
+
dynamicTarget.selector?.scope,
|
|
440
|
+
payloads,
|
|
441
|
+
(value) => value === "BOTH_SIDES" || value === "LEFT_SIDE" || value === "RIGHT_SIDE"
|
|
442
|
+
) ?? "BOTH_SIDES";
|
|
443
|
+
};
|
|
444
|
+
var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payloads) => {
|
|
445
|
+
const selector = dynamicTarget.selector;
|
|
446
|
+
if (!selector) {
|
|
447
|
+
return sortedPlayers;
|
|
448
|
+
}
|
|
449
|
+
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
450
|
+
if (!anchorSeat) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
|
|
454
|
+
if (anchorIdx < 0) {
|
|
455
|
+
return [];
|
|
456
|
+
}
|
|
457
|
+
const total = sortedPlayers.length;
|
|
458
|
+
if (scope === "LEFT_SIDE") {
|
|
459
|
+
const left2 = [];
|
|
460
|
+
for (let step = 1; step < total; step += 1) {
|
|
461
|
+
left2.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
462
|
+
}
|
|
463
|
+
return left2;
|
|
464
|
+
}
|
|
465
|
+
if (scope === "RIGHT_SIDE") {
|
|
466
|
+
const right2 = [];
|
|
467
|
+
for (let step = 1; step < total; step += 1) {
|
|
468
|
+
right2.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
469
|
+
}
|
|
470
|
+
return right2;
|
|
471
|
+
}
|
|
472
|
+
const left = [];
|
|
473
|
+
const right = [];
|
|
474
|
+
for (let step = 1; step < total; step += 1) {
|
|
475
|
+
left.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
476
|
+
right.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
477
|
+
}
|
|
478
|
+
return [...left, ...right];
|
|
479
|
+
};
|
|
480
|
+
var matchesCondition = (player, condition, payloads, characterMap) => {
|
|
481
|
+
if (condition.field === "IS_DEAD") {
|
|
482
|
+
const expected = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "boolean");
|
|
483
|
+
if (typeof expected !== "boolean") {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return Boolean(player.isDead) === expected;
|
|
487
|
+
}
|
|
488
|
+
if (condition.field === "SEAT") {
|
|
489
|
+
if (condition.operator === "IN") {
|
|
490
|
+
const expectedSeats = resolveDynamicValue(condition.value, payloads, (value) => Array.isArray(value) && value.every((item) => typeof item === "number"));
|
|
491
|
+
return Array.isArray(expectedSeats) && expectedSeats.includes(player.seat);
|
|
492
|
+
}
|
|
493
|
+
const expectedSeat = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "number");
|
|
494
|
+
return typeof expectedSeat === "number" && player.seat === expectedSeat;
|
|
495
|
+
}
|
|
496
|
+
const character = characterMap.get(player.characterId);
|
|
497
|
+
const kind = character?.kind;
|
|
498
|
+
if (!kind) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (condition.operator === "IN") {
|
|
502
|
+
const expectedKinds = resolveDynamicValue(condition.value, payloads, (value) => Array.isArray(value) && value.every((item) => typeof item === "string"));
|
|
503
|
+
return Array.isArray(expectedKinds) && expectedKinds.includes(kind);
|
|
504
|
+
}
|
|
505
|
+
const expectedKind = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "string");
|
|
506
|
+
return typeof expectedKind === "string" && kind === expectedKind;
|
|
507
|
+
};
|
|
508
|
+
var filterByWhere = (candidates, dynamicTarget, payloads, characterMap) => {
|
|
509
|
+
const where = dynamicTarget.where;
|
|
510
|
+
const conditions = where?.conditions ?? [];
|
|
511
|
+
if (conditions.length === 0) {
|
|
512
|
+
return candidates;
|
|
513
|
+
}
|
|
514
|
+
const isAllMode = where?.mode !== "ANY";
|
|
515
|
+
return candidates.filter((player) => {
|
|
516
|
+
const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
|
|
517
|
+
return isAllMode ? results.every(Boolean) : results.some(Boolean);
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
var applySort = (candidates, dynamicTarget, payloads, sortedPlayers, anchorSeat) => {
|
|
521
|
+
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
522
|
+
if (scope !== "BOTH_SIDES") {
|
|
523
|
+
return [...candidates];
|
|
524
|
+
}
|
|
525
|
+
if (!anchorSeat || sortedPlayers.length <= 1) {
|
|
526
|
+
return [...candidates].sort((left, right) => left.seat - right.seat);
|
|
527
|
+
}
|
|
528
|
+
const seatIndexMap = new Map(sortedPlayers.map((player, idx) => [player.seat, idx]));
|
|
529
|
+
const anchorIdx = seatIndexMap.get(anchorSeat);
|
|
530
|
+
if (typeof anchorIdx !== "number") {
|
|
531
|
+
return [...candidates].sort((left, right) => left.seat - right.seat);
|
|
532
|
+
}
|
|
533
|
+
return [...candidates].sort((left, right) => {
|
|
534
|
+
const leftIdx = seatIndexMap.get(left.seat);
|
|
535
|
+
const rightIdx = seatIndexMap.get(right.seat);
|
|
536
|
+
if (typeof leftIdx !== "number" || typeof rightIdx !== "number") {
|
|
537
|
+
return left.seat - right.seat;
|
|
538
|
+
}
|
|
539
|
+
const leftDistance = getCircularDistance(anchorIdx, leftIdx, sortedPlayers.length);
|
|
540
|
+
const rightDistance = getCircularDistance(anchorIdx, rightIdx, sortedPlayers.length);
|
|
541
|
+
if (leftDistance !== rightDistance) {
|
|
542
|
+
return leftDistance - rightDistance;
|
|
543
|
+
}
|
|
544
|
+
return left.seat - right.seat;
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
var pickTargets = (sortedCandidates, dynamicTarget, payloads) => {
|
|
548
|
+
const dedupedCandidates = [...new Map(sortedCandidates.map((player) => [player.seat, player])).values()];
|
|
549
|
+
const configuredLimit = resolveDynamicValue(dynamicTarget.limit, payloads, (value) => typeof value === "number");
|
|
550
|
+
if (typeof configuredLimit === "undefined") {
|
|
551
|
+
return dedupedCandidates;
|
|
552
|
+
}
|
|
553
|
+
if (configuredLimit <= 0) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
return dedupedCandidates.slice(0, configuredLimit);
|
|
557
|
+
};
|
|
558
|
+
var resolveEffectTargets = ({
|
|
559
|
+
effect,
|
|
560
|
+
operation,
|
|
561
|
+
payloads,
|
|
562
|
+
effector,
|
|
563
|
+
playerSeatMap,
|
|
564
|
+
characterMap
|
|
565
|
+
}) => {
|
|
566
|
+
if (!("target" in effect)) {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
if (effect.target.from === "EFFECTOR") {
|
|
570
|
+
if (!effector) {
|
|
571
|
+
console.warn("Effector player not found for operation:", operation, "effector index:", operation.effector);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
return [effector];
|
|
575
|
+
}
|
|
576
|
+
if (effect.target.from === "PAYLOAD") {
|
|
577
|
+
const seatValue = getValueFromPayloads(payloads, effect.target.value);
|
|
578
|
+
if (typeof seatValue !== "number" && !Array.isArray(seatValue)) {
|
|
579
|
+
console.warn("Expected seat number or array of seat numbers from payloads, got:", seatValue);
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const seats = Array.isArray(seatValue) ? seatValue : [seatValue];
|
|
583
|
+
return seats.map((seat) => playerSeatMap.get(seat)).filter((player) => Boolean(player));
|
|
584
|
+
}
|
|
585
|
+
if (effect.target.from === "DYNAMIC") {
|
|
586
|
+
const dynamicTarget = effect.target;
|
|
587
|
+
const sortedPlayers = getSortedPlayers(playerSeatMap);
|
|
588
|
+
const anchorSeat = getAnchorSeat(dynamicTarget, payloads, effector);
|
|
589
|
+
const candidates = getCandidatesBySelector(dynamicTarget, sortedPlayers, anchorSeat, payloads);
|
|
590
|
+
const filtered = filterByWhere(candidates, dynamicTarget, payloads, characterMap);
|
|
591
|
+
const ordered = applySort(filtered, dynamicTarget, payloads, sortedPlayers, anchorSeat);
|
|
592
|
+
return pickTargets(ordered, dynamicTarget, payloads);
|
|
593
|
+
}
|
|
594
|
+
return [];
|
|
595
|
+
};
|
|
596
|
+
|
|
159
597
|
// src/apply-operation.ts
|
|
598
|
+
var isDeferredOperation = (operation, abilityMap) => {
|
|
599
|
+
const ability = abilityMap.get(operation.abilityId);
|
|
600
|
+
return ability?.executionTiming === "DEFER_TO_END";
|
|
601
|
+
};
|
|
160
602
|
var applyOperationToPlayers = ({
|
|
161
603
|
players,
|
|
162
604
|
operations,
|
|
@@ -182,7 +624,7 @@ var applyOperationToPlayers = ({
|
|
|
182
624
|
}
|
|
183
625
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
184
626
|
});
|
|
185
|
-
const
|
|
627
|
+
const applyOpsSequence = (ops) => {
|
|
186
628
|
ops.forEach((operation) => {
|
|
187
629
|
const operationInTimelineIdx = operationTimelineMap.get(operation) ?? -1;
|
|
188
630
|
const ability = abilityMap.get(operation.abilityId);
|
|
@@ -193,7 +635,7 @@ var applyOperationToPlayers = ({
|
|
|
193
635
|
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
194
636
|
const effector = playerSeatMap.get(operation.effector);
|
|
195
637
|
const payloads = operation.payloads || [];
|
|
196
|
-
const
|
|
638
|
+
const writableInputs = ability.inputs.filter((part) => !part.readonly);
|
|
197
639
|
let snapshotSeatMap = null;
|
|
198
640
|
const getSnapshotSeatMap = () => {
|
|
199
641
|
if (!snapshotSeatMap) {
|
|
@@ -202,164 +644,52 @@ var applyOperationToPlayers = ({
|
|
|
202
644
|
return snapshotSeatMap;
|
|
203
645
|
};
|
|
204
646
|
ability.effects.forEach((effect) => {
|
|
205
|
-
const makePlayersEffect =
|
|
206
|
-
|
|
207
|
-
|
|
647
|
+
const makePlayersEffect = resolveEffectTargets({
|
|
648
|
+
effect,
|
|
649
|
+
operation,
|
|
650
|
+
payloads,
|
|
651
|
+
effector,
|
|
652
|
+
playerSeatMap,
|
|
653
|
+
characterMap
|
|
654
|
+
});
|
|
655
|
+
if (!makePlayersEffect) {
|
|
208
656
|
return;
|
|
209
657
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
makePlayersEffect.push(effector);
|
|
216
|
-
} else if (effect.target.from === "PAYLOAD") {
|
|
217
|
-
const seatValue = getValueFromPayloads(payloads, effect.target.value);
|
|
218
|
-
if (typeof seatValue !== "number" && !Array.isArray(seatValue)) {
|
|
219
|
-
console.warn("Expected seat number or array of seat numbers from payloads, got:", seatValue);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const seats = Array.isArray(seatValue) ? seatValue : [seatValue];
|
|
223
|
-
seats.forEach((seat) => {
|
|
224
|
-
const player = playerSeatMap.get(seat);
|
|
225
|
-
if (player) {
|
|
226
|
-
makePlayersEffect.push(player);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
} else if (effect.target.from === "CUSTOM") {
|
|
230
|
-
console.warn("Custom target handling not implemented yet.");
|
|
231
|
-
}
|
|
232
|
-
switch (effect.type) {
|
|
233
|
-
case "ABILITY_CHANGE": {
|
|
234
|
-
console.warn("ABILITY_CHANGE effect handling not implemented yet.");
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
case "ALIGNMENT_CHANGE": {
|
|
238
|
-
const resolved = resolveSourceValue(
|
|
239
|
-
effect.source,
|
|
240
|
-
writableParts,
|
|
241
|
-
payloads,
|
|
242
|
-
effector,
|
|
243
|
-
getSnapshotSeatMap(),
|
|
244
|
-
(player) => player.alignment
|
|
245
|
-
);
|
|
246
|
-
let alignment;
|
|
247
|
-
if (resolved === "GOOD" || resolved === "EVIL") {
|
|
248
|
-
alignment = resolved;
|
|
249
|
-
}
|
|
250
|
-
if (!alignment) {
|
|
251
|
-
console.warn("Alignment not found for ALIGNMENT_CHANGE effect:", effect.source);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
makePlayersEffect.forEach((player) => {
|
|
255
|
-
player.alignment = alignment;
|
|
256
|
-
});
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case "CHARACTER_CHANGE":
|
|
260
|
-
case "PERCEIVED_CHARACTER_CHANGE": {
|
|
261
|
-
const resolvedCharacterId = resolveSourceValue(
|
|
262
|
-
effect.source,
|
|
263
|
-
writableParts,
|
|
264
|
-
payloads,
|
|
265
|
-
effector,
|
|
266
|
-
getSnapshotSeatMap(),
|
|
267
|
-
(player) => player.characterId
|
|
268
|
-
);
|
|
269
|
-
let characterId;
|
|
270
|
-
if (typeof resolvedCharacterId === "string") {
|
|
271
|
-
characterId = resolvedCharacterId;
|
|
272
|
-
} else if (Array.isArray(resolvedCharacterId) && typeof resolvedCharacterId[0] === "string") {
|
|
273
|
-
characterId = resolvedCharacterId[0];
|
|
274
|
-
}
|
|
275
|
-
if (!characterId) {
|
|
276
|
-
console.warn("Character ID not found for " + effect.type + " effect:", effect.source);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
const newCharacter = characterMap.get(characterId);
|
|
280
|
-
if (!newCharacter) {
|
|
281
|
-
console.warn("Character not found for " + effect.type + " effect:", characterId);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (effect.type === "CHARACTER_CHANGE") {
|
|
285
|
-
const abilityIds = newCharacter.abilities.map((ability2) => ability2.id);
|
|
286
|
-
makePlayersEffect.forEach((player) => {
|
|
287
|
-
player.characterId = newCharacter.id;
|
|
288
|
-
player.abilities = abilityIds;
|
|
289
|
-
});
|
|
290
|
-
} else {
|
|
291
|
-
makePlayersEffect.forEach((player) => {
|
|
292
|
-
player.perceivedCharacter = {
|
|
293
|
-
characterId: newCharacter.id,
|
|
294
|
-
abilities: newCharacter.abilities.map((ability2) => ability2.id),
|
|
295
|
-
asCharacter: effect.followPriority || false
|
|
296
|
-
};
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
case "REMINDER_ADD": {
|
|
302
|
-
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
303
|
-
if (!isReminder(maybeReminder)) {
|
|
304
|
-
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
308
|
-
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
makePlayersEffect.forEach((player) => {
|
|
312
|
-
if (!player.reminders) {
|
|
313
|
-
player.reminders = [];
|
|
314
|
-
}
|
|
315
|
-
player.reminders.push(maybeReminder);
|
|
316
|
-
});
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
case "REMINDER_REMOVE": {
|
|
320
|
-
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
321
|
-
if (!isPlayerReminderArray(maybeReminder)) {
|
|
322
|
-
console.warn("Invalid reminder data for REMINDER_REMOVE effect:", maybeReminder);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
maybeReminder.sort((left, right) => right.index - left.index).forEach((reminder) => {
|
|
326
|
-
const player = playerSeatMap.get(reminder.playerSeat);
|
|
327
|
-
if (!player) {
|
|
328
|
-
console.warn("Player not found for REMINDER_REMOVE effect:", reminder.playerSeat);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (!player.reminders) {
|
|
332
|
-
player.reminders = [];
|
|
333
|
-
}
|
|
334
|
-
player.reminders = player.reminders.filter((_value, index) => index !== reminder.index);
|
|
335
|
-
});
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
case "SEAT_CHANGE": {
|
|
339
|
-
console.warn("SEAT_CHANGE effect handling not implemented yet.");
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
case "STATUS_CHANGE": {
|
|
343
|
-
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
344
|
-
if (maybeStatus === "DEAD") {
|
|
345
|
-
makePlayersEffect.forEach((player) => {
|
|
346
|
-
player.isDead = true;
|
|
347
|
-
});
|
|
348
|
-
} else if (maybeStatus === "ALIVE") {
|
|
349
|
-
makePlayersEffect.forEach((player) => {
|
|
350
|
-
player.isDead = false;
|
|
351
|
-
});
|
|
352
|
-
} else {
|
|
353
|
-
console.warn("Invalid status value for STATUS_CHANGE effect:", maybeStatus);
|
|
354
|
-
}
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
default: {
|
|
358
|
-
}
|
|
658
|
+
const handler = effectHandlers[effect.type];
|
|
659
|
+
if (!handler) {
|
|
660
|
+
console.warn("Unknown effect type:", effect.type);
|
|
661
|
+
return;
|
|
359
662
|
}
|
|
663
|
+
const context = {
|
|
664
|
+
effect,
|
|
665
|
+
operation,
|
|
666
|
+
payloads,
|
|
667
|
+
effector,
|
|
668
|
+
writableInputs,
|
|
669
|
+
playerSeatMap,
|
|
670
|
+
getSnapshotSeatMap,
|
|
671
|
+
characterMap,
|
|
672
|
+
nowTimelineIndex,
|
|
673
|
+
operationInTimelineIdx,
|
|
674
|
+
makePlayersEffect
|
|
675
|
+
};
|
|
676
|
+
handler(context);
|
|
360
677
|
});
|
|
361
678
|
});
|
|
362
679
|
};
|
|
680
|
+
const applyOps = (ops) => {
|
|
681
|
+
const normalOps = [];
|
|
682
|
+
const deferredOps = [];
|
|
683
|
+
ops.forEach((operation) => {
|
|
684
|
+
if (isDeferredOperation(operation, abilityMap)) {
|
|
685
|
+
deferredOps.push(operation);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
normalOps.push(operation);
|
|
689
|
+
});
|
|
690
|
+
applyOpsSequence(normalOps);
|
|
691
|
+
applyOpsSequence(deferredOps);
|
|
692
|
+
};
|
|
363
693
|
const settingsOps = operationsByTimeline.get(-1);
|
|
364
694
|
if (settingsOps) {
|
|
365
695
|
applyOps(settingsOps);
|