From 7d3d7318d570ef18b7038d6eddb9e95bc91cedeb Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 18:18:18 +0800 Subject: [PATCH] =?UTF-8?q?feedback3=20WB:=20=E7=90=B3=E7=90=85=E8=AF=97?= =?UTF-8?q?=E6=80=80=E9=9B=85=20S1=20heals=20an=20injured=20ally=20beside?= =?UTF-8?q?=20her=20without=20needing=20an=20attack=20or=20an=20enemy,=20a?= =?UTF-8?q?t=20most=20once=20per=20attack=20cycle=20(owner's=20decision=20?= =?UTF-8?q?2026-10-04,=20community=20report=20#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/sim/content/kits/tier3.js | 30 +++++++++++++++++----- test/content/kits_alt_t3.test.js | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/server/sim/content/kits/tier3.js b/server/sim/content/kits/tier3.js index e786827f..25a6c6ea 100644 --- a/server/sim/content/kits/tier3.js +++ b/server/sim/content/kits/tier3.js @@ -399,8 +399,9 @@ const KITS = { // ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range; // 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles) - // S1 仗义疏财 (passive, 2 coins): an attack spends a coin to heal the most injured ally (< 70 % HP) of the 8 - // surrounding tiles for attack@heal_scale × ATK. S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin; + // S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 8 surrounding tiles for + // attack@heal_scale × ATK — on her attack, or with no enemy to attack on her own attack timer (owner's decision + // 2026-10-04, against the official 「下一次攻击会为…」; installS1). S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin; // closing it spends every coin on random ground enemies of range 2-4 in front and those she blocks (atk_scale phys + // a small push, radial despite the text's 向前 — PRTS 备注 "推开效果为径向推动"; client charpack char_1033_swire2: // the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位, @@ -448,10 +449,19 @@ const KITS = { }, }); const healRatio = textNum(d.skill?.description, /血量不足(\d+)%/, 70) / 100; - const installS1 = (battle, unit) => { // 仗义疏财 + /** + * 仗义疏财 — official text 「消耗一枚金币,下一次攻击会为周围八格内血量不足70%的一名友方单位恢复相当于攻击力40%的生命」: the heal rides on an + * attack, so with no enemy around she never healed (community report #5 「琳琅诗怀雅1技能不会主动奶身边受伤的干员」). Owner's + * decision 2026-10-04 (a deliberate deviation, like §21.29's 重装 casts): she heals an injured ally beside her whether + * she attacks or not. Same target (the lowest HP ratio below 70 % of the 8 surrounding tiles, no 禁疗 / 孤立 unit, no + * device), coin and heal_scale × ATK. Cadence [ASSUMED]: at most one heal per attack cycle (her attack interval, ASPD + * included) — while she attacks, on the attack as before; when her last attack attempt found no target, on her own + * timer (the 'tick' hook runs after the attacks, so an attack due in the same tick takes it). + */ + const installS1 = (battle, unit) => { const hs = num(bb['attack@heal_scale'], num(bb.heal_scale, 0)); - battle.on('attack', (ctx) => { - if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0)) return; + let nextAt = -Infinity; + const healTarget = () => { let best = null; for (const a of battle.allyUnits) { if (a === unit || !alive(a) || a.hidden || a.kind === 'device' || a.hpRatio >= healRatio) continue; @@ -459,11 +469,19 @@ const KITS = { if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) !== 1) continue; // 周围八格 if (!best || a.hpRatio < best.hpRatio || (a.hpRatio === best.hpRatio && a.deploySeq < best.deploySeq)) best = a; } + return best; + }; + const tryHeal = () => { + if (!alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0) || battle.time < nextAt - 1e-9) return; + const best = healTarget(); if (!best) return; unit.mem.coins -= coinCost; + nextAt = battle.time + unit.s.interval; battle.heal(unit, best, unit.s.atk * hs); fx(battle, 'coin', unit, { n: unit.mem.coins, heal: best.id, skill: 'swire2_1' }); - }, { owner: unit, priority: -10 }); + }; + battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 }); + battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit }); }; const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币" battle.on('kill', (ctx) => { diff --git a/test/content/kits_alt_t3.test.js b/test/content/kits_alt_t3.test.js index 2fea0b59..6d4fb48f 100644 --- a/test/content/kits_alt_t3.test.js +++ b/test/content/kits_alt_t3.test.js @@ -226,6 +226,50 @@ test('3_04 琳琅诗怀雅 S1 仗义疏财: 2 coins; an attack spends one to hea } }); +// Owner's decision 2026-10-04 (community report #5 「琳琅诗怀雅1技能不会主动奶身边受伤的干员」), a deliberate deviation from the official +// 「下一次攻击会为…」: S1 heals an injured ally beside her without an attack or an enemy — at most once per attack cycle +// (her attack interval, ASPD included) [ASSUMED cadence]; with an enemy the heal stays on her attack. +test('3_04 琳琅诗怀雅 S1 仗义疏财 (owner\'s decision): an injured ally beside her is healed with no enemy on the field, once per attack cycle; with an enemy, once per attack', () => { + const id = 'chess_char_3_04_a'; + const b = SB(id, 'skchr_swire2_1'); + const mk = (enemies = []) => makeBattle({ defs: { enemies: { enemy_d: dummy('enemy_d') } }, timeLimit: 60, autoFinish: false, hooks: ['heal', 'attack'], captureNoisy: true, + units: [U(id, 'skchr_swire2_1', 9, 5), { chessId: 'chess_char_3_16_a', row: 10, col: 5 }], enemies }); + // no enemy: an ally at 80 % is left alone, one at 50 % is healed and a coin is spent + let h = mk(); + let u = h.unit(id), ally = h.unit('chess_char_3_16_a'); + h.step(); + u.mem.coins = 2; + ally.hp = ally.s.maxHp * 0.8; + h.run(3); + assert.equal(h.hooksOf('heal').filter((c) => c.source === u).length, 0, '80 %: not below 70 %'); + assert.equal(u.mem.coins, 2, 'the coin is kept'); + ally.hp = ally.s.maxHp * 0.5; + h.step(); + const heals = h.hooksOf('heal').filter((c) => c.source === u); + assert.equal(heals.length, 1, 'healed at once, no enemy needed'); + assert.equal(heals[0].target, ally); + approx(heals[0].amount, u.s.atk * b['attack@heal_scale']); + assert.equal(u.mem.coins, 1, 'one coin spent'); + assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u).length, 0, 'without an attack'); + // kept injured with coins to spare: one heal per attack interval, never more + const n0 = h.hooksOf('heal').length, t0 = h.b.time; + for (let i = 0; i < 300; i++) { ally.hp = ally.s.maxHp * 0.3; u.mem.coins = 2; h.step(); } + const per = h.hooksOf('heal').slice(n0).filter((c) => c.source === u).length; + assert.equal(per, Math.floor((h.b.time - t0) / u.s.interval + 1e-6), `one heal per ${u.s.interval} s`); + done(h); + // with an enemy in her range: the heal rides on each attack, never a second one in the cycle + h = mk([{ key: 'enemy_d', pos: [9, 6] }]); + u = h.unit(id); ally = h.unit('chess_char_3_16_a'); + h.step(); + const a0 = h.hooksOf('attack').length, h0 = h.hooksOf('heal').length; // (its first attack, at deployment, found the ally unhurt) + for (let i = 0; i < 300; i++) { ally.hp = ally.s.maxHp * 0.3; u.mem.coins = 2; h.step(); } + const atk = h.hooksOf('attack').slice(a0).filter((c) => c.attacker === u).length; + const hl = h.hooksOf('heal').slice(h0).filter((c) => c.source === u).length; + assert.ok(atk >= 5, `attacks (${atk})`); + assert.equal(hl, atk, 'one heal per attack'); + done(h); +}); + test('3_04 琳琅诗怀雅 S3 千金一掷: 二连击, kills pay a coin, a full purse is cashed out on the front range (hits + push)', () => { for (const id of BOTH('chess_char_3_04_a')) { const b = SB(id, 'skchr_swire2_3'), t0 = TB(id, 0);