@vibemancer/core 1.0.9 → 1.0.11
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/{chunk-OLGKLCCB.js → chunk-EO7JO2RZ.js} +199 -24
- package/dist/chunk-EO7JO2RZ.js.map +1 -0
- package/dist/{index-browser-BTHlrB_s.d.ts → index-browser-CM0uuzWp.d.ts} +156 -18
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +5 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/bot-compute-budget.ts +10 -0
- package/src/engine/bot-error-capture.ts +179 -0
- package/src/engine/manual-match.ts +2 -0
- package/src/engine/physics.ts +76 -0
- package/src/engine/sandbox-harness.ts +46 -0
- package/src/engine/sandbox.ts +347 -341
- package/src/engine/simulation.ts +120 -10
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +112 -5
- package/src/hooks/state-hooks.ts +409 -407
- package/src/hooks/threat-analysis.ts +57 -18
- package/src/hooks/types.ts +16 -4
- package/src/rules.ts +8 -0
- package/src/types.ts +10 -6
- package/src/utils/combat.ts +379 -371
- package/dist/chunk-OLGKLCCB.js.map +0 -1
|
@@ -213,7 +213,7 @@ function currentRuleset() {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// src/engine-version.ts
|
|
216
|
-
var ENGINE_VERSION =
|
|
216
|
+
var ENGINE_VERSION = 2983169471988242;
|
|
217
217
|
var EQUIVALENT_ENGINE_VERSIONS = [1688330189011034];
|
|
218
218
|
|
|
219
219
|
// src/replay-compat.ts
|
|
@@ -520,7 +520,51 @@ function extractMissileAction(action) {
|
|
|
520
520
|
return action._toMissileAction();
|
|
521
521
|
}
|
|
522
522
|
function extractAction(finalAction) {
|
|
523
|
-
|
|
523
|
+
if (finalAction === null || finalAction === void 0 || typeof finalAction._toAction !== "function") {
|
|
524
|
+
const describeReturned = (value) => {
|
|
525
|
+
if (value === null) return "null";
|
|
526
|
+
if (value === void 0) return "undefined";
|
|
527
|
+
if (Array.isArray(value)) return "an array";
|
|
528
|
+
if (typeof value === "object") {
|
|
529
|
+
if ("then" in value && typeof value.then === "function") {
|
|
530
|
+
return "a Promise \u2014 your bot function is `async`, and it must not be: remove the async keyword";
|
|
531
|
+
}
|
|
532
|
+
return "a plain object";
|
|
533
|
+
}
|
|
534
|
+
return `a ${typeof value}`;
|
|
535
|
+
};
|
|
536
|
+
const got = describeReturned(finalAction);
|
|
537
|
+
throw new Error(
|
|
538
|
+
`Your bot returned ${got}. Return one of: idle(), move(x, y), missile(config, ai, angle), shield(), blink(x, y), cancel() or aim(degrees). A bot must return an action every tick \u2014 returning nothing throws, it does not skip the tick.`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const action = finalAction._toAction();
|
|
542
|
+
if (action === null || typeof action !== "object" || typeof action.move !== "object" || action.move === null) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
"Your bot returned something that is not a valid action. Build actions with the provided functions \u2014 idle(), move(x, y), missile(config, ai, angle), shield(), blink(x, y), cancel() or aim(degrees) \u2014 rather than constructing the object yourself."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return normaliseActionNumbers(action);
|
|
548
|
+
}
|
|
549
|
+
function normaliseActionNumbers(action) {
|
|
550
|
+
const num = (value) => Number(value);
|
|
551
|
+
const normalised = {
|
|
552
|
+
...action,
|
|
553
|
+
move: { x: num(action.move?.x), y: num(action.move?.y) }
|
|
554
|
+
};
|
|
555
|
+
if (action.aimDirection !== void 0) normalised.aimDirection = num(action.aimDirection);
|
|
556
|
+
if (action.startCast?.spell === "missile") {
|
|
557
|
+
normalised.startCast = {
|
|
558
|
+
...action.startCast,
|
|
559
|
+
...action.startCast.direction !== void 0 ? { direction: num(action.startCast.direction) } : {}
|
|
560
|
+
};
|
|
561
|
+
} else if (action.startCast?.spell === "blink") {
|
|
562
|
+
normalised.startCast = {
|
|
563
|
+
...action.startCast,
|
|
564
|
+
target: { x: num(action.startCast.target?.x), y: num(action.startCast.target?.y) }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return normalised;
|
|
524
568
|
}
|
|
525
569
|
|
|
526
570
|
// src/engine/physics.ts
|
|
@@ -551,6 +595,27 @@ function moveProjectile(projectile, deltaTicks) {
|
|
|
551
595
|
y: projectile.position.y + Math.sin(radians) * projectile.speed * deltaTicks
|
|
552
596
|
};
|
|
553
597
|
}
|
|
598
|
+
function clampToSafeZone(position, radius) {
|
|
599
|
+
const min = ARENA_MIN + radius;
|
|
600
|
+
const max = ARENA_MAX - radius;
|
|
601
|
+
const centre = ARENA_SIZE / 2;
|
|
602
|
+
const axis = (value) => {
|
|
603
|
+
if (!Number.isFinite(value)) return centre;
|
|
604
|
+
return Math.max(min, Math.min(max, value));
|
|
605
|
+
};
|
|
606
|
+
return { x: axis(position?.x), y: axis(position?.y) };
|
|
607
|
+
}
|
|
608
|
+
function moveTowardSafely(from, target) {
|
|
609
|
+
const dx = target?.x - from?.x;
|
|
610
|
+
const dy = target?.y - from?.y;
|
|
611
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return { x: 0, y: 0 };
|
|
612
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
613
|
+
if (distance === 0) return { x: 0, y: 0 };
|
|
614
|
+
if (distance >= RULES.MOVEMENT_SPEED) return { x: dx / distance, y: dy / distance };
|
|
615
|
+
const travel = Math.max(0, distance - 1e-6);
|
|
616
|
+
const fraction = travel / RULES.MOVEMENT_SPEED;
|
|
617
|
+
return { x: dx / distance * fraction, y: dy / distance * fraction };
|
|
618
|
+
}
|
|
554
619
|
function clampToArena(position, radius) {
|
|
555
620
|
return {
|
|
556
621
|
x: Math.max(radius, Math.min(ARENA_SIZE - radius, position.x)),
|
|
@@ -729,6 +794,69 @@ function createEntitySeed(matchSeed, entityId, tick2) {
|
|
|
729
794
|
return combined;
|
|
730
795
|
}
|
|
731
796
|
|
|
797
|
+
// src/engine/bot-error-capture.ts
|
|
798
|
+
var MAX_MESSAGE_LENGTH = 1e3;
|
|
799
|
+
var MAX_ERRORS_PER_MATCH = 500;
|
|
800
|
+
function describeThrown(thrown) {
|
|
801
|
+
return truncate(rawDescription(thrown));
|
|
802
|
+
}
|
|
803
|
+
function rawDescription(thrown) {
|
|
804
|
+
if (thrown instanceof Error) {
|
|
805
|
+
const message = typeof thrown.message === "string" ? thrown.message : "";
|
|
806
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
807
|
+
return truncate(message) + " [stack omitted: message too large]";
|
|
808
|
+
}
|
|
809
|
+
const stack = typeof thrown.stack === "string" ? stripBundleCoordinates(thrown.stack.trim()) : "";
|
|
810
|
+
if (stack) return stack;
|
|
811
|
+
return message || `${thrown.name || "Error"} (thrown with no message)`;
|
|
812
|
+
}
|
|
813
|
+
if (typeof thrown === "string") return thrown || "(empty string thrown)";
|
|
814
|
+
if (thrown === null) return "null was thrown";
|
|
815
|
+
if (thrown === void 0) return "undefined was thrown";
|
|
816
|
+
try {
|
|
817
|
+
const json = JSON.stringify(thrown);
|
|
818
|
+
if (typeof json === "string" && json !== "{}") return json;
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const text = String(thrown);
|
|
823
|
+
if (text === "[object Object]") return describeOpaqueObject(thrown);
|
|
824
|
+
return text;
|
|
825
|
+
} catch {
|
|
826
|
+
return "(a value was thrown that could not be described)";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function describeOpaqueObject(thrown) {
|
|
830
|
+
let name = "object";
|
|
831
|
+
try {
|
|
832
|
+
const ctor = thrown.constructor;
|
|
833
|
+
if (ctor && typeof ctor.name === "string" && ctor.name) name = ctor.name;
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
let keys = [];
|
|
837
|
+
try {
|
|
838
|
+
keys = Object.keys(thrown).slice(0, 12);
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
const shape = keys.length > 0 ? ` with keys: ${keys.join(", ")}` : " with no enumerable keys";
|
|
842
|
+
return `a non-serialisable ${name} was thrown${shape} (it could not be converted to text \u2014 usually a circular reference, or a getter that throws)`;
|
|
843
|
+
}
|
|
844
|
+
function stripBundleCoordinates(stack) {
|
|
845
|
+
return stack.replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, "");
|
|
846
|
+
}
|
|
847
|
+
function truncate(message) {
|
|
848
|
+
if (message.length <= MAX_MESSAGE_LENGTH) return message;
|
|
849
|
+
const dropped = message.length - MAX_MESSAGE_LENGTH;
|
|
850
|
+
return `${message.slice(0, MAX_MESSAGE_LENGTH)}\u2026 [truncated, ${dropped} more characters]`;
|
|
851
|
+
}
|
|
852
|
+
function cappedNotice(entityId, tick2) {
|
|
853
|
+
return {
|
|
854
|
+
tick: tick2,
|
|
855
|
+
entityId,
|
|
856
|
+
message: `\u2026 stopped recording errors after ${MAX_ERRORS_PER_MATCH} in this match. The bot is still being called and still failing; only the recording stopped.`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
732
860
|
// src/engine/simulation.ts
|
|
733
861
|
function createInitialState(_seed, spawnDist = SPAWN_DISTANCE) {
|
|
734
862
|
const center = { x: ARENA_SIZE / 2, y: ARENA_SIZE / 2 };
|
|
@@ -815,6 +943,7 @@ function buildWizardContext(state, config, random) {
|
|
|
815
943
|
function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, missileAIs, matchSeed, budgets) {
|
|
816
944
|
const nextTick = currentTick + 1;
|
|
817
945
|
const errors = [];
|
|
946
|
+
const budgetExhausted = [];
|
|
818
947
|
const events = [];
|
|
819
948
|
const random1 = createRandom(createEntitySeed(matchSeed, wizards[0].id, nextTick));
|
|
820
949
|
const random2 = createRandom(createEntitySeed(matchSeed, wizards[1].id, nextTick));
|
|
@@ -822,13 +951,25 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
822
951
|
const nextBudgets = budgets ? [budgets.states[0], budgets.states[1]] : void 0;
|
|
823
952
|
const runBot = (index, entityId, invoke) => {
|
|
824
953
|
const state = nextBudgets?.[index];
|
|
825
|
-
if (state && !mayAct(state))
|
|
954
|
+
if (state && !mayAct(state)) {
|
|
955
|
+
if (!state.reported) {
|
|
956
|
+
state.reported = true;
|
|
957
|
+
budgetExhausted.push({ tick: nextTick, entityId, spentMs: state.spentMs });
|
|
958
|
+
}
|
|
959
|
+
return IDLE_ACTION();
|
|
960
|
+
}
|
|
826
961
|
const startedAt = state ? Date.now() : 0;
|
|
827
962
|
let action;
|
|
828
963
|
try {
|
|
829
964
|
action = invoke();
|
|
830
965
|
} catch (e) {
|
|
831
|
-
|
|
966
|
+
const alreadyDead = (wizards[index]?.health ?? 1) <= 0;
|
|
967
|
+
errors.push({
|
|
968
|
+
tick: nextTick,
|
|
969
|
+
entityId,
|
|
970
|
+
message: describeThrown(e),
|
|
971
|
+
...alreadyDead ? { afterDeath: true } : {}
|
|
972
|
+
});
|
|
832
973
|
action = IDLE_ACTION();
|
|
833
974
|
}
|
|
834
975
|
if (state && budgets) {
|
|
@@ -911,10 +1052,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
911
1052
|
delete wizard.missileConfig;
|
|
912
1053
|
delete wizard.missileAI;
|
|
913
1054
|
} else if (spell === "blink" && wizard.blinkTarget) {
|
|
914
|
-
const
|
|
915
|
-
const
|
|
1055
|
+
const targetX = Number.isFinite(wizard.blinkTarget.x) ? wizard.blinkTarget.x : wizard.position.x;
|
|
1056
|
+
const targetY = Number.isFinite(wizard.blinkTarget.y) ? wizard.blinkTarget.y : wizard.position.y;
|
|
1057
|
+
const dx = targetX - wizard.position.x;
|
|
1058
|
+
const dy = targetY - wizard.position.y;
|
|
916
1059
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
917
|
-
let targetPos =
|
|
1060
|
+
let targetPos = { x: targetX, y: targetY };
|
|
918
1061
|
if (distance > RULES.BLINK_RANGE) {
|
|
919
1062
|
const scale = RULES.BLINK_RANGE / distance;
|
|
920
1063
|
targetPos = {
|
|
@@ -1043,12 +1186,12 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1043
1186
|
() => extractMissileAction(withMissileContext(missileCtx, ai))
|
|
1044
1187
|
) ?? {};
|
|
1045
1188
|
} catch (e) {
|
|
1046
|
-
errors.push({ tick: nextTick, entityId: projectile.id, message:
|
|
1189
|
+
errors.push({ tick: nextTick, entityId: projectile.id, message: describeThrown(e) });
|
|
1047
1190
|
}
|
|
1048
1191
|
targetAngle = missileActions.turnToward ? angleTo(projectile.position, missileActions.turnToward) : missileActions.turnToAngle !== void 0 ? missileActions.turnToAngle : null;
|
|
1049
1192
|
}
|
|
1050
1193
|
}
|
|
1051
|
-
if (targetAngle !== null) {
|
|
1194
|
+
if (targetAngle !== null && Number.isFinite(targetAngle)) {
|
|
1052
1195
|
const diff = angleDiff(projectile.rotation, targetAngle);
|
|
1053
1196
|
const turn = Math.max(-projectile.turnRate, Math.min(projectile.turnRate, diff));
|
|
1054
1197
|
projectile.rotation = normalizeAngle(projectile.rotation + turn);
|
|
@@ -1109,6 +1252,7 @@ function tick(currentTick, wizard1AI, wizard2AI, config, wizards, projectiles, m
|
|
|
1109
1252
|
projectiles: remainingProjectiles,
|
|
1110
1253
|
events,
|
|
1111
1254
|
errors,
|
|
1255
|
+
budgetExhausted,
|
|
1112
1256
|
budgets: nextBudgets
|
|
1113
1257
|
};
|
|
1114
1258
|
}
|
|
@@ -1248,6 +1392,7 @@ var FIGHT_SPAWN_DISTANCES = [500, 550, 600, 650, 700];
|
|
|
1248
1392
|
function fight(wizard1AI, wizard2AI, options = {}) {
|
|
1249
1393
|
const matches = [];
|
|
1250
1394
|
const allErrors = [];
|
|
1395
|
+
const allBudgetExhausted = [];
|
|
1251
1396
|
let matchNumber = 1;
|
|
1252
1397
|
let wizard1Wins = 0;
|
|
1253
1398
|
let wizard2Wins = 0;
|
|
@@ -1262,6 +1407,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1262
1407
|
});
|
|
1263
1408
|
matches.push(result);
|
|
1264
1409
|
allErrors.push(...result.errors.map((e) => ({ ...e, match: matchNumber })));
|
|
1410
|
+
allBudgetExhausted.push(...result.budgetExhausted.map((b) => ({ ...b, match: matchNumber })));
|
|
1265
1411
|
matchNumber++;
|
|
1266
1412
|
if (result.winner === "wizard-1") {
|
|
1267
1413
|
wizard1Wins++;
|
|
@@ -1282,6 +1428,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1282
1428
|
budget.states = [swappedBudget.states[1], swappedBudget.states[0]];
|
|
1283
1429
|
}
|
|
1284
1430
|
allErrors.push(...swapped.errors.map((e) => ({ ...e, entityId: swapSide(e.entityId), match: matchNumber })));
|
|
1431
|
+
allBudgetExhausted.push(...swapped.budgetExhausted.map((b) => ({ ...b, entityId: swapSide(b.entityId), match: matchNumber })));
|
|
1285
1432
|
matchNumber++;
|
|
1286
1433
|
if (swapped.winner === "wizard-1") {
|
|
1287
1434
|
wizard2Wins++;
|
|
@@ -1292,7 +1439,7 @@ function fight(wizard1AI, wizard2AI, options = {}) {
|
|
|
1292
1439
|
}
|
|
1293
1440
|
}
|
|
1294
1441
|
const winner = wizard1Wins > wizard2Wins ? "wizard-1" : wizard2Wins > wizard1Wins ? "wizard-2" : "draw";
|
|
1295
|
-
return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors };
|
|
1442
|
+
return { wizard1Wins, wizard2Wins, draws, winner, matches, allErrors, budgetExhausted: allBudgetExhausted };
|
|
1296
1443
|
}
|
|
1297
1444
|
function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
1298
1445
|
if (typeof wizard1AI !== "function") {
|
|
@@ -1345,6 +1492,7 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1345
1492
|
let projectiles = [];
|
|
1346
1493
|
const missileAIs = /* @__PURE__ */ new Map();
|
|
1347
1494
|
const allErrors = [];
|
|
1495
|
+
const allBudgetExhausted = [];
|
|
1348
1496
|
let deathTick = null;
|
|
1349
1497
|
const sharedBudget = options.budget;
|
|
1350
1498
|
const budgetLimits = sharedBudget?.limits ?? options.budgetLimits;
|
|
@@ -1368,7 +1516,15 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1368
1516
|
currentTick = result.nextTick;
|
|
1369
1517
|
wizards = result.wizards;
|
|
1370
1518
|
projectiles = result.projectiles;
|
|
1371
|
-
|
|
1519
|
+
for (const error of result.errors) {
|
|
1520
|
+
if (allErrors.length > MAX_ERRORS_PER_MATCH) break;
|
|
1521
|
+
if (allErrors.length === MAX_ERRORS_PER_MATCH) {
|
|
1522
|
+
allErrors.push(cappedNotice(error.entityId, error.tick));
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
allErrors.push(error);
|
|
1526
|
+
}
|
|
1527
|
+
if (result.budgetExhausted.length > 0) allBudgetExhausted.push(...result.budgetExhausted);
|
|
1372
1528
|
if (!skipHistory) {
|
|
1373
1529
|
history.push(getPlayerState(0, wizards, projectiles, currentTick, result.events));
|
|
1374
1530
|
}
|
|
@@ -1393,7 +1549,8 @@ function simulate(wizard1AI, wizard2AI, options = {}) {
|
|
|
1393
1549
|
ticks: currentTick,
|
|
1394
1550
|
finalState,
|
|
1395
1551
|
history: skipHistory ? [] : history,
|
|
1396
|
-
errors: allErrors
|
|
1552
|
+
errors: allErrors,
|
|
1553
|
+
budgetExhausted: allBudgetExhausted
|
|
1397
1554
|
};
|
|
1398
1555
|
}
|
|
1399
1556
|
|
|
@@ -1567,7 +1724,9 @@ var ManualMatch = class {
|
|
|
1567
1724
|
ticks: this.currentTick,
|
|
1568
1725
|
finalState: this.getGameState(),
|
|
1569
1726
|
history: this.history,
|
|
1570
|
-
errors: this.allErrors
|
|
1727
|
+
errors: this.allErrors,
|
|
1728
|
+
// Manual play is unbudgeted — the clock is never read, so nobody can be cut off.
|
|
1729
|
+
budgetExhausted: []
|
|
1571
1730
|
};
|
|
1572
1731
|
}
|
|
1573
1732
|
dispose() {
|
|
@@ -1975,7 +2134,8 @@ function analyzeOneThreat(targetPos, projectile, ticksUntilReady) {
|
|
|
1975
2134
|
targetPos,
|
|
1976
2135
|
canDodgeLeft,
|
|
1977
2136
|
canDodgeRight,
|
|
1978
|
-
canOutrun
|
|
2137
|
+
canOutrun,
|
|
2138
|
+
ticksToImpact
|
|
1979
2139
|
);
|
|
1980
2140
|
}
|
|
1981
2141
|
const canBlockInTime = ticksToImpact > ticksUntilReady + RULES.SHIELD_CAST_TIME + 1;
|
|
@@ -2090,15 +2250,28 @@ function getDodgeDirection(projectile, targetPos, direction) {
|
|
|
2090
2250
|
return { x: 0, y: 0 };
|
|
2091
2251
|
}
|
|
2092
2252
|
}
|
|
2093
|
-
function
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2253
|
+
function roomBeforeLava(from, dir) {
|
|
2254
|
+
const min = ARENA_MIN + COLLISION_RADIUS;
|
|
2255
|
+
const max = ARENA_MAX - COLLISION_RADIUS;
|
|
2256
|
+
const along = (pos, d, lo, hi) => {
|
|
2257
|
+
if (Math.abs(d) < 1e-9) return Number.POSITIVE_INFINITY;
|
|
2258
|
+
return d > 0 ? (hi - pos) / d : (lo - pos) / d;
|
|
2259
|
+
};
|
|
2260
|
+
return Math.max(0, Math.min(along(from.x, dir.x, min, max), along(from.y, dir.y, min, max)));
|
|
2261
|
+
}
|
|
2262
|
+
function calculateBestDodgeDirection(projectile, targetPos, canDodgeLeft, canDodgeRight, canOutrun, ticksToImpact) {
|
|
2263
|
+
const travel = RULES.MOVEMENT_SPEED * Math.max(1, Math.min(ticksToImpact, 600));
|
|
2264
|
+
const survives = (dir) => roomBeforeLava(targetPos, dir) >= travel;
|
|
2265
|
+
const sides = [];
|
|
2266
|
+
if (canDodgeLeft) sides.push(getDodgeDirection(projectile, targetPos, "left"));
|
|
2267
|
+
if (canDodgeRight) sides.push(getDodgeDirection(projectile, targetPos, "right"));
|
|
2268
|
+
const safeSides = sides.filter(survives);
|
|
2269
|
+
if (safeSides.length > 0) {
|
|
2270
|
+
return safeSides.reduce((best, dir) => roomBeforeLava(targetPos, dir) > roomBeforeLava(targetPos, best) ? dir : best);
|
|
2271
|
+
}
|
|
2272
|
+
if (canOutrun) {
|
|
2273
|
+
const away = getDodgeDirection(projectile, targetPos, "away");
|
|
2274
|
+
if (survives(away)) return away;
|
|
2102
2275
|
}
|
|
2103
2276
|
return null;
|
|
2104
2277
|
}
|
|
@@ -10585,6 +10758,8 @@ export {
|
|
|
10585
10758
|
extractAction,
|
|
10586
10759
|
moveWizard,
|
|
10587
10760
|
moveProjectile,
|
|
10761
|
+
clampToSafeZone,
|
|
10762
|
+
moveTowardSafely,
|
|
10588
10763
|
clampToArena,
|
|
10589
10764
|
isInLava,
|
|
10590
10765
|
resolveWizardCollision,
|
|
@@ -10723,4 +10898,4 @@ export {
|
|
|
10723
10898
|
diagnoseTrace,
|
|
10724
10899
|
formatDiagnosis
|
|
10725
10900
|
};
|
|
10726
|
-
//# sourceMappingURL=chunk-
|
|
10901
|
+
//# sourceMappingURL=chunk-EO7JO2RZ.js.map
|