0.2.3: prepare follow-up fixes and client features (#407, #408, #410, #413, #431, #435, #436, #440, #444)
Add special-target activation without bypassing attack cadence, cooperative card preferences and emotes, skill sound banks, per-operator voice choices, text sizing, install prompts, closed-tab recovery and unanimous setup rerolls. Retain late deployment elapsed time and resolve echo/counter pulses without recursive hook overflow. Independent final review remains pending. Golden: roster-016, roster-035, roster-036, roster-045; boss-boss_1-pair, boss-boss_3-pair, boss-boss_5-pair, boss-boss_7-pair, hidden-boss_9-pair; solo-HARD-9-boosted: Lemuen attack cadence. diy-132: Necras extra activation targets. coop2-NORMAL-14-standins: cooperative purchase preference and simulation fixes. hidden-boss_10-solo: echo pulse FIFO resolution. See DESIGN 28.13-28.24 for assumptions, sources and contributor credit.
This commit is contained in:
1 parent
314564c139
commit
f3658a0dd4
104 files changed
+3674
-741
No files matched your search
@@ -774,15 +774,25 @@ export function setEchoForm(b, echo, form) {
|
||||
export function echoHit(b, echo) {
|
||||
const ab = echo.mem.ab;
|
||||
if (!ab || !echo.alive) return;
|
||||
const atk = echo.s.atk;
|
||||
b.fx('explode', { x: echo.x, y: echo.y, r: ECHO_PULSE_RADIUS, kind: 'echoPulse' });
|
||||
for (const u of areaAllies(b, echo, echo.x, echo.y, ECHO_PULSE_RADIUS)) {
|
||||
hurt(b, echo, u, atk * (T(ab, '3.atk_scale') ?? 0), 'arts');
|
||||
elem(b, echo, u, 'apoptosis', atk * (T(ab, '3.ep_damage_ratio') ?? 0));
|
||||
}
|
||||
const need = ab.form === 'gold' ? T(ab, '1.hit_times_to_switch') : T(ab, '2.hit_times_to_switch');
|
||||
ab.strikes = (ab.strikes ?? 0) + 1;
|
||||
if (need > 0 && ab.strikes >= need) setEchoForm(b, echo, ab.form === 'gold' ? 'dark' : 'gold');
|
||||
// [ASSUMED] Resolve recursively earned pulses FIFO after the current pulse. A counter can remove dozens of
|
||||
// hit-count HP here; synchronous damaged → counter → pulse nesting used to trip the engine's 32-hook guard.
|
||||
// Keep the pulse earned by the lethal hit, too. No timer, extra RNG or skipped damage (DESIGN §28.24).
|
||||
(ab.pulses ||= []).push(echo.s.atk);
|
||||
if (ab.pulsing) return;
|
||||
ab.pulsing = true;
|
||||
try {
|
||||
for (let i = 0; i < ab.pulses.length; i++) {
|
||||
const atk = ab.pulses[i];
|
||||
b.fx('explode', { x: echo.x, y: echo.y, r: ECHO_PULSE_RADIUS, kind: 'echoPulse' });
|
||||
for (const u of areaAllies(b, echo, echo.x, echo.y, ECHO_PULSE_RADIUS)) {
|
||||
hurt(b, echo, u, atk * (T(ab, '3.atk_scale') ?? 0), 'arts');
|
||||
elem(b, echo, u, 'apoptosis', atk * (T(ab, '3.ep_damage_ratio') ?? 0));
|
||||
}
|
||||
const need = ab.form === 'gold' ? T(ab, '1.hit_times_to_switch') : T(ab, '2.hit_times_to_switch');
|
||||
ab.strikes = (ab.strikes ?? 0) + 1;
|
||||
if (echo.alive && need > 0 && ab.strikes >= need) setEchoForm(b, echo, ab.form === 'gold' ? 'dark' : 'gold');
|
||||
}
|
||||
} finally { ab.pulses.length = 0; ab.pulsing = false; }
|
||||
}
|
||||
|
||||
function kitEcho(ab, e) {
|
||||
|
||||
@@ -232,7 +232,7 @@ function lemuen(bb, chess, def) {
|
||||
return keys;
|
||||
};
|
||||
// #428: extra attack range also opens her skill under the existing content-trigger contract.
|
||||
unit.skill?.addTriggerRange(() => [{ keys: wantedKeys(), profile: unit.profile }]);
|
||||
unit.skill?.addTriggerRange(() => [{ keys: wantedKeys(), profile: unit.profile }], { attackOnly: true });
|
||||
let sig = null;
|
||||
const refresh = () => { // update immediately when the last source leaves, before another attack can run
|
||||
const keys = live(unit) ? wantedKeys() : [];
|
||||
|
||||
@@ -380,12 +380,21 @@ export default {
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// PRTS 卫戍协议/帮助: special targeting also supplies the DEFAULT attack opportunity (#440).
|
||||
// Query live owned blockers for both paths; S2's links still use its own effect range, S3 keeps SKILL_RANGE.
|
||||
const blockedKeys = () => {
|
||||
if (!up(unit)) return [];
|
||||
const keys = [];
|
||||
for (const s of skelsUp(unit)) for (const e of s.blocking) if (e.alive && e.blockedBy === s) keys.push(tileKey(Math.round(e.y), Math.round(e.x)));
|
||||
return [...new Set(keys)].sort((a, b) => a - b);
|
||||
};
|
||||
if (unit.skill?.id === S1 || unit.skill?.id === S2) {
|
||||
unit.skill.addTriggerRange(() => [{ keys: blockedKeys(), profile: unit.profile }], { attackOnly: true });
|
||||
}
|
||||
// trait: the enemies her summons block are her targets too
|
||||
battle.on('tick', () => {
|
||||
if (!up(unit)) return;
|
||||
const keys = [];
|
||||
for (const s of skelsUp(unit)) for (const e of s.blocking) if (e.alive && e.blockedBy === s) keys.push(tileKey(Math.round(e.y), Math.round(e.x)));
|
||||
const uniq = [...new Set(keys)].sort((a, b) => a - b);
|
||||
const uniq = blockedKeys();
|
||||
const cur = unit.extraRangeKeys ?? [];
|
||||
if (uniq.length === cur.length && uniq.every((k, i) => k === cur[i])) return;
|
||||
battle.setExtraRange(unit, uniq.length ? uniq : null);
|
||||
|
||||
+10
-6
@@ -125,6 +125,7 @@ export class SkillRuntime {
|
||||
this._trigKeys = null;
|
||||
this._trigSet = null;
|
||||
this.triggerRanges = []; // content trigger ranges (addTriggerRange)
|
||||
this.attackTriggerRanges = new Set(); // special targeting checked only at the next attack
|
||||
this.noSkill = !spec; // unit without any skill spec
|
||||
}
|
||||
|
||||
@@ -147,20 +148,23 @@ export class SkillRuntime {
|
||||
* keys, or `{ keys, profile }` — tile keys with the enemy profile the effect selects by (`canHitFly` false: ground
|
||||
* enemies only — the owner's larger-range rule through a summon's area, kits/shared/summoner.js summonTriggerArea;
|
||||
* 0.2.0 WV, additive). A targetable enemy (flyers included unless the entry's profile says otherwise) on those tiles
|
||||
* satisfies the DEFAULT rule (and unknown DEFAULT-like rules); it is checked every tick, since the unit itself may
|
||||
* have nothing to attack. Returns an unregister fn.
|
||||
* satisfies the DEFAULT rule (and unknown DEFAULT-like rules). Summon effect areas are checked every tick;
|
||||
* { attackOnly: true } is for special targeting that must wait for the next attack (蕾缪安, 死芒).
|
||||
* Returns an unregister fn.
|
||||
*/
|
||||
addTriggerRange(fn) {
|
||||
addTriggerRange(fn, { attackOnly = false } = {}) {
|
||||
if (typeof fn !== 'function') return () => {};
|
||||
this.triggerRanges.push(fn);
|
||||
return () => { const i = this.triggerRanges.indexOf(fn); if (i >= 0) this.triggerRanges.splice(i, 1); };
|
||||
if (attackOnly) this.attackTriggerRanges.add(fn);
|
||||
return () => { this.attackTriggerRanges.delete(fn); const i = this.triggerRanges.indexOf(fn); if (i >= 0) this.triggerRanges.splice(i, 1); };
|
||||
}
|
||||
|
||||
/** An enemy inside one of the content trigger ranges. */
|
||||
_extraTriggerSatisfied() {
|
||||
_extraTriggerSatisfied(allowAttackOnly = true) {
|
||||
const b = this.battle;
|
||||
const u = this.unit;
|
||||
for (const fn of this.triggerRanges) {
|
||||
if (!allowAttackOnly && this.attackTriggerRanges.has(fn)) continue;
|
||||
const list = b._safe(() => fn(b, u), 'skill.triggerRange', u);
|
||||
if (!list || typeof list[Symbol.iterator] !== 'function') continue;
|
||||
for (const x of list) {
|
||||
@@ -405,7 +409,7 @@ export class SkillRuntime {
|
||||
const prof = u.profile;
|
||||
if (prof && (prof.noAttack || (prof.noAttackUnlessSkill && !this.active))) {
|
||||
if (this._defaultCondition()) this.activate('DEFAULT');
|
||||
} else if (this.triggerRanges.length && !this.healSkill && this._extraTriggerSatisfied()) this.activate('DEFAULT');
|
||||
} else if (this.triggerRanges.length && !this.healSkill && this._extraTriggerSatisfied(false)) this.activate('DEFAULT');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user