@bct-app/game-engine 0.1.6-beta.2 → 0.1.6-beta.3

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.js CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  adjustValueAsNumberArray: () => adjustValueAsNumberArray,
25
25
  applyOperationToPlayers: () => applyOperationToPlayers,
26
26
  buildPlayerSeatMap: () => buildPlayerSeatMap,
27
+ canInvokeAbility: () => canInvokeAbility,
27
28
  copyPlayers: () => copyPlayers,
28
29
  getSourceValue: () => getSourceValue,
29
30
  getValueFromPayloads: () => getValueFromPayloads,
@@ -159,8 +160,7 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
159
160
  }
160
161
  const characterIds = typeof payloadValue === "string" ? [payloadValue] : payloadValue;
161
162
  return characterIds.flatMap((id) => {
162
- const resolved = resolveFromCharacter ? resolveFromCharacter(id) : void 0;
163
- return resolved !== void 0 ? [resolved] : [];
163
+ return resolveFromCharacter ? resolveFromCharacter(id) : [];
164
164
  });
165
165
  }
166
166
  return castOrValidate(payloadValue);
@@ -393,8 +393,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
393
393
  var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
394
394
  effect,
395
395
  payloads,
396
- nowTimelineIndex,
397
- operationInTimelineIdx,
398
396
  makePlayersEffect
399
397
  }) => {
400
398
  const maybeReminder = getSourceValue(effect.source, payloads);
@@ -402,10 +400,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
402
400
  console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
403
401
  return;
404
402
  }
