Files
ark-stronghold-protocol/test/render/feedback2-enemy-attack-clip.test.js
T

103 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/render/feedback2-enemy-attack-clip.test.js — GitHub #58, client side: an enemy plays its attack clip once per
// attack at the clip's own speed (faster only when the attacks come quicker than the clip) and then its resting clip —
// Move while the sim walks it again (server/sim/ai.js attackStand stands it for exactly that clip). It used to stretch
// the clip over the whole attack interval and loop it for 1.4 intervals, so a ranged enemy seemed to slide while it
// attacked. Operators keep the clip looping over their attack rhythm. Headless fake PIXI (test/render/fakepixi.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, fakeViewCtx } from './fakepixi.js';
import { presetCamera } from '../../public/js/render/projection.js';
import { ANIM } from '../../shared/constants.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const assets = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8'));
const JSHOOT = 'enemy_1019_jshoot'; // 隐形弩手: Attack 1.0 s, strike at 0.533 s; attacks every 2.7 s
let fake, SpineActor, UnitView;
before(async () => {
fake = installFakePixi();
({ SpineActor } = await import('../../public/js/render/spine.js'));
({ UnitView } = await import('../../public/js/render/units.js'));
});
after(() => fake.restore());
const entryOf = (id) => assets.enemies[id].spine;
const dataOf = (entry) => ({ animations: Object.keys(entry.animations).map((name) => ({ name })) });
function actor(id, perAttack) {
const entry = entryOf(id);
const a = new SpineActor(dataOf(entry), entry);
a.clipPerAttack = perAttack;
a.setBase('move');
return a;
}
const track = (a) => a.spine.state.tracks[0];
const run = (a, s, dt = 1 / 60) => { for (let t = 0; t < s - 1e-9; t += dt) a.update(dt); };
test('an enemy: wound up before the strike, the Attack clip plays once at its own speed, then Move again — no stretching over the interval', () => {
const a = actor(JSHOOT, true);
assert.equal(a.current, 'Move');
assert.ok(a.windUp(2.7, 0.2), 'winds up 0.2 s before the strike');
assert.equal(a.current, 'Attack');
assert.equal(track(a).loop, false, 'one clip per attack');
run(a, 0.2);
a.attack(2.7);
assert.equal(track(a).timeScale, 1, 'its own speed (the 2.7 s interval used to slow it to 0.37)');
run(a, 0.45);
assert.equal(a.current, 'Attack', 'the clip plays to its end (0.467 s after the strike)');
run(a, 0.05);
assert.equal(a.current, 'Move', 'then walks on its Move clip (it used to hold the attack for 1.4 intervals: 3.8 s)');
// the next attack starts the clip anew
run(a, 1.5);
assert.ok(a.windUp(2.7, 0.3));
assert.equal(a.current, 'Attack');
run(a, 0.3);
a.attack(2.7);
run(a, 0.5);
assert.equal(a.current, 'Move');
});
test('an enemy attacking quicker than its clip plays it faster, one clip per attack; an attack seen late starts on the strike frame', () => {
const a = actor(JSHOOT, true);
a.attack(0.5); // no wind-up seen: from the strike frame, at 1.0 / 0.5 = 2×
assert.equal(a.current, 'Attack');
assert.equal(track(a).loop, false);
assert.equal(track(a).timeScale, 2);
assert.ok(Math.abs(track(a).trackTime - 0.533) < 1e-9, 'on the strike frame');
run(a, 0.25);
assert.equal(a.current, 'Move', 'the rest of the clip at 2×: 0.23 s');
});
test('an operator keeps its attack loop stretched over the attack rhythm (unchanged)', () => {
const a = actor(JSHOOT, false);
a.attack(2.7);
assert.equal(track(a).loop, true);
assert.ok(Math.abs(track(a).timeScale - 1 / 2.7) < 1e-9);
run(a, 2);
assert.equal(a.current, 'Attack', 'still in attack mode 2 s later (1.4 × the interval)');
});
test('UnitView: enemy models play a clip per attack, operator models do not', async () => {
const entry = entryOf(JSHOOT);
const store = { picture: () => null, image: async () => null, spineEntry: () => entry, spine: { acquire: async () => dataOf(entry), release() {} } };
const cam = () => presetCamera('normal', { width: 1280, height: 720 });
const tick = () => new Promise((r) => setImmediate(r));
const make = async (side) => {
const v = new UnitView(fakeViewCtx(fake.P, { assets: store, cam }), { id: side === 'enemy' ? 7 : 8, side, kind: side === 'enemy' ? 'enemy' : 'op', defId: JSHOOT, spine: JSHOOT, tier: 1, x: 8, y: 9, maxHp: 1000, facing: -1 });
await tick(); await tick();
assert.ok(v.actor, 'model built');
return v;
};
const e = await make('enemy');
assert.equal(e.actor.clipPerAttack, true);
e.sync({ x: 8, y: 9, hp: 1000, maxHp: 1000, sp: 0, spMax: 0, flags: 0, anim: ANIM.MOVE, vx: 0 }, 1);
e.onAttack(null, 1);
assert.equal(e.actor.current, 'Attack');
assert.equal(e.actor.spine.state.tracks[0].loop, false);
const o = await make('ally');
assert.equal(o.actor.clipPerAttack, false);
});