Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_3_12-blemsh.js
T
sganggs ac6454d171 feedback5: merge feedback4 (7ee815e) — W4D ported: a sleeping enemy cannot be blocked and takes no block slot (_checkBlock in server/sim/battle/blocking.js, the applyStatus release in battle/status.js, the plain-buff release re-applied to ai.js updateEnemy's cannot-act branch after WE1's swing cut, 瑕光's comment in kits/ops/chess_char_3_12-blemsh.js, buffs.js; 酒神's 迷狂牢笼 skips a sleeper too); golden: roster-003 / 005 / 013 / 032 / 038 / 039 / 042, bond-maniShip-high, solo-FUNNY-1, coop4-FUNNY-6, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-70-diy, diy-131 / 132 moved
New on feedback4 since the last merge: W4D (bbe59b9, merged as 7ee815e) and the 0.1.4 version commit (30e7ace).
feedback5 keeps its own package.json / package-lock.json / APP_VERSION / README / DEPLOY version lines (0.2.0 sets
its own version later) and takes feedback4's CHANGELOG 0.1.4 entry, with W4D's line in it.

- Port (PRTS 异常效果 SLEEPING 沉睡 = 无法行动+无敌+不可阻挡; GitHub #140; the owner's decision of 2026-10-06):
  `_checkBlock` refuses `flags.sleep` (battle/blocking.js) and `applyStatus` releases an enemy that falls asleep
  (battle/status.js). Battle.js stays feedback5's façade, and tier3.js stays deleted (the 瑕光 comment is in her kits/ops
  file). ai.js: WE1 (89f730d) cuts a swing before enemyAttack on a stun / freeze / sleep. The cannot-act branch is still
  there, and a plain-buff `flags.sleep` still makes the enemy `stun` (units.js), so the plain-buff release stays in that
  branch, after the cut, with its comment rewritten for the new order.
- New on feedback5: 酒神's 迷狂牢笼 (kits/ops/op-phatm2.js cageBlock, O22) duplicated the old three-flag 不可阻挡 check.
  It now skips a sleeper too. No golden scenario spawns a 牢笼 over a sleeper: the corpus is identical with that line
  reverted. 予愿安洁莉娜's S3 helper is unchanged, since canTargetEnemy already drops sleepers.
- Readers that follow without code: blockedTargets (blocked-first targeting, the DEFAULT trigger, hitAllBlocked), the 要塞
  branch of acquireTargets (u.blocking), “庞贝”'s clock (blockedBy), the snapshot's UF.BLOCKED and the client's block
  marker (render/units.js). 死芒 S2 折朽 (自选) lets go a linked enemy that her 悲叹的仆役 held.