405
- const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
406
- if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
407
- return;
408
- }
409
403
  makePlayersEffect.forEach((player) => {
410
404
  if (!player.reminders) {
411
405
  player.reminders = [];
@@ -675,10 +669,16 @@ var resolveEffectTargets = ({
675
669
  };
676
670
 
677
671
  // src/effects/evaluate-lifetime.ts
678
- var getLifetime = (effect) => {
679
- if ("lifetime" in effect) {
672
+ var getLifetime = (effect, payloads) => {
673
+ if ("lifetime" in effect && effect.lifetime) {
680
674
  return effect.lifetime;
681
675
  }
676
+ if (effect.type === "REMINDER_ADD") {
677
+ const maybeReminder = getSourceValue(effect.source, payloads);
678
+ if (isReminder(maybeReminder) && typeof maybeReminder.duration === "number" && maybeReminder.duration > 0) {
679
+ return { kind: "PHASES", count: maybeReminder.duration };
680
+ }
681
+ }
682
682
  return void 0;
683
683
  };
684
684
  var matchesLifetimeCondition = (player, condition, characterMap) => {
@@ -723,9 +723,10 @@ var evaluateLifetime = ({
723
723
  effector,
724
724
  targets,
725
725
  snapshotSeatMap,
726
- characterMap
726
+ characterMap,
727
+ payloads
727
728
  }) => {
728
- const lifetime = getLifetime(effect);
729
+ const lifetime = getLifetime(effect, payloads);
729
730
  if (!lifetime || lifetime.kind === "PERMANENT") {
730
731
  return targets;
731
732
  }
@@ -737,6 +738,10 @@ var evaluateLifetime = ({
737
738
  }
738
739
  return nowTurn - opTurn < lifetime.count ? targets : [];
739
740
  }
741
+ if (lifetime.kind === "PHASES") {
742
+ if (operationTimelineIdx < 0) return targets;
743
+ return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
744
+ }
740
745
  if (lifetime.kind === "UNTIL_EVENT") {
741
746
  if (operationTimelineIdx < 0) return targets;
742
747
  for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
@@ -844,7 +849,8 @@ var applyOperationToPlayers = ({
844
849
  effector,
845
850
  targets: resolvedTargets,
846
851
  snapshotSeatMap: lifetimeSnapshotSeatMap,
847
- characterMap
852
+ characterMap,
853
+ payloads
848
854
  });
849
855
  const handler = effectHandlers[effect.type];
850
856
  if (!handler) {
@@ -922,12 +928,84 @@ var applyOperationToPlayers = ({
922
928
  const final = runReplay(groundTruthSeatMap);
923
929
  return transformEmptyArray(final);
924
930
  };
931
+
932
+ // src/can-invoke-ability.ts
933
+ var checkTurns = (turns, currentTurn) => {
934
+ if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
935
+ return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
936
+ }
937
+ if (typeof turns.from === "number" && currentTurn < turns.from) {
938
+ return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
939
+ }
940
+ if (typeof turns.to === "number" && currentTurn > turns.to) {
941
+ return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
942
+ }
943
+ return null;
944
+ };
945
+ var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operationTimelineMap, matchTurn, matchTime, timelines) => {
946
+ return priorOperations.filter((op) => {
947
+ if (op.abilityId !== abilityId) return false;
948
+ if (typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
949
+ if (matchTurn === null && matchTime === null) return true;
950
+ const idx = operationTimelineMap.get(op);
951
+ if (typeof idx !== "number") return false;
952
+ const tl = timelines[idx];
953
+ if (!tl) return false;
954
+ if (matchTurn !== null && tl.turn !== matchTurn) return false;
955
+ if (matchTime !== null && tl.time !== matchTime) return false;
956
+ return true;
957
+ }).length;
958
+ };
959
+ var canInvokeAbility = ({
960
+ ability,
961
+ effector,
962
+ currentTimelineIdx,
963
+ timelines,
964
+ priorOperations = []
965
+ }) => {
966
+ const trigger = ability.triggerWindow;
967
+ if (!trigger) return { allowed: true };
968
+ const currentTimeline = timelines[currentTimelineIdx];
969
+ if (!currentTimeline) {
970
+ return { allowed: false, reason: `no timeline at index ${currentTimelineIdx}` };
971
+ }
972
+ if (trigger.turns) {
973
+ const turnError = checkTurns(trigger.turns, currentTimeline.turn);
974
+ if (turnError) return { allowed: false, reason: turnError };
975
+ }
976
+ if (trigger.phases && trigger.phases.length > 0 && !trigger.phases.includes(currentTimeline.time)) {
977
+ return { allowed: false, reason: `phase ${currentTimeline.time} not in allowed phases ${JSON.stringify(trigger.phases)}` };
978
+ }
979
+ if (trigger.requireEffectorAlive && (!effector || effector.isDead)) {
980
+ return { allowed: false, reason: "effector is not alive" };
981
+ }
982
+ if (trigger.frequency && priorOperations.length > 0) {
983
+ const operationTimelineMap = /* @__PURE__ */ new Map();
984
+ timelines.forEach((tl, idx) => tl.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
985
+ const matchTurn = trigger.frequency === "ONCE_PER_GAME" ? null : currentTimeline.turn;
986
+ const matchTime = trigger.frequency === "ONCE_PER_GAME" ? null : trigger.frequency === "ONCE_PER_NIGHT" ? "night" : "day";
987
+ const count = countMatchingPriorOps(
988
+ priorOperations,
989
+ ability.id,
990
+ effector?.seat,
991
+ operationTimelineMap,
992
+ matchTurn,
993
+ matchTime,
994
+ timelines
995
+ );
996
+ if (count > 0) {
997
+ return { allowed: false, reason: `frequency ${trigger.frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})` };
998
+ }
999
+ }
1000
+ return { allowed: true };
1001
+ };
925
1002
  // Annotate the CommonJS export names for ESM import in node:
926
1003
  0 && (module.exports = {
927
1004
  adjustValueAsNumber,
928
1005
  adjustValueAsNumberArray,
929
1006
  applyOperationToPlayers,
930
1007
  buildPlayerSeatMap,
1008
+ canInvokeAbility,
931
1009
  copyPlayers,
932
1010
  getSourceValue,
933
1011
  getValueFromPayloads,
package/dist/index.mjs CHANGED
@@ -122,8 +122,7 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
122
122
  }
123
123
  const characterIds = typeof payloadValue === "string" ? [payloadValue] : payloadValue;
124
124
  return characterIds.flatMap((id) => {
125
- const resolved = resolveFromCharacter ? resolveFromCharacter(id) : void 0;
126
- return resolved !== void 0 ? [resolved] : [];
125
+ return resolveFromCharacter ? resolveFromCharacter(id) : [];
127
126
  });
128
127
  }
129
128
  return castOrValidate(payloadValue);
@@ -356,8 +355,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
356
355
  var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
357
356
  effect,
358
357
  payloads,
359
- nowTimelineIndex,
360
- operationInTimelineIdx,
361
358
  makePlayersEffect
362
359
  }) => {
363
360
  const maybeReminder = getSourceValue(effect.source, payloads);
@@ -365,10 +362,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
365
362
  console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
366
363
  return;
367
364
  }
368
- const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
369
- if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
370
- return;
371
- }
372
365
  makePlayersEffect.forEach((player) => {
373
366
  if (!player.reminders) {
374
367
  player.reminders = [];
@@ -638,10 +631,16 @@ var resolveEffectTargets = ({
638
631
  };
639
632
 
640
633
  // src/effects/evaluate-lifetime.ts
641
- var getLifetime = (effect) => {
642
- if ("lifetime" in effect) {
634
+ var getLifetime = (effect, payloads) => {
635
+ if ("lifetime" in effect && effect.lifetime) {
643
636
  return effect.lifetime;
644
637
  }
638
+ if (effect.type === "REMINDER_ADD") {
639
+ const maybeReminder = getSourceValue(effect.source, payloads);
640
+ if (isReminder(maybeReminder) && typeof maybeReminder.duration === "number" && maybeReminder.duration > 0) {
641
+ return { kind: "PHASES", count: maybeReminder.duration };
642
+ }
643
+ }
645
644
  return void 0;
646
645
  };
647
646
  var matchesLifetimeCondition = (player, condition, characterMap) => {
@@ -686,9 +685,10 @@ var evaluateLifetime = ({
686
685
  effector,
687
686
  targets,
688
687
  snapshotSeatMap,
689
- characterMap
688
+ characterMap,
689
+ payloads
690
690
  }) => {
691
- const lifetime = getLifetime(effect);
691
+ const lifetime = getLifetime(effect, payloads);
692
692
  if (!lifetime || lifetime.kind === "PERMANENT") {
693
693
  return targets;
694
694
  }
@@ -700,6 +700,10 @@ var evaluateLifetime = ({
700
700
  }
701
701
  return nowTurn - opTurn < lifetime.count ? targets : [];
702
702
  }
703
+ if (lifetime.kind === "PHASES") {
704
+ if (operationTimelineIdx < 0) return targets;
705
+ return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
706
+ }
703
707
  if (lifetime.kind === "UNTIL_EVENT") {
704
708
  if (operationTimelineIdx < 0) return targets;
705
709
  for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
@@ -807,7 +811,8 @@ var applyOperationToPlayers = ({
807
811
  effector,
808
812
  targets: resolvedTargets,
809
813
  snapshotSeatMap: lifetimeSnapshotSeatMap,
810
- characterMap
814
+ characterMap,
815
+ payloads
811
816
  });
812
817
  const handler = effectHandlers[effect.type];
813
818
  if (!handler) {
@@ -885,11 +890,83 @@ var applyOperationToPlayers = ({
885
890
  const final = runReplay(groundTruthSeatMap);
886
891
  return transformEmptyArray(final);
887
892
  };
893
+
894
+ // src/can-invoke-ability.ts
895
+ var checkTurns = (turns, currentTurn) => {
896
+ if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
897
+ return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
898
+ }
899
+ if (typeof turns.from === "number" && currentTurn < turns.from) {
900
+ return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
901
+ }
902
+ if (typeof turns.to === "number" && currentTurn > turns.to) {
903
+ return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
904
+ }
905
+ return null;
906
+ };
907
+ var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operationTimelineMap, matchTurn, matchTime, timelines) => {
908
+ return priorOperations.filter((op) => {
909
+ if (op.abilityId !== abilityId) return false;
910
+ if (typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
911
+ if (matchTurn === null && matchTime === null) return true;
912
+ const idx = operationTimelineMap.get(op);
913
+ if (typeof idx !== "number") return false;
914
+ const tl = timelines[idx];
915
+ if (!tl) return false;
916
+ if (matchTurn !== null && tl.turn !== matchTurn) return false;
917
+ if (matchTime !== null && tl.time !== matchTime) return false;
918
+ return true;
919
+ }).length;
920
+ };
921
+ var canInvokeAbility = ({
922
+ ability,
923
+ effector,
924
+ currentTimelineIdx,
925
+ timelines,
926
+ priorOperations = []
927
+ }) => {
928
+ const trigger = ability.triggerWindow;
929
+ if (!trigger) return { allowed: true };
930
+ const currentTimeline = timelines[currentTimelineIdx];
931
+ if (!currentTimeline) {
932
+ return { allowed: false, reason: `no timeline at index ${currentTimelineIdx}` };
933
+ }
934
+ if (trigger.turns) {
935
+ const turnError = checkTurns(trigger.turns, currentTimeline.turn);
936
+ if (turnError) return { allowed: false, reason: turnError };
937
+ }
938
+ if (trigger.phases && trigger.phases.length > 0 && !trigger.phases.includes(currentTimeline.time)) {
939
+ return { allowed: false, reason: `phase ${currentTimeline.time} not in allowed phases ${JSON.stringify(trigger.phases)}` };
940
+ }
941
+ if (trigger.requireEffectorAlive && (!effector || effector.isDead)) {
942
+ return { allowed: false, reason: "effector is not alive" };
943
+ }
944
+ if (trigger.frequency && priorOperations.length > 0) {
945
+ const operationTimelineMap = /* @__PURE__ */ new Map();
946
+ timelines.forEach((tl, idx) => tl.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
947
+ const matchTurn = trigger.frequency === "ONCE_PER_GAME" ? null : currentTimeline.turn;
948
+ const matchTime = trigger.frequency === "ONCE_PER_GAME" ? null : trigger.frequency === "ONCE_PER_NIGHT" ? "night" : "day";
949
+ const count = countMatchingPriorOps(
950
+ priorOperations,
951
+ ability.id,
952
+ effector?.seat,
953
+ operationTimelineMap,
954
+ matchTurn,
955
+ matchTime,
956
+ timelines
957
+ );
958
+ if (count > 0) {
959
+ return { allowed: false, reason: `frequency ${trigger.frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})` };
960
+ }
961
+ }
962
+ return { allowed: true };
963
+ };
888
964
  export {
889
965
  adjustValueAsNumber,
890
966
  adjustValueAsNumberArray,
891
967
  applyOperationToPlayers,
892
968
  buildPlayerSeatMap,
969
+ canInvokeAbility,
893
970
  copyPlayers,
894
971
  getSourceValue,
895
972
  getValueFromPayloads,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bct-app/game-engine",
3
- "version": "0.1.6-beta.2",
3
+ "version": "0.1.6-beta.3",
4
4
  "description": "Game engine utilities for BCT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",