@bct-app/game-engine 0.1.6-beta.1 → 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.
Files changed (3) hide show
  1. package/dist/index.js +102 -13
  2. package/dist/index.mjs +101 -13
  3. package/package.json +1 -1
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,
@@ -149,8 +150,18 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
149
150
  return player ? resolveFromPlayer(player) : void 0;
150
151
  }
151
152
  if (input?.type === "CHARACTER") {
152
- const characterId = typeof payloadValue === "string" ? payloadValue : void 0;
153
- return characterId && resolveFromCharacter ? resolveFromCharacter(characterId) : void 0;
153
+ if (typeof payloadValue !== "string" && !Array.isArray(payloadValue)) {
154
+ console.warn("Expected string or string[] for CHARACTER type input in PAYLOAD source, got:", payloadValue);
155
+ return void 0;
156
+ }
157
+ if (Array.isArray(payloadValue) && payloadValue.some((v) => typeof v !== "string")) {
158
+ console.warn("Expected string[] for CHARACTER type input in PAYLOAD source, got:", payloadValue);
159
+ return void 0;
160
+ }
161
+ const characterIds = typeof payloadValue === "string" ? [payloadValue] : payloadValue;
162
+ return characterIds.flatMap((id) => {
163
+ return resolveFromCharacter ? resolveFromCharacter(id) : [];
164
+ });
154
165
  }
155
166
  return castOrValidate(payloadValue);
156
167
  }
@@ -382,8 +393,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
382
393
  var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
383
394
  effect,
384
395
  payloads,
385
- nowTimelineIndex,
386
- operationInTimelineIdx,
387
396
  makePlayersEffect
388
397
  }) => {
389
398
  const maybeReminder = getSourceValue(effect.source, payloads);
@@ -391,10 +400,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
391
400
  console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
392
401
  return;
393
402
  }
