From 5d3ca339e4e506cc341602dac07e41f85c33fcba Mon Sep 17 00:00:00 2001 From: sganggs Date: Wed, 7 Oct 2026 02:12:34 +0800 Subject: [PATCH] =?UTF-8?q?feedback5=20CB2:=20=E7=90=B3=E7=90=85=E8=AF=97?= =?UTF-8?q?=E6=80=80=E9=9B=85's=20S1=20=E4=BB=97=E4=B9=89=E7=96=8F?= =?UTF-8?q?=E8=B4=A2=20heals=20herself=20too=20=E2=80=94=20the=203=C3=973?= =?UTF-8?q?=20around=20her=20includes=20her=20own=20tile=20(community=20re?= =?UTF-8?q?port=20of=202026-10-06=20=E3=80=8C=E7=90=B3=E7=90=85=E8=AF=97?= =?UTF-8?q?=E6=80=80=E9=9B=85=E4=B8=8D=E4=BC=9A=E6=B2=BB=E7=96=97=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E3=80=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Official: S1 「下一次攻击会为周围八格内血量不足70%的一名友方单位恢复…」; the client charpack (char_1033_swire2, mode S1, ability HealAlly) selects with range x-4 (the 3×3 with her tile), ally side, _excludeOwner 0, max HP ratio 0.7, one target. Cause: kits/ops/chess_char_3_04-swire2.js healTarget skipped her (a === unit) and kept only Chebyshev distance 1. Now: the lowest HP ratio below 70 % of the 3×3, herself included (her own 禁疗 still stops it); the owner's 2026-10-04 cadence (a heal without an attack, once per attack cycle) unchanged. Golden: roster-027 (琳琅诗怀雅 elite S1 in band 4: she heals herself below 70 %). Tests: test/content/feedback5-swire2-self-heal.test.js (2, fail on the parent). --- .../kits/ops/chess_char_3_04-swire2.js | 16 ++++--- .../feedback5-swire2-self-heal.test.js | 48 +++++++++++++++++++ test/golden/roster.json | 18 +++---- 3 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 test/content/feedback5-swire2-self-heal.test.js diff --git a/server/sim/content/kits/ops/chess_char_3_04-swire2.js b/server/sim/content/kits/ops/chess_char_3_04-swire2.js index 784f79f4..92aa848e 100644 --- a/server/sim/content/kits/ops/chess_char_3_04-swire2.js +++ b/server/sim/content/kits/ops/chess_char_3_04-swire2.js @@ -40,8 +40,8 @@ export default { // ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): with a coin and a free tile of range x-6 her attack's turn // throws a champagne bomb instead (onto an enemy's tile when one stands there; installS2); // 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles) - // 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 + // S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 3×3 around her — herself + // included — 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: @@ -96,10 +96,13 @@ export default { * 仗义疏财 — 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 + * she attacks or not. Same target (the lowest HP ratio below 70 % of the 3×3 around her, 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). + * 「周围八格」 includes herself: the client charpack (char_1033_swire2, mode S1, ability HealAlly) selects with range + * x-4 — the 3×3 with her own tile — ally side, `_excludeOwner` 0, max HP ratio 0.7, one target. Until 0.2.0 she was + * left out and never healed herself (community report of 2026-10-06 「琳琅诗怀雅不会治疗自己」). */ const installS1 = (battle, unit) => { const hs = num(bb['attack@heal_scale'], num(bb.heal_scale, 0)); @@ -107,9 +110,10 @@ export default { 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; - if (a.s.flags.noHeal || a.profile?.noHeal) continue; // 禁疗 / 孤立: never a heal target - if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) !== 1) continue; // 周围八格 + if (!alive(a) || a.hidden || a.kind === 'device' || a.hpRatio >= healRatio) continue; + if (a !== unit && (a.s.flags.noHeal || a.profile?.noHeal)) continue; // 禁疗 / 孤立: never a heal target + if (a === unit && a.s.flags.healFree) continue; // her own 禁疗 (HEAL_FREE) stops a heal of herself too + if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) > 1) continue; // x-4: the 3×3, her tile too if (!best || a.hpRatio < best.hpRatio || (a.hpRatio === best.hpRatio && a.deploySeq < best.deploySeq)) best = a; } return best; diff --git a/test/content/feedback5-swire2-self-heal.test.js b/test/content/feedback5-swire2-self-heal.test.js new file mode 100644 index 00000000..c235a70c --- /dev/null +++ b/test/content/feedback5-swire2-self-heal.test.js @@ -0,0 +1,48 @@ +// test/content/feedback5-swire2-self-heal.test.js — 琳琅诗怀雅 S1 仗义疏财 heals herself too (community report of 2026-10-06 +// 「干员琳琅诗怀雅不会治疗自己」): 「下一次攻击会为周围八格内血量不足70%的一名友方单位恢复…」 — the client charpack +// (char_1033_swire2, mode S1, ability HealAlly) selects with range x-4 (the 3×3 with her own tile), ally side, +// `_excludeOwner` 0, max HP ratio 0.7, one target. kits/ops/chess_char_3_04-swire2.js used to skip her own tile. +// Run: node --test test/content/feedback5-swire2-self-heal.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; +import { getDefaultSource } from '../../server/sim/simdata.js'; + +const ds = getDefaultSource(); +const ID = 'chess_char_3_04_a', S1 = 'skchr_swire2_1'; +const S1_INDEX = ds.rawChess(ID).skills.find((s) => s.skillId === S1).index; +const NEIGHBOUR = 'chess_char_3_16_a'; + +/** 琳琅诗怀雅 (S1) on (9, 5), an ally on (10, 5); HP ratios set after the deployment; coins to spare. */ +function field(herRatio, allyRatio) { + const h = makeBattle({ + defs: { enemies: { enemy_d: enemyRec({ key: 'enemy_d', hp: 1e7, speed: 0 }) } }, + units: [{ chessId: ID, row: 9, col: 5, skillIndex: S1_INDEX }, { chessId: NEIGHBOUR, row: 10, col: 5 }], + timeLimit: 60, autoFinish: false, hooks: ['heal'], captureNoisy: true, flags: { dpInit: 60, dpMax: 99 }, + }); + h.step(); + const u = h.unit(ID), ally = h.unit(NEIGHBOUR); + u.mem.coins = 2; + u.hp = u.s.maxHp * herRatio; + ally.hp = ally.s.maxHp * allyRatio; + h.run(1); + return { h, u, ally, heals: h.hooksOf('heal').filter((c) => c.source === u) }; +} + +test('alone below 70 %: a coin heals herself (her own tile is in the x-4 selector)', () => { + const { h, u, heals } = field(0.5, 1); + assert.equal(heals.length, 1, 'one heal'); + assert.equal(heals[0].target, u, 'on herself'); + assert.equal(u.mem.coins, 1, 'one coin spent'); + checkInvariants(h.b); + assert.equal(h.b.errors.length, 0); +}); + +test('the lowest HP ratio of the 3×3 wins: herself below the neighbour, the neighbour below her; 70 % or more is never healed', () => { + assert.equal(field(0.4, 0.6).heals[0].target.defId, ID, 'she is lower: herself'); + assert.equal(field(0.6, 0.4).heals[0].target.defId, NEIGHBOUR, 'the neighbour is lower: the neighbour'); + const none = field(0.75, 1); + assert.equal(none.heals.length, 0, 'not below 70 %: no heal, the coin is kept'); + assert.equal(none.u.mem.coins, 2); +}); diff --git a/test/golden/roster.json b/test/golden/roster.json index 635d4f4c..eaf85dc4 100644 --- a/test/golden/roster.json +++ b/test/golden/roster.json @@ -1692,25 +1692,25 @@ "time": 170, "ticks": 5100, "reason": "timeout", - "killed": 30, + "killed": 31, "total": 53, "unspawned": 0, "bossHpLeft": null, "rngDraws": 140, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":30,"total":53,"perfect":false,"leaks":23,"leaksUncounted":0,"coins":4,"dmg":375198,"bossDmg":0,"heal":14642,"deaths":9,"layerGains":{"raidShip":18}} + "p1": {"killed":31,"total":53,"perfect":false,"leaks":22,"leaksUncounted":0,"coins":4,"dmg":381570,"bossDmg":0,"heal":20570,"deaths":8,"layerGains":{"raidShip":18}} }, "units": [ ["p1",1,"op","chess_char_3_06_b",0,"none",0,0,2,0,952,6276,0,10,1], ["p1",2,"op","chess_char_3_12_b",0,"uniequip_003_blemsh",0,0,14,5,31951,14072,11110,46,21], ["p1",3,"op","chess_char_3_02_b",0,"none",0,0,6,0,15577,2291,0,37,12], ["p1",4,"op","chess_char_3_03_b",0,"none",0,0,8,0,3718,1778,0,35,1], - ["p1",5,"op","chess_char_3_04_b",0,"uniequip_003_swire2",0,0,59,4,48389,6778,3531,66,0], + ["p1",5,"op","chess_char_3_04_b",0,"uniequip_003_swire2",1913.64,1,59,5,55982,7885,9460,73,0], ["p1",6,"op","chess_char_3_05_b",0,"uniequip_003_skadi",0,0,12,2,26200,12824,0,29,1], ["p1",7,"op","chess_char_3_21_a",0,null,506.83,1,10,0,8795,857,0,35,14], ["p1",8,"op","chess_char_3_01_b",0,"uniequip_003_angel",470.41,1,11,1,38052,1246,0,75,32], - ["p1",9,"op","chess_char_3_08_b",0,"none",233.39,1,1,7,80992,3015,0,23,4], + ["p1",9,"op","chess_char_3_08_b",0,"none",233.39,1,1,7,79772,3015,0,23,4], ["p1",10,"op","chess_char_3_09_b",0,"none",0,0,3,0,12044,1534,0,13,4], ["p1",11,"op","chess_char_3_10_b",0,"none",0,0,5,4,54938,2120,0,24,7], ["p1",12,"op","chess_char_3_11_b",0,"none",0,0,7,6,53591,1610,0,19,10] @@ -1726,12 +1726,12 @@ "enemy_1283_sgkill_2": [1,0,1,8304,8764], "enemy_1425_lrcmra": [1,0,1,1808,10651], "enemy_2008_flking": [1,0,1,7980,4818], - "enemy_9006_actoxi": [12,5,5,4229,136218] + "enemy_9006_actoxi": [12,6,5,5082,142471] }, - "hooks": [1,67,5100,528,528,1936,1936,28,39,60,107,108,0,7439,40,31,53,19,1,41,3,0,0], - "events": {"atk":733,"bounty":2,"deploy":14,"die":41,"dmg":1871,"engage":12,"fx":110,"heal":28,"layer":3,"leak":19,"skill":217,"spawn":65,"status":185}, - "fx": {"blink":1,"buff":1,"coin":18,"explode":25,"garrison":3,"revive":2,"shield":20,"shieldBreak":1,"strike":10,"volley":7,"zone":22}, - "snaps": "96282387 24613d95 49d36ca6 4e91020f f1d25080 9e767196 bdb45ea5 0f508e6f c1c13843 91b6d342 0d44ebac 29e98739 a430c9be c8c5ec6c 2fd74d54 48089465 456f6920" + "hooks": [1,67,5100,536,536,1944,1944,36,39,60,107,107,0,7439,40,31,53,19,1,39,3,0,0], + "events": {"atk":740,"bounty":2,"deploy":14,"die":41,"dmg":1879,"engage":12,"fx":125,"heal":36,"layer":3,"leak":19,"skill":217,"spawn":65,"status":185}, + "fx": {"blink":1,"buff":1,"coin":34,"explode":25,"garrison":3,"shield":20,"shieldBreak":1,"strike":10,"volley":7,"zone":23}, + "snaps": "96282387 24613d95 49d36ca6 4e91020f f1d25080 9e767196 bdb45ea5 0f508e6f c1c13843 91b6d342 0d44ebac 29e98739 5de387ca e01250d2 66bd1e5b 4aa99d1f 8cb0a6a5" }, "roster-028": { "about": "pass 1 band 4: chess_char_3_16_b chess_char_4_01_a chess_char_3_18_b chess_char_4_04_a chess_char_3_13_b chess_char_3_14_b chess_char_3_17_b chess_char_3_19_b chess_char_3_20_b chess_char_3_21_b chess_char_4_02_a chess_char_4_06_a token_10028_vigil_wolf",