/* 見せる写真を撮る道具(2026-09-13)。 npm test(tools/shot.mjs)は「崩れていないか」の検品。こちらは「どう見えるか」の写真。 iPhoneの実寸390x797と390x664で、タイトル・遊んでいる所・狙っている所・大岩・ 倒した所・精算・ショップ・アバター を撮る。 使い方: node tools/photo.mjs [--prod] */ let webkit; try { ({ webkit } = await import('playwright')); } catch (e) { ({ webkit } = await import('file:///C:/平成ゾンビ学園/node_modules/playwright/index.mjs')); } import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /* 静的ファイルは public/ が本体(Workersの[assets]と同じ) */ const pick = (u) => { const a = path.join(ROOT, 'public', u); return fs.existsSync(a) ? a : path.join(ROOT, u); }; const MIME = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json' }; const argv = process.argv.slice(2); let url = 'https://ishi-to-zombie.kaiten-sushi-online.workers.dev/', srv = null; if (!argv.includes('--prod')) { const port = 8600 + Math.floor(Math.random() * 90); srv = http.createServer((q, r) => { let u = decodeURIComponent(q.url.split('?')[0]); if (u === '/') u = '/index.html'; fs.readFile(pick(u), (e, d) => { if (e) { r.writeHead(404); r.end(); return; } r.writeHead(200, { 'Content-Type': MIME[path.extname(u).toLowerCase()] || 'application/octet-stream' }); r.end(d); }); }); await new Promise(ok => srv.listen(port, ok)); url = 'http://localhost:' + port + '/index.html'; } fs.mkdirSync(path.join(ROOT, 'tmp'), { recursive: true }); const browser = await webkit.launch(); for (const h of [797, 664]) { const page = await browser.newPage({ viewport: { width: 390, height: h } }); const errs = []; page.on('pageerror', e => errs.push(String(e.message).slice(0, 160))); await page.goto(url, { waitUntil: 'load' }); await page.waitForTimeout(900); const shot = (nm) => page.screenshot({ path: path.join(ROOT, 'tmp/photo_' + nm + '_' + h + '.png') }); await shot('title'); /* 遊んでいる所。ゾンビを並べて、カラスも1羽 */ await page.evaluate(() => { window.ZG.start(false); window.ZG.freeze(true); window.ZG.clearZ(); [2.6, 3.9, 5.2, 6.6].forEach(m => window.ZG.spawn(m, 0.45)); window.ZG.spawnCrow(4.6, 3.0); for (let i = 0; i < 40; i++) window.ZG.step(1 / 60); }); await page.waitForTimeout(400); await shot('play'); /* 狙っている所(本物のpointerで引く) */ const box = await page.locator('#stage').boundingBox(); await page.mouse.move(box.x + 250, box.y + box.height - 150); await page.mouse.down(); await page.mouse.move(box.x + 250 + 118, box.y + box.height - 150 + 128, { steps: 8 }); await page.waitForTimeout(260); await shot('aim'); await page.mouse.up(); /* 大岩をかまえた所 */ await page.evaluate(() => { window.ZG.start(false); window.ZG.freeze(true); window.ZG.clearZ(); [2.6, 3.4, 4.3].forEach(m => window.ZG.spawn(m, 0)); window.ZG.armRock(true); for (let i = 0; i < 20; i++) window.ZG.step(1 / 60); }); await page.waitForTimeout(300); await shot('rock'); /* 倒した瞬間。当たる組み合わせを先に総当たりで見つけてから撃ち直す */ const sol = await page.evaluate(() => { for (let a = 84; a >= 30; a -= 2) for (let k = 8; k >= 2; k--) if (window.ZG.trial(4.0, a, k / 10).r === 'kill') return { a, pf: k / 10 }; return null; }); await page.evaluate((sh) => { window.ZG.start(false); window.ZG.freeze(true); window.ZG.clearZ(); [2.4, 5.6].forEach(m => window.ZG.spawn(m, 0.45)); window.ZG.spawn(4.0, 0); window.ZG.throwAt(sh ? sh.a : 70, sh ? sh.pf : 0.6); for (let i = 0; i < 400; i++) { window.ZG.step(1 / 60); if (window.ZG.last() === 'kill') break; } for (let i = 0; i < 14; i++) window.ZG.step(1 / 60); }, sol); await page.waitForTimeout(400); await shot('kill'); /* 精算の画面。倒してスクラップを貯め、95秒生かしてから倒される */ await page.evaluate(() => { window.ZG.start(false); window.ZG.freeze(true); for (let i = 0; i < 24; i++) { window.ZG.clearZ(); window.ZG.spawn(3, 0); window.ZG.hitNearest('kill'); } /* ★時間を進めるには freeze を外す(凍らせたままだと生存秒が0のままになる。2026-09-13にそれで撮り直した) */ window.ZG.freeze(false); for (let i = 0; i < 60 * 95; i++) { window.ZG.step(1 / 60); if (i % 40 === 0) window.ZG.clearZ(); } window.ZG.hurt(999); for (let i = 0; i < 60; i++) window.ZG.step(1 / 60); }); await page.waitForTimeout(600); await shot('result'); /* ショップとアバター(ここだけDOM) */ await page.evaluate(() => window.ZG.title()); await page.waitForTimeout(300); await page.mouse.click(195, Math.round(h * 0.50) + 62 + 22); await page.waitForTimeout(600); await shot('shop'); await page.evaluate(() => document.getElementById('spX').click()); await page.waitForTimeout(250); await page.mouse.click(195, Math.round(h * 0.50) + 114 + 22); await page.waitForTimeout(600); await shot('avatar'); console.log('390x' + h + ' 撮った / JSエラー: ' + (errs.length ? errs.join(' / ') : 'なし')); await page.close(); } await browser.close(); if (srv) srv.close(); process.exit(0);