394
- const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
395
- if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
396
- return;
397
- }
398
403
  makePlayersEffect.forEach((player) => {
399
404
  if (!player.reminders) {
400
405
  player.reminders = [];
@@ -664,10 +669,16 @@ var resolveEffectTargets = ({
664
669
  };
665
670
 
666
671
  // src/effects/evaluate-lifetime.ts
667
- var getLifetime = (effect) => {
668
- if ("lifetime" in effect) {
672
+ var getLifetime = (effect, payloads) => {
673
+ if ("lifetime" in effect && effect.lifetime) {
669
674
  return effect.lifetime;
670
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
+ }
671
682
  return void 0;
672
683
  };
673
684
  var matchesLifetimeCondition = (player, condition, characterMap) => {
@@ -712,9 +723,10 @@ var evaluateLifetime = ({
712
723
  effector,
713
724
  targets,
714
725
  snapshotSeatMap,
715
- characterMap
726
+ characterMap,
727
+ payloads
716
728
  }) => {
717
- const lifetime = getLifetime(effect);
729
+ const lifetime = getLifetime(effect, payloads);
718
730
  if (!lifetime || lifetime.kind === "PERMANENT") {
719
731
  return targets;
720
732
  }
@@ -726,6 +738,10 @@ var evaluateLifetime = ({
726
738
  }
727
739
  return nowTurn - opTurn < lifetime.count ? targets : [];
728
740
  }
741
+ if (lifetime.kind === "PHASES") {
742
+ if (operationTimelineIdx < 0) return targets;
743
+ return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
744
+ }
729
745
  if (lifetime.kind === "UNTIL_EVENT") {
730
746
  if (operationTimelineIdx < 0) return targets;
731
747
  for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
@@ -833,7 +849,8 @@ var applyOperationToPlayers = ({
833
849
  effector,
834
850
  targets: resolvedTargets,
835
851
  snapshotSeatMap: lifetimeSnapshotSeatMap,
836
- characterMap
852
+ characterMap,
853
+ payloads
837
854
  });
838
855
  const handler = effectHandlers[effect.type];
839
856
  if (!handler) {
@@ -911,12 +928,84 @@ var applyOperationToPlayers = ({
911
928
  const final = runReplay(groundTruthSeatMap);
912
929
  return transformEmptyArray(final);
913
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
+ };
914
1002
  // Annotate the CommonJS export names for ESM import in node:
915
1003
  0 && (module.exports = {
916
1004
  adjustValueAsNumber,
917
1005
  adjustValueAsNumberArray,
918
1006
  applyOperationToPlayers,
919
1007
  buildPlayerSeatMap,
1008
+ canInvokeAbility,
920
1009
  copyPlayers,
921
1010
  getSourceValue,
922
1011
  getValueFromPayloads,
package/dist/index.mjs CHANGED
@@ -112,8 +112,18 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
112
112
  return player ? resolveFromPlayer(player) : void 0;
113
113
  }
114
114
  if (input?.type === "CHARACTER") {
115
- const characterId = typeof payloadValue === "string" ? payloadValue : void 0;
116
- return characterId && resolveFromCharacter ? resolveFromCharacter(characterId) : void 0;
115
+ if (typeof payloadValue !== "string" && !Array.isArray(payloadValue)) {
116
+ console.warn("Expected string or string[] for CHARACTER type input in PAYLOAD source, got:", payloadValue);
117
+ return void 0;
118
+ }
119
+ if (Array.isArray(payloadValue) && payloadValue.some((v) => typeof v !== "string")) {
120
+ console.warn("Expected string[] for CHARACTER type input in PAYLOAD source, got:", payloadValue);
121
+ return void 0;
122
+ }
123
+ const characterIds = typeof payloadValue === "string" ? [payloadValue] : payloadValue;
124
+ return characterIds.flatMap((id) => {
125
+ return resolveFromCharacter ? resolveFromCharacter(id) : [];
126
+ });
117
127
  }
118
128
  return castOrValidate(payloadValue);
119
129
  }
@@ -345,8 +355,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
345
355
  var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
346
356
  effect,
347
357
  payloads,
348
- nowTimelineIndex,
349
- operationInTimelineIdx,
350
358
  makePlayersEffect
351
359
  }) => {
352
360
  const maybeReminder = getSourceValue(effect.source, payloads);
@@ -354,10 +362,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
354
362
  console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
355
363
  return;
356
364
  }
357
- const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
358
- if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
359
- return;
360
- }
361
365
  makePlayersEffect.forEach((player) => {
362
366
  if (!player.reminders) {
363
367
  player.reminders = [];
@@ -627,10 +631,16 @@ var resolveEffectTargets = ({
627
631
  };
628
632
 
629
633
  // src/effects/evaluate-lifetime.ts
630
- var getLifetime = (effect) => {
631
- if ("lifetime" in effect) {
634
+ var getLifetime = (effect, payloads) => {
635
+ if ("lifetime" in effect && effect.lifetime) {
632
636
  return effect.lifetime;
633
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
+ }
634
644
  return void 0;
635
645
  };
636
646
  var matchesLifetimeCondition = (player, condition, characterMap) => {
@@ -675,9 +685,10 @@ var evaluateLifetime = ({
675
685
  effector,
676
686
  targets,
677
687
  snapshotSeatMap,
678
- characterMap
688
+ characterMap,
689
+ payloads
679
690
  }) => {
680
- const lifetime = getLifetime(effect);
691
+ const lifetime = getLifetime(effect, payloads);
681
692
  if (!lifetime || lifetime.kind === "PERMANENT") {
682
693
  return targets;
683
694
  }
@@ -689,6 +700,10 @@ var evaluateLifetime = ({
689
700
  }
690
701
  return nowTurn - opTurn < lifetime.count ? targets : [];
691
702
  }
703
+ if (lifetime.kind === "PHASES") {
704
+ if (operationTimelineIdx < 0) return targets;
705
+ return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
706
+ }
692
707
  if (lifetime.kind === "UNTIL_EVENT") {
693
708
  if (operationTimelineIdx < 0) return targets;
694
709
  for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
@@ -796,7 +811,8 @@ var applyOperationToPlayers = ({
796
811
  effector,
797
812
  targets: resolvedTargets,
798
813
  snapshotSeatMap: lifetimeSnapshotSeatMap,
799
- characterMap
814
+ characterMap,
815
+ payloads
800
816
  });
801
817
  const handler = effectHandlers[effect.type];
802
818
  if (!handler) {
@@ -874,11 +890,83 @@ var applyOperationToPlayers = ({
874
890
  const final = runReplay(groundTruthSeatMap);
875
891
  return transformEmptyArray(final);
876
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
+ };
877
964
  export {
878
965
  adjustValueAsNumber,
879
966
  adjustValueAsNumberArray,
880
967
  applyOperationToPlayers,
881
968
  buildPlayerSeatMap,
969
+ canInvokeAbility,
882
970
  copyPlayers,
883
971
  getSourceValue,
884
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.1",
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",