feedback5 WD: generic kit — an ON_DEPLOY passive without a duration key lasts the skill's own duration from every deployment (一击即退, 10 s), 冲锋号令 gives its DP at full SP, genericTalents for the stand-ins without a kit

Every pool skill these rules meet already has a hand-authored spec (checked over all 266 chess records x skills and the
22 tokens), so only the 补位 stand-ins change; golden fast and full unchanged.
This commit is contained in:
sganggs committed 2026-10-05 17:44:33 +08:00
1 parent 8ab6d021da
commit a325fb41ad
1 file changed
+78 -4
+78 -4
View File
@@ -28,8 +28,13 @@
// "附带…凋亡/灼燃/神经损伤" + ep_damage_ratio ⇒ element damage on hit (× damage dealt when the text says "伤害N%的…损伤",
// else × ATK); "屏障" + shield_max_hp_ratio / hp_ratio ⇒ self shield at start decaying over its duration (砾, 新约能天使);
// "立即流失N%当前生命" + hp_ratio ⇒ self HP loss at start (宴, 风丸); hp_ratio + "恢复/回复…生命" ⇒ self heal at start.
// Passive skills only apply stat mods (for bb.duration s when the text says "N秒内": 宴) and the self/counter effects
// above — their scales describe procs (bombs, sword rain, counters) that need a hand-authored kit.
// Passive skills only apply stat mods (for bb.duration s when the text says "N秒内": 宴; for the skill's own duration
// when an ON_DEPLOY passive says "部署后…" without one in its blackboard: 一击即退, duration 10 — PRTS 技能 持续 10) and
// the self/counter effects above — their scales describe procs (bombs, sword rain, counters) that need a kit.
// "立即获得N点部署费用" + cost ⇒ +cost DP for the player at the start (冲锋号令); an AUTO skill that does nothing else
// fires as soon as its SP is full (as 德克萨斯's kit casts the same skill: kits/ops/chess_char_1_08-texas.js).
// genericTalents(def): the talents a generic kit can apply exactly — an unconditional stat line ("攻击力+8%",
// "攻击速度+9") — used for 补位 stand-ins without a kit of their own (content/index.js); a pool chess always has a kit.
// force→onHit displacement with the official 力度 − 重量 rules (Battle.push / pullToFront): a pull "至面前" when the text says
// 拖拽 or for hookmasters, else a push — along the unit's direction when the text says 往攻击方向 (the 推击手 wording, PRTS
// 推与拉 方向力) / 朝部署方向 / 向前 / 身前方向 or for 推击手 (directional), otherwise away from the unit (radial); a skill of
@@ -277,14 +282,24 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
ammo,
};
// passive stat buffs limited in time ("部署后…在14秒内攻击力+65%", 宴) become a timed buff at each deployment
const passiveTimed = passive && Object.keys(mods).length && num(bb.duration) > 0 && /\d+(\.\d+)?秒内/.test(desc) ? num(bb.duration) : 0;
// passive stat buffs limited in time ("部署后…在14秒内攻击力+65%", 宴) become a timed buff at each deployment; so do the
// ON_DEPLOY passives whose duration is the skill's own (一击即退 "部署后攻击力+X%,防御力+Y%", duration 10, no bb key)
const ownDuration = num(bb.duration) === undefined && sk.duration > 0 && /^部署后/.test(desc) ? sk.duration : 0;
const passiveTimed = passive && Object.keys(mods).length
? (num(bb.duration) > 0 && /\d+(\.\d+)?秒内/.test(desc) ? num(bb.duration) : ownDuration)
: 0;
if (Object.keys(mods).length && !passiveTimed) spec.mods = mods;
if (Object.keys(targeting).length) spec.targeting = targeting;
// instant/charges skills act on the next attack: mods/targeting without an explicit attack still need one
if (!Object.keys(attack).length && (kind === 'instant' || kind === 'charges') && (spec.mods || spec.targeting)) spec.attack = {};
if (Object.keys(attack).length) spec.attack = attack;
// ---- deployment cost: "立即获得N点部署费用" (冲锋号令 `cost`) — the player gains it at the start
const dpGain = !passive && /获得\d+点部署费用/.test(desc) ? g('cost') : undefined;
const gainsDp = dpGain !== undefined && dpGain > 0;
// an AUTO skill with nothing else to do fires as soon as its SP is full (德克萨斯's kit casts 冲锋号令·γ型 so)
if (gainsDp && sk.skillType === 'AUTO' && !spec.mods && !spec.targeting && !spec.attack) spec.trigger = 'SP_FULL';
// ---- start / end effects
const starts = [];
const ends = [];
@@ -292,6 +307,12 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
const m = { ...mods };
starts.push(({ battle, unit }) => battle.addBuff(unit, { key: `generic:passive:${sk.id ?? 'skill'}`, duration: passiveTimed, mods: m, tags: ['skill'] }));
}
if (gainsDp) {
starts.push(({ battle, unit }) => {
battle.addDp(unit.ownerId, dpGain);
battle.fx('dp', { x: unit.x, y: unit.y, n: dpGain, id: unit.id });
});
}
const hr = g('hp_ratio');
if (hr !== undefined && hr > 0 && hr <= 1 && /立即流失\d+(\.\d+)?%(的)?当前生命/.test(desc)) {
starts.push(({ battle, unit }) => { const loss = unit.hp * hr; if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit }); });
@@ -377,6 +398,59 @@ function installGeneric(spec) {
};
}
/** Stat talents genericTalents applies: the stat named in the text → [blackboard key, mod for "+N%", mod for "+N"]. */
const TALENT_STATS = Object.freeze({
攻击力: ['atk', 'atkPct', 'atkFlat'], 防御力: ['def', 'defPct', 'defFlat'], 生命上限: ['max_hp', 'hpPct', 'hpFlat'],
攻击速度: ['attack_speed', null, 'aspd'],
});
const STAT_CLAUSE = /^(攻击力|防御力|生命上限|攻击速度)\+(\d+(?:\.\d+)?)(%?)$/;
/**
* The mods of an unconditional stat talent — its text is only stat clauses ("攻击力+8%", "攻击速度+9", joined by "," /
* "、") and each clause's number is its blackboard key's (×100 for a percentage); every other blackboard key is 0 —
* else null.
*/
export function statTalentMods(t) {
const text = String(t?.description ?? t?.desc ?? '').trim();
const bb = t?.bb ?? {};
if (!text) return null;
const mods = {};
const used = new Set();
for (const part of text.split(/[,、,]/)) {
const m = STAT_CLAUSE.exec(part.trim());
if (!m) return null;
const [key, pctMod, flatMod] = TALENT_STATS[m[1]];
const v = num(bb[key]);
const pct = m[3] === '%';
const mod = pct ? pctMod : flatMod;
if (v === undefined || !mod || used.has(key) || Math.abs((pct ? v * 100 : v) - Number(m[2])) > 1e-6) return null;
mods[mod] = v;
used.add(key);
}
if (Object.keys(bb).some((k) => !used.has(k) && num(bb[k]) !== 0)) return null;
return Object.keys(mods).length ? mods : null;
}
/**
* Talents the generic kit can apply exactly — the 补位 stand-ins without a kit of their own (content/index.js
* setupUnitKit; 预备干员 "攻击提升 攻击力+8%", "防御提升 防御力+10%", "施法速度提升 攻击速度+9"): every unconditional stat
* talent (statTalentMods) as a persistent buff, as the operator kits write theirs (`talent:` key, persist, allowDead).
* Any other talent — a condition, a proc, a key its text does not state — needs a hand-authored kit and is left out,
* never half applied.
* @param {object} def normalised operator def
* @returns {{ install(battle: object, unit: object): void }[]}
*/
export function genericTalents(def) {
const out = [];
(def?.talents ?? []).forEach((t, i) => {
const mods = statTalentMods(t);
if (!mods) return;
const key = `talent:generic:${i}`;
out.push({ install(battle, unit) { battle.addBuff(unit, { key, mods: { ...mods }, persist: true, allowDead: true, tags: ['talent'] }); } });
});
return out;
}
/**
* Generic kit: `(bb, chess, def?) => Kit`. `chess` is the data record; `def` the normalised def when available.
*/