@bct-app/game-engine 0.1.16-beta.1 → 0.1.17

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 CHANGED
@@ -1074,11 +1074,16 @@ var evaluatePrecondition = ({
1074
1074
  };
1075
1075
 
1076
1076
  // src/apply-operation.ts
1077
- var isDeferredOperation = (operation, abilityMap) => {
1078
- const ability = abilityMap.get(operation.abilityId);
1079
- return ability?.executionTiming === "DEFER_TO_END";
1080
- };
1081
1077
  var FIXED_POINT_MAX_ITERATIONS = 32;
1078
+ var seatSetsEqual = (left, right) => {
1079
+ if (left.length !== right.length) return false;
1080
+ const sortedLeft = [...left].sort((a, b) => a - b);
1081
+ const sortedRight = [...right].sort((a, b) => a - b);
1082
+ for (let i = 0; i < sortedLeft.length; i += 1) {
1083
+ if (sortedLeft[i] !== sortedRight[i]) return false;
1084
+ }
1085
+ return true;
1086
+ };
1082
1087
  var applyOperationToPlayers = ({
1083
1088
  players,
1084
1089
  operations,
@@ -1104,20 +1109,11 @@ var applyOperationToPlayers = ({
1104
1109
  }
1105
1110
  operationsByTimeline.get(timelineIndex)?.push(operation);
1106
1111
  });
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
- };
1112
+ const opsForTimeline = (timelineIdx) => operationsByTimeline.get(timelineIdx) ?? [];
1117
1113
  const allOpsInOrder = [];
1118
- allOpsInOrder.push(...orderedOpsForTimeline(-1));
1114
+ allOpsInOrder.push(...opsForTimeline(-1));
1119
1115
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1120
- if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
1116
+ if (timelines[i]) allOpsInOrder.push(...opsForTimeline(i));
1121
1117
  }
1122
1118
  const processedOps = /* @__PURE__ */ new Set();
1123
1119
  const records = [];
@@ -1166,7 +1162,7 @@ var applyOperationToPlayers = ({
1166
1162
  };
1167
1163
  const rebuildState = () => {
1168
1164
  const playersWithStatus = copyPlayers(players);
1169
- const settingsOps = orderedOpsForTimeline(-1);
1165
+ const settingsOps = opsForTimeline(-1);
1170
1166
  settingsOps.forEach((op) => {
1171
1167
  if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1172
1168
  });
@@ -1187,7 +1183,7 @@ var applyOperationToPlayers = ({
1187
1183
  });
1188
1184
  });
1189
1185
  }
