@bct-app/game-engine 0.1.6-beta.2 → 0.1.6-beta.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.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +129 -47
- package/dist/index.mjs +128 -47
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -10,6 +10,21 @@ type TApplyOperationToPlayersArgs = {
|
|
|
10
10
|
};
|
|
11
11
|
declare const applyOperationToPlayers: ({ players, operations, allAbilities, characters, timelines, timelineIndexAtNow, }: TApplyOperationToPlayersArgs) => TPlayer[];
|
|
12
12
|
|
|
13
|
+
type TCanInvokeAbilityArgs = {
|
|
14
|
+
ability: TAbility;
|
|
15
|
+
effector: TPlayer | undefined;
|
|
16
|
+
currentTimelineIdx: number;
|
|
17
|
+
timelines: TTimeline[];
|
|
18
|
+
priorOperations?: TOperation[];
|
|
19
|
+
};
|
|
20
|
+
type TCanInvokeAbilityResult = {
|
|
21
|
+
allowed: true;
|
|
22
|
+
} | {
|
|
23
|
+
allowed: false;
|
|
24
|
+
reason: string;
|
|
25
|
+
};
|
|
26
|
+
declare const canInvokeAbility: ({ ability, effector, currentTimelineIdx, timelines, priorOperations, }: TCanInvokeAbilityArgs) => TCanInvokeAbilityResult;
|
|
27
|
+
|
|
13
28
|
declare const transformEmptyArray: <T>(arr: T[]) => T[];
|
|
14
29
|
declare const copyPlayers: (players: TPlayer[]) => TPlayer[];
|
|
15
30
|
declare const buildPlayerSeatMap: (players: TPlayer[]) => Map<number, TPlayer>;
|
|
@@ -42,4 +57,4 @@ declare const isReminder: (value: unknown) => value is TReminder;
|
|
|
42
57
|
*/
|
|
43
58
|
declare const isPlayerReminderArray: (value: unknown) => value is TPlayerReminder[];
|
|
44
59
|
|
|
45
|
-
export { adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
|
60
|
+
export { type TCanInvokeAbilityArgs, type TCanInvokeAbilityResult, adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, canInvokeAbility, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,21 @@ type TApplyOperationToPlayersArgs = {
|
|
|
10
10
|
};
|
|
11
11
|
declare const applyOperationToPlayers: ({ players, operations, allAbilities, characters, timelines, timelineIndexAtNow, }: TApplyOperationToPlayersArgs) => TPlayer[];
|
|
12
12
|
|
|
13
|
+
type TCanInvokeAbilityArgs = {
|
|
14
|
+
ability: TAbility;
|
|
15
|
+
effector: TPlayer | undefined;
|
|
16
|
+
currentTimelineIdx: number;
|
|
17
|
+
timelines: TTimeline[];
|
|
18
|
+
priorOperations?: TOperation[];
|
|
19
|
+
};
|
|
20
|
+
type TCanInvokeAbilityResult = {
|
|
21
|
+
allowed: true;
|
|
22
|
+
} | {
|
|
23
|
+
allowed: false;
|
|
24
|
+
reason: string;
|
|
25
|
+
};
|
|
26
|
+
declare const canInvokeAbility: ({ ability, effector, currentTimelineIdx, timelines, priorOperations, }: TCanInvokeAbilityArgs) => TCanInvokeAbilityResult;
|
|
27
|
+
|
|
13
28
|
declare const transformEmptyArray: <T>(arr: T[]) => T[];
|
|
14
29
|
declare const copyPlayers: (players: TPlayer[]) => TPlayer[];
|
|
15
30
|
declare const buildPlayerSeatMap: (players: TPlayer[]) => Map<number, TPlayer>;
|
|
@@ -42,4 +57,4 @@ declare const isReminder: (value: unknown) => value is TReminder;
|
|
|
42
57
|
*/
|
|
43
58
|
declare const isPlayerReminderArray: (value: unknown) => value is TPlayerReminder[];
|
|
44
59
|
|
|
45
|
-
export { adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
|
60
|
+
export { type TCanInvokeAbilityArgs, type TCanInvokeAbilityResult, adjustValueAsNumber, adjustValueAsNumberArray, applyOperationToPlayers, buildPlayerSeatMap, canInvokeAbility, copyPlayers, getSourceValue, getValueFromPayloads, isNumberOrNumberArray, isPlayerReminderArray, isReminder, resolveSourceValue, transformEmptyArray };
|
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
|
-
|
|
163
|
-
return resolved !== void 0 ? [resolved] : [];
|
|
163
|
+
return resolveFromCharacter ? resolveFromCharacter(id) : [];
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
return castOrValidate(payloadValue);
|
|
@@ -193,7 +193,7 @@ var createEffectHandler = (type, handler) => {
|
|
|
193
193
|
};
|
|
194
194
|
|
|
195
195
|
// src/effects/handlers/ability-change.ts
|
|
196
|
-
var
|
|
196
|
+
var toIdList = (value) => {
|
|
197
197
|
if (typeof value === "string") {
|
|
198
198
|
return value.length > 0 ? [value] : void 0;
|
|
199
199
|
}
|
|
@@ -211,8 +211,8 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
211
211
|
writableInputs,
|
|
212
212
|
getSnapshotSeatMap,
|
|
213
213
|
abilityMap,
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
characterMap,
|
|
215
|
+
makePlayersEffect
|
|
216
216
|
}) => {
|
|
217
217
|
const resolvedRaw = resolveSourceValue(
|
|
218
218
|
effect.source,
|
|
@@ -220,45 +220,49 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
220
220
|
payloads,
|
|
221
221
|
effector,
|
|
222
222
|
getSnapshotSeatMap(),
|
|
223
|
-
(player) => player.
|
|
223
|
+
(player) => player.characterId,
|
|
224
224
|
(value) => typeof value === "string" && value.length > 0 || Array.isArray(value) && value.every((item) => typeof item === "string"),
|
|
225
|
-
(characterId) =>
|
|
226
|
-
const character = characterMap.get(characterId);
|
|
227
|
-
if (!character) {
|
|
228
|
-
console.warn("Character not found for ABILITY_CHANGE effect source:", characterId);
|
|
229
|
-
return [];
|
|
230
|
-
}
|
|
231
|
-
return character.abilities.map((a) => a.id);
|
|
232
|
-
}
|
|
225
|
+
(characterId) => characterId
|
|
233
226
|
);
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
console.warn("
|
|
227
|
+
const resolvedCharacterIds = toIdList(resolvedRaw);
|
|
228
|
+
if (!resolvedCharacterIds) {
|
|
229
|
+
console.warn("Character ID not found for ABILITY_CHANGE effect:", effect.source);
|
|
237
230
|
return;
|
|
238
231
|
}
|
|
232
|
+
const mode = effect.mode ?? "REPLACE";
|
|
239
233
|
makePlayersEffect.forEach((player) => {
|
|
240
234
|
const overrideRaw = operation.abilityChangeOverrides?.[String(player.seat)];
|
|
241
|
-
const overrideIds =
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
abilities[idx] = finalAbilityId;
|
|
253
|
-
} else {
|
|
254
|
-
abilities.push(finalAbilityId);
|
|
235
|
+
const overrideIds = toIdList(overrideRaw);
|
|
236
|
+
const finalCharacterIds = overrideIds ?? resolvedCharacterIds;
|
|
237
|
+
if (!player.gainCharactersAbility) {
|
|
238
|
+
player.gainCharactersAbility = [];
|
|
239
|
+
}
|
|
240
|
+
if (mode === "REPLACE") {
|
|
241
|
+
player.gainCharactersAbility = [...finalCharacterIds];
|
|
242
|
+
} else {
|
|
243
|
+
for (const characterId of finalCharacterIds) {
|
|
244
|
+
if (!player.gainCharactersAbility.includes(characterId)) {
|
|
245
|
+
player.gainCharactersAbility.push(characterId);
|
|
255
246
|
}
|
|
256
|
-
};
|
|
257
|
-
replaceAtSameStage(player.abilities);
|
|
258
|
-
if (player.perceivedCharacter?.asCharacter) {
|
|
259
|
-
replaceAtSameStage(player.perceivedCharacter.abilities);
|
|
260
247
|
}
|
|
261
|
-
}
|
|
248
|
+
}
|
|
249
|
+
if (player.perceivedCharacter?.asCharacter) {
|
|
250
|
+
const perceived = player.perceivedCharacter;
|
|
251
|
+
finalCharacterIds.forEach((characterId) => {
|
|
252
|
+
const character = characterMap.get(characterId);
|
|
253
|
+
if (!character) return;
|
|
254
|
+
character.abilities.forEach((newAbility) => {
|
|
255
|
+
const idx = perceived.abilities.findIndex(
|
|
256
|
+
(aid) => abilityMap.get(aid)?.stage === newAbility.stage
|
|
257
|
+
);
|
|
258
|
+
if (idx >= 0) {
|
|
259
|
+
perceived.abilities[idx] = newAbility.id;
|
|
260
|
+
} else {
|
|
261
|
+
perceived.abilities.push(newAbility.id);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
262
266
|
});
|
|
263
267
|
});
|
|
264
268
|
|
|
@@ -393,8 +397,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
|
|
|
393
397
|
var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
|
|
394
398
|
effect,
|
|
395
399
|
payloads,
|
|
396
|
-
nowTimelineIndex,
|
|
397
|
-
operationInTimelineIdx,
|
|
398
400
|
makePlayersEffect
|
|
399
401
|
}) => {
|
|
400
402
|
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
@@ -402,10 +404,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
|
|
|
402
404
|
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
403
405
|
return;
|
|
404
406
|
}
|
|
405
|
-
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
406
|
-
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
407
|
makePlayersEffect.forEach((player) => {
|
|
410
408
|
if (!player.reminders) {
|
|
411
409
|
player.reminders = [];
|
|
@@ -675,10 +673,16 @@ var resolveEffectTargets = ({
|
|
|
675
673
|
};
|
|
676
674
|
|
|
677
675
|
// src/effects/evaluate-lifetime.ts
|
|
678
|
-
var getLifetime = (effect) => {
|
|
679
|
-
if ("lifetime" in effect) {
|
|
676
|
+
var getLifetime = (effect, payloads) => {
|
|
677
|
+
if ("lifetime" in effect && effect.lifetime) {
|
|
680
678
|
return effect.lifetime;
|
|
681
679
|
}
|
|
680
|
+
if (effect.type === "REMINDER_ADD") {
|
|
681
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
682
|
+
if (isReminder(maybeReminder) && typeof maybeReminder.duration === "number" && maybeReminder.duration > 0) {
|
|
683
|
+
return { kind: "PHASES", count: maybeReminder.duration };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
682
686
|
return void 0;
|
|
683
687
|
};
|
|
684
688
|
var matchesLifetimeCondition = (player, condition, characterMap) => {
|
|
@@ -723,9 +727,10 @@ var evaluateLifetime = ({
|
|
|
723
727
|
effector,
|
|
724
728
|
targets,
|
|
725
729
|
snapshotSeatMap,
|
|
726
|
-
characterMap
|
|
730
|
+
characterMap,
|
|
731
|
+
payloads
|
|
727
732
|
}) => {
|
|
728
|
-
const lifetime = getLifetime(effect);
|
|
733
|
+
const lifetime = getLifetime(effect, payloads);
|
|
729
734
|
if (!lifetime || lifetime.kind === "PERMANENT") {
|
|
730
735
|
return targets;
|
|
731
736
|
}
|
|
@@ -737,6 +742,10 @@ var evaluateLifetime = ({
|
|
|
737
742
|
}
|
|
738
743
|
return nowTurn - opTurn < lifetime.count ? targets : [];
|
|
739
744
|
}
|
|
745
|
+
if (lifetime.kind === "PHASES") {
|
|
746
|
+
if (operationTimelineIdx < 0) return targets;
|
|
747
|
+
return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
|
|
748
|
+
}
|
|
740
749
|
if (lifetime.kind === "UNTIL_EVENT") {
|
|
741
750
|
if (operationTimelineIdx < 0) return targets;
|
|
742
751
|
for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
|
|
@@ -844,7 +853,8 @@ var applyOperationToPlayers = ({
|
|
|
844
853
|
effector,
|
|
845
854
|
targets: resolvedTargets,
|
|
846
855
|
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
847
|
-
characterMap
|
|
856
|
+
characterMap,
|
|
857
|
+
payloads
|
|
848
858
|
});
|
|
849
859
|
const handler = effectHandlers[effect.type];
|
|
850
860
|
if (!handler) {
|
|
@@ -922,12 +932,84 @@ var applyOperationToPlayers = ({
|
|
|
922
932
|
const final = runReplay(groundTruthSeatMap);
|
|
923
933
|
return transformEmptyArray(final);
|
|
924
934
|
};
|
|
935
|
+
|
|
936
|
+
// src/can-invoke-ability.ts
|
|
937
|
+
var checkTurns = (turns, currentTurn) => {
|
|
938
|
+
if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
|
|
939
|
+
return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
|
|
940
|
+
}
|
|
941
|
+
if (typeof turns.from === "number" && currentTurn < turns.from) {
|
|
942
|
+
return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
|
|
943
|
+
}
|
|
944
|
+
if (typeof turns.to === "number" && currentTurn > turns.to) {
|
|
945
|
+
return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
};
|
|
949
|
+
var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operationTimelineMap, matchTurn, matchTime, timelines) => {
|
|
950
|
+
return priorOperations.filter((op) => {
|
|
951
|
+
if (op.abilityId !== abilityId) return false;
|
|
952
|
+
if (typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
|
|
953
|
+
if (matchTurn === null && matchTime === null) return true;
|
|
954
|
+
const idx = operationTimelineMap.get(op);
|
|
955
|
+
if (typeof idx !== "number") return false;
|
|
956
|
+
const tl = timelines[idx];
|
|
957
|
+
if (!tl) return false;
|
|
958
|
+
if (matchTurn !== null && tl.turn !== matchTurn) return false;
|
|
959
|
+
if (matchTime !== null && tl.time !== matchTime) return false;
|
|
960
|
+
return true;
|
|
961
|
+
}).length;
|
|
962
|
+
};
|
|
963
|
+
var canInvokeAbility = ({
|
|
964
|
+
ability,
|
|
965
|
+
effector,
|
|
966
|
+
currentTimelineIdx,
|
|
967
|
+
timelines,
|
|
968
|
+
priorOperations = []
|
|
969
|
+
}) => {
|
|
970
|
+
const trigger = ability.triggerWindow;
|
|
971
|
+
if (!trigger) return { allowed: true };
|
|
972
|
+
const currentTimeline = timelines[currentTimelineIdx];
|
|
973
|
+
if (!currentTimeline) {
|
|
974
|
+
return { allowed: false, reason: `no timeline at index ${currentTimelineIdx}` };
|
|
975
|
+
}
|
|
976
|
+
if (trigger.turns) {
|
|
977
|
+
const turnError = checkTurns(trigger.turns, currentTimeline.turn);
|
|
978
|
+
if (turnError) return { allowed: false, reason: turnError };
|
|
979
|
+
}
|
|
980
|
+
if (trigger.phases && trigger.phases.length > 0 && !trigger.phases.includes(currentTimeline.time)) {
|
|
981
|
+
return { allowed: false, reason: `phase ${currentTimeline.time} not in allowed phases ${JSON.stringify(trigger.phases)}` };
|
|
982
|
+
}
|
|
983
|
+
if (trigger.requireEffectorAlive && (!effector || effector.isDead)) {
|
|
984
|
+
return { allowed: false, reason: "effector is not alive" };
|
|
985
|
+
}
|
|
986
|
+
if (trigger.frequency && priorOperations.length > 0) {
|
|
987
|
+
const operationTimelineMap = /* @__PURE__ */ new Map();
|
|
988
|
+
timelines.forEach((tl, idx) => tl.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
|
|
989
|
+
const matchTurn = trigger.frequency === "ONCE_PER_GAME" ? null : currentTimeline.turn;
|
|
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,
|
|
994
|
+
effector?.seat,
|
|
995
|
+
operationTimelineMap,
|
|
996
|
+
matchTurn,
|
|
997
|
+
matchTime,
|
|
998
|
+
timelines
|
|
999
|
+
);
|
|
1000
|
+
if (count > 0) {
|
|
1001
|
+
return { allowed: false, reason: `frequency ${trigger.frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})` };
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { allowed: true };
|
|
1005
|
+
};
|
|
925
1006
|
// Annotate the CommonJS export names for ESM import in node:
|
|
926
1007
|
0 && (module.exports = {
|
|
927
1008
|
adjustValueAsNumber,
|
|
928
1009
|
adjustValueAsNumberArray,
|
|
929
1010
|
applyOperationToPlayers,
|
|
930
1011
|
buildPlayerSeatMap,
|
|
1012
|
+
canInvokeAbility,
|
|
931
1013
|
copyPlayers,
|
|
932
1014
|
getSourceValue,
|
|
933
1015
|
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
|
-
|
|
126
|
-
return resolved !== void 0 ? [resolved] : [];
|
|
125
|
+
return resolveFromCharacter ? resolveFromCharacter(id) : [];
|
|
127
126
|
});
|
|
128
127
|
}
|
|
129
128
|
return castOrValidate(payloadValue);
|
|
@@ -156,7 +155,7 @@ var createEffectHandler = (type, handler) => {
|
|
|
156
155
|
};
|
|
157
156
|
|
|
158
157
|
// src/effects/handlers/ability-change.ts
|
|
159
|
-
var
|
|
158
|
+
var toIdList = (value) => {
|
|
160
159
|
if (typeof value === "string") {
|
|
161
160
|
return value.length > 0 ? [value] : void 0;
|
|
162
161
|
}
|
|
@@ -174,8 +173,8 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
174
173
|
writableInputs,
|
|
175
174
|
getSnapshotSeatMap,
|
|
176
175
|
abilityMap,
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
characterMap,
|
|
177
|
+
makePlayersEffect
|
|
179
178
|
}) => {
|
|
180
179
|
const resolvedRaw = resolveSourceValue(
|
|
181
180
|
effect.source,
|
|
@@ -183,45 +182,49 @@ var applyAbilityChange = createEffectHandler("ABILITY_CHANGE", ({
|
|
|
183
182
|
payloads,
|
|
184
183
|
effector,
|
|
185
184
|
getSnapshotSeatMap(),
|
|
186
|
-
(player) => player.
|
|
185
|
+
(player) => player.characterId,
|
|
187
186
|
(value) => typeof value === "string" && value.length > 0 || Array.isArray(value) && value.every((item) => typeof item === "string"),
|
|
188
|
-
(characterId) =>
|
|
189
|
-
const character = characterMap.get(characterId);
|
|
190
|
-
if (!character) {
|
|
191
|
-
console.warn("Character not found for ABILITY_CHANGE effect source:", characterId);
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
return character.abilities.map((a) => a.id);
|
|
195
|
-
}
|
|
187
|
+
(characterId) => characterId
|
|
196
188
|
);
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
199
|
-
console.warn("
|
|
189
|
+
const resolvedCharacterIds = toIdList(resolvedRaw);
|
|
190
|
+
if (!resolvedCharacterIds) {
|
|
191
|
+
console.warn("Character ID not found for ABILITY_CHANGE effect:", effect.source);
|
|
200
192
|
return;
|
|
201
193
|
}
|
|
194
|
+
const mode = effect.mode ?? "REPLACE";
|
|
202
195
|
makePlayersEffect.forEach((player) => {
|
|
203
196
|
const overrideRaw = operation.abilityChangeOverrides?.[String(player.seat)];
|
|
204
|
-
const overrideIds =
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
abilities[idx] = finalAbilityId;
|
|
216
|
-
} else {
|
|
217
|
-
abilities.push(finalAbilityId);
|
|
197
|
+
const overrideIds = toIdList(overrideRaw);
|
|
198
|
+
const finalCharacterIds = overrideIds ?? resolvedCharacterIds;
|
|
199
|
+
if (!player.gainCharactersAbility) {
|
|
200
|
+
player.gainCharactersAbility = [];
|
|
201
|
+
}
|
|
202
|
+
if (mode === "REPLACE") {
|
|
203
|
+
player.gainCharactersAbility = [...finalCharacterIds];
|
|
204
|
+
} else {
|
|
205
|
+
for (const characterId of finalCharacterIds) {
|
|
206
|
+
if (!player.gainCharactersAbility.includes(characterId)) {
|
|
207
|
+
player.gainCharactersAbility.push(characterId);
|
|
218
208
|
}
|
|
219
|
-
};
|
|
220
|
-
replaceAtSameStage(player.abilities);
|
|
221
|
-
if (player.perceivedCharacter?.asCharacter) {
|
|
222
|
-
replaceAtSameStage(player.perceivedCharacter.abilities);
|
|
223
209
|
}
|
|
224
|
-
}
|
|
210
|
+
}
|
|
211
|
+
if (player.perceivedCharacter?.asCharacter) {
|
|
212
|
+
const perceived = player.perceivedCharacter;
|
|
213
|
+
finalCharacterIds.forEach((characterId) => {
|
|
214
|
+
const character = characterMap.get(characterId);
|
|
215
|
+
if (!character) return;
|
|
216
|
+
character.abilities.forEach((newAbility) => {
|
|
217
|
+
const idx = perceived.abilities.findIndex(
|
|
218
|
+
(aid) => abilityMap.get(aid)?.stage === newAbility.stage
|
|
219
|
+
);
|
|
220
|
+
if (idx >= 0) {
|
|
221
|
+
perceived.abilities[idx] = newAbility.id;
|
|
222
|
+
} else {
|
|
223
|
+
perceived.abilities.push(newAbility.id);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
225
228
|
});
|
|
226
229
|
});
|
|
227
230
|
|
|
@@ -356,8 +359,6 @@ var applyPerceivedCharacterChange = createEffectHandler("PERCEIVED_CHARACTER_CHA
|
|
|
356
359
|
var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
|
|
357
360
|
effect,
|
|
358
361
|
payloads,
|
|
359
|
-
nowTimelineIndex,
|
|
360
|
-
operationInTimelineIdx,
|
|
361
362
|
makePlayersEffect
|
|
362
363
|
}) => {
|
|
363
364
|
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
@@ -365,10 +366,6 @@ var applyReminderAdd = createEffectHandler("REMINDER_ADD", ({
|
|
|
365
366
|
console.warn("Invalid reminder data for REMINDER_ADD effect:", maybeReminder);
|
|
366
367
|
return;
|
|
367
368
|
}
|
|
368
|
-
const timelineDistance = nowTimelineIndex - operationInTimelineIdx;
|
|
369
|
-
if (typeof maybeReminder.duration === "number" && maybeReminder.duration > 0 && timelineDistance >= maybeReminder.duration) {
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
369
|
makePlayersEffect.forEach((player) => {
|
|
373
370
|
if (!player.reminders) {
|
|
374
371
|
player.reminders = [];
|
|
@@ -638,10 +635,16 @@ var resolveEffectTargets = ({
|
|
|
638
635
|
};
|
|
639
636
|
|
|
640
637
|
// src/effects/evaluate-lifetime.ts
|
|
641
|
-
var getLifetime = (effect) => {
|
|
642
|
-
if ("lifetime" in effect) {
|
|
638
|
+
var getLifetime = (effect, payloads) => {
|
|
639
|
+
if ("lifetime" in effect && effect.lifetime) {
|
|
643
640
|
return effect.lifetime;
|
|
644
641
|
}
|
|
642
|
+
if (effect.type === "REMINDER_ADD") {
|
|
643
|
+
const maybeReminder = getSourceValue(effect.source, payloads);
|
|
644
|
+
if (isReminder(maybeReminder) && typeof maybeReminder.duration === "number" && maybeReminder.duration > 0) {
|
|
645
|
+
return { kind: "PHASES", count: maybeReminder.duration };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
645
648
|
return void 0;
|
|
646
649
|
};
|
|
647
650
|
var matchesLifetimeCondition = (player, condition, characterMap) => {
|
|
@@ -686,9 +689,10 @@ var evaluateLifetime = ({
|
|
|
686
689
|
effector,
|
|
687
690
|
targets,
|
|
688
691
|
snapshotSeatMap,
|
|
689
|
-
characterMap
|
|
692
|
+
characterMap,
|
|
693
|
+
payloads
|
|
690
694
|
}) => {
|
|
691
|
-
const lifetime = getLifetime(effect);
|
|
695
|
+
const lifetime = getLifetime(effect, payloads);
|
|
692
696
|
if (!lifetime || lifetime.kind === "PERMANENT") {
|
|
693
697
|
return targets;
|
|
694
698
|
}
|
|
@@ -700,6 +704,10 @@ var evaluateLifetime = ({
|
|
|
700
704
|
}
|
|
701
705
|
return nowTurn - opTurn < lifetime.count ? targets : [];
|
|
702
706
|
}
|
|
707
|
+
if (lifetime.kind === "PHASES") {
|
|
708
|
+
if (operationTimelineIdx < 0) return targets;
|
|
709
|
+
return nowTimelineIndex - operationTimelineIdx < lifetime.count ? targets : [];
|
|
710
|
+
}
|
|
703
711
|
if (lifetime.kind === "UNTIL_EVENT") {
|
|
704
712
|
if (operationTimelineIdx < 0) return targets;
|
|
705
713
|
for (let idx = operationTimelineIdx + 1; idx <= nowTimelineIndex; idx += 1) {
|
|
@@ -807,7 +815,8 @@ var applyOperationToPlayers = ({
|
|
|
807
815
|
effector,
|
|
808
816
|
targets: resolvedTargets,
|
|
809
817
|
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
810
|
-
characterMap
|
|
818
|
+
characterMap,
|
|
819
|
+
payloads
|
|
811
820
|
});
|
|
812
821
|
const handler = effectHandlers[effect.type];
|
|
813
822
|
if (!handler) {
|
|
@@ -885,11 +894,83 @@ var applyOperationToPlayers = ({
|
|
|
885
894
|
const final = runReplay(groundTruthSeatMap);
|
|
886
895
|
return transformEmptyArray(final);
|
|
887
896
|
};
|
|
897
|
+
|
|
898
|
+
// src/can-invoke-ability.ts
|
|
899
|
+
var checkTurns = (turns, currentTurn) => {
|
|
900
|
+
if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
|
|
901
|
+
return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
|
|
902
|
+
}
|
|
903
|
+
if (typeof turns.from === "number" && currentTurn < turns.from) {
|
|
904
|
+
return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
|
|
905
|
+
}
|
|
906
|
+
if (typeof turns.to === "number" && currentTurn > turns.to) {
|
|
907
|
+
return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
};
|
|
911
|
+
var countMatchingPriorOps = (priorOperations, abilityId, effectorSeat, operationTimelineMap, matchTurn, matchTime, timelines) => {
|
|
912
|
+
return priorOperations.filter((op) => {
|
|
913
|
+
if (op.abilityId !== abilityId) return false;
|
|
914
|
+
if (typeof effectorSeat === "number" && op.effector !== effectorSeat) return false;
|
|
915
|
+
if (matchTurn === null && matchTime === null) return true;
|
|
916
|
+
const idx = operationTimelineMap.get(op);
|
|
917
|
+
if (typeof idx !== "number") return false;
|
|
918
|
+
const tl = timelines[idx];
|
|
919
|
+
if (!tl) return false;
|
|
920
|
+
if (matchTurn !== null && tl.turn !== matchTurn) return false;
|
|
921
|
+
if (matchTime !== null && tl.time !== matchTime) return false;
|
|
922
|
+
return true;
|
|
923
|
+
}).length;
|
|
924
|
+
};
|
|
925
|
+
var canInvokeAbility = ({
|
|
926
|
+
ability,
|
|
927
|
+
effector,
|
|
928
|
+
currentTimelineIdx,
|
|
929
|
+
timelines,
|
|
930
|
+
priorOperations = []
|
|
931
|
+
}) => {
|
|
932
|
+
const trigger = ability.triggerWindow;
|
|
933
|
+
if (!trigger) return { allowed: true };
|
|
934
|
+
const currentTimeline = timelines[currentTimelineIdx];
|
|
935
|
+
if (!currentTimeline) {
|
|
936
|
+
return { allowed: false, reason: `no timeline at index ${currentTimelineIdx}` };
|
|
937
|
+
}
|
|
938
|
+
if (trigger.turns) {
|
|
939
|
+
const turnError = checkTurns(trigger.turns, currentTimeline.turn);
|
|
940
|
+
if (turnError) return { allowed: false, reason: turnError };
|
|
941
|
+
}
|
|
942
|
+
if (trigger.phases && trigger.phases.length > 0 && !trigger.phases.includes(currentTimeline.time)) {
|
|
943
|
+
return { allowed: false, reason: `phase ${currentTimeline.time} not in allowed phases ${JSON.stringify(trigger.phases)}` };
|
|
944
|
+
}
|
|
945
|
+
if (trigger.requireEffectorAlive && (!effector || effector.isDead)) {
|
|
946
|
+
return { allowed: false, reason: "effector is not alive" };
|
|
947
|
+
}
|
|
948
|
+
if (trigger.frequency && priorOperations.length > 0) {
|
|
949
|
+
const operationTimelineMap = /* @__PURE__ */ new Map();
|
|
950
|
+
timelines.forEach((tl, idx) => tl.operations?.forEach((op) => operationTimelineMap.set(op, idx)));
|
|
951
|
+
const matchTurn = trigger.frequency === "ONCE_PER_GAME" ? null : currentTimeline.turn;
|
|
952
|
+
const matchTime = trigger.frequency === "ONCE_PER_GAME" ? null : trigger.frequency === "ONCE_PER_NIGHT" ? "night" : "day";
|
|
953
|
+
const count = countMatchingPriorOps(
|
|
954
|
+
priorOperations,
|
|
955
|
+
ability.id,
|
|
956
|
+
effector?.seat,
|
|
957
|
+
operationTimelineMap,
|
|
958
|
+
matchTurn,
|
|
959
|
+
matchTime,
|
|
960
|
+
timelines
|
|
961
|
+
);
|
|
962
|
+
if (count > 0) {
|
|
963
|
+
return { allowed: false, reason: `frequency ${trigger.frequency} already exhausted (${count} prior op${count === 1 ? "" : "s"})` };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return { allowed: true };
|
|
967
|
+
};
|
|
888
968
|
export {
|
|
889
969
|
adjustValueAsNumber,
|
|
890
970
|
adjustValueAsNumberArray,
|
|
891
971
|
applyOperationToPlayers,
|
|
892
972
|
buildPlayerSeatMap,
|
|
973
|
+
canInvokeAbility,
|
|
893
974
|
copyPlayers,
|
|
894
975
|
getSourceValue,
|
|
895
976
|
getValueFromPayloads,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bct-app/game-engine",
|
|
3
|
-
"version": "0.1.6-beta.
|
|
3
|
+
"version": "0.1.6-beta.4",
|
|
4
4
|
"description": "Game engine utilities for BCT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bct-app/game-model": "0.1.3-beta.
|
|
40
|
+
"@bct-app/game-model": "0.1.3-beta.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@vitest/ui": "^4.1.3",
|