- Docs: DESIGN §24.9, its §24 intro entry, the §5.5 Blocking sentence (on feedback5's 阻挡半径倍率 text) and §22.8's
  "(since §24.9)", all as on feedback4; SIM.md as on feedback4 (the plain-buff sentence merged with feedback5's
  noNewBlock / undying text).
- Golden: a probe (counting successful sleeps on a blocked enemy, plain-buff sleeps and 牢笼 spawns over sleepers) ran
  over all 283 scenarios. Every scenario that moved sleeps blocked enemies; the counts are in brackets:
  roster-003 (小满 S2 [5]: same fight, blocked 27 → 29, snapshot hashes); roster-005 (elite 小满 S2 [3]: the 140 s
  timeout with 83 kills and 1 leak, where it used to clear at 135.43 s with 84); roster-013 (缇缇 S3 [11]: same kills,
  blocked 22 → 33); roster-032 (缇缇 S1 [10]: 71 kills instead of 68, 8 leaks instead of 11, 4 knock-outs instead of 8,
  萨尔贡 layers 41 → 35); roster-038 (elite 缇缇 S2 [1]: ends at 197.40 s instead of 197.27 s); roster-039 (瑕光 S2
  normal + elite [31 + 12]: same kills and counted leaks, the 窃贼 gets away, blocked 53 → 87); roster-042 (缇缇 S2 [3]:
  ends at 168.77 s instead of 166.93 s); bond-maniShip-high (小满 [1]: blocked 29 → 30, snapshot hashes); solo-FUNNY-1
  (ai_0's 缇缇 [3]: only ai_0's damage dealt); coop4-FUNNY-6 (ai_0's 缇缇 [15 + 56]: from round 9 ai_0's 精准 / 萨尔贡
  layers and boss damage move, ai_1 loses no LP from round 11, team LP 124 → 130); coop4-ABYSS-7 (ai_2's 小满 [18]: ai_2
  one kill fewer, ai_0 one more LP lost in round 9); coop2-NORMAL-8-serverrun (ai_1's 缇缇 [83]: only ai_1's damage dealt;
  WE1 had already turned this match into a loss); coop2-NORMAL-70-diy (the 自选 seat h_0's 缇缇 [171], ai_0's 缇缇 [74]
  and 小满 [19 + 21]: h_0's layers move from round 4, then its AI 托管 buys differently (boards from round 7); ai_0 246
  kills instead of 233, same end); diy-131 (死芒 S2 at tiers 5 and 6 [1 + 1]: snapshot hashes only); diy-132 (elite
  死芒 S2 [2]: ends at 199.50 s instead of 198.13 s, same kills). One scenario sleeps a blocked enemy and did not move:
  coop2-HARD-4 [1]. ai_1's 小满 lets go the enemy it holds; another enemy takes the slot; the sleeper wakes with no room
  and is blocked further on by 2_16_a. The match digest has no snapshot hashes, and its kills, leaks, damage and funds
  are identical. fields and standins did not move. Most outcomes differ from feedback4's list because feedback5's
  baselines differ (WE1's damage frame, etc.).
- Tests: test/sim/feedback4-sleep-unblock.test.js (from feedback4) passes 5/5 on the split layout. Every case fails with
  the three sim hunks reverted, and case 3 (plain buff) fails with only the ai.js release reverted. Targeted sim tests
  pass 184/184 (blocking, block targets, enemy swing, feedback2-enemy-attack-stand, combat / statuses, robustness,
  stun SP, professions, healers, fidelity …). Content tests plus kits_layout and docs-consistency pass 658/658 (kits
  t1t2 / t3 / t5 and alt t2 / t3 / t5 for 小满 / 瑕光 / 缇缇, 酒神, 死芒, garrisons, bosses, items). Stealth / liftoff /
  enemies pass 36/36. npm run golden: all 283 match. GOLDEN_FULL=1 test/golden.test.js passes 9/9 (no stored engine
  error). lint 0 errors, check:imports and typecheck clean.
2026-10-06 17:49:10 +08:00

150 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/chess_char_3_12-blemsh.js — 瑕光 (char_423_blemsh) kit, tier 3.
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
import { bodyOnTile } from '../../../body.js';
import {
num, defOf, talentBb, traitBb, altSkills, instantKindOf, alive, onTiles, gridKeys, fx, copyGrid, NINE, isLeader,
giveSp,
} from '../shared/tier3.js';
/** Refresh period (s) of 瑕光 S2's 生命回复速度 buff (it lasts two periods, so it never lapses while the skill runs). */
const REGEN_IV = 0.25;
export default {
// ---- 3_12 瑕光 · 守护者 — S3 先贤化身: ATK/DEF +, bonus arts per hit, heals another nearby ally per attack;
// 剑盾骑士: hurt-SP skills of the team also gain SP on attack; 仁慈: attacks sleeping enemies (first, ×atk_scale);
// 精锐 module GUA-Y: damage taken −15 %
// S1 光芒涌动 (自动触发 ⇒ DEFAULT, charges): next attack ×atk_scale phys + heals the most injured ally of the 3×3 (herself
// included) for heal_scale × ATK; S2 慑敌辉光: ATK +, puts every ground enemy on her own tile and every enemy she
// blocks to sleep (PRTS 备注; for the skill's duration: no own value in the data) and gives every ally of the
// skill range 生命回复速度 +ATK × ratio while it runs (PRTS 技能2 备注 "生命恢复的提供方式为基于自己的攻击力,增加目标的
// “生命回复速度”属性,不受治疗加成和禁疗影响": an hpRegen buff, no heal — GitHub #137);
// 精锐 module GUA-X: her heals on allies under hp_ratio HP × heal_scale
chess_char_3_12_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
const tb = traitBb(d);
const bonus = num(bb['attack@blemsh_s_3_extra_dmg[magic].atk_scale'], 0);
const heal = num(bb.heal_scale, 0);
const healGrid = copyGrid(d.skill?.rangeGrid) ?? NINE;
const mercy = num(t1.atk_scale, 1);
const sleepersInRange = (battle, unit) => {
const set = unit.rangeKeySet;
return set ? battle.enemies.filter((e) => e.alive && !e.hidden && e.s.flags.sleep && !e.s.flags.untargetable && !e.isFlying && onTiles(e, set)) : [];
};
const kit = {
trait: { hitSleep: true }, // 仁慈: 自身可以攻击…沉睡的目标 (沉睡 = 无敌 for everyone else)
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
heal: false,
attack: { onHit({ battle, unit, target }) {
if (bonus > 0 && target && target.alive) battle.dealDamage(unit, target, { amount: unit.s.atk * bonus, type: 'arts', isSkill: true, tags: ['skill', 'blemshBonus'] });
} },
onAttack({ battle, unit }) {
if (!(heal > 0)) return;
const ally = battle.injuredAlliesInKeys(new Set(gridKeys(healGrid, unit)), unit).find((a) => a !== unit);
if (ally) battle.heal(unit, ally, unit.s.atk * heal);
},
},
skills: altSkills(chess, d, bb, {
skchr_blemsh_1: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const hs = num(s.bb.heal_scale, 0);
return {
kind: instantKindOf(s),
heal: false,
attack: {
atkScale: num(s.bb.atk_scale, 1),
onHit({ battle, unit }) { // "恢复周围一名友方单位"
const ally = hs > 0 ? battle.injuredAlliesInKeys(new Set(gridKeys(grid, unit)), unit)[0] : null;
if (ally) battle.heal(unit, ally, unit.s.atk * hs);
},
},
};
},
skchr_blemsh_2: (s) => {
const grid = copyGrid(s.rangeGrid) ?? NINE;
const ratio = num(s.bb['attack@atk_to_hp_recovery_ratio'], num(s.bb.atk_to_hp_recovery_ratio, 0));
// "周围的所有友方单位每秒恢复相当于攻击力N%的生命值": an hpRegen buff on every ally on the skill range's tiles (no
// device, no 孤立 unit), refreshed every REGEN_IV while the skill runs and removed at its end [ASSUMED: from the
// start of the skill — unlike 铃兰's, the note names no delay]
const regenKey = (unit) => `blemsh:regen:${unit.id}`;
const regen = (battle, unit) => {
if (!(ratio > 0)) return;
const keys = new Set(gridKeys(grid, unit));
const v = unit.s.atk * ratio;
for (const a of battle.allyUnits) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || !onTiles(a, keys) || !battle.allySelectable(a, unit)) continue;
battle.addBuff(a, { key: regenKey(unit), duration: REGEN_IV * 2, source: unit, mods: { hpRegen: v } });
}
};
return {
kind: 'duration',
heal: false,
mods: { atkPct: num(s.bb.atk) },
onStart({ battle, unit, skill }) {
const dur = skill.timeLeft > 0 ? skill.timeLeft : Math.max(0.1, num(s.duration, 10));
let n = 0;
// PRTS 备注: "技能生效对象实际为“自身这格内的所有地面敌人及自身阻挡的敌人”,即使阻挡的是飞行敌人" — a blocked
// enemy stands at the block radius, outside her tile (Battle._checkBlock). Asleep they are no longer blocked
// (沉睡 = 不可阻挡, DESIGN §24.9; GitHub #140): her slots free for the next enemies, and a sleeper that wakes
// is held again only while she has room
for (const e of battle.enemies) {
if (!e.alive || e.hidden) continue;
if (e.blockedBy !== unit && (e.isFlying || !bodyOnTile(e, unit.tileR, unit.tileC))) continue;
if (battle.applyStatus(e, 'sleep', { duration: dur, source: unit })) n++;
}
fx(battle, 'aoe', unit, { radius: 0.5, skill: 'blemsh_2', status: 'sleep', n });
unit.mem.blemshRegen = 0;
regen(battle, unit);
},
onTick({ battle, unit, dt }) {
unit.mem.blemshRegen = (unit.mem.blemshRegen ?? 0) + dt;
if (unit.mem.blemshRegen < REGEN_IV - 1e-9) return;
unit.mem.blemshRegen = 0;
regen(battle, unit);
},
onEnd({ battle, unit }) {
for (const a of battle.allyUnits) if (a.findBuff(regenKey(unit))) battle.removeBuff(a, regenKey(unit));
},
};
},
}),
talents: [
{ install(battle, unit) {
unit.mem.blemshKnight = true;
battle.on('attack', (ctx) => {
const a = ctx.attacker;
if (!a || a.kind !== 'op' || a.ownerId !== unit.ownerId || a.skill?.spType !== 'hurt') return;
if (!alive(unit) || !isLeader(battle, unit, 'blemshKnight')) return;
giveSp(a, num(t0.sp, 1), 'talent');
}, { owner: unit });
} },
{ install(battle, unit) {
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit) return;
const s = sleepersInRange(battle, unit);
if (s.length) ctx.targets = [s[0], ...ctx.targets.filter((t) => t !== s[0])].slice(0, Math.max(1, ctx.targets.length));
}, { owner: unit });
battle.on('hit', (ctx) => {
if (ctx.source === unit && ctx.target.side === 'enemy' && ctx.target.s.flags.sleep) ctx.dmg.mul *= mercy;
}, { owner: unit });
// (a lone sleeper is found by the engine's own target search: `trait.hitSleep` makes sleepers targetable
// and damageable for her only)
} },
],
};
if (num(tb.damage_resistance) > 0) {
kit.install = (battle, unit) => battle.addBuff(unit, { key: 'trait:blemsh_guard', mods: { dmgTakenMul: 1 - num(tb.damage_resistance) }, persist: true, allowDead: true });
} else if (num(tb.heal_scale) > 0) {
// GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%" (the HP before the heal; strictly below — the client's blemsh_e_trait
// is set_heal_scale_by_hpratio, FilterByTargetHpRatio LT, unlike 黍's heal_scale_up[hpratio][LE])
kit.install = (battle, unit) => battle.on('heal', (ctx) => {
if (ctx.source === unit && ctx.target && ctx.target.hpRatio < num(tb.hp_ratio, 0.5)) ctx.amount *= num(tb.heal_scale, 1);
}, { owner: unit });
}
return kit;
},
};