feedback1e WP: review round 3 — 烛煌 S3 last bolt keeps the burst bonus, splash fx; prep overlay and wheel show the deployed range

- 烛煌 S3: the burn-burst bonus is gated on the attack's own isSkill (captured when the bolt was fired), not on the live
  skill, so the last bullet (it lands after end('ammo')) and bolts in flight when the skill ends early still carry it.
- 烛煌 S3: every landed bolt emits fx 'splash' (r 1.7, burn) at the main target, so the splash shows on screen.
- shared/loadoutRecord.js attackRangeGrid: the range a unit is deployed with — a selected passive range skill
  (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1; not 空弦 ISW-A's 集成战略-only one) —
  so the board overlay, the deploy wheel and the card agree with the prep live range. extendedGrid moves there
  (re-exported by targeting.js).
- runner.js header: unitStatsEntry carries the ally's live range.
This commit is contained in:
sganggs committed 2026-10-03 06:25:12 +08:00
1 parent 5963a40d2d
commit a657f809f1
11 files changed
+247 -52

No files matched your search

+14 -5
View File
@@ -565,7 +565,7 @@ const KITS = {
// after 0.1.0), +attack@atk_scale ATK elemental damage to every enemy of the attack in a burn burst (main and splash),
// dealt BEFORE the attack's own damage ("于攻击造成伤害前判定元素爆发并造成元素伤害": a 'hit' hook — so a hit that kills
// or starts the burst keeps / does not get it); loses 3 % max HP/s; any burn burst on the field refills ammo_recover
// bullets, never above the skill's ammo ("补充后的弹药数量无法超过上限").
// bullets, never above the skill's ammo ("补充后的弹药数量无法超过上限"). Each landed bolt shows its splash (fx 'splash' r 1.7).
// T1 熔点引爆: burn burst anywhere → 350 % ATK elemental damage to it + heal 12 % max HP. T2 绝处重燃: downed instead of
// dying (6000 shield, no attack, no heal, 3 %/s regen) → revives at full HP and stuns nearby enemies.
// Module (elite): ×damage_scale vs enemies in an element burst.
@@ -631,7 +631,14 @@ const KITS = {
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 18),
mods: mods({ atkPct: num(bb.atk), batPct: batPct(bb.base_attack_time, chess) }),
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
attack: { splashRadius: BLAZE_S3_SPLASH },
attack: {
splashRadius: BLAZE_S3_SPLASH,
// the fire of each landed bolt (the screen rings splash attacks by sub-profession; 本源术师 is not one): once
// per attack at the main target / its spot — from the profile it was fired with, so the last bolt too
onHit({ battle, unit, target, x, y }) {
battle.fx('splash', { x, y, ...(target && target.alive ? { id: target.id } : {}), src: unit.id, r: BLAZE_S3_SPLASH, element: 'burn' });
},
},
onStart({ unit }) { unit.mem.blazeAcc = 0; },
onTick({ battle, unit, dt }) {
if (!(lose > 0)) return;
@@ -682,10 +689,12 @@ const KITS = {
burstSpUp(battle, unit, 'blaze2:module', num(tm.sp_recovery_per_sec));
if (sid === 'skchr_blaze2_2') whileOn(battle, unit, AURA_IV, () => burnTiles(battle, unit));
if (sid && sid !== 'skchr_blaze2_3') return;
// S3: each hit of her skill attack (main and splash) on an enemy in a burn burst deals the bonus first (PRTS 备注)
// S3: each hit of her skill attack (main and splash) on an enemy in a burn burst deals the bonus first (PRTS 备注).
// Gated on the attack's own isSkill (captured when the bolt was fired), not on the live skill: her bolts land after
// the last bullet's end('ammo') / an early end (downed), and those still carry it. Only S3 runs here (sid gate).
battle.on('hit', (c) => {
const d = c.dmg, t = c.target, sk = unit.skill;
if (c.source !== unit || !d.isAttack || !d.isSkill || d.cancel || !sk?.active || sk.kind !== 'ammo') return;
const d = c.dmg, t = c.target;
if (c.source !== unit || !d.isAttack || !d.isSkill || d.cancel) return;
if (t.side === 'enemy' && t.alive && t.findBuff('burnBurst')) elementHit(battle, unit, t, unit.s.atk * num(bb['attack@atk_scale']), 'blazeBurn', 'burn');
}, { owner: unit });
const maxAmmo = num(bb['attack@trigger_time'], 18);
+3 -23
View File
@@ -43,29 +43,9 @@ export function absoluteRangeKeys(grid, r, c, dir, extend = 0) {
return keys;
}
/**
* A range grid (`[dRow, dCol]`, facing RIGHT) grown by `extend` (rangeExtend, DESIGN §3): every row gains the whole
* tiles 1 … ⌊extend⌋ beyond its far (+dCol) end — the relative form of what absoluteRangeKeys builds, deduplicated, junk
* entries dropped. Battle._refreshRange keeps it as `unit.liveRangeGrid` (the detail card's live 攻击范围) when an extend
* applies (else the grid itself).
* @param {Array<[number, number]>|null|undefined} grid
* @param {number} [extend]
* @returns {Array<[number, number]>}
*/
export function extendedGrid(grid, extend = 0) {
const out = [];
const seen = new Set();
const add = (dr, dc) => { const k = `${dr},${dc}`; if (!seen.has(k)) { seen.add(k); out.push([dr, dc]); } };
if (!Array.isArray(grid)) return out;
const cells = grid.filter((p) => Array.isArray(p) && Number.isInteger(p[0]) && Number.isInteger(p[1]));
for (const [dr, dc] of cells) add(dr, dc);
if (extend > 0 && Number.isFinite(extend)) {
const maxByRow = new Map();
for (const [dr, dc] of cells) maxByRow.set(dr, Math.max(maxByRow.get(dr) ?? -Infinity, dc));
for (const [dr, mx] of maxByRow) for (let k = 1; k <= Math.min(extend, COLS); k++) add(dr, mx + k);
}
return out;
}
// extendedGrid(grid, extend): the relative form of absoluteRangeKeys' rangeExtend — one implementation with the card's
// record range (shared/loadoutRecord.js attackRangeGrid), re-exported here for the battle (Battle._refreshRange).
export { extendedGrid } from '../../shared/loadoutRecord.js';
/** Tile key of a unit's current position. */
export function tileKeyOf(u) {