From a325fb41adf1d54fc06718390b6dc81751343327 Mon Sep 17 00:00:00 2001 From: sganggs Date: Mon, 5 Oct 2026 17:44:33 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20WD:=20generic=20kit=20=E2=80=94=20a?= =?UTF-8?q?n=20ON=5FDEPLOY=20passive=20without=20a=20duration=20key=20last?= =?UTF-8?q?s=20the=20skill's=20own=20duration=20from=20every=20deployment?= =?UTF-8?q?=20(=E4=B8=80=E5=87=BB=E5=8D=B3=E9=80=80,=2010=20s),=20?= =?UTF-8?q?=E5=86=B2=E9=94=8B=E5=8F=B7=E4=BB=A4=20gives=20its=20DP=20at=20?= =?UTF-8?q?full=20SP,=20genericTalents=20for=20the=20stand-ins=20without?= =?UTF-8?q?=20a=20kit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- server/sim/content/generic.js | 82 +++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/server/sim/content/generic.js b/server/sim/content/generic.js index 50c750a2..223cf77c 100644 --- a/server/sim/content/generic.js +++ b/server/sim/content/generic.js @@ -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. */