- 重命名 tileMap.ts 为 bottomTileMap.ts 并扩展支持字牌(东南西北、中发白) - 新增 leftTileMap.ts、rightTileMap.ts 和 topTileMap.ts 支持多位置牌面渲染 - 实现牌面图像类型区分(手牌、明牌、盖牌)和动态图像键构建 - 添加牌面验证函数支持不同花色的数值范围检查 - 更新 ChengduGamePage.vue 使用新的底部牌面配置文件 - 实现玩家手牌可见性控制仅在非等待阶段显示 - 重构服务器响应解析逻辑适配新的数据结构 - 添加玩家手牌响应处理器实时更新手牌状态 - 将玩家手牌显示从文本改为图像展示提升用户体验 - 重构CSS样式实现牌面图像的响应式布局和阴影效果
299 lines
6.4 KiB
TypeScript
299 lines
6.4 KiB
TypeScript
// src/config/bottomTileMap.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 = 'bottom'
|
||
|
||
/**
|
||
* 手牌图索引:
|
||
* p4b1_x => 万
|
||
* p4b2_x => 筒
|
||
* p4b3_x => 条
|
||
* p4b4_x => 东南西北中发白
|
||
*/
|
||
const HAND_SUIT_INDEX_MAP: Record<Suit, 1 | 2 | 3 | 4> = {
|
||
W: 1,
|
||
T: 2,
|
||
B: 3,
|
||
F: 4,
|
||
D: 4,
|
||
}
|
||
|
||
/**
|
||
* 明牌图索引:
|
||
* p4s1_x => 万
|
||
* p4s2_x => 筒
|
||
* p4s3_x => 条
|
||
* p4s4_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/bottom/p4b1_1.png
|
||
* /src/assets/images/tiles/bottom/p4b4_5.png
|
||
*/
|
||
function buildHandTileImageKey(
|
||
suit: Suit,
|
||
value: number,
|
||
position: TilePosition = 'bottom',
|
||
): string {
|
||
const suitIndex = HAND_SUIT_INDEX_MAP[suit]
|
||
const imageValue = suit === 'F' || suit === 'D'
|
||
? getHonorImageValue(suit, value)
|
||
: value
|
||
|
||
return `/src/assets/images/tiles/${position}/p4b${suitIndex}_${imageValue}.png`
|
||
}
|
||
|
||
/**
|
||
* 构建明牌图片 key(碰/杠/胡漏出的牌)
|
||
* 例如:
|
||
* /src/assets/images/tiles/bottom/p4s1_1.png
|
||
* /src/assets/images/tiles/bottom/p4s4_5.png
|
||
*/
|
||
function buildExposedTileImageKey(
|
||
suit: Suit,
|
||
value: number,
|
||
position: TilePosition = 'bottom',
|
||
): string {
|
||
const suitIndex = EXPOSED_SUIT_INDEX_MAP[suit]
|
||
const imageValue = suit === 'F' || suit === 'D'
|
||
? getHonorImageValue(suit, value)
|
||
: value
|
||
|
||
return `/src/assets/images/tiles/${position}/p4s${suitIndex}_${imageValue}.png`
|
||
}
|
||
|
||
/**
|
||
* 构建盖牌图片 key
|
||
*/
|
||
function buildCoveredTileImageKey(position: TilePosition = 'bottom'): string {
|
||
return `/src/assets/images/tiles/${position}/tdbgs_4.png`
|
||
}
|
||
|
||
/**
|
||
* 通过 Vite 收集所有麻将牌资源
|
||
*/
|
||
const tileImageModules = import.meta.glob(
|
||
'/src/assets/images/tiles/bottom/*.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(
|
||
tile: Pick<Tile, 'suit' | 'value'>,
|
||
position: TilePosition = 'bottom',
|
||
): string {
|
||
if (!isValidTile(tile)) {
|
||
return ''
|
||
}
|
||
|
||
const key = buildHandTileImageKey(tile.suit, tile.value, position)
|
||
return tileImageModules[key] || ''
|
||
}
|
||
|
||
/**
|
||
* 获取碰/杠/胡漏出的明牌图片
|
||
*/
|
||
export function getExposedTileImage(
|
||
tile: Pick<Tile, 'suit' | 'value'>,
|
||
position: TilePosition = 'bottom',
|
||
): string {
|
||
if (!isValidTile(tile)) {
|
||
return ''
|
||
}
|
||
|
||
const key = buildExposedTileImageKey(tile.suit, tile.value, position)
|
||
return tileImageModules[key] || ''
|
||
}
|
||
|
||
/**
|
||
* 获取盖住的牌图片
|
||
*/
|
||
export function getCoveredTileImage(position: TilePosition = 'bottom'): string {
|
||
const key = buildCoveredTileImageKey(position)
|
||
return tileImageModules[key] || ''
|
||
}
|
||
|
||
/**
|
||
* 统一获取牌图片
|
||
*/
|
||
export function getTileImage(
|
||
tile: Pick<Tile, 'suit' | 'value'>,
|
||
imageType: TileImageType = 'hand',
|
||
position: TilePosition = 'bottom',
|
||
): string {
|
||
if (imageType === 'covered') {
|
||
return getCoveredTileImage(position)
|
||
}
|
||
|
||
if (!isValidTile(tile)) {
|
||
return ''
|
||
}
|
||
|
||
if (imageType === 'exposed') {
|
||
return getExposedTileImage(tile, position)
|
||
}
|
||
|
||
return getHandTileImage(tile, position)
|
||
}
|
||
|
||
/**
|
||
* 获取所有基础牌(不含重复)
|
||
* 包含:
|
||
* - 万 1~9
|
||
* - 筒 1~9
|
||
* - 条 1~9
|
||
* - 东南西北
|
||
* - 中发白
|
||
*/
|
||
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 ''
|
||
}
|
||
} |