Compare commits
	
		
			13 Commits
		
	
	
		
			2024.8.0-r
			...
			bubble-gam
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7627da62ee | ||
| 
						 | 
					31a25f9332 | ||
| 
						 | 
					f85b9107e2 | ||
| 
						 | 
					9181e3db7d | ||
| 
						 | 
					028800c0bb | ||
| 
						 | 
					e33c8bf43a | ||
| 
						 | 
					4918923635 | ||
| 
						 | 
					eda727c487 | ||
| 
						 | 
					333fac00c8 | ||
| 
						 | 
					aa7dd98119 | ||
| 
						 | 
					c0d28358d6 | ||
| 
						 | 
					17f368e228 | ||
| 
						 | 
					14d4ffaa36 | 
							
								
								
									
										3
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1195,6 +1195,9 @@ export interface Locale {
 | 
				
			|||||||
    "bubbleGame": string;
 | 
					    "bubbleGame": string;
 | 
				
			||||||
    "sfx": string;
 | 
					    "sfx": string;
 | 
				
			||||||
    "soundWillBePlayed": string;
 | 
					    "soundWillBePlayed": string;
 | 
				
			||||||
 | 
					    "showReplay": string;
 | 
				
			||||||
 | 
					    "replay": string;
 | 
				
			||||||
 | 
					    "replaying": string;
 | 
				
			||||||
    "_announcement": {
 | 
					    "_announcement": {
 | 
				
			||||||
        "forExistingUsers": string;
 | 
					        "forExistingUsers": string;
 | 
				
			||||||
        "forExistingUsersDescription": string;
 | 
					        "forExistingUsersDescription": string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1192,6 +1192,9 @@ enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
 | 
				
			|||||||
bubbleGame: "バブルゲーム"
 | 
					bubbleGame: "バブルゲーム"
 | 
				
			||||||
sfx: "効果音"
 | 
					sfx: "効果音"
 | 
				
			||||||
soundWillBePlayed: "サウンドが再生されます"
 | 
					soundWillBePlayed: "サウンドが再生されます"
 | 
				
			||||||
 | 
					showReplay: "リプレイを見る"
 | 
				
			||||||
 | 
					replay: "リプレイ"
 | 
				
			||||||
 | 
					replaying: "リプレイ中"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_announcement:
 | 
					_announcement:
 | 
				
			||||||
  forExistingUsers: "既存ユーザーのみ"
 | 
					  forExistingUsers: "既存ユーザーのみ"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/gameover.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/gameover.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -58,6 +58,7 @@
 | 
				
			|||||||
		"rollup": "4.9.1",
 | 
							"rollup": "4.9.1",
 | 
				
			||||||
		"sanitize-html": "2.11.0",
 | 
							"sanitize-html": "2.11.0",
 | 
				
			||||||
		"sass": "1.69.5",
 | 
							"sass": "1.69.5",
 | 
				
			||||||
 | 
							"seedrandom": "^3.0.5",
 | 
				
			||||||
		"shiki": "0.14.7",
 | 
							"shiki": "0.14.7",
 | 
				
			||||||
		"strict-event-emitter-types": "2.0.0",
 | 
							"strict-event-emitter-types": "2.0.0",
 | 
				
			||||||
		"textarea-caret": "3.1.0",
 | 
							"textarea-caret": "3.1.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: gameOver }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
 | 
								<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
 | 
				
			||||||
				<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 | 
									<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 | 
				
			||||||
				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
 | 
									<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
 | 
				
			||||||
				<canvas ref="canvasEl" :class="$style.canvas"/>
 | 
									<canvas ref="canvasEl" :class="$style.canvas"/>
 | 
				
			||||||
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
				>
 | 
									>
 | 
				
			||||||
					<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
 | 
										<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
 | 
				
			||||||
				</Transition>
 | 
									</Transition>
 | 
				
			||||||
				<div :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }">
 | 
									<div v-if="!isGameOver && !replaying" :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }">
 | 
				
			||||||
					<!--<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>-->
 | 
										<!--<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>-->
 | 
				
			||||||
					<Transition
 | 
										<Transition
 | 
				
			||||||
						:enterActiveClass="$style.transition_picked_enterActive"
 | 
											:enterActiveClass="$style.transition_picked_enterActive"
 | 
				
			||||||
@@ -91,16 +91,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
						<div :class="$style.dropGuide"/>
 | 
											<div :class="$style.dropGuide"/>
 | 
				
			||||||
					</template>
 | 
										</template>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div v-if="gameOver" :class="$style.gameOverLabel">
 | 
									<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
 | 
				
			||||||
					<div class="_gaps_s">
 | 
										<div class="_gaps_s">
 | 
				
			||||||
						<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
 | 
											<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
 | 
				
			||||||
						<div>SCORE: <MkNumber :value="score"/></div>
 | 
											<div>SCORE: <MkNumber :value="score"/></div>
 | 
				
			||||||
						<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
 | 
											<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
 | 
				
			||||||
						<div class="_buttonsCenter">
 | 
					 | 
				
			||||||
							<MkButton primary rounded @click="restart">Restart</MkButton>
 | 
					 | 
				
			||||||
							<MkButton primary rounded @click="share">Share</MkButton>
 | 
					 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
									<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div v-if="replaying" style="display: flex;">
 | 
				
			||||||
 | 
									<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
 | 
				
			||||||
 | 
										<div :class="$style.frameInner">
 | 
				
			||||||
 | 
											<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END REPLAY</MkButton>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div v-if="isGameOver" :class="$style.frame">
 | 
				
			||||||
 | 
									<div :class="$style.frameInner">
 | 
				
			||||||
 | 
										<div class="_buttonsCenter">
 | 
				
			||||||
 | 
											<MkButton primary rounded @click="end">{{ i18n.ts.done }}</MkButton>
 | 
				
			||||||
 | 
											<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton>
 | 
				
			||||||
 | 
											<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton>
 | 
				
			||||||
 | 
											<MkButton rounded @click="exportLog">Copy replay data</MkButton>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div style="display: flex;">
 | 
								<div style="display: flex;">
 | 
				
			||||||
@@ -139,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div :class="$style.frame">
 | 
								<div :class="$style.frame">
 | 
				
			||||||
				<div :class="$style.frameInner">
 | 
									<div :class="$style.frameInner">
 | 
				
			||||||
					<MkButton @click="restart">Restart</MkButton>
 | 
										<MkButton danger @click="surrender">Retry</MkButton>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
@@ -168,6 +182,7 @@ import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js';
 | 
				
			|||||||
import * as sound from '@/scripts/sound.js';
 | 
					import * as sound from '@/scripts/sound.js';
 | 
				
			||||||
import MkRange from '@/components/MkRange.vue';
 | 
					import MkRange from '@/components/MkRange.vue';
 | 
				
			||||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
					import MkSwitch from '@/components/MkSwitch.vue';
 | 
				
			||||||
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NORMAL_BASE_SIZE = 30;
 | 
					const NORMAL_BASE_SIZE = 30;
 | 
				
			||||||
const NORAML_MONOS: Mono[] = [{
 | 
					const NORAML_MONOS: Mono[] = [{
 | 
				
			||||||
@@ -401,6 +416,8 @@ const GAME_HEIGHT = 600;
 | 
				
			|||||||
let viewScale = 1;
 | 
					let viewScale = 1;
 | 
				
			||||||
let game: DropAndFusionGame;
 | 
					let game: DropAndFusionGame;
 | 
				
			||||||
let containerElRect: DOMRect | null = null;
 | 
					let containerElRect: DOMRect | null = null;
 | 
				
			||||||
 | 
					let seed: string;
 | 
				
			||||||
 | 
					let logs: ReturnType<DropAndFusionGame['getLogs']> | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const containerEl = shallowRef<HTMLElement>();
 | 
					const containerEl = shallowRef<HTMLElement>();
 | 
				
			||||||
const canvasEl = shallowRef<HTMLCanvasElement>();
 | 
					const canvasEl = shallowRef<HTMLCanvasElement>();
 | 
				
			||||||
@@ -414,22 +431,25 @@ const comboPrev = ref(0);
 | 
				
			|||||||
const maxCombo = ref(0);
 | 
					const maxCombo = ref(0);
 | 
				
			||||||
const dropReady = ref(true);
 | 
					const dropReady = ref(true);
 | 
				
			||||||
const gameMode = ref<'normal' | 'square'>('normal');
 | 
					const gameMode = ref<'normal' | 'square'>('normal');
 | 
				
			||||||
const gameOver = ref(false);
 | 
					const isGameOver = ref(false);
 | 
				
			||||||
const gameStarted = ref(false);
 | 
					const gameStarted = ref(false);
 | 
				
			||||||
const highScore = ref<number | null>(null);
 | 
					const highScore = ref<number | null>(null);
 | 
				
			||||||
const showConfig = ref(false);
 | 
					const showConfig = ref(false);
 | 
				
			||||||
 | 
					const replaying = ref(false);
 | 
				
			||||||
const mute = ref(false);
 | 
					const mute = ref(false);
 | 
				
			||||||
const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
 | 
					const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
 | 
				
			||||||
const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
 | 
					const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onClick(ev: MouseEvent) {
 | 
					function onClick(ev: MouseEvent) {
 | 
				
			||||||
	if (!containerElRect) return;
 | 
						if (!containerElRect) return;
 | 
				
			||||||
 | 
						if (replaying.value) return;
 | 
				
			||||||
	const x = (ev.clientX - containerElRect.left) / viewScale;
 | 
						const x = (ev.clientX - containerElRect.left) / viewScale;
 | 
				
			||||||
	game.drop(x);
 | 
						game.drop(x);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onTouchend(ev: TouchEvent) {
 | 
					function onTouchend(ev: TouchEvent) {
 | 
				
			||||||
	if (!containerElRect) return;
 | 
						if (!containerElRect) return;
 | 
				
			||||||
 | 
						if (replaying.value) return;
 | 
				
			||||||
	const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale;
 | 
						const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale;
 | 
				
			||||||
	game.drop(x);
 | 
						game.drop(x);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -454,9 +474,18 @@ function hold() {
 | 
				
			|||||||
	game.hold();
 | 
						game.hold();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function restart() {
 | 
					async function surrender() {
 | 
				
			||||||
 | 
						const { canceled } = await os.confirm({
 | 
				
			||||||
 | 
							type: 'warning',
 | 
				
			||||||
 | 
							text: i18n.ts.areYouSure,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						game.surrender();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function end() {
 | 
				
			||||||
	game.dispose();
 | 
						game.dispose();
 | 
				
			||||||
	gameOver.value = false;
 | 
						isGameOver.value = false;
 | 
				
			||||||
	currentPick.value = null;
 | 
						currentPick.value = null;
 | 
				
			||||||
	dropReady.value = true;
 | 
						dropReady.value = true;
 | 
				
			||||||
	stock.value = [];
 | 
						stock.value = [];
 | 
				
			||||||
@@ -467,6 +496,45 @@ function restart() {
 | 
				
			|||||||
	gameStarted.value = false;
 | 
						gameStarted.value = false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function replay() {
 | 
				
			||||||
 | 
						replaying.value = true;
 | 
				
			||||||
 | 
						game.dispose();
 | 
				
			||||||
 | 
						game = new DropAndFusionGame({
 | 
				
			||||||
 | 
							width: GAME_WIDTH,
 | 
				
			||||||
 | 
							height: GAME_HEIGHT,
 | 
				
			||||||
 | 
							canvas: canvasEl.value!,
 | 
				
			||||||
 | 
							seed: seed,
 | 
				
			||||||
 | 
							sfxVolume: mute.value ? 0 : sfxVolume.value,
 | 
				
			||||||
 | 
							...(
 | 
				
			||||||
 | 
								gameMode.value === 'normal' ? {
 | 
				
			||||||
 | 
									monoDefinitions: NORAML_MONOS,
 | 
				
			||||||
 | 
								} : {
 | 
				
			||||||
 | 
									monoDefinitions: SQUARE_MONOS,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						attachGameEvents();
 | 
				
			||||||
 | 
						os.promiseDialog(game.load(), async () => {
 | 
				
			||||||
 | 
							game.start(logs!);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function endReplay() {
 | 
				
			||||||
 | 
						replaying.value = false;
 | 
				
			||||||
 | 
						game.dispose();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function exportLog() {
 | 
				
			||||||
 | 
						if (!logs) return;
 | 
				
			||||||
 | 
						const data = JSON.stringify({
 | 
				
			||||||
 | 
							seed: seed,
 | 
				
			||||||
 | 
							date: new Date().toISOString(),
 | 
				
			||||||
 | 
							logs: logs,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						copyToClipboard(data);
 | 
				
			||||||
 | 
						os.success();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function attachGameEvents() {
 | 
					function attachGameEvents() {
 | 
				
			||||||
	game.addListener('changeScore', value => {
 | 
						game.addListener('changeScore', value => {
 | 
				
			||||||
		score.value = value;
 | 
							score.value = value;
 | 
				
			||||||
@@ -492,9 +560,11 @@ function attachGameEvents() {
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	game.addListener('dropped', () => {
 | 
						game.addListener('dropped', () => {
 | 
				
			||||||
 | 
							if (replaying.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		dropReady.value = false;
 | 
							dropReady.value = false;
 | 
				
			||||||
		window.setTimeout(() => {
 | 
							window.setTimeout(() => {
 | 
				
			||||||
			if (!gameOver.value) {
 | 
								if (!isGameOver.value) {
 | 
				
			||||||
				dropReady.value = true;
 | 
									dropReady.value = true;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}, game.DROP_INTERVAL);
 | 
							}, game.DROP_INTERVAL);
 | 
				
			||||||
@@ -511,6 +581,8 @@ function attachGameEvents() {
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	game.addListener('monoAdded', (mono) => {
 | 
						game.addListener('monoAdded', (mono) => {
 | 
				
			||||||
 | 
							if (replaying.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 実績関連
 | 
							// 実績関連
 | 
				
			||||||
		if (mono.level === 10) {
 | 
							if (mono.level === 10) {
 | 
				
			||||||
			claimAchievement('bubbleGameExplodingHead');
 | 
								claimAchievement('bubbleGameExplodingHead');
 | 
				
			||||||
@@ -523,9 +595,15 @@ function attachGameEvents() {
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	game.addListener('gameOver', () => {
 | 
						game.addListener('gameOver', () => {
 | 
				
			||||||
 | 
							if (replaying.value) {
 | 
				
			||||||
 | 
								endReplay();
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logs = game.getLogs();
 | 
				
			||||||
		currentPick.value = null;
 | 
							currentPick.value = null;
 | 
				
			||||||
		dropReady.value = false;
 | 
							dropReady.value = false;
 | 
				
			||||||
		gameOver.value = true;
 | 
							isGameOver.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (score.value > (highScore.value ?? 0)) {
 | 
							if (score.value > (highScore.value ?? 0)) {
 | 
				
			||||||
			highScore.value = score.value;
 | 
								highScore.value = score.value;
 | 
				
			||||||
@@ -551,10 +629,13 @@ async function start() {
 | 
				
			|||||||
		highScore.value = null;
 | 
							highScore.value = null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						seed = Date.now().toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	game = new DropAndFusionGame({
 | 
						game = new DropAndFusionGame({
 | 
				
			||||||
		width: GAME_WIDTH,
 | 
							width: GAME_WIDTH,
 | 
				
			||||||
		height: GAME_HEIGHT,
 | 
							height: GAME_HEIGHT,
 | 
				
			||||||
		canvas: canvasEl.value!,
 | 
							canvas: canvasEl.value!,
 | 
				
			||||||
 | 
							seed: seed,
 | 
				
			||||||
		sfxVolume: mute.value ? 0 : sfxVolume.value,
 | 
							sfxVolume: mute.value ? 0 : sfxVolume.value,
 | 
				
			||||||
		...(
 | 
							...(
 | 
				
			||||||
			gameMode.value === 'normal' ? {
 | 
								gameMode.value === 'normal' ? {
 | 
				
			||||||
@@ -690,7 +771,7 @@ useInterval(() => {
 | 
				
			|||||||
}, 1000, { immediate: false, afterMounted: true });
 | 
					}, 1000, { immediate: false, afterMounted: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onDeactivated(() => {
 | 
					onDeactivated(() => {
 | 
				
			||||||
	restart();
 | 
						end();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
definePageMetadata({
 | 
					definePageMetadata({
 | 
				
			||||||
@@ -922,6 +1003,28 @@ definePageMetadata({
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.replayIndicator {
 | 
				
			||||||
 | 
						position: absolute;
 | 
				
			||||||
 | 
						z-index: 10;
 | 
				
			||||||
 | 
						left: 10px;
 | 
				
			||||||
 | 
						bottom: 10px;
 | 
				
			||||||
 | 
						padding: 6px 8px;
 | 
				
			||||||
 | 
						color: #f00;
 | 
				
			||||||
 | 
						background: #0008;
 | 
				
			||||||
 | 
						border-radius: 6px;
 | 
				
			||||||
 | 
						pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.replayIndicatorText {
 | 
				
			||||||
 | 
						animation: replayIndicator-blink 2s infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes replayIndicator-blink {
 | 
				
			||||||
 | 
						0% { opacity: 1; }
 | 
				
			||||||
 | 
						50% { opacity: 0; }
 | 
				
			||||||
 | 
						100% { opacity: 1; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes currentMonoArrow {
 | 
					@keyframes currentMonoArrow {
 | 
				
			||||||
	0% { transform: translateY(0); }
 | 
						0% { transform: translateY(0); }
 | 
				
			||||||
	25% { transform: translateY(-8px); }
 | 
						25% { transform: translateY(-8px); }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { EventEmitter } from 'eventemitter3';
 | 
					import { EventEmitter } from 'eventemitter3';
 | 
				
			||||||
import * as Matter from 'matter-js';
 | 
					import * as Matter from 'matter-js';
 | 
				
			||||||
 | 
					import seedrandom from 'seedrandom';
 | 
				
			||||||
import * as sound from '@/scripts/sound.js';
 | 
					import * as sound from '@/scripts/sound.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Mono = {
 | 
					export type Mono = {
 | 
				
			||||||
@@ -20,6 +21,18 @@ export type Mono = {
 | 
				
			|||||||
	spriteScale: number;
 | 
						spriteScale: number;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Log = {
 | 
				
			||||||
 | 
						frame: number;
 | 
				
			||||||
 | 
						operation: 'drop';
 | 
				
			||||||
 | 
						x: number;
 | 
				
			||||||
 | 
					} | {
 | 
				
			||||||
 | 
						frame: number;
 | 
				
			||||||
 | 
						operation: 'hold';
 | 
				
			||||||
 | 
					} | {
 | 
				
			||||||
 | 
						frame: number;
 | 
				
			||||||
 | 
						operation: 'surrender';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class DropAndFusionGame extends EventEmitter<{
 | 
					export class DropAndFusionGame extends EventEmitter<{
 | 
				
			||||||
	changeScore: (newScore: number) => void;
 | 
						changeScore: (newScore: number) => void;
 | 
				
			||||||
	changeCombo: (newCombo: number) => void;
 | 
						changeCombo: (newCombo: number) => void;
 | 
				
			||||||
@@ -35,18 +48,23 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
	public readonly DROP_INTERVAL = 500;
 | 
						public readonly DROP_INTERVAL = 500;
 | 
				
			||||||
	public readonly PLAYAREA_MARGIN = 25;
 | 
						public readonly PLAYAREA_MARGIN = 25;
 | 
				
			||||||
	private STOCK_MAX = 4;
 | 
						private STOCK_MAX = 4;
 | 
				
			||||||
 | 
						private TICK_DELTA = 1000 / 60; // 60fps
 | 
				
			||||||
	private loaded = false;
 | 
						private loaded = false;
 | 
				
			||||||
 | 
						private frame = 0;
 | 
				
			||||||
	private engine: Matter.Engine;
 | 
						private engine: Matter.Engine;
 | 
				
			||||||
	private render: Matter.Render;
 | 
						private render: Matter.Render;
 | 
				
			||||||
	private runner: Matter.Runner;
 | 
						private tickRaf: ReturnType<typeof requestAnimationFrame> | null = null;
 | 
				
			||||||
 | 
						private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
 | 
				
			||||||
	private overflowCollider: Matter.Body;
 | 
						private overflowCollider: Matter.Body;
 | 
				
			||||||
	private isGameOver = false;
 | 
						private isGameOver = false;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	private gameWidth: number;
 | 
						private gameWidth: number;
 | 
				
			||||||
	private gameHeight: number;
 | 
						private gameHeight: number;
 | 
				
			||||||
	private monoDefinitions: Mono[] = [];
 | 
						private monoDefinitions: Mono[] = [];
 | 
				
			||||||
	private monoTextures: Record<string, Blob> = {};
 | 
						private monoTextures: Record<string, Blob> = {};
 | 
				
			||||||
	private monoTextureUrls: Record<string, string> = {};
 | 
						private monoTextureUrls: Record<string, string> = {};
 | 
				
			||||||
 | 
						private rng: () => number;
 | 
				
			||||||
 | 
						private logs: Log[] = [];
 | 
				
			||||||
 | 
						private replaying = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private sfxVolume = 1;
 | 
						private sfxVolume = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,13 +105,17 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
		width: number;
 | 
							width: number;
 | 
				
			||||||
		height: number;
 | 
							height: number;
 | 
				
			||||||
		monoDefinitions: Mono[];
 | 
							monoDefinitions: Mono[];
 | 
				
			||||||
 | 
							seed: string;
 | 
				
			||||||
		sfxVolume?: number;
 | 
							sfxVolume?: number;
 | 
				
			||||||
	}) {
 | 
						}) {
 | 
				
			||||||
		super();
 | 
							super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.tick = this.tick.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.gameWidth = opts.width;
 | 
							this.gameWidth = opts.width;
 | 
				
			||||||
		this.gameHeight = opts.height;
 | 
							this.gameHeight = opts.height;
 | 
				
			||||||
		this.monoDefinitions = opts.monoDefinitions;
 | 
							this.monoDefinitions = opts.monoDefinitions;
 | 
				
			||||||
 | 
							this.rng = seedrandom(opts.seed);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (opts.sfxVolume) {
 | 
							if (opts.sfxVolume) {
 | 
				
			||||||
			this.sfxVolume = opts.sfxVolume;
 | 
								this.sfxVolume = opts.sfxVolume;
 | 
				
			||||||
@@ -129,9 +151,6 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		Matter.Render.run(this.render);
 | 
							Matter.Render.run(this.render);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.runner = Matter.Runner.create();
 | 
					 | 
				
			||||||
		Matter.Runner.run(this.runner, this.engine);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.engine.world.bodies = [];
 | 
							this.engine.world.bodies = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//#region walls
 | 
							//#region walls
 | 
				
			||||||
@@ -223,9 +242,12 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
			Matter.Composite.add(this.engine.world, body);
 | 
								Matter.Composite.add(this.engine.world, body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
 | 
								// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
 | 
				
			||||||
			window.setTimeout(() => {
 | 
								this.tickCallbackQueue.push({
 | 
				
			||||||
 | 
									frame: this.frame + 6,
 | 
				
			||||||
 | 
									callback: () => {
 | 
				
			||||||
					this.activeBodyIds.push(body.id);
 | 
										this.activeBodyIds.push(body.id);
 | 
				
			||||||
			}, 100);
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const comboBonus = 1 + ((this.combo - 1) / 5);
 | 
								const comboBonus = 1 + ((this.combo - 1) / 5);
 | 
				
			||||||
			const additionalScore = Math.round(currentMono.score * comboBonus);
 | 
								const additionalScore = Math.round(currentMono.score * comboBonus);
 | 
				
			||||||
@@ -244,7 +266,7 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			//const VELOCITY = 30;
 | 
								//const VELOCITY = 30;
 | 
				
			||||||
			//for (let i = 0; i < 10; i++) {
 | 
								//for (let i = 0; i < 10; i++) {
 | 
				
			||||||
			//	const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2)));
 | 
								//	const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(this.rng() * 3)))!, x + ((this.rng() * VELOCITY) - (VELOCITY / 2)), y + ((this.rng() * VELOCITY) - (VELOCITY / 2)));
 | 
				
			||||||
			//	Matter.Composite.add(world, body);
 | 
								//	Matter.Composite.add(world, body);
 | 
				
			||||||
			//	bodies.push(body);
 | 
								//	bodies.push(body);
 | 
				
			||||||
			//}
 | 
								//}
 | 
				
			||||||
@@ -255,10 +277,25 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public surrender() {
 | 
				
			||||||
 | 
							this.logs.push({
 | 
				
			||||||
 | 
								frame: this.frame,
 | 
				
			||||||
 | 
								operation: 'surrender',
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.gameOver();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private gameOver() {
 | 
						private gameOver() {
 | 
				
			||||||
		this.isGameOver = true;
 | 
							this.isGameOver = true;
 | 
				
			||||||
		Matter.Runner.stop(this.runner);
 | 
							if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf);
 | 
				
			||||||
 | 
							this.tickRaf = null;
 | 
				
			||||||
		this.emit('gameOver');
 | 
							this.emit('gameOver');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO: 効果音再生はコンポーネント側の責務なので移動する
 | 
				
			||||||
 | 
							sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', {
 | 
				
			||||||
 | 
								volume: this.sfxVolume,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/** テクスチャをすべてキャッシュする */
 | 
						/** テクスチャをすべてキャッシュする */
 | 
				
			||||||
@@ -292,13 +329,14 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
		return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this)));
 | 
							return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this)));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public start() {
 | 
						public start(logs?: Log[]) {
 | 
				
			||||||
		if (!this.loaded) throw new Error('game is not loaded yet');
 | 
							if (!this.loaded) throw new Error('game is not loaded yet');
 | 
				
			||||||
 | 
							if (logs) this.replaying = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (let i = 0; i < this.STOCK_MAX; i++) {
 | 
							for (let i = 0; i < this.STOCK_MAX; i++) {
 | 
				
			||||||
			this.stock.push({
 | 
								this.stock.push({
 | 
				
			||||||
				id: Math.random().toString(),
 | 
									id: this.rng().toString(),
 | 
				
			||||||
				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
									mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.emit('changeStock', this.stock);
 | 
							this.emit('changeStock', this.stock);
 | 
				
			||||||
@@ -327,10 +365,13 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
						this.fusion(bodyA, bodyB);
 | 
											this.fusion(bodyA, bodyB);
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						fusionReservedPairs.push({ bodyA, bodyB });
 | 
											fusionReservedPairs.push({ bodyA, bodyB });
 | 
				
			||||||
						window.setTimeout(() => {
 | 
											this.tickCallbackQueue.push({
 | 
				
			||||||
 | 
												frame: this.frame + 6,
 | 
				
			||||||
 | 
												callback: () => {
 | 
				
			||||||
								fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
 | 
													fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
 | 
				
			||||||
								this.fusion(bodyA, bodyB);
 | 
													this.fusion(bodyA, bodyB);
 | 
				
			||||||
						}, 100);
 | 
												},
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					const energy = pairs.collision.depth;
 | 
										const energy = pairs.collision.depth;
 | 
				
			||||||
@@ -354,6 +395,69 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
				this.combo = 0;
 | 
									this.combo = 0;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}, 500);
 | 
							}, 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (logs) {
 | 
				
			||||||
 | 
								const playTick = () => {
 | 
				
			||||||
 | 
									this.frame++;
 | 
				
			||||||
 | 
									const log = logs.find(x => x.frame === this.frame - 1);
 | 
				
			||||||
 | 
									if (log) {
 | 
				
			||||||
 | 
										switch (log.operation) {
 | 
				
			||||||
 | 
											case 'drop': {
 | 
				
			||||||
 | 
												this.drop(log.x);
 | 
				
			||||||
 | 
												break;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											case 'hold': {
 | 
				
			||||||
 | 
												this.hold();
 | 
				
			||||||
 | 
												break;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											case 'surrender': {
 | 
				
			||||||
 | 
												this.surrender();
 | 
				
			||||||
 | 
												break;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											default:
 | 
				
			||||||
 | 
												break;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (!this.isGameOver) {
 | 
				
			||||||
 | 
										this.tickRaf = window.requestAnimationFrame(playTick);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								playTick();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.tick();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public getLogs() {
 | 
				
			||||||
 | 
							return this.logs;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private tick() {
 | 
				
			||||||
 | 
							this.frame++;
 | 
				
			||||||
 | 
							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);
 | 
				
			||||||
 | 
							if (!this.isGameOver) {
 | 
				
			||||||
 | 
								this.tickRaf = window.requestAnimationFrame(this.tick);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async load() {
 | 
						public async load() {
 | 
				
			||||||
@@ -387,17 +491,22 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public drop(_x: number) {
 | 
						public drop(_x: number) {
 | 
				
			||||||
		if (this.isGameOver) return;
 | 
							if (this.isGameOver) return;
 | 
				
			||||||
		if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return;
 | 
							if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const head = this.stock.shift()!;
 | 
							const head = this.stock.shift()!;
 | 
				
			||||||
		this.stock.push({
 | 
							this.stock.push({
 | 
				
			||||||
			id: Math.random().toString(),
 | 
								id: this.rng().toString(),
 | 
				
			||||||
			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
								mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		this.emit('changeStock', this.stock);
 | 
							this.emit('changeStock', this.stock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x));
 | 
							const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), Math.round(_x)));
 | 
				
			||||||
		const body = this.createBody(head.mono, x, 50 + head.mono.size / 2);
 | 
							const body = this.createBody(head.mono, x, 50 + head.mono.size / 2);
 | 
				
			||||||
 | 
							this.logs.push({
 | 
				
			||||||
 | 
								frame: this.frame,
 | 
				
			||||||
 | 
								operation: 'drop',
 | 
				
			||||||
 | 
								x,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		Matter.Composite.add(this.engine.world, body);
 | 
							Matter.Composite.add(this.engine.world, body);
 | 
				
			||||||
		this.activeBodyIds.push(body.id);
 | 
							this.activeBodyIds.push(body.id);
 | 
				
			||||||
		this.latestDroppedBodyId = body.id;
 | 
							this.latestDroppedBodyId = body.id;
 | 
				
			||||||
@@ -416,6 +525,11 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
	public hold() {
 | 
						public hold() {
 | 
				
			||||||
		if (this.isGameOver) return;
 | 
							if (this.isGameOver) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.logs.push({
 | 
				
			||||||
 | 
								frame: this.frame,
 | 
				
			||||||
 | 
								operation: 'hold',
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (this.holding) {
 | 
							if (this.holding) {
 | 
				
			||||||
			const head = this.stock.shift()!;
 | 
								const head = this.stock.shift()!;
 | 
				
			||||||
			this.stock.unshift(this.holding);
 | 
								this.stock.unshift(this.holding);
 | 
				
			||||||
@@ -426,8 +540,8 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
			const head = this.stock.shift()!;
 | 
								const head = this.stock.shift()!;
 | 
				
			||||||
			this.holding = head;
 | 
								this.holding = head;
 | 
				
			||||||
			this.stock.push({
 | 
								this.stock.push({
 | 
				
			||||||
				id: Math.random().toString(),
 | 
									id: this.rng().toString(),
 | 
				
			||||||
				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
									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('changeHolding', this.holding);
 | 
				
			||||||
			this.emit('changeStock', this.stock);
 | 
								this.emit('changeStock', this.stock);
 | 
				
			||||||
@@ -440,8 +554,9 @@ export class DropAndFusionGame extends EventEmitter<{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	public dispose() {
 | 
						public dispose() {
 | 
				
			||||||
		if (this.comboIntervalId) window.clearInterval(this.comboIntervalId);
 | 
							if (this.comboIntervalId) window.clearInterval(this.comboIntervalId);
 | 
				
			||||||
 | 
							if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf);
 | 
				
			||||||
 | 
							this.tickRaf = null;
 | 
				
			||||||
		Matter.Render.stop(this.render);
 | 
							Matter.Render.stop(this.render);
 | 
				
			||||||
		Matter.Runner.stop(this.runner);
 | 
					 | 
				
			||||||
		Matter.World.clear(this.engine.world, false);
 | 
							Matter.World.clear(this.engine.world, false);
 | 
				
			||||||
		Matter.Engine.clear(this.engine);
 | 
							Matter.Engine.clear(this.engine);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -787,6 +787,9 @@ importers:
 | 
				
			|||||||
      sass:
 | 
					      sass:
 | 
				
			||||||
        specifier: 1.69.5
 | 
					        specifier: 1.69.5
 | 
				
			||||||
        version: 1.69.5
 | 
					        version: 1.69.5
 | 
				
			||||||
 | 
					      seedrandom:
 | 
				
			||||||
 | 
					        specifier: ^3.0.5
 | 
				
			||||||
 | 
					        version: 3.0.5
 | 
				
			||||||
      shiki:
 | 
					      shiki:
 | 
				
			||||||
        specifier: 0.14.7
 | 
					        specifier: 0.14.7
 | 
				
			||||||
        version: 0.14.7
 | 
					        version: 0.14.7
 | 
				
			||||||
@@ -7401,7 +7404,7 @@ packages:
 | 
				
			|||||||
    hasBin: true
 | 
					    hasBin: true
 | 
				
			||||||
    peerDependencies:
 | 
					    peerDependencies:
 | 
				
			||||||
      '@swc/core': ^1.2.66
 | 
					      '@swc/core': ^1.2.66
 | 
				
			||||||
      chokidar: ^3.5.1
 | 
					      chokidar: 3.5.3
 | 
				
			||||||
    peerDependenciesMeta:
 | 
					    peerDependenciesMeta:
 | 
				
			||||||
      chokidar:
 | 
					      chokidar:
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user