Honey Village: six model builds + hub frontend, Dockerized for Dokploy
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
// 七日全流程自动化验收:任务/线索/悲伤行为/黄昏/梦境/结局 + 控制台零报错
|
||||
// 运行: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);
|
||||
Reference in New Issue
Block a user