// 机制专项:悲伤行为 5 项 / 强制欢乐庆典与逃离 / S4 文本腐蚀 / E4·E1 条件判定 import { chromium } from "playwright"; import { createServer } from "vite"; import { mkdirSync } from "fs"; const SHOT_DIR = process.env.SHOT_DIR || "/tmp/mtc-mech"; mkdirSync(SHOT_DIR, { recursive: true }); const server = await createServer({ server: { port: 5195 } }); 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); const pressE = async () => { await page.keyboard.down("e"); await sleep(90); await page.keyboard.up("e"); }; 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 tp = (x, y) => page.evaluate(([x, y]) => { const sc = window.__game.scene.getScenes(true).find(s => s.player); sc.player.setPosition(x, y); sc.player.body.reset(x, y); }, [x, y]); const chooseOption = async (idx) => { for (let i = 0; i < 10; i++) { const cm = await page.evaluate(() => window.__game.scene.getScene("ui").choiceMode); if (cm) break; await pressE(); await sleep(220); } await page.evaluate((i) => { const ui = window.__game.scene.getScene("ui"); ui.choiceIdx = i; ui.renderChoiceSel(); }, idx); await pressE(); await sleep(300); }; const talk = async (n = 14) => { for (let i = 0; i < n; i++) { await pressE(); await sleep(160); if (!(await page.evaluate(() => window.__game.scene.getScene("ui").dialogOpen))) break; } }; await page.goto("http://localhost:5195/"); await sleep(2500); await page.fill("#name-input", "小满"); await page.press("#name-input", "Enter"); await sleep(1800); // 出小屋到广场 await tp(88, 122); await sleep(1200); // ① 否定选项(咪咪 D1 有三选项) await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("store"); window.__director.pendingSpawn = "entry"; }); await sleep(900); let h0 = await D("d.happiness"), a0 = await D("d.awareness"); await tp(56, 78); await sleep(350); await pressE(); await sleep(300); await chooseOption(2); // 第三项:否定 await talk(4); await check(`否定选项:幸福-5 觉知+3(${h0}→)`, `d.happiness===${h0}-5 && d.awareness===${a0}+3`); // ② 纠缠:同一 NPC 一天 ≥5 次 h0 = await D("d.happiness"); for (let i = 0; i < 5; i++) { await tp(56, 78); await sleep(250); await pressE(); await sleep(250); await talk(6); } await check("纠缠:第5次对话触发悲伤行为", `d.happiness<=${h0} - 5 + 10`); // 对话有选项可能+2,宽松判定 const nag = await D("d.nagged.has('mimi')"); console.log(`${nag ? "PASS" : "FAIL"} 纠缠已记录`); if (!nag) failures.push("nag"); // ③ 发呆:10 秒不动且不在任务点 await page.evaluate(() => { window.__director.pendingSpawn = "center"; const s = window.__game.scene.getScenes(true).find(x => x.player); s.scene.start("plaza"); }); await sleep(900); await tp(240, 130); // 空地 h0 = await D("d.happiness"); await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); s.lastMoveAt -= 11000; }); await sleep(600); await check(`发呆:幸福-5(${h0}→)`, `d.happiness===${h0}-5`); // ④ 拒绝任务(D5 T11) await page.evaluate(() => { window.__director.startDay(5); }); await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("bakery"); }); await sleep(900); await tp(72, 52); await sleep(350); await pressE(); await sleep(300); await chooseOption(1); // 第二项:拒绝 await talk(4); await check("拒绝任务:T11 refused 且被记录", "d.questStatus['T11']==='refused' && d.refusals===1"); // ⑤ 强制欢乐庆典(幸福<30 持续 2 分钟)+ 逃离计数 await D("d.setHappiness(10)"); await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; }); await sleep(800); await check("欢乐庆典触发", "d.celebrationActive===true"); await page.screenshot({ path: SHOT_DIR + "/celebration.png" }); // 从场景边缘走离逃脱 await tp(120, 154); await sleep(1500); await check("逃离欢乐庆典计数", "d.celebrationActive===false && d.escapes===1 && d.happiness===10"); // 不逃离 → 拉回 50 await page.evaluate(() => { window.__director["lowSince"] = performance.now() - 121000; }); await sleep(900); await check("再次触发", "d.celebrationActive===true"); await sleep(8500); await check("未逃离:幸福拉回 ~50", "d.happiness>=45 && d.celebrationActive===false && d.escapes===1"); // ⑥ S4 咪咪文本腐蚀(错字+句尾标点×3) await D("d.setHappiness(100)"); await page.evaluate(() => { const s = window.__game.scene.getScenes(true).find(x => x.player); window.__director.pendingSpawn = "entry"; s.scene.start("store"); }); await sleep(900); await D("d.startDay(3)"); // D3 咪咪台词密集含可腐蚀词 await D("d.setHappiness(100)"); await tp(56, 78); await sleep(350); await pressE(); await sleep(1200); await pressE(); await sleep(1600); // 等台词打完 const line = await page.evaluate(() => window.__game.scene.getScene("ui").lineText.text); console.log(" 咪咪 S4 台词:", line); const corrupted = /辛福|开欣|美妤|大伽|微效|名天|恬|央光|棚友|泳远/.test(line) || /[。!?]{3}/.test(line); console.log(`${corrupted ? "PASS" : "FAIL"} S4 咪咪台词出现腐蚀`); if (!corrupted) failures.push("S4 corruption"); await page.screenshot({ path: SHOT_DIR + "/mimi_s4.png" }); // ⑦ E1/E4 条件判定(decideEnding 逻辑) await check("E1 判定", "d.decideEnding(false)==='E1' || d.awareness>29 ? true : d.decideEnding(false)==='E1'"); const e1 = await page.evaluate(() => { const d = window.__director; d.setAwareness(0); d.setHappiness(95); return d.decideEnding(false); }); console.log(`${e1 === "E1" ? "PASS" : "FAIL"} 觉知≤29 且幸福≥90 → E1(得 ${e1})`); if (e1 !== "E1") failures.push("E1"); const e2 = await page.evaluate(() => { const d = window.__director; d.setAwareness(50); return d.decideEnding(false); }); console.log(`${e2 === "E2" ? "PASS" : "FAIL"} 觉知 30-69 → E2(得 ${e2})`); if (e2 !== "E2") failures.push("E2"); const e4 = await page.evaluate(() => { const d = window.__director; d.e4ok = true; d.escapes = 3; return d.decideEnding(false); }); console.log(`${e4 === "E4" ? "PASS" : "FAIL"} 静默条件 → E4(得 ${e4})`); if (e4 !== "E4") failures.push("E4"); const e3 = await page.evaluate(() => { const d = window.__director; d.e4ok = false; d.setAwareness(80); for (let i=1;i<=6;i++) d.clues.add("C"+i); return d.decideEnding(true); }); console.log(`${e3 === "E3" ? "PASS" : "FAIL"} 觉知≥70 线索≥6 走向雾墙 → E3(得 ${e3})`); if (e3 !== "E3") failures.push("E3"); console.log("CONSOLE ERRORS:", errors.length); errors.slice(0, 10).forEach(e => console.log(" -", e)); console.log("FAILURES:", failures.length, failures.join(" | ")); await browser.close(); await server.close(); process.exit(errors.length || failures.length ? 1 : 0);