feedback5 O13: 假日威龙陈's slime follows the client data (her attack range, per source, DEF as a final addition) and the water TAG is one flag; 莱伊's S2 bullet recovery takes HUN-X's extra on an empty magazine

- op-chen2.js: the S2 / S3 slime (projectile_chr_chen2_s2 / _s3: fired by each attack, sitting on her in her direction,
  5 s, every 0.2 s chen2_slime 0.25 s on the ground enemies of its range, ignoring 无法选择) now covers the tiles of her
  attack range at each attack instead of the struck enemies' tiles; one effect per 假日威龙陈 (independentCharacterSource);
  the DEF cut compensated for the target's DEF multipliers (FINAL_ADDITION). RPR-Y stage 3's water TAG is the level's one
  flag (chen2_e_003_t[map_tag]: set at her skill's start, cleared at its end or her exit). S2's attacks stay skill attacks.
- op-ray.js: the 沙地兽's recovered bullets go through ray_tr_add (≤ its counter of 8, HUN-X's extra_add first when she is
  empty, ≤ her magazine); kit headers cite the client templates (also op-ascln.js).
- Tests: op_chen2.test.js slime test rewritten (her range, stealthed walkers, the final-addition DEF, two sources).
- Golden: only diy moved — diy-049 / diy-050 (假日威龙陈's slime); every other scenario and family unchanged.
This commit is contained in:
sganggs committed 2026-10-06 01:38:31 +08:00
1 parent f44e524afd
commit 1d62a062d5
5 files changed
+201 -152

No files matched your search

+5 -1
View File
@@ -7,7 +7,11 @@
// account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS
// 阿斯卡纶 (死亡拘审 备注 and its {{**}} 最终乘算 (100−18×层数)%, S2 修正 / 备注, S3 备注, the AMB-X note), PRTS 分支特性信息
// 伏击客 ("嘲讽等级为-1"), PRTS 命中率, Arknights Terra Wiki "Ascalon" (死亡拘审: "The MSPD reduction and Arts damage stack
// additively").
// additively"); the client's battle data read from the local install — buff_template_data ascln_t_1 / ascln_t_1[debuff]
// (ON_OUTPUT_DAMAGE of NORMAL attacks; STACK, the duration refreshed; per trigger buff count × atk_ratio × ATK, MAGICAL,
// attack type BUFF), ascln_t_2 (CheckHeightTypeInRange x-5 ≥ cnt), ascln_s_2[enemy] (ON_OWNER_KILLED of a WALK enemy:
// the debuff in a circle of range_radius on the WALK_ONLY enemies but the dead one), ascln_s_3[self] (heal on
// ON_EVADE_DAMAGE and on a HIT_FAILED damage), ascln_e_003_t_1[heal] (a heal when the enemy holding it finishes).
// - Trait (伏击客) "对攻击范围内所有敌人造成伤害;拥有50%的物理和法术闪避且不容易成为敌人的攻击目标": the stalker profile — every
// enemy of her range at once, melee, ground only (no "可对空"), physical — and 50 % physical / arts dodge (trait bb prob;
// AMB-Y: 65 %). "不容易成为敌人的攻击目标" is her taunt level −1, which her data stats already carry (PRTS 伏击客 "嘲讽等级为
+98 -82
View File
@@ -4,58 +4,67 @@
//
// Forms (data/backups.json units.char_1013_chen2): normal = E2 Lv1, skills at rank 4, no module; elite = E2 Lv60, rank 7,
// the picked module at stage 1 (tier 5) or 3 (tier 6) — the owner's decision of 2026-10-05. Potential 0 [ASSUMED: no
// account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json) and PRTS
// 假日威龙陈 (节约风气 修正 and 备注, 假日余韵 备注, S2 / S3 备注, the RPR-X stage-1 note); PRTS 作战机制 §地形TAG; PRTS 分支特性
// 信息 散射手 ("可对空", the trait's 1-3 front grid); gamedata_const ba.charged (蓄力).
// account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 假日威龙陈
// (节约风气 修正 and 备注, 假日余韵 备注, S2 / S3 备注, the RPR-X stage-1 note); PRTS 作战机制 §地形TAG; PRTS 分支特性信息 散射手
// ("可对空", the trait's 1-3 front grid); gamedata_const ba.charged (蓄力); the client's battle data read from the local
// install — charpack char_1013_chen2 (the S2 / S3 modes: an attack plus a RangeAttack firing projectile_chr_chen2_s2 / _s3),
// battle/prefabs [uc]projectiles projectile_chr_chen2_s2 / _s3, buff_template_data chen2_s_debuff, chen2_s_2_enhance_judge,
// chen2_tr, chen2_t_002[spareShot], chen2_e_003_t[map_tag].
// - Trait (散射手) "攻击范围内的所有敌人,对自己前方一横排的敌人攻击力提升至150%": the reaperrange profile — every enemy of
// her range, air units too, ×atk_scale (trait bb) on the trait's front grid (her tile and the three ahead). RPR-X
// overrides it with atk_scale 1.6 (the same trait, 160 %); RPR-Y adds "部署费用减少" (cost −8, in the stats: her
// redeploy after a knock-out costs 8 DP less). Ranged physical arrows; blocks 1; ground enemies target her (no flag).
// her range, air units too, ×atk_scale (trait bb; chen2_tr AtkScaleUp) on the trait's front grid (her tile and the three
// ahead). RPR-X overrides it with atk_scale 1.6; RPR-Y adds "部署费用减少" (cost −8, in the stats: her redeploy after a
// knock-out costs 8 DP less). Ranged physical arrows; blocks 1; ground enemies target her (no flag).
// - T1 节约风气 "在场时自身弹药类技能攻击时20%概率不消耗对应弹药,其他【狙击】干员的弹药类技能改为10%概率不消耗(同类效果取最
// 高)": while she is on the field, an attack made by a running 弹药类技能 (PRTS 备注: a skill whose text says "攻击装有X发
// 弹药 / 子弹" — never the 猎手 bullets) spends no ammo with that chance: hers spareshot_chen.prob, another 【狙击】
// operator's 20 % — PRTS 修正 (原因 "描述与游戏实际表现不符合", and the RPR-X stage-1 note "实际应为…其他【狙击】干员…20%"):
// the game gives the other snipers what she has, spareshot_chen.prob, not the text's / `prob`'s 10 % [the mechanism is
// ASSUMED; the 20 % is PRTS's]. RPR-X stage 3 (talent change): herself e_spareshot_chen.prob 25 %, other 【狙击】
// operators …[sniper].prob 20 %, other 远程 (RANGED) operators …[other].prob 12 %. 同类效果取最高: the highest chance of
// every 假日威龙陈 on the field. One roll per attack ("一次性消耗多发弹药时仅进行一次判定"); a spared attack spends nothing
// of what it would ("强制改为实耗0发"), and every `ammoUsed` of it still fires ("不影响以'消耗弹药'为效果触发条件的侦测":
// the bullet is put back in that event, before the skill checks for its last bullet) — 拉特兰 / garrison counters see it.
// ASSUMED; the 20 % is PRTS's]. RPR-X stage 3 (talent change; chen2_t_002[spareShot]: 【狙击】 profession ⇒ [sniper],
// else deploy class RANGED ⇒ [other]): herself e_spareshot_chen.prob 25 %, other 【狙击】 operators 20 %, other 远程
// operators 12 %. 同类效果取最高: the highest chance of every 假日威龙陈 on the field. One roll per attack ("一次性消耗多发弹药
// 时仅进行一次判定"); a spared attack spends nothing of what it would ("强制改为实耗0发"), and every `ammoUsed` of it still
// fires ("不影响以'消耗弹药'为效果触发条件的侦测": the bullet is put back in that event, before the skill checks for its last
// bullet) — 拉特兰 / garrison counters see it.
// - T2 假日余韵 "攻击速度+8,当场地中存在水地形时改为攻击速度+12" ([common].attack_speed 8, [map].attack_speed +4). 水地形 is
// a map TAG (PRTS 作战机制 §地形TAG): no level of this mode carries one (the act1 / act2 autochess level files' mapData.tags
// are null), so +8. RPR-Y stage 3 "攻击力+15%,攻击速度+12,当场地中存在水地形时改为攻击力+28%,攻击速度+20,技能开启时,场地
// 视为水地形": +15 % / +12, and while her skill runs the field holds the water TAG (PRTS 备注 "在技能期间为当前关卡临时添加
// '水地形TAG';干员撤退时或技能结束时清除") — +28 % / +20 for her and for every 假日威龙陈 of that field. The module row's
// [map] keys are the text's totals (20 / 0.28), the base row's [map] 4 the step over [common] 8 (8 + 4 = 12): read so.
// 视为水地形": +15 % / +12, and while her skill runs the field holds the water TAG (chen2_e_003_t[map_tag]: SetRuntimeMapTag
// water at ON_SKILL_START, cleared at ON_SKILL_FINISH / ON_OWNER_FINISH — one flag of the level, no count: the first
// holder whose skill ends clears it) — +28 % / +20 for every 假日威龙陈 of that field. The module row's [map] keys are the
// text's totals (20 / 0.28), the base row's [map] 4 the step over [common] 8 (8 + 4 = 12): read so.
// - S1 高压冲击 (AUTO, attack SP, data DEFAULT — an attack skill): ATK +atk, 4 bullets (attack@trigger_time), every enemy of
// her range takes the trait's front-row multiplier ("对攻击范围内的所有敌人应用特性加成", as 送葬人 S1).
// - S2 “堇青之夜” (MANUAL, data DEFAULT): ATK +atk, 8 bullets (attack@trigger_time); 蓄力 (maxChargeTime 2; ba.charged "技力达
// 到上限可继续回复,回复至上限2倍时进入蓄力状态,此时开启技能会触发额外效果(任何时候开启均消耗全部技力)"): cast with both
// - S2 “堇青之夜” (MANUAL, data DEFAULT): ATK +atk, 8 bullets (attack@trigger_time); 蓄力 (maxChargeTime 2;
// chen2_s_2_enhance_judge: ≥ 2 charges at the cast ⇒ the enhanced mode, then ClearCharacterSp; ba.charged): cast with both
// charges stored ⇒ attack@another_trigger_time (20) bullets; every cast empties the SP. Every attack leaves slime (below).
// - S3 “假日风暴” (MANUAL, data ACTIVE_RANGE on its 2-6): range 2-6 while it runs, ATK +atk, every attack strikes twice and
// takes the trait multiplier on every enemy, leaves slime; 32 bullets, 2 per attack — PRTS 备注 "即使剩余弹药数小于2发仍
// 可'消耗2发弹药'进行攻击;技能仅在弹药数归零后自动结束" (one `ammoUsed` per bullet).
// - Slime (S2 / S3 "每次攻击在范围内生成持续5秒的粘液,地面敌人经过时移动速度-N%、防御力-M(不叠加)"): each struck enemy's tile
// holds slime for attack@projectile_life_time s [ASSUMED: one tile under each enemy an attack strikes — the data calls it
// a projectile with a life time]; a ground enemy standing on slime moves ×(1 + attack@move_speed) and has DEF
// attack@def (flat), refreshed every SLIME_IV s so it lapses just after it leaves [ASSUMED]; 不叠加 — one effect, the
// strongest slime under it; PRTS 备注 "粘液的效果无视无法选择类效果(如隐匿与无敌)": stealthed / invulnerable enemies too.
// The slime outlives her [ASSUMED]. Air units never (地面敌人).
// - S3 “假日风暴” (MANUAL, data ACTIVE_RANGE on its 2-6): range 2-6 while it runs, ATK +atk, every attack strikes twice
// (MultiMeleeAttack _additionalTimes 1) and takes the trait multiplier on every enemy, leaves slime; 32 bullets, 2 per
// attack — PRTS 备注 "即使剩余弹药数小于2发仍可'消耗2发弹药'进行攻击;技能仅在弹药数归零后自动结束" (one `ammoUsed` per bullet).
// - Slime (S2 / S3 "每次攻击在范围内生成持续5秒的粘液,地面敌人经过时移动速度-N%、防御力-M(不叠加)"): each attack's RangeAttack
// fires a projectile that lands at once on her own ground mount point in her direction and lives
// attack@projectile_life_time s; every 0.2 s its range selector (ground enemies, `_ignoreTargetFree`: PRTS 备注 "无视无法选
// 择类效果(如隐匿与无敌)") gives chen2_slime for 0.25 s — move speed FINAL_SCALER ×(1 + attack@move_speed), DEF
// FINAL_ADDITION attack@def. Here: the tiles of her attack range at the attack hold slime for the life time (a later attack
// refreshes them) [ASSUMED: the projectile's selector range is her attack range — the prefab names no grid; its effect is
// "chen2_skill_02_range_01"], pulsed every 0.2 s; 不叠加 — one effect per 假日威龙陈 (independentCharacterSource: two of
// them each slow); the DEF cut is compensated for the target's DEF multipliers (a final addition). It outlives her: the
// projectiles are not hers to clear [ASSUMED]. Air units never (地面敌人). (Each struck ground enemy's own 0.1 s
// chen2_s_debuff from the attack is inside that range anyway.)
import { num, talentBb, skillRec, up } from '../shared/tier1.js';
import { bodyKeys } from '../../../body.js';
import { COLS } from '../../../constants.js';
import { aggregateMods } from '../../../buffs.js';
const S1 = 'skchr_chen2_1';
const S2 = 'skchr_chen2_2';
const S3 = 'skchr_chen2_3';
/** PRTS 节约风气 备注: a 弹药类技能 is one whose text says "攻击装有X发弹药" or "攻击装有X发子弹". */
const AMMO_SKILL = /攻击装有\d+发(?:弹药|子弹)/;
/** Slime refresh period and the length of one refresh (the effect lapses SLIME_DUR s after the enemy leaves it). */
const SLIME_IV = 0.1;
const SLIME_DUR = 0.15;
/** Buff key of the slime effect on an enemy (one for every 假日威龙陈: 不叠加). */
export const SLIME_KEY = 'chen2:slime';
/** projectile_chr_chen2_s2 / _s3 `_interval` (the slime's selection period) and chen2_slime's lifeTime. */
const SLIME_IV = 0.2;
const SLIME_DUR = 0.25;
/** Buff key of a 假日威龙陈's slime effect on an enemy (independentCharacterSource: one per source). */
export const slimeKey = (src) => `chen2:slime#${src.id}`;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const count = (v, d) => Math.max(1, Math.floor(num(v, d)));
@@ -102,15 +111,15 @@ function installSpare(battle, unit, probs) {
st.srcs.set(unit, probs);
}
// ---- 水地形 TAG (RPR-Y stage 3): the units holding it on a battle -------------------------------------------------------
/** battle → Set(units whose running skill makes the field count as water). */
// ---- 水地形 TAG (RPR-Y stage 3): the level's one runtime flag ----------------------------------------------------------
/** battle → { on } (SetRuntimeMapTag water: no count — the last set / clear wins). */
const WATER = new WeakMap();
/** Whether the field of `battle` counts as water terrain now (no level of the mode has the TAG of its own). */
export const waterOn = (battle) => (WATER.get(battle)?.size ?? 0) > 0;
function setWater(battle, unit, on) {
export const waterOn = (battle) => !!WATER.get(battle)?.on;
function setWater(battle, on) {
let s = WATER.get(battle);
if (!s) WATER.set(battle, (s = new Set()));
if (on) s.add(unit); else s.delete(unit);
if (!s) WATER.set(battle, (s = { on: false }));
s.on = on;
}
/**
@@ -125,61 +134,59 @@ export function holidayLines(t) {
return { common: { atkPct: ca, aspd: cs }, water: total ? { atkPct: ma || ca, aspd: ms || cs } : { atkPct: ca + ma, aspd: cs + ms } };
}
// ---- slime (S2 / S3): one battle-wide pulse over every puddle ----------------------------------------------------------
/** battle → Map(tileKey → { until, ms, def, src }). */
// ---- slime (S2 / S3): one battle-wide pulse over every 假日威龙陈's slimed tiles ------------------------------------------
/** battle → Map(source unit → Map(tileKey → { until, ms, def })). */
const SLIME = new WeakMap();
/** The slime puddles of a battle (tile key → puddle), for tests and the pulse. */
export const slimeOf = (battle) => SLIME.get(battle) ?? null;
/** A source's slimed tiles (tile key → { until, ms, def }), for tests. */
export const slimeOf = (battle, src) => SLIME.get(battle)?.get(src) ?? null;
/** The enemy's DEF multiplier from every buff but `key`'s ((1 + Σ defPct) × Π defMul): a FINAL_ADDITION is divided by it. */
function defFactor(e, key) {
const { add, mul } = aggregateMods(e.buffs.filter((b) => b.key !== key));
return Math.max(0, 1 + (add.defPct ?? 0)) * (mul.defMul ?? 1);
}
function slimeState(battle) {
let m = SLIME.get(battle);
if (m) return m;
m = new Map();
SLIME.set(battle, m);
// no owner: puddles outlive the unit that left them [ASSUMED]
// no owner: the slime outlives the unit that left it [ASSUMED]
battle.every(SLIME_IV, () => {
if (!m.size) return;
const now = battle.time;
for (const [k, p] of m) if (p.until <= now + 1e-9) m.delete(k);
if (!m.size) return;
for (const e of battle.enemies) {
// 地面敌人 only; 无视无法选择 — no targetability check (stealth, invulnerable)
if (!e.alive || e.hidden || !e.deployed || e.isFlying) continue;
let best = null;
for (const k of bodyKeys(e)) {
const p = m.get(k);
if (p && (!best || Math.abs(p.def) > Math.abs(best.def) || (Math.abs(p.def) === Math.abs(best.def) && Math.abs(p.ms) > Math.abs(best.ms)))) best = p;
for (const [src, tiles] of m) {
for (const [k, p] of tiles) if (p.until <= now + 1e-9) tiles.delete(k);
if (!tiles.size) continue;
const key = slimeKey(src);
for (const e of battle.enemies) {
// 地面敌人 only; 无视无法选择 — no targetability check (stealth, invulnerable)
if (!e.alive || e.hidden || !e.deployed || e.isFlying) continue;
let p = null;
for (const k of bodyKeys(e)) { p = tiles.get(k) ?? null; if (p) break; }
if (!p) continue;
const f = defFactor(e, key);
battle.addBuff(e, {
key, duration: SLIME_DUR, source: src, tags: ['skill', 'slime'],
mods: { moveMul: Math.max(0, 1 + p.ms), defFlat: f > 1e-6 ? p.def / f : p.def },
});
}
if (!best) continue;
battle.addBuff(e, { key: SLIME_KEY, duration: SLIME_DUR, mods: { moveMul: Math.max(0, 1 + best.ms), defFlat: best.def }, source: best.src, tags: ['skill', 'slime'] });
}
});
return m;
}
/**
* S2 / S3 attack.onHit (once per struck enemy, at the arrow's impact): slime on that enemy's tile for life s; a puddle
* already there is refreshed, keeping the stronger numbers [ASSUMED: one puddle per tile].
*/
function slimeOnHit(b) {
/** S2 / S3 onAttack: the tiles of her attack range hold her slime for life s (refreshed by every attack). */
function leaveSlime(battle, unit, b) {
const life = num(b['attack@projectile_life_time'], 5), ms = num(b['attack@move_speed']), def = num(b['attack@def']);
return ({ battle, unit, x, y }) => {
if (!(life > 0) || (!ms && !def)) return;
const r = Math.round(y), c = Math.round(x);
if (!battle.grid.inBounds(r, c)) return;
const m = slimeState(battle);
const k = r * COLS + c, until = battle.time + life, old = m.get(k);
if (!old) {
m.set(k, { until, ms, def, src: unit });
battle.fx('zone', { x: c, y: r, radius: 0.5, dur: life, id: unit.id, skill: 'chen2:slime' });
return;
}
if (Math.abs(def) > Math.abs(old.def) || (Math.abs(def) === Math.abs(old.def) && Math.abs(ms) > Math.abs(old.ms))) {
old.ms = ms; old.def = def; old.src = unit;
}
old.until = Math.max(old.until, until);
};
if (!(life > 0) || (!ms && !def) || !unit.rangeKeys) return;
const m = slimeState(battle);
let tiles = m.get(unit);
if (!tiles) m.set(unit, (tiles = new Map()));
const fresh = !tiles.size;
const until = battle.time + life;
for (const k of unit.rangeKeys) tiles.set(k, { until, ms, def });
if (fresh) battle.fx('zone', { x: unit.x, y: unit.y, radius: 2, dur: life, id: unit.id, skill: 'chen2:slime' });
}
export default {
@@ -209,7 +216,7 @@ export default {
kind: 'ammo',
ammo: count(b2['attack@trigger_time'], 8),
mods: { atkPct: num(b2.atk) },
attack: { onHit: slimeOnHit(b2) },
attack: {}, // (its attacks are the skill's: isSkill)
onStart({ unit, skill }) {
// 蓄力: cast with every charge stored (activate took one) ⇒ the charged bullets; any cast spends all the SP
const charged = skill.maxCharges > 1 && skill.charges + 1 >= skill.maxCharges;
@@ -218,6 +225,7 @@ export default {
skill.charges = 0;
skill.sp = 0;
},
onAttack({ battle, unit }) { leaveSlime(battle, unit, b2); },
onEnd({ unit }) { unit.mem.chen2Charged = false; },
},
[S3]: {
@@ -225,9 +233,10 @@ export default {
ammo: count(b3['attack@trigger_time'], 32),
mods: { atkPct: num(b3.atk) },
targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid.map((p) => [p[0], p[1]]) } : undefined,
attack: { hits: 2, dmgMul: frontAll, onHit: slimeOnHit(b3) },
// 每次攻击消耗2发: the second bullet (the engine spends the other one); with one left she still attacks
attack: { hits: 2, dmgMul: frontAll },
onAttack({ battle, unit, skill }) {
leaveSlime(battle, unit, b3);
// 每次攻击消耗2发: the second bullet (the engine spends the other one); with one left she still attacks
if (skill.ammoLeft > 1) {
skill.ammoLeft--;
battle.emit('ammoUsed', { unit, left: skill.ammoLeft, skill });
@@ -255,10 +264,17 @@ export default {
apply();
battle.on('tick', apply, { owner: unit });
if (!waterSkill) return;
// RPR-Y stage 3 "技能开启时,场地视为水地形": the TAG while her skill runs, gone when it ends or she leaves
battle.on('skillStart', (ctx) => { if (ctx.unit === unit) { setWater(battle, unit, true); apply(); } }, { owner: unit });
battle.on('skillEnd', (ctx) => { if (ctx.unit === unit) { setWater(battle, unit, false); apply(); } }, { owner: unit });
battle.on('death', (ctx) => { if (ctx.unit === unit) setWater(battle, unit, false); }, { owner: unit });
// RPR-Y stage 3 "技能开启时,场地视为水地形" (chen2_e_003_t[map_tag]): set at her skill's start unless she holds it,
// cleared when her skill ends or she leaves — if she set it
const clear = () => { if (unit.mem.chen2WaterSet) { unit.mem.chen2WaterSet = false; setWater(battle, false); apply(); } };
battle.on('skillStart', (ctx) => {
if (ctx.unit !== unit || unit.mem.chen2WaterSet) return;
unit.mem.chen2WaterSet = true;
setWater(battle, true);
apply();
}, { owner: unit });
battle.on('skillEnd', (ctx) => { if (ctx.unit === unit) clear(); }, { owner: unit });
battle.on('death', (ctx) => { if (ctx.unit === unit) clear(); }, { owner: unit });
} },
],
};
+15 -6
View File
@@ -9,7 +9,10 @@
// built into backups.json) and PRTS 莱伊 (巡哨伙伴 / 入神 / S1 / S2 / S3 备注), PRTS 沙地兽 (备注 "持有禁疗、无敌", "退场时返还1
// 个可部署的沙地兽", "莱伊退场时强制撤退场上的沙地兽"; its skill "仅在持有者携带技能2时才会携带"), PRTS 分支特性信息 猎手, BWIKI
// 莱伊 ("装填间隔即为攻击间隔,初始子弹数即为最大子弹数"), PRTS 卫戍协议/帮助 (a placed summon that leaves is deployed again on
// its tile "在满足条件后").
// its tile "在满足条件后"); the client's battle data read from the local install — buff_template_data ray_tr / ray_tr_add /
// ray_tr_sub (the magazine), ray_s_1[konckback] (Knockback _useSourceDirection, _decreaseForceLevelWhenNotInDirection 1),
// ray_s_1[kill_add_bullet], ray_t_1[damage_scale] (PHYSICAL DamageScale on targets holding ray_sndbst_aura), ray_s_2[deck],
// ray_s_3[reload] / [check_full] / [sp], ray_sndbst_tr / ray_sndbst_collect, and charpack char_4117_ray.
// - Trait (猎手) "攻击时需要消耗子弹且攻击力提升至120%,不攻击时会缓慢地装填子弹(最多8发)" (trait bb value / atk_scale; the
// hunter profile's ×atk_scale on every bullet attack, skill attacks included). PRTS 分支特性信息 猎手: "入场时持有自身子弹上限
// 数的子弹" — full at every deployment; "存在子弹且攻击范围内存在目标时进行攻击,不存在子弹/子弹未满且攻击范围内无目标时进行
@@ -44,9 +47,10 @@
// - S2 广域警觉 (AUTO, attack SP; 持续时间无限 — a toggle): range 4-10, ATK +atk, the 沙地兽's redeploy time ×(1 +
// respawn_time). Self / summon effects only: it fires at full SP (`trigger: 'SP_FULL'`, the owner's AUTO rule).
// Passive "沙地兽撤退时回收命中该区域的子弹": while she carries S2, the bullets of her attacks that hit an enemy inside a
// scouting 沙地兽's area are counted and come back to her magazine when it leaves (≤ the magazine, while she is on the
// field) [ASSUMED: capped at the trait's maximum]; S1's special bullet is no bullet. PRTS "装弹行为不触发攻击回复": no
// reload gives attack SP.
// scouting 沙地兽's area are counted (its own counter: ≤ its trait value 8 — ray_sndbst_tr) and come back to her magazine
// when it leaves, while she is on the field (ray_sndbst_collect → ray_tr_add: HUN-X's extra_add first on an empty
// magazine, capped at her maximum); S1's special bullet is no bullet. PRTS "装弹行为不触发攻击回复": no reload gives
// attack SP.
// - S3 “得见光芒” (MANUAL, 16 s, data ACTIVE_RANGE on its 3-8): "立即停止攻击直至子弹装满,装填间隔大幅缩短(-1.2)": at the cast
// she stops attacking and reloads (a reload under way is cut to the new interval) until her magazine is full, every reload
// while it runs lasting base attack time + reload_interval (0.4 s); range 3-8; every attack attack@atk_scale × ATK
@@ -233,8 +237,13 @@ export default {
if (!mine(t) || battle.finished) return;
t.removed = false; // the piece stays (Battle.redeploy needs it; its hooks are kept)
if (m.rayBeast === t) endArea();
// S2 passive: the bullets that hit in its area come back
if (picked === S2 && m.rayBullets > 0 && up(unit)) unit.trait.ammo = Math.min(ammoMax(unit), (unit.trait.ammo ?? 0) + m.rayBullets);
// S2 passive: the bullets that hit in its area come back (ray_sndbst_collect → ray_tr_add: its count — ≤ its own
// trait value — onto her magazine, HUN-X's extra_add first when she is empty, capped at her maximum)
if (picked === S2 && m.rayBullets > 0 && up(unit)) {
const back = Math.min(m.rayBullets, Math.max(1, Math.floor(num(t.def?.traitBb?.value, ammoMax(unit)))));
const empty = (unit.trait.ammo ?? 0) <= 0 ? extraAdd : 0;
unit.trait.ammo = Math.min(ammoMax(unit), (unit.trait.ammo ?? 0) + empty + back);
}
m.rayBullets = 0;
const left = battle.time;
battle.every(RETRY, (b, sched) => {