feat(frontend): バブルゲームを追加 (MisskeyIO#336)
|
@ -1190,6 +1190,7 @@ export interface Locale {
|
||||||
"decorate": string;
|
"decorate": string;
|
||||||
"addMfmFunction": string;
|
"addMfmFunction": string;
|
||||||
"enableQuickAddMfmFunction": string;
|
"enableQuickAddMfmFunction": string;
|
||||||
|
"bubbleGame": string;
|
||||||
"abuseReportCategory": string;
|
"abuseReportCategory": string;
|
||||||
"selectCategory": string;
|
"selectCategory": string;
|
||||||
"reportComplete": string;
|
"reportComplete": string;
|
||||||
|
|
|
@ -1187,6 +1187,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
||||||
decorate: "デコる"
|
decorate: "デコる"
|
||||||
addMfmFunction: "装飾を追加"
|
addMfmFunction: "装飾を追加"
|
||||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||||
|
bubbleGame: "バブルゲーム"
|
||||||
abuseReportCategory: "通報の種類"
|
abuseReportCategory: "通報の種類"
|
||||||
selectCategory: "カテゴリを選択"
|
selectCategory: "カテゴリを選択"
|
||||||
reportComplete: "通報完了"
|
reportComplete: "通報完了"
|
||||||
|
|
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 646 B |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 44 KiB |
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||||
<span class="text" :class="{ up }">+1</span>
|
<span class="text" :class="{ up }">+{{ value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ import * as os from '@/os.js';
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
value?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
value: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|
|
@ -531,6 +531,10 @@ export const routes = [{
|
||||||
path: '/clicker',
|
path: '/clicker',
|
||||||
component: page(() => import('./pages/clicker.vue')),
|
component: page(() => import('./pages/clicker.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/bubble-game',
|
||||||
|
component: page(() => import('./pages/drop-and-fusion.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
path: '/timeline',
|
path: '/timeline',
|
||||||
component: page(() => import('./pages/timeline.vue')),
|
component: page(() => import('./pages/timeline.vue')),
|
||||||
|
|
|
@ -154,7 +154,13 @@ export type OperationType = typeof operationTypes[number];
|
||||||
* @param soundStore サウンド設定
|
* @param soundStore サウンド設定
|
||||||
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||||
*/
|
*/
|
||||||
export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
|
export async function loadAudio(soundStore: {
|
||||||
|
type: Exclude<SoundType, '_driveFile_'>;
|
||||||
|
} | {
|
||||||
|
type: '_driveFile_';
|
||||||
|
fileId: string;
|
||||||
|
fileUrl: string;
|
||||||
|
}, options?: { useCache?: boolean; }) {
|
||||||
if (_DEV_) console.log('loading audio. opts:', options);
|
if (_DEV_) console.log('loading audio. opts:', options);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
if (soundStore.type === null || (soundStore.type === '_driveFile_' && (!$i?.policies.canUseDriveFileInSoundSettings || !soundStore.fileUrl))) {
|
if (soundStore.type === null || (soundStore.type === '_driveFile_' && (!$i?.policies.canUseDriveFileInSoundSettings || !soundStore.fileUrl))) {
|
||||||
|
@ -241,18 +247,31 @@ export async function playFile(soundStore: SoundStore) {
|
||||||
createSourceNode(buffer, soundStore.volume)?.start();
|
createSourceNode(buffer, soundStore.volume)?.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
|
export async function playRaw(type: Exclude<SoundType, '_driveFile_'>, volume = 1, pan = 0, playbackRate = 1) {
|
||||||
|
const buffer = await loadAudio({ type });
|
||||||
|
if (!buffer) return;
|
||||||
|
createSourceNode(buffer, volume, pan, playbackRate)?.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1) : AudioBufferSourceNode | null {
|
||||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||||
if (isMute() || masterVolume === 0 || volume === 0) {
|
if (isMute() || masterVolume === 0 || volume === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const panNode = ctx.createStereoPanner();
|
||||||
|
panNode.pan.value = pan;
|
||||||
|
|
||||||
const gainNode = ctx.createGain();
|
const gainNode = ctx.createGain();
|
||||||
gainNode.gain.value = masterVolume * volume;
|
gainNode.gain.value = masterVolume * volume;
|
||||||
|
|
||||||
const soundSource = ctx.createBufferSource();
|
const soundSource = ctx.createBufferSource();
|
||||||
soundSource.buffer = buffer;
|
soundSource.buffer = buffer;
|
||||||
soundSource.connect(gainNode).connect(ctx.destination);
|
soundSource.playbackRate.value = playbackRate;
|
||||||
|
soundSource
|
||||||
|
.connect(panNode)
|
||||||
|
.connect(gainNode)
|
||||||
|
.connect(ctx.destination);
|
||||||
|
|
||||||
return soundSource;
|
return soundSource;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,11 @@ function toolsMenuItems(): MenuItem[] {
|
||||||
to: '/clicker',
|
to: '/clicker',
|
||||||
text: '🍪👈',
|
text: '🍪👈',
|
||||||
icon: 'ti ti-cookie',
|
icon: 'ti ti-cookie',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/bubble-game',
|
||||||
|
text: i18n.ts.bubbleGame,
|
||||||
|
icon: 'ti ti-apple',
|
||||||
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
to: '/custom-emojis-manager',
|
to: '/custom-emojis-manager',
|
||||||
|
|