feedback4: a skill restarted inside its own end owns its events — Skill.end emits no trailing skillEnd / client 'skill off' when onEnd re-activated it (耀骑士临光 S2 retreats at its duration end and 不屈 redeploys her inside that call: the trailing skillEnd cleared 骑士戒律 for the whole second cast, a PR #109 regression); 伊内丝's 影哨 death hooks run before 不屈's redeploy (priority 20 over 10), so the next deployment's S3 recalls the sentry she left (0.1.4 release review); test/sim/feedback4-restart-in-end.test.js, each case fails without its fix; golden unchanged
This commit is contained in:
1 parent
cea1ad1295
commit
47789aba8d
3 files changed
+62
-5
No files matched your search
@@ -575,11 +575,12 @@ const kits = {
|
||||
} },
|
||||
{ install(battle, unit) { // 影哨: reveal + −30 % move speed in range; a sentry keeps it after she leaves (max 1)
|
||||
const mods = { moveMul: Math.max(0, 1 + num(t1.move_speed, -0.3)) };
|
||||
// priority 20: before 不屈's redeploy (death priority 10), whose deployment starts S3 and its recall
|
||||
battle.on('death', (c) => {
|
||||
if (c.unit !== unit || c.reason === 'expired') return;
|
||||
unit.mem.sentry = new Set(unit.baseRangeKeys || unit.rangeKeys || []);
|
||||
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
|
||||
}, { owner: unit });
|
||||
}, { owner: unit, priority: 20 });
|
||||
battle.every(AURA, () => {
|
||||
const set = new Set();
|
||||
if (unit.alive && unit.deployed) for (const k of unit.rangeKeys || []) set.add(k);
|
||||
@@ -608,7 +609,8 @@ const kits = {
|
||||
}, { owner: unit });
|
||||
}, { owner: unit });
|
||||
// where the talent's 影哨 stays (every leave but an expiry), for the recall of the next deployment
|
||||
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 5 });
|
||||
// (priority 20: recorded before 不屈's redeploy at death priority 10 starts the next deployment's S3 and its recall)
|
||||
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 20 });
|
||||
// 技能期间每对一个敌人造成伤害就获得1点部署费用
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.skill?.active || !(unit.skill.timeLeft > 0)) return;
|
||||
|
||||
@@ -503,8 +503,8 @@ export class SkillRuntime {
|
||||
|
||||
/**
|
||||
* End the active skill. onEnd runs while the skill's mods / range are still applied (end-of-skill effects — finishers,
|
||||
* bombardments — use the skill's stats and range; `active` is already false), then they are removed (kept when onEnd
|
||||
* re-activated the skill), then `skillEnd` fires.
|
||||
* bombardments — use the skill's stats and range; `active` is already false), then they are removed and `skillEnd`
|
||||
* fires — neither when onEnd re-activated the skill (the new cast owns both).
|
||||
*/
|
||||
end(reason = 'end') {
|
||||
if (!this.active || this.kind === 'passive' && reason !== 'death') return;
|
||||
@@ -517,7 +517,11 @@ export class SkillRuntime {
|
||||
this.ammoMax = 0;
|
||||
const n = this.activations;
|
||||
this._call('onEnd', { reason });
|
||||
if (!this.active && this.activations === n) this._removeMods();
|
||||
// onEnd started the next cast (耀骑士临光 S2 retreats on its duration end and 不屈 redeploys her inside that call; the
|
||||
// deploy-timed skill starts again, PR #109): that cast owns the mods and its events — a trailing skillEnd would make
|
||||
// listeners (骑士戒律) clear the new cast and the client would see the skill off while it runs
|
||||
if (this.active || this.activations !== n) return;
|
||||
this._removeMods();
|
||||
if (b._hooks.skillEnd) b.emit('skillEnd', { unit: u, skill: this, reason });
|
||||
if (this.kind !== 'passive') b._ev(['skill', u.id, 0]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// A skill restarted inside its own end (0.1.4 release review): 耀骑士临光 S2 retreats when its duration ends and 不屈
|
||||
// redeploys her inside that call, starting the deploy-timed skill again (PR #109) — the new cast keeps its effects and
|
||||
// the client never sees it off; 伊内丝 S3's recall still finds its 影哨 when 不屈 redeploys her at once.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec } from '../helpers/battleHarness.js';
|
||||
|
||||
const bond = (tier, layers = 0, count = 3) => ({ count, active: tier > 0, tier, layers });
|
||||
|
||||
test('耀骑士临光 S2 restarted by 不屈 inside its duration end keeps 骑士戒律 and stays on for the client', () => {
|
||||
const NEARL = 'chess_char_6_17_a';
|
||||
const ends = [];
|
||||
const h = makeBattle({
|
||||
seed: 1, autoFinish: false, timeLimit: 220, flags: { dpInit: 99 },
|
||||
bonds: { kazimierzShip: bond(1, 0), indomShip: bond(1, 999) },
|
||||
defs: { enemies: { enemy_hit: enemyRec({ key: 'enemy_hit', hp: 10, atk: 0, speed: 0 }) } },
|
||||
units: [{ chessId: NEARL, row: 10, col: 5, dir: 'RIGHT', skillIndex: 1, items: ['chess_item_6_10_e_a', 'chess_item_4_07_e_a'] }],
|
||||
setup(b) { b.on('skillEnd', (c) => { if (c.unit?.defId === NEARL) ends.push({ active: c.skill.active, reason: c.reason }); }); },
|
||||
});
|
||||
const u = h.unit(NEARL);
|
||||
const combo = () => u.buffs.some((x) => x.key.endsWith(':combo'));
|
||||
h.run(0.3);
|
||||
assert.ok(u.skill.active && combo(), 'the first cast runs with its combo');
|
||||
h.runUntil(() => u.alive && u.skill.activations >= 2 && u.skill.active, 60);
|
||||
h.run(0.3);
|
||||
assert.ok(u.skill.active, 'the second cast runs');
|
||||
assert.ok(combo(), 'the restarted cast keeps 骑士戒律');
|
||||
assert.ok(ends.every((e) => !e.active), 'no skillEnd fires for a skill that is running again');
|
||||
const own = h.eventsOf('skill').filter((e) => e[1] === u.id).map((e) => e[2]);
|
||||
assert.equal(own.at(-1), 1, 'the client sees the skill on');
|
||||
});
|
||||
|
||||
test('伊内丝 S3: knocked out mid-battle, 不屈\'s immediate redeploy recalls the 影哨 she leaves', () => {
|
||||
const INES = 'chess_char_4_04_a';
|
||||
const h = makeBattle({
|
||||
seed: 1, autoFinish: false, timeLimit: 10, flags: { dpInit: 99 },
|
||||
bonds: { indomShip: bond(1, 999) },
|
||||
defs: { enemies: { enemy_ines: enemyRec({ key: 'enemy_ines', hp: 80000, speed: 0, atk: 0, def: 0 }) } },
|
||||
units: [{ chessId: INES, row: 10, col: 5, dir: 'RIGHT', skillIndex: 2 }],
|
||||
enemies: [{ key: 'enemy_ines', time: 0, pos: [10, 6] }],
|
||||
});
|
||||
h.step(1);
|
||||
const u = h.unit(INES), e = h.enemies()[0];
|
||||
const recalls = [];
|
||||
h.b.on('damaged', (c) => { if (c.source === u && (c.dmg?.tags || []).includes('sentryRecall')) recalls.push(c.amount); });
|
||||
h.b.dealDamage(e, u, { amount: u.s.maxHp * 10, type: 'true', canDodge: false }); // knocked out: 不屈 redeploys at once
|
||||
h.step(0.2);
|
||||
assert.ok(u.alive && u.deployed, 'redeployed by 不屈');
|
||||
assert.equal(recalls.length, 1, 'the redeploy\'s S3 recalls the 影哨 left at her fall');
|
||||
assert.equal(u.mem.sentry, null, 'and the 影哨 is spent');
|
||||
});
|
||||
Reference in new issue
Block a user