feedback7: merge fb7-unbalance — the official 失衡 (UNBALANCE) after a push / pull (the idea of PR #392 by @xcdoge, re-implemented from PRTS): no walking, attacking or skills while it lasts; pushes by the 推力-位移 time table, pulls by the force window (0.5 / 1 s), static bodies 0.1 s pushed / the window pulled; 弧光锋卫 bleeds 400 / 0.066 s while unbalanced; the client slide uses the same time
- The owner asked to verify the official rule before deciding (research: PRTS 失衡位移机制 / 推与拉 / 游戏数据基础 / 特殊机制,
the client's Rigidbody2D data); the PR's 0.6387·√tiles formula and fear exemption were not taken.
- Grok review: MERGE AFTER FIXES — two HIGH (static pulls, 弧光锋卫's bleed), skills during the hold, husk 重生, exact
frames — fixed on the branch (1db84d79).
- Golden union: bond-miraShip-low differs from HEAD (fewer enemy attacks: 169 → 166 events, same kills) though the
branch alone left it — HEAD's ×1.1 练度 path now displaces an enemy there. Broad node --test 5681 (5679 pass, 0 fail,
2 skipped); lint 0 errors.
This commit is contained in:
commit
0ae2fac2b1
27 files changed
+1452
-1058
No files matched your search
+14
-7
@@ -537,13 +537,17 @@ export function updateEnemy(b, e, dt) {
|
||||
// hidden (teleporting) enemies only advance wait legs
|
||||
const stunned = e.s.flags.stun;
|
||||
if (stunned || e.hidden || e.s.flags.fear) b._cutAttackStand(e);
|
||||
// 失衡 (UNBALANCE, battle/displacement.js _unbalance): a state machine, not a status — 浮空 ends it (PRTS 术语释义 浮空
|
||||
// 「触发浮空时清除受到的推/拉力…无法陷入失衡」); while it lasts the enemy neither walks nor starts a normal attack
|
||||
if (e.s.flags.levitate && e.unbalanceUntil > b.time) e.unbalanceUntil = -Infinity;
|
||||
const unbalanced = b.time < e.unbalanceUntil - 1e-9; // (the 1e-9: a float-summed end such as 35/30 s keeps its frame)
|
||||
const prevCd = e.atkCd;
|
||||
if (e.atkCd > 0 && !stunned && !e.hidden) e.atkCd = Math.max(0, e.atkCd - dt);
|
||||
// a stun / freeze / sleep / 浮空 — or leaving the field — takes the enemy out of its attack: a swing short of its damage
|
||||
// frame does not land, and the attack starts again from its wind-up afterwards (enemyAttack)
|
||||
if (e.swing && (stunned || e.hidden)) e.swing = false;
|
||||
// a stun / freeze / sleep / 浮空 / 失衡 — or leaving the field — takes the enemy out of its attack: a swing short of its
|
||||
// damage frame does not land, and the attack starts again from its wind-up afterwards (enemyAttack)
|
||||
if (e.swing && (stunned || e.hidden || unbalanced)) e.swing = false;
|
||||
// true: an unblocked ranged enemy in the wind-up of its next attack with a target in range (it stands)
|
||||
const winding = !e.hidden && !stunned && enemyAttack(b, e, prevCd);
|
||||
const winding = !e.hidden && !stunned && !unbalanced && enemyAttack(b, e, prevCd);
|
||||
if (!e.alive) return;
|
||||
// a stun / freeze / sleep cuts the attack clip short: no stand left once it ends [ASSUMED]. 沉睡 also holds 不可阻挡
|
||||
// (PRTS 异常效果 SLEEPING = 无法行动+无敌+不可阻挡): a sleeper's blocker lets go — its swing was cut above; Battle.applyStatus
|
||||
@@ -572,12 +576,13 @@ export function updateEnemy(b, e, dt) {
|
||||
if (b.time < e.pauseUntil) return;
|
||||
// standing for an attack clip (attackStand, GitHub #58): only the walking waits — a checkpoint's WAIT keeps running
|
||||
// and DISAPPEAR / APPEAR legs still happen (advanceRoute); drawn idle (the client plays the clip, then Move again);
|
||||
// a 恐惧 runs at once (it cannot attack)
|
||||
const standing = winding || (b.time < e.atkStandUntil && !e.s.flags.fear);
|
||||
// a 恐惧 runs at once (it cannot attack). 失衡 holds the walking too — under 恐惧 / 诱导 as well (失衡免疫 says 「失衡期间
|
||||
// 无法自主移动」; no source exempts a fear)
|
||||
const standing = winding || unbalanced || (b.time < e.atkStandUntil && !e.s.flags.fear);
|
||||
if (e.s.flags.noMove) { e.moving = false; return; } // standing (a 重生, a form change): drawn idle, not walking
|
||||
// 恐惧 (ba.fear "无法被阻挡并四散逃跑"; PRTS 诱发移动: 恐惧 outranks 诱导): runs to random tiles of the fan away from
|
||||
// its source — a self-inflicted fear flutters inside its own tile (fear.js); the route re-plans once it ends
|
||||
if (e.s.flags.fear && !e.hidden) { moveFeared(b, e, dt); return; }
|
||||
if (e.s.flags.fear && !e.hidden) { if (unbalanced) e.moving = false; else moveFeared(b, e, dt); return; }
|
||||
if (e.mem.fearMove) endFear(e);
|
||||
// 诱导 (ba.attract "无法被阻挡并向目标位置移动"): walks to the attract point instead of following its route
|
||||
if (e.s.flags.attract) { if (standing) e.moving = false; else moveAttracted(b, e, dt); return; }
|
||||
@@ -658,6 +663,8 @@ function advanceRoute(b, e, dt, R, standing = false) {
|
||||
if (leg.t === 'appear') {
|
||||
e.x = leg.c; e.y = leg.r;
|
||||
b._setHidden(e, false);
|
||||
// a relocation is a forced state switch: it ends a 失衡 (PRTS 失衡位移机制 「被强制切换至其他状态机」)
|
||||
e.unbalanceUntil = -Infinity;
|
||||
R.legIdx++; R.pts = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// directional), pull, the raw mover displace and who can be moved at all.
|
||||
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
||||
|
||||
import { COLS, PUSH_TILES, PUSH_TILES_EFFECT, PULL_WEAK_SHARE, PULL_CRAWL, PULL_ORIGIN, PULL_STOP_RADIUS, PUSH_DIRECTIONAL_MIN_DIST } from '../constants.js';
|
||||
import {
|
||||
COLS, PUSH_TILES, PUSH_TILES_EFFECT, PULL_WEAK_SHARE, PULL_CRAWL, PULL_ORIGIN, PULL_STOP_RADIUS, PUSH_DIRECTIONAL_MIN_DIST,
|
||||
PUSH_UNBALANCE, PULL_UNBALANCE, PULL_UNBALANCE_WEAK, UNBALANCE_MIN,
|
||||
} from '../constants.js';
|
||||
import { fin } from './util.js';
|
||||
import { hypot } from '../detmath.js';
|
||||
|
||||
@@ -16,6 +19,18 @@ export function pushTiles(level, effect = false) {
|
||||
return (effect ? PUSH_TILES_EFFECT : PUSH_TILES)[Math.min(3, l)];
|
||||
}
|
||||
|
||||
/** 失衡 (UNBALANCE) of a push of 受力等级 `level`, game seconds (constants.js PUSH_UNBALANCE: ≤ −3 → 0, ≥ 3 → the 3 value). */
|
||||
export function pushUnbalance(level) {
|
||||
const l = Math.round(fin(level, -99));
|
||||
return l <= -3 ? 0 : PUSH_UNBALANCE[Math.min(3, l)];
|
||||
}
|
||||
|
||||
/** 失衡 of a pull of 受力等级 `level`: its force window (PULL_UNBALANCE, 0.5 s below −1), none at ≤ −3 (force 0). */
|
||||
export function pullUnbalance(level) {
|
||||
const l = Math.round(fin(level, -99));
|
||||
return l <= -3 ? 0 : l < -1 ? PULL_UNBALANCE_WEAK : PULL_UNBALANCE;
|
||||
}
|
||||
|
||||
export class BattleDisplacement {
|
||||
/**
|
||||
* 受力等级 of enemy `e` under a push / pull of 力度 `force` (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3, 大力+1 4, 特大力 5):
|
||||
@@ -40,7 +55,7 @@ export class BattleDisplacement {
|
||||
* Returns the tiles moved.
|
||||
*/
|
||||
push(e, force, { from = null, dir = null, fixed = false, fixedAngle = false, inward = false, effect = false } = {}) {
|
||||
if (!this._displaceable(e)) return 0;
|
||||
if (!this._displaceable(e)) { this._staticForce(e, force, false); return 0; }
|
||||
let level = this.forceLevel(e, force);
|
||||
const fx0 = fin(from?.x, e.x), fy0 = fin(from?.y, e.y);
|
||||
const vx = e.x - fx0, vy = e.y - fy0, d = hypot(vx, vy);
|
||||
@@ -58,7 +73,12 @@ export class BattleDisplacement {
|
||||
else return 0;
|
||||
let dist = pushTiles(level, effect);
|
||||
if (inward && !(dl > 0)) { ux = -ux; uy = -uy; dist = Math.min(dist, Math.max(0, d - PULL_STOP_RADIUS)); }
|
||||
return this.displace(e, { x: ux, y: uy }, dist);
|
||||
// 失衡 for the row's 位移时间 — also when the 特效 column or a wall shortens the slide [ASSUMED for the wall]; a slide a
|
||||
// wall stops at the first step gets the 0.1 s floor, like a body that cannot move [ASSUMED: 碰撞、停止 is 待补充]
|
||||
const hold = pushUnbalance(level);
|
||||
const moved = this.displace(e, { x: ux, y: uy }, dist, { dur: hold });
|
||||
if (hold > 0) this._unbalance(e, moved > 0 ? hold : UNBALANCE_MIN);
|
||||
return moved;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,12 +88,17 @@ export class BattleDisplacement {
|
||||
* the official 拉力起点 in front of an operator. Returns the tiles moved.
|
||||
*/
|
||||
pull(e, force, { to, center = null, stop = PULL_STOP_RADIUS } = {}) {
|
||||
if (!this._displaceable(e) || !to) return 0;
|
||||
if (!to) return 0;
|
||||
// an enemy the puller itself blocks already stands in front of it (at contact) [ASSUMED: no pull, no unblocking]
|
||||
if (center && center.side === 'ally' && e.blockedBy === center) return 0;
|
||||
if (e && center && center.side === 'ally' && e.blockedBy === center) return 0;
|
||||
if (!this._displaceable(e)) { this._staticForce(e, force, true); return 0; }
|
||||
const level = this.forceLevel(e, force);
|
||||
// 失衡 for the force window, whatever the travel: the 急停 zeroes the movement, the state lasts to the window's end
|
||||
const hold = pullUnbalance(level);
|
||||
if (!(hold > 0)) return 0;
|
||||
const tx = fin(to.x, e.x), ty = fin(to.y, e.y);
|
||||
const dx = tx - e.x, dy = ty - e.y, d0 = hypot(dx, dy);
|
||||
if (!(d0 > 1e-6)) return 0;
|
||||
if (!(d0 > 1e-6)) { this._unbalance(e, hold); return 0; }
|
||||
const ux = dx / d0, uy = dy / d0;
|
||||
// travel until inside the stop circle around `center` (smaller root of |e + t·u − c| = stop), else up to `to`
|
||||
let full = d0;
|
||||
@@ -84,9 +109,10 @@ export class BattleDisplacement {
|
||||
const disc = wu * wu - w2 + r * r;
|
||||
if (disc >= 0) { const t = -wu - Math.sqrt(disc); if (t >= 0) full = Math.min(full, t); }
|
||||
}
|
||||
const level = this.forceLevel(e, force);
|
||||
const dist = level >= 0 ? full : level === -1 ? Math.min(full, PULL_WEAK_SHARE * d0) : level === -2 ? Math.min(full, PULL_CRAWL) : 0;
|
||||
return dist > 1e-6 ? this.displace(e, { x: ux, y: uy }, dist) : 0;
|
||||
const dist = level >= 0 ? full : level === -1 ? Math.min(full, PULL_WEAK_SHARE * d0) : Math.min(full, PULL_CRAWL);
|
||||
const moved = dist > 1e-6 ? this.displace(e, { x: ux, y: uy }, dist, { dur: hold }) : 0;
|
||||
this._unbalance(e, hold);
|
||||
return moved;
|
||||
}
|
||||
|
||||
/** Official distance (tiles) a push of 力度 `force` would move `e` on open ground (0 when it cannot be displaced). */
|
||||
@@ -107,12 +133,43 @@ export class BattleDisplacement {
|
||||
* 失衡状态 … 但物理层面上无法产生任何速度或移动" — every air unit of the mode except “炎佑”, plus the boss 昆图斯 (build-data
|
||||
* STATIC_BODIES; player report after 0.1.0, "飞机可以被薄绿的技能拉走"). A skill that reaches it still hits it (its targeting
|
||||
* is the skill's own: 锏 S3, 薄绿, the 钩索师 …); only the movement is 0, so distance-based effects (drag damage, 见行者 S2's
|
||||
* wall stun) come to nothing. [ASSUMED] the 0.1 s 失衡硬直 a 静态刚体 still gets is not modelled (no displacement models it).
|
||||
* wall stun) come to nothing — but a force > 0 still puts it in the 失衡 state for its 0.1 s floor (_staticForce).
|
||||
*/
|
||||
_displaceable(e) {
|
||||
return !!(e && e.alive && e.side === 'enemy' && !e.isBoss && !e.s.flags.noDisplace && !(e.def && e.def.staticBody));
|
||||
}
|
||||
|
||||
/**
|
||||
* A push (`pull` false) / pull of 力度 `force` on a 静态刚体 (data `staticBody`; not 失衡免疫 `noDisplace` — PRTS 特殊机制
|
||||
* 静态刚体: unlike 失衡免疫 it "可以进入失衡状态并启用物理,但物理层面上无法产生任何速度或移动", leaving it 「在失去施力后立刻」 with
|
||||
* 「0.1 秒保底持续时间」): no movement, the state as long as the force acts — a push's is instant (推与拉 「作用时间:瞬间」) so
|
||||
* the UNBALANCE_MIN floor, a pull's is its window (pullUnbalance: 1 s, 0.5 s below −1); a force > 0 (受力等级 ≥ −2) only.
|
||||
* Leaders (the boss pool) stay out.
|
||||
*/
|
||||
_staticForce(e, force, pull = false) {
|
||||
if (!(e && e.alive && e.side === 'enemy' && !e.isBoss && !e.s.flags.noDisplace && e.def && e.def.staticBody)) return;
|
||||
const level = this.forceLevel(e, force);
|
||||
if (level >= -2) this._unbalance(e, pull ? Math.max(UNBALANCE_MIN, pullUnbalance(level)) : UNBALANCE_MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put enemy `e` in the 失衡 (UNBALANCE) state for `dur` game seconds from now (a running one is kept when it lasts longer):
|
||||
* a state machine, not a status (PRTS 异常效果: no 阻止攻击 status — no stun, no immunity, SP untouched) — ai.js
|
||||
* updateEnemy reads `unbalanceUntil`: the enemy neither walks (恐惧 / 诱导 included) nor starts a normal attack, a swing
|
||||
* short of its frame is cut; checks that ignore the state (水遁忍者's damage, content abilities) go on. The state machine
|
||||
* switch ends the attack clip it stood for (PRTS 状态机: the states are exclusive — UNBALANCE, then DEFAULT → MOVE). It
|
||||
* ends with its time, or at once with 浮空 (术语释义 浮空 「触发浮空时清除受到的推/拉力…无法陷入失衡」, ai.js), a 重生 or a route
|
||||
* APPEAR (forced state switches). No skill meanwhile either (失衡免疫 「…使用技能」): content skills, blinks and scripted moves
|
||||
* check `enemies/helpers.js unbalancedNow` (docs/SIM.md lists what deliberately keeps running).
|
||||
*/
|
||||
_unbalance(e, dur) {
|
||||
if (!(dur > 0) || !e || !e.alive) return;
|
||||
const until = this.time + dur;
|
||||
if (!(e.unbalanceUntil >= until)) e.unbalanceUntil = until;
|
||||
this._cutAttackStand(e);
|
||||
e.atkStandUntil = -Infinity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an enemy `distance` tiles along `dir` = {x, y} (normalised internally) over passable tiles — the raw mover of
|
||||
* push() / pull(), which apply the official 力度 − 重量 rules (content uses those; the old `force` option is gone).
|
||||
@@ -120,7 +177,7 @@ export class BattleDisplacement {
|
||||
* ⇒ no movement (_displaceable). The tiles it may cross follow its movement (`motion`): a hovering enemy walks the
|
||||
* ground, so it stays on ground-passable tiles.
|
||||
*/
|
||||
displace(e, dir, distance) {
|
||||
displace(e, dir, distance, { dur = 0 } = {}) {
|
||||
if (!this._displaceable(e) || !dir) return 0;
|
||||
const dxv = fin(dir.x, 0), dyv = fin(dir.y, 0);
|
||||
const len = hypot(dxv, dyv);
|
||||
@@ -144,7 +201,8 @@ export class BattleDisplacement {
|
||||
this._cutAttackStand(e);
|
||||
e.atkStandUntil = -Infinity;
|
||||
if (e.route) e.route.pts = null;
|
||||
this.fx('displace', { x: e.x, y: e.y, id: e.id });
|
||||
// `dur` (game s): the 失衡 the push / pull gives — the client's slide takes that long (render/units.js slideTo)
|
||||
this.fx('displace', dur > 0 ? { x: e.x, y: e.y, id: e.id, dur } : { x: e.x, y: e.y, id: e.id });
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ export class BattleSpawns {
|
||||
e.atkCd = 0;
|
||||
e.pauseUntil = -Infinity; // content holds (暴鸰's drop)
|
||||
e.atkStandUntil = -Infinity; // standing for its attack clip (ai.js attackStand)
|
||||
e.unbalanceUntil = -Infinity; // 失衡 (UNBALANCE) after a push / pull (battle/displacement.js _unbalance, ai.js)
|
||||
e.atkStandCutAt = -Infinity; // display metadata: when its stand was last cut or ignored (snapshot standCut)
|
||||
e.swing = false; // a normal attack swung, its damage frame not reached yet (ai.js enemyAttack)
|
||||
// every enemy profile starts with the same fields (stable object shapes keep the hot loop's property reads fast);
|
||||
|
||||
@@ -128,6 +128,22 @@ export const PULL_CRAWL = 0.03;
|
||||
export const PULL_ORIGIN = 0.5;
|
||||
export const PULL_STOP_RADIUS = 0.6708;
|
||||
export const PUSH_DIRECTIONAL_MIN_DIST = 0.25;
|
||||
/**
|
||||
* 失衡 (UNBALANCE) — the state machine a force > 0 puts an enemy in (PRTS 失衡位移机制 「当一名敌方单位受到一个任意来源的力,
|
||||
* 并且受力大小 > 0 时,该敌方单位将进入失衡(UNBALANCE)状态机」; 异常效果图鉴/失衡免疫 「失衡期间无法自主移动、发动攻击、使用技能」 —
|
||||
* not an abnormal status: 异常效果 「失衡…不属于异常效果,它是一种状态机」, so it is no stun). Its length in game seconds:
|
||||
* PUSH_UNBALANCE = a push by 受力等级 — PRTS 游戏数据基础 推力-位移近似对应表 「位移时间」 (whole frames at 30 fps: 6, 12, 24,
|
||||
* 27, 32, 35; the 0.1 s floor is inside them); PULL_UNBALANCE / PULL_UNBALANCE_WEAK = a pull's force window — 推与拉 §拉力
|
||||
* 「作用时间默认为 1 s;若本次受力等级 < −1,作用时间改为 0.5 s」, the state lasting to its end after the 急停 too (「目标将仍保持
|
||||
* 失衡状态至拉力作用时间结束为止」); UNBALANCE_MIN = the 失衡硬直 floor (「立刻拥有 0.1 s 的“失衡硬直”。此期间无法解除失衡状态机
|
||||
* ——哪怕已经没有被移动或者受力」) — what a 静态刚体 hit by a push gets (特殊机制 静态刚体 「失衡状态拥有 0.1 秒保底持续时间」; a pulled
|
||||
* one stays for the pull's window, its force lasting that long).
|
||||
* PR #392 by @xcdoge brought the state; its 0.6387·√tiles was derived from the page's μ = 0.5 单位假设, not printed.
|
||||
*/
|
||||
export const PUSH_UNBALANCE = Object.freeze({ '-2': 0.2, '-1': 0.4, 0: 0.8, 1: 0.9, 2: 32 / 30, 3: 35 / 30 });
|
||||
export const PULL_UNBALANCE = 1;
|
||||
export const PULL_UNBALANCE_WEAK = 0.5;
|
||||
export const UNBALANCE_MIN = 0.1;
|
||||
|
||||
/**
|
||||
* Fallback freeze when a second 寒冷 lands and neither the remaining cold nor the incoming one has a duration
|
||||
|
||||
@@ -73,7 +73,7 @@ import { compileRoute } from '../ai.js';
|
||||
import { normalizeRoute } from '../simdata.js';
|
||||
import {
|
||||
ensureInstalled, abOf, attach, T, elem, hurt, targetsNear, allTargets, byPriority, areaAllies, areaAlliesInTiles, fieldAllies,
|
||||
remainingRoute, stayRoute, stepToward, setHits, hitCount, lpLoss, blinkForward, canCast, absorbArts, nthOf,
|
||||
remainingRoute, stayRoute, stepToward, setHits, hitCount, lpLoss, blinkForward, canCast, unbalancedNow, absorbArts, nthOf,
|
||||
} from './enemies.js';
|
||||
import { hypot, powi } from '../detmath.js';
|
||||
|
||||
@@ -539,7 +539,7 @@ function kitBlade(ab, e, b, tpl) {
|
||||
if (P.state === 'dive' && e2.alive && ++P.hits >= (bb.max_hit_cnt ?? Infinity)) shotDown(b2, e2); // 仅1次
|
||||
},
|
||||
tick(b2, e2, a, dt) {
|
||||
if (P.state !== 'dive' || !canCast(e2)) return; // a stunned blade does not fly on
|
||||
if (P.state !== 'dive' || !canCast(e2, false, b2)) return; // a stunned blade does not fly on
|
||||
const t = P.target;
|
||||
if (!stepToward(e2, t.c, t.r, e2.s.moveSpeed * MOVE_SCALE * dt)) return;
|
||||
// arrival: 3×3 stun + DoT (无来源: credited to 胄, as the shell's), then the 初始模式 copy takes over
|
||||
@@ -593,7 +593,7 @@ function kitGun(ab, e, b) {
|
||||
dealt(c, b2, e2) { elem(b2, e2, c.target, 'erosion', e2.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); }, // 攻击时附带侵蚀损伤
|
||||
tick(b2, e2, a, dt) {
|
||||
const ch = P.charge;
|
||||
if (!ch) return;
|
||||
if (!ch || unbalancedNow(b2, e2)) return; // (失衡: no scripted move meanwhile)
|
||||
const t = ch.target;
|
||||
const sp = (e2.s.moveSpeed + (T(ab, '3.move_speed') ?? 0)) * MOVE_SCALE * dt;
|
||||
const nx = e2.x, ny = e2.y;
|
||||
@@ -711,7 +711,7 @@ function kitSpring(ab, e) {
|
||||
tick(b, e2, a, dt) {
|
||||
if (!P.up && P.downAt != null && b.time - P.downAt >= regen) raise(b);
|
||||
const d = e2.mem.ab.dash;
|
||||
if (!d) return;
|
||||
if (!d || unbalancedNow(b, e2)) return; // (失衡: no scripted move meanwhile)
|
||||
// 追逐模式 lasts the whole gain (PRTS 末日布道 "令全场的“碎铳之簧”获得5秒增益…增益期间切换为追逐模式"; “碎铳之簧”
|
||||
// 追逐模式 "持续追踪令其进入该形态的场上的假想敌:铳移动" — no arrival clause, unlike the 铳's own 冲锋模式): reaching
|
||||
// the gun does not end it, the spring keeps following the gun until the 5 s are over (PR #347 by @CXUtk; until
|
||||
@@ -820,7 +820,7 @@ function pipeCore(ab, e, b, tpl, { form, prefix }) {
|
||||
tick(b2, e2, a, dt) {
|
||||
P.acc += dt;
|
||||
if (P.acc >= (e2.hpRatio < thr ? lo : hi)) { P.acc = 0; summon(b2, e2); }
|
||||
if (!canCast(e2, false)) return;
|
||||
if (!canCast(e2, false, b2)) return;
|
||||
P.atk += dt;
|
||||
if (P.atk < e2.s.interval) return;
|
||||
const t = b2.rng.pick(echoesOf(b2, form)); // 优先攻击…形态余音
|
||||
@@ -1052,7 +1052,7 @@ function kitLion(ab, e, b) {
|
||||
b2.addBuff(e2, { key: 'boss:advance', persist: true, visible: true, mods: { defPct: T(ab, 'advance.def') ?? 0, resFlat: T(ab, 'advance.magic_resistance') ?? 0 } });
|
||||
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'lionAdvance' });
|
||||
}
|
||||
if (!canCast(e2, false)) return;
|
||||
if (!canCast(e2, false, b2)) return;
|
||||
P.sp += dt * ENEMY_SP_PER_SEC;
|
||||
if (P.sp >= cost && decree(b2, e2)) P.sp = 0;
|
||||
// 【莫非王土】: ANIMATE_DELAY s after equipment first lies on the field, then once per 王权号令 cycle
|
||||
@@ -1100,7 +1100,7 @@ function kitDeer(ab, e) {
|
||||
b.addBuff(e2, { key: 'boss:madness', persist: true, visible: true, mods: { physTakenMul: 1 - dr, artsTakenMul: 1 - dr } }); // 物理和法术伤害降低
|
||||
b.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'deerMadness' });
|
||||
}
|
||||
if (!canCast(e2, false)) return;
|
||||
if (!canCast(e2, false, b)) return;
|
||||
P.acc += dt;
|
||||
if (P.acc < e2.s.interval) return;
|
||||
const cands = fairOrder(b, e2, allTargets(b, e2), P);
|
||||
|
||||
@@ -119,7 +119,7 @@ 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,
|
||||
EROSION, EROSION_BURST, HUSK_REBIRTH, nthOf, abOf, attach, T, silenced, canCast, unbalancedNow, elem, hurt, alliesInTiles,
|
||||
targetsNear, allTargets, areaAllies, areaAlliesInTiles, fieldAllies, auraAllies, targetAndArea, byPriority, zone,
|
||||
remainingRoute, stayRoute, spawnChildren, stepToward, setHits, hitCount, isHitCount, setForm, lpLoss, expose,
|
||||
expectedFinal, absorbArts,
|
||||
@@ -260,7 +260,7 @@ function onTick(b, dt) {
|
||||
}
|
||||
if (a.fire && a.cd != null) {
|
||||
a.left -= dt;
|
||||
if (a.left <= 1e-9 && canCast(e, a.sil) && (!a.cond || safe(b, e, () => a.cond(b, e, a)))) {
|
||||
if (a.left <= 1e-9 && canCast(e, a.sil, b) && (!a.cond || safe(b, e, () => a.cond(b, e, a)))) {
|
||||
a.left = Math.max(TICK, a.cd);
|
||||
a.casts = (a.casts ?? 0) + 1;
|
||||
e.skillAnimUntil = b.time + 0.5;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MOVE_SCALE, COLS, STEALTH_RESTORE } from '../../constants.js';
|
||||
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,
|
||||
unbalancedNow,
|
||||
} from './helpers.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
|
||||
@@ -356,6 +357,7 @@ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = nu
|
||||
a.state = 'reborn';
|
||||
a.t0 = b.time;
|
||||
a.noAtk = e.profile.noAttack;
|
||||
e.unbalanceUntil = -Infinity; // a 重生 is a forced state change: it ends a 失衡 (PRTS 失衡位移机制 「例如复活」)
|
||||
rebirthCleanse(b, e);
|
||||
e.hp = Math.min(1, e.s.maxHp);
|
||||
if (onKo) safe(b, e, () => onKo(b, e, a));
|
||||
@@ -477,6 +479,7 @@ function husk({ hits, delay, stealthy = true, unblock = false, onHusk = null, ke
|
||||
if (a.state === 'husk' || !(hits > 0) || !(delay > 0)) return false; // the husk's knock-out is the real death
|
||||
a.state = 'husk';
|
||||
a.rebornUntil = b.time + HUSK_REBIRTH;
|
||||
e.unbalanceUntil = -Infinity; // a 重生 is a forced state change: it ends a 失衡 (PRTS 失衡位移机制 「例如复活」)
|
||||
a.noAtk = e.profile.noAttack;
|
||||
a.max = a.max ?? e.base.maxHp;
|
||||
rebirthCleanse(b, e);
|
||||
@@ -601,6 +604,7 @@ const skill = (s, fire, { cond = null, sil = false, cd = null, icd = null, id =
|
||||
|
||||
/** Blink past the blocker along the path (弑君者 / 卢西恩). Returns the start position. */
|
||||
export function blinkForward(b, e, dist) {
|
||||
if (unbalancedNow(b, e)) return null; // 失衡: 「无法…使用技能」 — no blink meanwhile
|
||||
const from = { x: e.x, y: e.y };
|
||||
const R = e.route;
|
||||
let left = dist;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { TICK, MOVE_SCALE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS } from '../../constants.js';
|
||||
import {
|
||||
num, T, hurt, targetsNear, allTargets, areaAllies, areaAlliesInTiles, fieldAllies, byPriority, spawnChildren,
|
||||
stepToward, setForm, expose,
|
||||
stepToward, setForm, expose, unbalancedNow,
|
||||
} from './helpers.js';
|
||||
import { enemyAura, allyAura, selfFear, skill, kitSelfFear } from './archetypes.js';
|
||||
import { hypot } from '../../detmath.js';
|
||||
@@ -258,6 +258,7 @@ export const FLY_KITS = Object.freeze({
|
||||
spawn(b, e) { e.profile.noAttack = true; },
|
||||
tick(b, e, a, dt) {
|
||||
if (a.t && !a.t.alive) { a.t = null; b.removeBuff(e, 'ab:dive'); }
|
||||
if (unbalancedNow(b, e)) return; // 失衡: no dive, no blast meanwhile
|
||||
if (!a.t) {
|
||||
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;
|
||||
|
||||
@@ -132,7 +132,15 @@ export function T(ab, ...keys) { for (const k of keys) { const v = ab.t[k]; if (
|
||||
|
||||
export function silenced(e) { return !!e.s.flags.silence; }
|
||||
|
||||
export function canCast(e, sil) { return e.alive && !e.hidden && !e.s.flags.stun && !(sil && e.s.flags.silence); }
|
||||
/**
|
||||
* Is enemy `e` in its 失衡 (UNBALANCE) state now (battle/displacement.js _unbalance; the 1e-9 keeps a float-summed
|
||||
* end — 35/30 s — on its frame)? PRTS 异常效果图鉴/失衡免疫: 「失衡期间无法自主移动、发动攻击、使用技能」 — content skills
|
||||
* (canCast), blinks and scripted moves wait for its end.
|
||||
*/
|
||||
export function unbalancedNow(b, e) { return !!(b && e && b.time < e.unbalanceUntil - 1e-9); }
|
||||
|
||||
/** Can enemy `e` use a skill now: alive, on the field, not stunned, not silenced (`sil`), not in its 失衡 state (`b` given). */
|
||||
export function canCast(e, sil, b = null) { return e.alive && !e.hidden && !e.s.flags.stun && !(sil && e.s.flags.silence) && !unbalancedNow(b, e); }
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// helpers (exported for bosses.js)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { aggroCmp, areaSelectable } from '../../targeting.js';
|
||||
import { periodicDamage } from '../../damage.js';
|
||||
import {
|
||||
nthOf, T, hurt, alliesInTiles, targetsNear, allTargets, areaAllies, areaAlliesInTiles, auraAllies, targetAndArea,
|
||||
byPriority, setForm, auraBuff, watchDeaths,
|
||||
byPriority, setForm, auraBuff, watchDeaths, unbalancedNow,
|
||||
} from './helpers.js';
|
||||
import {
|
||||
unblockable, maxTargets, onHitStatus, splashAttack, pathKeysAhead, noAirTargets, resist, nthAttackStatus, lowHpBuff,
|
||||
@@ -60,8 +60,6 @@ const XI_CROSS_REACH = 2;
|
||||
* Until 0.1.3: 2.5 / 2.5 / −30, the aura only during the 重生 [ASSUMED]. */
|
||||
const WOLF_RANGE = 1.25, WOLF_AWE_RADIUS = 1.5, WOLF_AWE_ASPD = -50;
|
||||
|
||||
/** 失衡 movement speed that turns 弧光锋卫's per-interval bleed into HP per tile moved [ASSUMED]. */
|
||||
const UNBALANCE_SPEED = 5;
|
||||
|
||||
/** 乌顶巨角卢鲁 【角力对决】: the operator cannot be pushed (fixed tiles) ⇒ "更多伤害" multiplier [ASSUMED]. */
|
||||
const ELK_FAIL_SCALE = 2;
|
||||
@@ -722,7 +720,7 @@ function kitElk(ab) {
|
||||
const ch = P.ch;
|
||||
if (!ch) return;
|
||||
if (e.s.flags.stun || !ch.t.alive || e.blockedBy !== ch.t) { stop(b, e); b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'chargeBroken' }); return; }
|
||||
if (b.time + 1e-9 < ch.until) return;
|
||||
if (b.time + 1e-9 < ch.until || unbalancedNow(b, e)) return; // a 失衡 that did not move it holds the clash (no skill meanwhile)
|
||||
stop(b, e);
|
||||
b.fx('explode', { x: ch.t.x, y: ch.t.y, r: 0.5, kind: 'elkClash' });
|
||||
hurt(b, e, ch.t, e.s.atk * ((s && s.bb.atk_scale_s) ?? 1) * ELK_FAIL_SCALE, 'phys');
|
||||
@@ -773,12 +771,23 @@ export const LEADER_KITS = Object.freeze({
|
||||
enemy_2085_skzjxd: (ab) => [unblockable(), frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)],
|
||||
enemy_2085_skzjxd_2: (ab) => [unblockable(), frontGuard(T(ab, 'Weakness.damage_resistance') ?? 0, faceCrowd)], // (鸭爵 strategy) same
|
||||
// 失衡 (pushed / pulled by operators)
|
||||
enemy_1328_cbjedi: (ab) => [unbalanced((b, e, a, d) => { // 弧光锋卫 · takes damage in proportion to the distance moved while unbalanced
|
||||
// PRTS 修正 "失衡移动时持续受到真实伤害", "处于失衡状态时,每0.066s受到400点无来源真实持续伤害" (伤害分类: 弧光锋卫失衡状态下的自残
|
||||
// 伤害 is BUFF damage): damage, not a 流失 (player report D1 audit)
|
||||
const v = T(ab, 'unbalanced_bleed.damage') ?? 0, iv = T(ab, 'unbalanced_bleed.interval') ?? 1;
|
||||
if (v > 0 && iv > 0) b.dealDamage(null, e, { ...periodicDamage((v * d) / (iv * UNBALANCE_SPEED)), tags: ['dot', 'periodic', 'unbalanced'] });
|
||||
})],
|
||||
enemy_1328_cbjedi: (ab) => { // 弧光锋卫 · bleeds while in its 失衡 state
|
||||
// PRTS 修正 "失衡移动时持续受到真实伤害", 天赋 "处于失衡状态时,每0.066s受到400点无来源真实持续伤害" (data unbalanced_bleed.damage /
|
||||
// .interval; 伤害分类: 弧光锋卫失衡状态下的自残伤害 is BUFF damage): damage, not a 流失 (player report D1 audit) — every `interval`
|
||||
// s for as long as the state lasts (battle/displacement.js _unbalance: a push's 位移时间, a pull's force window even
|
||||
// after the 急停 or with no movement). Until 0.2.2 one hit in proportion to the tiles moved [ASSUMED], from the position jump.
|
||||
const v = T(ab, 'unbalanced_bleed.damage') ?? 0, iv = T(ab, 'unbalanced_bleed.interval') ?? 0;
|
||||
return [{
|
||||
tick(b, e, a, dt) {
|
||||
if (!(v > 0 && iv > 0) || !unbalancedNow(b, e)) { a.bleed = 0; return; }
|
||||
a.bleed = (a.bleed ?? 0) + dt;
|
||||
while (a.bleed >= iv - 1e-9 && e.alive) {
|
||||
a.bleed -= iv;
|
||||
b.dealDamage(null, e, { ...periodicDamage(v), tags: ['dot', 'periodic', 'unbalanced'] });
|
||||
}
|
||||
},
|
||||
}];
|
||||
},
|
||||
enemy_10112_ymgds: (ab) => [unbalanced((b, e) => { // 冒失的小弟 · stunned after being unbalanced
|
||||
const st = T(ab, 'StunAfterUnbalance.stun') ?? 0;
|
||||
if (st > 0) b.applyStatus(e, 'stun', { duration: st, source: null });
|
||||
|
||||
Reference in new issue
Block a user