A skill clip with no Begin and no Idle plays once at its own length: 德克萨斯 S2 剑雨 and the other instant skills of that shape (#160)
Deploy-time passives without a duration send the same 0.5 s skill window, so 琳琅诗怀雅's passives play their clip too. Contributed by @xcdoge.
This commit is contained in:
1 parent
ffdb1293c4
commit
64715116ba
5 files changed
+232
-3
No files matched your search
@@ -2123,3 +2123,20 @@ Decisions of the owner, 2026-10-04: a 联防 devour counts members who enter alr
|
||||
- **Now**: one attempt at `local` or `assets` is abandoned after `ART_MANIFEST_TIMEOUT_MS`. A timeout or any other failure sets that file `missing` and notifies immediately, which is the existing glyph path. The status stays `missing` through the retry wait (going back to `loading` would blank the button again). A later attempt that succeeds notifies `ready` and the picture replaces the glyph. A 404 or bad JSON is still not retried. Chess and the other text files are not on this clock and still stay `loading` across retries.
|
||||
- [ASSUMED]: 8 seconds. A response that arrives after that attempt's clock is ignored; the retry is a new request, and the in-flight fetch is not aborted. After the two existing retries the glyph stays until the next `invalidate` or page load.
|
||||
- **Tests**: `test/ui/emotes.test.js` (stub timer, no network: hung `local` is `missing` before the retry wait, then `ready`; a thrown fetch is the same; a timeout with no retry and a 404 stay `missing`; `chess` arms no timer). `test/ui/playtest3.test.js` data-store retry and `test/client-static.test.js` data.js still pass. No browser render: `EmoteArt` already picks the glyph from `missing`.
|
||||
|
||||
---
|
||||
|
||||
## 24. Community reports after 0.1.3
|
||||
|
||||
Reports after the 0.1.3 release. Each was checked against the official data and PRTS; anything with no source is marked [ASSUMED]. Where each is handled: a skill clip with no Begin and no own Idle plays once (德克萨斯 S2 剑雨) → §24.1.
|
||||
|
||||
### 24.1 A skill clip with no Begin and no own Idle plays once — 德克萨斯 S2 剑雨 (player report) — `render/spine.js SpineActor.setSkill / update` (`SKILL_CLIP_MIN`, `skillClipOnce`, `skillEndPending`)
|
||||
|
||||
- **Seen** (player report after 0.1.3): 德克萨斯's S2 剑雨 casts with no skill animation — she stays in her idle through the whole cast.
|
||||
- **Official** [DATA]: her Front model has a single skill clip, `Skill` (2.167 s) — no `Skill_Begin`, no `Skill_Idle`, no `Skill_End`, so the manifest roles are `anims.skill {begin: null, loop: 'Skill', end: null}` and `anims.skills["0"] / ["1"]` are the same clip. Both her skills are instant (`durationType NONE`, duration 0; S1 冲锋号令·γ型, S2 剑雨). The official plays that `Skill` clip on the cast.
|
||||
- **Cause**: `setSkill(true)` played a clip only when the skill had a Begin clip; with `begin === null` it fell through to `else if (this.mode === 'base') this._play(this._baseName())` — the idle. The loop clip was reachable only through an attack (`_attackClip`), and an instant skill makes no attack inside its activation window: sim `skills.js` fires `['skill', id, 1]` and `['skill', id, 0]` in the same tick (`activate` → `end('instant')`) and only holds the anim code `SKILL` for 0.5 s (`snapshot.js animOf`), so the actor was back on its idle before a frame passed.
|
||||
- **Now**: a skill clip with neither a Begin nor its own Idle (its loop is not its idle, and it is not borrowed from the attack clip: `via !== 'attack'`) plays that clip once for its own length — `max(SKILL_CLIP_MIN 0.2 s, dur(loop))` — as the skill's own window; a `setSkill(false)` inside that window only marks the transition, so when the window ends the actor plays the skill's End clip (if it has one) or the idle. An instant skill therefore shows its animation instead of a one-frame flash, and a longer skill of the same shape still rests in its idle between attacks while its attacks use the loop, exactly as before. Untouched: a Begin clip with its queued idle / loop, a skill with an idle clip of its own (§23.13), `via: 'attack'` clips, `loop === idle` stances and enemies.
|
||||
- **Scale**: 90 such clips on 61 Front models (96 clips have no Begin at all — the 6 with an own Idle keep the §23.13 path); 57 of the 90 belong to instant skills.
|
||||
- **A PASSIVE skill reaches none of this through `activate()`**: `Skill.activate()` returns early for `kind === 'passive'` (`sim/skills.js`); a passive goes through `_startPassive()` (active + mods + `onStart`). Three of the 57 instant clips are passives in the data — 琳琅诗怀雅 S1 仗义疏财 / S2 “见面礼” and 凯瑟琳 S1; 凯瑟琳 S1 turned out to be `kind: instant` in the sim (so it already casts), and the two 琳琅诗怀雅 ones now fire the same window from `_startPassive` — on at the deployment, off after `SKILL_ANIM_WINDOW` (0.5 s) while the passive itself stays active. So all 57 are live.
|
||||
- **Deploy-time passives with a duration are left alone** (缄默德克萨斯 S1–S3 8–12 s, 野鬃 S1, 伊内丝 S3, 耀骑士临光 S2): the sim keeps a passive active until death, so how long its stance should show is a separate question. A clip that IS the normal attack clip (`anims.skill.loop === anims.attack.loop`: 缄默德克萨斯 S1, 砾, 普罗旺斯, 斯卡蒂 S2) is never played as a skill animation — the new branch requires a distinct clip, so a deploy-time passive on those shows no attack twitch.
|
||||
- **Tests**: `test/render/feedback4-texas-skill-clip.test.js` (德克萨斯 S2: the clip plays, holds through the same-tick off, then the idle, with no base flash in between; two more real instant entries — `char_140_whitew` S1 日晷, `char_206_gnosis` S2 零度爆发; 银灰 S3 真银斩 — the same shape but long — rests without freezing on the clip's last frame and still attacks with the loop; the deploy-time passives 琳琅诗怀雅 S1/S2 hold their 1 s clip through the sim's 0.5 s window and then idle; a clip that is the attack clip falls through to the base clip; unchanged: 折桠 S2 begin → idle, 耀骑士临光 S3 own idle, 宴 begin → loop). `test/sim/skills.test.js` covers the sim side (the passive window opens at the deployment and closes 0.5 s later, the passive staying active; a timed passive fires nothing). `test/render/feedback3-skill-idle.test.js`, `feedback3-move-deploy.test.js`, `loadout-skill.test.js` and the full suite stay green.
|
||||
@@ -16,7 +16,12 @@
|
||||
// setSkill(on) skill begin, then — when the skill has an idle clip of its own (skill.idle,
|
||||
// not its loop) — that idle between attacks, its loop (the skill's attack clip)
|
||||
// only on attacks (community report #23: 折桠's S2 jump attack looped with no
|
||||
// enemy engaged); a skill without one keeps its loop as the stance; end on stop
|
||||
// enemy engaged); end on stop. A skill clip with NEITHER a Begin nor an own Idle
|
||||
// (德克萨斯 剑雨 and the other 58 instant skills: anims.skill {begin:null,
|
||||
// loop:'Skill', end:null}) plays that clip once for its own length: an instant
|
||||
// skill's flag is off again within the same 0.5 s window (sim skills.js), so
|
||||
// without this the actor fell through to its idle and played no skill clip at
|
||||
// all (player report).
|
||||
// deploy() 'Start' once, then base
|
||||
// die() die clip once (callers fade out afterwards); a skeleton without one holds its
|
||||
// idle clip's first frame (GitHub issue #25: the attack loop went on)
|
||||
@@ -36,6 +41,9 @@ const clampN = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
/** Longest wind-up compression (× the rhythm speed) when the look-ahead is shorter than the natural wind-up. */
|
||||
export const MAX_WIND_SPEEDUP = 2.5;
|
||||
|
||||
/** Floor for the one-shot window of a skill clip with no Begin / own Idle (a very short clip still gets frames). */
|
||||
const SKILL_CLIP_MIN = 0.2;
|
||||
|
||||
/**
|
||||
* Attack clip timing (pure). `loopDur` / `hit` are clip seconds (strike frame at `hit`), `interval` and `lead` game
|
||||
* seconds. The clip plays at `ts = loopDur / interval` (one loop per attack); started `lead` before the attack it
|
||||
@@ -100,6 +108,8 @@ export class SpineActor {
|
||||
this.endAt = 0; // landing in endRoles (the next form's) when given
|
||||
this.endRoles = null;
|
||||
this.skillOn = false;
|
||||
this.skillClipOnce = false; // skillBegin: the skill's own clip plays once (a clip with no Begin / own Idle)
|
||||
this.skillEndPending = false; // … and the skill turned off inside it: play its End clip / base when it ends
|
||||
this.attackUntil = 0;
|
||||
this.clock = 0;
|
||||
this.current = '';
|
||||
@@ -360,6 +370,8 @@ export class SpineActor {
|
||||
// his S3's 'skill' off event — lets the deploy clip play out (then the plain idle) instead of cutting it with the End
|
||||
if (!on && this.mode === 'deploy') return;
|
||||
if (on && sk) {
|
||||
this.skillEndPending = false;
|
||||
this.skillClipOnce = false;
|
||||
if (this.has(sk.begin)) {
|
||||
this.mode = 'skillBegin';
|
||||
this._play(sk.begin, false, { mix: 0.08 });
|
||||
@@ -367,8 +379,25 @@ export class SpineActor {
|
||||
// then the skill's own idle until an attack plays its loop (community report #23); without one, the loop
|
||||
const next = this._skillIdle() || (this.has(sk.loop) ? sk.loop : null);
|
||||
if (next) this._queue(next, true);
|
||||
} else if (!this.has(sk.idle) && this.has(sk.loop) && sk.via !== 'attack' && sk.loop !== this.roles.attack?.loop && !this._skillIsBuffOnly()) {
|
||||
// No Begin and no own Idle: the skill's clip IS its animation (德克萨斯 剑雨 — anims.skill {begin:null,
|
||||
// loop:'Skill', end:null}, the 2.17 s clip; 58 instant skills in all). Play it once for its own length. An
|
||||
// instant skill turns the flag off inside the same tick (sim skills.js fires 'skill' 1 and 0 together and only
|
||||
// holds the anim code SKILL for 0.5 s), so this window is what keeps it on screen: the actor used to fall
|
||||
// through to the base clip and show no skill animation at all (player report).
|
||||
this.mode = 'skillBegin';
|
||||
this.skillClipOnce = true;
|
||||
this._play(sk.loop, false, { mix: 0.08 });
|
||||
this.skillBeginUntil = this.clock + Math.max(SKILL_CLIP_MIN, this.dur(sk.loop));
|
||||
} else if (this.mode === 'base') this._play(this._baseName(), true);
|
||||
} else if (!on && sk) {
|
||||
// an instant skill switches off while its own one-shot clip runs: let the clip finish — the state machine plays
|
||||
// its End clip (or the base) when the window ends — instead of cutting it with End / base right now
|
||||
if (this.skillClipOnce && this.clock < this.skillBeginUntil) {
|
||||
this.skillEndPending = true;
|
||||
return;
|
||||
}
|
||||
this.skillClipOnce = false;
|
||||
if (this.has(sk.end)) {
|
||||
this.mode = 'skillEnd';
|
||||
this._play(sk.end, false, { mix: 0.08 });
|
||||
@@ -457,7 +486,24 @@ export class SpineActor {
|
||||
}
|
||||
break;
|
||||
case 'skillBegin':
|
||||
if (this.clock >= this.skillBeginUntil) { this.mode = 'base'; if (!this.has(this.roles.skill?.loop)) this._play(this._baseName(), true); }
|
||||
if (this.clock >= this.skillBeginUntil) {
|
||||
const sk = this.roles.skill;
|
||||
const once = this.skillClipOnce;
|
||||
const pending = this.skillEndPending;
|
||||
this.skillClipOnce = false;
|
||||
this.skillEndPending = false;
|
||||
this.mode = 'base';
|
||||
if (pending && this.has(sk?.end)) {
|
||||
// the skill switched off inside its own clip: its End clip closes it now
|
||||
this.mode = 'skillEnd';
|
||||
this._play(sk.end, false, { mix: 0.08 });
|
||||
this.skillEndUntil = this.clock + this.dur(sk.end);
|
||||
} else if (once || pending || !this.has(sk?.loop)) {
|
||||
// the one-shot clip is over (or the skill has no loop clip at all): rest. A Begin clip with a queued next
|
||||
// clip (the loop, or the skill's own idle) needs no play here — it is already on the track
|
||||
this._play(this._baseName(), true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'skillEnd':
|
||||
if (this.clock >= this.skillEndUntil) { this.mode = 'base'; this._play(this._baseName(), true); }
|
||||
|
||||
+21
-1
@@ -52,6 +52,12 @@ const TRIGGER_PROFILE = Object.freeze({ canHitFly: true });
|
||||
/** Every tile of the stage (GDGLOW_SKILL_2: the whole field). */
|
||||
const ALL_TILES = new Set(Array.from({ length: ROWS * COLS }, (_, i) => i));
|
||||
|
||||
/**
|
||||
* The 'skill' animation window the sim reports to the client: `unit.skillAnimUntil` (snapshot.js `animOf` → ANIM.SKILL,
|
||||
* and the `['skill', id, 1]` / `0` events). An instant cast and a deploy-time passive both use it.
|
||||
*/
|
||||
const SKILL_ANIM_WINDOW = 0.5;
|
||||
|
||||
export class SkillRuntime {
|
||||
/**
|
||||
* @param {object} battle
|
||||
@@ -218,6 +224,20 @@ export class SkillRuntime {
|
||||
this.active = true;
|
||||
this._applyMods();
|
||||
this._call('onStart', { reason: 'passive' });
|
||||
// A deploy-time passive (琳琅诗怀雅 S1 仗义疏财 / S2 “见面礼”: kind 'passive' with no duration) never goes through
|
||||
// `activate()`, so the client saw no 'skill' event at all and its model never played the skill clip the manifest
|
||||
// carries for it (player report follow-up: the 57 instant clips with no Begin / own Idle — 2 of them passives;
|
||||
// 凯瑟琳 S1 is `kind: instant` in the sim and already casts). Fire the same bounded window an instant cast uses, so
|
||||
// the actor plays that clip once and then goes back to its idle with attacks on the normal clip. A passive WITH a
|
||||
// duration (缄默德克萨斯 S1–S3, 野鬃 S1, 伊内丝 S3, 耀骑士临光 S2) is left alone: the sim keeps it active until
|
||||
// death, so "how long should its stance show" is a separate question.
|
||||
if (!this.noSkill && !(this.duration > 0)) {
|
||||
const b = this.battle;
|
||||
const u = this.unit;
|
||||
b._ev(['skill', u.id, 1]);
|
||||
u.skillAnimUntil = b.time + SKILL_ANIM_WINDOW;
|
||||
b.after(SKILL_ANIM_WINDOW, () => { if (u.alive) b._ev(['skill', u.id, 0]); }, { owner: u });
|
||||
}
|
||||
}
|
||||
|
||||
_applyMods() {
|
||||
@@ -442,7 +462,7 @@ export class SkillRuntime {
|
||||
if (this.spec.mods || this.spec.flags || this.spec.targeting) this._applyMods();
|
||||
}
|
||||
b._ev(['skill', u.id, 1]);
|
||||
u.skillAnimUntil = b.time + 0.5;
|
||||
u.skillAnimUntil = b.time + SKILL_ANIM_WINDOW;
|
||||
this._call('onStart', { reason });
|
||||
if (b._hooks.skillStart) b.emit('skillStart', { unit: u, skill: this, reason });
|
||||
// bullets added in skillStart (拉特兰's ×(1.05 + 0.015 × layers), 逃犯引渡手续, talents): the bar's full mark
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// test/render/feedback4-texas-skill-clip.test.js — player report (after 0.1.3): 德克萨斯 S2「剑雨」释放时不播放技能动画.
|
||||
//
|
||||
// Her Front model's skill roles are { begin: null, loop: 'Skill', end: null } — the 2.17 s 剑雨 clip — and both her
|
||||
// skills are instant (server/sim/skills.js fires 'skill' 1 and 0 inside one tick and holds only the anim code SKILL for
|
||||
// 0.5 s, snapshot.js animOf). spine.js setSkill(true) had a branch for a Begin clip and otherwise just re-played the
|
||||
// base clip, so the skill clip never played: the actor showed its idle through the whole cast. The same clip shape
|
||||
// (no Begin, no own Idle) carries 58 instant skills across the manifest.
|
||||
//
|
||||
// Real manifest entries, headless fake PIXI (test/render/fakepixi.js), like feedback3-skill-idle.test.js.
|
||||
|
||||
import { test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { installFakePixi } from './fakepixi.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const assets = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8'));
|
||||
|
||||
let fake, SpineActor;
|
||||
before(async () => {
|
||||
fake = installFakePixi();
|
||||
({ SpineActor } = await import('../../public/js/render/spine.js'));
|
||||
});
|
||||
after(() => fake.restore());
|
||||
|
||||
/** An actor of a real operator's Front model with its equipped skill, recording every clip set / queued. */
|
||||
function actor(charId, skillIndex) {
|
||||
const entry = assets.chars[charId].spine.front;
|
||||
const a = new SpineActor({ animations: Object.keys(entry.animations).map((name) => ({ name })) }, entry);
|
||||
a.setSkillIndex(skillIndex);
|
||||
a.log = [];
|
||||
const st = a.spine.state;
|
||||
const set = st.setAnimation, add = st.addAnimation;
|
||||
st.setAnimation = (i, name, loop) => { a.log.push(['set', name, !!loop]); return set(i, name, loop); };
|
||||
st.addAnimation = (i, name, loop) => { a.log.push(['queue', name, !!loop]); return add(i, name, loop); };
|
||||
return a;
|
||||
}
|
||||
const run = (a, s, dt = 1 / 60) => { for (let t = 0; t < s - 1e-9; t += dt) a.update(dt); };
|
||||
const names = (a) => a.log.map(([, n]) => n);
|
||||
|
||||
test('德克萨斯 S2 剑雨: an instant skill with no Begin clip plays its Skill clip and keeps it for the clip length', () => {
|
||||
const a = actor('char_102_texas', 1);
|
||||
assert.deepEqual([a.roles.skill.begin, a.roles.skill.loop, a.roles.skill.end, a.roles.skill.idle], [null, 'Skill', null, null], 'the manifest roles');
|
||||
assert.equal(a.dur('Skill'), 2.167, 'the 剑雨 clip');
|
||||
a.setSkill(true);
|
||||
assert.deepEqual(a.log, [['set', 'Skill', false]], 'the skill clip itself, played once');
|
||||
a.setSkill(false); // the sim fires 'skill' 1 and 0 in the same tick: the flag is already off
|
||||
run(a, 1);
|
||||
assert.equal(a.mode, 'skillBegin');
|
||||
assert.equal(a.current, 'Skill', 'still on screen halfway through the cast');
|
||||
run(a, 1.3);
|
||||
assert.equal(a.mode, 'base');
|
||||
assert.equal(a.current, 'Idle', 'back to the idle once the clip has run');
|
||||
assert.deepEqual(names(a), ['Skill', 'Idle'], 'no end clip, no base flash before it');
|
||||
});
|
||||
|
||||
test('the other instant skills with the same clip shape play their clip too (char_140_whitew S1 日晷, char_206_gnosis S2 零度爆发, 德克萨斯 S1)', () => {
|
||||
for (const [charId, idx, clip] of [['char_140_whitew', 0, 'Skill'], ['char_206_gnosis', 1, 'Skill_2'], ['char_102_texas', 0, 'Skill']]) {
|
||||
const a = actor(charId, idx);
|
||||
assert.equal(a.roles.skill.begin, null, `${charId} S${idx + 1}: no Begin clip`);
|
||||
assert.equal(a.roles.skill.idle, null, `${charId} S${idx + 1}: no own Idle`);
|
||||
a.setSkill(true);
|
||||
assert.equal(a.current, clip, `${charId} S${idx + 1} plays ${clip}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a skill that stays on (银灰 S3 真银斩: same clip shape, 1.33 s clip) plays it once and then rests — no freeze on its last frame', () => {
|
||||
const a = actor('char_172_svrash', 2);
|
||||
assert.deepEqual([a.roles.skill.begin, a.roles.skill.loop, a.roles.skill.idle], [null, 'Skill', null]);
|
||||
a.setSkill(true);
|
||||
assert.equal(a.current, 'Skill');
|
||||
run(a, 2);
|
||||
assert.equal(a.mode, 'base', 'the one-shot window is over');
|
||||
assert.equal(a.current, 'Idle', 'rests on the idle while the skill is still running');
|
||||
a.log.length = 0;
|
||||
a.attack(1.2);
|
||||
assert.equal(a.current, 'Skill', 'attacks still use the skill loop, as before');
|
||||
a.setSkill(false);
|
||||
assert.equal(a.current, 'Idle', 'no End clip: straight back to the idle');
|
||||
});
|
||||
|
||||
test('unchanged: a Begin clip still runs into its queued next clip, a skill with its own Idle still idles, and End still closes the skill', () => {
|
||||
const b = actor('char_4207_branch', 1); // 折桠 S2: Skill_2_Begin → Skill_2_Idle (community report #23)
|
||||
b.setSkill(true);
|
||||
assert.deepEqual(b.log, [['set', 'Skill_2_Begin', false], ['queue', 'Skill_2_Idle', true]]);
|
||||
const n = actor('char_1014_nearl2', 2); // 耀骑士临光 S3: no Begin, own Idle — plays that idle at once
|
||||
n.setSkill(true);
|
||||
assert.deepEqual(n.log, [['set', 'Skill_3_Idle', true]]);
|
||||
const u = actor('char_337_utage', 1); // 宴: Begin → loop, unchanged
|
||||
u.setSkill(true);
|
||||
assert.deepEqual(u.log, [['set', 'Skill_Start', false], ['queue', 'Skill_Loop', true]]);
|
||||
});
|
||||
|
||||
test('the deploy-time passives the sim now opens a window for play their clip (琳琅诗怀雅 S1/S2: kind passive, no duration)', () => {
|
||||
// server/sim/skills.js `_startPassive` fires ['skill', id, 1] at the deployment and 0 after SKILL_ANIM_WINDOW (0.5 s),
|
||||
// which is shorter than her 1 s clips: the actor holds the clip to its own end and then idles.
|
||||
for (const [idx, clip] of [[0, 'Skill_1'], [1, 'Skill_2']]) {
|
||||
const a = actor('char_1033_swire2', idx);
|
||||
assert.deepEqual([a.roles.skill.begin, a.roles.skill.loop, a.roles.skill.idle], [null, clip, null]);
|
||||
assert.equal(a.dur(clip), 1);
|
||||
a.setSkill(true);
|
||||
assert.equal(a.current, clip, `S${idx + 1} plays ${clip} at the deployment`);
|
||||
run(a, 0.5);
|
||||
a.setSkill(false); // the sim closes the window at 0.5 s …
|
||||
run(a, 0.4);
|
||||
assert.equal(a.current, clip, '… the clip is still running (it is 1 s long)');
|
||||
run(a, 0.2);
|
||||
assert.equal(a.mode, 'base');
|
||||
assert.equal(a.current, 'Idle', 'and then back to the idle');
|
||||
}
|
||||
});
|
||||
|
||||
test('a skill clip that IS the normal attack clip is not played as a skill animation (no attack twitch at the deployment)', () => {
|
||||
const a = actor('char_1028_texas2', 0); // 缄默德克萨斯 S1: anims.skill.loop === anims.attack.loop ('Attack')
|
||||
assert.equal(a.roles.skill.loop, a.roles.attack.loop);
|
||||
a.setSkill(true);
|
||||
assert.deepEqual(a.log, [['set', 'Idle', true]], 'falls through to the base clip, as before');
|
||||
assert.equal(a.current, 'Idle');
|
||||
});
|
||||
@@ -433,3 +433,28 @@ test('spCostMul changes (绝技-style) keep SP within bounds and convert overflo
|
||||
assert.equal(u.skill.charges, 1);
|
||||
assert.ok(u.skill.sp <= u.skill.spCost);
|
||||
});
|
||||
|
||||
test('a deploy-time passive fires the skill animation window; a passive with a duration does not (yet)', () => {
|
||||
// 琳琅诗怀雅 S1 仗义疏财 / S2 “见面礼”: kind 'passive', no duration — the sim used to start them silently, so the
|
||||
// client got no 'skill' event and never played the clip the manifest carries (player report follow-up). The window is
|
||||
// the same one an instant cast uses: on at the deployment, off 0.5 s later (SKILL_ANIM_WINDOW), the passive itself
|
||||
// staying active (it never ends before the unit does).
|
||||
const h = makeBattle({ units: [{ chessId: 'chess_char_3_04_a', row: 10, col: 6 }], autoFinish: false, timeLimit: 30 });
|
||||
h.run(0.2);
|
||||
const u = h.unit('chess_char_3_04_a');
|
||||
assert.equal(u.skill.kind, 'passive');
|
||||
assert.equal(u.skill.active, true, 'the passive is active from the deployment');
|
||||
assert.deepEqual(h.eventsOf('skill').map((e) => e[2]), [1], 'the window opens at the deployment');
|
||||
assert.ok(u.skillAnimUntil > h.b.time, 'skillAnimUntil drives ANIM.SKILL on the client');
|
||||
h.run(0.6);
|
||||
assert.deepEqual(h.eventsOf('skill').map((e) => e[2]), [1, 0], 'and closes 0.5 s later');
|
||||
assert.equal(u.skill.active, true, 'the passive itself never ends');
|
||||
|
||||
// a passive WITH a duration (缄默德克萨斯 S2 阵雨连绵, 8 s): left alone — the sim holds it active until death, so how
|
||||
// long its stance should show is a separate question
|
||||
const t = makeBattle({ units: [{ chessId: 'chess_char_4_16_a', row: 10, col: 6, skillIndex: 1 }], autoFinish: false, timeLimit: 30 });
|
||||
t.run(0.6);
|
||||
assert.equal(t.unit('chess_char_4_16_a').skill.kind, 'passive');
|
||||
assert.equal(t.unit('chess_char_4_16_a').skill.active, true);
|
||||
assert.deepEqual(t.eventsOf('skill'), [], 'no window for a timed passive');
|
||||
});
|
||||
Reference in new issue
Block a user