Files

292 lines
12 KiB
JavaScript
Raw Permalink 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.
// 七日全流程自动化验收:任务/线索/悲伤行为/黄昏/梦境/结局 + 控制台零报错
// 运行:node tools/e2e_flow.mjs
import { chromium } from "playwright";
import { createServer } from "vite";
import { mkdirSync } from "fs";
const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-flow";
mkdirSync(SHOT_DIR, { recursive: true });
const server = await createServer({ server: { port: 5198 } });
await server.listen();
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
const errors = [];
page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
page.on("pageerror", (e) => errors.push("PAGEERROR: " + e.message));
const sleep = (ms) => page.waitForTimeout(ms);
// 无头环境下 press() 的 down/up 同帧到达会吞掉 JustDown,改为跨帧按键
const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); };
const shot = (n) => page.screenshot({ path: `${SHOT_DIR}/${n}.png` });
const D = (expr) => page.evaluate(`(() => { const d = window.__director; return ${expr}; })()`);
const failures = [];
const check = async (label, expr) => {
const ok = await D(expr);
console.log(`${ok ? "PASS" : "FAIL"} ${label}`);
if (!ok) {
failures.push(label);
const diag = await page.evaluate(() => {
const d = window.__director;
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
return `day=${d.day} scene=${d.currentScene} wipes=${d.wipes} clues=[${[...d.clues]}] cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
});
console.log(" diag:", diag);
}
};
// 把玩家瞬移到当前场景某像素点
const tp = (x, y) => page.evaluate(([x, y]) => {
const g = window.__game;
const sc = g.scene.getScenes(true).find((s) => s.player);
sc.player.setPosition(x, y);
sc.player.body.reset(x, y);
}, [x, y]);
const curScene = () => page.evaluate(() => window.__director.currentScene);
// 连按 E 直到对话结束
const talk = async (times = 14) => {
for (let i = 0; i < times; i++) {
await pressE();
await sleep(160);
const open = await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen);
if (!open) break;
}
};
const interactAt = async (x, y, presses = 14) => {
await tp(x, y); await sleep(350);
await pressE(); await sleep(300);
await talk(presses);
await sleep(200);
};
// 走进门(瞬移到门内并等场景切换)
const enterDoor = async (x, y, expectScene) => {
await tp(x, y);
for (let i = 0; i < 30; i++) { await sleep(200); if ((await curScene()) === expectScene) break; }
const ok = (await curScene()) === expectScene;
console.log(`${ok ? "PASS" : "FAIL"} 进入 ${expectScene}`);
if (!ok) {
failures.push("进入 " + expectScene);
const diag = await page.evaluate(() => {
const d = window.__director;
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
return `scene=${d.currentScene} cutscene=${d.cutscene} dialog=${window.__game.scene.getScene("ui").dialogOpen} player=${sc ? Math.round(sc.player.x) + "," + Math.round(sc.player.y) : "-"} trans=${sc ? sc.transitioning : "-"}`;
});
console.log(" diag:", diag);
}
await sleep(400);
};
const sleepToNextDay = async () => {
await enterDoor(72, 68, "house"); // 回小屋(从广场小屋门)
await interactAt(30, 70, 6); // 床
await sleep(14500); // 梦境过场
await sleep(1200);
};
await page.goto("http://localhost:5198/");
await sleep(2500);
await page.fill("#name-input", "验收员");
await page.press("#name-input", "Enter");
await sleep(1800);
// ===== Day 1 =====
await check("D1 开局幸福 30", "d.day===1 && d.happiness===30 && d.awareness===0");
await interactAt(30, 40, 4); // 日历(D1 无线索)
await check("D1 日历尚无 C1", "!d.clues.has('C1')");
await enterDoor(88, 122, "plaza"); // 出小屋
await interactAt(188, 126); // 公告板
await check("D1 公告已读", "d.announced===true");
// T2 浇水 ×3
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
await check("T2 完成 +10", "d.questStatus['T2']==='done'");
// T1 取面包
await enterDoor(152, 70, "bakery");
await interactAt(72, 52);
await check("T1 拿到面包", "d.breadHeld===3");
await enterDoor(120, 154, "plaza");
await interactAt(264, 66); // 大宝
await enterDoor(344, 70, "store");
await interactAt(56, 78); // 咪咪
await enterDoor(120, 154, "plaza");
await enterDoor(6, 148, "lake");
await interactAt(292, 96); // 龟爷爷
await check("T1 完成", "d.questStatus['T1']==='done'");
await check("T3 完成(问好×3", "d.questStatus['T3']==='done'");
await check("D1 黄昏已触发", "d.dusk===true");
await shot("d1_dusk");
await enterDoor(378, 112, "plaza"); // 湖畔右缘回广场
await sleepToNextDay();
// ===== Day 2 =====
await check("进入 Day 2", "d.day===2");
await interactAt(30, 40, 6); // 日历 C1
await check("C1 获得(觉知+8", "d.clues.has('C1') && d.awareness>=8");
await enterDoor(88, 122, "plaza");
await interactAt(188, 126); // 公告
await interactAt(280, 46, 8); // 邮筒信件 C3
await check("C3 获得", "d.clues.has('C3')");
await interactAt(264, 66); // 大宝取信
await check("T4 拿到信", "d.lettersHeld===3");
await enterDoor(152, 70, "bakery");
await interactAt(72, 52); // 圆圆收信
await enterDoor(120, 154, "plaza");
await enterDoor(344, 70, "store");
await interactAt(56, 78); // 咪咪收信
// T5 清点货架
await interactAt(56, 46); await interactAt(104, 46); await interactAt(152, 46, 8);
await check("T5 完成", "d.questStatus['T5']==='done'");
await interactAt(198, 92, 8); // C5 货架
await check("C5 获得", "d.clues.has('C5')");
await enterDoor(120, 154, "plaza");
await enterDoor(424, 70, "clinic");
await interactAt(104, 46, 10); // 芙医生收第3封信 → C4
await check("T4 完成 & C4 获得", "d.questStatus['T4']==='done' && d.clues.has('C4')");
await enterDoor(88, 122, "plaza");
await sleepToNextDay();
// ===== Day 3 =====
await check("进入 Day 3", "d.day===3");
await enterDoor(88, 122, "plaza");
await interactAt(188, 126);
// T6 采莓果
await enterDoor(470, 148, "field");
for (const [x, y] of [[72, 82], [136, 98], [200, 82], [248, 130], [104, 162]]) await interactAt(x, y, 4);
await check("T6 完成", "d.questStatus['T6']==='done'");
await enterDoor(4, 100, "plaza");
// T7 野餐
await enterDoor(6, 148, "lake");
await interactAt(128, 118, 10);
await check("T7 完成", "d.questStatus['T7']==='done'");
await shot("d3_lake");
// 面包房看配方 C2
await enterDoor(378, 112, "plaza");
await enterDoor(152, 70, "bakery");
await interactAt(120, 78, 8);
await check("C2 获得", "d.clues.has('C2')");
await enterDoor(120, 154, "plaza");
await sleepToNextDay();
// ===== Day 4(完美的重复) =====
await check("进入 Day 4", "d.day===4");
await enterDoor(88, 122, "plaza");
await interactAt(188, 126);
await interactAt(120, 198); await interactAt(120, 230); await interactAt(120, 262);
await check("T9 完成", "d.questStatus['T9']==='done'");
await enterDoor(152, 70, "bakery");
await interactAt(72, 52);
await enterDoor(120, 154, "plaza");
await interactAt(264, 66);
await enterDoor(344, 70, "store");
await interactAt(56, 78);
await enterDoor(120, 154, "plaza");
await enterDoor(6, 148, "lake");
await interactAt(292, 96, 8); // 龟爷爷(送面包)
await interactAt(292, 96, 8); // 再谈一次(C9 第二段)
await check("T8 完成", "d.questStatus['T8']==='done'");
await check("T10 完成", "d.questStatus['T10']==='done'");
await check("C9 链已记 D2/D4", "d.guiChain.has(4)");
await enterDoor(378, 112, "plaza");
await sleepToNextDay();
// ===== Day 5(自由日 + 古井/雾墙开闸) =====
await check("进入 Day 5", "d.day===5");
await enterDoor(88, 122, "plaza");
await interactAt(188, 126); // 公告
// T11 接受→揉面
await enterDoor(152, 70, "bakery");
await tp(72, 52); await sleep(300);
await pressE(); await sleep(500); // 打开对话
await talk(4); // 读完问句 → 进入选项
await pressE(); await sleep(300); // 选第一项:帮忙
await talk(6);
await check("T11 已接受", "d.t11Accepted===true");
await interactAt(168, 102, 8); // 揉面台
await check("T11 完成", "d.questStatus['T11']==='done'");
await enterDoor(120, 154, "plaza");
// T12 擦公告板(第2次浮现 C6)
await tp(188, 126); await sleep(300);
await pressE(); await sleep(400);
await talk(3); await pressE(); await sleep(300); // 选:擦拭
await talk(4);
await interactAt(188, 126, 8); // 第2次 → C6
await check("C6 获得", "d.clues.has('C6')");
await interactAt(188, 126, 8); // 第3次 → 完成
await check("T12 完成", "d.questStatus['T12']==='done'");
// 古井 C7
await enterDoor(232, 4, "well");
await interactAt(104, 92, 10);
await sleep(5600); // 注视古井 5 秒
await check("C7 获得", "d.clues.has('C7')");
await check("注视古井 +3 觉知", "d.stared.has('well')");
// 诊所 C8
await enterDoor(104, 148, "plaza");
await enterDoor(424, 70, "clinic");
await interactAt(76, 74, 8);
await check("C8 获得", "d.clues.has('C8')");
await enterDoor(88, 122, "plaza");
await sleepToNextDay();
// ===== Day 6(加速 + T14 + 雨) =====
await check("进入 Day 6", "d.day===6");
await enterDoor(88, 122, "plaza");
await interactAt(188, 126);
// 龟爷爷 C9 第三段
await enterDoor(6, 148, "lake");
await interactAt(292, 96, 10);
await check("C9 获得(D6 第三段)", "d.clues.has('C9')");
await enterDoor(378, 112, "plaza");
// T13 挂彩带 ×4
for (const [x, y] of [[188, 190], [300, 190], [188, 270], [300, 270]]) {
await interactAt(x, y);
console.log(" ribbons:", await D("[...d.ribbons].join('|')"));
}
await check("T13 完成", "d.questStatus['T13']==='done'");
// T14 保持微笑 60 秒(把计时快进以免真等 60s)
await interactAt(240, 210, 4); // 启动
await sleep(1500);
await page.evaluate(() => {
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
sc.t14Start -= 61000; // 快进
});
await sleep(1500);
await talk(6);
await check("T14 完成", "d.questStatus['T14']==='done'");
await check("D6 黄昏(脚本雨)", "d.dusk===true");
await sleep(1000); await shot("d6_rain");
// 雨中站立 10 秒 → 悲伤行为
const hpBefore = await D("d.happiness");
await page.evaluate(() => {
const sc = window.__game.scene.getScenes(true).find((s) => s.player);
sc.rainStillAt -= 11000;
});
await sleep(800);
const hpAfter = await D("d.happiness");
console.log(`${hpAfter <= hpBefore ? "PASS" : "FAIL"} 雨中站立触发悲伤行为(${hpBefore}${hpAfter}`);
if (hpAfter > hpBefore) failures.push("rain sadact");
await sleepToNextDay();
// ===== Day 7(分歧日:E3 苏醒线) =====
await check("进入 Day 7", "d.day===7");
await check("线索 ≥6", "d.clues.size>=6");
await D("d.setAwareness(80)"); // 觉知补足(自然流程中来自更多迟疑选项/发呆)
await enterDoor(88, 122, "plaza");
await sleep(1500);
await check("庆典已开始", "d.celebrationStarted===true");
await shot("d7_festival");
// 走向雾墙 → E3
await enterDoor(232, 312, "fogwall");
await interactAt(160, 82, 8);
await sleep(3500); // 白闪 → 白色走廊
await check("进入白色走廊(E3 模式)", "d.e3Mode===true && d.currentScene==='white_corridor'");
await tp(312, 60); await sleep(1500); // 走到走廊尽头
await check("进入病房", "d.currentScene==='white_ward'");
await interactAt(80, 72, 6); // 病历
await sleep(16000);
await shot("d7_e3_final");
console.log("CONSOLE ERRORS:", errors.length);
errors.slice(0, 20).forEach((e) => console.log(" -", e));
console.log("FAILURES:", failures.length);
await browser.close();
await server.close();
process.exit(errors.length || failures.length ? 1 : 0);