feedback3: merge W3B — 碎骨's grenade (community report #14) and the enemy numbers from the 0.1.2 review against PRTS (扎罗, 复仇者, 庞贝, 巨大的丑东西, 墓碑, 烹泉, 集团军重型火炮, 清明, 重弩突袭者); 圣聆初雪 S2 freezes the blue gate (#32); 琳琅诗怀雅 S1 heals without an attack (owner's decision, #5)

This commit is contained in:
sganggs committed 2026-10-04 18:19:16 +08:00
commit 2729a2137e
11 files changed
+690 -133

No files matched your search

+232 -78
View File
@@ -92,7 +92,7 @@
// hidden tabs (shared/protocol.js fxForm).
// Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss.
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS } from '../constants.js';
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS, COLS } from '../constants.js';
import { canTargetAlly, sortAllyTargets, aggroCmp, enemyStealthed, areaSelectable, auraSelectable } from '../targeting.js';
import { mitigate, periodicDamage } from '../damage.js';
@@ -141,8 +141,9 @@ const STEALTH_RESTORE_BY_KEY = Object.freeze({
});
/** 假想敌:再生 shield aura radius (PRTS 假想敌:再生 天赋 "进入此形态时,使半径1.8范围内的其他敌方单位(无视其可选性)获得5层…护盾"). */
const ACPUPP_AURA_RADIUS = 1.8;
/** 重弩突袭者 直击 reach along a row/column [ASSUMED]. */
const CROSS_REACH = 6;
/** 重弩突袭者 直击 reach along a row/column [ASSUMED]; the skill's length and its charge before the bolt (PRTS 直击 "蓄力1.4s后
* 向目标方向发射1支弩箭…※技能持续2.5s"). */
const CROSS_REACH = 6, CROSS_SKILL = 2.5, CROSS_CHARGE = 1.4;
/** 暴鸰 投弹: the target's tile and its 8 neighbours (PRTS "对目标及其周围八格的我方单位造成100%物理伤害"). */
const BOMB_REACH = 1;
/** 暴鸰 投弹: the bomb leaves on the OnAttack event of the cast's Attack clip (the official battle prefab's Boomb ability:
@@ -175,8 +176,32 @@ const BUGS_PER_CAST = 3;
const TORTURER_RADIUS = 1;
/** Barrel zone radius of 咸鳞汁推荐者 [ASSUMED]. */
const BARREL_RADIUS = 1.5;
/** "短时间" DEF-down of 碎骨's grenade [ASSUMED]. */
const SKULSR_DEFDOWN_DUR = 3;
/** 碎骨's grenade (PRTS 碎骨 天赋 "未被阻挡时发射榴弹对目标及其周围八格的我方单位造成相当于攻击力26%的物理伤害,并令其在5秒内
* 防御力下降50%"; the blackboard has only defdown.def −0.5): share of ATK on every unit hit, DEF-down duration (s).
* Until 0.1.3: the full attack on the target, 100 % on the 4 neighbours, 3 s [ASSUMED]. */
const SKULSR_GRENADE_SCALE = 0.26, SKULSR_DEFDOWN_DUR = 5;
/** Splash radius (中点判定 around the target) of 烹泉 / 沏虹's attack (PRTS 天赋 "普通攻击对目标对及目标周围半径1.0范围内的所有
* 我方单位造成法术普通伤害(无视迷彩,不可对空)") and of 集团军重型火炮's shell (PRTS "对目标半径1.0范围内的所有我方单位造成攻击力
* 100%的物理伤害(此弹道会强制击中主目标,碰撞无视迷彩,不可对空)"). */
const TEA_SPLASH_RADIUS = 1, SHELL_SPLASH_RADIUS = 1;
/** 集团军重型火炮's 【燃烧区域】: PRTS "碰撞半径1.5" with the source note "1.5倍可变半径" — ×1.5 on the blackboard's
* ProjectileBoomRange.attack@projectile_range (1). Until 0.1.3 the zone took the blackboard's 1. */
const UACANN_ZONE_SCALE = 1.5;
/** 烹泉 / 沏虹 死亡爆炸 radius (PRTS 天赋 "死亡爆炸(爆炸半径1.25,造成攻击力100%法术溅射伤害并施加15s【烹泉减益】…)"; until
* 0.1.3 the attack range, 2). */
const TEA_BOOM_RADIUS = 1.25;
/** “庞贝”'s self-blast while blocked (PRTS 天赋 "对<固定半径>半径1.4范围内的所有我方单位造成1000预计算的无途径法术溅射伤害(不可
* 对空)"; the blackboard has only the damage and the 10 s; until 0.1.3: radius 1 [ASSUMED]). */
const POMPEII_BLAST_RADIUS = 1.4;
/** “巨大的丑东西” first form: its unblocked ranged attack (PRTS 天赋 "未被阻挡时,可对半径2.5范围内的1名非飞行的我方单位进行远程攻击,
* 对目标及其周围8格内的所有我方单位造成攻击力100%的物理伤害" — the data's attack radius is 0, so it had no ranged attack until
* 0.1.3) and its self-destruct ("首次被击倒后进行持续10s的重生…:重生开始的2.17s后进行自爆,对半径3.0范围内所有我方单位造成攻击力
* 150%的物理伤害和16s晕眩"; until 0.1.3: radius 2.5 at once [ASSUMED]). */
const MCM_RANGE = 2.5, MCM_BOMB_DELAY = 2.17;
/** “复仇者” 【冲锋】 (PRTS 技能 "仅自身未被阻挡且存在符合条件的可选目标时可触发:选择3.0半径内位于自身下个检查点前的后续路径上(包含
* 自身当前所在地块)的距离自身最近的我方单位…"): the search radius. The blackboard's range_radius is 1.5; PRTS's figure is the
* one used. Until 0.1.3: any unit within 1.5, 隐匿 / 迷彩 ones included. */
const RUSH_RADIUS = 3;
/** W's C4 fuse and blast radius [ASSUMED]. */
const C4_FUSE = 1, C4_RADIUS = 1;
/** Every Nth attack for "数次攻击后" abilities without an SP cost in the data [ASSUMED]; with a cost N = spCost + 1
@@ -185,13 +210,14 @@ const NTH_ATTACK = 3;
export const nthOf = (s) => (s && s.sp > 0 ? s.sp + 1 : NTH_ATTACK);
/** 鼠王 【唱沙】 cross reach and 【沙狱】 "及其周围" radius [ASSUMED]. */
const DRIFT_REACH = 1, SANDSTORM_RADIUS = 1;
/** 巨大的丑东西 self-destruct radius ("大范围晕眩") [ASSUMED]. */
const MCM_BOMB_RADIUS = 2.5;
/** “巨大的丑东西” self-destruct radius (PRTS 天赋 "对半径3.0范围内所有我方单位…"; 2.5 [ASSUMED] until 0.1.3). */
const MCM_BOMB_RADIUS = 3;
/** 自在 【纬地经天】: tiles covered along the row and the column of the centre ("十字型") [ASSUMED]. */
const XI_CROSS_REACH = 2;
/** 扎罗: form-2 "远程" attack radius, 【远古威慑】 aura radius / ASPD (no numbers in the data), 溶血骇惧 HP loss per second
* reached at the end of duration_bleed (share of max HP, rising linearly — "逐渐加速失去生命值") [ASSUMED]. */
const WOLF_RANGE = 2.5, WOLF_AWE_RADIUS = 2.5, WOLF_AWE_ASPD = -30, WOLF_BLEED_PEAK = 0.05;
/** 扎罗 (PRTS 扎罗,“狼之主” 天赋; no numbers in the data): form-2 attack radius ("进行远程攻击,普通攻击为2连击,攻击范围半径1.25"),
* 【远古威慑】 aura radius / ASPD ("自身1.5半径范围内的我方单位攻击速度-50(指定状况下生效)" — during the 重生 and in the second form).
* Until 0.1.3: 2.5 / 2.5 / −30, the aura only during the 重生 [ASSUMED]. */
const WOLF_RANGE = 1.25, WOLF_AWE_RADIUS = 1.5, WOLF_AWE_ASPD = -50;
/** 失衡 movement speed that turns 弧光锋卫's per-interval bleed into HP per tile moved [ASSUMED]. */
const UNBALANCE_SPEED = 5;
/** 乌顶巨角卢鲁 【角力对决】: the operator cannot be pushed (fixed tiles) ⇒ "更多伤害" multiplier [ASSUMED]. */
@@ -680,6 +706,55 @@ const onHitStatus = (key, dur, value) => ({ dealt(c, b, e) { if (dur > 0) b.appl
* 受到…伤害": areaAllies — a 迷彩 ally is hit, an unblocking 隐匿 one not; the attack itself still needs a target).
*/
const hitAllInRadius = (r) => ({ before(c, b, e) { const l = areaAllies(b, e, e.x, e.y, r); if (l.length) c.targets = byPriority(e, l); } });
/**
* A normal attack that splashes around its target (PRTS: "对主目标造成…普通伤害,对溅射目标造成…溅射伤害"): 碎骨's grenade,
* “巨大的丑东西” / “墓碑”'s unblocked ranged attack, 烹泉 / 沏虹's attack, 集团军重型火炮's shell. Its mode is fixed when the
* attack starts (`before`): `unblocked` = only an attack begun while not blocked splashes (the others are the plain melee
* hit at `meleeScale`). The main target takes the engine's attack at `scale` × ATK (e.profile.atkScale, a normal attack:
* on-hit effects apply); when it lands (the main hit's 'hit', before its dodge) the others around the target take
* `scale` × ATK as a splash — an area selection (areaAllies / areaAlliesInTiles: no 隐匿 ally that does not block the
* enemy, no airborne 起飞 one for a ground enemy; 迷彩 is not checked — DESIGN §22.12). `tiles` 1 = the target's tile and
* its 8 neighbours (格子判定), else `radius` around the target (中点判定). `noAir`: "不可对空" (a flying ally — the 炎佑
* dragon — is skipped; without it "可溅射飞行单位"). `dodge`: the splash hits may be dodged (烹泉 / 沏虹, whose PRTS 天赋 calls
* them 法术普通伤害 — a normal attack's damage); a 溅射伤害 splash cannot. `onEach(b, e, u)` runs on every unit hit (the target
* after its damage).
*/
function splashAttack({ scale = 1, meleeScale = 1, unblocked = true, tiles = 0, radius = 0, noAir = false, dodge = false, fxKind = 'splash', onEach = null } = {}) {
return {
before(c, b, e, a) { a.splash = !unblocked || !e.blockedBy; e.profile.atkScale = a.splash ? scale : meleeScale; },
hitOut(c, b, e, a) {
const t = c.target;
if (!a.splash || !c.dmg.isAttack || !t) return;
b.fx('explode', { x: t.x, y: t.y, r: tiles > 0 ? tiles + 0.5 : radius, kind: fxKind, ...(tiles > 0 ? { tiles: 'box' } : {}) });
const area = tiles > 0 ? areaAlliesInTiles(b, e, t.tileR, t.tileC, 'box', tiles) : areaAllies(b, e, t.x, t.y, radius);
for (const u of area) {
if (u === t || (noAir && u.isFlying)) continue;
hurt(b, e, u, e.s.atk * scale, c.dmg.type, { tags: ['splash'], canDodge: dodge });
if (onEach && u.alive) onEach(b, e, u);
}
},
dealt(c, b, e, a) { if (a.splash && onEach && c.target.alive) onEach(b, e, c.target); },
};
}
/**
* Tile keys of an enemy's path up to its next checkpoint, its own tile included (PRTS “复仇者” 冲锋 "位于自身下个检查点前的后续
* 路径上(包含自身当前所在地块)"): the tiles its current move leg crosses — the straight segments through the leg's smoothed
* waypoints (ai.js planLeg; the last one is the leg's checkpoint), sampled every quarter tile.
*/
function pathKeysAhead(e) {
const keys = new Set([Math.round(e.y) * COLS + Math.round(e.x)]);
const R = e.route;
if (!R || !Array.isArray(R.pts)) return keys;
let x = e.x, y = e.y;
for (let i = R.ptIdx ?? 0; i < R.pts.length; i++) {
const p = R.pts[i], n = Math.max(1, Math.ceil(Math.hypot(p.x - x, p.y - y) * 4));
for (let k = 1; k <= n; k++) keys.add(Math.round(y + ((p.y - y) * k) / n) * COLS + Math.round(x + ((p.x - x) * k) / n));
x = p.x; y = p.y;
}
return keys;
}
/** "不会攻击飞行单位" (an engine candidate filter, ai.js enemyAttack: the 炎佑 dragon is a flying ally). */
const noAirTargets = () => ({ spawn(b, e) { e.profile.canTarget = (u) => !u.isFlying; } });
/**
* 抵抗 (ba.buffres, the engine `resist` status: control durations halved, 麻痹 loses 1 stack every 5 s) + optional
* immunities. Value from the talent's `Buff.one_minus_status_resistance` when present (−0.5 ⇒ half).
@@ -1120,10 +1195,11 @@ function pollution(b, src, x, y, r, life, low, high) {
* its area selection takes (areaAllies: no unblocking 隐匿, untargetable or sleeping ally; no airborne 起飞 one for a ground
* enemy — PRTS 集团军重型火炮 【燃烧区域】 "碰撞不受迷彩制约,不可对空": 迷彩 only). A sourceless zone (`src` null: 假想敌:蚀裂's
* 毒雾, an enemy's 死亡爆炸 with no 无视可选性 note on PRTS) selects as an effect with no selecting enemy: 隐匿 kept out, an
* airborne 起飞 ally reached (§21.22; until 0.1.2 it took everyone inside).
* airborne 起飞 ally reached (§21.22; until 0.1.2 it took everyone inside). `noAir`: "不可对空" — a flying ally (the 炎佑
* dragon) is skipped too (集团军重型火炮's 【燃烧区域】, since 0.1.3).
*/
function dmgZone(b, src, x, y, r, life, iv, amount, type = 'arts', kind = 'zone', el = null, elAmount = 0) {
zone(b, { x, y, r, life, iv, kind, pick: (zx, zy, zr) => areaAllies(b, src, zx, zy, zr), tick(units) {
function dmgZone(b, src, x, y, r, life, iv, amount, type = 'arts', kind = 'zone', el = null, elAmount = 0, { noAir = false } = {}) {
zone(b, { x, y, r, life, iv, kind, pick: (zx, zy, zr) => areaAllies(b, src, zx, zy, zr).filter((u) => !(noAir && u.isFlying)), tick(units) {
for (const u of units) {
hurt(b, src, u, amount, type, { tags: [kind] });
if (el && elAmount > 0) elem(b, src, u, el, elAmount, { tags: [kind] });
@@ -1425,7 +1501,9 @@ function kitInvisShield(ab, e) {
const s = ab.sk.InvisibleShield;
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2;
return [...kitDeathSpawn()(ab), skill(s, (b, e2) => {
const dur = s.bb.duration ?? T(ab, 'InvisibleShield.duration') ?? 0;
// 辉光照耀 "使【范围隐匿】生效3s" (PRTS 清明 / 堂皇) = the talent's InvisibleShield.duration (3 for both); 清明's skill
// blackboard says 5 — until 0.1.3 that 5 was used
const dur = T(ab, 'InvisibleShield.duration') ?? s.bb.duration ?? 0;
b.fx('telegraph', { x: e2.x, y: e2.y, r, kind: 'invisShield', id: e2.id });
// "获得隐匿(解除阻挡0秒后恢复)" (PRTS 清明 / 堂皇 天赋): the veil's 隐匿 is back as soon as a block ends
for (const o of b.enemiesInRadius(e2.x, e2.y, r)) if (o !== e2 && dur > 0) b.addBuff(o, { key: 'ab:veiled', duration: dur, flags: { stealth: true }, visible: true, data: { stealthRestore: 0 } });
@@ -1435,16 +1513,29 @@ function kitInvisShield(ab, e) {
function kitCrossbow(ab) {
const s = ab.sk.CrossAttack;
const aligned = (b, e) => b.allies().filter((u) => canTargetAlly(e, u, true) && (Math.abs(u.y - e.y) < 0.5 || Math.abs(u.x - e.x) < 0.5) && Math.hypot(u.x - e.x, u.y - e.y) <= CROSS_REACH);
const nearest = (b, e, l) => l.sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y))[0];
// PRTS 重弩突袭者: 天赋 "隐匿(被阻挡,主动攻击期间均可解除)"; 直击 "蓄力1.4s后向目标方向发射1支弩箭,对击中的首个目标造成攻击力100%
// 的法术伤害与5s晕眩 ※技能持续2.5s": revealed for the whole skill, the bolt leaves CROSS_CHARGE s in — at the nearest unit still
// in line in the aimed direction; it stands meanwhile [ASSUMED], and a stun / silence or its death before the release
// cancels the shot [ASSUMED]. The blackboard's duration (2) is not the skill's length PRTS gives. Until 0.1.3: revealed
// 2 s, the bolt at once.
return [stealth(), skill(s, (b, e) => {
const t = aligned(b, e).sort((p, q) => Math.hypot(p.x - e.x, p.y - e.y) - Math.hypot(q.x - e.x, q.y - e.y))[0];
const t = nearest(b, e, aligned(b, e));
if (!t) return;
const bb = s.bb;
if (bb.duration > 0) b.addBuff(e, { key: 'ab:revealed', duration: bb.duration, flags: { reveal: true } });
b.addProjectile({ from: e, target: t, speed: 12, visual: 'enemy', source: e, onHit: (c) => {
if (!c.target || !c.target.alive) return;
hurt(b, e, c.target, e.s.atk * (bb.atk_scale ?? 1), 'arts');
if (bb.stun > 0 && c.target.alive) b.applyStatus(c.target, 'stun', { duration: bb.stun, source: e });
} });
const dx = Math.sign(Math.round(t.x - e.x)), dy = Math.sign(Math.round(t.y - e.y));
b.addBuff(e, { key: 'ab:revealed', duration: CROSS_SKILL, flags: { reveal: true } });
b.addBuff(e, { key: 'ab:aim', duration: CROSS_SKILL, flags: { noMove: true, disarm: true } });
b.after(CROSS_CHARGE, () => {
if (!e.alive || e.hidden || e.s.flags.stun || e.s.flags.silence) return;
const tt = nearest(b, e, aligned(b, e).filter((u) => Math.sign(Math.round(u.x - e.x)) === dx && Math.sign(Math.round(u.y - e.y)) === dy));
if (!tt) return;
b.addProjectile({ from: e, target: tt, speed: 12, visual: 'enemy', source: e, onHit: (c) => {
if (!c.target || !c.target.alive) return;
hurt(b, e, c.target, e.s.atk * (bb.atk_scale ?? 1), 'arts');
if (bb.stun > 0 && c.target.alive) b.applyStatus(c.target, 'stun', { duration: bb.stun, source: e });
} });
}, { owner: e });
}, { sil: true, cond: (b, e) => aligned(b, e).length > 0 })];
}
@@ -1876,24 +1967,28 @@ function kitDekght(ab) {
return partnerRage;
}
/**
* 烹泉 / 沏虹 (PRTS 天赋): every normal attack hits its target and every unit within TEA_SPLASH_RADIUS of it ("法术普通伤害
* (无视迷彩,不可对空)" — splashAttack, blocked or not); 死亡爆炸 "(爆炸半径1.25,造成攻击力100%法术溅射伤害并施加15s【烹泉减益】,
* 无视迷彩,不可对空)", 【烹泉减益】 "攻击速度-40,且固定每3秒额外-0(可被抵抗,可叠加,每层持续时间和效果独立计算)" — DeadBoom.
* attack_speed / duration; one layer per blast, each its own buff. Until 0.1.3: no attack splash; the blast took the attack
* radius (2) and left a 15 s steam zone re-applying the ASPD cut to whoever stood in it [ASSUMED]. The attack splash, a 法术普通
* 伤害, can be dodged like the attack itself; the blast (法术溅射伤害) cannot.
*/
function kitTeapot(ab) {
return kitDeathSpawn([{
return kitDeathSpawn([splashAttack({ unblocked: false, radius: TEA_SPLASH_RADIUS, noAir: true, dodge: true, fxKind: 'artsSplash' }), {
sil: true,
death(c, b, e) {
if (c.reason !== 'killed') return;
const aspd = T(ab, 'DeadBoom.attack_speed') ?? 0, dur = T(ab, 'DeadBoom.duration') ?? 0, iv = T(ab, 'DeadBoom.interval') ?? 1;
const r = e.base.rangeRadius || 2, atk = e.s.atk, x = e.x, y = e.y;
// an area selection (PRTS 烹泉 "死亡爆炸(…无视迷彩,不可对空)", no 无视无法选择): an airborne 起飞 ally and an
// unblocking 隐匿 one are skipped, the steam's ticks too (the blast's debuff)
const pick = (zx, zy, zr) => areaAllies(b, e, zx, zy, zr);
// 被击倒后爆炸造成范围法术伤害 (one blast of ATK ×1 [ASSUMED scale]) …
const aspd = T(ab, 'DeadBoom.attack_speed') ?? 0, dur = T(ab, 'DeadBoom.duration') ?? 0;
const r = TEA_BOOM_RADIUS, atk = e.s.atk, x = e.x, y = e.y;
// an area selection (no 无视无法选择 note): an airborne 起飞 ally and an unblocking 隐匿 one are skipped, flyers too
b.fx('explode', { x, y, r, kind: 'teaBoom', id: e.id });
for (const u of pick(x, y, r)) hurt(b, null, u, atk, 'arts', { tags: ['teaBoom'] });
// … 使我方攻击速度大幅降低(可被抵抗): the steam lingers `duration` s; allies inside keep the ASPD cut (re-applied
// every `interval` s)
const slow = (units) => { for (const u of units) b.addBuff(u, { key: 'ab:teaBoom', duration: iv + 0.1, refresh: 'extend', mods: { aspd }, visible: true }); };
slow(pick(x, y, r));
if (dur > 0) zone(b, { x, y, r, life: dur, iv, kind: 'teaSteam', tick: slow, pick });
for (const u of areaAllies(b, e, x, y, r)) {
if (u.isFlying) continue;
hurt(b, null, u, atk, 'arts', { tags: ['teaBoom'] });
if (u.alive && dur > 0 && aspd) b.addBuff(u, { key: `ab:teaBoom:${e.id}`, duration: dur, refresh: 'replace', mods: { aspd }, visible: true });
}
},
}])(ab);
}
@@ -1918,25 +2013,32 @@ function kitLeaderMisc(key, ab, e) {
onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: dot.dmg, type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility'] }) });
},
}, {
iv: T(ab, 'rangedamage.interval') ?? 10,
tick(b, e2) {
if (!e2.blockedBy) return; // 被阻挡时会进行自爆造成高额伤害
b.fx('explode', { x: e2.x, y: e2.y, r: 1, kind: 'selfBlast' });
for (const u of areaAllies(b, e2, e2.x, e2.y, 1)) hurt(b, e2, u, T(ab, 'rangedamage.attack@damage') ?? 0, 'arts');
// 被阻挡时会进行自爆 — PRTS “庞贝” 天赋: "被阻挡时,每10秒(受晕眩/无法行动/沉睡/冻结/浮空影响时暂停计时,解除阻挡时重置计时)对
// 半径1.4范围内的所有我方单位造成1000预计算的无途径法术溅射伤害(不可对空)": the clock runs only while it is blocked and can
// act, and restarts at 0 on every new block (until 0.1.3: a fixed 10 s clock from its spawn, radius 1, flyers hit)
iv: 0,
tick(b, e2, a, dt) {
if (!e2.blockedBy) { a.held = 0; return; }
if (!e2.canAct) return;
a.held = (a.held || 0) + dt;
if (a.held + 1e-9 < (T(ab, 'rangedamage.interval') ?? 10)) return;
a.held = 0;
b.fx('explode', { x: e2.x, y: e2.y, r: POMPEII_BLAST_RADIUS, kind: 'selfBlast' });
for (const u of areaAllies(b, e2, e2.x, e2.y, POMPEII_BLAST_RADIUS)) if (!u.isFlying) hurt(b, e2, u, T(ab, 'rangedamage.attack@damage') ?? 0, 'arts', { tags: ['splash'] });
},
}];
}
case 'enemy_1500_skulsr':
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 }), {
dealt(c, b, e2) {
if (e2.blockedBy) return; // 未被阻挡时会发射榴弹
// the grenade's splash: an area selection — a 隐匿 ally next to the target is spared (GitHub issue #32 item 6)
for (const u of targetAndArea(c.target, areaAllies(b, e2, c.target.x, c.target.y, 1))) {
if (u !== c.target) hurt(b, e2, u, e2.s.atk, 'phys');
b.applyStatus(u, 'defDown', { duration: SKULSR_DEFDOWN_DUR, source: e2, value: -(T(ab, 'defdown.def') ?? 0) });
}
},
}];
case 'enemy_1500_skulsr': {
// PRTS 碎骨 天赋: "不会攻击飞行单位"; below half HP ATK +50 %; "未被阻挡时发射榴弹对目标及其周围八格的我方单位造成相当于攻击力
// 26%的物理伤害,并令其在5秒内防御力下降50%" ("榴弹对主目标造成物理普通伤害,对溅射目标造成物理溅射伤害,可溅射飞行单位") — the
// 26 % / 5 s / 3×3 are PRTS's, the −50 % is the blackboard's defdown.def. A blocked attack is its plain melee hit. The
// splash is an area selection: a 隐匿 operator next to the target that does not block it is spared (GitHub issue #32
// item 6, DESIGN §22.12). Community report #14 (0.1.2): the grenade used to be a full attack + 100 % on 4 neighbours.
const dd = -(T(ab, 'defdown.def') ?? 0);
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 }), noAirTargets(),
splashAttack({ scale: SKULSR_GRENADE_SCALE, tiles: 1, fxKind: 'grenade',
onEach: (b, e2, u) => { if (dd) b.applyStatus(u, 'defDown', { duration: SKULSR_DEFDOWN_DUR, source: e2, value: dd }); } })];
}
case 'enemy_1502_crowns': {
const s = ab.sk.blink;
return [skill(s, (b, e2) => blinkForward(b, e2, s.bb.dist ?? 1.5), { sil: true, cond: (b, e2) => !!e2.blockedBy })];
@@ -2029,9 +2131,17 @@ function kitLeaderMisc(key, ab, e) {
}
case 'enemy_1539_reid': {
// 生命值降至一半以下时攻击力大幅度提升; 首次被击倒后重生 (Reborn.duration s), 恢复一半生命值;
// 【冲锋】 a unit within range_radius ahead ⇒ move speed ×(1+move_speed) for `duration` s
// 【冲锋】 (PRTS 技能 "仅自身未被阻挡且存在符合条件的可选目标时可触发:选择3.0半径内位于自身下个检查点前的后续路径上(包含自身当前
// 所在地块)的距离自身最近的我方单位,将其所在地块中心设置为自身下一检查点;技能使用成功后,自身移动速度+200%,持续4.5s"): a target
// it may select (targetsNear: no 隐匿 / 迷彩 / 起飞 / untargetable operator) within RUSH_RADIUS standing on its own path to
// the next checkpoint (its tile included) ⇒ move speed ×(1+move_speed) for `duration` s. It walks that path anyway, so
// the re-aim at the target's tile is not a separate move. Until 0.1.3: any unit within the blackboard's 1.5, 隐匿 / 迷彩
// ones included, off its path too.
const rush = ab.sk.Rush;
const prey = (b, e2) => targetsNear(b, e2, rush.bb.range_radius ?? 1.5, { ranged: false }).filter((u) => u.ground);
const prey = (b, e2) => {
const keys = pathKeysAhead(e2);
return targetsNear(b, e2, RUSH_RADIUS).filter((u) => u.ground && keys.has(u.tileR * COLS + u.tileC));
};
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'AtkUp.atk', 'atkup.atk') ?? 0 }),
reborn({ dur: T(ab, 'Reborn.duration') ?? 0, hpRatio: T(ab, 'Reborn.hp_ratio') ?? 0.5, invincible: T(ab, 'Reborn.invincible') ?? 0 }),
skill(rush, (b, e2) => {
@@ -2082,7 +2192,10 @@ function kitLeaderMisc(key, ab, e) {
// 部署费用回复速度减半,再部署时间加倍; 周期性地添加正比于自身最大生命值的伤害防护屏障. The halving is a map effect
// (PRTS “墓碑” "※文字描述中的削弱实际属于地图效果,不属于敌人本身的能力"): every ally, 隐匿 / airborne ones included
const s = ab.sk.refreshshield;
return [{
// its attack — PRTS “墓碑” 天赋: "自身造成的远程途径伤害的攻击倍率降低至40%" (the blackboard's atkdown.atk_scale), "未被阻挡时会
// 进行远程攻击,对目标及其周围八格内的所有我方单位造成物理伤害,不会攻击飞行单位" ("可溅射飞行单位"); blocked, a melee hit at
// 100 %. Until 0.1.3: a single-target hit at 100 % either way.
return [noAirTargets(), splashAttack({ scale: T(ab, 'atkdown.atk_scale') ?? 1, tiles: 1 }), {
iv: 0.5,
tick(b) {
for (const u of b.allies()) {
@@ -2184,15 +2297,23 @@ function kitUglyThing(ab) {
const run = { spd: T(ab, 'bird_run.move_speed') ?? 0, def: T(ab, 'bird_run.def'), res: T(ab, 'bird_run.magic_resistance'), dmg: T(ab, 'bird_run.damage') ?? 0 };
const P = { pilot: false };
return [
{ hitOut(c, b, e) { if (!P.pilot && c.dmg.isAttack && c.target === e.blockedBy) c.dmg.amount *= rage; } }, // 近战攻击造成更高伤害
// 远程攻击造成溅射伤害,近战攻击造成更高伤害 — PRTS 天赋: unblocked, a ranged attack on 1 non-flying unit within 2.5 hitting it
// and its 8 surrounding tiles for 100 % ATK ("可溅射飞行单位"); blocked, a melee hit at mcmstr_rage_attack.atk_scale
{ spawn(b, e) { e.base.rangeRadius = Math.max(e.base.rangeRadius || 0, MCM_RANGE); e.profile.canTarget = (u) => !u.isFlying; } },
splashAttack({ scale: 1, meleeScale: rage, tiles: 1 }),
reborn({
dur: T(ab, 'reborn.duration') ?? 0,
onKo(b, e) { // 生命值降至0后自爆,造成大范围晕眩 (an area selection)
b.fx('explode', { x: e.x, y: e.y, r: MCM_BOMB_RADIUS, kind: 'mechBomb', id: e.id });
for (const u of areaAllies(b, e, e.x, e.y, MCM_BOMB_RADIUS)) {
hurt(b, e, u, e.s.atk * (bomb.atk_scale ?? 0), 'phys');
if (u.alive && bomb.stun > 0) b.applyStatus(u, 'stun', { duration: bomb.stun, source: e });
}
onKo(b, e) { // 生命值降至0后自爆,造成大范围晕眩 (an area selection), 2.17 s into the 重生
const x = e.x, y = e.y;
b.fx('telegraph', { x, y, r: MCM_BOMB_RADIUS, dur: MCM_BOMB_DELAY, kind: 'mechBomb', id: e.id });
b.after(MCM_BOMB_DELAY, () => {
if (!e.alive || e.removed) return;
b.fx('explode', { x, y, r: MCM_BOMB_RADIUS, kind: 'mechBomb', id: e.id });
for (const u of areaAllies(b, e, x, y, MCM_BOMB_RADIUS)) {
hurt(b, e, u, e.s.atk * (bomb.atk_scale ?? 0), 'phys');
if (u.alive && bomb.stun > 0) b.applyStatus(u, 'stun', { duration: bomb.stun, source: e });
}
}, { owner: e });
},
onReborn(b, e) { // 大祭司形态: 不进行攻击,无法被阻挡,每秒受到真实伤害
P.pilot = true;
@@ -2342,38 +2463,68 @@ function kitMace(ab) {
})];
}
/** 扎罗,“狼之主” · first form: damage taken −30 %, not stunnable, 【溶血骇惧】 (3 units: ASPD −70, accelerating HP loss,
* cured once 扎罗 loses hp_ratio of its HP); KO ⇒ 【远古威慑】 rebirth (Reborn.duration s: ASPD down around it while its HP
* refills) ⇒ second form: ranged double hits, ATK/ASPD up, invincible at first. 血债账款 / 【狂暴怒嗥】 absent. */
/** 扎罗,“狼之主” · first form: damage taken −30 %, not stunnable, 【溶血骇惧】; KO ⇒ a Reborn.duration s 重生 (its HP refills,
* 【远古威慑】 slows the units around it) ⇒ second form: ranged double hits within WOLF_RANGE, ATK/ASPD up, invincible at first,
* 【远古威慑】 still on. 【溶血骇惧】 (PRTS 技能 "使场上最多3名我方单位攻击速度-70,获得无法撤退,逐渐流失生命(从0/秒开始线性递增,在
* 40秒后达到最大流失速度30%最大生命值/秒),持续时间无限 … 场上存在被此技能影响的单位时狼之主获得静默 … 释放此技能时狼之主记录自身当前
* 生命值,累计损失20%生命值后解除场上全部我方单位的技能效果"): the FearCage blackboard's attack_speed, hp_ratio (the loss rate
* reached at duration_bleed s), hp_ratio_offset (the cure); its SP (the cast's cooldown, spCost s at +1/s [ASSUMED]) stands
* still while a unit is caught (静默), each ended effect gives back sp, and the last one starts duration_wait s more of 静默
* ("每结束1个技能效果狼之主回复5SP,场上技能效果全部结束时狼之主获得7s静默"). Until 0.1.3 the loss peaked at 5 % [ASSUMED], the buff
* ended after 40 s, hp_ratio (30 %) was read as the cure and the cast came every spCost s whatever was caught. Not modelled:
* 血债账款 / 【狂暴怒嗥】. */
function kitWolfLord(ab) {
const fc = ab.sk.FearCage;
const P = { form2: false };
const P = { form2: false, caught: 0, waitUntil: -Infinity, cast: null };
const caught = (b) => b.allies().some((u) => u.findBuff('ab:fearCage'));
const cure = (b) => { for (const u of b.allies()) { const d = u.findBuff('ab:fearCage'); if (d) b.removeBuff(u, d); } };
// 【远古威慑】 (a buff aura, auraAllies): no 隐匿 operator that does not block it, an airborne 起飞 one still — as 深池伙友卫队's
const awe = (b, e) => { for (const u of auraAllies(b, e, e.x, e.y, WOLF_AWE_RADIUS)) auraBuff(b, u, 'ab:ancientAwe', 0.2, { aspd: WOLF_AWE_ASPD }, null, true); };
return [{
spawn(b, e, a, ab2) {
ab2.immune = new Set([...(ab2.immune || []), 'stun']); // 第一形态: 不可被晕眩
b.addBuff(e, { key: 'ab:wolfGuard', persist: true, visible: true, mods: { dmgTakenMul: 1 - (T(ab, 'Passive.damage_resistance') ?? 0) } });
},
iv: 0.25,
tick(b, e) { // afflicted units recover once 扎罗 lost hp_ratio since the cast
iv: 0.1,
tick(b, e, a, dt) {
if (P.form2) { awe(b, e); return; } // 第二形态: 【远古威慑】生效
if (!fc) return;
// afflicted units recover once 扎罗 lost hp_ratio_offset of its HP since the cast
const cut = Math.abs(fc.bb.hp_ratio_offset ?? 0.2);
let n = 0;
for (const u of b.allies()) {
const d = u.findBuff('ab:fearCage');
if (d && d.data.src === e && e.hpRatio <= d.data.hp0 - (fc.bb.hp_ratio ?? 0) + 1e-9) b.removeBuff(u, d);
if (!d || d.data.src !== e) continue;
if (e.hpRatio <= d.data.hp0 - cut + 1e-9) b.removeBuff(u, d); else n++;
}
if (P.cast) {
if (n < P.caught) { // ended effects (cured, knocked out): +sp each
P.cast.left -= (fc.bb.sp ?? 0) * (P.caught - n);
if (n === 0) P.waitUntil = b.time + (fc.bb.duration_wait ?? 0);
}
if (n > 0 || b.time < P.waitUntil) P.cast.left += dt; // 静默: its SP stands still
}
P.caught = n;
},
},
// 【溶血骇惧】 every spCost s (enemy SP +1/s [ASSUMED]), first form only
skill(fc, (b, e) => {
// 【溶血骇惧】 every spCost s (enemy SP +1/s [ASSUMED]), first form only, never during its 静默
P.cast = skill(fc, (b, e) => {
const l = b.rng.shuffle(allTargets(b, e).filter((u) => u.kind === 'op' && !u.findBuff('ab:fearCage'))).slice(0, fc.bb.max_target ?? 3);
const dur = fc.bb.duration_bleed ?? 0;
const ramp = Math.max(1, fc.bb.duration_bleed ?? 40), peak = fc.bb.hp_ratio ?? 0;
for (const u of l) {
b.fx('beam', { x: e.x, y: e.y, from: e.id, to: u.id, kind: 'fearCage' });
b.addBuff(u, { key: 'ab:fearCage', duration: dur, refresh: 'replace', visible: true, interval: 1, data: { src: e, hp0: e.hpRatio, t0: b.time },
b.addBuff(u, { key: 'ab:fearCage', refresh: 'replace', visible: true, interval: 1, data: { src: e, hp0: e.hpRatio, t0: b.time },
mods: { aspd: fc.bb.attack_speed ?? 0 },
onTick: ({ battle, unit, buff }) => { const k = Math.min(1, (battle.time - buff.data.t0) / Math.max(1, dur)); battle.loseHp(unit, unit.s.maxHp * WOLF_BLEED_PEAK * k, { source: e }); } });
onTick: ({ battle, unit, buff }) => {
// its 扎罗 gone from the field without a 重生 (leaked, removed): the effect ends, no loss this tick [ASSUMED — no
// official text; the 重生's onKo cure covers its knock-out]. Until 0.1.3 a leak left it draining for good.
const src = buff.data.src;
if (!src || !src.alive || src.removed) { battle.removeBuff(unit, buff); return; }
const k = Math.min(1, (battle.time - buff.data.t0) / ramp);
battle.loseHp(unit, unit.s.maxHp * peak * k, { source: e });
} });
}
}, { cd: fc ? fc.sp || fc.cd : null, icd: fc ? fc.sp || fc.icd : null, cond: (b, e) => !P.form2 && allTargets(b, e).some((u) => u.kind === 'op') }),
}, { cd: fc ? fc.sp || fc.cd : null, icd: fc ? fc.sp || fc.icd : null, cond: (b, e) => !P.form2 && !caught(b) && b.time >= P.waitUntil && allTargets(b, e).some((u) => u.kind === 'op') }),
doubleHit(() => P.form2), // 攻击变为远程二连击
reborn({
dur: T(ab, 'Reborn.duration') ?? 0, invincible: T(ab, 'Passive2.invincible_time') ?? 0,
@@ -2382,11 +2533,10 @@ function kitWolfLord(ab) {
b.removeBuff(e, 'ab:wolfGuard');
if (e.mem.ab.immune) e.mem.ab.immune.delete('stun');
},
during(b, e, a, t) { // 【远古威慑】: nearby operators slowed; its HP refills slowly
during(b, e, a, t) { // 重生期间【远古威慑】生效; its HP refills slowly
const dur = T(ab, 'Reborn.duration') ?? 1;
e.hp = Math.max(1, e.s.maxHp * Math.min(1, t / Math.max(TICK, dur)));
// a buff aura (auraAllies): no 隐匿 operator that does not block it, an airborne 起飞 one still — as 深池伙友卫队's
for (const u of auraAllies(b, e, e.x, e.y, WOLF_AWE_RADIUS)) auraBuff(b, u, 'ab:ancientAwe', 0.2, { aspd: WOLF_AWE_ASPD }, null, true);
awe(b, e);
},
onReborn(b, e) {
P.form2 = true;
@@ -2606,9 +2756,13 @@ export const KITS = Object.freeze({
else cloud(e.x, e.y);
},
}],
enemy_10122_uacann_2: (ab) => [{ // 集团军重型火炮 · each hit leaves a burning zone
// 集团军重型火炮 · PRTS 天赋: "不会攻击飞行单位"; the shell hits every unit within 1.0 of its target for 100 % ATK ("此弹道会强制
// 击中主目标,碰撞无视迷彩,不可对空"; no splash until 0.1.3) and each hit on the main target leaves a 3 s 【燃烧区域】 of radius
// projectile_range × UACANN_ZONE_SCALE
enemy_10122_uacann_2: (ab) => [noAirTargets(), splashAttack({ unblocked: false, radius: SHELL_SPLASH_RADIUS, noAir: true, fxKind: 'shell' }), {
dealt(c, b, e) {
dmgZone(b, e, c.target.x, c.target.y, T(ab, 'ProjectileBoomRange.attack@projectile_range') ?? 1, T(ab, 'ProjectileBoomRange.attack@projectile_life_time') ?? 0, 1, T(ab, 'ProjectileBoomRange.attack@value') ?? 0, 'arts', 'burning');
// 【燃烧区域】 "不可对空": a flying ally (the 炎佑 dragon) standing in it takes nothing (until 0.1.3 it burned)
dmgZone(b, e, c.target.x, c.target.y, (T(ab, 'ProjectileBoomRange.attack@projectile_range') ?? 1) * UACANN_ZONE_SCALE, T(ab, 'ProjectileBoomRange.attack@projectile_life_time') ?? 0, 1, T(ab, 'ProjectileBoomRange.attack@value') ?? 0, 'arts', 'burning', null, 0, { noAir: true });
},
}],
enemy_10054_cjhot: (ab, e) => [immuneTo('sluggish'), { // 鼎沸 · pulses arts + burn around itself; immune 停顿
+24 -6
View File
@@ -399,8 +399,9 @@ const KITS = {
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range;
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
// S1 仗义疏财 (passive, 2 coins): an attack spends a coin to heal the most injured ally (< 70 % HP) of the 8
// surrounding tiles for attack@heal_scale × ATK. S3 千金一掷 (持续时间无限): attacks hit twice, kills give a coin;
// 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
// 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:
// the RandomGold ability (Skill_3_End) carries swire2_s_3[knockback] of template knockback[relative]; 地面敌方单位,
@@ -448,10 +449,19 @@ const KITS = {
},
});
const healRatio = textNum(d.skill?.description, /血量不足(\d+)%/, 70) / 100;
const installS1 = (battle, unit) => { // 仗义疏财
/**
* 仗义疏财 — 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
* 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).
*/
const installS1 = (battle, unit) => {
const hs = num(bb['attack@heal_scale'], num(bb.heal_scale, 0));
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0)) return;
let nextAt = -Infinity;
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;
@@ -459,11 +469,19 @@ const KITS = {
if (Math.max(Math.abs(a.tileR - unit.tileR), Math.abs(a.tileC - unit.tileC)) !== 1) continue; // 周围八格
if (!best || a.hpRatio < best.hpRatio || (a.hpRatio === best.hpRatio && a.deploySeq < best.deploySeq)) best = a;
}
return best;
};
const tryHeal = () => {
if (!alive(unit) || (unit.mem.coins ?? 0) < coinCost || !(hs > 0) || battle.time < nextAt - 1e-9) return;
const best = healTarget();
if (!best) return;
unit.mem.coins -= coinCost;
nextAt = battle.time + unit.s.interval;
battle.heal(unit, best, unit.s.atk * hs);
fx(battle, 'coin', unit, { n: unit.mem.coins, heal: best.id, skill: 'swire2_1' });
}, { owner: unit, priority: -10 });
};
battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 });
battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit });
};
const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币"
battle.on('kill', (ctx) => {
+22 -7
View File
@@ -73,8 +73,9 @@
// 6_01 S2 the aim (disarmed meanwhile) spends its bullet when it begins; target = a wanted enemy (lowest DEF first).
// 6_02 S1 casts with an enemy in her range (data SEARCH read as "search in range" [ASSUMED]; the engine SEARCH =
// any enemy on the field wasted both charges); the snow spreads along her facing line (≤ trig_cnt tiles);
// S2 "目标点变为冻结状态" = the frozen 保护目标 token (icetgt, one at a time) on the tile reaching max snow, whose
// snow is used up; spreads go to the thinnest 4-neighbour; the 20 % DoT ticks once per second.
// S2 "目标点变为冻结状态" = the frozen 保护目标 token (icetgt, one at a time) on a protection point (goal tile) reaching
// max snow, deploy attributes ignored, whose snow is used up (PRTS 备注; until 0.1.3 any standable tile froze, never the
// gate); spreads go to the thinnest 4-neighbour; the 20 % DoT ticks once per second.
// 6_03 S2 teleport = the ground-reachable (grid path) enemies of the skill grid moved onto his tile, in the cast's
// tick (PRTS: 0.13 s after the damage), leaders included unless 自缚.
// 6_04 S1 transfer: the ally takes ×(1 − share), she takes the rest as true damage from the attacker; module 新生代
@@ -612,16 +613,20 @@ function sbell2(bb, chess, def) {
/**
* S2 霜涛覆岭 while it runs (`unit.mem.sbellS2`): a layer landing on a tile already at max snow spreads one layer to
* a neighbouring ground tile (at most max_cast_tile_count spreads per activation); ground enemies on snow take
* s2_magic_scale × ATK arts per second; an enemy leaving snow gets `cold` s of 寒冷; a tile reaching max snow turns
* into the frozen 保护目标 (token icetgt: blocks 3, one at a time) and its snow is used up.
* s2_magic_scale × ATK arts per second; an enemy leaving snow gets `cold` s of 寒冷; "积雪在目标点积累至5层时,使目标点变为
* 冻结状态": a protection point (the blue gate) reaching max snow turns into the frozen 保护目标 (freezeTile).
*/
const addSnow = (battle, unit, k) => {
const snow = unit.mem.snow;
if (!snow) return false;
const r = (k / COLS) | 0, c = k % COLS;
if (!battle.grid.inRect(r, c) || !battle.grid.groundPassable(r, c, true)) return false;
// PRTS 备注 "存在自身的该召唤物的地块不会积雪"
if (battle.allyUnits.some((t) => isTok(t, iceId, unit) && t.alive && t.tileR === r && t.tileC === c)) return false;
const S2 = unit.mem.sbellS2;
const cur = snow.get(k) ?? 0;
// a protection point already at max snow (its freeze waited for her other token to go) freezes on its next layer
if (S2 && cur >= maxL && freezeTile(battle, unit, k)) return true;
if (cur >= maxL) {
if (!S2 || S2.spreadLeft <= 0) return false;
// "积雪超过5层时会向周围扩散一层": the thinnest neighbouring ground tile gets the layer
@@ -640,14 +645,24 @@ function sbell2(bb, chess, def) {
if (S2 && cur + 1 >= maxL) freezeTile(battle, unit, k);
return true;
};
/**
* "使目标点变为冻结状态" — PRTS 圣聆初雪 S2 备注: "目标点冻结的实际效果为令圣聆初雪在该地块上召唤一个保护目标(冻结状态)(无视部署
* 属性),并去除相应地块上的积雪", "可以被'变为冻结状态'的目标点包括常规的保护目标点与促融共竞的保护目标点"; the token's page:
* "技能发动后于保护目标叠加5层积雪". So only a protection point (a goal tile, glyph E) freezes, whatever its deploy attributes
* (nobody may stand on a gate): her token icetgt (blocks 3; one at a time, maxDeployCount 1) appears on it and blocks the
* enemies walking in, and that tile's snow is used up. Until 0.1.3 (community report #32): any free tile a melee
* operator could stand on froze at max snow, and the gate never did. Returns true when the token appeared.
*/
const freezeTile = (battle, unit, k) => {
const r = (k / COLS) | 0, c = k % COLS;
if (battle.allyUnits.some((t) => isTok(t, iceId, unit) && t.alive)) return;
if (battle.isReservedTile(r, c) || !battle.grid.canStand(r, c, { ranged: false })) return;
if (battle.grid.tile(r, c).special !== 'end') return false;
if (battle.allyUnits.some((t) => isTok(t, iceId, unit) && t.alive)) return false;
if (battle.isReservedTile(r, c)) return false;
const ice = battle.spawnToken(unit, iceId, r, c);
if (!ice) return;
if (!ice) return false;
unit.mem.snow.delete(k);
battle.fx('summon', { x: c, y: r, id: ice.id, src: unit.id });
return true;
};
const talents = [
{ install(battle, unit) { // 无垠的雪景 (+ S2 snow rules)