@bct-app/game-engine 0.1.16 → 0.1.18
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 +125 -135
- package/dist/index.mjs +125 -135
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -572,11 +572,6 @@ var resolveDynamicValue = (value, payloads, isExpected) => {
|
|
|
572
572
|
var getSortedPlayers = (playerSeatMap) => {
|
|
573
573
|
return [...playerSeatMap.values()].sort((left, right) => left.seat - right.seat);
|
|
574
574
|
};
|
|
575
|
-
var getCircularDistance = (anchorIdx, targetIdx, total) => {
|
|
576
|
-
const clockwise = (targetIdx - anchorIdx + total) % total;
|
|
577
|
-
const anticlockwise = (anchorIdx - targetIdx + total) % total;
|
|
578
|
-
return Math.min(clockwise, anticlockwise);
|
|
579
|
-
};
|
|
580
575
|
var getAnchorSeat = (dynamicTarget, payloads, effector) => {
|
|
581
576
|
const anchor = dynamicTarget.anchor;
|
|
582
577
|
if (!anchor || anchor.from === "EFFECTOR") {
|
|
@@ -595,42 +590,6 @@ var resolveSelectorScope = (dynamicTarget, payloads) => {
|
|
|
595
590
|
(value) => value === "BOTH_SIDES" || value === "LEFT_SIDE" || value === "RIGHT_SIDE"
|
|
596
591
|
) ?? "BOTH_SIDES";
|
|
597
592
|
};
|
|
598
|
-
var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payloads) => {
|
|
599
|
-
const selector = dynamicTarget.selector;
|
|
600
|
-
if (!selector) {
|
|
601
|
-
return sortedPlayers;
|
|
602
|
-
}
|
|
603
|
-
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
604
|
-
if (!anchorSeat) {
|
|
605
|
-
return [];
|
|
606
|
-
}
|
|
607
|
-
const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
|
|
608
|
-
if (anchorIdx < 0) {
|
|
609
|
-
return [];
|
|
610
|
-
}
|
|
611
|
-
const total = sortedPlayers.length;
|
|
612
|
-
if (scope === "LEFT_SIDE") {
|
|
613
|
-
const left2 = [];
|
|
614
|
-
for (let step = 1; step < total; step += 1) {
|
|
615
|
-
left2.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
616
|
-
}
|
|
617
|
-
return left2;
|
|
618
|
-
}
|
|
619
|
-
if (scope === "RIGHT_SIDE") {
|
|
620
|
-
const right2 = [];
|
|
621
|
-
for (let step = 1; step < total; step += 1) {
|
|
622
|
-
right2.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
623
|
-
}
|
|
624
|
-
return right2;
|
|
625
|
-
}
|
|
626
|
-
const left = [];
|
|
627
|
-
const right = [];
|
|
628
|
-
for (let step = 1; step < total; step += 1) {
|
|
629
|
-
left.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
630
|
-
right.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
631
|
-
}
|
|
632
|
-
return [...left, ...right];
|
|
633
|
-
};
|
|
634
593
|
var matchesCondition = (player, condition, payloads, characterMap) => {
|
|
635
594
|
if (condition.field === "IS_DEAD") {
|
|
636
595
|
const expected = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "boolean");
|
|
@@ -659,55 +618,72 @@ var matchesCondition = (player, condition, payloads, characterMap) => {
|
|
|
659
618
|
const expectedKind = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "string");
|
|
660
619
|
return typeof expectedKind === "string" && kind === expectedKind;
|
|
661
620
|
};
|
|
662
|
-
var
|
|
621
|
+
var passesWhere = (player, dynamicTarget, payloads, characterMap) => {
|
|
663
622
|
const where = dynamicTarget.where;
|
|
664
623
|
const conditions = where?.conditions ?? [];
|
|
665
|
-
if (conditions.length === 0)
|
|
666
|
-
return candidates;
|
|
667
|
-
}
|
|
624
|
+
if (conditions.length === 0) return true;
|
|
668
625
|
const isAllMode = where?.mode !== "ANY";
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return isAllMode ? results.every(Boolean) : results.some(Boolean);
|
|
672
|
-
});
|
|
626
|
+
const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
|
|
627
|
+
return isAllMode ? results.every(Boolean) : results.some(Boolean);
|
|
673
628
|
};
|
|
674
|
-
var
|
|
629
|
+
var walkSidesWithFilter = (dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap) => {
|
|
675
630
|
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
676
|
-
if (
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
return [...candidates].sort((left, right) => {
|
|
688
|
-
const leftIdx = seatIndexMap.get(left.seat);
|
|
689
|
-
const rightIdx = seatIndexMap.get(right.seat);
|
|
690
|
-
if (typeof leftIdx !== "number" || typeof rightIdx !== "number") {
|
|
691
|
-
return left.seat - right.seat;
|
|
631
|
+
if (!anchorSeat) return { left: [], right: [] };
|
|
632
|
+
const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
|
|
633
|
+
if (anchorIdx < 0) return { left: [], right: [] };
|
|
634
|
+
const total = sortedPlayers.length;
|
|
635
|
+
const left = [];
|
|
636
|
+
const right = [];
|
|
637
|
+
if (scope === "LEFT_SIDE" || scope === "BOTH_SIDES") {
|
|
638
|
+
for (let step = 1; step < total; step += 1) {
|
|
639
|
+
const player = sortedPlayers[(anchorIdx + step) % total];
|
|
640
|
+
if (passesWhere(player, dynamicTarget, payloads, characterMap)) left.push(player);
|
|
692
641
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
642
|
+
}
|
|
643
|
+
if (scope === "RIGHT_SIDE" || scope === "BOTH_SIDES") {
|
|
644
|
+
for (let step = 1; step < total; step += 1) {
|
|
645
|
+
const player = sortedPlayers[(anchorIdx - step + total) % total];
|
|
646
|
+
if (passesWhere(player, dynamicTarget, payloads, characterMap)) right.push(player);
|
|
697
647
|
}
|
|
698
|
-
|
|
699
|
-
}
|
|
648
|
+
}
|
|
649
|
+
return { left, right };
|
|
700
650
|
};
|
|
701
|
-
var
|
|
702
|
-
const dedupedCandidates = [...new Map(sortedCandidates.map((player) => [player.seat, player])).values()];
|
|
651
|
+
var pickBalanced = (sides, dynamicTarget, payloads) => {
|
|
703
652
|
const configuredLimit = resolveDynamicValue(dynamicTarget.limit, payloads, (value) => typeof value === "number");
|
|
653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
654
|
+
const out = [];
|
|
655
|
+
const pushUnique = (player) => {
|
|
656
|
+
if (!player || seen.has(player.seat)) return false;
|
|
657
|
+
seen.add(player.seat);
|
|
658
|
+
out.push(player);
|
|
659
|
+
return true;
|
|
660
|
+
};
|
|
704
661
|
if (typeof configuredLimit === "undefined") {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
662
|
+
sides.left.forEach(pushUnique);
|
|
663
|
+
sides.right.forEach(pushUnique);
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
if (configuredLimit <= 0) return [];
|
|
667
|
+
let li = 0;
|
|
668
|
+
let ri = 0;
|
|
669
|
+
let preferLeft = true;
|
|
670
|
+
while (out.length < configuredLimit && (li < sides.left.length || ri < sides.right.length)) {
|
|
671
|
+
if (preferLeft && li < sides.left.length) {
|
|
672
|
+
if (pushUnique(sides.left[li])) li += 1;
|
|
673
|
+
else li += 1;
|
|
674
|
+
} else if (!preferLeft && ri < sides.right.length) {
|
|
675
|
+
if (pushUnique(sides.right[ri])) ri += 1;
|
|
676
|
+
else ri += 1;
|
|
677
|
+
} else if (li < sides.left.length) {
|
|
678
|
+
if (pushUnique(sides.left[li])) li += 1;
|
|
679
|
+
else li += 1;
|
|
680
|
+
} else if (ri < sides.right.length) {
|
|
681
|
+
if (pushUnique(sides.right[ri])) ri += 1;
|
|
682
|
+
else ri += 1;
|
|
683
|
+
}
|
|
684
|
+
preferLeft = !preferLeft;
|
|
709
685
|
}
|
|
710
|
-
return
|
|
686
|
+
return out;
|
|
711
687
|
};
|
|
712
688
|
var resolveEffectTargets = ({
|
|
713
689
|
effect,
|
|
@@ -740,10 +716,12 @@ var resolveEffectTargets = ({
|
|
|
740
716
|
const dynamicTarget = effect.target;
|
|
741
717
|
const sortedPlayers = getSortedPlayers(playerSeatMap);
|
|
742
718
|
const anchorSeat = getAnchorSeat(dynamicTarget, payloads, effector);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
719
|
+
if (!dynamicTarget.selector) {
|
|
720
|
+
const matches = sortedPlayers.filter((player) => passesWhere(player, dynamicTarget, payloads, characterMap));
|
|
721
|
+
return pickBalanced({ left: matches, right: [] }, dynamicTarget, payloads);
|
|
722
|
+
}
|
|
723
|
+
const sides = walkSidesWithFilter(dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap);
|
|
724
|
+
return pickBalanced(sides, dynamicTarget, payloads);
|
|
747
725
|
}
|
|
748
726
|
return [];
|
|
749
727
|
};
|
|
@@ -1074,11 +1052,16 @@ var evaluatePrecondition = ({
|
|
|
1074
1052
|
};
|
|
1075
1053
|
|
|
1076
1054
|
// 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
1055
|
var FIXED_POINT_MAX_ITERATIONS = 32;
|
|
1056
|
+
var seatSetsEqual = (left, right) => {
|
|
1057
|
+
if (left.length !== right.length) return false;
|
|
1058
|
+
const sortedLeft = [...left].sort((a, b) => a - b);
|
|
1059
|
+
const sortedRight = [...right].sort((a, b) => a - b);
|
|
1060
|
+
for (let i = 0; i < sortedLeft.length; i += 1) {
|
|
1061
|
+
if (sortedLeft[i] !== sortedRight[i]) return false;
|
|
1062
|
+
}
|
|
1063
|
+
return true;
|
|
1064
|
+
};
|
|
1082
1065
|
var applyOperationToPlayers = ({
|
|
1083
1066
|
players,
|
|
1084
1067
|
operations,
|
|
@@ -1104,20 +1087,11 @@ var applyOperationToPlayers = ({
|
|
|
1104
1087
|
}
|
|
1105
1088
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1106
1089
|
});
|
|
1107
|
-
const
|
|
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
|
-
};
|
|
1090
|
+
const opsForTimeline = (timelineIdx) => operationsByTimeline.get(timelineIdx) ?? [];
|
|
1117
1091
|
const allOpsInOrder = [];
|
|
1118
|
-
allOpsInOrder.push(...
|
|
1092
|
+
allOpsInOrder.push(...opsForTimeline(-1));
|
|
1119
1093
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1120
|
-
if (timelines[i]) allOpsInOrder.push(...
|
|
1094
|
+
if (timelines[i]) allOpsInOrder.push(...opsForTimeline(i));
|
|
1121
1095
|
}
|
|
1122
1096
|
const processedOps = /* @__PURE__ */ new Set();
|
|
1123
1097
|
const records = [];
|
|
@@ -1166,7 +1140,7 @@ var applyOperationToPlayers = ({
|
|
|
1166
1140
|
};
|
|
1167
1141
|
const rebuildState = () => {
|
|
1168
1142
|
const playersWithStatus = copyPlayers(players);
|
|
1169
|
-
const settingsOps =
|
|
1143
|
+
const settingsOps = opsForTimeline(-1);
|
|
1170
1144
|
settingsOps.forEach((op) => {
|
|
1171
1145
|
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1172
1146
|
});
|
|
@@ -1187,7 +1161,7 @@ var applyOperationToPlayers = ({
|
|
|
1187
1161
|
});
|
|
1188
1162
|
});
|
|
1189
1163
|
}
|
|
1190
|
-
|
|
1164
|
+
opsForTimeline(i).forEach((op) => {
|
|
1191
1165
|
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1192
1166
|
});
|
|
1193
1167
|
}
|
|
@@ -1199,10 +1173,10 @@ var applyOperationToPlayers = ({
|
|
|
1199
1173
|
console.warn("Ability not found for operation:", op);
|
|
1200
1174
|
return [];
|
|
1201
1175
|
}
|
|
1176
|
+
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1202
1177
|
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1203
1178
|
const effector = playerSeatMap.get(op.effector);
|
|
1204
1179
|
const payloads = op.payloads || [];
|
|
1205
|
-
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1206
1180
|
const newRecords = [];
|
|
1207
1181
|
ability.effects.forEach((effect, effectIdx) => {
|
|
1208
1182
|
const resolvedTargets = resolveEffectTargets({
|
|
@@ -1227,9 +1201,7 @@ var applyOperationToPlayers = ({
|
|
|
1227
1201
|
payloads,
|
|
1228
1202
|
customResolver
|
|
1229
1203
|
}) : resolvedTargets;
|
|
1230
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0)
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1204
|
+
if (gatedTargets.length === 0 && resolvedTargets.length > 0) return;
|
|
1233
1205
|
const lifeTargets = evaluateLifetime({
|
|
1234
1206
|
effect,
|
|
1235
1207
|
operationTimelineIdx: operationInTimelineIdx,
|
|
@@ -1246,42 +1218,60 @@ var applyOperationToPlayers = ({
|
|
|
1246
1218
|
operation: op,
|
|
1247
1219
|
effectIdx,
|
|
1248
1220
|
operationInTimelineIdx,
|
|
1249
|
-
activeTargetSeats: lifeTargets.map((t) => t.seat)
|
|
1221
|
+
activeTargetSeats: lifeTargets.map((t) => t.seat),
|
|
1222
|
+
resolvedSeatsAtApply: new Set(resolvedTargets.map((t) => t.seat)),
|
|
1223
|
+
gatedSeatsAtApply: new Set(gatedTargets.map((t) => t.seat))
|
|
1250
1224
|
});
|
|
1251
1225
|
});
|
|
1252
1226
|
return newRecords;
|
|
1253
1227
|
};
|
|
1254
|
-
const
|
|
1228
|
+
const reResolveRecord = (record, state) => {
|
|
1229
|
+
const ability = abilityMap.get(record.operation.abilityId);
|
|
1230
|
+
if (!ability) return [];
|
|
1231
|
+
const effect = ability.effects[record.effectIdx];
|
|
1232
|
+
if (!effect) return [];
|
|
1255
1233
|
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1256
|
-
|
|
1234
|
+
const effector = playerSeatMap.get(record.operation.effector);
|
|
1235
|
+
const payloads = record.operation.payloads || [];
|
|
1236
|
+
const resolvedTargets = resolveEffectTargets({
|
|
1237
|
+
effect,
|
|
1238
|
+
operation: record.operation,
|
|
1239
|
+
payloads,
|
|
1240
|
+
effector,
|
|
1241
|
+
playerSeatMap,
|
|
1242
|
+
characterMap
|
|
1243
|
+
});
|
|
1244
|
+
if (!resolvedTargets) return [];
|
|
1245
|
+
const gatedTargets = resolvedTargets.filter((target) => {
|
|
1246
|
+
if (record.resolvedSeatsAtApply.has(target.seat)) {
|
|
1247
|
+
return record.gatedSeatsAtApply.has(target.seat);
|
|
1248
|
+
}
|
|
1249
|
+
return true;
|
|
1250
|
+
});
|
|
1251
|
+
const lifeTargets = evaluateLifetime({
|
|
1252
|
+
effect,
|
|
1253
|
+
operationTimelineIdx: record.operationInTimelineIdx,
|
|
1254
|
+
nowTimelineIndex,
|
|
1255
|
+
timelines,
|
|
1256
|
+
effector,
|
|
1257
|
+
targets: gatedTargets,
|
|
1258
|
+
snapshotSeatMap: playerSeatMap,
|
|
1259
|
+
characterMap,
|
|
1260
|
+
payloads,
|
|
1261
|
+
customResolver
|
|
1262
|
+
});
|
|
1263
|
+
return lifeTargets.map((t) => t.seat);
|
|
1264
|
+
};
|
|
1265
|
+
const reResolveRecords = (state) => {
|
|
1266
|
+
let anyChanged = false;
|
|
1257
1267
|
records.forEach((record) => {
|
|
1258
|
-
|
|
1259
|
-
|
|
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) {
|
|
1268
|
+
const newSeats = reResolveRecord(record, state);
|
|
1269
|
+
if (!seatSetsEqual(newSeats, record.activeTargetSeats)) {
|
|
1280
1270
|
record.activeTargetSeats = newSeats;
|
|
1281
|
-
|
|
1271
|
+
anyChanged = true;
|
|
1282
1272
|
}
|
|
1283
1273
|
});
|
|
1284
|
-
return
|
|
1274
|
+
return anyChanged;
|
|
1285
1275
|
};
|
|
1286
1276
|
let lastState = copyPlayers(players);
|
|
1287
1277
|
allOpsInOrder.forEach((op) => {
|
|
@@ -1296,13 +1286,13 @@ var applyOperationToPlayers = ({
|
|
|
1296
1286
|
let state = rebuildState();
|
|
1297
1287
|
let iter = 0;
|
|
1298
1288
|
while (iter < FIXED_POINT_MAX_ITERATIONS) {
|
|
1299
|
-
const
|
|
1300
|
-
if (!
|
|
1289
|
+
const changed = reResolveRecords(state);
|
|
1290
|
+
if (!changed) break;
|
|
1301
1291
|
state = rebuildState();
|
|
1302
1292
|
iter += 1;
|
|
1303
1293
|
}
|
|
1304
1294
|
if (iter >= FIXED_POINT_MAX_ITERATIONS) {
|
|
1305
|
-
console.warn("applyOperationToPlayers:
|
|
1295
|
+
console.warn("applyOperationToPlayers: target re-resolution fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
|
|
1306
1296
|
}
|
|
1307
1297
|
lastState = state;
|
|
1308
1298
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -532,11 +532,6 @@ var resolveDynamicValue = (value, payloads, isExpected) => {
|
|
|
532
532
|
var getSortedPlayers = (playerSeatMap) => {
|
|
533
533
|
return [...playerSeatMap.values()].sort((left, right) => left.seat - right.seat);
|
|
534
534
|
};
|
|
535
|
-
var getCircularDistance = (anchorIdx, targetIdx, total) => {
|
|
536
|
-
const clockwise = (targetIdx - anchorIdx + total) % total;
|
|
537
|
-
const anticlockwise = (anchorIdx - targetIdx + total) % total;
|
|
538
|
-
return Math.min(clockwise, anticlockwise);
|
|
539
|
-
};
|
|
540
535
|
var getAnchorSeat = (dynamicTarget, payloads, effector) => {
|
|
541
536
|
const anchor = dynamicTarget.anchor;
|
|
542
537
|
if (!anchor || anchor.from === "EFFECTOR") {
|
|
@@ -555,42 +550,6 @@ var resolveSelectorScope = (dynamicTarget, payloads) => {
|
|
|
555
550
|
(value) => value === "BOTH_SIDES" || value === "LEFT_SIDE" || value === "RIGHT_SIDE"
|
|
556
551
|
) ?? "BOTH_SIDES";
|
|
557
552
|
};
|
|
558
|
-
var getCandidatesBySelector = (dynamicTarget, sortedPlayers, anchorSeat, payloads) => {
|
|
559
|
-
const selector = dynamicTarget.selector;
|
|
560
|
-
if (!selector) {
|
|
561
|
-
return sortedPlayers;
|
|
562
|
-
}
|
|
563
|
-
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
564
|
-
if (!anchorSeat) {
|
|
565
|
-
return [];
|
|
566
|
-
}
|
|
567
|
-
const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
|
|
568
|
-
if (anchorIdx < 0) {
|
|
569
|
-
return [];
|
|
570
|
-
}
|
|
571
|
-
const total = sortedPlayers.length;
|
|
572
|
-
if (scope === "LEFT_SIDE") {
|
|
573
|
-
const left2 = [];
|
|
574
|
-
for (let step = 1; step < total; step += 1) {
|
|
575
|
-
left2.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
576
|
-
}
|
|
577
|
-
return left2;
|
|
578
|
-
}
|
|
579
|
-
if (scope === "RIGHT_SIDE") {
|
|
580
|
-
const right2 = [];
|
|
581
|
-
for (let step = 1; step < total; step += 1) {
|
|
582
|
-
right2.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
583
|
-
}
|
|
584
|
-
return right2;
|
|
585
|
-
}
|
|
586
|
-
const left = [];
|
|
587
|
-
const right = [];
|
|
588
|
-
for (let step = 1; step < total; step += 1) {
|
|
589
|
-
left.push(sortedPlayers[(anchorIdx + step) % total]);
|
|
590
|
-
right.push(sortedPlayers[(anchorIdx - step + total) % total]);
|
|
591
|
-
}
|
|
592
|
-
return [...left, ...right];
|
|
593
|
-
};
|
|
594
553
|
var matchesCondition = (player, condition, payloads, characterMap) => {
|
|
595
554
|
if (condition.field === "IS_DEAD") {
|
|
596
555
|
const expected = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "boolean");
|
|
@@ -619,55 +578,72 @@ var matchesCondition = (player, condition, payloads, characterMap) => {
|
|
|
619
578
|
const expectedKind = resolveDynamicValue(condition.value, payloads, (value) => typeof value === "string");
|
|
620
579
|
return typeof expectedKind === "string" && kind === expectedKind;
|
|
621
580
|
};
|
|
622
|
-
var
|
|
581
|
+
var passesWhere = (player, dynamicTarget, payloads, characterMap) => {
|
|
623
582
|
const where = dynamicTarget.where;
|
|
624
583
|
const conditions = where?.conditions ?? [];
|
|
625
|
-
if (conditions.length === 0)
|
|
626
|
-
return candidates;
|
|
627
|
-
}
|
|
584
|
+
if (conditions.length === 0) return true;
|
|
628
585
|
const isAllMode = where?.mode !== "ANY";
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return isAllMode ? results.every(Boolean) : results.some(Boolean);
|
|
632
|
-
});
|
|
586
|
+
const results = conditions.map((condition) => matchesCondition(player, condition, payloads, characterMap));
|
|
587
|
+
return isAllMode ? results.every(Boolean) : results.some(Boolean);
|
|
633
588
|
};
|
|
634
|
-
var
|
|
589
|
+
var walkSidesWithFilter = (dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap) => {
|
|
635
590
|
const scope = resolveSelectorScope(dynamicTarget, payloads);
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
return [...candidates].sort((left, right) => {
|
|
648
|
-
const leftIdx = seatIndexMap.get(left.seat);
|
|
649
|
-
const rightIdx = seatIndexMap.get(right.seat);
|
|
650
|
-
if (typeof leftIdx !== "number" || typeof rightIdx !== "number") {
|
|
651
|
-
return left.seat - right.seat;
|
|
591
|
+
if (!anchorSeat) return { left: [], right: [] };
|
|
592
|
+
const anchorIdx = sortedPlayers.findIndex((player) => player.seat === anchorSeat);
|
|
593
|
+
if (anchorIdx < 0) return { left: [], right: [] };
|
|
594
|
+
const total = sortedPlayers.length;
|
|
595
|
+
const left = [];
|
|
596
|
+
const right = [];
|
|
597
|
+
if (scope === "LEFT_SIDE" || scope === "BOTH_SIDES") {
|
|
598
|
+
for (let step = 1; step < total; step += 1) {
|
|
599
|
+
const player = sortedPlayers[(anchorIdx + step) % total];
|
|
600
|
+
if (passesWhere(player, dynamicTarget, payloads, characterMap)) left.push(player);
|
|
652
601
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
602
|
+
}
|
|
603
|
+
if (scope === "RIGHT_SIDE" || scope === "BOTH_SIDES") {
|
|
604
|
+
for (let step = 1; step < total; step += 1) {
|
|
605
|
+
const player = sortedPlayers[(anchorIdx - step + total) % total];
|
|
606
|
+
if (passesWhere(player, dynamicTarget, payloads, characterMap)) right.push(player);
|
|
657
607
|
}
|
|
658
|
-
|
|
659
|
-
}
|
|
608
|
+
}
|
|
609
|
+
return { left, right };
|
|
660
610
|
};
|
|
661
|
-
var
|
|
662
|
-
const dedupedCandidates = [...new Map(sortedCandidates.map((player) => [player.seat, player])).values()];
|
|
611
|
+
var pickBalanced = (sides, dynamicTarget, payloads) => {
|
|
663
612
|
const configuredLimit = resolveDynamicValue(dynamicTarget.limit, payloads, (value) => typeof value === "number");
|
|
613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
614
|
+
const out = [];
|
|
615
|
+
const pushUnique = (player) => {
|
|
616
|
+
if (!player || seen.has(player.seat)) return false;
|
|
617
|
+
seen.add(player.seat);
|
|
618
|
+
out.push(player);
|
|
619
|
+
return true;
|
|
620
|
+
};
|
|
664
621
|
if (typeof configuredLimit === "undefined") {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
622
|
+
sides.left.forEach(pushUnique);
|
|
623
|
+
sides.right.forEach(pushUnique);
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
if (configuredLimit <= 0) return [];
|
|
627
|
+
let li = 0;
|
|
628
|
+
let ri = 0;
|
|
629
|
+
let preferLeft = true;
|
|
630
|
+
while (out.length < configuredLimit && (li < sides.left.length || ri < sides.right.length)) {
|
|
631
|
+
if (preferLeft && li < sides.left.length) {
|
|
632
|
+
if (pushUnique(sides.left[li])) li += 1;
|
|
633
|
+
else li += 1;
|
|
634
|
+
} else if (!preferLeft && ri < sides.right.length) {
|
|
635
|
+
if (pushUnique(sides.right[ri])) ri += 1;
|
|
636
|
+
else ri += 1;
|
|
637
|
+
} else if (li < sides.left.length) {
|
|
638
|
+
if (pushUnique(sides.left[li])) li += 1;
|
|
639
|
+
else li += 1;
|
|
640
|
+
} else if (ri < sides.right.length) {
|
|
641
|
+
if (pushUnique(sides.right[ri])) ri += 1;
|
|
642
|
+
else ri += 1;
|
|
643
|
+
}
|
|
644
|
+
preferLeft = !preferLeft;
|
|
669
645
|
}
|
|
670
|
-
return
|
|
646
|
+
return out;
|
|
671
647
|
};
|
|
672
648
|
var resolveEffectTargets = ({
|
|
673
649
|
effect,
|
|
@@ -700,10 +676,12 @@ var resolveEffectTargets = ({
|
|
|
700
676
|
const dynamicTarget = effect.target;
|
|
701
677
|
const sortedPlayers = getSortedPlayers(playerSeatMap);
|
|
702
678
|
const anchorSeat = getAnchorSeat(dynamicTarget, payloads, effector);
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
679
|
+
if (!dynamicTarget.selector) {
|
|
680
|
+
const matches = sortedPlayers.filter((player) => passesWhere(player, dynamicTarget, payloads, characterMap));
|
|
681
|
+
return pickBalanced({ left: matches, right: [] }, dynamicTarget, payloads);
|
|
682
|
+
}
|
|
683
|
+
const sides = walkSidesWithFilter(dynamicTarget, sortedPlayers, anchorSeat, payloads, characterMap);
|
|
684
|
+
return pickBalanced(sides, dynamicTarget, payloads);
|
|
707
685
|
}
|
|
708
686
|
return [];
|
|
709
687
|
};
|
|
@@ -1034,11 +1012,16 @@ var evaluatePrecondition = ({
|
|
|
1034
1012
|
};
|
|
1035
1013
|
|
|
1036
1014
|
// 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
1015
|
var FIXED_POINT_MAX_ITERATIONS = 32;
|
|
1016
|
+
var seatSetsEqual = (left, right) => {
|
|
1017
|
+
if (left.length !== right.length) return false;
|
|
1018
|
+
const sortedLeft = [...left].sort((a, b) => a - b);
|
|
1019
|
+
const sortedRight = [...right].sort((a, b) => a - b);
|
|
1020
|
+
for (let i = 0; i < sortedLeft.length; i += 1) {
|
|
1021
|
+
if (sortedLeft[i] !== sortedRight[i]) return false;
|
|
1022
|
+
}
|
|
1023
|
+
return true;
|
|
1024
|
+
};
|
|
1042
1025
|
var applyOperationToPlayers = ({
|
|
1043
1026
|
players,
|
|
1044
1027
|
operations,
|
|
@@ -1064,20 +1047,11 @@ var applyOperationToPlayers = ({
|
|
|
1064
1047
|
}
|
|
1065
1048
|
operationsByTimeline.get(timelineIndex)?.push(operation);
|
|
1066
1049
|
});
|
|
1067
|
-
const
|
|
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
|
-
};
|
|
1050
|
+
const opsForTimeline = (timelineIdx) => operationsByTimeline.get(timelineIdx) ?? [];
|
|
1077
1051
|
const allOpsInOrder = [];
|
|
1078
|
-
allOpsInOrder.push(...
|
|
1052
|
+
allOpsInOrder.push(...opsForTimeline(-1));
|
|
1079
1053
|
for (let i = 0; i <= nowTimelineIndex; i += 1) {
|
|
1080
|
-
if (timelines[i]) allOpsInOrder.push(...
|
|
1054
|
+
if (timelines[i]) allOpsInOrder.push(...opsForTimeline(i));
|
|
1081
1055
|
}
|
|
1082
1056
|
const processedOps = /* @__PURE__ */ new Set();
|
|
1083
1057
|
const records = [];
|
|
@@ -1126,7 +1100,7 @@ var applyOperationToPlayers = ({
|
|
|
1126
1100
|
};
|
|
1127
1101
|
const rebuildState = () => {
|
|
1128
1102
|
const playersWithStatus = copyPlayers(players);
|
|
1129
|
-
const settingsOps =
|
|
1103
|
+
const settingsOps = opsForTimeline(-1);
|
|
1130
1104
|
settingsOps.forEach((op) => {
|
|
1131
1105
|
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1132
1106
|
});
|
|
@@ -1147,7 +1121,7 @@ var applyOperationToPlayers = ({
|
|
|
1147
1121
|
});
|
|
1148
1122
|
});
|
|
1149
1123
|
}
|
|
1150
|
-
|
|
1124
|
+
opsForTimeline(i).forEach((op) => {
|
|
1151
1125
|
if (processedOps.has(op)) applyRecordsForOp(op, playersWithStatus);
|
|
1152
1126
|
});
|
|
1153
1127
|
}
|
|
@@ -1159,10 +1133,10 @@ var applyOperationToPlayers = ({
|
|
|
1159
1133
|
console.warn("Ability not found for operation:", op);
|
|
1160
1134
|
return [];
|
|
1161
1135
|
}
|
|
1136
|
+
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1162
1137
|
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1163
1138
|
const effector = playerSeatMap.get(op.effector);
|
|
1164
1139
|
const payloads = op.payloads || [];
|
|
1165
|
-
const operationInTimelineIdx = operationTimelineMap.get(op) ?? -1;
|
|
1166
1140
|
const newRecords = [];
|
|
1167
1141
|
ability.effects.forEach((effect, effectIdx) => {
|
|
1168
1142
|
const resolvedTargets = resolveEffectTargets({
|
|
@@ -1187,9 +1161,7 @@ var applyOperationToPlayers = ({
|
|
|
1187
1161
|
payloads,
|
|
1188
1162
|
customResolver
|
|
1189
1163
|
}) : resolvedTargets;
|
|
1190
|
-
if (gatedTargets.length === 0 && resolvedTargets.length > 0)
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1164
|
+
if (gatedTargets.length === 0 && resolvedTargets.length > 0) return;
|
|
1193
1165
|
const lifeTargets = evaluateLifetime({
|
|
1194
1166
|
effect,
|
|
1195
1167
|
operationTimelineIdx: operationInTimelineIdx,
|
|
@@ -1206,42 +1178,60 @@ var applyOperationToPlayers = ({
|
|
|
1206
1178
|
operation: op,
|
|
1207
1179
|
effectIdx,
|
|
1208
1180
|
operationInTimelineIdx,
|
|
1209
|
-
activeTargetSeats: lifeTargets.map((t) => t.seat)
|
|
1181
|
+
activeTargetSeats: lifeTargets.map((t) => t.seat),
|
|
1182
|
+
resolvedSeatsAtApply: new Set(resolvedTargets.map((t) => t.seat)),
|
|
1183
|
+
gatedSeatsAtApply: new Set(gatedTargets.map((t) => t.seat))
|
|
1210
1184
|
});
|
|
1211
1185
|
});
|
|
1212
1186
|
return newRecords;
|
|
1213
1187
|
};
|
|
1214
|
-
const
|
|
1188
|
+
const reResolveRecord = (record, state) => {
|
|
1189
|
+
const ability = abilityMap.get(record.operation.abilityId);
|
|
1190
|
+
if (!ability) return [];
|
|
1191
|
+
const effect = ability.effects[record.effectIdx];
|
|
1192
|
+
if (!effect) return [];
|
|
1215
1193
|
const playerSeatMap = buildPlayerSeatMap(state);
|
|
1216
|
-
|
|
1194
|
+
const effector = playerSeatMap.get(record.operation.effector);
|
|
1195
|
+
const payloads = record.operation.payloads || [];
|
|
1196
|
+
const resolvedTargets = resolveEffectTargets({
|
|
1197
|
+
effect,
|
|
1198
|
+
operation: record.operation,
|
|
1199
|
+
payloads,
|
|
1200
|
+
effector,
|
|
1201
|
+
playerSeatMap,
|
|
1202
|
+
characterMap
|
|
1203
|
+
});
|
|
1204
|
+
if (!resolvedTargets) return [];
|
|
1205
|
+
const gatedTargets = resolvedTargets.filter((target) => {
|
|
1206
|
+
if (record.resolvedSeatsAtApply.has(target.seat)) {
|
|
1207
|
+
return record.gatedSeatsAtApply.has(target.seat);
|
|
1208
|
+
}
|
|
1209
|
+
return true;
|
|
1210
|
+
});
|
|
1211
|
+
const lifeTargets = evaluateLifetime({
|
|
1212
|
+
effect,
|
|
1213
|
+
operationTimelineIdx: record.operationInTimelineIdx,
|
|
1214
|
+
nowTimelineIndex,
|
|
1215
|
+
timelines,
|
|
1216
|
+
effector,
|
|
1217
|
+
targets: gatedTargets,
|
|
1218
|
+
snapshotSeatMap: playerSeatMap,
|
|
1219
|
+
characterMap,
|
|
1220
|
+
payloads,
|
|
1221
|
+
customResolver
|
|
1222
|
+
});
|
|
1223
|
+
return lifeTargets.map((t) => t.seat);
|
|
1224
|
+
};
|
|
1225
|
+
const reResolveRecords = (state) => {
|
|
1226
|
+
let anyChanged = false;
|
|
1217
1227
|
records.forEach((record) => {
|
|
1218
|
-
|
|
1219
|
-
|
|
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) {
|
|
1228
|
+
const newSeats = reResolveRecord(record, state);
|
|
1229
|
+
if (!seatSetsEqual(newSeats, record.activeTargetSeats)) {
|
|
1240
1230
|
record.activeTargetSeats = newSeats;
|
|
1241
|
-
|
|
1231
|
+
anyChanged = true;
|
|
1242
1232
|
}
|
|
1243
1233
|
});
|
|
1244
|
-
return
|
|
1234
|
+
return anyChanged;
|
|
1245
1235
|
};
|
|
1246
1236
|
let lastState = copyPlayers(players);
|
|
1247
1237
|
allOpsInOrder.forEach((op) => {
|
|
@@ -1256,13 +1246,13 @@ var applyOperationToPlayers = ({
|
|
|
1256
1246
|
let state = rebuildState();
|
|
1257
1247
|
let iter = 0;
|
|
1258
1248
|
while (iter < FIXED_POINT_MAX_ITERATIONS) {
|
|
1259
|
-
const
|
|
1260
|
-
if (!
|
|
1249
|
+
const changed = reResolveRecords(state);
|
|
1250
|
+
if (!changed) break;
|
|
1261
1251
|
state = rebuildState();
|
|
1262
1252
|
iter += 1;
|
|
1263
1253
|
}
|
|
1264
1254
|
if (iter >= FIXED_POINT_MAX_ITERATIONS) {
|
|
1265
|
-
console.warn("applyOperationToPlayers:
|
|
1255
|
+
console.warn("applyOperationToPlayers: target re-resolution fixed-point did not converge within %d iterations after op %s", FIXED_POINT_MAX_ITERATIONS, op.abilityId);
|
|
1266
1256
|
}
|
|
1267
1257
|
lastState = state;
|
|
1268
1258
|
});
|
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.18",
|
|
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.11"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@vitest/ui": "^4.1.3",
|