@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.
- package/dist/index.js +102 -13
- package/dist/index.mjs +101 -13
- 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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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,
|