feedback5 CB2: 琳琅诗怀雅's S1 仗义疏财 heals herself too — the 3×3 around her includes her own tile (community report of 2026-10-06 「琳琅诗怀雅不会治疗自己」)

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).
This commit is contained in:
sganggs committed 2026-10-07 02:12:34 +08:00
1 parent 017390c93f
commit 5d3ca339e4
3 files changed
+67 -15

No files matched your search

@@ -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;
@@ -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);
});
+9 -9
View File
@@ -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",