feedback7: merge fb7-gh-sim — PR #348 (take-the-highest chains), PR #293 (REA-Y in the engine), #329 ports (异客 13 tiles, 歌蕾蒂娅 net radius 1, 搅拌机 latest first, 大帝 uncapped), PR #365 (骨刺 forms + PRTS targeting), #298 (instant non-attack skills recast at most once per attack interval, attacks untouched), #364 (先兆者 shell 3×3), #220 (速射手 air first), 余 skips noStack bonds (owner)
- Conflict: docs/PLAYING.md 技能自动释放 bullet — both insertions kept (the 铁钳号 rule from fb7-owner-sim, the #298
pacing sentence).
- Grok review: MERGE AFTER FIXES — the HIGH #298 finding (the cast replaced the due attack) fixed on the branch
(db29acf9: per-skill nextCastAt; golden of #298 alone shrank from 84 scenarios to 2).
- Golden union: 4 matches differ from HEAD though the branch alone left them (coop2-NORMAL-3, coop4-ABYSS-7,
coop2-NORMAL-8-serverrun, solo-HARD-9-boosted): HEAD's side had moved all four (#333's shop RNG …) and on those new
bot paths the merged rules apply — the diffs are in-battle layer gains / damage (卡西米尔 50 → 45, 精准 80 → 76,
叙拉古 176 → 168, kills ±2, solo-HARD-9 LP 397 → 399), no outcome change.
- Broad node --test 5571 (5570 pass, 0 fail, 1 skipped); lint 0 errors; typecheck.
This commit is contained in:
commit
52f56319ac
41 files changed
+2065
-648
No files matched your search
@@ -10,6 +10,7 @@ import { SkillRuntime } from '../skills.js';
|
||||
import { resolveProfile } from '../professions.js';
|
||||
import { normalizeToken } from '../simdata.js';
|
||||
import { setupUnitKit } from '../content/index.js';
|
||||
import { installTraitAttackSpeed } from '../content/traitMods.js';
|
||||
import { clone } from './util.js';
|
||||
|
||||
export class BattlePlayers {
|
||||
@@ -220,6 +221,13 @@ export class BattlePlayers {
|
||||
if (t && typeof t.install === 'function') this._safe(() => t.install(this, u), 'talent.install', u);
|
||||
}
|
||||
if (typeof u.kit.install === 'function') this._safe(() => u.kit.install(this, u), 'kit.install', u);
|
||||
// trait lines the ENGINE owns for every operator (content/traitMods.js: the module attack-speed riders whose
|
||||
// condition is a pure function of the field — 「攻击范围内存在N名及以上敌人时攻击速度+X」). Runs after kit.install:
|
||||
// a kit that implements its own line for this trait is never double-counted — traitMods.js refuses every condition
|
||||
// shape a kit already owns (`reason: 'kit'`), and the kits whose line it DOES take over (the two REA-Y ones) had
|
||||
// their hand-written copy deleted in the same change. Calling it before kit.install would let a kit's own buff and
|
||||
// this rule both land on the same unit.
|
||||
this._safe(() => installTraitAttackSpeed(this, u), 'traitMods.attackSpeed', u);
|
||||
return u.kit;
|
||||
}
|
||||
}
|
||||
@@ -274,23 +274,39 @@ export class BattleStatus {
|
||||
}
|
||||
|
||||
/**
|
||||
* "同名效果取最高": keep the strongest value; a weaker one that outlasts it resumes afterwards (buff.data.tail). A
|
||||
* "同名效果取最高": keep the strongest value, then resume weaker effects in order of strength at their original expiry times.
|
||||
* `buff.data.tail` links the waiting effects, with decreasing strength and increasing expiry times. A
|
||||
* `plain` template (applyStrongest) is an ordinary invisible buff, not a status. `stackAs` (finite): the strength this
|
||||
* application competes with instead of its value (applyStatus opts.stackAs); kept in `data.stackAs` / the tail.
|
||||
*/
|
||||
_applyValuedStatus(target, key, tpl, duration, value, source, stackAs = null) {
|
||||
const as = Number.isFinite(stackAs) ? stackAs : null;
|
||||
const strength = (v, s = null) => Math.abs(Number.isFinite(s) ? s : Number.isFinite(v) ? v : tpl.valued);
|
||||
// (no stackAs: the very objects of before — data { value, tail }, tails { value, until })
|
||||
const entry = (v, s, extra) => (Number.isFinite(s) ? { value: v, stackAs: s, ...extra } : { value: v, ...extra });
|
||||
const after = (tail, until) => {
|
||||
while (tail && tail.until <= until) tail = tail.tail;
|
||||
return tail || null;
|
||||
};
|
||||
const insertTail = (tail, incoming) => {
|
||||
if (!tail) return incoming;
|
||||
const diff = strength(incoming.value, incoming.stackAs) - strength(tail.value, tail.stackAs);
|
||||
if (diff > 1e-12) return { ...incoming, tail: after(tail, incoming.until) };
|
||||
if (diff < -1e-12) {
|
||||
if (incoming.until <= tail.until) return tail;
|
||||
return { ...tail, tail: insertTail(tail.tail, incoming) };
|
||||
}
|
||||
// Equal-priority waiting effects keep the longest-lived application's value, including its stackAs.
|
||||
const kept = incoming.until > tail.until ? incoming : tail;
|
||||
return { ...kept, tail: after(tail.tail, kept.until) };
|
||||
};
|
||||
const make = (v, dur, tail, s = null) => ({
|
||||
...(tpl.buff || null), // extra buff fields of the status (抵抗: the 麻痹 decay tick)
|
||||
key, duration: dur, refresh: 'replace', mods: tpl.mods(v), flags: tpl.flags || null, status: tpl.plain ? null : key,
|
||||
visible: !tpl.plain, source,
|
||||
data: entry(v, s, { tail }),
|
||||
onExpire: ({ battle, unit, buff }) => {
|
||||
const t = buff.data.tail;
|
||||
if (t && t.until - battle.time > 1e-6 && unit.alive) battle.addBuff(unit, make(t.value, t.until - battle.time, null, t.stackAs));
|
||||
const t = after(buff.data.tail, battle.time + 1e-6);
|
||||
if (t && unit.alive) battle.addBuff(unit, make(t.value, t.until - battle.time, t.tail || null, t.stackAs));
|
||||
},
|
||||
});
|
||||
const old = target.buffs.find((b) => b.key === key && (tpl.plain || b.status === key));
|
||||
@@ -299,17 +315,17 @@ export class BattleStatus {
|
||||
const oldAs = old.data && Number.isFinite(old.data.stackAs) ? old.data.stackAs : null;
|
||||
const oldEnd = this.time + old.timeLeft, newEnd = this.time + duration;
|
||||
const oldTail = old.data && old.data.tail;
|
||||
const longerTail = (a, b) => (!a ? b : !b ? a : (b.until > a.until ? b : a));
|
||||
if (strength(value, as) > strength(oldV, oldAs) + 1e-12) {
|
||||
// stronger: takes over now; the weaker old one (or its tail) resumes if it lasts longer
|
||||
const tail = longerTail(oldEnd > newEnd ? entry(oldV, oldAs, { until: oldEnd }) : null, oldTail && oldTail.until > newEnd ? oldTail : null);
|
||||
// Stronger: take over now, retaining every old effect that can still become active afterwards.
|
||||
const tail = after(entry(oldV, oldAs, { until: oldEnd, tail: oldTail }), newEnd);
|
||||
this.addBuff(target, make(value, duration, tail, as));
|
||||
} else if (strength(value, as) < strength(oldV, oldAs) - 1e-12) {
|
||||
// weaker: never overrides; remembered as the tail when it outlasts the running one
|
||||
if (newEnd > oldEnd && (!oldTail || newEnd > oldTail.until)) old.data = { ...old.data, value: oldV, tail: entry(value, as, { until: newEnd }) };
|
||||
// Weaker: insert among the waiting effects instead of replacing the one with the latest expiry.
|
||||
if (newEnd > oldEnd) old.data = { ...old.data, value: oldV, tail: insertTail(oldTail, entry(value, as, { until: newEnd })) };
|
||||
} else if (duration > old.timeLeft) {
|
||||
old.timeLeft = duration;
|
||||
old.duration = Math.max(old.duration, duration);
|
||||
old.data.tail = after(oldTail, newEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user