diff --git a/locales/index.d.ts b/locales/index.d.ts index 317a474dba..fb0f332ec1 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1200,6 +1200,7 @@ export interface Locale { "replay": string; "replaying": string; "ranking": string; + "lastNDays": string; "_bubbleGame": { "howToPlay": string; "_howToPlay": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d3c2b4d312..68c148da4c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1197,6 +1197,7 @@ showReplay: "リプレイを見る" replay: "リプレイ" replaying: "リプレイ中" ranking: "ランキング" +lastNDays: "直近{n}日" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/frontend/assets/drop-and-fusion/10000yen.png b/packages/frontend/assets/drop-and-fusion/10000yen.png new file mode 100644 index 0000000000..69b0dc926a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/10000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/1000yen.png b/packages/frontend/assets/drop-and-fusion/1000yen.png new file mode 100644 index 0000000000..4c462fb1f6 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/1000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/100yen.png b/packages/frontend/assets/drop-and-fusion/100yen.png new file mode 100644 index 0000000000..8911543af9 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/100yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/10yen.png b/packages/frontend/assets/drop-and-fusion/10yen.png new file mode 100644 index 0000000000..041f773891 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/10yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/1yen.png b/packages/frontend/assets/drop-and-fusion/1yen.png new file mode 100644 index 0000000000..cc6dcfd740 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/1yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/2000yen.png b/packages/frontend/assets/drop-and-fusion/2000yen.png new file mode 100644 index 0000000000..6048b7c996 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/2000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/5000yen.png b/packages/frontend/assets/drop-and-fusion/5000yen.png new file mode 100644 index 0000000000..b0fe26db11 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/5000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/500yen.png b/packages/frontend/assets/drop-and-fusion/500yen.png new file mode 100644 index 0000000000..9e3d2b766b Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/500yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/50yen.png b/packages/frontend/assets/drop-and-fusion/50yen.png new file mode 100644 index 0000000000..c8ef089972 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/50yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/5yen.png b/packages/frontend/assets/drop-and-fusion/5yen.png new file mode 100644 index 0000000000..b120bdca36 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/5yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/drop.mp3 similarity index 100% rename from packages/frontend/assets/drop-and-fusion/poi2.mp3 rename to packages/frontend/assets/drop-and-fusion/drop.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 new file mode 100644 index 0000000000..bbf385f15a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 new file mode 100644 index 0000000000..c7fdcb5c8f Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 differ diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index c222fdeb40..fa955806c0 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_picked_move" mode="out-in" > - <img v-if="currentPick" :key="currentPick.id" :src="getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ marginBottom: -((currentPick?.mono.size * viewScale) / 2) + 'px', left: -((currentPick?.mono.size * viewScale) / 2) + 'px', width: `${currentPick?.mono.size * viewScale}px` }"/> + <img v-if="currentPick" :key="currentPick.id" :src="getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ marginBottom: -((currentPick?.mono.sizeY * viewScale) / 2) + 'px', left: -((currentPick?.mono.sizeX * viewScale) / 2) + 'px', width: `${currentPick?.mono.sizeX * viewScale}px` }"/> </Transition> <template v-if="dropReady && currentPick"> <img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/> @@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="isGameOver && !replaying" :class="$style.gameOverLabel"> <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;"/> - <div>SCORE: <MkNumber :value="score"/></div> + <div>SCORE: <MkNumber :value="score"/>{{ gameMode === 'yen' ? '円' : 'pt' }}</div> <div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> </div> </div> @@ -90,8 +90,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.frameInner"> <div class="_buttonsCenter"> <MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END</MkButton> - <MkButton :primary="replayPlaybackRate === 2" @click="replayPlaybackRate = replayPlaybackRate === 2 ? 1 : 2"><i class="ti ti-player-track-next"></i> x2</MkButton> <MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton> + <MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton> </div> </div> </div> @@ -108,8 +108,8 @@ SPDX-License-Identifier: AGPL-3.0-only <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> (MAX CHAIN: <b><MkNumber :value="maxCombo"/></b>)</div> - <div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div> + <div>SCORE: <b><MkNumber :value="score"/>{{ gameMode === 'yen' ? '円' : 'pt' }}</b></div> + <div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/>{{ gameMode === 'yen' ? '円' : 'pt' }}</b><b v-else>-</b></div> </div> </div> <div :class="[$style.frame]" style="margin-left: auto;"> @@ -167,230 +167,404 @@ const NORMAL_BASE_SIZE = 30; const NORAML_MONOS: Mono[] = [{ id: '9377076d-c980-4d83-bdaf-175bc58275b7', level: 10, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, dropCandidate: false, sfxPitch: 0.25, img: '/client-assets/drop-and-fusion/exploding_head.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'be9f38d2-b267-4b1a-b420-904e22e80568', level: 9, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, shape: 'circle', score: 256, dropCandidate: false, sfxPitch: 0.5, img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'beb30459-b064-4888-926b-f572e4e72e0c', level: 8, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, shape: 'circle', score: 128, dropCandidate: false, sfxPitch: 0.75, img: '/client-assets/drop-and-fusion/cold_face.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0', level: 7, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, shape: 'circle', score: 64, dropCandidate: false, sfxPitch: 1, img: '/client-assets/drop-and-fusion/zany_face.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a', level: 6, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, shape: 'circle', score: 32, dropCandidate: false, sfxPitch: 1.5, img: '/client-assets/drop-and-fusion/pleading_face.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '249c728e-230f-4332-bbbf-281c271c75b2', level: 5, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, shape: 'circle', score: 16, dropCandidate: true, sfxPitch: 2, img: '/client-assets/drop-and-fusion/face_with_open_mouth.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '23d67613-d484-4a93-b71e-3e81b19d6186', level: 4, - size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25, shape: 'circle', score: 8, dropCandidate: true, sfxPitch: 2.5, img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99', level: 3, - size: NORMAL_BASE_SIZE * 1.25 * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25 * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25 * 1.25, shape: 'circle', score: 4, dropCandidate: true, sfxPitch: 3, img: '/client-assets/drop-and-fusion/grinning_squinting_face.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5', level: 2, - size: NORMAL_BASE_SIZE * 1.25, + sizeX: NORMAL_BASE_SIZE * 1.25, + sizeY: NORMAL_BASE_SIZE * 1.25, shape: 'circle', score: 2, dropCandidate: true, sfxPitch: 3.5, img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '64ec4add-ce39-42b4-96cb-33908f3f118d', level: 1, - size: NORMAL_BASE_SIZE, + sizeX: NORMAL_BASE_SIZE, + sizeY: NORMAL_BASE_SIZE, shape: 'circle', score: 1, dropCandidate: true, sfxPitch: 4, img: '/client-assets/drop-and-fusion/heart_suit.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }]; +const YEN_BASE_SIZE = 30; +const YEN_SATSU_BASE_SIZE = 70; +const YEN_MONOS: Mono[] = [{ + id: '880f9bd9-802f-4135-a7e1-fd0e0331f726', + level: 10, + sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25 * 1.25 * 1.25, + sizeY: YEN_SATSU_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'rectangle', + score: 10000, + dropCandidate: false, + sfxPitch: 0.25, + img: '/client-assets/drop-and-fusion/10000yen.png', + imgSizeX: 512, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: 'e807beb6-374a-4314-9cc2-aa5f17d96b6b', + level: 9, + sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25 * 1.25, + sizeY: YEN_SATSU_BASE_SIZE * 1.25 * 1.25, + shape: 'rectangle', + score: 5000, + dropCandidate: false, + sfxPitch: 0.5, + img: '/client-assets/drop-and-fusion/5000yen.png', + imgSizeX: 512, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '033445b7-8f90-4fc9-beca-71a9e87cb530', + level: 8, + sizeX: (YEN_SATSU_BASE_SIZE * 2) * 1.25, + sizeY: YEN_SATSU_BASE_SIZE * 1.25, + shape: 'rectangle', + score: 2000, + dropCandidate: false, + sfxPitch: 0.75, + img: '/client-assets/drop-and-fusion/2000yen.png', + imgSizeX: 512, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '410a09ec-5f7f-46f6-b26f-cbca4ccbd091', + level: 7, + sizeX: YEN_SATSU_BASE_SIZE * 2, + sizeY: YEN_SATSU_BASE_SIZE, + shape: 'rectangle', + score: 1000, + dropCandidate: false, + sfxPitch: 1, + img: '/client-assets/drop-and-fusion/1000yen.png', + imgSizeX: 512, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '2aae82bc-3fa4-49ad-a6b5-94d888e809f5', + level: 6, + sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 500, + dropCandidate: false, + sfxPitch: 1.5, + img: '/client-assets/drop-and-fusion/500yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: 'a619bd67-d08f-4cc0-8c7e-c8072a4950cd', + level: 5, + sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 100, + dropCandidate: true, + sfxPitch: 2, + img: '/client-assets/drop-and-fusion/100yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: 'c1c5d8e4-17d6-4455-befd-12154d731faa', + level: 4, + sizeX: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeY: YEN_BASE_SIZE * 1.25 * 1.25 * 1.25, + shape: 'circle', + score: 50, + dropCandidate: true, + sfxPitch: 2.5, + img: '/client-assets/drop-and-fusion/50yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '7082648c-e428-44c4-887a-25c07a8ebdd5', + level: 3, + sizeX: YEN_BASE_SIZE * 1.25 * 1.25, + sizeY: YEN_BASE_SIZE * 1.25 * 1.25, + shape: 'circle', + score: 10, + dropCandidate: true, + sfxPitch: 3, + img: '/client-assets/drop-and-fusion/10yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '0d8d40d5-e6e0-4d26-8a95-b8d842363379', + level: 2, + sizeX: YEN_BASE_SIZE * 1.25, + sizeY: YEN_BASE_SIZE * 1.25, + shape: 'circle', + score: 5, + dropCandidate: true, + sfxPitch: 3.5, + img: '/client-assets/drop-and-fusion/5yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}, { + id: '9dec1b38-d99d-40de-8288-37367b983d0d', + level: 1, + sizeX: YEN_BASE_SIZE, + sizeY: YEN_BASE_SIZE, + shape: 'circle', + score: 1, + dropCandidate: true, + sfxPitch: 4, + img: '/client-assets/drop-and-fusion/1yen.png', + imgSizeX: 256, + imgSizeY: 256, + spriteScale: 0.97, +}]; + 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, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 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, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 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, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 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, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 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, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0', level: 5, - size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a', level: 4, - size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919', level: 3, - size: SQUARE_BASE_SIZE * 1.25 * 1.25, + sizeX: SQUARE_BASE_SIZE * 1.25 * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d', level: 2, - size: SQUARE_BASE_SIZE * 1.25, + sizeX: SQUARE_BASE_SIZE * 1.25, + sizeY: 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, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }, { id: '35e476ee-44bd-4711-ad42-87be245d3efd', level: 1, - size: SQUARE_BASE_SIZE, + sizeX: SQUARE_BASE_SIZE, + sizeY: SQUARE_BASE_SIZE, shape: 'rectangle', score: 1, dropCandidate: true, sfxPitch: 4, img: '/client-assets/drop-and-fusion/keycap_1.png', - imgSize: 256, + imgSizeX: 256, + imgSizeY: 256, spriteScale: 1.12, }]; const props = defineProps<{ - gameMode: 'normal' | 'square'; + gameMode: 'normal' | 'square' | 'yen'; mute: boolean; }>(); @@ -398,7 +572,11 @@ const emit = defineEmits<{ (ev: 'end'): void; }>(); -const monoDefinitions = props.gameMode === 'normal' ? NORAML_MONOS : SQUARE_MONOS; +const monoDefinitions = + props.gameMode === 'normal' ? NORAML_MONOS : + props.gameMode === 'square' ? SQUARE_MONOS : + props.gameMode === 'yen' ? YEN_MONOS : + [] as never; let viewScale = 1; let seed: string = Date.now().toString(); @@ -413,6 +591,7 @@ let tickRaf: number | null = null; let game = new DropAndFusionGame({ seed: seed, monoDefinitions, + hasComboBonus: props.gameMode !== 'yen', }); attachGameEvents(); @@ -616,6 +795,7 @@ async function restart() { game = new DropAndFusionGame({ seed: seed, monoDefinitions, + hasComboBonus: props.gameMode !== 'yen', }); attachGameEvents(); await start(); @@ -640,7 +820,7 @@ function reset() { function dispose() { game.dispose(); - Matter.Render.stop(renderer); + if (renderer) Matter.Render.stop(renderer); if (tickRaf) { window.cancelAnimationFrame(tickRaf); } @@ -656,6 +836,7 @@ function replay() { game = new DropAndFusionGame({ seed: seed, monoDefinitions, + hasComboBonus: props.gameMode !== 'yen', replaying: true, }); attachGameEvents(); @@ -812,11 +993,19 @@ function attachGameEvents() { const panV = x - game.PLAYAREA_MARGIN; const panW = game.GAME_WIDTH - game.PLAYAREA_MARGIN - game.PLAYAREA_MARGIN; const pan = ((panV / panW) - 0.5) * 2; - sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', { - volume: sfxVolume.value, - pan, - playbackRate: replayPlaybackRate.value, - }); + if (props.gameMode === 'yen') { + sound.playUrl('/client-assets/drop-and-fusion/drop_yen.mp3', { + volume: sfxVolume.value, + pan, + playbackRate: replayPlaybackRate.value, + }); + } else { + sound.playUrl('/client-assets/drop-and-fusion/drop.mp3', { + volume: sfxVolume.value, + pan, + playbackRate: replayPlaybackRate.value, + }); + } if (replaying.value) return; @@ -853,9 +1042,15 @@ function attachGameEvents() { }); game.addListener('gameOver', () => { - sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { - volume: sfxVolume.value, - }); + if (props.gameMode === 'yen') { + sound.playUrl('/client-assets/drop-and-fusion/gameover_yen.mp3', { + volume: 0.5 * sfxVolume.value, + }); + } else { + sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { + volume: sfxVolume.value, + }); + } if (replaying.value) { endReplay(); @@ -1173,10 +1368,15 @@ definePageMetadata({ position: absolute; z-index: 10; top: 50%; - width: 100%; + left: 0; + right: 0; + margin: auto; + width: calc(100% - 50px); + max-width: 320px; padding: 16px; box-sizing: border-box; background: #0007; + border-radius: 16px; color: #fff; text-align: center; font-weight: bold; diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 0938ca6a87..80c466a2b1 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSelect v-model="gameMode"> <option value="normal">NORMAL</option> <option value="square">SQUARE</option> + <option value="yen">YEN</option> </MkSelect> <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> </div> @@ -42,12 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.frame"> <div :class="$style.frameInner"> <div class="_gaps_s" style="padding: 16px;"> - <div><b>{{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> + <div><b>{{ i18n.t('lastNDays', { n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> <div v-if="ranking" class="_gaps_s"> <div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord"> <MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/> <MkUserName :user="r.user" :nowrap="true"/> - <b style="margin-left: auto;">{{ r.score.toLocaleString() }} pt</b> + <b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ gameMode === 'yen' ? '円' : 'pt' }}</b> </div> </div> <div v-else>{{ i18n.ts.loading }}</div> @@ -94,7 +95,7 @@ import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -const gameMode = ref<'normal' | 'square'>('normal'); +const gameMode = ref<'normal' | 'square' | 'yen'>('normal'); const gameStarted = ref(false); const mute = ref(false); const ranking = ref(null); diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 41af9cb7a4..ad02c2832b 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -10,13 +10,15 @@ import seedrandom from 'seedrandom'; export type Mono = { id: string; level: number; - size: number; + sizeX: number; + sizeY: number; shape: 'circle' | 'rectangle'; score: number; dropCandidate: boolean; sfxPitch: number; img: string; - imgSize: number; + imgSizeX: number; + imgSizeY: number; spriteScale: number; }; @@ -59,6 +61,7 @@ export class DropAndFusionGame extends EventEmitter<{ private overflowCollider: Matter.Body; private isGameOver = false; private monoDefinitions: Mono[] = []; + private hasComboBonus = true; private rng: () => number; private logs: Log[] = []; private replaying = false; @@ -66,7 +69,9 @@ export class DropAndFusionGame extends EventEmitter<{ /** * フィールドに出ていて、かつ合体の対象となるアイテム */ - private activeBodyIds: Matter.Body['id'][] = []; + private fusionReadyBodyIds: Matter.Body['id'][] = []; + + private gameOverReadyBodyIds: Matter.Body['id'][] = []; /** * fusion予約アイテムのペア @@ -74,8 +79,6 @@ export class DropAndFusionGame extends EventEmitter<{ */ private fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = []; - private latestDroppedBodyId: Matter.Body['id'] | null = null; - private latestDroppedAt = 0; private latestFusionedAt = 0; // frame private stock: { id: string; mono: Mono }[] = []; @@ -101,11 +104,17 @@ export class DropAndFusionGame extends EventEmitter<{ public replayPlaybackRate = 1; - constructor(env: { monoDefinitions: Mono[]; seed: string; replaying?: boolean }) { + constructor(env: { + monoDefinitions: Mono[]; + seed: string; + hasComboBonus: boolean; + replaying?: boolean; + }) { super(); this.replaying = !!env.replaying; this.monoDefinitions = env.monoDefinitions; + this.hasComboBonus = env.hasComboBonus; this.rng = seedrandom(env.seed); this.tick = this.tick.bind(this); @@ -147,6 +156,7 @@ export class DropAndFusionGame extends EventEmitter<{ //#endregion this.overflowCollider = Matter.Bodies.rectangle(this.GAME_WIDTH / 2, 0, this.GAME_WIDTH, 200, { + label: '_overflow_', isStatic: true, isSensor: true, render: { @@ -165,7 +175,7 @@ export class DropAndFusionGame extends EventEmitter<{ const options: Matter.IBodyDefinition = { label: mono.id, //density: 0.0005, - density: mono.size / 1000, + density: ((mono.sizeX + mono.sizeY) / 2) / 1000, restitution: 0.2, frictionAir: 0.01, friction: 0.7, @@ -175,16 +185,16 @@ export class DropAndFusionGame extends EventEmitter<{ render: { sprite: { texture: mono.img, - xScale: (mono.size / mono.imgSize) * mono.spriteScale, - yScale: (mono.size / mono.imgSize) * mono.spriteScale, + xScale: (mono.sizeX / mono.imgSizeX) * mono.spriteScale, + yScale: (mono.sizeY / mono.imgSizeY) * mono.spriteScale, }, }, }; if (mono.shape === 'circle') { - return Matter.Bodies.circle(x, y, mono.size / 2, options); + return Matter.Bodies.circle(x, y, mono.sizeX / 2, options); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (mono.shape === 'rectangle') { - return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options); + return Matter.Bodies.rectangle(x, y, mono.sizeX, mono.sizeY, options); } else { throw new Error('unrecognized shape'); } @@ -202,8 +212,9 @@ export class DropAndFusionGame extends EventEmitter<{ const newX = (bodyA.position.x + bodyB.position.x) / 2; const newY = (bodyA.position.y + bodyB.position.y) / 2; + this.fusionReadyBodyIds = this.fusionReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); + this.gameOverReadyBodyIds = this.gameOverReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); - this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1); @@ -216,11 +227,11 @@ export class DropAndFusionGame extends EventEmitter<{ this.tickCallbackQueue.push({ frame: this.frame + this.msToFrame(100), callback: () => { - this.activeBodyIds.push(body.id); + this.fusionReadyBodyIds.push(body.id); }, }); - const comboBonus = 1 + ((this.combo - 1) / 5); + const comboBonus = this.hasComboBonus ? 1 + ((this.combo - 1) / 5) : 1; const additionalScore = Math.round(currentMono.score * comboBonus); this.score += additionalScore; @@ -245,14 +256,6 @@ export class DropAndFusionGame extends EventEmitter<{ for (const pairs of event.pairs) { const { bodyA, bodyB } = pairs; - if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) { - if (bodyA.id === this.latestDroppedBodyId || bodyB.id === this.latestDroppedBodyId) { - continue; - } - this.gameOver(); - break; - } - const shouldFusion = (bodyA.label === bodyB.label) && !this.fusionReservedPairs.some(x => x.bodyA.id === bodyA.id || @@ -261,7 +264,7 @@ export class DropAndFusionGame extends EventEmitter<{ x.bodyB.id === bodyB.id); if (shouldFusion) { - if (this.activeBodyIds.includes(bodyA.id) && this.activeBodyIds.includes(bodyB.id)) { + if (this.fusionReadyBodyIds.includes(bodyA.id) && this.fusionReadyBodyIds.includes(bodyB.id)) { this.fusion(bodyA, bodyB); } else { this.fusionReservedPairs.push({ bodyA, bodyB }); @@ -274,12 +277,19 @@ export class DropAndFusionGame extends EventEmitter<{ }); } } else { + if (bodyA.label === '_overflow_' || bodyB.label === '_overflow_') continue; + + if (bodyA.label !== '_wall_' && bodyB.label !== '_wall_') { + if (!this.gameOverReadyBodyIds.includes(bodyA.id)) this.gameOverReadyBodyIds.push(bodyA.id); + if (!this.gameOverReadyBodyIds.includes(bodyB.id)) this.gameOverReadyBodyIds.push(bodyB.id); + } + const energy = pairs.collision.depth; if (energy > minCollisionEnergyForSound) { const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; const panV = - pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN : - pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN : + bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN : + bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN : ((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN; const panW = this.GAME_WIDTH - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; const pan = ((panV / panW) - 0.5) * 2; @@ -290,6 +300,21 @@ export class DropAndFusionGame extends EventEmitter<{ } } + private onCollisionActive(event: Matter.IEventCollision<Matter.Engine>) { + for (const pairs of event.pairs) { + const { bodyA, bodyB } = pairs; + + // ハコからあふれたかどうかの判定 + if (bodyA.id === this.overflowCollider.id || bodyB.id === this.overflowCollider.id) { + if (this.gameOverReadyBodyIds.includes(bodyA.id) || this.gameOverReadyBodyIds.includes(bodyB.id)) { + this.gameOver(); + break; + } + continue; + } + } + } + public surrender() { this.logs.push({ frame: this.frame, @@ -314,6 +339,7 @@ export class DropAndFusionGame extends EventEmitter<{ this.emit('changeStock', this.stock); Matter.Events.on(this.engine, 'collisionStart', this.onCollision.bind(this)); + Matter.Events.on(this.engine, 'collisionActive', this.onCollisionActive.bind(this)); } public getLogs() { @@ -360,17 +386,18 @@ export class DropAndFusionGame extends EventEmitter<{ this.emit('changeStock', this.stock); const inputX = Math.round(_x); - const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), inputX)); - const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); + const x = Math.min(this.GAME_WIDTH - this.PLAYAREA_MARGIN - (head.mono.sizeX / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.sizeX / 2), inputX)); + const body = this.createBody(head.mono, x, 50 + head.mono.sizeY / 2); this.logs.push({ frame: this.frame, operation: 'drop', x: inputX, }); Matter.Composite.add(this.engine.world, body); - this.activeBodyIds.push(body.id); - this.latestDroppedBodyId = body.id; + + this.fusionReadyBodyIds.push(body.id); this.latestDroppedAt = Date.now(); + this.emit('dropped', x); this.emit('monoAdded', head.mono); }