feat(game): 添加玩家回合动作权限控制功能
- 在actions.ts中增加available_actions相关字段支持 - 移除未使用的watch导入和相关逻辑 - 新增selfTurnAllowActions响应式变量存储当前回合可执行动作 - 实现settlementOverlayDismissed控制结算弹窗显示状态 - 修改showSettlementOverlay计算属性加入弹窗已关闭条件判断 - 使用canSelfGang计算属性替代原有的concealedGangCandidates逻辑 - 新增readPlayerTurnAllowActions函数解析玩家回合允许的动作列表 - 实现readMissingSuitWithPresence函数增强缺门花色字段检测逻辑 - 更新玩家数据处理逻辑以兼容新的字段结构变化 - 调整游戏阶段映射增加ding_que到playing的转换支持 - 实现resetRoundStateForNextTurn函数重置回合状态 - 更新handlePlayerTurn消息处理逻辑 - 优化nextRound函数逻辑并设置结算弹窗为已关闭状态 - 简化submitSelfGang函数移除传入参数依赖 - 调整UI渲染逻辑适配新的动作权限控制模式
This commit is contained in:
@@ -41,6 +41,9 @@ export interface PlayerTurnPayload {
|
|||||||
allow_actions?: string[]
|
allow_actions?: string[]
|
||||||
allowActions?: string[]
|
allowActions?: string[]
|
||||||
AllowActions?: string[]
|
AllowActions?: string[]
|
||||||
|
available_actions?: string[]
|
||||||
|
availableActions?: string[]
|
||||||
|
AvailableActions?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onBeforeUnmount, onMounted, ref, watch} from 'vue'
|
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import deskImage from '../assets/images/desk/desk_01_1920_945.png'
|
import deskImage from '../assets/images/desk/desk_01_1920_945.png'
|
||||||
import robotIcon from '../assets/images/icons/robot.svg'
|
import robotIcon from '../assets/images/icons/robot.svg'
|
||||||
@@ -97,7 +97,9 @@ const dingQuePending = ref(false)
|
|||||||
const discardPending = ref(false)
|
const discardPending = ref(false)
|
||||||
const claimActionPending = ref(false)
|
const claimActionPending = ref(false)
|
||||||
const turnActionPending = ref(false)
|
const turnActionPending = ref(false)
|
||||||
|
const selfTurnAllowActions = ref<string[]>([])
|
||||||
const nextRoundPending = ref(false)
|
const nextRoundPending = ref(false)
|
||||||
|
const settlementOverlayDismissed = ref(false)
|
||||||
const settlementDeadlineMs = ref<number | null>(null)
|
const settlementDeadlineMs = ref<number | null>(null)
|
||||||
const selectedDiscardTileId = ref<number | null>(null)
|
const selectedDiscardTileId = ref<number | null>(null)
|
||||||
let clockTimer: number | null = null
|
let clockTimer: number | null = null
|
||||||
@@ -332,7 +334,7 @@ const roundText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showSettlementOverlay = computed(() => {
|
const showSettlementOverlay = computed(() => {
|
||||||
return gameStore.phase === 'settlement'
|
return gameStore.phase === 'settlement' && !settlementOverlayDismissed.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const isLastRound = computed(() => {
|
const isLastRound = computed(() => {
|
||||||
@@ -563,63 +565,24 @@ const canSelfHu = computed(() => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasMissingSuitTiles.value) {
|
return selfTurnAllowActions.value.includes('hu')
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSelfGang = computed(() => {
|
||||||
|
const player = myPlayer.value
|
||||||
|
if (!player || !gameStore.roomId || wsStatus.value !== 'connected') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return isWinningHand(player.handTiles)
|
|
||||||
})
|
|
||||||
|
|
||||||
const concealedGangCandidates = computed<Tile[]>(() => {
|
|
||||||
const player = myPlayer.value
|
|
||||||
if (!player || !gameStore.roomId || wsStatus.value !== 'connected') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDingQueChooser.value || gameStore.phase !== 'playing' || gameStore.needDraw || gameStore.pendingClaim) {
|
if (showDingQueChooser.value || gameStore.phase !== 'playing' || gameStore.needDraw || gameStore.pendingClaim) {
|
||||||
return []
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.seatIndex !== gameStore.currentTurn || turnActionPending.value) {
|
if (player.seatIndex !== gameStore.currentTurn || turnActionPending.value) {
|
||||||
return []
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTileByFace = new Map<string, Tile>()
|
return selfTurnAllowActions.value.includes('gang')
|
||||||
const faceCounts = new Map<string, number>()
|
|
||||||
player.handTiles.forEach((tile) => {
|
|
||||||
const key = tileFaceKey(tile)
|
|
||||||
faceCounts.set(key, (faceCounts.get(key) ?? 0) + 1)
|
|
||||||
if (!firstTileByFace.has(key)) {
|
|
||||||
firstTileByFace.set(key, tile)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return Array.from(faceCounts.entries())
|
|
||||||
.filter(([key, count]) => {
|
|
||||||
if (count < 4) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const tile = firstTileByFace.get(key)
|
|
||||||
if (!tile) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !player.missingSuit || tile.suit !== player.missingSuit
|
|
||||||
})
|
|
||||||
.map(([key]) => firstTileByFace.get(key))
|
|
||||||
.filter((tile): tile is Tile => Boolean(tile))
|
|
||||||
.sort((left, right) => {
|
|
||||||
const suitDiff = handSuitOrder[left.suit] - handSuitOrder[right.suit]
|
|
||||||
if (suitDiff !== 0) {
|
|
||||||
return suitDiff
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueDiff = left.value - right.value
|
|
||||||
if (valueDiff !== 0) {
|
|
||||||
return valueDiff
|
|
||||||
}
|
|
||||||
|
|
||||||
return left.id - right.id
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionCountdown = computed(() => {
|
const actionCountdown = computed(() => {
|
||||||
@@ -796,6 +759,25 @@ function readPlayerTurnPlayerId(payload: PlayerTurnPayload): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readPlayerTurnAllowActions(payload: PlayerTurnPayload): string[] {
|
||||||
|
const source =
|
||||||
|
payload.allow_actions ??
|
||||||
|
payload.allowActions ??
|
||||||
|
payload.AllowActions ??
|
||||||
|
payload.available_actions ??
|
||||||
|
payload.availableActions ??
|
||||||
|
payload.AvailableActions
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = source
|
||||||
|
.filter((item): item is string => typeof item === 'string')
|
||||||
|
.map((item) => item.trim().toLowerCase())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
return Array.from(new Set(actions))
|
||||||
|
}
|
||||||
|
|
||||||
function readMissingSuit(source: Record<string, unknown> | null | undefined): string | null {
|
function readMissingSuit(source: Record<string, unknown> | null | undefined): string | null {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return null
|
return null
|
||||||
@@ -804,12 +786,24 @@ function readMissingSuit(source: Record<string, unknown> | null | undefined): st
|
|||||||
return readString(source, 'missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit') || null
|
return readString(source, 'missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit') || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function tileToText(tile: Tile): string {
|
function readMissingSuitWithPresence(
|
||||||
return `${tile.suit}${tile.value}`
|
source: Record<string, unknown> | null | undefined,
|
||||||
|
): { present: boolean, value: string | null } {
|
||||||
|
if (!source) {
|
||||||
|
return { present: false, value: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = ['missing_suit', 'MissingSuit', 'ding_que', 'dingQue', 'suit', 'Suit']
|
||||||
|
const hasMissingSuitField = keys.some((key) => Object.prototype.hasOwnProperty.call(source, key))
|
||||||
|
if (!hasMissingSuitField) {
|
||||||
|
return { present: false, value: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { present: true, value: readMissingSuit(source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
function tileFaceKey(tile: Tile): string {
|
function tileToText(tile: Tile): string {
|
||||||
return `${tile.suit}_${tile.value}`
|
return `${tile.suit}${tile.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTurnActionPending(): void {
|
function clearTurnActionPending(): void {
|
||||||
@@ -830,106 +824,6 @@ function markTurnActionPending(kind: 'gang' | 'hu'): void {
|
|||||||
}, 2500)
|
}, 2500)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFaceKey(key: string): { suit: Tile['suit']; value: number } | null {
|
|
||||||
const [suitRaw, valueRaw] = key.split('_')
|
|
||||||
if ((suitRaw !== 'W' && suitRaw !== 'T' && suitRaw !== 'B') || !valueRaw) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = Number(valueRaw)
|
|
||||||
if (!Number.isInteger(value)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
suit: suitRaw,
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canFormWinningMelds(counts: Map<string, number>): boolean {
|
|
||||||
const currentKey = Array.from(counts.keys()).sort().find((key) => (counts.get(key) ?? 0) > 0)
|
|
||||||
if (!currentKey) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentCount = counts.get(currentKey) ?? 0
|
|
||||||
if (currentCount >= 3) {
|
|
||||||
counts.set(currentKey, currentCount - 3)
|
|
||||||
if (canFormWinningMelds(counts)) {
|
|
||||||
counts.set(currentKey, currentCount)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
counts.set(currentKey, currentCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseFaceKey(currentKey)
|
|
||||||
if (!parsed || parsed.value > 7) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondKey = `${parsed.suit}_${parsed.value + 1}`
|
|
||||||
const thirdKey = `${parsed.suit}_${parsed.value + 2}`
|
|
||||||
const secondCount = counts.get(secondKey) ?? 0
|
|
||||||
const thirdCount = counts.get(thirdKey) ?? 0
|
|
||||||
if (secondCount <= 0 || thirdCount <= 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
counts.set(currentKey, currentCount - 1)
|
|
||||||
counts.set(secondKey, secondCount - 1)
|
|
||||||
counts.set(thirdKey, thirdCount - 1)
|
|
||||||
const success = canFormWinningMelds(counts)
|
|
||||||
counts.set(currentKey, currentCount)
|
|
||||||
counts.set(secondKey, secondCount)
|
|
||||||
counts.set(thirdKey, thirdCount)
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWinningHand(tiles: Tile[]): boolean {
|
|
||||||
if (tiles.length % 3 !== 2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const counts = new Map<string, number>()
|
|
||||||
tiles.forEach((tile) => {
|
|
||||||
const key = tileFaceKey(tile)
|
|
||||||
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (tiles.length === 14) {
|
|
||||||
let pairCount = 0
|
|
||||||
let validQiDui = true
|
|
||||||
for (const count of counts.values()) {
|
|
||||||
if (count === 2) {
|
|
||||||
pairCount += 1
|
|
||||||
} else if (count === 4) {
|
|
||||||
pairCount += 2
|
|
||||||
} else {
|
|
||||||
validQiDui = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (validQiDui && pairCount === 7) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, count] of counts.entries()) {
|
|
||||||
if (count < 2) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
counts.set(key, count - 2)
|
|
||||||
if (canFormWinningMelds(counts)) {
|
|
||||||
counts.set(key, count)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
counts.set(key, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTile(tile: unknown): Tile | null {
|
function normalizeTile(tile: unknown): Tile | null {
|
||||||
const source = asRecord(tile)
|
const source = asRecord(tile)
|
||||||
if (!source) {
|
if (!source) {
|
||||||
@@ -1177,7 +1071,7 @@ function handleRoomStateResponse(message: unknown): void {
|
|||||||
player.claims,
|
player.claims,
|
||||||
)
|
)
|
||||||
const hasHu = Boolean(player.has_hu ?? player.hasHu)
|
const hasHu = Boolean(player.has_hu ?? player.hasHu)
|
||||||
const dingQue = readString(player, 'ding_que', 'dingQue')
|
const dingQue = readMissingSuitWithPresence(player)
|
||||||
const scores = asRecord(payload.scores)
|
const scores = asRecord(payload.scores)
|
||||||
const score = scores?.[playerId]
|
const score = scores?.[playerId]
|
||||||
|
|
||||||
@@ -1187,7 +1081,7 @@ function handleRoomStateResponse(message: unknown): void {
|
|||||||
displayName: previous?.displayName ?? playerId,
|
displayName: previous?.displayName ?? playerId,
|
||||||
avatarURL: previous?.avatarURL,
|
avatarURL: previous?.avatarURL,
|
||||||
isTrustee: previous?.isTrustee ?? false,
|
isTrustee: previous?.isTrustee ?? false,
|
||||||
missingSuit: dingQue || previous?.missingSuit || null,
|
missingSuit: dingQue.present ? dingQue.value : (previous?.missingSuit ?? null),
|
||||||
handTiles: previous?.handTiles ?? [],
|
handTiles: previous?.handTiles ?? [],
|
||||||
handCount,
|
handCount,
|
||||||
melds,
|
melds,
|
||||||
@@ -1211,6 +1105,7 @@ function handleRoomStateResponse(message: unknown): void {
|
|||||||
const phaseMap: Record<string, typeof gameStore.phase> = {
|
const phaseMap: Record<string, typeof gameStore.phase> = {
|
||||||
waiting: 'waiting',
|
waiting: 'waiting',
|
||||||
dealing: 'dealing',
|
dealing: 'dealing',
|
||||||
|
ding_que: 'playing',
|
||||||
playing: 'playing',
|
playing: 'playing',
|
||||||
action: 'action',
|
action: 'action',
|
||||||
settlement: 'settlement',
|
settlement: 'settlement',
|
||||||
@@ -1310,8 +1205,12 @@ function handleRoomStateResponse(message: unknown): void {
|
|||||||
}
|
}
|
||||||
if (phase !== 'settlement') {
|
if (phase !== 'settlement') {
|
||||||
nextRoundPending.value = false
|
nextRoundPending.value = false
|
||||||
|
settlementOverlayDismissed.value = false
|
||||||
settlementDeadlineMs.value = null
|
settlementDeadlineMs.value = null
|
||||||
}
|
}
|
||||||
|
if (phase !== 'playing' || currentTurnPlayerId !== loggedInUserId.value) {
|
||||||
|
selfTurnAllowActions.value = []
|
||||||
|
}
|
||||||
if (currentTurnPlayerId && currentTurnPlayerId !== loggedInUserId.value) {
|
if (currentTurnPlayerId && currentTurnPlayerId !== loggedInUserId.value) {
|
||||||
markDiscardCompleted()
|
markDiscardCompleted()
|
||||||
}
|
}
|
||||||
@@ -1450,7 +1349,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ??
|
readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ??
|
||||||
fallbackIndex
|
fallbackIndex
|
||||||
const displayName = existing?.gamePlayer.displayName || (playerId === loggedInUserId.value ? loggedInUserName.value : '')
|
const displayName = existing?.gamePlayer.displayName || (playerId === loggedInUserId.value ? loggedInUserName.value : '')
|
||||||
const missingSuit = readMissingSuit(player) || existing?.gamePlayer.missingSuit || null
|
const missingSuit = readMissingSuitWithPresence(player)
|
||||||
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
||||||
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
||||||
const melds = normalizeMelds(
|
const melds = normalizeMelds(
|
||||||
@@ -1466,7 +1365,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
index: seatIndex,
|
index: seatIndex,
|
||||||
playerId,
|
playerId,
|
||||||
displayName: displayName || undefined,
|
displayName: displayName || undefined,
|
||||||
missingSuit,
|
missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
|
||||||
ready: existing?.roomPlayer.ready ?? false,
|
ready: existing?.roomPlayer.ready ?? false,
|
||||||
trustee: existing?.roomPlayer.trustee ?? false,
|
trustee: existing?.roomPlayer.trustee ?? false,
|
||||||
hand: Array.from({length: handCount}, () => ''),
|
hand: Array.from({length: handCount}, () => ''),
|
||||||
@@ -1479,7 +1378,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
seatIndex,
|
seatIndex,
|
||||||
displayName: displayName || undefined,
|
displayName: displayName || undefined,
|
||||||
avatarURL: existing?.gamePlayer.avatarURL,
|
avatarURL: existing?.gamePlayer.avatarURL,
|
||||||
missingSuit,
|
missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
|
||||||
isReady: existing?.gamePlayer.isReady ?? false,
|
isReady: existing?.gamePlayer.isReady ?? false,
|
||||||
isTrustee: existing?.gamePlayer.isTrustee ?? false,
|
isTrustee: existing?.gamePlayer.isTrustee ?? false,
|
||||||
handTiles: existing?.gamePlayer.handTiles ?? [],
|
handTiles: existing?.gamePlayer.handTiles ?? [],
|
||||||
@@ -1496,12 +1395,16 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
if (loggedInUserId.value && playerMap.has(loggedInUserId.value)) {
|
if (loggedInUserId.value && playerMap.has(loggedInUserId.value)) {
|
||||||
const current = playerMap.get(loggedInUserId.value)
|
const current = playerMap.get(loggedInUserId.value)
|
||||||
if (current) {
|
if (current) {
|
||||||
const selfMissingSuit = readMissingSuit(playerView)
|
const selfMissingSuit = readMissingSuitWithPresence(playerView)
|
||||||
current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
|
current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
|
||||||
current.roomPlayer.missingSuit = selfMissingSuit || current.roomPlayer.missingSuit
|
if (selfMissingSuit.present) {
|
||||||
|
current.roomPlayer.missingSuit = selfMissingSuit.value
|
||||||
|
}
|
||||||
current.gamePlayer.handTiles = privateHand
|
current.gamePlayer.handTiles = privateHand
|
||||||
current.gamePlayer.handCount = privateHand.length
|
current.gamePlayer.handCount = privateHand.length
|
||||||
current.gamePlayer.missingSuit = selfMissingSuit || current.gamePlayer.missingSuit
|
if (selfMissingSuit.present) {
|
||||||
|
current.gamePlayer.missingSuit = selfMissingSuit.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1519,7 +1422,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
seatIndex: gamePlayer.seatIndex,
|
seatIndex: gamePlayer.seatIndex,
|
||||||
displayName: gamePlayer.displayName ?? previous?.displayName,
|
displayName: gamePlayer.displayName ?? previous?.displayName,
|
||||||
avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
|
avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
|
||||||
missingSuit: gamePlayer.missingSuit ?? previous?.missingSuit,
|
missingSuit: typeof gamePlayer.missingSuit === 'undefined' ? (previous?.missingSuit ?? null) : gamePlayer.missingSuit,
|
||||||
isTrustee: previous?.isTrustee ?? gamePlayer.isTrustee,
|
isTrustee: previous?.isTrustee ?? gamePlayer.isTrustee,
|
||||||
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
||||||
handCount: gamePlayer.handCount > 0
|
handCount: gamePlayer.handCount > 0
|
||||||
@@ -1563,6 +1466,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
const phaseMap: Record<string, typeof gameStore.phase> = {
|
const phaseMap: Record<string, typeof gameStore.phase> = {
|
||||||
waiting: 'waiting',
|
waiting: 'waiting',
|
||||||
dealing: 'dealing',
|
dealing: 'dealing',
|
||||||
|
ding_que: 'playing',
|
||||||
playing: 'playing',
|
playing: 'playing',
|
||||||
action: 'action',
|
action: 'action',
|
||||||
settlement: 'settlement',
|
settlement: 'settlement',
|
||||||
@@ -1603,6 +1507,9 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
if (typeof infoSdMs === 'number' && infoSdMs > 0) {
|
if (typeof infoSdMs === 'number' && infoSdMs > 0) {
|
||||||
settlementDeadlineMs.value = infoSdMs
|
settlementDeadlineMs.value = infoSdMs
|
||||||
}
|
}
|
||||||
|
if (gameStore.phase !== 'playing' || currentTurnPlayerId !== loggedInUserId.value) {
|
||||||
|
selfTurnAllowActions.value = []
|
||||||
|
}
|
||||||
|
|
||||||
setActiveRoom({
|
setActiveRoom({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -2039,13 +1946,55 @@ function applyPlayerTurnCountdown(payload: PlayerTurnPayload): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetRoundStateForNextTurn(payload: Record<string, unknown>): void {
|
||||||
|
const nextRound = readNumber(payload, 'current_round', 'currentRound')
|
||||||
|
const totalRounds = readNumber(payload, 'total_rounds', 'totalRounds')
|
||||||
|
if (typeof nextRound !== 'number' && typeof totalRounds !== 'number') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof nextRound === 'number') {
|
||||||
|
gameStore.currentRound = nextRound
|
||||||
|
}
|
||||||
|
if (typeof totalRounds === 'number') {
|
||||||
|
gameStore.totalRounds = totalRounds
|
||||||
|
}
|
||||||
|
|
||||||
|
nextRoundPending.value = false
|
||||||
|
settlementOverlayDismissed.value = false
|
||||||
|
settlementDeadlineMs.value = null
|
||||||
|
dingQuePending.value = false
|
||||||
|
roomCountdown.value = null
|
||||||
|
claimActionPending.value = false
|
||||||
|
selfTurnAllowActions.value = []
|
||||||
|
gameStore.pendingClaim = undefined
|
||||||
|
gameStore.winners = []
|
||||||
|
|
||||||
|
markDiscardCompleted()
|
||||||
|
clearTurnActionPending()
|
||||||
|
|
||||||
|
Object.values(gameStore.players).forEach((player) => {
|
||||||
|
player.missingSuit = null
|
||||||
|
player.hasHu = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const room = activeRoom.value
|
||||||
|
if (room && room.roomId === gameStore.roomId) {
|
||||||
|
room.players.forEach((player) => {
|
||||||
|
player.missingSuit = null
|
||||||
|
player.hasHu = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handlePlayerTurn(message: unknown): void {
|
function handlePlayerTurn(message: unknown): void {
|
||||||
const source = asRecord(message)
|
const source = asRecord(message)
|
||||||
if (!source || typeof source.type !== 'string') {
|
if (!source || typeof source.type !== 'string') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizeWsType(source.type) !== 'PLAYER_TURN') {
|
const normalizedType = normalizeWsType(source.type)
|
||||||
|
if (normalizedType !== 'PLAYER_TURN' && normalizedType !== 'NEXT_TURN') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2057,7 +2006,17 @@ function handlePlayerTurn(message: unknown): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPlayerTurnCountdown(payload as PlayerTurnPayload)
|
resetRoundStateForNextTurn(payload)
|
||||||
|
const turnPayload = payload as PlayerTurnPayload
|
||||||
|
const turnPlayerID = readPlayerTurnPlayerId(turnPayload)
|
||||||
|
if (turnPlayerID && turnPlayerID === loggedInUserId.value) {
|
||||||
|
selfTurnAllowActions.value = readPlayerTurnAllowActions(turnPayload)
|
||||||
|
} else {
|
||||||
|
selfTurnAllowActions.value = []
|
||||||
|
}
|
||||||
|
if (normalizedType === 'PLAYER_TURN') {
|
||||||
|
applyPlayerTurnCountdown(turnPayload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleActionError(message: unknown): void {
|
function handleActionError(message: unknown): void {
|
||||||
@@ -2169,6 +2128,7 @@ function toGameAction(message: unknown): GameAction | null {
|
|||||||
payload: source as unknown as GameActionPayload<'ROOM_TRUSTEE'>,
|
payload: source as unknown as GameActionPayload<'ROOM_TRUSTEE'>,
|
||||||
}
|
}
|
||||||
case 'PLAYER_TURN':
|
case 'PLAYER_TURN':
|
||||||
|
case 'NEXT_TURN':
|
||||||
if (payload && typeof payload === 'object') {
|
if (payload && typeof payload === 'object') {
|
||||||
return {type: 'PLAYER_TURN', payload: payload as GameActionPayload<'PLAYER_TURN'>}
|
return {type: 'PLAYER_TURN', payload: payload as GameActionPayload<'PLAYER_TURN'>}
|
||||||
}
|
}
|
||||||
@@ -2535,10 +2495,11 @@ function startGame(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextRound(): void {
|
function nextRound(): void {
|
||||||
if (nextRoundPending.value) {
|
if (nextRoundPending.value || !gameStore.roomId || gameStore.phase !== 'settlement') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settlementOverlayDismissed.value = true
|
||||||
nextRoundPending.value = true
|
nextRoundPending.value = true
|
||||||
sendWsMessage({
|
sendWsMessage({
|
||||||
type: 'next_round',
|
type: 'next_round',
|
||||||
@@ -2642,8 +2603,8 @@ function drawTile(): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitConcealedGang(tile: Tile): void {
|
function submitSelfGang(): void {
|
||||||
if (!gameStore.roomId || turnActionPending.value) {
|
if (!gameStore.roomId || !canSelfGang.value || turnActionPending.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2653,11 +2614,6 @@ function submitConcealedGang(tile: Tile): void {
|
|||||||
roomId: gameStore.roomId,
|
roomId: gameStore.roomId,
|
||||||
payload: {
|
payload: {
|
||||||
room_id: gameStore.roomId,
|
room_id: gameStore.roomId,
|
||||||
tile: {
|
|
||||||
id: tile.id,
|
|
||||||
suit: tile.suit,
|
|
||||||
value: tile.value,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2758,7 +2714,7 @@ function hydrateFromActiveRoom(routeRoomId: string): void {
|
|||||||
seatIndex: player.index,
|
seatIndex: player.index,
|
||||||
displayName: player.displayName || player.playerId,
|
displayName: player.displayName || player.playerId,
|
||||||
avatarURL: previous?.avatarURL,
|
avatarURL: previous?.avatarURL,
|
||||||
missingSuit: player.missingSuit ?? previous?.missingSuit,
|
missingSuit: typeof player.missingSuit === 'undefined' ? (previous?.missingSuit ?? null) : player.missingSuit,
|
||||||
isTrustee: player.trustee ?? previous?.isTrustee ?? false,
|
isTrustee: player.trustee ?? previous?.isTrustee ?? false,
|
||||||
isReady: player.ready,
|
isReady: player.ready,
|
||||||
handTiles: previous?.handTiles ?? [],
|
handTiles: previous?.handTiles ?? [],
|
||||||
@@ -2773,12 +2729,6 @@ function hydrateFromActiveRoom(routeRoomId: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
watch(settlementCountdown, (remaining) => {
|
|
||||||
if (remaining !== null && remaining <= 0 && !nextRoundPending.value && !isLastRound.value) {
|
|
||||||
nextRound()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
||||||
needsInitialRoomInfo = true
|
needsInitialRoomInfo = true
|
||||||
@@ -3279,17 +3229,15 @@ onBeforeUnmount(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="concealedGangCandidates.length > 0" class="hand-action-bar">
|
<div v-if="canSelfGang" class="hand-action-bar">
|
||||||
<button
|
<button
|
||||||
v-for="tile in concealedGangCandidates"
|
|
||||||
:key="`gang-${tile.id}`"
|
|
||||||
class="hand-action-tile"
|
class="hand-action-tile"
|
||||||
:data-testid="`hand-gang-${tile.suit}-${tile.value}`"
|
data-testid="hand-gang"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="turnActionPending"
|
:disabled="turnActionPending"
|
||||||
@click="submitConcealedGang(tile)"
|
@click="submitSelfGang"
|
||||||
>
|
>
|
||||||
杠 {{ formatTile(tile) }}
|
杠
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user