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
@@ -75,6 +75,7 @@ import {
|
||||
ensureInstalled, abOf, attach, T, elem, hurt, targetsNear, allTargets, byPriority, areaAllies, areaAlliesInTiles, fieldAllies,
|
||||
remainingRoute, stayRoute, stepToward, setHits, hitCount, lpLoss, blinkForward, canCast, absorbArts, nthOf,
|
||||
} from './enemies.js';
|
||||
import { hypot, powi } from '../detmath.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants
|
||||
@@ -209,7 +210,7 @@ function rawRouteOf(b, e, tpl) {
|
||||
function toGoal(b, x, y, motion = 'WALK') {
|
||||
const ends = b.grid.specialTiles('end');
|
||||
let best = ends[0] ?? [Math.round(y), Math.round(x)], bd = Infinity;
|
||||
for (const p of ends) { const d = Math.hypot(p[0] - y, p[1] - x); if (d < bd) { bd = d; best = p; } }
|
||||
for (const p of ends) { const d = hypot(p[0] - y, p[1] - x); if (d < bd) { bd = d; best = p; } }
|
||||
return { motion, start: [y, x], end: best, checkpoints: [] };
|
||||
}
|
||||
|
||||
@@ -318,7 +319,7 @@ function mirror(b, e, tpl) {
|
||||
const opsOnly = (list) => list.filter((u) => u.kind === 'op');
|
||||
const nearestOf = (b, e, pred) => {
|
||||
let best = null, bd = Infinity;
|
||||
for (const o of b.enemies) { if (!o.alive || o === e || !pred(o)) continue; const d = Math.hypot(o.x - e.x, o.y - e.y); if (d < bd) { bd = d; best = o; } }
|
||||
for (const o of b.enemies) { if (!o.alive || o === e || !pred(o)) continue; const d = hypot(o.x - e.x, o.y - e.y); if (d < bd) { bd = d; best = o; } }
|
||||
return best;
|
||||
};
|
||||
/** Apply a floor to an enemy whose data attack speed is 0 (skill-driven leaders: 铳/管/弦). */
|
||||
@@ -370,7 +371,7 @@ function stunBlast(b, src, by, r, c, stun, dot, dur, kind) {
|
||||
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));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -708,7 +709,7 @@ function kitSpring(ab, e) {
|
||||
const d = e2.mem.ab.dash;
|
||||
if (!d) return;
|
||||
const g = d.gun && d.gun.alive ? d.gun : gun(b);
|
||||
const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || Math.hypot(g.x - e2.x, g.y - e2.y) < 1;
|
||||
const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || hypot(g.x - e2.x, g.y - e2.y) < 1;
|
||||
// 追逐模式 "对进入自身0.35半径范围内的我方单位(包括飞行单位)造成一次攻击力100%的物理普通伤害" (until 0.1.3: radius 0.5)
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, CHARGE_RADIUS)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); }
|
||||
if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; }
|
||||
@@ -730,9 +731,9 @@ function kitSpring(ab, e) {
|
||||
for (let k = 0; k < times && t; k++) {
|
||||
hit.add(t);
|
||||
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'springBullet' });
|
||||
elem(b, e2, t, 'erosion', e2.s.atk * ratio * Math.pow(SPRING_BOUNCE_FALLOFF, k));
|
||||
elem(b, e2, t, 'erosion', e2.s.atk * ratio * powi(SPRING_BOUNCE_FALLOFF, k));
|
||||
const prev = t;
|
||||
t = areaAllies(b, e2, prev.x, prev.y, SPRING_BOUNCE_RANGE).filter((u) => !hit.has(u)).sort((p, q) => Math.hypot(p.x - prev.x, p.y - prev.y) - Math.hypot(q.x - prev.x, q.y - prev.y) || aggroCmp(p, q))[0];
|
||||
t = areaAllies(b, e2, prev.x, prev.y, SPRING_BOUNCE_RANGE).filter((u) => !hit.has(u)).sort((p, q) => hypot(p.x - prev.x, p.y - prev.y) - hypot(q.x - prev.x, q.y - prev.y) || aggroCmp(p, q))[0];
|
||||
}
|
||||
} else { // 十连击
|
||||
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t0.id, kind: 'springCombo' });
|
||||
|
||||
@@ -37,6 +37,7 @@ import { COLS, ROWS } from '../constants.js';
|
||||
import { performAttack } from '../ai.js';
|
||||
import { sortEnemyTargets } from '../targeting.js';
|
||||
import { DIR_VEC, normDir, oppositeDir } from '../dir.js';
|
||||
import { hypot } from '../detmath.js';
|
||||
|
||||
const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : (typeof v === 'string' && v.trim() !== '' && Number.isFinite(+v) ? +v : d));
|
||||
/** ASPD blackboard values: a fraction (|v| < 1, e.g. −0.6 / −0.05) is ×100 ASPD, otherwise flat (+20). */
|
||||
@@ -192,7 +193,7 @@ function deviceTile(battle, r0, c0) {
|
||||
const exact = r === r0 && c === c0;
|
||||
if (!exact && battle.grid.tile(r, c).build !== 'NONE') continue;
|
||||
const walk = !exact && battle.grid.groundPassable(r, c, true) ? 1 : 0;
|
||||
const s = [exact ? 0 : 1, walk, Math.hypot(r - r0, c - c0), r * COLS + c];
|
||||
const s = [exact ? 0 : 1, walk, hypot(r - r0, c - c0), r * COLS + c];
|
||||
let less = !bs;
|
||||
if (!less) for (let i = 0; i < s.length; i++) { if (s[i] < bs[i] - 1e-9) { less = true; break; } if (s[i] > bs[i] + 1e-9) break; }
|
||||
if (less) { best = [r, c]; bs = s; }
|
||||
@@ -455,7 +456,7 @@ function tickAirflowEnemy(battle, st, e) {
|
||||
const r = Math.round(e.y), c = Math.round(e.x);
|
||||
const f = r >= 0 && r < ROWS && c >= 0 && c < COLS ? st.flow.get(r * COLS + c) : null;
|
||||
const vx = e.x - px, vy = e.y - py;
|
||||
const v = Math.hypot(vx, vy);
|
||||
const v = hypot(vx, vy);
|
||||
if (f && v > 1e-6) {
|
||||
const rel = airflowRelation((vx * f.fx + vy * f.fy) / v);
|
||||
if (rel !== 'vertical') mul = 1 + num(f.bb[`blower_s_enemy[${rel}].move_speed`], 0);
|
||||
|
||||
@@ -116,6 +116,7 @@ import { REFLECTION_KITS } from './enemies/reflection.js';
|
||||
import { FLY_KITS } from './enemies/fly.js';
|
||||
import { SPECIAL_KITS } from './enemies/special.js';
|
||||
import { LEADER_KITS } from './enemies/leaders.js';
|
||||
import { hypot } from '../detmath.js';
|
||||
|
||||
export {
|
||||
EROSION, EROSION_BURST, HUSK_REBIRTH, nthOf, abOf, attach, T, silenced, canCast, elem, hurt, alliesInTiles,
|
||||
@@ -312,7 +313,7 @@ function chaliceShare(b, c) {
|
||||
for (const o of b.enemies) {
|
||||
if (!o.alive || o === t || o.defId !== 'enemy_1430_lrrook' || !o.mem.ab) continue;
|
||||
const a = o.mem.ab.list[0];
|
||||
if (!a || !(a.share > 0) || Math.hypot(o.x - t.x, o.y - t.y) > a.r) continue;
|
||||
if (!a || !(a.share > 0) || hypot(o.x - t.x, o.y - t.y) > a.r) continue;
|
||||
const part = c.dmg.amount * a.share;
|
||||
c.dmg.amount -= part;
|
||||
// a 无来源 burst's share stays 无来源, credited like the burst (damage.js)
|
||||
|
||||
@@ -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 } });
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
alliesAround, passiveBuff, fxOn, battleStore, contentInfo, itemsOf, directMods,
|
||||
} from '../support/index.js';
|
||||
import { mitigate, hasHp, periodicDamage, isHpLoss } from '../../damage.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
// =====================================================================================================================
|
||||
// data helpers
|
||||
@@ -465,7 +466,7 @@ const BY_ITEM = {
|
||||
const r = num(p.radius, 3), sc = num(p.damage_scale, 1);
|
||||
S.on('hit', (c) => {
|
||||
if (c.source !== u || !c.target || c.target.side !== 'enemy' || c.dmg.type === 'element') return;
|
||||
if (Math.hypot(c.target.x - u.x, c.target.y - u.y) >= r - 1e-6) c.dmg.mul *= sc;
|
||||
if (hypot(c.target.x - u.x, c.target.y - u.y) >= r - 1e-6) c.dmg.mul *= sc;
|
||||
});
|
||||
},
|
||||
// 炎国短刀: each skill activation +atk (≤ atk_buff_cnt stacks) — one 直接乘算 bonus of atk × stacks
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Conventions of the tier-2 kits: ../shared/tier2.js; kit contract and rules: ../README.md.
|
||||
|
||||
import { num, talentBb, traitBb, onHitBy, alliesInGridOf, toggleBuff } from '../shared/tier1.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
const mn = num(tb.min_dist), mx = num(tb.max_dist, 4), ds = num(tb.damage_scale);
|
||||
onHitBy(battle, unit, ({ target, dmg }) => {
|
||||
if (!dmg.isAttack) return;
|
||||
const d = Math.hypot(target.x - unit.x, target.y - unit.y);
|
||||
const d = hypot(target.x - unit.x, target.y - unit.y);
|
||||
dmg.amount *= 1 + ds * (mx > mn ? Math.max(0, Math.min(1, (d - mn) / (mx - mn))) : 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
num, defOf, talentBb, moduleTalentBb, selectedId, altSkills, alive, gridKeys, fx, copyGrid, textNum, freeTile,
|
||||
groundTile, enemiesOn,
|
||||
} from '../shared/tier3.js';
|
||||
import { powi } from '../../../detmath.js';
|
||||
|
||||
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
|
||||
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
|
||||
@@ -267,7 +268,7 @@ export default {
|
||||
battle.on('fatal', (ctx) => {
|
||||
if (ctx.unit !== unit || ctx.prevented) return;
|
||||
const n = unit.mem.saveCount ?? 0;
|
||||
const cost = Math.abs(num(t1.cost, -5)) * Math.pow(num(t1.cost_multi, 2), n);
|
||||
const cost = Math.abs(num(t1.cost, -5)) * powi(num(t1.cost_multi, 2), n);
|
||||
const pl = battle.getPlayer(unit.ownerId);
|
||||
if (!pl || pl.dp + 1e-9 < cost) return;
|
||||
battle.addDp(unit.ownerId, -cost);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js';
|
||||
import { num, defOf, talentBb, traitBb, onTiles, gridKeys, fx, copyGrid } from '../shared/tier3.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
/** Two enemy bodies touch within this distance (tiles) — 见行者 collision stun. */
|
||||
const COLLIDE = 0.6;
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
// (only the angle: a target nearer than 0.25 tile still turns radial at 受力等级 −2) — by the official
|
||||
// 力度 − 重量 distance of a 特效 push (PRTS 推与拉 names 见行者's skills 特效类; Battle.push / pushDistance);
|
||||
// stopped short of it ⇒ it hit a wall
|
||||
const near = Math.hypot(e.x - unit.x, e.y - unit.y) < PUSH_DIRECTIONAL_MIN_DIST;
|
||||
const near = hypot(e.x - unit.x, e.y - unit.y) < PUSH_DIRECTIONAL_MIN_DIST;
|
||||
const expect = battle.pushDistance(e, near ? force - 2 : force, { effect: true });
|
||||
const moved = battle.push(e, force, { from: unit, dir: { x: unit.fwd[1], y: unit.fwd[0] }, fixedAngle: true, effect: true });
|
||||
const wall = expect > 0 && moved + 0.05 < expect;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AURA, num, tbb, grid, nationOf, enemiesOnRange, targetsInRange, whileDeployed, reveal, skillActive, isSel, alt,
|
||||
instantKind, withDefaults,
|
||||
} from '../shared/tier4.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
/** Is module `id` the active one? (elites; `'none'` / normal chess ⇒ false) */
|
||||
const moduleIs = (def, id) => !!(def?.raw?.module?.active && def.raw.module.id === id);
|
||||
@@ -39,7 +40,7 @@ export default withDefaults({
|
||||
const n = num(bb.charge, 1);
|
||||
const cand = battle.alliesInRadius(unit.x, unit.y, 1.5, unit.ownerId)
|
||||
.filter((a) => a !== unit && a.kind === 'op' && nationOf(a) === 'laterano' && a.skill && a.skill.active && a.skill.kind === 'ammo')
|
||||
.sort((a, b) => Math.hypot(a.x - unit.x, a.y - unit.y) - Math.hypot(b.x - unit.x, b.y - unit.y) || a.id - b.id);
|
||||
.sort((a, b) => hypot(a.x - unit.x, a.y - unit.y) - hypot(b.x - unit.x, b.y - unit.y) || a.id - b.id);
|
||||
if (!cand[0] || !(n > 0)) return;
|
||||
cand[0].skill.addAmmo(n);
|
||||
battle.fx('reload', { x: cand[0].x, y: cand[0].y, id: cand[0].id, n });
|
||||
|
||||
@@ -49,7 +49,7 @@ export default withDefaults({
|
||||
unit.mem.sentry = null;
|
||||
unit.mem.inesSentryAt = null;
|
||||
const ax = from.x, ay = from.y, bx = unit.x, by = unit.y;
|
||||
const L2 = (bx - ax) ** 2 + (by - ay) ** 2;
|
||||
const L2 = (bx - ax) * (bx - ax) + (by - ay) * (by - ay);
|
||||
const w = num(bb.projectile_range, 1.4);
|
||||
const hits = [];
|
||||
for (const e of battle.aliveEnemies()) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { COLS } from '../../../constants.js';
|
||||
import {
|
||||
num, tbb, grid, targetsInRange, targetsInGrid, toggleBuff, skillActive, withDefaults,
|
||||
} from '../shared/tier4.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
/**
|
||||
* Free tile for a summon/device: in the rect, standable, not reserved (Battle.isReservedTile: empty, no knocked-out
|
||||
@@ -39,7 +40,7 @@ export default withDefaults({
|
||||
for (const k of unit.rangeKeys || []) {
|
||||
const r = (k / COLS) | 0, c = k % COLS;
|
||||
if (!freeTile(battle, r, c)) continue;
|
||||
const d = Math.hypot(c - ref.x, r - ref.y) + (battle.grid.groundPassable(r, c) ? 0 : 5);
|
||||
const d = hypot(c - ref.x, r - ref.y) + (battle.grid.groundPassable(r, c) ? 0 : 5);
|
||||
if (d < bd - 1e-9) { bd = d; best = [r, c]; }
|
||||
}
|
||||
if (!best) return;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { num, tbb, keySet, skillActive, isSel, alt, instantKind, withDefaults } from '../shared/tier4.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
/** 阿罗玛's 非首次标记 (talent 起泡性能测试): one per enemy, shared by every 阿罗玛 (PRTS 备注). */
|
||||
const AROMA_MARK = 'aroma:bubbled';
|
||||
@@ -71,7 +72,7 @@ export default withDefaults({
|
||||
const lo = num(tb.min_dist, 0), hi = num(tb.max_dist, 4);
|
||||
battle.on('hit', (c) => {
|
||||
if (c.source !== unit || !c.dmg.isAttack) return;
|
||||
const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y);
|
||||
const d = hypot(c.target.x - unit.x, c.target.y - unit.y);
|
||||
c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo)));
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AURA, num, tbb, moduleBb, grid, batFlat, enemyHasTag, targetsInRange, whileDeployed, pulse, alt, instantKind,
|
||||
pullToFront, withDefaults,
|
||||
} from '../shared/tier4.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const ABYSSAL = new Set(['char_143_ghost', 'char_263_skadi', 'char_474_glady', 'char_4145_ulpia', 'char_1023_ghost2']);
|
||||
const isAbyssal = (u) => u?.def?.raw?.groupId === 'abyssal' || ABYSSAL.has(u?.def?.charId ?? u?.def?.raw?.charId);
|
||||
@@ -28,7 +29,7 @@ export default withDefaults({
|
||||
};
|
||||
// module HOK-Y: "向自身拖拽较远的敌人时力度提升一个等级"
|
||||
const farR = num(mb['skill@range_radius'], num(mb['attack@range_radius'], 0)), farF = num(mb['skill@delta_force'], num(mb['attack@delta_force'], 0));
|
||||
const selfForce = (unit, e, f) => (farR > 0 && farF > 0 && Math.hypot(e.x - unit.x, e.y - unit.y) > farR + 1e-9 ? f + farF : f);
|
||||
const selfForce = (unit, e, f) => (farR > 0 && farF > 0 && hypot(e.x - unit.x, e.y - unit.y) > farR + 1e-9 ? f + farF : f);
|
||||
const pullSelf = (battle, unit, e, f) => { if (e && e.alive) dragDmg(battle, unit, e, pullToFront(battle, unit, e, selfForce(unit, e, f))); };
|
||||
return {
|
||||
skills: alt(def, {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { bodyInKeys } from '../../../body.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import { num, tbb, whileDeployed, toggleBuff, skillActive, isSel, alt, withDefaults } from '../shared/tier4.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const LINE = Object.freeze(Array.from({ length: COLS }, (_, i) => Object.freeze([0, i])));
|
||||
const tileKey = (u) => Math.round(u.y) * COLS + Math.round(u.x);
|
||||
@@ -83,7 +84,7 @@ export default withDefaults({
|
||||
battle.on('hit', (c) => {
|
||||
if (c.source !== unit || !c.dmg.isAttack) return;
|
||||
if (S3 && skillActive(unit) && unit.baseRangeKeys && !bodyInKeys(c.target, unit.baseRangeKeys)) c.dmg.mul *= num(bb.damage_scale, 1.25);
|
||||
if (ds > 0) { const d = Math.hypot(c.target.x - unit.x, c.target.y - unit.y); c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo))); }
|
||||
if (ds > 0) { const d = hypot(c.target.x - unit.x, c.target.y - unit.y); c.dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo))); }
|
||||
}, { owner: unit });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { rotateOffset } from '../../../dir.js';
|
||||
import { AURA_IV, AURA_DUR, num, talent, traitBb, skillGrid, isOp, leaderOf, whileOn } from '../shared/tier5.js';
|
||||
import { hypot, sin, cos } from '../../../detmath.js';
|
||||
|
||||
/** 魔王 T1 微尘: collision radius of one mote (PRTS 备注 0.4); orbit radius / speed come from the talent bb. */
|
||||
const MOTE_HIT_RADIUS = 0.4;
|
||||
@@ -82,11 +83,11 @@ export default {
|
||||
const ang = spd * t + (2 * Math.PI * k) / cnt;
|
||||
// the orbit turns with his direction: angle 0 = straight ahead, π/2 = his left hand
|
||||
const [fr, fc] = unit.fwd, [lr, lc] = rotateOffset(1, 0, unit.dir);
|
||||
const mx = unit.x + orbit * (fc * Math.cos(ang) + lc * Math.sin(ang)), my = unit.y + orbit * (fr * Math.cos(ang) + lr * Math.sin(ang));
|
||||
const mx = unit.x + orbit * (fc * cos(ang) + lc * sin(ang)), my = unit.y + orbit * (fr * cos(ang) + lr * sin(ang));
|
||||
let hit = null, hd = Infinity;
|
||||
for (const a of cands) {
|
||||
if (a.findBuff('cetsyr:mote')) continue;
|
||||
const d = Math.hypot(a.x - mx, a.y - my);
|
||||
const d = hypot(a.x - mx, a.y - my);
|
||||
if (d <= MOTE_HIT_RADIUS + 1e-9 && d < hd) { hd = d; hit = a; }
|
||||
}
|
||||
if (!hit) continue;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AURA_IV, AURA_DUR, RING1, num, on, talent, skillGrid, maxCharges, mods, dist, isOp, selectedId, lazySkills,
|
||||
instantKind, whileOn, permBuff,
|
||||
} from '../shared/tier5.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
/** Straight-road length through each tile (max of the horizontal and vertical runs of ground-passable tiles). */
|
||||
const ROAD_CACHE = new WeakMap();
|
||||
@@ -134,7 +135,7 @@ export default {
|
||||
}
|
||||
const extra = battle.alliesInGrid(unit).some((a) => a !== unit && isOp(a)) ? Math.min(num(t0.projectile_extend), num(t0.projectile_extend_max, Infinity)) : 0;
|
||||
// it drifts away from her deployment tile ("移动方向始终为远离棘刺部署位置中心的方向")
|
||||
const dx = t.x - unit.x, dy = t.y - unit.y, len = Math.hypot(dx, dy) || 1;
|
||||
const dx = t.x - unit.x, dy = t.y - unit.y, len = hypot(dx, dy) || 1;
|
||||
// `key`: the fx re-sent every second as the unit drifts and grows is this one zone — the client updates it in
|
||||
// place instead of stacking a new layer each second (render/fx/zones.js; community report of 2026-10-06)
|
||||
const key = `thorn2:${unit.id}:${unit.mem.zoneSeq = (unit.mem.zoneSeq ?? 0) + 1}`;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { num, tbb, moduleBb, live, keyOf, enemiesIn, isTok, instantKind, AROUND8, aura } from '../shared/tier6.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// 圣聆初雪 chess_char_6_02 (阵法术师) — S3 群山俯首; 无垠的雪景; 圣山的祝福; module 千分之一的心
|
||||
@@ -217,7 +218,7 @@ function lureGoal(battle, unit, e) {
|
||||
for (const [dr, dc] of AROUND8) {
|
||||
const r = unit.tileR + dr, c = unit.tileC + dc;
|
||||
if (!battle.grid.groundPassable(r, c)) continue;
|
||||
const d = Math.hypot(e.x - c, e.y - r);
|
||||
const d = hypot(e.x - c, e.y - r);
|
||||
if (d < bd - 1e-9) { bd = d; goal = [r, c]; }
|
||||
}
|
||||
return goal;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
||||
import { CHAIN_RADIUS } from '../../../constants.js';
|
||||
import { bodyDist, bodyInKeys } from '../../../body.js';
|
||||
import { num, bv, tbb, live, ANY, enemiesIn, batOf, N4, aura } from '../shared/tier6.js';
|
||||
import { powi } from '../../../detmath.js';
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// 异客 chess_char_6_05 (链术师) — S3 辉煌裂片; 机理分析; 孤卒
|
||||
@@ -25,7 +26,7 @@ function pasngr(bb, chess, def) {
|
||||
for (let i = 0; i < count && prev; i++) {
|
||||
hit.add(prev.id);
|
||||
battle.fx('lightning', { x: prev.x, y: prev.y, id: prev.id, src: unit.id });
|
||||
battle.dealDamage(unit, prev, { amount: unit.s.atk * scale * Math.pow(1 - num(ch.falloff, 0.15), i), type: 'arts', isSkill: true, isAttack: true, tags: ['skill', 'storm'] });
|
||||
battle.dealDamage(unit, prev, { amount: unit.s.atk * scale * powi(1 - num(ch.falloff, 0.15), i), type: 'arts', isSkill: true, isAttack: true, tags: ['skill', 'storm'] });
|
||||
if (slug > 0 && prev.alive) battle.applyStatus(prev, 'sluggish', { duration: slug, source: unit });
|
||||
let best = null, bd = Infinity;
|
||||
for (const x of battle.foesInRadius(prev.x, prev.y, ch.radius || CHAIN_RADIUS)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sortEnemyTargets, canTargetEnemy } from '../../../targeting.js';
|
||||
import { rotateOffset } from '../../../dir.js';
|
||||
import { bodyDist } from '../../../body.js';
|
||||
import { num, tbb, hasBond, ANY, selectedSkill, skillGridOf, WHOLE_FIELD, bstate, aura } from '../shared/tier6.js';
|
||||
import { hypot, sin, cos, atan2 } from '../../../detmath.js';
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// 荒芜拉普兰德 chess_char_6_18 (驭械术师) — S3 终幕·浩劫; 头狼; 叙拉古的荣幸
|
||||
@@ -39,10 +40,10 @@ function whitw2(bb, chess, def) {
|
||||
// ① `k` drones leave her evenly spread, the first along her facing (row 0 is the bottom row: angles in the (col, row)
|
||||
// plane, counter-clockwise)
|
||||
const releaseDrones = (unit, k) => {
|
||||
const a0 = Math.atan2(unit.fwd[0], unit.fwd[1]);
|
||||
const a0 = atan2(unit.fwd[0], unit.fwd[1]);
|
||||
for (let i = 0; i < k; i++) {
|
||||
const a = a0 + (2 * Math.PI * i) / k;
|
||||
unit.mem.drones.push({ x: unit.x, y: unit.y, hx: Math.cos(a), hy: Math.sin(a), v: WHITW2_SPREAD.v0, age: 0, phase: 'spread', t: null, cd: 0, rampId: null, ramp: 0, orbit: null });
|
||||
unit.mem.drones.push({ x: unit.x, y: unit.y, hx: cos(a), hy: sin(a), v: WHITW2_SPREAD.v0, age: 0, phase: 'spread', t: null, cd: 0, rampId: null, ramp: 0, orbit: null });
|
||||
}
|
||||
};
|
||||
// speed v → v + acc·dt (capped); the distance covered at the mean of the two (exact under constant acceleration)
|
||||
@@ -56,7 +57,7 @@ function whitw2(bb, chess, def) {
|
||||
let best = null, bd = Infinity, bh = Infinity;
|
||||
for (const e of battle.enemies) {
|
||||
if (!ok(e)) continue;
|
||||
const de = Math.hypot(e.x - d.x, e.y - d.y), dh = Math.hypot(e.x - unit.x, e.y - unit.y);
|
||||
const de = hypot(e.x - d.x, e.y - d.y), dh = hypot(e.x - unit.x, e.y - unit.y);
|
||||
if (de < bd - 1e-9 || (de <= bd + 1e-9 && dh < bh - 1e-9)) { best = e; bd = de; bh = dh; }
|
||||
}
|
||||
return best;
|
||||
@@ -65,9 +66,9 @@ function whitw2(bb, chess, def) {
|
||||
const circle = (d, dt) => {
|
||||
const { r, v } = WHITW2_ORBIT;
|
||||
if (!d.orbit) d.orbit = { cx: d.x - d.hy * r, cy: d.y + d.hx * r };
|
||||
const a = Math.atan2(d.y - d.orbit.cy, d.x - d.orbit.cx) + (v / r) * dt;
|
||||
d.x = d.orbit.cx + r * Math.cos(a); d.y = d.orbit.cy + r * Math.sin(a);
|
||||
d.hx = -Math.sin(a); d.hy = Math.cos(a);
|
||||
const a = atan2(d.y - d.orbit.cy, d.x - d.orbit.cx) + (v / r) * dt;
|
||||
d.x = d.orbit.cx + r * cos(a); d.y = d.orbit.cy + r * sin(a);
|
||||
d.hx = -sin(a); d.hy = cos(a);
|
||||
};
|
||||
// one tick of one drone: spread → (pick) → chase → on the target, attacking like a normal drone. [ASSUMED] its attack
|
||||
// clock runs all the time (one attack per interval of hers at most, whatever it chased in between) and its first hit
|
||||
@@ -103,7 +104,7 @@ function whitw2(bb, chess, def) {
|
||||
if (d.phase === 'chase') {
|
||||
const s = accelerate(d, WHITW2_CHASE, dt);
|
||||
if (bodyDist(t, d.x, d.y) > s + 1e-9) {
|
||||
const dx = t.x - d.x, dy = t.y - d.y, L = Math.hypot(dx, dy);
|
||||
const dx = t.x - d.x, dy = t.y - d.y, L = hypot(dx, dy);
|
||||
if (L > 1e-9) { d.hx = dx / L; d.hy = dy / L; }
|
||||
d.x += d.hx * s; d.y += d.hy * s;
|
||||
return;
|
||||
|
||||
@@ -63,6 +63,7 @@ import { bodyInKeys } from '../../../body.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { RESIST_STATUSES } from '../../../buffs.js';
|
||||
import { TICK } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_aglna2_1';
|
||||
const S2 = 'skchr_aglna2_2';
|
||||
@@ -342,7 +343,7 @@ export default {
|
||||
if (!st || !skillOn(unit, S3) || !up(unit) || st.flight || !unit.canAct || unit.blocking.length) return;
|
||||
const tgt = moveTarget(battle, unit);
|
||||
if (!tgt) return;
|
||||
const dur = Math.hypot(tgt.r - unit.tileR, tgt.c - unit.tileC) / MOVE_SPEED;
|
||||
const dur = hypot(tgt.r - unit.tileR, tgt.c - unit.tileC) / MOVE_SPEED;
|
||||
const act = unit.skill.activations;
|
||||
st.flight = tgt;
|
||||
battle.releaseBlocked(unit);
|
||||
|
||||
@@ -62,6 +62,7 @@ import { sortEnemyTargets, canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { hasHp, isHpLoss } from '../../../damage.js';
|
||||
import { COLS, PROJECTILE_SPEEDS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_cqbw_1';
|
||||
const S2 = 'skchr_cqbw_2';
|
||||
@@ -133,7 +134,7 @@ function mineTile(battle, unit) {
|
||||
let best = [], bd = Infinity;
|
||||
for (const t of tiles) {
|
||||
let d = Infinity;
|
||||
for (const e of foes) d = Math.min(d, Math.hypot(e.x - t[1], e.y - t[0]));
|
||||
for (const e of foes) d = Math.min(d, hypot(e.x - t[1], e.y - t[0]));
|
||||
if (d < bd - 1e-9) { bd = d; best = [t]; } else if (Math.abs(d - bd) <= 1e-9) best.push(t);
|
||||
}
|
||||
return best.length === 1 ? best[0] : battle.rng.pick(best);
|
||||
|
||||
@@ -67,6 +67,7 @@ import { num, talentBb, moduleBb, traitBb, skillRec, batMod, up, toggleBuff } fr
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_ebnhlz_1';
|
||||
const S2 = 'skchr_ebnhlz_2';
|
||||
@@ -116,7 +117,7 @@ function remnantTiles(battle, unit) {
|
||||
const rank = new Map();
|
||||
foes.forEach((e, i) => { const k = Math.round(e.y) * COLS + Math.round(e.x); if (!rank.has(k)) rank.set(k, i); });
|
||||
battle.rng.shuffle(tiles); // the random order of the last group, and of equal distances [ASSUMED]
|
||||
const dist = (r, c) => { let d = Infinity; for (const e of foes) d = Math.min(d, Math.hypot(e.x - c, e.y - r)); return d; };
|
||||
const dist = (r, c) => { let d = Infinity; for (const e of foes) d = Math.min(d, hypot(e.x - c, e.y - r)); return d; };
|
||||
const keyed = tiles.map(([r, c], i) => ({ r, c, i, on: rank.get(r * COLS + c) ?? Infinity, d: dist(r, c) }));
|
||||
keyed.sort((a, b) => a.on - b.on || a.d - b.d || a.i - b.i);
|
||||
return keyed.map((x) => [x.r, x.c]);
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
import { toLocal } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_haak_1';
|
||||
const S2 = 'skchr_haak_2';
|
||||
@@ -80,7 +81,7 @@ export function frontAlly(battle, unit, heal = false) {
|
||||
if (heal && (a.s.flags.noHeal || a.s.flags.healFree || a.profile?.noHeal)) continue;
|
||||
const dr = a.tileR - unit.tileR, dc = a.tileC - unit.tileC;
|
||||
const [lr, lc] = toLocal(dr, dc, unit.dir);
|
||||
const k = [lr === 0 && lc > 0 ? 0 : 1, Math.hypot(dr, dc), -(a.s.taunt || 0), -(a.aggroSeq || a.deploySeq || 0), a.id];
|
||||
const k = [lr === 0 && lc > 0 ? 0 : 1, hypot(dr, dc), -(a.s.taunt || 0), -(a.aggroSeq || a.deploySeq || 0), a.id];
|
||||
let less = !bk;
|
||||
for (let i = 0; !less && i < k.length; i++) {
|
||||
if (k[i] === bk[i]) continue;
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
import { num, up, traitBb, talentBb, skillRec, batMod, giveSp, protectMods } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_haruka_1';
|
||||
const S2 = 'skchr_haruka_2';
|
||||
@@ -217,7 +218,7 @@ export default {
|
||||
if (c.source !== unit || c.opts?.regen || !skillOn(unit, S2) || !(c.amount > 0) || !(scale > 0) || !n) return;
|
||||
const t = c.target;
|
||||
const foes = battle.foesInRadius(t.x, t.y, r);
|
||||
foes.sort((x, y) => Math.hypot(x.x - t.x, x.y - t.y) - Math.hypot(y.x - t.x, y.y - t.y) || x.spawnSeq - y.spawnSeq);
|
||||
foes.sort((x, y) => hypot(x.x - t.x, x.y - t.y) - hypot(y.x - t.x, y.y - t.y) || x.spawnSeq - y.spawnSeq);
|
||||
const amount = c.amount * scale;
|
||||
for (const e of foes.slice(0, n)) battle.dealDamage(unit, e, { amount, type: 'arts', isSkill: true, tags: ['skill', 'haruka:s2'] });
|
||||
if (foes.length) battle.fx('aoe', { x: t.x, y: t.y, radius: r, id: unit.id, skill: 'haruka:s2' });
|
||||
|
||||
@@ -57,6 +57,7 @@ import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyInRadius } from '../../../body.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { sin, cos, atan2 } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_hsgma2_1';
|
||||
const S2 = 'skchr_hsgma2_2';
|
||||
@@ -98,7 +99,7 @@ export default {
|
||||
/** S2: the shield's centre after `turned` degrees (row 0 is the bottom row: counter-clockwise = a growing angle). */
|
||||
const shieldAt = (unit, sh) => {
|
||||
const a = sh.a0 + (sh.turned * Math.PI) / 180;
|
||||
return { x: unit.x + SHIELD_START * Math.cos(a), y: unit.y + SHIELD_START * Math.sin(a) };
|
||||
return { x: unit.x + SHIELD_START * cos(a), y: unit.y + SHIELD_START * sin(a) };
|
||||
};
|
||||
const touching = (battle, unit, p) => battle.enemies.filter((e) => canTargetEnemy(unit, e, GROUND) && bodyInRadius(e, p.x, p.y, SHIELD_RADIUS));
|
||||
/** S2: one shield pulse — 停顿, the arts hit, and her heal of heal_ratio × the HP it removed (ignoring her 禁疗). */
|
||||
@@ -128,7 +129,7 @@ export default {
|
||||
if (!('hits' in s2Attack)) return;
|
||||
plainS2(); // the throw was this attack: the shield flies from now on
|
||||
const [fr, fc] = unit.fwd;
|
||||
unit.mem.hsShield = { a0: Math.atan2(fr, fc), turned: 0, acc: 0 };
|
||||
unit.mem.hsShield = { a0: atan2(fr, fc), turned: 0, acc: 0 };
|
||||
battle.fx('zone', { x: unit.x, y: unit.y, radius: SHIELD_START + SHIELD_RADIUS, dur: SHIELD_TURN / SHIELD_SPEED, id: unit.id, skill: 'hsgma2:shield' });
|
||||
shieldPulse(battle, unit, shieldAt(unit, unit.mem.hsShield));
|
||||
},
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_ifrit_1';
|
||||
const S2 = 'skchr_ifrit_2';
|
||||
@@ -197,7 +198,7 @@ export default {
|
||||
battle.on('hit', (c) => {
|
||||
const e = c.target;
|
||||
if (c.source !== unit || !e || e.side !== 'enemy' || c.dmg.type === 'element' || !up(unit)) return;
|
||||
const d = Math.hypot(e.x - unit.x, e.y - unit.y);
|
||||
const d = hypot(e.x - unit.x, e.y - unit.y);
|
||||
c.dmg.mul *= 1 + ds * (hi > lo ? Math.max(0, Math.min(1, (d - lo) / (hi - lo))) : 1);
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
import { num, talentBb, traitBb, moduleOn, skillRec, batMod, up, giveSp, installReveal } from '../shared/tier1.js';
|
||||
import { offsetTile, dirFromDelta, dirVec } from '../../../dir.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_jesca2_1';
|
||||
const S2 = 'skchr_jesca2_2';
|
||||
@@ -182,7 +183,7 @@ function fireBomb(battle, unit, b3) {
|
||||
};
|
||||
(unit.mem.jescaShells ??= []).push(bomb);
|
||||
unit.atkCd = unit.s.interval; // the shell is her attack: the timer restarts [ASSUMED]
|
||||
battle.fx('bombardShell', { x: bomb.tx, y: bomb.ty, id: unit.id, r: bomb.radius, t: Math.hypot(bomb.tx - bomb.x, bomb.ty - bomb.y) / BOMB_SPEED });
|
||||
battle.fx('bombardShell', { x: bomb.tx, y: bomb.ty, id: unit.id, r: bomb.radius, t: hypot(bomb.tx - bomb.x, bomb.ty - bomb.y) / BOMB_SPEED });
|
||||
}
|
||||
|
||||
/** Move the shells one tick; a shell touching an enemy or reaching its end point explodes. */
|
||||
@@ -192,7 +193,7 @@ function flyShells(battle, unit, dt) {
|
||||
const keep = [];
|
||||
for (const s of list) {
|
||||
const dx = s.tx - s.x, dy = s.ty - s.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const d = hypot(dx, dy);
|
||||
const step = BOMB_SPEED * dt;
|
||||
let hit = battle.foesInRadius(s.x, s.y, BOMB_HIT_RADIUS).length > 0;
|
||||
if (!hit) {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
import { num, talentBb, skillRec, batMod, up } from '../shared/tier1.js';
|
||||
import { releaseSkillSummon } from '../../tokens.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_kalts2_1';
|
||||
const S2 = 'skchr_kalts2_2';
|
||||
@@ -235,7 +236,7 @@ function startTransport(battle, unit, anchor) {
|
||||
}, { owner: unit });
|
||||
}
|
||||
const from = { x: unit.x, y: unit.y }, to = { x: anchor.x, y: anchor.y };
|
||||
const T = Math.hypot(to.x - from.x, to.y - from.y) / TRANSPORT_SPEED;
|
||||
const T = hypot(to.x - from.x, to.y - from.y) / TRANSPORT_SPEED;
|
||||
const f = { t0: battle.time, T, from, to, hit: new Set(), anchor, r: anchor.tileR, c: anchor.tileC, dir: anchor.dir };
|
||||
unit.mem.k2flight = f;
|
||||
battle.releaseBlocked(unit);
|
||||
|
||||
@@ -46,6 +46,7 @@ import { hasHp } from '../../../damage.js';
|
||||
import { canTargetEnemy, enemyStealthed } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { COLS, PALSY_MAX } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_mantra_1';
|
||||
const S2 = 'skchr_mantra_2';
|
||||
@@ -91,7 +92,7 @@ function nearestFoe(battle, unit, x, y, r, skip) {
|
||||
let best = null, bd = Infinity;
|
||||
for (const e of battle.enemiesInRadius(x, y, r)) {
|
||||
if (skip.has(e) || !canTargetEnemy(unit, e, ANY)) continue;
|
||||
const d = Math.hypot(e.x - x, e.y - y);
|
||||
const d = hypot(e.x - x, e.y - y);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && best && e.spawnSeq < best.spawnSeq)) { bd = d; best = e; }
|
||||
}
|
||||
return best;
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
import { num, skillRec, batMod, up } from '../shared/tier1.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { dirVec } from '../../../dir.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_mcnist_1';
|
||||
const S2 = 'skchr_mcnist_2';
|
||||
@@ -197,7 +198,7 @@ function grafKit(owner, { life, t2, b2, b3, aura }) {
|
||||
function chargeGraf(battle, mcn, t, b3) {
|
||||
if (!up(t)) return;
|
||||
const { stop, land } = chargeEnd(battle, t);
|
||||
const dist = Math.hypot(stop[0] - t.tileR, stop[1] - t.tileC);
|
||||
const dist = hypot(stop[0] - t.tileR, stop[1] - t.tileC);
|
||||
t.mem.charging = true;
|
||||
battle.fx('charge', { x: t.x, y: t.y, id: t.id, tx: stop[1], ty: stop[0] });
|
||||
battle.retreat(t, { reason: 'expired', permanent: true });
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, statBuff, toggleBuff, onHitBy, enemiesInGrid, cheb, up, giveSp } from '../shared/tier1.js';
|
||||
import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_narant_1';
|
||||
const S2 = 'skchr_narant_2';
|
||||
@@ -138,7 +139,7 @@ export default {
|
||||
if (left <= 0 || !ctx.home()) { ctx.comeBack(x, y); return; }
|
||||
const cands = battle.foesInRadius(x, y, BOUNCE_RADIUS).filter((e) => e !== on && canTargetEnemy(unit, e, ctx.profile));
|
||||
if (!cands.length) { ctx.comeBack(x, y); return; }
|
||||
const d2 = (e) => (e.x - x) ** 2 + (e.y - y) ** 2;
|
||||
const d2 = (e) => (e.x - x) * (e.x - x) + (e.y - y) * (e.y - y);
|
||||
cands.sort((a, b) => (hit.has(a) ? 1 : 0) - (hit.has(b) ? 1 : 0) || d2(a) - d2(b) || a.spawnSeq - b.spawnSeq);
|
||||
const tgt = cands[0];
|
||||
left--;
|
||||
@@ -158,7 +159,7 @@ export default {
|
||||
const dash = (ctx) => {
|
||||
const { battle, unit } = ctx;
|
||||
let dx = ctx.x - unit.x, dy = ctx.y - unit.y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
const len = hypot(dx, dy);
|
||||
if (len > 1e-9) { dx /= len; dy /= len; } else { const [fr, fc] = unit.fwd; dx = fc; dy = fr; }
|
||||
const ahead = DASH_SPEED * Math.max(0, num(b2['attack@move_ahead_time'], 0.5));
|
||||
battle.addProjectile({ from: { x: ctx.x, y: ctx.y }, to: { x: ctx.x + dx * ahead, y: ctx.y + dy * ahead }, speed: DASH_SPEED, visual: 'boomerang', source: unit, hitDead: true,
|
||||
|
||||
@@ -68,6 +68,7 @@ import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { hasHp, burstLocked } from '../../../damage.js';
|
||||
import { MIN_DAMAGE_RATIO } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_phatm2_1';
|
||||
const S2 = 'skchr_phatm2_2';
|
||||
@@ -134,7 +135,7 @@ function callKit(t, owner, b2) {
|
||||
const keys = keysAt(grid, tok);
|
||||
const cands = battle.enemiesInKeys([...keys], tok, ANY).filter((e) => !lured.includes(e) && !e.s.flags.attract
|
||||
&& (e.motion === 'FLY' || !!battle.grid.waypoints(Math.round(e.y), Math.round(e.x), tok.tileR, tok.tileC)));
|
||||
cands.sort((a, b) => (isElite(b) - isElite(a)) || (Math.hypot(a.x - tok.x, a.y - tok.y) - Math.hypot(b.x - tok.x, b.y - tok.y)) || a.spawnSeq - b.spawnSeq);
|
||||
cands.sort((a, b) => (isElite(b) - isElite(a)) || (hypot(a.x - tok.x, a.y - tok.y) - hypot(b.x - tok.x, b.y - tok.y)) || a.spawnSeq - b.spawnSeq);
|
||||
for (const e of cands) {
|
||||
if (lured.length >= cap) break;
|
||||
if (battle.applyStatus(e, 'attract', { duration: Math.max(0.05, endAt - battle.time), source: tok, point: [tok.tileR, tok.tileC] })) {
|
||||
@@ -146,7 +147,7 @@ function callKit(t, owner, b2) {
|
||||
// the first lured enemy within `arrive` of it: it leaves
|
||||
battle.on('tick', () => {
|
||||
if (!up(tok)) return;
|
||||
if (lured.some((e) => e.alive && mine(e) && Math.hypot(e.x - tok.x, e.y - tok.y) <= arrive + 1e-9)) battle.retreat(tok, { reason: 'expired', permanent: true });
|
||||
if (lured.some((e) => e.alive && mine(e) && hypot(e.x - tok.x, e.y - tok.y) <= arrive + 1e-9)) battle.retreat(tok, { reason: 'expired', permanent: true });
|
||||
}, { owner: tok });
|
||||
// leaving (any reason): 停顿 + the damage over time on every selectable enemy of its x-1; the 诱导 ends
|
||||
battle.on('death', (c) => {
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
import { num, talentBb, traitBb, moduleOn, skillRec, up, statBuff } from '../shared/tier1.js';
|
||||
import { toLocal } from '../../../dir.js';
|
||||
import { COLS, PROJECTILE_SPEEDS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_phenxi_1';
|
||||
const S2 = 'skchr_phenxi_2';
|
||||
@@ -136,7 +137,7 @@ function s3Shot(battle, unit, b3) {
|
||||
battle.addProjectile({ from: unit, to: { x, y }, speed: S3_SPEED, visual: 'bomb', source: unit, onHit: () => {
|
||||
battle.fx('aoe', { x, y, radius: S3_RADIUS, id: unit.id, skill: 'phenxi:s3' });
|
||||
for (const e of battle.foesInRadius(x, y, S3_RADIUS, true)) {
|
||||
const sc = Math.hypot(e.x - x, e.y - y) <= core + 1e-9 ? hi : lo;
|
||||
const sc = hypot(e.x - x, e.y - y) <= core + 1e-9 ? hi : lo;
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * sc * unit.s.atkScaleMul, type: 'phys', isAttack: true, isSkill: true, isSplash: true, tags: ['skill', 'phenxi:s3'] });
|
||||
}
|
||||
} });
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
import { num, talentBb, moduleBb, moduleOn, traitBb, skillRec, statBuff, onHitBy, enemiesInGrid, up } from '../shared/tier1.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skcom_atk_up[3]';
|
||||
const S2 = 'skchr_poca_2';
|
||||
@@ -161,7 +162,7 @@ export default {
|
||||
if (ds > 0) {
|
||||
onHitBy(battle, unit, ({ target, dmg }) => {
|
||||
if (!dmg.isAttack) return;
|
||||
const d = Math.hypot(target.x - unit.x, target.y - unit.y);
|
||||
const d = hypot(target.x - unit.x, target.y - unit.y);
|
||||
dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import { acquireTargets } from '../../../ai.js';
|
||||
import { aggregateMods } from '../../../buffs.js';
|
||||
import { dirVec } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_radian_1';
|
||||
const S2 = 'skchr_radian_2';
|
||||
@@ -160,7 +161,7 @@ function flyBullets(battle, t, dt) {
|
||||
const hit = () => {
|
||||
const near = battle.foesInRadius(b.x, b.y, BULLET_RADIUS).filter((e) => e.alive && !e.isFlying);
|
||||
if (!near.length) return false;
|
||||
near.sort((p, q) => Math.hypot(p.x - b.x, p.y - b.y) - Math.hypot(q.x - b.x, q.y - b.y) || p.spawnSeq - q.spawnSeq);
|
||||
near.sort((p, q) => hypot(p.x - b.x, p.y - b.y) - hypot(q.x - b.x, q.y - b.y) || p.spawnSeq - q.spawnSeq);
|
||||
battle.dealDamage(t, near[0], { amount: b.atk, type: 'phys', isAttack: true, tags: [TAG_BULLET] });
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ import { sortEnemyTargets } from '../../../targeting.js';
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js';
|
||||
import { startCountdown } from '../../tokens.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_ray_1';
|
||||
const S2 = 'skchr_ray_2';
|
||||
@@ -159,7 +160,7 @@ export default {
|
||||
if (!target.alive) { unit.mem.rayLoadExtra = num(unit.mem.rayLoadExtra) + Math.max(0, Math.floor(num(b1.cnt, 1))); return; }
|
||||
const force = num(b1.force, 1);
|
||||
const [fr, fc] = unit.fwd;
|
||||
const vx = target.x - unit.x, vy = target.y - unit.y, d = Math.hypot(vx, vy);
|
||||
const vx = target.x - unit.x, vy = target.y - unit.y, d = hypot(vx, vy);
|
||||
if (d >= PUSH_DIRECTIONAL_MIN_DIST && vx * fc + vy * fr >= d * Math.SQRT1_2) battle.push(target, force, { from: unit, dir: { x: fc, y: fr }, fixed: true });
|
||||
else battle.push(target, force - 1, { from: unit });
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ import { num, talentBb, moduleBb, traitBb, moduleOn, skillRec, up } from '../sha
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { offsetTile } from '../../../dir.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skcom_assist_cost[3]';
|
||||
const S2 = 'skchr_sleach_2';
|
||||
@@ -150,7 +151,7 @@ function s3Tile(battle, unit, grid) {
|
||||
for (const k of keys) {
|
||||
const r = (k / COLS) | 0, c = k % COLS;
|
||||
if (!ground(r, c) || battle.unitAt(r, c)) continue;
|
||||
const d = Math.hypot(c - top.x, r - top.y);
|
||||
const d = hypot(c - top.x, r - top.y);
|
||||
if (d < bd - 1e-9) { bd = d; best = [r, c]; }
|
||||
}
|
||||
return best ?? [unit.tileR, unit.tileC];
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
import { num, talentBb, moduleOn, traitBb, skillRec, batMod, onHitBy, skillBusy, up } from '../shared/tier1.js';
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { ROWS, COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skcom_quickattack[3]';
|
||||
const S2 = 'skchr_typhon_2';
|
||||
@@ -204,7 +205,7 @@ export default {
|
||||
if (ds > 0) {
|
||||
onHitBy(battle, unit, ({ target, dmg }) => {
|
||||
if (!dmg.isAttack) return;
|
||||
const d = Math.hypot(target.x - unit.x, target.y - unit.y);
|
||||
const d = hypot(target.x - unit.x, target.y - unit.y);
|
||||
dmg.mul *= 1 + ds * Math.max(0, Math.min(1, (d - lo) / Math.max(1e-6, hi - lo)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, toggleBuff } from '../shared/tier1.js';
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_veen_1';
|
||||
const S2 = 'skchr_veen_2';
|
||||
@@ -115,7 +116,7 @@ export default {
|
||||
if (!pool.length) pool = near;
|
||||
let next = null, bd = Infinity;
|
||||
for (const e of pool) {
|
||||
const d = Math.hypot(e.x - cur.x, e.y - cur.y);
|
||||
const d = hypot(e.x - cur.x, e.y - cur.y);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && next && e.spawnSeq < next.spawnSeq)) { bd = d; next = e; }
|
||||
}
|
||||
if (!next) return; // nobody within reach: the bounces left are lost
|
||||
|
||||
@@ -49,6 +49,7 @@ import { num, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { startCountdown } from '../../tokens.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_weedy_1';
|
||||
const S2 = 'skchr_weedy_2';
|
||||
@@ -92,7 +93,7 @@ export function rupture(battle, src, e, { duration, perTile, interval }) {
|
||||
return;
|
||||
}
|
||||
const st = { x: e.x, y: e.y, moved: 0, acc: 0 };
|
||||
const step = (u) => { st.moved += Math.hypot(u.x - st.x, u.y - st.y); st.x = u.x; st.y = u.y; };
|
||||
const step = (u) => { st.moved += hypot(u.x - st.x, u.y - st.y); st.x = u.x; st.y = u.y; };
|
||||
const flush = (u) => {
|
||||
const m = st.moved;
|
||||
st.moved = 0;
|
||||
@@ -121,7 +122,7 @@ export function liquidNitrogen(battle, { shooter, src, atk, sbb, grid, force, pi
|
||||
const keys = absoluteRangeKeys(grid ?? GRID_4_1, shooter.tileR, shooter.tileC, shooter.dir, 0);
|
||||
const cands = battle.enemiesInKeys(keys, shooter, { canHitFly: true });
|
||||
if (!cands.length) return false;
|
||||
const key = pick === 'nearest' ? (e) => Math.hypot(e.x - shooter.x, e.y - shooter.y) : (e) => battle.remainingDistance(e);
|
||||
const key = pick === 'nearest' ? (e) => hypot(e.x - shooter.x, e.y - shooter.y) : (e) => battle.remainingDistance(e);
|
||||
let target = null, best = Infinity;
|
||||
for (const e of cands) {
|
||||
const v = key(e);
|
||||
|
||||
@@ -61,6 +61,7 @@ import { num, talentBb, traitBb, skillRec, up, batMod } from '../shared/tier1.js
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
const S1 = 'skchr_wisdel_1';
|
||||
const S2 = 'skchr_wisdel_2';
|
||||
@@ -138,7 +139,7 @@ function shadowTile(battle, host) {
|
||||
for (const k of host.rangeKeys ?? []) {
|
||||
const r = Math.floor(k / COLS), c = k % COLS;
|
||||
if (!battle.grid.inRect(r, c) || !battle.grid.canStand(r, c, { ranged: true }) || battle.isReservedTile(r, c)) continue;
|
||||
const d = Math.hypot(r - host.tileR, c - host.tileC);
|
||||
const d = hypot(r - host.tileR, c - host.tileC);
|
||||
if (d < bd - 1e-9 || (Math.abs(d - bd) <= 1e-9 && (r < best[0] || (r === best[0] && c < best[1])))) { bd = d; best = [r, c]; }
|
||||
}
|
||||
return best;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
import { bodyInKeys } from '../../../body.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
// ---- text-only constants (the official blackboards carry no key for these) --------------------------------------
|
||||
/** 华法琳 S1 "只当目标生命值不满一半时才会触发"; 塞雷娅 S1 "血量小于等于一半"; 山 module "生命值高于50%时". */
|
||||
@@ -70,7 +71,7 @@ const maxCharges = (chess, def) => num(def?.skill?.maxCharges, num(chess?.skill?
|
||||
const batPct = (sec, chess) => { const b = num(chess?.stats?.bat, 1) || 1; const v = num(sec) / b; return v ? Math.max(-0.9, v) : 0; };
|
||||
/** Drop zero / non-finite entries (a zero mod is noise in the buff list). */
|
||||
const mods = (m) => { const o = {}; for (const k of Object.keys(m)) { const v = m[k]; if (typeof v === 'number' && Number.isFinite(v) && v !== 0) o[k] = v; } return o; };
|
||||
const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
||||
const dist = (a, b) => hypot(a.x - b.x, a.y - b.y);
|
||||
const nationOf = (u) => u?.def?.raw?.nationId ?? null;
|
||||
const inFaction = (u, bond, nations) => !!u?.def && ((u.def.bonds || []).includes(bond) || nations.includes(nationOf(u)));
|
||||
const isAbyssal = (u) => u?.def?.raw?.groupId === 'abyssal' || ABYSSAL.has(u?.def?.charId);
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS, ROWS, PULL_STOP_RADIUS } from '../../../constants.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { hypot } from '../../../detmath.js';
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
// helpers
|
||||
@@ -237,7 +238,7 @@ function bestTile(battle, tiles) {
|
||||
let best = tiles[0], bd = Infinity;
|
||||
for (const t of tiles) {
|
||||
let d = Infinity;
|
||||
for (const e of en) d = Math.min(d, Math.hypot(e.x - t[1], e.y - t[0]));
|
||||
for (const e of en) d = Math.min(d, hypot(e.x - t[1], e.y - t[0]));
|
||||
if (d < bd - 1e-9) { bd = d; best = t; }
|
||||
}
|
||||
return best;
|
||||
|
||||
@@ -92,6 +92,7 @@ import { genericKit } from './generic.js';
|
||||
import { bardRegen } from '../professions.js';
|
||||
import { normDir, localOrder } from '../dir.js';
|
||||
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
|
||||
import { hypot } from '../detmath.js';
|
||||
|
||||
const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : (typeof v === 'string' && v.trim() !== '' && Number.isFinite(+v) ? +v : d));
|
||||
const GRID_3X3 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
|
||||
@@ -451,7 +452,7 @@ export function findSummonTile(battle, owner, placement = 'melee', opts = {}) {
|
||||
let cover = 0;
|
||||
if (placement === 'enemy' || placement === 'melee') cover = countNear(enemies, r, c, 1);
|
||||
else if (placement === 'ally') cover = countNear(injured, r, c, 1);
|
||||
const d = placement === 'ally' ? Math.hypot(c - owner.x, r - owner.y) : Math.hypot(c - focus.x, r - focus.y);
|
||||
const d = placement === 'ally' ? hypot(c - owner.x, r - owner.y) : hypot(c - focus.x, r - focus.y);
|
||||
// last: the tile's offset in the owner's facing-RIGHT frame (for a RIGHT owner = the tile key order), so equal
|
||||
// candidates resolve the same way whichever direction the owner faces
|
||||
const [lr, lc] = localOrder(r - owner.tileR, c - owner.tileC, owner.dir);
|
||||
@@ -1236,7 +1237,7 @@ function yanyouKit(bb, raw) {
|
||||
const keys = [];
|
||||
const r0 = Math.floor(u.y - radius), r1 = Math.ceil(u.y + radius), c0 = Math.floor(u.x - radius), c1 = Math.ceil(u.x + radius);
|
||||
for (let r = Math.max(0, r0); r <= Math.min(ROWS - 1, r1); r++) {
|
||||
for (let c = Math.max(0, c0); c <= Math.min(COLS - 1, c1); c++) if (Math.hypot(c - u.x, r - u.y) <= radius + 1e-9) keys.push(r * COLS + c);
|
||||
for (let c = Math.max(0, c0); c <= Math.min(COLS - 1, c1); c++) if (hypot(c - u.x, r - u.y) <= radius + 1e-9) keys.push(r * COLS + c);
|
||||
}
|
||||
u.rangeKeys = keys;
|
||||
u.rangeKeySet = new Set(keys);
|
||||
@@ -1299,7 +1300,7 @@ function yanyouKit(bb, raw) {
|
||||
const tgt = lockOf(unit) ?? topAggroEnemy(battle, unit);
|
||||
if (tgt) {
|
||||
const dx = tgt.x - unit.x, dy = tgt.y + num(unit.mem.hoverDy, 0) - unit.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const d = hypot(dx, dy);
|
||||
if (d > YANYOU_STOP + 1e-6) {
|
||||
const step = Math.min(d - YANYOU_STOP, speed * dt);
|
||||
const R = battle.rect;
|
||||
@@ -1351,7 +1352,7 @@ function airTile(battle, playerId, taken) {
|
||||
if (taken.has(r * COLS + c) || held.has(r * COLS + c) || !tileFree(battle, r, c)) continue;
|
||||
const t = battle.grid.tile(r, c);
|
||||
const cls = t.build === 'NONE' && t.pass !== 'ALL' ? 0 : t.build === 'NONE' ? 1 : 2;
|
||||
const s = [cls, Math.hypot(r - cr, c - cc), r * COLS + c];
|
||||
const s = [cls, hypot(r - cr, c - cc), r * COLS + c];
|
||||
let less = !bs;
|
||||
if (!less) for (let i = 0; i < 3; i++) { if (s[i] < bs[i] - 1e-9) { less = true; break; } if (s[i] > bs[i] + 1e-9) break; }
|
||||
if (less) { best = [r, c]; bs = s; }
|
||||
|
||||
Reference in new issue
Block a user