139 lines
7.3 KiB
JavaScript
139 lines
7.3 KiB
JavaScript
// 机制专项:悲伤行为 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);
|