0.2.5-dev: community round 30 and follow-ups
- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
This commit is contained in:
1 parent
bc50cac01f
commit
221e6b8dd2
134 files changed
+4590
-1201
No files matched your search
+13
-2
@@ -171,6 +171,16 @@ export function acquireTargets(b, u, prof) {
|
||||
if (u.blocking.length) for (const e of b.blockedTargets(u, prof)) if (!cands.includes(e)) cands.push(e);
|
||||
// an ally target (Battle.setAllyTarget: 白铁's 铁钳号·原型机, an enemy-camp summon with 嘲讽等级 −2) after every enemy
|
||||
const extra = b._allyTargets && b._allyTargets.size ? b.allyTargetsInKeys(u.rangeKeys, u) : null;
|
||||
// Fortress has an extra melee tile even against enemies it cannot block (PRTS branch / 号角). Its ordinary
|
||||
// ranged targets still take priority; this does not fill the other tiles in its ranged blind spot.
|
||||
if (prof.fortress && !cands.length && !extra?.length) {
|
||||
const here = [u.tileR * COLS + u.tileC];
|
||||
const near = b.enemiesInKeys(here, u, prof);
|
||||
if (!near.length && b._allyTargets?.size) near.push(...b.allyTargetsInKeys(here, u));
|
||||
prof._fortressMelee = near.length > 0;
|
||||
sortEnemyTargets(b, u, near, prof.priority);
|
||||
return near.slice(0, 1);
|
||||
}
|
||||
if (extra && extra.length) {
|
||||
if (prof.allInRange) return cands.concat(extra);
|
||||
sortEnemyTargets(b, u, cands, prof.priority);
|
||||
@@ -225,7 +235,8 @@ export function performAttack(b, u, prof, targets, opts = null) {
|
||||
const t = targets[i];
|
||||
b._ev(['atk', u.id, t.id, vis]);
|
||||
if (isHeal) { doHeal(b, u, prof, t); continue; }
|
||||
const info = { isSkill, index: i, attackId, energy: i === 0 ? energy : 0 };
|
||||
// Target acquisition mutates the fortress profile. Keep this shot's mode while its shell is in flight.
|
||||
const info = { isSkill, index: i, attackId, fortressMelee: !!prof._fortressMelee, energy: i === 0 ? energy : 0 };
|
||||
const foe = t.side === 'enemy' || b.isAllyTarget(t);
|
||||
if (ranged && foe && prof.projectile === 'boomerang') {
|
||||
throwBoomerang(b, u, prof, t, info);
|
||||
@@ -323,7 +334,7 @@ export function resolveHit(b, u, prof, target, info, x, y) {
|
||||
x = target.x; y = target.y;
|
||||
}
|
||||
// splash: a 中点判定 radius around the target's position — a huge enemy counts by its 判定中心 (Battle.enemiesInRadius)
|
||||
if (prof.splashRadius > 0) {
|
||||
if (prof.splashRadius > 0 && (!info.fortressMelee || prof.fortressMeleeSplash)) {
|
||||
const r = prof.splashRadius;
|
||||
const sc = prof.splashScale ?? 1;
|
||||
for (const e of b.foesInRadius(x, y, r, true)) {
|
||||
|
||||
@@ -46,13 +46,15 @@ export class BattleDeploy {
|
||||
u.markDirty();
|
||||
u.hp = u.s.maxHp;
|
||||
// 联防 carry (PRTS 卫戍协议/帮助 §联防阶段 "将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力…)"):
|
||||
// an operator's HP ratio here, the SP in skill.reset and again after the `deploy` hook (below) — a summon's SP only
|
||||
// an operator restores HP/SP, a summon SP only; actual 联防 opening restoration is deferred until Battle.start ends
|
||||
const cs = initial || (first && this._startDeploying) ? (carry ?? u.carry) : null;
|
||||
if (cs && u.kind === 'op' && Number.isFinite(cs.hpPct)) u.hp = Math.max(1, u.s.maxHp * Math.max(0.01, Math.min(1, cs.hpPct)));
|
||||
// 联防 reconstructs deployment/battle-start effects first, then restores HP/SP once in Battle.start.
|
||||
const restoreLater = this.kind === 'unite' && this._startDeploying;
|
||||
if (!restoreLater && cs && u.kind === 'op' && Number.isFinite(cs.hpPct) && cs.hpPct > 0) u.hp = u.s.maxHp * Math.min(1, cs.hpPct);
|
||||
this._occ[k] = u;
|
||||
this._refreshRange(u); // also rebuilds baseRangeKeys (initial range incl. permanent rangeExtend)
|
||||
if (!u.skill) this._setupUnit(u);
|
||||
u.skill.reset(cs);
|
||||
u.skill.reset(restoreLater ? null : cs);
|
||||
const sk = u.skill;
|
||||
// "自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位将进入冷却" (PRTS 卫戍协议/帮助; skills.js)
|
||||
if (initial) sk.opReadyAt = this.time + this.flags.startOpCooldown;
|
||||
@@ -68,9 +70,9 @@ export class BattleDeploy {
|
||||
if (first) this._ev(['spawn', unitInfo(u)]);
|
||||
this._ev(['deploy', u.id]);
|
||||
if (this._hooks.deploy) this.emit('deploy', { unit: u, initial });
|
||||
// 联防: "部署完成后,将对应单位的…技力修改至与上一阶段结束时相同" — the carried SP is set again once the deployment is
|
||||
// done, so a deploy-time SP gift (独行, 黄沙罗盘 …) does not come on top of it; later redeploys keep those gifts
|
||||
if (cs && Number.isFinite(cs.sp) && u.alive && u.skill) u.skill.setSpTotal(cs.sp);
|
||||
// Explicit carry outside the 联防 opening follows this one deployment. The opening itself restores at the end
|
||||
// of Battle.start, so deployment/battle-start gifts do not come on top of it; later redeploys keep their gifts.
|
||||
if (!restoreLater && cs && Number.isFinite(cs.sp) && u.alive && u.skill) u.skill.setSpTotal(cs.sp);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,15 @@ export class BattleLifecycle {
|
||||
}
|
||||
}
|
||||
this.emit('battleStart', {});
|
||||
// 联防 restores the previous phase's state after all opening effects have established the new stats. In
|
||||
// particular, Egir rebuilds devour attributes without charging carried targets for its opening damage again.
|
||||
// Keep fractional HP: a legal survivor below 1% (or below 1 HP after max-HP changes) must not be healed by carry.
|
||||
if (this.kind === 'unite') for (const u of this.allyUnits) {
|
||||
const cs = u.carry;
|
||||
if (!cs || cs.down || !u.alive || !u.deployed || u.deploySeq <= seq0) continue;
|
||||
if (u.kind === 'op' && Number.isFinite(cs.hpPct) && cs.hpPct > 0) u.hp = u.s.maxHp * Math.min(1, cs.hpPct);
|
||||
if (Number.isFinite(cs.sp) && u.skill) u.skill.setSpTotal(cs.sp);
|
||||
}
|
||||
// redeploy-time effects that start with the battle (机变 征召 "所有干员的再部署时间-50%", added by a battleStart
|
||||
// handler) cover the operators forced out above too: their timer is re-read with them, as a later knock-out's is
|
||||
for (const u of this.allyUnits) {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
// elite or not) stand on the field get ATK +atk
|
||||
// act2autochess_band3_buff 伊奥莱塔 统御号令 at combat start n = elite operators on the field ⇒ every elite operator
|
||||
// ATK +atk_per_cnt × n, HP +max_hp_per_cnt × n (all 直接乘算)
|
||||
// auto_chess_change_map Touch 外勤医疗 "所有玩家场地上出现一名<预备干员-医疗>": while any alive player of the match
|
||||
// holds band_amedic, every player's field gets the map character of the
|
||||
// auto_chess_change_map Touch 外勤医疗 "所有玩家场地上出现一名<预备干员-医疗>": once selected in this match, its
|
||||
// retained team grant gives every player's field the map character of the
|
||||
// variant whose common_condition holds for that player's elites on the field
|
||||
// (condition_golden_chess_le cnt → 预备干员-医疗, _ge cnt → Touch), spawned at
|
||||
// combat start on its stage slot (tokens.spawnMapChar)
|
||||
|
||||
@@ -457,7 +457,11 @@ function devour(battle, pid, bb, members) {
|
||||
// start, resolves nothing (`knocked`: it is off the field)
|
||||
if (knocked(t)) continue;
|
||||
S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id });
|
||||
if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] });
|
||||
// The maintainer's 2026-10-11 联防 carry decision: reconstruct marks/attributes, but do not apply a second
|
||||
// opening HP loss (or trigger deaths/revives) to an operator restored from the previous field. Battle.start
|
||||
// applies its exact HP ratio and SP after the opening hooks. Fresh units and all other battle kinds still pay.
|
||||
const restored = battle.kind === 'unite' && Number.isFinite(t.carry?.hpPct) && t.carry.hpPct > 0;
|
||||
if (amount > 0 && !restored) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] });
|
||||
if (!layered.has(t)) {
|
||||
layered.add(t);
|
||||
S.gainLayers(battle, { playerId: pid, bonds: 'egirShip', n: S.tierOf(t), source: m, reason: 'bond' });
|
||||
|
||||
@@ -337,10 +337,11 @@ function rebirthCleanse(b, e) {
|
||||
* (no move / attack / skill).
|
||||
* `onKo(b, e, a)` runs at once (self-destruct, freeing prisoners …), `during(b, e, a, elapsed)` every tick of it; then the
|
||||
* enemy stands up with `hpRatio` of its max HP, `onReborn(b, e, a)` switches the form and `invincible` s of 无敌 follow.
|
||||
* `invincibleUntargetable` opts a verified enemy into hostile selection immunity for that same window (Xi, #510).
|
||||
* `a.state`: undefined → 'reborn' → 'form2'. The 'telegraph' fx of the knock-out carries `form: 'reborn'` and the 'revive'
|
||||
* fx `form: 'form2'`: models with those clip sets (render/units.js FORMS — 锏, 扎罗, “复仇者”, 杰斯顿) play them.
|
||||
*/
|
||||
function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = null, onReborn = null, key = 'ab:reborn' } = {}) {
|
||||
function reborn({ dur = 0, hpRatio = 1, invincible = 0, invincibleUntargetable = false, onKo = null, during = null, onReborn = null, key = 'ab:reborn' } = {}) {
|
||||
const finish = (b, e, a) => {
|
||||
if (!e.alive || a.state !== 'reborn') return;
|
||||
a.state = 'form2';
|
||||
@@ -348,7 +349,8 @@ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = nu
|
||||
e.profile.noAttack = a.noAtk;
|
||||
if (onReborn) safe(b, e, () => onReborn(b, e, a));
|
||||
e.hp = Math.max(1, e.s.maxHp * Math.max(0, Math.min(1, hpRatio)));
|
||||
if (invincible > 0) b.addBuff(e, { key: `${key}:inv`, duration: invincible, visible: true, flags: { invulnerable: true } });
|
||||
if (invincible > 0) b.addBuff(e, { key: `${key}:inv`, duration: invincible, visible: true,
|
||||
flags: { invulnerable: true, ...(invincibleUntargetable ? { untargetable: true } : {}) } });
|
||||
if (e.route) e.route.pts = null;
|
||||
e.atkCd = 0;
|
||||
rebirthCooldowns(e);
|
||||
|
||||
@@ -473,6 +473,9 @@ function kitXi(ab) {
|
||||
};
|
||||
const re = reborn({
|
||||
dur: T(ab, 'reborn.duration') ?? 0, invincible: T(ab, 'reborn.invincible') ?? 0,
|
||||
// #510: Xi's post-rebirth 无敌 also prevents hostile selection (PRTS “自在” / 无敌).
|
||||
// Explicit opt-in: the engine's general invulnerable flag remains damage protection only.
|
||||
invincibleUntargetable: true,
|
||||
onKo(b, e) { b.removeBuff(e, 'ab:xiShield'); },
|
||||
onReborn(b, e) {
|
||||
P.form2 = true;
|
||||
|
||||
@@ -412,7 +412,9 @@ export const SPECIAL_KITS = Object.freeze({
|
||||
// report of 2026-10-06, item 24); it picks by 仇恨值, not its blocker first
|
||||
spawn(b, e) { e.profile.blockFree = true; },
|
||||
dealt(c, b, e) {
|
||||
const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0;
|
||||
// PRTS 自制投石机: normal stones splash in radius 1.5. The talent's projectile_range (0.5) belongs to
|
||||
// the separate 入戏 random-offset attack, not its ordinary volley in this mode.
|
||||
const n = T(ab, 'Attack.attack@times') ?? 1, r = 1.5;
|
||||
const t = c.target;
|
||||
for (let i = 1; i < n; i++) hurt(b, e, t, e.s.atk, 'phys');
|
||||
// the stone's splash ("碰撞无视迷彩"): an area selection — no unblocking 隐匿 ally
|
||||
|
||||
@@ -239,7 +239,7 @@ H.SERVER_GAIN_EQUIP = {
|
||||
run(ctx) {
|
||||
const { bb, bbStr } = ctx.source;
|
||||
if (!ctx.gd.item(bbStr.chess)) return;
|
||||
for (let i = 0; i < Math.min(10, num(bb.count, 1)); i++) ctx.grantItem(bbStr.chess);
|
||||
for (let i = 0; i < Math.min(10, num(bb.count, 1)); i++) ctx.grantItem(bbStr.chess, { requirePool: false });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
// 0.2.0 both revived in place (`fatal` savers at −101 / −110): no 被击倒时 / 部署时 effect fired (community report
|
||||
// 「像砾和瑕光这种死亡和部署的叠层效果,如果有艾芒加德的3次复活似乎是无法触发」).
|
||||
// 骑士戒律's free in-skill undying runs early (20). Flat damage reduction 'hit' −10 (after the other damage modifiers).
|
||||
// Proc damage dealt by items carries the tag 'item' and never re-triggers item procs.
|
||||
// Proc damage dealt by items carries the tag 'item'; damage-producing item riders exclude it to avoid recursion.
|
||||
//
|
||||
// 蒸汽之心 (Victoria carrier) — LIVE model: the hammer types (灼燃/坚固/加速/战栗维式重锤) carried by the owner's operators
|
||||
// that are on the field right now (cached ≤ 0.5 s, dropped whenever an operator deploys or leaves) are granted to the
|
||||
@@ -389,6 +389,15 @@ function onAttackEnemies(S, u, fn) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Actual carrier damage to enemies, including DoT/skill/splash. Element gauge fill and HP loss are not damage. */
|
||||
function onDamageEnemies(S, u, fn) {
|
||||
S.on('damaged', (c) => {
|
||||
if (c.source !== u || c.type === 'element' || isHpLoss(c.dmg) || !(c.amount > 0)
|
||||
|| !c.target || c.target.side !== 'enemy' || !hasHp(c.target)) return;
|
||||
fn(c.target, c);
|
||||
});
|
||||
}
|
||||
|
||||
/** Generic buff-key behaviours (shared by several items). */
|
||||
const BY_BUFF = {
|
||||
// 源石溶剂: the text's "每秒流失 damage 点生命值" is officially 每秒受到 damage 点真实伤害 (PRTS 盟约记录 修正 "并非流失", 备注
|
||||
@@ -636,18 +645,19 @@ const BY_ITEM = {
|
||||
fxOn(battle, 'revive', u, 'item:chess_item_4_12_e', rec.id, { left: max - used });
|
||||
}, PRIO_RESPAWN); // a knock-out: after every 不死 (the hammer's lock), whatever the equip order (header)
|
||||
},
|
||||
// 催泪瓦斯: on attack prob ⇒ 1 麻痹 stack
|
||||
// PRTS 盟约记录 corrects both items' "攻击时" to "造成伤害时" (#489); no attack event is required.
|
||||
// 催泪瓦斯: on damage prob ⇒ 1 麻痹 stack
|
||||
chess_item_5_01_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1autochess_equip_acarm058_global_buff');
|
||||
const pr = p ? num(p.prob) : 0;
|
||||
if (pr > 0) onAttackEnemies(S, u, (t) => { if (chance(battle, pr)) battle.applyStatus(t, 'palsy', { value: 1, source: u }); });
|
||||
if (pr > 0) onDamageEnemies(S, u, (t) => { if (chance(battle, pr)) battle.applyStatus(t, 'palsy', { value: 1, source: u }); });
|
||||
},
|
||||
// 谢拉格不融冰: on attack prob ⇒ 寒冷 `cold` s
|
||||
// 谢拉格不融冰: on damage prob ⇒ 寒冷 `cold` s
|
||||
chess_item_5_02_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1vautochess_equip_acarm003_global_buff');
|
||||
if (!p) return;
|
||||
const pr = num(p.prob), d = num(p.cold);
|
||||
if (pr > 0 && d > 0) onAttackEnemies(S, u, (t) => { if (chance(battle, pr)) battle.applyStatus(t, 'cold', { duration: d, source: u }); });
|
||||
if (pr > 0 && d > 0) onDamageEnemies(S, u, (t) => { if (chance(battle, pr)) battle.applyStatus(t, 'cold', { duration: d, source: u }); });
|
||||
},
|
||||
// 双模机械臂: physical/arts damage becomes 弱点伤害 (whichever type the target resists less)
|
||||
chess_item_5_03_e(battle, u, rec, S) {
|
||||
|
||||
@@ -94,8 +94,9 @@ export default {
|
||||
battle.on('damaged', (ctx) => {
|
||||
const e = ctx.target;
|
||||
if (busy || ctx.source !== unit || e.side !== 'enemy' || ctx.type === 'element' || ctx.dmg?.tags?.includes('vulpisHunt')) return;
|
||||
const t = marks.get(e.id);
|
||||
if (t == null) { marks.set(e.id, battle.time); return; }
|
||||
let t = marks.get(e.id);
|
||||
// PRTS talent remarks: the first damage instance also receives the arts addition.
|
||||
if (t == null) { t = battle.time; marks.set(e.id, t); }
|
||||
if (battle.time - t <= num(t0.interval, 10) + 1e-9 && e.alive) {
|
||||
busy = true;
|
||||
// PRTS 备注 "伤害类型为法术附加伤害": tag addition (no 叙拉古 6 roll)
|
||||
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
}),
|
||||
skchr_horn_2: () => ({
|
||||
kind: 'ammo', ammo: s2Ammo,
|
||||
attack: { atkScale: num(bb['attack@s2.atk_scale'], 1) },
|
||||
attack: { atkScale: num(bb['attack@s2.atk_scale'], 1), fortressMeleeSplash: true },
|
||||
onStart({ unit }) { unit.mem.hornS2Over = false; },
|
||||
onEnd({ unit }) { unit.mem.hornS2Over = false; },
|
||||
}),
|
||||
|
||||
@@ -229,6 +229,8 @@ export default {
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'ammo',
|
||||
// The skill's initial attack area includes the eight surrounding tiles as well as its 3-9 grid.
|
||||
trigger: { rule: 'ACTIVE_RANGE', grid: g39x4 },
|
||||
ammo: Math.max(1, Math.floor(num(b3['attack@trigger_time'], 1))),
|
||||
mods: { atkPct: num(b3.atk) }, flags: LIFTOFF,
|
||||
targeting: s3Targeting,
|
||||
|
||||
@@ -222,7 +222,7 @@ export default {
|
||||
}]);
|
||||
}
|
||||
battle.on('elementBurst', ({ target, element }) => {
|
||||
if (element !== 'erosion' || !up(unit) || !unit.skill?.active || unit.skill.id !== S3
|
||||
if (element !== 'erosion' || target?.side !== 'enemy' || !up(unit) || !unit.skill?.active || unit.skill.id !== S3
|
||||
|| !canTargetEnemy(unit, target, GROUND)) return;
|
||||
unit.mem.clemntBombs?.marks.set(target, battle.time + num(b3.ep_break_duration, 1));
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
// 帮助 lists the 上半 row that did as removed in 下半, so in a fight the skill simply runs its `duration`.
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, up, byEnemyAttack } from '../shared/tier1.js';
|
||||
import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyInRadius } from '../../../body.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
@@ -256,6 +256,11 @@ export default {
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// S3 attacks air as well: its expanded auto-cast area must not inherit the inactive ground-only profile.
|
||||
if (unit.skill?.id === S3) unit.skill.addTriggerRange(() => [{
|
||||
keys: absoluteRangeKeys(skillRec(chess, S3)?.rangeGrid ?? X2, unit.tileR, unit.tileC, unit.dir, unit.s.baseRangeExtend || 0),
|
||||
profile: { canHitFly: true },
|
||||
}]);
|
||||
// S1 恶业苦果: every enemy damage instance she takes (the official hsgma2_s_1 on ON_TAKE_DAMAGE, not its attacks only:
|
||||
// tier1 byEnemyAttack) ⇒ atk_scale × ATK arts back to its source
|
||||
battle.on('damaged', (c) => {
|
||||
|
||||
@@ -294,6 +294,17 @@ export default {
|
||||
],
|
||||
install(battle, unit) {
|
||||
const id = unit.skill?.id;
|
||||
if (id === S2) {
|
||||
// The owner's larger-range auto-cast rule also covers this terrain-dependent range (no static rangeId).
|
||||
// Keep SEARCH when the actual running area does not strictly contain her initial range.
|
||||
unit.skill.addTriggerRange(() => {
|
||||
const extend = unit.s.baseRangeExtend || 0;
|
||||
const keys = absoluteRangeKeys(floodGrid(battle, unit), unit.tileR, unit.tileC, unit.dir, extend);
|
||||
const initial = absoluteRangeKeys(unit.def.rangeGrid, unit.tileR, unit.tileC, unit.dir, extend);
|
||||
const set = new Set(keys);
|
||||
return set.size > initial.length && initial.every(k => set.has(k)) ? [{ keys, profile: AIR }] : [];
|
||||
});
|
||||
}
|
||||
if (id === S1) {
|
||||
// 充能耗尽前特性不重置: the trait resets the ramp on skillEnd (priority 0) — keep it while a charge remains
|
||||
battle.on('skillEnd', (c) => {
|
||||
|
||||
@@ -129,7 +129,7 @@ export function contentInfo(battle, pid) {
|
||||
const ci = player(battle, pid)?.input?.contentInfo;
|
||||
return ci && typeof ci === 'object' ? ci : {};
|
||||
}
|
||||
/** Band ids of every alive player of the match (own first); falls back to this battle's players. */
|
||||
/** Alive players' bands (own first), plus a retained Touch team grant; falls back to this battle's players. */
|
||||
export function matchBands(battle, pid) {
|
||||
const mb = contentInfo(battle, pid).matchBands;
|
||||
if (Array.isArray(mb) && mb.length) return mb;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// handUnits pieces in the 整备区 (hand, 10 slots; operators, items and summon stacks),
|
||||
// handChess operators in the hand,
|
||||
// roundStats { refreshes, buys, sells, spent, gainedChess, arts } (this round's prep, ps.round),
|
||||
// matchBands band ids of every alive player (this player first),
|
||||
// matchBands band ids of every alive player (this player first), plus a retained Touch team grant,
|
||||
// teammates [{ playerId, bandId }] of the other alive players }
|
||||
// Global handlers run first in every dispatch, so band / bond / garrison / item handlers of the same onBattleStart can
|
||||
// read or extend `ev.input.contentInfo`.
|
||||
@@ -26,6 +26,10 @@ export function registerSupportMeta(registry) {
|
||||
const hand = ctx.hand();
|
||||
const rs = ctx.roundStats();
|
||||
const mates = ctx.teammates();
|
||||
const matchBands = [ctx.bandId(), ...mates.map(t => t.bandId())].filter(Boolean);
|
||||
// [ASSUMED] 外勤医疗 is a team grant for this match, not an aura revoked when its holder is eliminated.
|
||||
// Other team strategies and the teammate contexts still require a living player.
|
||||
if (ctx.teamMedicGranted?.() && !matchBands.includes('band_amedic')) matchBands.push('band_amedic');
|
||||
ev.input.contentInfo = {
|
||||
round: ctx.round,
|
||||
kind: ev.kind ?? null,
|
||||
@@ -37,7 +41,7 @@ export function registerSupportMeta(registry) {
|
||||
refreshes: rs.refreshes | 0, buys: rs.buys | 0, sells: rs.sells | 0, spent: rs.spent | 0,
|
||||
gainedChess: rs.gainedChess | 0, arts: rs.arts | 0,
|
||||
},
|
||||
matchBands: [ctx.bandId(), ...mates.map((t) => t.bandId())].filter(Boolean),
|
||||
matchBands,
|
||||
teammates: mates.map((t) => ({ playerId: t.playerId, bandId: t.bandId() })),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -493,6 +493,11 @@ export class SkillRuntime {
|
||||
|| (this.triggerEnemies && b.enemiesInKeys(keys, u, TRIGGER_PROFILE).length > 0);
|
||||
if (b.enemiesInKeys(keys, u, u.profile).length > 0 || this._allyTargetIn(keys)) return true;
|
||||
}
|
||||
if (!this.healSkill && u.profile?.fortress) {
|
||||
// The branch's own-tile melee range also satisfies the basic cast condition for unblockable fragments.
|
||||
const here = [u.tileR * COLS + u.tileC];
|
||||
if (b.enemiesInKeys(here, u, u.profile).length > 0 || this._allyTargetIn(here)) return true;
|
||||
}
|
||||
// the enemies a unit blocks are always its targets (Battle.blockedTargets), in range or not — PRTS 卫戍协议/帮助
|
||||
// "敌人被近战干员自身阻挡" satisfies the target condition of the basic strategy (a ranged blocker too: user playtest #6)
|
||||
if (!this.healSkill && u.blocking.length && b.blockedTargets(u, u.profile).length > 0) return true;
|
||||
|
||||
Reference in new issue
Block a user