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

299 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ''
}
}