From 4f93ddaf620421a99540087d54410820279f1936 Mon Sep 17 00:00:00 2001
From: mattyatea <mattyatea@pm.me>
Date: Sat, 20 Jan 2024 17:30:29 +0900
Subject: [PATCH] wakariyasuku

---
 locales/index.d.ts                        | 268 ++++++++++--
 packages/misskey-bubble-game/src/game.ts  |   5 +-
 packages/misskey-bubble-game/src/monos.ts | 489 +---------------------
 3 files changed, 259 insertions(+), 503 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 1a7da7ded7..54b370bd0d 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -60,11 +60,29 @@ export interface Locale extends ILocale {
      * OK
      */
     "ok": string;
+    /**
+     * このプロファイルをデフォルトにしますか?
+     */
     "setDefaultProfileConfirm": string;
+    /**
+     * 絵文字ピッカーのプロファイル
+     */
     "emojiPickerProfile": string;
+    /**
+     * 通知のインジケーターの数字を表示する
+     */
     "notificationIndicator": string;
+    /**
+     * アイコンとバナーを反転させる
+     */
     "hanntenn": string;
+    /**
+     * ダークだったらライトのアイコンに、ライトだったらダークのアイコンに。
+     */
     "hanntennInfo": string;
+    /**
+     * ルビ
+     */
     "ruby": string;
     /**
      * わかった
@@ -83,9 +101,12 @@ export interface Locale extends ILocale {
      */
     "enterUsername": string;
     /**
-     * {user}がリノート
+     * グローバルタイムラインを表示する
      */
     "showGlobalTimeline": string;
+    /**
+     * {user}がリノート
+     */
     "renotedBy": ParameterizedString<"user">;
     /**
      * ノートはありません
@@ -327,13 +348,25 @@ export interface Locale extends ILocale {
      * ファイル「{name}」を削除しますか?このファイルを使用した一部のコンテンツも削除されます。
      */
     "driveFileDeleteConfirm": ParameterizedString<"name">;
+    /**
+     * {name}つのファイルを削除しますか?このファイルを使用した一部のコンテンツも削除されます。
+     */
+    "driveFilesDeleteConfirm": ParameterizedString<"name">;
+    /**
+     * {name}つのファイルをセンシティブにしますか?
+     */
+    "driveFilesSensitiveonConfirm": ParameterizedString<"name">;
+    /**
+     * {name}つのファイルのセンシティブを解除しますか?
+     */
+    "driveFilesSensitiveoffConfirm": ParameterizedString<"name">;
+    /**
+     * フォルダ「{name}」を削除しますか?このフォルダの中に存在するファイルを使用した一部のコンテンツも削除されます。
+     */
+    "driveFolderDeleteConfirm": ParameterizedString<"name">;
     /**
      * {name}のフォローを解除しますか?
      */
-    "driveFilesDeleteConfirm": string;
-    "driveFilesSensitiveonConfirm": string;
-    "driveFilesSensitiveoffConfirm": string;
-    "driveFolderDeleteConfirm": string;
     "unfollowConfirm": ParameterizedString<"name">;
     /**
      * エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。
@@ -700,20 +733,32 @@ export interface Locale extends ILocale {
      */
     "flagAsCat": string;
     /**
-     * にゃにゃにゃ??
+     * ウホウホウホホウホウホウホウホホホ!!!!!!!!!!!
      */
     "flagAsGorilla": string;
+    /**
+     * にゃにゃにゃ??
+     */
     "flagAsCatDescription": string;
+    /**
+     * ウホウホウホ??
+     */
+    "flagAsGorillaDescription": string;
     /**
      * タイムラインにノートへの返信を表示する
      */
-    "flagAsGorillaDescription": string;
     "flagShowTimelineReplies": string;
+    /**
+     * メディアタイムラインを表示する
+     */
+    "showMediaTimeline": string;
+    /**
+     * オンにするとメディアタイムラインを上のバーに表示します。 オフにすると表示しなくなります。
+     */
+    "showMediaTimelineInfo": string;
     /**
      * オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。
      */
-    "showMediaTimeline": string;
-    "showMediaTimelineInfo": string;
     "flagShowTimelineRepliesDescription": string;
     /**
      * フォロー中ユーザーからのフォロリクを自動承認
@@ -1083,10 +1128,10 @@ export interface Locale extends ILocale {
      * 削除しました
      */
     "removed": string;
-    "requestApprovalAreYouSure": string;
-    "removeAreYouSure": string;
-    "deleteAreYouSure": string;
-    "undraftAreYouSure": string;
+    /**
+     * 「{x}」のリクエストを承認しますか?
+     */
+    "requestApprovalAreYouSure": ParameterizedString<"x">;
     /**
      * 「{x}」を削除しますか?
      */
@@ -1095,6 +1140,10 @@ export interface Locale extends ILocale {
      * 「{x}」を削除しますか?
      */
     "deleteAreYouSure": ParameterizedString<"x">;
+    /**
+     * 「{x}」をドラフト解除しますか?
+     */
+    "undraftAreYouSure": ParameterizedString<"x">;
     /**
      * リセットしますか?
      */
@@ -1119,6 +1168,9 @@ export interface Locale extends ILocale {
      * オリジナル画像を保持
      */
     "keepOriginalUploading": string;
+    /**
+     * ホーム投稿で通知する
+     */
     "isNotifyIsHome": string;
     /**
      * 画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。
@@ -1240,7 +1292,13 @@ export interface Locale extends ILocale {
      * ダークモードで使うテーマ
      */
     "themeForDarkMode": string;
+    /**
+     * ゲーミングモード
+     */
     "gamingMode": string;
+    /**
+     * ボタンなどの装飾をいい感じのグラデーションにします。 激しい点滅などはございません。
+     */
     "gamingModeInfo": string;
     /**
      * ライト
@@ -1254,6 +1312,9 @@ export interface Locale extends ILocale {
      * 明るいテーマ
      */
     "lightThemes": string;
+    /**
+     * アイコンなどが正常に表示されない場合にここをクリックしてください。
+     */
     "remoteUserInfoUpdate": string;
     /**
      * 暗いテーマ
@@ -1307,6 +1368,9 @@ export interface Locale extends ILocale {
      * フォルダーを削除
      */
     "deleteFolder": string;
+    /**
+     * フォルダー
+     */
     "Folder": string;
     /**
      * フォルダー
@@ -2221,10 +2285,16 @@ export interface Locale extends ILocale {
      */
     "showFixedPostFormInChannel": string;
     /**
-     * フォローする際、デフォルトで返信をTLに含むようにする
+     * ユーザーのページで最新のノートを表示する
      */
     "FeaturedOrNote": string;
+    /**
+     * ユーザーのページに行ったときにハイライトか最新のノートを表示するかを選択することができます。 オフでハイライト オンで最新のノート です
+     */
     "FeaturedOrNoteInfo": string;
+    /**
+     * フォローする際、デフォルトで返信をTLに含むようにする
+     */
     "withRepliesByDefaultForNewlyFollowed": string;
     /**
      * 新しいノートがあります
@@ -2470,6 +2540,9 @@ export interface Locale extends ILocale {
      * アンケート
      */
     "poll": string;
+    /**
+     * 予約投稿
+     */
     "schedulePost": string;
     /**
      * 内容を隠す
@@ -2563,6 +2636,9 @@ export interface Locale extends ILocale {
      * アクセストークンの発行
      */
     "generateAccessToken": string;
+    /**
+     * アクセストークン
+     */
     "accessToken": string;
     /**
      * 権限
@@ -2700,6 +2776,9 @@ export interface Locale extends ILocale {
      * ログ
      */
     "logs": string;
+    /**
+     * mfm 装飾
+     */
     "mfm": string;
     /**
      * 遅延
@@ -2753,7 +2832,13 @@ export interface Locale extends ILocale {
      * スペースで区切って複数設定できます。
      */
     "setMultipleBySeparatingWithSpace": string;
+    /**
+     * 名前には英数字と_が利用できます。
+     */
     "emojiNameValidation": string;
+    /**
+     * センシティブ
+     */
     "isSensitive": string;
     /**
      * ファイルIDまたはURL
@@ -2815,6 +2900,9 @@ export interface Locale extends ILocale {
      * 送信
      */
     "send": string;
+    /**
+     * ファイル付きのみ
+     */
     "fileAttachedOnly": string;
     /**
      * 対応済みにする
@@ -2944,7 +3032,13 @@ export interface Locale extends ILocale {
      * アンケートに投票した数
      */
     "pollVotesCount": string;
+    /**
+     * タイムラインの絞り込みを保存する
+     */
     "onlyAndWithSave": string;
+    /**
+     * ファイルのみ や リプライのみ などが保存されるようになります
+     */
     "onlyAndWithSaveInfo": string;
     /**
      * アンケートに投票された数
@@ -3430,8 +3524,17 @@ export interface Locale extends ILocale {
      * 低
      */
     "low": string;
+    /**
+     * 一覧
+     */
     "list": string;
+    /**
+     * ゲーミングの光るスピードの調整
+     */
     "GamingSpeedChange": string;
+    /**
+     * 左にすれば早くなる、右にすれば遅くなる。それだけ。
+     */
     "GamingSpeedChangeInfo": string;
     /**
      * メールアドレスの設定がされていません。
@@ -3441,8 +3544,17 @@ export interface Locale extends ILocale {
      * 比率
      */
     "ratio": string;
+    /**
+     * ノートの公開範囲を色付けする
+     */
     "showVisibilityColor": string;
+    /**
+     * 新しい絵文字
+     */
     "newEmojis": string;
+    /**
+     * 申請されている絵文字
+     */
     "draftEmojis": string;
     /**
      * 本文をプレビュー
@@ -4053,9 +4165,12 @@ export interface Locale extends ILocale {
      */
     "manageCustomEmojis": string;
     /**
-     * アバターデコレーションの管理
+     * カスタム絵文字のリクエスト
      */
     "requestCustomEmojis": string;
+    /**
+     * アバターデコレーションの管理
+     */
     "manageAvatarDecorations": string;
     /**
      * これ以上作成することはできません。
@@ -4233,10 +4348,25 @@ export interface Locale extends ILocale {
      * ライセンス
      */
     "license": string;
+    /**
+     * 申請中
+     */
     "requestPending": string;
+    /**
+     * 承認
+     */
     "approval": string;
+    /**
+     * リクエストされている絵文字
+     */
     "requestingEmojis": string;
+    /**
+     * ドラフト
+     */
     "draft": string;
+    /**
+     * ドラフト解除
+     */
     "undrafted": string;
     /**
      * お気に入り解除しますか?
@@ -4306,8 +4436,17 @@ export interface Locale extends ILocale {
      * データセーバー
      */
     "dataSaver": string;
+    /**
+     * モバイルデータ通信でデータセーバーをオンにする
+     */
     "cellularWithDataSaver": string;
+    /**
+     * 究極のデータセーバー
+     */
     "UltimateDataSaver": string;
+    /**
+     * モバイルデータ通信で究極のデータセーバーをオンにする
+     */
     "cellularWithUltimateDataSaver": string;
     /**
      * アカウントの移行
@@ -4685,10 +4824,6 @@ export interface Locale extends ILocale {
      * 相互フォロー
      */
     "mutualFollow": string;
-    /**
-     * ファイル付きのみ
-     */
-    "fileAttachedOnly": string;
     /**
      * TLに他の人への返信を含める
      */
@@ -6365,6 +6500,9 @@ export interface Locale extends ILocale {
              * グローバルタイムラインの閲覧
              */
             "gtlAvailable": string;
+            /**
+             * 絵文字ピッカーのプロファイルの上限数(最大5)
+             */
             "emojiPickerProfileLimit": string;
             /**
              * ローカルタイムラインの閲覧
@@ -6375,10 +6513,16 @@ export interface Locale extends ILocale {
              */
             "canPublicNote": string;
             /**
-             * サーバー招待コードの発行
+             * ノートの編集
              */
             "canEditNote": string;
+            /**
+             * 予約投稿の許可
+             */
             "canScheduleNote": string;
+            /**
+             * サーバー招待コードの発行
+             */
             "canInvite": string;
             /**
              * 招待コードの作成可能数
@@ -6396,6 +6540,9 @@ export interface Locale extends ILocale {
              * カスタム絵文字の管理
              */
             "canManageCustomEmojis": string;
+            /**
+             * カスタム絵文字のリクエスト
+             */
             "canRequestCustomEmojis": string;
             /**
              * アバターデコレーションの管理
@@ -6838,6 +6985,9 @@ export interface Locale extends ILocale {
          * ソースコード
          */
         "source": string;
+        /**
+         * 当フォークのソースコード
+         */
         "forksource": string;
         /**
          * Misskeyを翻訳
@@ -6983,6 +7133,9 @@ export interface Locale extends ILocale {
          * キーワードをスラッシュで囲むと正規表現になります。
          */
         "muteWordsDescription2": string;
+        /**
+         * ミュートされた単語を含むノートを非表示にする
+         */
         "hideMutedNotes": string;
     };
     "_instanceMute": {
@@ -7426,15 +7579,45 @@ export interface Locale extends ILocale {
         "day": string;
     };
     "_timelineTutorial": {
+        /**
+         * Misskeyの使い方
+         */
         "title": string;
-        "step1_1": string;
-        "step1_2": string;
-        "step1_3": string;
+        /**
+         * この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。
+         */
+        "step1_1": ParameterizedString<"name">;
+        /**
+         * タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。
+         */
+        "step1_2": ParameterizedString<"name">;
+        /**
+         * この2つ以外にも、「ソーシャルタイムライン」は ホームTL + ローカルTL のようなもので、 「メディアタイムライン」 には{name}で何かしらのファイル付きで投稿されたノートが流れます。
+         */
+        "step1_3": ParameterizedString<"name">;
+        /**
+         * 試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。
+         */
         "step2_1": string;
-        "step2_2": string;
+        /**
+         * 初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。
+         */
+        "step2_2": ParameterizedString<"name">;
+        /**
+         * 投稿できましたか?
+         */
         "step3_1": string;
+        /**
+         * あなたのノートがタイムラインに表示されていれば成功です。
+         */
         "step3_2": string;
+        /**
+         * ノートには、「リアクション」を付けることができます。
+         */
         "step4_1": string;
+        /**
+         * リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。
+         */
         "step4_2": string;
     };
     "_2fa": {
@@ -8000,7 +8183,13 @@ export interface Locale extends ILocale {
          * 通知
          */
         "notifications": string;
+        /**
+         * ゲーミングモード
+         */
         "gamingMode": string;
+        /**
+         * 反転モード
+         */
         "gyakubariMode": string;
         /**
          * タイムライン
@@ -8497,9 +8686,12 @@ export interface Locale extends ILocale {
          */
         "local": string;
         /**
-         * ソーシャル
+         * メディア
          */
         "media": string;
+        /**
+         * ソーシャル
+         */
         "social": string;
         /**
          * グローバル
@@ -9437,13 +9629,37 @@ export interface Locale extends ILocale {
         };
     };
     "_schedulePost": {
+        /**
+         * 予約投稿一覧
+         */
         "list": string;
+        /**
+         * 日付
+         */
         "postDate": string;
+        /**
+         * 時刻
+         */
         "postTime": string;
+        /**
+         * 端末に設定されているタイムゾーンの時刻で投稿されます。
+         */
         "localTime": string;
+        /**
+         * 予約設定
+         */
         "addSchedule": string;
-        "willBePostedAtX": string;
+        /**
+         * {date}に投稿予約しました。
+         */
+        "willBePostedAtX": ParameterizedString<"date">;
+        /**
+         * 予約投稿を削除しますか?
+         */
         "deleteAreYouSure": string;
+        /**
+         * 予約投稿を削除して編集しますか?
+         */
         "deleteAndEditConfirm": string;
     };
     "_dataSaver": {
diff --git a/packages/misskey-bubble-game/src/game.ts b/packages/misskey-bubble-game/src/game.ts
index e01a011eee..7c99f83959 100644
--- a/packages/misskey-bubble-game/src/game.ts
+++ b/packages/misskey-bubble-game/src/game.ts
@@ -6,7 +6,7 @@
 import { EventEmitter } from 'eventemitter3';
 import * as Matter from 'matter-js';
 import seedrandom from 'seedrandom';
-import { NORAML_MONOS, SQUARE_MONOS, SWEETS_MONOS, YEN_MONOS } from './monos.js';
+import { NORAML_MONOS, PRISMISSKEY_MONOS, SQUARE_MONOS, SWEETS_MONOS, YEN_MONOS } from './monos.js';
 
 export type Mono = {
 	id: string;
@@ -58,7 +58,7 @@ export class DropAndFusionGame extends EventEmitter<{
 	private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
 	private overflowCollider: Matter.Body;
 	private isGameOver = false;
-	private gameMode: 'normal' | 'yen' | 'square' | 'sweets' | 'space';
+	private gameMode: 'normal' | 'yen' | 'square' | 'sweets' | 'space' | 'prismisskey';
 	private rng: () => number;
 	private logs: Log[] = [];
 
@@ -87,6 +87,7 @@ export class DropAndFusionGame extends EventEmitter<{
 			case 'square': return SQUARE_MONOS;
 			case 'sweets': return SWEETS_MONOS;
 			case 'space': return NORAML_MONOS;
+			case 'prismisskey' : return PRISMISSKEY_MONOS;
 		}
 	}
 
diff --git a/packages/misskey-bubble-game/src/monos.ts b/packages/misskey-bubble-game/src/monos.ts
index d1b1b0c5da..eb122c6749 100644
--- a/packages/misskey-bubble-game/src/monos.ts
+++ b/packages/misskey-bubble-game/src/monos.ts
@@ -950,13 +950,14 @@ export const SWEETS_MONOS: Mono[] = [{
 	score: 30,
 	dropCandidate: true,
 }];
+
 const PRISMISSKEY_BASE_SIZE = 28;
 
-const PRISMISSKEY_MONOS: Mono[] = [{
+export const PRISMISSKEY_MONOS: Mono[] = [{
 	id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
 	level: 10,
-	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
-	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
+	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
+	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 	shape: 'custom',
 	vertices: [[
 		{ 'x': 1680, 'y': 270 },
@@ -979,7 +980,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		{ 'x': 1400, 'y': 670 },
 	]],
 	verticesSize: 1800,
-	score: 10000,
+	score: 512,
 	dropCandidate: false,
 }, {
 	id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
@@ -1012,13 +1013,13 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		],
 	],
 	verticesSize: 1300,
-	score: 1000,
+	score: 256,
 	dropCandidate: false,
 }, {
 	id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
 	level: 8,
-	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
-	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
+	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
+	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 	shape: 'custom',
 	vertices: [
 		[
@@ -1046,7 +1047,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		],
 	],
 	verticesSize: 1300,
-	score: 800,
+	score: 128,
 	dropCandidate: false,
 }, {
 	id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
@@ -1062,7 +1063,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 	shape: 'circle',
-	score: 500,
+	score: 32,
 	dropCandidate: false,
 }, {
 	id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
@@ -1129,7 +1130,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 	sizeX: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25,
 	sizeY: PRISMISSKEY_BASE_SIZE * 1.25 * 1.25 * 1.25,
 	shape: 'rectangle',
-	score: 200,
+	score: 8,
 	dropCandidate: true,
 }, {
 	id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
@@ -1167,7 +1168,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		],
 	],
 	verticesSize: 128,
-	score: 100,
+	score: 4,
 	dropCandidate: true,
 }, {
 	id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
@@ -1205,7 +1206,7 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		],
 	],
 	verticesSize: 1536,
-	score: 50,
+	score: 2,
 	dropCandidate: true,
 }, {
 	id: '35e476ee-44bd-4711-ad42-87be245d3efd',
@@ -1242,468 +1243,6 @@ const PRISMISSKEY_MONOS: Mono[] = [{
 		],
 	],
 	verticesSize: 128,
-	score: 10,
+	score: 1,
 	dropCandidate: true,
 }];
-export class DropAndFusionGame extends EventEmitter<{
-	changeScore: (newScore: number) => void;
-	changeCombo: (newCombo: number) => void;
-	changeStock: (newStock: { id: string; mono: Mono }[]) => void;
-	changeHolding: (newHolding: { id: string; mono: Mono } | null) => void;
-	dropped: (x: number) => void;
-	fusioned: (x: number, y: number, nextMono: Mono | null, scoreDelta: number) => void;
-	collision: (energy: number, bodyA: Matter.Body, bodyB: Matter.Body) => void;
-	monoAdded: (mono: Mono) => void;
-	gameOver: () => void;
-}> {
-	private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
-	private COMBO_INTERVAL = 60; // frame
-	public readonly GAME_VERSION = 3;
-	public readonly GAME_WIDTH = 450;
-	public readonly GAME_HEIGHT = 600;
-	public readonly DROP_COOLTIME = 30; // frame
-	public readonly PLAYAREA_MARGIN = 25;
-	private STOCK_MAX = 4;
-	private TICK_DELTA = 1000 / 60; // 60fps
-
-	public frame = 0;
-	public engine: Matter.Engine;
-	private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
-	private overflowCollider: Matter.Body;
-	private isGameOver = false;
-	private gameMode: 'normal' | 'yen' | 'square' | 'sweets' | 'space' | 'prismisskey';
-	private rng: () => number;
-	private logs: Log[] = [];
-
-	/**
-	 * フィールドに出ていて、かつ合体の対象となるアイテム
-	 */
-	private fusionReadyBodyIds: Matter.Body['id'][] = [];
-
-	private gameOverReadyBodyIds: Matter.Body['id'][] = [];
-
-	/**
-	 * fusion予約アイテムのペア
-	 * TODO: これらのモノは光らせるなどの演出をすると視覚的に楽しそう
-	 */
-	private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
-
-	private latestDroppedAt = 0; // frame
-	private latestFusionedAt = 0; // frame
-	private stock: { id: string; mono: Mono }[] = [];
-	private holding: { id: string; mono: Mono } | null = null;
-
-	public get monoDefinitions() {
-		switch (this.gameMode) {
-			case 'normal': return NORAML_MONOS;
-			case 'yen': return YEN_MONOS;
-			case 'square': return SQUARE_MONOS;
-			case 'sweets': return SWEETS_MONOS;
-			case 'prismisskey': return PRISMISSKEY_MONOS;
-			case 'space': return NORAML_MONOS;
-		}
-	}
-
-	private _combo = 0;
-	private get combo() {
-		return this._combo;
-	}
-	private set combo(value: number) {
-		this._combo = value;
-		this.emit('changeCombo', value);
-	}
-
-	private _score = 0;
-	private get score() {
-		return this._score;
-	}
-	private set score(value: number) {
-		this._score = value;
-		this.emit('changeScore', value);
-	}
-
-	private getMonoRenderOptions: null | ((mono: Mono) => Partial<Matter.IBodyRenderOptions>) = null;
-
-	public replayPlaybackRate = 1;
-
-	constructor(env: {
-		seed: string;
-		gameMode: DropAndFusionGame['gameMode'];
-		getMonoRenderOptions?: (mono: Mono) => Partial<Matter.IBodyRenderOptions>;
-	}) {
-		super();
-
-		//#region BIND
-		this.tick = this.tick.bind(this);
-		//#endregion
-
-		this.gameMode = env.gameMode;
-		this.getMonoRenderOptions = env.getMonoRenderOptions ?? null;
-		this.rng = seedrandom(env.seed);
-
-		// sweetsモードは重いため
-		const physicsQualityFactor = this.gameMode === 'sweets' ? 4 : this.PHYSICS_QUALITY_FACTOR;
-		this.engine = Matter.Engine.create({
-			constraintIterations: 2 * physicsQualityFactor,
-			positionIterations: 6 * physicsQualityFactor,
-			velocityIterations: 4 * physicsQualityFactor,
-			gravity: {
-				x: 0,
-				y: this.gameMode === 'space' ? 0.0125 : 1,
-			},
-			timing: {
-				timeScale: 2,
-			},
-			enableSleeping: false,
-		});
-
-		this.engine.world.bodies = [];
-
-		//#region walls
-		const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
-			label: '_wall_',
-			isStatic: true,
-			friction: 0.7,
-			slop: this.gameMode === 'space' ? 0.01 : 0.7,
-			render: {
-				strokeStyle: 'transparent',
-				fillStyle: 'transparent',
-			},
-		};
-
-		const thickness = 100;
-		Matter.Composite.add(this.engine.world, [
-			Matter.Bodies.rectangle(this.GAME_WIDTH / 2, this.GAME_HEIGHT + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_WIDTH, thickness, WALL_OPTIONS),
-			Matter.Bodies.rectangle(this.GAME_WIDTH + (thickness / 2) - this.PLAYAREA_MARGIN, this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS),
-			Matter.Bodies.rectangle(-((thickness / 2) - this.PLAYAREA_MARGIN), this.GAME_HEIGHT / 2, thickness, this.GAME_HEIGHT, WALL_OPTIONS),
-		]);
-		//#endregion
-
-		this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, {
-			label: '_overflow_',
-			isStatic: true,
-			isSensor: true,
-			render: {
-				strokeStyle: 'transparent',
-				fillStyle: 'transparent',
-			},
-		});
-		Matter.Composite.add(this.engine.world, this.overflowCollider);
-	}
-
-	public msToFrame(ms: number) {
-		return Math.round(ms / this.TICK_DELTA);
-	}
-
-	public frameToMs(frame: number) {
-		return frame * this.TICK_DELTA;
-	}
-
-	private createBody(mono: Mono, x: number, y: number) {
-		const options: Matter.IBodyDefinition = {
-			label: mono.id,
-			density: this.gameMode === 'space' ? 0.01 : ((mono.sizeX * mono.sizeY) / 10000),
-			restitution: this.gameMode === 'space' ? 0.5 : 0.2,
-			frictionAir: this.gameMode === 'space' ? 0 : 0.01,
-			friction: this.gameMode === 'space' ? 0.5 : 0.7,
-			frictionStatic: this.gameMode === 'space' ? 0 : 5,
-			slop: this.gameMode === 'space' ? 0.01 : 0.7,
-			//mass: 0,
-			render: this.getMonoRenderOptions ? this.getMonoRenderOptions(mono) : undefined,
-		};
-		if (mono.shape === 'circle') {
-			return Matter.Bodies.circle(x, y, mono.sizeX / 2, options);
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-		} else if (mono.shape === 'rectangle') {
-			return Matter.Bodies.rectangle(x, y, mono.sizeX, mono.sizeY, options);
-		} else if (mono.shape === 'custom') {
-			return Matter.Bodies.fromVertices(x, y, mono.vertices!.map(i => i.map(j => ({
-				x: (j.x / mono.verticesSize!) * mono.sizeX,
-				y: (j.y / mono.verticesSize!) * mono.sizeY,
-			}))), options);
-		} else {
-			throw new Error('unrecognized shape');
-		}
-	}
-
-	private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
-		if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) {
-			this.combo++;
-		} else {
-			this.combo = 1;
-		}
-		this.latestFusionedAt = this.frame;
-
-		const newX = (bodyA.position.x + bodyB.position.x) / 2;
-		const newY = (bodyA.position.y + bodyB.position.y) / 2;
-
-		this.fusionReadyBodyIds = this.fusionReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
-		this.gameOverReadyBodyIds = this.gameOverReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
-		Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
-
-		const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!;
-		const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1) ?? null;
-
-		if (nextMono) {
-			const body = this.createBody(nextMono, newX, newY);
-			Matter.Composite.add(this.engine.world, body);
-
-			// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
-			this.tickCallbackQueue.push({
-				frame: this.frame + this.msToFrame(100),
-				callback: () => {
-					this.fusionReadyBodyIds.push(body.id);
-				},
-			});
-
-			this.emit('monoAdded', nextMono);
-		}
-
-		const hasComboBonus = this.gameMode !== 'yen' && this.gameMode !== 'sweets';
-		const comboBonus = hasComboBonus ? 1 + ((this.combo - 1) / 5) : 1;
-		const additionalScore = Math.round(currentMono.score * comboBonus);
-		this.score += additionalScore;
-
-		this.emit('fusioned', newX, newY, nextMono, additionalScore);
-	}
-
-	private onCollision(event: Matter.IEventCollision<Matter.Engine>) {
-		for (const pairs of event.pairs) {
-			const { bodyA, bodyB } = pairs;
-
-			const shouldFusion = (bodyA.label === bodyB.label) &&
-				!this.fusionReservedPairs.some(x =>
-					x.bodyA.id === bodyA.id ||
-					x.bodyA.id === bodyB.id ||
-					x.bodyB.id === bodyA.id ||
-					x.bodyB.id === bodyB.id);
-
-			if (shouldFusion) {
-				if (this.fusionReadyBodyIds.includes(bodyA.id) && this.fusionReadyBodyIds.includes(bodyB.id)) {
-					this.fusion(bodyA, bodyB);
-				} else {
-					this.fusionReservedPairs.push({ bodyA, bodyB });
-					this.tickCallbackQueue.push({
-						frame: this.frame + this.msToFrame(100),
-						callback: () => {
-							this.fusionReservedPairs = this.fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
-							this.fusion(bodyA, bodyB);
-						},
-					});
-				}
-			} else {
-				const energy = pairs.collision.depth;
-
-				if (bodyA.label === '_overflow_' || bodyB.label === '_overflow_') continue;
-
-				if (bodyA.label !== '_wall_' && bodyB.label !== '_wall_') {
-					if (!this.gameOverReadyBodyIds.includes(bodyA.id)) this.gameOverReadyBodyIds.push(bodyA.id);
-					if (!this.gameOverReadyBodyIds.includes(bodyB.id)) this.gameOverReadyBodyIds.push(bodyB.id);
-				}
-
-				this.emit('collision', energy, bodyA, bodyB);
-			}
-		}
-	}
-
-	private onCollisionActive(event: Matter.IEventCollision<Matter.Engine>) {
-		for (const pairs of event.pairs) {
-			const { bodyA, bodyB } = pairs;
-
-			// ハコからあふれたかどうかの判定
-			if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) {
-				if (this.gameOverReadyBodyIds.includes(bodyA.id) || this.gameOverReadyBodyIds.includes(bodyB.id)) {
-					this.gameOver();
-					break;
-				}
-				continue;
-			}
-		}
-	}
-
-	public surrender() {
-		this.logs.push({
-			frame: this.frame,
-			operation: 'surrender',
-		});
-
-		this.gameOver();
-	}
-
-	private gameOver() {
-		this.isGameOver = true;
-		this.emit('gameOver');
-	}
-
-	public start() {
-		for (let i = 0; i < this.STOCK_MAX; i++) {
-			this.stock.push({
-				id: this.rng().toString(),
-				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
-			});
-		}
-		this.emit('changeStock', this.stock);
-
-		Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this));
-		Matter.Events.on(this.engine, 'collisionActive', this.onCollisionActive.bind(this));
-	}
-
-	public getLogs() {
-		return this.logs;
-	}
-
-	public tick() {
-		this.frame++;
-
-		if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) {
-			this.combo = 0;
-		}
-
-		this.tickCallbackQueue = this.tickCallbackQueue.filter(x => {
-			if (x.frame === this.frame) {
-				x.callback();
-				return false;
-			} else {
-				return true;
-			}
-		});
-
-		Matter.Engine.update(this.engine, this.TICK_DELTA);
-
-		const hasNextTick = !this.isGameOver;
-
-		return hasNextTick;
-	}
-
-	public getActiveMonos() {
-		return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined);
-	}
-
-	public drop(_x: number) {
-		if (this.isGameOver) return;
-		if (this.frame - this.latestDroppedAt < this.DROP_COOLTIME) return;
-
-		const head = this.stock.shift()!;
-		this.stock.push({
-			id: this.rng().toString(),
-			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
-		});
-		this.emit('changeStock', this.stock);
-
-		const inputX = Math.round(_x);
-		const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.sizeX / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.sizeX / 2), inputX));
-		const body = this.createBody(head.mono, x, 50 + head.mono.sizeY / 2);
-		this.logs.push({
-			frame: this.frame,
-			operation: 'drop',
-			x: inputX,
-		});
-
-		// add force
-		if (this.gameMode === 'space') {
-			Matter.Body.applyForce(body, body.position, {
-				x: 0,
-				y: (Math.PI * head.mono.sizeX * head.mono.sizeY) / 65536,
-			});
-		}
-
-		Matter.Composite.add(this.engine.world, body);
-
-		this.fusionReadyBodyIds.push(body.id);
-		this.latestDroppedAt = this.frame;
-
-		this.emit('dropped', x);
-		this.emit('monoAdded', head.mono);
-	}
-
-	public hold() {
-		if (this.isGameOver) return;
-
-		this.logs.push({
-			frame: this.frame,
-			operation: 'hold',
-		});
-
-		if (this.holding) {
-			const head = this.stock.shift()!;
-			this.stock.unshift(this.holding);
-			this.holding = head;
-			this.emit('changeHolding', this.holding);
-			this.emit('changeStock', this.stock);
-		} else {
-			const head = this.stock.shift()!;
-			this.holding = head;
-			this.stock.push({
-				id: this.rng().toString(),
-				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
-			});
-			this.emit('changeHolding', this.holding);
-			this.emit('changeStock', this.stock);
-		}
-	}
-
-	public static serializeLogs(logs: Log[]) {
-		const _logs: number[][] = [];
-
-		for (let i = 0; i < logs.length; i++) {
-			const log = logs[i];
-			const frameDelta = i === 0 ? log.frame : log.frame - logs[i - 1].frame;
-
-			switch (log.operation) {
-				case 'drop':
-					_logs.push([frameDelta, 0, log.x]);
-					break;
-				case 'hold':
-					_logs.push([frameDelta, 1]);
-					break;
-				case 'surrender':
-					_logs.push([frameDelta, 2]);
-					break;
-			}
-		}
-
-		return _logs;
-	}
-
-	public static deserializeLogs(logs: number[][]) {
-		const _logs: Log[] = [];
-
-		let frame = 0;
-
-		for (const log of logs) {
-			const frameDelta = log[0];
-			frame += frameDelta;
-
-			const operation = log[1];
-
-			switch (operation) {
-				case 0:
-					_logs.push({
-						frame,
-						operation: 'drop',
-						x: log[2],
-					});
-					break;
-				case 1:
-					_logs.push({
-						frame,
-						operation: 'hold',
-					});
-					break;
-				case 2:
-					_logs.push({
-						frame,
-						operation: 'surrender',
-					});
-					break;
-			}
-		}
-
-		return _logs;
-	}
-
-	public dispose() {
-		Matter.World.clear(this.engine.world, false);
-		Matter.Engine.clear(this.engine);
-	}
-}