tools/photo.mjs
既存の検品コード。今回の作業では実行していない
/* 見せる写真を撮る道具(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);