From 0f9e3bccefa11d7a00a06bb31918b6ddb8001089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:51:31 +0900 Subject: [PATCH 01/15] =?UTF-8?q?refactor(CI):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=AF=84=E5=9B=B2=E3=81=A8=E9=96=A2=E4=BF=82=E3=81=AA=E3=81=84?= =?UTF-8?q?Actions=E3=81=8C=E8=B5=B0=E3=82=8B=E3=81=AE=E3=82=92=E6=8A=91?= =?UTF-8?q?=E6=AD=A2=E3=81=99=E3=82=8B=20(#12918)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor?: 修正範囲と関係ないActionsが走るのを抑止する * fix * バックエンドの対象にmisskey-jsを追加&フロントエンドの対象にmisskey-jsとbackendを追加 --- .github/workflows/api-misskey-js.yml | 8 +++++++- .github/workflows/lint.yml | 12 ++++++++++++ .github/workflows/test-backend.yml | 8 ++++++++ .github/workflows/test-frontend.yml | 13 +++++++++++++ .github/workflows/test-misskey-js.yml | 4 ++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index 5cffbd81bc..e52cbc33e4 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -1,6 +1,12 @@ name: API report (misskey.js) -on: [push, pull_request] +on: + push: + paths: + - packages/misskey-js/** + pull_request: + paths: + - packages/misskey-js/** jobs: report: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f3074ab0a4..23cea7d565 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,19 @@ on: branches: - master - develop + paths: + - packages/backend/** + - packages/frontend/** + - packages/sw/** + - packages/misskey-js/** + - packages/shared/.eslintrc.js pull_request: + paths: + - packages/backend/** + - packages/frontend/** + - packages/sw/** + - packages/misskey-js/** + - packages/shared/.eslintrc.js jobs: pnpm_install: diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 3b49173f45..a6c12e2824 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -5,7 +5,15 @@ on: branches: - master - develop + paths: + - packages/backend/** + # for permissions + - packages/misskey-js/** pull_request: + paths: + - packages/backend/** + # for permissions + - packages/misskey-js/** jobs: unit: diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index 83740bf156..3fb880fac2 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -5,7 +5,20 @@ on: branches: - master - develop + paths: + - packages/frontend/** + # for permissions + - packages/misskey-js/** + # for e2e + - packages/backend/** + pull_request: + paths: + - packages/frontend/** + # for permissions + - packages/misskey-js/** + # for e2e + - packages/backend/** jobs: vitest: diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index 055152f321..10c7ccf4d3 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -6,8 +6,12 @@ name: Test (misskey.js) on: push: branches: [ develop ] + paths: + - packages/misskey-js/** pull_request: branches: [ develop ] + paths: + - packages/misskey-js/** jobs: test: From 34088ecd27c081b00f425ec2546ef0670dbb10fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Tue, 9 Jan 2024 08:34:23 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat(ci):=20api.json=E3=81=AE=E3=83=90?= =?UTF-8?q?=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AFCI=E3=82=92=E8=BF=BD=E5=8A=A0=20(#12?= =?UTF-8?q?950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ci): api.jsonのバリデーションチェックCIを追加 * fix name --- .github/workflows/validate-api-json.yml | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/validate-api-json.yml diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml new file mode 100644 index 0000000000..bc5ba20cb9 --- /dev/null +++ b/.github/workflows/validate-api-json.yml @@ -0,0 +1,47 @@ +name: Test (backend) + +on: + push: + branches: + - master + - develop + paths: + - packages/backend/** + pull_request: + paths: + - packages/backend/** + +jobs: + validate-api-json: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.10.0] + + steps: + - uses: actions/checkout@v4.1.1 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4.0.1 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install swagger-cli + run: npm i -g swagger-cli + - run: corepack enable + - run: pnpm i --frozen-lockfile + - name: Check pnpm-lock.yaml + run: git diff --exit-code pnpm-lock.yaml + - name: Copy Configure + run: cp .config/example.yml .config/default.yml + - name: Build and generate + run: pnpm build && pnpm --filter backend generate-api-json + - name: Validation + run: swagger-cli validate ./packages/backend/built/api.json 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 03/15] =?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 zcmeH~c|2760>IA6*=bzLHZfy~N{tB_#$?|kVak>zq?NIzxeB*IsBDiYwU{qa8U{q;WYocB3DpEIBFJLhwJ&+q#^XN;+l77xH3 z*wcm<3-baX<()vFv1 zCo?lAr?9ZFq@=8@qN3v2vxbJ2mX^-WzP^EhfuW(Xv8k!4`T6B#5aj-?J?2z~v6edb zxrM6QKqQ_A0PR{u%zQfl)NXK?yJS>v>_Eo9+J*Rh zPiOHbs{4az7V7DE&W(D-cB5|d*Es zOA%`;k{#4$r;xp!as9!p09606jF3sm__Zr;9qap5%&a-BQW7~{cNfl8X9o>Wi>@bP z&t18-R6x+TdN7qFXU&ONUpiA_uiwLm@wk6YCg9P4f z0oZ+;cz9Wghr9tmVt@q@1B`$JkvG14?~G;g@Q4fXaYv{^B-k913t$g{9U(WSU#*9) zUfT0no?l)Xw7J>L#}|Q}`JQ1QdB)Dj$Xbei<+01-lgXtOv9W@_Nzav>$xZ~%hM4G{ zis#BQC`(%}y+aoHvU;b}7Tey@pb z6`wvIwbkHgqMq_F(}6-0+xfUH^O0wf_L(wW1!u|5ezFSxq}TpeH%m2-CpxrU$fHNc zP5n|A@ggJl)9S^V6YsMYJgX{7Nlyb}Ogg$BoNK-QE=h>ezL@p#=mR&A<%19Bn}QuR zorSi$iA=;NZ)I7l6{$o^&hT4_eNGBgF*P6YC>Kpy8{YI0@!IR#Ose37<(Q(D8FER7ahkTe;&Tv#xov-|Xs)DN4KvG@lbA!kH&f?YAa za;KP&!Wh22z9;G)69E~KD<};ge=Xo4r!LrmU3uw*Z%gjvPh&r2?C$Jdqqp>Z9N1yb z-x{1fK2b4Dj}PrpwRY`2RvpGEdw25N&3d)u?iH%o&r;lH)s;g&%nFdc;$CT{^%x7Rxvw_<YthJ9;U-pid#p}q2i`gX~rSZmjjeLh8Voh}@T zv746l;U|v5k1<=s2&BJMC&}xmyG!o1z1vsLSMksW&)jTly+?5WkHS0ZCI!mln(XL9 z=N=6$@Ah-DDQR44k}l%iyN931&Hz(9n;KZWtkd0I-*sT4AL*p|;sa7q!xn(KVN8&q ziWvZ){vrwh1n%cWGz;Lp>pz`^FCqD0cFt)rtO&ELJW^zkyvJ-IZw zpGYJMX_lwTxQiOoWtFRVyc!gZ4~r`&)^cp+rM4tu8+ktE7!d`cYL{5{io)zz1?p_H zCi?VS0d_(oCN3_fwv^^bi|kWXRRfo1t*mSWz@+@QX{N{Rtii&ka=x=?S3ZFARu7C0 z2o{;QTSU!Dv6!bmDq;7C^Pu_B6ZKMa7eWL9Jw8;^AZe?^6t#T}Rq!&$Ta$*DMkPi8 z?yMKffl>0x5?%s)`_R!WJgG>A9~)U*HGtz|)dTM2PJoYP-~j{)i($&Z^!8t}GEuPr z%e05jU~^RN%R!GkM@g>|zJ3E#<<^hOqWIjx>sVq`MkB%hl%7Naz^8?0VHHsZz%dp{ z5-WkKjRdQFy%uiwOS>!0N%#`x

Jg%9Pc~$r|Dm5Cq4ws;~@gd>{x$5r9_-`g_8J zo{M@5oz*jJd^2ha^53QnO2gMAQ|(l=3~t zQBY2s&DNcPaoTA-t3kz)<-ed67=XGJILt2T%JQ*rj}OX^yB?xsYl*TE>8aEa?I+qO z+Vp;RoX)T9cL~#QYQIud>3h>$>RW2X=gZ3+*K6Ur0j`2Ay~744vtm$wugX^yl11-0 zvTE-brF{i}v1?oD(ro9Pxmjk+i||jQDS{0i8WY~FR&M$uw7_mF%KXD--xyr`=jc(F zA4B6kEk=0q9Trqq@2B~F_ozAfCGh(Vic~JU>&1|RLTK8KFpBK7Q_HC-*Xi(qU7zBm zr#BYn?_<4v|C;No%`0u^|C|BAl~q5PtWzZJzb<@e*2cXGU~lHLk1m%HbEyRN!^H(- z?=F6E@bp|x_&_1D=W>=!pY|6p7r`5o{nv|Jp@e{?jE-pS|F!;L8 zlI{)76H@T+_XQz{;T3s z)$YTrR9!HIui*H5!QOZN`#@Tm*Ht?S$Jurr7neqcFbFQs(IhWY|B9=2Lj?zAa#t(s zto%z{^|9X~vVM%yh>VQnPW2hJlr+1V_)6S$?%i#d?PI~6TWOR#lOEG_(>#};*o|<3H=%8~d&TEB86B z@k0vsqF{=5o@2OFX5LkgYoBCANshJ*?H@Q~nkqmQL)T(;NOxo|Dd*p|>~kh7S@xck zNFF`;D7&#@;%Y>?=uReCMZ{XT*(|m{ouZL{M#u2-jSC4ryWDpeiOS@86m1(!*)lp? zwd?t3dPb}Fe8#T0rAL`c=7vjMR%SO1Q`_DgJ1@TFPWZurA0GFb4B6y#H`I338^;_4 zPtJJHsD?^n=sd>MVoPA(#(MlYM#%*#-j-X5?h0n^3Q-JxOmpDDtL!mCrQn!V?Y(HV znDhVm@?rO19UJ`sTV?>jXX6F{0734JfZF){;{M8gtIYxc-hYeoZ+HGz(UFrwUk$xZ`0mcYOKQNXD0d%Av2uOf20@4qR!`;2LcjcjDYk5V|frjNBV() x1Q;VA{lHir1kjOwARqz82uMFLmIncJq#p=KfH4Bn4~*qO03GQE0uo@1@HgbA>$(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 04/15] 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 zcmeH~c|2760>IA6*=bzLHZfy~N{tB_#$?|kVak>zq?NIzxeB*IsBDiYwU{qa8U{q;WYocB3DpEIBFJLhwJ&+q#^XN;+l77xH3 z*wcm<3-baX<()vFv1 zCo?lAr?9ZFq@=8@qN3v2vxbJ2mX^-WzP^EhfuW(Xv8k!4`T6B#5aj-?J?2z~v6edb zxrM6QKqQ_A0PR{u%zQfl)NXK?yJS>v>_Eo9+J*Rh zPiOHbs{4az7V7DE&W(D-cB5|d*Es zOA%`;k{#4$r;xp!as9!p09606jF3sm__Zr;9qap5%&a-BQW7~{cNfl8X9o>Wi>@bP z&t18-R6x+TdN7qFXU&ONUpiA_uiwLm@wk6YCg9P4f z0oZ+;cz9Wghr9tmVt@q@1B`$JkvG14?~G;g@Q4fXaYv{^B-k913t$g{9U(WSU#*9) zUfT0no?l)Xw7J>L#}|Q}`JQ1QdB)Dj$Xbei<+01-lgXtOv9W@_Nzav>$xZ~%hM4G{ zis#BQC`(%}y+aoHvU;b}7Tey@pb z6`wvIwbkHgqMq_F(}6-0+xfUH^O0wf_L(wW1!u|5ezFSxq}TpeH%m2-CpxrU$fHNc zP5n|A@ggJl)9S^V6YsMYJgX{7Nlyb}Ogg$BoNK-QE=h>ezL@p#=mR&A<%19Bn}QuR zorSi$iA=;NZ)I7l6{$o^&hT4_eNGBgF*P6YC>Kpy8{YI0@!IR#Ose37<(Q(D8FER7ahkTe;&Tv#xov-|Xs)DN4KvG@lbA!kH&f?YAa za;KP&!Wh22z9;G)69E~KD<};ge=Xo4r!LrmU3uw*Z%gjvPh&r2?C$Jdqqp>Z9N1yb z-x{1fK2b4Dj}PrpwRY`2RvpGEdw25N&3d)u?iH%o&r;lH)s;g&%nFdc;$CT{^%x7Rxvw_<YthJ9;U-pid#p}q2i`gX~rSZmjjeLh8Voh}@T zv746l;U|v5k1<=s2&BJMC&}xmyG!o1z1vsLSMksW&)jTly+?5WkHS0ZCI!mln(XL9 z=N=6$@Ah-DDQR44k}l%iyN931&Hz(9n;KZWtkd0I-*sT4AL*p|;sa7q!xn(KVN8&q ziWvZ){vrwh1n%cWGz;Lp>pz`^FCqD0cFt)rtO&ELJW^zkyvJ-IZw zpGYJMX_lwTxQiOoWtFRVyc!gZ4~r`&)^cp+rM4tu8+ktE7!d`cYL{5{io)zz1?p_H zCi?VS0d_(oCN3_fwv^^bi|kWXRRfo1t*mSWz@+@QX{N{Rtii&ka=x=?S3ZFARu7C0 z2o{;QTSU!Dv6!bmDq;7C^Pu_B6ZKMa7eWL9Jw8;^AZe?^6t#T}Rq!&$Ta$*DMkPi8 z?yMKffl>0x5?%s)`_R!WJgG>A9~)U*HGtz|)dTM2PJoYP-~j{)i($&Z^!8t}GEuPr z%e05jU~^RN%R!GkM@g>|zJ3E#<<^hOqWIjx>sVq`MkB%hl%7Naz^8?0VHHsZz%dp{ z5-WkKjRdQFy%uiwOS>!0N%#`x

Jg%9Pc~$r|Dm5Cq4ws;~@gd>{x$5r9_-`g_8J zo{M@5oz*jJd^2ha^53QnO2gMAQ|(l=3~t zQBY2s&DNcPaoTA-t3kz)<-ed67=XGJILt2T%JQ*rj}OX^yB?xsYl*TE>8aEa?I+qO z+Vp;RoX)T9cL~#QYQIud>3h>$>RW2X=gZ3+*K6Ur0j`2Ay~744vtm$wugX^yl11-0 zvTE-brF{i}v1?oD(ro9Pxmjk+i||jQDS{0i8WY~FR&M$uw7_mF%KXD--xyr`=jc(F zA4B6kEk=0q9Trqq@2B~F_ozAfCGh(Vic~JU>&1|RLTK8KFpBK7Q_HC-*Xi(qU7zBm zr#BYn?_<4v|C;No%`0u^|C|BAl~q5PtWzZJzb<@e*2cXGU~lHLk1m%HbEyRN!^H(- z?=F6E@bp|x_&_1D=W>=!pY|6p7r`5o{nv|Jp@e{?jE-pS|F!;L8 zlI{)76H@T+_XQz{;T3s z)$YTrR9!HIui*H5!QOZN`#@Tm*Ht?S$Jurr7neqcFbFQs(IhWY|B9=2Lj?zAa#t(s zto%z{^|9X~vVM%yh>VQnPW2hJlr+1V_)6S$?%i#d?PI~6TWOR#lOEG_(>#};*o|<3H=%8~d&TEB86B z@k0vsqF{=5o@2OFX5LkgYoBCANshJ*?H@Q~nkqmQL)T(;NOxo|Dd*p|>~kh7S@xck zNFF`;D7&#@;%Y>?=uReCMZ{XT*(|m{ouZL{M#u2-jSC4ryWDpeiOS@86m1(!*)lp? zwd?t3dPb}Fe8#T0rAL`c=7vjMR%SO1Q`_DgJ1@TFPWZurA0GFb4B6y#H`I338^;_4 zPtJJHsD?^n=sd>MVoPA(#(MlYM#%*#-j-X5?h0n^3Q-JxOmpDDtL!mCrQn!V?Y(HV znDhVm@?rO19UJ`sTV?>jXX6F{0734JfZF){;{M8gtIYxc-hYeoZ+HGz(UFrwUk$xZ`0mcYOKQNXD0d%Av2uOf20@4qR!`;2LcjcjDYk5V|frjNBV() x1Q;VA{lHir1kjOwARqz82uMFLmIncJq#p=KfH4Bn4~*qO03GQE0uo@1@HgbA>$(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 literal 21941 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

Jg%9Pc~$r|Dm5Cq4ws;~@gd>{x$5r9_-`g_8J zo{M@5oz*jJd^2ha^53QnO2gMAQ|(l=3~t zQBY2s&DNcPaoTA-t3kz)<-ed67=XGJILt2T%JQ*rj}OX^yB?xsYl*TE>8aEa?I+qO z+Vp;RoX)T9cL~#QYQIud>3h>$>RW2X=gZ3+*K6Ur0j`2Ay~744vtm$wugX^yl11-0 zvTE-brF{i}v1?oD(ro9Pxmjk+i||jQDS{0i8WY~FR&M$uw7_mF%KXD--xyr`=jc(F zA4B6kEk=0q9Trqq@2B~F_ozAfCGh(Vic~JU>&1|RLTK8KFpBK7Q_HC-*Xi(qU7zBm zr#BYn?_<4v|C;No%`0u^|C|BAl~q5PtWzZJzb<@e*2cXGU~lHLk1m%HbEyRN!^H(- z?=F6E@bp|x_&_1D=W>=!pY|6p7r`5o{nv|Jp@e{?jE-pS|F!;L8 zlI{)76H@T+_XQz{;T3s z)$YTrR9!HIui*H5!QOZN`#@Tm*Ht?S$Jurr7neqcFbFQs(IhWY|B9=2Lj?zAa#t(s zto%z{^|9X~vVM%yh>VQnPW2hJlr+1V_)6S$?%i#d?PI~6TWOR#lOEG_(>#};*o|<3H=%8~d&TEB86B z@k0vsqF{=5o@2OFX5LkgYoBCANshJ*?H@Q~nkqmQL)T(;NOxo|Dd*p|>~kh7S@xck zNFF`;D7&#@;%Y>?=uReCMZ{XT*(|m{ouZL{M#u2-jSC4ryWDpeiOS@86m1(!*)lp? zwd?t3dPb}Fe8#T0rAL`c=7vjMR%SO1Q`_DgJ1@TFPWZurA0GFb4B6y#H`I338^;_4 zPtJJHsD?^n=sd>MVoPA(#(MlYM#%*#-j-X5?h0n^3Q-JxOmpDDtL!mCrQn!V?Y(HV znDhVm@?rO19UJ`sTV?>jXX6F{0734JfZF){;{M8gtIYxc-hYeoZ+HGz(UFrwUk$xZ`0mcYOKQNXD0d%Av2uOf20@4qR!`;2LcjcjDYk5V|frjNBV() x1Q;VA{lHir1kjOwARqz82uMFLmIncJq#p=KfH4Bn4~*qO03GQE0uo@1@HgbA>$(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, }); } From 1063d39de805a83169fc9ba1f841c1239be45da8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 9 Jan 2024 21:15:56 +0900 Subject: [PATCH 05/15] enhnace(frontend): tweak game --- locales/index.d.ts | 3 + locales/ja-JP.yml | 3 + .../assets/drop-and-fusion/gameover.mp3 | Bin 0 -> 31346 bytes packages/frontend/package.json | 1 + .../frontend/src/pages/drop-and-fusion.vue | 131 ++++++++++++-- .../src/scripts/drop-and-fusion-engine.ts | 163 +++++++++++++++--- pnpm-lock.yaml | 5 +- 7 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/gameover.mp3 diff --git a/locales/index.d.ts b/locales/index.d.ts index 96bc9099dd..df84412473 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1195,6 +1195,9 @@ export interface Locale { "bubbleGame": string; "sfx": string; "soundWillBePlayed": string; + "showReplay": string; + "replay": string; + "replaying": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c28fde56cb..997ddf9c6e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1192,6 +1192,9 @@ enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" bubbleGame: "バブルゲーム" sfx: "効果音" soundWillBePlayed: "サウンドが再生されます" +showReplay: "リプレイを見る" +replay: "リプレイ" +replaying: "リプレイ中" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/assets/drop-and-fusion/gameover.mp3 b/packages/frontend/assets/drop-and-fusion/gameover.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..23b41c56995a8adc0033d46b946f967876073d1b GIT binary patch literal 31346 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

|o%EnUC=>z1GR2{(NK@)YZP5tpIK9|o2oRV& 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 s`_whL&MocP@0V7;w|TAY

-
+
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only >
{{ comboPrev }} Chain!
-
+
-
+
SCORE:
MAX CHAIN:
-
- Restart - Share -
+
+
+
{{ i18n.ts.replaying }}
+
+
+
+
+ END REPLAY +
+
+
+
+
+
+ {{ i18n.ts.done }} + {{ i18n.ts.showReplay }} + {{ i18n.ts.share }} + Copy replay data
@@ -139,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- Restart + Retry
@@ -168,6 +182,7 @@ import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; import MkSwitch from '@/components/MkSwitch.vue'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const NORMAL_BASE_SIZE = 30; const NORAML_MONOS: Mono[] = [{ @@ -401,6 +416,8 @@ const GAME_HEIGHT = 600; let viewScale = 1; let game: DropAndFusionGame; let containerElRect: DOMRect | null = null; +let seed: string; +let logs: ReturnType | null = null; const containerEl = shallowRef(); const canvasEl = shallowRef(); @@ -414,22 +431,25 @@ const comboPrev = ref(0); const maxCombo = ref(0); const dropReady = ref(true); const gameMode = ref<'normal' | 'square'>('normal'); -const gameOver = ref(false); +const isGameOver = ref(false); const gameStarted = ref(false); const highScore = ref(null); const showConfig = ref(false); +const replaying = ref(false); 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; + if (replaying.value) return; const x = (ev.clientX - containerElRect.left) / viewScale; game.drop(x); } function onTouchend(ev: TouchEvent) { if (!containerElRect) return; + if (replaying.value) return; const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; game.drop(x); } @@ -454,9 +474,18 @@ function hold() { game.hold(); } -function restart() { +async function surrender() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }); + if (canceled) return; + game.surrender(); +} + +function end() { game.dispose(); - gameOver.value = false; + isGameOver.value = false; currentPick.value = null; dropReady.value = true; stock.value = []; @@ -467,6 +496,45 @@ function restart() { gameStarted.value = false; } +function replay() { + replaying.value = true; + game.dispose(); + game = new DropAndFusionGame({ + width: GAME_WIDTH, + height: GAME_HEIGHT, + canvas: canvasEl.value!, + seed: seed, + sfxVolume: mute.value ? 0 : sfxVolume.value, + ...( + gameMode.value === 'normal' ? { + monoDefinitions: NORAML_MONOS, + } : { + monoDefinitions: SQUARE_MONOS, + } + ), + }); + attachGameEvents(); + os.promiseDialog(game.load(), async () => { + game.start(logs!); + }); +} + +function endReplay() { + replaying.value = false; + game.dispose(); +} + +function exportLog() { + if (!logs) return; + const data = JSON.stringify({ + seed: seed, + date: new Date().toISOString(), + logs: logs, + }); + copyToClipboard(data); + os.success(); +} + function attachGameEvents() { game.addListener('changeScore', value => { score.value = value; @@ -492,9 +560,11 @@ function attachGameEvents() { }); game.addListener('dropped', () => { + if (replaying.value) return; + dropReady.value = false; window.setTimeout(() => { - if (!gameOver.value) { + if (!isGameOver.value) { dropReady.value = true; } }, game.DROP_INTERVAL); @@ -511,6 +581,8 @@ function attachGameEvents() { }); game.addListener('monoAdded', (mono) => { + if (replaying.value) return; + // 実績関連 if (mono.level === 10) { claimAchievement('bubbleGameExplodingHead'); @@ -523,9 +595,15 @@ function attachGameEvents() { }); game.addListener('gameOver', () => { + if (replaying.value) { + endReplay(); + return; + } + + logs = game.getLogs(); currentPick.value = null; dropReady.value = false; - gameOver.value = true; + isGameOver.value = true; if (score.value > (highScore.value ?? 0)) { highScore.value = score.value; @@ -551,10 +629,13 @@ async function start() { highScore.value = null; } + seed = Date.now().toString(); + game = new DropAndFusionGame({ width: GAME_WIDTH, height: GAME_HEIGHT, canvas: canvasEl.value!, + seed: seed, sfxVolume: mute.value ? 0 : sfxVolume.value, ...( gameMode.value === 'normal' ? { @@ -690,7 +771,7 @@ useInterval(() => { }, 1000, { immediate: false, afterMounted: true }); onDeactivated(() => { - restart(); + end(); }); definePageMetadata({ @@ -922,6 +1003,28 @@ definePageMetadata({ } } +.replayIndicator { + position: absolute; + z-index: 10; + left: 10px; + bottom: 10px; + padding: 6px 8px; + color: #f00; + background: #0008; + border-radius: 6px; + pointer-events: none; +} + +.replayIndicatorText { + animation: replayIndicator-blink 2s infinite; +} + +@keyframes replayIndicator-blink { + 0% { opacity: 1; } + 50% { opacity: 0; } + 100% { opacity: 1; } +} + @keyframes currentMonoArrow { 0% { transform: translateY(0); } 25% { transform: translateY(-8px); } diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index f71f3a668e..9db93d1534 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Matter from 'matter-js'; +import seedrandom from 'seedrandom'; import * as sound from '@/scripts/sound.js'; export type Mono = { @@ -20,6 +21,18 @@ export type Mono = { spriteScale: number; }; +type Log = { + frame: number; + operation: 'drop'; + x: number; +} | { + frame: number; + operation: 'hold'; +} | { + frame: number; + operation: 'surrender'; +}; + export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; @@ -35,18 +48,23 @@ export class DropAndFusionGame extends EventEmitter<{ public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; private STOCK_MAX = 4; + private TICK_DELTA = 1000 / 60; // 60fps private loaded = false; + private frame = 0; private engine: Matter.Engine; private render: Matter.Render; - private runner: Matter.Runner; + private tickRaf: ReturnType | null = null; + private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; private overflowCollider: Matter.Body; private isGameOver = false; - private gameWidth: number; private gameHeight: number; private monoDefinitions: Mono[] = []; private monoTextures: Record = {}; private monoTextureUrls: Record = {}; + private rng: () => number; + private logs: Log[] = []; + private replaying = false; private sfxVolume = 1; @@ -87,13 +105,17 @@ export class DropAndFusionGame extends EventEmitter<{ width: number; height: number; monoDefinitions: Mono[]; + seed: string; sfxVolume?: number; }) { super(); + this.tick = this.tick.bind(this); + this.gameWidth = opts.width; this.gameHeight = opts.height; this.monoDefinitions = opts.monoDefinitions; + this.rng = seedrandom(opts.seed); if (opts.sfxVolume) { this.sfxVolume = opts.sfxVolume; @@ -129,9 +151,6 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Render.run(this.render); - this.runner = Matter.Runner.create(); - Matter.Runner.run(this.runner, this.engine); - this.engine.world.bodies = []; //#region walls @@ -223,9 +242,12 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Composite.add(this.engine.world, body); // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする - window.setTimeout(() => { - this.activeBodyIds.push(body.id); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + this.activeBodyIds.push(body.id); + }, + }); const comboBonus = 1 + ((this.combo - 1) / 5); const additionalScore = Math.round(currentMono.score * comboBonus); @@ -244,7 +266,7 @@ export class DropAndFusionGame extends EventEmitter<{ } else { //const VELOCITY = 30; //for (let i = 0; i < 10; i++) { - // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2))); + // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(this.rng() * 3)))!, x + ((this.rng() * VELOCITY) - (VELOCITY / 2)), y + ((this.rng() * VELOCITY) - (VELOCITY / 2))); // Matter.Composite.add(world, body); // bodies.push(body); //} @@ -255,10 +277,25 @@ export class DropAndFusionGame extends EventEmitter<{ } } + public surrender() { + this.logs.push({ + frame: this.frame, + operation: 'surrender', + }); + + this.gameOver(); + } + private gameOver() { this.isGameOver = true; - Matter.Runner.stop(this.runner); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; this.emit('gameOver'); + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { + volume: this.sfxVolume, + }); } /** テクスチャをすべてキャッシュする */ @@ -292,13 +329,14 @@ export class DropAndFusionGame extends EventEmitter<{ return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); } - public start() { + public start(logs?: Log[]) { if (!this.loaded) throw new Error('game is not loaded yet'); + if (logs) this.replaying = true; for (let i = 0; i < this.STOCK_MAX; i++) { 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)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); } this.emit('changeStock', this.stock); @@ -327,10 +365,13 @@ export class DropAndFusionGame extends EventEmitter<{ this.fusion(bodyA, bodyB); } else { fusionReservedPairs.push({ bodyA, bodyB }); - window.setTimeout(() => { - fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); - this.fusion(bodyA, bodyB); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, + }); } } else { const energy = pairs.collision.depth; @@ -354,6 +395,69 @@ export class DropAndFusionGame extends EventEmitter<{ this.combo = 0; } }, 500); + + if (logs) { + const playTick = () => { + this.frame++; + const log = logs.find(x => x.frame === this.frame - 1); + if (log) { + switch (log.operation) { + case 'drop': { + this.drop(log.x); + break; + } + case 'hold': { + this.hold(); + break; + } + case 'surrender': { + this.surrender(); + break; + } + default: + break; + } + } + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); + + Matter.Engine.update(this.engine, this.TICK_DELTA); + + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(playTick); + } + }; + + playTick(); + } else { + this.tick(); + } + } + + public getLogs() { + return this.logs; + } + + private tick() { + this.frame++; + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); + Matter.Engine.update(this.engine, this.TICK_DELTA); + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(this.tick); + } } public async load() { @@ -387,17 +491,22 @@ export class DropAndFusionGame extends EventEmitter<{ public drop(_x: number) { if (this.isGameOver) return; - if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return; + if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return; 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)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeStock', this.stock); - const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x)); + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), Math.round(_x))); const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); + this.logs.push({ + frame: this.frame, + operation: 'drop', + x, + }); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; @@ -416,6 +525,11 @@ export class DropAndFusionGame extends EventEmitter<{ public hold() { if (this.isGameOver) return; + this.logs.push({ + frame: this.frame, + operation: 'hold', + }); + if (this.holding) { const head = this.stock.shift()!; this.stock.unshift(this.holding); @@ -426,8 +540,8 @@ export class DropAndFusionGame extends EventEmitter<{ 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)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeHolding', this.holding); this.emit('changeStock', this.stock); @@ -440,8 +554,9 @@ export class DropAndFusionGame extends EventEmitter<{ public dispose() { if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; Matter.Render.stop(this.render); - Matter.Runner.stop(this.runner); Matter.World.clear(this.engine.world, false); Matter.Engine.clear(this.engine); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0f74de843..9d98224822 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -787,6 +787,9 @@ importers: sass: specifier: 1.69.5 version: 1.69.5 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 shiki: specifier: 0.14.7 version: 0.14.7 @@ -7401,7 +7404,7 @@ packages: hasBin: true peerDependencies: '@swc/core': ^1.2.66 - chokidar: ^3.5.1 + chokidar: 3.5.3 peerDependenciesMeta: chokidar: optional: true From 358dc6289bb0da03fa62b68111feda99da701d9c Mon Sep 17 00:00:00 2001 From: Camilla Ett Date: Tue, 9 Jan 2024 21:18:09 +0900 Subject: [PATCH 06/15] =?UTF-8?q?Enhance(frontend):=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=80=85=E3=81=AE=E5=A0=B4=E5=90=88=E3=81=AFAPI=20token?= =?UTF-8?q?=E3=81=AE=E7=99=BA=E8=A1=8C=E7=94=BB=E9=9D=A2=E3=81=A7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=A9=9F=E8=83=BD=E3=81=AB=E9=96=A2=E3=81=99=E3=82=8B?= =?UTF-8?q?=E6=A8=A9=E9=99=90=E3=82=92=E4=BB=98=E4=B8=8E=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#12944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance(frontend): 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように * update CHANGELOG.md * tweak style * (refactor) remove unnecessary imports * fix lint --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Co-authored-by: kakkokari-gtyih --- CHANGELOG.md | 1 + locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../src/components/MkTokenGenerateWindow.vue | 64 ++++++++++++++++--- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f4210913..2b56ff9fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Feat: 新しいゲームを追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように +- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index df84412473..aa74ba54b0 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -634,6 +634,7 @@ export interface Locale { "small": string; "generateAccessToken": string; "permission": string; + "adminPermission": string; "enableAll": string; "disableAll": string; "tokenRequested": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 997ddf9c6e..4863bbe770 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -631,6 +631,7 @@ medium: "中" small: "小" generateAccessToken: "アクセストークンの発行" permission: "権限" +adminPermission: "管理者権限" enableAll: "全て有効にする" disableAll: "全て無効にする" tokenRequested: "アカウントへのアクセス許可" diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue index d024e1e593..a42767e1b6 100644 --- a/packages/frontend/src/components/MkTokenGenerateWindow.vue +++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue @@ -33,7 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.enableAll }}
- {{ i18n.t(`_permissions.${kind}`) }} + {{ i18n.t(`_permissions.${kind}`) }} +
+
+
{{ i18n.ts.adminPermission }}
+
+ {{ i18n.t(`_permissions.${kind}`) }} +
@@ -49,6 +55,7 @@ import MkButton from './MkButton.vue'; import MkInfo from './MkInfo.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; +import { iAmAdmin } from '@/account.js'; const props = withDefaults(defineProps<{ title?: string | null; @@ -68,37 +75,76 @@ const emit = defineEmits<{ }>(); const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin')); +const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin')); + const dialog = shallowRef>(); const name = ref(props.initialName); -const permissions = ref(>{}); +const permissionSwitches = ref(>{}); +const permissionSwitchesForAdmin = ref(>{}); if (props.initialPermissions) { for (const kind of props.initialPermissions) { - permissions.value[kind] = true; + permissionSwitches.value[kind] = true; } } else { for (const kind of defaultPermissions) { - permissions.value[kind] = false; + permissionSwitches.value[kind] = false; + } + + if (iAmAdmin) { + for (const kind of adminPermissions) { + permissionSwitchesForAdmin.value[kind] = false; + } } } function ok(): void { emit('done', { name: name.value, - permissions: Object.keys(permissions.value).filter(p => permissions.value[p]), + permissions: [ + ...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]), + ...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []), + ], }); dialog.value?.close(); } function disableAll(): void { - for (const p in permissions.value) { - permissions.value[p] = false; + for (const p in permissionSwitches.value) { + permissionSwitches.value[p] = false; + } + if (iAmAdmin) { + for (const p in permissionSwitchesForAdmin.value) { + permissionSwitchesForAdmin.value[p] = false; + } } } function enableAll(): void { - for (const p in permissions.value) { - permissions.value[p] = true; + for (const p in permissionSwitches.value) { + permissionSwitches.value[p] = true; + } + if (iAmAdmin) { + for (const p in permissionSwitchesForAdmin.value) { + permissionSwitchesForAdmin.value[p] = true; + } } } + + From 7e52ea4818029cbb7a981cb58a7eca0bf6b7e0e7 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Wed, 10 Jan 2024 00:44:13 +0900 Subject: [PATCH 07/15] Update CHANGELOG.md (#12953) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b56ff9fc9..6963d45f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 +- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正 ### Server - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました From f5b864df7bedc3b4a7abdfb09a3df9c2db8c3627 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 07:26:16 +0900 Subject: [PATCH 08/15] fix(frontend): fix game replay --- packages/frontend/src/scripts/drop-and-fusion-engine.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 9db93d1534..16fe87d97a 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -500,12 +500,13 @@ export class DropAndFusionGame extends EventEmitter<{ }); this.emit('changeStock', this.stock); - const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), Math.round(_x))); + const inputX = Math.round(_x); + const x = Math.min(this.gameWidth - 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); this.logs.push({ frame: this.frame, operation: 'drop', - x, + x: inputX, }); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); From 6bae440f3912f882fea1e3901aad2d18f2b6a3b8 Mon Sep 17 00:00:00 2001 From: FineArchs <133759614+FineArchs@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:47:47 +0900 Subject: [PATCH 09/15] bump aiscript version to 0.17.0 (#12955) * bump aiscript version to 0.17.0 * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ packages/frontend/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6963d45f63..244fd724a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように - Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように +- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md) + - 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意 - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 895aa47419..8c3ce30668 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,7 +24,7 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.5", "@rollup/pluginutils": "5.1.0", - "@syuilo/aiscript": "0.16.0", + "@syuilo/aiscript": "0.17.0", "@tabler/icons-webfont": "2.44.0", "@twemoji/parser": "15.0.0", "@vitejs/plugin-vue": "5.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d98224822..400051bce7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -686,8 +686,8 @@ importers: specifier: 5.1.0 version: 5.1.0(rollup@4.9.1) '@syuilo/aiscript': - specifier: 0.16.0 - version: 0.16.0 + specifier: 0.17.0 + version: 0.17.0 '@tabler/icons-webfont': specifier: 2.44.0 version: 2.44.0 @@ -7649,8 +7649,8 @@ packages: dev: false optional: true - /@syuilo/aiscript@0.16.0: - resolution: {integrity: sha512-CXvoWOq6kmOSUQtKv0IEf7Ebfkk5PO1LxAgLqgRRPgssPvDvINCXu/gFNXKdapkFMkmX+Gj8qjemKR1vnUS4ZA==} + /@syuilo/aiscript@0.17.0: + resolution: {integrity: sha512-3JtQ1rWJHMxQ3153zLCXMUOwrOgjPPYGBl0dPHhR0ohm4tn7okMQRugxMCT0t3YxByemb9FfiM6TUjd0tEGxdA==} dependencies: seedrandom: 3.0.5 stringz: 2.1.0 From 138a248a6ce875af812c8ab126b78817d495b0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:40:09 +0900 Subject: [PATCH 10/15] =?UTF-8?q?fix(drop-and-fusion):=20=E3=83=90?= =?UTF-8?q?=E3=83=96=E3=83=AB=E3=82=B2=E3=83=BC=E3=83=A0=E3=81=AE=E3=83=AA?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=82=A4=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=A7?= =?UTF-8?q?=E3=83=AA=E3=83=88=E3=83=A9=E3=82=A4=E3=81=8C=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#12957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ゲーム中なら諦める、ゲームオーバー画面の表示中はリスタートになるように --- packages/frontend/src/pages/drop-and-fusion.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 974daf35e4..d041a675f8 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -153,7 +153,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- Retry + Surrender + Retry
@@ -483,15 +484,22 @@ async function surrender() { game.surrender(); } +async function retry() { + end(); + await start(); +} + function end() { game.dispose(); isGameOver.value = false; + replaying.value = false; currentPick.value = null; dropReady.value = true; stock.value = []; score.value = 0; combo.value = 0; comboPrev.value = 0; + maxCombo.value = 0; bgmNodes?.soundSource.stop(); gameStarted.value = false; } From 3d9e42efca8792bcfa1be7bd6125cf732db50fdb Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 11:38:49 +0900 Subject: [PATCH 11/15] =?UTF-8?q?enhance(drop-and-fusion):=20=E3=83=AA?= =?UTF-8?q?=E3=83=97=E3=83=AC=E3=82=A4=E3=81=AE=E5=80=8D=E9=80=9F=E5=86=8D?= =?UTF-8?q?=E7=94=9F=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/pages/drop-and-fusion.vue | 12 ++- .../src/scripts/drop-and-fusion-engine.ts | 79 ++++++++++--------- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index d041a675f8..f585519459 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -103,7 +103,11 @@ SPDX-License-Identifier: AGPL-3.0-only
- END REPLAY +
+ END REPLAY + x2 + x4 +
@@ -437,10 +441,15 @@ const gameStarted = ref(false); const highScore = ref(null); const showConfig = ref(false); const replaying = ref(false); +const replayPlaybackRate = ref(1); const mute = ref(false); const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); +watch(replayPlaybackRate, (newValue) => { + game.replayPlaybackRate = newValue; +}); + function onClick(ev: MouseEvent) { if (!containerElRect) return; if (replaying.value) return; @@ -493,6 +502,7 @@ function end() { game.dispose(); isGameOver.value = false; replaying.value = false; + replayPlaybackRate.value = 1; currentPick.value = null; dropReady.value = true; stock.value = []; diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 16fe87d97a..a59eb271ec 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -44,7 +44,7 @@ export class DropAndFusionGame extends EventEmitter<{ gameOver: () => void; }> { private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる - private COMBO_INTERVAL = 1000; + private COMBO_INTERVAL = 60; // frame public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; private STOCK_MAX = 4; @@ -76,7 +76,7 @@ export class DropAndFusionGame extends EventEmitter<{ private latestDroppedBodyId: Matter.Body['id'] | null = null; private latestDroppedAt = 0; - private latestFusionedAt = 0; + private latestFusionedAt = 0; // frame private stock: { id: string; mono: Mono }[] = []; private holding: { id: string; mono: Mono } | null = null; @@ -100,6 +100,8 @@ export class DropAndFusionGame extends EventEmitter<{ private comboIntervalId: number | null = null; + public replayPlaybackRate = 1; + constructor(opts: { canvas: HTMLCanvasElement; width: number; @@ -219,13 +221,12 @@ export class DropAndFusionGame extends EventEmitter<{ } private fusion(bodyA: Matter.Body, bodyB: Matter.Body) { - const now = Date.now(); - if (this.latestFusionedAt > now - this.COMBO_INTERVAL) { + if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) { this.combo++; } else { this.combo = 1; } - this.latestFusionedAt = now; + this.latestFusionedAt = this.frame; // TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する? const newX = (bodyA.position.x + bodyB.position.x) / 2; @@ -390,44 +391,43 @@ export class DropAndFusionGame extends EventEmitter<{ } }); - this.comboIntervalId = window.setInterval(() => { - if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) { - this.combo = 0; - } - }, 500); - if (logs) { const playTick = () => { - this.frame++; - const log = logs.find(x => x.frame === this.frame - 1); - if (log) { - switch (log.operation) { - case 'drop': { - this.drop(log.x); - break; - } - case 'hold': { - this.hold(); - break; - } - case 'surrender': { - this.surrender(); - break; - } - default: - break; + for (let i = 0; i < this.replayPlaybackRate; i++) { + this.frame++; + if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) { + this.combo = 0; } - } - this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { - if (x.frame === this.frame) { - x.callback(); - return false; - } else { - return true; + const log = logs.find(x => x.frame === this.frame - 1); + if (log) { + switch (log.operation) { + case 'drop': { + this.drop(log.x); + break; + } + case 'hold': { + this.hold(); + break; + } + case 'surrender': { + this.surrender(); + break; + } + default: + break; + } } - }); + this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { + if (x.frame === this.frame) { + x.callback(); + return false; + } else { + return true; + } + }); - Matter.Engine.update(this.engine, this.TICK_DELTA); + Matter.Engine.update(this.engine, this.TICK_DELTA); + } if (!this.isGameOver) { this.tickRaf = window.requestAnimationFrame(playTick); @@ -446,6 +446,9 @@ export class DropAndFusionGame extends EventEmitter<{ private tick() { this.frame++; + if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) { + this.combo = 0; + } this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { if (x.frame === this.frame) { x.callback(); From 4bd9f664d7213e9d6d507ae1b8cb67e2b78e766a Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 13:44:00 +0900 Subject: [PATCH 12/15] enhance(drop-and-fusion): some tweaks --- .../frontend/src/pages/drop-and-fusion.vue | 1 + .../src/scripts/drop-and-fusion-engine.ts | 29 +++++++++++++------ packages/frontend/src/scripts/sound.ts | 2 -- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index f585519459..c5ab7a33f5 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -1028,6 +1028,7 @@ definePageMetadata({ bottom: 10px; padding: 6px 8px; color: #f00; + font-weight: bold; background: #0008; border-radius: 6px; pointer-events: none; diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index a59eb271ec..342e818905 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -157,6 +157,7 @@ export class DropAndFusionGame extends EventEmitter<{ //#region walls const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { + label: '_wall_', isStatic: true, friction: 0.7, slop: 1.0, @@ -254,12 +255,14 @@ export class DropAndFusionGame extends EventEmitter<{ const additionalScore = Math.round(currentMono.score * comboBonus); this.score += additionalScore; - // TODO: 効果音再生はコンポーネント側の責務なので移動する - const pan = ((newX / this.gameWidth) - 0.5) * 2; + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? + const panV = newX - this.PLAYAREA_MARGIN; + const panW = this.gameWidth - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; + const pan = ((panV / panW) - 0.5) * 2; sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', { volume: this.sfxVolume, pan, - playbackRate: nextMono.sfxPitch, + playbackRate: nextMono.sfxPitch * this.replayPlaybackRate, }); this.emit('monoAdded', nextMono); @@ -293,7 +296,7 @@ export class DropAndFusionGame extends EventEmitter<{ this.tickRaf = null; this.emit('gameOver'); - // TODO: 効果音再生はコンポーネント側の責務なので移動する + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { volume: this.sfxVolume, }); @@ -377,14 +380,19 @@ export class DropAndFusionGame extends EventEmitter<{ } else { const energy = pairs.collision.depth; if (energy > minCollisionEnergyForSound) { - // TODO: 効果音再生はコンポーネント側の責務なので移動する + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? 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 panV = + pairs.bodyA.label === '_wall_' ? bodyB.position.x - this.PLAYAREA_MARGIN : + pairs.bodyB.label === '_wall_' ? bodyA.position.x - this.PLAYAREA_MARGIN : + ((bodyA.position.x + bodyB.position.x) / 2) - this.PLAYAREA_MARGIN; + const panW = this.gameWidth - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; + const pan = ((panV / panW) - 0.5) * 2; const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10))); sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', { volume: vol, pan, - playbackRate: pitch, + playbackRate: pitch * this.replayPlaybackRate, }); } } @@ -518,11 +526,14 @@ export class DropAndFusionGame extends EventEmitter<{ this.emit('dropped'); this.emit('monoAdded', head.mono); - // TODO: 効果音再生はコンポーネント側の責務なので移動する - const pan = ((x / this.gameWidth) - 0.5) * 2; + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? + const panV = x - this.PLAYAREA_MARGIN; + const panW = this.gameWidth - this.PLAYAREA_MARGIN - this.PLAYAREA_MARGIN; + const pan = ((panV / panW) - 0.5) * 2; sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', { volume: this.sfxVolume, pan, + playbackRate: this.replayPlaybackRate, }); } diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 142ddf87c9..05c8977ecf 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -99,7 +99,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) } if (options?.useCache ?? true) { if (cache.has(url)) { - if (_DEV_) console.log('use cache'); return cache.get(url) as AudioBuffer; } } @@ -128,7 +127,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) */ 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; From c1c363bf08a391400e4b8b1df91962c26f2f3192 Mon Sep 17 00:00:00 2001 From: 1Step621 <86859447+1STEP621@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:06:04 +0900 Subject: [PATCH 13/15] =?UTF-8?q?Enhance(frontend):=20=E7=B5=B5=E6=96=87?= =?UTF-8?q?=E5=AD=97=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC/=E3=82=AA?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=82=B3=E3=83=B3=E3=83=97=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=A7=E5=AE=8C=E5=85=A8=E4=B8=80=E8=87=B4=E3=81=AE?= =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=82=92=E5=84=AA=E5=85=88=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#12928)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 絵文字ピッカー/オートコンプリートで完全一致の絵文字を優先するように * update CHANGELOG.md * improve performance --- CHANGELOG.md | 1 + .../frontend/src/components/MkAutocomplete.vue | 17 +++++++++++++---- .../frontend/src/components/MkEmojiPicker.vue | 13 +++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 244fd724a9..13ad3a3508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように - Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md) - 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意 +- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 49884c705f..15eda4499f 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -262,15 +262,24 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): } const matched = new Map(); - - // 前方一致(エイリアスなし) + // 完全一致(エイリアス込み) emojiDb.some(x => { - if (x.name.startsWith(query) && !x.aliasOf) { - matched.set(x.name, { emoji: x, score: query.length + 1 }); + if (x.name === query && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 }); } return matched.size === max; }); + // 前方一致(エイリアスなし) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.startsWith(query) && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length + 1 }); + } + return matched.size === max; + }); + } + // 前方一致(エイリアス込み) if (matched.size < max) { emojiDb.some(x => { diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index f36d46506f..84424c58ed 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -221,6 +221,19 @@ watch(q, () => { } } } else { + if (customEmojisMap.has(newQ)) { + matches.add(customEmojisMap.get(newQ)!); + } + if (matches.size >= max) return matches; + + for (const emoji of emojis) { + if (emoji.aliases.some(alias => alias === newQ)) { + matches.add(emoji); + if (matches.size >= max) break; + } + } + if (matches.size >= max) return matches; + for (const emoji of emojis) { if (emoji.name.startsWith(newQ)) { matches.add(emoji); From 5c786cace839147a11fadac6ee46da29db5f2457 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 17:31:59 +0900 Subject: [PATCH 14/15] enhance(drop-and-fusion): add game description --- locales/index.d.ts | 8 ++++++++ locales/ja-JP.yml | 7 +++++++ packages/frontend/src/pages/drop-and-fusion.vue | 16 +++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index aa74ba54b0..852cbdd27d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1199,6 +1199,14 @@ export interface Locale { "showReplay": string; "replay": string; "replaying": string; + "_bubbleGame": { + "howToPlay": string; + "_howToPlay": { + "section1": string; + "section2": string; + "section3": string; + }; + }; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4863bbe770..f85dc0fcf8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1197,6 +1197,13 @@ showReplay: "リプレイを見る" replay: "リプレイ" replaying: "リプレイ中" +_bubbleGame: + howToPlay: "遊び方" + _howToPlay: + section1: "位置を調整してハコにモノを落とします。" + section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" + section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!" + _announcement: forExistingUsers: "既存ユーザーのみ" forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index c5ab7a33f5..9fb7ab2e23 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -8,13 +8,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
-
+
@@ -33,6 +33,16 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+
{{ i18n.ts._bubbleGame.howToPlay }}
+
    +
  1. {{ i18n.ts._bubbleGame._howToPlay.section1 }}
  2. +
  3. {{ i18n.ts._bubbleGame._howToPlay.section2 }}
  4. +
  5. {{ i18n.ts._bubbleGame._howToPlay.section3 }}
  6. +
+
+
From 36fd7d17cf1c71fa59eae445d05498a7bf5ab173 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 19:54:59 +0900 Subject: [PATCH 15/15] enhance(drop-and-fusion): some tweaks --- .../src/pages/drop-and-fusion.game.vue | 1052 +++++++++++++++++ .../frontend/src/pages/drop-and-fusion.vue | 982 +-------------- .../src/scripts/drop-and-fusion-engine.ts | 2 +- 3 files changed, 1088 insertions(+), 948 deletions(-) create mode 100644 packages/frontend/src/pages/drop-and-fusion.game.vue diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue new file mode 100644 index 0000000000..acaebbadf7 --- /dev/null +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -0,0 +1,1052 @@ + + + + + + + diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 9fb7ab2e23..7bd0eef000 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -4,10 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 342e818905..d64c6015a5 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -33,6 +33,7 @@ type Log = { operation: 'surrender'; }; +// TODO: インスタンスを作り直さなくてもゲームをリスタートできるようにする export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; @@ -307,7 +308,6 @@ export class DropAndFusionGame extends EventEmitter<{ async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) { // Matter-js内にキャッシュがある場合はスキップ if (game.render.textures[mono.img]) return; - console.log('loading', mono.img); let src = mono.img; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition