@bct-app/game-engine 0.1.6-beta.4 → 0.1.7
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 +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +484 -95
- package/dist/index.mjs +484 -95
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -205,7 +205,6 @@ var toIdList = (value) => {
|
|
|
205
205
|
};
|
|
206
206
|
var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
207
207
|
effect,
|
|
208
|
-
operation,
|
|
209
208
|
payloads,
|
|
210
209
|
effector,
|
|
211
210
|
writableInputs,
|
|
@@ -231,16 +230,13 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
231
230
|
}
|
|
232
231
|
const mode = effect.mode ?? "REPLACE";
|
|
233
232
|
makePlayersEffect.forEach((player) => {
|
|
234
|
-
const overrideRaw = operation.abilityChangeOverrides?.[String(player.seat)];
|
|
235
|
-
const overrideIds = toIdList(overrideRaw);
|
|
236
|
-
const finalCharacterIds = overrideIds ?? resolvedCharacterIds;
|
|
237
233
|
if (!player.gainCharactersAbility) {
|
|
238
234
|
player.gainCharactersAbility = [];
|
|
239
235
|
}
|
|
240
236
|
if (mode === "REPLACE") {
|
|
241
|
-
player.gainCharactersAbility = [...
|
|
237
|
+
player.gainCharactersAbility = [...resolvedCharacterIds];
|
|
242
238
|
} else {
|
|
243
|
-
for (const characterId of
|
|
239
|
+
for (const characterId of resolvedCharacterIds) {
|
|
244
240
|
if (!player.gainCharactersAbility.includes(characterId)) {
|
|
245
241
|
player.gainCharactersAbility.push(characterId);
|
|
246
242
|
}
|
|
@@ -248,7 +244,7 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
248
244
|
}
|
|
249
245
|
if (player.perceivedCharacter?.asCharacter) {
|
|
250
246
|
const perceived = player.perceivedCharacter;
|
|
251
|
-
|
|
247
|
+
resolvedCharacterIds.forEach((characterId) => {
|
|
252
248
|
const character = characterMap.get(characterId);
|
|
253
249
|
if (!character) return;
|
|
254
250
|
character.abilities.forEach((newAbility) => {
|
|
@@ -680,44 +676,264 @@ var getLifetime = (effect, payloads) => {
|
|
|
680
676
|
if (effect.type === "REMINDER_ADD") {
|
|
681
677
|
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
682
678
|
if (isReminder(maybeReminder) && typeof maybeReminder.duration === "number" && maybeReminder.duration > 0) {
|
|
683
|
-
return {
|
|
679
|
+
return {
|
|
680
|
+
kind: "WHILE",
|
|
681
|
+
expr: {
|
|
682
|
+
op: "ATOM",
|
|
683
|
+
atom: {
|
|
684
|
+
type: "TIME",
|
|
685
|
+
spec: { kind: "WITHIN_PHASES", count: maybeReminder.duration }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
684
689
|
}
|
|
685
690
|
}
|
|
686
691
|
return void 0;
|
|
687
692
|
};
|
|
688
|
-
var
|
|
689
|
-
|
|
690
|
-
const
|
|
693
|
+
var resolveSubject = (subject, ctx) => {
|
|
694
|
+
if (!ctx.snapshotSeatMap) return [];
|
|
695
|
+
const map = ctx.snapshotSeatMap;
|
|
696
|
+
switch (subject.kind) {
|
|
697
|
+
case "EFFECTOR": {
|
|
698
|
+
if (!ctx.effector) return [];
|
|
699
|
+
const player = map.get(ctx.effector.seat);
|
|
700
|
+
return player ? [player] : [];
|
|
701
|
+
}
|
|
702
|
+
case "TARGET": {
|
|
703
|
+
if (!ctx.target) return [];
|
|
704
|
+
const player = map.get(ctx.target.seat);
|
|
705
|
+
return player ? [player] : [];
|
|
706
|
+
}
|
|
707
|
+
case "SEAT": {
|
|
708
|
+
const player = map.get(subject.seat);
|
|
709
|
+
return player ? [player] : [];
|
|
710
|
+
}
|
|
711
|
+
case "PAYLOAD_REF": {
|
|
712
|
+
const value = getValueFromPayloads(ctx.payloads, subject.payloadIndex);
|
|
713
|
+
const seat = adjustValueAsNumber(value);
|
|
714
|
+
if (typeof seat !== "number") return [];
|
|
715
|
+
const player = map.get(seat);
|
|
716
|
+
return player ? [player] : [];
|
|
717
|
+
}
|
|
718
|
+
case "CHARACTER": {
|
|
719
|
+
const matches = [];
|
|
720
|
+
map.forEach((player) => {
|
|
721
|
+
if (player.characterId === subject.characterId) matches.push(player);
|
|
722
|
+
});
|
|
723
|
+
return matches;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var compareString = (actual, operator, value) => {
|
|
728
|
+
const isInList = Array.isArray(value) && value.every((v) => typeof v === "string") ? value.includes(actual ?? "") : false;
|
|
729
|
+
const isEqScalar = typeof value === "string" && actual === value;
|
|
730
|
+
switch (operator) {
|
|
731
|
+
case "EQ":
|
|
732
|
+
return isEqScalar;
|
|
733
|
+
case "NE":
|
|
734
|
+
return !isEqScalar;
|
|
735
|
+
case "IN":
|
|
736
|
+
return actual !== void 0 && isInList;
|
|
737
|
+
case "NOT_IN":
|
|
738
|
+
return !isInList;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var compareBool = (actual, operator, value) => {
|
|
742
|
+
const expected = typeof value === "boolean" ? value : true;
|
|
743
|
+
switch (operator) {
|
|
744
|
+
case "EQ":
|
|
745
|
+
return actual === expected;
|
|
746
|
+
case "NE":
|
|
747
|
+
return actual !== expected;
|
|
748
|
+
case "IN":
|
|
749
|
+
case "NOT_IN":
|
|
750
|
+
return actual === expected;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
var evalPlayerStateOnPlayer = (player, atom, ctx) => {
|
|
754
|
+
const { field, operator, value } = atom;
|
|
755
|
+
const op = operator ?? "EQ";
|
|
756
|
+
if (field === "IS_IN_PLAY") {
|
|
757
|
+
return compareBool(Boolean(player), op, value);
|
|
758
|
+
}
|
|
759
|
+
if (!player) return false;
|
|
760
|
+
if (field === "IS_ALIVE") {
|
|
761
|
+
return compareBool(!player.isDead, op, value);
|
|
762
|
+
}
|
|
691
763
|
if (field === "IS_DEAD") {
|
|
692
|
-
return Boolean(player.isDead)
|
|
764
|
+
return compareBool(Boolean(player.isDead), op, value);
|
|
765
|
+
}
|
|
766
|
+
if (field === "IS_POISONED" || field === "IS_DRUNK" || field === "IS_HEALTHY") {
|
|
767
|
+
const resolved = ctx.statusResolver ? ctx.statusResolver({ player, status: field === "IS_POISONED" ? "POISONED" : field === "IS_DRUNK" ? "DRUNK" : "HEALTHY" }) : false;
|
|
768
|
+
return compareBool(resolved, op, value);
|
|
693
769
|
}
|
|
694
770
|
if (field === "CHARACTER_ID") {
|
|
695
|
-
|
|
696
|
-
return value.includes(player.characterId);
|
|
697
|
-
}
|
|
698
|
-
return typeof value === "string" && player.characterId === value;
|
|
771
|
+
return compareString(player.characterId, op, value);
|
|
699
772
|
}
|
|
700
773
|
if (field === "CHARACTER_KIND") {
|
|
701
|
-
const kind = characterMap.get(player.characterId)?.kind;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return typeof value === "string" && kind === value;
|
|
774
|
+
const kind = ctx.characterMap.get(player.characterId)?.kind;
|
|
775
|
+
return compareString(kind, op, value);
|
|
776
|
+
}
|
|
777
|
+
if (field === "ALIGNMENT") {
|
|
778
|
+
return compareString(player.alignment, op, value);
|
|
707
779
|
}
|
|
708
780
|
if (field === "HAS_ABILITY") {
|
|
709
|
-
if (
|
|
710
|
-
|
|
781
|
+
if (op === "EQ" || op === "NE") {
|
|
782
|
+
const has = typeof value === "string" && player.abilities.includes(value);
|
|
783
|
+
return op === "EQ" ? has : !has;
|
|
784
|
+
}
|
|
785
|
+
const list = Array.isArray(value) ? value : [];
|
|
786
|
+
const anyMatch = list.some((id) => player.abilities.includes(id));
|
|
787
|
+
return op === "IN" ? anyMatch : !anyMatch;
|
|
788
|
+
}
|
|
789
|
+
if (field === "HAS_REMINDER") {
|
|
790
|
+
const marks = (player.reminders ?? []).map((r) => r.mark);
|
|
791
|
+
if (op === "EQ" || op === "NE") {
|
|
792
|
+
const has = typeof value === "string" && marks.includes(value);
|
|
793
|
+
return op === "EQ" ? has : !has;
|
|
711
794
|
}
|
|
712
|
-
|
|
795
|
+
const list = Array.isArray(value) ? value : [];
|
|
796
|
+
const anyMatch = list.some((mark) => marks.includes(mark));
|
|
797
|
+
return op === "IN" ? anyMatch : !anyMatch;
|
|
713
798
|
}
|
|
714
799
|
return false;
|
|
715
800
|
};
|
|
716
|
-
var
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
801
|
+
var evalPlayerState = (atom, ctx) => {
|
|
802
|
+
const subject = atom.subject ?? { kind: "EFFECTOR" };
|
|
803
|
+
const players = resolveSubject(subject, ctx);
|
|
804
|
+
if (subject.kind === "CHARACTER") {
|
|
805
|
+
const quantifier = subject.quantifier ?? "ANY";
|
|
806
|
+
if (players.length === 0) {
|
|
807
|
+
return atom.field === "IS_IN_PLAY" ? evalPlayerStateOnPlayer(void 0, atom, ctx) : false;
|
|
808
|
+
}
|
|
809
|
+
const results = players.map((p) => evalPlayerStateOnPlayer(p, atom, ctx));
|
|
810
|
+
return quantifier === "ALL" ? results.every(Boolean) : results.some(Boolean);
|
|
811
|
+
}
|
|
812
|
+
const single = players[0];
|
|
813
|
+
return evalPlayerStateOnPlayer(single, atom, ctx);
|
|
814
|
+
};
|
|
815
|
+
var isExecutionTimeline = (tl) => {
|
|
816
|
+
if (!tl) return false;
|
|
817
|
+
return (tl.operations ?? []).some((op) => op.kind === "execution");
|
|
818
|
+
};
|
|
819
|
+
var evalTime = (atom, ctx) => {
|
|
820
|
+
const { spec } = atom;
|
|
821
|
+
const opIdx = ctx.operationTimelineIdx;
|
|
822
|
+
const nowIdx = ctx.nowTimelineIndex;
|
|
823
|
+
if (spec.kind === "WITHIN_TURNS") {
|
|
824
|
+
const opTurn = opIdx >= 0 ? ctx.timelines[opIdx]?.turn : void 0;
|
|
825
|
+
const nowTurn = nowIdx >= 0 ? ctx.timelines[nowIdx]?.turn : void 0;
|
|
826
|
+
if (typeof opTurn !== "number" || typeof nowTurn !== "number") return true;
|
|
827
|
+
return nowTurn - opTurn < spec.count;
|
|
828
|
+
}
|
|
829
|
+
if (spec.kind === "WITHIN_PHASES") {
|
|
830
|
+
if (opIdx < 0) return true;
|
|
831
|
+
return nowIdx - opIdx < spec.count;
|
|
832
|
+
}
|
|
833
|
+
if (spec.kind === "BEFORE_EVENT") {
|
|
834
|
+
if (opIdx < 0) return true;
|
|
835
|
+
let prevTime = ctx.timelines[opIdx]?.time;
|
|
836
|
+
for (let i = opIdx + 1; i <= nowIdx; i += 1) {
|
|
837
|
+
const tl = ctx.timelines[i];
|
|
838
|
+
if (!tl) continue;
|
|
839
|
+
if (spec.event === "NEXT_DAY" && tl.time === "day") return false;
|
|
840
|
+
if (spec.event === "NEXT_NIGHT" && tl.time === "night") return false;
|
|
841
|
+
if (spec.event === "NEXT_DUSK" && prevTime === "day" && tl.time === "night") return false;
|
|
842
|
+
if (spec.event === "NEXT_DAWN" && prevTime === "night" && tl.time === "day") return false;
|
|
843
|
+
if (spec.event === "NEXT_EXECUTION" && isExecutionTimeline(tl)) return false;
|
|
844
|
+
prevTime = tl.time;
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
if (spec.kind === "CURRENT_PHASE_IS") {
|
|
849
|
+
const current = nowIdx >= 0 ? ctx.timelines[nowIdx]?.time : void 0;
|
|
850
|
+
const want = spec.phase === "DAY" ? "day" : "night";
|
|
851
|
+
return current === want;
|
|
852
|
+
}
|
|
853
|
+
return true;
|
|
854
|
+
};
|
|
855
|
+
var compareCount = (actual, operator, expected) => {
|
|
856
|
+
switch (operator) {
|
|
857
|
+
case "EQ":
|
|
858
|
+
return actual === expected;
|
|
859
|
+
case "NE":
|
|
860
|
+
return actual !== expected;
|
|
861
|
+
case "GT":
|
|
862
|
+
return actual > expected;
|
|
863
|
+
case "GTE":
|
|
864
|
+
return actual >= expected;
|
|
865
|
+
case "LT":
|
|
866
|
+
return actual < expected;
|
|
867
|
+
case "LTE":
|
|
868
|
+
return actual <= expected;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
var evalGameState = (atom, ctx) => {
|
|
872
|
+
const { spec } = atom;
|
|
873
|
+
if (spec.kind === "ALIVE_COUNT") {
|
|
874
|
+
if (!ctx.snapshotSeatMap) return true;
|
|
875
|
+
let count = 0;
|
|
876
|
+
ctx.snapshotSeatMap.forEach((p) => {
|
|
877
|
+
if (!p.isDead) count += 1;
|
|
878
|
+
});
|
|
879
|
+
return compareCount(count, spec.operator, spec.count);
|
|
880
|
+
}
|
|
881
|
+
if (spec.kind === "EXECUTION_HAPPENED_TODAY") {
|
|
882
|
+
const turn = ctx.timelines[ctx.nowTimelineIndex]?.turn;
|
|
883
|
+
if (typeof turn !== "number") return false;
|
|
884
|
+
return ctx.timelines.some((tl) => tl.turn === turn && tl.time === "day" && isExecutionTimeline(tl));
|
|
885
|
+
}
|
|
886
|
+
if (spec.kind === "PLAYER_DIED_TODAY") {
|
|
887
|
+
const turn = ctx.timelines[ctx.nowTimelineIndex]?.turn;
|
|
888
|
+
if (typeof turn !== "number") return false;
|
|
889
|
+
return ctx.timelines.some((tl) => {
|
|
890
|
+
if (tl.turn !== turn || tl.time !== "day") return false;
|
|
891
|
+
return (tl.operations ?? []).some((op) => {
|
|
892
|
+
const kind = op.kind;
|
|
893
|
+
return kind === "execution" || kind === "death";
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return true;
|
|
898
|
+
};
|
|
899
|
+
var evalAtom = (atom, ctx) => {
|
|
900
|
+
switch (atom.type) {
|
|
901
|
+
case "TIME":
|
|
902
|
+
return evalTime(atom, ctx);
|
|
903
|
+
case "PLAYER_STATE":
|
|
904
|
+
if (!ctx.snapshotSeatMap) return true;
|
|
905
|
+
return evalPlayerState(atom, ctx);
|
|
906
|
+
case "GAME_STATE":
|
|
907
|
+
if (!ctx.snapshotSeatMap) return true;
|
|
908
|
+
return evalGameState(atom, ctx);
|
|
909
|
+
case "CUSTOM":
|
|
910
|
+
if (!ctx.snapshotSeatMap) return true;
|
|
911
|
+
return ctx.customResolver ? ctx.customResolver({
|
|
912
|
+
key: atom.key,
|
|
913
|
+
payload: atom.payload,
|
|
914
|
+
effector: ctx.effector,
|
|
915
|
+
target: ctx.target,
|
|
916
|
+
snapshotSeatMap: ctx.snapshotSeatMap
|
|
917
|
+
}) : false;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
var evalExpr = (expr, ctx) => {
|
|
921
|
+
switch (expr.op) {
|
|
922
|
+
case "AND":
|
|
923
|
+
return expr.children.every((child) => evalExpr(child, ctx));
|
|
924
|
+
case "OR":
|
|
925
|
+
return expr.children.some((child) => evalExpr(child, ctx));
|
|
926
|
+
case "NOT":
|
|
927
|
+
return !evalExpr(expr.child, ctx);
|
|
928
|
+
case "ATOM":
|
|
929
|
+
return evalAtom(expr.atom, ctx);
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
var expressionReferencesTarget = (expr) => {
|
|
933
|
+
if (expr.op === "AND" || expr.op === "OR") return expr.children.some(expressionReferencesTarget);
|
|
934
|
+
if (expr.op === "NOT") return expressionReferencesTarget(expr.child);
|
|
935
|
+
const atom = expr.atom;
|
|
936
|
+
return atom.type === "PLAYER_STATE" && atom.subject?.kind === "TARGET";
|
|
721
937
|
};
|
|
722
938
|
var evaluateLifetime = ({
|
|
723
939
|
effect,
|
|
@@ -728,52 +944,31 @@ var evaluateLifetime = ({
|
|
|
728
944
|
targets,
|
|
729
945
|
snapshotSeatMap,
|
|
730
946
|
characterMap,
|
|
731
|
-
payloads
|
|
947
|
+
payloads,
|
|
948
|
+
statusResolver,
|
|
949
|
+
customResolver
|
|
732
950
|
}) => {
|
|
733
951
|
const lifetime = getLifetime(effect, payloads);
|
|
734
952
|
if (!lifetime || lifetime.kind === "PERMANENT") {
|
|
735
953
|
return targets;
|
|
736
954
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const tl = timelines[idx];
|
|
753
|
-
if (!tl) continue;
|
|
754
|
-
if (lifetime.event === "NEXT_DAY" && tl.time === "day") return [];
|
|
755
|
-
if (lifetime.event === "NEXT_NIGHT" && tl.time === "night") return [];
|
|
756
|
-
if (lifetime.event === "NEXT_EXECUTION" && (tl.nominations?.length ?? 0) > 0) return [];
|
|
757
|
-
}
|
|
758
|
-
return targets;
|
|
759
|
-
}
|
|
760
|
-
if (lifetime.kind === "WHILE") {
|
|
761
|
-
if (!snapshotSeatMap) {
|
|
762
|
-
return targets;
|
|
763
|
-
}
|
|
764
|
-
const subject = lifetime.subject ?? "EFFECTOR";
|
|
765
|
-
const mode = lifetime.mode ?? "ALL";
|
|
766
|
-
const conditions = lifetime.conditions ?? [];
|
|
767
|
-
if (subject === "EFFECTOR") {
|
|
768
|
-
const current = effector ? snapshotSeatMap.get(effector.seat) : void 0;
|
|
769
|
-
return evaluateConditions(current, conditions, mode, characterMap) ? targets : [];
|
|
770
|
-
}
|
|
771
|
-
return targets.filter((target) => {
|
|
772
|
-
const current = snapshotSeatMap.get(target.seat);
|
|
773
|
-
return evaluateConditions(current, conditions, mode, characterMap);
|
|
774
|
-
});
|
|
955
|
+
const baseCtx = {
|
|
956
|
+
operationTimelineIdx,
|
|
957
|
+
nowTimelineIndex,
|
|
958
|
+
timelines,
|
|
959
|
+
effector,
|
|
960
|
+
snapshotSeatMap,
|
|
961
|
+
characterMap,
|
|
962
|
+
payloads,
|
|
963
|
+
statusResolver,
|
|
964
|
+
customResolver
|
|
965
|
+
};
|
|
966
|
+
const expr = lifetime.expr;
|
|
967
|
+
const dependsOnTarget = expressionReferencesTarget(expr);
|
|
968
|
+
if (!dependsOnTarget) {
|
|
969
|
+
return evalExpr(expr, { ...baseCtx, target: void 0 }) ? targets : [];
|
|
775
970
|
}
|
|
776
|
-
return targets;
|
|
971
|
+
return targets.filter((target) => evalExpr(expr, { ...baseCtx, target }));
|
|
777
972
|
};
|
|
778
973
|
|
|
779
974
|
// src/apply-operation.ts
|
|
@@ -794,7 +989,9 @@ var applyOperationToPlayers = ({
|
|
|
794
989
|
allAbilities,
|
|
795
990
|
characters,
|
|
796
991
|
timelines,
|
|
797
|
-
timelineIndexAtNow
|
|
992
|
+
timelineIndexAtNow,
|
|
993
|
+
statusResolver,
|
|
994
|
+
customResolver
|
|
798
995
|
}) => {
|
|
799
996
|
if (operations.length === 0) {
|
|
800
997
|
return transformEmptyArray(players);
|
|
@@ -854,7 +1051,9 @@ var applyOperationToPlayers = ({
|
|
|
854
1051
|
targets: resolvedTargets,
|
|
855
1052
|
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
856
1053
|
characterMap,
|
|
857
|
-
payloads
|
|
1054
|
+
payloads,
|
|
1055
|
+
statusResolver,
|
|
1056
|
+
customResolver
|
|
858
1057
|
});
|
|
859
1058
|
const handler = effectHandlers[effect.type];
|
|
860
1059
|
if (!handler) {
|
|
@@ -934,6 +1133,10 @@ var applyOperationToPlayers = ({
|
|
|
934
1133
|
};
|
|
935
1134
|
|
|
936
1135
|
// src/can-invoke-ability.ts
|
|
1136
|
+
var PHASE_ALIASES = {
|
|
1137
|
+
NIGHT: "night",
|
|
1138
|
+
DAY: "day"
|
|
1139
|
+
};
|
|
937
1140
|
var checkTurns = (turns, currentTurn) => {
|
|
938
1141
|
if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
|
|
939
1142
|
return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
|
|
@@ -946,10 +1149,159 @@ var checkTurns = (turns, currentTurn) => {
|
|
|
946
1149
|
}
|
|
947
1150
|
return null;
|
|
948
1151
|
};
|
|
949
|
-
var
|
|
1152
|
+
var checkEffectorState = (state, effector) => {
|
|
1153
|
+
if (state === "ANY") return null;
|
|
1154
|
+
if (state === "ALIVE") {
|
|
1155
|
+
if (!effector || effector.isDead) return "effector is not alive";
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
if (state === "DEAD") {
|
|
1159
|
+
if (!effector || !effector.isDead) return "effector is not dead";
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
};
|
|
1164
|
+
var isFirstPhaseForEffector = (timelines, currentIdx, phaseTime) => {
|
|
1165
|
+
for (let i = 0; i < currentIdx; i++) {
|
|
1166
|
+
if (timelines[i]?.time === phaseTime) return false;
|
|
1167
|
+
}
|
|
1168
|
+
return true;
|
|
1169
|
+
};
|
|
1170
|
+
var checkPhaseTrigger = (trigger, timelines, currentIdx) => {
|
|
1171
|
+
const expected = PHASE_ALIASES[trigger.phase];
|
|
1172
|
+
const current = timelines[currentIdx]?.time;
|
|
1173
|
+
if (!expected) {
|
|
1174
|
+
return `phase ${trigger.phase} not yet supported in timeline model`;
|
|
1175
|
+
}
|
|
1176
|
+
if (expected !== current) {
|
|
1177
|
+
return `phase ${current} does not match required ${trigger.phase}`;
|
|
1178
|
+
}
|
|
1179
|
+
const occurrence = trigger.occurrence ?? "EVERY";
|
|
1180
|
+
if (occurrence === "EVERY") return null;
|
|
1181
|
+
const isFirst = isFirstPhaseForEffector(timelines, currentIdx, expected);
|
|
1182
|
+
if (occurrence === "FIRST_ONLY" && !isFirst) {
|
|
1183
|
+
return `phase ${trigger.phase} occurrence FIRST_ONLY but this is not the first`;
|
|
1184
|
+
}
|
|
1185
|
+
if (occurrence === "EXCEPT_FIRST" && isFirst) {
|
|
1186
|
+
return `phase ${trigger.phase} occurrence EXCEPT_FIRST but this is the first`;
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
};
|
|
1190
|
+
var subjectMatches = (filter, seat, effectorSeat, players) => {
|
|
1191
|
+
if (filter.match === "ANY") return true;
|
|
1192
|
+
if (filter.match === "EFFECTOR") {
|
|
1193
|
+
return seat !== void 0 && seat === effectorSeat;
|
|
1194
|
+
}
|
|
1195
|
+
if (filter.match === "NEIGHBOR_OF_EFFECTOR") {
|
|
1196
|
+
if (seat === void 0 || effectorSeat === void 0 || !players?.length) return false;
|
|
1197
|
+
const alive = players.filter((p) => !p.isDead).sort((a, b) => a.seat - b.seat);
|
|
1198
|
+
const idx = alive.findIndex((p) => p.seat === effectorSeat);
|
|
1199
|
+
if (idx < 0) return false;
|
|
1200
|
+
const left = alive[(idx - 1 + alive.length) % alive.length]?.seat;
|
|
1201
|
+
const right = alive[(idx + 1) % alive.length]?.seat;
|
|
1202
|
+
return seat === left || seat === right;
|
|
1203
|
+
}
|
|
1204
|
+
if (filter.match === "ROLE") {
|
|
1205
|
+
const player = players?.find((p) => p.seat === seat);
|
|
1206
|
+
return !!player && filter.roles.includes(player.characterId);
|
|
1207
|
+
}
|
|
1208
|
+
if (filter.match === "KIND") {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
if (filter.match === "ALIGNMENT") {
|
|
1212
|
+
const player = players?.find((p) => p.seat === seat);
|
|
1213
|
+
if (!player?.alignment) return false;
|
|
1214
|
+
const want = filter.value === "good" ? "GOOD" : "EVIL";
|
|
1215
|
+
return player.alignment === want;
|
|
1216
|
+
}
|
|
1217
|
+
return false;
|
|
1218
|
+
};
|
|
1219
|
+
var checkEventTrigger = (trigger, event, effectorSeat, players, currentTime) => {
|
|
1220
|
+
if (!event) return "event trigger requires event context";
|
|
1221
|
+
if (event.event !== trigger.event) return `event ${event.event} does not match required ${trigger.event}`;
|
|
1222
|
+
if (trigger.phase) {
|
|
1223
|
+
const expected = PHASE_ALIASES[trigger.phase];
|
|
1224
|
+
if (!expected) return `phase ${trigger.phase} not yet supported in timeline model`;
|
|
1225
|
+
if (expected !== currentTime) return `phase ${currentTime} does not match required ${trigger.phase}`;
|
|
1226
|
+
}
|
|
1227
|
+
if (trigger.target && !subjectMatches(trigger.target, event.targetSeat, effectorSeat, players)) {
|
|
1228
|
+
return `event target does not match filter ${trigger.target.match}`;
|
|
1229
|
+
}
|
|
1230
|
+
if (trigger.actor && !subjectMatches(trigger.actor, event.actorSeat, effectorSeat, players)) {
|
|
1231
|
+
return `event actor does not match filter ${trigger.actor.match}`;
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
};
|
|
1235
|
+
var compareCount2 = (actual, operator, expected) => {
|
|
1236
|
+
switch (operator) {
|
|
1237
|
+
case "<":
|
|
1238
|
+
return actual < expected;
|
|
1239
|
+
case "<=":
|
|
1240
|
+
return actual <= expected;
|
|
1241
|
+
case ">":
|
|
1242
|
+
return actual > expected;
|
|
1243
|
+
case ">=":
|
|
1244
|
+
return actual >= expected;
|
|
1245
|
+
case "=":
|
|
1246
|
+
return actual === expected;
|
|
1247
|
+
case "!=":
|
|
1248
|
+
return actual !== expected;
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
var countExecutionsToday = (timelines, currentIdx) => {
|
|
1252
|
+
const turn = timelines[currentIdx]?.turn;
|
|
1253
|
+
if (typeof turn !== "number") return 0;
|
|
1254
|
+
let count = 0;
|
|
1255
|
+
for (const tl of timelines) {
|
|
1256
|
+
if (tl.turn !== turn || tl.time !== "day") continue;
|
|
1257
|
+
for (const op of tl.operations ?? []) {
|
|
1258
|
+
if (op.kind === "execution") count++;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return count;
|
|
1262
|
+
};
|
|
1263
|
+
var checkCondition = (condition, args) => {
|
|
1264
|
+
const { timelines, currentIdx, players, event, customConditionResolver } = args;
|
|
1265
|
+
if (condition.kind === "PLAYER_COUNT") {
|
|
1266
|
+
if (!players) return `condition ${condition.kind} needs players`;
|
|
1267
|
+
const turn = timelines[currentIdx]?.turn;
|
|
1268
|
+
let actual = 0;
|
|
1269
|
+
if (condition.subject === "ALIVE") actual = players.filter((p) => !p.isDead).length;
|
|
1270
|
+
else if (condition.subject === "DEAD") actual = players.filter((p) => p.isDead).length;
|
|
1271
|
+
else if (condition.subject === "EXECUTED_TODAY") actual = countExecutionsToday(timelines, currentIdx);
|
|
1272
|
+
else if (condition.subject === "NOMINATED_TODAY") {
|
|
1273
|
+
actual = timelines.filter((tl) => tl.turn === turn && tl.time === "day").reduce((acc, tl) => acc + (tl.nominations?.length ?? 0), 0);
|
|
1274
|
+
}
|
|
1275
|
+
if (!compareCount2(actual, condition.operator, condition.value)) {
|
|
1276
|
+
return `condition PLAYER_COUNT ${condition.subject} ${condition.operator} ${condition.value} (actual ${actual})`;
|
|
1277
|
+
}
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
if (condition.kind === "NO_EXECUTION_TODAY") {
|
|
1281
|
+
return countExecutionsToday(timelines, currentIdx) > 0 ? "condition NO_EXECUTION_TODAY violated" : null;
|
|
1282
|
+
}
|
|
1283
|
+
if (condition.kind === "EFFECTOR_HAS_STATUS") {
|
|
1284
|
+
if (!customConditionResolver) return null;
|
|
1285
|
+
const matched = customConditionResolver({ key: `STATUS:${condition.status}`, value: !condition.negate });
|
|
1286
|
+
return matched ? null : `condition EFFECTOR_HAS_STATUS ${condition.status} unsatisfied`;
|
|
1287
|
+
}
|
|
1288
|
+
if (condition.kind === "EVENT_PAYLOAD") {
|
|
1289
|
+
const payloadValue = event?.payload?.[condition.key];
|
|
1290
|
+
if (payloadValue !== condition.value) {
|
|
1291
|
+
return `condition EVENT_PAYLOAD ${condition.key}=${JSON.stringify(condition.value)} (actual ${JSON.stringify(payloadValue)})`;
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
if (condition.kind === "CUSTOM") {
|
|
1296
|
+
if (!customConditionResolver) return null;
|
|
1297
|
+
return customConditionResolver({ key: condition.key, value: condition.value }) ? null : `condition CUSTOM ${condition.key} unsatisfied`;
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
};
|
|
1301
|
+
var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operationTimelineMap, matchTurn, matchTime, timelines, countAcrossEffectors) => {
|
|
950
1302
|
return priorOperations.filter((op) => {
|
|
951
1303
|
if (op.abilityId !== abilityId) return false;
|
|
952
|
-
if (typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
|
|
1304
|
+
if (!countAcrossEffectors && typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
|
|
953
1305
|
if (matchTurn === null && matchTime === null) return true;
|
|
954
1306
|
const idx = operationTimelineMap.get(op);
|
|
955
1307
|
if (typeof idx !== "number") return false;
|
|
@@ -960,12 +1312,40 @@ var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operation
|
|
|
960
1312
|
return true;
|
|
961
1313
|
}).length;
|
|
962
1314
|
};
|
|
1315
|
+
var checkFrequency = (frequency, ability, effectorSeat, timelines, currentIdx, priorOperations) => {
|
|
1316
|
+
if (priorOperations.length === 0) return null;
|
|
1317
|
+
const operationTimelineMap = /* @__PURE__ */ new Map();
|
|
1318
|
+
timelines.forEach((tl, idx) => tl.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
|
|
1319
|
+
const currentTurn = timelines[currentIdx]?.turn ?? null;
|
|
1320
|
+
const isOncePerGame = frequency === "ONCE_PER_GAME";
|
|
1321
|
+
const isNightScoped = frequency === "ONCE_PER_NIGHT" || frequency === "FIRST_PER_NIGHT";
|
|
1322
|
+
const isFirstPer = frequency === "FIRST_PER_NIGHT" || frequency === "FIRST_PER_DAY";
|
|
1323
|
+
const matchTurn = isOncePerGame ? null : currentTurn;
|
|
1324
|
+
const matchTime = isOncePerGame ? null : isNightScoped ? "night" : "day";
|
|
1325
|
+
const count = countMatchingPriorOps(
|
|
1326
|
+
priorOperations,
|
|
1327
|
+
ability.id,
|
|
1328
|
+
effectorSeat,
|
|
1329
|
+
operationTimelineMap,
|
|
1330
|
+
matchTurn,
|
|
1331
|
+
matchTime,
|
|
1332
|
+
timelines,
|
|
1333
|
+
isFirstPer
|
|
1334
|
+
);
|
|
1335
|
+
if (count > 0) {
|
|
1336
|
+
return `frequency ${frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})`;
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
};
|
|
963
1340
|
var canInvokeAbility = ({
|
|
964
1341
|
ability,
|
|
965
1342
|
effector,
|
|
966
1343
|
currentTimelineIdx,
|
|
967
1344
|
timelines,
|
|
968
|
-
priorOperations = []
|
|
1345
|
+
priorOperations = [],
|
|
1346
|
+
players,
|
|
1347
|
+
event,
|
|
1348
|
+
customConditionResolver
|
|
969
1349
|
}) => {
|
|
970
1350
|
const trigger = ability.triggerWindow;
|
|
971
1351
|
if (!trigger) return { allowed: true };
|
|
@@ -977,29 +1357,38 @@ var canInvokeAbility = ({
|
|
|
977
1357
|
const turnError = checkTurns(trigger.turns, currentTimeline.turn);
|
|
978
1358
|
if (turnError) return { allowed: false, reason: turnError };
|
|
979
1359
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
if (
|
|
984
|
-
|
|
1360
|
+
const effectorStateError = checkEffectorState(trigger.effectorState ?? "ALIVE", effector);
|
|
1361
|
+
if (effectorStateError) return { allowed: false, reason: effectorStateError };
|
|
1362
|
+
const spec = trigger.trigger;
|
|
1363
|
+
if (spec.kind === "PHASE") {
|
|
1364
|
+
const phaseError = checkPhaseTrigger(spec, timelines, currentTimelineIdx);
|
|
1365
|
+
if (phaseError) return { allowed: false, reason: phaseError };
|
|
1366
|
+
} else if (spec.kind === "EVENT") {
|
|
1367
|
+
const eventError = checkEventTrigger(spec, event, effector?.seat, players, currentTimeline.time);
|
|
1368
|
+
if (eventError) return { allowed: false, reason: eventError };
|
|
1369
|
+
}
|
|
1370
|
+
if (trigger.conditions && trigger.conditions.length > 0) {
|
|
1371
|
+
for (const condition of trigger.conditions) {
|
|
1372
|
+
const condError = checkCondition(condition, {
|
|
1373
|
+
timelines,
|
|
1374
|
+
currentIdx: currentTimelineIdx,
|
|
1375
|
+
players,
|
|
1376
|
+
event,
|
|
1377
|
+
customConditionResolver
|
|
1378
|
+
});
|
|
1379
|
+
if (condError) return { allowed: false, reason: condError };
|
|
1380
|
+
}
|
|
985
1381
|
}
|
|
986
|
-
if (trigger.frequency
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const matchTime = trigger.frequency === "ONCE_PER_GAME" ? null : trigger.frequency === "ONCE_PER_NIGHT" ? "night" : "day";
|
|
991
|
-
const count = countMatchingPriorOps(
|
|
992
|
-
priorOperations,
|
|
993
|
-
ability.id,
|
|
1382
|
+
if (trigger.frequency) {
|
|
1383
|
+
const freqError = checkFrequency(
|
|
1384
|
+
trigger.frequency,
|
|
1385
|
+
ability,
|
|
994
1386
|
effector?.seat,
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
timelines
|
|
1387
|
+
timelines,
|
|
1388
|
+
currentTimelineIdx,
|
|
1389
|
+
priorOperations
|
|
999
1390
|
);
|
|
1000
|
-
if (
|
|
1001
|
-
return { allowed: false, reason: `frequency ${trigger.frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})` };
|
|
1002
|
-
}
|
|
1391
|
+
if (freqError) return { allowed: false, reason: freqError };
|
|
1003
1392
|
}
|
|
1004
1393
|
return { allowed: true };
|
|
1005
1394
|
};
|