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
@@ -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() })),
|
||||
};
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user