From 0d7f9308cc233cf0688364cb947a376afc656871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:25:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?enhance(frontend):=20=E3=83=90=E3=83=96?= =?UTF-8?q?=E3=83=AB=E3=82=B2=E3=83=BC=E3=83=A0=E3=81=AE=E8=AB=B8=E3=80=85?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=E3=83=BB=E6=94=B9=E8=89=AF2=20(#129?= =?UTF-8?q?48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (fix) ゲームが正常に終了するように * (enhance) 効果音の音量を設定可能に * (add) store * (add) スクショにロゴの透かしを入れる * Update packages/frontend/src/pages/drop-and-fusion.vue Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> * tweak * tweak * tweak * tweak * Update drop-and-fusion.vue * tweak * tweak --------- Co-authored-by: syuilo Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- locales/index.d.ts | 2 + locales/ja-JP.yml | 2 + .../frontend/assets/drop-and-fusion/hold.mp3 | Bin 0 -> 26496 bytes packages/frontend/src/boot/main-boot.ts | 2 +- packages/frontend/src/components/MkNote.vue | 4 +- .../src/components/MkNoteDetailed.vue | 4 +- packages/frontend/src/components/MkRange.vue | 2 + .../components/MkReactionsViewer.reaction.vue | 4 +- .../frontend/src/components/MkTimeline.vue | 2 +- .../src/components/global/MkCustomEmoji.vue | 2 +- .../src/components/global/MkEmoji.vue | 2 +- .../frontend/src/pages/drop-and-fusion.vue | 279 ++++++++++++------ .../src/pages/settings/sounds.sound.vue | 4 +- .../src/scripts/drop-and-fusion-engine.ts | 77 ++++- packages/frontend/src/scripts/sound.ts | 44 ++- packages/frontend/src/store.ts | 7 + packages/frontend/src/ui/_common_/common.vue | 2 +- .../frontend/src/widgets/WidgetJobQueue.vue | 2 +- 18 files changed, 311 insertions(+), 130 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/hold.mp3 diff --git a/locales/index.d.ts b/locales/index.d.ts index 7c73caaac9..96bc9099dd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1193,6 +1193,8 @@ export interface Locale { "addMfmFunction": string; "enableQuickAddMfmFunction": string; "bubbleGame": string; + "sfx": string; + "soundWillBePlayed": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 55ff3201f0..c28fde56cb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1190,6 +1190,8 @@ decorate: "デコる" addMfmFunction: "装飾を追加" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" bubbleGame: "バブルゲーム" +sfx: "効果音" +soundWillBePlayed: "サウンドが再生されます" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/assets/drop-and-fusion/hold.mp3 b/packages/frontend/assets/drop-and-fusion/hold.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef03e60f61f68af7b8f182db7e3148ea8ee8861f GIT binary patch literal 26496 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 hLE!Y!^fL$|VF){6H2n$(5{ literal 0 HcmV?d00001 diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 5011ce9e74..bdb145b39a 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -271,7 +271,7 @@ export async function mainBoot() { main.on('unreadAntenna', () => { updateAccount({ hasUnreadAntenna: true }); - sound.play('antenna'); + sound.playMisskeySfx('antenna'); }); main.on('readAllAnnouncements', () => { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 3ec9c3c46a..9c4354ef5f 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -345,7 +345,7 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); if (props.mock) { return; @@ -365,7 +365,7 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); if (props.mock) { emit('reaction', reaction); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 6f0c0323cc..e941827d74 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -370,7 +370,7 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, @@ -386,7 +386,7 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 04390c6f0c..1aee1aaac3 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'update:modelValue', value: number): void; + (ev: 'dragEnded', value: number): void; }>(); const containerEl = shallowRef(); @@ -143,6 +144,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { // 値が変わってたら通知 if (beforeValue !== finalValue.value) { emit('update:modelValue', finalValue.value); + emit('dragEnded', finalValue.value); } }; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 2e75f444da..5ca09fa822 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -62,7 +62,7 @@ async function toggleReaction() { if (confirm.canceled) return; if (oldReaction !== props.reaction) { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); } if (mock) { @@ -81,7 +81,7 @@ async function toggleReaction() { } }); } else { - sound.play('reaction'); + sound.playMisskeySfx('reaction'); if (mock) { emit('reactionToggled', props.reaction, (props.count + 1)); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 63f779dbde..8a5076ea1d 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -81,7 +81,7 @@ function prepend(note) { emit('note'); if (props.sound) { - sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note'); + sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note'); } } diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index a9643d68ca..dd3fe77251 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -91,7 +91,7 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-plus', action: () => { react(`:${props.name}:`); - sound.play('reaction'); + sound.playMisskeySfx('reaction'); }, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index f6b21343b6..cbdb3881c6 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -55,7 +55,7 @@ function onClick(ev: MouseEvent) { icon: 'ti ti-plus', action: () => { react(props.emoji); - sound.play('reaction'); + sound.playMisskeySfx('reaction'); }, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 0ddee55f5f..b8d3d8bf04 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -24,20 +24,31 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.start }} +
+
+
{{ i18n.ts.soundWillBePlayed }}
+ + + +
+
-
-
+
+
BUBBLE GAME
- {{ gameMode }} -
-
-
- NEXT >>> +
+
+ HOLD + +
+
-
- -
+
-
-
- - - - -
{{ comboPrev }} Chain!
-
- +
+ + + + +
{{ comboPrev }} Chain!
+
+
+ - + -
-
- -
SCORE:
-
MAX CHAIN:
-
- Restart - Share -
+
+
+
+ +
SCORE:
+
MAX CHAIN:
+
+ Restart + Share
@@ -109,15 +118,23 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - +
+ + + + + + +
-
-
-
Credit
-
BGM: @ys@misskey.design
+
+
Credit
+
+
Ai-chan illustration: @poteriri@misskey.io
+
BGM: @ys@misskey.design
+
+
@@ -150,10 +167,7 @@ import { $i } from '@/account.js'; import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; - -const containerEl = shallowRef(); -const canvasEl = shallowRef(); -const dropperX = ref(0); +import MkSwitch from '@/components/MkSwitch.vue'; const NORMAL_BASE_SIZE = 30; const NORAML_MONOS: Mono[] = [{ @@ -384,10 +398,16 @@ const SQUARE_MONOS: Mono[] = [{ const GAME_WIDTH = 450; const GAME_HEIGHT = 600; -let viewScaleX = 1; -let viewScaleY = 1; +let viewScale = 1; +let game: DropAndFusionGame; +let containerElRect: DOMRect | null = null; + +const containerEl = shallowRef(); +const canvasEl = shallowRef(); +const dropperX = ref(0); const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null); const stock = shallowRef<{ id: string; mono: Mono }[]>([]); +const holdingStock = shallowRef<{ id: string; mono: Mono } | null>(null); const score = ref(0); const combo = ref(0); const comboPrev = ref(0); @@ -398,20 +418,19 @@ const gameOver = ref(false); const gameStarted = ref(false); const highScore = ref(null); const showConfig = ref(false); -const bgmVolume = ref(0.1); - -let game: DropAndFusionGame; -let containerElRect: DOMRect | null = null; +const mute = ref(false); +const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); +const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); function onClick(ev: MouseEvent) { if (!containerElRect) return; - const x = (ev.clientX - containerElRect.left) / viewScaleX; + const x = (ev.clientX - containerElRect.left) / viewScale; game.drop(x); } function onTouchend(ev: TouchEvent) { if (!containerElRect) return; - const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScaleX; + const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; game.drop(x); } @@ -431,6 +450,10 @@ function moveDropper(rect: DOMRect, x: number) { dropperX.value = Math.min(rect.width * ((GAME_WIDTH - game.PLAYAREA_MARGIN) / GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / GAME_WIDTH), x)); } +function hold() { + game.hold(); +} + function restart() { game.dispose(); gameOver.value = false; @@ -440,6 +463,7 @@ function restart() { score.value = 0; combo.value = 0; comboPrev.value = 0; + bgmNodes?.soundSource.stop(); gameStarted.value = false; } @@ -463,6 +487,10 @@ function attachGameEvents() { stock.value = JSON.parse(JSON.stringify(value.slice(1))); }); + game.addListener('changeHolding', value => { + holdingStock.value = value; + }); + game.addListener('dropped', () => { dropReady.value = false; window.setTimeout(() => { @@ -476,8 +504,8 @@ function attachGameEvents() { if (!canvasEl.value) return; const rect = canvasEl.value.getBoundingClientRect(); - const domX = rect.left + (x * viewScaleX); - const domY = rect.top + (y * viewScaleY); + 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'); }); @@ -511,7 +539,7 @@ function attachGameEvents() { }); } -let bgmNodes: ReturnType = null; +let bgmNodes: ReturnType | null = null; async function start() { try { @@ -527,6 +555,7 @@ async function start() { width: GAME_WIDTH, height: GAME_HEIGHT, canvas: canvasEl.value!, + sfxVolume: mute.value ? 0 : sfxVolume.value, ...( gameMode.value === 'normal' ? { monoDefinitions: NORAML_MONOS, @@ -546,19 +575,50 @@ async function start() { } const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3'); if (!bgmBuffer) return; - bgmNodes = sound.createSourceNode(bgmBuffer, bgmVolume.value); + bgmNodes = sound.createSourceNode(bgmBuffer, { + volume: mute.value ? 0 : bgmVolume.value, + }); if (!bgmNodes) return; bgmNodes.soundSource.loop = true; bgmNodes.soundSource.start(); }); } -watch(bgmVolume, (value) => { +watch(bgmVolume, (newValue, oldValue) => { if (bgmNodes) { - bgmNodes.gainNode.gain.value = value; + bgmNodes.gainNode.gain.value = mute.value ? 0 : newValue; } }); +watch(sfxVolume, (newValue, oldValue) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (game) { + game.setSfxVolume(mute.value ? 0 : newValue); + } +}); + +function updateSettings< + K extends keyof typeof defaultStore.state.dropAndFusion, + V extends typeof defaultStore.state.dropAndFusion[K], +>(key: K, value: V) { + const changes: { [P in K]?: V } = {}; + changes[key] = value; + defaultStore.set('dropAndFusion', { + ...defaultStore.state.dropAndFusion, + ...changes, + }); +} + +function loadImage(url: string) { + return new Promise(res => { + const img = new Image(); + img.src = url; + img.addEventListener('load', () => { + res(img); + }); + }); +} + function getGameImageDriveFile() { return new Promise(res => { const dcanvas = document.createElement('canvas'); @@ -566,13 +626,18 @@ function getGameImageDriveFile() { dcanvas.height = GAME_HEIGHT; const ctx = dcanvas.getContext('2d'); if (!ctx || !canvasEl.value) return res(null); - const dimage = new Image(); - dimage.src = '/client-assets/drop-and-fusion/frame-light.svg'; - dimage.addEventListener('load', () => { + Promise.all([ + loadImage('/client-assets/drop-and-fusion/frame-light.svg'), + loadImage('/client-assets/drop-and-fusion/logo.png'), + ]).then((images) => { + const [frame, logo] = images; ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); - ctx.drawImage(dimage, 0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.drawImage(frame, 0, 0, GAME_WIDTH, GAME_HEIGHT); ctx.drawImage(canvasEl.value!, 0, 0, GAME_WIDTH, GAME_HEIGHT); + ctx.globalAlpha = 0.7; + ctx.drawImage(logo, GAME_WIDTH * 0.55, 6, GAME_WIDTH * 0.45, GAME_WIDTH * 0.45 * (logo.height / logo.width)); + ctx.globalAlpha = 1; dcanvas.toBlob(blob => { if (!blob) return res(null); @@ -610,22 +675,22 @@ async function share() { os.post({ initialText: `#BubbleGame MODE: ${gameMode.value} -SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})})`, +SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})`, initialFiles: [file], + instant: true, }); } useInterval(() => { if (!canvasEl.value) return; const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width; - const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height; - viewScaleX = actualCanvasWidth / GAME_WIDTH; - viewScaleY = actualCanvasHeight / GAME_HEIGHT; + if (actualCanvasWidth === 0) return; + viewScale = actualCanvasWidth / GAME_WIDTH; containerElRect = containerEl.value?.getBoundingClientRect() ?? null; }, 1000, { immediate: false, afterMounted: true }); onDeactivated(() => { - game.dispose(); + restart(); }); definePageMetadata({ @@ -697,16 +762,52 @@ definePageMetadata({ box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c; border-radius: 10px; } + +.frameH { + display: flex; + gap: 6px; +} + .frameInner { - padding: 4px 8px; + padding: 8px; + margin-top: 8px; background: #F1E8DC; box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410; border-radius: 6px; color: #693410; + + &:first-child { + margin-top: 0; + } } -.main { +.frameDivider { + height: 0; + border: none; + border-top: 1px solid #693410; + border-bottom: 1px solid #ce8a5c; +} + +.header { position: relative; + z-index: 10; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + gap: 8px; + + > .headerTitle { + text-align: center; + } + + @media (min-width: 500px) { + grid-template-columns: 1fr auto; + grid-template-rows: auto; + + > .headerTitle { + text-align: start; + } + } } .mainFrameImg { @@ -724,15 +825,15 @@ definePageMetadata({ position: relative; display: block; z-index: 1; - margin-top: -50px; width: 100% !important; height: auto !important; pointer-events: none; user-select: none; } -.container { +.gameContainer { position: relative; + margin-top: -20px; } .stock { @@ -755,45 +856,51 @@ definePageMetadata({ user-select: none; } -.currentMono { +.dropperContainer { position: absolute; - margin-top: 80px; + top: 0; + height: 100%; z-index: 2; - filter: drop-shadow(0 6px 16px #0007); pointer-events: none; user-select: none; + will-change: left; +} + +.currentMono { + position: absolute; + display: block; + bottom: 88%; + z-index: 2; + filter: drop-shadow(0 6px 16px #0007); } .dropper { - position: absolute; + position: relative; top: 0; width: 70px; margin-top: -10px; margin-left: -30px; z-index: 2; filter: drop-shadow(0 6px 16px #0007); - pointer-events: none; - user-select: none; } .currentMonoArrow { position: absolute; - margin-top: 100px; + width: 20px; + bottom: 80%; + left: -10px; z-index: 3; animation: currentMonoArrow 2s ease infinite; - pointer-events: none; - user-select: none; } .dropGuide { position: absolute; - top: 120px; z-index: 3; + bottom: 0; width: 3px; - height: calc(100% - 120px); + margin-left: -2px; + height: 85%; background: #f002; - pointer-events: none; - user-select: none; } .gameOverLabel { diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index 57bafce0ac..798980b3d1 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -33,7 +33,7 @@ import MkRange from '@/components/MkRange.vue'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; +import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js'; import { selectFile } from '@/scripts/select-file.js'; const props = defineProps<{ @@ -119,7 +119,7 @@ function listen() { return; } - playFile(type.value === '_driveFile_' ? { + playMisskeySfxFile(type.value === '_driveFile_' ? { type: '_driveFile_', fileId: fileId.value as string, fileUrl: fileUrl.value as string, diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index b6e735ddf2..03c52e00fe 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -20,17 +20,17 @@ export type Mono = { spriteScale: number; }; -const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる - export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; changeStock: (newStock: { id: string; mono: Mono }[]) => void; + changeHolding: (newHolding: { id: string; mono: Mono } | null) => void; dropped: () => void; fusioned: (x: number, y: number, scoreDelta: number) => void; monoAdded: (mono: Mono) => void; gameOver: () => void; }> { + private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる private COMBO_INTERVAL = 1000; public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; @@ -48,6 +48,8 @@ export class DropAndFusionGame extends EventEmitter<{ private monoTextures: Record = {}; private monoTextureUrls: Record = {}; + private sfxVolume = 1; + /** * フィールドに出ていて、かつ合体の対象となるアイテム */ @@ -58,6 +60,7 @@ export class DropAndFusionGame extends EventEmitter<{ private latestDroppedAt = 0; private latestFusionedAt = 0; private stock: { id: string; mono: Mono }[] = []; + private holding: { id: string; mono: Mono } | null = null; private _combo = 0; private get combo() { @@ -84,6 +87,7 @@ export class DropAndFusionGame extends EventEmitter<{ width: number; height: number; monoDefinitions: Mono[]; + sfxVolume?: number; }) { super(); @@ -91,10 +95,14 @@ export class DropAndFusionGame extends EventEmitter<{ this.gameHeight = opts.height; this.monoDefinitions = opts.monoDefinitions; + if (opts.sfxVolume) { + this.sfxVolume = opts.sfxVolume; + } + this.engine = Matter.Engine.create({ - constraintIterations: 2 * PHYSICS_QUALITY_FACTOR, - positionIterations: 6 * PHYSICS_QUALITY_FACTOR, - velocityIterations: 4 * PHYSICS_QUALITY_FACTOR, + constraintIterations: 2 * this.PHYSICS_QUALITY_FACTOR, + positionIterations: 6 * this.PHYSICS_QUALITY_FACTOR, + velocityIterations: 4 * this.PHYSICS_QUALITY_FACTOR, gravity: { x: 0, y: 1, @@ -183,6 +191,7 @@ export class DropAndFusionGame extends EventEmitter<{ }; if (mono.shape === 'circle') { return Matter.Bodies.circle(x, y, mono.size / 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); } else { @@ -224,7 +233,11 @@ export class DropAndFusionGame extends EventEmitter<{ // TODO: 効果音再生はコンポーネント側の責務なので移動する const pan = ((newX / this.gameWidth) - 0.5) * 2; - sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', 1, pan, nextMono.sfxPitch); + sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', { + volume: this.sfxVolume, + pan, + playbackRate: nextMono.sfxPitch, + }); this.emit('monoAdded', nextMono); this.emit('fusioned', newX, newY, additionalScore); @@ -237,7 +250,7 @@ export class DropAndFusionGame extends EventEmitter<{ //} //sound.playUrl({ // type: 'syuilo/bubble2', - // volume: 1, + // volume: this.sfxVolume, //}); } } @@ -323,10 +336,14 @@ export class DropAndFusionGame extends EventEmitter<{ const energy = pairs.collision.depth; if (energy > minCollisionEnergyForSound) { // TODO: 効果音再生はコンポーネント側の責務なので移動する - const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4; + const vol = ((Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4) * this.sfxVolume; const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2; const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); - sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', vol, pan, pitch); + sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', { + volume: vol, + pan, + playbackRate: pitch, + }); } } } @@ -344,6 +361,10 @@ export class DropAndFusionGame extends EventEmitter<{ this.loaded = true; } + public setSfxVolume(volume: number) { + this.sfxVolume = volume; + } + public getTextureImageUrl(mono: Mono) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.monoTextureUrls[mono.img]) { @@ -369,25 +390,53 @@ export class DropAndFusionGame extends EventEmitter<{ if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) { return; } - const st = this.stock.shift()!; + const head = this.stock.shift()!; this.stock.push({ id: Math.random().toString(), mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeStock', this.stock); - const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x)); - const body = this.createBody(st.mono, x, 50 + st.mono.size / 2); + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x)); + const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; this.latestDroppedAt = Date.now(); this.emit('dropped'); - this.emit('monoAdded', st.mono); + this.emit('monoAdded', head.mono); // TODO: 効果音再生はコンポーネント側の責務なので移動する const pan = ((x / this.gameWidth) - 0.5) * 2; - sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', 1, pan); + sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', { + volume: this.sfxVolume, + pan, + }); + } + + public hold() { + if (this.isGameOver) return; + + if (this.holding) { + const head = this.stock.shift()!; + this.stock.unshift(this.holding); + this.holding = head; + this.emit('changeHolding', this.holding); + this.emit('changeStock', this.stock); + } else { + const head = this.stock.shift()!; + this.holding = head; + this.stock.push({ + id: Math.random().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + }); + this.emit('changeHolding', this.holding); + this.emit('changeStock', this.stock); + } + + sound.playUrl('/client-assets/drop-and-fusion/hold.mp3', { + volume: this.sfxVolume, + }); } public dispose() { diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 690c342c85..142ddf87c9 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -126,13 +126,13 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) * 既定のスプライトを再生する * @param type スプライトの種類を指定 */ -export function play(operationType: OperationType) { +export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; if (_DEV_) console.log('play', operationType, sound); if (sound.type == null || !canPlay) return; canPlay = false; - playFile(sound).finally(() => { + playMisskeySfxFile(sound).finally(() => { // ごく短時間に音が重複しないように setTimeout(() => { canPlay = true; @@ -144,41 +144,53 @@ export function play(operationType: OperationType) { * サウンド設定形式で指定された音声を再生する * @param soundStore サウンド設定 */ -export async function playFile(soundStore: SoundStore) { +export async function playMisskeySfxFile(soundStore: SoundStore) { if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { return; } + const masterVolume = defaultStore.state.sound_masterVolume; + if (isMute() || masterVolume === 0 || soundStore.volume === 0) { + return; + } const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, soundStore.volume)?.soundSource.start(); + const volume = soundStore.volume * masterVolume; + createSourceNode(buffer, { volume }).soundSource.start(); } -export async function playUrl(url: string, volume = 1, pan = 0, playbackRate = 1) { +export async function playUrl(url: string, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}) { + if (opts.volume === 0) { + return; + } const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, volume, pan, playbackRate)?.soundSource.start(); + createSourceNode(buffer, opts).soundSource.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number, pan = 0, playbackRate = 1): { +export function createSourceNode(buffer: AudioBuffer, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}): { soundSource: AudioBufferSourceNode; panNode: StereoPannerNode; gainNode: GainNode; -} | null { - const masterVolume = defaultStore.state.sound_masterVolume; - if (isMute() || masterVolume === 0 || volume === 0) { - return null; - } - +} { const panNode = ctx.createStereoPanner(); - panNode.pan.value = pan; + panNode.pan.value = opts.pan ?? 0; const gainNode = ctx.createGain(); - gainNode.gain.value = masterVolume * volume; + + gainNode.gain.value = opts.volume ?? 1; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.playbackRate.value = playbackRate; + soundSource.playbackRate.value = opts.playbackRate ?? 1; soundSource .connect(panNode) .connect(gainNode) diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 46634af96b..e3a85377d8 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -420,6 +420,13 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, + dropAndFusion: { + where: 'device', + default: { + bgmVolume: 0.25, + sfxVolume: 1, + }, + }, sound_masterVolume: { where: 'device', diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 78af49cdc2..0ec036c5cb 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -83,7 +83,7 @@ function onNotification(notification: Misskey.entities.Notification, isClient = }, 6000); } - sound.play('notification'); + sound.playMisskeySfx('notification'); } if ($i) { diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 89ad3bf323..877406fe95 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -123,7 +123,7 @@ const onStats = (stats) => { current[domain].delayed = stats[domain].delayed; if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) { - const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1)?.soundSource; + const soundNode = sound.createSourceNode(jammedAudioBuffer.value, {}).soundSource; if (soundNode) { jammedSoundNodePlaying.value = true; soundNode.onended = () => jammedSoundNodePlaying.value = false; From 14aedc17ae4e3ca3db9e523f2663824e874e0569 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 9 Jan 2024 16:06:22 +0900 Subject: [PATCH 2/2] update sound --- .../frontend/assets/drop-and-fusion/click.mp3 | Bin 0 -> 26496 bytes .../frontend/assets/drop-and-fusion/hold.mp3 | Bin 26496 -> 21941 bytes .../src/scripts/drop-and-fusion-engine.ts | 7 +++---- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/click.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/click.mp3 b/packages/frontend/assets/drop-and-fusion/click.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef03e60f61f68af7b8f182db7e3148ea8ee8861f GIT binary patch literal 26496 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 hLE!Y!^fL$|VF){6H2n$(5{ literal 0 HcmV?d00001 diff --git a/packages/frontend/assets/drop-and-fusion/hold.mp3 b/packages/frontend/assets/drop-and-fusion/hold.mp3 index ef03e60f61f68af7b8f182db7e3148ea8ee8861f..f064c976d3b91b18fafe5efc68ca633a9cb334f1 100644 GIT binary patch delta 4024 zcmZoT&$x9pBg_Ba8*(NtRH*mNOUq|qVBlw9U{GRU2;IuS&d$%zFD51?Cnu+>s;aB2 zYhq$zV`Jm&?CkCB9TXH46&013n3$QFSx`_=QBl#*(B9tO-`_uN+O&D|<}F>ibnV); zTefW3vuDrY!-r3wK7HxZrCYadJ$(4^<;#~JK79D`Li3{RZ9Gnk#hCTfy{+(e*gPk z_i=K-{rivOCddWrKYsZ8_phK?r%%k>Q~m#?SGK|ZZ&Sm*g&4;T+`{isZKFynQ zeUHWOhV|+F_5JTYscL*|j5xVMkgfUsK?a8mg~z{|WgQ!sQXMoJRHYRdeU~H`FuBcn z{=9zW0Y(M~0n6$9f8^PJIyeZFHzoW!_+ApP)wtC+G4^pVF*Gmu zQNPeZhtXq7tA{M}#uH0|v;`-#aY!++F-SbT*uyJp(>SqnMugx}euW0DZE5T}6X!NJ z^B8a(J2cT`hpSl9EeXA7_lrv`dCZO+T4z~Y^B}zc;J)e3;yy2u=P!QdT4n37z9>ZD zVu#nN4-YM}-ptzKlJcs4imA_9mvx%kCx`tHh_nnLPs78uZBNhJn4c`yH?{AhCUTk&v zv(O=BQKn!v#{&UoCIbI|Z^{wpS{55qe|CE51E!$V$iHv4Fo>!4TD&v3(y2Ff*~w*` zSxpYE=8i|YkA6BR$(3p3wJ=8bK~L9%b`DMj#ivIO@;c_pC?`l9lr-U}>J-Y*m0gt@ zwAJOK&NpUhuHAx49_vpZ3(MZHIcxo+qxT*;w@StAPi^Hh<_>wYVeyVbjoa4-F1Tg3 z)vW&W`tN@?uYDeNuXgkIS#kH}w%uFHT#y)-e&N|Pk#%N!ZcMK{w$J=??7Jv`HlJHN z?gSVGbn;DM$$Q0lfq{XQfq^-2!!$83cg;C=Gn*Tx>t>(Ge0?eU_|D5ME2eUOl=)ce zdbsuKl*ztpeZS6B{2~w}a%-#I*0r-&?0Nrx#pC~1j@j35=~c>Dy;wf@#CPktFB|+m zge*A0RLD_~>5}Srvb*6h<6-I3{PhJk=O*0tQc$>}!4TEc$}SbX>GGXx5nY~qOV=OL zHZnW!-#ul^wo8eVSstn!snX>$&DfB#OHNzM(l(!uk84qsyT<{egKl%S%;{3sxf-mR zlC&)<;rX=HtXC4G>Q5Z<^E&ao$e<*6VwO&)$3}HGPMb4NZ8rINZ1LjKQJu7;+vSz1 z(8R0~KhNh654tTnX}3>i%E^;eWozFQ^S7>LIQX55x4C7ZW$v`C&(zdXo23#DJnUI8 zpGE6uOW@Cyof!`tZ6rKbgtCf1 zlewUDXUM=V?d`!^?Z8&_va){rnl%hjnzpVx&oD49UbynR+y;i06_*qz^Ji}i67b|Y zDZ;yQan(f!>r0QP-w|QbIxErKFqiwQmqgR?+}pdKz2A`|`}Z+-x zG^w5OPn)K4;f^_R;k=g*7w$}b*>PlZxT;`xeVg>TrJbj$D-VC%RaISkXi3s!o_fo3 z|NsA!D4zLzZgu$|gY&V~0geua4-^=JHa8y;Q{_Ey<^7?PB`qo@EsKSOw9U(6MSFW& zmG>%$s4+?VbET9=MpeGKaB+^a(wDd+lGfr=2arMbP|%p`vM+Q4HW?x!El|Fie||2I?W&+pm0pHcq8kByCh}rGCY}5T; z1scMJ867QVi4GkWA&gwJ1X^Uc4f*DLGQMmo>|41*N=>;!@>p9-_+wti9_LUso87A& zSKXSbu|`5uXv*WbEr(}s-((qhDD(Ti)l05TSetAS&@(x(+Vt^Zx~Y;O56uJ(kNA{HE03)}8@MNIv{CEtDf(7u}Kr{{NVUwZTD)W}@5 zpt-KgKK}ok&uy$5duRG#lP|&3KOft(Yp>mQ={J)4SDrXW9u{A9#(kOQ4M+RXzgf(S z7BDa{u483jlumnlJL|x$R-HTBPP89qb1aiR%wDkQSZ=W-6IcC%Cg^S&|JnRkSJr&5m)d$S z)AxSe2hFvo|KI<;egD3V-?O46_Pz9<{aWPO>h&H{*YfM%@4m$My56ULRq&xup+}D1{D!DW`{|Cniv8l zTuek~*h$Qg3zt9LH_L$O;iRJQ z1wu13rG$btIMPa7Jx(M|E}O=_mg7@%ii&L5Q9jq2sd{;mi}EzSh8%Lg9`$o&+?R%j zxfS)(T~AxOPB005;-?q*Jlbp0=781n_f=azJ9G1=+_Ru<_4oE|E1nws;@-TSTSK2E z^`=go6S>jqijHdAY@@?>XLC7l2s8*p%ycXbe&~8%V)x`Yx%9$^6W%{)kO)k<^;^Us z^w*}_Kj-|pHf@^N&9rlYGu5|c3nAB&uWQj0dG z9dKC@Q)1$}QM27(<)ZUSLe;C4wl**@&Ra0E!$P3vM0ca-VMoV9EX$9{IhhD#%zpNy zAh|$o^0myfX&Nc%ii{0@zh-T7Tdvl7eEzdd*Fot{riG2I_>^{zY*FT^$Tx}2auK4( z<#{(;Q>!)zjXbIR|X z+IsYetBdKEq~+#^VnS?n4{g2k=GHu0{=J8auiGW}uNGRZv~YG{oObNIr4Jjw&H9xp zye``GsOjE$Jzg2-&J_jvFs|4oxpd2qCwDI{>eR~0vzr~y&C;Tkw(_?xrU0XP7USqka z|HJrg4mrvf#p~+|6dd~U4H)@Mnd)b(J#o;Lv+KZuHn|fy2cON7$vl*xwRpjL!$l7= zBv?2&xGv3*?%1I0utb%&J@JUuM+sGifCqh>jv34o?soEaRpnZsHAm~Zw4YRi>vap3 zYi{;teg+dbG}YJ`<%>-`zIYrK^0;HEK3ysFq)60^sRvKmY?M5FVph_JX&X=fDvDOC zZ*YngGubhD>xnaaCLZpL&`{!jQh08Ysk-WYb$PATm%rVAy7IBL(6zN+{}u!$l3(P>Prl6w{~ZtD52ak-0Gt354}NxykQ!;aKnQx5B&i_dy?R9EWR`Ty_B)_7$| zTIigac7JBnk@=b@PJ4%6{Iqdjz_BoseeIe7%L>-iPtFQDJ+pHDt)-{Jrt;(^UD+#h z%y`P0r(vh3|9@5+X>3_`Ws5;zmRZT92H_Tkh^M?}4JQruvd1V(OwUZu zEKBn>|J&CnN8B`s+GG7acK84Pv)WkXY;TD&nuOjLeZ@3MUPrjRWyi(^D|#~$`uN;5 zj6zKq_-fW15OE7i4DDtWzsq-oVbMbksCt|$Hvqy+0^v(ys$to*U`?7 zP>Wq`H`KU#nGYE`r}uJs^i5T;d#TOa`}<_E@|{hKE`Hk9R+@fjmvqpT`_7D_4Xu`u zr*>J^*1cNu)II;Vr0o1JWk#DDCTV}(`AF}jn6mX|N1=R1=KsGpO=cFdn|wgfc(bDr YBNOA`BNr1BV?$#DW3V_1 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 hLE!Y!^fL$|VF){6H2n$(5{ diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 03c52e00fe..f71f3a668e 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -387,9 +387,8 @@ export class DropAndFusionGame extends EventEmitter<{ public drop(_x: number) { if (this.isGameOver) return; - if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) { - return; - } + if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return; + const head = this.stock.shift()!; this.stock.push({ id: Math.random().toString(), @@ -435,7 +434,7 @@ export class DropAndFusionGame extends EventEmitter<{ } sound.playUrl('/client-assets/drop-and-fusion/hold.mp3', { - volume: this.sfxVolume, + volume: 0.5 * this.sfxVolume, }); }