feedback1e WP: review fixes — 信仰搅拌机 S3 ignores 攻击距离, bounded range mini-map
- 信仰搅拌机 S3 退休前布道 attacks / counters on 3-13 exactly: SPT-Y's 攻击距离+1 does not reach it (PRTS 技能3 备注 "此技能的攻击范围不受“攻击距离”属性影响"; PRTS 修正 of the module text "(部分技能不受此影响)"). New SkillSpec flag targeting.noRangeExtend, read by Battle._refreshRange; S1 / S2 keep the +1. - The detail card's 攻击范围 draws a grid larger than its box (远牙 S3's 21-tile line) with smaller cells in the box's own space (rangeGridStyle, RANGE_FIT 6 columns × 7 rows), so the live stats beside it stay readable. - 荒芜拉普兰德 S1's whole-field grid only selects targets (no rangeId, no 攻击范围 text): targeting.showOwnRange keeps her 3-1 on the card. - 烛煌 S3's bonus on burning enemies is dealt before the attack's damage (备注 "于攻击造成伤害前判定元素爆发并造成元素伤害"), main and splash. - liveRangeGrid is the grid itself when nothing extends it (no copy per refresh). - Audit test: 攻击距离加长 / 缩短 (深巡 S2, 山 S2) and every module of the loadout; real BattleSpec for 信仰搅拌机 SPT-Y.
This commit is contained in:
1 parent
8b0e672bb8
commit
5963a40d2d
11 files changed
+276
-58
No files matched your search
+14
-9
@@ -1590,18 +1590,21 @@ export class Battle {
|
||||
|
||||
/**
|
||||
* Recompute a unit's absolute range tile keys: `rangeKeys` / `rangeKeySet` = current range (skill range override +
|
||||
* rangeExtend) ∪ `unit.extraRangeKeys` (content extra targets — setExtraRange); `baseRangeKeys` = the INITIAL range
|
||||
* of the DEFAULT skill trigger: the unit's own grid + its permanent rangeExtend (`s.baseRangeExtend`: persistent,
|
||||
* never-expiring buffs — talents, modules, bonds), no skill range, no temporary extend, no extra keys;
|
||||
* `liveRangeGrid` = the relative grid (facing RIGHT) behind `rangeKeys` without the extra keys (targeting.js
|
||||
* extendedGrid) — the detail card's live 攻击范围 (shared/protocol.js unitStatsEntry `range`; community report E1
|
||||
* after 0.1.0: the card kept the base grid while 烛煌 S3 attacked with 4-11). Rebuilt on deploy / relocate / skill
|
||||
* range switches and whenever either extend changes (rangeChanged).
|
||||
* rangeExtend — the unit's own 攻击距离 unless the running skill's range ignores it, `targeting.noRangeExtend`: 信仰搅拌机
|
||||
* S3, PRTS 备注 "此技能的攻击范围不受'攻击距离'属性影响" — plus the skill's own `targeting.rangeExtend`) ∪
|
||||
* `unit.extraRangeKeys` (content extra targets — setExtraRange); `baseRangeKeys` = the INITIAL range of the DEFAULT
|
||||
* skill trigger: the unit's own grid + its permanent rangeExtend (`s.baseRangeExtend`: persistent, never-expiring
|
||||
* buffs — talents, modules, bonds), no skill range, no temporary extend, no extra keys; `liveRangeGrid` = the
|
||||
* relative grid (facing RIGHT) of the 攻击范围 the detail card shows (shared/protocol.js unitStatsEntry `range`;
|
||||
* community report E1 after 0.1.0: the card kept the base grid while 烛煌 S3 attacked with 4-11): the grid behind
|
||||
* `rangeKeys` without the extra keys (targeting.js extendedGrid; the grid itself when nothing extends it), or the
|
||||
* unit's own range while a skill's grid only selects targets (`targeting.showOwnRange`: 荒芜拉普兰德 S1, no official
|
||||
* range change). Rebuilt on deploy / relocate / skill range switches and whenever either extend changes (rangeChanged).
|
||||
*/
|
||||
_refreshRange(u) {
|
||||
const tg = u.skill && u.skill.active ? u.skill.spec.targeting : null;
|
||||
const grid = (tg && tg.rangeGrid) || u.rangeGrid || [[0, 0]];
|
||||
const ext = u.s.rangeExtend + ((tg && tg.rangeExtend) || 0);
|
||||
const ext = (tg && tg.noRangeExtend ? 0 : u.s.rangeExtend) + ((tg && tg.rangeExtend) || 0);
|
||||
u._rangeExtend = u.s.rangeExtend;
|
||||
u._baseExtend = u.s.baseRangeExtend;
|
||||
const keys = absoluteRangeKeys(grid, u.tileR, u.tileC, u.dir, ext);
|
||||
@@ -1610,7 +1613,9 @@ export class Battle {
|
||||
if (extra) for (const k of extra) if (!set.has(k)) { set.add(k); keys.push(k); }
|
||||
u.rangeKeys = keys;
|
||||
u.rangeKeySet = set;
|
||||
u.liveRangeGrid = extendedGrid(grid, ext);
|
||||
const own = !!(tg && tg.showOwnRange);
|
||||
const lg = own ? u.rangeGrid || [[0, 0]] : grid, le = own ? u.s.rangeExtend : ext;
|
||||
u.liveRangeGrid = le > 0 ? extendedGrid(lg, le) : lg;
|
||||
if (u.kind !== 'device') u.baseRangeKeys = absoluteRangeKeys(u.rangeGrid || [[0, 0]], u.tileR, u.tileC, u.dir, u.s.baseRangeExtend);
|
||||
if (u.skill) u.skill._trigKeys = null; // CUSTOM_RANGE trigger grid is relative to the tile
|
||||
}
|
||||
|
||||
@@ -166,7 +166,11 @@ function lonely(battle, unit, diag = false) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/** Permanent forward range extension ("攻击距离+1(不受技能攻击范围变化影响)": every range, skill ranges included). */
|
||||
/**
|
||||
* Permanent forward range extension (攻击距离 +n): every range, skill ranges included — except a skill whose range ignores
|
||||
* 攻击距离 (`targeting.noRangeExtend`). SPT-Y's "攻击距离+1(不受技能攻击范围变化影响)" reads, as PRTS 修正 it, "(部分技能不受
|
||||
* 此影响)" (原因: 语序颠倒、表述模糊) — 信仰搅拌机 S3 备注 "此技能的攻击范围不受'攻击距离'属性影响".
|
||||
*/
|
||||
const rangeUp = (battle, unit, n = 1) => battle.addBuff(unit, { key: 'module:range', mods: { rangeExtend: n }, persist: true, allowDead: true });
|
||||
/**
|
||||
* Module "攻击范围扩大" given as a new range (the selected module's data-only talent `rangeGrid`, e.g. SPC-X / RIN-X:
|
||||
@@ -197,7 +201,7 @@ function pullToFront(battle, unit, e, force) {
|
||||
const kits = {
|
||||
// ===== 信仰搅拌机 (shotprotector) S3 退休前布道 — ammo 30 counters; talents 扫射迎宾仪礼 / 架盾送客仪礼; module: reveal
|
||||
// S1 铳骑主考官 (自动触发 ⇒ DEFAULT: next attack ×3 hits + reload an adjacent 拉特兰 ammo skill), S2 八臂电锯侠 (ammo;
|
||||
// a fatal hit is blocked for `ammo_cost` bullets); module SPT-Y 老朋友: range +1
|
||||
// a fatal hit is blocked for `ammo_cost` bullets); module SPT-Y 老朋友: range +1 (not on S3's range, PRTS 备注)
|
||||
chess_char_4_01_a: (bb, chess, def) => {
|
||||
const t0 = tbb(def, 0), t1 = tbb(def, 1);
|
||||
const counterMax = Math.max(1, Math.floor(num(bb['attack@max_target'], 3)));
|
||||
@@ -233,7 +237,8 @@ const kits = {
|
||||
skill: {
|
||||
kind: 'ammo', ammo: num(bb['attack@trigger_time'], 30),
|
||||
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), defPct: num(bb.def) },
|
||||
targeting: g ? { rangeGrid: g } : undefined,
|
||||
// 3-13 exactly: SPT-Y's 攻击距离+1 does not reach it (PRTS 备注; reviewer of community report E1 after 0.1.0)
|
||||
targeting: g ? { rangeGrid: g, noRangeExtend: true } : undefined,
|
||||
attack: { noAttack: true }, // 停止主动攻击敌人 (ammo is spent by counters)
|
||||
onStart({ battle, unit }) {
|
||||
unit.mem.counterReady = -Infinity;
|
||||
@@ -281,7 +286,7 @@ const kits = {
|
||||
if (sk.ammoLeft <= 0) sk.end('ammo');
|
||||
}, { owner: unit });
|
||||
}
|
||||
// module SPT-Y (elite, 老朋友): 攻击距离+1 (kept whatever range the skill sets)
|
||||
// module SPT-Y (elite, 老朋友): 攻击距离+1 — S1 / S2 keep it, S3's 3-13 does not (noRangeExtend above)
|
||||
if (num(def.traitBb?.ability_range_forward_extend, 0) > 0) rangeUp(battle, unit, num(def.traitBb.ability_range_forward_extend, 1));
|
||||
// module SPT-X (elite default): 攻击范围内敌人的隐匿效果失效
|
||||
if (moduleIs(def, 'uniequip_002_rmixer')) whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit)));
|
||||
|
||||
@@ -562,9 +562,10 @@ const KITS = {
|
||||
// 烛煌 — S3 众恶的焚场 (ammo): skill range 4-11 (the targets; the DEFAULT cast still needs an enemy in 3-1), ATK +,
|
||||
// BAT −1.3 s, "攻击变为群体攻击" = one target + a BLAZE_S3_SPLASH (1.7) splash around it (PRTS 备注 "攻击溅射半径1.7",
|
||||
// 中点判定 — so the fire reaches past the diamond; it used to hit every enemy inside it instead, community report E1
|
||||
// after 0.1.0), +attack@atk_scale ATK elemental damage to every enemy of the attack in a burn burst (main and splash;
|
||||
// after the hit [ASSUMED] — the 备注 checks it before); loses 3 % max HP/s; any burn burst on the field refills
|
||||
// ammo_recover bullets, never above the skill's ammo ("补充后的弹药数量无法超过上限").
|
||||
// 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 ("补充后的弹药数量无法超过上限").
|
||||
// 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.
|
||||
@@ -630,12 +631,7 @@ 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,
|
||||
onEachHit({ battle, unit, target }) {
|
||||
if (target && target.alive && target.findBuff('burnBurst')) elementHit(battle, unit, target, unit.s.atk * num(bb['attack@atk_scale']), 'blazeBurn', 'burn');
|
||||
},
|
||||
},
|
||||
attack: { splashRadius: BLAZE_S3_SPLASH },
|
||||
onStart({ unit }) { unit.mem.blazeAcc = 0; },
|
||||
onTick({ battle, unit, dt }) {
|
||||
if (!(lose > 0)) return;
|
||||
@@ -686,6 +682,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 备注)
|
||||
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;
|
||||
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);
|
||||
battle.on('elementBurst', (c) => { // S3: burn bursts refill ammo, up to the skill's ammo
|
||||
const sk = unit.skill;
|
||||
|
||||
@@ -2754,11 +2754,12 @@ function whitw2(bb, chess, def) {
|
||||
const skillGridW = skillGridOf(def);
|
||||
const skills = {
|
||||
// S1 慵怠者悲鸣: passive 浮游单元+1 (trait: one more hit per attack); toggled on: ATK +atk and the drones lock a random
|
||||
// non-moving enemy anywhere on the field (re-locking when it moves or falls; install), else her range
|
||||
// non-moving enemy anywhere on the field (re-locking when it moves or falls; install), else her range. The whole-field
|
||||
// grid only selects those targets — no rangeId, no 攻击范围 in the text — so the card keeps her 3-1 (showOwnRange)
|
||||
skchr_whitw2_1: {
|
||||
kind: 'toggle',
|
||||
mods: { atkPct: num(bb.atk) },
|
||||
targeting: { rangeGrid: WHOLE_FIELD },
|
||||
targeting: { rangeGrid: WHOLE_FIELD, showOwnRange: true },
|
||||
onStart({ unit }) { unit.mem.lazyLock = null; },
|
||||
},
|
||||
// S2 逐猎狂飙: 浮游单元+attack@cnt, skill range, ATK +atk: every drone locks a random enemy of the range until it falls
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
// instant (onStart + optional one-shot attack override for the next attack), charges (= instant with charges),
|
||||
// passive (always on from deployment, no SP), toggle (stays on until death once activated).
|
||||
// SkillSpec fields (all optional): kind, duration, ammo, spCost, initSp, charges, spType, trigger,
|
||||
// mods, flags, targeting {maxTargets, rangeGrid, priority, allInRange, rangeExtend},
|
||||
// mods, flags, targeting {maxTargets, rangeGrid, priority, allInRange, rangeExtend, noRangeExtend (the range ignores
|
||||
// the unit's 攻击距离), showOwnRange (rangeGrid only selects targets: the detail card keeps the unit's own range)},
|
||||
// attack {dmgType, atkScale, splashRadius, splashScale, hits, projectile, maxTargets, dmgMul, onHit, heal…},
|
||||
// heal (bool: heal-type skill for the trigger rule), onStart(ctx), onEnd(ctx), onHit(ctx), onAttack(ctx), onTick(ctx).
|
||||
// ctx passed to spec callbacks: { battle, unit, skill, bb, target?, dealt?, targets?, dt?, reason? }; onAttack's ctx
|
||||
@@ -43,6 +44,8 @@ import { absoluteRangeKeys, canTargetEnemy } from './targeting.js';
|
||||
import { AUTO_OP_COOLDOWN, COLS, ROWS } from './constants.js';
|
||||
|
||||
const TICK_RULES = new Set(['SP_FULL', 'SEARCH', 'CUSTOM_RANGE', 'SKILL_RANGE', 'GDGLOW_SKILL_2']);
|
||||
/** True when a SkillSpec `targeting` changes the unit's range while the skill runs (Battle._refreshRange). */
|
||||
const changesRange = (tg) => !!(tg && (tg.rangeGrid || tg.rangeExtend || tg.noRangeExtend));
|
||||
/** Enemies that satisfy a content trigger range (any targetable enemy, flyers included). */
|
||||
const TRIGGER_PROFILE = Object.freeze({ canHitFly: true });
|
||||
/** Every tile of the stage (GDGLOW_SKILL_2: the whole field). */
|
||||
@@ -194,12 +197,12 @@ export class SkillRuntime {
|
||||
if (s.mods || s.flags) {
|
||||
this.battle.addBuff(this.unit, { key: this._buffKey, mods: s.mods || null, flags: s.flags || null, tags: ['skill'] });
|
||||
}
|
||||
if (s.targeting && (s.targeting.rangeGrid || s.targeting.rangeExtend)) this.battle._refreshRange(this.unit);
|
||||
if (changesRange(s.targeting)) this.battle._refreshRange(this.unit);
|
||||
}
|
||||
|
||||
_removeMods() {
|
||||
this.battle.removeBuff(this.unit, this._buffKey);
|
||||
if (this.spec.targeting && (this.spec.targeting.rangeGrid || this.spec.targeting.rangeExtend)) this.battle._refreshRange(this.unit);
|
||||
if (changesRange(this.spec.targeting)) this.battle._refreshRange(this.unit);
|
||||
}
|
||||
|
||||
/** Add SP (fires the `spGain` hook). Ignored while a duration/ammo/toggle skill runs (its bar shows the skill). */
|
||||
|
||||
@@ -46,7 +46,8 @@ export function absoluteRangeKeys(grid, r, c, dir, extend = 0) {
|
||||
/**
|
||||
* 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 攻击范围).
|
||||
* 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]>}
|
||||
|
||||
Reference in new issue
Block a user