tweak game
							
								
								
									
										1
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -1192,6 +1192,7 @@ export interface Locale {
 | 
				
			||||||
    "decorate": string;
 | 
					    "decorate": string;
 | 
				
			||||||
    "addMfmFunction": string;
 | 
					    "addMfmFunction": string;
 | 
				
			||||||
    "enableQuickAddMfmFunction": string;
 | 
					    "enableQuickAddMfmFunction": string;
 | 
				
			||||||
 | 
					    "bubbleGame": string;
 | 
				
			||||||
    "_announcement": {
 | 
					    "_announcement": {
 | 
				
			||||||
        "forExistingUsers": string;
 | 
					        "forExistingUsers": string;
 | 
				
			||||||
        "forExistingUsersDescription": string;
 | 
					        "forExistingUsersDescription": string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1189,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
 | 
				
			||||||
decorate: "デコる"
 | 
					decorate: "デコる"
 | 
				
			||||||
addMfmFunction: "装飾を追加"
 | 
					addMfmFunction: "装飾を追加"
 | 
				
			||||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
 | 
					enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
 | 
				
			||||||
 | 
					bubbleGame: "バブルゲーム"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_announcement:
 | 
					_announcement:
 | 
				
			||||||
  forExistingUsers: "既存ユーザーのみ"
 | 
					  forExistingUsers: "既存ユーザーのみ"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_1.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_10.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 33 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_2.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_3.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_4.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_5.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_6.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_7.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_8.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/keycap_9.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
| 
						 | 
					@ -7,12 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
<MkStickyContainer>
 | 
					<MkStickyContainer>
 | 
				
			||||||
	<template #header><MkPageHeader/></template>
 | 
						<template #header><MkPageHeader/></template>
 | 
				
			||||||
	<MkSpacer :contentMax="800">
 | 
						<MkSpacer :contentMax="800">
 | 
				
			||||||
		<div class="_gaps_s" :class="$style.root" style="margin: 0 auto; max-width: 600px;">
 | 
							<div v-show="!gameStarted" class="_gaps_s" :class="$style.root">
 | 
				
			||||||
 | 
								<div style="text-align: center;">
 | 
				
			||||||
 | 
									<div>{{ i18n.ts.bubbleGame }}</div>
 | 
				
			||||||
 | 
									<MkSelect v-model="gameMode">
 | 
				
			||||||
 | 
										<option value="normal">NORMAL</option>
 | 
				
			||||||
 | 
										<option value="square">SQUARE</option>
 | 
				
			||||||
 | 
									</MkSelect>
 | 
				
			||||||
 | 
									<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div v-show="gameStarted" class="_gaps_s" :class="$style.root">
 | 
				
			||||||
			<div style="display: flex;">
 | 
								<div style="display: flex;">
 | 
				
			||||||
				<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
 | 
									<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
 | 
				
			||||||
					<div :class="$style.frameInner">
 | 
										<div :class="$style.frameInner">
 | 
				
			||||||
						<div>SCORE: <b><MkNumber :value="score"/></b></div>
 | 
											<b>BUBBLE GAME</b>
 | 
				
			||||||
						<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div>
 | 
											<div>- {{ gameMode }} -</div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div :class="[$style.frame, $style.stock]" style="margin-left: auto;">
 | 
									<div :class="[$style.frame, $style.stock]" style="margin-left: auto;">
 | 
				
			||||||
| 
						 | 
					@ -33,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div :class="$style.main">
 | 
								<div :class="$style.main">
 | 
				
			||||||
				<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
 | 
									<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @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"/>
 | 
				
			||||||
| 
						 | 
					@ -67,7 +77,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<MkButton @click="restart">Restart</MkButton>
 | 
								<div style="display: flex;">
 | 
				
			||||||
 | 
									<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
 | 
				
			||||||
 | 
										<div :class="$style.frameInner">
 | 
				
			||||||
 | 
											<div>SCORE: <b><MkNumber :value="score"/></b></div>
 | 
				
			||||||
 | 
											<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div :class="[$style.frame]" style="margin-left: auto;">
 | 
				
			||||||
 | 
										<div :class="$style.frameInner" style="text-align: center;">
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div :class="$style.frame">
 | 
				
			||||||
 | 
									<div :class="$style.frameInner">
 | 
				
			||||||
 | 
										<MkButton @click="restart">Restart</MkButton>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</MkSpacer>
 | 
						</MkSpacer>
 | 
				
			||||||
</MkStickyContainer>
 | 
					</MkStickyContainer>
 | 
				
			||||||
| 
						 | 
					@ -86,18 +112,35 @@ import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 | 
				
			||||||
import MkButton from '@/components/MkButton.vue';
 | 
					import MkButton from '@/components/MkButton.vue';
 | 
				
			||||||
import { defaultStore } from '@/store.js';
 | 
					import { defaultStore } from '@/store.js';
 | 
				
			||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
					import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
 | 
					import { useInterval } from '@/scripts/use-interval.js';
 | 
				
			||||||
 | 
					import MkSelect from '@/components/MkSelect.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Mono = {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						level: number;
 | 
				
			||||||
 | 
						size: number;
 | 
				
			||||||
 | 
						shape: 'circle' | 'rectangle';
 | 
				
			||||||
 | 
						score: number;
 | 
				
			||||||
 | 
						dropCandidate: boolean;
 | 
				
			||||||
 | 
						sfxPitch: number;
 | 
				
			||||||
 | 
						img: string;
 | 
				
			||||||
 | 
						imgSize: number;
 | 
				
			||||||
 | 
						spriteScale: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const containerEl = shallowRef<HTMLElement>();
 | 
					const containerEl = shallowRef<HTMLElement>();
 | 
				
			||||||
const canvasEl = shallowRef<HTMLCanvasElement>();
 | 
					const canvasEl = shallowRef<HTMLCanvasElement>();
 | 
				
			||||||
const mouseX = ref(0);
 | 
					const mouseX = ref(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BASE_SIZE = 30;
 | 
					const NORMAL_BASE_SIZE = 30;
 | 
				
			||||||
const FRUITS = [{
 | 
					const NORAML_MONOS: Mono[] = [{
 | 
				
			||||||
	id: '9377076d-c980-4d83-bdaf-175bc58275b7',
 | 
						id: '9377076d-c980-4d83-bdaf-175bc58275b7',
 | 
				
			||||||
	level: 10,
 | 
						level: 10,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 512,
 | 
						score: 512,
 | 
				
			||||||
	available: false,
 | 
						dropCandidate: false,
 | 
				
			||||||
	sfxPitch: 0.25,
 | 
						sfxPitch: 0.25,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/exploding_head.png',
 | 
						img: '/client-assets/drop-and-fusion/exploding_head.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -105,9 +148,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: 'be9f38d2-b267-4b1a-b420-904e22e80568',
 | 
						id: 'be9f38d2-b267-4b1a-b420-904e22e80568',
 | 
				
			||||||
	level: 9,
 | 
						level: 9,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 256,
 | 
						score: 256,
 | 
				
			||||||
	available: false,
 | 
						dropCandidate: false,
 | 
				
			||||||
	sfxPitch: 0.5,
 | 
						sfxPitch: 0.5,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png',
 | 
						img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -115,9 +159,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: 'beb30459-b064-4888-926b-f572e4e72e0c',
 | 
						id: 'beb30459-b064-4888-926b-f572e4e72e0c',
 | 
				
			||||||
	level: 8,
 | 
						level: 8,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 128,
 | 
						score: 128,
 | 
				
			||||||
	available: false,
 | 
						dropCandidate: false,
 | 
				
			||||||
	sfxPitch: 0.75,
 | 
						sfxPitch: 0.75,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/cold_face.png',
 | 
						img: '/client-assets/drop-and-fusion/cold_face.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -125,9 +170,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0',
 | 
						id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0',
 | 
				
			||||||
	level: 7,
 | 
						level: 7,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 64,
 | 
						score: 64,
 | 
				
			||||||
	available: false,
 | 
						dropCandidate: false,
 | 
				
			||||||
	sfxPitch: 1,
 | 
						sfxPitch: 1,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/zany_face.png',
 | 
						img: '/client-assets/drop-and-fusion/zany_face.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -135,9 +181,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a',
 | 
						id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a',
 | 
				
			||||||
	level: 6,
 | 
						level: 6,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 32,
 | 
						score: 32,
 | 
				
			||||||
	available: false,
 | 
						dropCandidate: false,
 | 
				
			||||||
	sfxPitch: 1.5,
 | 
						sfxPitch: 1.5,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/pleading_face.png',
 | 
						img: '/client-assets/drop-and-fusion/pleading_face.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -145,9 +192,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: '249c728e-230f-4332-bbbf-281c271c75b2',
 | 
						id: '249c728e-230f-4332-bbbf-281c271c75b2',
 | 
				
			||||||
	level: 5,
 | 
						level: 5,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 16,
 | 
						score: 16,
 | 
				
			||||||
	available: true,
 | 
						dropCandidate: true,
 | 
				
			||||||
	sfxPitch: 2,
 | 
						sfxPitch: 2,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/face_with_open_mouth.png',
 | 
						img: '/client-assets/drop-and-fusion/face_with_open_mouth.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -155,9 +203,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: '23d67613-d484-4a93-b71e-3e81b19d6186',
 | 
						id: '23d67613-d484-4a93-b71e-3e81b19d6186',
 | 
				
			||||||
	level: 4,
 | 
						level: 4,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 8,
 | 
						score: 8,
 | 
				
			||||||
	available: true,
 | 
						dropCandidate: true,
 | 
				
			||||||
	sfxPitch: 2.5,
 | 
						sfxPitch: 2.5,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png',
 | 
						img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -165,9 +214,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99',
 | 
						id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99',
 | 
				
			||||||
	level: 3,
 | 
						level: 3,
 | 
				
			||||||
	size: BASE_SIZE * 1.25 * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 4,
 | 
						score: 4,
 | 
				
			||||||
	available: true,
 | 
						dropCandidate: true,
 | 
				
			||||||
	sfxPitch: 3,
 | 
						sfxPitch: 3,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/grinning_squinting_face.png',
 | 
						img: '/client-assets/drop-and-fusion/grinning_squinting_face.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -175,9 +225,10 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5',
 | 
						id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5',
 | 
				
			||||||
	level: 2,
 | 
						level: 2,
 | 
				
			||||||
	size: BASE_SIZE * 1.25,
 | 
						size: NORMAL_BASE_SIZE * 1.25,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 2,
 | 
						score: 2,
 | 
				
			||||||
	available: true,
 | 
						dropCandidate: true,
 | 
				
			||||||
	sfxPitch: 3.5,
 | 
						sfxPitch: 3.5,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png',
 | 
						img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
| 
						 | 
					@ -185,14 +236,128 @@ const FRUITS = [{
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	id: '64ec4add-ce39-42b4-96cb-33908f3f118d',
 | 
						id: '64ec4add-ce39-42b4-96cb-33908f3f118d',
 | 
				
			||||||
	level: 1,
 | 
						level: 1,
 | 
				
			||||||
	size: BASE_SIZE,
 | 
						size: NORMAL_BASE_SIZE,
 | 
				
			||||||
 | 
						shape: 'circle',
 | 
				
			||||||
	score: 1,
 | 
						score: 1,
 | 
				
			||||||
	available: true,
 | 
						dropCandidate: true,
 | 
				
			||||||
	sfxPitch: 4,
 | 
						sfxPitch: 4,
 | 
				
			||||||
	img: '/client-assets/drop-and-fusion/heart_suit.png',
 | 
						img: '/client-assets/drop-and-fusion/heart_suit.png',
 | 
				
			||||||
	imgSize: 256,
 | 
						imgSize: 256,
 | 
				
			||||||
	spriteScale: 1.12,
 | 
						spriteScale: 1.12,
 | 
				
			||||||
}] as const;
 | 
					}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SQUARE_BASE_SIZE = 28;
 | 
				
			||||||
 | 
					const SQUARE_MONOS: Mono[] = [{
 | 
				
			||||||
 | 
						id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
 | 
				
			||||||
 | 
						level: 10,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 512,
 | 
				
			||||||
 | 
						dropCandidate: false,
 | 
				
			||||||
 | 
						sfxPitch: 0.25,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_10.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
 | 
				
			||||||
 | 
						level: 9,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 256,
 | 
				
			||||||
 | 
						dropCandidate: false,
 | 
				
			||||||
 | 
						sfxPitch: 0.5,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_9.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
 | 
				
			||||||
 | 
						level: 8,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 128,
 | 
				
			||||||
 | 
						dropCandidate: false,
 | 
				
			||||||
 | 
						sfxPitch: 0.75,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_8.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
 | 
				
			||||||
 | 
						level: 7,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 64,
 | 
				
			||||||
 | 
						dropCandidate: false,
 | 
				
			||||||
 | 
						sfxPitch: 1,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_7.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '1092e069-fe1a-450b-be97-b5d477ec398c',
 | 
				
			||||||
 | 
						level: 6,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 32,
 | 
				
			||||||
 | 
						dropCandidate: false,
 | 
				
			||||||
 | 
						sfxPitch: 1.5,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_6.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
 | 
				
			||||||
 | 
						level: 5,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 16,
 | 
				
			||||||
 | 
						dropCandidate: true,
 | 
				
			||||||
 | 
						sfxPitch: 2,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_5.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a',
 | 
				
			||||||
 | 
						level: 4,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 8,
 | 
				
			||||||
 | 
						dropCandidate: true,
 | 
				
			||||||
 | 
						sfxPitch: 2.5,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_4.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
 | 
				
			||||||
 | 
						level: 3,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25 * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 4,
 | 
				
			||||||
 | 
						dropCandidate: true,
 | 
				
			||||||
 | 
						sfxPitch: 3,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_3.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
 | 
				
			||||||
 | 
						level: 2,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE * 1.25,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 2,
 | 
				
			||||||
 | 
						dropCandidate: true,
 | 
				
			||||||
 | 
						sfxPitch: 3.5,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_2.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						id: '35e476ee-44bd-4711-ad42-87be245d3efd',
 | 
				
			||||||
 | 
						level: 1,
 | 
				
			||||||
 | 
						size: SQUARE_BASE_SIZE,
 | 
				
			||||||
 | 
						shape: 'rectangle',
 | 
				
			||||||
 | 
						score: 1,
 | 
				
			||||||
 | 
						dropCandidate: true,
 | 
				
			||||||
 | 
						sfxPitch: 4,
 | 
				
			||||||
 | 
						img: '/client-assets/drop-and-fusion/keycap_1.png',
 | 
				
			||||||
 | 
						imgSize: 256,
 | 
				
			||||||
 | 
						spriteScale: 1.12,
 | 
				
			||||||
 | 
					}];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const GAME_WIDTH = 450;
 | 
					const GAME_WIDTH = 450;
 | 
				
			||||||
const GAME_HEIGHT = 600;
 | 
					const GAME_HEIGHT = 600;
 | 
				
			||||||
| 
						 | 
					@ -200,12 +365,13 @@ const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高い
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let viewScaleX = 1;
 | 
					let viewScaleX = 1;
 | 
				
			||||||
let viewScaleY = 1;
 | 
					let viewScaleY = 1;
 | 
				
			||||||
const currentPick = shallowRef<{ id: string; fruit: typeof FRUITS[number] } | null>(null);
 | 
					const currentPick = shallowRef<{ id: string; fruit: Mono } | null>(null);
 | 
				
			||||||
const stock = shallowRef<{ id: string; fruit: typeof FRUITS[number] }[]>([]);
 | 
					const stock = shallowRef<{ id: string; fruit: Mono }[]>([]);
 | 
				
			||||||
const score = ref(0);
 | 
					const score = ref(0);
 | 
				
			||||||
const combo = ref(0);
 | 
					const combo = ref(0);
 | 
				
			||||||
const comboPrev = ref(0);
 | 
					const comboPrev = ref(0);
 | 
				
			||||||
const dropReady = ref(true);
 | 
					const dropReady = ref(true);
 | 
				
			||||||
 | 
					const gameMode = ref<'normal' | 'square'>('normal');
 | 
				
			||||||
const gameOver = ref(false);
 | 
					const gameOver = ref(false);
 | 
				
			||||||
const gameStarted = ref(false);
 | 
					const gameStarted = ref(false);
 | 
				
			||||||
const highScore = ref<number | null>(null);
 | 
					const highScore = ref<number | null>(null);
 | 
				
			||||||
| 
						 | 
					@ -213,7 +379,7 @@ const highScore = ref<number | null>(null);
 | 
				
			||||||
class Game extends EventEmitter<{
 | 
					class Game extends EventEmitter<{
 | 
				
			||||||
	changeScore: (score: number) => void;
 | 
						changeScore: (score: number) => void;
 | 
				
			||||||
	changeCombo: (combo: number) => void;
 | 
						changeCombo: (combo: number) => void;
 | 
				
			||||||
	changeStock: (stock: { id: string; fruit: typeof FRUITS[number] }[]) => void;
 | 
						changeStock: (stock: { id: string; fruit: Mono }[]) => void;
 | 
				
			||||||
	dropped: () => void;
 | 
						dropped: () => void;
 | 
				
			||||||
	fusioned: (x: number, y: number, score: number) => void;
 | 
						fusioned: (x: number, y: number, score: number) => void;
 | 
				
			||||||
	gameOver: () => void;
 | 
						gameOver: () => void;
 | 
				
			||||||
| 
						 | 
					@ -228,6 +394,8 @@ class Game extends EventEmitter<{
 | 
				
			||||||
	private overflowCollider: Matter.Body;
 | 
						private overflowCollider: Matter.Body;
 | 
				
			||||||
	private isGameOver = false;
 | 
						private isGameOver = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private monoDefinitions: Mono[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * フィールドに出ていて、かつ合体の対象となるアイテム
 | 
						 * フィールドに出ていて、かつ合体の対象となるアイテム
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
| 
						 | 
					@ -237,7 +405,7 @@ class Game extends EventEmitter<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private latestDroppedAt = 0;
 | 
						private latestDroppedAt = 0;
 | 
				
			||||||
	private latestFusionedAt = 0;
 | 
						private latestFusionedAt = 0;
 | 
				
			||||||
	private stock: { id: string; fruit: typeof FRUITS[number] }[] = [];
 | 
						private stock: { id: string; fruit: Mono }[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private _combo = 0;
 | 
						private _combo = 0;
 | 
				
			||||||
	private get combo() {
 | 
						private get combo() {
 | 
				
			||||||
| 
						 | 
					@ -259,9 +427,13 @@ class Game extends EventEmitter<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private comboIntervalId: number | null = null;
 | 
						private comboIntervalId: number | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor() {
 | 
						constructor(opts: {
 | 
				
			||||||
 | 
							monoDefinitions: Mono[];
 | 
				
			||||||
 | 
						}) {
 | 
				
			||||||
		super();
 | 
							super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.monoDefinitions = opts.monoDefinitions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.engine = Matter.Engine.create({
 | 
							this.engine = Matter.Engine.create({
 | 
				
			||||||
			constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
 | 
								constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
 | 
				
			||||||
			positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
 | 
								positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
 | 
				
			||||||
| 
						 | 
					@ -333,8 +505,8 @@ class Game extends EventEmitter<{
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private createBody(fruit: typeof FRUITS[number], x: number, y: number) {
 | 
						private createBody(fruit: Mono, x: number, y: number) {
 | 
				
			||||||
		return Matter.Bodies.circle(x, y, fruit.size / 2, {
 | 
							const options: Matter.IBodyDefinition = {
 | 
				
			||||||
			label: fruit.id,
 | 
								label: fruit.id,
 | 
				
			||||||
			//density: 0.0005,
 | 
								//density: 0.0005,
 | 
				
			||||||
			density: fruit.size / 1000,
 | 
								density: fruit.size / 1000,
 | 
				
			||||||
| 
						 | 
					@ -351,7 +523,14 @@ class Game extends EventEmitter<{
 | 
				
			||||||
					yScale: (fruit.size / fruit.imgSize) * fruit.spriteScale,
 | 
										yScale: (fruit.size / fruit.imgSize) * fruit.spriteScale,
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		});
 | 
							};
 | 
				
			||||||
 | 
							if (fruit.shape === 'circle') {
 | 
				
			||||||
 | 
								return Matter.Bodies.circle(x, y, fruit.size / 2, options);
 | 
				
			||||||
 | 
							} else if (fruit.shape === 'rectangle') {
 | 
				
			||||||
 | 
								return Matter.Bodies.rectangle(x, y, fruit.size, fruit.size, options);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new Error('unrecognized shape');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
 | 
						private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
 | 
				
			||||||
| 
						 | 
					@ -370,8 +549,8 @@ class Game extends EventEmitter<{
 | 
				
			||||||
		Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
 | 
							Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
 | 
				
			||||||
		this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
 | 
							this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const currentFruit = FRUITS.find(y => y.id === bodyA.label)!;
 | 
							const currentFruit = this.monoDefinitions.find(y => y.id === bodyA.label)!;
 | 
				
			||||||
		const nextFruit = FRUITS.find(x => x.level === currentFruit.level + 1);
 | 
							const nextFruit = this.monoDefinitions.find(x => x.level === currentFruit.level + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (nextFruit) {
 | 
							if (nextFruit) {
 | 
				
			||||||
			const body = this.createBody(nextFruit, newX, newY);
 | 
								const body = this.createBody(nextFruit, newX, newY);
 | 
				
			||||||
| 
						 | 
					@ -382,7 +561,8 @@ class Game extends EventEmitter<{
 | 
				
			||||||
				this.activeBodyIds.push(body.id);
 | 
									this.activeBodyIds.push(body.id);
 | 
				
			||||||
			}, 100);
 | 
								}, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const additionalScore = Math.round(currentFruit.score * (1 + ((this.combo - 1) / 3)));
 | 
								const comboBonus = 1 + ((this.combo - 1) / 5);
 | 
				
			||||||
 | 
								const additionalScore = Math.round(currentFruit.score * comboBonus);
 | 
				
			||||||
			this.score += additionalScore;
 | 
								this.score += additionalScore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const pan = ((newX / GAME_WIDTH) - 0.5) * 2;
 | 
								const pan = ((newX / GAME_WIDTH) - 0.5) * 2;
 | 
				
			||||||
| 
						 | 
					@ -413,7 +593,7 @@ class Game extends EventEmitter<{
 | 
				
			||||||
		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: Math.random().toString(),
 | 
				
			||||||
				fruit: FRUITS.filter(x => x.available)[Math.floor(Math.random() * FRUITS.filter(x => x.available).length)],
 | 
									fruit: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		this.emit('changeStock', this.stock);
 | 
							this.emit('changeStock', this.stock);
 | 
				
			||||||
| 
						 | 
					@ -474,7 +654,7 @@ class Game extends EventEmitter<{
 | 
				
			||||||
		const st = this.stock.shift()!;
 | 
							const st = this.stock.shift()!;
 | 
				
			||||||
		this.stock.push({
 | 
							this.stock.push({
 | 
				
			||||||
			id: Math.random().toString(),
 | 
								id: Math.random().toString(),
 | 
				
			||||||
			fruit: FRUITS.filter(x => x.available)[Math.floor(Math.random() * FRUITS.filter(x => x.available).length)],
 | 
								fruit: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		this.emit('changeStock', this.stock);
 | 
							this.emit('changeStock', this.stock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -533,9 +713,7 @@ function restart() {
 | 
				
			||||||
	score.value = 0;
 | 
						score.value = 0;
 | 
				
			||||||
	combo.value = 0;
 | 
						combo.value = 0;
 | 
				
			||||||
	comboPrev.value = 0;
 | 
						comboPrev.value = 0;
 | 
				
			||||||
	game = new Game();
 | 
						gameStarted.value = false;
 | 
				
			||||||
	attachGame();
 | 
					 | 
				
			||||||
	game.start();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function attachGame() {
 | 
					function attachGame() {
 | 
				
			||||||
| 
						 | 
					@ -584,36 +762,45 @@ function attachGame() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			misskeyApi('i/registry/set', {
 | 
								misskeyApi('i/registry/set', {
 | 
				
			||||||
				scope: ['dropAndFusionGame'],
 | 
									scope: ['dropAndFusionGame'],
 | 
				
			||||||
				key: 'highScore',
 | 
									key: 'highScore:' + gameMode.value,
 | 
				
			||||||
				value: highScore.value,
 | 
									value: highScore.value,
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(async () => {
 | 
					async function start() {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		highScore.value = await misskeyApi('i/registry/get', {
 | 
							highScore.value = await misskeyApi('i/registry/get', {
 | 
				
			||||||
			scope: ['dropAndFusionGame'],
 | 
								scope: ['dropAndFusionGame'],
 | 
				
			||||||
			key: 'highScore',
 | 
								key: 'highScore:' + gameMode.value,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	} catch (err) {
 | 
						} catch (err) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	game = new Game();
 | 
						gameStarted.value = true;
 | 
				
			||||||
 | 
						game = new Game(gameMode.value === 'normal' ? {
 | 
				
			||||||
 | 
							monoDefinitions: NORAML_MONOS,
 | 
				
			||||||
 | 
						} : {
 | 
				
			||||||
 | 
							monoDefinitions: SQUARE_MONOS,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
	attachGame();
 | 
						attachGame();
 | 
				
			||||||
 | 
					 | 
				
			||||||
	game.start();
 | 
						game.start();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					useInterval(() => {
 | 
				
			||||||
 | 
						if (!canvasEl.value) return;
 | 
				
			||||||
	const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width;
 | 
						const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width;
 | 
				
			||||||
	const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
 | 
						const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
 | 
				
			||||||
	viewScaleX = actualCanvasWidth / GAME_WIDTH;
 | 
						viewScaleX = actualCanvasWidth / GAME_WIDTH;
 | 
				
			||||||
	viewScaleY = actualCanvasHeight / GAME_HEIGHT;
 | 
						viewScaleY = actualCanvasHeight / GAME_HEIGHT;
 | 
				
			||||||
 | 
					}, 1000, { immediate: false, afterMounted: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
definePageMetadata({
 | 
					definePageMetadata({
 | 
				
			||||||
	title: 'Drop & Fusion',
 | 
						title: i18n.ts.bubbleGame,
 | 
				
			||||||
	icon: 'ti ti-apple',
 | 
						icon: 'ti ti-apple',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -666,6 +853,8 @@ definePageMetadata({
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.root {
 | 
					.root {
 | 
				
			||||||
 | 
						margin: 0 auto;
 | 
				
			||||||
 | 
						max-width: 600px;
 | 
				
			||||||
	user-select: none;
 | 
						user-select: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	* {
 | 
						* {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -528,7 +528,7 @@ export const routes = [{
 | 
				
			||||||
	component: page(() => import('./pages/clicker.vue')),
 | 
						component: page(() => import('./pages/clicker.vue')),
 | 
				
			||||||
	loginRequired: true,
 | 
						loginRequired: true,
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	path: '/drop-and-fusion',
 | 
						path: '/bubble-game',
 | 
				
			||||||
	component: page(() => import('./pages/drop-and-fusion.vue')),
 | 
						component: page(() => import('./pages/drop-and-fusion.vue')),
 | 
				
			||||||
	loginRequired: true,
 | 
						loginRequired: true,
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,8 +29,8 @@ function toolsMenuItems(): MenuItem[] {
 | 
				
			||||||
		icon: 'ti ti-cookie',
 | 
							icon: 'ti ti-cookie',
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		type: 'link',
 | 
							type: 'link',
 | 
				
			||||||
		to: '/drop-and-fusion',
 | 
							to: '/bubble-game',
 | 
				
			||||||
		text: 'Drop & Fusion',
 | 
							text: i18n.ts.bubbleGame,
 | 
				
			||||||
		icon: 'ti ti-apple',
 | 
							icon: 'ti ti-apple',
 | 
				
			||||||
	}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
 | 
						}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
 | 
				
			||||||
		type: 'link',
 | 
							type: 'link',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||