feat(tiles): 实现麻将牌图像系统并优化游戏界面显示

- 重命名 tileMap.ts 为 bottomTileMap.ts 并扩展支持字牌(东南西北、中发白)
- 新增 leftTileMap.ts、rightTileMap.ts 和 topTileMap.ts 支持多位置牌面渲染
- 实现牌面图像类型区分(手牌、明牌、盖牌)和动态图像键构建
- 添加牌面验证函数支持不同花色的数值范围检查
- 更新 ChengduGamePage.vue 使用新的底部牌面配置文件
- 实现玩家手牌可见性控制仅在非等待阶段显示
- 重构服务器响应解析逻辑适配新的数据结构
- 添加玩家手牌响应处理器实时更新手牌状态
- 将玩家手牌显示从文本改为图像展示提升用户体验
- 重构CSS样式实现牌面图像的响应式布局和阴影效果
This commit is contained in:
2026-03-27 15:34:59 +08:00
parent b1e394d675
commit fd8f6d47fa
8 changed files with 1167 additions and 158 deletions

251
src/config/topTileMap.ts Normal file
View File

@@ -0,0 +1,251 @@
// src/config/topTileMap.ts
export type Suit = 'W' | 'T' | 'B' | 'F' | 'D'
export interface Tile {
id: number
suit: Suit
value: number
}
/**
* 图片用途:
* - hand: 上方手牌背面
* - exposed: 上方碰/杠/胡等明牌
* - covered: 上方盖住的牌
*/
export type TileImageType = 'hand' | 'exposed' | 'covered'
export type TilePosition = 'top'
/**
* 明牌图索引:
* p2s1_x => 万
* p2s2_x => 筒
* p2s3_x => 条
* p2s4_x => 东南西北中发白
*/
const EXPOSED_SUIT_INDEX_MAP: Record<Suit, 1 | 2 | 3 | 4> = {
W: 1,
T: 2,
B: 3,
F: 4,
D: 4,
}
/**
* 字牌 value 映射:
* F:
* 1=东 2=南 3=西 4=北
*
* D:
* 1=中 2=发 3=白
*
* 图片资源中:
* 1=东 2=南 3=西 4=北 5=中 6=发 7=白
*/
function getHonorImageValue(suit: Suit, value: number): number {
if (suit === 'F') return value
if (suit === 'D') return value + 4
return value
}
/**
* 构建上方明牌图片 key
* 例如:
* /src/assets/images/tiles/top/p2s1_1.png
* /src/assets/images/tiles/top/p2s4_5.png
*/
function buildExposedTileImageKey(
suit: Suit,
value: number,
position: TilePosition = 'top',
): string {
const suitIndex = EXPOSED_SUIT_INDEX_MAP[suit]
const imageValue =
suit === 'F' || suit === 'D' ? getHonorImageValue(suit, value) : value
return `/src/assets/images/tiles/${position}/p2s${suitIndex}_${imageValue}.png`
}
/**
* 构建上方手牌背面图片 key
*/
function buildHandTileImageKey(position: TilePosition = 'top'): string {
return `/src/assets/images/tiles/${position}/tbgs_2.png`
}
/**
* 构建上方盖牌图片 key
*/
function buildCoveredTileImageKey(position: TilePosition = 'top'): string {
return `/src/assets/images/tiles/${position}/tdbgs_2.png`
}
/**
* 通过 Vite 收集上方麻将牌资源
*/
const tileImageModules = import.meta.glob('/src/assets/images/tiles/top/*.png', {
eager: true,
import: 'default',
}) as Record<string, string>
/**
* 判断是否为合法花色
*/
export function isValidSuit(suit: string): suit is Suit {
return suit === 'W' || suit === 'T' || suit === 'B' || suit === 'F' || suit === 'D'
}
/**
* 判断是否为合法点数
* W/T/B => 1~9
* F => 1~4
* D => 1~3
*/
export function isValidTileValueBySuit(suit: Suit, value: number): boolean {
if (!Number.isInteger(value)) {
return false
}
switch (suit) {
case 'W':
case 'T':
case 'B':
return value >= 1 && value <= 9
case 'F':
return value >= 1 && value <= 4
case 'D':
return value >= 1 && value <= 3
default:
return false
}
}
/**
* 判断是否为合法牌
*/
export function isValidTile(tile: {
suit: string
value: number
}): tile is Pick<Tile, 'suit' | 'value'> {
if (!isValidSuit(tile.suit)) {
return false
}
return isValidTileValueBySuit(tile.suit, tile.value)
}
/**
* 获取上方手牌背面图
*/
export function getHandTileImage(position: TilePosition = 'top'): string {
const key = buildHandTileImageKey(position)
return tileImageModules[key] || ''
}
/**
* 获取上方碰/杠/胡等明牌图片
*/
export function getExposedTileImage(
tile: Pick<Tile, 'suit' | 'value'>,
position: TilePosition = 'top',
): string {
if (!isValidTile(tile)) {
return ''
}
const key = buildExposedTileImageKey(tile.suit, tile.value, position)
return tileImageModules[key] || ''
}
/**
* 获取上方盖牌图片
*/
export function getCoveredTileImage(position: TilePosition = 'top'): string {
const key = buildCoveredTileImageKey(position)
return tileImageModules[key] || ''
}
/**
* 统一获取上方牌图片
*/
export function getTileImage(
tile?: Pick<Tile, 'suit' | 'value'>,
imageType: TileImageType = 'hand',
position: TilePosition = 'top',
): string {
if (imageType === 'hand') {
return getHandTileImage(position)
}
if (imageType === 'covered') {
return getCoveredTileImage(position)
}
if (!tile || !isValidTile(tile)) {
return ''
}
return getExposedTileImage(tile, position)
}
/**
* 获取所有基础牌(不含重复)
*/
export function getAllTiles(): Array<Pick<Tile, 'suit' | 'value'>> {
const result: Array<Pick<Tile, 'suit' | 'value'>> = []
const numberSuits: Array<'W' | 'T' | 'B'> = ['W', 'T', 'B']
for (const suit of numberSuits) {
for (let value = 1; value <= 9; value++) {
result.push({ suit, value })
}
}
for (let value = 1; value <= 4; value++) {
result.push({ suit: 'F', value })
}
for (let value = 1; value <= 3; value++) {
result.push({ suit: 'D', value })
}
return result
}
/**
* 获取牌的中文名称
*/
export function getTileLabel(tile: Pick<Tile, 'suit' | 'value'>): string {
if (!isValidTile(tile)) {
return ''
}
switch (tile.suit) {
case 'W':
return `${tile.value}`
case 'T':
return `${tile.value}`
case 'B':
return `${tile.value}`
case 'F': {
const map: Record<number, string> = {
1: '东',
2: '南',
3: '西',
4: '北',
}
return map[tile.value] || ''
}
case 'D': {
const map: Record<number, string> = {
1: '中',
2: '发',
3: '白',
}
return map[tile.value] || ''
}
default:
return ''
}
}