feedback7 R22-UNBAL: 失衡 review fixes — a pulled 静态刚体 is held for the pull's window, 弧光锋卫 bleeds 400 every 0.066 s while the state lasts, no enemy skill / blink / scripted move during it, a route APPEAR or a 逐火 重生 ends it, every push row holds its exact frames; golden: 32 move, 8 in outcomes (below)
Branch review of fb7-unbalance (MERGE AFTER FIXES): 1. A 静态刚体 pulled got the 0.1 s floor: _staticForce now knows push from pull — a push's force is instant (推与拉), so 0.1 s; a pull's lasts its window (pullUnbalance: 1 s, 0.5 s below −1) — PRTS 特殊机制 静态刚体 「在失去施力后立刻」 out, 「0.1 秒保底」. pull() checks the puller's own block before the static case. 2. 弧光锋卫 (PRTS 天赋 「处于失衡状态时,每0.066s受到400点无来源真实持续伤害」, data unbalanced_bleed): a tick bleed of 400 every 0.066 s while unbalancedNow holds; the per-tile one-shot from the position jump ([ASSUMED], UNBALANCE_SPEED) is gone. A 受力等级 0 push (0.8 s) → 12 hits = 4800 (was 2037), a 急停 pull with no movement (1 s) → 15 = 6000 (was 0), a wall-shortened slide keeps the 0.8 s row → 4800. 3. 失衡免疫 「失衡期间无法自主移动、发动攻击、使用技能」: enemies/helpers.js unbalancedNow; canCast(e, sil, battle) refuses while it runs (content skills; the boss parts' calls pass the battle), blinkForward returns null, 枯朽之种's dive / blast and the 铳 / 碎铳之簧 charges wait, 乌顶巨角卢鲁's clash waits for its end. A route APPEAR relocation clears it (a forced state switch, 失衡位移机制). Left running on purpose (docs/SIM.md): 水遁忍者's damage check (异常效果 「仍然能正常造成伤害」), passive auras and terrain / periodic effects, death / damage-taken triggers, skill cooldowns, 弧光锋卫's bleed itself. 4. husk() (逐火 余烬 重生) clears the state like reborn(). 5. The 35/30 row held 36 frames (t0 + 35/30 overshoots in floats): every check compares with 1e-9; the frames are asserted. Tests: test/sim/unbalance.test.js +6 (exact frames 6 / 12 / 24 / 27 / 32 / 35 with the walk on the next frame; static pull 1 s / 0.5 s, push 0.1 s; 弧光锋卫 4800 / 6000 / 4800; canCast / blinkForward refused, 枯朽之种 held then diving; the elk's clash after the state; APPEAR and a 逐火 重生 clear it — all six fail on the parent); enemies_bosses (失衡 kit test) and feedback1d-solvent (BUFF damage) updated to the state bleed. test/sim + test/content + test/render pass. Docs: SIM.md (push / pull row), PLAYING.md, design/engine.md (helpers, enemy AI), constants / displacement comments. Golden against the previous commit — outcome moves: roster-010 (歌蕾蒂娅 pulls 妖怪 drones, now held 1 s: killed 44→45, leaks 58→57), roster-040 (static pulls of 妖怪: killed 28→25, leaks 37→40, deaths 29→30), roster-043 / 044 (山: deaths 14→16 / 14→13), hidden-boss_9-pair (锏: boss HP 3636991→3586929), coop4-ABYSS-7 (ai_2 LP +1 from round 5), diy-115 (黑键: leaks 70→75), diy-116 (黑键: killed 2→3, leaks 121→120) — the static pulls (probe) and the 1e-9 end (a frame shorter where the float sum overshot: the 35/30 row, some pull windows); other moves (damage / snapshots only): roster-008 / 017 / 039, 9 bonds, boss-boss_4-pair / boss_6-pair / hidden-boss_10-pair, coop2-NORMAL-14-standins / coop2-NORMAL-3 / solo-ABYSS-1 / solo-HARD-1 / solo-HARD-9-boosted / solo-NORMAL-1, diy-053 / 085 / 102.
This commit is contained in:
1 parent
cdf2bd5c88
commit
1db84d799d
20 files changed
+440
-301
No files matched your search
@@ -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';
|
||||
|
||||
@@ -478,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);
|
||||
@@ -602,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