Files
ark-stronghold-protocol/test/ui/playtest6-elite.e2e.test.js
T
sganggs 620f88c0d8 Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit
- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item /
  机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one
  (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call)
- official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in
  boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11
- a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight,
  promotion effect
- 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪
- DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
2026-10-02 04:19:28 +08:00

107 lines
7.1 KiB
JavaScript

// Real-server browser E2E of the user's playtest #6 follow-up: "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个
// 位置" (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"). A solo prep (test/e2e/fastServer.mjs hooks
// SP_START_ROUND 2 + SP_START_CHESS: two 古米 in the hand + SP_START_SHOP: a third 古米 in the shop) is driven with real
// input: one copy goes onto the board through the direction wheel facing UP, the shop card is tapped once (armed: the
// gold merge highlight lights that copy's tile) and again (bought) — the elite appears on that tile facing UP with the
// promotion cue (render/app.js setPrep → fx.promote), the hand copy is gone, the deploy count is unchanged and the
// promotion reward shows. Zero console errors.
//
// SP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { Client, ROOT, sleep, hasChrome, startRealServer, problemsOf } from '../e2e/client.mjs';
const ENABLED = process.env.SP_E2E === '1' && hasChrome() && existsSync(path.join(ROOT, 'public/assets'));
const GUMMY = 'chess_char_1_10_a';
async function soloToPrep(c) {
await c.open();
await c.enter('煌');
await c.click('.mode-card', '独立模拟');
await c.click('.diff-card', '险境');
await c.click('.create-box button', '开始独立模拟');
await c.waitFor((s) => !!s.room, 'solo room');
if (!(await c.st()).phase) await c.click('.room-bar__right button', '开始模拟', { timeout: 20000 });
await c.waitFor((s) => s.phase === 'INFO_CHECK', 'briefing', 30000);
await c.click('.brief__foot .btn--primary', '准备就绪');
await c.waitFor((s) => s.phase === 'BAND_DRAFT', 'band draft', 30000);
await c.click('.dband', null, { nth: 1 });
await c.click('.draft-detail__btns .btn--primary', '确认选择');
await c.waitFor((x) => x.phase === 'PREP' && !x.ready, 'prep', 60000);
await sleep(1800);
}
const privOf = (c) => c.page.evaluate(() => globalThis.__SP__.store.get().match.private);
describe('user playtest #6 follow-up: a merge\'s elite takes the deployed copy\'s tile (real server, headless Chrome)', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome + assets) to run' }, () => {
test('buying the third 古米 with one deployed: the elite stands on that tile, facing kept, with the promotion cue', { timeout: 240000 }, async () => {
const puppeteer = (await import('puppeteer-core')).default;
const srv = await startRealServer({ fast: { timerScale: 0.5, combatSpeed: 4, startRound: 2, chess: [GUMMY, GUMMY], shop: [GUMMY] } });
const c = new Client(puppeteer, srv.base, 'elite', { prefix: 'pt6' });
try {
await soloToPrep(c);
await c.hookRequests();
const hand0 = (await c.handPieces('chess')).filter((p) => p.id === GUMMY);
assert.equal(hand0.length, 2, 'two 古米 in the starter kit');
// one copy onto the board, facing UP (drag + direction wheel)
const tile = await c.freeTileFor(hand0[0].uid);
const from = await c.piecePoint(hand0[0].uid);
const to = await c.tilePoint(tile.row, tile.col);
assert.ok(from && to, 'piece and tile on screen');
await c.drag(from, to);
await c.page.waitForSelector('.fwheel__dia', { timeout: 4000 });
await c.swipe('UP');
await sleep(500);
const placed = (await privOf(c)).board.find((p) => p.uid === hand0[0].uid);
assert.ok(placed && placed.row === tile.row && placed.col === tile.col && placed.dir === 'UP', `placed facing UP (${JSON.stringify(placed)})`);
const count0 = (await c.st()).deployCount;
const promos0 = await c.page.evaluate(() => globalThis.__SP_VIEW__?.raw?.debug?.promotions?.length ?? 0);
// first tap: armed — the card says 可晋升 and the gold highlight lights the copy's tile
const name = await c.page.evaluate((id) => globalThis.__SP__.data.lookup('chess', id)?.name, GUMMY);
await c.click('.shopbar__cards .scard', name);
await sleep(400);
const armed = await c.page.evaluate(() => {
const card = document.querySelector('.shopbar__cards .scard.is-armed');
const hl = globalThis.__SP_VIEW__?.raw?.debug?.tiles?.highlights?.get('mergeTile');
const hint = document.querySelector('.dpanel .dhint--merge');
return { merge: !!card?.querySelector('.scard__mergetag'), title: card?.querySelector('.scard__mergetag')?.title || null, tiles: hl ? hl.tiles : null,
hint: hint ? hint.textContent.trim() : null, hintVisible: !!(hint && hint.getBoundingClientRect().height > 0) };
});
assert.ok(armed.merge, 'the armed card is a merge (可晋升)');
assert.equal(armed.title, '精锐干员将出现在作战区原位置');
// touch never shows a title: the detail card the first tap opened carries the same line (QA 6b)
assert.equal(armed.hint, '可晋升:精锐干员将出现在作战区原位置');
assert.ok(armed.hintVisible, 'the line is laid out in the detail card');
assert.deepEqual(armed.tiles, [[tile.row, tile.col]], 'the merge highlight lights the deployed copy\'s tile');
await c.shot('elite-armed');
// second tap: bought
await c.click('.shopbar__cards .scard.is-armed');
await c.page.waitForFunction((id) => (globalThis.__SP__.store.get().match.private?.board || []).some((p) => p.golden && p.id === id), { timeout: 8000 }, `${GUMMY.slice(0, -2)}_b`);
const priv = await privOf(c);
const elite = priv.board.find((p) => p.golden);
assert.deepEqual([elite.row, elite.col, elite.dir], [tile.row, tile.col, 'UP'], 'the elite on the copy\'s tile, facing kept');
assert.ok(!priv.hand.some((p) => p && p.kind === 'chess'), 'the hand copy was consumed');
assert.equal((await c.st()).deployCount, count0, 'the deploy count is unchanged');
assert.ok(priv.shop.rewardOffer, 'the promotion reward is offered');
assert.ok((await c.requests('g.buy')).length === 1, 'one g.buy');
await sleep(300);
const promos = await c.page.evaluate(() => (globalThis.__SP_VIEW__?.raw?.debug?.promotions || []).slice());
assert.equal(promos.length, promos0 + 1, 'one promotion cue');
assert.deepEqual([promos.at(-1).area, promos.at(-1).row, promos.at(-1).col, promos.at(-1).uid], ['board', tile.row, tile.col, elite.uid], 'played on the elite\'s tile');
const hlAfter = await c.page.evaluate(() => globalThis.__SP_VIEW__?.raw?.debug?.tiles?.highlights?.get('mergeTile') || null);
assert.equal(hlAfter, null, 'the merge highlight is gone with the purchase');
// the elite's model stands on the tile (a prep view keyed by its uid)
const view = await c.page.evaluate((uid) => { const v = globalThis.__SP_VIEW__?.raw?.debug?.views?.get('p:' + uid); return v ? { golden: !!v.info?.golden, x: v._home?.x, y: v._home?.y } : null; }, elite.uid);
assert.ok(view && view.golden, `the elite's view exists (${JSON.stringify(view)})`);
await c.shot('elite-on-board');
assert.deepEqual(problemsOf([c]), []);
} finally {
await c.close();
await srv.stop();
}
});
});