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