@bct-app/game-engine 0.1.14 → 0.1.16-beta.0
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 -124
- package/dist/index.mjs +185 -124
- package/package.json +11 -10
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,120 +1104,81 @@ var applyOperationToPlayers = ({
|
|
|
1110
1104
|
}
|
|
1111
1105
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1112
1106
|
});
|
|
1113
|
-
const
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1116
|
-
|
|
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
|
-
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1147
|
-
const gatedTargets = precondition ? evaluatePrecondition({
|
|
1148
|
-
expr: precondition,
|
|
1149
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1150
|
-
nowTimelineIndex,
|
|
1151
|
-
timelines,
|
|
1152
|
-
effector,
|
|
1153
|
-
targets: resolvedTargets,
|
|
1154
|
-
snapshotSeatMap: playerSeatMap,
|
|
1155
|
-
characterMap,
|
|
1156
|
-
payloads,
|
|
1157
|
-
customResolver
|
|
1158
|
-
}) : resolvedTargets;
|
|
1159
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
const makePlayersEffect = evaluateLifetime({
|
|
1163
|
-
effect,
|
|
1164
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1165
|
-
nowTimelineIndex,
|
|
1166
|
-
timelines,
|
|
1167
|
-
effector,
|
|
1168
|
-
targets: gatedTargets,
|
|
1169
|
-
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
1170
|
-
characterMap,
|
|
1171
|
-
payloads,
|
|
1172
|
-
customResolver
|
|
1173
|
-
});
|
|
1174
|
-
const handler = effectHandlers[effect.type];
|
|
1175
|
-
if (!handler) {
|
|
1176
|
-
console.warn("Unknown effect type:", effect.type);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
const context = {
|
|
1180
|
-
effect,
|
|
1181
|
-
operation,
|
|
1182
|
-
payloads,
|
|
1183
|
-
effector,
|
|
1184
|
-
writableInputs,
|
|
1185
|
-
playerSeatMap,
|
|
1186
|
-
getSnapshotSeatMap,
|
|
1187
|
-
characterMap,
|
|
1188
|
-
abilityMap,
|
|
1189
|
-
nowTimelineIndex,
|
|
1190
|
-
operationInTimelineIdx,
|
|
1191
|
-
makePlayersEffect
|
|
1192
|
-
};
|
|
1193
|
-
handler(context);
|
|
1194
|
-
});
|
|
1195
|
-
});
|
|
1196
|
-
};
|
|
1197
|
-
const applyOps = (ops) => {
|
|
1198
|
-
const normalOps = [];
|
|
1199
|
-
const deferredOps = [];
|
|
1200
|
-
ops.forEach((operation) => {
|
|
1201
|
-
if (isDeferredOperation(operation, abilityMap)) {
|
|
1202
|
-
deferredOps.push(operation);
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
normalOps.push(operation);
|
|
1206
|
-
});
|
|
1207
|
-
applyOpsSequence(normalOps);
|
|
1208
|
-
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;
|
|
1209
1140
|
};
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
+
});
|
|
1214
1173
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1215
1174
|
const timeline = timelines[i];
|
|
1216
|
-
if (!timeline)
|
|
1217
|
-
continue;
|
|
1218
|
-
}
|
|
1175
|
+
if (!timeline) continue;
|
|
1219
1176
|
const nominations = timeline.nominations;
|
|
1220
1177
|
if (nominations && nominations.length > 0) {
|
|
1221
1178
|
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1222
1179
|
nominations.forEach((nomination) => {
|
|
1223
1180
|
const voterSeats = nomination.voterSeats;
|
|
1224
|
-
if (!voterSeats || voterSeats.length === 0)
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1181
|
+
if (!voterSeats || voterSeats.length === 0) return;
|
|
1227
1182
|
voterSeats.forEach((seat) => {
|
|
1228
1183
|
const player = playerSeatMap.get(seat);
|
|
1229
1184
|
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
@@ -1232,20 +1187,126 @@ var applyOperationToPlayers = ({
|
|
|
1232
1187
|
});
|
|
1233
1188
|
});
|
|
1234
1189
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
}
|
|
1190
|
+
orderedOpsForTimeline(i).forEach((op) => {
|
|
1191
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1192
|
+
});
|
|
1239
1193
|
}
|
|
1240
1194
|
return playersWithStatus;
|
|
1241
1195
|
};
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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);
|
|
1249
1310
|
};
|
|
1250
1311
|
|
|
1251
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,120 +1064,81 @@ var applyOperationToPlayers = ({
|
|
|
1070
1064
|
}
|
|
1071
1065
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1072
1066
|
});
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
|
|
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
|
-
const precondition = "precondition" in effect ? effect.precondition : void 0;
|
|
1107
|
-
const gatedTargets = precondition ? evaluatePrecondition({
|
|
1108
|
-
expr: precondition,
|
|
1109
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1110
|
-
nowTimelineIndex,
|
|
1111
|
-
timelines,
|
|
1112
|
-
effector,
|
|
1113
|
-
targets: resolvedTargets,
|
|
1114
|
-
snapshotSeatMap: playerSeatMap,
|
|
1115
|
-
characterMap,
|
|
1116
|
-
payloads,
|
|
1117
|
-
customResolver
|
|
1118
|
-
}) : resolvedTargets;
|
|
1119
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
const makePlayersEffect = evaluateLifetime({
|
|
1123
|
-
effect,
|
|
1124
|
-
operationTimelineIdx: operationInTimelineIdx,
|
|
1125
|
-
nowTimelineIndex,
|
|
1126
|
-
timelines,
|
|
1127
|
-
effector,
|
|
1128
|
-
targets: gatedTargets,
|
|
1129
|
-
snapshotSeatMap: lifetimeSnapshotSeatMap,
|
|
1130
|
-
characterMap,
|
|
1131
|
-
payloads,
|
|
1132
|
-
customResolver
|
|
1133
|
-
});
|
|
1134
|
-
const handler = effectHandlers[effect.type];
|
|
1135
|
-
if (!handler) {
|
|
1136
|
-
console.warn("Unknown effect type:", effect.type);
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
const context = {
|
|
1140
|
-
effect,
|
|
1141
|
-
operation,
|
|
1142
|
-
payloads,
|
|
1143
|
-
effector,
|
|
1144
|
-
writableInputs,
|
|
1145
|
-
playerSeatMap,
|
|
1146
|
-
getSnapshotSeatMap,
|
|
1147
|
-
characterMap,
|
|
1148
|
-
abilityMap,
|
|
1149
|
-
nowTimelineIndex,
|
|
1150
|
-
operationInTimelineIdx,
|
|
1151
|
-
makePlayersEffect
|
|
1152
|
-
};
|
|
1153
|
-
handler(context);
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
};
|
|
1157
|
-
const applyOps = (ops) => {
|
|
1158
|
-
const normalOps = [];
|
|
1159
|
-
const deferredOps = [];
|
|
1160
|
-
ops.forEach((operation) => {
|
|
1161
|
-
if (isDeferredOperation(operation, abilityMap)) {
|
|
1162
|
-
deferredOps.push(operation);
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
normalOps.push(operation);
|
|
1166
|
-
});
|
|
1167
|
-
applyOpsSequence(normalOps);
|
|
1168
|
-
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;
|
|
1169
1100
|
};
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
+
});
|
|
1174
1133
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1175
1134
|
const timeline = timelines[i];
|
|
1176
|
-
if (!timeline)
|
|
1177
|
-
continue;
|
|
1178
|
-
}
|
|
1135
|
+
if (!timeline) continue;
|
|
1179
1136
|
const nominations = timeline.nominations;
|
|
1180
1137
|
if (nominations && nominations.length > 0) {
|
|
1181
1138
|
const playerSeatMap = buildPlayerSeatMap(playersWithStatus);
|
|
1182
1139
|
nominations.forEach((nomination) => {
|
|
1183
1140
|
const voterSeats = nomination.voterSeats;
|
|
1184
|
-
if (!voterSeats || voterSeats.length === 0)
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1141
|
+
if (!voterSeats || voterSeats.length === 0) return;
|
|
1187
1142
|
voterSeats.forEach((seat) => {
|
|
1188
1143
|
const player = playerSeatMap.get(seat);
|
|
1189
1144
|
if (player && player.isDead && !player.hasUsedDeadVote) {
|
|
@@ -1192,20 +1147,126 @@ var applyOperationToPlayers = ({
|
|
|
1192
1147
|
});
|
|
1193
1148
|
});
|
|
1194
1149
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1150
|
+
orderedOpsForTimeline(i).forEach((op) => {
|
|
1151
|
+
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1152
|
+
});
|
|
1199
1153
|
}
|
|
1200
1154
|
return playersWithStatus;
|
|
1201
1155
|
};
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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);
|
|
1209
1270
|
};
|
|
1210
1271
|
|
|
1211
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.0",
|
|
4
4
|
"description": "Game engine utilities for BCT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"build": "tsup src/index.ts --dts --format cjs,esm",
|
|
21
|
+
"dev": "tsup src/index.ts --dts --format cjs,esm --watch",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:run": "vitest run",
|
|
24
|
+
"publish:beta": "npm publish --tag beta"
|
|
25
|
+
},
|
|
18
26
|
"keywords": [
|
|
19
27
|
"game-engine",
|
|
20
28
|
"bct"
|
|
@@ -30,19 +38,12 @@
|
|
|
30
38
|
"access": "public"
|
|
31
39
|
},
|
|
32
40
|
"dependencies": {
|
|
33
|
-
"@bct-app/game-model": "
|
|
41
|
+
"@bct-app/game-model": "workspace:*"
|
|
34
42
|
},
|
|
35
43
|
"devDependencies": {
|
|
36
44
|
"@vitest/ui": "^4.1.3",
|
|
37
45
|
"tsup": "^8.0.2",
|
|
38
46
|
"typescript": "^5.9.3",
|
|
39
47
|
"vitest": "^4.1.3"
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"clean": "rm -rf dist",
|
|
43
|
-
"build": "tsup src/index.ts --dts --format cjs,esm",
|
|
44
|
-
"dev": "tsup src/index.ts --dts --format cjs,esm --watch",
|
|
45
|
-
"test": "vitest",
|
|
46
|
-
"test:run": "vitest run"
|
|
47
48
|
}
|
|
48
|
-
}
|
|
49
|
+
}
|