Merge remote-tracking branch 'misskey-original/develop' into develop
This commit is contained in:
		
						commit
						1dce38dfed
					
				locales
packages
backend/src/server/api/endpoints/bubble-game
frontend
assets/drop-and-fusion
10000yen.png1000yen.png100yen.png10yen.png1yen.png2000yen.png5000yen.png500yen.png50yen.png5yen.pngcollision.mp3collision_yen.mp3drop.mp3drop_yen.mp3fusion.mp3fusion_yen.mp3gameover_yen.mp3
src
misskey-js
|  | @ -1243,6 +1243,8 @@ export interface Locale { | |||
|     "replay": string; | ||||
|     "replaying": string; | ||||
|     "ranking": string; | ||||
|     "lastNDays": string; | ||||
|     "backToTitle": string; | ||||
|     "_bubbleGame": { | ||||
|         "howToPlay": string; | ||||
|         "_howToPlay": { | ||||
|  |  | |||
|  | @ -1240,6 +1240,8 @@ showReplay: "リプレイを見る" | |||
| replay: "リプレイ" | ||||
| replaying: "リプレイ中" | ||||
| ranking: "ランキング" | ||||
| lastNDays: "直近{n}日" | ||||
| backToTitle: "タイトルへ" | ||||
| 
 | ||||
| _bubbleGame: | ||||
|   howToPlay: "遊び方" | ||||
|  |  | |||
|  | @ -11,8 +11,6 @@ import { DI } from '@/di-symbols.js'; | |||
| import { UserEntityService } from '@/core/entities/UserEntityService.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: [], | ||||
| 
 | ||||
| 	allowGet: true, | ||||
| 	cacheSec: 60, | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,8 +12,6 @@ import { DI } from '@/di-symbols.js'; | |||
| import { ApiError } from '../../error.js'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: [], | ||||
| 
 | ||||
| 	requireCredential: true, | ||||
| 
 | ||||
| 	kind: 'write:account', | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 91 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 96 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 54 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 66 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 56 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 86 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 92 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 66 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 41 KiB | 
										
											Binary file not shown.
										
									
								
							| After   (image error) Size: 59 KiB | 
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| <script lang="ts" setup> | ||||
| import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | ||||
|  | @ -23,9 +24,16 @@ import { initChart } from '@/scripts/init-chart.js'; | |||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	src: string; | ||||
| }>(); | ||||
| export type HeatmapSource = 'active-users' | 'notes' | 'ap-requests-inbox-received' | 'ap-requests-deliver-succeeded' | 'ap-requests-deliver-failed'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	src: HeatmapSource; | ||||
| 	user?: Misskey.entities.User; | ||||
| 	label?: string; | ||||
| }>(), { | ||||
| 	user: undefined, | ||||
| 	label: '', | ||||
| }); | ||||
| 
 | ||||
| const rootEl = shallowRef<HTMLDivElement>(null); | ||||
| const chartEl = shallowRef<HTMLCanvasElement>(null); | ||||
|  | @ -75,8 +83,13 @@ async function renderChart() { | |||
| 		const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); | ||||
| 		values = raw.readWrite; | ||||
| 	} else if (props.src === 'notes') { | ||||
| 		if (props.user) { | ||||
| 			const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 			values = raw.inc; | ||||
| 		} else { | ||||
| 			const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' }); | ||||
| 			values = raw.local.inc; | ||||
| 		} | ||||
| 	} else if (props.src === 'ap-requests-inbox-received') { | ||||
| 		const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' }); | ||||
| 		values = raw.inboxReceived; | ||||
|  | @ -105,7 +118,7 @@ async function renderChart() { | |||
| 		type: 'matrix', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				label: 'Read & Write', | ||||
| 				label: props.label, | ||||
| 				data: format(values), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
|  | @ -128,6 +141,9 @@ async function renderChart() { | |||
| 					const a = c.chart.chartArea ?? {}; | ||||
| 					return (a.bottom - a.top) / 7 - marginEachCell; | ||||
| 				}, | ||||
| 			/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> | ||||
| 			}] satisfies ChartData[], | ||||
| 			 */ | ||||
| 			}], | ||||
| 		}, | ||||
| 		options: { | ||||
|  | @ -195,7 +211,7 @@ async function renderChart() { | |||
| 						}, | ||||
| 						label(context) { | ||||
| 							const v = context.dataset.data[context.dataIndex]; | ||||
| 							return ['Active: ' + v.v]; | ||||
| 							return [v.v]; | ||||
| 						}, | ||||
| 					}, | ||||
| 					//mode: 'index', | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 			<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option> | ||||
| 		</MkSelect> | ||||
| 		<div class="_panel" :class="$style.heatmap"> | ||||
| 			<MkHeatmap :src="heatmapSrc"/> | ||||
| 			<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/> | ||||
| 		</div> | ||||
| 	</MkFoldableSection> | ||||
| 
 | ||||
