diff --git a/src/assets/images/desk/desk_01.png b/src/assets/images/desk/desk_01_1920_1080.png similarity index 100% rename from src/assets/images/desk/desk_01.png rename to src/assets/images/desk/desk_01_1920_1080.png diff --git a/src/assets/images/desk/desk_01_1920_945.png b/src/assets/images/desk/desk_01_1920_945.png new file mode 100644 index 0000000..801b68e Binary files /dev/null and b/src/assets/images/desk/desk_01_1920_945.png differ diff --git a/src/assets/styles/room.css b/src/assets/styles/room.css index f2f3f28..1afd81f 100644 --- a/src/assets/styles/room.css +++ b/src/assets/styles/room.css @@ -1,8 +1,7 @@ .picture-scene { - min-height: 100vh; min-height: 100dvh; - padding: 18px; + padding: 0; background: radial-gradient(circle at top, rgba(116, 58, 41, 0.28), transparent 20%), linear-gradient(180deg, #3f2119 0%, #27140f 100%); @@ -10,44 +9,51 @@ .picture-layout { display: grid; - grid-template-columns: minmax(0, 1fr) 320px; - gap: 18px; + grid-template-columns: minmax(0, 1fr); + gap: 0; align-items: stretch; - min-height: calc(100vh - 36px); + min-height: 100vh; + min-height: 100dvh; } -.table-stage { +.picture-scene .table-stage { position: relative; display: grid; place-items: center; - align-content: start; + align-content: stretch; width: 100%; - min-height: calc(100vh - 36px); + min-height: 100vh; + min-height: 100dvh; + overflow: hidden; } -.table-desk, -.table-felt { - width: 100%; - max-width: 100%; - max-height: calc(100dvh - 72px); - aspect-ratio: 16 / 9; -} - -.table-desk { - grid-area: 1 / 1; +.picture-scene .table-desk { + position: absolute; + inset: 0; display: block; - margin-top: 18px; - border-radius: 26px; + width: 100%; + height: 100%; object-fit: cover; + object-position: center; + border-radius: 0; box-shadow: 0 24px 44px rgba(0, 0, 0, 0.34); } -.table-felt { - grid-area: 1 / 1; - position: relative; - margin-top: 18px; - border-radius: 26px; +.picture-scene .table-felt { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + max-width: none; + max-height: none; + min-height: 100vh; + min-height: 100dvh; + aspect-ratio: auto; + margin-top: 0; + border-radius: 0; overflow: hidden; + justify-self: stretch; + align-self: stretch; } .table-surface { @@ -312,10 +318,57 @@ z-index: 5; } -.action-countdown { +.room-status-panel { position: absolute; top: 92px; right: 40px; + width: min(320px, calc(100% - 80px)); + padding: 12px; + border: 1px solid rgba(255, 226, 175, 0.12); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(45, 24, 18, 0.82), rgba(26, 14, 11, 0.88)), + radial-gradient(circle at top, rgba(255, 219, 154, 0.05), transparent 44%); + box-shadow: 0 14px 26px rgba(0, 0, 0, 0.22); + z-index: 5; +} + +.room-status-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.room-status-item { + padding: 10px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); +} + +.room-status-item span { + display: block; + font-size: 11px; + color: rgba(244, 233, 208, 0.62); +} + +.room-status-item strong { + display: block; + margin-top: 6px; + color: #fff0c2; + font-size: 15px; + word-break: break-word; +} + +.room-status-error { + margin-top: 10px; + color: #ffc1c1; + font-size: 13px; +} + +.action-countdown { + position: absolute; + top: 210px; + right: 40px; min-width: 188px; padding: 10px 12px; border: 1px solid rgba(255, 219, 131, 0.22); @@ -713,6 +766,8 @@ background: transparent; appearance: none; cursor: pointer; + transform: translateY(0); + transition: transform 150ms cubic-bezier(0.22, 0.82, 0.32, 1), filter 150ms ease-out; } .wall-live-tile-lack-tag { @@ -738,6 +793,21 @@ opacity: 1; } +.wall-live-tile-button:not(:disabled):hover { + transform: translateY(-4px); +} + +.wall-live-tile-button.is-selected { + transform: translateY(-18px); + z-index: 3; +} + +.wall-live-tile-button.is-selected .wall-live-tile { + filter: + drop-shadow(0 14px 18px rgba(0, 0, 0, 0.24)) + drop-shadow(0 0 10px rgba(255, 214, 111, 0.42)); +} + .wall-live-tile-button:disabled .wall-live-tile { opacity: 1; filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.18)); @@ -1142,6 +1212,18 @@ margin-bottom: 10px; } +.discard-confirm-button { + min-width: 168px; + background: + linear-gradient(180deg, rgba(110, 32, 20, 0.94), rgba(72, 16, 9, 0.98)), + radial-gradient(circle at 20% 24%, rgba(255, 214, 153, 0.14), transparent 34%); + border-color: rgba(255, 184, 112, 0.34); + box-shadow: + inset 0 1px 0 rgba(255, 232, 205, 0.14), + inset 0 -1px 0 rgba(0, 0, 0, 0.28), + 0 12px 22px rgba(0, 0, 0, 0.26); +} + .hand-action-bar { display: flex; flex-wrap: wrap; @@ -1357,126 +1439,9 @@ background: radial-gradient(circle at 35% 28%, #fff6c2 0%, #ffe16c 42%, #e3aa23 100%); } -.ws-sidebar { - display: flex; - flex-direction: column; - height: calc(100vh - 36px); - min-height: calc(100vh - 36px); - padding: 16px; - border-radius: 18px; - border: 1px solid rgba(255, 226, 175, 0.12); - background: - linear-gradient(180deg, rgba(45, 24, 18, 0.94), rgba(26, 14, 11, 0.96)), - radial-gradient(circle at top, rgba(255, 219, 154, 0.06), transparent 40%); - box-shadow: 0 16px 28px rgba(0, 0, 0, 0.22); -} - -.sidebar-head { - display: flex; - justify-content: space-between; - gap: 12px; - align-items: flex-start; -} - -.sidebar-title { - font-size: 18px; - font-weight: 800; - color: #ffe2a0; -} - -.sidebar-head small { - color: rgba(248, 233, 199, 0.68); -} - -.sidebar-btn { - min-width: 76px; - height: 38px; - border: 1px solid rgba(255, 223, 164, 0.16); - border-radius: 999px; - color: #ffe9b7; - background: rgba(0, 0, 0, 0.18); -} - -.sidebar-stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; - margin-top: 16px; -} - -.sidebar-stat { - padding: 12px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.04); -} - -.sidebar-stat span { - display: block; - font-size: 11px; - color: rgba(244, 233, 208, 0.62); -} - -.sidebar-stat strong { - display: block; - margin-top: 6px; - color: #fff0c2; - font-size: 15px; - word-break: break-word; -} - -.sidebar-error { - margin-top: 14px; - color: #ffc1c1; - font-size: 13px; -} - -.sidebar-log { - flex: 1 1 auto; - margin-top: 14px; - padding: 12px; - border-radius: 14px; - background: rgba(9, 12, 19, 0.34); - overflow: auto; -} - -.sidebar-empty, -.sidebar-line { - font-size: 12px; - color: #e6eef8; - line-height: 1.5; -} - -.sidebar-line + .sidebar-line { - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.06); -} - -@media (max-width: 1280px) { - .picture-layout { - grid-template-columns: 1fr; - } - - .table-desk, - .table-felt { - width: min(100%, calc((100dvh - 290px) * 16 / 9)); - } - - .ws-sidebar { - height: auto; - min-height: 240px; - } -} - @media (max-width: 980px) { .picture-scene { - padding: 10px; - } - - .table-desk, - .table-felt { - width: 100%; - margin-top: 8px; + padding: 0; } .wall-right { @@ -1529,14 +1494,15 @@ right: 20px; min-width: 164px; } + + .room-status-panel { + top: 92px; + right: 20px; + width: min(300px, calc(100% - 40px)); + } } @media (max-width: 640px) { - .table-desk, - .table-felt { - aspect-ratio: 9 / 16; - } - .inner-outline.mid { inset: 92px 34px 190px; } @@ -1596,8 +1562,23 @@ padding: 7px 10px; } + .room-status-panel { + top: 58px; + right: 16px; + width: calc(100% - 32px); + padding: 10px; + } + + .room-status-grid { + gap: 8px; + } + + .room-status-item { + padding: 8px 10px; + } + .action-countdown { - top: 62px; + top: 176px; right: 16px; min-width: 0; width: calc(100% - 32px); diff --git a/src/assets/styles/windowSquare.css b/src/assets/styles/windowSquare.css new file mode 100644 index 0000000..229111a --- /dev/null +++ b/src/assets/styles/windowSquare.css @@ -0,0 +1,151 @@ +.wind-square { + position: relative; + width: 96px; + height: 96px; + border-radius: 22px; + overflow: hidden; + box-shadow: 0 10px 18px rgba(0, 0, 0, 0.28), + inset 0 0 0 1px rgba(255, 240, 196, 0.2); +} + +.square-base { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + filter: sepia(1) hue-rotate(92deg) saturate(3.3) brightness(0.22); +} + +.wind-square::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 28% 22%, rgba(255, 238, 191, 0.08), transparent 42%), + linear-gradient(145deg, rgba(5, 33, 24, 0.34), rgba(0, 0, 0, 0.16)); + pointer-events: none; + z-index: 0; +} + +/* ===== 四个三角形区域 ===== */ +.quadrant { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + z-index: 1; + transition: opacity 0.2s ease; +} + +/* 上三角 */ +.quadrant-top { + clip-path: polygon(50% 50%, 0 0, 100% 0); + background: radial-gradient(circle at 50% 38%, rgba(255, 225, 180, 0.30), transparent 68%), + linear-gradient(to bottom, rgba(180, 95, 55, 0.28), rgba(80, 35, 20, 0.12)); +} + +/* 右三角 */ +.quadrant-right { + clip-path: polygon(50% 50%, 100% 0, 100% 100%); + background: radial-gradient(circle at 62% 50%, rgba(255, 225, 180, 0.30), transparent 68%), + linear-gradient(to left, rgba(180, 95, 55, 0.28), rgba(80, 35, 20, 0.12)); +} + +/* 下三角 */ +.quadrant-bottom { + clip-path: polygon(50% 50%, 0 100%, 100% 100%); + background: radial-gradient(circle at 50% 62%, rgba(255, 225, 180, 0.30), transparent 68%), + linear-gradient(to top, rgba(180, 95, 55, 0.28), rgba(80, 35, 20, 0.12)); +} + +/* 左三角 */ +.quadrant-left { + clip-path: polygon(50% 50%, 0 0, 0 100%); + background: radial-gradient(circle at 38% 50%, rgba(255, 225, 180, 0.30), transparent 68%), + linear-gradient(to right, rgba(180, 95, 55, 0.28), rgba(80, 35, 20, 0.12)); +} + +/* 激活时闪烁 */ +.quadrant.active { + opacity: 1; + animation: quadrant-pulse 1.2s ease-in-out infinite; +} + +@keyframes quadrant-pulse { + 0% { + opacity: 0.22; + filter: brightness(0.95); + } + 50% { + opacity: 0.72; + filter: brightness(1.18); + } + 100% { + opacity: 0.22; + filter: brightness(0.95); + } +} + +.diagonal { + position: absolute; + left: 50%; + top: 50%; + width: 160%; + height: 2px; + border-radius: 999px; + background: linear-gradient( + 90deg, + rgba(0, 0, 0, 0) 0%, + rgba(80, 35, 20, 0.6) 25%, + rgba(160, 85, 50, 0.9) 50%, + rgba(80, 35, 20, 0.6) 75%, + rgba(0, 0, 0, 0) 100% + ); + transform-origin: center; + z-index: 2; +} + +.diagonal-a { + transform: translate(-50%, -50%) rotate(45deg); +} + +.diagonal-b { + transform: translate(-50%, -50%) rotate(-45deg); +} + +.wind-slot { + position: absolute; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + z-index: 3; +} + +.wind-icon { + width: 100%; + height: 100%; + object-fit: contain; + filter: brightness(0) invert(1) drop-shadow(0 0 2px rgba(255, 220, 180, 0.8)) drop-shadow(0 0 4px rgba(120, 60, 30, 0.6)); +} + +.wind-top { + top: 5px; + left: 34px; +} + +.wind-right { + top: 34px; + right: 5px; +} + +.wind-bottom { + bottom: 5px; + left: 34px; +} + +.wind-left { + top: 34px; + left: 5px; +} \ No newline at end of file diff --git a/src/components/game/WindSquare.vue b/src/components/game/WindSquare.vue index 3e9efa9..c33fc27 100644 --- a/src/components/game/WindSquare.vue +++ b/src/components/game/WindSquare.vue @@ -1,5 +1,6 @@ - - + \ No newline at end of file diff --git a/src/config/deskImageMap.ts b/src/config/deskImageMap.ts new file mode 100644 index 0000000..85c085f --- /dev/null +++ b/src/config/deskImageMap.ts @@ -0,0 +1,24 @@ +// src/config/deskImageMap.ts + +export interface DeskAsset { + width: number + height: number + ratio: number + src: string +} + +// 所有桌面资源 +export const DESK_ASSETS: DeskAsset[] = [ + { + width: 1920, + height: 1080, + ratio: 1920 / 1080, + src: new URL('@/assets/images/desk/desk_01_1920_1080.png', import.meta.url).href, + }, + { + width: 1920, + height: 945, + ratio: 1920 / 945, + src: new URL('@/assets/images/desk/desk_01_1920_945.png', import.meta.url).href, + }, +] \ No newline at end of file diff --git a/src/game/actions.ts b/src/game/actions.ts index f4c0e04..0ce3677 100644 --- a/src/game/actions.ts +++ b/src/game/actions.ts @@ -29,6 +29,20 @@ export interface RoomTrusteePayload { reason?: string } +export interface PlayerTurnPayload { + player_id?: string + playerId?: string + PlayerID?: string + timeout?: number + Timeout?: number + start_at?: number + startAt?: number + StartAt?: number + allow_actions?: string[] + allowActions?: string[] + AllowActions?: string[] +} + /** * 游戏动作定义(只描述“发生了什么”) @@ -92,3 +106,8 @@ export type GameAction = type: 'ROOM_TRUSTEE' payload: RoomTrusteePayload } + + | { + type: 'PLAYER_TURN' + payload: PlayerTurnPayload +} diff --git a/src/game/dispatcher.ts b/src/game/dispatcher.ts index 1dfeb24..d59e837 100644 --- a/src/game/dispatcher.ts +++ b/src/game/dispatcher.ts @@ -38,6 +38,10 @@ export function dispatchGameAction(action: GameAction) { store.onRoomTrustee(action.payload) break + case 'PLAYER_TURN': + store.onPlayerTurn(action.payload) + break + default: throw new Error('Invalid game action') diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 4df6903..6c7db13 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -4,7 +4,8 @@ import { type GameState, type PendingClaimState, } from '../types/state' -import type { RoomPlayerUpdatePayload, RoomTrusteePayload } from '../game/actions' +import type { PlayerTurnPayload, RoomPlayerUpdatePayload, RoomTrusteePayload } from '../game/actions' +import { readStoredAuth } from '../utils/auth-storage' import type { Tile } from '../types/tile' @@ -16,6 +17,7 @@ export const useGameStore = defineStore('game', { dealerIndex: 0, currentTurn: 0, + currentPlayerId: '', needDraw: false, players: {}, @@ -34,7 +36,7 @@ export const useGameStore = defineStore('game', { this.$reset() }, - // 初始化 + // 初始�? initGame(data: GameState) { Object.assign(this, data) }, @@ -53,8 +55,9 @@ export const useGameStore = defineStore('game', { // 剩余牌数减少 this.remainingTiles = Math.max(0, this.remainingTiles - 1) - // 更新回合(seatIndex) + // 更新回合(seatIndex�? this.currentTurn = player.seatIndex + this.currentPlayerId = player.playerId // 清除操作窗口 this.pendingClaim = undefined @@ -84,7 +87,7 @@ export const useGameStore = defineStore('game', { } player.handCount = Math.max(0, player.handCount - 1) - // 加入出牌区 + // 加入出牌�? player.discardTiles.push(data.tile) // 更新回合 @@ -95,7 +98,7 @@ export const useGameStore = defineStore('game', { this.phase = GAME_PHASE.ACTION }, - // 触发操作窗口(碰/杠/胡) + // 触发操作窗口(碰/�?胡) onPendingClaim(data: PendingClaimState) { this.pendingClaim = data this.needDraw = false @@ -220,14 +223,49 @@ export const useGameStore = defineStore('game', { }, // 清理操作窗口 + onPlayerTurn(payload: PlayerTurnPayload) { + const playerId = + (typeof payload.player_id === 'string' && payload.player_id) || + (typeof payload.playerId === 'string' && payload.playerId) || + (typeof payload.PlayerID === 'string' && payload.PlayerID) || + '' + if (!playerId) { + return + } + + const player = this.players[playerId] + if (player) { + this.currentTurn = player.seatIndex + } + this.currentPlayerId = playerId + + this.needDraw = false + this.pendingClaim = undefined + this.phase = GAME_PHASE.PLAYING + }, + clearPendingClaim() { this.pendingClaim = undefined this.phase = GAME_PHASE.PLAYING }, - // 获取当前玩家ID(后续建议放到 userStore) + // 获取当前玩家ID(后续建议放�?userStore�? getMyPlayerId(): string { - return Object.keys(this.players)[0] || '' + const auth = readStoredAuth() + const source = auth?.user as Record | undefined + const rawId = + source?.id ?? + source?.userID ?? + source?.user_id + if (typeof rawId === 'string' && rawId.trim()) { + return rawId + } + if (typeof rawId === 'number') { + return String(rawId) + } + return '' }, }, }) + + diff --git a/src/types/state/gameState.ts b/src/types/state/gameState.ts index d968587..340280e 100644 --- a/src/types/state/gameState.ts +++ b/src/types/state/gameState.ts @@ -13,6 +13,8 @@ export interface GameState { // 当前操作玩家(座位) currentTurn: number + // 当前操作玩家ID + currentPlayerId: string // 当前回合是否需要先摸牌 needDraw: boolean diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index 57c1503..7c2f2d0 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -1,7 +1,7 @@