@bct-app/game-engine 0.1.8 → 0.1.10
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 +303 -225
- package/dist/index.mjs +301 -225
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -139,6 +139,47 @@ var resolveSourceValue = (source, writableInputs, payloads, effector, snapshotSe
|
|
|
139
139
|
}
|
|
140
140
|
return void 0;
|
|
141
141
|
};
|
|
142
|
+
var resolveTargetRef = ({
|
|
143
|
+
ref,
|
|
144
|
+
players,
|
|
145
|
+
effectorSeat,
|
|
146
|
+
characterMap
|
|
147
|
+
}) => {
|
|
148
|
+
if (ref.ref === "ANY") return players;
|
|
149
|
+
if (ref.ref === "EFFECTOR") {
|
|
150
|
+
if (effectorSeat === void 0) return [];
|
|
151
|
+
return players.filter((p) => p.seat === effectorSeat);
|
|
152
|
+
}
|
|
153
|
+
if (ref.ref === "ROLE") {
|
|
154
|
+
return players.filter((p) => p.characterId === ref.characterId);
|
|
155
|
+
}
|
|
156
|
+
if (ref.ref === "KIND") {
|
|
157
|
+
if (!characterMap) return [];
|
|
158
|
+
return players.filter((p) => characterMap.get(p.characterId)?.kind === ref.kind);
|
|
159
|
+
}
|
|
160
|
+
if (ref.ref === "ALIGNMENT") {
|
|
161
|
+
return players.filter((p) => p.alignment === ref.alignment);
|
|
162
|
+
}
|
|
163
|
+
if (ref.ref === "NEIGHBOR") {
|
|
164
|
+
if (effectorSeat === void 0 || players.length === 0) return [];
|
|
165
|
+
const alive = players.filter((p) => !p.isDead).sort((a, b) => a.seat - b.seat);
|
|
166
|
+
if (alive.length === 0) return [];
|
|
167
|
+
const idx = alive.findIndex((p) => p.seat === effectorSeat);
|
|
168
|
+
if (idx < 0) return [];
|
|
169
|
+
const left = alive[(idx - 1 + alive.length) % alive.length];
|
|
170
|
+
const right = alive[(idx + 1) % alive.length];
|
|
171
|
+
if (ref.side === "LEFT") return left ? [left] : [];
|
|
172
|
+
if (ref.side === "RIGHT") return right ? [right] : [];
|
|
173
|
+
const both = [left, right].filter((p) => Boolean(p));
|
|
174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
175
|
+
return both.filter((p) => {
|
|
176
|
+
if (seen.has(p.seat)) return false;
|
|
177
|
+
seen.add(p.seat);
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return [];
|
|
182
|
+
};
|
|
142
183
|
|
|
143
184
|
// src/effects/handler-types.ts
|
|
144
185
|
var createEffectHandler = (type, handler) => {
|
|
@@ -396,17 +437,31 @@ var applySeatChange = createEffectHandler("SEAT_CHANGE", () => {
|
|
|
396
437
|
});
|
|
397
438
|
|
|
398
439
|
// src/effects/handlers/status-change.ts
|
|
399
|
-
var applyStatusChange = createEffectHandler("STATUS_CHANGE", ({
|
|
440
|
+
var applyStatusChange = createEffectHandler("STATUS_CHANGE", ({
|
|
441
|
+
effect,
|
|
442
|
+
payloads,
|
|
443
|
+
makePlayersEffect,
|
|
444
|
+
operationInTimelineIdx,
|
|
445
|
+
abilityMap,
|
|
446
|
+
operation
|
|
447
|
+
}) => {
|
|
400
448
|
const maybeStatus = getSourceValue(effect.source, payloads);
|
|
401
449
|
if (maybeStatus === "DEAD") {
|
|
450
|
+
const ability = abilityMap.get(operation.abilityId);
|
|
451
|
+
const turn = operationInTimelineIdx >= 0 ? operationInTimelineIdx : 0;
|
|
452
|
+
const cause = effect.deathCause ?? (ability?.category === "STORYTELLER" ? "STORYTELLER" : "ABILITY");
|
|
402
453
|
makePlayersEffect.forEach((player) => {
|
|
403
454
|
player.isDead = true;
|
|
455
|
+
player.deathCause = cause;
|
|
456
|
+
player.deathTurn = turn;
|
|
404
457
|
});
|
|
405
458
|
return;
|
|
406
459
|
}
|
|
407
460
|
if (maybeStatus === "ALIVE") {
|
|
408
461
|
makePlayersEffect.forEach((player) => {
|
|
409
462
|
player.isDead = false;
|
|
463
|
+
delete player.deathCause;
|
|
464
|
+
delete player.deathTurn;
|
|
410
465
|
});
|
|
411
466
|
return;
|
|
412
467
|
}
|
|
@@ -494,22 +549,22 @@ var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payload
|
|
|
494
549
|
if (scope === "LEFT_SIDE") {
|
|
495
550
|
const left2 = [];
|
|
496
551
|
for (let step = 1; step < total; step += 1) {
|
|
497
|
-
left2.push(sortedPlayers[(anchorIdx
|
|
552
|
+
left2.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
498
553
|
}
|
|
499
554
|
return left2;
|
|
500
555
|
}
|
|
501
556
|
if (scope === "RIGHT_SIDE") {
|
|
502
557
|
const right2 = [];
|
|
503
558
|
for (let step = 1; step < total; step += 1) {
|
|
504
|
-
right2.push(sortedPlayers[(anchorIdx +
|
|
559
|
+
right2.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
505
560
|
}
|
|
506
561
|
return right2;
|
|
507
562
|
}
|
|
508
563
|
const left = [];
|
|
509
564
|
const right = [];
|
|
510
565
|
for (let step = 1; step < total; step += 1) {
|
|
511
|
-
left.push(sortedPlayers[(anchorIdx
|
|
512
|
-
right.push(sortedPlayers[(anchorIdx +
|
|
566
|
+
left.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
567
|
+
right.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
513
568
|
}
|
|
514
569
|
return [...left, ...right];
|
|
515
570
|
};
|
|
@@ -1095,105 +1150,17 @@ var applyOperationToPlayers = ({
|
|
|
1095
1150
|
};
|
|
1096
1151
|
|
|
1097
1152
|
// src/can-invoke-ability.ts
|
|
1098
|
-
var
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
};
|
|
1102
|
-
var checkTurns = (turns, currentTurn) => {
|
|
1103
|
-
if (turns.only && turns.only.length > 0 && !turns.only.includes(currentTurn)) {
|
|
1104
|
-
return `turn ${currentTurn} not in allowed turns ${JSON.stringify(turns.only)}`;
|
|
1105
|
-
}
|
|
1106
|
-
if (typeof turns.from === "number" && currentTurn < turns.from) {
|
|
1107
|
-
return `turn ${currentTurn} is before allowed start turn ${turns.from}`;
|
|
1108
|
-
}
|
|
1109
|
-
if (typeof turns.to === "number" && currentTurn > turns.to) {
|
|
1110
|
-
return `turn ${currentTurn} is after allowed end turn ${turns.to}`;
|
|
1111
|
-
}
|
|
1112
|
-
return null;
|
|
1113
|
-
};
|
|
1114
|
-
var checkEffectorState = (state, effector) => {
|
|
1115
|
-
if (state === "ANY") return null;
|
|
1116
|
-
if (state === "ALIVE") {
|
|
1117
|
-
if (!effector || effector.isDead) return "effector is not alive";
|
|
1118
|
-
return null;
|
|
1119
|
-
}
|
|
1120
|
-
if (state === "DEAD") {
|
|
1121
|
-
if (!effector || !effector.isDead) return "effector is not dead";
|
|
1122
|
-
return null;
|
|
1123
|
-
}
|
|
1124
|
-
return null;
|
|
1153
|
+
var TIME_TO_PHASE = {
|
|
1154
|
+
night: "NIGHT",
|
|
1155
|
+
day: "DAY"
|
|
1125
1156
|
};
|
|
1126
|
-
var
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1157
|
+
var checkTurnRange = (range, turn) => {
|
|
1158
|
+
if (!range) return true;
|
|
1159
|
+
if (range.only && range.only.length > 0 && !range.only.includes(turn)) return false;
|
|
1160
|
+
if (typeof range.from === "number" && turn < range.from) return false;
|
|
1161
|
+
if (typeof range.to === "number" && turn > range.to) return false;
|
|
1130
1162
|
return true;
|
|
1131
1163
|
};
|
|
1132
|
-
var checkPhaseTrigger = (trigger, timelines, currentIdx) => {
|
|
1133
|
-
const expected = PHASE_ALIASES[trigger.phase];
|
|
1134
|
-
const current = timelines[currentIdx]?.time;
|
|
1135
|
-
if (!expected) {
|
|
1136
|
-
return `phase ${trigger.phase} not yet supported in timeline model`;
|
|
1137
|
-
}
|
|
1138
|
-
if (expected !== current) {
|
|
1139
|
-
return `phase ${current} does not match required ${trigger.phase}`;
|
|
1140
|
-
}
|
|
1141
|
-
const occurrence = trigger.occurrence ?? "EVERY";
|
|
1142
|
-
if (occurrence === "EVERY") return null;
|
|
1143
|
-
const isFirst = isFirstPhaseForEffector(timelines, currentIdx, expected);
|
|
1144
|
-
if (occurrence === "FIRST_ONLY" && !isFirst) {
|
|
1145
|
-
return `phase ${trigger.phase} occurrence FIRST_ONLY but this is not the first`;
|
|
1146
|
-
}
|
|
1147
|
-
if (occurrence === "EXCEPT_FIRST" && isFirst) {
|
|
1148
|
-
return `phase ${trigger.phase} occurrence EXCEPT_FIRST but this is the first`;
|
|
1149
|
-
}
|
|
1150
|
-
return null;
|
|
1151
|
-
};
|
|
1152
|
-
var subjectMatches = (filter, seat, effectorSeat, players) => {
|
|
1153
|
-
if (filter.match === "ANY") return true;
|
|
1154
|
-
if (filter.match === "EFFECTOR") {
|
|
1155
|
-
return seat !== void 0 && seat === effectorSeat;
|
|
1156
|
-
}
|
|
1157
|
-
if (filter.match === "NEIGHBOR_OF_EFFECTOR") {
|
|
1158
|
-
if (seat === void 0 || effectorSeat === void 0 || !players?.length) return false;
|
|
1159
|
-
const alive = players.filter((p) => !p.isDead).sort((a, b) => a.seat - b.seat);
|
|
1160
|
-
const idx = alive.findIndex((p) => p.seat === effectorSeat);
|
|
1161
|
-
if (idx < 0) return false;
|
|
1162
|
-
const left = alive[(idx - 1 + alive.length) % alive.length]?.seat;
|
|
1163
|
-
const right = alive[(idx + 1) % alive.length]?.seat;
|
|
1164
|
-
return seat === left || seat === right;
|
|
1165
|
-
}
|
|
1166
|
-
if (filter.match === "ROLE") {
|
|
1167
|
-
const player = players?.find((p) => p.seat === seat);
|
|
1168
|
-
return !!player && filter.roles.includes(player.characterId);
|
|
1169
|
-
}
|
|
1170
|
-
if (filter.match === "KIND") {
|
|
1171
|
-
return false;
|
|
1172
|
-
}
|
|
1173
|
-
if (filter.match === "ALIGNMENT") {
|
|
1174
|
-
const player = players?.find((p) => p.seat === seat);
|
|
1175
|
-
if (!player?.alignment) return false;
|
|
1176
|
-
const want = filter.value === "good" ? "GOOD" : "EVIL";
|
|
1177
|
-
return player.alignment === want;
|
|
1178
|
-
}
|
|
1179
|
-
return false;
|
|
1180
|
-
};
|
|
1181
|
-
var checkEventTrigger = (trigger, event, effectorSeat, players, currentTime) => {
|
|
1182
|
-
if (!event) return "event trigger requires event context";
|
|
1183
|
-
if (event.event !== trigger.event) return `event ${event.event} does not match required ${trigger.event}`;
|
|
1184
|
-
if (trigger.phase) {
|
|
1185
|
-
const expected = PHASE_ALIASES[trigger.phase];
|
|
1186
|
-
if (!expected) return `phase ${trigger.phase} not yet supported in timeline model`;
|
|
1187
|
-
if (expected !== currentTime) return `phase ${currentTime} does not match required ${trigger.phase}`;
|
|
1188
|
-
}
|
|
1189
|
-
if (trigger.target && !subjectMatches(trigger.target, event.targetSeat, effectorSeat, players)) {
|
|
1190
|
-
return `event target does not match filter ${trigger.target.match}`;
|
|
1191
|
-
}
|
|
1192
|
-
if (trigger.actor && !subjectMatches(trigger.actor, event.actorSeat, effectorSeat, players)) {
|
|
1193
|
-
return `event actor does not match filter ${trigger.actor.match}`;
|
|
1194
|
-
}
|
|
1195
|
-
return null;
|
|
1196
|
-
};
|
|
1197
1164
|
var compareCount2 = (actual, operator, expected) => {
|
|
1198
1165
|
switch (operator) {
|
|
1199
1166
|
case "<":
|
|
@@ -1210,149 +1177,256 @@ var compareCount2 = (actual, operator, expected) => {
|
|
|
1210
1177
|
return actual !== expected;
|
|
1211
1178
|
}
|
|
1212
1179
|
};
|
|
1213
|
-
var
|
|
1214
|
-
const
|
|
1215
|
-
if (
|
|
1216
|
-
let
|
|
1217
|
-
|
|
1218
|
-
if (tl.turn !== turn || tl.time !== "day") continue;
|
|
1219
|
-
for (const op of tl.operations ?? []) {
|
|
1220
|
-
if (op.kind === "execution") count++;
|
|
1221
|
-
}
|
|
1180
|
+
var isFirstOfPhase = (timelines, lastIdx) => {
|
|
1181
|
+
const last = timelines[lastIdx];
|
|
1182
|
+
if (!last) return false;
|
|
1183
|
+
for (let i = 0; i < lastIdx; i++) {
|
|
1184
|
+
if (timelines[i]?.time === last.time) return false;
|
|
1222
1185
|
}
|
|
1223
|
-
return
|
|
1186
|
+
return true;
|
|
1224
1187
|
};
|
|
1225
|
-
var
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1188
|
+
var deriveContext = ({
|
|
1189
|
+
timelines,
|
|
1190
|
+
candidate,
|
|
1191
|
+
initialPlayers,
|
|
1192
|
+
allAbilities,
|
|
1193
|
+
characters
|
|
1194
|
+
}) => {
|
|
1195
|
+
const lastIdx = timelines.length - 1;
|
|
1196
|
+
const last = timelines[lastIdx];
|
|
1197
|
+
if (!last) {
|
|
1198
|
+
return { error: "timelines must contain at least one entry" };
|
|
1199
|
+
}
|
|
1200
|
+
const players = applyOperationToPlayers({
|
|
1201
|
+
players: initialPlayers,
|
|
1202
|
+
operations: timelines.flatMap((tl) => tl.operations ?? []),
|
|
1203
|
+
allAbilities,
|
|
1204
|
+
characters,
|
|
1205
|
+
timelines,
|
|
1206
|
+
timelineIndexAtNow: lastIdx
|
|
1207
|
+
});
|
|
1208
|
+
const effector = players.find((p) => p.seat === candidate.effector);
|
|
1209
|
+
const operationTurnMap = /* @__PURE__ */ new Map();
|
|
1210
|
+
const operationTimeMap = /* @__PURE__ */ new Map();
|
|
1211
|
+
timelines.forEach((tl) => {
|
|
1212
|
+
tl.operations?.forEach((op) => {
|
|
1213
|
+
operationTurnMap.set(op, tl.turn);
|
|
1214
|
+
operationTimeMap.set(op, tl.time);
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
const allOperations = timelines.flatMap((tl) => tl.operations ?? []);
|
|
1218
|
+
const todayOperations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "day").flatMap((tl) => tl.operations ?? []);
|
|
1219
|
+
const tonightOperations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "night").flatMap((tl) => tl.operations ?? []);
|
|
1220
|
+
const yesterdayDayOperations = timelines.filter((tl) => tl.turn === last.turn - 1 && tl.time === "day").flatMap((tl) => tl.operations ?? []);
|
|
1221
|
+
const yesterdayNightOperations = timelines.filter((tl) => tl.turn === last.turn - 1 && tl.time === "night").flatMap((tl) => tl.operations ?? []);
|
|
1222
|
+
const todayNominations = timelines.filter((tl) => tl.turn === last.turn && tl.time === "day").flatMap((tl) => tl.nominations ?? []).map((n) => ({ nominator: n.nominator, nominee: n.nominee }));
|
|
1223
|
+
return {
|
|
1224
|
+
currentTurn: last.turn,
|
|
1225
|
+
currentPhase: TIME_TO_PHASE[last.time],
|
|
1226
|
+
isFirstOfPhase: isFirstOfPhase(timelines, lastIdx),
|
|
1227
|
+
players,
|
|
1228
|
+
allOperations,
|
|
1229
|
+
todayOperations,
|
|
1230
|
+
tonightOperations,
|
|
1231
|
+
yesterdayDayOperations,
|
|
1232
|
+
yesterdayNightOperations,
|
|
1233
|
+
todayNominations,
|
|
1234
|
+
effectorSeat: candidate.effector,
|
|
1235
|
+
effector,
|
|
1236
|
+
abilityId: candidate.abilityId,
|
|
1237
|
+
characterMap: new Map(characters.map((c) => [c.id, c])),
|
|
1238
|
+
operationTurnMap,
|
|
1239
|
+
operationTimeMap
|
|
1240
|
+
};
|
|
1241
|
+
};
|
|
1242
|
+
var operationsForActionScope = (scope, ctx) => {
|
|
1243
|
+
if (scope === "TONIGHT") return ctx.tonightOperations;
|
|
1244
|
+
if (scope === "TODAY") return ctx.todayOperations;
|
|
1245
|
+
if (scope === "YESTERDAY") {
|
|
1246
|
+
return [...ctx.yesterdayDayOperations, ...ctx.yesterdayNightOperations];
|
|
1241
1247
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1248
|
+
return ctx.allOperations;
|
|
1249
|
+
};
|
|
1250
|
+
var seatsMatchingTargetRef = (ref, ctx) => {
|
|
1251
|
+
const matched = resolveTargetRef({
|
|
1252
|
+
ref,
|
|
1253
|
+
players: ctx.players,
|
|
1254
|
+
effectorSeat: ctx.effectorSeat,
|
|
1255
|
+
characterMap: ctx.characterMap
|
|
1256
|
+
});
|
|
1257
|
+
return new Set(matched.map((p) => p.seat));
|
|
1258
|
+
};
|
|
1259
|
+
var evalAtomTime = (atom, ctx) => {
|
|
1260
|
+
if (atom.phase !== ctx.currentPhase) {
|
|
1261
|
+
return `phase is ${ctx.currentPhase}, required ${atom.phase}`;
|
|
1244
1262
|
}
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
const matched = customConditionResolver({ key: `STATUS:${condition.status}`, value: !condition.negate });
|
|
1248
|
-
return matched ? null : `condition EFFECTOR_HAS_STATUS ${condition.status} unsatisfied`;
|
|
1263
|
+
if (atom.occurrence === "FIRST" && !ctx.isFirstOfPhase) {
|
|
1264
|
+
return `expected FIRST occurrence of ${atom.phase} but is not the first`;
|
|
1249
1265
|
}
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1266
|
+
if (atom.occurrence === "EXCEPT_FIRST" && ctx.isFirstOfPhase) {
|
|
1267
|
+
return `expected EXCEPT_FIRST occurrence of ${atom.phase} but is the first`;
|
|
1268
|
+
}
|
|
1269
|
+
if (!checkTurnRange(atom.turns, ctx.currentTurn)) {
|
|
1270
|
+
return `turn ${ctx.currentTurn} not in allowed turn range`;
|
|
1271
|
+
}
|
|
1272
|
+
return null;
|
|
1273
|
+
};
|
|
1274
|
+
var evalAtomState = (atom, ctx) => {
|
|
1275
|
+
const seats = seatsMatchingTargetRef(atom.subject, ctx);
|
|
1276
|
+
if (seats.size === 0) return `no players match subject ${atom.subject.ref}`;
|
|
1277
|
+
for (const seat of seats) {
|
|
1278
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1279
|
+
if (!player) continue;
|
|
1280
|
+
const isDead = Boolean(player.isDead);
|
|
1281
|
+
if (atom.aliveness === "ALIVE" && isDead) continue;
|
|
1282
|
+
if (atom.aliveness === "DEAD" && !isDead) continue;
|
|
1283
|
+
if (atom.aliveness === "DEAD") {
|
|
1284
|
+
if (atom.deathCause && player.deathCause !== atom.deathCause) continue;
|
|
1285
|
+
if (atom.deathTurn && !checkTurnRange(atom.deathTurn, player.deathTurn ?? -1)) continue;
|
|
1254
1286
|
}
|
|
1255
1287
|
return null;
|
|
1256
1288
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1289
|
+
return `no subject in ${atom.subject.ref} satisfies aliveness=${atom.aliveness}`;
|
|
1290
|
+
};
|
|
1291
|
+
var evalAtomAction = (atom, ctx) => {
|
|
1292
|
+
const subjectSeats = seatsMatchingTargetRef(atom.subject, ctx);
|
|
1293
|
+
if (subjectSeats.size === 0) return `no players match subject ${atom.subject.ref}`;
|
|
1294
|
+
const actorSeats = atom.actor ? seatsMatchingTargetRef(atom.actor, ctx) : null;
|
|
1295
|
+
if (atom.action === "NOMINATED") {
|
|
1296
|
+
const noms = ctx.todayNominations;
|
|
1297
|
+
const found = noms.some(
|
|
1298
|
+
(n) => subjectSeats.has(n.nominee) && (actorSeats === null || actorSeats.has(n.nominator))
|
|
1299
|
+
);
|
|
1300
|
+
return found ? null : "no matching NOMINATED event in today";
|
|
1301
|
+
}
|
|
1302
|
+
const scoped = operationsForActionScope(atom.scope, ctx);
|
|
1303
|
+
if (atom.action === "CHOSEN_AS_TARGET") {
|
|
1304
|
+
const found = scoped.some((op) => {
|
|
1305
|
+
if (actorSeats !== null && !actorSeats.has(op.effector)) return false;
|
|
1306
|
+
const payloadSeats = (op.payloads ?? []).flatMap(
|
|
1307
|
+
(v) => typeof v === "number" ? [v] : Array.isArray(v) ? v.filter((x) => typeof x === "number") : []
|
|
1308
|
+
);
|
|
1309
|
+
return payloadSeats.some((s) => subjectSeats.has(s));
|
|
1310
|
+
});
|
|
1311
|
+
return found ? null : "no matching CHOSEN_AS_TARGET in scope";
|
|
1260
1312
|
}
|
|
1261
|
-
|
|
1313
|
+
const turnsInScope = (() => {
|
|
1314
|
+
if (atom.scope === "TONIGHT" || atom.scope === "TODAY") return [ctx.currentTurn];
|
|
1315
|
+
if (atom.scope === "YESTERDAY") return [ctx.currentTurn - 1];
|
|
1316
|
+
return null;
|
|
1317
|
+
})();
|
|
1318
|
+
const turnInScope = (turn) => {
|
|
1319
|
+
if (typeof turn !== "number") return false;
|
|
1320
|
+
if (turnsInScope === null) return true;
|
|
1321
|
+
return turnsInScope.includes(turn);
|
|
1322
|
+
};
|
|
1323
|
+
if (atom.action === "KILLED") {
|
|
1324
|
+
for (const seat of subjectSeats) {
|
|
1325
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1326
|
+
if (!player?.isDead) continue;
|
|
1327
|
+
if (!turnInScope(player.deathTurn)) continue;
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
return "no matching KILLED in scope";
|
|
1331
|
+
}
|
|
1332
|
+
if (atom.action === "EXECUTED") {
|
|
1333
|
+
for (const seat of subjectSeats) {
|
|
1334
|
+
const player = ctx.players.find((p) => p.seat === seat);
|
|
1335
|
+
if (!player?.isDead) continue;
|
|
1336
|
+
if (player.deathCause !== "EXECUTION") continue;
|
|
1337
|
+
if (!turnInScope(player.deathTurn)) continue;
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
return "no matching EXECUTED in scope";
|
|
1341
|
+
}
|
|
1342
|
+
void scoped;
|
|
1343
|
+
return `unknown action ${atom.action}`;
|
|
1262
1344
|
};
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (
|
|
1272
|
-
if (
|
|
1273
|
-
if (
|
|
1345
|
+
var evalAtomCount = (atom, ctx) => {
|
|
1346
|
+
const ops = atom.scope === "GAME" ? ctx.allOperations : atom.scope === "NIGHT" ? ctx.tonightOperations : ctx.todayOperations;
|
|
1347
|
+
let resetTurn = -1;
|
|
1348
|
+
let resetTime = null;
|
|
1349
|
+
if (atom.resetOn?.length && ctx.effector?.isDead && atom.resetOn.includes("DEATH")) {
|
|
1350
|
+
resetTurn = ctx.effector.deathTurn ?? -1;
|
|
1351
|
+
}
|
|
1352
|
+
const used = ops.filter((op) => {
|
|
1353
|
+
if (op.abilityId !== ctx.abilityId) return false;
|
|
1354
|
+
if (op.effector !== ctx.effectorSeat) return false;
|
|
1355
|
+
if (resetTurn >= 0) {
|
|
1356
|
+
const t = ctx.operationTurnMap.get(op);
|
|
1357
|
+
if (typeof t !== "number") return false;
|
|
1358
|
+
if (t < resetTurn) return false;
|
|
1359
|
+
if (t === resetTurn) {
|
|
1360
|
+
const time = ctx.operationTimeMap.get(op);
|
|
1361
|
+
if (resetTime !== null && time !== resetTime) return false;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1274
1364
|
return true;
|
|
1275
1365
|
}).length;
|
|
1366
|
+
return compareCount2(used, atom.operator, atom.value) ? null : `count ${used} fails ${atom.operator} ${atom.value} in ${atom.scope}`;
|
|
1276
1367
|
};
|
|
1277
|
-
var
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1368
|
+
var evalAtomGlobalCount = (atom, ctx) => {
|
|
1369
|
+
let actual = 0;
|
|
1370
|
+
if (atom.subject === "ALIVE") actual = ctx.players.filter((p) => !p.isDead).length;
|
|
1371
|
+
else if (atom.subject === "DEAD") actual = ctx.players.filter((p) => p.isDead).length;
|
|
1372
|
+
else if (atom.subject === "NOMINATED_TODAY") actual = ctx.todayNominations.length;
|
|
1373
|
+
else if (atom.subject === "EXECUTED_TODAY") {
|
|
1374
|
+
actual = ctx.players.filter((p) => p.isDead && p.deathCause === "EXECUTION" && p.deathTurn === ctx.currentTurn).length;
|
|
1375
|
+
}
|
|
1376
|
+
return compareCount2(actual, atom.operator, atom.value) ? null : `global count ${atom.subject}=${actual} fails ${atom.operator} ${atom.value}`;
|
|
1377
|
+
};
|
|
1378
|
+
var evalAtom2 = (atom, ctx, customResolver) => {
|
|
1379
|
+
if (atom.type === "TIME") return evalAtomTime(atom, ctx);
|
|
1380
|
+
if (atom.type === "STATE") return evalAtomState(atom, ctx);
|
|
1381
|
+
if (atom.type === "ACTION") return evalAtomAction(atom, ctx);
|
|
1382
|
+
if (atom.type === "COUNT") return evalAtomCount(atom, ctx);
|
|
1383
|
+
if (atom.type === "GLOBAL_COUNT") return evalAtomGlobalCount(atom, ctx);
|
|
1384
|
+
if (atom.type === "CUSTOM") {
|
|
1385
|
+
if (!customResolver) return `no custom resolver registered for ${atom.resolverId}`;
|
|
1386
|
+
return customResolver({ resolverId: atom.resolverId, args: atom.args }, ctx) ? null : `custom resolver ${atom.resolverId} returned false`;
|
|
1387
|
+
}
|
|
1388
|
+
return "unknown atom";
|
|
1389
|
+
};
|
|
1390
|
+
var evalExpr2 = (expr, ctx, customResolver) => {
|
|
1391
|
+
if (expr.op === "AND") {
|
|
1392
|
+
for (const child of expr.children) {
|
|
1393
|
+
const reason = evalExpr2(child, ctx, customResolver);
|
|
1394
|
+
if (reason) return reason;
|
|
1395
|
+
}
|
|
1396
|
+
return null;
|
|
1299
1397
|
}
|
|
1300
|
-
|
|
1398
|
+
if (expr.op === "OR") {
|
|
1399
|
+
const reasons = [];
|
|
1400
|
+
for (const child of expr.children) {
|
|
1401
|
+
const reason = evalExpr2(child, ctx, customResolver);
|
|
1402
|
+
if (!reason) return null;
|
|
1403
|
+
reasons.push(reason);
|
|
1404
|
+
}
|
|
1405
|
+
return `OR: all branches failed (${reasons.join("; ")})`;
|
|
1406
|
+
}
|
|
1407
|
+
if (expr.op === "NOT") {
|
|
1408
|
+
const reason = evalExpr2(expr.child, ctx, customResolver);
|
|
1409
|
+
return reason ? null : "NOT: inner expression was true";
|
|
1410
|
+
}
|
|
1411
|
+
return evalAtom2(expr.atom, ctx, customResolver);
|
|
1301
1412
|
};
|
|
1302
1413
|
var canInvokeAbility = ({
|
|
1303
1414
|
ability,
|
|
1304
|
-
effector,
|
|
1305
|
-
currentTimelineIdx,
|
|
1306
1415
|
timelines,
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1416
|
+
candidate,
|
|
1417
|
+
initialPlayers,
|
|
1418
|
+
allAbilities,
|
|
1419
|
+
characters,
|
|
1420
|
+
customResolver
|
|
1311
1421
|
}) => {
|
|
1312
|
-
const
|
|
1313
|
-
if (!
|
|
1314
|
-
const
|
|
1315
|
-
if (
|
|
1316
|
-
return { allowed: false, reason:
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (turnError) return { allowed: false, reason: turnError };
|
|
1321
|
-
}
|
|
1322
|
-
const effectorStateError = checkEffectorState(trigger.effectorState ?? "ALIVE", effector);
|
|
1323
|
-
if (effectorStateError) return { allowed: false, reason: effectorStateError };
|
|
1324
|
-
const spec = trigger.trigger;
|
|
1325
|
-
if (spec.kind === "PHASE") {
|
|
1326
|
-
const phaseError = checkPhaseTrigger(spec, timelines, currentTimelineIdx);
|
|
1327
|
-
if (phaseError) return { allowed: false, reason: phaseError };
|
|
1328
|
-
} else if (spec.kind === "EVENT") {
|
|
1329
|
-
const eventError = checkEventTrigger(spec, event, effector?.seat, players, currentTimeline.time);
|
|
1330
|
-
if (eventError) return { allowed: false, reason: eventError };
|
|
1331
|
-
}
|
|
1332
|
-
if (trigger.conditions && trigger.conditions.length > 0) {
|
|
1333
|
-
for (const condition of trigger.conditions) {
|
|
1334
|
-
const condError = checkCondition(condition, {
|
|
1335
|
-
timelines,
|
|
1336
|
-
currentIdx: currentTimelineIdx,
|
|
1337
|
-
players,
|
|
1338
|
-
event,
|
|
1339
|
-
customConditionResolver
|
|
1340
|
-
});
|
|
1341
|
-
if (condError) return { allowed: false, reason: condError };
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
if (trigger.frequency) {
|
|
1345
|
-
const freqError = checkFrequency(
|
|
1346
|
-
trigger.frequency,
|
|
1347
|
-
ability,
|
|
1348
|
-
effector?.seat,
|
|
1349
|
-
timelines,
|
|
1350
|
-
currentTimelineIdx,
|
|
1351
|
-
priorOperations
|
|
1352
|
-
);
|
|
1353
|
-
if (freqError) return { allowed: false, reason: freqError };
|
|
1354
|
-
}
|
|
1355
|
-
return { allowed: true };
|
|
1422
|
+
const window = ability.triggerWindow;
|
|
1423
|
+
if (!window) return { allowed: true };
|
|
1424
|
+
const derived = deriveContext({ timelines, candidate, initialPlayers, allAbilities, characters });
|
|
1425
|
+
if ("error" in derived) {
|
|
1426
|
+
return { allowed: false, reason: derived.error };
|
|
1427
|
+
}
|
|
1428
|
+
const reason = evalExpr2(window, derived, customResolver);
|
|
1429
|
+
return reason ? { allowed: false, reason } : { allowed: true };
|
|
1356
1430
|
};
|
|
1357
1431
|
export {
|
|
1358
1432
|
adjustValueAsNumber,
|
|
@@ -1361,11 +1435,13 @@ export {
|
|
|
1361
1435
|
buildPlayerSeatMap,
|
|
1362
1436
|
canInvokeAbility,
|
|
1363
1437
|
copyPlayers,
|
|
1438
|
+
deriveContext,
|
|
1364
1439
|
getSourceValue,
|
|
1365
1440
|
getValueFromPayloads,
|
|
1366
1441
|
isNumberOrNumberArray,
|
|
1367
1442
|
isPlayerReminderArray,
|
|
1368
1443
|
isReminder,
|
|
1369
1444
|
resolveSourceValue,
|
|
1445
|
+
resolveTargetRef,
|
|
1370
1446
|
transformEmptyArray
|
|
1371
1447
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bct-app/game-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Game engine utilities for BCT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@bct-app/game-model": "0.1.
|
|
33
|
+
"@bct-app/game-model": "0.1.5"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@vitest/ui": "^4.1.3",
|