tools/photo.mjs

既存の検品コード。今回の作業では実行していない

127行 · 5,525 bytes · SHA-256 b4df12a4e2852e490ac6f2e3a720c0b9ea94592d0264ae0018369c235d9e3519

分割 1/1
/* 見せる写真を撮る道具(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);