fix: correct community-reported combat and strategy draft behavior
Guard lethal stuns against a replacement deployment; keep bard auras running through stun; require an active Aegir bond for Clementia's strategy; allow Compass SP to bypass recovery locks; and refresh Hidden Core gun mitigation against current gun/spring companions. Set multiplayer strategy picks to 50s and describe renderer fallback without diagnosing unsupported hardware. Golden moves (full 285-scenario regeneration): - roster-008: inactive Aegir no longer gains 18 strategy layers. - boss-boss_4-solo: bard regeneration continues through stun. - hidden-boss_9-pair: mitigation covers first hits/current companions. - standin-07: hammer stun cannot follow an immediate revival deployment. No ending-reason changes. Sources and report-by-report findings: DESIGN 28.25.
This commit is contained in:
1 parent
77cd29f07a
commit
708caf7d06
37 files changed
+277
-103
No files matched your search
@@ -568,7 +568,7 @@ const recJson = (rec) => { try { return rec ? JSON.stringify(rec) : ''; } catch
|
||||
/**
|
||||
* Bonds an IN_BATTLE layer gain of this player can name: its bond snapshot (every bond its lineup counts), plus bonds
|
||||
* its band, its effects (机变 cards, 驻守 …), its units and their items mention (content grants layers to those —
|
||||
* e.g. 克莱门莎's <阿戈尔>, requireActive: false). Anything else is a forged gain.
|
||||
* including referenced bonds outside the lineup snapshot). Anything else is a forged gain.
|
||||
*/
|
||||
function layerBondsOf(p, gd) {
|
||||
const out = new Set(Object.keys(p.bonds && typeof p.bonds === 'object' ? p.bonds : {}));
|
||||
|
||||
@@ -26,6 +26,6 @@ export const DELAYS = Object.freeze({
|
||||
* Seconds of one turn of the co-op strategy draft (user playtest #4 item 4: the old 12 s per turn — research 06 §724,
|
||||
* itself [ASSUMED] — inside the 50 s step was far too little and counted apart from the header's 50 s). [ASSUMED]: the
|
||||
* official data only gives the whole BAND_CHECK step (autoChessData.enterStepList: 50 s, hint 15 s); the turn clock is
|
||||
* the remake's. It is also the step's only countdown (m.public.deadline = draft.turnDeadline). × timerScale.
|
||||
* the remake's (50 s per turn per the owner's 2026-10-10 official-play report). It is also the step's only countdown (m.public.deadline = draft.turnDeadline). × timerScale.
|
||||
*/
|
||||
export const BAND_TURN_SECONDS = 30;
|
||||
export const BAND_TURN_SECONDS = 50;
|
||||
+3
-1
@@ -906,6 +906,8 @@ function enemyAttack(b, e, prevCd) {
|
||||
// — the 'atk' event (kind `e.profile.shot`, drawn by the content's own fx), cooldown, pause, the 'attack' hook — and
|
||||
// the content's 'attack' handler deals its damage
|
||||
const deferred = !!(e.profile && e.profile.deferHit);
|
||||
// A lethal strike may immediately redeploy its target. Post-attack effects still belong to the struck deployment.
|
||||
const targetDeployments = new Map(targets.map((t) => [t, t.deploySeq]));
|
||||
for (const t of targets) {
|
||||
b._ev(['atk', e.id, t.id, deferred ? (e.profile.shot || 'none') : rangedShot ? 'enemy' : 'none']);
|
||||
if (deferred) continue;
|
||||
@@ -919,7 +921,7 @@ function enemyAttack(b, e, prevCd) {
|
||||
b.addProjectile({ from: e, target: t, speed: PROJECTILE_SPEEDS.enemy, visual: 'enemy', source: e, onHit: (c) => hit(c.target, true) });
|
||||
} else hit(t);
|
||||
}
|
||||
if (b._hooks.attack) b.emit('attack', { attacker: e, targets, isSkill: false });
|
||||
if (b._hooks.attack) b.emit('attack', { attacker: e, targets, targetDeployments, isSkill: false });
|
||||
e.atkCd = e.s.interval;
|
||||
// stands for the rest of its attack clip (attackStand; the wind-up was stood before the strike) — every enemy, blocked
|
||||
// or not: PRTS 状态机, an enemy's ATTACK / COMBAT state "攻击结束后回退到MOVE状态" and checks only 异常状态 every frame
|
||||
|
||||
@@ -84,7 +84,7 @@ const BY_KEY = {
|
||||
}, { priority: PRIO_BAND_REVIVE });
|
||||
},
|
||||
|
||||
// 克莱门莎 崇高牺牲
|
||||
// 克莱门莎 崇高牺牲: PRTS requires the 阿戈尔 bond to be active.
|
||||
act1autochess_band13_buff(battle, ps, p, bandId) {
|
||||
const bond = typeof p.bond_id === 'string' && p.bond_id ? p.bond_id : null;
|
||||
if (!bond) return;
|
||||
@@ -93,7 +93,7 @@ const BY_KEY = {
|
||||
const u = c.unit;
|
||||
if (c.reason !== 'killed' || !isOp(u) || u.ownerId !== ps.playerId || !unitBonds(u).includes(bond)) return;
|
||||
const n = byLevel ? tierOf(u) : Math.max(1, Math.floor(num(p.value, 1)));
|
||||
if (gainLayers(battle, { playerId: ps.playerId, bonds: bond, n, requireActive: false, source: u, reason: 'band' }) > 0) {
|
||||
if (gainLayers(battle, { playerId: ps.playerId, bonds: bond, n, requireActive: true, source: u, reason: 'band' }) > 0) {
|
||||
fxOn(battle, 'layer', u, keyOf(bandId), bandId, { bond, n });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -621,13 +621,16 @@ function kitGun(ab, e, b) {
|
||||
},
|
||||
];
|
||||
if (hidden) {
|
||||
list.push({
|
||||
iv: 0.25,
|
||||
tick(b2, e2) { // 【未尽的告解】 physical / arts damage taken ×4.damage_scale while the 碎铳之簧 it came with remain
|
||||
const springs = b2.enemies.some((o) => o.alive && isSpring(o));
|
||||
if (springs) { const m = T(ab, '4.damage_scale') ?? 1; b2.addBuff(e2, { key: 'boss:confession', duration: 0.35, refresh: 'replace', mods: { physTakenMul: m, artsTakenMul: m } }); }
|
||||
},
|
||||
});
|
||||
// PRTS 未尽的告解: another gun OR spring keeps the reduction. Refresh on hit as well so the first hit and
|
||||
// a hit immediately after the last companion dies see the current condition, not a stale polling result.
|
||||
const confession = (b2, e2) => {
|
||||
const other = b2.enemies.some((o) => o !== e2 && o.alive && (isGun(o) || isSpring(o)));
|
||||
if (other) {
|
||||
const m = T(ab, '4.damage_scale') ?? 1;
|
||||
b2.addBuff(e2, { key: 'boss:confession', duration: 0.35, refresh: 'replace', mods: { physTakenMul: m, artsTakenMul: m } });
|
||||
} else b2.removeBuff(e2, 'boss:confession');
|
||||
};
|
||||
list.push({ iv: 0.25, tick: confession, hitIn(c, b2, e2) { confession(b2, e2); } });
|
||||
if (s3) list.push({
|
||||
iv: s3.bb.interval ?? 1,
|
||||
tick(b2, e2) { // 【盲信之誓】 links: phys per second on operators standing on a line ("无视无法选择、迷彩": 起飞 too)
|
||||
|
||||
@@ -138,7 +138,10 @@ const nthAttackStatus = (n, key, dur, sil = true) => ({
|
||||
attack(c, b, e, a) {
|
||||
a.n++;
|
||||
if (a.n % n) return;
|
||||
for (const t of c.targets) if (t.alive && dur > 0) b.applyStatus(t, key, { duration: dur, source: e });
|
||||
for (const t of c.targets) {
|
||||
if (!t.alive || !(dur > 0) || (c.targetDeployments && c.targetDeployments.get(t) !== t.deploySeq)) continue;
|
||||
b.applyStatus(t, key, { duration: dur, source: e });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -735,7 +735,7 @@ const BY_ITEM = {
|
||||
}
|
||||
});
|
||||
},
|
||||
// 黄沙罗盘: 初始技力 +init_sp each deploy; (萨尔贡) first skill end +sp; + 萨尔贡浓茶: each cast all 萨尔贡 +addition_sp
|
||||
// 黄沙罗盘 (PRTS: all its SP ignores 阻回): 初始技力 +init_sp each deploy; (萨尔贡) first skill end +sp; + 萨尔贡浓茶: each cast all 萨尔贡 +addition_sp
|
||||
chess_item_6_07_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1autochess_equip_acarm103_global_buff');
|
||||
if (!p) return;
|
||||
@@ -749,14 +749,14 @@ const BY_ITEM = {
|
||||
S.on('skillEnd', (c) => {
|
||||
if (c.unit !== u || firstDone || c.reason === 'death' || !memberOf(battle, u, 'sargonShip')) return;
|
||||
firstDone = true;
|
||||
if (u.skill && u.alive) u.skill.gainSp(num(p.sp), 'item');
|
||||
if (u.skill && u.alive) u.skill.gainSp(num(p.sp), 'item', false, { ignoreLock: true });
|
||||
});
|
||||
S.on('skillStart', (c) => {
|
||||
if (c.unit !== u || !partner || !memberOf(battle, u, 'sargonShip') || !carries(battle, u, partner)) return;
|
||||
const n = num(p.addition_sp);
|
||||
if (!(n > 0)) return;
|
||||
for (const a of battle.allyUnits) {
|
||||
if (a.kind === 'op' && a.ownerId === u.ownerId && onField(a) && a.skill && memberOf(battle, a, 'sargonShip')) a.skill.gainSp(n, 'item');
|
||||
if (a.kind === 'op' && a.ownerId === u.ownerId && onField(a) && a.skill && memberOf(battle, a, 'sargonShip')) a.skill.gainSp(n, 'item', false, { ignoreLock: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ function skadi2(bb, chess, def) {
|
||||
if (unit.skill) unit.skill.addTriggerRange(() => seaborns(battle, unit));
|
||||
let n = 0;
|
||||
battle.every(0.5, () => {
|
||||
if (!unit.canAct) return;
|
||||
if (!live(unit)) return; // the continuous trait (including its skill conversion) survives stun
|
||||
for (let i = unit.buffs.length - 1; i >= 0; i--) if (isInspire(unit.buffs[i])) battle.removeBuff(unit, unit.buffs[i]);
|
||||
n++;
|
||||
const toks = seaborns(battle, unit);
|
||||
|
||||
@@ -19,7 +19,7 @@ const GROUND = Object.freeze({ canHitFly: false, groundOnly: true });
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
const SPEED = 2, DISTANCE = 3, CATCH_HALF = 0.55, VORTEX_RADIUS = 1.5;
|
||||
const SPLASH_RADIUS = 1.5; // [ASSUMED] PRTS has not published S2's normal attack splash radius.
|
||||
const FIRST_BOMB = 0.5; // [ASSUMED] PRTS lists the landing delay as unknown.
|
||||
const FIRST_BOMB = 0.5; // [ASSUMED] Neither PRTS nor the edited official demo settles the first landing offset.
|
||||
const CAST = 'clemnt:cast', CARRY = 'clemnt:carry:';
|
||||
const skillBb = (raw, id) => skillRec(raw, id)?.bb ?? {};
|
||||
const tile = (u) => [Math.round(u.y), Math.round(u.x)];
|
||||
|
||||
@@ -430,7 +430,8 @@ export function bardRegen(battle, bard, ally, value, duration = BARD_REGEN_DUR)
|
||||
|
||||
const installBard = (battle, unit) => {
|
||||
battle.every(BARD_REGEN_IV, () => {
|
||||
if (!unit.canAct) return;
|
||||
// Continuous regeneration is an aura, so stun does not stop it.
|
||||
if (!unit.alive || !unit.deployed || unit.hidden) return;
|
||||
const v = unit.s.atk * (unit.profile.auraRatio ?? 0.1);
|
||||
for (const ally of battle.alliesInGrid(unit)) bardRegen(battle, unit, ally, v);
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -322,11 +322,12 @@ export class SkillRuntime {
|
||||
}
|
||||
|
||||
/** Add SP (fires the `spGain` hook). Ignored while a duration/ammo/toggle skill runs (its bar shows the skill). */
|
||||
gainSp(amount, reason = 'time', silent = false) {
|
||||
// ignoreLock is reserved for explicitly documented exceptions (黄沙罗盘), not ordinary SP gifts.
|
||||
gainSp(amount, reason = 'time', silent = false, { ignoreLock = false } = {}) {
|
||||
if (this.noSkill || this.kind === 'passive' || !(amount > 0)) return 0;
|
||||
if (this.active && this.isTimed && reason !== 'init') return 0;
|
||||
if (this.active && this.isTimed && reason !== 'init' && !ignoreLock) return 0;
|
||||
// 阻回 (the operators' 凋亡 burst, damage.js): "停止并阻止任意形式的技力回复" — no SP of any kind (time, attack, hurt, gifts)
|
||||
if (reason !== 'init' && this.unit.s.flags.noSp) return 0;
|
||||
if (reason !== 'init' && !ignoreLock && this.unit.s.flags.noSp) return 0;
|
||||
let cost = this.spCost;
|
||||
if (this.charges >= this.maxCharges && this.sp >= cost) return 0;
|
||||
let amt = amount;
|
||||
|
||||
Reference in new issue
Block a user