From eac9b9737d76c500029e9b18cc41b30bad9fde4c Mon Sep 17 00:00:00 2001 From: sganggs Date: Mon, 5 Oct 2026 18:20:48 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20K2:=20applyStatus=20stackAs=20?= =?UTF-8?q?=E2=80=94=20a=20=E5=90=8C=E5=90=8D=E6=95=88=E6=9E=9C=E5=8F=96?= =?UTF-8?q?=E6=9C=80=E9=AB=98=20status=20can=20compete=20with=20another=20?= =?UTF-8?q?strength=20than=20its=20effect=20(Raidian=20S3's=20=E8=99=9A?= =?UTF-8?q?=E5=BC=B1,=20PRTS=20=E5=A4=87=E6=B3=A8=20"=E5=9C=A8=E5=8F=A0?= =?UTF-8?q?=E5=8A=A0=E6=97=B6=E8=A7=86=E4=B8=BA90%=E2=80=A6=E7=9A=84?= =?UTF-8?q?=E8=99=9A=E5=BC=B1=EF=BC=8C=E4=BB=85=E5=BD=B1=E5=93=8D=E5=8F=A0?= =?UTF-8?q?=E5=8A=A0=E4=BC=98=E5=85=88=E7=BA=A7");=20no=20caller=20passes?= =?UTF-8?q?=20it=20yet,=20golden=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/SIM.md | 8 +++++--- server/sim/battle/status.js | 39 ++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/SIM.md b/docs/SIM.md index 78de8df2..a932542b 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -540,7 +540,7 @@ HEAL_FREE "受到的治疗量变为0"; DESIGN §22.7), `stealthOff` = an enemy (`stealthOff:`, below), `camou` = 迷彩 (below)). `taunt: true` as a flag counts as +1 taunt level (DESIGN §5.3). -**Statuses** — `battle.applyStatus(target, key, { duration, source, value, force, refresh, point })` (returns true if +**Statuses** — `battle.applyStatus(target, key, { duration, source, value, force, refresh, point, stackAs })` (returns true if applied); a unit that is 无敌 and 无法选中 at once (flags `invulnerable` + `untargetable`: a 重生 in progress, a hovering or 永久无敌 leader part) takes no status from the other side, `force` included (PRTS 无敌 "无法被不同阵营选中" — so the status a knocking-out hit carries, 妮芙 S2's fear say, does not land after the 重生's cleanse; since 0.1.1's QA); refused when @@ -551,7 +551,9 @@ and the 浮空 weight rule, then `statusApplied { source, target, status, durati = the target carried no buff of that status before (a refresh / a weaker "取最高" application is not an entry: "进入…时"). Effects follow the official term table (`gamedata_const.termDescriptionDict`, `ba.*`). Same-key statuses refresh to the longer duration, except the "同名效果取最高" ones marked *strongest* below: the strongest value wins, a weaker application never overrides it and, -if it outlasts it, resumes when the strong one expires (pass `refresh` to opt out). +if it outlasts it, resumes when the strong one expires (pass `refresh` to opt out); `stackAs` = the strength such an +application competes with instead of its `value`, which stays its effect (Raidian S3's 虚弱: PRTS 备注 "在叠加时视为90%…的 +虚弱(仅影响叠加优先级,不影响实际效果)"). `battle.applyStrongest(target, key, { duration, value, mods: (v) => mods, source })` gives a content effect that is not a catalogue status the same rule (one invisible buff `key` per target whatever applies it — no immunity, 抵抗, status hooks or icon): the engine default for two same-named buffs (PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"). 奥术 uses @@ -755,7 +757,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged | helper | notes | |---|---| | `dealDamage(src, tgt, dmg)`, `heal(src, tgt, amount, opts)`, `loseHp(tgt, amount, {source, from, tags, sourceless})` | §4 | -| `applyStatus(tgt, key, {duration, source, value, force, point})`, `removeStatus(tgt, key)`, `resistOf(unit)` | §3 | +| `applyStatus(tgt, key, {duration, source, value, force, point, stackAs})`, `removeStatus(tgt, key)`, `resistOf(unit)` | §3 | | `applyStrongest(tgt, key, {duration, value, mods, source})` | §3 — "同名效果取最高" for a non-catalogue effect | | `addBuff(unit, buff)`, `removeBuff(unit, key)` | §3 | | `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise | diff --git a/server/sim/battle/status.js b/server/sim/battle/status.js index a4983963..ef6a2e1c 100644 --- a/server/sim/battle/status.js +++ b/server/sim/battle/status.js @@ -130,15 +130,17 @@ export class BattleStatus { } /** - * Apply a catalogue status. opts: { duration, source, value, force, refresh, point, resistApplied } — returns true - * when applied. Honours enemy immunities (stun/silence/sleep/frozen/levitate/feared) unless `force`. `beforeStatus` + * Apply a catalogue status. opts: { duration, source, value, force, refresh, point, resistApplied, stackAs } — returns + * true when applied. Honours enemy immunities (stun/silence/sleep/frozen/levitate/feared) unless `force`. `beforeStatus` * handlers may cancel it or change `duration` / `value`. Official rules (buffs.js STATUS): 抵抗 (the `resist` status) * shortens the RESIST_STATUSES by its value (default half; applied after `beforeStatus`; `resistApplied` skips that * pass — the cold-on-cold 冻结 below already used post-抵抗 lengths). A second 寒冷 while 寒冷 remains applies 冻结 for * max(remaining, this cold after 抵抗) (PRTS 术语释义 寒冷 「持续时间取双方之中最高」). 浮空 lasts half as long on units heavier * than LEVITATE_HALF_WEIGHT (current massLevel); 冻结's RES cut hits enemies only; 麻痹 adds stacks; "同名效果取最高" * statuses (`valued`) keep the strongest value — a weaker application only extends past the stronger one's end (it - * then resumes); other statuses refresh to the longer duration. 诱导 (`attract`) walks the enemy to `point` + * then resumes); `stackAs` = the value such an application competes with instead of its own (its effect stays + * `value`): an effect the game stacks as another strength — Raidian S3's 虚弱, PRTS 备注 "在叠加时视为90%(1级~6级)/80% + * (7级~专精二)…的虚弱(仅影响叠加优先级,不影响实际效果)"; other statuses refresh to the longer duration. 诱导 (`attract`) walks the enemy to `point` * ([r, c] or {x, y}; default the source's tile — a new application moves the point); 恐惧 (`fear`) stamps where it * was applied and from where (fear.js stampFear: the fan of 恐惧可达地块 its movement uses). A stunned/sleeping operator * releases the enemies it blocks; a feared/levitated/unblockable/attracted enemy is released by its blocker. @@ -199,7 +201,7 @@ export class BattleStatus { if (tpl.palsy) { this.addBuff(target, { ...palsyBuff(value ?? 1), duration, source }); } else if (tpl.valued != null && typeof tpl.mods === 'function' && opts.refresh == null) { - this._applyValuedStatus(target, key, tpl, duration, value ?? tpl.valued, source); + this._applyValuedStatus(target, key, tpl, duration, value ?? tpl.valued, source, opts.stackAs); } else { const mods = tpl.enemyOnlyMods && target.side !== 'enemy' ? null : typeof tpl.mods === 'function' ? tpl.mods(value) : (tpl.mods || null); const b = this.addBuff(target, { key, duration, refresh: opts.refresh ?? 'extend', mods, flags: tpl.flags || null, status: key, visible: true, source }); @@ -257,33 +259,38 @@ export class BattleStatus { /** * "同名效果取最高": keep the strongest value; a weaker one that outlasts it resumes afterwards (buff.data.tail). A - * `plain` template (applyStrongest) is an ordinary invisible buff, not a status. + * `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) { - const strength = (v) => Math.abs(Number.isFinite(v) ? v : tpl.valued); - const make = (v, dur, 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 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: { value: v, tail }, + 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)); + if (t && t.until - battle.time > 1e-6 && unit.alive) battle.addBuff(unit, make(t.value, t.until - battle.time, null, t.stackAs)); }, }); const old = target.buffs.find((b) => b.key === key && (tpl.plain || b.status === key)); - if (!old) { this.addBuff(target, make(value, duration, null)); return; } + if (!old) { this.addBuff(target, make(value, duration, null, as)); return; } const oldV = old.data && Number.isFinite(old.data.value) ? old.data.value : tpl.valued; + 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) > strength(oldV) + 1e-12) { + 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 ? { value: oldV, until: oldEnd } : null, oldTail && oldTail.until > newEnd ? oldTail : null); - this.addBuff(target, make(value, duration, tail)); - } else if (strength(value) < strength(oldV) - 1e-12) { + const tail = longerTail(oldEnd > newEnd ? entry(oldV, oldAs, { until: oldEnd }) : null, oldTail && oldTail.until > newEnd ? oldTail : null); + 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: { value, until: newEnd } }; + if (newEnd > oldEnd && (!oldTail || newEnd > oldTail.until)) old.data = { ...old.data, value: oldV, tail: entry(value, as, { until: newEnd }) }; } else if (duration > old.timeLeft) { old.timeLeft = duration; old.duration = Math.max(old.duration, duration);