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,8 +77,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
<MkButton @click="restart">Restart</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -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',
|
||||||
|
|