feedback7: the battle sim uses engine-independent math — Chrome, Firefox, Safari and the server agree bit for bit (PR #373 by @Cloudnyco)
- server/sim/detmath.js: hypot (sqrt of the squares, rescaled outside 2^±500), powi (integer powers in double-double, one rounding), sin / cos (fdlibm 5.3) and atan2 (fdlibm / FreeBSD msun) built only from correctly rounded IEEE operations; the sim's Math.hypot / sin / cos / atan2 / pow calls go through it (65 files); eslint.config.js forbids the approximated Math functions under server/sim and test/sim/detmath.test.js scans for them. - sin / cos / atan2 are the algorithms V8 runs in Node, so the server's results stay; only hypot differs from V8's. Golden: 2 moved — diy-027 (a hypot distance, damage +80) and coop3-ABYSS-5 (a later card pick from round 9, same end). - Applied unchanged after review (Grok static review: MERGE; the diff read in full: no network, no new dependency). - Tests: broad node --test 5155 / 5155 with the new file tracked (detmath 7 / 7, package 10 / 10); lint 0 errors, typecheck, check:imports.
This commit is contained in:
1 parent
2580152a3a
commit
ae22335cfe
65 files changed
+616
-141
No files matched your search
@@ -8,6 +8,7 @@ import {
|
||||
HUSK_REBIRTH, BOOM_RADIUS, POLLUTION_INTERVAL, STEALTH_RESTORE_BY_KEY, DUCK_STEALTH_RESTORE, nthOf, stOf, safe, num, T, elem, hurt,
|
||||
areaAllies, areaAlliesInTiles, zone, spawnChildren, stepToward, setHits, hitCount, setForm, absorbArts, auraBuff,
|
||||
} from './helpers.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// archetypes
|
||||
@@ -105,7 +106,7 @@ function pathKeysAhead(e) {
|
||||
if (!R || !Array.isArray(R.pts)) return keys;
|
||||
let x = e.x, y = e.y;
|
||||
for (let i = R.ptIdx ?? 0; i < R.pts.length; i++) {
|
||||
const p = R.pts[i], n = Math.max(1, Math.ceil(Math.hypot(p.x - x, p.y - y) * 4));
|
||||
const p = R.pts[i], n = Math.max(1, Math.ceil(hypot(p.x - x, p.y - y) * 4));
|
||||
for (let k = 1; k <= n; k++) keys.add(Math.round(y + ((p.y - y) * k) / n) * COLS + Math.round(x + ((p.x - x) * k) / n));
|
||||
x = p.x; y = p.y;
|
||||
}
|
||||
@@ -405,7 +406,7 @@ function unbalanced(onMove) {
|
||||
a.px = e.x; a.py = e.y; a.hid = e.hidden;
|
||||
if (px == null || hid || e.hidden) return;
|
||||
const own = e.s.moveSpeed * MOVE_SCALE * dt * 1.5 + 1e-3;
|
||||
const extra = Math.hypot(e.x - px, e.y - py) - own;
|
||||
const extra = hypot(e.x - px, e.y - py) - own;
|
||||
a.lx = px; a.ly = py; // where the move started (direction for onMove)
|
||||
if (extra > 0.05) onMove(b, e, a, extra);
|
||||
},
|
||||
@@ -605,7 +606,7 @@ export function blinkForward(b, e, dist) {
|
||||
let i = R.ptIdx;
|
||||
while (left > 1e-9 && i < R.pts.length) {
|
||||
const p = R.pts[i];
|
||||
const d = Math.hypot(p.x - e.x, p.y - e.y);
|
||||
const d = hypot(p.x - e.x, p.y - e.y);
|
||||
if (d <= left) { e.x = p.x; e.y = p.y; left -= d; i++; } else { stepToward(e, p.x, p.y, left); left = 0; }
|
||||
}
|
||||
} else if (R && R.legs && R.legs[R.legIdx] && R.legs[R.legIdx].r != null) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
stepToward, setForm, expose,
|
||||
} from './helpers.js';
|
||||
import { enemyAura, allyAura, selfFear, skill, kitSelfFear } from './archetypes.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -62,7 +63,7 @@ function kitBlackCloud(ab, e) {
|
||||
const prey = (b, e2) => b.enemiesInRadius(e2.x, e2.y, grabR).filter((o) => isPrey(b, e2, o));
|
||||
return [
|
||||
skill(k, (b, e2) => {
|
||||
const list = prey(b, e2).sort((p, q) => Math.hypot(p.x - e2.x, p.y - e2.y) - Math.hypot(q.x - e2.x, q.y - e2.y)).slice(0, GRAB_MAX_PREY);
|
||||
const list = prey(b, e2).sort((p, q) => hypot(p.x - e2.x, p.y - e2.y) - hypot(q.x - e2.x, q.y - e2.y)).slice(0, GRAB_MAX_PREY);
|
||||
if (!list.length) return;
|
||||
b.addBuff(e2, { key: 'ab:grabbing', duration: dur, flags: { bind: true, noMove: true } }); // 技能持续4秒,期间持有束缚
|
||||
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: list[0].id, kind: 'devour' });
|
||||
@@ -250,12 +251,12 @@ export const FLY_KITS = Object.freeze({
|
||||
tick(b, e, a, dt) {
|
||||
if (a.t && !a.t.alive) { a.t = null; b.removeBuff(e, 'ab:dive'); }
|
||||
if (!a.t) {
|
||||
const l = targetsNear(b, e, e.base.rangeRadius || 1).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y));
|
||||
const l = targetsNear(b, e, e.base.rangeRadius || 1).sort((p, q) => hypot(p.x - e.x, p.y - e.y) - hypot(q.x - e.x, q.y - e.y));
|
||||
if (!l.length) return;
|
||||
a.t = l[0];
|
||||
b.addBuff(e, { key: 'ab:dive', flags: { noMove: true } });
|
||||
}
|
||||
if (!stepToward(e, a.t.x, a.t.y, Math.max(e.s.moveSpeed, 0.5) * MOVE_SCALE * dt) && Math.hypot(a.t.x - e.x, a.t.y - e.y) > 0.3) return;
|
||||
if (!stepToward(e, a.t.x, a.t.y, Math.max(e.s.moveSpeed, 0.5) * MOVE_SCALE * dt) && hypot(a.t.x - e.x, a.t.y - e.y) > 0.3) return;
|
||||
hurt(b, e, a.t, e.s.atk, 'phys');
|
||||
b.fx('explode', { x: e.x, y: e.y, r: 0.5, kind: 'seed' });
|
||||
b.kill(e, null);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ELEMENT } from '../../constants.js';
|
||||
import { canTargetAlly, sortAllyTargets, areaSelectable, auraSelectable } from '../../targeting.js';
|
||||
import { mitigate } from '../../damage.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -295,7 +296,7 @@ export function spawnChildren(b, parent, key, n, opts = {}) {
|
||||
/** Move a unit straight toward (tx, ty) by `dist` tiles. Returns true on arrival. */
|
||||
export function stepToward(u, tx, ty, dist) {
|
||||
const dx = tx - u.x, dy = ty - u.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const d = hypot(dx, dy);
|
||||
if (d <= dist + 1e-9) { u.x = tx; u.y = ty; return true; }
|
||||
u.x += (dx / d) * dist;
|
||||
u.y += (dy / d) * dist;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { canTargetAlly, enemyStealthed } from '../../targeting.js';
|
||||
import { T, elem, hurt, targetsNear, byPriority, auraBuff } from './helpers.js';
|
||||
import { stealth, onHitStatus, skill, kitStealth } from './archetypes.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -20,8 +21,8 @@ const CROSS_REACH = 6, CROSS_SKILL = 2.5, CROSS_CHARGE = 1.4;
|
||||
|
||||
function kitCrossbow(ab) {
|
||||
const s = ab.sk.CrossAttack;
|
||||
const aligned = (b, e) => b.allies().filter((u) => canTargetAlly(e, u, true) && (Math.abs(u.y - e.y) < 0.5 || Math.abs(u.x - e.x) < 0.5) && Math.hypot(u.x - e.x, u.y - e.y) <= CROSS_REACH);
|
||||
const nearest = (b, e, l) => l.sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y))[0];
|
||||
const aligned = (b, e) => b.allies().filter((u) => canTargetAlly(e, u, true) && (Math.abs(u.y - e.y) < 0.5 || Math.abs(u.x - e.x) < 0.5) && hypot(u.x - e.x, u.y - e.y) <= CROSS_REACH);
|
||||
const nearest = (b, e, l) => l.sort((p, q) => hypot(p.x - e.x, p.y - e.y) - hypot(q.x - e.x, q.y - e.y))[0];
|
||||
// PRTS 重弩突袭者: 天赋 "隐匿(被阻挡,主动攻击期间均可解除)"; 直击 "蓄力1.4s后向目标方向发射1支弩箭,对击中的首个目标造成攻击力100%
|
||||
// 的法术伤害与5s晕眩 ※技能持续2.5s": revealed for the whole skill, the bolt leaves CROSS_CHARGE s in — at the nearest unit still
|
||||
// in line in the aimed direction; it stands meanwhile [ASSUMED], and a stun / silence or its death before the release
|
||||
@@ -108,7 +109,7 @@ function kitShadowBlade(ab) {
|
||||
tick(b, e) {
|
||||
const partner = b.enemiesInRadius(e.x, e.y, 3).find((o) => /enemy_1174_duholy/.test(o.defId));
|
||||
const r = partner && partner.mem.ab ? (T(partner.mem.ab, 'traitAbility.range_radius') ?? 1.1) : 1.1;
|
||||
if (partner && Math.hypot(partner.x - e.x, partner.y - e.y) <= r + 1e-9 && e.base.bat > 0) auraBuff(b, e, 'ab:shadowSync', 0.5, { batPct: bat / e.base.bat });
|
||||
if (partner && hypot(partner.x - e.x, partner.y - e.y) <= r + 1e-9 && e.base.bat > 0) auraBuff(b, e, 'ab:shadowSync', 0.5, { batPct: bat / e.base.bat });
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
unblockable, maxTargets, onHitStatus, splashAttack, pathKeysAhead, noAirTargets, resist, nthAttackStatus, lowHpBuff,
|
||||
freeAllPrisoners, reborn, frontGuard, faceCrowd, unbalanced, artsBarrier, skill, blinkForward,
|
||||
} from './archetypes.js';
|
||||
import { hypot, powi } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -312,7 +313,7 @@ function kitLeaderMisc(key, ab, e) {
|
||||
const nx = areaAllies(b, e2, prev.x, prev.y, jr).find((u) => !hit.has(u));
|
||||
if (!nx) break;
|
||||
hit.add(nx);
|
||||
hurt(b, e2, nx, e2.s.atk * Math.pow(fall, k), 'arts');
|
||||
hurt(b, e2, nx, e2.s.atk * powi(fall, k), 'arts');
|
||||
if (nx.alive && cold > 0) b.applyStatus(nx, 'cold', { duration: cold, source: e2 });
|
||||
prev = nx;
|
||||
}
|
||||
@@ -323,7 +324,7 @@ function kitLeaderMisc(key, ab, e) {
|
||||
skill(cb, (b, e2) => {
|
||||
const bb = cb.bb, jump = bb.projectile_range ?? jr, max = bb['chain.max_target'] ?? 3;
|
||||
const near = (x, y, r, seen) => b.enemiesInRadius(x, y, r).filter((o) => o !== e2 && !seen.has(o))
|
||||
.sort((p, q) => Math.hypot(p.x - x, p.y - y) - Math.hypot(q.x - x, q.y - y) || p.spawnSeq - q.spawnSeq)[0];
|
||||
.sort((p, q) => hypot(p.x - x, p.y - y) - hypot(q.x - x, q.y - y) || p.spawnSeq - q.spawnSeq)[0];
|
||||
const seen = new Set();
|
||||
let prev = e2, cur = near(e2.x, e2.y, e2.base.rangeRadius || 3.5, seen);
|
||||
while (cur && seen.size < max) {
|
||||
@@ -357,7 +358,7 @@ function kitLeaderMisc(key, ab, e) {
|
||||
function segDist(px, py, ax, ay, bx, by) {
|
||||
const dx = bx - ax, dy = by - ay, L = dx * dx + dy * dy;
|
||||
const t = L > 0 ? Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / L)) : 0;
|
||||
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
||||
return hypot(px - (ax + t * dx), py - (ay + t * dy));
|
||||
}
|
||||
|
||||
/** A second normal hit on the same target ("二连击"): same damage type, no further on-hit procs. */
|
||||
@@ -454,7 +455,7 @@ function kitWarden(ab) {
|
||||
function kitXi(ab) {
|
||||
const cross = ab.sk.CrossAttack, sb = ab.sk.ShieldBurst, sb2 = ab.sk.ShieldBurstReborn;
|
||||
const P = { form2: false };
|
||||
const byDist = (b, e) => allTargets(b, e).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y) || aggroCmp(p, q));
|
||||
const byDist = (b, e) => allTargets(b, e).sort((p, q) => hypot(p.x - e.x, p.y - e.y) - hypot(q.x - e.x, q.y - e.y) || aggroCmp(p, q));
|
||||
const crossAt = (b, e, t) => {
|
||||
const r0 = t.tileR, c0 = t.tileC;
|
||||
b.fx('telegraph', { x: c0, y: r0, r: XI_CROSS_REACH, kind: 'xiCross', tiles: 'plus', id: e.id });
|
||||
@@ -783,7 +784,7 @@ export const LEADER_KITS = Object.freeze({
|
||||
if (st > 0) b.applyStatus(e, 'stun', { duration: st, source: null });
|
||||
})],
|
||||
enemy_10138_xdsnow: (ab) => [unbalanced((b, e, a) => { // 雪孩子 · pushed / pulled into high ground ⇒ hitWall.value damage
|
||||
const dx = e.x - a.lx, dy = e.y - a.ly, d = Math.hypot(dx, dy);
|
||||
const dx = e.x - a.lx, dy = e.y - a.ly, d = hypot(dx, dy);
|
||||
if (!(d > 0)) return;
|
||||
const r = Math.round(e.y + (dy / d) * 0.6), c = Math.round(e.x + (dx / d) * 0.6);
|
||||
if (b.grid.isLow(r, c) && b.grid.groundPassable(r, c)) return; // stopped by nothing: no collision
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
unblockable, runWhenHit, blockWeight, taunt, maxTargets, ep, nthAttackPower, lowHpBuff, deathBoom, setFloat, float,
|
||||
reborn, frontGuard, faceMove, unbalanced, skill, kitEp, kitPrisoner, kitStun3,
|
||||
} from './archetypes.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -245,7 +246,7 @@ export const SPECIAL_KITS = Object.freeze({
|
||||
enemy_1183_mlasrt: (ab) => [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), nthAttackPower(nthOf(ab.sk.PowerAttack), (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1)], // 无胄盟清扫小队 · erosion; every 4th attack ×1.5
|
||||
enemy_1273_stmgun_2: (ab) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit in range, bombards the highest HP% on its 9 tiles
|
||||
// the allies in its range (the engine's ranged reach: radius + the ally collider)
|
||||
const inRange = (b, e) => allTargets(b, e).filter((a) => Math.hypot(a.x - e.x, a.y - e.y) <= (e.base.rangeRadius || 0) + ALLY_COLLIDER_RADIUS + 1e-9);
|
||||
const inRange = (b, e) => allTargets(b, e).filter((a) => hypot(a.x - e.x, a.y - e.y) <= (e.base.rangeRadius || 0) + ALLY_COLLIDER_RADIUS + 1e-9);
|
||||
return [skill(ab.sk.Cannon, (b, e) => {
|
||||
const lock = inRange(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
|
||||
if (!lock) return;
|
||||
@@ -279,7 +280,7 @@ export const SPECIAL_KITS = Object.freeze({
|
||||
spawn(b, e) { watchDeaths(b, e); },
|
||||
otherDeath(c, b, e) {
|
||||
const u = c.unit;
|
||||
if (!u || u === e || c.reason !== 'killed' || Math.hypot(u.x - e.x, u.y - e.y) > (T(ab, 'Attack.range_radius') ?? TORTURER_RADIUS) + 1e-9) return;
|
||||
if (!u || u === e || c.reason !== 'killed' || hypot(u.x - e.x, u.y - e.y) > (T(ab, 'Attack.range_radius') ?? TORTURER_RADIUS) + 1e-9) return;
|
||||
b.heal(e, e, e.s.maxHp * (T(ab, 'Attack.hp_ratio') ?? 0), { self: true });
|
||||
b.addBuff(e, { key: 'ab:torture', refresh: 'stack', stacks: 1, maxStacks: T(ab, 'Attack.max_stack_cnt') ?? 1, persist: true, mods: { atkPct: T(ab, 'Attack.atk') ?? 0 } });
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user