docs(game): 添加游戏模块数据
- 新增 useGameBoardVm 数据层实施说明文档 - 添加 36字花核心玩法与前端规则摘要 - 创建游戏模块数据与界面分层第一阶段实施稿 - 定义四层架构:api/dto、store、view-model hooks、ui层 - 规范 PC 与 Mobile 共享业务逻辑的改造方案 - 明确各层职责边界和组件改造顺序
This commit is contained in:
42
src/features/game/hooks/use-game-control-vm.ts
Normal file
42
src/features/game/hooks/use-game-control-vm.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from 'react'
|
||||
import { CHIP_OPTIONS } from '@/constants'
|
||||
import { selectSelectionTotal, useGameRoundStore } from '@/store/game'
|
||||
|
||||
const CHIP_IMAGE_MAP = new Map(
|
||||
CHIP_OPTIONS.map((chip) => [chip.value, chip.src] as const),
|
||||
)
|
||||
|
||||
export function useGameControlVm() {
|
||||
const chips = useGameRoundStore((state) => state.chips)
|
||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||
const selections = useGameRoundStore((state) => state.selections)
|
||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||
const selectChip = useGameRoundStore((state) => state.selectChip)
|
||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||
|
||||
const chipItems = useMemo(
|
||||
() =>
|
||||
chips.map((chip) => ({
|
||||
amount: chip.amount,
|
||||
id: chip.id,
|
||||
isSelected: chip.id === activeChipId,
|
||||
src: CHIP_IMAGE_MAP.get(chip.amount) ?? CHIP_OPTIONS[0]?.src ?? '',
|
||||
valueLabel: String(chip.amount),
|
||||
})),
|
||||
[activeChipId, chips],
|
||||
)
|
||||
|
||||
const selectedChip =
|
||||
chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
|
||||
|
||||
return {
|
||||
canClear: selections.length > 0,
|
||||
onChipSelect: selectChip,
|
||||
onClearSelections: clearSelections,
|
||||
selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
|
||||
selectedChipId: activeChipId,
|
||||
selectedCountLabel: `${selections.length}/5`,
|
||||
totalBetAmountLabel: String(totalBetAmount),
|
||||
chips: chipItems,
|
||||
}
|
||||
}
|
||||
43
src/features/game/hooks/use-game-history-vm.ts
Normal file
43
src/features/game/hooks/use-game-history-vm.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useGameRoundStore } from '@/store/game'
|
||||
|
||||
function formatSettledTime(iso: string) {
|
||||
const date = new Date(iso)
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
hour12: false,
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
export function useGameHistoryVm() {
|
||||
const history = useGameRoundStore((state) => state.history)
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
history.map((entry) => ({
|
||||
id: entry.roundId,
|
||||
payoutMultiplierLabel: `${entry.payoutMultiplier}x`,
|
||||
roundId: entry.roundId,
|
||||
settledAtLabel: formatSettledTime(entry.settledAt),
|
||||
statusLabel: 'settled',
|
||||
totalPoolAmountLabel: entry.totalPoolAmount.toFixed(2),
|
||||
winningCellIdLabel: String(entry.winningCellId),
|
||||
})),
|
||||
[history],
|
||||
)
|
||||
|
||||
return {
|
||||
emptyText: 'No history yet',
|
||||
isEmpty: items.length === 0,
|
||||
items,
|
||||
}
|
||||
}
|
||||
59
src/features/game/hooks/use-game-status-vm.ts
Normal file
59
src/features/game/hooks/use-game-status-vm.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMemo } from 'react'
|
||||
import { getRoundCountdownMs } from '@/features/game/shared/selectors'
|
||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||
|
||||
const PHASE_META = {
|
||||
betting: {
|
||||
description: '(Menerima Taruhan)',
|
||||
label: 'OPEN',
|
||||
toneClassName: 'text-[#78FF7F]',
|
||||
},
|
||||
locked: {
|
||||
description: '(Taruhan Ditutup)',
|
||||
label: 'LOCKED',
|
||||
toneClassName: 'text-[#FFE375]',
|
||||
},
|
||||
revealing: {
|
||||
description: '(Mengundi Hasil)',
|
||||
label: 'DRAWING',
|
||||
toneClassName: 'text-[#57E8FF]',
|
||||
},
|
||||
settled: {
|
||||
description: '(Putaran Selesai)',
|
||||
label: 'SETTLED',
|
||||
toneClassName: 'text-[#FF9C6B]',
|
||||
},
|
||||
waiting: {
|
||||
description: '(Menunggu Putaran Berikutnya)',
|
||||
label: 'WAITING',
|
||||
toneClassName: 'text-[#A7B6C7]',
|
||||
},
|
||||
} as const
|
||||
|
||||
export function useGameStatusVm() {
|
||||
const cells = useGameRoundStore((state) => state.cells)
|
||||
const round = useGameRoundStore((state) => state.round)
|
||||
const trends = useGameRoundStore((state) => state.trends)
|
||||
const dashboard = useGameSessionStore((state) => state.dashboard)
|
||||
|
||||
return useMemo(() => {
|
||||
const oddsValue = cells[0]?.odds ?? '--'
|
||||
const featuredTrend = trends.find(
|
||||
(entry) => entry.cellId === dashboard.featuredCellId,
|
||||
)
|
||||
const phaseMeta = PHASE_META[round.phase]
|
||||
|
||||
return {
|
||||
acceptingBets: round.phase === 'betting',
|
||||
countdownMs: getRoundCountdownMs(round),
|
||||
limitLabel: `${dashboard.tableLimitMin}-${dashboard.tableLimitMax}`,
|
||||
oddsLabel: `1:${oddsValue}`,
|
||||
phase: round.phase,
|
||||
phaseDescription: phaseMeta.description,
|
||||
phaseLabel: phaseMeta.label,
|
||||
phaseToneClassName: phaseMeta.toneClassName,
|
||||
roundId: round.id,
|
||||
streakLabel: featuredTrend ? `X${featuredTrend.currentStreak}` : '--',
|
||||
}
|
||||
}, [cells, dashboard, round, trends])
|
||||
}
|
||||
Reference in New Issue
Block a user