- 实现 ChengduGamePage.vue 组件,包含完整的麻将游戏界面 - 实现 HallPage.vue 组件,支持房间列表展示、创建和加入功能 - 添加 mahjong API 接口用于房间管理操作 - 集成 store 状态管理和本地存储功能 - 实现 ChengduBottomActions 等游戏控制组件 - 添加 websocket 连接和游戏会话管理逻辑 - 实现游戏倒计时、结算等功能模块
242 lines
9.1 KiB
Vue
242 lines
9.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import deskImage from '../assets/images/desk/desk_01_1920_945.png'
|
|
import robotIcon from '../assets/images/icons/robot.svg'
|
|
import exitIcon from '../assets/images/icons/exit.svg'
|
|
import '../assets/styles/room.css'
|
|
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
|
|
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
|
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
|
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
|
|
import WindSquare from '../components/game/WindSquare.vue'
|
|
import ChengduBottomActions from '../components/chengdu/ChengduBottomActions.vue'
|
|
import ChengduDeskZones from '../components/chengdu/ChengduDeskZones.vue'
|
|
import ChengduSettlementOverlay from '../components/chengdu/ChengduSettlementOverlay.vue'
|
|
import ChengduTableHeader from '../components/chengdu/ChengduTableHeader.vue'
|
|
import ChengduWallSeats from '../components/chengdu/ChengduWallSeats.vue'
|
|
import { useGameStore } from '../store/gameStore'
|
|
import { useRoomMetaSnapshotState } from '../store'
|
|
import { useChengduGameActions } from './chengdu/composables/useChengduGameActions'
|
|
import { useChengduGameSession } from './chengdu/composables/useChengduGameSession'
|
|
import { useChengduGameSocket } from './chengdu/composables/useChengduGameSocket'
|
|
import { formatTile, useChengduTableView } from './chengdu/composables/useChengduTableView'
|
|
import type { ActionCountdownView, DisplayPlayer } from './chengdu/types'
|
|
|
|
const gameStore = useGameStore()
|
|
const roomMeta = useRoomMetaSnapshotState()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const session = useChengduGameSession({
|
|
route,
|
|
router,
|
|
gameStore,
|
|
roomMeta,
|
|
})
|
|
|
|
const routeRoomName = computed(() => (typeof route.query.roomName === 'string' ? route.query.roomName : ''))
|
|
const myPlayer = computed(() => gameStore.players[session.loggedInUserId.value] as DisplayPlayer | undefined)
|
|
const myHandTiles = computed(() => myPlayer.value?.handTiles ?? [])
|
|
const gamePlayers = computed<DisplayPlayer[]>(() =>
|
|
Object.values(gameStore.players).sort((a, b) => a.seatIndex - b.seatIndex) as DisplayPlayer[],
|
|
)
|
|
|
|
const tableView = useChengduTableView({
|
|
roomMeta,
|
|
gamePlayers,
|
|
gameStore,
|
|
localCachedAvatarUrl: session.localCachedAvatarUrl,
|
|
loggedInUserId: session.loggedInUserId,
|
|
loggedInUserName: session.loggedInUserName,
|
|
myHandTiles,
|
|
myPlayer,
|
|
routeRoomName,
|
|
})
|
|
|
|
const socket = useChengduGameSocket({
|
|
route,
|
|
router,
|
|
gameStore,
|
|
roomMeta,
|
|
roomName: tableView.roomName,
|
|
myHandTiles,
|
|
myPlayer,
|
|
session,
|
|
})
|
|
|
|
const actions = useChengduGameActions({
|
|
gameStore,
|
|
roomMeta,
|
|
gamePlayers,
|
|
myPlayer,
|
|
session,
|
|
})
|
|
|
|
const actionCountdown = computed<ActionCountdownView | null>(() => {
|
|
const countdown = session.roomCountdown.value
|
|
if (!countdown) {
|
|
return null
|
|
}
|
|
|
|
const deadlineAt = countdown.actionDeadlineAt ? Date.parse(countdown.actionDeadlineAt) : Number.NaN
|
|
const fallbackRemaining = countdown.remaining > 0 ? countdown.remaining : countdown.countdownSeconds
|
|
const derivedRemaining = Number.isFinite(deadlineAt)
|
|
? Math.ceil((deadlineAt - session.now.value) / 1000)
|
|
: fallbackRemaining
|
|
const remaining = Math.max(0, derivedRemaining)
|
|
|
|
if (remaining <= 0) {
|
|
return null
|
|
}
|
|
|
|
const targetPlayerIds = countdown.playerIds.filter((playerId) => typeof playerId === 'string' && playerId.trim())
|
|
if (targetPlayerIds.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const playerLabel = targetPlayerIds
|
|
.map((playerId) => {
|
|
if (playerId === session.loggedInUserId.value) {
|
|
return '你'
|
|
}
|
|
const targetPlayer = gameStore.players[playerId]
|
|
if (targetPlayer?.displayName) {
|
|
return targetPlayer.displayName
|
|
}
|
|
if (targetPlayer) {
|
|
return `玩家${targetPlayer.seatIndex + 1}`
|
|
}
|
|
return '玩家'
|
|
})
|
|
.join('、')
|
|
const duration = countdown.duration > 0 ? countdown.duration : Math.max(remaining, fallbackRemaining, 1)
|
|
const includesSelf = targetPlayerIds.includes(session.loggedInUserId.value)
|
|
|
|
return {
|
|
playerLabel,
|
|
remaining,
|
|
duration,
|
|
isSelf: includesSelf,
|
|
progress: Math.max(0, Math.min(100, (remaining / duration) * 100)),
|
|
}
|
|
})
|
|
|
|
function handleLeaveRoom(): void {
|
|
session.menuOpen.value = false
|
|
session.backHall()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="picture-scene">
|
|
<div class="picture-layout">
|
|
<section class="table-stage">
|
|
<img class="table-desk" :src="deskImage" alt=""/>
|
|
|
|
<div class="table-felt">
|
|
<div class="table-surface"></div>
|
|
<div class="inner-outline outer"></div>
|
|
<div class="inner-outline mid"></div>
|
|
|
|
<ChengduTableHeader
|
|
:leave-room-pending="session.leaveRoomPending"
|
|
:menu-open="session.menuOpen"
|
|
:menu-trigger-active="session.menuTriggerActive"
|
|
:is-trust-mode="session.isTrustMode"
|
|
:wall-count="tableView.roomState.game.state.wall.length || 48"
|
|
:network-label="session.networkLabel"
|
|
:ws-status="session.wsStatus"
|
|
:formatted-clock="session.formattedClock"
|
|
:room-name="tableView.roomState.name || tableView.roomName"
|
|
:current-phase-text="tableView.currentPhaseText"
|
|
:player-count="tableView.roomState.playerCount"
|
|
:max-players="tableView.roomState.maxPlayers"
|
|
:round-text="tableView.roundText"
|
|
:room-status-text="tableView.roomStatusText"
|
|
:ws-error="session.wsError"
|
|
:action-countdown="actionCountdown"
|
|
@toggle-menu="session.toggleMenu"
|
|
@toggle-trust-mode="session.toggleTrustMode"
|
|
@leave-room="handleLeaveRoom"
|
|
>
|
|
<template #robot-icon>
|
|
<span class="menu-item-icon">
|
|
<img :src="robotIcon" alt=""/>
|
|
</span>
|
|
</template>
|
|
<template #exit-icon>
|
|
<span class="menu-item-icon">
|
|
<img :src="exitIcon" alt=""/>
|
|
</span>
|
|
</template>
|
|
</ChengduTableHeader>
|
|
|
|
<TopPlayerCard :player="tableView.seatDecor.top"/>
|
|
<RightPlayerCard :player="tableView.seatDecor.right"/>
|
|
<BottomPlayerCard :player="tableView.seatDecor.bottom"/>
|
|
<LeftPlayerCard :player="tableView.seatDecor.left"/>
|
|
|
|
<ChengduDeskZones :desk-seats="tableView.deskSeats"/>
|
|
|
|
<ChengduWallSeats
|
|
:wall-seats="tableView.wallSeats"
|
|
:selected-discard-tile-id="session.selectedDiscardTileId"
|
|
:discard-blocked-reason="discardBlockedReason"
|
|
:discard-tile-blocked-reason="discardTileBlockedReason"
|
|
:format-tile="formatTile"
|
|
@select-discard-tile="selectDiscardTile"
|
|
/>
|
|
|
|
<WindSquare class="center-wind-square" :seat-winds="tableView.seatWinds"
|
|
:active-position="tableView.currentTurnSeat"/>
|
|
|
|
<ChengduSettlementOverlay
|
|
:show="socket.showSettlementOverlay"
|
|
:is-last-round="actions.isLastRound"
|
|
:current-round="gameStore.currentRound"
|
|
:total-rounds="gameStore.totalRounds"
|
|
:settlement-players="tableView.settlementPlayers"
|
|
:logged-in-user-id="session.loggedInUserId"
|
|
:next-round-pending="session.nextRoundPending"
|
|
:settlement-countdown="socket.settlementCountdown"
|
|
@next-round="actions.nextRound"
|
|
@back-hall="session.backHall"
|
|
/>
|
|
|
|
<ChengduBottomActions
|
|
:show-ding-que-chooser="actions.showDingQueChooser"
|
|
:show-ready-toggle="actions.showReadyToggle"
|
|
:show-start-game-button="actions.showStartGameButton"
|
|
:selected-discard-tile="actions.selectedDiscardTile"
|
|
:ding-que-pending="session.dingQuePending"
|
|
:can-confirm-discard="actions.canConfirmDiscard"
|
|
:discard-pending="session.discardPending"
|
|
:confirm-discard-label="actions.confirmDiscardLabel"
|
|
:ready-toggle-pending="session.readyTogglePending"
|
|
:my-ready-state="actions.myReadyState"
|
|
:can-draw-tile="actions.canDrawTile"
|
|
:can-start-game="actions.canStartGame"
|
|
:is-room-owner="actions.isRoomOwner"
|
|
:can-self-gang="actions.canSelfGang"
|
|
:can-self-hu="actions.canSelfHu"
|
|
:show-claim-actions="actions.showClaimActions"
|
|
:turn-action-pending="session.turnActionPending"
|
|
:visible-claim-options="actions.visibleClaimOptions"
|
|
:claim-action-pending="session.claimActionPending"
|
|
:show-waiting-owner-tip="actions.showWaitingOwnerTip"
|
|
@choose-ding-que="actions.chooseDingQue"
|
|
@confirm-discard="actions.confirmDiscard"
|
|
@toggle-ready-state="actions.toggleReadyState"
|
|
@draw-tile="actions.drawTile"
|
|
@start-game="actions.startGame"
|
|
@submit-self-gang="actions.submitSelfGang"
|
|
@submit-self-hu="actions.submitSelfHu"
|
|
@submit-claim="actions.submitClaim"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
</template>
|