feedback1 WD: late views keep the form in the client, 重生 clears statuses, blocked 隐匿 drawn solid
- render/app.js: the views' unit info is built by the exported renderInfo(UnitInfo), which now keeps `form`; addInfo dropped it, so a view built from fieldMeta (a teammate's field watched later, 联防 observers, a reconnect) still drew the first form. Tested through renderInfo (fake PIXI) and the real enterBattle(meta) path in headless Chrome (opt-in). - content/enemies.js: rebirthCleanse at every 重生 (reborn, husk, statue): operator-applied buffs and source-less statuses go, own talents / enemy-given buffs / field state stay (PRTS 特殊机制 §重生). A 逐火 knocked out while feared is no longer an unblockable ember. - snapshot.js: an enemy's stealth bit only while its 隐匿 is on (not blocked, not revealed). - Docs: area damage reaching an unblocked 隐匿 enemy is documented as an engine deviation (PRTS 作战机制 §AOE伤害判定), not as the rule.
This commit is contained in:
1 parent
5c23eeb804
commit
e1ce0316dd
10 files changed
+274
-49
No files matched your search
@@ -4,7 +4,10 @@
|
||||
// #5 转译基底·α walks on its A_* clips, takes 4 physical hits without losing HP, plays its 2 s A_Die_B change and goes on
|
||||
// as 寻仇者 (B_*), dying on B_Die — it used to die in its first form, on B_Die ("加载变身动画然后就没了");
|
||||
// #8 a 深池逐火战士 knocked out in front of 百炼嘉维尔 plays 'Die' (its 1 s 重生), stands there as the 隐匿 ember on
|
||||
// Idle_2 (blocked, so its blocker beats it) and dies on Die_2 — it used to stay an untargetable ember forever.
|
||||
// Idle_2 (blocked, so its blocker beats it) and dies on Die_2 — it used to stay an untargetable ember forever;
|
||||
// a view built mid-battle — the real `view.enterBattle(b.fieldMeta())` after a silent catch-up, as for a teammate's
|
||||
// field watched later, 联防 observers or a reconnect — starts in the current forms (UnitInfo `form` → render/app.js
|
||||
// renderInfo): it used to draw the warrior / the A model again (the look of report #5) and 掠海漂移体 hovering.
|
||||
//
|
||||
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/render/feedback1-forms.browser.test.js
|
||||
// Chrome path: $CHROME_PATH or the macOS default. Screenshots → test/e2e/out/feedback1-*.png.
|
||||
@@ -76,6 +79,48 @@ async function runInPage(page, port, s, enemyKey, secs, actSrc) {
|
||||
}, s, enemyKey, secs, actSrc);
|
||||
}
|
||||
|
||||
/**
|
||||
* The late view: `spec` runs silently in the page (events drained and dropped, like the client runner's catch-up) while
|
||||
* `setupSrc(b, ally)` changes the enemies' forms; at `atSecs` the field view enters the battle from `b.fieldMeta()` and
|
||||
* follows the feed. Returns { [name]: { clip, form, alive } } of the views of the enemies `setupSrc` returns by name.
|
||||
*/
|
||||
async function lateViewInPage(page, port, s, atSecs, setupSrc) {
|
||||
await page.goto(`http://127.0.0.1:${port}/dev/render-demo.html?scene=normal-m01&paused=1&panel=0`);
|
||||
await page.waitForFunction('window.__demo && (window.__demo.ready || window.__demo.error)', { timeout: 30000 });
|
||||
return page.evaluate(async (spec, atSecs, setupSrc) => {
|
||||
const setup = new Function('b', 'ally', setupSrc);
|
||||
const { loadBrowserSim } = await import('/js/battle/runner.js');
|
||||
const { data } = await import('/js/data.js');
|
||||
const { spec: S, ds } = await loadBrowserSim();
|
||||
const b = S.createBattleFromSpec(spec, ds, { quiet: true });
|
||||
const v = window.__demo.view;
|
||||
v.setStage(data.lookup('stages', spec.stageId));
|
||||
b.step();
|
||||
const named = setup(b, b.allyUnits[0]);
|
||||
while (b.time < atSecs) { b.step(); b.drainEvents(); }
|
||||
const meta = b.fieldMeta();
|
||||
v.enterBattle(meta);
|
||||
v.setCamera('normal', { rect: meta.rect, side: 'L', instant: true });
|
||||
v.setLocalFeed({ on: true, speed: 2 });
|
||||
const raf = () => new Promise((r) => requestAnimationFrame(() => r()));
|
||||
const read = () => Object.fromEntries(Object.entries(named).map(([k, u]) => {
|
||||
const view = v.debug.views.get(u.id);
|
||||
return [k, { clip: view?.actor?.current ?? null, form: view?.form ?? null, spine: !!view?.spineReady, alive: u.alive, simForm: u.form }];
|
||||
}));
|
||||
for (let i = 0; i < 30; i++) {
|
||||
b.step();
|
||||
if (i % 3 === 2) {
|
||||
const ev = b.drainEvents();
|
||||
if (ev.length) v.pushEvents({ t: 'b.ev', fieldId: meta.fieldId, gt: b.time, ev });
|
||||
v.pushSnapshot(b.snapshot());
|
||||
await raf();
|
||||
}
|
||||
if (i === 15) await new Promise((r) => setTimeout(r, 2500)); // the Spine models load
|
||||
}
|
||||
return read();
|
||||
}, s, atSecs, setupSrc);
|
||||
}
|
||||
|
||||
describe('player reports after 0.1.0: the models follow the knock-out forms (headless Chrome, real sim)', { skip }, () => {
|
||||
let srv, browser;
|
||||
before(async () => {
|
||||
@@ -151,4 +196,33 @@ describe('player reports after 0.1.0: the models follow the knock-out forms (hea
|
||||
await p.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('a view built mid-battle (enterBattle(fieldMeta) after a silent catch-up) starts in the current forms: 特战术师 D_*, 幽灵 C_*, the ember Idle_2 / Move_2, 掠海漂移体 *_02', async () => {
|
||||
const { p, problems } = await page();
|
||||
try {
|
||||
const got = await lateViewInPage(p, srv.port, spec('enemy_1007_slime', 'late', 9, 8), 7.5, `
|
||||
ally.base.atk = 0; ally.markDirty(); // 百炼嘉维尔 only blocks
|
||||
const still = { mods: { speedMul: 0 } };
|
||||
const shushi = b.spawnEnemy('enemy_10081_mpplai', { pos: [11, 9], ...still });
|
||||
const youling = b.spawnEnemy('enemy_10081_mpplai', { pos: [9, 8.4], ...still }); // blocked ⇒ 幽灵
|
||||
const ember = b.spawnEnemy('enemy_1288_duskls', { pos: [12, 9], ...still });
|
||||
const drift = b.spawnEnemy('enemy_2025_syufo', { pos: [11, 6], ...still });
|
||||
for (let i = 0; i < 4; i++) b.dealDamage(ally, shushi, { amount: 1, type: 'arts' });
|
||||
b.kill(ember, ally);
|
||||
b.applyStatus(drift, 'stun', { duration: 1, source: ally });
|
||||
return { shushi, youling, ember, drift };
|
||||
`);
|
||||
await p.screenshot({ path: path.join(OUT, 'feedback1-lateview.png') });
|
||||
assert.deepEqual(Object.fromEntries(Object.entries(got).map(([k, x]) => [k, x.simForm])),
|
||||
{ shushi: 'translator_shushi', youling: 'translator_youling', ember: 'husk', drift: 'crawl' }, 'the sim\'s forms');
|
||||
for (const [k, x] of Object.entries(got)) assert.ok(x.spine && x.alive && x.form === x.simForm, `${k}: a live Spine view in the sim's form (${JSON.stringify(x)})`);
|
||||
assert.match(got.shushi.clip, /^D_/, `特战术师 (${got.shushi.clip})`);
|
||||
assert.match(got.youling.clip, /^C_/, `幽灵 (${got.youling.clip})`);
|
||||
assert.match(got.ember.clip, /^(Idle|Move)_2$/, `the ember (${got.ember.clip})`);
|
||||
assert.match(got.drift.clip, /_02$/, `crawling (${got.drift.clip})`);
|
||||
assert.deepEqual(problems, []);
|
||||
} finally {
|
||||
await p.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@
|
||||
// 'Die' (its 1 s 重生) and walks as the ember on Idle_2 / Move_2, dying on Die_2, and 'Revive' — timed from the 'ember'
|
||||
// fx's `dur` to end as it stands up — brings the warrior back (sim fx 'ember' / 'revive' { form: 'husk' | 'revived' });
|
||||
// 假想敌:再生's 傀儡 the same with A_Die / B_* / B_Revive; the leaders' 重生 close on their last clip as the 重生 ends.
|
||||
// A view built mid-battle gets the current form from UnitInfo `form` through render/app.js renderInfo (enterBattle).
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -23,10 +24,11 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const assets = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8'));
|
||||
const SYUFO = 'enemy_2025_syufo';
|
||||
|
||||
let fake, UnitView, FORMS;
|
||||
let fake, UnitView, FORMS, renderInfo;
|
||||
before(async () => {
|
||||
fake = installFakePixi();
|
||||
({ UnitView, FORMS } = await import('../../public/js/render/units.js'));
|
||||
({ renderInfo } = await import('../../public/js/render/app.js'));
|
||||
});
|
||||
after(() => fake.restore());
|
||||
|
||||
@@ -297,26 +299,32 @@ describe('the leaders\' 重生 and 守墓石像 (audit of the knock-out forms af
|
||||
});
|
||||
});
|
||||
|
||||
test('a view built mid-battle from the real sim\'s fieldMeta (fx dropped by a silent catch-up) starts in the current form: the changed 转译基底·α on D_Idle, the 逐火 ember on Idle_2, 锏 after its 重生 on B_Idle', async () => {
|
||||
test('a view built mid-battle from the real sim\'s fieldMeta (fx dropped by a silent catch-up), through render/app.js renderInfo — what enterBattle(meta) hands the views — starts in the current form: the changed 转译基底·α on D_Idle / C_Idle, the 逐火 ember on Idle_2, 锏 after its 重生 on B_Idle, 掠海漂移体 crawling', async () => {
|
||||
const mage = chessRec({ id: 't_mage', profession: 'CASTER', stats: { atk: 50, blockCnt: 0 }, rangeGrid: [[0, 0]], skill: null });
|
||||
const wall = chessRec({ id: 't_wall', profession: 'TANK', stats: { atk: 0, maxHp: 1e7, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null });
|
||||
const quiet = () => ({ trait: { noAttack: true } });
|
||||
const h = makeBattle({
|
||||
stageId: 'act2autochess_m01', content: 'full', seed: 7, autoFinish: false, timeLimit: 600, modeId: 'mode_multi_hard', round: 3,
|
||||
defs: { chess: { t_mage: mage } }, units: [{ chessId: 't_mage', row: 12, col: 2 }], kits: { t_mage: () => ({ trait: { noAttack: true } }) },
|
||||
defs: { chess: { t_mage: mage, t_wall: wall } }, units: [{ chessId: 't_mage', row: 12, col: 2 }, { chessId: 't_wall', row: 9, col: 4 }],
|
||||
kits: { t_mage: quiet, t_wall: quiet },
|
||||
});
|
||||
h.step();
|
||||
const still = { mods: { speedMul: 0 } };
|
||||
const tr = h.b.spawnEnemy('enemy_10081_mpplai', { pos: [9, 8], ...still });
|
||||
const ember = h.b.spawnEnemy('enemy_1288_duskls', { pos: [10, 8], ...still });
|
||||
const mace = h.b.spawnEnemy('enemy_1525_blkswb', { pos: [11, 8], ...still });
|
||||
const tr = h.b.spawnEnemy('enemy_10081_mpplai', { pos: [10, 8], ...still });
|
||||
const ghost = h.b.spawnEnemy('enemy_10081_mpplai', { pos: [9, 4.4], ...still }); // walks into the wall: blocked ⇒ 幽灵
|
||||
const ember = h.b.spawnEnemy('enemy_1288_duskls', { pos: [11, 8], ...still });
|
||||
const mace = h.b.spawnEnemy('enemy_1525_blkswb', { pos: [12, 8], ...still });
|
||||
const drift = h.b.spawnEnemy(SYUFO, { pos: [11, 10], ...still });
|
||||
const m = h.unit('t_mage');
|
||||
for (let i = 0; i < 4; i++) h.b.dealDamage(m, tr, { amount: 1, type: 'arts' }); // the 4th arts hit ⇒ 特战术师
|
||||
h.b.kill(ember, m);
|
||||
h.b.kill(mace, m);
|
||||
h.b.applyStatus(drift, 'stun', { duration: 1, source: m }); // 掠海漂移体 drops to 爬行模式
|
||||
const until = h.b.time + 6;
|
||||
while (h.b.time < until) { h.b.step(); h.b.drainEvents(); }
|
||||
const meta = h.b.fieldMeta();
|
||||
const view = async (u) => {
|
||||
const info = meta.units.find((x) => x.id === u.id);
|
||||
const info = renderInfo(meta.units.find((x) => x.id === u.id));
|
||||
const ctx = fakeViewCtx(fake.P, { assets: store(info.spine), cam });
|
||||
const v = new UnitView(ctx, info);
|
||||
await tick(); await tick();
|
||||
@@ -324,8 +332,20 @@ test('a view built mid-battle from the real sim\'s fieldMeta (fx dropped by a si
|
||||
return v;
|
||||
};
|
||||
assert.equal(clip(await view(tr)), 'D_Idle', '特战术师, not the A model');
|
||||
assert.equal(clip(await view(ember)), 'Idle_2', 'the ember, not the warrior');
|
||||
assert.equal(clip(await view(ghost)), 'C_Idle', '幽灵 (blocked first), not the A model');
|
||||
const e = await view(ember);
|
||||
assert.equal(clip(e), 'Idle_2', 'the ember, not the warrior');
|
||||
e.die();
|
||||
assert.equal(clip(e), 'Die_2', 'and it dies on the ember\'s clip');
|
||||
assert.equal(clip(await view(mace)), 'B_Idle', '锏\'s second form');
|
||||
assert.equal(clip(await view(drift)), 'Idle_02', '掠海漂移体 crawls (DESIGN §19.1: a reload used to hover)');
|
||||
});
|
||||
|
||||
test('render/app.js renderInfo keeps UnitInfo `form` (it used to drop it: views built from fieldMeta drew the first form)', () => {
|
||||
assert.equal(renderInfo({ id: 3, kind: 'enemy', side: 'enemy', defId: 'enemy_1288_duskls', form: 'husk' }).form, 'husk');
|
||||
assert.equal(renderInfo({ id: 3, kind: 'enemy', side: 'enemy', defId: 'enemy_1288_duskls' }).form, undefined);
|
||||
assert.equal(renderInfo({ id: 3, form: 7 }).form, undefined, 'only a string');
|
||||
assert.equal(renderInfo({ kind: 'enemy' }), null, 'no id: no info');
|
||||
});
|
||||
|
||||
test('render/app.js hands the sim\'s fx \'phase\' kind — or the `form` of any fx, with the fx — to the view and keeps the mode on the unit info', () => {
|
||||
|
||||
Reference in new issue
Block a user