diff --git a/packages/frontend/assets/reversi/put.mp3 b/packages/frontend/assets/reversi/put.mp3 new file mode 100644 index 000000000..baa1b8319 Binary files /dev/null and b/packages/frontend/assets/reversi/put.mp3 differ diff --git a/packages/frontend/assets/reversi/stone_b.png b/packages/frontend/assets/reversi/stone_b.png new file mode 100644 index 000000000..9e98455a3 Binary files /dev/null and b/packages/frontend/assets/reversi/stone_b.png differ diff --git a/packages/frontend/assets/reversi/stone_w.png b/packages/frontend/assets/reversi/stone_w.png new file mode 100644 index 000000000..f2bee593d Binary files /dev/null and b/packages/frontend/assets/reversi/stone_w.png differ diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index bf45fc411..fbe44d016 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -6,7 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkSpacer :contentMax="600"> <div :class="$style.root" class="_gaps"> - <header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ i18n.ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ i18n.ts._reversi.white }})</header> + <div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> + <span>({{ i18n.ts._reversi.black }})</span> + <MkAvatar style="width: 32px; height: 32px;" :user="blackUser" :showIndicator="true"/> + <span> vs </span> + <MkAvatar style="width: 32px; height: 32px;" :user="whiteUser" :showIndicator="true"/> + <span>({{ i18n.ts._reversi.white }})</span> + </div> <div style="overflow: clip; line-height: 28px;"> <div v-if="!iAmPlayer && !game.isEnded && turnUser" class="turn"> @@ -49,8 +55,14 @@ SPDX-License-Identifier: AGPL-3.0-only }]" @click="putStone(i)" > - <img v-if="stone === true" style="pointer-events: none; user-select: none; display: block; width: 100%; height: 100%;" :src="blackUser.avatarUrl"> - <img v-if="stone === false" style="pointer-events: none; user-select: none; display: block; width: 100%; height: 100%;" :src="whiteUser.avatarUrl"> + <template v-if="useAvatarAsStone"> + <img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl"/> + <img v-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl"/> + </template> + <template v-else> + <img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/> + <img v-if="stone === false" :class="$style.boardCellStone" src="/client-assets/reversi/stone_w.png"/> + </template> </div> </div> <div v-if="showBoardLabels" :class="$style.labelsY"> @@ -62,10 +74,41 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <div class="status"><b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}</div> + <div class="_panel" style="padding: 16px;"> + <div> + <b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }} + </div> + <div> + <div style="display: flex; align-items: center;"> + <span style="margin-right: 8px;">({{ i18n.ts._reversi.black }})</span> + <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="blackUser" :showIndicator="true"/> + <MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA> + </div> + <div> vs </div> + <div style="display: flex; align-items: center;"> + <span style="margin-right: 8px;">({{ i18n.ts._reversi.white }})</span> + <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="whiteUser" :showIndicator="true"/> + <MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA> + </div> + </div> + <div> + <p v-if="game.isLlotheo">{{ i18n.ts._reversi.isLlotheo }}</p> + <p v-if="game.loopedBoard">{{ i18n.ts._reversi.loopedMap }}</p> + <p v-if="game.canPutEverywhere">{{ i18n.ts._reversi.canPutEverywhere }}</p> + </div> + </div> - <div v-if="!game.isEnded && iAmPlayer" class="_buttonsCenter"> - <MkButton danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton> + <MkFolder> + <template #label>{{ i18n.ts.options }}</template> + <div class="_gaps_s" style="text-align: left;"> + <MkSwitch v-model="showBoardLabels">Show labels</MkSwitch> + <MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch> + </div> + </MkFolder> + + <div class="_buttonsCenter"> + <MkButton v-if="!game.isEnded && iAmPlayer" danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton> + <MkButton @click="share">{{ i18n.ts.share }}</MkButton> </div> <div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;"> @@ -79,12 +122,6 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton> </div> - <div> - <p v-if="game.isLlotheo">{{ i18n.ts._reversi.isLlotheo }}</p> - <p v-if="game.loopedBoard">{{ i18n.ts._reversi.loopedMap }}</p> - <p v-if="game.canPutEverywhere">{{ i18n.ts._reversi.canPutEverywhere }}</p> - </div> - <MkA v-if="game.isEnded" :to="`/reversi`"> <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; width: 200px; margin: auto;"/> </MkA> @@ -98,12 +135,16 @@ import * as CRC32 from 'crc-32'; import * as Misskey from 'misskey-js'; import * as Reversi from 'misskey-reversi'; import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/scripts/clone.js'; import { useInterval } from '@/scripts/use-interval.js'; import { signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; +import * as sound from '@/scripts/sound.js'; +import * as os from '@/os.js'; const $i = signinRequired(); @@ -112,7 +153,8 @@ const props = defineProps<{ connection: Misskey.ChannelConnection; }>(); -const showBoardLabels = true; +const showBoardLabels = ref<boolean>(false); +const useAvatarAsStone = ref<boolean>(true); const autoplaying = ref<boolean>(false); const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game)); const logPos = ref<number>(game.value.logs.length); @@ -206,8 +248,10 @@ function putStone(pos) { triggerRef(engine); - // サウンドを再生する - //sound.play(myColor.value ? 'reversiPutBlack' : 'reversiPutWhite'); + sound.playUrl('/client-assets/reversi/put.mp3', { + volume: 1, + playbackRate: 1, + }); const id = Math.random().toString(36).slice(2); props.connection.send('putStone', { @@ -232,8 +276,13 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) { case 'put': { engine.value.putStone(log.pos); triggerRef(engine); + + sound.playUrl('/client-assets/reversi/put.mp3', { + volume: 1, + playbackRate: 1, + }); + checkEnd(); - //sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); break; } @@ -281,7 +330,13 @@ function onStreamRescue(_game) { checkEnd(); } -function surrender() { +async function surrender() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }); + if (canceled) return; + misskeyApi('reversi/surrender', { gameId: game.value.id, }); @@ -317,6 +372,13 @@ function autoplay() { }, 1000); } +function share() { + os.post({ + initialText: `#MisskeyReversi ${location.href}`, + instant: true, + }); +} + onMounted(() => { props.connection.on('log', onStreamLog); props.connection.on('rescue', onStreamRescue); @@ -341,7 +403,7 @@ $gap: 4px; } .board { - width: calc(100% - 16px); + width: 100%; max-width: 500px; margin: 0 auto; } @@ -437,4 +499,12 @@ $gap: 4px; border-color: transparent !important; } } + +.boardCellStone { + pointer-events: none; + user-select: none; + display: block; + width: 100%; + height: 100%; +} </style> diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index c483e36c2..d33acc95a 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -10,9 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> </div> - <div class="_buttonsCenter"> - <MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton> - <MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton> + <div class="_panel" style="padding: 16px;"> + <div class="_buttonsCenter"> + <MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton> + <MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton> + </div> </div> <MkFolder v-if="invitations.length > 0" :defaultOpen="true"> @@ -28,12 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="$i" :defaultOpen="true"> <template #label>{{ i18n.ts._reversi.myGames }}</template> - <MkPagination :pagination="myGamesPagination"> + <MkPagination :pagination="myGamesPagination" :disableAutoLoad="true"> <template #default="{ items }"> <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="$style.gamePreview" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> + <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> vs <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> </div> <div :class="$style.gamePreviewFooter"> <span :style="!g.isEnded ? 'color: var(--accent);' : ''">{{ g.isEnded ? i18n.ts._reversi.ended : i18n.ts._reversi.playing }}</span> @@ -47,12 +49,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts._reversi.allGames }}</template> - <MkPagination :pagination="gamesPagination"> + <MkPagination :pagination="gamesPagination" :disableAutoLoad="true"> <template #default="{ items }"> <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="$style.gamePreview" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> + <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> vs <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> </div> <div :class="$style.gamePreviewFooter"> <span :style="!g.isEnded ? 'color: var(--accent);' : ''">{{ g.isEnded ? i18n.ts._reversi.ended : i18n.ts._reversi.playing }}</span>