22ファイル全文

以下は完全収録版の全文です。長さのため取得できない部分は、目次のファイル別・分割ページで読めます。

石とゾンビ — ChatGPT(メープル)向け最新コード一式・完全収録版
書き出し日時: 2026-09-13T20:27:28+09:00(日本時間)
正本フォルダ: C:\石とゾンビ
納品ファイル: C:\石とゾンビ_受け渡し\石とゾンビ_最新コード一式_完全収録版_20260913.txt
本番URL(K1指定): https://ishi-to-zombie.kaiten-sushi-online.workers.dev/
ローカルコードの表示版: v20260913b
収録: 現行コード・設定・仕様 21ファイル + 参考検品スクリプト1ファイル = 22ファイル全文
ソース総容量: 405,742 bytes(案内・区切りを除く)

【この資料の目的と範囲】
K1からの症状報告: 「石がゾンビに当たり、ノックバックするときの表示がラグく/カクついて見える」。
原因調査・原因の断定・修正案の評価は今回行っていない。現在のソースと関連仕様を渡すための資料。
対象端末・発生頻度・通信状態・ソロ/2人の別など、上記以外の再現条件は今回の依頼文に記載されていない。
正本は書き出し時点のローカル実ファイル。既存まとめからの転載ではなく、C:\石とゾンビから読み直した。
本番サーバーの配備コードとのバイト一致・稼働状態は照合していない。表示版だけで同一と断定しない。
コード編集・git操作・デプロイ・ゲームの再現試験は行っていない。

【読む順序】
1. この案内と索引 → public/index.html → public/lib/world.js → src/worker.js。
2. docs/K1仕様書_磨き込みとマルチ_20260913.md と docs/決めたこと.md の後半で関連仕様を確認。
3. 必要に応じて描画ライブラリ・開発道具・検品コード・Matter.js本体を読む。
各 BEGIN FILE ブロックは元ファイル全文。コードへ行番号・省略記号・整形・書換えを加えていない。
索引の「元ファイル:行」は元ファイル内の1始まりの行番号。目次の「本文開始行」はこのテキスト内の行番号。
各ブロックの Source-Bytes は原文部分のUTF-8バイト数。原文末尾の後に区切り用LFを1個だけ追加する。
改行形式・BOM・コメント・圧縮済みの長い行も原文のまま保持。原文バイト列のSHA-256を併記した。
このまとめ全体の文字コードはUTF-8(先頭BOM付き)。Matter.jsの長い圧縮行も途中省略していない。

【仕様の時点を取り違えないための案内】
・初期仕様のPages公開先は旧情報。現行配置設定はwrangler.tomlのWorkers + Durable Objects + public/配信。
・初期の「負けライン到達で即終了」は、後続仕様で個別HP・取り付き・前衛/後衛へ変更されている。
・docs/決めたこと.mdには初期と移行後の記録が同居する。記録内の実測・完了表記は開発担当による過去の記録。
・以下の読み取りメモは今回の現行ソースで確認できる実装経路。設計要望の達成度・不具合原因の判定ではない。

【当たり判定・ノックバック・物理の索引】
・画角・m↔px変換: public/index.html:180 / public/index.html:188 / public/index.html:189
・Matter.js設定・床との衝突イベント: public/index.html:212 / public/index.html:216
・ゾンビの頭/胴/足の形状: public/index.html:242
・線分対円/矩形判定: public/index.html:252 / public/index.html:263
・石対ゾンビ/カラスの全判定と石の衝突後速度: public/index.html:279 / public/index.html:280
・命中のローカル/オンライン分岐と即時演出: public/index.html:319 / public/index.html:324
・投げる入力・初速・石の生成: public/index.html:463 / public/index.html:467 / public/index.html:476
・物理の固定ステップと更新順: public/index.html:547 / public/index.html:574
・ノックバック定数: public/lib/world.js:29
・命中結果の適用・ノックバック量・取り付き解除: public/lib/world.js:144
・世界の毎フレーム更新・ノックバック減衰・よろけ・前進: public/lib/world.js:194
・ゾンビ描画: public/index.html:673 / 描画ループ: public/index.html:1014
・物理エンジン配布物全文: public/lib/matter.min.js(版・ライセンスはファイル先頭にも記載)

【通信・同期・補間の索引】
・開始/接続/再接続/切断: public/index.html:353 / public/index.html:385 / public/index.html:393 / public/index.html:425
・スナップショット受信とイベント処理: public/index.html:430 / public/index.html:439
・サーバーの更新間隔: src/worker.js:150
・サーバーの命中等メッセージ処理: src/worker.js:218
・サーバーの世界更新と全端末への送信: src/worker.js:264
・送信パケットの生成: public/lib/world.js:287
・受信データの反映・位置補正: public/lib/world.js:310
・実オンライン協力の既存検品コード: tools/two.mjs / 端末内の検品口: public/index.html:1032

【現行コードの経路を辿るための読み取りメモ】
・石はpublic/index.html内のMatter.jsで動く。ゾンビはWorldのオブジェクトとして動き、石との判定はhitTestの線分判定。
・通常石の頭頂部判定は vy>0、ey<=G.hy-G.hr*0.25、vy>=abs(vx)*0.60 の全条件。大岩とカラスは別扱い。
・ノックバック適用値は kb=min(2.6,kb+1.7)、stag=0.40。World.stepでx+=kb*dt、kb*=pow(0.015,dt)などを実行する。
・投石の固定ステップは1000/60 ms、1回のphysics呼出しで最大4ステップ。描画ループのdt上限は0.05秒。
・接続前/通信なしの世界はsim='full'。接続成功のwelcome受信時はsim='motion'へ切り替える。
・オンライン命中の経路: hitTest → claim → {y:'hit',k:kind,i:tid}送信+fxHit → Game.onMsg → World.hit → tick/World.snap → applySnap → World.apply/onEvent。
・通信なしの命中経路: hitTest → claim → World.hit。同じ端末のWorld.stepとonEventが続く。
・サーバーのTICK_MS=80(設定上12.5回/秒)。tickでは実経過時間を最大0.25秒に制限してWorld.stepを呼び、snapを全接続へ送る。
・World.snapのトップレベルは y,t,q,k,o,m,z,c,p,e。各配列の列順はsnap全文を参照すること。
・World.applyのゾンビx補正は差が0.75m超なら代入、それ以外は差の34%を加算。実行箇所は受信時のapplyであり、描画フレームごとではない。
・上記は処理位置と式を記載したもの。カクつきの原因・改善策や計測結果は記載していない。

【補助ファイルと収録対象外】
・public/全8ファイル、src/全1ファイル、docs/全4ファイル、tools/全5ファイル、ルート設定3ファイルを全文収録。
・tmp/near.mjsは近距離/大岩の検品コードなので参考付録に収録。tmp/のPNGは過去の検品画像であり、画像バイナリは収録しない。
・.deploy/は.gitignore対象の旧配備用コピーで、現行のwrangler.tomlが参照するmain/assetsではない。最新版へ混在させないため対象外。
・.git/、.wrangler/、node_modules/などの履歴・生成物・キャッシュ・依存インストール先は対象外。
・正本のassets/に収録対象ファイルは無い。ゲームの描画/音生成コード自体は全文収録した。
・検品コードにはPlaywrightの共有インストール先 C:\平成ゾンビ学園\node_modules\playwright\index.mjs へのフォールバックがある。外部インストール本体は同梱しない。
・tools/play.mjs・tools/sweep.mjsには既定URLとして旧Pages先が残っている。原文を保持したため、現行配備先と同一と読まないこと。
・参考まとめ C:\石とゾンビ_受け渡し\石とゾンビ_最新コード一式_20260913.txt は保持したまま、別ファイルとしてこの完全収録版を作成した。
・参考まとめのSHA-256: 943b7ab7c91e7c81a853775e0750e64526a071f0540ee6bcefe737407f2ada41

【ファイル目次・照合情報】
01. public/index.html | 1,094行 | 51,323 bytes | 本文開始行 0000161
    更新日時(JST): 2026-09-13T19:59:37+09:00 | SHA-256: ee47bb0b20b31375359bd2e16c1c7c97b9672245ee86cfa9310d26e218ccebd8
    内容: ゲーム画面全体。HTML/CSS・入力・投石物理・当たり判定・通信・描画・音・検品口
02. public/lib/world.js | 358行 | 14,485 bytes | 本文開始行 0001267
    更新日時(JST): 2026-09-13T19:55:11+09:00 | SHA-256: 24dcc8b97f3be7ae17e83bbe6d2728d4834776bfe526e37cdde8410aa23cfa07
    内容: クライアント/サーバー共用の世界。ノックバック・移動・HP・報酬・状態送受信
03. src/worker.js | 349行 | 15,352 bytes | 本文開始行 0001637
    更新日時(JST): 2026-09-13T19:56:46+09:00 | SHA-256: 39727b7c8659e443ef66a0fa2a52f3ec2945df5c8298d9749de8cb5f36b74365
    内容: Workerルーティング・Game/Lobby/Meter/Bug Durable Objects・WebSocket・tick
04. wrangler.toml | 41行 | 1,274 bytes | 本文開始行 0001998
    更新日時(JST): 2026-09-13T19:33:29+09:00 | SHA-256: d47da1693a33398fb6d93cb1965f0276eea541da92abc86a219cf07301e26367
    内容: 現行Workers配置設定・静的配信・Durable Objects・ガード設定
05. package.json | 13行 | 337 bytes | 本文開始行 0002051
    更新日時(JST): 2026-09-13T19:45:28+09:00 | SHA-256: f22af21fa27825a529313dd204061e2ccff3f0eb0ac58be78644043a9c83b3a2
    内容: 開発/配備/検品コマンド定義
06. .gitignore | 4行 | 39 bytes | 本文開始行 0002076
    更新日時(JST): 2026-09-13T11:38:23+09:00 | SHA-256: debe9e6f288f82479412a140e3be7be4012f6d834a7e06f4e29942e0956105fa
    内容: 管理対象から除外するディレクトリの定義
07. docs/K1仕様書_20260913.md | 32行 | 1,891 bytes | 本文開始行 0002092
    更新日時(JST): 2026-09-13T11:09:37+09:00 | SHA-256: 78ee3d3aefdd05c81d2fa26154526d94ce0091dea0d4def692a4237148d71299
    内容: 最初のK1仕様原文。後続仕様で変更された内容も原文のまま収録
08. docs/K1仕様書_磨き込みとマルチ_20260913.md | 229行 | 12,147 bytes | 本文開始行 0002136
    更新日時(JST): 2026-09-13T19:25:14+09:00 | SHA-256: e14c116c3719b903e79ef1cd5243e686f78af189122c888e2963c0a975ffcc66
    内容: 磨き込み・2人協力・HP・ラグ対策等のK1追加仕様原文
09. docs/決めたこと.md | 146行 | 12,287 bytes | 本文開始行 0002377
    更新日時(JST): 2026-09-13T20:01:15+09:00 | SHA-256: 16dbf9bc2ef6f7f75a7cd5a4784d61d8e79764574ab47cce1a3c5b65268e9224
    内容: 初期判断と、後半の磨き込み/マルチ移行時の実装判断・数値・過去の実測記録
10. docs/担当のやることリスト.md | 30行 | 2,140 bytes | 本文開始行 0002535
    更新日時(JST): 2026-09-13T20:00:19+09:00 | SHA-256: 92c326f2f13a776f0dca579fdbdb73c0eca2aa6e91383dbd178c0b926eb622c6
    内容: 開発担当が記録した作業状況。今回Codexが再検証した結果ではない
11. public/lib/shurechara.js | 158行 | 7,373 bytes | 本文開始行 0002577
    更新日時(JST): 2026-09-13T11:14:36+09:00 | SHA-256: a012d11798a0e45c6af30f1f58339d3b96129be5cc80bbaea7c66d03726eb548
    内容: キャラクター描画ライブラリ
12. public/lib/wardrobe.js | 1,003行 | 59,103 bytes | 本文開始行 0002747
    更新日時(JST): 2026-09-13T19:53:21+09:00 | SHA-256: 215bc7c69944497987aa8e600d49517e397535d564bad08da10683d1b209cfae
    内容: 衣装・帽子・石スキンのデータ/描画
13. public/wardrobe_sheet.html | 126行 | 5,602 bytes | 本文開始行 0003762
    更新日時(JST): 2026-09-13T19:51:40+09:00 | SHA-256: a86d0c7f6c83d4f4d2037f316aee2c0e214ea181dbdac94fe751f920a3a2491f
    内容: 衣装見本ページ
14. public/sfx_volume.js | 40行 | 1,733 bytes | 本文開始行 0003900
    更新日時(JST): 2026-08-29T06:41:00+09:00 | SHA-256: 1c12200425de2c6090d7bf4e531874f46afad48fd0930c752cdb6d62a8c2a14b
    内容: 効果音音量設定コード
15. public/dev_tools.js | 1,712行 | 102,814 bytes | 本文開始行 0003952
    更新日時(JST): 2026-09-12T03:07:54+09:00 | SHA-256: c1ae188b528f8c7420d83e149ff6a0cd2a551d59d18e3d1b489a6c32d8339cf6
    内容: 共通開発道具・バグ報告等。省略せず全文収録
16. tools/shot.mjs | 128行 | 6,538 bytes | 本文開始行 0005676
    更新日時(JST): 2026-09-13T19:45:08+09:00 | SHA-256: e2e2117487f6c103b7f9f1b5440181d6dafa92fc2ec6a59c557f8f8be46bcb6b
    内容: 既存の検品コード。今回の作業では実行していない
17. tools/sweep.mjs | 129行 | 6,297 bytes | 本文開始行 0005816
    更新日時(JST): 2026-09-13T19:45:57+09:00 | SHA-256: 0fa11506fd3cf44f0850249f81305055057df4299271f60153898b8ed8a9e21f
    内容: 既存の検品コード。今回の作業では実行していない
18. tools/play.mjs | 150行 | 6,716 bytes | 本文開始行 0005957
    更新日時(JST): 2026-09-13T19:56:07+09:00 | SHA-256: 15e4c72ff3cf301a7e046af037fc89ed3354c900302ef46ed393c756e39ad1b6
    内容: 既存の検品コード。今回の作業では実行していない
19. tools/photo.mjs | 127行 | 5,525 bytes | 本文開始行 0006119
    更新日時(JST): 2026-09-13T19:59:37+09:00 | SHA-256: b4df12a4e2852e490ac6f2e3a720c0b9ea94592d0264ae0018369c235d9e3519
    内容: 既存の検品コード。今回の作業では実行していない
20. tools/two.mjs | 145行 | 7,075 bytes | 本文開始行 0006258
    更新日時(JST): 2026-09-13T19:56:57+09:00 | SHA-256: cdd77076c4c19077c1da8850d409fa2b32af130a36a1306ef0198bb9f2f9f02b
    内容: 既存の検品コード。今回の作業では実行していない
21. public/lib/matter.min.js | 6行 | 83,476 bytes | 本文開始行 0006415
    更新日時(JST): 2026-09-13T11:13:51+09:00 | SHA-256: 72d30be0f579eb02ce1e0b6f9d359a4f392e6837e5a26ba8be5dbee7f88e24ae
    内容: 実際に配置されているMatter.js 0.20.0配布物の全文(MIT表記も保持)
22. tmp/near.mjs | 37行 | 2,215 bytes | 本文開始行 0006432
    更新日時(JST): 2026-09-13T19:47:50+09:00 | SHA-256: 4cad95c7ff108f57cc7fdcd03c4011404ad9f6d342f818eb4c52faa5c728ddb3
    内容: 参考付録。tmp内の近距離投石/大岩検品スクリプト。現行配信コードではない

【原文全文】

===== BEGIN FILE 01: public/index.html =====
Source-Path: C:\石とゾンビ\public\index.html
Source-Bytes: 51323
Source-Lines: 1094
Source-LastWriteTime-JST: 2026-09-13T19:59:37+09:00
Source-SHA256: ee47bb0b20b31375359bd2e16c1c7c97b9672245ee86cfa9310d26e218ccebd8
Role: ゲーム画面全体。HTML/CSS・入力・投石物理・当たり判定・通信・描画・音・検品口
----- BEGIN ORIGINAL CONTENT -----
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="robots" content="noindex,nofollow,noarchive">
<title>石とゾンビ</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;
  -webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
html,body{height:100%}
/* 遊ぶ画面はスクロールしない(五月雨開発の決まり) */
body{background:#05070b;color:#f2ead6;overflow:hidden;overscroll-behavior:none;
  font-family:"Hiragino Kaku Gothic ProN","Yu Gothic",sans-serif}
#app{height:100dvh;display:flex;flex-direction:column;
  padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom)}
#stage{flex:1 1 auto;min-height:0;position:relative;overflow:hidden;touch-action:none}
#cv{display:block;width:100%;height:100%}
/* 着せ替えとお店だけDOM。ここは縦に流してよい(遊ぶ画面ではないので) */
.sheet{position:absolute;inset:0;z-index:20;background:#0d131a;display:none;flex-direction:column}
.sheet.on{display:flex}
.sheet .top{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:10px 12px;
  background:#16202b;border-bottom:2px solid #2b3b4a}
.sheet .top b{font-size:15px}
.sheet .top .sc{margin-left:auto;font-size:12px;color:#ffce6a;font-weight:bold}
.sheet .top .x{font-size:12px;font-weight:bold;color:#f2ead6;background:#22303d;
  border:1.5px solid #3a4d5e;border-radius:9px;padding:7px 13px}
.sheet .tabs{flex:0 0 auto;display:flex;gap:6px;padding:8px 12px 0}
.sheet .tabs button{flex:1;font-size:12px;font-weight:bold;color:#b9c6d2;background:#16202b;
  border:1.5px solid #2b3b4a;border-radius:9px 9px 0 0;padding:8px 0}
.sheet .tabs button.on{color:#0d131a;background:#ffce6a;border-color:#ffce6a}
.sheet .body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:10px 12px 24px}
.row{display:flex;align-items:center;gap:10px;background:#16202b;border:1.5px solid #263442;
  border-radius:11px;padding:8px 10px;margin-bottom:7px}
.row canvas{flex:0 0 auto;background:#0a1016;border-radius:8px}
.row .nm{font-size:13px;font-weight:bold}
.row .pr{font-size:11px;color:#9fb0c0;margin-top:2px}
.row .sp{flex:1 1 auto}
.row button{flex:0 0 auto;font-size:12px;font-weight:bold;border-radius:9px;
  padding:9px 12px 17px;border:1.5px solid #3a4d5e;color:#f2ead6;background:#22303d;min-width:76px}
.row button.buy{background:#ffce6a;border-color:#ffce6a;color:#0d131a}
.row button.on{background:#3f7d52;border-color:#3f7d52;color:#fff}
.row button.ng{background:#1b232c;border-color:#2b3b4a;color:#6d7d8c;position:relative}
.row button.ng i{position:absolute;left:0;right:0;bottom:3px;color:#c8604f;font-style:normal;
  font-size:9px;font-weight:bold;text-align:center}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}
.grid button{background:#16202b;border:2px solid #263442;border-radius:10px;padding:7px 0;
  color:#f2ead6;font-size:11px}
.grid button.on{border-color:#ffce6a}
.lab{font-size:11px;color:#9fb0c0;font-weight:bold;margin:12px 2px 6px}
</style>
</head>
<body>
<div id="app"><div id="stage">
  <canvas id="cv"></canvas>
  <div class="sheet" id="shAv">
    <div class="top"><b>アバター変更</b><span class="sc" id="avSc"></span><button class="x" id="avX">とじる</button></div>
    <div class="body" id="avBody"></div>
  </div>
  <div class="sheet" id="shShop">
    <div class="top"><b>ショップ</b><span class="sc" id="spSc"></span><button class="x" id="spX">とじる</button></div>
    <div class="tabs" id="spTabs"></div>
    <div class="body" id="spBody"></div>
  </div>
</div></div>
<script>window.HZG_BUILD='';</script>
<script src="lib/matter.min.js"></script>
<script src="lib/shurechara.js?v=20260913b"></script>
<script src="lib/world.js?v=20260913b"></script>
<script src="lib/wardrobe.js?v=20260913b"></script>
<script>
'use strict';
/* ===== 石とゾンビ =====
   正本: docs/K1仕様書_20260913.md(芯) と docs/K1仕様書_磨き込みとマルチ_20260913.md(磨き込みとマルチ)

   芯(立ち上げの日から1文字も変えていない): **上から落とす軌道を作れるか**の遊び。
   横から当てても押し戻すだけ。頭の上から急な角度で落ちてきた石だけが倒せる。

   絵は1枚も使わない。人は共有財産のシュールキャラ(lib/shurechara.js)。
   世界の中身(ゾンビ・カラス・HP・スクラップ)は lib/world.js。
   **サーバーと端末が同じ1本を回す。**石だけはここ(投げた本人の端末)で飛ばす——
   通信を待たせないため(K1「投石は操作レスポンス最優先」)。 */
var VER='v20260913b';

/* ===== 音(合成。音のファイルは持たない) ===== */
var actx=null;
function ac(){
  if(!actx){ var C=window.AudioContext||window.webkitAudioContext; if(!C) return null; actx=new C(); }
  if(actx.state==='suspended'){ try{ actx.resume(); }catch(e){} }
  return actx;
}
function tone(f1,f2,vol,dur,type){
  var c=ac(); if(!c) return;
  var o=c.createOscillator(), g=c.createGain(); o.type=type||'sine';
  o.frequency.setValueAtTime(f1,c.currentTime);
  o.frequency.exponentialRampToValueAtTime(Math.max(24,f2),c.currentTime+dur);
  g.gain.setValueAtTime(vol,c.currentTime);
  g.gain.exponentialRampToValueAtTime(0.0001,c.currentTime+dur);
  o.connect(g); g.connect(c.destination); o.start(); o.stop(c.currentTime+dur+0.03);
}
function nz(vol,dur,type,f1,f2,q){
  var c=ac(); if(!c) return;
  var n=Math.max(1,Math.floor(c.sampleRate*dur)), b=c.createBuffer(1,n,c.sampleRate), d=b.getChannelData(0);
  for(var i=0;i<n;i++) d[i]=Math.random()*2-1;
  var s=c.createBufferSource(); s.buffer=b;
  var f=c.createBiquadFilter(); f.type=type||'bandpass';
  f.frequency.setValueAtTime(f1,c.currentTime);
  if(f2) f.frequency.exponentialRampToValueAtTime(Math.max(24,f2),c.currentTime+dur);
  f.Q.value=(q==null?1:q);
  var g=c.createGain(); g.gain.setValueAtTime(vol,c.currentTime);
  g.gain.exponentialRampToValueAtTime(0.0001,c.currentTime+dur);
  s.connect(f); f.connect(g); g.connect(c.destination); s.start();
}
function seThrow(){ nz(.055,.10,'highpass',1700,3200); }
function seKill(){ tone(190,62,.20,.15,'square'); nz(.16,.20,'bandpass',430,170,.8);
  setTimeout(function(){ nz(.11,.34,'lowpass',320,90,.6); },70); }
function seKnock(){ tone(124,84,.09,.09,'triangle'); nz(.06,.10,'bandpass',820,460,1.3); }
function seGround(v){ nz(Math.min(.09,.012+v*0.00010),.07,'bandpass',1500,700,1.4); }
function seLose(){ tone(240,46,.20,.95,'sawtooth'); setTimeout(function(){ nz(.12,.6,'lowpass',260,70,.6); },90); }
function seStart(){ tone(520,790,.10,.12,'triangle'); }
function seScrap(){ tone(880,1320,.045,.07,'triangle'); }
function seRock(){ tone(70,38,.26,.55,'sawtooth'); nz(.20,.45,'lowpass',260,70,.6); }
function seRockRdy(){ tone(420,720,.07,.10,'triangle'); setTimeout(function(){ tone(720,980,.06,.10,'triangle'); },90); }
function seCrow(){ tone(1250,760,.09,.13,'sawtooth'); setTimeout(function(){ tone(980,620,.07,.11,'sawtooth'); },110); }
function seGrab(){ tone(150,96,.12,.30,'sawtooth'); nz(.09,.30,'lowpass',500,180,.7); }
function seSwap(){ tone(560,820,.08,.09,'square'); }
function seJoin(){ tone(640,960,.10,.11,'triangle'); setTimeout(function(){ tone(960,1240,.09,.12,'triangle'); },110); }

/* ===== 端末に覚えておく物 ===== */
var SAVE={ scrap:0, av:{h:0,hc:0,sk:0,w:0}, own:{c:[0],ht:[0],st:[0]}, eq:{c:0,ht:0,st:0}, best:0 };
function loadSave(){
  try{
    var j=JSON.parse(localStorage.getItem('ishizombie:v1')||'null');
    if(j){ for(var k in SAVE) if(j[k]!=null) SAVE[k]=j[k]; }
  }catch(e){}
  if(!SAVE.own) SAVE.own={c:[0],ht:[0],st:[0]};
  if(!SAVE.own.c||!SAVE.own.c.length) SAVE.own.c=[0];
  if(!SAVE.own.ht||!SAVE.own.ht.length) SAVE.own.ht=[0];
  if(!SAVE.own.st||!SAVE.own.st.length) SAVE.own.st=[0];
}
function putSave(){ try{ localStorage.setItem('ishizombie:v1',JSON.stringify(SAVE)); }catch(e){} }
loadSave();
function devId(){
  var k='ishizombie:dev';
  try{ var v=localStorage.getItem(k); if(v) return v;
    v='d'+Date.now().toString(36)+Math.random().toString(36).slice(2,7);
    localStorage.setItem(k,v); return v; }catch(e){ return 'd0'; }
}

/* ===== ゾンビの死人の肌と汚れた服 =====
   共有財産のシュールキャラの色表へ**後ろから足すだけ**。
   lib/shurechara.js そのものは1文字も直していない(あれは共有財産なので) */
var ZSKIN=['#93a97b','#a6b78e','#82996f'];
var ZSK0=SHURE.SKIN_COL.length;
SHURE.SKIN_COL.push.apply(SHURE.SKIN_COL,ZSKIN);
SHURE.WEAR_COL.push.apply(SHURE.WEAR_COL,['#5a5348','#6b6256','#4a5250','#7a6f5c']);

/* ===== ショップの品(lib/wardrobe.js)。届いていなくても遊べるようにする ===== */
var WR=window.WARDROBE||null;
if(!WR||!WR.STONES||!WR.STONES.length){
  WR={ COSTUMES:[{id:'plain',nm:'そのまま',price:0,tone:'neutral',draw:function(){}}],
       HATS:[{id:'none',nm:'なし',price:0,draw:function(){}}],
       STONES:[{id:'plain',nm:'ふつうの石',price:0,draw:function(g,r){
         g.fillStyle='#8d8e86'; g.beginPath();
         for(var i=0;i<7;i++){ var a=i/7*Math.PI*2, rr=r*(0.82+((i*5)%3)*0.13);
           if(i===0) g.moveTo(Math.cos(a)*rr,Math.sin(a)*rr); else g.lineTo(Math.cos(a)*rr,Math.sin(a)*rr); }
         g.closePath(); g.fill();
         g.fillStyle='rgba(255,255,240,.30)'; g.beginPath(); g.arc(-r*0.28,-r*0.30,r*0.34,0,7); g.fill();
       }}] };
}
function wrGet(list,i){ return list[Math.max(0,Math.min(list.length-1,i|0))]; }

/* ===== 盤面の寸法 =====
   ★長さはメートル。画面の左端が0m、右端が7.8m。px へ直すのはここだけの仕事 */
var stage=document.getElementById('stage'), cv=document.getElementById('cv'), g2=cv.getContext('2d');
var WC=ZWORLD.C;
var W=390,H=700,L={},stars=[],bldg=[],cloud=[];
function calcL(){
  var floorY=H-Math.max(64,H*0.17);
  var ppm=W/WC.VIEW_M;
  return { ppm:ppm, floorY:floorY,
    hr:0.294*ppm,                 /* シュールキャラの頭の半径(身長1.72m=5.85r) */
    stoneR:Math.max(3.2,0.11*ppm),
    maxPull:Math.min(205,H*0.34) };
}
function MX(m){ return m*L.ppm; }
function MY(m){ return L.floorY-m*L.ppm; }
function layout(){
  W=stage.clientWidth||390; H=stage.clientHeight||700;
  var dpr=Math.min(3,window.devicePixelRatio||1);
  cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr);
  g2.setTransform(dpr,0,0,dpr,0,0);
  L=calcL(); build(); makeBG();
}
function makeBG(){
  stars=[];
  for(var i=0;i<64;i++) stars.push({ x:Math.random()*W, y:Math.random()*L.floorY*0.74,
    r:0.3+Math.random()*1.1, a:0.18+Math.random()*0.55 });
  bldg=[];
  var x=-10;
  while(x<W+20){ var w=40+Math.random()*70, h=(0.9+Math.random()*1.9)*L.ppm;
    bldg.push({x:x,w:w,h:h}); x+=w+2+Math.random()*8; }
  cloud=[];
  for(var j=0;j<5;j++) cloud.push({ x:Math.random()*W, y:L.floorY*(0.16+Math.random()*0.42),
    w:70+Math.random()*130, h:10+Math.random()*16, a:0.035+Math.random()*0.045 });
}

/* ===== 石の物理(フリースローの流用。ここは1文字も作り変えていない) ===== */
var Mt=window.Matter;
var STONE={ restitution:0.46, friction:0.30, frictionAir:0.0022, density:0.0016,
            collisionFilter:{group:-1} };
var eng=null, acc=0, STEP=1000/60;
var S=[];
function build(){
  if(eng){ try{ Mt.Events.off(eng); Mt.Composite.clear(eng.world,false); Mt.Engine.clear(eng); }catch(e){} }
  eng=Mt.Engine.create();
  eng.gravity.y=(9.8*L.ppm)/1000;
  var floor=Mt.Bodies.rectangle(W/2,L.floorY+200,W*6,400,
    { isStatic:true, label:'floor', restitution:0.46, friction:0.30 });
  Mt.Composite.add(eng.world,floor);
  Mt.Events.on(eng,'collisionStart',function(ev){
    for(var i=0;i<ev.pairs.length;i++){
      var p=ev.pairs[i];
      var st=(p.bodyA.label==='stone')?p.bodyA:((p.bodyB.label==='stone')?p.bodyB:null);
      if(!st) continue;
      var v=Math.abs(st.velocity.y)*60;
      st.zgB=(st.zgB||0)+1;
      if(v>55){ seGround(v); dust(st.position.x,L.floorY,st.zgRock?7:3); }
    }
  });
  S.length=0;
}

/* ===== 世界 ===== */
var world=new ZWORLD.World({sim:'full'});
var meId='solo';
var NET={ on:false, ws:null, room:'', tries:0, mode:'off', ping:0 };

/* ===== 当たり(メートル→px) ===== */
function zGeo(z){
  var r=L.hr, feet=L.floorY, x=MX(z.x);
  var hy=feet-4.85*r, shY=hy+1.30*r, bot=shY+2.40*r;
  return { r:r, x:x, feet:feet, hy:hy, shY:shY, bot:bot, hr:r*1.04,
    bx0:x-1.16*r, bx1:x+1.16*r, by0:shY-0.10*r, by1:bot,
    lx0:x-0.66*r, lx1:x+0.80*r, ly0:bot, ly1:feet+r*0.10 };
}
/* カラスの当たりと大きさ。0.95だと夜空で小石と見分けが付かなかったので1.28にした(2026-09-13 実測) */
function cGeo(c){ return { x:MX(c.x), y:MY(c.y), r:L.hr*1.28 }; }

function segCircle(x0,y0,x1,y1,cx,cy,r){
  var dx=x1-x0, dy=y1-y0, fx=x0-cx, fy=y0-cy;
  var a=dx*dx+dy*dy, b=2*(fx*dx+fy*dy), c=fx*fx+fy*fy-r*r;
  if(a<1e-9) return c<=0?0:-1;
  var d=b*b-4*a*c; if(d<0) return -1;
  d=Math.sqrt(d);
  var t1=(-b-d)/(2*a), t2=(-b+d)/(2*a);
  if(t1>=0&&t1<=1) return t1;
  if(t1<0&&t2>=0) return 0;
  return -1;
}
function segRect(x0,y0,x1,y1,ax,ay,bx,by){
  var dx=x1-x0, dy=y1-y0, t0=0, t1=1;
  var p=[-dx,dx,-dy,dy], q=[x0-ax,bx-x0,y0-ay,by-y0];
  for(var i=0;i<4;i++){
    if(p[i]===0){ if(q[i]<0) return -1; }
    else{ var t=q[i]/p[i];
      if(p[i]<0){ if(t>t1) return -1; if(t>t0) t0=t; }
      else{ if(t<t0) return -1; if(t<t1) t1=t; } }
  }
  return t0;
}
/* ★この関数がゲームの芯★
   倒せるのは「頭の丸へ、上から、急な角度で入った石」だけ。
   それ以外(体・足・頭の横・寝た角度)は全部ノックバックで終わる。
   ただし**大岩だけは例外**で、どこに当たっても倒せる(K1指示)。
   カラスは空の敵なので、どこに当たっても落ちる(地上と別の仕事にして2人の分担を作る)。 */
var TOP_STEEP=0.60, TOP_BAND=0.25;
function hitTest(s){
  if(s.cool>0) return;
  var x0=s.px, y0=s.py, b=s.b, x1=b.position.x, y1=b.position.y;
  var vx=b.velocity.x*60, vy=b.velocity.y*60;
  var bt=2, bz=null, bk='', bc=null, i;
  for(i=0;i<world.cs.length;i++){
    var c=world.cs[i]; if(c.dead) continue;
    var G2=cGeo(c);
    var tcc=segCircle(x0,y0,x1,y1,G2.x,G2.y,G2.r+s.r);
    if(tcc>=0&&tcc<bt){ bt=tcc; bc=c; bz=null; bk='crow'; }
  }
  for(i=0;i<world.zs.length;i++){
    var z=world.zs[i]; if(z.dead) continue;
    var G=zGeo(z);
    var tc=segCircle(x0,y0,x1,y1,G.x,G.hy,G.hr+s.r);
    if(tc>=0 && tc<bt){
      var ey=y0+(y1-y0)*tc;
      var top=(vy>0 && ey<=G.hy-G.hr*TOP_BAND && vy>=Math.abs(vx)*TOP_STEEP);
      bt=tc; bz=z; bc=null; bk=(s.rock||top)?'kill':'knock';
    }
    var tb=segRect(x0,y0,x1,y1,G.bx0-s.r,G.by0-s.r,G.bx1+s.r,G.by1+s.r);
    if(tb>=0 && tb<bt){ bt=tb; bz=z; bc=null; bk=s.rock?'kill':'knock'; }
    var tl=segRect(x0,y0,x1,y1,G.lx0-s.r,G.ly0-s.r,G.lx1+s.r,G.ly1+s.r);
    if(tl>=0 && tl<bt){ bt=tl; bz=z; bc=null; bk=s.rock?'kill':'knock'; }
  }
  if(!bz&&!bc) return;
  s.cool=s.rock?0.05:0.10; LASTB=s.b.zgB||0;
  var kind=(bk==='crow')?'crow':(bk==='kill'?(s.rock?'rock':'kill'):'knock');
  claim(kind,bc?bc.id:bz.id,bc?cGeo(bc):zGeo(bz));
  if(s.rock){ Mt.Body.setVelocity(b,{ x:b.velocity.x*0.86, y:b.velocity.y*0.86 }); }
  else if(bk==='kill'||bk==='crow'){
    Mt.Body.setVelocity(b,{ x:b.velocity.x*0.35, y:Math.abs(b.velocity.y)*0.30 });
  }else{
    Mt.Body.setVelocity(b,{ x:-Math.abs(b.velocity.x)*0.30, y:b.velocity.y*0.25-0.5 });
  }
}
/* 当たったと**自分の端末が決めた**結果を、その場で見せつつサーバーへ渡す。
   K1「自分では明らかに当たったのに何も起きない、は極力避ける」——だから先に見せる。
   通信している時はサーバーが正なので、次に届いた中身で静かに揃う */
function claim(kind,tid,G){
  LASTEV=(kind==='knock')?'knock':'kill';
  if(NET.on){ netSend({y:'hit',k:kind,i:tid}); fxHit(kind,G); }
  else world.hit(meId,kind,tid);
}
function fxHit(kind,G){
  if(kind==='knock'){ dust(G.x-L.hr*0.4,(G.shY+G.bot)/2,5); seKnock(); }
  else if(kind==='crow'){ burst(G.x,G.y,12,'#3a3f46'); burst(G.x,G.y,6,'#8f98a2'); seCrow(); }
  else{ burst(G.x,G.hy,16,'#9fbf83'); burst(G.x,G.hy,8,'#d8cf9e'); seKill(); }
}

/* ===== 粒と浮き文字 ===== */
var P=[], FL=[];
function burst(x,y,n,col){
  for(var i=0;i<n;i++) P.push({ x:x, y:y, vx:(Math.random()*2-1)*200, vy:-Math.random()*250-40,
    t:0, life:0.75+Math.random()*0.35, col:col, r:1+Math.random()*2.3 });
}
function dust(x,y,n){
  for(var i=0;i<n;i++) P.push({ x:x, y:y, vx:(Math.random()*2-1)*70, vy:-Math.random()*70,
    t:0, life:0.35+Math.random()*0.25, col:'rgba(190,180,150,0.7)', r:1+Math.random()*1.6 });
}
function floatUp(x,y,s,col){ FL.push({ x:x, y:y, s:s, t:0, col:col||'#ffce6a' }); }

/* ===== 遊びの流れ ===== */
var scr='title';
var throws=0, overT=0, pop=0, result=null;
var FREEZE=false, RENDER=true, LASTEV='', LASTB=0;
var joinMsg=0, netMsg='';

function myP(){ return world.find(meId); }
function myX(){ var p=myP(); return p?world.xOf(p.id):WC.SLOT_X[0]; }
function handX(){ return MX(myX()+WC.HAND_DX); }
function handY(){ return MY(WC.HAND_Y); }

function startGame(){
  scr='play'; throws=0; overT=0; pop=0; result=null;
  P.length=0; FL.length=0; S.length=0; build();
  world=new ZWORLD.World({sim:'full'});
  meId='solo';
  world.addPlayer(meId,SAVE.av,SAVE.eq);
  world.ev.length=0;
  rockArm=false; arm=62; armT=0; cool=0; shake=0;
  seStart();
  netStart();
}
function toTitle(){
  netStop();
  scr='title'; result=null;
  world=new ZWORLD.World({sim:'full'});
}
function endGame(){
  if(scr!=='play') return;
  var p=myP();
  var pay=p?world.payout(p):{base:0,mult:world.mult(),total:0};
  result={ base:pay.base, mult:pay.mult, total:pay.total, kills:world.kills, t:world.t };
  SAVE.scrap+=pay.total;
  if(world.kills>SAVE.best) SAVE.best=world.kills;
  putSave();
  scr='result'; overT=0; seLose();
  netStop();
}

/* ===== 通信 =====
   「はじめる」で**即**遊び始め、裏で相手を探す。繋がったらその場で世界を預ける。
   繋がらなければそのままひとりで遊ぶ(遊びは絶対に止めない) */
function netSend(o){ if(NET.ws&&NET.ws.readyState===1){ try{ NET.ws.send(JSON.stringify(o)); }catch(e){} } }
function netStart(){
  NET.mode='finding'; NET.tries=0; netMsg='';
  if(location.protocol==='file:'){ NET.mode='off'; return; }
  fetch('/api/join',{method:'POST'}).then(function(r){ return r.json(); }).then(function(j){
    if(!j||j.busy||!j.room){ NET.mode='off'; if(j&&j.busy){ netMsg='ただいま混み合っています'; joinMsg=2.5; } return; }
    NET.room=j.room; netOpen();
  }).catch(function(){ NET.mode='off'; });
}
function netOpen(){
  if(scr!=='play') return;
  var pr=(location.protocol==='https:')?'wss:':'ws:';
  var ws;
  try{ ws=new WebSocket(pr+'//'+location.host+'/ws?r='+NET.room+'&id='+encodeURIComponent(devId())); }
  catch(e){ NET.mode='off'; return; }
  NET.ws=ws;
  ws.onopen=function(){ NET.tries=0; sendLook();
    if(NET.ping) clearInterval(NET.ping);
    NET.ping=setInterval(function(){ netSend({y:'ping',t:Date.now()}); },8000); };
  ws.onmessage=function(ev){
    var d; try{ d=JSON.parse(ev.data); }catch(e){ return; }
    if(d.y==='full'){ NET.on=false; NET.mode='finding'; setTimeout(netStart,700); return; }
    if(d.y==='w'){
      meId=d.me; NET.on=true; NET.mode='online';
      world.sim='motion';
      applySnap(d.snap);
      sendLook();
      return;
    }
    if(d.y==='s') applySnap(d);
  };
  ws.onclose=function(e){
    NET.on=false;
    if(NET.ping){ clearInterval(NET.ping); NET.ping=0; }
    if(e&&e.code===4001) return;
    if(scr!=='play'){ NET.mode='off'; return; }
    NET.tries++;
    if(NET.tries<3) setTimeout(netOpen,700*NET.tries);
    else{ NET.mode='off'; world.sim='full'; netMsg='ひとりで続けます'; joinMsg=2.2; }
  };
}
function netStop(){
  NET.on=false; NET.mode='off';
  if(NET.ping){ clearInterval(NET.ping); NET.ping=0; }
  if(NET.ws){ try{ NET.ws.close(1000,'bye'); }catch(e){} NET.ws=null; }
}
function applySnap(s){
  var before=world.ps.length;
  var ev=world.apply(s,meId);
  for(var i=0;i<ev.length;i++) onEvent(ev[i]);
  if(world.ps.length>before && before>0){ seJoin(); netMsg='相方が来た'; joinMsg=2.2; }
  else if(world.ps.length<before){ netMsg='相方が抜けた'; joinMsg=2.2; }
  var p=myP();
  if(world.over || !p || !p.alive) endGame();
}
function onEvent(e){
  var z,G;
  if(e.e==='zkill'){
    z=world.zById(e.id); G=z?zGeo(z):{x:MX(e.x),hy:MY(1.42),shY:MY(1.2),bot:L.floorY};
    if(!NET.on){ burst(G.x,G.hy,16,'#9fbf83'); burst(G.x,G.hy,8,'#d8cf9e'); seKill(); }
    if(e.rock) seRock();
    floatUp(G.x,G.hy-14,'+'+e.n); pop=1; seScrap();
  }else if(e.e==='ckill'){
    if(!NET.on){ burst(MX(e.x),MY(e.y),12,'#3a3f46'); seCrow(); }
    floatUp(MX(e.x),MY(e.y)-12,'+'+e.n,'#9fd8ff'); pop=1; seScrap();
  }else if(e.e==='zknock'){
    if(!NET.on){ z=world.zById(e.id); if(z){ G=zGeo(z); dust(G.x-L.hr*0.4,(G.shY+G.bot)/2,5); } seKnock(); }
  }else if(e.e==='grab'){ seGrab(); if(e.p===meId) shake=1; }
  else if(e.e==='peck'){ if(e.p===meId) shake=0.7; seCrow(); }
  else if(e.e==='free'){ if(e.p===meId) floatUp(MX(myX()),MY(2.1),'はなれた','#9fd8ff'); }
  else if(e.e==='swap'){ seSwap(); }
  else if(e.e==='rock'){ if(e.p!==meId) seRock(); }
  else if(e.e==='down'){ if(e.p===meId) endGame(); }
  else if(e.e==='over'){ endGame(); }
  else if(e.e==='join'){ if(e.p!==meId&&NET.on){ seJoin(); netMsg='相方が来た'; joinMsg=2.2; } }
  else if(e.e==='left'){ if(e.p!==meId&&NET.on){ netMsg='相方が抜けた'; joinMsg=2.2; } }
}

/* ===== 指: 引いて離す(フリースローと同じ手触り。数字も同じ) ===== */
var V0=4.6, VR=4.6, EXP=1.15;
var aiming=false, sx=0, sy=0, ax=0, ay=0, pull=0, ang=52, arm=62, armT=0, cool=0;
var rockArm=false, shake=0;
function speedMS(){ return V0+VR*Math.pow(pull/L.maxPull,EXP); }
function aimFrom(px,py){
  var dx=sx-px, dy=sy-py;
  pull=Math.min(L.maxPull,Math.hypot(dx,dy));
  var a=Math.atan2(-dy,dx)*180/Math.PI;
  if(dx<=0 && Math.abs(a)>90) a=(a>0?180-a:-180-a);
  ang=Math.max(14,Math.min(84,a));
  ax=px; ay=py;
}
function rockLeft(){ var p=myP(); return p?Math.max(0,p.bT-world.t):WC.BOULDER_CD; }
function shoot(){
  var rock=rockArm&&rockLeft()<=0;
  if(rock){ if(NET.on) netSend({y:'rock'}); else world.useRock(meId); rockArm=false; seRock(); }
  else if(rockArm){ rockArm=false; }
  var rr=L.stoneR*(rock?3:1);
  var v=speedMS()*L.ppm*(rock?0.82:1), r=ang*Math.PI/180;
  var b=Mt.Bodies.circle(handX(),handY(),rr,Object.assign({label:'stone'},STONE));
  b.zgRock=rock?1:0;
  Mt.Body.setVelocity(b,{ x:Math.cos(r)*v/60, y:-Math.sin(r)*v/60 });
  Mt.Body.setAngularVelocity(b,rock?-0.07:-0.16);
  Mt.Composite.add(eng.world,b);
  S.push({ b:b, t:0, rest:0, cool:0, rock:rock, r:rr, px:b.position.x, py:b.position.y });
  throws++; armT=1; cool=rock?0.35:0.14; pull=0; seThrow();
}
function dropStone(s){
  try{ Mt.Composite.remove(eng.world,s.b); }catch(e){}
  var i=S.indexOf(s); if(i>=0) S.splice(i,1);
}
function canThrow(){
  var p=myP();
  return scr==='play' && cool<=0 && S.length<3 && !!p && p.alive;
}

/* ===== 押せる所(canvasの上のボタン) ===== */
var BTN=[];
function btn(id,x,y,w,h){ BTN.push({id:id,x:x,y:y,w:w,h:h}); }
function btnAt(px,py){
  for(var i=BTN.length-1;i>=0;i--){ var b=BTN[i];
    if(px>=b.x&&px<=b.x+b.w&&py>=b.y&&py<=b.y+b.h) return b.id; }
  return null;
}
function down(p){
  ac();
  var hit=btnAt(p.x,p.y);
  if(scr==='title'){
    if(hit==='start') startGame();
    else if(hit==='shop') openShop();
    else if(hit==='avatar') openAvatar();
    return;
  }
  if(scr==='result'){
    if(overT>0.5){ if(hit==='again') startGame(); else if(hit==='top') toTitle(); }
    return;
  }
  if(hit==='rock'){ if(rockLeft()<=0){ rockArm=!rockArm; seSwap(); } return; }
  if(hit==='swap'){ if(world.canSwap()){ if(NET.on) netSend({y:'swap'}); else world.swap(); } return; }
  if(!canThrow()) return;
  aiming=true; sx=p.x; sy=p.y; ax=p.x; ay=p.y; pull=0;
}
function move(p){ if(aiming) aimFrom(p.x,p.y); }
function up(){
  if(!aiming) return;
  aiming=false;
  if(pull<18){ pull=0; return; }
  if(canThrow()) shoot(); else pull=0;
}
(function(){
  var id=null;
  function pt(e){ var r=stage.getBoundingClientRect(); return { x:e.clientX-r.left, y:e.clientY-r.top }; }
  stage.addEventListener('pointerdown',function(e){
    if(id!==null) return; id=e.pointerId;
    try{ stage.setPointerCapture(id); }catch(err){}
    down(pt(e));
  });
  stage.addEventListener('pointermove',function(e){ if(e.pointerId===id) move(pt(e)); });
  function end(e){ if(e.pointerId!==id) return; up(); id=null; }
  stage.addEventListener('pointerup',end);
  stage.addEventListener('pointercancel',end);
})();

/* ===== 1コマ ===== */
function physics(dt){
  acc+=dt*1000; var n=0;
  while(acc>=STEP && n<4){
    for(var i=0;i<S.length;i++){ S[i].px=S[i].b.position.x; S[i].py=S[i].b.position.y; }
    Mt.Engine.update(eng,STEP);
    for(var j=S.length-1;j>=0;j--) hitTest(S[j]);
    acc-=STEP; n++;
  }
  if(acc>STEP*4) acc=0;
  for(var k=S.length-1;k>=0;k--){
    var s=S[k]; s.t+=dt; if(s.cool>0) s.cool-=dt;
    var p=s.b.position, v=s.b.velocity, sp=Math.hypot(v.x,v.y)*60;
    if(p.x<-40||p.x>W+80||p.y>H+90||s.t>7){ dropStone(s); continue; }
    if(sp<44 && p.y+s.r>L.floorY-3){ s.rest+=dt; if(s.rest>1.0) dropStone(s); }
    else s.rest=0;
  }
}
function stepP(dt){
  var i,q;
  for(i=P.length-1;i>=0;i--){
    q=P[i]; q.t+=dt;
    if(q.t>q.life){ P.splice(i,1); continue; }
    q.vy+=900*dt; q.x+=q.vx*dt; q.y+=q.vy*dt;
    if(q.y>L.floorY){ q.y=L.floorY; q.vy*=-0.3; q.vx*=0.6; }
  }
  for(i=FL.length-1;i>=0;i--){ FL[i].t+=dt; FL[i].y-=26*dt; if(FL[i].t>1.0) FL.splice(i,1); }
}
function step(dt){
  if(cool>0) cool-=dt;
  if(shake>0) shake-=dt*2.4;
  if(joinMsg>0) joinMsg-=dt;
  physics(dt);
  if(scr==='play'&&!FREEZE){
    var wasR=rockLeft();
    world.step(dt);
    if(!NET.on){ var ev=world.ev.slice(); world.ev.length=0; for(var i=0;i<ev.length;i++) onEvent(ev[i]); }
    else world.ev.length=0;
    if(wasR>0 && rockLeft()<=0) seRockRdy();
  }else if(scr==='result'){ overT+=dt; world.step(dt); world.ev.length=0; }
  stepP(dt);
  if(pop>0) pop-=dt*2.2;
  var want=(aiming?ang:62);
  if(armT>0){ armT-=dt*3.2; want=8; }
  arm+=(want-arm)*Math.min(1,dt*(armT>0?26:9));
}

/* ===== 絵(全部コードで描く) ===== */
function drawBG(){
  var sky=g2.createLinearGradient(0,0,0,L.floorY);
  sky.addColorStop(0,'#080c14'); sky.addColorStop(0.62,'#141d29'); sky.addColorStop(1,'#232b31');
  g2.fillStyle=sky; g2.fillRect(-20,-20,W+40,L.floorY+20);
  var mx=W*0.80, my=L.floorY*0.20, mr=Math.max(12,L.ppm*0.42);
  var gl=g2.createRadialGradient(mx,my,mr*0.5,mx,my,mr*4.6);
  gl.addColorStop(0,'rgba(226,226,200,.22)'); gl.addColorStop(1,'rgba(226,226,200,0)');
  g2.fillStyle=gl; g2.beginPath(); g2.arc(mx,my,mr*4.6,0,7); g2.fill();
  g2.fillStyle='#e8e4cc'; g2.beginPath(); g2.arc(mx,my,mr,0,7); g2.fill();
  g2.fillStyle='#141d29'; g2.beginPath(); g2.arc(mx+mr*0.42,my-mr*0.30,mr*0.88,0,7); g2.fill();
  var i;
  for(i=0;i<stars.length;i++){ var s=stars[i];
    g2.fillStyle='rgba(240,240,215,'+s.a+')'; g2.beginPath(); g2.arc(s.x,s.y,s.r,0,7); g2.fill(); }
  for(i=0;i<cloud.length;i++){ var cl=cloud[i];
    g2.fillStyle='rgba(190,200,215,'+cl.a+')';
    for(var ci=0;ci<3;ci++){ g2.beginPath();
      g2.ellipse(cl.x+(ci-1)*cl.w*0.26, cl.y+(ci===1?-cl.h*0.30:0),
        cl.w*(ci===1?0.30:0.24), cl.h*(ci===1?0.62:0.44),0,0,7); g2.fill(); } }
  for(var j=0;j<bldg.length;j++){ var b=bldg[j];
    g2.fillStyle='#0e1419'; g2.fillRect(b.x,L.floorY-b.h,b.w,b.h);
    g2.fillStyle='rgba(210,170,90,.18)';
    for(var k=0;k<3;k++) for(var m=0;m<2;m++){ if((j+k+m)%3) continue;
      g2.fillRect(b.x+6+m*(b.w*0.45),L.floorY-b.h+7+k*(b.h*0.28),b.w*0.20,b.h*0.13); } }
  g2.fillStyle='#181a14'; g2.fillRect(-20,L.floorY,W+40,H-L.floorY+20);
  g2.strokeStyle='rgba(220,210,170,.38)'; g2.lineWidth=2;
  g2.beginPath(); g2.moveTo(0,L.floorY); g2.lineTo(W,L.floorY); g2.stroke();
  g2.fillStyle='rgba(200,190,150,.10)';
  for(var n=0;n<26;n++){ var gx=(n*137)%W, gy=L.floorY+6+((n*61)%Math.max(8,(H-L.floorY)-10));
    g2.fillRect(gx,gy,3+((n*7)%9),1.5); }
}
function drawStoneAt(x,y,rot,r,rock){
  g2.save(); g2.translate(x,y); g2.rotate(rot);
  if(rock){
    g2.fillStyle='#6f6a60';
    g2.beginPath();
    for(var i=0;i<9;i++){ var a=i/9*Math.PI*2, rr=r*(0.84+((i*7)%4)*0.09);
      if(i===0) g2.moveTo(Math.cos(a)*rr,Math.sin(a)*rr); else g2.lineTo(Math.cos(a)*rr,Math.sin(a)*rr); }
    g2.closePath(); g2.fill();
    g2.fillStyle='rgba(255,250,230,.22)'; g2.beginPath(); g2.arc(-r*0.30,-r*0.32,r*0.36,0,7); g2.fill();
    g2.strokeStyle='rgba(20,16,10,.5)'; g2.lineWidth=Math.max(1,r*0.08);
    g2.beginPath(); g2.moveTo(-r*0.5,r*0.1); g2.lineTo(r*0.3,-r*0.2); g2.stroke();
  }else{
    wrGet(WR.STONES,SAVE.eq.st).draw(g2,r,rot);
  }
  g2.restore();
}
function drawPerson(p,isMe){
  var r=L.hr, feet=L.floorY, x=MX(world.xOf(p.id));
  var hy=feet-4.85*r, shY=hy+1.30*r, bot=shY+2.40*r;
  var dead=!p.alive, av=SHURE.fix(p.av||{});
  g2.save();
  if(dead) g2.globalAlpha=0.32;
  g2.strokeStyle='#33323a'; g2.lineWidth=r*0.28; g2.lineCap='round';
  g2.beginPath(); g2.moveTo(x,bot-r*0.2); g2.lineTo(x-r*0.34,feet+r*1.12); g2.stroke();
  g2.beginPath(); g2.moveTo(x,bot-r*0.2); g2.lineTo(x+r*0.42,feet+r*1.12); g2.stroke();
  SHURE.draw(g2,x,hy,r,1,av,{},bot);
  var lk=p.look||{c:0,ht:0,st:0};
  var cst=wrGet(WR.COSTUMES,lk.c); if(cst&&cst.draw) cst.draw(g2,x,shY,bot,r,SHURE.WEAR_COL[av.w],1);
  var hat=wrGet(WR.HATS,lk.ht); if(hat&&hat.draw) hat.draw(g2,x,hy,r,1,SHURE.HAIR_COL[av.hc]);
  var skin=SHURE.SKIN_COL[av.sk], ex, ey;
  if(isMe&&!dead){
    var a=arm*Math.PI/180;
    ex=x+Math.cos(-a)*2.2*r; ey=shY+Math.sin(-a)*2.2*r;
    if(canThrow()||aiming){ ex=handX(); ey=handY()+r*0.10; }
  }else{ ex=x+1.5*r; ey=shY+1.5*r; }
  g2.strokeStyle=skin; g2.lineWidth=r*0.28; g2.lineCap='round';
  g2.beginPath(); g2.moveTo(x,shY+r*0.12); g2.lineTo(ex,ey); g2.stroke();
  g2.fillStyle=skin; g2.beginPath(); g2.arc(ex,ey,r*0.20,0,7); g2.fill();
  if(isMe&&canThrow()) drawStoneAt(handX(),handY(),0,L.stoneR*(rockArm&&rockLeft()<=0?3:1),rockArm&&rockLeft()<=0);
  g2.restore();
  if(!dead){
    var bw=r*2.6, bx=x-bw/2, by=hy-r*2.0;
    g2.fillStyle='rgba(10,14,18,.75)'; g2.fillRect(bx-1,by-1,bw+2,6);
    var f=Math.max(0,Math.min(1,p.hp/WC.MAXHP));
    g2.fillStyle=(f>0.5?'#6fc07a':(f>0.22?'#e0b64a':'#d2604c'));
    g2.fillRect(bx,by,bw*f,4);
    if(isMe){ g2.strokeStyle='rgba(255,206,106,.9)'; g2.lineWidth=1; g2.strokeRect(bx-1.5,by-1.5,bw+3,7); }
  }
}
function drawZombie(z){
  var G=zGeo(z), r=G.r, x=G.x;
  g2.save();
  g2.translate(x,G.feet);
  if(z.dead){ g2.rotate(-Math.min(1.48,z.dt*4.2)); g2.globalAlpha=Math.max(0,Math.min(1,(1.5-z.dt)/0.45)); }
  else g2.rotate(z.tilt+Math.sin(z.ph*2.0)*0.035);
  g2.translate(-x,-G.feet);
  var av=SHURE.fix({h:z.look.h,hc:z.look.hc,sk:ZSK0+(z.look.sk%3),w:z.look.w});
  var skin=SHURE.SKIN_COL[av.sk];
  var sw=Math.sin(z.ph*3.0);
  g2.strokeStyle='#2f2f28'; g2.lineWidth=r*0.28; g2.lineCap='round';
  g2.beginPath(); g2.moveTo(x,G.bot-r*0.2); g2.lineTo(x-r*(0.26+0.42*Math.max(0,sw)),G.feet+r*1.10); g2.stroke();
  g2.beginPath(); g2.moveTo(x,G.bot-r*0.2); g2.lineTo(x+r*0.80,G.feet+r*1.02); g2.stroke();
  SHURE.draw(g2,x,G.hy,r,-1,av,{},G.bot);
  g2.strokeStyle=skin; g2.lineWidth=r*0.25; g2.lineCap='round';
  for(var k=0;k<2;k++){
    var dy=r*(0.06+k*0.26), hxx=x-r*(2.00+0.16*Math.sin(z.ph*2.4+k*1.7));
    g2.beginPath(); g2.moveTo(x-r*0.62,G.shY+dy); g2.lineTo(hxx,G.shY+dy+r*0.14); g2.stroke();
    g2.fillStyle=skin; g2.beginPath(); g2.arc(hxx,G.shY+dy+r*0.14,r*0.18,0,7); g2.fill();
  }
  if(!z.dead){
    g2.lineCap='butt';
    g2.strokeStyle='rgba(20,16,10,.75)'; g2.lineWidth=Math.max(3,r*0.30);
    g2.beginPath(); g2.arc(x,G.hy,r*1.16,Math.PI*1.24,Math.PI*1.76); g2.stroke();
    g2.strokeStyle=z.gr?'rgba(255,120,90,.95)':'rgba(255,214,96,.92)'; g2.lineWidth=Math.max(1.6,r*0.16);
    g2.beginPath(); g2.arc(x,G.hy,r*1.16,Math.PI*1.24,Math.PI*1.76); g2.stroke();
    g2.lineCap='round';
  }
  g2.restore();
}
function drawCrow(c){
  var G=cGeo(c), r=G.r;
  g2.save(); g2.translate(G.x,G.y);
  if(c.dead){ g2.rotate(c.dt*7); g2.globalAlpha=Math.max(0,(1.4-c.dt)/0.5); }
  var flap=Math.sin(c.ph)*0.9;
  /* 夜空に沈まないよう、体の後ろに薄い縁を敷く */
  g2.strokeStyle='rgba(150,170,190,.34)'; g2.lineWidth=2.6;
  g2.beginPath(); g2.ellipse(0,0,r*0.92,r*0.60,0,0,7); g2.stroke();
  g2.fillStyle='#2c313a';
  g2.beginPath(); g2.ellipse(0,0,r*0.85,r*0.52,0,0,7); g2.fill();
  g2.beginPath(); g2.arc(-r*0.78,-r*0.18,r*0.40,0,7); g2.fill();
  g2.fillStyle='#c8a23a';
  g2.beginPath(); g2.moveTo(-r*1.12,-r*0.16); g2.lineTo(-r*1.62,-r*0.02); g2.lineTo(-r*1.10,r*0.12);
  g2.closePath(); g2.fill();
  g2.fillStyle='#3a414b';
  g2.beginPath(); g2.moveTo(-r*0.1,-r*0.2); g2.lineTo(r*0.5,-r*0.2-flap*r*1.05); g2.lineTo(r*0.95,r*0.1);
  g2.closePath(); g2.fill();
  g2.beginPath(); g2.moveTo(-r*0.1,-r*0.2); g2.lineTo(r*0.3,-r*0.2+flap*r*0.95); g2.lineTo(r*0.75,r*0.16);
  g2.closePath(); g2.fill();
  if(!c.dead){ g2.fillStyle='#ffd24a'; g2.beginPath(); g2.arc(-r*0.86,-r*0.24,r*0.10,0,7); g2.fill(); }
  g2.restore();
}
function guide(){
  if(!aiming||pull<18) return;
  var rock=rockArm&&rockLeft()<=0;
  var v=speedMS()*L.ppm*(rock?0.82:1), r=ang*Math.PI/180;
  var vx=Math.cos(r)*v, vy=-Math.sin(r)*v, x=handX(), y=handY(), h=1/120;
  g2.fillStyle=rock?'rgba(255,190,120,.72)':'rgba(255,238,205,.60)';
  for(var i=0;i<72;i++){
    vy+=9.8*L.ppm*h; x+=vx*h; y+=vy*h;
    if(i%7===6){ g2.beginPath(); g2.arc(x,y,rock?2.8:2.0,0,7); g2.fill(); }
  }
  g2.strokeStyle='rgba(255,238,205,.28)'; g2.lineWidth=2; g2.setLineDash([4,4]);
  g2.beginPath(); g2.moveTo(handX(),handY()); g2.lineTo(handX()-(sx-ax),handY()-(sy-ay)); g2.stroke();
  g2.setLineDash([]);
  var pf=pull/L.maxPull;
  g2.fillStyle='rgba(255,244,220,.16)'; g2.fillRect(10,L.floorY+8,W-20,4);
  g2.fillStyle=rock?'#ffb05a':'#ff8a3a'; g2.fillRect(10,L.floorY+8,(W-20)*pf,4);
}
function roundRect(x,y,w,h,r){
  g2.beginPath();
  g2.moveTo(x+r,y); g2.lineTo(x+w-r,y); g2.quadraticCurveTo(x+w,y,x+w,y+r);
  g2.lineTo(x+w,y+h-r); g2.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
  g2.lineTo(x+r,y+h); g2.quadraticCurveTo(x,y+h,x,y+h-r);
  g2.lineTo(x,y+r); g2.quadraticCurveTo(x,y,x+r,y); g2.closePath();
}
/* 押せない時も**大きさは変えない**。グレーアウトして赤字を重ねる(UI憲法) */
function uiBtn(id,x,y,w,h,label,sub,state){
  btn(id,x,y,w,h);
  g2.save();
  roundRect(x,y,w,h,9);
  g2.fillStyle=(state==='off')?'rgba(24,30,38,.92)':(state==='hot'?'rgba(255,206,106,.96)':'rgba(34,48,61,.94)');
  g2.fill();
  g2.lineWidth=1.6;
  g2.strokeStyle=(state==='off')?'rgba(60,74,88,.9)':(state==='hot'?'rgba(255,206,106,1)':'rgba(92,120,144,.95)');
  g2.stroke();
  g2.textAlign='center'; g2.textBaseline='middle';
  g2.font='bold 13px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle=(state==='off')?'#6d7d8c':(state==='hot'?'#0d131a':'#f2ead6');
  g2.fillText(label,x+w/2,y+h/2-(sub?7:0));
  if(sub){
    g2.font='bold 10px "Hiragino Kaku Gothic ProN",sans-serif';
    g2.fillStyle=(state==='off')?'#c8604f':(state==='hot'?'#0d131a':'rgba(242,234,214,.72)');
    g2.fillText(sub,x+w/2,y+h/2+9);
  }
  g2.restore();
  g2.textAlign='left'; g2.textBaseline='alphabetic';
}
function drawHUD(){
  var p=myP();
  g2.textAlign='left'; g2.textBaseline='alphabetic';
  var s=1+Math.max(0,pop)*0.22;
  g2.save(); g2.translate(12,28); g2.scale(s,s);
  g2.font='bold 16px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='#f4ecd6'; g2.fillText('倒した '+world.kills,0,0);
  g2.restore();
  /* 右上: 今の区間で貯めたスクラップと生存倍率(K1「右上などに常時表示」) */
  g2.textAlign='right';
  g2.font='bold 16px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='#ffce6a'; g2.fillText('スクラップ '+(p?p.base:0),W-12,28);
  g2.font='bold 11px "Hiragino Kaku Gothic ProN",sans-serif';
  var mu=world.mult();
  g2.fillStyle=(mu>1?'#9fd8ff':'rgba(244,236,214,.5)');
  g2.fillText('生存 '+Math.floor(world.t)+'秒 ×'+mu.toFixed(1),W-12,45);
  g2.font='10px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.30)';
  g2.fillText(VER+(NET.on?(world.ps.length>=2?' 2人':' 相手待ち'):''),W-12,60);
  g2.textAlign='left';
  var by=H-52, bw=Math.min(104,(W-36)/2), bh=40;
  var left=rockLeft();
  uiBtn('rock',12,by,bw,bh,'大岩',left>0?Math.ceil(left)+'秒':(rockArm?'かまえた':'いける'),
    left>0?'off':(rockArm?'hot':'on'));
  if(world.ps.length>=2){
    uiBtn('swap',12+bw+8,by,bw,bh,'交代',world.canSwap()?'前後を入れ替え':'取り付かれ中',
      world.canSwap()?'on':'off');
  }
  if(joinMsg>0&&netMsg){
    g2.save(); g2.globalAlpha=Math.min(1,joinMsg*1.4);
    g2.textAlign='center'; g2.font='bold 14px "Hiragino Kaku Gothic ProN",sans-serif';
    g2.fillStyle='#9fd8ff'; g2.fillText(netMsg,W/2,84);
    g2.restore(); g2.textAlign='left';
  }
}
function drawTitle(){
  g2.fillStyle='rgba(6,9,14,.66)'; g2.fillRect(0,0,W,H);
  g2.textAlign='center';
  g2.font='bold 34px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='#f4ecd6'; g2.fillText('石とゾンビ',W/2,H*0.24);
  g2.font='13px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.80)';
  g2.fillText('どこでも引いて離すと石が飛ぶ',W/2,H*0.24+32);
  g2.fillText('頭の上から落とした石だけが倒せる',W/2,H*0.24+54);
  g2.fillText('横から当てても押し戻すだけ',W/2,H*0.24+76);
  g2.font='11px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(159,216,255,.85)';
  g2.fillText('はじめると、誰か来た時にそのまま2人になります',W/2,H*0.24+104);
  g2.textAlign='left';
  var bw=Math.min(250,W-60), bx=(W-bw)/2, y=H*0.50;
  uiBtn('start',bx,y,bw,52,'はじめる','','hot');
  uiBtn('shop',bx,y+62,bw,44,'ショップ','スクラップ '+SAVE.scrap,'on');
  uiBtn('avatar',bx,y+114,bw,44,'アバター変更','','on');
  g2.textAlign='center';
  g2.font='11px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.4)';
  g2.fillText('さいこう '+SAVE.best+'体 '+VER,W/2,y+186);
  g2.textAlign='left';
}
function drawResult(){
  g2.fillStyle='rgba(18,8,8,.74)'; g2.fillRect(0,0,W,H);
  var r=result||{base:0,mult:1,total:0,kills:0,t:0};
  g2.textAlign='center';
  g2.font='bold 28px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='#f0d2c4'; g2.fillText('やられた',W/2,H*0.20);
  g2.font='13px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.72)';
  g2.fillText('倒した '+r.kills+'体 / 生き残り '+Math.floor(r.t)+'秒',W/2,H*0.20+26);
  /* ★増えた理由を並べて見せる(K1「基礎→生存ボーナス×1.5→最終獲得、のように明示する」) */
  var y=H*0.33, bw=Math.min(284,W-44), bx=(W-bw)/2;
  g2.textAlign='left';
  roundRect(bx,y,bw,118,12); g2.fillStyle='rgba(12,18,24,.92)'; g2.fill();
  g2.strokeStyle='rgba(92,120,144,.6)'; g2.lineWidth=1.5; g2.stroke();
  g2.font='13px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.8)'; g2.fillText('基礎スクラップ',bx+16,y+30);
  g2.textAlign='right'; g2.fillStyle='#f4ecd6';
  g2.font='bold 15px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillText(''+r.base,bx+bw-16,y+30);
  g2.textAlign='left';
  g2.font='13px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(159,216,255,.9)'; g2.fillText('生存ボーナス',bx+16,y+58);
  g2.textAlign='right'; g2.font='bold 15px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillText('×'+r.mult.toFixed(1),bx+bw-16,y+58);
  g2.textAlign='left';
  g2.strokeStyle='rgba(92,120,144,.45)'; g2.lineWidth=1;
  g2.beginPath(); g2.moveTo(bx+14,y+72); g2.lineTo(bx+bw-14,y+72); g2.stroke();
  g2.font='bold 14px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='#ffce6a'; g2.fillText('もらった',bx+16,y+100);
  g2.textAlign='right'; g2.font='bold 22px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillText(''+r.total,bx+bw-16,y+102);
  g2.textAlign='center';
  g2.font='11px "Hiragino Kaku Gothic ProN",sans-serif';
  g2.fillStyle='rgba(244,236,214,.5)';
  g2.fillText('ぜんぶで '+SAVE.scrap+' スクラップ',W/2,y+140);
  g2.textAlign='left';
  if(overT>0.5){
    var by=y+156;
    uiBtn('again',bx,by,bw*0.56-4,46,'もう一度','','hot');
    uiBtn('top',bx+bw*0.56+4,by,bw*0.44-4,46,'トップへ','','on');
  }
}
function draw(){
  BTN.length=0;
  g2.save();
  if(shake>0) g2.translate((Math.random()*2-1)*shake*5,(Math.random()*2-1)*shake*5);
  drawBG();
  var i;
  for(i=0;i<world.zs.length;i++) if(world.zs[i].dead) drawZombie(world.zs[i]);
  for(i=0;i<world.ps.length;i++) if(!world.ps[i].alive) drawPerson(world.ps[i],world.ps[i].id===meId);
  for(i=0;i<world.zs.length;i++) if(!world.zs[i].dead) drawZombie(world.zs[i]);
  for(i=0;i<world.ps.length;i++) if(world.ps[i].alive) drawPerson(world.ps[i],world.ps[i].id===meId);
  for(i=0;i<world.cs.length;i++) drawCrow(world.cs[i]);
  for(i=0;i<S.length;i++) drawStoneAt(S[i].b.position.x,S[i].b.position.y,S[i].b.angle,S[i].r,S[i].rock);
  for(i=0;i<P.length;i++){
    var q=P[i];
    g2.globalAlpha=Math.max(0,1-q.t/q.life);
    g2.fillStyle=q.col; g2.beginPath(); g2.arc(q.x,q.y,q.r,0,7); g2.fill();
  }
  g2.globalAlpha=1;
  guide();
  g2.restore();
  g2.textAlign='center';
  for(i=0;i<FL.length;i++){
    var f=FL[i];
    g2.globalAlpha=Math.max(0,1-f.t);
    g2.font='bold 15px "Hiragino Kaku Gothic ProN",sans-serif';
    g2.fillStyle=f.col; g2.fillText(f.s,f.x,f.y);
  }
  g2.globalAlpha=1; g2.textAlign='left';
  if(scr==='play') drawHUD();
  else if(scr==='title') drawTitle();
  else if(scr==='result'){ drawHUD(); drawResult(); }
}

/* ===== 着せ替えとお店(ここだけDOM。縦に流してよい) ===== */
var shAv=document.getElementById('shAv'), shShop=document.getElementById('shShop');
function miniCanvas(w,h,fn){
  var c=document.createElement('canvas');
  var dpr=Math.min(3,window.devicePixelRatio||1);
  c.width=w*dpr; c.height=h*dpr; c.style.width=w+'px'; c.style.height=h+'px';
  var g=c.getContext('2d'); g.setTransform(dpr,0,0,dpr,0,0);
  fn(g,w,h);
  return c;
}
function drawMiniPerson(g,w,h,av0,cIdx,htIdx){
  var av=SHURE.fix(av0);
  var r=h*0.135, x=w/2, hy=h*0.30, shY=hy+1.30*r, bot=shY+2.40*r;
  g.strokeStyle='#33323a'; g.lineWidth=r*0.28; g.lineCap='round';
  g.beginPath(); g.moveTo(x,bot-r*0.2); g.lineTo(x-r*0.34,bot+r*1.3); g.stroke();
  g.beginPath(); g.moveTo(x,bot-r*0.2); g.lineTo(x+r*0.42,bot+r*1.3); g.stroke();
  SHURE.draw(g,x,hy,r,1,av,{},bot);
  var cst=wrGet(WR.COSTUMES,cIdx); if(cst&&cst.draw) cst.draw(g,x,shY,bot,r,SHURE.WEAR_COL[av.w],1);
  var hat=wrGet(WR.HATS,htIdx); if(hat&&hat.draw) hat.draw(g,x,hy,r,1,SHURE.HAIR_COL[av.hc]);
  var skin=SHURE.SKIN_COL[av.sk];
  g.strokeStyle=skin; g.lineWidth=r*0.28;
  g.beginPath(); g.moveTo(x,shY+r*0.12); g.lineTo(x+1.5*r,shY+1.5*r); g.stroke();
  g.fillStyle=skin; g.beginPath(); g.arc(x+1.5*r,shY+1.5*r,r*0.20,0,7); g.fill();
}
function openAvatar(){ shAv.classList.add('on'); renderAvatar(); }
function openShop(){ shShop.classList.add('on'); renderShop(); }
document.getElementById('avX').onclick=function(){ shAv.classList.remove('on'); };
document.getElementById('spX').onclick=function(){ shShop.classList.remove('on'); };

function renderAvatar(){
  document.getElementById('avSc').textContent='スクラップ '+SAVE.scrap;
  var b=document.getElementById('avBody');
  b.innerHTML='';
  var prev=document.createElement('div');
  prev.style.cssText='display:flex;justify-content:center;padding:6px 0 10px';
  prev.appendChild(miniCanvas(150,190,function(g,w,h){
    g.fillStyle='#0a1016'; g.fillRect(0,0,w,h);
    drawMiniPerson(g,w,h,SAVE.av,SAVE.eq.c,SAVE.eq.ht);
  }));
  b.appendChild(prev);
  var parts=[
    ['髪型','h',SHURE.HAIR_N,function(i){ return SHURE.HAIR_NM[i]; },null],
    ['髪の色','hc',8,null,SHURE.HAIR_COL],
    ['肌','sk',4,null,SHURE.SKIN_COL],
    ['服の色','w',8,null,SHURE.WEAR_COL]
  ];
  parts.forEach(function(pt){
    var lab=document.createElement('div'); lab.className='lab';
    lab.textContent=pt[0]+'(ぜんぶ無料)';
    b.appendChild(lab);
    var gd=document.createElement('div'); gd.className='grid';
    for(var i=0;i<pt[2];i++) (function(i){
      var bt=document.createElement('button');
      if(SAVE.av[pt[1]]===i) bt.className='on';
      if(pt[4]){ bt.style.background=pt[4][i]; bt.style.height='34px'; bt.textContent=''; }
      else bt.textContent=pt[3]?pt[3](i):(''+(i+1));
      bt.onclick=function(){ SAVE.av[pt[1]]=i; putSave(); renderAvatar(); sendLook(); };
      gd.appendChild(bt);
    })(i);
    b.appendChild(gd);
  });
}
var SPTAB='c';
function renderShop(){
  document.getElementById('spSc').textContent='スクラップ '+SAVE.scrap;
  var tabs=document.getElementById('spTabs');
  tabs.innerHTML='';
  [['c','衣装'],['ht','帽子'],['st','石']].forEach(function(t){
    var b=document.createElement('button'); b.textContent=t[1];
    if(SPTAB===t[0]) b.className='on';
    b.onclick=function(){ SPTAB=t[0]; renderShop(); };
    tabs.appendChild(b);
  });
  var list=(SPTAB==='c')?WR.COSTUMES:(SPTAB==='ht'?WR.HATS:WR.STONES);
  var body=document.getElementById('spBody');
  body.innerHTML='';
  list.forEach(function(it,i){
    var row=document.createElement('div'); row.className='row';
    row.appendChild(miniCanvas(54,60,function(g,w,h){
      g.fillStyle='#0a1016'; g.fillRect(0,0,w,h);
      if(SPTAB==='st'){ g.save(); g.translate(w/2,h/2); it.draw(g,16,0); g.restore(); }
      else drawMiniPerson(g,w,h,SAVE.av,SPTAB==='c'?i:SAVE.eq.c,SPTAB==='ht'?i:SAVE.eq.ht);
    }));
    var tx=document.createElement('div');
    var nm=document.createElement('div'); nm.className='nm'; nm.textContent=it.nm;
    var pr=document.createElement('div'); pr.className='pr';
    var own=SAVE.own[SPTAB].indexOf(i)>=0;
    pr.textContent=own?'持っている':(it.price+' スクラップ');
    tx.appendChild(nm); tx.appendChild(pr); row.appendChild(tx);
    var sp=document.createElement('div'); sp.className='sp'; row.appendChild(sp);
    var bt=document.createElement('button');
    if(SAVE.eq[SPTAB]===i){ bt.className='on'; bt.textContent=(SPTAB==='st')?'使用中':'着ている'; }
    else if(own){ bt.textContent=(SPTAB==='st')?'使う':'着る';
      bt.onclick=function(){ SAVE.eq[SPTAB]=i; putSave(); renderShop(); sendLook(); }; }
    else if(SAVE.scrap>=it.price){ bt.className='buy'; bt.textContent='買う';
      bt.onclick=function(){ SAVE.scrap-=it.price; SAVE.own[SPTAB].push(i); SAVE.eq[SPTAB]=i;
        putSave(); renderShop(); sendLook(); }; }
    else{ bt.className='ng'; bt.innerHTML='買う<i>たりない</i>'; }
    row.appendChild(bt);
    body.appendChild(row);
  });
}
function sendLook(){
  if(NET.ws&&NET.ws.readyState===1) netSend({y:'me',h:SAVE.av.h,hc:SAVE.av.hc,sk:SAVE.av.sk,w:SAVE.av.w,
    c:SAVE.eq.c,ht:SAVE.eq.ht,st:SAVE.eq.st});
  var p=myP(); if(p&&!NET.on){ p.av=SAVE.av; p.look=SAVE.eq; }
}

/* ===== 毎コマ ===== */
function loop(fn){
  var last=performance.now(), on=true;
  function tick(now){
    if(!on) return;
    var dt=Math.min(0.05,(now-last)/1000); last=now;
    fn(dt); requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
  document.addEventListener('visibilitychange',function(){
    if(document.hidden) on=false;
    else if(!on){ on=true; last=performance.now(); requestAnimationFrame(tick); }
  });
}
layout();
window.addEventListener('resize',function(){ layout(); });
loop(function(dt){ step(dt); if(RENDER) draw(); });

/* ===== 検品の口(tools/*.mjs から叩く。遊びには出てこない) ===== */
window.ZG={
  ver:VER,
  state:function(){ var p=myP(); return { mode:scr, scr:scr,
    kills:world.kills, throws:throws, stones:S.length, zombies:world.zs.length, crows:world.cs.length,
    players:world.ps.length, hp:p?Math.round(p.hp):0, base:p?p.base:0, mult:world.mult(),
    t:+world.t.toFixed(1), net:NET.mode, on:NET.on?1:0, room:NET.room, me:meId,
    rock:+rockLeft().toFixed(1), canSwap:world.canSwap()?1:0,
    W:W, H:H, ppm:L.ppm, floorY:L.floorY, hx:handX(), ry:handY(), px:MX(myX()),
    maxPull:L.maxPull, hr:L.hr, stoneR:L.stoneR, over:world.over?1:0,
    scrap:SAVE.scrap, result:result, wardrobe:[WR.COSTUMES.length,WR.HATS.length,WR.STONES.length] }; },
  start:function(net){ startGame(); if(net===false){ netStop(); NET.mode='off'; } },
  title:toTitle,
  offline:function(){ netStop(); NET.mode='off'; world.sim='full'; },
  freeze:function(b){ FREEZE=!!b; },
  render:function(b){ RENDER=b!==false; },
  clearZ:function(){ world.zs.length=0; world.cs.length=0; },
  spawn:function(m,spd){ world.spawnZ(m,spd==null?0:spd); return true; },
  spawnCrow:function(m,y){ var c=world.spawnC(); c.x=m; if(y!=null){ c.y=y; c.y0=y; } return true; },
  throwAt:function(a,pf){ ang=Math.max(14,Math.min(84,a)); pull=Math.max(0,Math.min(1,pf))*L.maxPull;
    cool=0; shoot(); },
  armRock:function(b){ var p=myP(); if(p) p.bT=0; rockArm=(b!==false); },
  swap:function(){ return world.canSwap()? (NET.on?(netSend({y:'swap'}),true):world.swap()) : false; },
  step:function(dt){ step(dt==null?1/60:dt); },
  last:function(){ return LASTEV; },
  can:function(){ return canThrow(); },
  zs:function(){
    var a=[];
    for(var i=0;i<world.zs.length;i++){ var z=world.zs[i]; if(z.dead) continue; var G=zGeo(z);
      a.push({ id:z.id, x:G.x, m:z.x, hy:G.hy, hr:G.hr, spd:z.spd, gr:z.gr?1:0 }); }
    a.sort(function(p,q){ return p.x-q.x; });
    return a;
  },
  /* 一番手前の1体へ、当たったことにして知らせる。石は飛ばさない。
     **本物と同じ道(claim)を通す**ので、2人で繋がっている時はサーバーへ出て返ってくる */
  hitNearest:function(kind){
    var b=null;
    for(var i=0;i<world.zs.length;i++){ var z=world.zs[i];
      if(!z.dead && (!b||z.x<b.x)) b=z; }
    if(!b) return false;
    claim(kind||'kill',b.id,zGeo(b));
    return true;
  },
  hurt:function(n){ var p=myP(); if(p){ p.hp-=n; if(p.hp<=0){ p.hp=0; p.alive=false; } } },
  /* 1回だけ撃って何が起きたかを返す(大根おろし検査) */
  trial:function(distM,a,pf,rock){
    scr='play'; FREEZE=true; netStop(); NET.on=false; NET.mode='off';
    world=new ZWORLD.World({sim:'full'}); meId='solo';
    world.addPlayer(meId,SAVE.av,SAVE.eq); world.ev.length=0;
    S.length=0; P.length=0; FL.length=0; LASTEV=''; LASTB=0; cool=0; acc=0;
    world.spawnZ(world.xOf(meId)+distM,0);
    rockArm=!!rock; if(rock) world.find(meId).bT=0;
    this.throwAt(a,pf);
    for(var i=0;i<700;i++){ step(1/60); if(LASTEV) break; if(!S.length) break; }
    FREEZE=false; rockArm=false;
    return { r:LASTEV||'miss', b:LASTB };
  }
};
</script>
<!-- 🐛報告の箱だけ相続する。fps/メモリの計器板は盤面に被るので既定で消す -->
<script>window.SETTINGS={memMeter:false};</script>
<script src="dev_tools.js?v=20260906a"></script>
</body>
</html>

----- END ORIGINAL CONTENT -----
===== END FILE 01: public/index.html =====

===== BEGIN FILE 02: public/lib/world.js =====
Source-Path: C:\石とゾンビ\public\lib\world.js
Source-Bytes: 14485
Source-Lines: 358
Source-LastWriteTime-JST: 2026-09-13T19:55:11+09:00
Source-SHA256: 24dcc8b97f3be7ae17e83bbe6d2728d4834776bfe526e37cdde8410aa23cfa07
Role: クライアント/サーバー共用の世界。ノックバック・移動・HP・報酬・状態送受信
----- BEGIN ORIGINAL CONTENT -----
/* ===== 石とゾンビ・世界の中身(2026-09-13) =====

   ★この1本を**サーバー(Durable Object)と端末の両方が回す。**二重に書かない。
     ・サーバー: `sim:'full'` で全部回す。**ここが正**(HP・死亡・撃破・スクラップ・大岩・前後交代・参加退出)
     ・端末    : `sim:'motion'` で**動きだけ**回す。ゾンビは受け取った位置の間をなめらかに進み、
                 中身はサーバーから届いた物で上書きする
     ・ひとりで遊ぶ時(通信が繋がらない時): 端末が `sim:'full'` で回す。中身は同じ

   ★長さは全部**メートル**。画面の幅が端末で違っても同じ世界になる(マルチの前提)。
     画面の左端が0m、右端が VIEW_M(7.8m)。px への直しは絵を描く側の仕事。

   ★石はここに入っていない。石は**投げた本人の端末**で飛ばす(通信を待たない)。
     当たったかどうかも本人の端末が決めて、結果だけをここへ渡す(`hit`)。
     K1「投石は操作レスポンス最優先」「自分では明らかに当たったのに何も起きない、は極力避ける」 */
(function(root){
'use strict';

var C={
  VIEW_M:7.8,               /* 画面に入る横幅(m) */
  SLOT_X:[1.48,0.60],       /* 0=前衛 / 1=後衛 の立ち位置(m)。
                             1.30/0.58だと2人の体が触れて頭のHP帯が重なった(2026-09-13 実測) */
  HAND_DX:0.30,             /* 手を離す所(立ち位置からの前) */
  HAND_Y:1.58,              /* 手を離す高さ(足もとから) */
  BODY_H:1.72,              /* 身長 */
  GRAB_GAP:0.44,            /* 前衛のこれだけ前まで来たら取り付く */

  MAXHP:100,
  DRAIN:11,                 /* 取り付かれている間、1体につき毎秒これだけ減る */
  KB:1.7, KBMAX:2.6, STAG:0.40,   /* ノックバックの強さ・上限・よろける秒数 */

  SPAWN0:4.6, SPAWNMIN:1.25, SPAWNK:0.11,
  TWOP:0.68,                /* 2人いる時は湧く間隔をこの割合にする(私が決めた) */
  SPD0:0.34, SPDK:0.013, SPDMAX:0.95,
  SPAWNX:[8.15,9.05],

  BOULDER_CD:30,            /* 大岩のクールダウン(秒) */

  CROW_FIRST:28,            /* 最初のカラスが来るまで(秒) */
  CROW_INT0:15, CROW_INTMIN:5.5, CROW_INTK:0.10,
  CROW_Y:[2.5,3.6],         /* 飛ぶ高さ(m)。山なりの邪魔をしない所 */
  CROW_SPD:0.95,
  CROW_DIVE_DMG:7,          /* 突いた時のダメージ */
  CROW_DIVE_CD:3.2,

  /* 生存時間倍率。K1の仮例(60秒1.2 / 90秒1.3 / 120秒1.5 / 以降さらに上昇)をそのまま段にした */
  MULT:[[0,1.0],[60,1.2],[90,1.3],[120,1.5],[150,1.7],[180,1.9],[210,2.1],[240,2.3],[300,2.6],[360,3.0]],

  SCRAP_Z:1,                /* ゾンビ1体 */
  SCRAP_C:3                 /* カラス1羽 */
};

function rndFn(seed){
  var s=(seed|0)||12345;
  return function(){ s^=s<<13; s^=s>>>17; s^=s<<5; s|=0; return ((s>>>0)%100000)/100000; };
}

function World(o){
  o=o||{};
  this.sim=o.sim||'full';
  this.seed=(o.seed|0)||((Math.random()*1e9)|0);
  this.rnd=rndFn(this.seed);
  this.t=0; this.nid=1; this.seq=0;
  this.zs=[]; this.cs=[]; this.ps=[];
  this.kills=0; this.spawnT=1.1; this.crowT=C.CROW_FIRST;
  this.over=false; this.ev=[];
}
World.C=C;

/* ---------- 人 ---------- */
World.prototype.addPlayer=function(id,av,look){
  if(this.find(id)) return this.find(id);
  var p={ id:id, hp:C.MAXHP, alive:true, base:0, joinT:this.t,
          av:av||{h:0,hc:0,sk:0,w:0}, look:look||{c:0,ht:0,st:0},
          bT:0, grabs:0, ping:0 };
  this.ps.push(p);
  this.ev.push({e:'join',p:id});
  return p;
};
World.prototype.rmPlayer=function(id){
  var i=this.idx(id); if(i<0) return null;
  var p=this.ps.splice(i,1)[0];
  for(var j=0;j<this.zs.length;j++) if(this.zs[j].gr===id) this.zs[j].gr=null;
  this.ev.push({e:'left',p:id});
  return p;
};
World.prototype.idx=function(id){ for(var i=0;i<this.ps.length;i++) if(this.ps[i].id===id) return i; return -1; };
World.prototype.find=function(id){ var i=this.idx(id); return i<0?null:this.ps[i]; };
/* 前衛=並びの先頭で生きている人。ゾンビはこの人を狙う */
World.prototype.front=function(){
  for(var i=0;i<this.ps.length;i++) if(this.ps[i].alive) return this.ps[i];
  return null;
};
World.prototype.slotOf=function(id){
  var n=0;
  for(var i=0;i<this.ps.length;i++){ if(this.ps[i].id===id) return n; n++; }
  return 0;
};
World.prototype.xOf=function(id){
  var s=this.slotOf(id);
  return C.SLOT_X[Math.min(s,C.SLOT_X.length-1)];
};
/* 1操作で前後を入れ替える。取り付かれている人がいる間は交代できない */
World.prototype.canSwap=function(){
  if(this.ps.length<2) return false;
  for(var i=0;i<this.zs.length;i++) if(this.zs[i].gr) return false;
  return true;
};
World.prototype.swap=function(){
  if(!this.canSwap()) return false;
  this.ps.reverse();
  this.ev.push({e:'swap'});
  return true;
};

/* ---------- ゾンビ ---------- */
World.prototype.zSpd=function(){ return Math.min(C.SPDMAX, C.SPD0+this.kills*C.SPDK); };
World.prototype.spawnZ=function(x,spd){
  var r=this.rnd;
  var z={ id:this.nid++, x:(x==null? C.SPAWNX[0]+r()*(C.SPAWNX[1]-C.SPAWNX[0]) : x),
    spd:(spd==null? this.zSpd()*(0.86+r()*0.28) : spd),
    kb:0, stag:0, gr:null, dead:0, dt:0,
    look:{ h:(r()*8)|0, hc:(r()*8)|0, sk:(r()*3)|0, w:(r()*12)|0 },
    ph:r()*6.28, tilt:(r()*2-1)*0.09 };
  this.zs.push(z);
  return z;
};
World.prototype.zById=function(id){ for(var i=0;i<this.zs.length;i++) if(this.zs[i].id===id) return this.zs[i]; return null; };
World.prototype.cById=function(id){ for(var i=0;i<this.cs.length;i++) if(this.cs[i].id===id) return this.cs[i]; return null; };

/* ---------- カラス ---------- */
World.prototype.spawnC=function(){
  var r=this.rnd;
  var c={ id:this.nid++, x:C.VIEW_M+0.6, y:C.CROW_Y[0]+r()*(C.CROW_Y[1]-C.CROW_Y[0]),
    y0:0, dead:0, dt:0, dive:0, cd:1.6, ph:r()*6.28 };
  c.y0=c.y;
  this.cs.push(c);
  return c;
};

/* ---------- 当たった、の受け口 ----------
   石が当たったかどうかは**投げた本人の端末**が決める(頭頂部の判定はそちら)。
   ここは結果を受けて、世界の中身(倒れた・押された・スクラップ)を動かすだけ。
   kind: 'kill'(頭頂部) / 'knock'(横から) / 'crow'(カラスに当てた) / 'rock'(大岩) */
World.prototype.hit=function(pid,kind,tid){
  if(this.sim!=='full') return false;
  if(kind==='crow'){
    var c=this.cById(tid);
    if(!c||c.dead) return false;
    c.dead=1; c.dt=0;
    this.award(C.SCRAP_C);
    this.ev.push({e:'ckill',id:c.id,x:c.x,y:c.y,n:C.SCRAP_C});
    return true;
  }
  var z=this.zById(tid);
  if(!z||z.dead) return false;
  if(kind==='kill'||kind==='rock'){
    z.dead=1; z.dt=0; z.gr=null; this.kills++;
    this.award(C.SCRAP_Z);
    this.ev.push({e:'zkill',id:z.id,x:z.x,n:C.SCRAP_Z,rock:kind==='rock'?1:0});
    return true;
  }
  /* 横から当たった: 押し戻すだけ。**取り付きはここで外れる**(相方の救出) */
  z.kb=Math.min(C.KBMAX,z.kb+C.KB); z.stag=C.STAG;
  if(z.gr){ this.ev.push({e:'free',p:z.gr,id:z.id}); z.gr=null; }
  this.ev.push({e:'zknock',id:z.id,x:z.x});
  return true;
};
/* スクラップは**その時いる人全員へ同じだけ**入る。キルの取り合いを作らない(K1指示)。
   参加前の分は入らない——入るのは「今いる人」だけなので、それで足りている */
World.prototype.award=function(n){
  for(var i=0;i<this.ps.length;i++) if(this.ps[i].alive) this.ps[i].base+=n;
};
World.prototype.mult=function(t){
  t=(t==null?this.t:t);
  var m=1.0;
  for(var i=0;i<C.MULT.length;i++) if(t>=C.MULT[i][0]) m=C.MULT[i][1];
  return m;
};
World.prototype.payout=function(p){
  var m=this.mult();
  return { base:p.base, mult:m, total:Math.floor(p.base*m) };
};
/* 大岩。30秒に1回・スクラップは要らない */
World.prototype.rockReady=function(p){ return this.t>=p.bT; };
World.prototype.useRock=function(pid){
  var p=this.find(pid); if(!p||!p.alive) return false;
  if(this.sim==='full'){ if(this.t<p.bT) return false; p.bT=this.t+C.BOULDER_CD; }
  else { p.bT=this.t+C.BOULDER_CD; }
  this.ev.push({e:'rock',p:pid});
  return true;
};

/* ---------- 1コマ ---------- */
World.prototype.step=function(dt){
  this.t+=dt;
  var full=(this.sim==='full');
  var i,z,c,p;

  /* 湧き(サーバー側だけ。受信側は届いた物を並べる) */
  if(full && !this.over){
    var np=0; for(i=0;i<this.ps.length;i++) if(this.ps[i].alive) np++;
    this.spawnT-=dt;
    if(this.spawnT<=0 && np>0){
      this.spawnZ();
      var iv=Math.max(C.SPAWNMIN, C.SPAWN0-this.kills*C.SPAWNK);
      if(np>=2) iv*=C.TWOP;
      this.spawnT=iv;
    }
    this.crowT-=dt;
    if(this.crowT<=0 && np>0 && this.t>=C.CROW_FIRST){
      this.spawnC();
      this.crowT=Math.max(C.CROW_INTMIN, C.CROW_INT0-this.kills*C.CROW_INTK);
    }
  }

  var fr=this.front();
  var frX=fr?this.xOf(fr.id):C.SLOT_X[0];

  /* ゾンビ */
  for(i=this.zs.length-1;i>=0;i--){
    z=this.zs[i];
    if(z.dead){ z.dt+=dt; if(z.dt>1.5) this.zs.splice(i,1); continue; }
    z.ph+=dt*(1.5+z.spd*2.2);
    if(z.kb>0.05){ z.x+=z.kb*dt; z.kb*=Math.pow(0.015,dt); }
    if(z.gr){
      /* 取り付いている。HPを削り続ける(サーバー側だけ) */
      if(full){
        p=this.find(z.gr);
        if(!p||!p.alive){ z.gr=null; }
        else{
          p.hp-=C.DRAIN*dt;
          if(p.hp<=0){ p.hp=0; p.alive=false; this.ev.push({e:'down',p:p.id}); z.gr=null; }
        }
      }
    }else if(z.stag>0){ z.stag-=dt; }
    else{
      z.x-=z.spd*dt;
      if(full && fr && z.x<=frX+C.GRAB_GAP){
        z.x=frX+C.GRAB_GAP; z.gr=fr.id;
        this.ev.push({e:'grab',p:fr.id,id:z.id});
      }
      if(!full && z.x<=frX+C.GRAB_GAP) z.x=frX+C.GRAB_GAP;
    }
    if(z.x>C.VIEW_M+2.2) z.x=C.VIEW_M+2.2;
  }

  /* カラス。左へ流れながら、下の人めがけて突きに降りる */
  for(i=this.cs.length-1;i>=0;i--){
    c=this.cs[i];
    if(c.dead){ c.dt+=dt; c.y-=0; c.y+=4.2*dt; if(c.dt>1.4) this.cs.splice(i,1); continue; }
    c.ph+=dt*9;
    if(c.dive>0){
      c.dive-=dt;
      var k=1-Math.abs(c.dive/C.CROW_DIVE_CD*2-1);        /* 0→1→0 で降りて戻る */
      c.y=c.y0-(c.y0-1.42)*Math.max(0,k);
      if(full && c.dive>C.CROW_DIVE_CD*0.45 && c.dive<C.CROW_DIVE_CD*0.55){
        p=this.front();
        if(p&&p.alive&&Math.abs(c.x-this.xOf(p.id))<0.55&&!c.bit){
          c.bit=1; p.hp-=C.CROW_DIVE_DMG;
          this.ev.push({e:'peck',p:p.id,x:c.x,y:c.y});
          if(p.hp<=0){ p.hp=0; p.alive=false; this.ev.push({e:'down',p:p.id}); }
        }
      }
      if(c.dive<=0){ c.y=c.y0; c.cd=C.CROW_DIVE_CD; c.bit=0; }
    }else{
      c.x-=C.CROW_SPD*dt;
      c.y=c.y0+Math.sin(c.ph*0.3)*0.10;
      if(c.cd>0) c.cd-=dt;
      p=this.front();
      if(full && c.cd<=0 && p && p.alive && Math.abs(c.x-this.xOf(p.id))<0.35){
        c.dive=C.CROW_DIVE_CD; c.bit=0;
      }
      if(c.x<-0.8) this.cs.splice(i,1);
    }
  }

  /* 終わり。生きている人が1人もいなくなったら世界は終わる */
  if(full && !this.over && this.ps.length>0){
    var any=false;
    for(i=0;i<this.ps.length;i++) if(this.ps[i].alive) any=true;
    if(!any){ this.over=true; this.ev.push({e:'over'}); }
  }
};

/* ---------- サーバー→端末に送る中身 ----------
   数だけの短い形にする。1秒に12回送る物なので、名前の長い鍵を使わない */
World.prototype.snap=function(){
  var z=[],c=[],p=[],i;
  for(i=0;i<this.zs.length;i++){
    var a=this.zs[i];
    z.push([a.id, +a.x.toFixed(3), +a.spd.toFixed(3), a.dead?1:0, a.gr?1:0,
            a.look.h, a.look.hc, a.look.sk, a.look.w, +a.tilt.toFixed(2)]);
  }
  for(i=0;i<this.cs.length;i++){
    var b=this.cs[i];
    c.push([b.id, +b.x.toFixed(3), +b.y.toFixed(3), b.dead?1:0, b.dive>0?1:0]);
  }
  for(i=0;i<this.ps.length;i++){
    var q=this.ps[i];
    p.push([q.id, Math.round(q.hp), q.alive?1:0, q.base, +Math.max(0,q.bT-this.t).toFixed(1),
            q.av.h,q.av.hc,q.av.sk,q.av.w, q.look.c,q.look.ht,q.look.st]);
  }
  var e=this.ev; this.ev=[];
  return { y:'s', t:+this.t.toFixed(2), q:++this.seq, k:this.kills, o:this.over?1:0,
           m:this.mult(), z:z, c:c, p:p, e:e };
};
/* 端末側: 届いた中身を今の世界へ合わせる。
   ゾンビの横位置は**飛ばさずに寄せる**(カクカクさせない。K1「ガクガクしない」)。
   離れすぎている時だけ飛ばす(入ってきた直後・裏に回って戻った時) */
World.prototype.apply=function(s,meId){
  var i,j,seen={},z;
  this.t=s.t; this.kills=s.k; this.over=!!s.o; this.seq=s.q;
  for(i=0;i<s.z.length;i++){
    var a=s.z[i]; seen[a[0]]=1;
    z=this.zById(a[0]);
    if(!z){
      z={ id:a[0], x:a[1], spd:a[2], kb:0, stag:0, gr:null, dead:0, dt:0,
          look:{h:a[5],hc:a[6],sk:a[7],w:a[8]}, ph:(a[0]*0.7)%6.28, tilt:a[9] };
      this.zs.push(z);
    }
    z.spd=a[2];
    if(Math.abs(z.x-a[1])>0.75) z.x=a[1]; else z.x+=(a[1]-z.x)*0.34;
    if(a[3]&&!z.dead){ z.dead=1; z.dt=0; }
    z.gr=a[4]?'x':null;
  }
  for(i=this.zs.length-1;i>=0;i--) if(!seen[this.zs[i].id] && !this.zs[i].dead) this.zs.splice(i,1);

  seen={};
  for(i=0;i<s.c.length;i++){
    var b=s.c[i]; seen[b[0]]=1;
    var c=this.cById(b[0]);
    if(!c){ c={ id:b[0], x:b[1], y:b[2], y0:b[2], dead:0, dt:0, dive:0, cd:0, ph:(b[0]*1.3)%6.28 }; this.cs.push(c); }
    if(Math.abs(c.x-b[1])>0.75) c.x=b[1]; else c.x+=(b[1]-c.x)*0.34;
    c.y+=(b[2]-c.y)*0.34;
    c.dive=b[4]?1:0;
    if(b[3]&&!c.dead){ c.dead=1; c.dt=0; }
  }
  for(i=this.cs.length-1;i>=0;i--) if(!seen[this.cs[i].id] && !this.cs[i].dead) this.cs.splice(i,1);

  /* 人は並び順がそのまま前衛/後衛。届いた順に作り直す */
  var np=[];
  for(i=0;i<s.p.length;i++){
    var q=s.p[i], old=this.find(q[0]);
    var pl=old||{ id:q[0], av:{h:0,hc:0,sk:0,w:0}, look:{c:0,ht:0,st:0} };
    pl.hp=q[1]; pl.alive=!!q[2]; pl.base=q[3]; pl.bT=this.t+q[4];
    pl.av={h:q[5],hc:q[6],sk:q[7],w:q[8]};
    pl.look={c:q[9],ht:q[10],st:q[11]};
    np.push(pl);
  }
  this.ps=np;
  return s.e||[];
};
/* 端末側の毎コマ: 動きだけ進める(湧かない・HPを削らない・倒さない) */

var M={ World:World, C:C };
if(typeof module!=='undefined'&&module.exports) module.exports=M;
root.ZWORLD=M;
})(typeof globalThis!=='undefined'?globalThis:this);

----- END ORIGINAL CONTENT -----
===== END FILE 02: public/lib/world.js =====

===== BEGIN FILE 03: src/worker.js =====
Source-Path: C:\石とゾンビ\src\worker.js
Source-Bytes: 15352
Source-Lines: 349
Source-LastWriteTime-JST: 2026-09-13T19:56:46+09:00
Source-SHA256: 39727b7c8659e443ef66a0fa2a52f3ec2945df5c8298d9749de8cb5f36b74365
Role: Workerルーティング・Game/Lobby/Meter/Bug Durable Objects・WebSocket・tick
----- BEGIN ORIGINAL CONTENT -----
/* ===== 石とゾンビ サーバー(Cloudflare Workers + Durable Objects) =====
   手本は C:\回転寿司オンライン(読むだけ)。

   ・1プレイ = 1部屋(Game)。**待合も部屋作りも作らない**——「はじめる」で即。
     空いている部屋があれば途中参加、無ければ新しい部屋。最大2人。
   ・**正はここ**: HP・死亡・撃破・スクラップ・大岩・前後交代・参加退出。
     石の飛びと当たり判定は投げた本人の端末(通信を待たせないため)。
   ・世界の中身は public/lib/world.js。**サーバーと端末が同じ1本を回す。**

   ★誰も繋がっていない部屋が回り続けないこと(2026-09-13 ぷっちーずで実際に起きた穴)。
     ・人が0人になったら時計を止める(setIntervalを解く)
     ・そのうえで目覚ましを3分後に仕掛け、まだ0人なら**部屋を畳んで目覚ましも消す**
     ・止まる条件を「全滅」のような遊びの中の出来事にしない */
import '../public/lib/world.js';
const ZW = globalThis.ZWORLD;

/* ===== DoSガード 盾2: 自分で止める門 =====
   Cloudflareには「ここで止める」設定が無い。使った分だけ請求が伸びる。
   ★上限は月2000万回(2026-09-13 K1指示「全てのガードを月2000万回にしてよ」
     「今開発段階やし本格化したら外せばええんやし」)。
   数えるのは変数+1だけ。1000回たまった時に1回保存。判定は**新しく繋ぐ時だけ**。
   遊んでいる最中は一度も確かめない(K1「ゲームに影響出すな」)。 */
const MONTH_LIMIT = 20000000;
const SAVE_EVERY  = 1000;
const GATE_CACHE_MS = 60000;
let hits = 0, gate = { at: 0, busy: false, n: 0 };

function ym(){ const d=new Date(); return d.getUTCFullYear()+'-'+(d.getUTCMonth()+1); }
async function saveHits(env, n){
  try{ await env.METER.get(env.METER.idFromName('m')).fetch('https://do/add?n='+n+'&m='+ym()); }catch(e){}
}
async function tooMuch(env){
  const now = Date.now();
  if (now - gate.at < GATE_CACHE_MS) return gate.busy;
  let n = 0;
  try{
    const r = await env.METER.get(env.METER.idFromName('m')).fetch('https://do/get?m='+ym());
    n = (await r.json()).n || 0;
  }catch(e){}
  gate = { at: now, busy: n >= MONTH_LIMIT, n };
  return gate.busy;
}
const J = (o, st) => new Response(JSON.stringify(o), {
  status: st||200, headers: { 'content-type':'application/json; charset=utf-8' } });

export default {
  async fetch(req, env, ctx){
    const u = new URL(req.url);
    hits++;
    if (hits >= SAVE_EVERY){ const n = hits; hits = 0; ctx.waitUntil(saveHits(env, n)); }

    if (u.pathname === '/api/meter'){
      let n=0; try{
        const r = await env.METER.get(env.METER.idFromName('m')).fetch('https://do/get?m='+ym());
        n = (await r.json()).n||0;
      }catch(e){}
      return J({ 月:ym(), 今いくつ:n, 上限:MONTH_LIMIT, 残り:Math.max(0,MONTH_LIMIT-n) });
    }

    /* ★門をかけるのは「新しく繋ぐ入口」だけ */
    const isNew = (u.pathname === '/api/join' || u.pathname === '/ws' || u.pathname === '/api/bug');
    if (isNew){
      if (env.RL){
        try{
          const ip = req.headers.get('CF-Connecting-IP') || 'x';
          const { success } = await env.RL.limit({ key: ip });
          if (!success) return J({ busy:1, why:'rate', msg:'ただいま混み合っています' }, 429);
        }catch(e){}
      }
      if (await tooMuch(env)) return J({ busy:1, why:'month', msg:'ただいま混み合っています' }, 503);
    }

    if (u.pathname === '/api/join'){
      const r = await env.LOBBY.get(env.LOBBY.idFromName('hall')).fetch('https://do/claim');
      return new Response(r.body, { status:r.status, headers:{'content-type':'application/json; charset=utf-8'} });
    }
    if (u.pathname === '/ws'){
      const room = (u.searchParams.get('r')||'').replace(/[^a-z0-9]/gi,'').slice(0,24);
      if (!room) return new Response('no room', { status:400 });
      return env.GAME.get(env.GAME.idFromName(room)).fetch(req);
    }
    if (u.pathname === '/api/bug'){
      return env.BUG.get(env.BUG.idFromName('box')).fetch(req);
    }
    /* 部屋の様子を覗く窓(点検用)。
       ★ここは**人の気配を更新しない**。見ているこちらが部屋を延命させないため
       (DoSガードの落とし穴。C:\五月雨開発\DoSガード.md に書いてある) */
    if (u.pathname === '/api/room'){
      const room = (u.searchParams.get('r')||'').replace(/[^a-z0-9]/gi,'').slice(0,24);
      if (!room) return J({ ok:0 }, 400);
      return env.GAME.get(env.GAME.idFromName(room)).fetch('https://do/count');
    }
    if (env.ASSETS) return env.ASSETS.fetch(req);
    return new Response('not found', { status:404 });
  }
};

/* ===== 使った回数を数えるだけの入れ物 ===== */
export class Meter {
  constructor(state){ this.s = state; }
  async fetch(req){
    const u = new URL(req.url), m = u.searchParams.get('m')||ym();
    const key = 'n:'+m;
    if (u.pathname === '/add'){
      const add = parseInt(u.searchParams.get('n')||'0',10)||0;
      const cur = (await this.s.storage.get(key))||0;
      await this.s.storage.put(key, cur+add);
      return J({ n: cur+add });
    }
    return J({ n: (await this.s.storage.get(key))||0 });
  }
}

/* ===== 相手探し(1個だけ) =====
   持っているのは「空いている部屋 1件」だけ。
   ★案内する前に必ず今の人数を確かめる(回転寿司の実測コメントの通り。
     確かめないと、強制終了で席が埋まったままの部屋へ送ってしまう) */
export class Lobby {
  constructor(state, env){ this.s = state; this.env = env; }
  async fetch(req){
    const u = new URL(req.url), now = Date.now();
    if (u.pathname === '/report'){
      const r = u.searchParams.get('r')||'', n = parseInt(u.searchParams.get('n')||'0',10);
      const open = u.searchParams.get('o') === '1';
      const w = await this.s.storage.get('waiting');
      if (open && n === 1) await this.s.storage.put('waiting', { id:r, at:now });
      else if (w && w.id === r) await this.s.storage.delete('waiting');
      return J({ ok:1 });
    }
    /* /claim */
    const w = await this.s.storage.get('waiting');
    if (w && now - w.at < 5*60*1000){
      let n = 2, open = false;
      try{
        const r = await this.env.GAME.get(this.env.GAME.idFromName(w.id)).fetch('https://do/count');
        const j = await r.json(); n = j.n; open = !!j.open;
      }catch(e){}
      if (open && n === 1){ await this.s.storage.delete('waiting'); return J({ room:w.id, join:1 }); }
      /* 取った直後でまだ誰も繋いでいない部屋だけ、20秒間は使い回す */
      if (n === 0 && now - w.at < 20000) return J({ room:w.id, join:0 });
      await this.s.storage.delete('waiting');
    }
    const id = 'g' + now.toString(36) + Math.random().toString(36).slice(2,6);
    await this.s.storage.put('waiting', { id, at:now });
    return J({ room:id, join:0 });
  }
}

/* ===== 1プレイ = 1部屋 ===== */
const TICK_MS   = 80;        /* 1秒に12.5回 */
const IDLE_MS   = 180000;    /* ★誰も繋がっていない状態がこれだけ続いたら畳む(3分) */
const STALE_MS  = 60000;     /* 合図が来なくなった接続を切る */
const MSG_MIN   = 45;        /* 1本の繋がりが1秒に受け付ける通の上限の元(45通/秒) */

export class Game {
  constructor(state, env){
    this.s = state; this.env = env;
    this.socks = new Set();
    this.w = null; this.timer = null; this.last = 0; this.emptyAt = Date.now();
    this.reported = -1;
    /* ★自分の部屋の名前。**内側の番号(state.id)ではなく名前**を相手探しへ伝える。
       2026-09-13、ここで番号を伝えていたせいで、相手探しが
       `idFromName(番号)` という**別の空っぽの部屋**へ案内していた
       (2人で開いても永久に合流しなかった。実測で捕まえた) */
    this.name = '';
  }
  world(){
    if (!this.w) this.w = new ZW.World({ sim:'full' });
    return this.w;
  }
  info(ws){ return ws.__zg || (ws.__zg = {}); }
  send(ws, o){ try{ ws.send(JSON.stringify(o)); }catch(e){} }
  all(o, except){ for (const w of this.socks) if (w !== except) this.send(w, o); }
  players(){ let n=0; for (const w of this.socks) if (this.info(w).pid) n++; return n; }
  isOpen(){ return this.w ? (!this.w.over && this.players() < 2) : true; }

  async fetch(req){
    const u = new URL(req.url);
    const rn = (u.searchParams.get('r')||'').replace(/[^a-z0-9]/gi,'').slice(0,24);
    if (rn) this.name = rn;
    /* tick=時計が回っているか。**人が0人なら必ずfalse**でなければならない(空の部屋が回り続けない証拠) */
    if (u.pathname === '/count') return J({ n: this.players(), open: this.isOpen(), tick: !!this.timer });
    if (req.headers.get('Upgrade') !== 'websocket') return new Response('ws only', { status:426 });

    const dev = (u.searchParams.get('id')||'').replace(/[^a-z0-9]/gi,'').slice(0,40);
    const pair = new WebSocketPair();
    const client = pair[0], server = pair[1];
    /* ★hibernationは使わない。時計(setInterval)を回し続けたいので普通のacceptにする */
    server.accept();

    /* 同じ端末の古い繋がりを先に追い出す(幽霊が席を持ったまま満席になるのを防ぐ) */
    for (const w of Array.from(this.socks)){
      if (dev && this.info(w).dev === dev){ try{ w.close(4001,'same-device'); }catch(e){} this.gone(w); }
    }
    if (!this.isOpen()){
      this.send(server, { y:'full' });
      try{ server.close(1000,'full'); }catch(e){}
      return new Response(null, { status:101, webSocket: client });
    }

    const pid = 'p' + (Date.now()%100000).toString(36) + Math.random().toString(36).slice(2,5);
    server.__zg = { pid, dev, t: Date.now(), n:0, nt:0 };
    this.socks.add(server);
    this.emptyAt = 0;

    server.addEventListener('message', (ev) => this.onMsg(server, ev.data));
    server.addEventListener('close', () => this.gone(server));
    server.addEventListener('error', () => this.gone(server));

    const w = this.world();
    w.addPlayer(pid, null, null);
    this.send(server, { y:'w', me:pid, snap:w.snap(), C:{ } });
    this.start();
    this.report();
    return new Response(null, { status:101, webSocket: client });
  }

  onMsg(ws, raw){
    const a = this.info(ws);
    /* 1本の繋がりの連打を断る(盾1の内側の守り) */
    const now = Date.now();
    if (now - a.nt > 1000){ a.nt = now; a.n = 0; }
    if (++a.n > MSG_MIN) return;
    a.t = now;
    let d; try{ d = JSON.parse(raw); }catch(e){ return; }
    const w = this.world(), p = w.find(a.pid);
    if (!p) return;
    switch (d.y){
      case 'ping': this.send(ws, { y:'pong', t:d.t }); break;
      case 'me':
        /* 見た目(アバターと買った物)。数字しか受け取らない */
        p.av = { h:(d.h|0)&7, hc:(d.hc|0)&7, sk:(d.sk|0)%4, w:(d.w|0)%8 };
        p.look = { c:Math.max(0,Math.min(80,d.c|0)), ht:Math.max(0,Math.min(40,d.ht|0)),
                   st:Math.max(0,Math.min(20,d.st|0)) };
        break;
      case 'hit': {
        const k = (d.k==='kill'||d.k==='knock'||d.k==='crow'||d.k==='rock') ? d.k : null;
        if (k) w.hit(a.pid, k, d.i|0);
        break;
      }
      case 'swap': w.swap(); break;
      case 'rock': w.useRock(a.pid); break;
    }
  }

  gone(ws){
    if (!this.socks.has(ws)) return;
    const a = this.info(ws);
    this.socks.delete(ws);
    if (this.w && a.pid) this.w.rmPlayer(a.pid);
    if (this.socks.size === 0){ this.emptyAt = Date.now(); this.stop(); }
    this.report();
  }

  start(){
    if (this.timer) return;
    this.last = Date.now();
    this.timer = setInterval(() => this.tick(), TICK_MS);
    /* ★目覚ましは「畳むため」だけに使う。遊びの進行には使わない */
    this.s.storage.setAlarm(Date.now() + IDLE_MS + 5000).catch(()=>{});
  }
  stop(){ if (this.timer){ clearInterval(this.timer); this.timer = null; } }

  tick(){
    /* ★人が0人なら1コマも進めない */
    if (this.socks.size === 0){ this.stop(); return; }
    const now = Date.now();
    const dt = Math.min(0.25, (now - this.last)/1000);
    this.last = now;
    /* 合図の来なくなった繋がりを切る */
    for (const w of Array.from(this.socks)){
      if (now - (this.info(w).t||now) > STALE_MS){ try{ w.close(1001,'stale'); }catch(e){} this.gone(w); }
    }
    if (this.socks.size === 0){ this.stop(); return; }
    const w = this.world();
    w.step(dt);
    this.all(w.snap());
    if (w.over){
      /* 終わったら部屋は使い回さない。相手探しからも外す */
      this.report(true);
    }
  }

  async report(force){
    if (!this.name) return;
    const n = this.players(), open = this.isOpen();
    const key = n + (open?'o':'x');
    if (!force && key === this.reported) return;
    this.reported = key;
    try{
      await this.env.LOBBY.get(this.env.LOBBY.idFromName('hall'))
        .fetch('https://do/report?r='+this.name+'&n='+n+'&o='+(open?1:0));
    }catch(e){}
  }

  /* ★誰も繋がっていない部屋を畳む門。ここが無いと空の部屋が回り続ける */
  async alarm(){
    if (this.socks.size > 0){
      await this.s.storage.setAlarm(Date.now() + IDLE_MS + 5000).catch(()=>{});
      return;
    }
    if (this.emptyAt && Date.now() - this.emptyAt >= IDLE_MS){
      this.stop(); this.w = null;
      await this.s.storage.deleteAll().catch(()=>{});
      await this.s.storage.deleteAlarm().catch(()=>{});
      return;
    }
    await this.s.storage.setAlarm(Date.now() + 30000).catch(()=>{});
  }
}

/* ===== バグ報告の箱(平成ゾンビ学園から相続した🐛の受け先) ===== */
const BUG_MAX = 900*1024, BUG_KEEP = 200;
export class Bug {
  constructor(state){ this.s = state; }
  async fetch(req){
    const u = new URL(req.url);
    if (req.method === 'POST'){
      const body = await req.text();
      if (body.length > BUG_MAX) return J({ ok:0, why:'big' }, 413);
      const id = Date.now().toString(36) + Math.random().toString(36).slice(2,6);
      await this.s.storage.put('b:'+id, { at:Date.now(), body });
      const list = (await this.s.storage.get('list'))||[];
      list.unshift(id);
      while (list.length > BUG_KEEP){ const old = list.pop(); await this.s.storage.delete('b:'+old); }
      await this.s.storage.put('list', list);
      return J({ ok:1, id });
    }
    const id = u.searchParams.get('id');
    if (id){
      if (u.searchParams.get('del') === '1'){
        await this.s.storage.delete('b:'+id);
        const l = ((await this.s.storage.get('list'))||[]).filter(x=>x!==id);
        await this.s.storage.put('list', l);
        return J({ ok:1 });
      }
      const r = await this.s.storage.get('b:'+id);
      if (!r) return J({ ok:0 }, 404);
      return new Response(r.body, { headers:{'content-type':'application/json; charset=utf-8'} });
    }
    const list = (await this.s.storage.get('list'))||[];
    const out = [];
    for (const x of list.slice(0,60)){
      const r = await this.s.storage.get('b:'+x);
      if (r) out.push({ id:x, at:r.at, bytes:r.body.length });
    }
    return J(out);
  }
}

----- END ORIGINAL CONTENT -----
===== END FILE 03: src/worker.js =====

===== BEGIN FILE 04: wrangler.toml =====
Source-Path: C:\石とゾンビ\wrangler.toml
Source-Bytes: 1274
Source-Lines: 41
Source-LastWriteTime-JST: 2026-09-13T19:33:29+09:00
Source-SHA256: d47da1693a33398fb6d93cb1965f0276eea541da92abc86a219cf07301e26367
Role: 現行Workers配置設定・静的配信・Durable Objects・ガード設定
----- BEGIN ORIGINAL CONTENT -----
name = "ishi-to-zombie"
main = "src/worker.js"
compatibility_date = "2026-09-01"
account_id = "e2b1d2a75fb3d207171ffbfab30ef1c1"
workers_dev = true

# 静的ファイルも同じWorkerから出す。
# ★Pagesではクラス(Durable Object)を持てないので、回転寿司オンラインと同じ形にした。
[assets]
directory = "./public"
binding   = "ASSETS"

# 1プレイ=1部屋(Game)。相手探しは1個だけのLobby。使った回数を数えるのがMeter。
[[durable_objects.bindings]]
name = "GAME"
class_name = "Game"
[[durable_objects.bindings]]
name = "LOBBY"
class_name = "Lobby"
[[durable_objects.bindings]]
name = "METER"
class_name = "Meter"
[[durable_objects.bindings]]
name = "BUG"
class_name = "Bug"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["Game", "Lobby", "Meter", "Bug"]

# ===== DoSガード(C:\五月雨開発\DoSガード.md) =====
# 盾3: 1回の呼び出しで使えるCPUの上限
[limits]
cpu_ms = 100

# 盾1: 連打を断る。workers.devにはWAFが使えないのでWorkers標準のレート制限で代用。
# namespace_idは他のアプリと数えを分けるため専用の番号(回転寿司1001・フリースロー観戦1101)
[[ratelimits]]
name = "RL"
namespace_id = "1201"
simple = { limit = 20, period = 10 }

----- END ORIGINAL CONTENT -----
===== END FILE 04: wrangler.toml =====

===== BEGIN FILE 05: package.json =====
Source-Path: C:\石とゾンビ\package.json
Source-Bytes: 337
Source-Lines: 13
Source-LastWriteTime-JST: 2026-09-13T19:45:28+09:00
Source-SHA256: f22af21fa27825a529313dd204061e2ccff3f0eb0ac58be78644043a9c83b3a2
Role: 開発/配備/検品コマンド定義
----- BEGIN ORIGINAL CONTENT -----
{
  "name": "ishi-to-zombie",
  "private": true,
  "scripts": {
    "deploy": "npx --yes wrangler deploy",
    "dev": "npx --yes wrangler dev",
    "test": "node tools/shot.mjs index.html",
    "sweep": "node tools/sweep.mjs",
    "play": "node tools/play.mjs",
    "photo": "node tools/photo.mjs",
    "two": "node tools/two.mjs"
  }
}

----- END ORIGINAL CONTENT -----
===== END FILE 05: package.json =====

===== BEGIN FILE 06: .gitignore =====
Source-Path: C:\石とゾンビ\.gitignore
Source-Bytes: 39
Source-Lines: 4
Source-LastWriteTime-JST: 2026-09-13T11:38:23+09:00
Source-SHA256: debe9e6f288f82479412a140e3be7be4012f6d834a7e06f4e29942e0956105fa
Role: 管理対象から除外するディレクトリの定義
----- BEGIN ORIGINAL CONTENT -----
tmp/
.deploy/
.wrangler/
node_modules/

----- END ORIGINAL CONTENT -----
===== END FILE 06: .gitignore =====

===== BEGIN FILE 07: docs/K1仕様書_20260913.md =====
Source-Path: C:\石とゾンビ\docs\K1仕様書_20260913.md
Source-Bytes: 1891
Source-Lines: 32
Source-LastWriteTime-JST: 2026-09-13T11:09:37+09:00
Source-SHA256: 78ee3d3aefdd05c81d2fa26154526d94ce0091dea0d4def692a4237148d71299
Role: 最初のK1仕様原文。後続仕様で変更された内容も原文のまま収録
----- BEGIN ORIGINAL CONTENT -----
# 石とゾンビ K1仕様書(2026-09-13)

**K1の言葉をそのまま置く紙。ここに無い条件を足さない。**
プロジェクト名は **石とゾンビ**(K1命名)。公開先 https://ishi-to-zombie.pages.dev/

## K1の言葉(原文のまま)

> 子セッションで新ゲーム石とゾンビってゲーム作って欲しい
>
> イメージとしてはフリースローのあのエンジンを流用しつつキャラをもう少し小さくした引きの画角にした右からゾンビが迫ってくる
>
> それにボールを頭頂部に当てた倒すというゲーム
> つまり横なげしてもゾンビは致命傷にならず少しノックバックするだけ
> ゾンビは複数こちらに向かってくる

> プロジェクト名は石とゾンビ

## 読み取れる芯

- **フリースローの投げる手触り(物理)をそのまま流用する**。`C:\フリースロー` は**読むだけ**
- 画角は**引き**。キャラはフリースローより**小さく**
- ゾンビは**右から**、**複数**こちらへ迫ってくる
- **頭頂部に当てると倒せる**。横から当ててもノックバックするだけで致命傷にならない
  = 「上から落とす」軌道を作れるかどうかの遊び

## 親が決めた事(2026-09-13)

- 置き場は **Cloudflare Pages**(`ishi-to-zombie` / https://ishi-to-zombie.pages.dev/ )。新しいサービスは足さない
- 公開は `npm run deploy`、検品は `npm test`(Playwright WebKit・iPhone実寸)
- 素材が要る所はCodexへ発注(親が窓口)。**届くまでは色の四角で面白さを先に立てる**
- 音はElevenLabs(`C:\平成ゾンビ学園\tools\make_sfx.mjs` を写して使う。鍵は環境変数)
- 決まり: **遊ぶ画面はスクロールしない / 画面に物を詰め込まない / iPhone実寸390×797と390×664で崩れない**

----- END ORIGINAL CONTENT -----
===== END FILE 07: docs/K1仕様書_20260913.md =====

===== BEGIN FILE 08: docs/K1仕様書_磨き込みとマルチ_20260913.md =====
Source-Path: C:\石とゾンビ\docs\K1仕様書_磨き込みとマルチ_20260913.md
Source-Bytes: 12147
Source-Lines: 229
Source-LastWriteTime-JST: 2026-09-13T19:25:14+09:00
Source-SHA256: e14c116c3719b903e79ef1cd5243e686f78af189122c888e2963c0a975ffcc66
Role: 磨き込み・2人協力・HP・ラグ対策等のK1追加仕様原文
----- BEGIN ORIGINAL CONTENT -----
# 石とゾンビ — 磨き込み・マルチプレイ追加仕様書(2026-09-13 K1)

**K1の言葉をそのまま置く紙。ここに無い条件を足さない。**

## K1の言葉(原文のまま)

> 石とゾンビ — 磨き込み・マルチプレイ追加仕様書
> 作成日: 2026-09-13
>
> 【大前提】
> 既存「石とゾンビ」を土台に磨き込む。別ゲームにしない。
> 既存の芯は維持する。
> ・右から複数ゾンビが迫る
> ・石を物理挙動で投げる
> ・通常石は横から当ててもノックバックのみ
> ・頭頂部へ上から急角度で落ちた石だけが通常ゾンビを倒す
> ・「上から落とす軌道を作れるか」が遊びの中心
> ・石は無限
> ・縦画面を維持
> ・現在のコード描画中心の見た目と手触りを土台にする
>
> 【今回追加する柱】
> 1. シームレスな2人協力マルチ
> 2. HP制と前衛/後衛の交代
> 3. カラス
> 4. 30秒クールダウンの大岩
> 5. スクラップ報酬+生存時間倍率
> 6. アバター/ショップ
> 7. ラグを感じにくいネットワーク表示
>
> 【モード選択を作らない】
> トップ画面の主要導線:
> ・はじめる
> ・ショップ
> ・アバター変更
>
> 「シングル」「マルチ」の選択は作らない。
> 「はじめる」で即ゲーム開始。最初は1人で遊び、同時に裏でマッチング対象になる。
> 別端末のプレイヤーが「はじめる」を押し、参加可能なプレイがあれば途中参加して自然に2人プレイへ移行する。
> 誰も来なければそのままソロ。
> 最大2人。
> ロビー、部屋作成、準備完了等は初期仕様では不要。
> 途中参加・途中退出を許容し、退出ペナルティは設けない。相方が抜ければ残った人はソロで続行。
>
> 【前衛/後衛】
> 2人は画面左側に横並びで配置し、右側(ゾンビに近い側)が前衛、左側が後衛。
> ゾンビの攻撃は基本的に前衛が受ける。
> 1操作で2人の前後を交代できる。
> HPが減った人を後ろへ下げ、余裕のある相方が前へ出られる。
> ゾンビに取り付かれている最中は交代不可。相方が石でノックバックさせて救出する。
>
> 【HP制】
> 既存の「負けライン到達で即ゲームオーバー」はマルチ向けに変更。
> 各プレイヤーに個別HPを持たせる。
> ゾンビが前衛に取り付くとHPが継続減少。
> 相方が石を当ててゾンビをノックバックさせれば取り付きを解除できる。
> HP0のプレイヤーはそのプレイから脱落。
> 相方が生存していればゲームは続行し、1人状態へ戻る。
> 最大HPや秒間ダメージは調整しやすい定数にしてプレイテストで決める。
>
> 【協力の自然発生】
> 役職選択は作らない。2人とも基本操作は石投げ。
> ルールから自然に役割分担を発生させる。
> ・片方が横から当ててゾンビをノックバック/密集させる
> ・片方が上から落として密集したゾンビをまとめて倒す
> ・取り付かれた相方を救出する
> ・片方が地上、片方がカラスを担当する
> ・HPを見て前後交代する
> 重なった複数ゾンビを一投でまとめて倒せる現在の気持ちよさは活かす。
>
> 【新敵: カラス】
> 空中から来る敵。
> 単なる敵数増加ではなく、2人の役割分担を生む目的で追加。
> 「自分はカラス、相方は地上」という分担が効率的になるようにする。
> 画面上部を利用するが、通常石の山なり軌道を邪魔しすぎない。
> HP、倒し方、攻撃方法は最初は簡潔なプロトタイプで実装し、触って調整する。
>
> 【特殊技: 大岩】
> 初期版の特殊技は1種類のみ。
> ・通常石のおよそ3倍の大きさ
> ・クールダウン約30秒
> ・スクラップ消費なし
> ・通常石と違い、頭頂部へ上から当てなくても通常ゾンビを倒せる
> ・危険な状況をひっくり返す救済/爽快手段
> ・重量感、衝突感を出す
> 特殊技を最初から大量に増やさない。
>
> 【スクラップ】
> ゾンビ撃破でゲーム内通貨「スクラップ」を獲得。
> 倒すたびに「+1」「+3」等を視覚表示。
> 右上などに、そのプレイヤーが現在の参加区間で獲得している基礎スクラップ合計を常時表示する。
> マルチでは参加タイミングが違うため、2人の表示値は異なり得る。
>
> 【マルチ時のスクラップ共有】
> 「参加している時間帯のチーム撃破報酬は共有」「参加前の過去報酬は共有しない」。
>
> 例:
> Aがソロ中に500相当を稼ぐ。
> その後Bが途中参加。
> Bは過去の500を受け取らない。
> B参加後は、Aが倒した敵のスクラップもBへ入り、Bが倒した敵のスクラップもAへ入る。
> キルの取り合いを発生させない。
>
> 【生存時間倍率】
> プレイ世界/セッションが長く続くほど、最終精算時のスクラップ倍率を上げる。
> 倍率は基本的に「個人の参加時間」ではなく「そのゲーム自体が何秒生き残っているか」を基準にする。
> 途中参加者も、参加後に獲得した基礎スクラップについては現在の高い生存時間倍率を受けられる。
>
> 仮例:
> 序盤 ×1.0
> 60秒 ×1.2
> 90秒 ×1.3
> 120秒 ×1.5
> 以降さらに上昇
> 正確な数値はプレイテストで調整。
>
> 【精算】
> 死亡、途中退出、セッション終了時に精算。
> 基本式:
> 最終獲得スクラップ = 参加中の基礎スクラップ × 精算時点の生存時間倍率
>
> 「基礎スクラップ → 生存ボーナス×1.5 → 最終獲得」のように増えた理由を明示する。
> 途中退出でもその時点までの正当な報酬を受け取れる。
>
> 【スクラップで能力強化しない】
> スクラップは基本的に見た目の解放用。
> 投石速度、攻撃力、HP、大岩威力等の恒久強化には使わない。
> このゲームの上達軸はプレイヤー自身の投球技術。
> 長期プレイヤーの差は腕前と見た目のコレクションで表現する。
>
> 【アバター変更】
> 既存shurechara.jsの基本要素は最初から利用可能。
> ・髪型8
> ・髪色8
> ・肌色4
> ・基本服色8
> トップ画面の「アバター変更」で自由に基本アバターを変更可能。
> これらはショップ商品にしない。
>
> 【ショップ】
> 初期カテゴリは3つ。
> A. 特別衣装 約50種類
> B. 帽子/頭装備 約20種類
> C. 石スキン 約10種類
> 眼鏡等のアクセサリーは初期版では入れない。
> 全商品は見た目だけで性能差なし。
>
> 【衣装 約50種類】
> 既存の色違いではなく形そのものが違う衣装。
> 約70%をかわいい/女性向け寄り、約30%を男性向け/中性的にする。
> 例: フリル、メイド服風、アイドル風、リボン、ワンピース風、セーラー風、パーカー、作業着、スーツ、スポーツユニフォーム、オーバーオール、ジャケット等。
> 実在ブランドや既存キャラクターの模倣はしない。
> 凝った衣装ほどスクラップ価格を高くするが、性能差はない。
> 既存の素朴なコード描画の味を維持する。
>
> 【帽子 約20種類】
> 例: キャップ、ニット帽、麦わら帽子、ハット、ベレー帽、リボン、猫耳風頭装備、王冠風、ヘルメット等。
> 髪型との組み合わせで破綻しないようにする。
>
> 【石スキン 約10種類】
> 物理性能・当たり判定は通常石と同一。見た目のみ変更。
> 例: 通常石、色違い、ハート型、少し光る石、丸い石、角張った石、模様入り等。
> 季節限定は現段階では不要。
>
> 【ネットワーク/ラグ対策】
> 目標は「通信しているから操作が重い」と感じさせないこと。
>
> 自分の操作:
> ローカルで即時反映。投石時に通信往復を待たない。
>
> 相方:
> 受信位置をそのままカクカク切り替えず補間して滑らかに表示。
>
> ゾンビ:
> 同期情報間を補間し、必要に応じて予測して滑らかに表示。
>
> HP、死亡、ゾンビ撃破、スクラップ、大岩使用、前後交代、参加/退出等の重要状態は、プレイヤー間で食い違わない権威的な状態管理を行う。
>
> 投石は操作レスポンス最優先。本人端末では即座に飛ばし、その後必要な同期・補正を行う。
> 協力ゲームなので対戦FPSほどの公平性は不要だが、「自分では明らかに当たったのに何も起きない」は極力避ける。
>
> 具体的な通信方式・サーバー構成は既存の五月雨開発環境を確認して選択する。
> 新しい外部サービスを勝手に追加しない。
>
> 【既存難易度】
> 現状は主に撃破数に応じてゾンビ速度が上がり、出現間隔も短くなる。
> この「続けるほど苦しくなる」性質は生存時間倍率と相性が良いため、まず残して検証する。
> 時間制クリアにはせず、どこまで生き残れるかを基本とする。
>
> 【実装優先順位】
> 既存の投石物理と頭頂部判定を壊さないことが最優先。
>
> 順序の目安:
> 1. 既存シングルの挙動を保持
> 2. HP制
> 3. 2人参加/退出と前後交代
> 4. マルチ同期と補間
> 5. スクラップ+共有+生存倍率+精算
> 6. 大岩
> 7. カラス
> 8. アバター変更
> 9. ショップと衣装/帽子/石スキン
> 10. iPhone実寸で検品・調整
>
> 【完成条件】
> ・「はじめる」だけで即遊べる
> ・誰か来れば途中から自然に2人になる
> ・抜けてもゲームが壊れない
> ・2人でノックバック、まとめ撃破、救出、前後交代、対カラスの分担が自然に起こる
> ・投石操作が通信待ちで重くならない
> ・ゾンビと相方が通信でガクガクしない
> ・撃破のたびスクラップ獲得が視覚的に分かる
> ・死亡/退出時に生存倍率がかかり、報酬が明快
> ・スクラップを貯めて見た目を集めたくなる
> ・課金や能力強化ではなく、腕前と見た目が継続動機になる
> ・何より既存「上から落とす軌道を作る」面白さを損なわない

## 親が決めた環境(2026-09-13)

マルチが入るので、置き場が変わります。**新しい外部サービスは足しません。**

| 決めた事 | 中身 |
|---|---|
| 置き場 | **Cloudflare Workers + Durable Objects + WebSocket**。**Pagesではクラスを持てない**ので、回転寿司オンラインとぷっちーずと同じ形にする(`[assets]` で静的ファイルも同じWorkerから出す) |
| 部屋 | 1プレイ=1 Durable Object。待合・部屋作成は作らない(「はじめる」で即・空きがあれば途中参加) |
| 正 | **HP・死亡・撃破・スクラップ・大岩・前後交代・参加退出はサーバーが正。**投石の見た目は自分の端末で即時、後から合わせる |
| 守り | **DoSガード3盾を必ず入れる**(`C:\五月雨開発\DoSガード.md`)。①連打を断る ②月の上限で止める ③CPU上限 |
| ★目覚まし | **人が誰もいない部屋が回り続けないこと。**誰も繋がっていない状態が数分続いたら畳む。止める条件を「全滅」のような遊びの中の出来事にしない(2026-09-13にぷっちーずで実際に起きた) |
| 見た目 | **今のコード描画のまま。**絵は足さない(衣装・帽子・石スキンもコードで描く) |

----- END ORIGINAL CONTENT -----
===== END FILE 08: docs/K1仕様書_磨き込みとマルチ_20260913.md =====

===== BEGIN FILE 09: docs/決めたこと.md =====
Source-Path: C:\石とゾンビ\docs\決めたこと.md
Source-Bytes: 12287
Source-Lines: 146
Source-LastWriteTime-JST: 2026-09-13T20:01:15+09:00
Source-SHA256: 16dbf9bc2ef6f7f75a7cd5a4784d61d8e79764574ab47cce1a3c5b65268e9224
Role: 初期判断と、後半の磨き込み/マルチ移行時の実装判断・数値・過去の実測記録
----- BEGIN ORIGINAL CONTENT -----
# 決めたこと(石とゾンビ)

**K1が決めた事は `docs/K1仕様書_20260913.md`。この紙は私(子セッション)が決めた事の記録。**
1行ずつ、決めた事と理由。後から「なぜこうなっているか」を探させないため。

## 2026-09-13(立ち上げの日)

### 絵と素材

- **絵は1枚も使わない。全部コードで描く**(K1「フリースローはそもそもイラストないやん」)。`docs/asset_prompts.md` は作らない・Codexへの絵の発注もしない
- **人は共有財産のシュールキャラを流用**(`C:\五月雨開発\シュールキャラ\shurechara.js` を `lib/shurechara.js` へ写した)。**元のファイルは1文字も直していない**——あれは共有財産で、直すなら回転寿司側にも入れる決まりだから
- **ゾンビはシュールキャラを崩した形**。崩し方は4つだけ: ①死人の肌 ②体を傾ける ③片足を引きずる ④腕を前へ出す。新しい絵は作らない
- **死人の肌と汚れた服は、シュールキャラの色表へ**実行時に後ろから足す**だけにした**。元の8色(町の人の服)もゾンビに使う——**町の人がそのまま変わり果てた**方が、全員が同じ死人の制服を着ているより気味が悪い
- ゾンビの見た目は**1体ずつバラバラに引く**。`SHURE.fromSeed` に連番を渡すと服の色が `n/256` で決まるので、**4体並べたら4体とも黄色**になった(実測して直した)
- 石・空・月・雲・校舎・地面も全部コード。画像の読み込みはゼロ

### 遊びの芯

- **倒せるのは「頭の丸へ・上から・急な角度で入った石」だけ。**判定は3つ同時: ①落ちている(vy>0) ②入った所が頭の中心より上 ③落ちる速さ >= 横の速さ×0.60(=水平から31度より急)
- **判定は「投げた角度」ではなく「当たる時に落ちている角度」で見る。**だから低い山なりでも、届く頃に急降下していれば倒せる。跳ね返らせて上から落とすのも同じ理屈で通る(K1の言う「上から落とす軌道を作れるか」はそういう意味だと読んだ)
- 体・足・頭の横に当たったら**ノックバックだけ**。右へ押し戻し(最大2.6m/s相当)+0.40秒よろける。連打しても積み上がらないよう上限を付けた
- **石は無限・同時に3個まで・投げた後0.14秒で次**。弾数を資源にしない(狙いの遊びであって節約の遊びではない)。同時3個が上限なので横なげ連打では押さえきれない
- **1体でも負ける線(投げる人から0.80m)まで来たら即負け。**体力・残機は作らない。短く何度も遊ぶ形

### 画角と寸法

- **横に7.8m入れる引きの画角**(フリースローは4.3m)。シュールキャラの頭の半径は15px=フリースローの7割。K1の「もう少し小さく」
- 9.0m(頭の半径13px)も作って撮ったが、**頭が25px径まで落ちて的が読めなくなった**ので7.8mへ戻した
- 投げる物理の数字は**フリースローそのまま**(引いた長さ→速さ 4.6〜9.2m/s・角度14〜84度・重力9.8・空気抵抗0.0022)。跳ね返りだけ石らしく0.62→0.46へ落とした
- **頭のてっぺんに金色の弧を描く。**「どこを狙うか」を文字で説明しないため。最初は薄い線にしたら夜の髪に沈んで1本も見えなかったので、暗い線を敷いてから明るい線を乗せた

### やってみて捨てた物

- **地平線をぞろぞろ歩く遠い群れ**(「まだいくらでも来る」の気配)。校舎より奥へ置くと1体も見えず、手前へ置くと近くのゾンビと取り違える。どちらでも値打ちが出ないので**消した**
- fps・メモリの計器板(dev_tools.js の物)。盤面に被るので `window.SETTINGS={memMeter:false}` で既定オフ。**🐛報告の箱は残す**(あれは財産)

### 音

- **音のファイルを持たない。**全部その場で合成する(フリースローと同じ作り)。倒した「ゴッ」・押した「ドス」・投げる息・石が地面を打つ音・負けた時の唸り
- **iOSのマナーモードでは効果音が黙る。**合成した音は着信音の仲間で、曲(`<audio>`)が鳴るまで出ない。このゲームは曲を持たないので、今は**仕様として受け入れる**。曲を入れる時に無音mp3の支えごと入れる

### 検品の道具(全部このフォルダに置いた)

- `tools/sweep.mjs` … **大根おろし検査**。距離×角度×力を全部なめて表にする。見るのは2つ: ①どの距離にも倒せる投げ方があるか ②横なげで倒せていないか
- `tools/play.mjs` … **自分で何十回も遊ぶ道具**。総当たりで狙う子を走らせて、何秒生きて何体倒せるかを数える。K1にテストをやらせないため
- `tools/photo.mjs` … 見せる写真をiPhone実寸で撮る。`npm test` は崩れの検品、こちらは見え方
- `index.html` の `window.ZG` が検品の口。遊びには出てこない

### 今の手応え(2026-09-13 実測)

- 総当たりで狙う子(=上手い人の上限)で **平均140秒・52体倒して負ける**。ゲームは必ず終わる
- 距離ごとの倒せる割合は 4.9%〜15.3%(でたらめに投げた場合)。フリースローのゴール(7〜13%)と同じくらい
- 横なげ(14〜28度)384通りで**直接倒せたのは0件**。芯は効いている

---

## 2026-09-13(その2・磨き込みとマルチ)

正本は `docs/K1仕様書_磨き込みとマルチ_20260913.md`。ここは私が決めた数字と理由。

### 置き場が変わった

- **Cloudflare Pages → Workers + Durable Objects へ引っ越した。**Pagesではクラスを持てない
- **公開URLが変わった: https://ishi-to-zombie.kaiten-sushi-online.workers.dev/**
  (旧 `ishi-to-zombie.pages.dev` はもう更新していない)
- 静的ファイルは `public/` に寄せた(`[assets]` で同じWorkerから出る)。手本は回転寿司オンライン
- 🐛バグ報告の受け先は Pages Functions から Durable Object へ移した(`/api/bug`)

### 世界の作り(ここが一番大事)

- **長さを全部メートルに直した。**画面の幅が端末で違っても同じ世界になる。マルチの前提
- **遊びの中身を `lib/world.js` 1本にまとめ、サーバーと端末の両方が同じ物を回す。**二重に書かない
  - サーバー = `sim:'full'`(**ここが正**: HP・死亡・撃破・スクラップ・大岩・前後交代・参加退出)
  - 端末 = `sim:'motion'`(**動きだけ**回して、中身は届いた物で上書き)
  - 通信が繋がらない時は端末が `sim:'full'` で回す。**中身は同じ物なのでソロも作り分けない**
- **石だけは世界に入れていない。**投げた本人の端末で飛ばし、当たり判定も本人が決めて
  結果だけをサーバーへ渡す(K1「投石は操作レスポンス最優先」)。当たった見た目と音は**先に出す**
- ゾンビの位置は**飛ばさずに寄せる**(毎コマ34%ずつ)。0.75m以上ずれた時だけ飛ばす
- **1〜4を入れても、大根おろし検査の数字は1つも変わらなかった**(倒せる割合・角度・力が
  refactorの前後で完全に一致)。芯は壊していない

### 数字(全部私が決めた。触って変える前提)

| 物 | 値 | 理由 |
|---|---|---|
| HP | 100 | |
| 取り付かれている間の減り | 毎秒11(1体につき) | 1体なら約9秒もつ。2体なら4.5秒 |
| 取り付く間合い | 前衛の0.44m前 | |
| 立ち位置 | 前衛1.48m / 後衛0.60m | 1.30/0.58だと体が触れて頭のHP帯が重なった(実測) |
| ノックバック | 1.7m/s(上限2.6)・0.40秒よろける | 連打しても積み上がらない |
| 2人の時の湧き | 間隔を0.68倍 | 2人で楽になりすぎないように。K1指示には無いので私の判断 |
| 大岩 | 30秒・大きさ3倍・速さ0.82倍 | 遅い分だけ重い。どこに当たっても倒せる |
| カラス | 高さ2.5〜3.6m・28秒目から・15秒ごと(最短5.5秒) | 山なりの邪魔をしない高さ |
| カラスの倒し方 | **どこに当たっても落ちる** | 地上と別の仕事にして、2人の分担を作るため |
| カラスの突き | 7ダメージ・3.2秒かけて降りて戻る | |
| スクラップ | ゾンビ+1 / カラス+3 | |
| 生存倍率 | 60秒×1.2 / 90秒×1.3 / 120秒×1.5 / 150×1.7 / 180×1.9 / 以降+0.2 | K1の仮例をそのまま段にした |

### 決めた作りの理由

- **取り付かれた時、自分の石でも外せる**(実測: その距離では360通り全部がノックバックで、
  倒すことはできないが**必ず外せる**)。だからソロでも詰まない。相方がいれば救出が速い
- **取り付かれている間は自分の石が全部その1体に当たる**ので、奥のゾンビを狙えなくなる。
  これがそのまま「相方に前を任せる」動機になる
- **スクラップはその時いる人全員へ同じだけ入る。**倒した人が誰かは見ていない
  (K1「キルの取り合いを発生させない」)。参加前の分が入らないのは、入るのが「今いる人」だけだから
- 大岩・交代のボタンは**押せない時も大きさを変えない。**グレーアウトして赤字を重ねる(UI憲法)
- 着せ替えとお店だけDOMにした。**遊ぶ画面はcanvasのままスクロールしない**

### DoSガード(3盾)

- **盾1** Workers標準のレート制限 20回/10秒(namespace 1201。他のアプリと数えを分けた)
- **盾2** 月**2000万回**で新規接続を断る(2026-09-13 K1指示「全てのガードを月2000万回にしてよ」
  「今開発段階やし本格化したら外せばええんやし」)。数えるのは変数+1、1000回ごとに保存、
  判定は**新しく繋ぐ時だけ**。遊んでいる最中は一度も確かめない。今いくつかは `/api/meter`
- **盾3** `[limits] cpu_ms = 100`
- 1本の繋がりが1秒に45通を超えたら、その分は黙って捨てる

### ★誰も繋がっていない部屋を畳む

- 人が0人になったら**その場で時計(setInterval)を解く**。1コマも進めない
- そのうえで3分後に目覚ましを仕掛け、まだ0人なら部屋を畳んで目覚ましも消す
- **止まる条件を「全滅」のような遊びの中の出来事にしない**(2026-09-13にぷっちーずで起きた穴)
- 点検の窓 `/api/room?r=部屋名` は**人の気配を更新しない**(見ているこちらが部屋を延命させない)
- 実測: 全員抜けたあと `人数=0 / 時計=止まっている`

### 相手探しで踏んだ穴(実測して直した)

- 部屋が自分の**名前**ではなく**内側の番号(`state.id`)**を相手探しへ伝えていた。
  相手探しは `idFromName(番号)` という**別の空っぽの部屋**へ案内していて、
  2人で開いても永久に合流しなかった。部屋の名前をURLから受け取って持つようにして直した

### ショップの品(全部コード描画・見た目だけ・性能差なし)

- 特別衣装51種(かわいい寄り35 / 中性・男性寄り15 = 7:3)・帽子21種・石スキン10種
- 値段: 衣装0〜400 / 帽子0〜220 / 石0〜150。各列の1つ目は必ず0(既定品)
- 見本は https://ishi-to-zombie.kaiten-sushi-online.workers.dev/wardrobe_sheet.html
- **スクラップで強くなる物は1つも無い**(K1指示)

### 検品の道具

- `node tools/two.mjs` … **2つの端末で本当に繋がるか**を数字で確かめる。
  合流・同じ部屋・スクラップの共有・前後交代・退出・空き部屋の畳み・DoSガードの残量まで見る
- `node tools/sweep.mjs` … 大根おろし検査(芯が壊れていないか)
- `node tools/play.mjs` … 総当たりで狙う子に何十回も遊ばせてテンポを見る
- `node tools/photo.mjs` … 全部の画面をiPhone実寸で撮る

----- END ORIGINAL CONTENT -----
===== END FILE 09: docs/決めたこと.md =====

===== BEGIN FILE 10: docs/担当のやることリスト.md =====
Source-Path: C:\石とゾンビ\docs\担当のやることリスト.md
Source-Bytes: 2140
Source-Lines: 30
Source-LastWriteTime-JST: 2026-09-13T20:00:19+09:00
Source-SHA256: 92c326f2f13a776f0dca579fdbdb73c0eca2aa6e91383dbd178c0b926eb622c6
Role: 開発担当が記録した作業状況。今回Codexが再検証した結果ではない
----- BEGIN ORIGINAL CONTENT -----
# 担当のやることリスト(石とゾンビ・磨き込みとマルチ)

**正本は `docs/K1仕様書_磨き込みとマルチ_20260913.md`。この紙は順番と進み具合。**
K1の実装優先順位のとおり、上から片付ける。**既存の投石物理と頭頂部判定を壊さないことが最優先。**

済 = 終わってデプロイまで通した物。

| # | やること | 状態 |
|---|---|---|
| 0 | 世界の座標を**メートル**に直す(端末の幅が違っても同じ世界になる。マルチの前提) | 済 |
| 0b | 遊びの中身を `lib/world.js` へ切り出す。**サーバーと端末の両方が同じ1本を回す**(二重に書かない) | 済 |
| 1 | 既存シングルの挙動を保持(投げる物理・頭頂部判定・大根おろし検査が前と同じ数字を出す) | 済 |
| 2 | HP制。取り付き→HP継続減少→0で脱落。負けラインは廃止 | 済 |
| 3 | 2人の参加/退出と前後交代。前衛が攻撃を受ける。取り付かれている間は交代不可 | 済 |
| 4 | マルチ同期と補間。Workers+Durable Objects+WebSocket。**自分の投石は通信を待たない** | 済 |
| 4b | **DoSガード3盾**(①レート制限 ②自分で止める門 ③CPU上限) | 済 |
| 4c | ★**誰も繋がっていない部屋を数分で畳む**(止める条件を遊びの出来事にしない) | 済 |
| 5 | スクラップ(+1/+3表示)+参加区間の共有+生存時間倍率+精算画面 | 済 |
| 6 | 大岩(30秒・3倍・頭頂部でなくても倒せる・重量感) | 済 |
| 7 | カラス(空から・2人の分担を生む) | 済 |
| 8 | アバター変更(髪型8/髪色8/肌4/服8・最初から無料) | 済 |
| 9 | ショップ(衣装約50・帽子約20・石スキン約10。全部コード描画・見た目だけ) | 済 |
| 10 | iPhone実寸(390×797/664)で検品・調整 | 済 |

## 区切り

- **1〜4まで来たら一度報告**(2つの端末で繋がった証拠つき)。← 済(2026-09-13)
- そのあと5〜10を止まらずに片付ける。← 済(2026-09-13)

## 決めた数字は `docs/決めたこと.md` へ

----- END ORIGINAL CONTENT -----
===== END FILE 10: docs/担当のやることリスト.md =====

===== BEGIN FILE 11: public/lib/shurechara.js =====
Source-Path: C:\石とゾンビ\public\lib\shurechara.js
Source-Bytes: 7373
Source-Lines: 158
Source-LastWriteTime-JST: 2026-09-13T11:14:36+09:00
Source-SHA256: a012d11798a0e45c6af30f1f58339d3b96129be5cc80bbaea7c66d03726eb548
Role: キャラクター描画ライブラリ
----- BEGIN ORIGINAL CONTENT -----
/* ===== アバターの部品(数字で持つ。絵は1枚も要らない) ===== */
var HAIR_N = 8;                     /* 髪型8種 */
var HAIR_NM = ['短髪', 'ぼさ', 'ボブ', 'ロング', 'ポニー', 'アフロ', '三つ編み', 'つるり'];
var HAIR_COL = ['#241a12', '#4a2f1c', '#7b4a22', '#b07a35', '#d9b95e', '#8b9099', '#e9e3d3', '#b4443a'];
var SKIN_COL = ['#f7dcc2', '#eec49f', '#d29b6e', '#a97244'];
var WEAR_COL = ['#3f6fa8', '#c0563f', '#4f8a5b', '#7a5aa0', '#d3a33c', '#2f3b46', '#cc7096', '#e3ded1'];
function avDefault() {
  return { h: 0, hc: 0, sk: 0, w: 0 };
}
function avFix(a) {
  a = a || {};
  return {
    h: ((a.h | 0) % HAIR_N + HAIR_N) % HAIR_N,
    hc: ((a.hc | 0) % HAIR_COL.length + HAIR_COL.length) % HAIR_COL.length,
    sk: ((a.sk | 0) % SKIN_COL.length + SKIN_COL.length) % SKIN_COL.length,
    w: ((a.w | 0) % WEAR_COL.length + WEAR_COL.length) % WEAR_COL.length
  };
}

/* ===== 人 =====
   g … 描く先 / x,yHead … 頭の中心 / r … 頭の半径 / dir … +1で右向き(左の席) / av … アバター
   st … {mouth:0..1 口の開き, walk:0..1 歩いている, lift:0..1 腰を浮かす, gone:帰った}
   bottom … 体の下端(テーブルの高さ) */
function drawChar(g, x, yHead, r, dir, av, st, bottom) {
  av = avFix(av); st = st || {};
  var skin = SKIN_COL[av.sk], wear = WEAR_COL[av.w], hair = HAIR_COL[av.hc];
  var shY = yHead + r * 1.30;
  var bot = bottom != null ? bottom : shY + r * 2.4;
  g.save();
  g.lineCap = 'round'; g.lineJoin = 'round';

  /* 歩いている時だけ足が見える(座ったらテーブルに隠れる) */
  if (st.walk) {
    var sw = Math.sin(st.walk * Math.PI * 6);
    g.strokeStyle = '#33323a'; g.lineWidth = r * 0.28;
    g.beginPath(); g.moveTo(x, bot - r * 0.2); g.lineTo(x + sw * r * 0.55, bot + r * 1.15); g.stroke();
    g.beginPath(); g.moveTo(x, bot - r * 0.2); g.lineTo(x - sw * r * 0.55, bot + r * 1.15); g.stroke();
  }

  /* 胴。上が狭く下が広い台形 */
  g.fillStyle = wear;
  g.beginPath();
  g.moveTo(x - r * 0.74, shY);
  g.lineTo(x + r * 0.74, shY);
  g.lineTo(x + r * 1.16, bot);
  g.lineTo(x - r * 1.16, bot);
  g.closePath(); g.fill();

  /* 腕。テーブルの上に片手を置いている */
  g.strokeStyle = wear; g.lineWidth = r * 0.34;
  g.beginPath(); g.moveTo(x + dir * r * 0.62, shY + r * 0.18);
  g.lineTo(x + dir * r * 1.30, shY + r * 0.95); g.stroke();
  g.strokeStyle = skin; g.lineWidth = r * 0.26;
  g.beginPath(); g.moveTo(x + dir * r * 1.28, shY + r * 0.92);
  g.lineTo(x + dir * r * 1.80, bot - r * 0.10); g.stroke();
  g.fillStyle = skin;
  g.beginPath(); g.arc(x + dir * r * 1.80, bot - r * 0.10, r * 0.20, 0, 7); g.fill();

  /* 首 */
  g.fillStyle = skin;
  g.fillRect(x - r * 0.26, yHead + r * 0.55, r * 0.52, r * 0.90);

  /* 頭 */
  g.fillStyle = skin;
  g.beginPath(); g.arc(x, yHead, r, 0, 7); g.fill();
  /* 耳(向いている反対側) */
  g.beginPath(); g.arc(x - dir * r * 0.93, yHead + r * 0.06, r * 0.17, 0, 7); g.fill();

  /* 髪 */
  g.fillStyle = hair;
  var H = av.h;
  if (H !== 7) {  /* 7 = つるり(髪なし) */
    g.beginPath(); g.arc(x, yHead - r * 0.05, r * 1.03, Math.PI * 1.02, Math.PI * 2.04); g.fill();
  }
  if (H === 1) { /* ぼさ */
    for (var i = -3; i <= 3; i++) {
      g.beginPath(); g.arc(x + i * r * 0.30, yHead - r * 0.72 + Math.abs(i) * r * 0.12, r * 0.26, 0, 7); g.fill();
    }
  } else if (H === 2) { /* ボブ */
    g.fillRect(x - r * 1.04, yHead - r * 0.10, r * 0.30, r * 0.95);
    g.fillRect(x + r * 0.74, yHead - r * 0.10, r * 0.30, r * 0.95);
  } else if (H === 3) { /* ロング */
    g.fillRect(x - r * 1.04, yHead - r * 0.10, r * 0.32, r * 2.30);
    g.fillRect(x + r * 0.72, yHead - r * 0.10, r * 0.32, r * 2.30);
  } else if (H === 4) { /* ポニーテール(後ろへ垂らす) */
    var px = x - dir * r * 1.05;
    g.beginPath(); g.moveTo(px, yHead - r * 0.35);
    g.quadraticCurveTo(px - dir * r * 0.75, yHead + r * 0.55, px - dir * r * 0.32, yHead + r * 1.65);
    g.lineTo(px + dir * r * 0.22, yHead + r * 1.45);
    g.quadraticCurveTo(px - dir * r * 0.10, yHead + r * 0.45, px + dir * r * 0.28, yHead - r * 0.30);
    g.closePath(); g.fill();
  } else if (H === 5) { /* アフロ */
    for (var j = 0; j < 9; j++) {
      var an = Math.PI * (1.02 + j / 8 * 1.0);
      g.beginPath(); g.arc(x + Math.cos(an) * r * 0.92, yHead + Math.sin(an) * r * 0.92, r * 0.40, 0, 7); g.fill();
    }
  } else if (H === 6) { /* 三つ編み(2026-09-09 K1指示でちょんまげから差し替え) */
    var ss = [-1, 1];
    for (var q = 0; q < 2; q++) {
      var sd = ss[q], sx = x + sd * r * 0.94;
      for (var k = 0; k < 4; k++) {
        g.beginPath();
        g.arc(sx + sd * r * 0.06 * ((k % 2) ? 1 : -1), yHead + r * (0.18 + k * 0.34), r * 0.19, 0, 7);
        g.fill();
      }
    }
  } else if (H === 0) { /* 短髪 */
    g.fillRect(x - r * 1.02, yHead - r * 0.28, r * 2.04, r * 0.22);
  }

  /* 顔。向いている側に寄せる */
  var ex = x + dir * r * 0.40, ey = yHead - r * 0.10;
  g.fillStyle = '#2b2620';
  g.beginPath(); g.arc(ex, ey, r * 0.115, 0, 7); g.fill();
  g.beginPath(); g.arc(x + dir * r * 0.02, ey - r * 0.02, r * 0.10, 0, 7); g.fill();
  /* 眉 */
  g.strokeStyle = hair; g.lineWidth = r * 0.09;
  g.beginPath(); g.moveTo(ex - r * 0.16, ey - r * 0.34); g.lineTo(ex + r * 0.18, ey - r * 0.30); g.stroke();

  /* 口。食べている時だけ開く */
  var mo = st.mouth || 0;
  var mx = x + dir * r * 0.34, my = yHead + r * 0.38;
  g.fillStyle = '#7a3b33';
  if (mo > 0.04) {
    g.beginPath(); g.ellipse(mx, my, r * (0.16 + 0.20 * mo), r * (0.10 + 0.26 * mo), 0, 0, 7); g.fill();
  } else {
    g.strokeStyle = '#7a3b33'; g.lineWidth = r * 0.10;
    g.beginPath(); g.moveTo(mx - r * 0.20, my); g.lineTo(mx + r * 0.20, my); g.stroke();
  }
  g.restore();
}
/* 口の位置(食べる判定に使う)。drawChar と同じ式でなければならない */
function mouthAt(x, yHead, r, dir) { return { x: x + dir * r * 0.34, y: yHead + r * 0.38 }; }


/* ===== 外から使う口 =====
   使い方(canvasの文脈gへ1人描く):
     SHURE.draw(g, x, yHead, r, dir, {h:0,hc:0,sk:0,w:0}, {mouth:0, walk:0}, bottom);
   r=頭の半径。全部この r を基準に組んであるので、r を変えれば丸ごと拡大縮小できる。
   dir は +1 で右向き、-1 で左向き。
   av を省くと既定(短髪・黒髪・肌0・青服)。st を省くと口を閉じて座っている姿。 */
var SHURE = {
  draw: drawChar,
  mouthAt: mouthAt,
  fix: avFix,
  def: avDefault,
  HAIR_N: HAIR_N, HAIR_NM: HAIR_NM,
  HAIR_COL: HAIR_COL, SKIN_COL: SKIN_COL, WEAR_COL: WEAR_COL,
  /* 組み合わせの総数 = 8 x 8 x 4 x 8 = 2048通り */
  total: function () { return HAIR_N * HAIR_COL.length * SKIN_COL.length * WEAR_COL.length; },
  /* 番号から1人作る(同じ番号なら必ず同じ人になる) */
  fromSeed: function (n) {
    n = Math.abs(n | 0);
    return { h: n % HAIR_N, hc: (n / 8 | 0) % HAIR_COL.length, sk: (n / 64 | 0) % SKIN_COL.length, w: (n / 256 | 0) % WEAR_COL.length };
  }
};
if (typeof window !== 'undefined') { window.SHURE = SHURE; }
if (typeof module !== 'undefined' && module.exports) { module.exports = SHURE; }

----- END ORIGINAL CONTENT -----
===== END FILE 11: public/lib/shurechara.js =====

===== BEGIN FILE 12: public/lib/wardrobe.js =====
Source-Path: C:\石とゾンビ\public\lib\wardrobe.js
Source-Bytes: 59103
Source-Lines: 1003
Source-LastWriteTime-JST: 2026-09-13T19:53:21+09:00
Source-SHA256: 215bc7c69944497987aa8e600d49517e397535d564bad08da10683d1b209cfae
Role: 衣装・帽子・石スキンのデータ/描画
----- BEGIN ORIGINAL CONTENT -----
/* ===== ショップの品を描く部品(絵は1枚も使わない) =====
   ここに入っているのは3つの棚。
     COSTUMES … 特別衣装 51種(1つ目は「そのまま」=何も描かない)
     HATS     … 帽子・頭装備 21種(1つ目は「なし」=何も描かない)
     STONES   … 石の見た目 10種(1つ目は今の石そのもの)
   全部見た目だけ。強さ・当たり判定は一切変わらない。

   衣装の描き方:
     draw(g, x, shY, bot, r, col, dir)
       x   … 体の真ん中の横位置
       shY … 肩の高さ / bot … 体の下端
       r   … 頭の半径(大きさは全部これ基準。r を変えれば丸ごと伸び縮みする)
       col … その人の服の色 / dir … 向き(+1で右・-1で左)
     人の胴は上が狭く下が広い台形。その上から描くので、台形が透けないよう必ず塗りつぶす。
     裾は下へ r*0.9、横へ r*1.7 まで出してよい。

   帽子の描き方:
     draw(g, x, yHead, r, dir, hairCol)
     髪はもう描かれているので、頭のてっぺんをきちんと隠す形にする。上へは r*2.2 まで。

   石の描き方:
     draw(g, r, rot)   原点(0,0)が石の真ん中。r*1.3 の外へはみ出さない。 */
(function () {

  /* ---- 色を作る小道具(その人の服の色から明るい色・暗い色を作る) ---- */
  function hx(c) {
    c = String(c).replace('#', '');
    if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
    return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
  }
  function rgb(a) { return 'rgb(' + (a[0] | 0) + ',' + (a[1] | 0) + ',' + (a[2] | 0) + ')'; }
  function lite(c, f) { var a = hx(c); return rgb([a[0] + (255 - a[0]) * f, a[1] + (255 - a[1]) * f, a[2] + (255 - a[2]) * f]); }
  function dark(c, f) { var a = hx(c); return rgb([a[0] * (1 - f), a[1] * (1 - f), a[2] * (1 - f)]); }

  /* ---- 決め打ちの差し色(人ごとに変わらない) ---- */
  var WH = '#f3f0e7', CR = '#e7e0cc', BK = '#2b2b31', GD = '#e0b24a', PK = '#e58aa8',
      RD = '#c4443a', DN = '#4a6f9a', KH = '#8a8256', SV = '#b9bcc2', BR = '#8a6240', GR = '#5f8f63';

  /* ---- 胴の形を測る小道具 ----
     台形の上辺は幅 0.74r、下辺は 1.16r。t=0で肩、t=1で下端。 */
  function T(shY, bot, t) { return shY + (bot - shY) * t; }
  function HW(r, t) { return r * (0.74 + 0.42 * t); }

  /* 胴をまるごと塗る(下の服を完全に隠す。どの衣装も最初にこれを呼ぶ) */
  function base(g, x, shY, bot, r, col) {
    g.fillStyle = col;
    g.beginPath();
    g.moveTo(x - r * 0.74, shY); g.lineTo(x + r * 0.74, shY);
    g.lineTo(x + r * 1.16, bot); g.lineTo(x - r * 1.16, bot);
    g.closePath(); g.fill();
  }
  /* 胴の横一段ぶんを別の色で塗る(胸の切り替え・帯・ベルトに使う) */
  function panel(g, x, shY, bot, r, t0, t1, col) {
    var y0 = T(shY, bot, t0), y1 = T(shY, bot, t1), w0 = HW(r, t0), w1 = HW(r, t1);
    g.fillStyle = col;
    g.beginPath(); g.moveTo(x - w0, y0); g.lineTo(x + w0, y0);
    g.lineTo(x + w1, y1); g.lineTo(x - w1, y1); g.closePath(); g.fill();
  }
  /* 腰から下を広がるスカートにする(flare=下辺の広さ・drop=下端より下へ出す量) */
  function skirt(g, x, shY, bot, r, t, col, flare, drop) {
    var y0 = T(shY, bot, t), y1 = bot + r * (drop == null ? 0.5 : drop);
    var w0 = HW(r, t), w1 = r * (flare == null ? 1.45 : flare);
    g.fillStyle = col;
    g.beginPath(); g.moveTo(x - w0, y0); g.lineTo(x + w0, y0);
    g.lineTo(x + w1, y1); g.lineTo(x - w1, y1); g.closePath(); g.fill();
  }
  /* ふわふわの縁(丸を横に並べる) */
  function frill(g, x, y, halfW, rad, col, n) {
    g.fillStyle = col;
    for (var i = 0; i < n; i++) {
      g.beginPath(); g.arc(x - halfW + (i + 0.5) * (halfW * 2 / n), y, rad, 0, 7); g.fill();
    }
  }
  /* たての筋(プリーツ) */
  function pleat(g, x, y0, y1, hw0, hw1, n, col, w) {
    g.strokeStyle = col; g.lineWidth = w || 0.9;
    for (var i = 1; i < n; i++) {
      var f = i / n * 2 - 1;
      g.beginPath(); g.moveTo(x + hw0 * f, y0); g.lineTo(x + hw1 * f, y1); g.stroke();
    }
  }
  /* よこの筋(編み目・ダウンの段) */
  function rib(g, x, shY, bot, r, t0, t1, n, col, w) {
    g.strokeStyle = col; g.lineWidth = w || 0.9;
    for (var i = 0; i < n; i++) {
      var t = t0 + (t1 - t0) * (i + 0.5) / n, y = T(shY, bot, t), hw = HW(r, t);
      g.beginPath(); g.moveTo(x - hw, y); g.lineTo(x + hw, y); g.stroke();
    }
  }
  /* リボン */
  function bow(g, cx, cy, s, col) {
    g.fillStyle = col;
    g.beginPath(); g.moveTo(cx, cy); g.lineTo(cx - s, cy - s * 0.72); g.lineTo(cx - s, cy + s * 0.72); g.closePath(); g.fill();
    g.beginPath(); g.moveTo(cx, cy); g.lineTo(cx + s, cy - s * 0.72); g.lineTo(cx + s, cy + s * 0.72); g.closePath(); g.fill();
    g.beginPath(); g.arc(cx, cy, s * 0.36, 0, 7); g.fill();
  }
  /* Vに開いた胸元 */
  function vneck(g, x, shY, r, col, dep) {
    g.fillStyle = col;
    g.beginPath(); g.moveTo(x - r * 0.42, shY); g.lineTo(x + r * 0.42, shY);
    g.lineTo(x, shY + r * (dep || 0.62)); g.closePath(); g.fill();
  }
  /* 丸い襟 */
  function rcollar(g, x, shY, r, col) {
    g.fillStyle = col;
    g.beginPath(); g.arc(x - r * 0.30, shY + r * 0.02, r * 0.30, 0, 7); g.fill();
    g.beginPath(); g.arc(x + r * 0.30, shY + r * 0.02, r * 0.30, 0, 7); g.fill();
  }
  /* 合わせの襟(着物・道着) */
  function xcollar(g, x, shY, bot, r, col) {
    var yb = T(shY, bot, 0.42);
    g.fillStyle = col;
    g.beginPath(); g.moveTo(x - r * 0.66, shY); g.lineTo(x - r * 0.34, shY); g.lineTo(x + r * 0.16, yb); g.lineTo(x - r * 0.10, yb); g.closePath(); g.fill();
    g.beginPath(); g.moveTo(x + r * 0.66, shY); g.lineTo(x + r * 0.34, shY); g.lineTo(x - r * 0.16, yb); g.lineTo(x + r * 0.10, yb); g.closePath(); g.fill();
  }
  /* 前を留めるボタン */
  function buttons(g, x, shY, bot, r, col, n, t0, t1) {
    g.fillStyle = col;
    for (var i = 0; i < n; i++) {
      var t = (t0 == null ? 0.28 : t0) + ((t1 == null ? 0.9 : t1) - (t0 == null ? 0.28 : t0)) * (n === 1 ? 0.5 : i / (n - 1));
      g.beginPath(); g.arc(x, T(shY, bot, t), r * 0.075, 0, 7); g.fill();
    }
  }
  /* ポケット */
  function pocket(g, cx, cy, w, h, col) {
    g.fillStyle = col; g.fillRect(cx - w / 2, cy, w, h);
  }
  /* パーカーのフード(首の後ろのふくらみ) */
  function hood(g, x, shY, r, col) {
    g.fillStyle = col;
    g.beginPath(); g.ellipse(x, shY + r * 0.24, r * 1.00, r * 0.66, 0, Math.PI, Math.PI * 2); g.closePath(); g.fill();
    g.fillStyle = dark(col, 0.18);
    g.beginPath(); g.ellipse(x, shY + r * 0.26, r * 0.66, r * 0.44, 0, Math.PI, Math.PI * 2); g.closePath(); g.fill();
  }
  /* 肩から吊る紐(サロペット・キャミ) */
  function straps(g, x, shY, bot, r, col, w) {
    g.strokeStyle = col; g.lineWidth = w || r * 0.13;
    g.beginPath(); g.moveTo(x - r * 0.55, shY); g.lineTo(x - r * 0.34, T(shY, bot, 0.36)); g.stroke();
    g.beginPath(); g.moveTo(x + r * 0.55, shY); g.lineTo(x + r * 0.34, T(shY, bot, 0.36)); g.stroke();
  }
  /* 前を開けた上着(中が見える) */
  function openFront(g, x, shY, bot, r, outer, inner) {
    base(g, x, shY, bot, r, inner);
    g.fillStyle = outer;
    g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.14, shY);
    g.lineTo(x - r * 0.30, bot); g.lineTo(x - r * 1.16, bot); g.closePath(); g.fill();
    g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.14, shY);
    g.lineTo(x + r * 0.30, bot); g.lineTo(x + r * 1.16, bot); g.closePath(); g.fill();
  }

  /* ================= 特別衣装 51種 ================= */
  var COSTUMES = [
    /* 何も着せない(買う前の姿) */
    { id: 'plain', nm: 'そのまま', price: 0, tone: 'neutral', draw: function () { } },

    /* ---- かわいい・女性向け寄り(35種) ---- */
    { id: 'frill', nm: 'フリル', price: 180, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      skirt(g, x, shY, bot, r, 0.44, col, 1.42, 0.42);
      var f = lite(col, 0.55);
      /* 下半分に明るい布を重ね、その縁を丸く波打たせる */
      g.fillStyle = f;
      g.beginPath();
      g.moveTo(x - r * 1.16, T(shY, bot, 0.74)); g.lineTo(x + r * 1.16, T(shY, bot, 0.74));
      g.lineTo(x + r * 1.42, bot + r * 0.42); g.lineTo(x - r * 1.42, bot + r * 0.42); g.closePath(); g.fill();
      frill(g, x, T(shY, bot, 0.74), r * 1.16, r * 0.15, f, 5);
      frill(g, x, bot + r * 0.42, r * 1.42, r * 0.17, f, 7);
      frill(g, x, T(shY, bot, 0.10), HW(r, 0.10), r * 0.11, lite(col, 0.78), 5);
    } },
    { id: 'maid', nm: 'メイド服風', price: 320, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.62));
      skirt(g, x, shY, bot, r, 0.50, dark(col, 0.62), 1.48, 0.46);
      /* 白いエプロン */
      g.fillStyle = WH;
      g.beginPath(); g.moveTo(x - r * 0.34, T(shY, bot, 0.16)); g.lineTo(x + r * 0.34, T(shY, bot, 0.16));
      g.lineTo(x + r * 0.86, bot + r * 0.36); g.lineTo(x - r * 0.86, bot + r * 0.36); g.closePath(); g.fill();
      rcollar(g, x, shY, r, WH);
      frill(g, x, bot + r * 0.46, r * 1.44, r * 0.16, WH, 7);
      panel(g, x, shY, bot, r, 0.50, 0.58, WH);
      bow(g, x, T(shY, bot, 0.20), r * 0.26, PK);
    } },
    { id: 'idol', nm: 'アイドル風', price: 360, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.42));
      skirt(g, x, shY, bot, r, 0.52, WH, 1.52, 0.30);
      pleat(g, x, T(shY, bot, 0.52), bot + r * 0.30, HW(r, 0.52), r * 1.52, 6, '#c9c4b4', 1);
      panel(g, x, shY, bot, r, 0.48, 0.56, GD);
      /* 肩のふくらみ */
      g.fillStyle = WH;
      g.beginPath(); g.arc(x - r * 0.72, shY + r * 0.06, r * 0.24, 0, 7); g.fill();
      g.beginPath(); g.arc(x + r * 0.72, shY + r * 0.06, r * 0.24, 0, 7); g.fill();
      /* 胸の星 */
      g.fillStyle = GD;
      g.beginPath();
      for (var i = 0; i < 10; i++) {
        var a = -Math.PI / 2 + i * Math.PI / 5, rr = r * (i % 2 ? 0.10 : 0.24);
        if (i === 0) g.moveTo(x + Math.cos(a) * rr, T(shY, bot, 0.26) + Math.sin(a) * rr);
        else g.lineTo(x + Math.cos(a) * rr, T(shY, bot, 0.26) + Math.sin(a) * rr);
      }
      g.closePath(); g.fill();
    } },
    { id: 'ribbondress', nm: 'リボンドレス', price: 240, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      skirt(g, x, shY, bot, r, 0.46, col, 1.50, 0.52);
      panel(g, x, shY, bot, r, 0.44, 0.52, WH);
      bow(g, x, T(shY, bot, 0.24), r * 0.40, WH);
      g.strokeStyle = WH; g.lineWidth = r * 0.09;
      g.beginPath(); g.moveTo(x - r * 1.46, bot + r * 0.42); g.lineTo(x + r * 1.46, bot + r * 0.42); g.stroke();
    } },
    { id: 'onepiece', nm: 'ワンピース風', price: 140, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      skirt(g, x, shY, bot, r, 0.38, col, 1.40, 0.55);
      panel(g, x, shY, bot, r, 0, 0.34, lite(col, 0.32));
      g.strokeStyle = dark(col, 0.25); g.lineWidth = 1;
      var y = T(shY, bot, 0.36); g.beginPath(); g.moveTo(x - HW(r, 0.36), y); g.lineTo(x + HW(r, 0.36), y); g.stroke();
    } },
    { id: 'sailor', nm: 'セーラー風', price: 260, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.54, col, 1.46, 0.34);
      pleat(g, x, T(shY, bot, 0.54), bot + r * 0.34, HW(r, 0.54), r * 1.46, 6, dark(col, 0.25), 1);
      /* 大きな襟 */
      g.fillStyle = col;
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x + r * 0.74, shY);
      g.lineTo(x + r * 0.66, T(shY, bot, 0.30)); g.lineTo(x - r * 0.66, T(shY, bot, 0.30)); g.closePath(); g.fill();
      g.strokeStyle = WH; g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 0.70, T(shY, bot, 0.20)); g.lineTo(x + r * 0.70, T(shY, bot, 0.20)); g.stroke();
      g.beginPath(); g.moveTo(x - r * 0.68, T(shY, bot, 0.26)); g.lineTo(x + r * 0.68, T(shY, bot, 0.26)); g.stroke();
      /* 胸のスカーフ */
      g.fillStyle = RD;
      g.beginPath(); g.moveTo(x, T(shY, bot, 0.14)); g.lineTo(x + r * 0.18, T(shY, bot, 0.42));
      g.lineTo(x - r * 0.18, T(shY, bot, 0.42)); g.closePath(); g.fill();
    } },
    { id: 'blouse', nm: 'ブラウス', price: 120, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.74));
      rcollar(g, x, shY, r, WH);
      buttons(g, x, shY, bot, r, dark(col, 0.4), 4, 0.28, 0.92);
      frill(g, x, bot, r * 1.16, r * 0.13, lite(col, 0.86), 6);
    } },
    { id: 'sundress', nm: 'サンドレス', price: 150, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.30));
      skirt(g, x, shY, bot, r, 0.42, lite(col, 0.30), 1.55, 0.56);
      panel(g, x, shY, bot, r, 0, 0.12, WH);
      straps(g, x, shY, bot, r, WH, r * 0.10);
      g.fillStyle = WH;
      for (var i = 0; i < 5; i++) {
        g.beginPath(); g.arc(x + (i - 2) * r * 0.46, T(shY, bot, 0.62 + (i % 2) * 0.22), r * 0.09, 0, 7); g.fill();
      }
    } },
    { id: 'camisole', nm: 'キャミソール', price: 100, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.52));
      panel(g, x, shY, bot, r, 0, 0.10, lite(col, 0.80));
      straps(g, x, shY, bot, r, lite(col, 0.52), r * 0.09);
      frill(g, x, T(shY, bot, 0.12), HW(r, 0.12), r * 0.10, WH, 5);
    } },
    { id: 'knitdress', nm: 'ニットワンピ', price: 170, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.22));
      skirt(g, x, shY, bot, r, 0.60, lite(col, 0.22), 1.24, 0.62);
      rib(g, x, shY, bot, r, 0.05, 0.95, 8, dark(col, 0.20), 1);
      panel(g, x, shY, bot, r, 0, 0.08, dark(col, 0.15));
    } },
    { id: 'cardigan', nm: 'カーディガン', price: 190, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      openFront(g, x, shY, bot, r, lite(col, 0.20), WH);
      buttons(g, x, shY, bot, r, GD, 3, 0.24, 0.70);
      rib(g, x, shY, bot, r, 0.92, 0.99, 2, dark(col, 0.3), 1);
    } },
    { id: 'bolero', nm: 'ボレロ', price: 200, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.40, WH, 1.44, 0.54);
      /* 短い上着だけ肩に乗る */
      g.fillStyle = col;
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.20, shY);
      g.lineTo(x - r * 0.40, T(shY, bot, 0.34)); g.lineTo(x - r * 0.90, T(shY, bot, 0.34)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.20, shY);
      g.lineTo(x + r * 0.40, T(shY, bot, 0.34)); g.lineTo(x + r * 0.90, T(shY, bot, 0.34)); g.closePath(); g.fill();
      bow(g, x, T(shY, bot, 0.44), r * 0.24, col);
    } },
    { id: 'tutu', nm: 'チュチュ', price: 260, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.15));
      /* 腰から下を薄い布にして、その上に横へ張り出す層を重ねる
         (張り出しだけ描くと、盆を持っているように見えてしまう) */
      var y = T(shY, bot, 0.54), f = lite(col, 0.70);
      g.fillStyle = lite(col, 0.42);
      g.beginPath(); g.moveTo(x - HW(r, 0.54), y); g.lineTo(x + HW(r, 0.54), y);
      g.lineTo(x + r * 1.30, bot + r * 0.24); g.lineTo(x - r * 1.30, bot + r * 0.24); g.closePath(); g.fill();
      g.fillStyle = f;
      g.beginPath(); g.ellipse(x, y, r * 1.52, r * 0.60, 0, 0, Math.PI); g.closePath(); g.fill();
      panel(g, x, shY, bot, r, 0.46, 0.54, PK);
      frill(g, x, T(shY, bot, 0.10), HW(r, 0.10), r * 0.11, f, 5);
    } },
    { id: 'yukata', nm: '浴衣風', price: 300, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.55));
      panel(g, x, shY, bot, r, 0.95, 1.16, lite(col, 0.55));   /* 裾を少し伸ばす */
      xcollar(g, x, shY, bot, r, WH);
      panel(g, x, shY, bot, r, 0.50, 0.66, dark(col, 0.45));   /* 帯 */
      g.strokeStyle = GD; g.lineWidth = 1;
      var y = T(shY, bot, 0.58); g.beginPath(); g.moveTo(x - HW(r, 0.58), y); g.lineTo(x + HW(r, 0.58), y); g.stroke();
      g.fillStyle = WH;
      for (var i = 0; i < 4; i++) { g.beginPath(); g.arc(x + (i % 2 ? 0.5 : -0.6) * r, T(shY, bot, 0.74 + i * 0.10), r * 0.08, 0, 7); g.fill(); }
    } },
    { id: 'hakama', nm: '袴風', price: 330, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.62));
      xcollar(g, x, shY, bot, r, WH);
      /* 下は幅広の袴 */
      skirt(g, x, shY, bot, r, 0.44, dark(col, 0.42), 1.34, 0.70);
      pleat(g, x, T(shY, bot, 0.46), bot + r * 0.70, HW(r, 0.46), r * 1.34, 5, dark(col, 0.62), 1);
      g.strokeStyle = WH; g.lineWidth = r * 0.10;
      g.beginPath(); g.moveTo(x - HW(r, 0.44), T(shY, bot, 0.44)); g.lineTo(x + HW(r, 0.44), T(shY, bot, 0.44)); g.stroke();
    } },
    { id: 'cheer', nm: 'チア風', price: 280, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      skirt(g, x, shY, bot, r, 0.56, col, 1.50, 0.26);
      pleat(g, x, T(shY, bot, 0.56), bot + r * 0.26, HW(r, 0.56), r * 1.50, 7, WH, 1.2);
      g.fillStyle = WH;
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.40, shY);
      g.lineTo(x, T(shY, bot, 0.34)); g.lineTo(x + r * 0.40, shY); g.lineTo(x + r * 0.74, shY);
      g.lineTo(x, T(shY, bot, 0.50)); g.closePath(); g.fill();
      panel(g, x, shY, bot, r, 0.52, 0.58, WH);
    } },
    { id: 'apron', nm: 'エプロン', price: 130, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      g.fillStyle = lite(col, 0.88);
      g.beginPath(); g.moveTo(x - r * 0.32, T(shY, bot, 0.14)); g.lineTo(x + r * 0.32, T(shY, bot, 0.14));
      g.lineTo(x + r * 0.78, bot); g.lineTo(x - r * 0.78, bot); g.closePath(); g.fill();
      g.strokeStyle = lite(col, 0.88); g.lineWidth = r * 0.09;
      g.beginPath(); g.moveTo(x - r * 0.30, T(shY, bot, 0.14)); g.lineTo(x - r * 0.52, shY); g.stroke();
      g.beginPath(); g.moveTo(x + r * 0.30, T(shY, bot, 0.14)); g.lineTo(x + r * 0.52, shY); g.stroke();
      pocket(g, x, T(shY, bot, 0.62), r * 0.44, r * 0.30, dark(col, 0.10));
    } },
    { id: 'lolita', nm: 'ロリータ風', price: 400, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.55));
      skirt(g, x, shY, bot, r, 0.48, dark(col, 0.55), 1.48, 0.48);
      frill(g, x, T(shY, bot, 0.74), r * 1.24, r * 0.17, WH, 6);
      frill(g, x, bot + r * 0.46, r * 1.48, r * 0.19, WH, 8);
      frill(g, x, T(shY, bot, 0.12), HW(r, 0.12), r * 0.12, WH, 5);
      /* 胸の紐編み */
      g.strokeStyle = WH; g.lineWidth = 1;
      for (var i = 0; i < 3; i++) {
        var y0 = T(shY, bot, 0.20 + i * 0.09), y1 = T(shY, bot, 0.29 + i * 0.09);
        g.beginPath(); g.moveTo(x - r * 0.22, y0); g.lineTo(x + r * 0.22, y1); g.stroke();
        g.beginPath(); g.moveTo(x + r * 0.22, y0); g.lineTo(x - r * 0.22, y1); g.stroke();
      }
      bow(g, x, T(shY, bot, 0.50), r * 0.30, PK);
    } },
    { id: 'nurse', nm: 'ナース風', price: 210, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.62, WH, 1.30, 0.34);
      vneck(g, x, shY, r, lite(col, 0.30), 0.70);
      g.fillStyle = dark(col, 0.22);
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.42, shY); g.lineTo(x - r * 0.20, T(shY, bot, 0.26)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.42, shY); g.lineTo(x + r * 0.20, T(shY, bot, 0.26)); g.closePath(); g.fill();
      pocket(g, x + r * 0.60, T(shY, bot, 0.56), r * 0.36, r * 0.26, lite(col, 0.55));
      panel(g, x, shY, bot, r, 0.58, 0.64, lite(col, 0.35));
    } },
    { id: 'waitress', nm: 'ウェイトレス風', price: 250, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.50));
      skirt(g, x, shY, bot, r, 0.54, dark(col, 0.50), 1.44, 0.40);
      rcollar(g, x, shY, r, WH);
      /* 腰の小さな白エプロン */
      g.fillStyle = WH;
      g.beginPath(); g.moveTo(x - r * 0.62, T(shY, bot, 0.58)); g.lineTo(x + r * 0.62, T(shY, bot, 0.58));
      g.lineTo(x + r * 0.78, bot + r * 0.30); g.lineTo(x - r * 0.78, bot + r * 0.30); g.closePath(); g.fill();
      panel(g, x, shY, bot, r, 0.54, 0.60, WH);
      bow(g, x - r * 0.70, T(shY, bot, 0.58), r * 0.22, WH);
    } },
    { id: 'pinafore', nm: 'ジャンパースカート', price: 220, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.40, dark(col, 0.25), 1.48, 0.50);
      g.fillStyle = dark(col, 0.25);
      g.beginPath(); g.moveTo(x - r * 0.40, T(shY, bot, 0.16)); g.lineTo(x + r * 0.40, T(shY, bot, 0.16));
      g.lineTo(x + HW(r, 0.44), T(shY, bot, 0.44)); g.lineTo(x - HW(r, 0.44), T(shY, bot, 0.44)); g.closePath(); g.fill();
      straps(g, x, shY, bot, r, dark(col, 0.25), r * 0.14);
      buttons(g, x - r * 0.30, shY, bot, r, GD, 1, 0.18, 0.18);
      buttons(g, x + r * 0.30, shY, bot, r, GD, 1, 0.18, 0.18);
    } },
    { id: 'tiered', nm: '三段スカート', price: 230, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.35));
      var y0 = T(shY, bot, 0.40);
      var cs = [lite(col, 0.35), lite(col, 0.18), col];
      for (var i = 0; i < 3; i++) {
        var ya = y0 + (bot + r * 0.52 - y0) * (i / 3), yb = y0 + (bot + r * 0.52 - y0) * ((i + 1) / 3);
        var wa = r * (0.90 + i * 0.26), wb = r * (0.90 + (i + 1) * 0.26);
        g.fillStyle = cs[i];
        g.beginPath(); g.moveTo(x - wa, ya); g.lineTo(x + wa, ya); g.lineTo(x + wb, yb); g.lineTo(x - wb, yb); g.closePath(); g.fill();
      }
      panel(g, x, shY, bot, r, 0.36, 0.42, WH);
    } },
    { id: 'wrap', nm: 'カシュクール', price: 200, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      skirt(g, x, shY, bot, r, 0.56, col, 1.34, 0.48);
      /* 斜めの合わせ */
      g.fillStyle = lite(col, 0.28);
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x + r * 0.30, shY);
      g.lineTo(x + HW(r, 0.56), T(shY, bot, 0.56)); g.lineTo(x - HW(r, 0.56), T(shY, bot, 0.56)); g.closePath(); g.fill();
      g.strokeStyle = dark(col, 0.35); g.lineWidth = 1.2;
      g.beginPath(); g.moveTo(x + r * 0.30, shY); g.lineTo(x - r * 0.30, T(shY, bot, 0.56)); g.stroke();
      bow(g, x + r * 0.50, T(shY, bot, 0.54), r * 0.20, dark(col, 0.35));
    } },
    { id: 'peplum', nm: 'ペプラム', price: 240, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      panel(g, x, shY, bot, r, 0.46, 0.54, BK);
      /* 腰のひらひら */
      g.fillStyle = lite(col, 0.35);
      g.beginPath(); g.moveTo(x - HW(r, 0.54), T(shY, bot, 0.54)); g.lineTo(x + HW(r, 0.54), T(shY, bot, 0.54));
      g.lineTo(x + r * 1.46, T(shY, bot, 0.82)); g.lineTo(x - r * 1.46, T(shY, bot, 0.82)); g.closePath(); g.fill();
      frill(g, x, T(shY, bot, 0.82), r * 1.46, r * 0.14, lite(col, 0.55), 7);
      vneck(g, x, shY, r, lite(col, 0.60), 0.58);
    } },
    { id: 'hoodiedress', nm: 'パーカーワンピ', price: 190, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.20));
      skirt(g, x, shY, bot, r, 0.66, lite(col, 0.20), 1.32, 0.66);
      hood(g, x, shY, r, dark(col, 0.10));
      g.strokeStyle = WH; g.lineWidth = 1.4;
      g.beginPath(); g.moveTo(x - r * 0.16, T(shY, bot, 0.18)); g.lineTo(x - r * 0.20, T(shY, bot, 0.42)); g.stroke();
      g.beginPath(); g.moveTo(x + r * 0.16, T(shY, bot, 0.18)); g.lineTo(x + r * 0.20, T(shY, bot, 0.42)); g.stroke();
      pocket(g, x, T(shY, bot, 0.68), r * 0.90, r * 0.28, dark(col, 0.14));
    } },
    { id: 'blazer_f', nm: 'ブレザー', price: 290, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.58, dark(col, 0.40), 1.46, 0.32);
      pleat(g, x, T(shY, bot, 0.58), bot + r * 0.32, HW(r, 0.58), r * 1.46, 6, dark(col, 0.60), 1);
      g.fillStyle = dark(col, 0.55);
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.22, shY);
      g.lineTo(x - r * 0.34, T(shY, bot, 0.60)); g.lineTo(x - HW(r, 0.60), T(shY, bot, 0.60)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.22, shY);
      g.lineTo(x + r * 0.34, T(shY, bot, 0.60)); g.lineTo(x + HW(r, 0.60), T(shY, bot, 0.60)); g.closePath(); g.fill();
      bow(g, x, T(shY, bot, 0.24), r * 0.26, RD);
    } },
    { id: 'vestskirt', nm: 'ベストとスカート', price: 210, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.60, dark(col, 0.30), 1.36, 0.40);
      g.fillStyle = col;
      g.beginPath(); g.moveTo(x - r * 0.70, shY); g.lineTo(x - r * 0.26, shY); g.lineTo(x, T(shY, bot, 0.34));
      g.lineTo(x + r * 0.26, shY); g.lineTo(x + r * 0.70, shY);
      g.lineTo(x + HW(r, 0.60), T(shY, bot, 0.60)); g.lineTo(x - HW(r, 0.60), T(shY, bot, 0.60)); g.closePath(); g.fill();
      buttons(g, x, shY, bot, r, GD, 2, 0.42, 0.54);
      panel(g, x, shY, bot, r, 0.58, 0.64, BK);
    } },
    { id: 'miko', nm: '巫女風', price: 310, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      skirt(g, x, shY, bot, r, 0.42, RD, 1.36, 0.72);
      xcollar(g, x, shY, bot, r, CR);
      g.strokeStyle = WH; g.lineWidth = r * 0.11;
      g.beginPath(); g.moveTo(x - HW(r, 0.42), T(shY, bot, 0.42)); g.lineTo(x + HW(r, 0.42), T(shY, bot, 0.42)); g.stroke();
      pleat(g, x, T(shY, bot, 0.46), bot + r * 0.70, HW(r, 0.46), r * 1.36, 4, '#a33429', 1);
    } },
    { id: 'gothic', nm: 'ゴシック風', price: 380, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.78));
      skirt(g, x, shY, bot, r, 0.44, dark(col, 0.78), 1.48, 0.62);
      frill(g, x, T(shY, bot, 0.06), HW(r, 0.06), r * 0.13, CR, 5);
      frill(g, x, bot + r * 0.60, r * 1.48, r * 0.16, CR, 8);
      /* 前で締める紐 */
      g.strokeStyle = CR; g.lineWidth = 1;
      for (var i = 0; i < 4; i++) {
        var y0 = T(shY, bot, 0.16 + i * 0.08), y1 = T(shY, bot, 0.24 + i * 0.08);
        g.beginPath(); g.moveTo(x - r * 0.24, y0); g.lineTo(x + r * 0.24, y1); g.stroke();
        g.beginPath(); g.moveTo(x + r * 0.24, y0); g.lineTo(x - r * 0.24, y1); g.stroke();
      }
      panel(g, x, shY, bot, r, 0.46, 0.52, '#6b2f46');
    } },
    { id: 'leotard', nm: 'レオタード', price: 230, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.10));
      /* 脚の付け根が高い形(切り抜くと下着に見えるので、線で示すだけ) */
      g.strokeStyle = lite(col, 0.55); g.lineWidth = Math.max(1, r * 0.09);
      g.beginPath(); g.arc(x - r * 1.04, bot + r * 0.14, r * 0.58, Math.PI * 1.5, Math.PI * 2); g.stroke();
      g.beginPath(); g.arc(x + r * 1.04, bot + r * 0.14, r * 0.58, Math.PI, Math.PI * 1.5); g.stroke();
      panel(g, x, shY, bot, r, 0.44, 0.50, WH);
      vneck(g, x, shY, r, lite(col, 0.42), 0.52);
    } },
    { id: 'poncho', nm: 'ポンチョ', price: 200, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.30));
      /* 三角に広がる1枚布 */
      g.fillStyle = col;
      g.beginPath(); g.moveTo(x - r * 0.50, shY - r * 0.04); g.lineTo(x + r * 0.50, shY - r * 0.04);
      g.lineTo(x + r * 1.56, T(shY, bot, 0.88)); g.lineTo(x - r * 1.56, T(shY, bot, 0.88)); g.closePath(); g.fill();
      g.strokeStyle = lite(col, 0.55); g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 1.40, T(shY, bot, 0.70)); g.lineTo(x + r * 1.40, T(shY, bot, 0.70)); g.stroke();
      /* 房 */
      g.strokeStyle = lite(col, 0.70); g.lineWidth = 1.2;
      for (var i = 0; i < 7; i++) {
        var px = x - r * 1.44 + i * r * 0.48;
        g.beginPath(); g.moveTo(px, T(shY, bot, 0.88)); g.lineTo(px, T(shY, bot, 0.98)); g.stroke();
      }
    } },
    { id: 'highwaist', nm: 'ハイウエスト', price: 160, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.72));
      skirt(g, x, shY, bot, r, 0.30, col, 1.44, 0.56);
      panel(g, x, shY, bot, r, 0.28, 0.36, dark(col, 0.40));
      buttons(g, x, shY, bot, r, GD, 2, 0.30, 0.34);
      rcollar(g, x, shY, r, lite(col, 0.88));
    } },
    { id: 'kappougi', nm: '割烹着風', price: 170, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      g.fillStyle = lite(col, 0.90);
      g.beginPath(); g.moveTo(x - r * 0.64, shY + r * 0.10); g.lineTo(x + r * 0.64, shY + r * 0.10);
      g.lineTo(x + r * 1.06, bot + r * 0.20); g.lineTo(x - r * 1.06, bot + r * 0.20); g.closePath(); g.fill();
      g.strokeStyle = lite(col, 0.60); g.lineWidth = r * 0.10;
      g.beginPath(); g.moveTo(x - r * 1.00, T(shY, bot, 0.60)); g.lineTo(x + r * 1.00, T(shY, bot, 0.60)); g.stroke();
      bow(g, x, T(shY, bot, 0.60), r * 0.22, lite(col, 0.40));
    } },
    { id: 'cape', nm: 'ケープドレス', price: 300, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.40));
      skirt(g, x, shY, bot, r, 0.48, lite(col, 0.40), 1.48, 0.62);
      /* 肩に丸いケープ */
      g.fillStyle = dark(col, 0.30);
      g.beginPath(); g.ellipse(x, shY + r * 0.02, r * 1.00, r * 0.70, 0, 0, Math.PI); g.closePath(); g.fill();
      g.fillStyle = GD;
      g.beginPath(); g.arc(x, shY + r * 0.06, r * 0.13, 0, 7); g.fill();
      frill(g, x, bot + r * 0.58, r * 1.48, r * 0.15, WH, 7);
    } },
    { id: 'whitedress', nm: '白いドレス', price: 400, tone: 'cute', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.86));
      /* 長く引きずる裾 */
      g.fillStyle = lite(col, 0.86);
      g.beginPath(); g.moveTo(x - HW(r, 0.40), T(shY, bot, 0.40)); g.lineTo(x + HW(r, 0.40), T(shY, bot, 0.40));
      g.lineTo(x + r * 1.68, bot + r * 0.86); g.lineTo(x - r * 1.68, bot + r * 0.86); g.closePath(); g.fill();
      frill(g, x, T(shY, bot, 0.10), HW(r, 0.10), r * 0.12, WH, 5);
      panel(g, x, shY, bot, r, 0.38, 0.44, lite(col, 0.55));
      g.strokeStyle = lite(col, 0.60); g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 1.60, bot + r * 0.70); g.lineTo(x + r * 1.60, bot + r * 0.70); g.stroke();
    } },

    /* ---- 男性向け・中性的(15種) ---- */
    { id: 'hoodie', nm: 'パーカー', price: 140, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      hood(g, x, shY, r, dark(col, 0.18));
      g.strokeStyle = WH; g.lineWidth = 1.4;
      g.beginPath(); g.moveTo(x - r * 0.14, T(shY, bot, 0.20)); g.lineTo(x - r * 0.18, T(shY, bot, 0.46)); g.stroke();
      g.beginPath(); g.moveTo(x + r * 0.14, T(shY, bot, 0.20)); g.lineTo(x + r * 0.18, T(shY, bot, 0.46)); g.stroke();
      pocket(g, x, T(shY, bot, 0.62), r * 1.00, r * 0.30, dark(col, 0.14));
    } },
    { id: 'workwear', nm: '作業着', price: 160, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.20));
      g.strokeStyle = dark(col, 0.50); g.lineWidth = 1.2;
      g.beginPath(); g.moveTo(x, shY); g.lineTo(x, bot); g.stroke();
      pocket(g, x - r * 0.46, T(shY, bot, 0.22), r * 0.40, r * 0.26, dark(col, 0.38));
      pocket(g, x + r * 0.46, T(shY, bot, 0.22), r * 0.40, r * 0.26, dark(col, 0.38));
      panel(g, x, shY, bot, r, 0.56, 0.64, dark(col, 0.45));
      g.fillStyle = GD; g.fillRect(x - r * 0.10, T(shY, bot, 0.57), r * 0.20, r * 0.11);
    } },
    { id: 'suit', nm: 'スーツ', price: 340, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      var d = dark(col, 0.68);
      g.fillStyle = d;
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.16, shY);
      g.lineTo(x - r * 0.34, bot); g.lineTo(x - r * 1.16, bot); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.16, shY);
      g.lineTo(x + r * 0.34, bot); g.lineTo(x + r * 1.16, bot); g.closePath(); g.fill();
      /* 襟 */
      g.fillStyle = dark(col, 0.50);
      g.beginPath(); g.moveTo(x - r * 0.58, shY); g.lineTo(x - r * 0.16, shY); g.lineTo(x - r * 0.26, T(shY, bot, 0.40)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.58, shY); g.lineTo(x + r * 0.16, shY); g.lineTo(x + r * 0.26, T(shY, bot, 0.40)); g.closePath(); g.fill();
      /* ネクタイ */
      g.fillStyle = RD;
      g.beginPath(); g.moveTo(x, shY + r * 0.04); g.lineTo(x + r * 0.11, T(shY, bot, 0.50));
      g.lineTo(x, T(shY, bot, 0.58)); g.lineTo(x - r * 0.11, T(shY, bot, 0.50)); g.closePath(); g.fill();
      buttons(g, x - r * 0.30, shY, bot, r, BK, 2, 0.58, 0.74);
    } },
    { id: 'sports', nm: 'スポーツユニフォーム', price: 180, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      g.fillStyle = WH;
      g.fillRect(x - r * 0.74, shY, r * 0.18, (bot - shY) * 0.30);
      g.fillRect(x + r * 0.56, shY, r * 0.18, (bot - shY) * 0.30);
      vneck(g, x, shY, r, WH, 0.50);
      g.fillStyle = WH;
      g.beginPath(); g.arc(x, T(shY, bot, 0.48), r * 0.28, 0, 7); g.fill();
      g.fillStyle = col;
      g.fillRect(x - r * 0.06, T(shY, bot, 0.36), r * 0.12, r * 0.36);
      panel(g, x, shY, bot, r, 0.82, 0.90, WH);
    } },
    { id: 'overall', nm: 'オーバーオール', price: 200, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.66));
      g.fillStyle = DN;
      g.beginPath(); g.moveTo(x - r * 0.44, T(shY, bot, 0.24)); g.lineTo(x + r * 0.44, T(shY, bot, 0.24));
      g.lineTo(x + r * 1.16, bot); g.lineTo(x - r * 1.16, bot); g.closePath(); g.fill();
      g.strokeStyle = DN; g.lineWidth = r * 0.16;
      g.beginPath(); g.moveTo(x - r * 0.58, shY); g.lineTo(x - r * 0.36, T(shY, bot, 0.26)); g.stroke();
      g.beginPath(); g.moveTo(x + r * 0.58, shY); g.lineTo(x + r * 0.36, T(shY, bot, 0.26)); g.stroke();
      g.fillStyle = GD;
      g.beginPath(); g.arc(x - r * 0.40, T(shY, bot, 0.26), r * 0.08, 0, 7); g.fill();
      g.beginPath(); g.arc(x + r * 0.40, T(shY, bot, 0.26), r * 0.08, 0, 7); g.fill();
      pocket(g, x, T(shY, bot, 0.42), r * 0.46, r * 0.30, '#3d5d82');
    } },
    { id: 'jacket', nm: 'ジャケット', price: 250, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      openFront(g, x, shY, bot, r, dark(col, 0.42), lite(col, 0.70));
      g.fillStyle = dark(col, 0.60);
      g.beginPath(); g.moveTo(x - r * 0.62, shY); g.lineTo(x - r * 0.14, shY); g.lineTo(x - r * 0.30, T(shY, bot, 0.44)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.62, shY); g.lineTo(x + r * 0.14, shY); g.lineTo(x + r * 0.30, T(shY, bot, 0.44)); g.closePath(); g.fill();
      pocket(g, x - r * 0.66, T(shY, bot, 0.62), r * 0.34, r * 0.10, dark(col, 0.62));
      pocket(g, x + r * 0.66, T(shY, bot, 0.62), r * 0.34, r * 0.10, dark(col, 0.62));
    } },
    { id: 'tshirt', nm: 'Tシャツ', price: 60, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.10));
      g.strokeStyle = lite(col, 0.55); g.lineWidth = r * 0.10;
      g.beginPath(); g.arc(x, shY - r * 0.10, r * 0.32, 0.2, Math.PI - 0.2); g.stroke();
      g.strokeStyle = dark(col, 0.20); g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 1.10, T(shY, bot, 0.92)); g.lineTo(x + r * 1.10, T(shY, bot, 0.92)); g.stroke();
    } },
    { id: 'jersey', nm: 'ジャージ', price: 150, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, dark(col, 0.25));
      g.fillStyle = WH;
      g.beginPath(); g.moveTo(x - r * 0.70, shY); g.lineTo(x - r * 0.56, shY); g.lineTo(x - r * 0.96, bot); g.lineTo(x - r * 1.12, bot); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.70, shY); g.lineTo(x + r * 0.56, shY); g.lineTo(x + r * 0.96, bot); g.lineTo(x + r * 1.12, bot); g.closePath(); g.fill();
      g.strokeStyle = SV; g.lineWidth = 1.2;
      g.beginPath(); g.moveTo(x, shY); g.lineTo(x, T(shY, bot, 0.90)); g.stroke();
      panel(g, x, shY, bot, r, 0, 0.08, dark(col, 0.50));
    } },
    { id: 'tanktop', nm: 'タンクトップ', price: 90, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, lite(col, 0.14));
      g.fillStyle = dark(col, 0.45);
      g.beginPath(); g.moveTo(x - r * 0.78, shY - r * 0.04); g.lineTo(x - r * 0.34, shY - r * 0.04);
      g.lineTo(x - r * 0.72, T(shY, bot, 0.42)); g.lineTo(x - r * 0.98, T(shY, bot, 0.42)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.78, shY - r * 0.04); g.lineTo(x + r * 0.34, shY - r * 0.04);
      g.lineTo(x + r * 0.72, T(shY, bot, 0.42)); g.lineTo(x + r * 0.98, T(shY, bot, 0.42)); g.closePath(); g.fill();
      g.strokeStyle = WH; g.lineWidth = 1.2;
      g.beginPath(); g.moveTo(x - r * 0.34, shY); g.lineTo(x + r * 0.34, shY); g.stroke();
    } },
    { id: 'shirttie', nm: 'ワイシャツ', price: 190, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      g.fillStyle = CR;
      g.beginPath(); g.moveTo(x - r * 0.52, shY - r * 0.02); g.lineTo(x - r * 0.10, shY); g.lineTo(x - r * 0.22, T(shY, bot, 0.24)); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.52, shY - r * 0.02); g.lineTo(x + r * 0.10, shY); g.lineTo(x + r * 0.22, T(shY, bot, 0.24)); g.closePath(); g.fill();
      g.fillStyle = dark(col, 0.30);
      g.beginPath(); g.moveTo(x, shY + r * 0.02); g.lineTo(x + r * 0.12, T(shY, bot, 0.56));
      g.lineTo(x, T(shY, bot, 0.64)); g.lineTo(x - r * 0.12, T(shY, bot, 0.56)); g.closePath(); g.fill();
      buttons(g, x, shY, bot, r, '#cdc8bb', 3, 0.70, 0.94);
    } },
    { id: 'labcoat', nm: '白衣', price: 240, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, col);
      g.fillStyle = WH;
      g.beginPath(); g.moveTo(x - r * 0.74, shY); g.lineTo(x - r * 0.20, shY);
      g.lineTo(x - r * 0.24, bot + r * 0.34); g.lineTo(x - r * 1.24, bot + r * 0.34); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.74, shY); g.lineTo(x + r * 0.20, shY);
      g.lineTo(x + r * 0.24, bot + r * 0.34); g.lineTo(x + r * 1.24, bot + r * 0.34); g.closePath(); g.fill();
      pocket(g, x - r * 0.72, T(shY, bot, 0.64), r * 0.38, r * 0.28, '#e2ddd0');
      pocket(g, x + r * 0.72, T(shY, bot, 0.64), r * 0.38, r * 0.28, '#e2ddd0');
      pocket(g, x - r * 0.48, T(shY, bot, 0.20), r * 0.30, r * 0.20, '#e2ddd0');
    } },
    { id: 'denim', nm: 'デニムジャケット', price: 260, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      openFront(g, x, shY, bot, r, DN, lite(col, 0.40));
      g.strokeStyle = '#d9c9a2'; g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 1.02, T(shY, bot, 0.30)); g.lineTo(x - r * 0.28, T(shY, bot, 0.30)); g.stroke();
      g.beginPath(); g.moveTo(x + r * 1.02, T(shY, bot, 0.30)); g.lineTo(x + r * 0.28, T(shY, bot, 0.30)); g.stroke();
      g.fillStyle = '#d9c9a2';
      g.beginPath(); g.arc(x - r * 0.56, T(shY, bot, 0.40), r * 0.07, 0, 7); g.fill();
      g.beginPath(); g.arc(x + r * 0.56, T(shY, bot, 0.40), r * 0.07, 0, 7); g.fill();
      panel(g, x, shY, bot, r, 0.92, 1.00, '#3b5c80');
    } },
    { id: 'downvest', nm: 'ダウンベスト', price: 220, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, BK);
      g.fillStyle = col;
      g.beginPath(); g.moveTo(x - r * 0.70, shY); g.lineTo(x - r * 0.20, shY);
      g.lineTo(x - r * 0.24, bot); g.lineTo(x - r * 1.16, bot); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(x + r * 0.70, shY); g.lineTo(x + r * 0.20, shY);
      g.lineTo(x + r * 0.24, bot); g.lineTo(x + r * 1.16, bot); g.closePath(); g.fill();
      rib(g, x, shY, bot, r, 0.18, 0.92, 4, dark(col, 0.35), 1.2);
      g.strokeStyle = SV; g.lineWidth = 1.2;
      g.beginPath(); g.moveTo(x, shY); g.lineTo(x, bot); g.stroke();
    } },
    { id: 'dogi', nm: '道着', price: 230, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, CR);
      xcollar(g, x, shY, bot, r, lite(col, 0.30));
      panel(g, x, shY, bot, r, 0.54, 0.66, BK);
      g.strokeStyle = BK; g.lineWidth = r * 0.08;
      g.beginPath(); g.moveTo(x - r * 0.10, T(shY, bot, 0.66)); g.lineTo(x - r * 0.16, T(shY, bot, 0.86)); g.stroke();
      g.beginPath(); g.moveTo(x + r * 0.10, T(shY, bot, 0.66)); g.lineTo(x + r * 0.16, T(shY, bot, 0.86)); g.stroke();
    } },
    { id: 'chef', nm: 'コックコート', price: 250, tone: 'neutral', draw: function (g, x, shY, bot, r, col) {
      base(g, x, shY, bot, r, WH);
      g.fillStyle = CR;
      g.beginPath(); g.moveTo(x - r * 0.40, shY); g.lineTo(x + r * 0.40, shY);
      g.lineTo(x + r * 0.30, T(shY, bot, 0.18)); g.lineTo(x - r * 0.30, T(shY, bot, 0.18)); g.closePath(); g.fill();
      buttons(g, x - r * 0.30, shY, bot, r, '#cdc8bb', 3, 0.28, 0.72);
      buttons(g, x + r * 0.30, shY, bot, r, '#cdc8bb', 3, 0.28, 0.72);
      /* 首元のスカーフ(白い服に埋もれないよう濃いめ) */
      g.fillStyle = dark(col, 0.30);
      g.beginPath(); g.moveTo(x - r * 0.34, shY - r * 0.04); g.lineTo(x + r * 0.34, shY - r * 0.04);
      g.lineTo(x, T(shY, bot, 0.16)); g.closePath(); g.fill();
      panel(g, x, shY, bot, r, 0.76, 0.84, '#5a5a60');
    } }
  ];

  /* ================= 帽子・頭装備 21種 ================= */
  /* 頭のてっぺんを隠すお椀。y をずらして髪ごと覆う */
  function dome(g, x, y, rx, ry, col) {
    g.fillStyle = col;
    g.beginPath(); g.ellipse(x, y, rx, ry, 0, Math.PI, Math.PI * 2); g.closePath(); g.fill();
  }
  /* つば(横に長い楕円) */
  function brim(g, x, y, rx, ry, col) {
    g.fillStyle = col;
    g.beginPath(); g.ellipse(x, y, rx, ry, 0, 0, 7); g.fill();
  }
  /* 前に突き出すつば */
  function peak(g, x, y, r, dir, col, len) {
    g.fillStyle = col;
    g.beginPath(); g.ellipse(x + dir * r * (len || 0.62), y, r * (len || 0.62), r * 0.18, 0, 0, 7); g.fill();
  }

  /* どの帽子も、眉の少し上(yHead-0.34r あたり)を縁にして、
     そこから上を丸ごと覆う。こうすると顔が隠れず、髪型が何であっても
     てっぺんが出てこない(アフロの膨らみまで入るよう上は 1.3r まで取る)。 */
  var HATS = [
    { id: 'none', nm: 'なし', price: 0, draw: function () { } },

    { id: 'cap', nm: 'キャップ', price: 90, draw: function (g, x, yHead, r, dir) {
      peak(g, x + dir * r * 0.10, yHead - r * 0.46, r, dir, '#2f4f7a', 0.62);
      dome(g, x, yHead - r * 0.34, r * 1.24, r * 1.04, '#3f6fa8');
      g.fillStyle = '#2f4f7a';
      g.fillRect(x - r * 1.24, yHead - r * 0.50, r * 2.48, r * 0.16);
      g.fillStyle = WH;
      g.beginPath(); g.arc(x, yHead - r * 1.18, r * 0.10, 0, 7); g.fill();
    } },
    { id: 'knit', nm: 'ニット帽', price: 100, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.30, r * 1.26, r * 1.12, '#7a5aa0');
      g.fillStyle = '#6a4b8e';
      g.fillRect(x - r * 1.26, yHead - r * 0.52, r * 2.52, r * 0.26);
      g.strokeStyle = '#8d6cb5'; g.lineWidth = 1;
      for (var i = -2; i <= 2; i++) {
        /* 編み目の縦線。お椀の縁からはみ出さないよう端ほど短くする */
        g.beginPath(); g.moveTo(x + i * r * 0.42, yHead - r * 0.52);
        g.lineTo(x + i * r * 0.46, yHead - r * (1.28 - Math.abs(i) * 0.18)); g.stroke();
      }
      g.fillStyle = CR;
      g.beginPath(); g.arc(x, yHead - r * 1.56, r * 0.26, 0, 7); g.fill();
    } },
    { id: 'straw', nm: '麦わら帽子', price: 140, draw: function (g, x, yHead, r) {
      brim(g, x, yHead - r * 0.48, r * 1.48, r * 0.30, '#e3c67c');
      dome(g, x, yHead - r * 0.50, r * 1.00, r * 0.92, '#d9b96a');
      g.fillStyle = RD;
      g.fillRect(x - r * 1.00, yHead - r * 0.74, r * 2.00, r * 0.18);
      g.strokeStyle = '#c0a055'; g.lineWidth = 1;
      g.beginPath(); g.ellipse(x, yHead - r * 0.48, r * 1.26, r * 0.24, 0, 0, 7); g.stroke();
    } },
    { id: 'hat', nm: 'ハット', price: 170, draw: function (g, x, yHead, r) {
      brim(g, x, yHead - r * 0.54, r * 1.40, r * 0.26, '#4a4038');
      dome(g, x, yHead - r * 0.52, r * 0.96, r * 1.14, '#5a4e44');
      g.fillStyle = '#3b332d';
      g.fillRect(x - r * 0.96, yHead - r * 0.78, r * 1.92, r * 0.22);
      g.strokeStyle = '#41382f'; g.lineWidth = 1.4;
      g.beginPath(); g.moveTo(x - r * 0.28, yHead - r * 1.60); g.lineTo(x + r * 0.28, yHead - r * 1.60); g.stroke();
    } },
    { id: 'beret', nm: 'ベレー帽', price: 110, draw: function (g, x, yHead, r, dir) {
      dome(g, x, yHead - r * 0.40, r * 1.18, r * 0.88, '#c4443a');
      g.fillStyle = '#c4443a';
      g.beginPath(); g.ellipse(x - dir * r * 0.10, yHead - r * 0.92, r * 1.20, r * 0.56, -dir * 0.18, 0, 7); g.fill();
      g.fillStyle = '#a3362e';
      g.beginPath(); g.arc(x + dir * r * 0.60, yHead - r * 1.28, r * 0.14, 0, 7); g.fill();
    } },
    { id: 'ribbon', nm: 'リボン帽', price: 130, draw: function (g, x, yHead, r, dir) {
      dome(g, x, yHead - r * 0.32, r * 1.24, r * 1.04, PK);
      g.fillStyle = '#d1769a';
      g.fillRect(x - r * 1.24, yHead - r * 0.46, r * 2.48, r * 0.14);
      bow(g, x + dir * r * 1.06, yHead - r * 0.64, r * 0.40, '#f4b3c6');
    } },
    { id: 'catear', nm: '猫耳風', price: 160, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.34, r * 1.24, r * 1.04, '#3b3640');
      var ear = function (sx) {
        g.fillStyle = '#3b3640';
        g.beginPath(); g.moveTo(x + sx * r * 0.28, yHead - r * 1.06);
        g.lineTo(x + sx * r * 0.64, yHead - r * 1.96); g.lineTo(x + sx * r * 0.96, yHead - r * 0.96); g.closePath(); g.fill();
        g.fillStyle = PK;
        g.beginPath(); g.moveTo(x + sx * r * 0.46, yHead - r * 1.10);
        g.lineTo(x + sx * r * 0.64, yHead - r * 1.62); g.lineTo(x + sx * r * 0.80, yHead - r * 1.04); g.closePath(); g.fill();
      };
      ear(-1); ear(1);
    } },
    { id: 'crown', nm: '王冠風', price: 220, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.46, r * 1.16, r * 0.90, '#c99a34');
      g.fillStyle = GD;
      g.beginPath();
      g.moveTo(x - r * 1.04, yHead - r * 1.10);
      for (var i = 0; i < 3; i++) {
        var x0 = x - r * 1.04 + i * r * 0.693;
        g.lineTo(x0 + r * 0.346, yHead - r * 1.98);
        g.lineTo(x0 + r * 0.693, yHead - r * 1.10);
      }
      g.lineTo(x + r * 1.04, yHead - r * 0.64); g.lineTo(x - r * 1.04, yHead - r * 0.64);
      g.closePath(); g.fill();
      g.fillStyle = RD;
      g.beginPath(); g.arc(x, yHead - r * 0.86, r * 0.14, 0, 7); g.fill();
      g.fillStyle = '#5fa8c4';
      g.beginPath(); g.arc(x - r * 0.60, yHead - r * 0.84, r * 0.09, 0, 7); g.fill();
      g.beginPath(); g.arc(x + r * 0.60, yHead - r * 0.84, r * 0.09, 0, 7); g.fill();
    } },
    { id: 'helmet', nm: 'ヘルメット', price: 150, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.40, r * 1.28, r * 1.10, '#e0b24a');
      brim(g, x, yHead - r * 0.44, r * 1.40, r * 0.16, '#c9982f');
      g.fillStyle = '#c9982f';
      g.fillRect(x - r * 0.10, yHead - r * 1.48, r * 0.20, r * 1.04);
      g.strokeStyle = '#8d7327'; g.lineWidth = 1;
      g.beginPath(); g.ellipse(x, yHead - r * 0.48, r * 1.24, r * 0.30, 0, Math.PI, Math.PI * 2); g.stroke();
    } },
    { id: 'bucket', nm: 'バケットハット', price: 120, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.40, r * 1.18, r * 1.04, '#8a8256');
      g.fillStyle = '#77704a';
      g.beginPath();
      g.moveTo(x - r * 1.18, yHead - r * 0.44); g.lineTo(x + r * 1.18, yHead - r * 0.44);
      g.lineTo(x + r * 1.42, yHead - r * 0.06); g.lineTo(x - r * 1.42, yHead - r * 0.06);
      g.closePath(); g.fill();
      g.strokeStyle = '#5f5a3a'; g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 1.00, yHead - r * 0.66); g.lineTo(x + r * 1.00, yHead - r * 0.66); g.stroke();
    } },
    { id: 'cowboy', nm: 'つば広帽', price: 190, draw: function (g, x, yHead, r) {
      g.fillStyle = '#7a5a3c';
      g.beginPath();
      g.moveTo(x - r * 1.52, yHead - r * 0.54);
      g.quadraticCurveTo(x, yHead - r * 0.22, x + r * 1.52, yHead - r * 0.54);
      g.quadraticCurveTo(x, yHead - r * 0.86, x - r * 1.52, yHead - r * 0.54);
      g.closePath(); g.fill();
      dome(g, x, yHead - r * 0.56, r * 0.94, r * 1.14, '#8f6b47');
      g.fillStyle = '#5d4630';
      g.fillRect(x - r * 0.94, yHead - r * 0.84, r * 1.88, r * 0.22);
      g.strokeStyle = '#6d5136'; g.lineWidth = 1.4;
      g.beginPath(); g.moveTo(x, yHead - r * 1.70); g.lineTo(x, yHead - r * 1.30); g.stroke();
    } },
    { id: 'witch', nm: 'とんがり帽子', price: 200, draw: function (g, x, yHead, r, dir) {
      brim(g, x, yHead - r * 0.52, r * 1.50, r * 0.28, '#3a3450');
      g.fillStyle = '#4a4368';
      g.beginPath(); g.moveTo(x - r * 0.90, yHead - r * 0.56);
      g.lineTo(x + r * 0.90, yHead - r * 0.56);
      g.lineTo(x + dir * r * 0.34, yHead - r * 2.16); g.closePath(); g.fill();
      g.fillStyle = GD;
      g.fillRect(x - r * 0.84, yHead - r * 0.80, r * 1.68, r * 0.20);
      g.beginPath(); g.arc(x + dir * r * 0.34, yHead - r * 2.10, r * 0.10, 0, 7); g.fill();
    } },
    { id: 'nightcap', nm: 'ナイトキャップ', price: 100, draw: function (g, x, yHead, r, dir) {
      dome(g, x, yHead - r * 0.32, r * 1.24, r * 1.06, '#cfd6e2');
      /* てっぺんから後ろへ垂れる先っぽ */
      g.fillStyle = '#cfd6e2';
      g.beginPath();
      g.moveTo(x - dir * r * 0.10, yHead - r * 1.36);
      g.quadraticCurveTo(x - dir * r * 1.10, yHead - r * 1.52, x - dir * r * 1.34, yHead - r * 0.94);
      g.quadraticCurveTo(x - dir * r * 0.90, yHead - r * 1.02, x - dir * r * 0.66, yHead - r * 1.14);
      g.closePath(); g.fill();
      g.fillStyle = WH;
      g.beginPath(); g.arc(x - dir * r * 1.34, yHead - r * 0.92, r * 0.22, 0, 7); g.fill();
      g.fillStyle = '#b6bfd0';
      g.fillRect(x - r * 1.24, yHead - r * 0.48, r * 2.48, r * 0.16);
    } },
    { id: 'hachimaki', nm: 'はちまき', price: 70, draw: function (g, x, yHead, r, dir) {
      dome(g, x, yHead - r * 0.36, r * 1.22, r * 1.02, '#e8e3d6');
      g.fillStyle = WH;
      g.fillRect(x - r * 1.24, yHead - r * 0.58, r * 2.48, r * 0.26);
      g.fillStyle = RD;
      g.beginPath(); g.arc(x + dir * r * 0.30, yHead - r * 0.45, r * 0.13, 0, 7); g.fill();
      g.strokeStyle = WH; g.lineWidth = r * 0.12;
      g.beginPath(); g.moveTo(x - dir * r * 1.08, yHead - r * 0.50);
      g.lineTo(x - dir * r * 1.46, yHead - r * 0.02); g.stroke();
    } },
    { id: 'bandana', nm: 'バンダナ', price: 80, draw: function (g, x, yHead, r, dir) {
      dome(g, x, yHead - r * 0.34, r * 1.24, r * 1.04, '#c4443a');
      g.fillStyle = WH;
      for (var i = -2; i <= 2; i++) {
        g.beginPath(); g.arc(x + i * r * 0.42, yHead - r * (0.72 + (i % 2 ? 0.22 : 0)), r * 0.08, 0, 7); g.fill();
      }
      g.fillStyle = '#a3362e';
      g.beginPath(); g.moveTo(x - dir * r * 1.00, yHead - r * 0.52);
      g.lineTo(x - dir * r * 1.50, yHead - r * 0.18); g.lineTo(x - dir * r * 1.00, yHead - r * 0.10);
      g.closePath(); g.fill();
    } },
    { id: 'mobcap', nm: 'ふんわり帽', price: 140, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.26, r * 1.22, r * 1.16, WH);
      frill(g, x, yHead - r * 0.34, r * 1.24, r * 0.19, CR, 6);
      dome(g, x, yHead - r * 0.44, r * 1.14, r * 0.98, WH);
      g.fillStyle = PK;
      g.beginPath(); g.arc(x, yHead - r * 1.26, r * 0.12, 0, 7); g.fill();
    } },
    { id: 'usagi', nm: 'うさ耳風', price: 170, draw: function (g, x, yHead, r) {
      dome(g, x, yHead - r * 0.34, r * 1.24, r * 1.02, '#f0ece2');
      var ear = function (sx) {
        g.fillStyle = '#f0ece2';
        g.beginPath(); g.ellipse(x + sx * r * 0.48, yHead - r * 1.42, r * 0.32, r * 0.72, sx * 0.16, 0, 7); g.fill();
        g.strokeStyle = '#d6cfbe'; g.lineWidth = 1;
        g.beginPath(); g.ellipse(x + sx * r * 0.48, yHead - r * 1.42, r * 0.32, r * 0.72, sx * 0.16, 0, 7); g.stroke();
        g.fillStyle = PK;
        g.beginPath(); g.ellipse(x + sx * r * 0.48, yHead - r * 1.44, r * 0.13, r * 0.42, sx * 0.16, 0, 7); g.fill();
      };
      ear(-1); ear(1);
    } },
    { id: 'casquette', nm: 'ハンチング', price: 120, draw: function (g, x, yHead, r, dir) {
      peak(g, x + dir * r * 0.40, yHead - r * 0.44, r, dir, '#6a6350', 0.50);
      g.fillStyle = '#7d7560';
      g.beginPath(); g.ellipse(x - dir * r * 0.08, yHead - r * 0.56, r * 1.12, r * 0.80, -dir * 0.10, Math.PI, Math.PI * 2); g.closePath(); g.fill();
      g.fillRect(x - r * 1.04, yHead - r * 0.60, r * 2.08, r * 0.24);
      g.strokeStyle = '#5e5849'; g.lineWidth = 1;
      g.beginPath(); g.moveTo(x - r * 0.58, yHead - r * 1.24); g.lineTo(x + r * 0.68, yHead - r * 0.86); g.stroke();
    } },
    { id: 'chefhat', nm: 'コック帽', price: 130, draw: function (g, x, yHead, r) {
      g.fillStyle = WH;
      g.fillRect(x - r * 1.00, yHead - r * 1.20, r * 2.00, r * 0.86);
      g.beginPath(); g.ellipse(x, yHead - r * 1.52, r * 1.12, r * 0.60, 0, 0, 7); g.fill();
      g.beginPath(); g.ellipse(x - r * 0.54, yHead - r * 1.72, r * 0.50, r * 0.44, 0, 0, 7); g.fill();
      g.beginPath(); g.ellipse(x + r * 0.54, yHead - r * 1.72, r * 0.50, r * 0.44, 0, 0, 7); g.fill();
      g.fillStyle = '#dcd6c6';
      g.fillRect(x - r * 1.00, yHead - r * 0.60, r * 2.00, r * 0.26);
    } },
    { id: 'hood', nm: 'フード', price: 110, draw: function (g, x, yHead, r, dir) {
      g.fillStyle = '#4f5a68';
      g.beginPath(); g.ellipse(x - dir * r * 0.10, yHead - r * 0.06, r * 1.32, r * 1.34, 0, Math.PI, Math.PI * 2); g.closePath(); g.fill();
      g.fillStyle = '#5d6a7a';
      g.beginPath(); g.ellipse(x + dir * r * 0.14, yHead - r * 0.34, r * 1.02, r * 1.06, 0, Math.PI, Math.PI * 2); g.closePath(); g.fill();
      g.fillStyle = '#3f4854';
      g.beginPath(); g.ellipse(x - dir * r * 0.78, yHead - r * 0.34, r * 0.40, r * 0.60, 0, 0, 7); g.fill();
    } }
  ];

  /* ================= 石の見た目 10種 ================= */
  /* でこぼこの多角形(今の石と同じ作り) */
  function chunk(g, r, n, lo, hi, col) {
    g.fillStyle = col;
    g.beginPath();
    for (var i = 0; i < n; i++) {
      var a = i / n * Math.PI * 2, rr = r * (lo + ((i * 5) % 3) * hi);
      if (i === 0) g.moveTo(Math.cos(a) * rr, Math.sin(a) * rr); else g.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
    }
    g.closePath(); g.fill();
  }
  /* 左上の照り返し */
  function gloss(g, r, col, sz) {
    g.fillStyle = col;
    g.beginPath(); g.arc(-r * 0.28, -r * 0.30, r * (sz || 0.34), 0, 7); g.fill();
  }

  var STONES = [
    /* 今の石そのまま(本編の drawStoneAt と同じ形・同じ色) */
    { id: 'plain', nm: 'ふつうの石', price: 0, draw: function (g, r) {
      chunk(g, r, 7, 0.82, 0.13, '#8d8e86');
      gloss(g, r, 'rgba(255,255,240,.30)');
    } },
    { id: 'blue', nm: '青い石', price: 30, draw: function (g, r) {
      chunk(g, r, 7, 0.82, 0.13, '#5f7f9e');
      gloss(g, r, 'rgba(230,245,255,.40)');
    } },
    { id: 'heart', nm: 'ハートの石', price: 120, draw: function (g, r) {
      g.fillStyle = '#d4607e';
      g.beginPath();
      g.moveTo(0, r * 1.05);
      g.bezierCurveTo(-r * 1.30, r * 0.10, -r * 0.62, -r * 1.10, 0, -r * 0.34);
      g.bezierCurveTo(r * 0.62, -r * 1.10, r * 1.30, r * 0.10, 0, r * 1.05);
      g.closePath(); g.fill();
      gloss(g, r, 'rgba(255,255,255,.35)', 0.24);
    } },
    { id: 'shiny', nm: '光る石', price: 150, draw: function (g, r) {
      g.fillStyle = 'rgba(255,240,170,.28)';
      g.beginPath(); g.arc(0, 0, r * 1.28, 0, 7); g.fill();
      chunk(g, r, 7, 0.80, 0.14, '#c8b465');
      gloss(g, r, 'rgba(255,255,220,.70)', 0.30);
      g.strokeStyle = 'rgba(255,250,200,.85)'; g.lineWidth = Math.max(0.6, r * 0.10);
      g.beginPath(); g.moveTo(-r * 0.9, -r * 0.9); g.lineTo(-r * 0.5, -r * 0.5); g.stroke();
    } },
    { id: 'round', nm: 'つるつるの丸石', price: 60, draw: function (g, r) {
      g.fillStyle = '#9a958c';
      g.beginPath(); g.arc(0, 0, r * 1.00, 0, 7); g.fill();
      gloss(g, r, 'rgba(255,255,250,.45)', 0.32);
    } },
    { id: 'angular', nm: '角張った石', price: 70, draw: function (g, r) {
      g.fillStyle = '#79776f';
      g.beginPath();
      g.moveTo(-r * 1.10, -r * 0.30); g.lineTo(-r * 0.20, -r * 1.10); g.lineTo(r * 1.05, -r * 0.45);
      g.lineTo(r * 0.70, r * 0.95); g.lineTo(-r * 0.60, r * 1.05);
      g.closePath(); g.fill();
      g.fillStyle = 'rgba(255,255,240,.22)';
      g.beginPath(); g.moveTo(-r * 0.20, -r * 1.05); g.lineTo(r * 0.95, -r * 0.42); g.lineTo(r * 0.10, -r * 0.10); g.closePath(); g.fill();
    } },
    { id: 'striped', nm: '模様入りの石', price: 90, draw: function (g, r) {
      chunk(g, r, 7, 0.82, 0.13, '#8a7f6d');
      g.save();
      g.beginPath(); g.arc(0, 0, r * 0.98, 0, 7); g.clip();
      g.strokeStyle = '#e6ddc6'; g.lineWidth = Math.max(0.8, r * 0.20);
      for (var i = -1; i <= 1; i++) {
        g.beginPath(); g.moveTo(-r * 1.2, i * r * 0.62 - r * 0.20); g.lineTo(r * 1.2, i * r * 0.62 + r * 0.20); g.stroke();
      }
      g.restore();
    } },
    { id: 'brick', nm: 'レンガのかけら', price: 80, draw: function (g, r) {
      g.fillStyle = '#a85b46';
      g.beginPath(); g.moveTo(-r * 1.15, -r * 0.72); g.lineTo(r * 1.10, -r * 0.80);
      g.lineTo(r * 1.15, r * 0.78); g.lineTo(-r * 1.05, r * 0.72); g.closePath(); g.fill();
      g.fillStyle = 'rgba(255,240,230,.30)';
      g.fillRect(-r * 1.05, -r * 0.70, r * 2.05, r * 0.26);
      g.fillStyle = 'rgba(60,30,20,.25)';
      g.fillRect(-r * 1.00, r * 0.34, r * 2.00, r * 0.30);
    } },
    { id: 'gold', nm: '金の石', price: 150, draw: function (g, r) {
      chunk(g, r, 8, 0.80, 0.15, '#d3a33c');
      g.fillStyle = 'rgba(255,245,190,.55)';
      g.beginPath(); g.moveTo(-r * 0.70, -r * 0.70); g.lineTo(r * 0.20, -r * 0.90);
      g.lineTo(-r * 0.10, -r * 0.05); g.closePath(); g.fill();
      gloss(g, r, 'rgba(255,255,225,.50)', 0.22);
    } },
    { id: 'star', nm: '星の石', price: 140, draw: function (g, r) {
      g.fillStyle = '#e6c84f';
      g.beginPath();
      for (var i = 0; i < 10; i++) {
        var a = -Math.PI / 2 + i * Math.PI / 5, rr = r * (i % 2 ? 0.50 : 1.22);
        if (i === 0) g.moveTo(Math.cos(a) * rr, Math.sin(a) * rr); else g.lineTo(Math.cos(a) * rr, Math.sin(a) * rr);
      }
      g.closePath(); g.fill();
      gloss(g, r, 'rgba(255,255,220,.40)', 0.22);
    } }
  ];

  window.WARDROBE = { COSTUMES: COSTUMES, HATS: HATS, STONES: STONES };
})();

----- END ORIGINAL CONTENT -----
===== END FILE 12: public/lib/wardrobe.js =====

===== BEGIN FILE 13: public/wardrobe_sheet.html =====
Source-Path: C:\石とゾンビ\public\wardrobe_sheet.html
Source-Bytes: 5602
Source-Lines: 126
Source-LastWriteTime-JST: 2026-09-13T19:51:40+09:00
Source-SHA256: a86d0c7f6c83d4f4d2037f316aee2c0e214ea181dbdac94fe751f920a3a2491f
Role: 衣装見本ページ
----- BEGIN ORIGINAL CONTENT -----
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="robots" content="noindex,nofollow">
<title>ショップ品の見本(石とゾンビ)</title>
<style>
  html,body{margin:0;padding:0;background:#efece4;color:#33302a;
    font-family:-apple-system,"Hiragino Kaku Gothic ProN","Yu Gothic",sans-serif;}
  #cv{display:block;width:390px;}
</style>
</head>
<body>
<canvas id="cv"></canvas>
<script src="lib/shurechara.js"></script>
<script src="lib/wardrobe.js"></script>
<script>
/* ショップに並ぶ品を全部その場で描いて並べる紙。
   ふだん見る大きさ(頭の半径15px)で潰れないかを確かめるために作った。
   URLの後ろに ?big=1 を付けると衣装だけ大きく描く(形を確かめる用)。
   ?big=1&from=27 で28番目から。 */
(function () {
  var W = 390, DPR = Math.min(2, window.devicePixelRatio || 1);
  var q = location.search, BIG = /big=1/.test(q);
  var FROM = (q.match(/from=(\d+)/) || [0, 0])[1] | 0;
  var cv = document.getElementById('cv'), g = cv.getContext('2d');
  var CO = WARDROBE.COSTUMES, HA = WARDROBE.HATS, ST = WARDROBE.STONES;

  /* ---- 大きさの決め方 ---- */
  var cCols = 6, cCellW = W / cCols, cCellH = 104, cR = 15;          /* 衣装(本編と同じ小ささ) */
  var hCols = 3, hCellW = W / hCols, hCellH = 92, hR = 12;           /* 帽子(1つにつき髪型3種) */
  var sCols = 5, sCellW = W / sCols, sCellH = 70;                    /* 石 */
  var bCols = 3, bCellW = W / bCols, bCellH = 172, bR = 26;          /* 大きく見る用 */

  var list = BIG ? CO.slice(FROM) : CO;
  var cRows = Math.ceil((BIG ? list.length : CO.length) / (BIG ? bCols : cCols));
  var hRows = Math.ceil(HA.length / hCols), sRows = Math.ceil(ST.length / sCols);
  var H = BIG ? (30 + cRows * bCellH + 14)
             /* 見出しは3つとも 24px ぶん場所を取る(1つぶんしか数えないと下の名前が切れる) */
             : (14 + 24 * 3 + cRows * cCellH + 26 + hRows * hCellH + 26 + sRows * sCellH + 16);

  cv.width = W * DPR; cv.height = H * DPR; cv.style.height = H + 'px';
  g.setTransform(DPR, 0, 0, DPR, 0, 0);
  g.fillStyle = '#efece4'; g.fillRect(0, 0, W, H);
  g.textAlign = 'center'; g.lineJoin = 'round'; g.lineCap = 'round';

  /* 見出し */
  function head(y, s) {
    g.fillStyle = '#6a6152'; g.fillRect(8, y - 9, W - 16, 1);
    g.fillStyle = '#33302a'; g.font = 'bold 11px sans-serif';
    g.textAlign = 'left'; g.fillText(s, 8, y + 4); g.textAlign = 'center';
  }
  /* 品名と値段 */
  function tag(cx, y, nm, price) {
    g.fillStyle = '#33302a'; g.font = '8px sans-serif';
    g.fillText(nm, cx, y);
    g.fillStyle = '#8a7d5e'; g.font = '7px sans-serif';
    g.fillText(price === 0 ? '既定' : (price + ''), cx, y + 8);
  }
  /* 人を1人描いて衣装を着せる */
  function wear(item, cx, yHead, r, seed) {
    var av = SHURE.fromSeed(seed), dir = (seed % 2) ? 1 : -1;
    var shY = yHead + r * 1.30, bot = yHead + r * 3.70;
    SHURE.draw(g, cx, yHead, r, dir, av, {}, bot);
    item.draw(g, cx, shY, bot, r, SHURE.WEAR_COL[av.w], dir);
  }

  var y = 14;

  if (BIG) {
    /* ---- 形を確かめるための大きい並び ---- */
    g.fillStyle = '#33302a'; g.font = 'bold 11px sans-serif';
    g.textAlign = 'left'; g.fillText('衣装を大きく見る(' + (FROM + 1) + '番目から)', 8, 16); g.textAlign = 'center';
    y = 30;
    for (var bi = 0; bi < list.length; bi++) {
      var bc = bi % bCols, br = (bi / bCols) | 0;
      var bx = bc * bCellW + bCellW / 2, by = y + br * bCellH;
      wear(list[bi], bx, by + 30, bR, (FROM + bi) * 37 + 3);
      tag(bx, by + 152, list[bi].nm, list[bi].price);
    }
    return;
  }

  /* ---- 衣装 ---- */
  head(y + 8, '特別衣装 ' + CO.length + '種(頭の半径15px=本編と同じ大きさ)');
  y += 24;
  for (var i = 0; i < CO.length; i++) {
    var c = i % cCols, rw = (i / cCols) | 0;
    var cx = c * cCellW + cCellW / 2, cy = y + rw * cCellH;
    wear(CO[i], cx, cy + 20, cR, i * 37 + 3);
    tag(cx, cy + 94, CO[i].nm, CO[i].price);
  }
  y += cRows * cCellH + 26;

  /* ---- 帽子(1つにつき髪型3種。いちばん破綻しやすいアフロ・ロング・ぼさで見る) ---- */
  head(y + 8, '帽子・頭装備 ' + HA.length + '種(髪型はアフロ・ロング・ぼさ)');
  y += 24;
  var hairs = [5, 3, 1];
  for (var k = 0; k < HA.length; k++) {
    var hc = k % hCols, hr = (k / hCols) | 0;
    var gx = hc * hCellW, gy = y + hr * hCellH;
    for (var t = 0; t < 3; t++) {
      var av = SHURE.fix({ h: hairs[t], hc: (k + t) % SHURE.HAIR_COL.length, sk: (k + t) % 4, w: (k * 3 + t) % 8 });
      var px = gx + 22 + t * 43, pyH = gy + 36, dir = t === 1 ? -1 : 1;
      SHURE.draw(g, px, pyH, hR, dir, av, {}, pyH + hR * 3.0);
      HA[k].draw(g, px, pyH, hR, dir, SHURE.HAIR_COL[av.hc]);
    }
    tag(gx + hCellW / 2, gy + 78, HA[k].nm, HA[k].price);
  }
  y += hRows * hCellH + 26;

  /* ---- 石(投げている時の小ささと、形が分かる大きさの2つ) ---- */
  head(y + 8, '石の見た目 ' + ST.length + '種(左が実際の大きさ・上が拡大)');
  y += 24;
  for (var s = 0; s < ST.length; s++) {
    var sc = s % sCols, sr = (s / sCols) | 0;
    var sx = sc * sCellW + sCellW / 2, sy = y + sr * sCellH;
    g.save(); g.translate(sx + 6, sy + 20); ST[s].draw(g, 16, 0); g.restore();
    g.save(); g.translate(sx - 22, sy + 24); ST[s].draw(g, 5, 0); g.restore();
    tag(sx, sy + 48, ST[s].nm, ST[s].price);
  }
})();
</script>
</body>
</html>

----- END ORIGINAL CONTENT -----
===== END FILE 13: public/wardrobe_sheet.html =====

===== BEGIN FILE 14: public/sfx_volume.js =====
Source-Path: C:\石とゾンビ\public\sfx_volume.js
Source-Bytes: 1733
Source-Lines: 40
Source-LastWriteTime-JST: 2026-08-29T06:41:00+09:00
Source-SHA256: 1c12200425de2c6090d7bf4e531874f46afad48fd0930c752cdb6d62a8c2a14b
Role: 効果音音量設定コード
----- BEGIN ORIGINAL CONTENT -----
/* 効果音の音量つまみ(2026-08-11 K1指示)。
   サウンドエディター(editor_sound.html)で決めた音ごとの倍率を、ゲーム側の再生に掛ける。
   idは sfx_ingame.json と同じ `cls:キー` / `raid:キー` の形。
   保存はこの端末のlocalStorageなので、決定版はエディターの「コピー」でJSONを渡して焼き込む。 */
(function(){
  'use strict';
  var KEY='hzg_sfx_vol_v1';
  /* 焼き込み値(エディターのコピーで更新)。1.0=素材のまま。ここに無い音は1.0 */
  var BAKED={};
  var vol=null;
  function load(){
    vol={};
    for(var k in BAKED) vol[k]=BAKED[k];
    try{
      var d=JSON.parse(((typeof window!=='undefined'&&window.HZG_BUILD)?null:localStorage.getItem(KEY))||'null');   // 本編では端末調整を読まない(2026-08-13 K1決定)
      if(d && d.v) for(var k2 in d.v) vol[k2]=d.v[k2];
    }catch(e){}
    return vol;
  }
  /* 倍率を返す。part='cls'|'raid'、name=表のキー */
  window.hzgSfxVol=function(part, name){
    if(!vol) load();
    var v=vol[part+':'+name];
    return (typeof v==='number' && v>=0) ? v : 1;
  };
  /* 曲の倍率(2026-08-30 K1指示)。idは 'bgm:キー'。効果音と同じ保存に相乗り */
  window.hzgBgmVol=function(name){
    if(!vol) load();
    var v=vol['bgm:'+name];
    return (typeof v==='number' && v>=0) ? v : 1;
  };
  window.hzgSfxVolAll=function(){ if(!vol) load(); return vol; };
  window.hzgSfxVolSet=function(id, v){
    if(!vol) load();
    if(v===null || v===1) delete vol[id]; else vol[id]=v;
    try{ localStorage.setItem(KEY, JSON.stringify({version:1, v:vol})); }catch(e){}
  };
  window.hzgSfxVolReload=load;
  window.HZG_SFX_VOL_BAKED=BAKED;
})();

----- END ORIGINAL CONTENT -----
===== END FILE 14: public/sfx_volume.js =====

===== BEGIN FILE 15: public/dev_tools.js =====
Source-Path: C:\石とゾンビ\public\dev_tools.js
Source-Bytes: 102814
Source-Lines: 1712
Source-LastWriteTime-JST: 2026-09-12T03:07:54+09:00
Source-SHA256: c1ae188b528f8c7420d83e149ff6a0cd2a551d59d18e3d1b489a6c32d8339cf6
Role: 共通開発道具・バグ報告等。省略せず全文収録
----- BEGIN ORIGINAL CONTENT -----
/* 開発者向け隠しツール(2026-08-07 K1指示)。
   ★リリース時はこの1行をfalseにするだけで全部止まる(K1指示: 隠し機能を消せるように)★
   呼び出し: 画面をトリプルタップ(3連打)して3回目を長押し→ツールメニュー
   ・px物差し: ドラッグで移動、右端の○で回転(左端が軸)。半透明・300px・10px刻み
   ・タップ座標: ONの間、タップした場所の教室内座標(390×632系)を表示
   ・スナップショット: いまの戦況/ゲーム状態をJSONでクリップボードへ(バグ報告用) */
window.HZG_DEV_TOOLS = true;

/* 計器の入切(2026-09-06 K1指示「規定は全部オフ」「計器を完全にオフのモードも作れ」)。
   毎コマ・毎回、入れ物を作ったりDOMを触る計器は**既定オフ**。
   出来事が起きた時だけ動く軽い物(起動・音の口・曲・画面が隠れた・コマ仕事・物理の重さ)は既定オン。
   HZG_METERS.全部切る=true にすると、軽い物も含めて**1つ残らず止まる**。
   ここは端末に保存する(🐛の「計器」から切り替え)。バグを追う時だけ点けて、終わったら消す。 */
window.HZG_METERS = (function(){
  var D = { 全部切る:false, 効果音:false, 床の物:false, 必殺技:false, 絵の大きさ:false,
            起動:true, 音の口:true, 曲:true, 画面:true, コマ仕事:true, 物理:true,
            /* ★スポット計器(そのバグ専用・直したら消す)。追っている間だけ点ける物なので既定オン */
            スポット:true };
  var v = {};
  for(var k in D) v[k]=D[k];
  try{ var j=JSON.parse(localStorage.getItem('hzg_meters_v1')||'null');
    if(j) for(var k2 in D) if(typeof j[k2]==='boolean') v[k2]=j[k2]; }catch(e){}
  v.既定 = D;
  v.on = function(name){ return !v.全部切る && !!v[name]; };
  v.set = function(name, b){ v[name]=!!b;
    try{ var o={}; for(var k3 in D) o[k3]=v[k3]; localStorage.setItem('hzg_meters_v1', JSON.stringify(o)); }catch(e){} };
  return v;
})();

(function(){
'use strict';
if(!window.HZG_DEV_TOOLS) return;

/* ---------- 共通: 教室シーンの座標系(本編・襲撃どちらも390×632の#scene) ---------- */
function findScene(){
  const m=document.getElementById('mount');
  if(m && m.shadowRoot){ const sc=m.shadowRoot.getElementById('scene'); if(sc) return sc; }
  return document.getElementById('scene');
}
function toSceneXY(cx,cy){
  const sc=findScene(); if(!sc) return null;
  const r=sc.getBoundingClientRect(); if(!r.width) return null;
  const k=r.width/390;
  return { x:Math.round((cx-r.left)/k), y:Math.round((cy-r.top)/k) };
}

/* ---------- ① px物差し ---------- */
let ruler=null, angle=0, px=60, py=200;
function buildRuler(){
  const r=document.createElement('div');
  r.id='hzgRuler';
  r.style.cssText='position:fixed; left:0; top:0; width:300px; height:44px; z-index:999999;'
    +'touch-action:none; -webkit-user-select:none; user-select:none; transform-origin:0 50%;'
    +'background:rgba(255,244,200,.42); border:1px solid rgba(120,90,20,.6); border-radius:4px;'
    +'box-shadow:0 2px 8px rgba(0,0,0,.25);';
  const ticks=document.createElement('div');
  ticks.style.cssText='position:absolute; inset:0; pointer-events:none;'
    +'background:'
    +'repeating-linear-gradient(90deg, rgba(60,40,10,.75) 0 1px, transparent 1px 10px),'
    +'repeating-linear-gradient(90deg, rgba(60,40,10,.9) 0 1px, transparent 1px 50px);'
    +'background-size:100% 8px, 100% 15px; background-repeat:no-repeat;';
  r.appendChild(ticks);
  for(let v=0; v<=300; v+=50){
    const lb=document.createElement('span');
    lb.textContent=v;
    lb.style.cssText='position:absolute; top:17px; left:'+(v-11)+'px; width:22px; text-align:center;'
      +'font:9px/1 -apple-system,sans-serif; color:rgba(60,40,10,.85); pointer-events:none;';
    r.appendChild(lb);
  }
  const deg=document.createElement('span');
  deg.id='hzgRulerDeg';
  deg.style.cssText='position:absolute; left:50%; bottom:2px; transform:translateX(-50%);'
    +'font:9px/1 -apple-system,sans-serif; color:rgba(60,40,10,.7); pointer-events:none;';
  r.appendChild(deg);
  const hd=document.createElement('div');
  hd.id='hzgRulerHandle';
  hd.textContent='↻';
  hd.style.cssText='position:absolute; right:-15px; top:50%; margin-top:-15px; width:30px; height:30px;'
    +'border-radius:50%; background:rgba(40,83,63,.85); color:#f7f0dd; font:16px/30px sans-serif;'
    +'text-align:center; border:1px solid rgba(255,255,255,.4); touch-action:none;';
  r.appendChild(hd);
  let drag=null;
  r.addEventListener('pointerdown', ev=>{
    if(ev.target===hd) return;
    ev.stopPropagation(); ev.preventDefault();
    drag={x0:ev.clientX, y0:ev.clientY, px0:px, py0:py};
    try{ r.setPointerCapture(ev.pointerId); }catch(e){}
  });
  r.addEventListener('pointermove', ev=>{
    if(!drag) return;
    px=drag.px0+(ev.clientX-drag.x0); py=drag.py0+(ev.clientY-drag.y0);
    placeRuler();
  });
  const dEnd=()=>{ drag=null; };
  r.addEventListener('pointerup', dEnd);
  r.addEventListener('pointercancel', dEnd);
  hd.addEventListener('pointerdown', ev=>{
    ev.stopPropagation(); ev.preventDefault();
    try{ hd.setPointerCapture(ev.pointerId); }catch(e){}
    const mv=e2=>{ angle=Math.atan2(e2.clientY-py, e2.clientX-px)*180/Math.PI; placeRuler(); };
    const up=()=>{ hd.removeEventListener('pointermove',mv); hd.removeEventListener('pointerup',up); hd.removeEventListener('pointercancel',up); };
    hd.addEventListener('pointermove',mv);
    hd.addEventListener('pointerup',up);
    hd.addEventListener('pointercancel',up);
  });
  document.body.appendChild(r);
  return r;
}
function placeRuler(){
  ruler.style.transform='translate('+px+'px,'+(py-22)+'px) rotate('+angle+'deg)';
  const d=document.getElementById('hzgRulerDeg');
  if(d){ const a=((angle%360)+360)%360; d.textContent=Math.round(a)+'°'; }
}
function toggleRuler(x,y){
  if(ruler){ ruler.remove(); ruler=null; return; }
  px=Math.min(Math.max(10,x), innerWidth-60); py=y; angle=0;
  ruler=buildRuler(); placeRuler();
}

/* ---------- ② タップ座標インスペクタ ---------- */
let inspectOn=false;
document.addEventListener('pointerdown', ev=>{
  if(!inspectOn || !ev.isPrimary) return;
  if(ev.target && ev.target.closest && ev.target.closest('#hzgDevMenu,#hzgRuler')) return;
  const p=toSceneXY(ev.clientX, ev.clientY);
  const lb=document.createElement('div');
  lb.textContent=p ? (p.x+', '+p.y) : (Math.round(ev.clientX)+', '+Math.round(ev.clientY)+' (画面px)');
  lb.style.cssText='position:fixed; z-index:999998; pointer-events:none;'
    +'left:'+(ev.clientX+10)+'px; top:'+(ev.clientY-30)+'px;'
    +'background:rgba(12,20,16,.9); color:#ffe184; font:bold 12px/1 -apple-system,sans-serif;'
    +'padding:5px 8px; border-radius:7px; border:1px solid rgba(255,214,106,.5); white-space:nowrap;';
  document.body.appendChild(lb);
  const a=lb.animate([{opacity:1},{opacity:1,offset:.7},{opacity:0}],{duration:1400});
  a.finished.then(()=>lb.remove()).catch(()=>lb.remove());
}, true);

/* ---------- ③ 不具合スナップショット ---------- */
/* DOM・関数・循環参照を安全に落とすstringify(フィールドを列挙しないので実装が変わっても壊れない) */
function safeJson(obj){
  /* 「同じ物を2度見たら循環」だと、共有しているだけの値(席の座標など)まで潰れる。
     今いる枝をたどって、本当に自分へ戻ってきた時だけ循環とする(2026-08-18) */
  const stack=[];
  return JSON.stringify(obj, function(k,v){
    if(typeof v==='function') return undefined;
    if(v && typeof v==='object'){
      if(typeof Element!=='undefined' && v instanceof Element) return undefined;
      if(typeof Animation!=='undefined' && v instanceof Animation) return undefined;
      while(stack.length && stack[stack.length-1]!==this) stack.pop();
      if(stack.indexOf(v)>=0) return '[循環]';
      stack.push(v);
    }
    if(typeof v==='number') return Math.round(v*100)/100;
    return v;
  });
}
/* gameの丸ごと書き出しをやめ、開始時から積み上がる巨大データは要約に置き換える(2026-08-10 K1指示)。
   mobPool=140人の名簿+フレーバー全文/blood=血痕1滴ごとの座標/corpses=死体のフレーバー/
   既読セリフ6種の番号の束——調査で使わないのに全体の8割を占めていた */
function slimDaily(g){
  const d={};
  for(const k in g){
    if(k==='mobPool'){ const p=g[k]||{}; d.mobPool='残り 2年'+((p.y2||[]).length)+'人・他学年'+((p.other||[]).length)+'人'; continue; }
    if(k==='blood'){ let n=(g[k]||[]).length;
      try{ if(typeof battleHandle!=='undefined' && battleHandle && battleHandle.bloodRefs) n=battleHandle.bloodRefs().length; }catch(e){}
      d.blood='血痕'+n+'滴'; continue; }   // 段2: 正本はエンジン帳面
    if(k==='corpses'){ d.corpses=(g[k]||[]).map(c=>({x:c.x, y:c.y, label:c.label, fall:c.fall})); continue; }
    if(/Seen$/.test(k)){ const v=g[k]; d[k]='既読'+((v&&v.size!=null)?v.size:(v?Object.keys(v).length:0))+'件'; continue; }
    d[k]=g[k];
  }
  return d;
}
function takeSnapshot(){
  const out={ at:new Date().toISOString(), ver:window.HZG_BUILD||'?', url:location.pathname+location.search };
  // 職員室凍結の遠隔調査(2026-08-13): 部屋の組み立て結果と端末に残る保存キーも吐く
  try{ if(typeof placeHandle!=='undefined' && placeHandle && placeHandle.roomInfo) out.roomInfo=placeHandle.roomInfo(); }catch(e){ out.roomInfo='err:'+e.message; }
  try{ out.lsKeys=Object.keys(localStorage).map(k=>k+':'+(localStorage.getItem(k)||'').length); }catch(e){}
  try{ if(typeof window.HZG_SNAPSHOT==='function') out.raid=window.HZG_SNAPSHOT(); }catch(e){ out.raidErr=String(e); }
  try{ if(typeof game!=='undefined') out.daily=slimDaily(game); }catch(e){}
  let txt;
  try{ txt=safeJson(out); }catch(e){ txt='{"err":"'+String(e)+'"}'; }
  const kb=Math.round(txt.length/102.4)/10;
  const done=okFlag=>toast(okFlag ? 'スナップショットをコピーした ('+kb+'KB)' : 'コピー失敗→手動コピー画面を出す');
  if(navigator.clipboard && navigator.clipboard.writeText){
    navigator.clipboard.writeText(txt).then(()=>done(true)).catch(()=>{ done(false); manualCopy(txt); });
  } else { done(false); manualCopy(txt); }
}
function manualCopy(txt){
  const w=document.createElement('div');
  w.style.cssText='position:fixed; inset:8% 5%; z-index:999999; background:rgba(12,20,16,.97);'
    +'border:1px solid #4d7a5f; border-radius:12px; padding:10px; display:flex; flex-direction:column; gap:8px;';
  const ta=document.createElement('textarea');
  ta.value=txt; ta.readOnly=true;
  ta.style.cssText='flex:1; background:#0b130f; color:#cfe0d5; font:10px/1.4 monospace; border:0; border-radius:8px; padding:8px;';
  const bt=document.createElement('button');
  bt.textContent='閉じる';
  bt.style.cssText='padding:10px; border:0; border-radius:9px; background:#28533f; color:#f7f0dd; font-weight:bold;';
  bt.addEventListener('click',()=>w.remove());
  w.appendChild(ta); w.appendChild(bt);
  document.body.appendChild(w);
  ta.focus(); ta.select();
}
function toast(msg){
  const t=document.createElement('div');
  t.textContent=msg;
  t.style.cssText='position:fixed; left:50%; top:14%; transform:translateX(-50%); z-index:999999;'
    +'background:rgba(12,20,16,.92); color:#f7f0dd; font:bold 12px/1.4 -apple-system,sans-serif;'
    +'padding:9px 14px; border-radius:9px; border:1px solid #4d7a5f; pointer-events:none; white-space:nowrap;';
  document.body.appendChild(t);
  const a=t.animate([{opacity:0},{opacity:1,offset:.1},{opacity:1,offset:.8},{opacity:0}],{duration:1900});
  a.finished.then(()=>t.remove()).catch(()=>t.remove());
}

/* ---------- メモ帳(2026-08-07 K1指示) ----------
   開いたら毎回さっきの続き(書きかけは自動保存)。保存=1行目を見出しに一覧へ入れて新規に。
   削除=書きかけを捨てて新規に。本文コピーあり。保存済みは見出しタップで続きを書ける */
const MEMO_DRAFT='hzg_memo_draft_v1', MEMO_LIST='hzg_memo_list_v1';
let memoWin=null;
function memoList(){ try{ return JSON.parse(localStorage.getItem(MEMO_LIST)||'[]')||[]; }catch(e){ return []; } }
function openMemo(){
  if(memoWin){ memoWin.remove(); memoWin=null; }
  const w=document.createElement('div');
  memoWin=w;
  w.style.cssText='position:fixed; inset:6% 4% 8%; z-index:999999; display:flex; flex-direction:column; gap:7px;'
    +'background:rgba(12,20,16,.97); border:1px solid #4d7a5f; border-radius:13px; padding:10px;';
  const ta=document.createElement('textarea');
  ta.placeholder='メモ(書きかけは勝手に残る)';
  ta.value=localStorage.getItem(MEMO_DRAFT)||'';
  ta.style.cssText='flex:1; min-height:0; background:#0b130f; color:#eee6d4; font:13px/1.6 -apple-system,sans-serif;'
    +'border:1px solid #2c4638; border-radius:9px; padding:9px; resize:none; -webkit-user-select:text; user-select:text;';
  ta.addEventListener('input', ()=>localStorage.setItem(MEMO_DRAFT, ta.value));
  const row=document.createElement('div');
  row.style.cssText='display:flex; gap:6px;';
  const mkB=(label,bg,fn)=>{
    const b=document.createElement('button');
    b.textContent=label;
    b.style.cssText='flex:1; padding:11px 4px; border:0; border-radius:9px; background:'+bg+';'
      +'color:#f7f0dd; font:bold 12px/1 -apple-system,sans-serif;';
    b.addEventListener('click', ev=>{ ev.stopPropagation(); fn(); });
    row.appendChild(b);
  };
  const list=document.createElement('div');
  list.style.cssText='flex:0 0 auto; max-height:32%; overflow-y:auto; display:flex; flex-direction:column; gap:4px;';
  const renderList=()=>{
    list.innerHTML='';
    for(const [i,m] of memoList().entries()){
      const r=document.createElement('div');
      r.style.cssText='display:flex; align-items:center; gap:6px; background:#1a2a21; border:1px solid #2c4638;'
        +'border-radius:8px; padding:8px 9px;';
      const t=document.createElement('span');
      t.textContent=m.t||'(無題)';
      t.style.cssText='flex:1; font:bold 11.5px/1.3 -apple-system,sans-serif; color:#cfe0d5;'
        +'white-space:nowrap; overflow:hidden; text-overflow:ellipsis;';
      t.addEventListener('click', ()=>{   // 見出しタップ=そのメモの続きを書く(一覧からは外す)
        const arr=memoList(); const [mm]=arr.splice(i,1);
        localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
        ta.value=mm.body; localStorage.setItem(MEMO_DRAFT, mm.body);
        renderList();
      });
      const x=document.createElement('button');
      x.textContent='✕';
      x.style.cssText='flex:0 0 auto; width:30px; height:30px; border:0; border-radius:7px;'
        +'background:#7a4040; color:#f7f0dd; font:bold 12px/1 sans-serif;';
      x.addEventListener('click', ()=>{
        const arr=memoList(); arr.splice(i,1);
        localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
        renderList();
      });
      r.appendChild(t); r.appendChild(x); list.appendChild(r);
    }
  };
  mkB('保存','#28533f',()=>{
    const v=ta.value.trim();
    if(!v){ toast('空のメモは保存しない'); return; }
    const arr=memoList();
    arr.unshift({ t:v.split('\n')[0].slice(0,40), body:ta.value, at:Date.now() });
    localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
    ta.value=''; localStorage.setItem(MEMO_DRAFT,'');
    renderList(); toast('保存した(新規メモに)');
  });
  mkB('本文コピー','#3f6f56',()=>{
    const v=ta.value;
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(v).then(()=>toast('本文をコピーした')).catch(()=>manualCopy(v));
    } else manualCopy(v);
  });
  mkB('削除','#7a4040',()=>{ ta.value=''; localStorage.setItem(MEMO_DRAFT,''); toast('消して新規メモに'); });
  mkB('閉じる','#5d5040',()=>{ w.remove(); memoWin=null; });
  w.appendChild(ta); w.appendChild(row); w.appendChild(list);
  w.addEventListener('pointerdown', ev=>ev.stopPropagation());
  document.body.appendChild(w);
  renderList();
}

/* ---------- ④ UIレイアウト編集モード(2026-08-07 K1指示) ----------
   対象のUI部品をドラッグで動かし、動かした差分だけをJSONで吐く。
   差分はlocalStorage(hzg_uiedit_v1)にも残して次回起動時も適用——
   K1がJSONをくれたらコードへ焼き込み、リセットで消す運用 */
const UIED_KEY='hzg_uiedit_v1';
/* 【2026-09-02 K1指摘の根治】K1「デプロイごとのUIの差分をJSONで吐かないといけないところを、
   UIレイアウト編集を開くごとの差分にしてないか」——そのとおりだった。
   端末に残った調整値はコードの値の上に重ねて掛かるので、こちらが数値を焼き込むと
   「焼いた値+端末の値」の二重になり、次に出す差分は**焼き込み後の位置からの差**になっていた。
   これでは何度やっても合わない。
   直し方: 調整値に**その時の版番号**を付けて持ち、版が変わったら丸ごと捨てる。
   つまり調整値は必ず「今デプロイされているコードの値」からの差になる。
   部品ごとに手で UIED_BAKED_IDS へ足していく運用も要らなくなる */
function uiedVer(){
  try{
    const js=document.querySelector('script[src*="raid_battle.js"]');
    return String((js && js.getAttribute('src')) || '').split('?')[1] || '?';
  }catch(e){ return '?'; }
}
/* 焼き込みの印(2026-08-26 K1報告の根治): K1が調整した値をコードへ焼いた時は、この文字列を変える。
   端末に残っている調整値は、焼いた値と二重に掛かって「調整が無かったことにされた」ように見える——
   実際に下段ラベルで発生した(焼き込みで11px下げ+端末の11pxが1.2秒後の再適用で乗り、22px下がった)。
   印が変わっていたら、起動時に端末の調整値を自動で捨てる(=リセットを押したのと同じ)。
   K1に「リセットを押してください」と頼まなくて済むようにするための仕掛け */
const UIED_BAKED='20260901v';
const UIED_BAKED_KEY='hzg_uiedit_baked';
/* armoryRowは armory の中にあるので、**必ずarmoryより先**に並べる——
   先に書いた方が掴める(2026-09-02) */
const UIED_IDS=['armoryRow','spPanel','wazaBtn','ctrlFab','pauseTag','dmgLbl','itemBtn','raidTestClose','armory','readyBtn','sleepBtn','goPop','hdr',
  'rationBar','amTidy','setBtn',
  'partyAsk',   // 「誰が行きますか」バー(2026-08-21 K1指示)
  /* 下段ラベル(2026-08-26 K1指示「素材に圧迫感がある」)。板と中身を別々に動かせる:
     tabBar=ドックの板そのもの / tabRow=アイコンと文字の5個ぶんまとめて。
     大きさのつまみ(%)で縮めれば圧迫感を弱められる */
  'tabBar','tabRow',
  /* 所持金と探索ライブ中継の小窓(2026-09-01 K1指示)。
     小窓は**探索に出している間しか出ない**ので、動かす時は隊を出してから編集モードに入る */
  'moneyHud','lfChip',
  /* インベントリ(2026-09-02 K1指示)。**中身と縁を別々に動かす**:
     ・armoryRow = 中のアイテム欄(66pxのマスの格子)そのもの
     ・armory    = 外の縁とタブボタンが合体した板ぜんぶ
     armoryRowをarmoryより先に並べる——重なっているので、先に書いた方が掴める。
     縁(armory)を大きくしても中のマスは大きくならない(下のuiedRowSyncが打ち消す)ので、
     「縁だけ広げて、中身は中身で動かす」ができる */
  ];
/* 【2026-09-02 K1指摘の根治その2】つまみは開くと必ず100%から始まり、
   触った瞬間にCSSへ焼いてある値(例 #armory の scale:.86)を**上書き**していた。
   つまり編集を始めた瞬間に部品が跳び、K1は「今どこにいるか」を知らないまま調整していた。
   位置も同じで、CSSに translate が焼いてある部品は掴んだ瞬間0へ戻っていた。
   直し方: 初めて掴んだ時に**今そう見えている値**(computed)を読んで、そこから始める。
   これでつまみの数字は常に「今の姿」を指し、出てくる差分は
   **そのままCSSへ書ける最終の値**になる(こちらで掛け算しない=ずれる余地が消える) */
function uiedInit(el){
  const o={dx:0, dy:0};
  try{
    const cs=getComputedStyle(el);
    const t=cs.translate;
    if(t && t!=='none'){ const p=t.split(/\s+/); o.dx=parseFloat(p[0])||0; o.dy=parseFloat(p[1]||'0')||0; }
    const sc=cs.scale;
    if(sc && sc!=='none'){ const v=parseFloat(sc); if(v>0) o.s=v; }
  }catch(e){}
  return o;
}
/* 縁(armory)の拡大を中のアイテム欄に効かせない。
   armoryRow自身のずらし・大きさは、その上に足す */
function uiedRowSync(){
  const el=uiedEl('armoryRow'); if(!el) return;
  const S=(uiedOff.armory && uiedOff.armory.s) || 1;
  const r=uiedOff.armoryRow || {};
  const rs=(r.s!=null? r.s : 1);
  el.style.scale=String(rs/S);
  el.style.translate=((r.dx||0)/S).toFixed(1)+'px '+((r.dy||0)/S).toFixed(1)+'px';
}
let uiedOn=false, uiedOff={}, uiedBar=null, uiedDrag=null, uiedLast=null, uiedSzInput=null, uiedSzLabel=null;
try{
  const raw=JSON.parse(localStorage.getItem(UIED_KEY)||'null');
  if(raw && raw.__ver!==undefined){
    if(raw.__ver===uiedVer()){ uiedOff=raw.off||{}; }
    else { console.log('[UI編集] 版が変わったので端末の調整値を捨てた:', raw.__ver, '→', uiedVer());
           localStorage.removeItem(UIED_KEY); }
  } else if(raw){
    /* 版の印が無い古い形は、いつのコードに対する差か分からないので捨てる */
    console.log('[UI編集] 版の印が無い古い調整値を捨てた');
    localStorage.removeItem(UIED_KEY);
  }
}catch(e){}
/* 保存はいつも版の印つき。読む時に版が違えば捨てられる */
function uiedSave(){
  try{ localStorage.setItem(UIED_KEY, JSON.stringify({__ver:uiedVer(), off:uiedOff})); }catch(e){}
}
/* 焼き込み済みの部品は、起動のたびに端末の調整値を必ず捨てる(2026-08-26 K1報告の根治)。
   「印が変わった時だけ捨てる」方式は実機で効かなかった——報告のJSONで、焼き込みの上に
   端末の値(dy26・1.06倍・歯車+143,+41)が乗ったままなのが数字で確定した
   (下段ラベルの実測 高さ98=93×1.06 / 上端715 / 歯車の右端512=343+143+26)。
   条件付きだと、印を書いた後に編集し直された値が次の起動で復活して二度と消えない。
   ここに載っている部品は「コードが正本」。編集は開いている間だけ効き、読み込み直すと
   必ずコードの値へ戻る——二重掛けが構造的に起きなくなる。
   K1から新しい数値をもらったらコードへ焼く、という運用はそのまま */
const UIED_BAKED_IDS=['tabBar','setBtn','moneyHud','lfChip'];   // 所持金と中継の小窓も焼いた(2026-09-01)
try{
  let drop=false;
  for(const id of UIED_BAKED_IDS) if(uiedOff[id]){ delete uiedOff[id]; drop=true; }
  if(drop){
    console.log('[UI編集] 焼き込み済みの部品なので端末の調整値を捨てた:', UIED_BAKED_IDS.join(','));
    if(Object.keys(uiedOff).length) uiedSave();
    else localStorage.removeItem(UIED_KEY);
  }
  localStorage.setItem(UIED_BAKED_KEY, UIED_BAKED);
}catch(e){}
/* Shadow DOMの中も総当たりで探す(2026-08-07 K1指摘: 肝心のSPバーが掴めない)。
   本編では襲撃エンジンのホストが#mountではないので、全要素のshadowRootを潜る */
function uiedFind(id){
  const direct=document.getElementById(id);
  if(direct) return direct;
  const stack=[document];
  while(stack.length){
    const root=stack.pop();
    const hit=root.getElementById ? root.getElementById(id) : root.querySelector('#'+id);
    if(hit) return hit;
    for(const el of root.querySelectorAll('*')) if(el.shadowRoot) stack.push(el.shadowRoot);
  }
  return null;
}
const uiedCache={};   // 深い探索は重いので見つけた要素は覚えておく
function uiedEl(id){
  const c=uiedCache[id];
  if(c && c.isConnected) return c;
  const el=uiedFind(id);
  if(el) uiedCache[id]=el;
  return el;
}
function uiedApply(){
  for(const id in uiedOff){
    const el=uiedEl(id); if(!el) continue;
    const o=uiedOff[id];
    if(id==='armoryRow') continue;   // 中のアイテム欄は下でまとめて合成する
    el.style.translate=(o.dx||0)+'px '+(o.dy||0)+'px';
    el.style.scale=(o.s!=null && o.s!==1) ? String(o.s) : '';
  }
}
/* 画面の中央に十字のガイドを出し、近づいたら吸い付く(2026-08-07 K1指示。
   まっすぐ縦・横に動かしたい時に手が震えても中心を外さないため) */
const UIED_SNAP=10;   // この距離まで近づいたら吸着(px)
let uiedCross=null;
function uiedCrossShow(){
  if(uiedCross) return;
  uiedCross=document.createElement('div');
  uiedCross.id='hzgUiedCross';
  uiedCross.style.cssText='position:fixed; inset:0; z-index:999998; pointer-events:none;';
  uiedCross.innerHTML='<i class="v"></i><i class="h"></i>';
  const st=document.createElement('style');
  st.textContent='#hzgUiedCross i{position:absolute;background:rgba(120,200,255,.5)}'
    +'#hzgUiedCross .v{left:50%;top:0;bottom:0;width:1px;margin-left:-.5px}'
    +'#hzgUiedCross .h{top:50%;left:0;right:0;height:1px;margin-top:-.5px}'
    +'#hzgUiedCross i.hot{background:rgba(255,214,106,.95);box-shadow:0 0 6px rgba(255,214,106,.8)}';
  uiedCross.appendChild(st);
  document.body.appendChild(uiedCross);
}
function uiedCrossHide(){ if(uiedCross){ uiedCross.remove(); uiedCross=null; } }
/* 動かしている部品の中心が画面の中心線に近ければ、その軸を中心へ吸わせる */
function uiedSnap(el, dx, dy){
  if(!uiedCross) return {dx, dy};
  const r=el.getBoundingClientRect();
  const cx=r.left+r.width/2, cy=r.top+r.height/2;
  const tx=window.innerWidth/2, ty=window.innerHeight/2;
  let sx=false, sy=false;
  if(Math.abs(cx-tx)<UIED_SNAP){ dx+=tx-cx; sx=true; }
  if(Math.abs(cy-ty)<UIED_SNAP){ dy+=ty-cy; sy=true; }
  uiedCross.querySelector('.v').classList.toggle('hot', sx);
  uiedCross.querySelector('.h').classList.toggle('hot', sy);
  return {dx, dy};
}
/* 保存済みの差分は起動後もずっと当て続ける(襲撃の部品は後から生まれるため) */
if(Object.keys(uiedOff).length) setInterval(uiedApply, 1200);
function uiedMark(on){
  for(const id of UIED_IDS){
    const el=uiedEl(id); if(!el) continue;
    el.style.outline=on?'2px dashed rgba(120,200,255,.9)':'';
    el.style.outlineOffset=on?'2px':'';
    /* 指を通す(2026-09-01 K1報告「所持金が動かせない。青い枠には囲まれてるのに」)。
       #moneyHud のように pointer-events:none の飾りは、枠は描けても**指が当たらない**ので
       掴めなかった。編集中だけ受け取る側へ回し、終わったら元の指定へ戻す
       (空文字にすればCSSの指定が戻る)。他の部品にも同じ物が増えても勝手に効く */
    el.style.pointerEvents=on?'auto':'';
  }
}
function uiedStart(){
  uiedOn=true;
  uiedApply(); uiedMark(true); uiedCrossShow();
  uiedBar=document.createElement('div');
  uiedBar.id='hzgUiedBar';
  uiedBar.style.cssText='position:fixed; left:50%; top:calc(6px + env(safe-area-inset-top)); transform:translateX(-50%);'
    +'z-index:999999; display:flex; gap:6px; align-items:center; padding:6px 8px;'
    +'background:rgba(12,20,16,.95); border:1px solid #4d7a5f; border-radius:10px;'
    +'-webkit-user-select:none; user-select:none;';
  const tag=document.createElement('span');
  tag.textContent='🧲UI編集中';
  tag.style.cssText='font:bold 11px/1 -apple-system,sans-serif; color:#ffe184; padding:0 2px;';
  uiedBar.appendChild(tag);
  const mkB=(label,fn)=>{
    const b=document.createElement('button');
    b.textContent=label;
    b.style.cssText='padding:8px 10px; border:0; border-radius:8px; background:#28533f; color:#f7f0dd; font:bold 11px/1 -apple-system,sans-serif;';
    b.addEventListener('click', ev=>{ ev.stopPropagation(); fn(); });
    uiedBar.appendChild(b);
  };
  // 選んだ部品の大きさ(2026-08-07 K1指示)。最後に触った部品に効く
  const sz=document.createElement('input');
  sz.type='range'; sz.min='50'; sz.max='200'; sz.step='2'; sz.value='100';
  sz.style.cssText='width:88px;';
  const szl=document.createElement('span');
  szl.textContent='100%';
  szl.style.cssText='font:bold 10px/1 -apple-system,sans-serif; color:#ffd76a; min-width:32px; text-align:right;';
  sz.addEventListener('input', ()=>{
    const id=uiedLast; szl.textContent=sz.value+'%';
    if(!id) return;
    const o=uiedOff[id]||(uiedOff[id]=uiedInit(uiedEl(id)));
    o.s=+sz.value/100;
    const el=uiedEl(id);
    if(id==='armoryRow' || id==='armory') uiedRowSync();
    if(id!=='armoryRow' && el) el.style.scale=String(o.s);
    uiedSave();
  });
  uiedSzInput=sz; uiedSzLabel=szl;
  uiedBar.appendChild(sz); uiedBar.appendChild(szl);
  mkB('差分コピー', ()=>{
    const out={};
    for(const id in uiedOff){
      const o=uiedOff[id]; if(!o) continue;
      /* 実際に要素へ当たっている値を出す。入れ子(armoryRowはarmoryの中)の割り算も
         済んだ後の値なので、**そのままCSSへ書ける**。こちらで計算し直さない */
      const el0=uiedEl(id);
      if(el0){
        const t0=el0.style.translate, s0=el0.style.scale;
        const e0={};
        if(t0){ const p=t0.split(/\s+/); const a0=parseFloat(p[0])||0, b0=parseFloat(p[1]||'0')||0;
                if(a0||b0){ e0.translate=Math.round(a0)+'px '+Math.round(b0)+'px'; } }
        if(s0 && parseFloat(s0)!==1) e0.scale=Math.round(parseFloat(s0)*1000)/1000;
        /* K1指示(2026-09-02)「10px上に、ではなく絶対値でないと怖い」。
           画面上の最終的な位置と大きさもそのまま出す。
           焼き込んだ後、こちらで同じ数字になるか測って確かめられる */
        try{
          const r0=el0.getBoundingClientRect();
          e0.実際の位置=Math.round(r0.left)+','+Math.round(r0.top);
          e0.実際の大きさ=Math.round(r0.width)+'x'+Math.round(r0.height);
        }catch(e2){}
        if(Object.keys(e0).length) out[id]=e0;
      }
      const e={};
      if(o.dx) e.dx=Math.round(o.dx);
      if(o.dy) e.dy=Math.round(o.dy);
      if(o.s!=null && o.s!==1) e.scale=Math.round(o.s*100)/100;
      if(Object.keys(e).length) out[id]=e;
    }
    const txt=JSON.stringify(out);
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(txt).then(()=>toast('差分をコピーした '+txt.slice(0,60))).catch(()=>manualCopy(txt));
    } else manualCopy(txt);
  });
  mkB('リセット', ()=>{
    for(const id in uiedOff){ const el=uiedEl(id); if(el){ el.style.translate=''; el.style.scale=''; } }
    uiedLast=null; if(uiedSzInput){ uiedSzInput.value='100'; uiedSzLabel.textContent='100%'; }
    uiedOff={}; localStorage.removeItem(UIED_KEY);
    toast('UI差分をリセットした');
  });
  mkB('終了', uiedStop);
  uiedBar.title='部品をドラッグで移動 / つまみで大きさ / 中央の十字に吸着';
  uiedBar.addEventListener('pointerdown', ev=>ev.stopPropagation());
  document.body.appendChild(uiedBar);
  toast('点線の部品をドラッグで動かせる');
}
function uiedStop(){
  uiedOn=false; uiedMark(false); uiedCrossHide();
  if(uiedBar){ uiedBar.remove(); uiedBar=null; }
}
/* 編集中はdocumentのキャプチャで先取りして、ゲーム側のタップ処理へ渡さない */
document.addEventListener('pointerdown', ev=>{
  if(!uiedOn) return;
  const t=(ev.composedPath && ev.composedPath()[0]) || ev.target;
  if(uiedBar && (t===uiedBar || uiedBar.contains(t))) return;
  for(const id of UIED_IDS){
    const el=uiedEl(id);
    if(el && (t===el || el.contains(t))){
      ev.stopPropagation(); ev.preventDefault();
      const o=uiedOff[id]||(uiedOff[id]=uiedInit(el));   // 今そう見えている値から始める(2026-09-02)
      uiedLast=id;
      if(uiedSzInput){ const v=Math.round((o.s!=null?o.s:1)*100); uiedSzInput.value=String(v); uiedSzLabel.textContent=v+'%'; }
      uiedDrag={id, el, x0:ev.clientX, y0:ev.clientY, dx0:o.dx||0, dy0:o.dy||0};
      return;
    }
  }
}, true);
document.addEventListener('pointermove', ev=>{
  if(!uiedOn || !uiedDrag) return;
  ev.stopPropagation(); ev.preventDefault();
  const o=uiedOff[uiedDrag.id];
  o.dx=uiedDrag.dx0+(ev.clientX-uiedDrag.x0);
  o.dy=uiedDrag.dy0+(ev.clientY-uiedDrag.y0);
  if(uiedDrag.id==='armoryRow') uiedRowSync();
  else uiedDrag.el.style.translate=o.dx+'px '+o.dy+'px';
  const sn=uiedSnap(uiedDrag.el, o.dx, o.dy);   // 画面中央の十字に吸わせる
  if(sn.dx!==o.dx || sn.dy!==o.dy){
    o.dx=sn.dx; o.dy=sn.dy;
    if(uiedDrag.id==='armoryRow') uiedRowSync();
    else uiedDrag.el.style.translate=o.dx+'px '+o.dy+'px';
  }
}, true);
document.addEventListener('pointerup', ev=>{
  if(!uiedOn || !uiedDrag) return;
  ev.stopPropagation();
  uiedDrag=null;
  uiedSave();
}, true);

/* ---------- ツールメニュー(ジェスチャで開く) ---------- */
let menu=null;
function closeMenu(){ if(menu){ menu.remove(); menu=null; } }
function openMenu(x,y){
  closeMenu();
  menu=document.createElement('div');
  menu.id='hzgDevMenu';
  const mx=Math.min(Math.max(8,x-70), innerWidth-160);
  const my=Math.min(Math.max(8,y-40), innerHeight-190);
  menu.style.cssText='position:fixed; left:'+mx+'px; top:'+my+'px; z-index:999999; width:152px;'
    +'background:rgba(12,20,16,.95); border:1px solid #4d7a5f; border-radius:11px; padding:7px;'
    +'display:flex; flex-direction:column; gap:6px; -webkit-user-select:none; user-select:none;';
  const mk=(label,fn)=>{
    const b=document.createElement('button');
    b.textContent=label;
    b.style.cssText='padding:11px 8px; border:0; border-radius:8px; background:#28533f; color:#f7f0dd;'
      +'font:bold 12px/1 -apple-system,sans-serif; text-align:left;';
    b.addEventListener('click', ev=>{ ev.stopPropagation(); closeMenu(); fn(); });
    menu.appendChild(b);
  };
  mk('📏 px物差し'+(ruler?'(片づける)':''), ()=>toggleRuler(x,y));
  mk('🎯 タップ座標 '+(inspectOn?'ON→OFF':'OFF→ON'), ()=>{ inspectOn=!inspectOn; toast('タップ座標: '+(inspectOn?'ON(タップで教室内座標)':'OFF')); });
  mk('📋 状況スナップショット', takeSnapshot);
  mk('🧲 UIレイアウト編集'+(uiedOn?'(終了)':''), ()=>{ uiedOn ? uiedStop() : uiedStart(); });
  mk('📝 メモ帳', openMemo);
  menu.addEventListener('pointerdown', ev=>ev.stopPropagation());
  document.body.appendChild(menu);
  setTimeout(()=>{
    const away=ev=>{ if(menu && !(ev.target.closest && ev.target.closest('#hzgDevMenu'))){ closeMenu(); document.removeEventListener('pointerdown', away, true); } };
    document.addEventListener('pointerdown', away, true);
  },0);
}

/* 呼び出しジェスチャ: 500ms以内・45px以内の3連打+3打目を550ms長押し */
let taps=[], lp=null;
document.addEventListener('pointerdown', ev=>{
  if(!ev.isPrimary) return;
  if(ev.target && ev.target.closest && ev.target.closest('#hzgDevMenu,#hzgRuler')) return;
  const now=performance.now();
  taps=taps.filter(t=>now-t.t<500 && Math.hypot(t.x-ev.clientX,t.y-ev.clientY)<45);
  taps.push({x:ev.clientX, y:ev.clientY, t:now});
  if(lp){ clearTimeout(lp.t); lp=null; }
  if(taps.length>=3){
    const x=ev.clientX, y=ev.clientY;
    lp={x, y, t:setTimeout(()=>{ lp=null; taps=[]; openMenu(x,y); }, 550)};
  }
}, true);
const lpCancel=ev=>{ if(lp && (ev.type!=='pointermove' || Math.hypot(ev.clientX-lp.x,ev.clientY-lp.y)>12)){ clearTimeout(lp.t); lp=null; } };
document.addEventListener('pointerup', lpCancel, true);
document.addEventListener('pointermove', lpCancel, true);
document.addEventListener('pointercancel', lpCancel, true);

/* ---------- ④ メモリ計(2026-08-15 K1指示: 既定ON・設定でOFF) ----------
   iOSのSafariは本物のメモリ量を読むAPIが無いので、リーク検知に効く代理指標を出す:
   ・音声=展開済み音声バッファの推定MB(戦闘SFX共有キャッシュ+本編のSE/BGM)
   ・DOM=画面部品の総数(戦闘のShadow DOM内も含む。捨て忘れがあると膨らむ)
   ・監視=windowに張られた戦闘系リスナー数(pointermove/up/cancel/resize。
     バグ#30の犯人と同種のリーク。マウント中+1〜4、教室に戻ったら0が正常)
   ・heap=JSヒープ実測(Chrome系のみ。iPhoneでは出ない)
   表示はタップを一切吸わない小さな浮き札。リリース時はHZG_DEV_TOOLS=falseで丸ごと消える */
const LCNT={};   // 追跡するリスナーの純増カウント({once:true}系は追跡対象外の型なので狂わない)
{
  const TRACK=new Set(['pointermove','pointerup','pointercancel','resize']);
  const add=window.addEventListener.bind(window), rem=window.removeEventListener.bind(window);
  window.addEventListener=function(type,fn,opt){ if(TRACK.has(type)) LCNT[type]=(LCNT[type]||0)+1; return add(type,fn,opt); };
  window.removeEventListener=function(type,fn,opt){ if(TRACK.has(type)) LCNT[type]=(LCNT[type]||0)-1; return rem(type,fn,opt); };
}
function memMeterOn(){
  try{ if(typeof SETTINGS!=='undefined' && SETTINGS && 'memMeter' in SETTINGS) return SETTINGS.memMeter!==false; }catch(e){}
  try{ const s=JSON.parse(localStorage.getItem('hzg_settings_v1')||'{}'); return s.memMeter!==false; }catch(e){}
  return true;
}
function audioBufMB(){
  let bytes=0;
  const sum=o=>{ if(!o) return; for(const k in o){ const b=o[k]; if(b && b.length && b.numberOfChannels) bytes+=b.length*b.numberOfChannels*4; } };
  try{ sum(window.__HZG_SFX_BUF); }catch(e){}
  try{ if(typeof seBuf!=='undefined') sum(seBuf); }catch(e){}
  try{ if(typeof bgmBuf!=='undefined') sum(bgmBuf); }catch(e){}
  return bytes/1048576;
}
function domCount(){
  let n=document.getElementsByTagName('*').length;
  try{
    const m=document.getElementById('raidMount');
    if(m) m.querySelectorAll('*').forEach(el=>{ if(el.shadowRoot) n+=el.shadowRoot.querySelectorAll('*').length; });
  }catch(e){}
  return n;
}
/* フレーム計(2026-08-18 K1指示)。実機でカクつきを数字で見るための表示。
   毎コマの間隔を1秒ぶん貯めて「平均コマ数/秒」と「その1秒でいちばん詰まったコマ」を出す。
   30fpsなら33ms、60fpsなら17msが基準で、大きく超えた回数がそのままカクつきの回数 */
const FR={last:0, gaps:[]};
(function frameLoop(){
  const t=performance.now();
  if(FR.last){ const g=t-FR.last; if(g<2000) FR.gaps.push(g); }
  FR.last=t;
  requestAnimationFrame(frameLoop);
})();
function frameStat(){
  const g=FR.gaps; FR.gaps=[];
  if(!g.length) return '';
  const fps=Math.round(1000/(g.reduce((a,b)=>a+b,0)/g.length));
  const worst=Math.round(Math.max(...g));
  const slow=g.filter(x=>x>50).length;   // 50ms超=はっきり分かるカクつき
  return fps+'fps 最悪'+worst+'ms'+(slow?' 詰まり'+slow:'');
}
let memEl=null;
/* GPUの板(まとめ絵)。テクスチャは使っている量に関わらず 幅×上限×4バイト を確保しているので、
   そこが常時の取り分。使用率と焼いた枚数は「これから溢れるか」の目安として並べる */
function glLine(){
  try{
    const st=(window.HZG_GLSHARE && HZG_GLSHARE.stats) ? HZG_GLSHARE.stats() : null;
    if(!st) return 'GPU板 まだ無し';
    const mb=((st.幅||2048)*(st.上限||0)*4/1048576).toFixed(0);
    return 'GPU板'+mb+'MB '+(st.使用率||'?')+' '+(st.枚数||0)+'枚'+(st.あふれ?' あふれ!':'');
  }catch(e){ return 'GPU板 ?'; }
}
/* 読み込んだ絵の量。展開後の実バイトはブラウザが教えてくれないので、
   落としてきた枚数と伸長後の大きさを出す——増え続けているかを見るための目安 */
function imgLine(){
  try{
    const rs=performance.getEntriesByType('resource')
      .filter(r=>/\.(webp|png|jpe?g|svg)(\?|$)/i.test(r.name));
    const by=rs.reduce((a,r)=>a+(r.decodedBodySize||r.transferSize||0),0);
    return '絵'+rs.length+'枚 '+(by/1048576).toFixed(1)+'MB';
  }catch(e){ return '絵 ?'; }
}
function memTick(){
  if(!memMeterOn()){ if(memEl){ memEl.remove(); memEl=null; } return; }
  if(!memEl){
    memEl=document.createElement('div');
    memEl.id='hzgMemMeter';
    memEl.style.cssText='position:fixed; right:6px; top:94px; z-index:999998; pointer-events:none;'
      +'font:9px/1.5 -apple-system,Menlo,monospace; color:#d9ffe8; text-align:right;'
      +'background:rgba(10,20,14,.62); border:1px solid rgba(120,180,140,.4); border-radius:7px; padding:3px 6px;'
      +'text-shadow:0 1px 1px #000;';
    document.body.appendChild(memEl);
  }
  const ls=Object.entries(LCNT).filter(([,v])=>v>0).map(([k,v])=>v).reduce((a,b)=>a+b,0);
  /* ヒープの実測はChromeにしか無い(Safariはperformance.memoryを持たない=iPhoneでは空)。
     なので合計は名乗らず、食っている物の内訳を並べる(2026-08-25 K1採用=A案) */
  let heap='';
  try{ if(performance.memory) heap=' / heap'+(performance.memory.usedJSHeapSize/1048576).toFixed(0)+'MB'; }catch(e){}
  const fr=frameStat();
  memEl.textContent=(fr?fr+'\n':'')+glLine()+'\n'+imgLine()
    +'\n音'+audioBufMB().toFixed(1)+'MB / DOM'+domCount()+' / 監視'+ls+heap;
  memEl.style.whiteSpace='pre';
  memEl.style.borderColor=(ls>8||domCount()>9000) ? 'rgba(255,110,90,.8)' : 'rgba(120,180,140,.4)';   // 異常域は枠を赤く
}
setInterval(memTick, 1000);

/* ---------- ⑤ バグ報告箱(2026-08-18 K1正式依頼) ----------
   常設の「報告」ボタン→押した瞬間の状態を先に確定→専用の入れ物(IndexedDB)へ貯める。
   何件でも貯めて、後からメモを足し、まとめて出せる。ゲームのセーブとは別なので混ざらない。
   出したJSONは bugview.html で読み込んで同じ場面を作り直せる。 */
const BB_DB='hzg_bugbox_v1', BB_STORE='reports';
let bbBtn=null, bbWin=null, bbCount=0;
/* バグ報告を書いている間は襲撃の時間を止める(2026-08-31 K1指示
   「バグ報告押したらその間は時間停止するようにしろよそもそも」)。
   作戦タイムとは別の口を使う——作戦タイムだと彩度が落ちてHPバーと狙い線が出るので、
   報告のスクショに写る画そのものが変わってしまう。こちらは見た目を変えずに時間だけ止める。
   襲撃をやっていない時は何も起きない(教室に止める時間が無い) */
function bbTimeHold(on){
  try{ if(typeof battleHandle!=='undefined' && battleHandle && battleHandle.reportHold) battleHandle.reportHold(!!on); }catch(e){}
}
/* 報告の窓が2つとも閉じたら時間を戻す。どちらか一方が開いている間は止めたまま */
function bbHoldSync(){ bbTimeHold(!!(bbWin || bbQuickWin)); }

/* --- 直近の出来事(軽い輪) --- */
const BB_EV=[], BB_EV_MAX=120;
window.HZG_EV=(tag, data)=>{
  try{
    BB_EV.push({t:Math.round(performance.now()), tag:String(tag).slice(0,40),
      d:(data==null?null:String(typeof data==='object'?JSON.stringify(data):data).slice(0,120))});
    if(BB_EV.length>BB_EV_MAX) BB_EV.shift();
  }catch(e){}
};
/* 本編のログ関数が生えたら、そこも出来事として拾う(二重に包まない) */
{
  const hook=()=>{
    for(const n of ['logF','logS','logA','logHead']){
      const f=window[n];
      if(typeof f==='function' && !f.__bb){
        const w=function(){ try{ window.HZG_EV(n, arguments[0]); }catch(e){} return f.apply(this, arguments); };
        w.__bb=true; window[n]=w;
      }
    }
  };
  hook();
  const iv=setInterval(hook, 2000);
  setTimeout(function(){ clearInterval(iv); }, 60000);
}
/* --- 落ちた時の記録 --- */
const BB_ERR=[];
function bbErr(o){ BB_ERR.push(o); if(BB_ERR.length>20) BB_ERR.shift(); }
window.addEventListener('error', function(e){
  bbErr({t:new Date().toISOString(), kind:'error', msg:String(e.message||'').slice(0,200),
    at:String(e.filename||'').split('/').pop()+':'+e.lineno,
    stack:String((e.error&&e.error.stack)||'').slice(0,700)});
});
window.addEventListener('unhandledrejection', function(e){
  const r=e.reason;
  bbErr({t:new Date().toISOString(), kind:'promise', msg:String((r&&r.message)||r||'').slice(0,200),
    stack:String((r&&r.stack)||'').slice(0,700)});
});
/* --- 絵や音の読み込み失敗 --- */
const BB_IMGERR=[];
window.addEventListener('error', function(e){
  const t=e.target;
  if(t && (t.tagName==='IMG'||t.tagName==='AUDIO') && t.src){
    const s=String(t.src).split('/').pop();
    if(BB_IMGERR.indexOf(s)<0 && BB_IMGERR.length<20) BB_IMGERR.push(s);
  }
}, true);

/* --- コマ落ちの記録(直近10秒) --- */
const BB_FPS=[];
/* JSの忙しさも録る(2026-08-19 K1の切り分け用):
   「コマが遅い」だけでは、JSが忙しいのか・描画が重いのか・端末側の事情かが分からない。
   長いタスク(50ms超)の合計時間を録り、直近10秒の「主スレッドが塞がっていた割合」を出す */
let BB_BUSY=[];
try{
  new PerformanceObserver(function(list){
    for(const e of list.getEntries()) BB_BUSY.push([performance.now(), e.duration]);
  }).observe({entryTypes:['longtask']});
}catch(e){}
/* 1コマの実際の仕事時間(rAFの中身のms)。「長いタスク0」は50ms超が無いだけで、
   毎コマ15ms級の仕事は写らない——60fpsに必要なのは毎コマ16ms以内なのでこちらが本命(2026-08-19) */
let BB_WORK=[];
(function(){
  const orig=window.requestAnimationFrame.bind(window);
  window.requestAnimationFrame=function(cb){
    return orig(function(t){
      const a2=performance.now();
      cb(t);
      const ms=performance.now()-a2;
      if(ms>0.2) BB_WORK.push([a2, ms]);
      while(BB_WORK.length && a2-BB_WORK[0][0]>10000) BB_WORK.shift();
    });
  };
})();
(function bbFrame(){
  const t=performance.now();
  if(bbFrame._l){ const g=t-bbFrame._l; if(g<2000) BB_FPS.push([t,g]); }
  bbFrame._l=t;
  while(BB_FPS.length && t-BB_FPS[0][0]>10000) BB_FPS.shift();
  while(BB_BUSY.length && t-BB_BUSY[0][0]>10000) BB_BUSY.shift();
  requestAnimationFrame(bbFrame);
})();
function bbFps(){
  if(!BB_FPS.length) return null;
  const g=BB_FPS.map(function(x){ return x[1]; }).sort(function(a,b){ return a-b; });
  const q=function(p){ return Math.round(g[Math.min(g.length-1, Math.floor(g.length*p))]*10)/10; };
  const avg=g.reduce(function(a,b){ return a+b; },0)/g.length;
  const busy=BB_BUSY.reduce(function(a,x){ return a+x[1]; },0);
  const span=BB_FPS.length?(BB_FPS[BB_FPS.length-1][0]-BB_FPS[0][0]):1;
  const wk=BB_WORK.map(function(x){ return x[1]; }).sort(function(a,b){ return a-b; });
  const wq=function(p2){ return wk.length?Math.round(wk[Math.min(wk.length-1,Math.floor(wk.length*p2))]*10)/10:0; };
  const wsum=wk.reduce(function(a,b){ return a+b; },0);
  return { コマ数:g.length, fps:Math.round(1000/avg), 中央:q(.5), 遅い方1割:q(.9), 最悪:q(.999),
    詰まり:g.filter(function(x){ return x>50; }).length,
    JSが塞いだ割合:Math.round(busy/Math.max(1,span)*100)+'%', 長いタスク:BB_BUSY.length,
    コマ仕事:{中央:wq(.5), 遅い方1割:wq(.9), 最悪:wq(.999), 割合:Math.round(wsum/Math.max(1,span)*100)+'%'} };
}

/* --- 入れ物(IndexedDB。セーブとは別) --- */
function bbOpen(){
  return new Promise(function(res,rej){
    const q=indexedDB.open(BB_DB, 1);
    q.onupgradeneeded=function(){ const db=q.result;
      if(!db.objectStoreNames.contains(BB_STORE)) db.createObjectStore(BB_STORE, {keyPath:'id'}); };
    q.onsuccess=function(){ res(q.result); };
    q.onerror=function(){ rej(q.error); };
  });
}
function bbAll(){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const st=db.transaction(BB_STORE,'readonly').objectStore(BB_STORE);
    const r=st.getAll(); r.onsuccess=function(){ res(r.result||[]); }; r.onerror=function(){ rej(r.error); };
  }); });
}
function bbPut(rep){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const tx=db.transaction(BB_STORE,'readwrite');
    tx.objectStore(BB_STORE).put(rep);
    tx.oncomplete=function(){ res(true); }; tx.onerror=function(){ rej(tx.error); };
  }); });
}
function bbDel(id){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const tx=db.transaction(BB_STORE,'readwrite');
    tx.objectStore(BB_STORE).delete(id);
    tx.oncomplete=function(){ res(true); }; tx.onerror=function(){ rej(tx.error); };
  }); });
}

/* --- 押した瞬間の状態を確定する(UIを開く前に必ずこれを呼ぶ) --- */
function bbCapture(){
  /* 画面の実寸(2026-08-26 K1報告「下段ラベルが埋まる」の計測)。
     手元のヘッドレスでは再現しないので、実機の生の数字をここで拾う。
     レイアウト用の高さ・実際に見えている高さ・拡大率・横のはみ出し・板の位置 */
  const gaMen=(()=>{ try{
    const vv=window.visualViewport, el=document.getElementById('tabBar');
    const q=el?el.getBoundingClientRect():null;
    const probe=document.createElement('div');
    probe.style.cssText='position:fixed;left:0;top:0;width:1px;height:100dvh;visibility:hidden';
    document.body.appendChild(probe);
    const dvh=Math.round(probe.getBoundingClientRect().height);
    probe.style.height='100vh'; const vh=Math.round(probe.getBoundingClientRect().height);
    probe.remove();
    let widest=null, wmax=0;
    document.querySelectorAll('*').forEach(e=>{ const r=e.getBoundingClientRect();
      if(r.width&&r.right>wmax){ wmax=r.right; widest=e.tagName+(e.id?'#'+e.id:''); } });
    return { 見えている高さ:vv?Math.round(vv.height):null, 見えている幅:vv?Math.round(vv.width):null,
      拡大率:vv?+vv.scale.toFixed(3):null, ずれ上:vv?Math.round(vv.offsetTop):null,
      innerHeight:window.innerHeight, innerWidth:window.innerWidth,
      dvh:dvh, vh:vh, 文書の幅:document.documentElement.scrollWidth,
      vvh変数:getComputedStyle(document.documentElement).getPropertyValue('--vvh').trim()||'(なし)',
      下段ラベル:q?[Math.round(q.top),Math.round(q.bottom),Math.round(q.height)]:null,
      一番右に出ている物:widest+' '+Math.round(wmax) };
  }catch(e){ return {読めず:String(e)}; } })();
  const rep={ 画面:gaMen, id:'R'+Date.now().toString(36)+Math.random().toString(36).slice(2,5),
    at:new Date().toISOString(), memo:'',
    build:window.HZG_BUILD||'?',
    url:location.pathname+location.search,
    ua:String(navigator.userAgent).slice(0,180),
    view:{w:innerWidth, h:innerHeight, dpr:window.devicePixelRatio||1},
    /* SETTINGSはトップレベルconst(windowに載らない)——window.SETTINGSは常にundefinedで
       GPU設定中でも'dom'と記録されていた(2026-08-20 Codex指摘)。裸の識別子で読む */
    draw:(typeof SETTINGS!=='undefined' && SETTINGS.gl) ? 'webgl' : 'dom',
    fps:bbFps(), errors:BB_ERR.slice(-8), imgErrors:BB_IMGERR.slice(0,10),
    /* メモリの内訳(2026-08-26 K1報告「画面が一切描き直されない・音とタップ音だけ生きている・
       アプリを立ち上げ直すと直る」の原因特定用)。これまで画面のメーターにしか出ておらず、
       報告のJSONには入っていなかった。iOSはメモリ逼迫でタブの合成だけ止めることがあるので、
       止まった時の実数を残す */
    mem:(()=>{ try{
      const gl=(window.HZG_GLSHARE&&HZG_GLSHARE.stats)?HZG_GLSHARE.stats():null;
      return {
        音MB:+audioBufMB().toFixed(1),
        GPU板MB: gl? +(((gl.幅||2048)*(gl.上限||0)*4/1048576).toFixed(0)) : null,
        GPU板使用率: gl? (gl.使用率||null) : null,
        焼いた枚数: gl? (gl.枚数||null) : null,
        画像枚数: (()=>{ try{ return performance.getEntriesByType('resource').filter(r=>r.initiatorType==='img').length; }catch(e){ return null; } })(),
        JSヒープMB: (performance.memory&&performance.memory.usedJSHeapSize)? +(performance.memory.usedJSHeapSize/1048576).toFixed(1) : null,
        画面の状態: document.visibilityState,
      };
    }catch(e){ return {読めず:String(e)}; } })(),
    /* 教室の描き手の健康状態(2026-08-26 K1報告「教室の絵だけ止まった・音楽は正常」の原因特定用)。
       これまで報告に載っていたのは襲撃側のglAbnだけで、教室が描けているかは記録が無かった。
       板を持っているか/最後に描けてから何秒経ったか/画面に板が載っているか、で止まった枝が分かる */
    clsDraw:(()=>{ try{
      const C=window.ClassGL||null;
      const sc=document.getElementById('scene');
      const cv=sc?sc.querySelector('canvas'):null;
      return {
        板を持っている: C?!!C.owning:null,
        最後に描けてから: (C&&C.lastDraw)? +((performance.now()-C.lastDraw)/1000).toFixed(1) : null,
        画面に板がある: !!cv,
        板の大きさ: cv? cv.width+'x'+cv.height : null,
        GPU設定: (typeof SETTINGS!=='undefined') ? {全体:!!SETTINGS.gl, 教室:!!SETTINGS.glClass} : null,
        教室の印: document.body.classList.contains('glcOn'),
        止まった関所: (C&&C.why)||null,   // 2026-09-01 K1指示: 描いていない時、どの関所で戻っているか
      };
    }catch(e){ return {読めず:String(e)}; } })(),
    /* ロード画面の失敗記録(2026-08-29 Codex納品キットの依頼)。
       準備が間に合わなかった/例外で落ちた時だけ中身が入る。空なら順調 */
    loadLog:(()=>{ try{ return (window.__HZG_LOADLOG||[]).slice(-12); }catch(e){ return null; } })(),
    /* 音の記録(2026-08-29 K1依頼)。タイトルの曲の頭が壊れる件の断定用。
       時刻・曲の位置・読み込み具合(0〜4)・音量を、下ごしらえから鳴らし終わりまで並べてある。
       読み方: 「鳴らし始めて+○ms」の行で位置が滑らかに進んでいなければ、そこが壊れた瞬間。
       曲の取得の「控えから:true」はキャッシュから来たという意味(タスクキル直後の起動で出る) */
    bgmLog:(()=>{ try{ return (window.__HZG_BGMLOG||[]).slice(-90); }catch(e){ return null; } })(),
    /* プロローグADVの文字送りの記録(2026-09-02 K1指示)。実機で「2文字目から1秒詰まる」件の断定用。
       行ごとに、何文字目が何msに「置かれ」何msに「出た」かが並ぶ。
       読み方:
       ・置いた が26msずつ進んでいない → 本体(JS)がそこで止まっている
       ・置いた は26msずつなのに 出た が飛んでいる → 絵が追いつけていない(描画側)
       ・前のコマから が大きい所 = 画面が止まっていた長さ
       幕が「開きながら」の行は、プロローグの1行目(700msかけて幕を開けている最中) */
    advLog:(()=>{ try{ return (window.__HZG_ADVLOG||[]).slice(-6); }catch(e){ return null; } })(),
    /* 指の操作が何msかかったか(2026-09-02)。10ms以上かかった物だけ載せる。
       文字送りが止まった時刻と重なっていれば、その出来事の処理が犯人 */
    evLog:(()=>{ try{ return (window.__HZG_EVLOG||[]).filter(e=>e.かかった>=10).slice(-24); }catch(e){ return null; } })(),
    /* 浮き文字・吹き出しの位置(2026-09-02 K1報告「一瞬上に行ってまた元の位置に戻る」)。
       直近3秒ぶんを、コマごと・物ごとに並べてある。
       読み方: 同じidの「上」を縦に追う。1コマだけ大きく減って次で戻っていれば、そいつが跳んだ物。
       跳び幅が46px前後なら、盤面の縦のずらし幅と同じ=座標の取り違え */
    /* 押下音の計器(2026-09-03 K1許可)。
       「呼ばれた」に対して「鳴った」が足りない時、その差がどの理由で消えたかが分かる。
       状態別に interrupted や suspended が積まれていれば、音の口が寝ていたのが原因 */
    kmLog:(()=>{ try{ return window.__HZG_KMLOG||null; }catch(e){ return null; } })(),
    /* 曲の出入り(2026-09-03 K1指示)。直近80件。
       「かけようとした」に対して「鳴らし始めた」が無ければ、その次の行に理由が出ている */
    bgmEv:(()=>{ try{ return (window.__HZG_BGMEV||[]).slice(-40); }catch(e){ return []; } })(),
    /* 曲の位置の物差し(2026-09-04 K1指示「計器をたせ」)。0.5秒ごと・直近60秒ぶん。
       読み方: 「位置」を縦に追う。0付近を行ったり来たり=頭が繰り返している /
       同じ数字が続く=止まっている / 1回だけ大きく戻る=ふつうのループの継ぎ目 */
    bgmPos:(()=>{ try{ return (window.__HZG_BGMPOS||[]).slice(-120); }catch(e){ return []; } })(),
    /* 曲の要素自身の出来事。ゲームが頼んでいない再生や頭出しが混ざっていればここに出る */
    bgmElEv:(()=>{ if(!HZG_METERS.on('曲')) return []; try{ return (window.__HZG_BGMEL_EV||[]).slice(-60); }catch(e){ return []; } })(),
    /* 画面が隠れた・戻った時刻(2026-09-04)。今までは合計秒数しか無かった */
    visLog:(()=>{ if(!HZG_METERS.on('画面')) return []; try{ return (window.__HZG_VISLOG||[]).slice(-40); }catch(e){ return []; } })(),
    /* 効果音の計器(2026-09-04 K1指示「どの効果音がどのタイミングで鳴ったか」)。
       直近160件。「幕が画面に」がtrueの行は、K1 STUDIOのロゴが出ている間に鳴った音 */
    seLog:(()=>{ if(!HZG_METERS.on('効果音')) return []; try{ return (window.__HZG_SELOG||[]).slice(-160); }catch(e){ return []; } })(),
    /* 音の口(AudioContext)の顛末(2026-09-05 K1指示)。
       いつ作り、いつresumeを頼み、それが成功したか失敗したか、0.5秒後にどうなっていたか */
    acLog:(()=>{ if(!HZG_METERS.on('音の口')) return []; try{ return (window.__HZG_ACLOG||[]).slice(-120); }catch(e){ return []; } })(),
    /* 物理の重さ(2026-09-06 K1指示「計器足しとけよ」)。発熱の疑いの切り分け用。
       1コマぶんのミリ秒(直近240コマ)・平均・最悪・回したコマ数・
       そのうち何も動いていなかったコマ数・床の物の数・机や壁の数。
       「コマ仕事」(perf.raid)と並べれば、物理が占める割合が出る */
    /* ★スポット計器(2026-09-06 K1指示)。大群予告の曲が鳴らない件**専用**。直ったら消す */
    spotBgm:(()=>{ if(!HZG_METERS.on('スポット')) return []; try{ return (window.__HZG_SPOTBGM||[]).slice(-40); }catch(e){ return []; } })(),
    /* ★スポット計器(2026-09-06 K1指示「スポット計器つけとけ」)。
       床のバスケットボールがつかめない件**専用**。直ったら消す。
       入切の札に関わらず必ず載せる——この件は「記録が残らないこと」が問題なので、
       K1が計器をオフにしていても取りこぼさない */
    spotBall:(()=>{ try{ return (window.__HZG_SPOTBALL||[]).slice(-40); }catch(e){ return []; } })(),
    /* ★【スポット計器 20260912 掴まれたまま】K1報告 gctjwu「佐藤がゾンビに掴まれたまま離されなくなった」。直ったら消す。
       読み方: 「放した」の行=掴んでいない敵に掴まれたままだった(前ならそのまま固まっていた)瞬間。敵の状態と狙いで、どの道で外れたかが分かる */
    spotGrab:(()=>{ try{ return (window.__HZG_SPOTGRAB||[]).slice(-40); }catch(e){ return []; } })(),
    physPerf:(()=>{ if(!HZG_METERS.on('物理')) return null; try{
      const P=window.__HZG_PHYSPERF; if(!P) return null;
      const a=P.ms||[]; const n=a.length||1;
      const sum=a.reduce((x,y)=>x+y,0);
      return {平均ms:+(sum/n).toFixed(2), 最悪ms:+Math.max.apply(null,a.concat([0])).toFixed(2),
        回したコマ数:P.n, 何も動いていないコマ数:P.idle,
        無駄の割合:P.n?(+(P.idle/P.n*100).toFixed(1)+'%'):'-',
        台数:P.台数, 動いている:P.動いている, 机:P.机, 壁:P.壁, バリケード:P.バリケード,
        進めた回数:P.進めた回数, 直近のms:a.slice(-40)};
    }catch(e){ return null; } })(),
    /* 床の物(武器・ボール)の動き(2026-09-06 K1指示「バグ報告に計器足すなりしろ」)。
       0.1秒ごとに、教室か襲撃か・種類・位置・高さ・平面の速さ・回っている速さ・
       材質(空気抵抗/摩擦/跳ね返り)・世界を進めた刻み。K1報告「襲撃で床を滑ってる」の切り分け用。
       両方の場面が同じ一覧に入るので、そのまま並べて見比べられる */
    dropLog:(()=>{ if(!HZG_METERS.on('床の物')) return []; try{ return (window.__HZG_DROPLOG||[]).slice(-260); }catch(e){ return []; } })(),
    /* 起動の一部始終(2026-09-05 K1指示)。触った回数・幕が開いた時刻・判断材料が揃った時刻 */
    bootLog:(()=>{ if(!HZG_METERS.on('起動')) return []; try{ return (window.__HZG_BOOTLOG||[]).slice(-150); }catch(e){ return []; } })(),
    /* 絵が異常に大きくなった記録(2026-09-05 K1報告)。空なら一度も起きていない。
       「誰」がキャラの番号なら席の札の変形が原因、「写した枠」なら測り方(K)が原因 */
    bigLog:(()=>{ if(!HZG_METERS.on('絵の大きさ')) return []; try{ return (window.__HZG_BIG||[]).slice(-12); }catch(e){ return []; } })(),
    /* 今のKと盤面の実幅。上の記録と見比べる基準 */
    glK:(()=>{ try{ const sc=document.getElementById('scene'); if(!sc) return null;
      const r=sc.getBoundingClientRect();
      return {盤面の実幅:Math.round(r.width), K:+(r.width/390).toFixed(3)}; }catch(e){ return null; } })(),
    /* 必殺技の計器(2026-09-03 K1許可)。技の最中と直後3秒の様子。
       「進んだ」がほぼ0のまま続き、「道」が0なら、道が引けずに踏み込んでは戻される痙攣 */
    spLog:(()=>{ try{ const L=(window.__HZG_SPLOG||[]);
      return L.length ? L.slice(-160) : []; }catch(e){ return []; } })(),
    floatLog:(()=>{ try{ const L=(window.__HZG_FLOATLOG||[]);
      /* 跳んだ物だけに絞って載せる(全部載せると報告が太る) */
      const byId={};
      for(const f of L) for(const o of (f.物||[])){ (byId[o.id]=byId[o.id]||[]).push({t:f.t, 上:o.上, 字:o.字}); }
      const out={};
      for(const id in byId){ const a=byId[id]; let mx=0;
        for(let i=1;i<a.length;i++) mx=Math.max(mx, Math.abs(a[i].上-a[i-1].上));
        if(mx>=12) out[id+'('+a[0].字+')  最大'+Math.round(mx)+'px']=a.map(x=>x.上); }
      /* 盤面のずらしが動いていないか(跳びの本命)。値が変わったコマだけ並べる */
      const cam=[]; let pv=null;
      for(const f of L){
        const k=f.盤面の位置+'|'+f.親たち+'/'+f.送り+'/'+f.見える窓のずれ+'/'+f.ずらし+'/'+f.倍率+'/'+f.拡大+'/'+f.縦ずれ+'/'+f.箱の高さ;
        if(k!==pv){ cam.push({t:f.t, 盤面の位置:f.盤面の位置, 親たち:f.親たち, 送り:f.送り,
                              見える窓のずれ:f.見える窓のずれ, ずらし:f.ずらし, 倍率:f.倍率,
                              拡大:f.拡大, 縦ずれ:f.縦ずれ, 箱の高さ:f.箱の高さ}); pv=k; }
      }
      if(cam.length>1) out['★土台が動いた']=cam.slice(0,24);
      else if(cam.length===1) out['★土台は動いていない']=cam[0];
      return Object.keys(out).length? out : {跳んだ物:'なし(直近3秒)'};
    }catch(e){ return null; } })(),
    /* コマ仕事の内訳(2026-08-21): 襲撃ループ/教室ループの自己時間。19ms級の犯人をここで切り分ける */
    perf:(()=>{ try{ const q=a=>{ if(!a||!a.length) return null; const b=a.slice().sort((x,y)=>x-y);
        return {中央:+b[Math.floor(b.length/2)].toFixed(1), 遅い方1割:+b[Math.floor(b.length*0.9)].toFixed(1), 最悪:+b[b.length-1].toFixed(1)}; };
      const P=window.__hzgPerf||{}; return {襲撃ループ:q(P.raid), 教室ループ:q(P.cls)}; }catch(e){ return null; } })(),
    /* 切り替え工程の時刻印(全撤去TODO段0・2026-08-23): 実機のカクつきがどの工程間で起きたかを読む */
    switchLog:(()=>{ try{ return ((window.__hzgPerf||{}).sw||[]).slice(-24); }catch(e){ return null; } })(),
    /* 発熱計(2026-08-23 K1報告): 描画回数/秒と隠れている間の活動。隠れて動いた秒数>0なら異常 */
    heat:(()=>{ try{ const R=window.__heatRing||[]; if(!R.length) return null;
      const last=R.slice(-60);
      const sum=(a,k)=>a.reduce((x,y)=>x+y[k],0);
      const hid=R.filter(x=>x.h);
      return { 直近60秒:{ 教室描画_平均毎秒:+(sum(last,'p')/last.length).toFixed(1),
          教室描画_最大毎秒:Math.max.apply(null,last.map(x=>x.p)),
          戦闘描画_平均毎秒:+(sum(last,'d')/last.length).toFixed(1) },
        隠れていた秒数:hid.length,
        隠れて動いた秒数:hid.filter(x=>x.p>0||x.d>0).length };
    }catch(e){ return null; } })(),
    events:BB_EV.slice(-100) };
  try{
    const js=document.querySelector('script[src*="raid_battle.js"]');
    if(js) rep.jsver=String(js.getAttribute('src')||'').split('?')[1]||'';
  }catch(e){}
  try{ if(typeof game!=='undefined' && game){
    rep.screen={ phase:game.phase, day:game.day, slot:game.slot,
      alive:(game.students||[]).filter(function(s){ return ['healthy','injured','suspect'].indexOf(s.state)>=0; }).length };
    rep.daily=slimDaily(game);
  } }catch(e){ rep.screenErr=String(e).slice(0,120); }
  try{ if(typeof window.HZG_SNAPSHOT==='function') rep.raid=window.HZG_SNAPSHOT(); }catch(e){ rep.raidErr=String(e).slice(0,200); }
  try{ rep.dom=domCount(); }catch(e){}
  /* BGMの内部状態(2026-08-21 K1報告「小刻みにリピート」の原因特定用の覗き窓)。
     曲名・どちら側が鳴っているか・再生位置・終端との距離・停止/終了/エラーの旗を写す */
  try{
    const out={wanted:(typeof bgmWanted!=='undefined')?bgmWanted:undefined,
      playing:(typeof bgmSrc!=='undefined')?!!bgmSrc:undefined,
      ctx:(typeof actx!=='undefined'&&actx)?actx.state:null, tracks:{}};
    if(typeof bgmEls!=='undefined') for(const k in bgmEls){
      const e2=bgmEls[k]; if(!e2) continue;
      const side=s2=>({t:+(s2.el.currentTime||0).toFixed(2), dur:+(s2.el.duration||0).toFixed(2),
        paused:s2.el.paused, ended:s2.el.ended, ready:s2.el.readyState,
        gain:+(s2.g&&s2.g.gain?s2.g.gain.value:0).toFixed(3),
        err:s2.el.error?String(s2.el.error.code):null});
      out.tracks[k]={cur:e2.cur?(e2.cur===e2.a?'a':'b'):null, timer:!!e2.timer, a:side(e2.a), b:side(e2.b)};
    }
    const rb=window.__HZG_BGM_EL||{};
    for(const k in rb){ const e3=rb[k]; if(e3&&e3.el) out.tracks[k]={t:+(e3.el.currentTime||0).toFixed(2),
      paused:e3.el.paused, ended:e3.el.ended, gain:+(e3.g&&e3.g.gain?e3.g.gain.value:0).toFixed(3)}; }
    rep.bgm=out;
  }catch(e){ rep.bgmErr=String(e).slice(0,120); }
  /* IndexedDBはDOM要素や循環参照を保存できないので、ここで安全な形へ落としてから返す */
  try{ return JSON.parse(safeJson(rep)); }
  catch(e){ return {id:rep.id, at:rep.at, memo:'', err:'保存できない値が混じっていた: '+String(e).slice(0,120)}; }
}

/* --- 常設ボタンと件数バッジ --- */
const BB_MARU='①②③④⑤⑥⑦⑧⑨⑩';
function bbBuildBtn(){
  if(bbBtn) return;
  bbBtn=document.createElement('button');
  bbBtn.id='hzgBugBtn';
  bbBtn.type='button';
  /* 置き場所(2026-08-25 K1指示): 画面右の縦置きをやめ、下段ラベルの工作(右端)の真上へ横書きで置く。
     左右は工作ボタンの中心(5個並びなので幅の90%)に合わせ、下は下段ラベルのすぐ上(2026-08-25 K1指示でさらに10px下げた。--tabHは index.html の :root) */
  bbBtn.style.cssText='position:fixed; left:90%; bottom:calc(var(--tabH, 62px) - 4px); transform:translateX(-50%); z-index:2147483000;'
    +'font:bold 11px/1.15 -apple-system,sans-serif; color:#fff2e0; background:rgba(150,40,30,.8);'
    +'border:1px solid rgba(255,150,120,.65); border-radius:9px; padding:5px 8px; letter-spacing:.06em;'
    +'white-space:nowrap; box-shadow:0 2px 8px rgba(0,0,0,.5);'
    +'-webkit-user-select:none; user-select:none; -webkit-tap-highlight-color:transparent;';
  /* 押し方で分ける(2026-08-28 K1指示。テスターに使いやすく)。
     ・軽く押す = 「何が起きた?」だけ書いて送る小さな窓(テスター用)
     ・長押し   = 今までの報告箱(K1用。一覧・選択・書き出し)
     どちらの場合も、窓を出す前に必ず状態を確定する——画面を触ると状態が変わるため */
  var bbHold=null, bbLong=false;
  function bbStart(){ bbLong=false; clearTimeout(bbHold); bbTimeHold(true);
    bbHold=setTimeout(function(){ bbLong=true;
      const rep=bbCapture();
      bbPut(rep).then(function(){ bbRefresh(); bbOpenWin(rep.id); })
                .catch(function(e){ toast('報告の保存に失敗: '+e); });
    }, 600); }
  function bbEnd(ev){ clearTimeout(bbHold);
    if(bbLong){ bbLong=false; if(ev){ ev.preventDefault(); ev.stopPropagation(); } return; }
    if(ev){ ev.preventDefault(); ev.stopPropagation(); }
    bbQuickOpen();
  }
  bbBtn.addEventListener('pointerdown', function(ev){ ev.stopPropagation(); bbStart(); });
  bbBtn.addEventListener('pointerup', bbEnd);
  bbBtn.addEventListener('pointercancel', function(){ clearTimeout(bbHold); bbLong=false; });
  bbBtn.addEventListener('pointerleave', function(){ clearTimeout(bbHold); });
  bbBtn.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); });
  document.body.appendChild(bbBtn);
  bbRefresh();
}
function bbRefresh(){
  bbAll().then(function(list){
    bbCount=list.length;
    if(bbBtn) bbBtn.textContent='報告'+(bbCount?BB_MARU[Math.min(9,bbCount-1)]:'');
  }).catch(function(){});
}

/* --- テスター用の小さな窓(2026-08-28 K1指示) ---
   軽く押した時に出る。書くのは「何が起きたか」だけで、裏では今までどおり
   押した瞬間の状態をJSONで確定して端末へ保存し、そのままサーバーへ送る。
   送った直後だけ「間違えて送った」を出す——消しはせず、印だけ付ける
   (K1が一覧で「これは間違いだな」と目で分かればいい、という指示) */
var bbQuickWin=null, bbLastSent=null;
function bbQuickClose(){ if(bbQuickWin){ bbQuickWin.remove(); bbQuickWin=null; } bbHoldSync(); }
function bbQuickOpen(){
  if(bbQuickWin) return;
  const rep=bbCapture();          /* ★窓を出す前に確定する */
  const w=document.createElement('div');
  bbQuickWin=w;
  w.style.cssText='position:fixed; inset:0; z-index:2147483002; display:flex; align-items:center;'
    +'justify-content:center; padding:18px; background:rgba(6,10,8,.72);'
    +'font:13px/1.6 -apple-system,"Hiragino Kaku Gothic ProN",sans-serif;';
  const box=document.createElement('div');
  box.style.cssText='width:100%; max-width:330px; background:#f7f2e2; color:#2e3a2e; border-radius:14px;'
    +'padding:16px 15px 14px; box-shadow:0 10px 30px rgba(0,0,0,.6);';
  box.innerHTML='<div style="font-size:15px;font-weight:900;letter-spacing:.04em">なにが起こったか教えてください</div>'
    +'<div style="margin-top:5px;font-size:11px;color:#6b6250;line-height:1.7">'
    +'簡単な説明で大丈夫です。画面の様子は自動で一緒に送られます。</div>';
  const ta=document.createElement('textarea');
  ta.placeholder='例: 扉を押したら動かなくなった';
  ta.style.cssText='width:100%; height:92px; margin-top:10px; padding:9px; border-radius:9px;'
    +'border:1px solid #b3a888; background:#fffdf5; color:#2e3a2e;'
    +'font:14px/1.6 inherit; resize:none; -webkit-appearance:none;';
  box.appendChild(ta);
  /* 画面の絵(2026-09-02 K1指示)。文字だけでも送れるし、絵に印を付けても送れる */
  const shotRow=document.createElement('div');
  shotRow.style.cssText='display:flex; gap:8px; align-items:center; margin-top:10px;';
  const shotBtn=document.createElement('button');
  shotBtn.type='button'; shotBtn.textContent='画面の絵を付ける';
  shotBtn.style.cssText='flex:1; padding:11px 6px; border:1px dashed #a8997a; border-radius:10px;'
    +'font-weight:900; font-size:13px; background:#efe8d4; color:#4a4436;';
  const shotThumb=document.createElement('img');
  shotThumb.style.cssText='display:none; width:46px; height:46px; object-fit:cover;'
    +'border-radius:8px; border:1px solid #b3a888;';
  const shotDel=document.createElement('button');
  shotDel.type='button'; shotDel.textContent='外す';
  shotDel.style.cssText='display:none; padding:11px 10px; border:0; border-radius:10px;'
    +'font-weight:900; font-size:12px; background:#ddd5c0; color:#4a4436;';
  shotRow.appendChild(shotBtn); shotRow.appendChild(shotThumb); shotRow.appendChild(shotDel);
  box.appendChild(shotRow);
  const shotHint=document.createElement('div');
  shotHint.style.cssText='margin-top:5px; font-size:10.5px; color:#8a8069; line-height:1.7;';
  shotHint.textContent='押すと今の画面がそのまま出ます。気になる所を指でなぞってください。';
  box.appendChild(shotHint);
  function shotShow(){
    const has=!!rep.shot;
    shotThumb.style.display=has?'block':'none';
    shotDel.style.display=has?'block':'none';
    if(has) shotThumb.src=rep.shot;
    shotBtn.textContent=has?'描き直す':'画面の絵を付ける';
    shotHint.textContent=has
      ? ('絵を付けました('+Math.round(rep.shot.length/1024)+'KB)。押すと描き直せます。')
      : '押すと今の画面がそのまま出ます。気になる所を指でなぞってください。';
  }
  shotBtn.onclick=function(ev){ ev.stopPropagation();
    shotBtn.disabled=true; shotBtn.textContent='画面を写しています…';
    /* 報告の窓が写り込まないよう、写している間だけ隠す */
    var w0=bbQuickWin; if(w0) w0.style.visibility='hidden';
    bbShotNow().then(function(url){
      if(w0) w0.style.visibility='';
      shotBtn.disabled=false; shotShow();
      if(url) bbShotEdit(url, function(u){ rep.shot=u; shotShow(); });
      else toast('画面を写せませんでした');
    }).catch(function(e){
      if(w0) w0.style.visibility='';
      shotBtn.disabled=false; shotShow(); toast('画面を写せなかった: '+e.message);
    });
  };
  shotDel.onclick=function(ev){ ev.stopPropagation(); rep.shot=null; shotShow(); };

  const row=document.createElement('div');
  row.style.cssText='display:flex; gap:8px; margin-top:12px;';
  const mk=function(label,bg,fg){
    const b=document.createElement('button'); b.type='button'; b.textContent=label;
    b.style.cssText='flex:1; padding:13px 6px; border:0; border-radius:10px; font-weight:900;'
      +'font-size:15px; background:'+bg+'; color:'+fg+';';
    row.appendChild(b); return b; };
  const cancel=mk('キャンセル','#ddd5c0','#4a4436');
  const send=mk('報告する','#a94037','#fff4ea');
  box.appendChild(row);
  const note=document.createElement('div');
  note.style.cssText='margin-top:10px; text-align:center; min-height:18px; font-size:11.5px; color:#6b6250;';
  box.appendChild(note);
  w.appendChild(box); document.body.appendChild(w);
  setTimeout(function(){ try{ ta.focus(); }catch(e){} }, 60);

  cancel.onclick=function(ev){ ev.stopPropagation(); bbQuickClose(); };
  w.addEventListener('click', function(ev){ if(ev.target===w) bbQuickClose(); });
  send.onclick=function(ev){
    ev.stopPropagation();
    send.disabled=true; cancel.disabled=true; send.textContent='送っています…';
    rep.memo=ta.value.slice(0,400);
    bbPut(rep).then(function(){ bbRefresh(); }).catch(function(){});
    const body=JSON.stringify(Object.assign({}, rep, {
      note:rep.memo, ua:navigator.userAgent,
      who:(function(){ try{ return localStorage.getItem('hzg_tester')||''; }catch(e){ return ''; } })()
    }));
    fetch('/api/bug', {method:'POST', headers:{'content-type':'application/json'}, body:body})
      .then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); })
      .then(function(d){ bbLastSent=d.id; bbQuickDone(box, note, true); })
      .catch(function(){ bbQuickDone(box, note, false); });
  };
}
/* --- 画面の絵を付ける(2026-09-02 K1指示) ---
   K1「スクショしてそこに印つけて送れる機能を作れ。従来の文字だけで送るか、
       その画面のスクショにまるとかをペンで書いて送るかできる感じに」

   絵はゲームの中で写さず、**端末で撮ったスクショを選んでもらう**。
   理由: GPU描画のcanvasはtoDataURLで空の絵になることがある(描いた直後でないと中身が残らない)。
   撮った本人の画面をそのまま持ってくる方が確実で、ゲームの外の崩れも写る。
   選んだ絵は900pxまで縮めてJPEGにする——1件512KBの上限に収めるため */
var BB_SHOT_MAX=1200, BB_SHOT_BYTES=260*1024;   // 900→1200(2026-09-04): 900だと3倍の画面から落としすぎて別物に見えていた
/* 今の画面をそのまま1枚の絵にする(2026-09-02 K1指示)。
   報告を押した瞬間ゲームの時間は止まっているので、次に描かれるコマは今見えている画そのもの。
   GPUの盤面は「描き終えた直後」でないと中身が残らないので、絵の受け取りはgl_sprites側の
   コマの終わりに仕込んである(window.__HZG_GRAB)。
   盤面が動いていない画面(タイトル・名簿など)では取れないので、その時は写真を選ぶ方へ回す */
function bbGrabScreen(){
  return new Promise(function(res){
    var done=false;
    try{
      window.__HZG_GRAB=function(url){ if(done) return; done=true; res(url||null); };
    }catch(e){ res(null); return; }
    setTimeout(function(){ if(done) return; done=true;
      try{ window.__HZG_GRAB=null; }catch(e){}
      res(null); }, 700);   // 描かれないまま待たされない
  });
}
/* 画面をその場で1枚の絵にする(2026-09-02 K1指示の作り直し)。
   K1「なんでiPhoneから写真を添付する画面出るんだよ。押したらその画面のスクショを
   そのままペイントして報告と一緒に送れるようにしろ」——写真選びへ逃げるのをやめる。
   ・盤面(教室・戦闘)はGPUが描いているので、コマの終わりに取る(__HZG_GRAB)
   ・それ以外の画面(購買部・名簿・タイトル…)はHTMLなので、html2canvasで写す
   ・両方ある時は、HTMLの絵の上へ盤面の絵を同じ位置で重ねる
   html2canvasは同梱(lib_html2canvas.js)。押した時に初めて読み込む */
function bbH2C(){
  if(window.html2canvas) return Promise.resolve(window.html2canvas);
  if(bbH2C._p) return bbH2C._p;
  return bbH2C._p=new Promise(function(res,rej){
    var sc=document.createElement('script');
    sc.src='lib_html2canvas.js?v=20260902zp';
    sc.onload=function(){ res(window.html2canvas); };
    sc.onerror=function(){ rej(new Error('画面を写す道具が読めなかった')); };
    document.head.appendChild(sc);
  });
}
/* 報告の窓など「写したくない物」を外す */
function bbShotSkip(el){
  try{
    if(!el || !el.classList) return false;
    if(el===bbQuickWin || el===bbWin || el===bbBtn) return true;
    if(el.id==='hzgUiedCross') return true;
    return false;
  }catch(e){ return false; }
}
/* 報告に付ける絵(2026-09-04 K1報告「iPhone13でそのまま表示されてるスクショになってねえよ」の直し)。
   直したのは2つ。
   ①**粗さ**: HTMLの写しを等倍(390x797)で作っていた。iPhone 13の実際の画面は3倍の細かさなので、
     字も線も潰れて別物に見えていた。端末の細かさぶん(上限2倍)で写す。
   ②**盤面の貼り位置**: GPUの板を重ねる時、置き場所を「文書で最初に見つかったcanvas」から
     取っていた。別のcanvasが先にあると見当違いの所へ貼られる。板そのものを名指しする。
   なお、iOSのSafariにはJavaScriptから本物のスクショを撮る方法が無い。
   HTMLの部分は html2canvas が描き直した近似で、盤面だけが本物の画素。 */
function bbGlCanvas(){
  try{ if(window.__HZG_GLCANVAS && window.__HZG_GLCANVAS.getBoundingClientRect) return window.__HZG_GLCANVAS; }catch(e){}
  /* 名指しが無い時は、画面に出ている一番大きいcanvasを選ぶ(最初の1個ではなく) */
  try{
    var best=null, area=0;
    var all=document.querySelectorAll('canvas');
    for(var i=0;i<all.length;i++){
      var r=all[i].getBoundingClientRect();
      if(r.width<40 || r.height<40) continue;
      if(r.width*r.height>area){ area=r.width*r.height; best=all[i]; }
    }
    return best;
  }catch(e){ return null; }
}
function bbShotNow(){
  var K=Math.max(1, Math.min(2, window.devicePixelRatio||1));   // 端末の細かさぶん(上限2倍)
  return bbH2C().then(function(h2c){
    return h2c(document.body, {
      backgroundColor:'#000', scale:K, logging:false, useCORS:true, allowTaint:true,
      width:window.innerWidth, height:window.innerHeight,
      windowWidth:window.innerWidth, windowHeight:window.innerHeight,
      ignoreElements:bbShotSkip
    });
  }).then(function(page){
    /* 盤面(GPU)が動いていれば、その絵を同じ場所へ重ねる */
    return bbGrabScreen().then(function(gl){
      if(!gl) return page.toDataURL('image/jpeg', .9);
      return new Promise(function(res){
        var im=new Image();
        im.onload=function(){
          try{
            var el=bbGlCanvas();
            var g=page.getContext('2d');
            if(el){
              var r=el.getBoundingClientRect();
              /* html2canvasの絵はK倍で作ってあるので、貼る枠も同じ倍率に合わせる */
              g.drawImage(im, r.left*K, r.top*K, r.width*K, r.height*K);
            }
            res(page.toDataURL('image/jpeg', .9));
          }catch(e){ res(page.toDataURL('image/jpeg', .9)); }
        };
        im.onerror=function(){ res(page.toDataURL('image/jpeg', .9)); };
        im.src=gl;
      });
    });
  });
}
function bbShotPick(cb){
  var inp=document.createElement('input');
  inp.type='file'; inp.accept='image/*';
  inp.style.cssText='position:fixed;left:-9999px;top:0;';
  document.body.appendChild(inp);
  inp.onchange=function(){
    var f=inp.files && inp.files[0];
    inp.remove();
    if(!f) return;
    var fr=new FileReader();
    fr.onload=function(){ bbShotEdit(String(fr.result), cb); };
    fr.readAsDataURL(f);
  };
  inp.click();
}
/* 絵の上に指で描く窓。丸でも矢印でも、指で好きに */
function bbShotEdit(src, cb){
  var im=new Image();
  im.onload=function(){
    var k=Math.min(1, BB_SHOT_MAX/Math.max(im.width, im.height));
    var W=Math.round(im.width*k), H=Math.round(im.height*k);
    var base=document.createElement('canvas'); base.width=W; base.height=H;
    base.getContext('2d').drawImage(im,0,0,W,H);

    var w=document.createElement('div');
    w.style.cssText='position:fixed; inset:0; z-index:2147483003; background:#0b0e0c;'
      +'display:flex; flex-direction:column; touch-action:none;'
      +'font:13px/1.6 -apple-system,"Hiragino Kaku Gothic ProN",sans-serif; color:#f7f0dd;';
    var head=document.createElement('div');
    head.style.cssText='flex:0 0 auto; padding:calc(8px + env(safe-area-inset-top)) 10px 8px;'
      +'display:flex; gap:6px; align-items:center; background:#1a241e; border-bottom:1px solid #3d5c4c;';
    /* 確定と取り消しは上に小さく置く。下に6個並べると折り返して潰れ、
       どれで確定するのか分からなくなっていた(2026-09-03 K1指摘) */
    var bCancel=document.createElement('button'); bCancel.type='button'; bCancel.textContent='やめる';
    bCancel.style.cssText='flex:0 0 auto;padding:6px 11px;border:0;border-radius:8px;'
      +'font-weight:900;font-size:12px;background:#3d5c4c;color:#cfe0d5;';
    var ttl=document.createElement('span');
    ttl.style.cssText='flex:1 1 auto;text-align:center;font-weight:900;font-size:12.5px;';
    ttl.textContent='気になる所に印を付ける';
    var bOk=document.createElement('button'); bOk.type='button'; bOk.textContent='確定';
    bOk.style.cssText='flex:0 0 auto;padding:6px 15px;border:0;border-radius:8px;'
      +'font-weight:900;font-size:12.5px;background:#e0b048;color:#1a241e;';
    head.appendChild(bCancel); head.appendChild(ttl); head.appendChild(bOk);
    var stage=document.createElement('div');
    stage.style.cssText='flex:1 1 auto; min-height:0; display:flex; align-items:center;'
      +'justify-content:center; padding:6px; position:relative;';
    var wrap=document.createElement('div');
    wrap.style.cssText='position:relative; max-width:100%; max-height:100%;';
    base.style.cssText='display:block; max-width:100%; max-height:100%; width:auto; height:auto;';
    var pen=document.createElement('canvas'); pen.width=W; pen.height=H;
    pen.style.cssText='position:absolute; left:0; top:0; width:100%; height:100%; touch-action:none;';
    wrap.appendChild(base); wrap.appendChild(pen); stage.appendChild(wrap);

    var foot=document.createElement('div');
    foot.style.cssText='flex:0 0 auto; padding:8px 10px calc(8px + env(safe-area-inset-bottom));'
      +'background:#1a241e; border-top:1px solid #3d5c4c; display:flex; gap:6px;';
    function btn(label,bg,fg,grow){
      var b=document.createElement('button'); b.type='button'; b.textContent=label;
      b.style.cssText='flex:'+(grow||1)+' 1 0; padding:11px 4px; border:0; border-radius:10px;'
        +'font-weight:900; font-size:13px; background:'+bg+'; color:'+fg+';';
      foot.appendChild(b); return b;
    }
    var col='#ff3b2f';
    var bRed=btn('赤','#ff3b2f','#fff'), bYel=btn('黄','#ffd23b','#3a2c00');
    var bUndo=btn('1つ戻す','#3d5c4c','#e8f0ea'), bClr=btn('全部消す','#3d5c4c','#e8f0ea');
    function mark(){ bRed.style.outline=(col==='#ff3b2f')?'3px solid #fff':'none';
                     bYel.style.outline=(col==='#ffd23b')?'3px solid #fff':'none'; }
    bRed.onclick=function(){ col='#ff3b2f'; mark(); };
    bYel.onclick=function(){ col='#ffd23b'; mark(); };
    mark();

    w.appendChild(head); w.appendChild(stage); w.appendChild(foot);
    document.body.appendChild(w);

    var g=pen.getContext('2d');
    g.lineCap='round'; g.lineJoin='round';
    var strokes=[], cur=null, id=null;
    function redraw(){
      g.clearRect(0,0,W,H);
      for(var i=0;i<strokes.length;i++){
        var st=strokes[i]; if(st.p.length<2) continue;
        g.strokeStyle=st.c; g.lineWidth=st.w;
        g.beginPath(); g.moveTo(st.p[0].x, st.p[0].y);
        for(var q=1;q<st.p.length;q++) g.lineTo(st.p[q].x, st.p[q].y);
        g.stroke();
      }
    }
    function pt(ev){ var r=pen.getBoundingClientRect();
      return {x:(ev.clientX-r.left)*(W/r.width), y:(ev.clientY-r.top)*(H/r.height)}; }
    pen.addEventListener('pointerdown', function(ev){
      if(id!==null) return; id=ev.pointerId;
      try{ pen.setPointerCapture(id); }catch(e){}
      cur={c:col, w:Math.max(3, Math.round(W/110)), p:[pt(ev)]};
      strokes.push(cur); ev.preventDefault();
    });
    pen.addEventListener('pointermove', function(ev){
      if(ev.pointerId!==id || !cur) return;
      cur.p.push(pt(ev)); redraw(); ev.preventDefault();
    });
    function up(ev){ if(ev.pointerId!==id) return; id=null; cur=null; }
    pen.addEventListener('pointerup', up);
    pen.addEventListener('pointercancel', up);

    bUndo.onclick=function(){ strokes.pop(); redraw(); };
    bClr.onclick=function(){ strokes.length=0; redraw(); };
    bCancel.onclick=function(){ w.remove(); };
    bOk.onclick=function(){
      var out=document.createElement('canvas'); out.width=W; out.height=H;
      var o=out.getContext('2d');
      o.drawImage(base,0,0); o.drawImage(pen,0,0);
      var q=0.72, url=out.toDataURL('image/jpeg', q);
      while(url.length>BB_SHOT_BYTES && q>0.35){ q-=0.12; url=out.toDataURL('image/jpeg', q); }
      w.remove();
      cb(url);
    };
  };
  im.onerror=function(){ toast('その絵は読めませんでした'); };
  im.src=src;
}
/* 送った後の姿。ここで初めて「間違えて送った」を出す */
function bbQuickDone(box, note, ok){
  box.innerHTML='';
  const h=document.createElement('div');
  h.style.cssText='font-size:15px;font-weight:900;letter-spacing:.04em;text-align:center;';
  h.textContent = ok ? 'ありがとうございます。送りました。' : '送れませんでした';
  box.appendChild(h);
  if(!ok){   // 失敗した時だけ、どうすればいいかを1行だけ出す
    const p=document.createElement('div');
    p.style.cssText='margin-top:7px;font-size:11.5px;color:#6b6250;line-height:1.7;text-align:center;';
    p.textContent='電波の届く所でもう一度お願いします。';
    box.appendChild(p);
  }
  const row=document.createElement('div');
  row.style.cssText='display:flex; gap:8px; margin-top:13px;';
  const close=document.createElement('button');
  close.type='button'; close.textContent='閉じる';
  close.style.cssText='flex:1; padding:13px 6px; border:0; border-radius:10px; font-weight:900;'
    +'font-size:15px; background:#28533f; color:#f7f0dd;';
  close.onclick=function(ev){ ev.stopPropagation(); bbQuickClose(); };
  row.appendChild(close);
  box.appendChild(row);
  if(ok && bbLastSent){
    const oops=document.createElement('button');
    oops.type='button'; oops.textContent='間違えて送ってしまった';
    oops.style.cssText='width:100%; margin-top:9px; padding:11px 6px; border:0; border-radius:10px;'
      +'font-weight:700; font-size:13px; background:#e6dcc4; color:#7a3b32;';
    oops.onclick=function(ev){
      ev.stopPropagation();
      oops.disabled=true; oops.textContent='印を付けています…';
      fetch('/api/bug?oops=1&id='+encodeURIComponent(bbLastSent), {cache:'no-store'})
        .then(function(){ oops.textContent='「間違い」と印を付けました'; })
        .catch(function(){ oops.textContent='印を付けられませんでした'; });
    };
    box.appendChild(oops);
  }
}

/* --- 一覧の画面 --- */
function bbOpenWin(focusId){
  if(bbWin){ bbWin.remove(); bbWin=null; }
  bbWin=document.createElement('div');
  bbWin.style.cssText='position:fixed; inset:4% 3%; z-index:2147483001; background:rgba(10,16,13,.97);'
    +'border:1px solid #4d7a5f; border-radius:12px; padding:9px; display:flex; flex-direction:column; gap:7px;'
    +'font:12px/1.5 -apple-system,sans-serif; color:#e6f2ea;';
  const head=document.createElement('div');
  head.style.cssText='display:flex; align-items:center; gap:6px;';
  head.innerHTML='<b style="font-size:13px">バグ報告箱</b>'
    +'<span style="font-size:10px;opacity:.7">押した瞬間の状態を保存しました</span>';
  const close=document.createElement('button');
  close.textContent='閉じる';
  close.style.cssText='margin-left:auto; padding:6px 10px; border:0; border-radius:8px; background:#28533f; color:#f7f0dd; font-weight:bold;';
  close.onclick=function(){ bbWin.remove(); bbWin=null; bbHoldSync(); };
  head.appendChild(close);
  const listEl=document.createElement('div');
  listEl.style.cssText='flex:1; overflow:auto; display:flex; flex-direction:column; gap:6px; -webkit-overflow-scrolling:touch;';
  /* 計器の入切(2026-09-06 K1指示「規定は全部オフ」「完全にオフのモードも作れ」)。
     追う時だけ点けて、終わったら消す。切ってある計器は記録を作る所へ入らないので、
     点けていない間は本当に何もしない */
  const met=document.createElement('div');
  met.style.cssText='display:flex; gap:4px; flex-wrap:wrap; align-items:center;'
    +'padding:6px 0 2px; border-top:1px solid #3d5c4c; margin-top:4px;';
  const NAMES=['効果音','床の物','必殺技','絵の大きさ','起動','音の口','曲','画面','コマ仕事','物理','スポット'];
  function metDraw(){
    met.innerHTML='<b style="font-size:10.5px; color:#ffd88a; flex:0 0 100%">計器'
      +(HZG_METERS.全部切る?'(全部切っています)':'')+'</b>';
    const all=document.createElement('button');
    all.textContent=HZG_METERS.全部切る?'全部入れる':'全部切る';
    all.style.cssText='font-size:10.5px; font-weight:bold; padding:6px 9px; border-radius:8px;'
      +'border:1px solid #b0523f; background:'+(HZG_METERS.全部切る?'#c2503a':'#2a1a16')+';'
      +'color:#f7f0dd;';
    all.onclick=function(){ HZG_METERS.set('全部切る', !HZG_METERS.全部切る); metDraw(); };
    met.appendChild(all);
    NAMES.forEach(function(nm){
      const b=document.createElement('button');
      const on=HZG_METERS.on(nm);
      b.textContent=nm;
      b.style.cssText='font-size:10.5px; font-weight:bold; padding:6px 9px; border-radius:8px;'
        +'border:1px solid #4d7a5f; background:'+(on?'#d98d2b':'#22322a')+';'
        +'color:'+(on?'#2b1a06':'#7f8f99')+';'
        +(HZG_METERS.全部切る?'opacity:.45;':'');
      b.onclick=function(){ HZG_METERS.set(nm, !HZG_METERS[nm]); metDraw(); };
      met.appendChild(b);
    });
    const note=document.createElement('div');
    note.style.cssText='flex:0 0 100%; font-size:9.5px; color:#8fa2ab; line-height:1.6; margin-top:3px;';
    note.textContent='橙=点いている。既定は「効果音・床の物・必殺技・絵の大きさ」が消えています'
      +'(毎回DOMを触ったり入れ物を作るので重い)。追う時だけ点けてください。';
    met.appendChild(note);
  }
  metDraw();
  const foot=document.createElement('div');
  foot.style.cssText='display:flex; gap:5px; flex-wrap:wrap;';
  bbWin.appendChild(head); bbWin.appendChild(listEl); bbWin.appendChild(met); bbWin.appendChild(foot);
  document.body.appendChild(bbWin);

  const sel={};
  function draw(){
    return bbAll().then(function(list){
      list.sort(function(a,b){ return a.at<b.at?1:-1; });
      listEl.innerHTML='';
      if(!list.length){ listEl.innerHTML='<div style="opacity:.6">まだ1件もありません</div>'; return; }
      list.forEach(function(rep){
        const row=document.createElement('div');
        row.style.cssText='background:#182821; border:1px solid #33564a; border-radius:9px; padding:7px;'
          +(rep.id===focusId?'outline:2px solid #d98d2b;':'');
        const t=new Date(rep.at);
        const hh=String(t.getHours()).padStart(2,'0')+':'+String(t.getMinutes()).padStart(2,'0');
        const r=rep.raid||{};
        const where=rep.screen ? (rep.screen.day+'日目 '+(rep.screen.phase||'')) : '(教室の外)';
        const info=[hh, where, (rep.draw==='webgl'?'GPU':'標準'),
          (r.zombies?('敵'+r.zombies.length):''),
          (rep.fps?(rep.fps.fps+'fps 最悪'+rep.fps.最悪+'ms'):''),
          ((rep.errors&&rep.errors.length)?('エラー'+rep.errors.length):'')].filter(Boolean).join(' / ');
        const line=document.createElement('div');
        line.style.cssText='display:flex; align-items:center; gap:6px;';
        const cb=document.createElement('input'); cb.type='checkbox'; cb.checked=!!sel[rep.id];
        cb.onchange=function(){ sel[rep.id]=cb.checked; };
        const lab=document.createElement('span'); lab.textContent=info;
        lab.style.cssText='flex:1; font-size:11px;';
        const del=document.createElement('button'); del.textContent='削除';
        del.style.cssText='padding:5px 8px; border:0; border-radius:7px; background:#7a2f23; color:#ffe9e5; font-size:11px;';
        del.onclick=function(){ bbDel(rep.id).then(function(){ delete sel[rep.id]; bbRefresh(); draw(); }); };
        line.appendChild(cb); line.appendChild(lab); line.appendChild(del);
        const memo=document.createElement('input');
        memo.placeholder='何が起きた?(後から書ける)'; memo.value=rep.memo||'';
        memo.style.cssText='width:100%; margin-top:5px; padding:6px; border-radius:7px; border:1px solid #3d6b58;'
          +'background:#0e1a15; color:#e6f2ea; font-size:12px;';
        memo.onchange=function(){ rep.memo=memo.value; bbPut(rep); };
        row.appendChild(line); row.appendChild(memo);
        listEl.appendChild(row);
      });
    });
  }
  function pick(){
    return bbAll().then(function(l){
      const any=Object.keys(sel).some(function(k){ return sel[k]; });
      return any ? l.filter(function(r){ return sel[r.id]; }) : l;
    });
  }
  function out(list, how){
    if(!list.length){ toast('選ばれていません'); return; }
    const txt=JSON.stringify({bundleVersion:1, at:new Date().toISOString(), reports:list});
    const kb=Math.round(txt.length/102.4)/10;
    if(how==='copy'){
      if(navigator.clipboard && navigator.clipboard.writeText)
        navigator.clipboard.writeText(txt).then(function(){ toast('コピーした('+kb+'KB)'); })
          .catch(function(){ manualCopy(txt); });
      else manualCopy(txt);
    } else if(how==='share' && navigator.share){
      const f=new File([txt], 'hzg_bug.json', {type:'application/json'});
      const p=(navigator.canShare && navigator.canShare({files:[f]}))
        ? navigator.share({files:[f], title:'平成ゾンビ学園 バグ報告'})
        : navigator.share({title:'平成ゾンビ学園 バグ報告', text:txt.slice(0,60000)});
      p.catch(function(){ manualCopy(txt); });
    } else {
      const a=document.createElement('a');
      a.href=URL.createObjectURL(new Blob([txt],{type:'application/json'}));
      a.download='hzg_bug_'+new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)+'.json';
      document.body.appendChild(a); a.click();
      setTimeout(function(){ URL.revokeObjectURL(a.href); a.remove(); }, 1000);
      toast('保存した('+kb+'KB)');
    }
  }
  function mkBtn(label, fn, bg){
    const b=document.createElement('button'); b.textContent=label;
    b.style.cssText='flex:1; min-width:86px; padding:9px 6px; border:0; border-radius:9px; font-weight:bold;'
      +'background:'+(bg||'#28533f')+'; color:#f7f0dd;';
    b.onclick=fn; foot.appendChild(b); return b;
  }
  /* サーバーへ送る(2026-08-28 K1指示。テスターの報告が端末に溜まるだけだったのを、
     押した先でK1が読めるようにする)。宛先は同じCloudflareの /api/bug 。
     成功したら「送った」と出す。圏外などで失敗しても端末の箱には残るので、後でもう一度送れる */
  mkBtn('サーバーへ送る', function(){
    pick().then(function(l){
      if(!l.length){ toast('選ばれていません'); return; }
      var ok=0, ng=0, done=0;
      l.forEach(function(rep){
        var body=JSON.stringify(Object.assign({}, rep, {
          note:rep.memo||'', who:(function(){ try{ return localStorage.getItem('hzg_tester')||''; }catch(e){ return ''; } })(),
          ua:navigator.userAgent
        }));
        fetch('/api/bug', {method:'POST', headers:{'content-type':'application/json'}, body:body})
          .then(function(r){ if(r.ok) ok++; else ng++; })
          .catch(function(){ ng++; })
          .then(function(){ done++;
            if(done===l.length) toast(ok+'件 送った'+(ng?(' / '+ng+'件 失敗'):''));
          });
      });
    });
  }, '#3d6ab4');
  mkBtn('コピー', function(){ pick().then(function(l){ out(l,'copy'); }); });
  mkBtn('ファイル保存', function(){ pick().then(function(l){ out(l,'file'); }); }, '#8a6d1c');
  if(navigator.share) mkBtn('共有', function(){ pick().then(function(l){ out(l,'share'); }); }, '#3d6ab4');
  mkBtn('全部消す', function(){
    if(!confirm('全部消していい?')) return;
    bbAll().then(function(l){ return Promise.all(l.map(function(r){ return bbDel(r.id); })); })
      .then(function(){ bbRefresh(); draw(); });
  }, '#7a2f23');
  draw();
}
/* 起動時にボタンを出す(開発ツールが止まっていればここまで来ない) */
setTimeout(bbBuildBtn, 800);

})();

----- END ORIGINAL CONTENT -----
===== END FILE 15: public/dev_tools.js =====

===== BEGIN FILE 16: tools/shot.mjs =====
Source-Path: C:\石とゾンビ\tools\shot.mjs
Source-Bytes: 6538
Source-Lines: 128
Source-LastWriteTime-JST: 2026-09-13T19:45:08+09:00
Source-SHA256: e2e2117487f6c103b7f9f1b5440181d6dafa92fc2ec6a59c557f8f8be46bcb6b
Role: 既存の検品コード。今回の作業では実行していない
----- BEGIN ORIGINAL CONTENT -----
/* 画面をiPhoneの実寸で写して検品するコマンド(2026-09-02 K1指示
   「ブラウザ使えないなら使えるようにしたれよ」)。
   Codexは自前のブラウザを持てないので、こちらのPlaywright(WebKit=Safariと同じ土台)を
   叩けるようにした。Codex・Claudeのどちらから呼んでもいい。

   使い方:
     node tools/shot.mjs <対象>            … 390x797 と 390x664 の2枚を撮って検品
     node tools/shot.mjs <対象> --h 900     … 高さを指定して1枚だけ
     node tools/shot.mjs <対象> --w 375     … 横幅を指定(既定390。SE・miniは375、Pro Maxは440)
     node tools/shot.mjs <対象> --out tmp/x … 出力先の頭(既定 tmp/shot)
     node tools/shot.mjs <対象> --wait 4000 … 撮る前に待つms(既定 2500)
     node tools/shot.mjs <対象> --click "#id" … 撮る前に押す(複数可)

   <対象>は次のどれでもいい:
     ・プロジェクト内の相対パス   例) mock_shopname.html
     ・絶対パス                    例) C:\平成ゾンビ学園\index.html
     ・http(s)のURL               例) https://heisei-zombie-kyoshitsu.pages.dev/

   出るもの:
     ・PNG(高さごとに1枚)
     ・JSエラー / 404 / 横のはみ出し / 一番下の要素が画面に収まっているか
   これで「見た目は確かめていません」と書かずに済む。 */
/* playwright は平成ゾンビ学園のnode_modulesに1つだけ入れてある。
   他のプロジェクトへこのファイルをコピーしても動くよう、無ければそちらから借りる。 */
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', '.mjs':'text/javascript', '.css':'text/css',
  '.json':'application/json', '.webmanifest':'application/json',
  '.webp':'image/webp', '.png':'image/png', '.jpg':'image/jpeg', '.jpeg':'image/jpeg',
  '.svg':'image/svg+xml', '.gif':'image/gif',
  '.mp3':'audio/mpeg', '.wav':'audio/wav', '.m4a':'audio/mp4', '.ogg':'audio/ogg',
  '.woff':'font/woff', '.woff2':'font/woff2', '.ttf':'font/ttf',
};

const argv = process.argv.slice(2);
if (!argv.length) {
  console.log('使い方: node tools/shot.mjs <対象> [--h 797] [--out tmp/shot] [--wait 2500] [--click "#id"]');
  process.exit(1);
}
const target = argv[0];
const opt = (name, def) => { const i = argv.indexOf('--' + name); return i > 0 ? argv[i + 1] : def; };
const clicks = argv.reduce((a, v, i) => (v === '--click' ? a.concat([argv[i + 1]]) : a), []);
const outBase = opt('out', 'tmp/shot');
const waitMs = +opt('wait', 2500);
const heights = opt('h', null) ? [+opt('h')] : [797, 664];
const vw = +opt('w', 390);   // 端末の横幅(2026-09-03: iPhoneは375〜440まである)

let url = target, srv = null;
if (!/^https?:\/\//i.test(target)) {
  const port = 8900 + Math.floor(Math.random() * 90);
  srv = http.createServer((q, r) => {
    let u = decodeURIComponent(q.url.split('?')[0]);
    if (u === '/') u = '/index.html';
    const f = pick(u);
    fs.readFile(f, (e, d) => {
      if (e) { r.writeHead(404); r.end(); return; }
      r.writeHead(200, { 'Content-Type': MIME[path.extname(f).toLowerCase()] || 'application/octet-stream' });
      r.end(d);
    });
  });
  await new Promise(ok => srv.listen(port, ok));
  const rel = path.isAbsolute(target) ? path.relative(ROOT, target) : target;
  url = 'http://localhost:' + port + '/' + rel.split(path.sep).join('/');
}

fs.mkdirSync(path.dirname(path.join(ROOT, outBase)), { recursive: true });

const browser = await webkit.launch();
let ng = 0;
for (const h of heights) {
  const page = await browser.newPage({ viewport: { width: vw, height: h } });
  const errs = [], bad = [];
  page.on('pageerror', e => errs.push(String(e.message).slice(0, 160)));
  page.on('response', r => { if (r.status() >= 400) bad.push(r.status() + ' ' + r.url().split('/').pop()); });
  await page.goto(url, { waitUntil: 'load' }).catch(e => errs.push('開けない: ' + e.message.slice(0, 80)));
  await page.waitForTimeout(waitMs);
  for (const c of clicks) {
    try { await page.locator(c).first().click({ timeout: 3000 }); await page.waitForTimeout(600); }
    catch (e) { errs.push('押せない ' + c); }
  }
  const st = await page.evaluate(() => {
    const doc = document.documentElement;
    const all = [...document.body.querySelectorAll('*')];
    let low = 0, lowName = '';
    for (const e of all) {
      const r = e.getBoundingClientRect();
      if (!(r.width > 0 && r.height > 0)) continue;
      const cs = getComputedStyle(e);
      if (cs.visibility === 'hidden' || cs.display === 'none' || +cs.opacity === 0) continue;   // 隠れている物は数えない
      if (r.bottom > low) { low = r.bottom; lowName = (e.id || e.className || e.tagName).toString().slice(0, 24); }
    }
    return {
      横はみ出し: doc.scrollWidth > doc.clientWidth,
      一番下: Math.round(low), 画面の高さ: window.innerHeight, 一番下の物: lowName,
    };
  });
  const file = outBase + '_' + h + '.png';
  await page.screenshot({ path: path.join(ROOT, file), fullPage: false });
  const over = st.一番下 > st.画面の高さ + 1;
  if (errs.length || bad.length || st.横はみ出し || over) ng++;
  console.log(
    vw + 'x' + h + '  → ' + file +
    '\n   JSエラー: ' + (errs.length ? errs.slice(0, 3).join(' / ') : 'なし') +
    '\n   404など : ' + (bad.length ? bad.slice(0, 3).join(' / ') : 'なし') +
    '\n   横はみ出し: ' + (st.横はみ出し ? 'あり' : 'なし') +
    '\n   一番下の物: ' + st.一番下 + 'px (' + st.一番下の物 + ') / 画面 ' + st.画面の高さ + 'px' +
    (over ? '  ← 画面からはみ出している' : '  収まっている')
  );
  await page.close();
}
await browser.close();
if (srv) srv.close();
console.log(ng ? ('検品: ' + ng + '件の高さで問題あり') : '検品: 問題なし');
process.exit(0);

----- END ORIGINAL CONTENT -----
===== END FILE 16: tools/shot.mjs =====

===== BEGIN FILE 17: tools/sweep.mjs =====
Source-Path: C:\石とゾンビ\tools\sweep.mjs
Source-Bytes: 6297
Source-Lines: 129
Source-LastWriteTime-JST: 2026-09-13T19:45:57+09:00
Source-SHA256: 0fa11506fd3cf44f0850249f81305055057df4299271f60153898b8ed8a9e21f
Role: 既存の検品コード。今回の作業では実行していない
----- BEGIN ORIGINAL CONTENT -----
/* 大根おろし検査(2026-09-13)。
   犯人を探しに行かず、**距離 x 角度 x 力**を1刻みで全部なめて表にする。
   石とゾンビの芯は「上から落とす軌道を作れるか」なので、ここで測るのは2つだけ:
     ① 頭に落として倒せる組み合わせが、どの距離にもちゃんとあるか(=詰みが無いか)
     ② 横なげ(低い角度)では倒せないか(=芯が効いているか)

   使い方: node tools/sweep.mjs [--prod] [--ang 4] [--pull 8]
     --prod  本番URLを撃つ(既定は手元のファイル)
     --ang   角度の刻み(度。既定4)
     --pull  力の刻み数(既定8 = 12.5%刻み) */
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);
const opt = (n, d) => { const i = argv.indexOf('--' + n); return i >= 0 ? argv[i + 1] : d; };
const angStep = +opt('ang', 4);
const pullSteps = +opt('pull', 8);

let url = 'https://ishi-to-zombie.pages.dev/', srv = null;
if (!argv.includes('--prod')) {
  const port = 8800 + 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';
}

const browser = await webkit.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 797 } });
const errs = [];
page.on('pageerror', e => errs.push(String(e.message).slice(0, 200)));
await page.goto(url, { waitUntil: 'load' });
await page.waitForTimeout(1200);

const st = await page.evaluate(() => window.ZG && window.ZG.state());
if (!st) { console.log('ZGが無い。index.htmlの検品の口が壊れている'); process.exit(1); }
console.log('版: ' + (await page.evaluate(() => window.ZG.ver)));
console.log('画面 ' + st.W + 'x' + st.H + ' / 1m=' + st.ppm.toFixed(1) + 'px / 頭の半径 ' +
  st.hr.toFixed(1) + 'px / 石の半径 ' + st.stoneR.toFixed(1) + 'px');
console.log('投げる所 x=' + st.hx.toFixed(0) + ' y=' + st.ry.toFixed(0) + ' / 立ち位置 x=' + st.px.toFixed(0));
console.log('画面に入る距離: ' + ((st.W - st.px) / st.ppm).toFixed(2) + 'm まで\n');

const dists = [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5];
const res = await page.evaluate(({ dists, angStep, pullSteps }) => {
  const out = [];
  for (const d of dists) {
    const row = { d, kill: 0, knock: 0, miss: 0, n: 0, bank: 0, killAng: [], killPull: [] };
    for (let a = 14; a <= 84; a += angStep) {
      for (let k = 1; k <= pullSteps; k++) {
        const pf = k / pullSteps;
        const o = window.ZG.trial(d, a, pf);
        row.n++;
        row[o.r]++;
        if (o.r === 'kill') {
          row.killAng.push(a); row.killPull.push(+pf.toFixed(3));
          if (o.b > 0) row.bank++;
        }
      }
    }
    out.push(row);
  }
  return out;
}, { dists, angStep, pullSteps });

console.log('距離  試行  倒した  うち跳ね  押した  外れ   倒せる角度      倒せる力');
let dead = 0;
for (const r of res) {
  const a = r.killAng.length ? Math.min(...r.killAng) + '〜' + Math.max(...r.killAng) + '度' : '—';
  const p = r.killPull.length
    ? Math.round(Math.min(...r.killPull) * 100) + '〜' + Math.round(Math.max(...r.killPull) * 100) + '%' : '—';
  if (!r.kill) dead++;
  console.log(
    String(r.d.toFixed(1) + 'm').padEnd(6) + String(r.n).padEnd(6) +
    (r.kill + '(' + (r.kill / r.n * 100).toFixed(1) + '%)').padEnd(14) +
    (r.bank + '').padEnd(10) +
    (r.knock + '').padEnd(8) + (r.miss + '').padEnd(7) +
    a.padEnd(16) + p);
}

/* 芯の確認: 低い角度(横なげ)で**直接**倒せてしまっていないか。
   跳ね返らせてから落とした物は芯どおりの手なので、別に数える。
   帯を14〜28度にしてあるのは実測の結果(2026-09-13):
   30度・5m・62%は、頭に届く時点ですでに水平から33度下へ落ちているので
   「横なげ」ではなく**低い山なり**。倒れて正しい。判定は落ちる角度で見ており、
   投げた角度では見ていない——だから投げた角度で線を引くならここまで。 */
const flat = await page.evaluate(() => {
  let direct = 0, bank = 0, knock = 0, miss = 0;
  for (const d of [2, 3, 4, 5, 6, 7]) {
    for (let a = 14; a <= 28; a += 2) {
      for (let k = 1; k <= 8; k++) {
        const o = window.ZG.trial(d, a, k / 8);
        if (o.r === 'kill') { if (o.b > 0) bank++; else direct++; }
        else if (o.r === 'knock') knock++; else miss++;
      }
    }
  }
  return { direct, bank, knock, miss };
});
console.log('\n横なげ(14〜28度)だけ: 直接倒した ' + flat.direct + ' / 跳ね返らせて倒した ' + flat.bank +
  ' / 押した ' + flat.knock + ' / 外れ ' + flat.miss);
console.log(flat.direct === 0
  ? '  → 芯は効いている(横なげが直接当たって倒れることは一度も無い)'
  : '  ← 横なげが直接当たって倒れている。判定が緩い');
console.log(dead ? '\n詰み: ' + dead + '件の距離で一度も倒せない ← 直すこと' : '\n詰み: 無し(どの距離にも倒せる投げ方がある)');
if (errs.length) console.log('\nJSエラー: ' + errs.slice(0, 4).join(' / '));

await browser.close();
if (srv) srv.close();
process.exit(0);

----- END ORIGINAL CONTENT -----
===== END FILE 17: tools/sweep.mjs =====

===== BEGIN FILE 18: tools/play.mjs =====
Source-Path: C:\石とゾンビ\tools\play.mjs
Source-Bytes: 6716
Source-Lines: 150
Source-LastWriteTime-JST: 2026-09-13T19:56:07+09:00
Source-SHA256: 15e4c72ff3cf301a7e046af037fc89ed3354c900302ef46ed393c756e39ad1b6
Role: 既存の検品コード。今回の作業では実行していない
----- BEGIN ORIGINAL CONTENT -----
/* 自分で何十回も遊ぶ道具(2026-09-13)。K1にテストをやらせないため。
   人の代わりに投げる子を動かして、**何秒生き延びて何体倒せるか**を数える。
   狙いは1つ: テンポ(湧く間隔・歩く速さ)が理不尽になっていないかを数字で見る。

   投げる子は「山なりで頭の上へ落ちる軌道」を総当たりで探して投げる=上手い人の上限。
   これで死ぬなら人には無理。これで楽勝なら緩すぎる。

   使い方: node tools/play.mjs [--n 10] [--sec 120] [--aim 0.6] [--prod]
     --n    何回遊ぶか(既定10)
     --sec  1回の上限(ゲーム内秒。既定120)
     --aim  1投に使う狙いの時間(秒。人の手の遅さ。既定0.6) */
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);
const opt = (n, d) => { const i = argv.indexOf('--' + n); return i >= 0 ? argv[i + 1] : d; };
const N = +opt('n', 10), SEC = +opt('sec', 120), AIM = +opt('aim', 0.6);

let url = 'https://ishi-to-zombie.pages.dev/', srv = null;
if (!argv.includes('--prod')) {
  const port = 8700 + 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';
}

const browser = await webkit.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 797 } });
const errs = [];
page.on('pageerror', e => errs.push(String(e.message).slice(0, 200)));
await page.goto(url, { waitUntil: 'load' });
await page.waitForTimeout(1200);
await page.evaluate(() => window.ZG.render(false));   /* 絵は描かない(速く回すため) */

const runs = await page.evaluate(({ N, SEC, AIM }) => {
  const Z = window.ZG, st = Z.state();
  const ppm = st.ppm, G = 9.8 * ppm, DRAG = 0.0022, h = 1 / 60;

  /* 石の軌道を、本物と同じ式で先読みする(重力・空気抵抗・1/60刻み) */
  function fly(ang, pf) {
    const v = (4.6 + 4.6 * Math.pow(pf, 1.15)) * ppm, r = ang * Math.PI / 180;
    let vx = Math.cos(r) * v, vy = -Math.sin(r) * v, x = st.hx, y = st.ry;
    const pts = [];
    for (let i = 0; i < 260; i++) {
      vx *= (1 - DRAG); vy *= (1 - DRAG); vy += G * h;
      const px = x, py = y;
      x += vx * h; y += vy * h;
      pts.push([px, py, x, y, vx, vy]);
      if (y > st.floorY || x > st.W + 60) break;
    }
    return pts;
  }
  /* 目標の高さ(頭のてっぺん)を、落ちながら急な角度で通る投げ方を探す */
  function aim(tx, crownY, tol) {
    let bestE = 1e9, best = null;
    for (let ang = 28; ang <= 84; ang += 1) {
      for (let k = 4; k <= 40; k++) {
        const pf = k / 40, pts = fly(ang, pf);
        for (const [x0, y0, x1, y1, vx, vy] of pts) {
          if (!(vy > 0 && y0 <= crownY && y1 > crownY)) continue;
          if (vy < Math.abs(vx) * 0.75) continue;      /* 余裕をみて0.6より厳しく狙う */
          const t = (crownY - y0) / (y1 - y0 || 1), cx = x0 + (x1 - x0) * t;
          const e = Math.abs(cx - tx);
          if (e < bestE) { bestE = e; best = { ang, pf, e }; }
          break;
        }
      }
    }
    return (best && best.e < tol) ? best : null;
  }
  /* 何コマ後に届くか(先回りして狙うため) */
  function timeTo(ang, pf, crownY) {
    const pts = fly(ang, pf);
    for (let i = 0; i < pts.length; i++) {
      const [, y0, , y1, , vy] = pts[i];
      if (vy > 0 && y0 <= crownY && y1 > crownY) return i / 60;
    }
    return 0.8;
  }

  const out = [];
  for (let run = 0; run < N; run++) {
    Z.start(false); Z.freeze(false);   /* false=通信を使わない(ひとりで回す) */
    let t = 0, aimT = 0, thrown = 0, maxAlive = 0;
    const steps = Math.round(SEC * 60);
    for (let i = 0; i < steps; i++) {
      Z.step(1 / 60); t += 1 / 60;
      const s = Z.state();
      if (s.mode !== 'play') break;
      const zs = Z.zs();
      if (zs.length > maxAlive) maxAlive = zs.length;
      aimT -= 1 / 60;
      if (aimT <= 0 && Z.can() && zs.length) {
        const z = zs[0];
        const crownY = z.hy - z.hr;
        /* 先回り: だいたいの飛行時間だけ手前へ */
        let tx = z.x, sol = null;
        for (let it = 0; it < 3; it++) {
          sol = aim(tx, crownY, (z.hr + s.stoneR) * 0.85);
          if (!sol) break;
          const ft = timeTo(sol.ang, sol.pf, crownY);
          tx = z.x - z.spd * ppm * ft;
        }
        if (sol) { Z.throwAt(sol.ang, sol.pf); thrown++; aimT = AIM; }
        else aimT = 0.2;
      }
    }
    const s2 = Z.state();
    out.push({ sec: +t.toFixed(1), kills: s2.kills, thrown, maxAlive, died: s2.mode !== 'play' });
  }
  return out;
}, { N, SEC, AIM });

console.log('回  生きた秒   倒した  投げた  命中率   同時に最大  結末');
let sum = { sec: 0, kills: 0, thrown: 0, died: 0 };
for (let i = 0; i < runs.length; i++) {
  const r = runs[i];
  sum.sec += r.sec; sum.kills += r.kills; sum.thrown += r.thrown; sum.died += r.died ? 1 : 0;
  console.log(String(i + 1).padEnd(4) + String(r.sec).padEnd(11) + String(r.kills).padEnd(8) +
    String(r.thrown).padEnd(8) + ((r.thrown ? r.kills / r.thrown * 100 : 0).toFixed(0) + '%').padEnd(9) +
    String(r.maxAlive).padEnd(12) + (r.died ? 'やられた' : '時間切れ'));
}
const n = runs.length;
console.log('\n平均: ' + (sum.sec / n).toFixed(1) + '秒 / ' + (sum.kills / n).toFixed(1) + '体 / ' +
  (sum.thrown ? sum.kills / sum.thrown * 100 : 0).toFixed(0) + '%命中 / やられた ' + sum.died + '/' + n + '回');
if (errs.length) console.log('JSエラー: ' + errs.slice(0, 4).join(' / '));
await browser.close();
if (srv) srv.close();
process.exit(0);

----- END ORIGINAL CONTENT -----
===== END FILE 18: tools/play.mjs =====

===== BEGIN FILE 19: tools/photo.mjs =====
Source-Path: C:\石とゾンビ\tools\photo.mjs
Source-Bytes: 5525
Source-Lines: 127
Source-LastWriteTime-JST: 2026-09-13T19:59:37+09:00
Source-SHA256: b4df12a4e2852e490ac6f2e3a720c0b9ea94592d0264ae0018369c235d9e3519
Role: 既存の検品コード。今回の作業では実行していない
----- BEGIN ORIGINAL CONTENT -----
/* 見せる写真を撮る道具(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);

----- END ORIGINAL CONTENT -----
===== END FILE 19: tools/photo.mjs =====

===== BEGIN FILE 20: tools/two.mjs =====
Source-Path: C:\石とゾンビ\tools\two.mjs
Source-Bytes: 7075
Source-Lines: 145
Source-LastWriteTime-JST: 2026-09-13T19:56:57+09:00
Source-SHA256: cdd77076c4c19077c1da8850d409fa2b32af130a36a1306ef0198bb9f2f9f02b
Role: 既存の検品コード。今回の作業では実行していない
----- BEGIN ORIGINAL CONTENT -----
/* 2つの端末で本当に繋がるかを確かめる道具(2026-09-13)。
   ブラウザを2枚開いて、片方が倒した物がもう片方に届くか・スクラップが共有されるか・
   抜けても壊れないかを、数字で確かめる。写真も撮る。

   使い方: node tools/two.mjs [--url https://...] [--local] */
let webkit;
try { ({ webkit } = await import('playwright')); }
catch (e) { ({ webkit } = await import('file:///C:/平成ゾンビ学園/node_modules/playwright/index.mjs')); }
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const argv = process.argv.slice(2);
const opt = (n,d)=>{ const i=argv.indexOf('--'+n); return i>=0?argv[i+1]:d; };
const URL_ = opt('url', 'https://ishi-to-zombie.kaiten-sushi-online.workers.dev/');

const wait = ms => new Promise(r=>setTimeout(r,ms));
const browser = await webkit.launch();
async function openOne(name){
  const ctx = await browser.newContext({ viewport:{width:390,height:797} });
  const page = await ctx.newPage();
  const errs = [];
  page.on('pageerror', e=>errs.push(name+': '+String(e.message).slice(0,140)));
  await page.goto(URL_, { waitUntil:'load' });
  await page.waitForTimeout(900);
  return { name, ctx, page, errs };
}
const st = p => p.page.evaluate(()=>window.ZG.state());
const line = (tag,s)=> tag+'  通信='+s.net+' 部屋='+(s.room||'—')+' 人数='+s.players+
  ' ゾンビ='+s.zombies+' 倒した='+s.kills+' スクラップ='+s.base+' HP='+s.hp;

console.log('相手: '+URL_+'\n');
const A = await openOne('A');
await A.page.evaluate(()=>window.ZG.start());
await wait(2500);
let sa = await st(A);
console.log(line('A ひとりで開始 ', sa));
if (sa.net !== 'online'){ console.log('  ← つながっていない。ここで止める'); await browser.close(); process.exit(1); }

const B = await openOne('B');
await B.page.evaluate(()=>window.ZG.start());
await wait(2800);
sa = await st(A); let sb = await st(B);
console.log(line('B が途中参加  ', sb));
console.log(line('  その時のA   ', sa));
const two = (sa.players===2 && sb.players===2);
console.log(two ? '  → 2人になった(モード選択なしで合流できている)'
                : '  ← 2人になっていない');

/* 同じ部屋か・ゾンビが揃っているか */
console.log('\n同じ部屋: ' + (sa.room===sb.room ? 'はい ('+sa.room+')' : 'いいえ ('+sa.room+' / '+sb.room+')'));
const zz = await Promise.all([A,B].map(p=>p.page.evaluate(()=>window.ZG.zs().map(z=>+z.m.toFixed(2)))));
console.log('Aから見たゾンビの位置: '+JSON.stringify(zz[0]));
console.log('Bから見たゾンビの位置: '+JSON.stringify(zz[1]));

/* ★スクラップの共有: Aが倒した分がBにも入るか(K1「キルの取り合いを発生させない」) */
const b0 = { a:sa.base, b:sb.base, k:sa.kills };
await A.page.evaluate(()=>window.ZG.hitNearest('kill'));
await wait(1400);
sa = await st(A); sb = await st(B);
console.log('\nAが1体倒した(石は同じ道を通してサーバーへ出している)');
console.log(line('  A ', sa));
console.log(line('  B ', sb));
console.log((sa.kills>b0.k && sa.base>b0.a && sb.base>b0.b)
  ? '  → 倒した数もスクラップも**2人に同じだけ**入った(取り合いにならない)'
  : '  ← 共有できていない');

/* Bが倒した分もAへ入るか */
const b1 = { a:sa.base, b:sb.base };
await B.page.evaluate(()=>window.ZG.hitNearest('kill'));
await wait(1400);
sa = await st(A); sb = await st(B);
console.log('こんどはBが1体倒した → A '+b1.a+'→'+sa.base+' / B '+b1.b+'→'+sb.base);

/* 横から当てるとノックバックだけ(芯が2人でも効いているか) */
const zb = (await A.page.evaluate(()=>window.ZG.zs()))[0];
if (zb){
  await A.page.evaluate(()=>window.ZG.hitNearest('knock'));
  await wait(1200);
  const za = (await A.page.evaluate(()=>window.ZG.zs()))[0];
  console.log('横から当てた: ゾンビの位置 '+zb.m.toFixed(2)+'m → '+(za?za.m.toFixed(2)+'m':'—')+
    (za && za.m>zb.m ? '  → 押し戻されただけ(倒れていない)' : '  ← おかしい'));
}

/* 交代 */
const sw = await A.page.evaluate(()=>window.ZG.swap());
await wait(1000);
const pa = sa.px, pb = sb.px;
sa = await st(A); sb = await st(B);
console.log('\n前後交代をAが押した: '+(sw?'受け付けた':'今は押せない'));
console.log('  Aの立ち位置 '+Math.round(pa)+'px → '+Math.round(sa.px)+'px / '+
            'Bの立ち位置 '+Math.round(pb)+'px → '+Math.round(sb.px)+'px');
console.log((sa.px!==pa && sb.px!==pb) ? '  → 2人とも前後が入れ替わった' : '  ← 入れ替わっていない');

/* 写真 */
fs.mkdirSync(path.join(ROOT,'tmp'),{recursive:true});
await A.page.screenshot({ path: path.join(ROOT,'tmp/two_A.png') });
await B.page.screenshot({ path: path.join(ROOT,'tmp/two_B.png') });
console.log('\n写真: tmp/two_A.png / tmp/two_B.png');

/* Bが抜ける → Aは壊れずに続くか */
await B.ctx.close();
await wait(2500);
sa = await st(A);
console.log('\nBが抜けたあと');
console.log(line('  A ', sa));
console.log(sa.players===1 && sa.net==='online' ? '  → ひとりに戻って続いている(壊れていない)'
                                                : '  ← おかしい');
await A.page.screenshot({ path: path.join(ROOT,'tmp/two_A_alone.png') });

/* Aも抜ける → 部屋が使い回されないか(相手探しから外れたか) */
const room = sa.room;
await A.ctx.close();
await wait(1500);
const C = await openOne('C');
await C.page.evaluate(()=>window.ZG.start());
await wait(2500);
const sc = await st(C);
console.log('\n全員抜けたあとに新しい人が来た');
console.log(line('  C ', sc));
console.log(sc.room!==room ? '  → 前の部屋は配られない(畳まれる部屋へ案内しない)'
                           : '  ← 前の部屋がまた配られた');
await C.ctx.close();

/* ★誰も繋がっていない部屋が回り続けていないか(2026-09-13にぷっちーずで実際に起きた穴) */
await wait(4000);
try{
  const r = await fetch(URL_.replace(/\/$/,'')+'/api/room?r='+room);
  const j = await r.json();
  console.log('\n誰もいない部屋のようす: 人数='+j.n+' / 時計='+(j.tick?'まだ回っている':'止まっている'));
  console.log((!j.n && !j.tick) ? '  → 空の部屋は1コマも進めていない(3分後に畳まれる)'
                                : '  ← 空なのに回っている。直すこと');
}catch(e){ console.log('\n部屋のようすを見られなかった: '+e.message); }

/* DoSガードの門がいくつまで来ているか */
try{
  const m = await (await fetch(URL_.replace(/\/$/,'')+'/api/meter')).json();
  console.log('DoSガード(盾2): '+m['今いくつ']+' / 上限 '+m['上限']);
}catch(e){}

const errs = [...A.errs, ...B.errs, ...C.errs];
console.log('\nJSエラー: '+(errs.length?errs.join(' / '):'なし'));
await browser.close();
process.exit(0);

----- END ORIGINAL CONTENT -----
===== END FILE 20: tools/two.mjs =====

===== BEGIN FILE 21: public/lib/matter.min.js =====
Source-Path: C:\石とゾンビ\public\lib\matter.min.js
Source-Bytes: 83476
Source-Lines: 6
Source-LastWriteTime-JST: 2026-09-13T11:13:51+09:00
Source-SHA256: 72d30be0f579eb02ce1e0b6f9d359a4f392e6837e5a26ba8be5dbee7f88e24ae
Role: 実際に配置されているMatter.js 0.20.0配布物の全文(MIT表記も保持)
----- BEGIN ORIGINAL CONTENT -----
/*!
 * matter-js 0.20.0 by @liabru
 * http://brm.io/matter-js/
 * License MIT
 */
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Matter",[],t):"object"==typeof exports?exports.Matter=t():e.Matter=t()}(this,(function(){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=20)}([function(e,t){var n={};e.exports=n,function(){n._baseDelta=1e3/60,n._nextId=0,n._seed=0,n._nowStartTime=+new Date,n._warnedOnce={},n._decomp=null,n.extend=function(e,t){var o,i;"boolean"==typeof t?(o=2,i=t):(o=1,i=!0);for(var r=o;r<arguments.length;r++){var a=arguments[r];if(a)for(var s in a)i&&a[s]&&a[s].constructor===Object?e[s]&&e[s].constructor!==Object?e[s]=a[s]:(e[s]=e[s]||{},n.extend(e[s],i,a[s])):e[s]=a[s]}return e},n.clone=function(e,t){return n.extend({},t,e)},n.keys=function(e){if(Object.keys)return Object.keys(e);var t=[];for(var n in e)t.push(n);return t},n.values=function(e){var t=[];if(Object.keys){for(var n=Object.keys(e),o=0;o<n.length;o++)t.push(e[n[o]]);return t}for(var i in e)t.push(e[i]);return t},n.get=function(e,t,n,o){t=t.split(".").slice(n,o);for(var i=0;i<t.length;i+=1)e=e[t[i]];return e},n.set=function(e,t,o,i,r){var a=t.split(".").slice(i,r);return n.get(e,t,0,-1)[a[a.length-1]]=o,o},n.shuffle=function(e){for(var t=e.length-1;t>0;t--){var o=Math.floor(n.random()*(t+1)),i=e[t];e[t]=e[o],e[o]=i}return e},n.choose=function(e){return e[Math.floor(n.random()*e.length)]},n.isElement=function(e){return"undefined"!=typeof HTMLElement?e instanceof HTMLElement:!!(e&&e.nodeType&&e.nodeName)},n.isArray=function(e){return"[object Array]"===Object.prototype.toString.call(e)},n.isFunction=function(e){return"function"==typeof e},n.isPlainObject=function(e){return"object"==typeof e&&e.constructor===Object},n.isString=function(e){return"[object String]"===toString.call(e)},n.clamp=function(e,t,n){return e<t?t:e>n?n:e},n.sign=function(e){return e<0?-1:1},n.now=function(){if("undefined"!=typeof window&&window.performance){if(window.performance.now)return window.performance.now();if(window.performance.webkitNow)return window.performance.webkitNow()}return Date.now?Date.now():new Date-n._nowStartTime},n.random=function(t,n){return n=void 0!==n?n:1,(t=void 0!==t?t:0)+e()*(n-t)};var e=function(){return n._seed=(9301*n._seed+49297)%233280,n._seed/233280};n.colorToNumber=function(e){return 3==(e=e.replace("#","")).length&&(e=e.charAt(0)+e.charAt(0)+e.charAt(1)+e.charAt(1)+e.charAt(2)+e.charAt(2)),parseInt(e,16)},n.logLevel=1,n.log=function(){console&&n.logLevel>0&&n.logLevel<=3&&console.log.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.info=function(){console&&n.logLevel>0&&n.logLevel<=2&&console.info.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.warn=function(){console&&n.logLevel>0&&n.logLevel<=3&&console.warn.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.warnOnce=function(){var e=Array.prototype.slice.call(arguments).join(" ");n._warnedOnce[e]||(n.warn(e),n._warnedOnce[e]=!0)},n.deprecated=function(e,t,o){e[t]=n.chain((function(){n.warnOnce("🔅 deprecated 🔅",o)}),e[t])},n.nextId=function(){return n._nextId++},n.indexOf=function(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0;n<e.length;n++)if(e[n]===t)return n;return-1},n.map=function(e,t){if(e.map)return e.map(t);for(var n=[],o=0;o<e.length;o+=1)n.push(t(e[o]));return n},n.topologicalSort=function(e){var t=[],o=[],i=[];for(var r in e)o[r]||i[r]||n._topologicalSort(r,o,i,e,t);return t},n._topologicalSort=function(e,t,o,i,r){var a=i[e]||[];o[e]=!0;for(var s=0;s<a.length;s+=1){var l=a[s];o[l]||(t[l]||n._topologicalSort(l,t,o,i,r))}o[e]=!1,t[e]=!0,r.push(e)},n.chain=function(){for(var e=[],t=0;t<arguments.length;t+=1){var n=arguments[t];n._chained?e.push.apply(e,n._chained):e.push(n)}var o=function(){for(var t,n=new Array(arguments.length),o=0,i=arguments.length;o<i;o++)n[o]=arguments[o];for(o=0;o<e.length;o+=1){var r=e[o].apply(t,n);void 0!==r&&(t=r)}return t};return o._chained=e,o},n.chainPathBefore=function(e,t,o){return n.set(e,t,n.chain(o,n.get(e,t)))},n.chainPathAfter=function(e,t,o){return n.set(e,t,n.chain(n.get(e,t),o))},n.setDecomp=function(e){n._decomp=e},n.getDecomp=function(){var e=n._decomp;try{e||"undefined"==typeof window||(e=window.decomp),e||"undefined"==typeof global||(e=global.decomp)}catch(t){e=null}return e}}()},function(e,t){var n={};e.exports=n,n.create=function(e){var t={min:{x:0,y:0},max:{x:0,y:0}};return e&&n.update(t,e),t},n.update=function(e,t,n){e.min.x=1/0,e.max.x=-1/0,e.min.y=1/0,e.max.y=-1/0;for(var o=0;o<t.length;o++){var i=t[o];i.x>e.max.x&&(e.max.x=i.x),i.x<e.min.x&&(e.min.x=i.x),i.y>e.max.y&&(e.max.y=i.y),i.y<e.min.y&&(e.min.y=i.y)}n&&(n.x>0?e.max.x+=n.x:e.min.x+=n.x,n.y>0?e.max.y+=n.y:e.min.y+=n.y)},n.contains=function(e,t){return t.x>=e.min.x&&t.x<=e.max.x&&t.y>=e.min.y&&t.y<=e.max.y},n.overlaps=function(e,t){return e.min.x<=t.max.x&&e.max.x>=t.min.x&&e.max.y>=t.min.y&&e.min.y<=t.max.y},n.translate=function(e,t){e.min.x+=t.x,e.max.x+=t.x,e.min.y+=t.y,e.max.y+=t.y},n.shift=function(e,t){var n=e.max.x-e.min.x,o=e.max.y-e.min.y;e.min.x=t.x,e.max.x=t.x+n,e.min.y=t.y,e.max.y=t.y+o}},function(e,t){var n={};e.exports=n,n.create=function(e,t){return{x:e||0,y:t||0}},n.clone=function(e){return{x:e.x,y:e.y}},n.magnitude=function(e){return Math.sqrt(e.x*e.x+e.y*e.y)},n.magnitudeSquared=function(e){return e.x*e.x+e.y*e.y},n.rotate=function(e,t,n){var o=Math.cos(t),i=Math.sin(t);n||(n={});var r=e.x*o-e.y*i;return n.y=e.x*i+e.y*o,n.x=r,n},n.rotateAbout=function(e,t,n,o){var i=Math.cos(t),r=Math.sin(t);o||(o={});var a=n.x+((e.x-n.x)*i-(e.y-n.y)*r);return o.y=n.y+((e.x-n.x)*r+(e.y-n.y)*i),o.x=a,o},n.normalise=function(e){var t=n.magnitude(e);return 0===t?{x:0,y:0}:{x:e.x/t,y:e.y/t}},n.dot=function(e,t){return e.x*t.x+e.y*t.y},n.cross=function(e,t){return e.x*t.y-e.y*t.x},n.cross3=function(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)},n.add=function(e,t,n){return n||(n={}),n.x=e.x+t.x,n.y=e.y+t.y,n},n.sub=function(e,t,n){return n||(n={}),n.x=e.x-t.x,n.y=e.y-t.y,n},n.mult=function(e,t){return{x:e.x*t,y:e.y*t}},n.div=function(e,t){return{x:e.x/t,y:e.y/t}},n.perp=function(e,t){return{x:(t=!0===t?-1:1)*-e.y,y:t*e.x}},n.neg=function(e){return{x:-e.x,y:-e.y}},n.angle=function(e,t){return Math.atan2(t.y-e.y,t.x-e.x)},n._temp=[n.create(),n.create(),n.create(),n.create(),n.create(),n.create()]},function(e,t,n){var o={};e.exports=o;var i=n(2),r=n(0);o.create=function(e,t){for(var n=[],o=0;o<e.length;o++){var i=e[o],r={x:i.x,y:i.y,index:o,body:t,isInternal:!1};n.push(r)}return n},o.fromPath=function(e,t){var n=[];return e.replace(/L?\s*([-\d.e]+)[\s,]*([-\d.e]+)*/gi,(function(e,t,o){n.push({x:parseFloat(t),y:parseFloat(o)})})),o.create(n,t)},o.centre=function(e){for(var t,n,r,a=o.area(e,!0),s={x:0,y:0},l=0;l<e.length;l++)r=(l+1)%e.length,t=i.cross(e[l],e[r]),n=i.mult(i.add(e[l],e[r]),t),s=i.add(s,n);return i.div(s,6*a)},o.mean=function(e){for(var t={x:0,y:0},n=0;n<e.length;n++)t.x+=e[n].x,t.y+=e[n].y;return i.div(t,e.length)},o.area=function(e,t){for(var n=0,o=e.length-1,i=0;i<e.length;i++)n+=(e[o].x-e[i].x)*(e[o].y+e[i].y),o=i;return t?n/2:Math.abs(n)/2},o.inertia=function(e,t){for(var n,o,r=0,a=0,s=e,l=0;l<s.length;l++)o=(l+1)%s.length,r+=(n=Math.abs(i.cross(s[o],s[l])))*(i.dot(s[o],s[o])+i.dot(s[o],s[l])+i.dot(s[l],s[l])),a+=n;return t/6*(r/a)},o.translate=function(e,t,n){n=void 0!==n?n:1;var o,i=e.length,r=t.x*n,a=t.y*n;for(o=0;o<i;o++)e[o].x+=r,e[o].y+=a;return e},o.rotate=function(e,t,n){if(0!==t){var o,i,r,a,s=Math.cos(t),l=Math.sin(t),c=n.x,u=n.y,d=e.length;for(a=0;a<d;a++)i=(o=e[a]).x-c,r=o.y-u,o.x=c+(i*s-r*l),o.y=u+(i*l+r*s);return e}},o.contains=function(e,t){for(var n,o=t.x,i=t.y,r=e.length,a=e[r-1],s=0;s<r;s++){if(n=e[s],(o-a.x)*(n.y-a.y)+(i-a.y)*(a.x-n.x)>0)return!1;a=n}return!0},o.scale=function(e,t,n,r){if(1===t&&1===n)return e;var a,s;r=r||o.centre(e);for(var l=0;l<e.length;l++)a=e[l],s=i.sub(a,r),e[l].x=r.x+s.x*t,e[l].y=r.y+s.y*n;return e},o.chamfer=function(e,t,n,o,a){t="number"==typeof t?[t]:t||[8],n=void 0!==n?n:-1,o=o||2,a=a||14;for(var s=[],l=0;l<e.length;l++){var c=e[l-1>=0?l-1:e.length-1],u=e[l],d=e[(l+1)%e.length],p=t[l<t.length?l:t.length-1];if(0!==p){var f=i.normalise({x:u.y-c.y,y:c.x-u.x}),v=i.normalise({x:d.y-u.y,y:u.x-d.x}),m=Math.sqrt(2*Math.pow(p,2)),y=i.mult(r.clone(f),p),g=i.normalise(i.mult(i.add(f,v),.5)),x=i.sub(u,i.mult(g,m)),h=n;-1===n&&(h=1.75*Math.pow(p,.32)),(h=r.clamp(h,o,a))%2==1&&(h+=1);for(var b=Math.acos(i.dot(f,v))/h,S=0;S<h;S++)s.push(i.add(i.rotate(y,b*S),x))}else s.push(u)}return s},o.clockwiseSort=function(e){var t=o.mean(e);return e.sort((function(e,n){return i.angle(t,e)-i.angle(t,n)})),e},o.isConvex=function(e){var t,n,o,i,r=0,a=e.length;if(a<3)return null;for(t=0;t<a;t++)if(o=(t+2)%a,i=(e[n=(t+1)%a].x-e[t].x)*(e[o].y-e[n].y),(i-=(e[n].y-e[t].y)*(e[o].x-e[n].x))<0?r|=1:i>0&&(r|=2),3===r)return!1;return 0!==r||null},o.hull=function(e){var t,n,o=[],r=[];for((e=e.slice(0)).sort((function(e,t){var n=e.x-t.x;return 0!==n?n:e.y-t.y})),n=0;n<e.length;n+=1){for(t=e[n];r.length>=2&&i.cross3(r[r.length-2],r[r.length-1],t)<=0;)r.pop();r.push(t)}for(n=e.length-1;n>=0;n-=1){for(t=e[n];o.length>=2&&i.cross3(o[o.length-2],o[o.length-1],t)<=0;)o.pop();o.push(t)}return o.pop(),r.pop(),o.concat(r)}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(2),a=n(7),s=n(0),l=n(1),c=n(11);!function(){o._timeCorrection=!0,o._inertiaScale=4,o._nextCollidingGroupId=1,o._nextNonCollidingGroupId=-1,o._nextCategory=1,o._baseDelta=1e3/60,o.create=function(t){var n={id:s.nextId(),type:"body",label:"Body",parts:[],plugin:{},angle:0,vertices:i.fromPath("L 0 0 L 40 0 L 40 40 L 0 40"),position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},totalContacts:0,speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isSensor:!1,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionStatic:.5,frictionAir:.01,collisionFilter:{category:1,mask:4294967295,group:0},slop:.05,timeScale:1,render:{visible:!0,opacity:1,strokeStyle:null,fillStyle:null,lineWidth:null,sprite:{xScale:1,yScale:1,xOffset:0,yOffset:0}},events:null,bounds:null,chamfer:null,circleRadius:0,positionPrev:null,anglePrev:0,parent:null,axes:null,area:0,mass:0,inertia:0,deltaTime:1e3/60,_original:null},o=s.extend(n,t);return e(o,t),o},o.nextGroup=function(e){return e?o._nextNonCollidingGroupId--:o._nextCollidingGroupId++},o.nextCategory=function(){return o._nextCategory=o._nextCategory<<1,o._nextCategory};var e=function(e,t){t=t||{},o.set(e,{bounds:e.bounds||l.create(e.vertices),positionPrev:e.positionPrev||r.clone(e.position),anglePrev:e.anglePrev||e.angle,vertices:e.vertices,parts:e.parts||[e],isStatic:e.isStatic,isSleeping:e.isSleeping,parent:e.parent||e}),i.rotate(e.vertices,e.angle,e.position),c.rotate(e.axes,e.angle),l.update(e.bounds,e.vertices,e.velocity),o.set(e,{axes:t.axes||e.axes,area:t.area||e.area,mass:t.mass||e.mass,inertia:t.inertia||e.inertia});var n=e.isStatic?"#14151f":s.choose(["#f19648","#f5d259","#f55a3c","#063e7b","#ececd1"]),a=e.isStatic?"#555":"#ccc",u=e.isStatic&&null===e.render.fillStyle?1:0;e.render.fillStyle=e.render.fillStyle||n,e.render.strokeStyle=e.render.strokeStyle||a,e.render.lineWidth=e.render.lineWidth||u,e.render.sprite.xOffset+=-(e.bounds.min.x-e.position.x)/(e.bounds.max.x-e.bounds.min.x),e.render.sprite.yOffset+=-(e.bounds.min.y-e.position.y)/(e.bounds.max.y-e.bounds.min.y)};o.set=function(e,t,n){var i;for(i in"string"==typeof t&&(i=t,(t={})[i]=n),t)if(Object.prototype.hasOwnProperty.call(t,i))switch(n=t[i],i){case"isStatic":o.setStatic(e,n);break;case"isSleeping":a.set(e,n);break;case"mass":o.setMass(e,n);break;case"density":o.setDensity(e,n);break;case"inertia":o.setInertia(e,n);break;case"vertices":o.setVertices(e,n);break;case"position":o.setPosition(e,n);break;case"angle":o.setAngle(e,n);break;case"velocity":o.setVelocity(e,n);break;case"angularVelocity":o.setAngularVelocity(e,n);break;case"speed":o.setSpeed(e,n);break;case"angularSpeed":o.setAngularSpeed(e,n);break;case"parts":o.setParts(e,n);break;case"centre":o.setCentre(e,n);break;default:e[i]=n}},o.setStatic=function(e,t){for(var n=0;n<e.parts.length;n++){var o=e.parts[n];t?(o.isStatic||(o._original={restitution:o.restitution,friction:o.friction,mass:o.mass,inertia:o.inertia,density:o.density,inverseMass:o.inverseMass,inverseInertia:o.inverseInertia}),o.restitution=0,o.friction=1,o.mass=o.inertia=o.density=1/0,o.inverseMass=o.inverseInertia=0,o.positionPrev.x=o.position.x,o.positionPrev.y=o.position.y,o.anglePrev=o.angle,o.angularVelocity=0,o.speed=0,o.angularSpeed=0,o.motion=0):o._original&&(o.restitution=o._original.restitution,o.friction=o._original.friction,o.mass=o._original.mass,o.inertia=o._original.inertia,o.density=o._original.density,o.inverseMass=o._original.inverseMass,o.inverseInertia=o._original.inverseInertia,o._original=null),o.isStatic=t}},o.setMass=function(e,t){var n=e.inertia/(e.mass/6);e.inertia=n*(t/6),e.inverseInertia=1/e.inertia,e.mass=t,e.inverseMass=1/e.mass,e.density=e.mass/e.area},o.setDensity=function(e,t){o.setMass(e,t*e.area),e.density=t},o.setInertia=function(e,t){e.inertia=t,e.inverseInertia=1/e.inertia},o.setVertices=function(e,t){t[0].body===e?e.vertices=t:e.vertices=i.create(t,e),e.axes=c.fromVertices(e.vertices),e.area=i.area(e.vertices),o.setMass(e,e.density*e.area);var n=i.centre(e.vertices);i.translate(e.vertices,n,-1),o.setInertia(e,o._inertiaScale*i.inertia(e.vertices,e.mass)),i.translate(e.vertices,e.position),l.update(e.bounds,e.vertices,e.velocity)},o.setParts=function(e,t,n){var r;for(t=t.slice(0),e.parts.length=0,e.parts.push(e),e.parent=e,r=0;r<t.length;r++){var a=t[r];a!==e&&(a.parent=e,e.parts.push(a))}if(1!==e.parts.length){if(n=void 0===n||n){var s=[];for(r=0;r<t.length;r++)s=s.concat(t[r].vertices);i.clockwiseSort(s);var l=i.hull(s),c=i.centre(l);o.setVertices(e,l),i.translate(e.vertices,c)}var u=o._totalProperties(e);e.area=u.area,e.parent=e,e.position.x=u.centre.x,e.position.y=u.centre.y,e.positionPrev.x=u.centre.x,e.positionPrev.y=u.centre.y,o.setMass(e,u.mass),o.setInertia(e,u.inertia),o.setPosition(e,u.centre)}},o.setCentre=function(e,t,n){n?(e.positionPrev.x+=t.x,e.positionPrev.y+=t.y,e.position.x+=t.x,e.position.y+=t.y):(e.positionPrev.x=t.x-(e.position.x-e.positionPrev.x),e.positionPrev.y=t.y-(e.position.y-e.positionPrev.y),e.position.x=t.x,e.position.y=t.y)},o.setPosition=function(e,t,n){var o=r.sub(t,e.position);n?(e.positionPrev.x=e.position.x,e.positionPrev.y=e.position.y,e.velocity.x=o.x,e.velocity.y=o.y,e.speed=r.magnitude(o)):(e.positionPrev.x+=o.x,e.positionPrev.y+=o.y);for(var a=0;a<e.parts.length;a++){var s=e.parts[a];s.position.x+=o.x,s.position.y+=o.y,i.translate(s.vertices,o),l.update(s.bounds,s.vertices,e.velocity)}},o.setAngle=function(e,t,n){var o=t-e.angle;n?(e.anglePrev=e.angle,e.angularVelocity=o,e.angularSpeed=Math.abs(o)):e.anglePrev+=o;for(var a=0;a<e.parts.length;a++){var s=e.parts[a];s.angle+=o,i.rotate(s.vertices,o,e.position),c.rotate(s.axes,o),l.update(s.bounds,s.vertices,e.velocity),a>0&&r.rotateAbout(s.position,o,e.position,s.position)}},o.setVelocity=function(e,t){var n=e.deltaTime/o._baseDelta;e.positionPrev.x=e.position.x-t.x*n,e.positionPrev.y=e.position.y-t.y*n,e.velocity.x=(e.position.x-e.positionPrev.x)/n,e.velocity.y=(e.position.y-e.positionPrev.y)/n,e.speed=r.magnitude(e.velocity)},o.getVelocity=function(e){var t=o._baseDelta/e.deltaTime;return{x:(e.position.x-e.positionPrev.x)*t,y:(e.position.y-e.positionPrev.y)*t}},o.getSpeed=function(e){return r.magnitude(o.getVelocity(e))},o.setSpeed=function(e,t){o.setVelocity(e,r.mult(r.normalise(o.getVelocity(e)),t))},o.setAngularVelocity=function(e,t){var n=e.deltaTime/o._baseDelta;e.anglePrev=e.angle-t*n,e.angularVelocity=(e.angle-e.anglePrev)/n,e.angularSpeed=Math.abs(e.angularVelocity)},o.getAngularVelocity=function(e){return(e.angle-e.anglePrev)*o._baseDelta/e.deltaTime},o.getAngularSpeed=function(e){return Math.abs(o.getAngularVelocity(e))},o.setAngularSpeed=function(e,t){o.setAngularVelocity(e,s.sign(o.getAngularVelocity(e))*t)},o.translate=function(e,t,n){o.setPosition(e,r.add(e.position,t),n)},o.rotate=function(e,t,n,i){if(n){var r=Math.cos(t),a=Math.sin(t),s=e.position.x-n.x,l=e.position.y-n.y;o.setPosition(e,{x:n.x+(s*r-l*a),y:n.y+(s*a+l*r)},i),o.setAngle(e,e.angle+t,i)}else o.setAngle(e,e.angle+t,i)},o.scale=function(e,t,n,r){var a=0,s=0;r=r||e.position;for(var u=0;u<e.parts.length;u++){var d=e.parts[u];i.scale(d.vertices,t,n,r),d.axes=c.fromVertices(d.vertices),d.area=i.area(d.vertices),o.setMass(d,e.density*d.area),i.translate(d.vertices,{x:-d.position.x,y:-d.position.y}),o.setInertia(d,o._inertiaScale*i.inertia(d.vertices,d.mass)),i.translate(d.vertices,{x:d.position.x,y:d.position.y}),u>0&&(a+=d.area,s+=d.inertia),d.position.x=r.x+(d.position.x-r.x)*t,d.position.y=r.y+(d.position.y-r.y)*n,l.update(d.bounds,d.vertices,e.velocity)}e.parts.length>1&&(e.area=a,e.isStatic||(o.setMass(e,e.density*a),o.setInertia(e,s))),e.circleRadius&&(t===n?e.circleRadius*=t:e.circleRadius=null)},o.update=function(e,t){var n=(t=(void 0!==t?t:1e3/60)*e.timeScale)*t,a=o._timeCorrection?t/(e.deltaTime||t):1,u=1-e.frictionAir*(t/s._baseDelta),d=(e.position.x-e.positionPrev.x)*a,p=(e.position.y-e.positionPrev.y)*a;e.velocity.x=d*u+e.force.x/e.mass*n,e.velocity.y=p*u+e.force.y/e.mass*n,e.positionPrev.x=e.position.x,e.positionPrev.y=e.position.y,e.position.x+=e.velocity.x,e.position.y+=e.velocity.y,e.deltaTime=t,e.angularVelocity=(e.angle-e.anglePrev)*u*a+e.torque/e.inertia*n,e.anglePrev=e.angle,e.angle+=e.angularVelocity;for(var f=0;f<e.parts.length;f++){var v=e.parts[f];i.translate(v.vertices,e.velocity),f>0&&(v.position.x+=e.velocity.x,v.position.y+=e.velocity.y),0!==e.angularVelocity&&(i.rotate(v.vertices,e.angularVelocity,e.position),c.rotate(v.axes,e.angularVelocity),f>0&&r.rotateAbout(v.position,e.angularVelocity,e.position,v.position)),l.update(v.bounds,v.vertices,e.velocity)}},o.updateVelocities=function(e){var t=o._baseDelta/e.deltaTime,n=e.velocity;n.x=(e.position.x-e.positionPrev.x)*t,n.y=(e.position.y-e.positionPrev.y)*t,e.speed=Math.sqrt(n.x*n.x+n.y*n.y),e.angularVelocity=(e.angle-e.anglePrev)*t,e.angularSpeed=Math.abs(e.angularVelocity)},o.applyForce=function(e,t,n){var o=t.x-e.position.x,i=t.y-e.position.y;e.force.x+=n.x,e.force.y+=n.y,e.torque+=o*n.y-i*n.x},o._totalProperties=function(e){for(var t={mass:0,area:0,inertia:0,centre:{x:0,y:0}},n=1===e.parts.length?0:1;n<e.parts.length;n++){var o=e.parts[n],i=o.mass!==1/0?o.mass:1;t.mass+=i,t.area+=o.area,t.inertia+=o.inertia,t.centre=r.add(t.centre,r.mult(o.position,i))}return t.centre=r.div(t.centre,t.mass),t}}()},function(e,t,n){var o={};e.exports=o;var i=n(0);o.on=function(e,t,n){for(var o,i=t.split(" "),r=0;r<i.length;r++)o=i[r],e.events=e.events||{},e.events[o]=e.events[o]||[],e.events[o].push(n);return n},o.off=function(e,t,n){if(t){"function"==typeof t&&(n=t,t=i.keys(e.events).join(" "));for(var o=t.split(" "),r=0;r<o.length;r++){var a=e.events[o[r]],s=[];if(n&&a)for(var l=0;l<a.length;l++)a[l]!==n&&s.push(a[l]);e.events[o[r]]=s}}else e.events={}},o.trigger=function(e,t,n){var o,r,a,s,l=e.events;if(l&&i.keys(l).length>0){n||(n={}),o=t.split(" ");for(var c=0;c<o.length;c++)if(a=l[r=o[c]]){(s=i.clone(n,!1)).name=r,s.source=e;for(var u=0;u<a.length;u++)a[u].apply(e,[s])}}}},function(e,t,n){var o={};e.exports=o;var i=n(5),r=n(0),a=n(1),s=n(4);o.create=function(e){return r.extend({id:r.nextId(),type:"composite",parent:null,isModified:!1,bodies:[],constraints:[],composites:[],label:"Composite",plugin:{},cache:{allBodies:null,allConstraints:null,allComposites:null}},e)},o.setModified=function(e,t,n,i){if(e.isModified=t,t&&e.cache&&(e.cache.allBodies=null,e.cache.allConstraints=null,e.cache.allComposites=null),n&&e.parent&&o.setModified(e.parent,t,n,i),i)for(var r=0;r<e.composites.length;r++){var a=e.composites[r];o.setModified(a,t,n,i)}},o.add=function(e,t){var n=[].concat(t);i.trigger(e,"beforeAdd",{object:t});for(var a=0;a<n.length;a++){var s=n[a];switch(s.type){case"body":if(s.parent!==s){r.warn("Composite.add: skipped adding a compound body part (you must add its parent instead)");break}o.addBody(e,s);break;case"constraint":o.addConstraint(e,s);break;case"composite":o.addComposite(e,s);break;case"mouseConstraint":o.addConstraint(e,s.constraint)}}return i.trigger(e,"afterAdd",{object:t}),e},o.remove=function(e,t,n){var r=[].concat(t);i.trigger(e,"beforeRemove",{object:t});for(var a=0;a<r.length;a++){var s=r[a];switch(s.type){case"body":o.removeBody(e,s,n);break;case"constraint":o.removeConstraint(e,s,n);break;case"composite":o.removeComposite(e,s,n);break;case"mouseConstraint":o.removeConstraint(e,s.constraint)}}return i.trigger(e,"afterRemove",{object:t}),e},o.addComposite=function(e,t){return e.composites.push(t),t.parent=e,o.setModified(e,!0,!0,!1),e},o.removeComposite=function(e,t,n){var i=r.indexOf(e.composites,t);if(-1!==i){var a=o.allBodies(t);o.removeCompositeAt(e,i);for(var s=0;s<a.length;s++)a[s].sleepCounter=0}if(n)for(s=0;s<e.composites.length;s++)o.removeComposite(e.composites[s],t,!0);return e},o.removeCompositeAt=function(e,t){return e.composites.splice(t,1),o.setModified(e,!0,!0,!1),e},o.addBody=function(e,t){return e.bodies.push(t),o.setModified(e,!0,!0,!1),e},o.removeBody=function(e,t,n){var i=r.indexOf(e.bodies,t);if(-1!==i&&(o.removeBodyAt(e,i),t.sleepCounter=0),n)for(var a=0;a<e.composites.length;a++)o.removeBody(e.composites[a],t,!0);return e},o.removeBodyAt=function(e,t){return e.bodies.splice(t,1),o.setModified(e,!0,!0,!1),e},o.addConstraint=function(e,t){return e.constraints.push(t),o.setModified(e,!0,!0,!1),e},o.removeConstraint=function(e,t,n){var i=r.indexOf(e.constraints,t);if(-1!==i&&o.removeConstraintAt(e,i),n)for(var a=0;a<e.composites.length;a++)o.removeConstraint(e.composites[a],t,!0);return e},o.removeConstraintAt=function(e,t){return e.constraints.splice(t,1),o.setModified(e,!0,!0,!1),e},o.clear=function(e,t,n){if(n)for(var i=0;i<e.composites.length;i++)o.clear(e.composites[i],t,!0);return t?e.bodies=e.bodies.filter((function(e){return e.isStatic})):e.bodies.length=0,e.constraints.length=0,e.composites.length=0,o.setModified(e,!0,!0,!1),e},o.allBodies=function(e){if(e.cache&&e.cache.allBodies)return e.cache.allBodies;for(var t=[].concat(e.bodies),n=0;n<e.composites.length;n++)t=t.concat(o.allBodies(e.composites[n]));return e.cache&&(e.cache.allBodies=t),t},o.allConstraints=function(e){if(e.cache&&e.cache.allConstraints)return e.cache.allConstraints;for(var t=[].concat(e.constraints),n=0;n<e.composites.length;n++)t=t.concat(o.allConstraints(e.composites[n]));return e.cache&&(e.cache.allConstraints=t),t},o.allComposites=function(e){if(e.cache&&e.cache.allComposites)return e.cache.allComposites;for(var t=[].concat(e.composites),n=0;n<e.composites.length;n++)t=t.concat(o.allComposites(e.composites[n]));return e.cache&&(e.cache.allComposites=t),t},o.get=function(e,t,n){var i,r;switch(n){case"body":i=o.allBodies(e);break;case"constraint":i=o.allConstraints(e);break;case"composite":i=o.allComposites(e).concat(e)}return i?0===(r=i.filter((function(e){return e.id.toString()===t.toString()}))).length?null:r[0]:null},o.move=function(e,t,n){return o.remove(e,t),o.add(n,t),e},o.rebase=function(e){for(var t=o.allBodies(e).concat(o.allConstraints(e)).concat(o.allComposites(e)),n=0;n<t.length;n++)t[n].id=r.nextId();return e},o.translate=function(e,t,n){for(var i=n?o.allBodies(e):e.bodies,r=0;r<i.length;r++)s.translate(i[r],t);return e},o.rotate=function(e,t,n,i){for(var r=Math.cos(t),a=Math.sin(t),l=i?o.allBodies(e):e.bodies,c=0;c<l.length;c++){var u=l[c],d=u.position.x-n.x,p=u.position.y-n.y;s.setPosition(u,{x:n.x+(d*r-p*a),y:n.y+(d*a+p*r)}),s.rotate(u,t)}return e},o.scale=function(e,t,n,i,r){for(var a=r?o.allBodies(e):e.bodies,l=0;l<a.length;l++){var c=a[l],u=c.position.x-i.x,d=c.position.y-i.y;s.setPosition(c,{x:i.x+u*t,y:i.y+d*n}),s.scale(c,t,n)}return e},o.bounds=function(e){for(var t=o.allBodies(e),n=[],i=0;i<t.length;i+=1){var r=t[i];n.push(r.bounds.min,r.bounds.max)}return a.create(n)}},function(e,t,n){var o={};e.exports=o;var i=n(4),r=n(5),a=n(0);o._motionWakeThreshold=.18,o._motionSleepThreshold=.08,o._minBias=.9,o.update=function(e,t){for(var n=t/a._baseDelta,r=o._motionSleepThreshold,s=0;s<e.length;s++){var l=e[s],c=i.getSpeed(l),u=i.getAngularSpeed(l),d=c*c+u*u;if(0===l.force.x&&0===l.force.y){var p=Math.min(l.motion,d),f=Math.max(l.motion,d);l.motion=o._minBias*p+(1-o._minBias)*f,l.sleepThreshold>0&&l.motion<r?(l.sleepCounter+=1,l.sleepCounter>=l.sleepThreshold/n&&o.set(l,!0)):l.sleepCounter>0&&(l.sleepCounter-=1)}else o.set(l,!1)}},o.afterCollisions=function(e){for(var t=o._motionSleepThreshold,n=0;n<e.length;n++){var i=e[n];if(i.isActive){var r=i.collision,a=r.bodyA.parent,s=r.bodyB.parent;if(!(a.isSleeping&&s.isSleeping||a.isStatic||s.isStatic)&&(a.isSleeping||s.isSleeping)){var l=a.isSleeping&&!a.isStatic?a:s,c=l===a?s:a;!l.isStatic&&c.motion>t&&o.set(l,!1)}}}},o.set=function(e,t){var n=e.isSleeping;t?(e.isSleeping=!0,e.sleepCounter=e.sleepThreshold,e.positionImpulse.x=0,e.positionImpulse.y=0,e.positionPrev.x=e.position.x,e.positionPrev.y=e.position.y,e.anglePrev=e.angle,e.speed=0,e.angularSpeed=0,e.motion=0,n||r.trigger(e,"sleepStart")):(e.isSleeping=!1,e.sleepCounter=0,n&&r.trigger(e,"sleepEnd"))}},function(e,t,n){var o={};e.exports=o;var i,r,a,s=n(3),l=n(9);i=[],r={overlap:0,axis:null},a={overlap:0,axis:null},o.create=function(e,t){return{pair:null,collided:!1,bodyA:e,bodyB:t,parentA:e.parent,parentB:t.parent,depth:0,normal:{x:0,y:0},tangent:{x:0,y:0},penetration:{x:0,y:0},supports:[null,null],supportCount:0}},o.collides=function(e,t,n){if(o._overlapAxes(r,e.vertices,t.vertices,e.axes),r.overlap<=0)return null;if(o._overlapAxes(a,t.vertices,e.vertices,t.axes),a.overlap<=0)return null;var i,c,u=n&&n.table[l.id(e,t)];u?i=u.collision:((i=o.create(e,t)).collided=!0,i.bodyA=e.id<t.id?e:t,i.bodyB=e.id<t.id?t:e,i.parentA=i.bodyA.parent,i.parentB=i.bodyB.parent),e=i.bodyA,t=i.bodyB,c=r.overlap<a.overlap?r:a;var d=i.normal,p=i.tangent,f=i.penetration,v=i.supports,m=c.overlap,y=c.axis,g=y.x,x=y.y;g*(t.position.x-e.position.x)+x*(t.position.y-e.position.y)>=0&&(g=-g,x=-x),d.x=g,d.y=x,p.x=-x,p.y=g,f.x=g*m,f.y=x*m,i.depth=m;var h=o._findSupports(e,t,d,1),b=0;if(s.contains(e.vertices,h[0])&&(v[b++]=h[0]),s.contains(e.vertices,h[1])&&(v[b++]=h[1]),b<2){var S=o._findSupports(t,e,d,-1);s.contains(t.vertices,S[0])&&(v[b++]=S[0]),b<2&&s.contains(t.vertices,S[1])&&(v[b++]=S[1])}return 0===b&&(v[b++]=h[0]),i.supportCount=b,i},o._overlapAxes=function(e,t,n,o){var i,r,a,s,l,c,u=t.length,d=n.length,p=t[0].x,f=t[0].y,v=n[0].x,m=n[0].y,y=o.length,g=Number.MAX_VALUE,x=0;for(l=0;l<y;l++){var h=o[l],b=h.x,S=h.y,w=p*b+f*S,A=v*b+m*S,P=w,B=A;for(c=1;c<u;c+=1)(s=t[c].x*b+t[c].y*S)>P?P=s:s<w&&(w=s);for(c=1;c<d;c+=1)(s=n[c].x*b+n[c].y*S)>B?B=s:s<A&&(A=s);if((i=(r=P-A)<(a=B-w)?r:a)<g&&(g=i,x=l,i<=0))break}e.axis=o[x],e.overlap=g},o._findSupports=function(e,t,n,o){var r,a,s,l=t.vertices,c=l.length,u=e.position.x,d=e.position.y,p=n.x*o,f=n.y*o,v=l[0],m=v,y=p*(u-m.x)+f*(d-m.y);for(s=1;s<c;s+=1)(a=p*(u-(m=l[s]).x)+f*(d-m.y))<y&&(y=a,v=m);return y=p*(u-(r=l[(c+v.index-1)%c]).x)+f*(d-r.y),p*(u-(m=l[(v.index+1)%c]).x)+f*(d-m.y)<y?(i[0]=v,i[1]=m,i):(i[0]=v,i[1]=r,i)}},function(e,t,n){var o={};e.exports=o;var i=n(16);o.create=function(e,t){var n=e.bodyA,r=e.bodyB,a={id:o.id(n,r),bodyA:n,bodyB:r,collision:e,contacts:[i.create(),i.create()],contactCount:0,separation:0,isActive:!0,isSensor:n.isSensor||r.isSensor,timeCreated:t,timeUpdated:t,inverseMass:0,friction:0,frictionStatic:0,restitution:0,slop:0};return o.update(a,e,t),a},o.update=function(e,t,n){var o=t.supports,i=t.supportCount,r=e.contacts,a=t.parentA,s=t.parentB;e.isActive=!0,e.timeUpdated=n,e.collision=t,e.separation=t.depth,e.inverseMass=a.inverseMass+s.inverseMass,e.friction=a.friction<s.friction?a.friction:s.friction,e.frictionStatic=a.frictionStatic>s.frictionStatic?a.frictionStatic:s.frictionStatic,e.restitution=a.restitution>s.restitution?a.restitution:s.restitution,e.slop=a.slop>s.slop?a.slop:s.slop,e.contactCount=i,t.pair=e;var l=o[0],c=r[0],u=o[1],d=r[1];d.vertex!==l&&c.vertex!==u||(r[1]=c,r[0]=c=d,d=r[1]),c.vertex=l,d.vertex=u},o.setActive=function(e,t,n){t?(e.isActive=!0,e.timeUpdated=n):(e.isActive=!1,e.contactCount=0)},o.id=function(e,t){return e.id<t.id?e.id.toString(36)+":"+t.id.toString(36):t.id.toString(36)+":"+e.id.toString(36)}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(2),a=n(7),s=n(1),l=n(11),c=n(0);o._warming=.4,o._torqueDampen=1,o._minLength=1e-6,o.create=function(e){var t=e;t.bodyA&&!t.pointA&&(t.pointA={x:0,y:0}),t.bodyB&&!t.pointB&&(t.pointB={x:0,y:0});var n=t.bodyA?r.add(t.bodyA.position,t.pointA):t.pointA,o=t.bodyB?r.add(t.bodyB.position,t.pointB):t.pointB,i=r.magnitude(r.sub(n,o));t.length=void 0!==t.length?t.length:i,t.id=t.id||c.nextId(),t.label=t.label||"Constraint",t.type="constraint",t.stiffness=t.stiffness||(t.length>0?1:.7),t.damping=t.damping||0,t.angularStiffness=t.angularStiffness||0,t.angleA=t.bodyA?t.bodyA.angle:t.angleA,t.angleB=t.bodyB?t.bodyB.angle:t.angleB,t.plugin={};var a={visible:!0,lineWidth:2,strokeStyle:"#ffffff",type:"line",anchors:!0};return 0===t.length&&t.stiffness>.1?(a.type="pin",a.anchors=!1):t.stiffness<.9&&(a.type="spring"),t.render=c.extend(a,t.render),t},o.preSolveAll=function(e){for(var t=0;t<e.length;t+=1){var n=e[t],o=n.constraintImpulse;n.isStatic||0===o.x&&0===o.y&&0===o.angle||(n.position.x+=o.x,n.position.y+=o.y,n.angle+=o.angle)}},o.solveAll=function(e,t){for(var n=c.clamp(t/c._baseDelta,0,1),i=0;i<e.length;i+=1){var r=e[i],a=!r.bodyA||r.bodyA&&r.bodyA.isStatic,s=!r.bodyB||r.bodyB&&r.bodyB.isStatic;(a||s)&&o.solve(e[i],n)}for(i=0;i<e.length;i+=1)a=!(r=e[i]).bodyA||r.bodyA&&r.bodyA.isStatic,s=!r.bodyB||r.bodyB&&r.bodyB.isStatic,a||s||o.solve(e[i],n)},o.solve=function(e,t){var n=e.bodyA,i=e.bodyB,a=e.pointA,s=e.pointB;if(n||i){n&&!n.isStatic&&(r.rotate(a,n.angle-e.angleA,a),e.angleA=n.angle),i&&!i.isStatic&&(r.rotate(s,i.angle-e.angleB,s),e.angleB=i.angle);var l=a,c=s;if(n&&(l=r.add(n.position,a)),i&&(c=r.add(i.position,s)),l&&c){var u=r.sub(l,c),d=r.magnitude(u);d<o._minLength&&(d=o._minLength);var p,f,v,m,y,g=(d-e.length)/d,x=e.stiffness>=1||0===e.length?e.stiffness*t:e.stiffness*t*t,h=e.damping*t,b=r.mult(u,g*x),S=(n?n.inverseMass:0)+(i?i.inverseMass:0),w=S+((n?n.inverseInertia:0)+(i?i.inverseInertia:0));if(h>0){var A=r.create();v=r.div(u,d),y=r.sub(i&&r.sub(i.position,i.positionPrev)||A,n&&r.sub(n.position,n.positionPrev)||A),m=r.dot(v,y)}n&&!n.isStatic&&(f=n.inverseMass/S,n.constraintImpulse.x-=b.x*f,n.constraintImpulse.y-=b.y*f,n.position.x-=b.x*f,n.position.y-=b.y*f,h>0&&(n.positionPrev.x-=h*v.x*m*f,n.positionPrev.y-=h*v.y*m*f),p=r.cross(a,b)/w*o._torqueDampen*n.inverseInertia*(1-e.angularStiffness),n.constraintImpulse.angle-=p,n.angle-=p),i&&!i.isStatic&&(f=i.inverseMass/S,i.constraintImpulse.x+=b.x*f,i.constraintImpulse.y+=b.y*f,i.position.x+=b.x*f,i.position.y+=b.y*f,h>0&&(i.positionPrev.x+=h*v.x*m*f,i.positionPrev.y+=h*v.y*m*f),p=r.cross(s,b)/w*o._torqueDampen*i.inverseInertia*(1-e.angularStiffness),i.constraintImpulse.angle+=p,i.angle+=p)}}},o.postSolveAll=function(e){for(var t=0;t<e.length;t++){var n=e[t],c=n.constraintImpulse;if(!(n.isStatic||0===c.x&&0===c.y&&0===c.angle)){a.set(n,!1);for(var u=0;u<n.parts.length;u++){var d=n.parts[u];i.translate(d.vertices,c),u>0&&(d.position.x+=c.x,d.position.y+=c.y),0!==c.angle&&(i.rotate(d.vertices,c.angle,n.position),l.rotate(d.axes,c.angle),u>0&&r.rotateAbout(d.position,c.angle,n.position,d.position)),s.update(d.bounds,d.vertices,n.velocity)}c.angle*=o._warming,c.x*=o._warming,c.y*=o._warming}}},o.pointAWorld=function(e){return{x:(e.bodyA?e.bodyA.position.x:0)+(e.pointA?e.pointA.x:0),y:(e.bodyA?e.bodyA.position.y:0)+(e.pointA?e.pointA.y:0)}},o.pointBWorld=function(e){return{x:(e.bodyB?e.bodyB.position.x:0)+(e.pointB?e.pointB.x:0),y:(e.bodyB?e.bodyB.position.y:0)+(e.pointB?e.pointB.y:0)}},o.currentLength=function(e){var t=(e.bodyA?e.bodyA.position.x:0)+(e.pointA?e.pointA.x:0),n=(e.bodyA?e.bodyA.position.y:0)+(e.pointA?e.pointA.y:0),o=t-((e.bodyB?e.bodyB.position.x:0)+(e.pointB?e.pointB.x:0)),i=n-((e.bodyB?e.bodyB.position.y:0)+(e.pointB?e.pointB.y:0));return Math.sqrt(o*o+i*i)}},function(e,t,n){var o={};e.exports=o;var i=n(2),r=n(0);o.fromVertices=function(e){for(var t={},n=0;n<e.length;n++){var o=(n+1)%e.length,a=i.normalise({x:e[o].y-e[n].y,y:e[n].x-e[o].x}),s=0===a.y?1/0:a.x/a.y;t[s=s.toFixed(3).toString()]=a}return r.values(t)},o.rotate=function(e,t){if(0!==t)for(var n=Math.cos(t),o=Math.sin(t),i=0;i<e.length;i++){var r,a=e[i];r=a.x*n-a.y*o,a.y=a.x*o+a.y*n,a.x=r}}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(0),a=n(4),s=n(1),l=n(2);o.rectangle=function(e,t,n,o,s){s=s||{};var l={label:"Rectangle Body",position:{x:e,y:t},vertices:i.fromPath("L 0 0 L "+n+" 0 L "+n+" "+o+" L 0 "+o)};if(s.chamfer){var c=s.chamfer;l.vertices=i.chamfer(l.vertices,c.radius,c.quality,c.qualityMin,c.qualityMax),delete s.chamfer}return a.create(r.extend({},l,s))},o.trapezoid=function(e,t,n,o,s,l){l=l||{},s>=1&&r.warn("Bodies.trapezoid: slope parameter must be < 1.");var c,u=n*(s*=.5),d=u+(1-2*s)*n,p=d+u;c=s<.5?"L 0 0 L "+u+" "+-o+" L "+d+" "+-o+" L "+p+" 0":"L 0 0 L "+d+" "+-o+" L "+p+" 0";var f={label:"Trapezoid Body",position:{x:e,y:t},vertices:i.fromPath(c)};if(l.chamfer){var v=l.chamfer;f.vertices=i.chamfer(f.vertices,v.radius,v.quality,v.qualityMin,v.qualityMax),delete l.chamfer}return a.create(r.extend({},f,l))},o.circle=function(e,t,n,i,a){i=i||{};var s={label:"Circle Body",circleRadius:n};a=a||25;var l=Math.ceil(Math.max(10,Math.min(a,n)));return l%2==1&&(l+=1),o.polygon(e,t,l,n,r.extend({},s,i))},o.polygon=function(e,t,n,s,l){if(l=l||{},n<3)return o.circle(e,t,s,l);for(var c=2*Math.PI/n,u="",d=.5*c,p=0;p<n;p+=1){var f=d+p*c,v=Math.cos(f)*s,m=Math.sin(f)*s;u+="L "+v.toFixed(3)+" "+m.toFixed(3)+" "}var y={label:"Polygon Body",position:{x:e,y:t},vertices:i.fromPath(u)};if(l.chamfer){var g=l.chamfer;y.vertices=i.chamfer(y.vertices,g.radius,g.quality,g.qualityMin,g.qualityMax),delete l.chamfer}return a.create(r.extend({},y,l))},o.fromVertices=function(e,t,n,o,c,u,d,p){var f,v,m,y,g,x,h,b,S,w,A=r.getDecomp();for(f=Boolean(A&&A.quickDecomp),o=o||{},m=[],c=void 0!==c&&c,u=void 0!==u?u:.01,d=void 0!==d?d:10,p=void 0!==p?p:.01,r.isArray(n[0])||(n=[n]),S=0;S<n.length;S+=1)if(g=n[S],!(y=i.isConvex(g))&&!f&&r.warnOnce("Bodies.fromVertices: Install the 'poly-decomp' library and use Common.setDecomp or provide 'decomp' as a global to decompose concave vertices."),y||!f)g=y?i.clockwiseSort(g):i.hull(g),m.push({position:{x:e,y:t},vertices:g});else{var P=g.map((function(e){return[e.x,e.y]}));A.makeCCW(P),!1!==u&&A.removeCollinearPoints(P,u),!1!==p&&A.removeDuplicatePoints&&A.removeDuplicatePoints(P,p);var B=A.quickDecomp(P);for(x=0;x<B.length;x++){var M=B[x].map((function(e){return{x:e[0],y:e[1]}}));d>0&&i.area(M)<d||m.push({position:i.centre(M),vertices:M})}}for(x=0;x<m.length;x++)m[x]=a.create(r.extend(m[x],o));if(c)for(x=0;x<m.length;x++){var _=m[x];for(h=x+1;h<m.length;h++){var C=m[h];if(s.overlaps(_.bounds,C.bounds)){var k=_.vertices,I=C.vertices;for(b=0;b<_.vertices.length;b++)for(w=0;w<C.vertices.length;w++){var T=l.magnitudeSquared(l.sub(k[(b+1)%k.length],I[w])),R=l.magnitudeSquared(l.sub(k[b],I[(w+1)%I.length]));T<5&&R<5&&(k[b].isInternal=!0,I[w].isInternal=!0)}}}}return m.length>1?(v=a.create(r.extend({parts:m.slice(0)},o)),a.setPosition(v,{x:e,y:t}),v):m[0]}},function(e,t,n){var o={};e.exports=o;var i=n(0),r=n(8);o.create=function(e){return i.extend({bodies:[],collisions:[],pairs:null},e)},o.setBodies=function(e,t){e.bodies=t.slice(0)},o.clear=function(e){e.bodies=[],e.collisions=[]},o.collisions=function(e){var t,n,i=e.pairs,a=e.bodies,s=a.length,l=o.canCollide,c=r.collides,u=e.collisions,d=0;for(a.sort(o._compareBoundsX),t=0;t<s;t++){var p=a[t],f=p.bounds,v=p.bounds.max.x,m=p.bounds.max.y,y=p.bounds.min.y,g=p.isStatic||p.isSleeping,x=p.parts.length,h=1===x;for(n=t+1;n<s;n++){var b=a[n];if((C=b.bounds).min.x>v)break;if(!(m<C.min.y||y>C.max.y)&&(!g||!b.isStatic&&!b.isSleeping)&&l(p.collisionFilter,b.collisionFilter)){var S=b.parts.length;if(h&&1===S)(M=c(p,b,i))&&(u[d++]=M);else for(var w=S>1?1:0,A=x>1?1:0;A<x;A++)for(var P=p.parts[A],B=(f=P.bounds,w);B<S;B++){var M,_=b.parts[B],C=_.bounds;f.min.x>C.max.x||f.max.x<C.min.x||f.max.y<C.min.y||f.min.y>C.max.y||(M=c(P,_,i))&&(u[d++]=M)}}}}return u.length!==d&&(u.length=d),u},o.canCollide=function(e,t){return e.group===t.group&&0!==e.group?e.group>0:0!=(e.mask&t.category)&&0!=(t.mask&e.category)},o._compareBoundsX=function(e,t){return e.bounds.min.x-t.bounds.min.x}},function(e,t,n){var o={};e.exports=o;var i=n(0);o.create=function(e){var t={};return e||i.log("Mouse.create: element was undefined, defaulting to document.body","warn"),t.element=e||document.body,t.absolute={x:0,y:0},t.position={x:0,y:0},t.mousedownPosition={x:0,y:0},t.mouseupPosition={x:0,y:0},t.offset={x:0,y:0},t.scale={x:1,y:1},t.wheelDelta=0,t.button=-1,t.pixelRatio=parseInt(t.element.getAttribute("data-pixel-ratio"),10)||1,t.sourceEvents={mousemove:null,mousedown:null,mouseup:null,mousewheel:null},t.mousemove=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches&&(t.button=0,e.preventDefault()),t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.sourceEvents.mousemove=e},t.mousedown=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches?(t.button=0,e.preventDefault()):t.button=e.button,t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.mousedownPosition.x=t.position.x,t.mousedownPosition.y=t.position.y,t.sourceEvents.mousedown=e},t.mouseup=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches&&e.preventDefault(),t.button=-1,t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.mouseupPosition.x=t.position.x,t.mouseupPosition.y=t.position.y,t.sourceEvents.mouseup=e},t.mousewheel=function(e){t.wheelDelta=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail)),e.preventDefault(),t.sourceEvents.mousewheel=e},o.setElement(t,t.element),t},o.setElement=function(e,t){e.element=t,t.addEventListener("mousemove",e.mousemove,{passive:!0}),t.addEventListener("mousedown",e.mousedown,{passive:!0}),t.addEventListener("mouseup",e.mouseup,{passive:!0}),t.addEventListener("wheel",e.mousewheel,{passive:!1}),t.addEventListener("touchmove",e.mousemove,{passive:!1}),t.addEventListener("touchstart",e.mousedown,{passive:!1}),t.addEventListener("touchend",e.mouseup,{passive:!1})},o.clearSourceEvents=function(e){e.sourceEvents.mousemove=null,e.sourceEvents.mousedown=null,e.sourceEvents.mouseup=null,e.sourceEvents.mousewheel=null,e.wheelDelta=0},o.setOffset=function(e,t){e.offset.x=t.x,e.offset.y=t.y,e.position.x=e.absolute.x*e.scale.x+e.offset.x,e.position.y=e.absolute.y*e.scale.y+e.offset.y},o.setScale=function(e,t){e.scale.x=t.x,e.scale.y=t.y,e.position.x=e.absolute.x*e.scale.x+e.offset.x,e.position.y=e.absolute.y*e.scale.y+e.offset.y},o._getRelativeMousePosition=function(e,t,n){var o,i,r=t.getBoundingClientRect(),a=document.documentElement||document.body.parentNode||document.body,s=void 0!==window.pageXOffset?window.pageXOffset:a.scrollLeft,l=void 0!==window.pageYOffset?window.pageYOffset:a.scrollTop,c=e.changedTouches;return c?(o=c[0].pageX-r.left-s,i=c[0].pageY-r.top-l):(o=e.pageX-r.left-s,i=e.pageY-r.top-l),{x:o/(t.clientWidth/(t.width||t.clientWidth)*n),y:i/(t.clientHeight/(t.height||t.clientHeight)*n)}}},function(e,t,n){var o={};e.exports=o;var i=n(0);o._registry={},o.register=function(e){if(o.isPlugin(e)||i.warn("Plugin.register:",o.toString(e),"does not implement all required fields."),e.name in o._registry){var t=o._registry[e.name],n=o.versionParse(e.version).number,r=o.versionParse(t.version).number;n>r?(i.warn("Plugin.register:",o.toString(t),"was upgraded to",o.toString(e)),o._registry[e.name]=e):n<r?i.warn("Plugin.register:",o.toString(t),"can not be downgraded to",o.toString(e)):e!==t&&i.warn("Plugin.register:",o.toString(e),"is already registered to different plugin object")}else o._registry[e.name]=e;return e},o.resolve=function(e){return o._registry[o.dependencyParse(e).name]},o.toString=function(e){return"string"==typeof e?e:(e.name||"anonymous")+"@"+(e.version||e.range||"0.0.0")},o.isPlugin=function(e){return e&&e.name&&e.version&&e.install},o.isUsed=function(e,t){return e.used.indexOf(t)>-1},o.isFor=function(e,t){var n=e.for&&o.dependencyParse(e.for);return!e.for||t.name===n.name&&o.versionSatisfies(t.version,n.range)},o.use=function(e,t){if(e.uses=(e.uses||[]).concat(t||[]),0!==e.uses.length){for(var n=o.dependencies(e),r=i.topologicalSort(n),a=[],s=0;s<r.length;s+=1)if(r[s]!==e.name){var l=o.resolve(r[s]);l?o.isUsed(e,l.name)||(o.isFor(l,e)||(i.warn("Plugin.use:",o.toString(l),"is for",l.for,"but installed on",o.toString(e)+"."),l._warned=!0),l.install?l.install(e):(i.warn("Plugin.use:",o.toString(l),"does not specify an install function."),l._warned=!0),l._warned?(a.push("🔶 "+o.toString(l)),delete l._warned):a.push("✅ "+o.toString(l)),e.used.push(l.name)):a.push("❌ "+r[s])}a.length>0&&i.info(a.join("  "))}else i.warn("Plugin.use:",o.toString(e),"does not specify any dependencies to install.")},o.dependencies=function(e,t){var n=o.dependencyParse(e),r=n.name;if(!(r in(t=t||{}))){e=o.resolve(e)||e,t[r]=i.map(e.uses||[],(function(t){o.isPlugin(t)&&o.register(t);var r=o.dependencyParse(t),a=o.resolve(t);return a&&!o.versionSatisfies(a.version,r.range)?(i.warn("Plugin.dependencies:",o.toString(a),"does not satisfy",o.toString(r),"used by",o.toString(n)+"."),a._warned=!0,e._warned=!0):a||(i.warn("Plugin.dependencies:",o.toString(t),"used by",o.toString(n),"could not be resolved."),e._warned=!0),r.name}));for(var a=0;a<t[r].length;a+=1)o.dependencies(t[r][a],t);return t}},o.dependencyParse=function(e){return i.isString(e)?(/^[\w-]+(@(\*|[\^~]?\d+\.\d+\.\d+(-[0-9A-Za-z-+]+)?))?$/.test(e)||i.warn("Plugin.dependencyParse:",e,"is not a valid dependency string."),{name:e.split("@")[0],range:e.split("@")[1]||"*"}):{name:e.name,range:e.range||e.version}},o.versionParse=function(e){var t=/^(\*)|(\^|~|>=|>)?\s*((\d+)\.(\d+)\.(\d+))(-[0-9A-Za-z-+]+)?$/;t.test(e)||i.warn("Plugin.versionParse:",e,"is not a valid version or range.");var n=t.exec(e),o=Number(n[4]),r=Number(n[5]),a=Number(n[6]);return{isRange:Boolean(n[1]||n[2]),version:n[3],range:e,operator:n[1]||n[2]||"",major:o,minor:r,patch:a,parts:[o,r,a],prerelease:n[7],number:1e8*o+1e4*r+a}},o.versionSatisfies=function(e,t){t=t||"*";var n=o.versionParse(t),i=o.versionParse(e);if(n.isRange){if("*"===n.operator||"*"===e)return!0;if(">"===n.operator)return i.number>n.number;if(">="===n.operator)return i.number>=n.number;if("~"===n.operator)return i.major===n.major&&i.minor===n.minor&&i.patch>=n.patch;if("^"===n.operator)return n.major>0?i.major===n.major&&i.number>=n.number:n.minor>0?i.minor===n.minor&&i.patch>=n.patch:i.patch===n.patch}return e===t||"*"===e}},function(e,t){var n={};e.exports=n,n.create=function(e){return{vertex:e,normalImpulse:0,tangentImpulse:0}}},function(e,t,n){var o={};e.exports=o;var i=n(7),r=n(18),a=n(13),s=n(19),l=n(5),c=n(6),u=n(10),d=n(0),p=n(4);o._deltaMax=1e3/60,o.create=function(e){e=e||{};var t=d.extend({positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,events:[],plugin:{},gravity:{x:0,y:1,scale:.001},timing:{timestamp:0,timeScale:1,lastDelta:0,lastElapsed:0,lastUpdatesPerFrame:0}},e);return t.world=e.world||c.create({label:"World"}),t.pairs=e.pairs||s.create(),t.detector=e.detector||a.create(),t.detector.pairs=t.pairs,t.grid={buckets:[]},t.world.gravity=t.gravity,t.broadphase=t.grid,t.metrics={},t},o.update=function(e,t){var n,p=d.now(),f=e.world,v=e.detector,m=e.pairs,y=e.timing,g=y.timestamp;t>o._deltaMax&&d.warnOnce("Matter.Engine.update: delta argument is recommended to be less than or equal to",o._deltaMax.toFixed(3),"ms."),t=void 0!==t?t:d._baseDelta,t*=y.timeScale,y.timestamp+=t,y.lastDelta=t;var x={timestamp:y.timestamp,delta:t};l.trigger(e,"beforeUpdate",x);var h=c.allBodies(f),b=c.allConstraints(f);for(f.isModified&&(a.setBodies(v,h),c.setModified(f,!1,!1,!0)),e.enableSleeping&&i.update(h,t),o._bodiesApplyGravity(h,e.gravity),t>0&&o._bodiesUpdate(h,t),l.trigger(e,"beforeSolve",x),u.preSolveAll(h),n=0;n<e.constraintIterations;n++)u.solveAll(b,t);u.postSolveAll(h);var S=a.collisions(v);s.update(m,S,g),e.enableSleeping&&i.afterCollisions(m.list),m.collisionStart.length>0&&l.trigger(e,"collisionStart",{pairs:m.collisionStart,timestamp:y.timestamp,delta:t});var w=d.clamp(20/e.positionIterations,0,1);for(r.preSolvePosition(m.list),n=0;n<e.positionIterations;n++)r.solvePosition(m.list,t,w);for(r.postSolvePosition(h),u.preSolveAll(h),n=0;n<e.constraintIterations;n++)u.solveAll(b,t);for(u.postSolveAll(h),r.preSolveVelocity(m.list),n=0;n<e.velocityIterations;n++)r.solveVelocity(m.list,t);return o._bodiesUpdateVelocities(h),m.collisionActive.length>0&&l.trigger(e,"collisionActive",{pairs:m.collisionActive,timestamp:y.timestamp,delta:t}),m.collisionEnd.length>0&&l.trigger(e,"collisionEnd",{pairs:m.collisionEnd,timestamp:y.timestamp,delta:t}),o._bodiesClearForces(h),l.trigger(e,"afterUpdate",x),e.timing.lastElapsed=d.now()-p,e},o.merge=function(e,t){if(d.extend(e,t),t.world){e.world=t.world,o.clear(e);for(var n=c.allBodies(e.world),r=0;r<n.length;r++){var a=n[r];i.set(a,!1),a.id=d.nextId()}}},o.clear=function(e){s.clear(e.pairs),a.clear(e.detector)},o._bodiesClearForces=function(e){for(var t=e.length,n=0;n<t;n++){var o=e[n];o.force.x=0,o.force.y=0,o.torque=0}},o._bodiesApplyGravity=function(e,t){var n=void 0!==t.scale?t.scale:.001,o=e.length;if((0!==t.x||0!==t.y)&&0!==n)for(var i=0;i<o;i++){var r=e[i];r.isStatic||r.isSleeping||(r.force.y+=r.mass*t.y*n,r.force.x+=r.mass*t.x*n)}},o._bodiesUpdate=function(e,t){for(var n=e.length,o=0;o<n;o++){var i=e[o];i.isStatic||i.isSleeping||p.update(i,t)}},o._bodiesUpdateVelocities=function(e){for(var t=e.length,n=0;n<t;n++)p.updateVelocities(e[n])}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(0),a=n(1);o._restingThresh=2,o._restingThreshTangent=Math.sqrt(6),o._positionDampen=.9,o._positionWarming=.8,o._frictionNormalMultiplier=5,o._frictionMaxStatic=Number.MAX_VALUE,o.preSolvePosition=function(e){var t,n,o,i=e.length;for(t=0;t<i;t++)(n=e[t]).isActive&&(o=n.contactCount,n.collision.parentA.totalContacts+=o,n.collision.parentB.totalContacts+=o)},o.solvePosition=function(e,t,n){var i,a,s,l,c,u,d,p,f=o._positionDampen*(n||1),v=r.clamp(t/r._baseDelta,0,1),m=e.length;for(i=0;i<m;i++)(a=e[i]).isActive&&!a.isSensor&&(l=(s=a.collision).parentA,c=s.parentB,u=s.normal,a.separation=s.depth+u.x*(c.positionImpulse.x-l.positionImpulse.x)+u.y*(c.positionImpulse.y-l.positionImpulse.y));for(i=0;i<m;i++)(a=e[i]).isActive&&!a.isSensor&&(l=(s=a.collision).parentA,c=s.parentB,u=s.normal,p=a.separation-a.slop*v,(l.isStatic||c.isStatic)&&(p*=2),l.isStatic||l.isSleeping||(d=f/l.totalContacts,l.positionImpulse.x+=u.x*p*d,l.positionImpulse.y+=u.y*p*d),c.isStatic||c.isSleeping||(d=f/c.totalContacts,c.positionImpulse.x-=u.x*p*d,c.positionImpulse.y-=u.y*p*d))},o.postSolvePosition=function(e){for(var t=o._positionWarming,n=e.length,r=i.translate,s=a.update,l=0;l<n;l++){var c=e[l],u=c.positionImpulse,d=u.x,p=u.y,f=c.velocity;if(c.totalContacts=0,0!==d||0!==p){for(var v=0;v<c.parts.length;v++){var m=c.parts[v];r(m.vertices,u),s(m.bounds,m.vertices,f),m.position.x+=d,m.position.y+=p}c.positionPrev.x+=d,c.positionPrev.y+=p,d*f.x+p*f.y<0?(u.x=0,u.y=0):(u.x*=t,u.y*=t)}}},o.preSolveVelocity=function(e){var t,n,o=e.length;for(t=0;t<o;t++){var i=e[t];if(i.isActive&&!i.isSensor){var r=i.contacts,a=i.contactCount,s=i.collision,l=s.parentA,c=s.parentB,u=s.normal,d=s.tangent;for(n=0;n<a;n++){var p=r[n],f=p.vertex,v=p.normalImpulse,m=p.tangentImpulse;if(0!==v||0!==m){var y=u.x*v+d.x*m,g=u.y*v+d.y*m;l.isStatic||l.isSleeping||(l.positionPrev.x+=y*l.inverseMass,l.positionPrev.y+=g*l.inverseMass,l.anglePrev+=l.inverseInertia*((f.x-l.position.x)*g-(f.y-l.position.y)*y)),c.isStatic||c.isSleeping||(c.positionPrev.x-=y*c.inverseMass,c.positionPrev.y-=g*c.inverseMass,c.anglePrev-=c.inverseInertia*((f.x-c.position.x)*g-(f.y-c.position.y)*y))}}}}},o.solveVelocity=function(e,t){var n,i,a,s,l=t/r._baseDelta,c=l*l*l,u=-o._restingThresh*l,d=o._restingThreshTangent,p=o._frictionNormalMultiplier*l,f=o._frictionMaxStatic,v=e.length;for(a=0;a<v;a++){var m=e[a];if(m.isActive&&!m.isSensor){var y=m.collision,g=y.parentA,x=y.parentB,h=y.normal.x,b=y.normal.y,S=y.tangent.x,w=y.tangent.y,A=m.inverseMass,P=m.friction*m.frictionStatic*p,B=m.contacts,M=m.contactCount,_=1/M,C=g.position.x-g.positionPrev.x,k=g.position.y-g.positionPrev.y,I=g.angle-g.anglePrev,T=x.position.x-x.positionPrev.x,R=x.position.y-x.positionPrev.y,D=x.angle-x.anglePrev;for(s=0;s<M;s++){var V=B[s],E=V.vertex,L=E.x-g.position.x,F=E.y-g.position.y,O=E.x-x.position.x,H=E.y-x.position.y,q=C-F*I-(T-H*D),j=k+L*I-(R+O*D),U=h*q+b*j,W=S*q+w*j,N=m.separation+U,G=Math.min(N,1),z=(G=N<0?0:G)*P;W<-z||W>z?(i=W>0?W:-W,(n=m.friction*(W>0?1:-1)*c)<-i?n=-i:n>i&&(n=i)):(n=W,i=f);var X=L*b-F*h,Q=O*b-H*h,Y=_/(A+g.inverseInertia*X*X+x.inverseInertia*Q*Q),Z=(1+m.restitution)*U*Y;if(n*=Y,U<u)V.normalImpulse=0;else{var $=V.normalImpulse;V.normalImpulse+=Z,V.normalImpulse>0&&(V.normalImpulse=0),Z=V.normalImpulse-$}if(W<-d||W>d)V.tangentImpulse=0;else{var J=V.tangentImpulse;V.tangentImpulse+=n,V.tangentImpulse<-i&&(V.tangentImpulse=-i),V.tangentImpulse>i&&(V.tangentImpulse=i),n=V.tangentImpulse-J}var K=h*Z+S*n,ee=b*Z+w*n;g.isStatic||g.isSleeping||(g.positionPrev.x+=K*g.inverseMass,g.positionPrev.y+=ee*g.inverseMass,g.anglePrev+=(L*ee-F*K)*g.inverseInertia),x.isStatic||x.isSleeping||(x.positionPrev.x-=K*x.inverseMass,x.positionPrev.y-=ee*x.inverseMass,x.anglePrev-=(O*ee-H*K)*x.inverseInertia)}}}}},function(e,t,n){var o={};e.exports=o;var i=n(9),r=n(0);o.create=function(e){return r.extend({table:{},list:[],collisionStart:[],collisionActive:[],collisionEnd:[]},e)},o.update=function(e,t,n){var o,r,a,s=i.update,l=i.create,c=i.setActive,u=e.table,d=e.list,p=d.length,f=p,v=e.collisionStart,m=e.collisionEnd,y=e.collisionActive,g=t.length,x=0,h=0,b=0;for(a=0;a<g;a++)(r=(o=t[a]).pair)?(r.isActive&&(y[b++]=r),s(r,o,n)):(u[(r=l(o,n)).id]=r,v[x++]=r,d[f++]=r);for(f=0,p=d.length,a=0;a<p;a++)(r=d[a]).timeUpdated>=n?d[f++]=r:(c(r,!1,n),r.collision.bodyA.sleepCounter>0&&r.collision.bodyB.sleepCounter>0?d[f++]=r:(m[h++]=r,delete u[r.id]));d.length!==f&&(d.length=f),v.length!==x&&(v.length=x),m.length!==h&&(m.length=h),y.length!==b&&(y.length=b)},o.clear=function(e){return e.table={},e.list.length=0,e.collisionStart.length=0,e.collisionActive.length=0,e.collisionEnd.length=0,e}},function(e,t,n){var o=e.exports=n(21);o.Axes=n(11),o.Bodies=n(12),o.Body=n(4),o.Bounds=n(1),o.Collision=n(8),o.Common=n(0),o.Composite=n(6),o.Composites=n(22),o.Constraint=n(10),o.Contact=n(16),o.Detector=n(13),o.Engine=n(17),o.Events=n(5),o.Grid=n(23),o.Mouse=n(14),o.MouseConstraint=n(24),o.Pair=n(9),o.Pairs=n(19),o.Plugin=n(15),o.Query=n(25),o.Render=n(26),o.Resolver=n(18),o.Runner=n(27),o.SAT=n(28),o.Sleeping=n(7),o.Svg=n(29),o.Vector=n(2),o.Vertices=n(3),o.World=n(30),o.Engine.run=o.Runner.run,o.Common.deprecated(o.Engine,"run","Engine.run ➤ use Matter.Runner.run(engine) instead")},function(e,t,n){var o={};e.exports=o;var i=n(15),r=n(0);o.name="matter-js",o.version="0.20.0",o.uses=[],o.used=[],o.use=function(){i.use(o,Array.prototype.slice.call(arguments))},o.before=function(e,t){return e=e.replace(/^Matter./,""),r.chainPathBefore(o,e,t)},o.after=function(e,t){return e=e.replace(/^Matter./,""),r.chainPathAfter(o,e,t)}},function(e,t,n){var o={};e.exports=o;var i=n(6),r=n(10),a=n(0),s=n(4),l=n(12),c=a.deprecated;o.stack=function(e,t,n,o,r,a,l){for(var c,u=i.create({label:"Stack"}),d=e,p=t,f=0,v=0;v<o;v++){for(var m=0,y=0;y<n;y++){var g=l(d,p,y,v,c,f);if(g){var x=g.bounds.max.y-g.bounds.min.y,h=g.bounds.max.x-g.bounds.min.x;x>m&&(m=x),s.translate(g,{x:.5*h,y:.5*x}),d=g.bounds.max.x+r,i.addBody(u,g),c=g,f+=1}else d+=r}p+=m+a,d=e}return u},o.chain=function(e,t,n,o,s,l){for(var c=e.bodies,u=1;u<c.length;u++){var d=c[u-1],p=c[u],f=d.bounds.max.y-d.bounds.min.y,v=d.bounds.max.x-d.bounds.min.x,m=p.bounds.max.y-p.bounds.min.y,y={bodyA:d,pointA:{x:v*t,y:f*n},bodyB:p,pointB:{x:(p.bounds.max.x-p.bounds.min.x)*o,y:m*s}},g=a.extend(y,l);i.addConstraint(e,r.create(g))}return e.label+=" Chain",e},o.mesh=function(e,t,n,o,s){var l,c,u,d,p,f=e.bodies;for(l=0;l<n;l++){for(c=1;c<t;c++)u=f[c-1+l*t],d=f[c+l*t],i.addConstraint(e,r.create(a.extend({bodyA:u,bodyB:d},s)));if(l>0)for(c=0;c<t;c++)u=f[c+(l-1)*t],d=f[c+l*t],i.addConstraint(e,r.create(a.extend({bodyA:u,bodyB:d},s))),o&&c>0&&(p=f[c-1+(l-1)*t],i.addConstraint(e,r.create(a.extend({bodyA:p,bodyB:d},s)))),o&&c<t-1&&(p=f[c+1+(l-1)*t],i.addConstraint(e,r.create(a.extend({bodyA:p,bodyB:d},s))))}return e.label+=" Mesh",e},o.pyramid=function(e,t,n,i,r,a,l){return o.stack(e,t,n,i,r,a,(function(t,o,a,c,u,d){var p=Math.min(i,Math.ceil(n/2)),f=u?u.bounds.max.x-u.bounds.min.x:0;if(!(c>p||a<(c=p-c)||a>n-1-c))return 1===d&&s.translate(u,{x:(a+(n%2==1?1:-1))*f,y:0}),l(e+(u?a*f:0)+a*r,o,a,c,u,d)}))},o.newtonsCradle=function(e,t,n,o,a){for(var s=i.create({label:"Newtons Cradle"}),c=0;c<n;c++){var u=l.circle(e+c*(1.9*o),t+a,o,{inertia:1/0,restitution:1,friction:0,frictionAir:1e-4,slop:1}),d=r.create({pointA:{x:e+c*(1.9*o),y:t},bodyB:u});i.addBody(s,u),i.addConstraint(s,d)}return s},c(o,"newtonsCradle","Composites.newtonsCradle ➤ moved to newtonsCradle example"),o.car=function(e,t,n,o,a){var c=s.nextGroup(!0),u=.5*-n+20,d=.5*n-20,p=i.create({label:"Car"}),f=l.rectangle(e,t,n,o,{collisionFilter:{group:c},chamfer:{radius:.5*o},density:2e-4}),v=l.circle(e+u,t+0,a,{collisionFilter:{group:c},friction:.8}),m=l.circle(e+d,t+0,a,{collisionFilter:{group:c},friction:.8}),y=r.create({bodyB:f,pointB:{x:u,y:0},bodyA:v,stiffness:1,length:0}),g=r.create({bodyB:f,pointB:{x:d,y:0},bodyA:m,stiffness:1,length:0});return i.addBody(p,f),i.addBody(p,v),i.addBody(p,m),i.addConstraint(p,y),i.addConstraint(p,g),p},c(o,"car","Composites.car ➤ moved to car example"),o.softBody=function(e,t,n,i,r,s,c,u,d,p){d=a.extend({inertia:1/0},d),p=a.extend({stiffness:.2,render:{type:"line",anchors:!1}},p);var f=o.stack(e,t,n,i,r,s,(function(e,t){return l.circle(e,t,u,d)}));return o.mesh(f,n,i,c,p),f.label="Soft Body",f},c(o,"softBody","Composites.softBody ➤ moved to softBody and cloth examples")},function(e,t,n){var o={};e.exports=o;var i=n(9),r=n(0),a=r.deprecated;o.create=function(e){return r.extend({buckets:{},pairs:{},pairsList:[],bucketWidth:48,bucketHeight:48},e)},o.update=function(e,t,n,i){var r,a,s,l,c,u=n.world,d=e.buckets,p=!1;for(r=0;r<t.length;r++){var f=t[r];if((!f.isSleeping||i)&&(!u.bounds||!(f.bounds.max.x<u.bounds.min.x||f.bounds.min.x>u.bounds.max.x||f.bounds.max.y<u.bounds.min.y||f.bounds.min.y>u.bounds.max.y))){var v=o._getRegion(e,f);if(!f.region||v.id!==f.region.id||i){f.region&&!i||(f.region=v);var m=o._regionUnion(v,f.region);for(a=m.startCol;a<=m.endCol;a++)for(s=m.startRow;s<=m.endRow;s++){l=d[c=o._getBucketId(a,s)];var y=a>=v.startCol&&a<=v.endCol&&s>=v.startRow&&s<=v.endRow,g=a>=f.region.startCol&&a<=f.region.endCol&&s>=f.region.startRow&&s<=f.region.endRow;!y&&g&&g&&l&&o._bucketRemoveBody(e,l,f),(f.region===v||y&&!g||i)&&(l||(l=o._createBucket(d,c)),o._bucketAddBody(e,l,f))}f.region=v,p=!0}}}p&&(e.pairsList=o._createActivePairsList(e))},a(o,"update","Grid.update ➤ replaced by Matter.Detector"),o.clear=function(e){e.buckets={},e.pairs={},e.pairsList=[]},a(o,"clear","Grid.clear ➤ replaced by Matter.Detector"),o._regionUnion=function(e,t){var n=Math.min(e.startCol,t.startCol),i=Math.max(e.endCol,t.endCol),r=Math.min(e.startRow,t.startRow),a=Math.max(e.endRow,t.endRow);return o._createRegion(n,i,r,a)},o._getRegion=function(e,t){var n=t.bounds,i=Math.floor(n.min.x/e.bucketWidth),r=Math.floor(n.max.x/e.bucketWidth),a=Math.floor(n.min.y/e.bucketHeight),s=Math.floor(n.max.y/e.bucketHeight);return o._createRegion(i,r,a,s)},o._createRegion=function(e,t,n,o){return{id:e+","+t+","+n+","+o,startCol:e,endCol:t,startRow:n,endRow:o}},o._getBucketId=function(e,t){return"C"+e+"R"+t},o._createBucket=function(e,t){return e[t]=[]},o._bucketAddBody=function(e,t,n){var o,r=e.pairs,a=i.id,s=t.length;for(o=0;o<s;o++){var l=t[o];if(!(n.id===l.id||n.isStatic&&l.isStatic)){var c=a(n,l),u=r[c];u?u[2]+=1:r[c]=[n,l,1]}}t.push(n)},o._bucketRemoveBody=function(e,t,n){var o,a=e.pairs,s=i.id;t.splice(r.indexOf(t,n),1);var l=t.length;for(o=0;o<l;o++){var c=a[s(n,t[o])];c&&(c[2]-=1)}},o._createActivePairsList=function(e){var t,n,o=e.pairs,i=r.keys(o),a=i.length,s=[];for(n=0;n<a;n++)(t=o[i[n]])[2]>0?s.push(t):delete o[i[n]];return s}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(7),a=n(14),s=n(5),l=n(13),c=n(10),u=n(6),d=n(0),p=n(1);o.create=function(e,t){var n=(e?e.mouse:null)||(t?t.mouse:null);n||(e&&e.render&&e.render.canvas?n=a.create(e.render.canvas):t&&t.element?n=a.create(t.element):(n=a.create(),d.warn("MouseConstraint.create: options.mouse was undefined, options.element was undefined, may not function as expected")));var i={type:"mouseConstraint",mouse:n,element:null,body:null,constraint:c.create({label:"Mouse Constraint",pointA:n.position,pointB:{x:0,y:0},length:.01,stiffness:.1,angularStiffness:1,render:{strokeStyle:"#90EE90",lineWidth:3}}),collisionFilter:{category:1,mask:4294967295,group:0}},r=d.extend(i,t);return s.on(e,"beforeUpdate",(function(){var t=u.allBodies(e.world);o.update(r,t),o._triggerEvents(r)})),r},o.update=function(e,t){var n=e.mouse,o=e.constraint,a=e.body;if(0===n.button){if(o.bodyB)r.set(o.bodyB,!1),o.pointA=n.position;else for(var c=0;c<t.length;c++)if(a=t[c],p.contains(a.bounds,n.position)&&l.canCollide(a.collisionFilter,e.collisionFilter))for(var u=a.parts.length>1?1:0;u<a.parts.length;u++){var d=a.parts[u];if(i.contains(d.vertices,n.position)){o.pointA=n.position,o.bodyB=e.body=a,o.pointB={x:n.position.x-a.position.x,y:n.position.y-a.position.y},o.angleB=a.angle,r.set(a,!1),s.trigger(e,"startdrag",{mouse:n,body:a});break}}}else o.bodyB=e.body=null,o.pointB=null,a&&s.trigger(e,"enddrag",{mouse:n,body:a})},o._triggerEvents=function(e){var t=e.mouse,n=t.sourceEvents;n.mousemove&&s.trigger(e,"mousemove",{mouse:t}),n.mousedown&&s.trigger(e,"mousedown",{mouse:t}),n.mouseup&&s.trigger(e,"mouseup",{mouse:t}),a.clearSourceEvents(t)}},function(e,t,n){var o={};e.exports=o;var i=n(2),r=n(8),a=n(1),s=n(12),l=n(3);o.collides=function(e,t){for(var n=[],o=t.length,i=e.bounds,s=r.collides,l=a.overlaps,c=0;c<o;c++){var u=t[c],d=u.parts.length,p=1===d?0:1;if(l(u.bounds,i))for(var f=p;f<d;f++){var v=u.parts[f];if(l(v.bounds,i)){var m=s(v,e);if(m){n.push(m);break}}}}return n},o.ray=function(e,t,n,r){r=r||1e-100;for(var a=i.angle(t,n),l=i.magnitude(i.sub(t,n)),c=.5*(n.x+t.x),u=.5*(n.y+t.y),d=s.rectangle(c,u,l,r,{angle:a}),p=o.collides(d,e),f=0;f<p.length;f+=1){var v=p[f];v.body=v.bodyB=v.bodyA}return p},o.region=function(e,t,n){for(var o=[],i=0;i<e.length;i++){var r=e[i],s=a.overlaps(r.bounds,t);(s&&!n||!s&&n)&&o.push(r)}return o},o.point=function(e,t){for(var n=[],o=0;o<e.length;o++){var i=e[o];if(a.contains(i.bounds,t))for(var r=1===i.parts.length?0:1;r<i.parts.length;r++){var s=i.parts[r];if(a.contains(s.bounds,t)&&l.contains(s.vertices,t)){n.push(i);break}}}return n}},function(e,t,n){var o={};e.exports=o;var i=n(4),r=n(0),a=n(6),s=n(1),l=n(5),c=n(2),u=n(14);!function(){var e,t;"undefined"!=typeof window&&(e=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout((function(){e(r.now())}),1e3/60)},t=window.cancelAnimationFrame||window.mozCancelAnimationFrame||window.webkitCancelAnimationFrame||window.msCancelAnimationFrame),o._goodFps=30,o._goodDelta=1e3/60,o.create=function(e){var t={engine:null,element:null,canvas:null,mouse:null,frameRequestId:null,timing:{historySize:60,delta:0,deltaHistory:[],lastTime:0,lastTimestamp:0,lastElapsed:0,timestampElapsed:0,timestampElapsedHistory:[],engineDeltaHistory:[],engineElapsedHistory:[],engineUpdatesHistory:[],elapsedHistory:[]},options:{width:800,height:600,pixelRatio:1,background:"#14151f",wireframeBackground:"#14151f",wireframeStrokeStyle:"#bbb",hasBounds:!!e.bounds,enabled:!0,wireframes:!0,showSleeping:!0,showDebug:!1,showStats:!1,showPerformance:!1,showBounds:!1,showVelocity:!1,showCollisions:!1,showSeparations:!1,showAxes:!1,showPositions:!1,showAngleIndicator:!1,showIds:!1,showVertexNumbers:!1,showConvexHulls:!1,showInternalEdges:!1,showMousePosition:!1}},n=r.extend(t,e);return n.canvas&&(n.canvas.width=n.options.width||n.canvas.width,n.canvas.height=n.options.height||n.canvas.height),n.mouse=e.mouse,n.engine=e.engine,n.canvas=n.canvas||p(n.options.width,n.options.height),n.context=n.canvas.getContext("2d"),n.textures={},n.bounds=n.bounds||{min:{x:0,y:0},max:{x:n.canvas.width,y:n.canvas.height}},n.controller=o,n.options.showBroadphase=!1,1!==n.options.pixelRatio&&o.setPixelRatio(n,n.options.pixelRatio),r.isElement(n.element)&&n.element.appendChild(n.canvas),n},o.run=function(t){!function i(r){t.frameRequestId=e(i),n(t,r),o.world(t,r),t.context.setTransform(t.options.pixelRatio,0,0,t.options.pixelRatio,0,0),(t.options.showStats||t.options.showDebug)&&o.stats(t,t.context,r),(t.options.showPerformance||t.options.showDebug)&&o.performance(t,t.context,r),t.context.setTransform(1,0,0,1,0,0)}()},o.stop=function(e){t(e.frameRequestId)},o.setPixelRatio=function(e,t){var n=e.options,o=e.canvas;"auto"===t&&(t=f(o)),n.pixelRatio=t,o.setAttribute("data-pixel-ratio",t),o.width=n.width*t,o.height=n.height*t,o.style.width=n.width+"px",o.style.height=n.height+"px"},o.setSize=function(e,t,n){e.options.width=t,e.options.height=n,e.bounds.max.x=e.bounds.min.x+t,e.bounds.max.y=e.bounds.min.y+n,1!==e.options.pixelRatio?o.setPixelRatio(e,e.options.pixelRatio):(e.canvas.width=t,e.canvas.height=n)},o.lookAt=function(e,t,n,o){o=void 0===o||o,t=r.isArray(t)?t:[t],n=n||{x:0,y:0};for(var i={min:{x:1/0,y:1/0},max:{x:-1/0,y:-1/0}},a=0;a<t.length;a+=1){var s=t[a],l=s.bounds?s.bounds.min:s.min||s.position||s,c=s.bounds?s.bounds.max:s.max||s.position||s;l&&c&&(l.x<i.min.x&&(i.min.x=l.x),c.x>i.max.x&&(i.max.x=c.x),l.y<i.min.y&&(i.min.y=l.y),c.y>i.max.y&&(i.max.y=c.y))}var d=i.max.x-i.min.x+2*n.x,p=i.max.y-i.min.y+2*n.y,f=e.canvas.height,v=e.canvas.width/f,m=d/p,y=1,g=1;m>v?g=m/v:y=v/m,e.options.hasBounds=!0,e.bounds.min.x=i.min.x,e.bounds.max.x=i.min.x+d*y,e.bounds.min.y=i.min.y,e.bounds.max.y=i.min.y+p*g,o&&(e.bounds.min.x+=.5*d-d*y*.5,e.bounds.max.x+=.5*d-d*y*.5,e.bounds.min.y+=.5*p-p*g*.5,e.bounds.max.y+=.5*p-p*g*.5),e.bounds.min.x-=n.x,e.bounds.max.x-=n.x,e.bounds.min.y-=n.y,e.bounds.max.y-=n.y,e.mouse&&(u.setScale(e.mouse,{x:(e.bounds.max.x-e.bounds.min.x)/e.canvas.width,y:(e.bounds.max.y-e.bounds.min.y)/e.canvas.height}),u.setOffset(e.mouse,e.bounds.min))},o.startViewTransform=function(e){var t=e.bounds.max.x-e.bounds.min.x,n=e.bounds.max.y-e.bounds.min.y,o=t/e.options.width,i=n/e.options.height;e.context.setTransform(e.options.pixelRatio/o,0,0,e.options.pixelRatio/i,0,0),e.context.translate(-e.bounds.min.x,-e.bounds.min.y)},o.endViewTransform=function(e){e.context.setTransform(e.options.pixelRatio,0,0,e.options.pixelRatio,0,0)},o.world=function(e,t){var n,i=r.now(),d=e.engine,p=d.world,f=e.canvas,v=e.context,y=e.options,g=e.timing,x=a.allBodies(p),h=a.allConstraints(p),b=y.wireframes?y.wireframeBackground:y.background,S=[],w=[],A={timestamp:d.timing.timestamp};if(l.trigger(e,"beforeRender",A),e.currentBackground!==b&&m(e,b),v.globalCompositeOperation="source-in",v.fillStyle="transparent",v.fillRect(0,0,f.width,f.height),v.globalCompositeOperation="source-over",y.hasBounds){for(n=0;n<x.length;n++){var P=x[n];s.overlaps(P.bounds,e.bounds)&&S.push(P)}for(n=0;n<h.length;n++){var B=h[n],M=B.bodyA,_=B.bodyB,C=B.pointA,k=B.pointB;M&&(C=c.add(M.position,B.pointA)),_&&(k=c.add(_.position,B.pointB)),C&&k&&((s.contains(e.bounds,C)||s.contains(e.bounds,k))&&w.push(B))}o.startViewTransform(e),e.mouse&&(u.setScale(e.mouse,{x:(e.bounds.max.x-e.bounds.min.x)/e.options.width,y:(e.bounds.max.y-e.bounds.min.y)/e.options.height}),u.setOffset(e.mouse,e.bounds.min))}else w=h,S=x,1!==e.options.pixelRatio&&e.context.setTransform(e.options.pixelRatio,0,0,e.options.pixelRatio,0,0);!y.wireframes||d.enableSleeping&&y.showSleeping?o.bodies(e,S,v):(y.showConvexHulls&&o.bodyConvexHulls(e,S,v),o.bodyWireframes(e,S,v)),y.showBounds&&o.bodyBounds(e,S,v),(y.showAxes||y.showAngleIndicator)&&o.bodyAxes(e,S,v),y.showPositions&&o.bodyPositions(e,S,v),y.showVelocity&&o.bodyVelocity(e,S,v),y.showIds&&o.bodyIds(e,S,v),y.showSeparations&&o.separations(e,d.pairs.list,v),y.showCollisions&&o.collisions(e,d.pairs.list,v),y.showVertexNumbers&&o.vertexNumbers(e,S,v),y.showMousePosition&&o.mousePosition(e,e.mouse,v),o.constraints(w,v),y.hasBounds&&o.endViewTransform(e),l.trigger(e,"afterRender",A),g.lastElapsed=r.now()-i},o.stats=function(e,t,n){for(var o=e.engine,i=o.world,r=a.allBodies(i),s=0,l=0,c=0;c<r.length;c+=1)s+=r[c].parts.length;var u={Part:s,Body:r.length,Cons:a.allConstraints(i).length,Comp:a.allComposites(i).length,Pair:o.pairs.list.length};for(var d in t.fillStyle="#0e0f19",t.fillRect(l,0,302.5,44),t.font="12px Arial",t.textBaseline="top",t.textAlign="right",u){var p=u[d];t.fillStyle="#aaa",t.fillText(d,l+55,8),t.fillStyle="#eee",t.fillText(p,l+55,26),l+=55}},o.performance=function(e,t){var n=e.engine,i=e.timing,a=i.deltaHistory,s=i.elapsedHistory,l=i.timestampElapsedHistory,c=i.engineDeltaHistory,u=i.engineUpdatesHistory,p=i.engineElapsedHistory,f=n.timing.lastUpdatesPerFrame,v=n.timing.lastDelta,m=d(a),y=d(s),g=d(c),x=d(u),h=d(p),b=d(l)/m||0,S=Math.round(m/v),w=1e3/m||0,A=10,P=69;t.fillStyle="#0e0f19",t.fillRect(0,50,442,34),o.status(t,A,P,60,4,a.length,Math.round(w)+" fps",w/o._goodFps,(function(e){return a[e]/m-1})),o.status(t,82,P,60,4,c.length,v.toFixed(2)+" dt",o._goodDelta/v,(function(e){return c[e]/g-1})),o.status(t,154,P,60,4,u.length,f+" upf",Math.pow(r.clamp(x/S||1,0,1),4),(function(e){return u[e]/x-1})),o.status(t,226,P,60,4,p.length,h.toFixed(2)+" ut",1-f*h/o._goodFps,(function(e){return p[e]/h-1})),o.status(t,298,P,60,4,s.length,y.toFixed(2)+" rt",1-y/o._goodFps,(function(e){return s[e]/y-1})),o.status(t,370,P,60,4,l.length,b.toFixed(2)+" x",b*b*b,(function(e){return(l[e]/a[e]/b||0)-1}))},o.status=function(e,t,n,o,i,a,s,l,c){e.strokeStyle="#888",e.fillStyle="#444",e.lineWidth=1,e.fillRect(t,n+7,o,1),e.beginPath(),e.moveTo(t,n+7-i*r.clamp(.4*c(0),-2,2));for(var u=0;u<o;u+=1)e.lineTo(t+u,n+7-(u<a?i*r.clamp(.4*c(u),-2,2):0));e.stroke(),e.fillStyle="hsl("+r.clamp(25+95*l,0,120)+",100%,60%)",e.fillRect(t,n-7,4,4),e.font="12px Arial",e.textBaseline="middle",e.textAlign="right",e.fillStyle="#eee",e.fillText(s,t+o,n-5)},o.constraints=function(e,t){for(var n=t,o=0;o<e.length;o++){var i=e[o];if(i.render.visible&&i.pointA&&i.pointB){var a,s,l=i.bodyA,u=i.bodyB;if(a=l?c.add(l.position,i.pointA):i.pointA,"pin"===i.render.type)n.beginPath(),n.arc(a.x,a.y,3,0,2*Math.PI),n.closePath();else{if(s=u?c.add(u.position,i.pointB):i.pointB,n.beginPath(),n.moveTo(a.x,a.y),"spring"===i.render.type)for(var d,p=c.sub(s,a),f=c.perp(c.normalise(p)),v=Math.ceil(r.clamp(i.length/5,12,20)),m=1;m<v;m+=1)d=m%2==0?1:-1,n.lineTo(a.x+p.x*(m/v)+f.x*d*4,a.y+p.y*(m/v)+f.y*d*4);n.lineTo(s.x,s.y)}i.render.lineWidth&&(n.lineWidth=i.render.lineWidth,n.strokeStyle=i.render.strokeStyle,n.stroke()),i.render.anchors&&(n.fillStyle=i.render.strokeStyle,n.beginPath(),n.arc(a.x,a.y,3,0,2*Math.PI),n.arc(s.x,s.y,3,0,2*Math.PI),n.closePath(),n.fill())}}},o.bodies=function(e,t,n){var o,i,r,a,s=n,l=(e.engine,e.options),c=l.showInternalEdges||!l.wireframes;for(r=0;r<t.length;r++)if((o=t[r]).render.visible)for(a=o.parts.length>1?1:0;a<o.parts.length;a++)if((i=o.parts[a]).render.visible){if(l.showSleeping&&o.isSleeping?s.globalAlpha=.5*i.render.opacity:1!==i.render.opacity&&(s.globalAlpha=i.render.opacity),i.render.sprite&&i.render.sprite.texture&&!l.wireframes){var u=i.render.sprite,d=v(e,u.texture);s.translate(i.position.x,i.position.y),s.rotate(i.angle),s.drawImage(d,d.width*-u.xOffset*u.xScale,d.height*-u.yOffset*u.yScale,d.width*u.xScale,d.height*u.yScale),s.rotate(-i.angle),s.translate(-i.position.x,-i.position.y)}else{if(i.circleRadius)s.beginPath(),s.arc(i.position.x,i.position.y,i.circleRadius,0,2*Math.PI);else{s.beginPath(),s.moveTo(i.vertices[0].x,i.vertices[0].y);for(var p=1;p<i.vertices.length;p++)!i.vertices[p-1].isInternal||c?s.lineTo(i.vertices[p].x,i.vertices[p].y):s.moveTo(i.vertices[p].x,i.vertices[p].y),i.vertices[p].isInternal&&!c&&s.moveTo(i.vertices[(p+1)%i.vertices.length].x,i.vertices[(p+1)%i.vertices.length].y);s.lineTo(i.vertices[0].x,i.vertices[0].y),s.closePath()}l.wireframes?(s.lineWidth=1,s.strokeStyle=e.options.wireframeStrokeStyle,s.stroke()):(s.fillStyle=i.render.fillStyle,i.render.lineWidth&&(s.lineWidth=i.render.lineWidth,s.strokeStyle=i.render.strokeStyle,s.stroke()),s.fill())}s.globalAlpha=1}},o.bodyWireframes=function(e,t,n){var o,i,r,a,s,l=n,c=e.options.showInternalEdges;for(l.beginPath(),r=0;r<t.length;r++)if((o=t[r]).render.visible)for(s=o.parts.length>1?1:0;s<o.parts.length;s++){for(i=o.parts[s],l.moveTo(i.vertices[0].x,i.vertices[0].y),a=1;a<i.vertices.length;a++)!i.vertices[a-1].isInternal||c?l.lineTo(i.vertices[a].x,i.vertices[a].y):l.moveTo(i.vertices[a].x,i.vertices[a].y),i.vertices[a].isInternal&&!c&&l.moveTo(i.vertices[(a+1)%i.vertices.length].x,i.vertices[(a+1)%i.vertices.length].y);l.lineTo(i.vertices[0].x,i.vertices[0].y)}l.lineWidth=1,l.strokeStyle=e.options.wireframeStrokeStyle,l.stroke()},o.bodyConvexHulls=function(e,t,n){var o,i,r,a=n;for(a.beginPath(),i=0;i<t.length;i++)if((o=t[i]).render.visible&&1!==o.parts.length){for(a.moveTo(o.vertices[0].x,o.vertices[0].y),r=1;r<o.vertices.length;r++)a.lineTo(o.vertices[r].x,o.vertices[r].y);a.lineTo(o.vertices[0].x,o.vertices[0].y)}a.lineWidth=1,a.strokeStyle="rgba(255,255,255,0.2)",a.stroke()},o.vertexNumbers=function(e,t,n){var o,i,r,a=n;for(o=0;o<t.length;o++){var s=t[o].parts;for(r=s.length>1?1:0;r<s.length;r++){var l=s[r];for(i=0;i<l.vertices.length;i++)a.fillStyle="rgba(255,255,255,0.2)",a.fillText(o+"_"+i,l.position.x+.8*(l.vertices[i].x-l.position.x),l.position.y+.8*(l.vertices[i].y-l.position.y))}}},o.mousePosition=function(e,t,n){var o=n;o.fillStyle="rgba(255,255,255,0.8)",o.fillText(t.position.x+"  "+t.position.y,t.position.x+5,t.position.y-5)},o.bodyBounds=function(e,t,n){var o=n,i=(e.engine,e.options);o.beginPath();for(var r=0;r<t.length;r++){if(t[r].render.visible)for(var a=t[r].parts,s=a.length>1?1:0;s<a.length;s++){var l=a[s];o.rect(l.bounds.min.x,l.bounds.min.y,l.bounds.max.x-l.bounds.min.x,l.bounds.max.y-l.bounds.min.y)}}i.wireframes?o.strokeStyle="rgba(255,255,255,0.08)":o.strokeStyle="rgba(0,0,0,0.1)",o.lineWidth=1,o.stroke()},o.bodyAxes=function(e,t,n){var o,i,r,a,s=n,l=(e.engine,e.options);for(s.beginPath(),i=0;i<t.length;i++){var c=t[i],u=c.parts;if(c.render.visible)if(l.showAxes)for(r=u.length>1?1:0;r<u.length;r++)for(o=u[r],a=0;a<o.axes.length;a++){var d=o.axes[a];s.moveTo(o.position.x,o.position.y),s.lineTo(o.position.x+20*d.x,o.position.y+20*d.y)}else for(r=u.length>1?1:0;r<u.length;r++)for(o=u[r],a=0;a<o.axes.length;a++)s.moveTo(o.position.x,o.position.y),s.lineTo((o.vertices[0].x+o.vertices[o.vertices.length-1].x)/2,(o.vertices[0].y+o.vertices[o.vertices.length-1].y)/2)}l.wireframes?(s.strokeStyle="indianred",s.lineWidth=1):(s.strokeStyle="rgba(255, 255, 255, 0.4)",s.globalCompositeOperation="overlay",s.lineWidth=2),s.stroke(),s.globalCompositeOperation="source-over"},o.bodyPositions=function(e,t,n){var o,i,r,a,s=n,l=(e.engine,e.options);for(s.beginPath(),r=0;r<t.length;r++)if((o=t[r]).render.visible)for(a=0;a<o.parts.length;a++)i=o.parts[a],s.arc(i.position.x,i.position.y,3,0,2*Math.PI,!1),s.closePath();for(l.wireframes?s.fillStyle="indianred":s.fillStyle="rgba(0,0,0,0.5)",s.fill(),s.beginPath(),r=0;r<t.length;r++)(o=t[r]).render.visible&&(s.arc(o.positionPrev.x,o.positionPrev.y,2,0,2*Math.PI,!1),s.closePath());s.fillStyle="rgba(255,165,0,0.8)",s.fill()},o.bodyVelocity=function(e,t,n){var o=n;o.beginPath();for(var r=0;r<t.length;r++){var a=t[r];if(a.render.visible){var s=i.getVelocity(a);o.moveTo(a.position.x,a.position.y),o.lineTo(a.position.x+s.x,a.position.y+s.y)}}o.lineWidth=3,o.strokeStyle="cornflowerblue",o.stroke()},o.bodyIds=function(e,t,n){var o,i,r=n;for(o=0;o<t.length;o++)if(t[o].render.visible){var a=t[o].parts;for(i=a.length>1?1:0;i<a.length;i++){var s=a[i];r.font="12px Arial",r.fillStyle="rgba(255,255,255,0.5)",r.fillText(s.id,s.position.x+10,s.position.y-10)}}},o.collisions=function(e,t,n){var o,i,r,a,s=n,l=e.options;for(s.beginPath(),r=0;r<t.length;r++)if((o=t[r]).isActive)for(i=o.collision,a=0;a<o.contactCount;a++){var c=o.contacts[a].vertex;s.rect(c.x-1.5,c.y-1.5,3.5,3.5)}for(l.wireframes?s.fillStyle="rgba(255,255,255,0.7)":s.fillStyle="orange",s.fill(),s.beginPath(),r=0;r<t.length;r++)if((o=t[r]).isActive&&(i=o.collision,o.contactCount>0)){var u=o.contacts[0].vertex.x,d=o.contacts[0].vertex.y;2===o.contactCount&&(u=(o.contacts[0].vertex.x+o.contacts[1].vertex.x)/2,d=(o.contacts[0].vertex.y+o.contacts[1].vertex.y)/2),i.bodyB===i.supports[0].body||!0===i.bodyA.isStatic?s.moveTo(u-8*i.normal.x,d-8*i.normal.y):s.moveTo(u+8*i.normal.x,d+8*i.normal.y),s.lineTo(u,d)}l.wireframes?s.strokeStyle="rgba(255,165,0,0.7)":s.strokeStyle="orange",s.lineWidth=1,s.stroke()},o.separations=function(e,t,n){var o,i,r,a,s,l=n,c=e.options;for(l.beginPath(),s=0;s<t.length;s++)if((o=t[s]).isActive){r=(i=o.collision).bodyA;var u=1;(a=i.bodyB).isStatic||r.isStatic||(u=.5),a.isStatic&&(u=0),l.moveTo(a.position.x,a.position.y),l.lineTo(a.position.x-i.penetration.x*u,a.position.y-i.penetration.y*u),u=1,a.isStatic||r.isStatic||(u=.5),r.isStatic&&(u=0),l.moveTo(r.position.x,r.position.y),l.lineTo(r.position.x+i.penetration.x*u,r.position.y+i.penetration.y*u)}c.wireframes?l.strokeStyle="rgba(255,165,0,0.5)":l.strokeStyle="orange",l.stroke()},o.inspector=function(e,t){e.engine;var n,o=e.selected,i=e.render,r=i.options;if(r.hasBounds){var a=i.bounds.max.x-i.bounds.min.x,s=i.bounds.max.y-i.bounds.min.y,l=a/i.options.width,c=s/i.options.height;t.scale(1/l,1/c),t.translate(-i.bounds.min.x,-i.bounds.min.y)}for(var u=0;u<o.length;u++){var d=o[u].data;switch(t.translate(.5,.5),t.lineWidth=1,t.strokeStyle="rgba(255,165,0,0.9)",t.setLineDash([1,2]),d.type){case"body":n=d.bounds,t.beginPath(),t.rect(Math.floor(n.min.x-3),Math.floor(n.min.y-3),Math.floor(n.max.x-n.min.x+6),Math.floor(n.max.y-n.min.y+6)),t.closePath(),t.stroke();break;case"constraint":var p=d.pointA;d.bodyA&&(p=d.pointB),t.beginPath(),t.arc(p.x,p.y,10,0,2*Math.PI),t.closePath(),t.stroke()}t.setLineDash([]),t.translate(-.5,-.5)}null!==e.selectStart&&(t.translate(.5,.5),t.lineWidth=1,t.strokeStyle="rgba(255,165,0,0.6)",t.fillStyle="rgba(255,165,0,0.1)",n=e.selectBounds,t.beginPath(),t.rect(Math.floor(n.min.x),Math.floor(n.min.y),Math.floor(n.max.x-n.min.x),Math.floor(n.max.y-n.min.y)),t.closePath(),t.stroke(),t.fill(),t.translate(-.5,-.5)),r.hasBounds&&t.setTransform(1,0,0,1,0,0)};var n=function(e,t){var n=e.engine,i=e.timing,r=i.historySize,a=n.timing.timestamp;i.delta=t-i.lastTime||o._goodDelta,i.lastTime=t,i.timestampElapsed=a-i.lastTimestamp||0,i.lastTimestamp=a,i.deltaHistory.unshift(i.delta),i.deltaHistory.length=Math.min(i.deltaHistory.length,r),i.engineDeltaHistory.unshift(n.timing.lastDelta),i.engineDeltaHistory.length=Math.min(i.engineDeltaHistory.length,r),i.timestampElapsedHistory.unshift(i.timestampElapsed),i.timestampElapsedHistory.length=Math.min(i.timestampElapsedHistory.length,r),i.engineUpdatesHistory.unshift(n.timing.lastUpdatesPerFrame),i.engineUpdatesHistory.length=Math.min(i.engineUpdatesHistory.length,r),i.engineElapsedHistory.unshift(n.timing.lastElapsed),i.engineElapsedHistory.length=Math.min(i.engineElapsedHistory.length,r),i.elapsedHistory.unshift(i.lastElapsed),i.elapsedHistory.length=Math.min(i.elapsedHistory.length,r)},d=function(e){for(var t=0,n=0;n<e.length;n+=1)t+=e[n];return t/e.length||0},p=function(e,t){var n=document.createElement("canvas");return n.width=e,n.height=t,n.oncontextmenu=function(){return!1},n.onselectstart=function(){return!1},n},f=function(e){var t=e.getContext("2d");return(window.devicePixelRatio||1)/(t.webkitBackingStorePixelRatio||t.mozBackingStorePixelRatio||t.msBackingStorePixelRatio||t.oBackingStorePixelRatio||t.backingStorePixelRatio||1)},v=function(e,t){var n=e.textures[t];return n||((n=e.textures[t]=new Image).src=t,n)},m=function(e,t){var n=t;/(jpg|gif|png)$/.test(t)&&(n="url("+t+")"),e.canvas.style.background=n,e.canvas.style.backgroundSize="contain",e.currentBackground=t}}()},function(e,t,n){var o={};e.exports=o;var i=n(5),r=n(17),a=n(0);!function(){o._maxFrameDelta=1e3/15,o._frameDeltaFallback=1e3/60,o._timeBufferMargin=1.5,o._elapsedNextEstimate=1,o._smoothingLowerBound=.1,o._smoothingUpperBound=.9,o.create=function(e){var t=a.extend({delta:1e3/60,frameDelta:null,frameDeltaSmoothing:!0,frameDeltaSnapping:!0,frameDeltaHistory:[],frameDeltaHistorySize:100,frameRequestId:null,timeBuffer:0,timeLastTick:null,maxUpdates:null,maxFrameTime:1e3/30,lastUpdatesDeferred:0,enabled:!0},e);return t.fps=0,t},o.run=function(e,t){return e.timeBuffer=o._frameDeltaFallback,function n(i){e.frameRequestId=o._onNextFrame(e,n),i&&e.enabled&&o.tick(e,t,i)}(),e},o.tick=function(t,n,s){var l=a.now(),c=t.delta,u=0,d=s-t.timeLastTick;if((!d||!t.timeLastTick||d>Math.max(o._maxFrameDelta,t.maxFrameTime))&&(d=t.frameDelta||o._frameDeltaFallback),t.frameDeltaSmoothing){t.frameDeltaHistory.push(d),t.frameDeltaHistory=t.frameDeltaHistory.slice(-t.frameDeltaHistorySize);var p=t.frameDeltaHistory.slice(0).sort(),f=t.frameDeltaHistory.slice(p.length*o._smoothingLowerBound,p.length*o._smoothingUpperBound);d=e(f)||d}t.frameDeltaSnapping&&(d=1e3/Math.round(1e3/d)),t.frameDelta=d,t.timeLastTick=s,t.timeBuffer+=t.frameDelta,t.timeBuffer=a.clamp(t.timeBuffer,0,t.frameDelta+c*o._timeBufferMargin),t.lastUpdatesDeferred=0;var v=t.maxUpdates||Math.ceil(t.maxFrameTime/c),m={timestamp:n.timing.timestamp};i.trigger(t,"beforeTick",m),i.trigger(t,"tick",m);for(var y=a.now();c>0&&t.timeBuffer>=c*o._timeBufferMargin;){i.trigger(t,"beforeUpdate",m),r.update(n,c),i.trigger(t,"afterUpdate",m),t.timeBuffer-=c,u+=1;var g=a.now()-l,x=a.now()-y,h=g+o._elapsedNextEstimate*x/u;if(u>=v||h>t.maxFrameTime){t.lastUpdatesDeferred=Math.round(Math.max(0,t.timeBuffer/c-o._timeBufferMargin));break}}n.timing.lastUpdatesPerFrame=u,i.trigger(t,"afterTick",m),t.frameDeltaHistory.length>=100&&(t.lastUpdatesDeferred&&Math.round(t.frameDelta/c)>v?a.warnOnce("Matter.Runner: runner reached runner.maxUpdates, see docs."):t.lastUpdatesDeferred&&a.warnOnce("Matter.Runner: runner reached runner.maxFrameTime, see docs."),void 0!==t.isFixed&&a.warnOnce("Matter.Runner: runner.isFixed is now redundant, see docs."),(t.deltaMin||t.deltaMax)&&a.warnOnce("Matter.Runner: runner.deltaMin and runner.deltaMax were removed, see docs."),0!==t.fps&&a.warnOnce("Matter.Runner: runner.fps was replaced by runner.delta, see docs."))},o.stop=function(e){o._cancelNextFrame(e)},o._onNextFrame=function(e,t){if("undefined"==typeof window||!window.requestAnimationFrame)throw new Error("Matter.Runner: missing required global window.requestAnimationFrame.");return e.frameRequestId=window.requestAnimationFrame(t),e.frameRequestId},o._cancelNextFrame=function(e){if("undefined"==typeof window||!window.cancelAnimationFrame)throw new Error("Matter.Runner: missing required global window.cancelAnimationFrame.");window.cancelAnimationFrame(e.frameRequestId)};var e=function(e){for(var t=0,n=e.length,o=0;o<n;o+=1)t+=e[o];return t/n||0}}()},function(e,t,n){var o={};e.exports=o;var i=n(8),r=n(0).deprecated;o.collides=function(e,t){return i.collides(e,t)},r(o,"collides","SAT.collides ➤ replaced by Collision.collides")},function(e,t,n){var o={};e.exports=o;n(1);var i=n(0);o.pathToVertices=function(e,t){"undefined"==typeof window||"SVGPathSeg"in window||i.warn("Svg.pathToVertices: SVGPathSeg not defined, a polyfill is required.");var n,r,a,s,l,c,u,d,p,f,v,m=[],y=0,g=0,x=0;t=t||15;var h=function(e,t,n){var o=n%2==1&&n>1;if(!p||e!=p.x||t!=p.y){p&&o?(f=p.x,v=p.y):(f=0,v=0);var i={x:f+e,y:v+t};!o&&p||(p=i),m.push(i),g=f+e,x=v+t}},b=function(e){var t=e.pathSegTypeAsLetter.toUpperCase();if("Z"!==t){switch(t){case"M":case"L":case"T":case"C":case"S":case"Q":g=e.x,x=e.y;break;case"H":g=e.x;break;case"V":x=e.y}h(g,x,e.pathSegType)}};for(o._svgPathToAbsolute(e),a=e.getTotalLength(),c=[],n=0;n<e.pathSegList.numberOfItems;n+=1)c.push(e.pathSegList.getItem(n));for(u=c.concat();y<a;){if((l=c[e.getPathSegAtLength(y)])!=d){for(;u.length&&u[0]!=l;)b(u.shift());d=l}switch(l.pathSegTypeAsLetter.toUpperCase()){case"C":case"T":case"S":case"Q":case"A":s=e.getPointAtLength(y),h(s.x,s.y,0)}y+=t}for(n=0,r=u.length;n<r;++n)b(u[n]);return m},o._svgPathToAbsolute=function(e){for(var t,n,o,i,r,a,s=e.pathSegList,l=0,c=0,u=s.numberOfItems,d=0;d<u;++d){var p=s.getItem(d),f=p.pathSegTypeAsLetter;if(/[MLHVCSQTA]/.test(f))"x"in p&&(l=p.x),"y"in p&&(c=p.y);else switch("x1"in p&&(o=l+p.x1),"x2"in p&&(r=l+p.x2),"y1"in p&&(i=c+p.y1),"y2"in p&&(a=c+p.y2),"x"in p&&(l+=p.x),"y"in p&&(c+=p.y),f){case"m":s.replaceItem(e.createSVGPathSegMovetoAbs(l,c),d);break;case"l":s.replaceItem(e.createSVGPathSegLinetoAbs(l,c),d);break;case"h":s.replaceItem(e.createSVGPathSegLinetoHorizontalAbs(l),d);break;case"v":s.replaceItem(e.createSVGPathSegLinetoVerticalAbs(c),d);break;case"c":s.replaceItem(e.createSVGPathSegCurvetoCubicAbs(l,c,o,i,r,a),d);break;case"s":s.replaceItem(e.createSVGPathSegCurvetoCubicSmoothAbs(l,c,r,a),d);break;case"q":s.replaceItem(e.createSVGPathSegCurvetoQuadraticAbs(l,c,o,i),d);break;case"t":s.replaceItem(e.createSVGPathSegCurvetoQuadraticSmoothAbs(l,c),d);break;case"a":s.replaceItem(e.createSVGPathSegArcAbs(l,c,p.r1,p.r2,p.angle,p.largeArcFlag,p.sweepFlag),d);break;case"z":case"Z":l=t,c=n}"M"!=f&&"m"!=f||(t=l,n=c)}}},function(e,t,n){var o={};e.exports=o;var i=n(6);n(0);o.create=i.create,o.add=i.add,o.remove=i.remove,o.clear=i.clear,o.addComposite=i.addComposite,o.addBody=i.addBody,o.addConstraint=i.addConstraint}])}));
----- END ORIGINAL CONTENT -----
===== END FILE 21: public/lib/matter.min.js =====

===== BEGIN FILE 22: tmp/near.mjs =====
Source-Path: C:\石とゾンビ\tmp\near.mjs
Source-Bytes: 2215
Source-Lines: 37
Source-LastWriteTime-JST: 2026-09-13T19:47:50+09:00
Source-SHA256: 4cad95c7ff108f57cc7fdcd03c4011404ad9f6d342f818eb4c52faa5c728ddb3
Role: 参考付録。tmp内の近距離投石/大岩検品スクリプト。現行配信コードではない
----- BEGIN ORIGINAL CONTENT -----
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';
const ROOT='C:/石とゾンビ';
const pick=u=>{const a=path.join(ROOT,'public',u); return fs.existsSync(a)?a:path.join(ROOT,u);};
const 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':u.endsWith('.js')?'text/javascript':'text/html'});r.end(d);});});
await new Promise(ok=>srv.listen(8855,ok));
const b=await webkit.launch(); const p=await b.newPage({viewport:{width:390,height:797}});
const errs=[]; p.on('pageerror',e=>errs.push(String(e.message).slice(0,160)));
await p.goto('http://localhost:8855/index.html',{waitUntil:'load'}); await p.waitForTimeout(900);
const out=await p.evaluate(()=>{
  const res=[];
  for(const d of [0.44,0.6,0.8,1.0,1.2]){
    let kill=0,knock=0,miss=0,angs=[];
    for(let a=14;a<=84;a+=2) for(let k=1;k<=10;k++){
      const o=window.ZG.trial(d,a,k/10);
      if(o.r==='kill'){kill++;angs.push(a);} else if(o.r==='knock')knock++; else miss++;
    }
    res.push({d,kill,knock,miss,lo:angs.length?Math.min(...angs):0,hi:angs.length?Math.max(...angs):0});
  }
  // 大岩も見る
  let rk=0,rn=0,rm=0;
  for(const d of [2,3,4,5]) for(let a=14;a<=84;a+=4) for(let k=2;k<=10;k+=2){
    const o=window.ZG.trial(d,a,k/10,true);
    if(o.r==='kill')rk++; else if(o.r==='knock')rn++; else rm++;
  }
  return {res,rock:{rk,rn,rm}};
});
console.log('取り付かれた時の自力脱出(近い距離)');
console.log('距離   倒した  押した  外れ   倒せる角度');
for(const r of out.res) console.log((r.d+'m').padEnd(7)+String(r.kill).padEnd(8)+String(r.knock).padEnd(8)+String(r.miss).padEnd(7)+(r.kill?r.lo+'〜'+r.hi+'度':'—'));
console.log('\n大岩(2〜5m・360通り): 倒した '+out.rock.rk+' / 押した '+out.rock.rn+' / 外れ '+out.rock.rm);
console.log('JSエラー: '+(errs.length?errs.join(' / '):'なし'));
await b.close(); srv.close(); process.exit(0);

----- END ORIGINAL CONTENT -----
===== END FILE 22: tmp/near.mjs =====

===== END OF BUNDLE: 22 FILES / 全文収録ここまで =====