0.2.5-dev: community round 30 and follow-ups
- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
This commit is contained in:
1 parent
bc50cac01f
commit
221e6b8dd2
134 files changed
+4590
-1201
No files matched your search
@@ -2,7 +2,7 @@
|
||||
// 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).
|
||||
// attacked. Operators retain rhythm scaling but stop after each attack (#506). Headless fake PIXI.
|
||||
|
||||
import { test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -88,20 +88,69 @@ test('an attack seen late plays the whole clip from its wind-up when the rhythm
|
||||
quick.attack(2); // quicker than the clip: no room for the wind-up
|
||||
assert.ok(Math.abs(track(quick).trackTime - 1) < 1e-9, 'from the strike frame');
|
||||
const op = actor(LIFBOS, false);
|
||||
op.attack(3); // an operator's looping attack is unchanged
|
||||
op.attack(3); // late operator events still start at the strike frame
|
||||
assert.ok(Math.abs(track(op).trackTime - 1) < 1e-9);
|
||||
});
|
||||
|
||||
test('an operator keeps its attack loop stretched over the attack rhythm (unchanged)', () => {
|
||||
test('an operator keeps rhythm scaling but stops after this attack recovery instead of swinging twice', () => {
|
||||
const a = actor(JSHOOT, false);
|
||||
a.attack(2.7);
|
||||
assert.equal(track(a).loop, true);
|
||||
assert.equal(track(a).loop, false);
|
||||
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)');
|
||||
assert.equal(a.current, 'Move', 'no second swing without a second actual attack');
|
||||
assert.ok(a.windUp(2.7, 0.2)); run(a, 0.2); const prepared = track(a);
|
||||
a.attack(2.7); assert.equal(track(a), prepared, 'consume the prepared attack without replaying it');
|
||||
assert.equal(track(a).loop, false); run(a, 2); assert.equal(a.current, 'Move');
|
||||
});
|
||||
|
||||
test('UnitView: enemy models play a clip per attack, operator models do not', async () => {
|
||||
test('operator attacks with no look-ahead restart once each, including attacks faster than recovery', () => {
|
||||
const a = actor(JSHOOT, false);
|
||||
a.attack(0.08); const first = track(a); run(a, 0.08);
|
||||
a.attack(0.08); assert.notEqual(track(a), first, 'the next real attack starts a new clip');
|
||||
assert.equal(track(a).loop, false); run(a, 0.4); assert.equal(a.current, 'Move');
|
||||
a.windUp(0.08, 0.02); run(a, 0.02); a.attack(0.08); run(a, 0.02);
|
||||
assert.ok(a.windUp(0.08, 0.02), 'the next queued attack can interrupt the preceding recovery');
|
||||
});
|
||||
|
||||
test('Provence: one real attack has one visual clip across slow/fast rhythms; late events keep the strike frame', () => {
|
||||
const entry = assets.chars.char_145_prove.spine.front;
|
||||
for (const interval of [0.08, 1.6, 8]) {
|
||||
const a = new SpineActor(dataOf(entry), entry);
|
||||
const clip = entry.anims.attack.loop, hit = entry.hits[clip][0];
|
||||
a.attack(interval);
|
||||
assert.equal(track(a).loop, false); assert.equal(track(a).trackTime, hit);
|
||||
run(a, interval + 4); assert.notEqual(a.current, clip, 'no extra swing without an attack');
|
||||
assert.ok(a.windUp(interval, 0.1)); run(a, 0.1); const prepared = track(a);
|
||||
a.attack(interval); assert.equal(track(a), prepared); assert.equal(track(a).loop, false);
|
||||
run(a, interval + 4); assert.notEqual(a.current, clip);
|
||||
a.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('real Spine timeline: batched wind-up updates retain operator hit-phase correction', async () => {
|
||||
const { AnimationState, AnimationStateData, SkeletonData, Skeleton, Animation } = await import('@pixi-spine/runtime-3.8');
|
||||
const FakeSpine = fake.P.spine.Spine;
|
||||
fake.P.spine.Spine = class extends FakeSpine {
|
||||
constructor(data) { super(data); this.skeleton = new Skeleton(data); this.stateData = new AnimationStateData(data); this.state = new AnimationState(this.stateData); }
|
||||
update(dt) { this.state.update(dt); this.state.apply(this.skeleton); }
|
||||
};
|
||||
try {
|
||||
const entry = assets.chars.char_145_prove.spine.front;
|
||||
for (const dt of [1 / 60, 1 / 15, 0.1, 0.2]) {
|
||||
const data = new SkeletonData(); data.animations = Object.entries(entry.animations).map(([name, duration]) => new Animation(name, [], duration));
|
||||
const a = new SpineActor(data, entry);
|
||||
assert.ok(a.windUp(1.6, 0.2)); run(a, 0.2, dt); const prepared = track(a);
|
||||
a.attack(1.6);
|
||||
const hit = entry.hits[entry.anims.attack.loop][0];
|
||||
assert.ok(Math.abs(hit - track(a).trackTime) / track(a).timeScale <= 0.065, `dt ${dt}: the prepared strike must stay near the real attack`);
|
||||
assert.equal(track(a), prepared); assert.equal(track(a).loop, false);
|
||||
run(a, 3); assert.notEqual(a.current, entry.anims.attack.loop); a.destroy();
|
||||
}
|
||||
} finally { fake.P.spine.Spine = FakeSpine; }
|
||||
});
|
||||
|
||||
test('UnitView: enemy models retain clipPerAttack speed rules; operators retain rhythm scaling', 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 });
|
||||
|
||||
Reference in new issue
Block a user