1190
- orderedOpsForTimeline(i).forEach((op) => {
1186
+ opsForTimeline(i).forEach((op) => {
1191
1187
  if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1192
1188
  });
1193
1189
  }
@@ -1199,10 +1195,10 @@ var applyOperationToPlayers = ({
1199
1195
  console.warn("Ability not found for operation:", op);
1200
1196
  return [];
1201
1197
  }
1198
+ const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1202
1199
  const playerSeatMap = buildPlayerSeatMap(state);
1203
1200
  const effector = playerSeatMap.get(op.effector);
1204
1201
  const payloads = op.payloads || [];
1205
- const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1206
1202
  const newRecords = [];
1207
1203
  ability.effects.forEach((effect, effectIdx) => {
1208
1204
  const resolvedTargets = resolveEffectTargets({
@@ -1227,9 +1223,7 @@ var applyOperationToPlayers = ({
1227
1223
  payloads,
1228
1224
  customResolver
1229
1225
  }) : resolvedTargets;
1230
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1231
- return;
1232
- }
1226
+ if (gatedTargets.length === 0 && resolvedTargets.length > 0) return;
1233
1227
  const lifeTargets = evaluateLifetime({
1234
1228
  effect,
1235
1229
  operationTimelineIdx: operationInTimelineIdx,
@@ -1246,42 +1240,60 @@ var applyOperationToPlayers = ({
1246
1240
  operation: op,
1247
1241
  effectIdx,
1248
1242
  operationInTimelineIdx,
1249
- activeTargetSeats: lifeTargets.map((t) => t.seat)
1243
+ activeTargetSeats: lifeTargets.map((t) => t.seat),
1244
+ resolvedSeatsAtApply: new Set(resolvedTargets.map((t) => t.seat)),
1245
+ gatedSeatsAtApply: new Set(gatedTargets.map((t) => t.seat))
1250
1246
  });
1251
1247
  });
1252
1248
  return newRecords;
1253
1249
  };
1254
- const reEvaluateLifetimes = (state) => {
1250
+ const reResolveRecord = (record, state) => {
1251
+ const ability = abilityMap.get(record.operation.abilityId);
1252
+ if (!ability) return [];
1253
+ const effect = ability.effects[record.effectIdx];
1254
+ if (!effect) return [];
1255
1255
  const playerSeatMap = buildPlayerSeatMap(state);
1256
- let anyShrunk = false;
1256
+ const effector = playerSeatMap.get(record.operation.effector);
1257
+ const payloads = record.operation.payloads || [];
1258
+ const resolvedTargets = resolveEffectTargets({
1259
+ effect,
1260
+ operation: record.operation,
1261
+ payloads,
1262
+ effector,
1263
+ playerSeatMap,
1264
+ characterMap
1265
+ });
1266
+ if (!resolvedTargets) return [];
1267
+ const gatedTargets = resolvedTargets.filter((target) => {
1268
+ if (record.resolvedSeatsAtApply.has(target.seat)) {
1269
+ return record.gatedSeatsAtApply.has(target.seat);
1270
+ }
1271
+ return true;
1272
+ });
1273
+ const lifeTargets = evaluateLifetime({
1274
+ effect,
1275
+ operationTimelineIdx: record.operationInTimelineIdx,
1276
+ nowTimelineIndex,
1277
+ timelines,
1278
+ effector,
1279
+ targets: gatedTargets,
1280
+ snapshotSeatMap: playerSeatMap,
1281
+ characterMap,
1282
+ payloads,
1283
+ customResolver
1284
+ });
1285
+ return lifeTargets.map((t) => t.seat);
1286
+ };
1287
+ const reResolveRecords = (state) => {
1288
+ let anyChanged = false;
1257
1289
  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) {
1290
+ const newSeats = reResolveRecord(record, state);
1291
+ if (!seatSetsEqual(newSeats, record.activeTargetSeats)) {
1280
1292
  record.activeTargetSeats = newSeats;
1281
- anyShrunk = true;
1293
+ anyChanged = true;
1282
1294
  }
1283
1295
  });
1284
- return anyShrunk;
1296
+ return anyChanged;
1285
1297
  };
1286
1298
  let lastState = copyPlayers(players);
1287
1299
  allOpsInOrder.forEach((op) => {
@@ -1296,13 +1308,13 @@ var applyOperationToPlayers = ({
1296
1308
  let state = rebuildState();
1297
1309
  let iter = 0;
1298
1310
  while (iter < FIXED_POINT_MAX_ITERATIONS) {
1299
- const shrunk = reEvaluateLifetimes(state);
1300
- if (!shrunk) break;
1311
+ const changed = reResolveRecords(state);
1312
+ if (!changed) break;
1301
1313
  state = rebuildState();
1302
1314
  iter += 1;
1303
1315
  }
1304
1316
  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);
1317
+ console.warn("applyOperationToPlayers: target re-resolution fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
1306
1318
  }
1307
1319
  lastState = state;
1308
1320
  });
package/dist/index.mjs CHANGED
@@ -1034,11 +1034,16 @@ var evaluatePrecondition = ({
1034
1034
  };
1035
1035
 
1036
1036
  // src/apply-operation.ts
1037
- var isDeferredOperation = (operation, abilityMap) => {
1038
- const ability = abilityMap.get(operation.abilityId);
1039
- return ability?.executionTiming === "DEFER_TO_END";
1040
- };
1041
1037
  var FIXED_POINT_MAX_ITERATIONS = 32;
1038
+ var seatSetsEqual = (left, right) => {
1039
+ if (left.length !== right.length) return false;
1040
+ const sortedLeft = [...left].sort((a, b) => a - b);
1041
+ const sortedRight = [...right].sort((a, b) => a - b);
1042
+ for (let i = 0; i < sortedLeft.length; i += 1) {
1043
+ if (sortedLeft[i] !== sortedRight[i]) return false;
1044
+ }
1045
+ return true;
1046
+ };
1042
1047
  var applyOperationToPlayers = ({
1043
1048
  players,
1044
1049
  operations,
@@ -1064,20 +1069,11 @@ var applyOperationToPlayers = ({
1064
1069
  }
1065
1070
  operationsByTimeline.get(timelineIndex)?.push(operation);
1066
1071
  });
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
- };
1072
+ const opsForTimeline = (timelineIdx) => operationsByTimeline.get(timelineIdx) ?? [];
1077
1073
  const allOpsInOrder = [];
1078
- allOpsInOrder.push(...orderedOpsForTimeline(-1));
1074
+ allOpsInOrder.push(...opsForTimeline(-1));
1079
1075
  for (let i = 0; i <= nowTimelineIndex; i += 1) {
1080
- if (timelines[i]) allOpsInOrder.push(...orderedOpsForTimeline(i));
1076
+ if (timelines[i]) allOpsInOrder.push(...opsForTimeline(i));
1081
1077
  }
1082
1078
  const processedOps = /* @__PURE__ */ new Set();
1083
1079
  const records = [];
@@ -1126,7 +1122,7 @@ var applyOperationToPlayers = ({
1126
1122
  };
1127
1123
  const rebuildState = () => {
1128
1124
  const playersWithStatus = copyPlayers(players);
1129
- const settingsOps = orderedOpsForTimeline(-1);
1125
+ const settingsOps = opsForTimeline(-1);
1130
1126
  settingsOps.forEach((op) => {
1131
1127
  if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1132
1128
  });
@@ -1147,7 +1143,7 @@ var applyOperationToPlayers = ({
1147
1143
  });
1148
1144
  });
1149
1145
  }
1150
- orderedOpsForTimeline(i).forEach((op) => {
1146
+ opsForTimeline(i).forEach((op) => {
1151
1147
  if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
1152
1148
  });
1153
1149
  }
@@ -1159,10 +1155,10 @@ var applyOperationToPlayers = ({
1159
1155
  console.warn("Ability not found for operation:", op);
1160
1156
  return [];
1161
1157
  }
1158
+ const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1162
1159
  const playerSeatMap = buildPlayerSeatMap(state);
1163
1160
  const effector = playerSeatMap.get(op.effector);
1164
1161
  const payloads = op.payloads || [];
1165
- const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
1166
1162
  const newRecords = [];
1167
1163
  ability.effects.forEach((effect, effectIdx) => {
1168
1164
  const resolvedTargets = resolveEffectTargets({
@@ -1187,9 +1183,7 @@ var applyOperationToPlayers = ({
1187
1183
  payloads,
1188
1184
  customResolver
1189
1185
  }) : resolvedTargets;
1190
- if (gatedTargets.length === 0 && resolvedTargets.length > 0) {
1191
- return;
1192
- }
1186
+ if (gatedTargets.length === 0 && resolvedTargets.length > 0) return;
1193
1187
  const lifeTargets = evaluateLifetime({
1194
1188
  effect,
1195
1189
  operationTimelineIdx: operationInTimelineIdx,
@@ -1206,42 +1200,60 @@ var applyOperationToPlayers = ({
1206
1200
  operation: op,
1207
1201
  effectIdx,
1208
1202
  operationInTimelineIdx,
1209
- activeTargetSeats: lifeTargets.map((t) => t.seat)
1203
+ activeTargetSeats: lifeTargets.map((t) => t.seat),
1204
+ resolvedSeatsAtApply: new Set(resolvedTargets.map((t) => t.seat)),
1205
+ gatedSeatsAtApply: new Set(gatedTargets.map((t) => t.seat))
1210
1206
  });
1211
1207
  });
1212
1208
  return newRecords;
1213
1209
  };
1214
- const reEvaluateLifetimes = (state) => {
1210
+ const reResolveRecord = (record, state) => {
1211
+ const ability = abilityMap.get(record.operation.abilityId);
1212
+ if (!ability) return [];
1213
+ const effect = ability.effects[record.effectIdx];
1214
+ if (!effect) return [];
1215
1215
  const playerSeatMap = buildPlayerSeatMap(state);
1216
- let anyShrunk = false;
1216
+ const effector = playerSeatMap.get(record.operation.effector);
1217
+ const payloads = record.operation.payloads || [];
1218
+ const resolvedTargets = resolveEffectTargets({
1219
+ effect,
1220
+ operation: record.operation,
1221
+ payloads,
1222
+ effector,
1223
+ playerSeatMap,
1224
+ characterMap
1225
+ });
1226
+ if (!resolvedTargets) return [];
1227
+ const gatedTargets = resolvedTargets.filter((target) => {
1228
+ if (record.resolvedSeatsAtApply.has(target.seat)) {
1229
+ return record.gatedSeatsAtApply.has(target.seat);
1230
+ }
1231
+ return true;
1232
+ });
1233
+ const lifeTargets = evaluateLifetime({
1234
+ effect,
1235
+ operationTimelineIdx: record.operationInTimelineIdx,
1236
+ nowTimelineIndex,
1237
+ timelines,
1238
+ effector,
1239
+ targets: gatedTargets,
1240
+ snapshotSeatMap: playerSeatMap,
1241
+ characterMap,
1242
+ payloads,
1243
+ customResolver
1244
+ });
1245
+ return lifeTargets.map((t) => t.seat);
1246
+ };
1247
+ const reResolveRecords = (state) => {
1248
+ let anyChanged = false;
1217
1249
  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) {
1250
+ const newSeats = reResolveRecord(record, state);
1251
+ if (!seatSetsEqual(newSeats, record.activeTargetSeats)) {
1240
1252
  record.activeTargetSeats = newSeats;
1241
- anyShrunk = true;
1253
+ anyChanged = true;
1242
1254
  }
1243
1255
  });
1244
- return anyShrunk;
1256
+ return anyChanged;
1245
1257
  };
1246
1258
  let lastState = copyPlayers(players);
1247
1259
  allOpsInOrder.forEach((op) => {
@@ -1256,13 +1268,13 @@ var applyOperationToPlayers = ({
1256
1268
  let state = rebuildState();
1257
1269
  let iter = 0;
1258
1270
  while (iter < FIXED_POINT_MAX_ITERATIONS) {
1259
- const shrunk = reEvaluateLifetimes(state);
1260
- if (!shrunk) break;
1271
+ const changed = reResolveRecords(state);
1272
+ if (!changed) break;
1261
1273
  state = rebuildState();
1262
1274
  iter += 1;
1263
1275
  }
1264
1276
  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);
1277
+ console.warn("applyOperationToPlayers: target re-resolution fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
1266
1278
  }
1267
1279
  lastState = state;
1268
1280
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bct-app/game-engine",
3
- "version": "0.1.16-beta.1",
3
+ "version": "0.1.17",
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.9"
33
+ "@bct-app/game-model": "0.1.11"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@vitest/ui": "^4.1.3",