|  | @ -92,7 +92,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | |||
| import * as os from '@/os.js'; | ||||
| import { misskeyApiGet } from '@/scripts/misskey-api.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import MkHeatmap from '@/components/MkHeatmap.vue'; | ||||
| import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; | ||||
| import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; | ||||
|  | @ -103,7 +103,7 @@ initChart(); | |||
| const chartLimit = 500; | ||||
| const chartSpan = ref<'hour' | 'day'>('hour'); | ||||
| const chartSrc = ref('active-users'); | ||||
| const heatmapSrc = ref('active-users'); | ||||
| const heatmapSrc = ref<HeatmapSource>('active-users'); | ||||
| const subDoughnutEl = shallowRef<HTMLCanvasElement>(); | ||||
| const pubDoughnutEl = shallowRef<HTMLCanvasElement>(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ import * as os from '@/os.js'; | |||
| const props = withDefaults(defineProps<{ | ||||
| 	x: number; | ||||
| 	y: number; | ||||
| 	value?: number; | ||||
| 	value?: number | string; | ||||
| }>(), { | ||||
| 	value: 1, | ||||
| }); | ||||
|  |  | |||
|  | @ -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,8 +75,9 @@ 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 v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/>円</b></div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div> | ||||
|  | @ -90,15 +91,15 @@ 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> | ||||
| 			<div v-if="isGameOver" :class="$style.frame"> | ||||
| 				<div :class="$style.frameInner"> | ||||
| 					<div class="_buttonsCenter"> | ||||
| 						<MkButton primary rounded @click="backToTitle">{{ i18n.ts.done }}</MkButton> | ||||
| 						<MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton> | ||||
| 						<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton> | ||||
| 						<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton> | ||||
| 						<MkButton rounded @click="exportLog">Copy replay data</MkButton> | ||||
|  | @ -108,8 +109,9 @@ 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 v-if="gameMode === 'yen'">TOTAL EARNINGS: <b v-if="yenTotal"><MkNumber :value="yenTotal"/>円</b><b v-else>-</b></div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div :class="[$style.frame]" style="margin-left: auto;"> | ||||
|  | @ -167,230 +169,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 +574,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 +593,7 @@ let tickRaf: number | null = null; | |||
| let game = new DropAndFusionGame({ | ||||
| 	seed: seed, | ||||
| 	monoDefinitions, | ||||
| 	hasComboBonus: props.gameMode !== 'yen', | ||||
| }); | ||||
| attachGameEvents(); | ||||
| 
 | ||||
|  | @ -430,6 +611,7 @@ const dropReady = ref(true); | |||
| const isGameOver = ref(false); | ||||
| const gameLoaded = ref(false); | ||||
| const highScore = ref<number | null>(null); | ||||
| const yenTotal = ref<number | null>(null); | ||||
| const showConfig = ref(false); | ||||
| const replaying = ref(false); | ||||
| const replayPlaybackRate = ref(1); | ||||
|  | @ -555,8 +737,8 @@ function tickReplay() { | |||
| } | ||||
| 
 | ||||
| async function start() { | ||||
| 	await loadMonoTextures(); | ||||
| 	renderer = createRendererInstance(game); | ||||
| 	await loadMonoTextures(); | ||||
| 	Matter.Render.lookAt(renderer, { | ||||
| 		min: { x: 0, y: 0 }, | ||||
| 		max: { x: game.GAME_WIDTH, y: game.GAME_HEIGHT }, | ||||
|  | @ -616,6 +798,7 @@ async function restart() { | |||
| 	game = new DropAndFusionGame({ | ||||
| 		seed: seed, | ||||
| 		monoDefinitions, | ||||
| 		hasComboBonus: props.gameMode !== 'yen', | ||||
| 	}); | ||||
| 	attachGameEvents(); | ||||
| 	await start(); | ||||
|  | @ -640,7 +823,7 @@ function reset() { | |||
| 
 | ||||
| function dispose() { | ||||
| 	game.dispose(); | ||||
| 	Matter.Render.stop(renderer); | ||||
| 	if (renderer) Matter.Render.stop(renderer); | ||||
| 	if (tickRaf) { | ||||
| 		window.cancelAnimationFrame(tickRaf); | ||||
| 	} | ||||
|  | @ -656,6 +839,7 @@ function replay() { | |||
| 	game = new DropAndFusionGame({ | ||||
| 		seed: seed, | ||||
| 		monoDefinitions, | ||||
| 		hasComboBonus: props.gameMode !== 'yen', | ||||
| 		replaying: true, | ||||
| 	}); | ||||
| 	attachGameEvents(); | ||||
|  | @ -732,7 +916,7 @@ function getGameImageDriveFile() { | |||
| 			ctx.fillStyle = '#000'; | ||||
| 			ctx.font = '16px bold sans-serif'; | ||||
| 			ctx.textBaseline = 'top'; | ||||
| 			ctx.fillText(`SCORE: ${score.value.toLocaleString()}`, 10, 10); | ||||
| 			ctx.fillText(`SCORE: ${score.value.toLocaleString()}${props.gameMode === 'yen' ? '円' : 'pt'}`, 10, 10); | ||||
| 
 | ||||
| 			ctx.globalAlpha = 0.7; | ||||
| 			ctx.drawImage(logo, game.GAME_WIDTH * 0.55, 6, game.GAME_WIDTH * 0.45, game.GAME_WIDTH * 0.45 * (logo.height / logo.width)); | ||||
|  | @ -772,9 +956,8 @@ async function share() { | |||
| 	const file = await uploading; | ||||
| 	if (!file) return; | ||||
| 	os.post({ | ||||
| 		initialText: `#BubbleGame | ||||
| MODE: ${props.gameMode} | ||||
| SCORE: ${score.value.toLocaleString()} (MAX CHAIN: ${maxCombo.value})`, | ||||
| 		initialText: `#BubbleGame (${props.gameMode}) | ||||
| SCORE: ${score.value.toLocaleString()}${props.gameMode === 'yen' ? '円' : 'pt'}`, | ||||
| 		initialFiles: [file], | ||||
| 		instant: true, | ||||
| 	}); | ||||
|  | @ -805,6 +988,7 @@ function attachGameEvents() { | |||
| 
 | ||||
| 		sound.playUrl('/client-assets/drop-and-fusion/hold.mp3', { | ||||
| 			volume: 0.5 * sfxVolume.value, | ||||
| 			playbackRate: replayPlaybackRate.value, | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -812,11 +996,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', { | ||||
| 		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; | ||||
| 
 | ||||
|  | @ -835,7 +1027,7 @@ function attachGameEvents() { | |||
| 		const domX = rect.left + (x * viewScale); | ||||
| 		const domY = rect.top + (y * viewScale); | ||||
| 		os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end'); | ||||
| 		os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta }, {}, 'end'); | ||||
| 		os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (props.gameMode === 'yen' ? '円' : '') }, {}, 'end'); | ||||
| 	}); | ||||
| 
 | ||||
| 	game.addListener('monoAdded', (mono) => { | ||||
|  | @ -853,9 +1045,15 @@ function attachGameEvents() { | |||
| 	}); | ||||
| 
 | ||||
| 	game.addListener('gameOver', () => { | ||||
| 		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(); | ||||
|  | @ -876,6 +1074,15 @@ function attachGameEvents() { | |||
| 			logs: DropAndFusionGame.serializeLogs(logs), | ||||
| 		}); | ||||
| 
 | ||||
| 		if (props.gameMode === 'yen') { | ||||
| 			yenTotal.value = (yenTotal.value ?? 0) + score.value; | ||||
| 			misskeyApi('i/registry/set', { | ||||
| 				scope: ['dropAndFusionGame'], | ||||
| 				key: 'yenTotal', | ||||
| 				value: yenTotal.value, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		if (score.value > (highScore.value ?? 0)) { | ||||
| 			highScore.value = score.value; | ||||
| 
 | ||||
|  | @ -890,16 +1097,35 @@ function attachGameEvents() { | |||
| 	game.addListener('sfx', (type, params) => { | ||||
| 		if (props.mute) return; | ||||
| 
 | ||||
| 		const soundUrl = | ||||
| 			type === 'fusion' ? '/client-assets/drop-and-fusion/bubble2.mp3' : | ||||
| 			type === 'collision' ? '/client-assets/drop-and-fusion/poi1.mp3' : | ||||
| 			null as never; | ||||
| 
 | ||||
| 		sound.playUrl(soundUrl, { | ||||
| 		if (type === 'fusion') { | ||||
| 			if (props.gameMode === 'yen') { | ||||
| 				sound.playUrl('/client-assets/drop-and-fusion/fusion_yen.mp3', { | ||||
| 					volume: 0.25 * params.volume * sfxVolume.value, | ||||
| 					pan: params.pan, | ||||
| 					playbackRate: (params.pitch / 4) * replayPlaybackRate.value, | ||||
| 				}); | ||||
| 			} else { | ||||
| 				sound.playUrl('/client-assets/drop-and-fusion/fusion.mp3', { | ||||
| 					volume: params.volume * sfxVolume.value, | ||||
| 					pan: params.pan, | ||||
| 					playbackRate: params.pitch * replayPlaybackRate.value, | ||||
| 				}); | ||||
| 			} | ||||
| 		} else if (type === 'collision') { | ||||
| 			if (props.gameMode === 'yen') { | ||||
| 				sound.playUrl('/client-assets/drop-and-fusion/collision_yen.mp3', { | ||||
| 					volume: params.volume * sfxVolume.value, | ||||
| 					pan: params.pan, | ||||
| 					playbackRate: Math.max(1, params.pitch) * replayPlaybackRate.value, | ||||
| 				}); | ||||
| 			} else { | ||||
| 				sound.playUrl('/client-assets/drop-and-fusion/collision.mp3', { | ||||
| 					volume: params.volume * sfxVolume.value, | ||||
| 					pan: params.pan, | ||||
| 					playbackRate: params.pitch * replayPlaybackRate.value, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | @ -921,6 +1147,25 @@ onMounted(async () => { | |||
| 		highScore.value = null; | ||||
| 	} | ||||
| 
 | ||||
| 	if (props.gameMode === 'yen') { | ||||
| 		try { | ||||
| 			yenTotal.value = await misskeyApi('i/registry/get', { | ||||
| 				scope: ['dropAndFusionGame'], | ||||
| 				key: 'yenTotal', | ||||
| 			}); | ||||
| 		} catch (err) { | ||||
| 			if (err.code === 'NO_SUCH_KEY') { | ||||
| 				// nop | ||||
| 			} else { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: i18n.ts.cannotLoad, | ||||
| 				}); | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	await start(); | ||||
| 
 | ||||
| 	const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3'); | ||||
|  | @ -1173,10 +1418,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; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -1,219 +0,0 @@ | |||
| <!-- | ||||
| SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
| SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
| 
 | ||||
| <template> | ||||
| <div ref="rootEl"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div v-else :class="$style.root" class="_panel"> | ||||
| 		<canvas ref="chartEl"></canvas> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import { onMounted, nextTick, watch, shallowRef, ref } from 'vue'; | ||||
| import { Chart } from 'chart.js'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { misskeyApi } from '@/scripts/misskey-api.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; | ||||
| import { alpha } from '@/scripts/color.js'; | ||||
| import { initChart } from '@/scripts/init-chart.js'; | ||||
| 
 | ||||
| initChart(); | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	src: string; | ||||
| 	user: Misskey.entities.User; | ||||
| }>(); | ||||
| 
 | ||||
| const rootEl = shallowRef<HTMLDivElement>(null); | ||||
| const chartEl = shallowRef<HTMLCanvasElement>(null); | ||||
| const now = new Date(); | ||||
| let chartInstance: Chart = null; | ||||
| const fetching = ref(true); | ||||
| 
 | ||||
| const { handler: externalTooltipHandler } = useChartTooltip({ | ||||
| 	position: 'middle', | ||||
| }); | ||||
| 
 | ||||
| async function renderChart() { | ||||
| 	if (chartInstance) { | ||||
| 		chartInstance.destroy(); | ||||
| 	} | ||||
| 
 | ||||
| 	const wide = rootEl.value.offsetWidth > 700; | ||||
| 	const narrow = rootEl.value.offsetWidth < 400; | ||||
| 
 | ||||
| 	const weeks = wide ? 50 : narrow ? 10 : 25; | ||||
| 	const chartLimit = 7 * weeks; | ||||
| 
 | ||||
| 	const getDate = (ago: number) => { | ||||
| 		const y = now.getFullYear(); | ||||
| 		const m = now.getMonth(); | ||||
| 		const d = now.getDate(); | ||||
| 
 | ||||
| 		return new Date(y, m, d - ago); | ||||
| 	}; | ||||
| 
 | ||||
| 	const format = (arr) => { | ||||
| 		return arr.map((v, i) => { | ||||
| 			const dt = getDate(i); | ||||
| 			const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`; | ||||
| 			return { | ||||
| 				x: iso, | ||||
| 				y: dt.getDay(), | ||||
| 				d: iso, | ||||
| 				v, | ||||
| 			}; | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	let values; | ||||
| 
 | ||||
| 	if (props.src === 'notes') { | ||||
| 		const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); | ||||
| 		values = raw.inc; | ||||
| 	} | ||||
| 
 | ||||
| 	fetching.value = false; | ||||
| 
 | ||||
| 	await nextTick(); | ||||
| 
 | ||||
| 	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300'; | ||||
| 
 | ||||
| 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする | ||||
| 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3; | ||||
| 
 | ||||
| 	const min = Math.max(0, Math.min(...values) - 1); | ||||
| 
 | ||||
| 	const marginEachCell = 4; | ||||
| 
 | ||||
| 	chartInstance = new Chart(chartEl.value, { | ||||
| 		type: 'matrix', | ||||
| 		data: { | ||||
| 			datasets: [{ | ||||
| 				label: '', | ||||
| 				data: format(values), | ||||
| 				pointRadius: 0, | ||||
| 				borderWidth: 0, | ||||
| 				borderJoinStyle: 'round', | ||||
| 				borderRadius: 3, | ||||
| 				backgroundColor(c) { | ||||
| 					const value = c.dataset.data[c.dataIndex].v; | ||||
| 					let a = (value - min) / max; | ||||
| 					if (value !== 0) { // 0でない限りは完全に不可視にはしない | ||||
| 						a = Math.max(a, 0.05); | ||||
| 					} | ||||
| 					return alpha(color, a); | ||||
| 				}, | ||||
| 				fill: true, | ||||
| 				width(c) { | ||||
| 					const a = c.chart.chartArea ?? {}; | ||||
| 					return (a.right - a.left) / weeks - marginEachCell; | ||||
| 				}, | ||||
| 				height(c) { | ||||
| 					const a = c.chart.chartArea ?? {}; | ||||
| 					return (a.bottom - a.top) / 7 - marginEachCell; | ||||
| 				}, | ||||
| 			/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> | ||||
| 			}] satisfies ChartData[], | ||||
| 			 */ | ||||
| 			}], | ||||
| 		}, | ||||
| 		options: { | ||||
| 			aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2, | ||||
| 			layout: { | ||||
| 				padding: { | ||||
| 					left: 8, | ||||
| 					right: 0, | ||||
| 					top: 0, | ||||
| 					bottom: 0, | ||||
| 				}, | ||||
| 			}, | ||||
| 			scales: { | ||||
| 				x: { | ||||
| 					type: 'time', | ||||
| 					offset: true, | ||||
| 					position: 'bottom', | ||||
| 					time: { | ||||
| 						unit: 'week', | ||||
| 						round: 'week', | ||||
| 						isoWeekday: 0, | ||||
| 						displayFormats: { | ||||
| 							day: 'M/d', | ||||
| 							month: 'Y/M', | ||||
| 							week: 'M/d', | ||||
| 						}, | ||||
| 					}, | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						display: true, | ||||
| 						maxRotation: 0, | ||||
| 						autoSkipPadding: 8, | ||||
| 					}, | ||||
| 				}, | ||||
| 				y: { | ||||
| 					offset: true, | ||||
| 					reverse: true, | ||||
| 					position: 'right', | ||||
| 					grid: { | ||||
| 						display: false, | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						maxRotation: 0, | ||||
| 						autoSkip: true, | ||||
| 						padding: 1, | ||||
| 						font: { | ||||
| 							size: 9, | ||||
| 						}, | ||||
| 						callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value], | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			plugins: { | ||||
| 				legend: { | ||||
| 					display: false, | ||||
| 				}, | ||||
| 				tooltip: { | ||||
| 					enabled: false, | ||||
| 					callbacks: { | ||||
| 						title(context) { | ||||
| 							const v = context[0].dataset.data[context[0].dataIndex]; | ||||
| 							return v.d; | ||||
| 						}, | ||||
| 						label(context) { | ||||
| 							const v = context.dataset.data[context.dataIndex]; | ||||
| 							return [v.v]; | ||||
| 						}, | ||||
| 					}, | ||||
| 					//mode: 'index', | ||||
| 					animation: { | ||||
| 						duration: 0, | ||||
| 					}, | ||||
| 					external: externalTooltipHandler, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| watch(() => props.src, () => { | ||||
| 	fetching.value = true; | ||||
| 	renderChart(); | ||||
| }); | ||||
| 
 | ||||
| onMounted(async () => { | ||||
| 	renderChart(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" module> | ||||
| .root { | ||||
| 	padding: 20px; | ||||
| } | ||||
| </style> | ||||
|  | @ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 	<div class="_gaps"> | ||||
| 		<MkFoldableSection class="item"> | ||||
| 			<template #header><i class="ti ti-activity"></i> Heatmap</template> | ||||
| 			<XHeatmap :user="user" :src="'notes'"/> | ||||
| 			<MkHeatmap :user="user" :src="'notes'"/> | ||||
| 		</MkFoldableSection> | ||||
| 		<MkFoldableSection class="item"> | ||||
| 			<template #header><i class="ti ti-pencil"></i> Notes</template> | ||||
|  | @ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only | |||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import XHeatmap from './activity.heatmap.vue'; | ||||
| import XPv from './activity.pv.vue'; | ||||
| import XNotes from './activity.notes.vue'; | ||||
| import XFollowing from './activity.following.vue'; | ||||
| import MkFoldableSection from '@/components/MkFoldableSection.vue'; | ||||
| import MkHeatmap from '@/components/MkHeatmap.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	user: Misskey.entities.User; | ||||
|  |  | |||
|  | @ -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; | ||||
| }; | ||||
| 
 | ||||
|  | @ -45,7 +47,7 @@ export class DropAndFusionGame extends EventEmitter<{ | |||
| }> { | ||||
| 	private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
 | ||||
| 	private COMBO_INTERVAL = 60; // frame
 | ||||
| 	public readonly GAME_VERSION = 1; | ||||
| 	public readonly GAME_VERSION = 2; | ||||
| 	public readonly GAME_WIDTH = 450; | ||||
| 	public readonly GAME_HEIGHT = 600; | ||||
| 	public readonly DROP_INTERVAL = 500; | ||||
|  | @ -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); | ||||
| 	} | ||||
|  |  | |||
|  | @ -473,6 +473,18 @@ type BlockingListRequest = operations['blocking/list']['requestBody']['content'] | |||
| // @public (undocumented) | ||||
| type BlockingListResponse = operations['blocking/list']['responses']['200']['content']['application/json']; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
| type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
| type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
| type BubbleGameRegisterRequest = operations['bubble-game/register']['requestBody']['content']['application/json']; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
| type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; | ||||
| 
 | ||||
| // @public (undocumented) | ||||
| type Channel = components['schemas']['Channel']; | ||||
| 
 | ||||
|  | @ -1607,6 +1619,10 @@ declare namespace entities { | |||
|         FetchExternalResourcesRequest, | ||||
|         FetchExternalResourcesResponse, | ||||
|         RetentionResponse, | ||||
|         BubbleGameRegisterRequest, | ||||
|         BubbleGameRegisterResponse, | ||||
|         BubbleGameRankingRequest, | ||||
|         BubbleGameRankingResponse, | ||||
|         Error_2 as Error, | ||||
|         UserLite, | ||||
|         UserDetailedNotMeOnly, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| /* | ||||
|  * version: 2023.12.2 | ||||
|  * generatedAt: 2024-01-07T15:22:15.630Z | ||||
|  * generatedAt: 2024-01-11T14:29:04.817Z | ||||
|  */ | ||||
| 
 | ||||
| import type { SwitchCaseResponseType } from '../api.js'; | ||||
|  | @ -3985,5 +3985,27 @@ declare module '../api.js' { | |||
|       params: P, | ||||
|       credential?: string | null, | ||||
|     ): Promise<SwitchCaseResponseType<E, P>>; | ||||
| 
 | ||||
|     /** | ||||
|      * No description provided. | ||||
|      *  | ||||
|      * **Credential required**: *Yes* / **Permission**: *write:account* | ||||
|      */ | ||||
|     request<E extends 'bubble-game/register', P extends Endpoints[E]['req']>( | ||||
|       endpoint: E, | ||||
|       params: P, | ||||
|       credential?: string | null, | ||||
|     ): Promise<SwitchCaseResponseType<E, P>>; | ||||
| 
 | ||||
|     /** | ||||
|      * No description provided. | ||||
|      *  | ||||
|      * **Credential required**: *No* | ||||
|      */ | ||||
|     request<E extends 'bubble-game/ranking', P extends Endpoints[E]['req']>( | ||||
|       endpoint: E, | ||||
|       params: P, | ||||
|       credential?: string | null, | ||||
|     ): Promise<SwitchCaseResponseType<E, P>>; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| /* | ||||
|  * version: 2023.12.2 | ||||
|  * generatedAt: 2024-01-07T15:22:15.626Z | ||||
|  * generatedAt: 2024-01-11T14:29:04.814Z | ||||
|  */ | ||||
| 
 | ||||
| import type { | ||||
|  | @ -540,6 +540,10 @@ import type { | |||
| 	FetchExternalResourcesRequest, | ||||
| 	FetchExternalResourcesResponse, | ||||
| 	RetentionResponse, | ||||
| 	BubbleGameRegisterRequest, | ||||
| 	BubbleGameRegisterResponse, | ||||
| 	BubbleGameRankingRequest, | ||||
| 	BubbleGameRankingResponse, | ||||
| } from './entities.js'; | ||||
| 
 | ||||
| export type Endpoints = { | ||||
|  | @ -901,4 +905,6 @@ export type Endpoints = { | |||
| 	'fetch-rss': { req: FetchRssRequest; res: FetchRssResponse }; | ||||
| 	'fetch-external-resources': { req: FetchExternalResourcesRequest; res: FetchExternalResourcesResponse }; | ||||
| 	'retention': { req: EmptyRequest; res: RetentionResponse }; | ||||
| 	'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; | ||||
| 	'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| /* | ||||
|  * version: 2023.12.2 | ||||
|  * generatedAt: 2024-01-07T15:22:15.624Z | ||||
|  * generatedAt: 2024-01-11T14:29:04.811Z | ||||
|  */ | ||||
| 
 | ||||
| import { operations } from './types.js'; | ||||
|  | @ -542,3 +542,7 @@ export type FetchRssResponse = operations['fetch-rss']['responses']['200']['cont | |||
| export type FetchExternalResourcesRequest = operations['fetch-external-resources']['requestBody']['content']['application/json']; | ||||
| export type FetchExternalResourcesResponse = operations['fetch-external-resources']['responses']['200']['content']['application/json']; | ||||
| export type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; | ||||
| export type BubbleGameRegisterRequest = operations['bubble-game/register']['requestBody']['content']['application/json']; | ||||
| export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; | ||||
| export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; | ||||
| export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| /* | ||||
|  * version: 2023.12.2 | ||||
|  * generatedAt: 2024-01-07T15:22:15.623Z | ||||
|  * generatedAt: 2024-01-11T14:29:04.810Z | ||||
|  */ | ||||
| 
 | ||||
| import { components } from './types.js'; | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 
 | ||||
| /* | ||||
|  * version: 2023.12.2 | ||||
|  * generatedAt: 2024-01-07T15:22:15.494Z | ||||
|  * generatedAt: 2024-01-11T14:29:04.681Z | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  | @ -3447,6 +3447,31 @@ export type paths = { | |||
|      */ | ||||
|     post: operations['retention']; | ||||
|   }; | ||||
|   '/bubble-game/register': { | ||||
|     /** | ||||
|      * bubble-game/register | ||||
|      * @description No description provided. | ||||
|      * | ||||
|      * **Credential required**: *Yes* / **Permission**: *write:account* | ||||
|      */ | ||||
|     post: operations['bubble-game/register']; | ||||
|   }; | ||||
|   '/bubble-game/ranking': { | ||||
|     /** | ||||
|      * bubble-game/ranking | ||||
|      * @description No description provided. | ||||
|      * | ||||
|      * **Credential required**: *No* | ||||
|      */ | ||||
|     get: operations['bubble-game/ranking']; | ||||
|     /** | ||||
|      * bubble-game/ranking | ||||
|      * @description No description provided. | ||||
|      * | ||||
|      * **Credential required**: *No* | ||||
|      */ | ||||
|     post: operations['bubble-game/ranking']; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export type webhooks = Record<string, never>; | ||||
|  | @ -25398,5 +25423,126 @@ export type operations = { | |||
|       }; | ||||
|     }; | ||||
|   }; | ||||
|   /** | ||||
|    * bubble-game/register | ||||
|    * @description No description provided. | ||||
|    * | ||||
|    * **Credential required**: *Yes* / **Permission**: *write:account* | ||||
|    */ | ||||
|   'bubble-game/register': { | ||||
|     requestBody: { | ||||
|       content: { | ||||
|         'application/json': { | ||||
|           score: number; | ||||
|           seed: string; | ||||
|           logs: unknown[]; | ||||
|           gameMode: string; | ||||
|           gameVersion: number; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|     responses: { | ||||
|       /** @description OK (with results) */ | ||||
|       200: { | ||||
|         content: { | ||||
|           'application/json': unknown; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Client error */ | ||||
|       400: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Authentication error */ | ||||
|       401: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Forbidden error */ | ||||
|       403: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description I'm Ai */ | ||||
|       418: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description To many requests */ | ||||
|       429: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Internal server error */ | ||||
|       500: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
|   /** | ||||
|    * bubble-game/ranking | ||||
|    * @description No description provided. | ||||
|    * | ||||
|    * **Credential required**: *No* | ||||
|    */ | ||||
|   'bubble-game/ranking': { | ||||
|     requestBody: { | ||||
|       content: { | ||||
|         'application/json': { | ||||
|           gameMode: string; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|     responses: { | ||||
|       /** @description OK (with results) */ | ||||
|       200: { | ||||
|         content: { | ||||
|           'application/json': { | ||||
|               /** Format: misskey:id */ | ||||
|               id: string; | ||||
|               score: number; | ||||
|               user: components['schemas']['UserLite']; | ||||
|             }[]; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Client error */ | ||||
|       400: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Authentication error */ | ||||
|       401: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Forbidden error */ | ||||
|       403: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description I'm Ai */ | ||||
|       418: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|       /** @description Internal server error */ | ||||
|       500: { | ||||
|         content: { | ||||
|           'application/json': components['schemas']['Error']; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue