import puppeteer from 'puppeteer-core'; import { spawn } from 'node:child_process'; import net from 'node:net'; const preview = spawn('npx', ['vite','preview','--host','127.0.0.1','--port','4815','--strictPort'], { stdio:'ignore' }); process.on('exit', () => { try{preview.kill()}catch{} }); for (let i=0;i<50;i++){ const ok = await new Promise(r=>{const s=net.connect(4815,'127.0.0.1');s.once('connect',()=>{s.end();r(true)});s.once('error',()=>r(false))}); if(ok)break; await new Promise(r=>setTimeout(r,300)); } const browser = await puppeteer.launch({ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', headless: true, args: ['--autoplay-policy=no-user-gesture-required','--mute-audio'] }); const page = await browser.newPage(); await page.setViewport({ width: 960, height: 560 }); page.on('pageerror', e => console.log('[pageerror]', (e.stack||e.message).slice(0,400))); await page.goto('http://127.0.0.1:4815/', { waitUntil: 'networkidle2' }); await page.waitForFunction('window.__game && window.__director', { timeout: 20000 }); await page.waitForFunction(() => window.__game.scene.isActive('TitleScene'), { timeout: 20000 }); await page.evaluate(() => { document.querySelector('#game input[type="text"]').closest('div').querySelector('button').click(); }); await page.waitForFunction(() => window.__game.scene.isActive('HouseScene'), { timeout: 15000 }); const sleep = (ms) => new Promise(r=>setTimeout(r,ms)); const tap = async (code, hold=90) => { await page.keyboard.down(code); await sleep(hold); await page.keyboard.up(code); await sleep(30); }; await sleep(400); // 记录对话开关 await page.evaluate(() => { window.__dlgLog = []; const d = window.__director; const origShow = d.showDialogue.bind(d); d.showDialogue = (p) => { window.__dlgLog.push(`OPEN 「${(p.lines&&p.lines[0]||'').slice(0,18)}」`); return origShow(p); }; const origRes = d.resolveDialogue.bind(d); d.resolveDialogue = (c) => { window.__dlgLog.push('CLOSE'); return origRes(c); }; const sm = window.__game.scene; for (const sc of sm.getScenes(true)) { const k = sc.scene.key; if (k !== 'UIScene' && k !== 'PlazaScene') sm.stop(k); } sm.start('PlazaScene', { spawn: 'default' }); }); await page.waitForFunction(() => window.__game.scene.isActive('PlazaScene'), { timeout: 9000 }); await sleep(500); // 清残留对话 for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } await page.evaluate(() => { window.__dlgLog.length = 0; }); // 传送到雾门正上方(触发圈内 dist≈10),模拟玩家从上方走近雾门后松开按键 const info = await page.evaluate(() => { const s = window.__game.scene.getScene('PlazaScene'); const d = s.doors.find(d => d.target === 'fog'); s.player.setPosition(d.x, d.y - 10 + 8); // 测量点 player.y-8 → py = d.y-10 s.player.setVelocity(0, 0); return { door: { x: d.x, y: d.y }, day: window.__director.day }; }); console.log('雾门位置', info.door, 'day', info.day); // 等待劝离对话自动弹出(靠近即触发,无需按键) await sleep(1500); const afterTrigger = await page.evaluate(() => window.__dlgLog.filter(l => l.startsWith('OPEN')).length); console.log('触发后 OPEN 次数:', afterTrigger); if (afterTrigger !== 1) { console.log('FAIL: 劝离对话应恰好弹出 1 次'); process.exit(1); } // 按 E 关闭对话(打字机:第一下补全文字,第二下才关闭;canUseDoor 在对话关闭后才返回并执行推离) for (let i = 0; i < 6; i++) { const o = await page.evaluate(() => { const u = window.__game.scene.getScene('UIScene'); return !!(u && u.payload); }); if (!o) break; await tap('KeyE'); await sleep(250); } await sleep(400); const afterClose = await page.evaluate(() => { const s = window.__game.scene.getScene('PlazaScene'); const d = s.doors.find(d => d.target === 'fog'); const py = s.player.y - 8; return { dist: Math.hypot(s.player.x - d.x, py - d.y).toFixed(1) }; }); console.log('关闭并推离后玩家距门:', afterClose.dist); if (parseFloat(afterClose.dist) <= 14) { console.log('FAIL: 劝离推离后玩家仍在触发圈内 dist=' + afterClose.dist); process.exit(1); } // 静止观察 5 秒,不得重开 await sleep(5000); const final = await page.evaluate(() => window.__dlgLog); console.log('最终日志:', final); const opens = final.filter(l => l.startsWith('OPEN')).length; const closes = final.filter(l => l === 'CLOSE').length; if (opens === 1 && closes >= 1) { console.log('PASS: 劝离对话只弹一次,关闭后 5 秒未重开'); } else { console.log(`FAIL: OPEN=${opens} CLOSE=${closes}`); process.exit(1); } await browser.close(); process.exit(0);