@bct-app/game-engine 0.1.15 → 0.1.16-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +185 -138
- package/dist/index.mjs +185 -138
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1078,13 +1078,7 @@ var isDeferredOperation = (operation, abilityMap) => {
|
|
|
1078
1078
|
const ability = abilityMap.get(operation.abilityId);
|
|
1079
1079
|
return ability?.executionTiming === "DEFER_TO_END";
|
|
1080
1080
|
};
|
|
1081
|
-
var
|
|
1082
|
-
return allAbilities.some((ability) => ability.effects.some((effect) => {
|
|
1083
|
-
if (!("lifetime" in effect)) return false;
|
|
1084
|
-
const lifetime = effect.lifetime;
|
|
1085
|
-
return Boolean(lifetime) && lifetime?.kind !== "PERMANENT";
|
|
1086
|
-
}));
|
|
1087
|
-
};
|
|
1081
|
+
var FIXED_POINT_MAX_ITERATIONS = 32;
|
|
1088
1082
|
var applyOperationToPlayers = ({
|
|
1089
1083
|
players,
|
|
1090
1084
|
operations,
|
|
@@ -1110,134 +1104,81 @@ var applyOperationToPlayers = ({
|
|
|
1110
1104
|
}
|
|
1111
1105
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1112
1106
|
});
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
const
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1148
|
-
let gatedTargets;
|
|
1149
|
-
if (precondition) {
|
|
1150
|
-
const cachedSeats = preconditionCache.get(operation)?.get(effectIdx);
|
|
1151
|
-
if (cachedSeats) {
|
|
1152
|
-
gatedTargets = resolvedTargets.filter((t) => cachedSeats.has(t.seat));
|
|
1153
|
-
} else {
|
|
1154
|
-
gatedTargets = evaluatePrecondition({
|
|
1155
|
-
expr: precondition,
|
|
1156
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1157
|
-
nowTimelineIndex,
|
|
1158
|
-
timelines,
|
|
1159
|
-
effector,
|
|
1160
|
-
targets: resolvedTargets,
|
|
1161
|
-
snapshotSeatMap: playerSeatMap,
|
|
1162
|
-
characterMap,
|
|
1163
|
-
payloads,
|
|
1164
|
-
customResolver
|
|
1165
|
-
});
|
|
1166
|
-
const opCache = preconditionCache.get(operation) ?? /* @__PURE__ */ new Map();
|
|
1167
|
-
opCache.set(effectIdx, new Set(gatedTargets.map((t) => t.seat)));
|
|
1168
|
-
preconditionCache.set(operation, opCache);
|
|
1169
|
-
}
|
|
1170
|
-
} else {
|
|
1171
|
-
gatedTargets = resolvedTargets;
|
|
1172
|
-
}
|
|
1173
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
const makePlayersEffect = evaluateLifetime({
|
|
1177
|
-
effect,
|
|
1178
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1179
|
-
nowTimelineIndex,
|
|
1180
|
-
timelines,
|
|
1181
|
-
effector,
|
|
1182
|
-
targets: gatedTargets,
|
|
1183
|
-
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
1184
|
-
characterMap,
|
|
1185
|
-
payloads,
|
|
1186
|
-
customResolver
|
|
1187
|
-
});
|
|
1188
|
-
const handler = effectHandlers[effect.type];
|
|
1189
|
-
if (!handler) {
|
|
1190
|
-
console.warn("Unknown effect type:", effect.type);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
const context = {
|
|
1194
|
-
effect,
|
|
1195
|
-
operation,
|
|
1196
|
-
payloads,
|
|
1197
|
-
effector,
|
|
1198
|
-
writableInputs,
|
|
1199
|
-
playerSeatMap,
|
|
1200
|
-
getSnapshotSeatMap,
|
|
1201
|
-
characterMap,
|
|
1202
|
-
abilityMap,
|
|
1203
|
-
nowTimelineIndex,
|
|
1204
|
-
operationInTimelineIdx,
|
|
1205
|
-
makePlayersEffect
|
|
1206
|
-
};
|
|
1207
|
-
handler(context);
|
|
1208
|
-
});
|
|
1209
|
-
});
|
|
1210
|
-
};
|
|
1211
|
-
const applyOps = (ops) => {
|
|
1212
|
-
const normalOps = [];
|
|
1213
|
-
const deferredOps = [];
|
|
1214
|
-
ops.forEach((operation) => {
|
|
1215
|
-
if (isDeferredOperation(operation, abilityMap)) {
|
|
1216
|
-
deferredOps.push(operation);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
normalOps.push(operation);
|
|
1220
|
-
});
|
|
1221
|
-
applyOpsSequence(normalOps);
|
|
1222
|
-
applyOpsSequence(deferredOps);
|
|
1107
|
+
const orderedOpsForTimeline = (timelineIdx) => {
|
|
1108
|
+
const ops = operationsByTimeline.get(timelineIdx) ?? [];
|
|
1109
|
+
const normal = [];
|
|
1110
|
+
const deferred = [];
|
|
1111
|
+
ops.forEach((op) => {
|
|
1112
|
+
if (isDeferredOperation(op, abilityMap)) deferred.push(op);
|
|
1113
|
+
else normal.push(op);
|
|
1114
|
+
});
|
|
1115
|
+
return [...normal, ...deferred];
|
|
1116
|
+
};
|
|
1117
|
+
const allOpsInOrder = [];
|
|
1118
|
+
allOpsInOrder.push(...orderedOpsForTimeline(-1));
|
|
1119
|
+
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1120
|
+
if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
|
|
1121
|
+
}
|
|
1122
|
+
const processedOps = /* @__PURE__ */ new Set();
|
|
1123
|
+
const records = [];
|
|
1124
|
+
const recordsByOp = /* @__PURE__ */ new Map();
|
|
1125
|
+
const applyRecordsForOp = (op, playersWithStatus) => {
|
|
1126
|
+
const opRecords = recordsByOp.get(op);
|
|
1127
|
+
if (!opRecords || opRecords.length === 0) return;
|
|
1128
|
+
const ability = abilityMap.get(op.abilityId);
|
|
1129
|
+
if (!ability) return;
|
|
1130
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1131
|
+
const effector = playerSeatMap.get(op.effector);
|
|
1132
|
+
const payloads = op.payloads || [];
|
|
1133
|
+
const writableInputs = ability.inputs.filter((part) => !part.readonly);
|
|
1134
|
+
let snapshotSeatMap = null;
|
|
1135
|
+
const getSnapshotSeatMap = () => {
|
|
1136
|
+
if (!snapshotSeatMap) {
|
|
1137
|
+
snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
|
|
1138
|
+
}
|
|
1139
|
+
return snapshotSeatMap;
|
|
1223
1140
|
};
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1141
|
+
opRecords.forEach((record) => {
|
|
1142
|
+
const effect = ability.effects[record.effectIdx];
|
|
1143
|
+
if (!effect) return;
|
|
1144
|
+
const handler = effectHandlers[effect.type];
|
|
1145
|
+
if (!handler) {
|
|
1146
|
+
console.warn("Unknown effect type:", effect.type);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const makePlayersEffect = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
|
|
1150
|
+
const context = {
|
|
1151
|
+
effect,
|
|
1152
|
+
operation: op,
|
|
1153
|
+
payloads,
|
|
1154
|
+
effector,
|
|
1155
|
+
writableInputs,
|
|
1156
|
+
playerSeatMap,
|
|
1157
|
+
getSnapshotSeatMap,
|
|
1158
|
+
characterMap,
|
|
1159
|
+
abilityMap,
|
|
1160
|
+
nowTimelineIndex,
|
|
1161
|
+
operationInTimelineIdx: record.operationInTimelineIdx,
|
|
1162
|
+
makePlayersEffect
|
|
1163
|
+
};
|
|
1164
|
+
handler(context);
|
|
1165
|
+
});
|
|
1166
|
+
};
|
|
1167
|
+
const rebuildState = () => {
|
|
1168
|
+
const playersWithStatus = copyPlayers(players);
|
|
1169
|
+
const settingsOps = orderedOpsForTimeline(-1);
|
|
1170
|
+
settingsOps.forEach((op) => {
|
|
1171
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1172
|
+
});
|
|
1228
1173
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1229
1174
|
const timeline = timelines[i];
|
|
1230
|
-
if (!timeline)
|
|
1231
|
-
continue;
|
|
1232
|
-
}
|
|
1175
|
+
if (!timeline) continue;
|
|
1233
1176
|
const nominations = timeline.nominations;
|
|
1234
1177
|
if (nominations && nominations.length > 0) {
|
|
1235
1178
|
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1236
1179
|
nominations.forEach((nomination) => {
|
|
1237
1180
|
const voterSeats = nomination.voterSeats;
|
|
1238
|
-
if (!voterSeats || voterSeats.length === 0)
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1181
|
+
if (!voterSeats || voterSeats.length === 0) return;
|
|
1241
1182
|
voterSeats.forEach((seat) => {
|
|
1242
1183
|
const player = playerSeatMap.get(seat);
|
|
1243
1184
|
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
@@ -1246,20 +1187,126 @@ var applyOperationToPlayers = ({
|
|
|
1246
1187
|
});
|
|
1247
1188
|
});
|
|
1248
1189
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
}
|
|
1190
|
+
orderedOpsForTimeline(i).forEach((op) => {
|
|
1191
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1192
|
+
});
|
|
1253
1193
|
}
|
|
1254
1194
|
return playersWithStatus;
|
|
1255
1195
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1196
|
+
const computeRecordsForOp = (op, state) => {
|
|
1197
|
+
const ability = abilityMap.get(op.abilityId);
|
|
1198
|
+
if (!ability) {
|
|
1199
|
+
console.warn("Ability not found for operation:", op);
|
|
1200
|
+
return [];
|
|
1201
|
+
}
|
|
1202
|
+
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1203
|
+
const effector = playerSeatMap.get(op.effector);
|
|
1204
|
+
const payloads = op.payloads || [];
|
|
1205
|
+
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1206
|
+
const newRecords = [];
|
|
1207
|
+
ability.effects.forEach((effect, effectIdx) => {
|
|
1208
|
+
const resolvedTargets = resolveEffectTargets({
|
|
1209
|
+
effect,
|
|
1210
|
+
operation: op,
|
|
1211
|
+
payloads,
|
|
1212
|
+
effector,
|
|
1213
|
+
playerSeatMap,
|
|
1214
|
+
characterMap
|
|
1215
|
+
});
|
|
1216
|
+
if (!resolvedTargets) return;
|
|
1217
|
+
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1218
|
+
const gatedTargets = precondition ? evaluatePrecondition({
|
|
1219
|
+
expr: precondition,
|
|
1220
|
+
operationTimelineIdx: operationInTimelineIdx,
|
|
1221
|
+
nowTimelineIndex,
|
|
1222
|
+
timelines,
|
|
1223
|
+
effector,
|
|
1224
|
+
targets: resolvedTargets,
|
|
1225
|
+
snapshotSeatMap: playerSeatMap,
|
|
1226
|
+
characterMap,
|
|
1227
|
+
payloads,
|
|
1228
|
+
customResolver
|
|
1229
|
+
}) : resolvedTargets;
|
|
1230
|
+
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const lifeTargets = evaluateLifetime({
|
|
1234
|
+
effect,
|
|
1235
|
+
operationTimelineIdx: operationInTimelineIdx,
|
|
1236
|
+
nowTimelineIndex,
|
|
1237
|
+
timelines,
|
|
1238
|
+
effector,
|
|
1239
|
+
targets: gatedTargets,
|
|
1240
|
+
snapshotSeatMap: playerSeatMap,
|
|
1241
|
+
characterMap,
|
|
1242
|
+
payloads,
|
|
1243
|
+
customResolver
|
|
1244
|
+
});
|
|
1245
|
+
newRecords.push({
|
|
1246
|
+
operation: op,
|
|
1247
|
+
effectIdx,
|
|
1248
|
+
operationInTimelineIdx,
|
|
1249
|
+
activeTargetSeats: lifeTargets.map((t) => t.seat)
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
return newRecords;
|
|
1253
|
+
};
|
|
1254
|
+
const reEvaluateLifetimes = (state) => {
|
|
1255
|
+
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1256
|
+
let anyShrunk = false;
|
|
1257
|
+
records.forEach((record) => {
|
|
1258
|
+
if (record.activeTargetSeats.length === 0) return;
|
|
1259
|
+
const ability = abilityMap.get(record.operation.abilityId);
|
|
1260
|
+
if (!ability) return;
|
|
1261
|
+
const effect = ability.effects[record.effectIdx];
|
|
1262
|
+
if (!effect) return;
|
|
1263
|
+
const effector = playerSeatMap.get(record.operation.effector);
|
|
1264
|
+
const payloads = record.operation.payloads || [];
|
|
1265
|
+
const targets = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
|
|
1266
|
+
const liveTargets = evaluateLifetime({
|
|
1267
|
+
effect,
|
|
1268
|
+
operationTimelineIdx: record.operationInTimelineIdx,
|
|
1269
|
+
nowTimelineIndex,
|
|
1270
|
+
timelines,
|
|
1271
|
+
effector,
|
|
1272
|
+
targets,
|
|
1273
|
+
snapshotSeatMap: playerSeatMap,
|
|
1274
|
+
characterMap,
|
|
1275
|
+
payloads,
|
|
1276
|
+
customResolver
|
|
1277
|
+
});
|
|
1278
|
+
const newSeats = liveTargets.map((t) => t.seat);
|
|
1279
|
+
if (newSeats.length < record.activeTargetSeats.length) {
|
|
1280
|
+
record.activeTargetSeats = newSeats;
|
|
1281
|
+
anyShrunk = true;
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
return anyShrunk;
|
|
1285
|
+
};
|
|
1286
|
+
let lastState = copyPlayers(players);
|
|
1287
|
+
allOpsInOrder.forEach((op) => {
|
|
1288
|
+
const newRecords = computeRecordsForOp(op, lastState);
|
|
1289
|
+
if (newRecords.length > 0) {
|
|
1290
|
+
records.push(...newRecords);
|
|
1291
|
+
const arr = recordsByOp.get(op) ?? [];
|
|
1292
|
+
arr.push(...newRecords);
|
|
1293
|
+
recordsByOp.set(op, arr);
|
|
1294
|
+
}
|
|
1295
|
+
processedOps.add(op);
|
|
1296
|
+
let state = rebuildState();
|
|
1297
|
+
let iter = 0;
|
|
1298
|
+
while (iter < FIXED_POINT_MAX_ITERATIONS) {
|
|
1299
|
+
const shrunk = reEvaluateLifetimes(state);
|
|
1300
|
+
if (!shrunk) break;
|
|
1301
|
+
state = rebuildState();
|
|
1302
|
+
iter += 1;
|
|
1303
|
+
}
|
|
1304
|
+
if (iter >= FIXED_POINT_MAX_ITERATIONS) {
|
|
1305
|
+
console.warn("applyOperationToPlayers: lifetime fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
|
|
1306
|
+
}
|
|
1307
|
+
lastState = state;
|
|
1308
|
+
});
|
|
1309
|
+
return transformEmptyArray(lastState);
|
|
1263
1310
|
};
|
|
1264
1311
|
|
|
1265
1312
|
// src/can-invoke-ability.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1038,13 +1038,7 @@ var isDeferredOperation = (operation, abilityMap) => {
|
|
|
1038
1038
|
const ability = abilityMap.get(operation.abilityId);
|
|
1039
1039
|
return ability?.executionTiming === "DEFER_TO_END";
|
|
1040
1040
|
};
|
|
1041
|
-
var
|
|
1042
|
-
return allAbilities.some((ability) => ability.effects.some((effect) => {
|
|
1043
|
-
if (!("lifetime" in effect)) return false;
|
|
1044
|
-
const lifetime = effect.lifetime;
|
|
1045
|
-
return Boolean(lifetime) && lifetime?.kind !== "PERMANENT";
|
|
1046
|
-
}));
|
|
1047
|
-
};
|
|
1041
|
+
var FIXED_POINT_MAX_ITERATIONS = 32;
|
|
1048
1042
|
var applyOperationToPlayers = ({
|
|
1049
1043
|
players,
|
|
1050
1044
|
operations,
|
|
@@ -1070,134 +1064,81 @@ var applyOperationToPlayers = ({
|
|
|
1070
1064
|
}
|
|
1071
1065
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1072
1066
|
});
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
const
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1108
|
-
let gatedTargets;
|
|
1109
|
-
if (precondition) {
|
|
1110
|
-
const cachedSeats = preconditionCache.get(operation)?.get(effectIdx);
|
|
1111
|
-
if (cachedSeats) {
|
|
1112
|
-
gatedTargets = resolvedTargets.filter((t) => cachedSeats.has(t.seat));
|
|
1113
|
-
} else {
|
|
1114
|
-
gatedTargets = evaluatePrecondition({
|
|
1115
|
-
expr: precondition,
|
|
1116
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1117
|
-
nowTimelineIndex,
|
|
1118
|
-
timelines,
|
|
1119
|
-
effector,
|
|
1120
|
-
targets: resolvedTargets,
|
|
1121
|
-
snapshotSeatMap: playerSeatMap,
|
|
1122
|
-
characterMap,
|
|
1123
|
-
payloads,
|
|
1124
|
-
customResolver
|
|
1125
|
-
});
|
|
1126
|
-
const opCache = preconditionCache.get(operation) ?? /* @__PURE__ */ new Map();
|
|
1127
|
-
opCache.set(effectIdx, new Set(gatedTargets.map((t) => t.seat)));
|
|
1128
|
-
preconditionCache.set(operation, opCache);
|
|
1129
|
-
}
|
|
1130
|
-
} else {
|
|
1131
|
-
gatedTargets = resolvedTargets;
|
|
1132
|
-
}
|
|
1133
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
const makePlayersEffect = evaluateLifetime({
|
|
1137
|
-
effect,
|
|
1138
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1139
|
-
nowTimelineIndex,
|
|
1140
|
-
timelines,
|
|
1141
|
-
effector,
|
|
1142
|
-
targets: gatedTargets,
|
|
1143
|
-
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
1144
|
-
characterMap,
|
|
1145
|
-
payloads,
|
|
1146
|
-
customResolver
|
|
1147
|
-
});
|
|
1148
|
-
const handler = effectHandlers[effect.type];
|
|
1149
|
-
if (!handler) {
|
|
1150
|
-
console.warn("Unknown effect type:", effect.type);
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
const context = {
|
|
1154
|
-
effect,
|
|
1155
|
-
operation,
|
|
1156
|
-
payloads,
|
|
1157
|
-
effector,
|
|
1158
|
-
writableInputs,
|
|
1159
|
-
playerSeatMap,
|
|
1160
|
-
getSnapshotSeatMap,
|
|
1161
|
-
characterMap,
|
|
1162
|
-
abilityMap,
|
|
1163
|
-
nowTimelineIndex,
|
|
1164
|
-
operationInTimelineIdx,
|
|
1165
|
-
makePlayersEffect
|
|
1166
|
-
};
|
|
1167
|
-
handler(context);
|
|
1168
|
-
});
|
|
1169
|
-
});
|
|
1170
|
-
};
|
|
1171
|
-
const applyOps = (ops) => {
|
|
1172
|
-
const normalOps = [];
|
|
1173
|
-
const deferredOps = [];
|
|
1174
|
-
ops.forEach((operation) => {
|
|
1175
|
-
if (isDeferredOperation(operation, abilityMap)) {
|
|
1176
|
-
deferredOps.push(operation);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
normalOps.push(operation);
|
|
1180
|
-
});
|
|
1181
|
-
applyOpsSequence(normalOps);
|
|
1182
|
-
applyOpsSequence(deferredOps);
|
|
1067
|
+
const orderedOpsForTimeline = (timelineIdx) => {
|
|
1068
|
+
const ops = operationsByTimeline.get(timelineIdx) ?? [];
|
|
1069
|
+
const normal = [];
|
|
1070
|
+
const deferred = [];
|
|
1071
|
+
ops.forEach((op) => {
|
|
1072
|
+
if (isDeferredOperation(op, abilityMap)) deferred.push(op);
|
|
1073
|
+
else normal.push(op);
|
|
1074
|
+
});
|
|
1075
|
+
return [...normal, ...deferred];
|
|
1076
|
+
};
|
|
1077
|
+
const allOpsInOrder = [];
|
|
1078
|
+
allOpsInOrder.push(...orderedOpsForTimeline(-1));
|
|
1079
|
+
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1080
|
+
if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
|
|
1081
|
+
}
|
|
1082
|
+
const processedOps = /* @__PURE__ */ new Set();
|
|
1083
|
+
const records = [];
|
|
1084
|
+
const recordsByOp = /* @__PURE__ */ new Map();
|
|
1085
|
+
const applyRecordsForOp = (op, playersWithStatus) => {
|
|
1086
|
+
const opRecords = recordsByOp.get(op);
|
|
1087
|
+
if (!opRecords || opRecords.length === 0) return;
|
|
1088
|
+
const ability = abilityMap.get(op.abilityId);
|
|
1089
|
+
if (!ability) return;
|
|
1090
|
+
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1091
|
+
const effector = playerSeatMap.get(op.effector);
|
|
1092
|
+
const payloads = op.payloads || [];
|
|
1093
|
+
const writableInputs = ability.inputs.filter((part) => !part.readonly);
|
|
1094
|
+
let snapshotSeatMap = null;
|
|
1095
|
+
const getSnapshotSeatMap = () => {
|
|
1096
|
+
if (!snapshotSeatMap) {
|
|
1097
|
+
snapshotSeatMap = buildPlayerSeatMap(copyPlayers(playersWithStatus));
|
|
1098
|
+
}
|
|
1099
|
+
return snapshotSeatMap;
|
|
1183
1100
|
};
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1101
|
+
opRecords.forEach((record) => {
|
|
1102
|
+
const effect = ability.effects[record.effectIdx];
|
|
1103
|
+
if (!effect) return;
|
|
1104
|
+
const handler = effectHandlers[effect.type];
|
|
1105
|
+
if (!handler) {
|
|
1106
|
+
console.warn("Unknown effect type:", effect.type);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const makePlayersEffect = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
|
|
1110
|
+
const context = {
|
|
1111
|
+
effect,
|
|
1112
|
+
operation: op,
|
|
1113
|
+
payloads,
|
|
1114
|
+
effector,
|
|
1115
|
+
writableInputs,
|
|
1116
|
+
playerSeatMap,
|
|
1117
|
+
getSnapshotSeatMap,
|
|
1118
|
+
characterMap,
|
|
1119
|
+
abilityMap,
|
|
1120
|
+
nowTimelineIndex,
|
|
1121
|
+
operationInTimelineIdx: record.operationInTimelineIdx,
|
|
1122
|
+
makePlayersEffect
|
|
1123
|
+
};
|
|
1124
|
+
handler(context);
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
const rebuildState = () => {
|
|
1128
|
+
const playersWithStatus = copyPlayers(players);
|
|
1129
|
+
const settingsOps = orderedOpsForTimeline(-1);
|
|
1130
|
+
settingsOps.forEach((op) => {
|
|
1131
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1132
|
+
});
|
|
1188
1133
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1189
1134
|
const timeline = timelines[i];
|
|
1190
|
-
if (!timeline)
|
|
1191
|
-
continue;
|
|
1192
|
-
}
|
|
1135
|
+
if (!timeline) continue;
|
|
1193
1136
|
const nominations = timeline.nominations;
|
|
1194
1137
|
if (nominations && nominations.length > 0) {
|
|
1195
1138
|
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1196
1139
|
nominations.forEach((nomination) => {
|
|
1197
1140
|
const voterSeats = nomination.voterSeats;
|
|
1198
|
-
if (!voterSeats || voterSeats.length === 0)
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1141
|
+
if (!voterSeats || voterSeats.length === 0) return;
|
|
1201
1142
|
voterSeats.forEach((seat) => {
|
|
1202
1143
|
const player = playerSeatMap.get(seat);
|
|
1203
1144
|
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
@@ -1206,20 +1147,126 @@ var applyOperationToPlayers = ({
|
|
|
1206
1147
|
});
|
|
1207
1148
|
});
|
|
1208
1149
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1150
|
+
orderedOpsForTimeline(i).forEach((op) => {
|
|
1151
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1152
|
+
});
|
|
1213
1153
|
}
|
|
1214
1154
|
return playersWithStatus;
|
|
1215
1155
|
};
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1156
|
+
const computeRecordsForOp = (op, state) => {
|
|
1157
|
+
const ability = abilityMap.get(op.abilityId);
|
|
1158
|
+
if (!ability) {
|
|
1159
|
+
console.warn("Ability not found for operation:", op);
|
|
1160
|
+
return [];
|
|
1161
|
+
}
|
|
1162
|
+
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1163
|
+
const effector = playerSeatMap.get(op.effector);
|
|
1164
|
+
const payloads = op.payloads || [];
|
|
1165
|
+
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1166
|
+
const newRecords = [];
|
|
1167
|
+
ability.effects.forEach((effect, effectIdx) => {
|
|
1168
|
+
const resolvedTargets = resolveEffectTargets({
|
|
1169
|
+
effect,
|
|
1170
|
+
operation: op,
|
|
1171
|
+
payloads,
|
|
1172
|
+
effector,
|
|
1173
|
+
playerSeatMap,
|
|
1174
|
+
characterMap
|
|
1175
|
+
});
|
|
1176
|
+
if (!resolvedTargets) return;
|
|
1177
|
+
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1178
|
+
const gatedTargets = precondition ? evaluatePrecondition({
|
|
1179
|
+
expr: precondition,
|
|
1180
|
+
operationTimelineIdx: operationInTimelineIdx,
|
|
1181
|
+
nowTimelineIndex,
|
|
1182
|
+
timelines,
|
|
1183
|
+
effector,
|
|
1184
|
+
targets: resolvedTargets,
|
|
1185
|
+
snapshotSeatMap: playerSeatMap,
|
|
1186
|
+
characterMap,
|
|
1187
|
+
payloads,
|
|
1188
|
+
customResolver
|
|
1189
|
+
}) : resolvedTargets;
|
|
1190
|
+
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const lifeTargets = evaluateLifetime({
|
|
1194
|
+
effect,
|
|
1195
|
+
operationTimelineIdx: operationInTimelineIdx,
|
|
1196
|
+
nowTimelineIndex,
|
|
1197
|
+
timelines,
|
|
1198
|
+
effector,
|
|
1199
|
+
targets: gatedTargets,
|
|
1200
|
+
snapshotSeatMap: playerSeatMap,
|
|
1201
|
+
characterMap,
|
|
1202
|
+
payloads,
|
|
1203
|
+
customResolver
|
|
1204
|
+
});
|
|
1205
|
+
newRecords.push({
|
|
1206
|
+
operation: op,
|
|
1207
|
+
effectIdx,
|
|
1208
|
+
operationInTimelineIdx,
|
|
1209
|
+
activeTargetSeats: lifeTargets.map((t) => t.seat)
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
return newRecords;
|
|
1213
|
+
};
|
|
1214
|
+
const reEvaluateLifetimes = (state) => {
|
|
1215
|
+
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1216
|
+
let anyShrunk = false;
|
|
1217
|
+
records.forEach((record) => {
|
|
1218
|
+
if (record.activeTargetSeats.length === 0) return;
|
|
1219
|
+
const ability = abilityMap.get(record.operation.abilityId);
|
|
1220
|
+
if (!ability) return;
|
|
1221
|
+
const effect = ability.effects[record.effectIdx];
|
|
1222
|
+
if (!effect) return;
|
|
1223
|
+
const effector = playerSeatMap.get(record.operation.effector);
|
|
1224
|
+
const payloads = record.operation.payloads || [];
|
|
1225
|
+
const targets = record.activeTargetSeats.map((seat) => playerSeatMap.get(seat)).filter((p) => Boolean(p));
|
|
1226
|
+
const liveTargets = evaluateLifetime({
|
|
1227
|
+
effect,
|
|
1228
|
+
operationTimelineIdx: record.operationInTimelineIdx,
|
|
1229
|
+
nowTimelineIndex,
|
|
1230
|
+
timelines,
|
|
1231
|
+
effector,
|
|
1232
|
+
targets,
|
|
1233
|
+
snapshotSeatMap: playerSeatMap,
|
|
1234
|
+
characterMap,
|
|
1235
|
+
payloads,
|
|
1236
|
+
customResolver
|
|
1237
|
+
});
|
|
1238
|
+
const newSeats = liveTargets.map((t) => t.seat);
|
|
1239
|
+
if (newSeats.length < record.activeTargetSeats.length) {
|
|
1240
|
+
record.activeTargetSeats = newSeats;
|
|
1241
|
+
anyShrunk = true;
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
return anyShrunk;
|
|
1245
|
+
};
|
|
1246
|
+
let lastState = copyPlayers(players);
|
|
1247
|
+
allOpsInOrder.forEach((op) => {
|
|
1248
|
+
const newRecords = computeRecordsForOp(op, lastState);
|
|
1249
|
+
if (newRecords.length > 0) {
|
|
1250
|
+
records.push(...newRecords);
|
|
1251
|
+
const arr = recordsByOp.get(op) ?? [];
|
|
1252
|
+
arr.push(...newRecords);
|
|
1253
|
+
recordsByOp.set(op, arr);
|
|
1254
|
+
}
|
|
1255
|
+
processedOps.add(op);
|
|
1256
|
+
let state = rebuildState();
|
|
1257
|
+
let iter = 0;
|
|
1258
|
+
while (iter < FIXED_POINT_MAX_ITERATIONS) {
|
|
1259
|
+
const shrunk = reEvaluateLifetimes(state);
|
|
1260
|
+
if (!shrunk) break;
|
|
1261
|
+
state = rebuildState();
|
|
1262
|
+
iter += 1;
|
|
1263
|
+
}
|
|
1264
|
+
if (iter >= FIXED_POINT_MAX_ITERATIONS) {
|
|
1265
|
+
console.warn("applyOperationToPlayers: lifetime fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
|
|
1266
|
+
}
|
|
1267
|
+
lastState = state;
|
|
1268
|
+
});
|
|
1269
|
+
return transformEmptyArray(lastState);
|
|
1223
1270
|
};
|
|
1224
1271
|
|
|
1225
1272
|
// src/can-invoke-ability.ts
|
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.16-beta.1",
|
|
4
4
|
"description": "Game engine utilities for BCT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"build": "tsup src/index.ts --dts --format cjs,esm",
|
|
44
44
|
"dev": "tsup src/index.ts --dts --format cjs,esm --watch",
|
|
45
45
|
"test": "vitest",
|
|
46
|
-
"test:run": "vitest run"
|
|
46
|
+
"test:run": "vitest run",
|
|
47
|
+
"publish:beta": "pnpm publish --tag beta --no-git-checks"
|
|
47
48
|
}
|
|
48
49
|
}
|