@bct-app/game-engine 0.1.8 → 0.1.9
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 +57 -18
- package/dist/index.d.ts +57 -18
- package/dist/index.js +299 -221
- package/dist/index.mjs +297 -221
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -26,12 +26,14 @@ __export(index_exports, {
|
|
|
26
26
|
buildPlayerSeatMap: () => buildPlayerSeatMap,
|
|
27
27
|
canInvokeAbility: () => canInvokeAbility,
|
|
28
28
|
copyPlayers: () => copyPlayers,
|
|
29
|
+
deriveContext: () => deriveContext,
|
|
29
30
|
getSourceValue: () => getSourceValue,
|
|
30
31
|
getValueFromPayloads: () => getValueFromPayloads,
|
|
31
32
|
isNumberOrNumberArray: () => isNumberOrNumberArray,
|
|
32
33
|
isPlayerReminderArray: () => isPlayerReminderArray,
|
|
33
34
|
isReminder: () => isReminder,
|
|
34
35
|
resolveSourceValue: () => resolveSourceValue,
|
|
36
|
+
resolveTargetRef: () => resolveTargetRef,
|
|
35
37
|
transformEmptyArray: () => transformEmptyArray
|
|
36
38
|
});
|
|
37
39
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -177,6 +179,47 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
|
|
|
177
179
|
}
|
|
178
180
|
return void 0;
|
|
179
181
|
};
|
|
182
|
+
var resolveTargetRef = ({
|
|
183
|
+
ref,
|
|
184
|
+
players,
|
|
185
|
+
effectorSeat,
|
|
186
|
+
characterMap
|
|
187
|
+
}) => {
|
|
188
|
+
if (ref.ref === "ANY") return players;
|
|
189
|
+
if (ref.ref === "EFFECTOR") {
|
|
190
|
+
if (effectorSeat === void 0) return [];
|
|
191
|
+
return players.filter((p) => p.seat === effectorSeat);
|
|
192
|
+
}
|
|
193
|
+
if (ref.ref === "ROLE") {
|
|
194
|
+
return players.filter((p) => p.characterId === ref.characterId);
|
|
195
|
+
}
|
|
196
|
+
if (ref.ref === "KIND") {
|
|
197
|
+
if (!characterMap) return [];
|
|
198
|
+
return players.filter((p) => characterMap.get(p.characterId)?.kind === ref.kind);
|
|
199
|
+
}
|
|
200
|
+
if (ref.ref === "ALIGNMENT") {
|
|
201
|
+
return players.filter((p) => p.alignment === ref.alignment);
|
|
202
|
+
}
|
|
203
|
+
if (ref.ref === "NEIGHBOR") {
|
|
204
|
+
if (effectorSeat === void 0 || players.length === 0) return [];
|
|
205
|
+
const alive = players.filter((p) => !p.isDead).sort((a, b) => a.seat - b.seat);
|
|
206
|
+
if (alive.length === 0) return [];
|
|
207
|
+
const idx = alive.findIndex((p) => p.seat === effectorSeat);
|
|
208
|
+
if (idx < 0) return [];
|
|
209
|
+
const left = alive[(idx - 1 + alive.length) % alive.length];
|
|
210
|
+
const right = alive[(idx + 1) % alive.length];
|
|
211
|
+
if (ref.side === "LEFT") return left ? [left] : [];
|
|
212
|
+
if (ref.side === "RIGHT") return right ? [right] : [];
|
|
213
|
+
const both = [left, right].filter((p) => Boolean(p));
|
|
214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
215
|
+
return both.filter((p) => {
|
|
216
|
+
if (seen.has(p.seat)) return false;
|
|
217
|
+
seen.add(p.seat);
|
|
218
|
+
return true;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
};
|
|
180
223
|
|
|
181
224
|
// src/effects/handler-types.ts
|
|
182
225
|
var createEffectHandler = (type, handler) => {
|
|
@@ -434,17 +477,31 @@ var applySeatChange = createEffectHandler("SEAT_CHANGE", () => {
|
|
|
434
477
|
});
|
|
435
478
|
|
|
436
479
|
// src/effects/handlers/status-change.ts
|
|
437
|
-
var applyStatusChange = createEffectHandler("STATUS_CHANGE", ({
|
|
480
|
+
var applyStatusChange = createEffectHandler("STATUS_CHANGE", ({
|
|
481
|
+
effect,
|
|
482
|
+
payloads,
|
|
483
|
+
makePlayersEffect,
|
|
484
|
+
operationInTimelineIdx,
|
|
485
|
+
abilityMap,
|
|
486
|
+
operation
|
|
487
|
+
}) => {
|
|
438
488
|
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
439
489
|
if (maybeStatus === "DEAD") {
|
|
490
|
+
const ability = abilityMap.get(operation.abilityId);
|
|
491
|
+
const turn = operationInTimelineIdx >= 0 ? operationInTimelineIdx : 0;
|
|
492
|
+
const cause = effect.deathCause ?? (ability?.category === "STORYTELLER" ? "STORYTELLER" : "ABILITY");
|
|
440
493
|
makePlayersEffect.forEach((player) => {
|
|
441
494
|
player.isDead = true;
|
|
495
|
+
player.deathCause = cause;
|
|
496
|
+
player.deathTurn = turn;
|
|
442
497
|
});
|
|
443
498
|
return;
|
|
444
499
|
}
|
|
445
500
|
if (maybeStatus === "ALIVE") {
|
|
446
501
|
makePlayersEffect.forEach((player) => {
|
|
447
502
|
player.isDead = false;
|
|
503
|
+
delete player.deathCause;
|
|
504
|
+
delete player.deathTurn;
|
|
448
505
|
});
|
|
449
506
|
return;
|
|
450
507
|
}
|
|
@@ -1133,105 +1190,17 @@ var applyOperationToPlayers = ({
|
|
|
1133
1190
|
};
|
|
1134
1191
|
|
|
1135
1192
|
// src/can-invoke-ability.ts
|
|
1136
|
-
var
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
};
|
|
1140
|
-
var checkTurns = (turns, currentTurn) => {
|
|
1141
|
-
if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
|
|
1142
|
-
return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
|
|
1143
|
-
}
|
|
1144
|
-
if (typeof turns.from === "number" && currentTurn < turns.from) {
|
|
1145
|
-
return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
|
|
1146
|
-
}
|
|
1147
|
-
if (typeof turns.to === "number" && currentTurn > turns.to) {
|
|
1148
|
-
return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
|
|
1149
|
-
}
|
|
1150
|
-
return null;
|
|
1151
|
-
};
|
|
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;
|
|
1193
|
+
var TIME_TO_PHASE = {
|
|
1194
|
+
night: "NIGHT",
|
|
1195
|
+
day: "DAY"
|
|
1163
1196
|
};
|
|
1164
|
-
var
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1197
|
+
var checkTurnRange = (range, turn) => {
|
|
1198
|
+
if (!range) return true;
|
|
1199
|
+
if (range.only && range.only.length > 0 && !range.only.includes(turn)) return false;
|
|
1200
|
+
if (typeof range.from === "number" && turn < range.from) return false;
|
|
1201
|
+
if (typeof range.to === "number" && turn > range.to) return false;
|
|
1168
1202
|
return true;
|
|
1169
1203
|
};
|
|
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
1204
|
var compareCount2 = (actual, operator, expected) => {
|
|
1236
1205
|
switch (operator) {
|
|
1237
1206
|
case "<":
|
|
@@ -1248,149 +1217,256 @@ var compareCount2 = (actual, operator, expected) => {
|
|
|
1248
1217
|
return actual !== expected;
|
|
1249
1218
|
}
|
|
1250
1219
|
};
|
|
1251
|
-
var
|
|
1252
|
-
const
|
|
1253
|
-
if (
|
|
1254
|
-
let
|
|
1255
|
-
|
|
1256
|
-
if (tl.turn !== turn || tl.time !== "day") continue;
|
|
1257
|
-
for (const op of tl.operations ?? []) {
|
|
1258
|
-
if (op.kind === "execution") count++;
|
|
1259
|
-
}
|
|
1220
|
+
var isFirstOfPhase = (timelines, lastIdx) => {
|
|
1221
|
+
const last = timelines[lastIdx];
|
|
1222
|
+
if (!last) return false;
|
|
1223
|
+
for (let i = 0; i < lastIdx; i++) {
|
|
1224
|
+
if (timelines[i]?.time === last.time) return false;
|
|
1260
1225
|
}
|
|
1261
|
-
return
|
|
1226
|
+
return true;
|
|
1262
1227
|
};
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1228
|
+
var deriveContext = ({
|
|
1229
|
+
timelines,
|
|
1230
|
+
candidate,
|
|
1231
|
+
initialPlayers,
|
|
1232
|
+
allAbilities,
|
|
1233
|
+
characters
|
|
1234
|
+
}) => {
|
|
1235
|
+
const lastIdx = timelines.length - 1;
|
|
1236
|
+
const last = timelines[lastIdx];
|
|
1237
|
+
if (!last) {
|
|
1238
|
+
return { error: "timelines must contain at least one entry" };
|
|
1239
|
+
}
|
|
1240
|
+
const players = applyOperationToPlayers({
|
|
1241
|
+
players: initialPlayers,
|
|
1242
|
+
operations: timelines.flatMap((tl) => tl.operations ?? []),
|
|
1243
|
+
allAbilities,
|
|
1244
|
+
characters,
|
|
1245
|
+
timelines,
|
|
1246
|
+
timelineIndexAtNow: lastIdx
|
|
1247
|
+
});
|
|
1248
|
+
const effector = players.find((p) => p.seat === candidate.effector);
|
|
1249
|
+
const operationTurnMap = /* @__PURE__ */ new Map();
|
|
1250
|
+
const operationTimeMap = /* @__PURE__ */ new Map();
|
|
1251
|
+
timelines.forEach((tl) => {
|
|
1252
|
+
tl.operations?.forEach((op) => {
|
|
1253
|
+
operationTurnMap.set(op, tl.turn);
|
|
1254
|
+
operationTimeMap.set(op, tl.time);
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
const allOperations = timelines.flatMap((tl) => tl.operations ?? []);
|
|
1258
|
+
const todayOperations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "day").flatMap((tl) => tl.operations ?? []);
|
|
1259
|
+
const tonightOperations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "night").flatMap((tl) => tl.operations ?? []);
|
|
1260
|
+
const yesterdayDayOperations = timelines.filter((tl) => tl.turn === last.turn - 1 && tl.time === "day").flatMap((tl) => tl.operations ?? []);
|
|
1261
|
+
const yesterdayNightOperations = timelines.filter((tl) => tl.turn === last.turn - 1 && tl.time === "night").flatMap((tl) => tl.operations ?? []);
|
|
1262
|
+
const todayNominations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "day").flatMap((tl) => tl.nominations ?? []).map((n) => ({ nominator: n.nominator, nominee: n.nominee }));
|
|
1263
|
+
return {
|
|
1264
|
+
currentTurn: last.turn,
|
|
1265
|
+
currentPhase: TIME_TO_PHASE[last.time],
|
|
1266
|
+
isFirstOfPhase: isFirstOfPhase(timelines, lastIdx),
|
|
1267
|
+
players,
|
|
1268
|
+
allOperations,
|
|
1269
|
+
todayOperations,
|
|
1270
|
+
tonightOperations,
|
|
1271
|
+
yesterdayDayOperations,
|
|
1272
|
+
yesterdayNightOperations,
|
|
1273
|
+
todayNominations,
|
|
1274
|
+
effectorSeat: candidate.effector,
|
|
1275
|
+
effector,
|
|
1276
|
+
abilityId: candidate.abilityId,
|
|
1277
|
+
characterMap: new Map(characters.map((c) => [c.id, c])),
|
|
1278
|
+
operationTurnMap,
|
|
1279
|
+
operationTimeMap
|
|
1280
|
+
};
|
|
1281
|
+
};
|
|
1282
|
+
var operationsForActionScope = (scope, ctx) => {
|
|
1283
|
+
if (scope === "TONIGHT") return ctx.tonightOperations;
|
|
1284
|
+
if (scope === "TODAY") return ctx.todayOperations;
|
|
1285
|
+
if (scope === "YESTERDAY") {
|
|
1286
|
+
return [...ctx.yesterdayDayOperations, ...ctx.yesterdayNightOperations];
|
|
1279
1287
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1288
|
+
return ctx.allOperations;
|
|
1289
|
+
};
|
|
1290
|
+
var seatsMatchingTargetRef = (ref, ctx) => {
|
|
1291
|
+
const matched = resolveTargetRef({
|
|
1292
|
+
ref,
|
|
1293
|
+
players: ctx.players,
|
|
1294
|
+
effectorSeat: ctx.effectorSeat,
|
|
1295
|
+
characterMap: ctx.characterMap
|
|
1296
|
+
});
|
|
1297
|
+
return new Set(matched.map((p) => p.seat));
|
|
1298
|
+
};
|
|
1299
|
+
var evalAtomTime = (atom, ctx) => {
|
|
1300
|
+
if (atom.phase !== ctx.currentPhase) {
|
|
1301
|
+
return `phase is ${ctx.currentPhase}, required ${atom.phase}`;
|
|
1282
1302
|
}
|
|
1283
|
-
if (
|
|
1284
|
-
|
|
1285
|
-
const matched = customConditionResolver({ key: `STATUS:${condition.status}`, value: !condition.negate });
|
|
1286
|
-
return matched ? null : `condition EFFECTOR_HAS_STATUS ${condition.status} unsatisfied`;
|
|
1303
|
+
if (atom.occurrence === "FIRST" && !ctx.isFirstOfPhase) {
|
|
1304
|
+
return `expected FIRST occurrence of ${atom.phase} but is not the first`;
|
|
1287
1305
|
}
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1306
|
+
if (atom.occurrence === "EXCEPT_FIRST" && ctx.isFirstOfPhase) {
|
|
1307
|
+
return `expected EXCEPT_FIRST occurrence of ${atom.phase} but is the first`;
|
|
1308
|
+
}
|
|
1309
|
+
if (!checkTurnRange(atom.turns, ctx.currentTurn)) {
|
|
1310
|
+
return `turn ${ctx.currentTurn} not in allowed turn range`;
|
|
1311
|
+
}
|
|
1312
|
+
return null;
|
|
1313
|
+
};
|
|
1314
|
+
var evalAtomState = (atom, ctx) => {
|
|
1315
|
+
const seats = seatsMatchingTargetRef(atom.subject, ctx);
|
|
1316
|
+
if (seats.size === 0) return `no players match subject ${atom.subject.ref}`;
|
|
1317
|
+
for (const seat of seats) {
|
|
1318
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1319
|
+
if (!player) continue;
|
|
1320
|
+
const isDead = Boolean(player.isDead);
|
|
1321
|
+
if (atom.aliveness === "ALIVE" && isDead) continue;
|
|
1322
|
+
if (atom.aliveness === "DEAD" && !isDead) continue;
|
|
1323
|
+
if (atom.aliveness === "DEAD") {
|
|
1324
|
+
if (atom.deathCause && player.deathCause !== atom.deathCause) continue;
|
|
1325
|
+
if (atom.deathTurn && !checkTurnRange(atom.deathTurn, player.deathTurn ?? -1)) continue;
|
|
1292
1326
|
}
|
|
1293
1327
|
return null;
|
|
1294
1328
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1329
|
+
return `no subject in ${atom.subject.ref} satisfies aliveness=${atom.aliveness}`;
|
|
1330
|
+
};
|
|
1331
|
+
var evalAtomAction = (atom, ctx) => {
|
|
1332
|
+
const subjectSeats = seatsMatchingTargetRef(atom.subject, ctx);
|
|
1333
|
+
if (subjectSeats.size === 0) return `no players match subject ${atom.subject.ref}`;
|
|
1334
|
+
const actorSeats = atom.actor ? seatsMatchingTargetRef(atom.actor, ctx) : null;
|
|
1335
|
+
if (atom.action === "NOMINATED") {
|
|
1336
|
+
const noms = ctx.todayNominations;
|
|
1337
|
+
const found = noms.some(
|
|
1338
|
+
(n) => subjectSeats.has(n.nominee) && (actorSeats === null || actorSeats.has(n.nominator))
|
|
1339
|
+
);
|
|
1340
|
+
return found ? null : "no matching NOMINATED event in today";
|
|
1341
|
+
}
|
|
1342
|
+
const scoped = operationsForActionScope(atom.scope, ctx);
|
|
1343
|
+
if (atom.action === "CHOSEN_AS_TARGET") {
|
|
1344
|
+
const found = scoped.some((op) => {
|
|
1345
|
+
if (actorSeats !== null && !actorSeats.has(op.effector)) return false;
|
|
1346
|
+
const payloadSeats = (op.payloads ?? []).flatMap(
|
|
1347
|
+
(v) => typeof v === "number" ? [v] : Array.isArray(v) ? v.filter((x) => typeof x === "number") : []
|
|
1348
|
+
);
|
|
1349
|
+
return payloadSeats.some((s) => subjectSeats.has(s));
|
|
1350
|
+
});
|
|
1351
|
+
return found ? null : "no matching CHOSEN_AS_TARGET in scope";
|
|
1298
1352
|
}
|
|
1299
|
-
|
|
1353
|
+
const turnsInScope = (() => {
|
|
1354
|
+
if (atom.scope === "TONIGHT" || atom.scope === "TODAY") return [ctx.currentTurn];
|
|
1355
|
+
if (atom.scope === "YESTERDAY") return [ctx.currentTurn - 1];
|
|
1356
|
+
return null;
|
|
1357
|
+
})();
|
|
1358
|
+
const turnInScope = (turn) => {
|
|
1359
|
+
if (typeof turn !== "number") return false;
|
|
1360
|
+
if (turnsInScope === null) return true;
|
|
1361
|
+
return turnsInScope.includes(turn);
|
|
1362
|
+
};
|
|
1363
|
+
if (atom.action === "KILLED") {
|
|
1364
|
+
for (const seat of subjectSeats) {
|
|
1365
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1366
|
+
if (!player?.isDead) continue;
|
|
1367
|
+
if (!turnInScope(player.deathTurn)) continue;
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
return "no matching KILLED in scope";
|
|
1371
|
+
}
|
|
1372
|
+
if (atom.action === "EXECUTED") {
|
|
1373
|
+
for (const seat of subjectSeats) {
|
|
1374
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1375
|
+
if (!player?.isDead) continue;
|
|
1376
|
+
if (player.deathCause !== "EXECUTION") continue;
|
|
1377
|
+
if (!turnInScope(player.deathTurn)) continue;
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
return "no matching EXECUTED in scope";
|
|
1381
|
+
}
|
|
1382
|
+
void scoped;
|
|
1383
|
+
return `unknown action ${atom.action}`;
|
|
1300
1384
|
};
|
|
1301
|
-
var
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (
|
|
1310
|
-
if (
|
|
1311
|
-
if (
|
|
1385
|
+
var evalAtomCount = (atom, ctx) => {
|
|
1386
|
+
const ops = atom.scope === "GAME" ? ctx.allOperations : atom.scope === "NIGHT" ? ctx.tonightOperations : ctx.todayOperations;
|
|
1387
|
+
let resetTurn = -1;
|
|
1388
|
+
let resetTime = null;
|
|
1389
|
+
if (atom.resetOn?.length && ctx.effector?.isDead && atom.resetOn.includes("DEATH")) {
|
|
1390
|
+
resetTurn = ctx.effector.deathTurn ?? -1;
|
|
1391
|
+
}
|
|
1392
|
+
const used = ops.filter((op) => {
|
|
1393
|
+
if (op.abilityId !== ctx.abilityId) return false;
|
|
1394
|
+
if (op.effector !== ctx.effectorSeat) return false;
|
|
1395
|
+
if (resetTurn >= 0) {
|
|
1396
|
+
const t = ctx.operationTurnMap.get(op);
|
|
1397
|
+
if (typeof t !== "number") return false;
|
|
1398
|
+
if (t < resetTurn) return false;
|
|
1399
|
+
if (t === resetTurn) {
|
|
1400
|
+
const time = ctx.operationTimeMap.get(op);
|
|
1401
|
+
if (resetTime !== null && time !== resetTime) return false;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1312
1404
|
return true;
|
|
1313
1405
|
}).length;
|
|
1406
|
+
return compareCount2(used, atom.operator, atom.value) ? null : `count ${used} fails ${atom.operator} ${atom.value} in ${atom.scope}`;
|
|
1314
1407
|
};
|
|
1315
|
-
var
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1408
|
+
var evalAtomGlobalCount = (atom, ctx) => {
|
|
1409
|
+
let actual = 0;
|
|
1410
|
+
if (atom.subject === "ALIVE") actual = ctx.players.filter((p) => !p.isDead).length;
|
|
1411
|
+
else if (atom.subject === "DEAD") actual = ctx.players.filter((p) => p.isDead).length;
|
|
1412
|
+
else if (atom.subject === "NOMINATED_TODAY") actual = ctx.todayNominations.length;
|
|
1413
|
+
else if (atom.subject === "EXECUTED_TODAY") {
|
|
1414
|
+
actual = ctx.players.filter((p) => p.isDead && p.deathCause === "EXECUTION" && p.deathTurn === ctx.currentTurn).length;
|
|
1415
|
+
}
|
|
1416
|
+
return compareCount2(actual, atom.operator, atom.value) ? null : `global count ${atom.subject}=${actual} fails ${atom.operator} ${atom.value}`;
|
|
1417
|
+
};
|
|
1418
|
+
var evalAtom2 = (atom, ctx, customResolver) => {
|
|
1419
|
+
if (atom.type === "TIME") return evalAtomTime(atom, ctx);
|
|
1420
|
+
if (atom.type === "STATE") return evalAtomState(atom, ctx);
|
|
1421
|
+
if (atom.type === "ACTION") return evalAtomAction(atom, ctx);
|
|
1422
|
+
if (atom.type === "COUNT") return evalAtomCount(atom, ctx);
|
|
1423
|
+
if (atom.type === "GLOBAL_COUNT") return evalAtomGlobalCount(atom, ctx);
|
|
1424
|
+
if (atom.type === "CUSTOM") {
|
|
1425
|
+
if (!customResolver) return `no custom resolver registered for ${atom.resolverId}`;
|
|
1426
|
+
return customResolver({ resolverId: atom.resolverId, args: atom.args }, ctx) ? null : `custom resolver ${atom.resolverId} returned false`;
|
|
1427
|
+
}
|
|
1428
|
+
return "unknown atom";
|
|
1429
|
+
};
|
|
1430
|
+
var evalExpr2 = (expr, ctx, customResolver) => {
|
|
1431
|
+
if (expr.op === "AND") {
|
|
1432
|
+
for (const child of expr.children) {
|
|
1433
|
+
const reason = evalExpr2(child, ctx, customResolver);
|
|
1434
|
+
if (reason) return reason;
|
|
1435
|
+
}
|
|
1436
|
+
return null;
|
|
1337
1437
|
}
|
|
1338
|
-
|
|
1438
|
+
if (expr.op === "OR") {
|
|
1439
|
+
const reasons = [];
|
|
1440
|
+
for (const child of expr.children) {
|
|
1441
|
+
const reason = evalExpr2(child, ctx, customResolver);
|
|
1442
|
+
if (!reason) return null;
|
|
1443
|
+
reasons.push(reason);
|
|
1444
|
+
}
|
|
1445
|
+
return `OR: all branches failed (${reasons.join("; ")})`;
|
|
1446
|
+
}
|
|
1447
|
+
if (expr.op === "NOT") {
|
|
1448
|
+
const reason = evalExpr2(expr.child, ctx, customResolver);
|
|
1449
|
+
return reason ? null : "NOT: inner expression was true";
|
|
1450
|
+
}
|
|
1451
|
+
return evalAtom2(expr.atom, ctx, customResolver);
|
|
1339
1452
|
};
|
|
1340
1453
|
var canInvokeAbility = ({
|
|
1341
1454
|
ability,
|
|
1342
|
-
effector,
|
|
1343
|
-
currentTimelineIdx,
|
|
1344
1455
|
timelines,
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1456
|
+
candidate,
|
|
1457
|
+
initialPlayers,
|
|
1458
|
+
allAbilities,
|
|
1459
|
+
characters,
|
|
1460
|
+
customResolver
|
|
1349
1461
|
}) => {
|
|
1350
|
-
const
|
|
1351
|
-
if (!
|
|
1352
|
-
const
|
|
1353
|
-
if (
|
|
1354
|
-
return { allowed: false, reason:
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
if (turnError) return { allowed: false, reason: turnError };
|
|
1359
|
-
}
|
|
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
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
if (trigger.frequency) {
|
|
1383
|
-
const freqError = checkFrequency(
|
|
1384
|
-
trigger.frequency,
|
|
1385
|
-
ability,
|
|
1386
|
-
effector?.seat,
|
|
1387
|
-
timelines,
|
|
1388
|
-
currentTimelineIdx,
|
|
1389
|
-
priorOperations
|
|
1390
|
-
);
|
|
1391
|
-
if (freqError) return { allowed: false, reason: freqError };
|
|
1392
|
-
}
|
|
1393
|
-
return { allowed: true };
|
|
1462
|
+
const window = ability.triggerWindow;
|
|
1463
|
+
if (!window) return { allowed: true };
|
|
1464
|
+
const derived = deriveContext({ timelines, candidate, initialPlayers, allAbilities, characters });
|
|
1465
|
+
if ("error" in derived) {
|
|
1466
|
+
return { allowed: false, reason: derived.error };
|
|
1467
|
+
}
|
|
1468
|
+
const reason = evalExpr2(window, derived, customResolver);
|
|
1469
|
+
return reason ? { allowed: false, reason } : { allowed: true };
|
|
1394
1470
|
};
|
|
1395
1471
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1396
1472
|
0 && (module.exports = {
|
|
@@ -1400,11 +1476,13 @@ var canInvokeAbility = ({
|
|
|
1400
1476
|
buildPlayerSeatMap,
|
|
1401
1477
|
canInvokeAbility,
|
|
1402
1478
|
copyPlayers,
|
|
1479
|
+
deriveContext,
|
|
1403
1480
|
getSourceValue,
|
|
1404
1481
|
getValueFromPayloads,
|
|
1405
1482
|
isNumberOrNumberArray,
|
|
1406
1483
|
isPlayerReminderArray,
|
|
1407
1484
|
isReminder,
|
|
1408
1485
|
resolveSourceValue,
|
|
1486
|
+
resolveTargetRef,
|
|
1409
1487
|
transformEmptyArray
|
|
1410
1488
|
});
|