提交 9a27f2a4 authored 作者: lidongxu's avatar lidongxu

大屏幕效果ok版本

上级 c8fd4e62
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
* 大屏展示页入口:支持多玩家并排渲染 * 大屏展示页入口:支持多玩家并排渲染
*/ */
import { initScaler } from './scaler.js' import { initScaler } from './scaler.js'
import { getAllPlayerStates, clearGameState, getCurrentRoom, getPlayerTeam, getCountdown } from './stateManager.js' import { getAllPlayerStates, clearGameState, getCurrentRoom, getPlayerTeam, getCountdown, getRoomTimer } from './stateManager.js'
import { initSocket, getConnectionStatus } from './socket.js' import { initSocket, getConnectionStatus } from './socket.js'
import { drawBackground } from './renderer/background.js' import { drawBackground } from './renderer/background.js'
import { drawBubbleGrid } from './renderer/bubbleGrid.js' import { drawBubbleGrid } from './renderer/bubbleGrid.js'
import { drawBubble3D, getBubbleRadius, configureBubbleLayout } from './renderer/bubble.js' import { drawBubble3D, getBubbleRadius, configureBubbleLayout } from './renderer/bubble.js'
import { drawShooter } from './renderer/shooter.js' import { drawShooter } from './renderer/shooter.js'
import { drawGameInfo, drawTeamResultOverlay } from './renderer/gameinfo.js' import { drawGameInfo, drawTeamResultOverlay, drawRoomTimerCard } from './renderer/gameinfo.js'
import { detectAndCreateBursts, updateAndDrawBursts, clearPrevGrid } from './renderer/explosion.js' import { detectAndCreateBursts, updateAndDrawBursts, clearPrevGrid } from './renderer/explosion.js'
import { drawIdleScreen } from './renderer/idleScreen.js' import { drawIdleScreen } from './renderer/idleScreen.js'
import { SCREEN_WIDTH, SCREEN_HEIGHT, configureScreenRatio, configureSafeArea } from './constants.js' import { SCREEN_WIDTH, SCREEN_HEIGHT, configureScreenRatio, configureSafeArea } from './constants.js'
...@@ -25,6 +25,10 @@ gameBgImg.src = GAME_BG_URL ...@@ -25,6 +25,10 @@ gameBgImg.src = GAME_BG_URL
/** 每个玩家独立的碎裂效果列表:Map<playerId, BubbleBurst[]> */ /** 每个玩家独立的碎裂效果列表:Map<playerId, BubbleBurst[]> */
const playerBursts = new Map() const playerBursts = new Map()
const PLAYER_FRAME_MARGIN_X = 18
const PLAYER_FRAME_MARGIN_Y = 18
const PLAYER_FRAME_RADIUS = 18
let frameCount = 0 let frameCount = 0
// ─── 缩放 ───────────────────────────────────────────────────────────────────── // ─── 缩放 ─────────────────────────────────────────────────────────────────────
...@@ -50,10 +54,76 @@ function drawPlayerGameBackground() { ...@@ -50,10 +54,76 @@ function drawPlayerGameBackground() {
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
} }
function roundRectPath(x, y, w, h, r) {
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.lineTo(x + w - r, y)
ctx.arcTo(x + w, y, x + w, y + r, r)
ctx.lineTo(x + w, y + h - r)
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
ctx.lineTo(x + r, y + h)
ctx.arcTo(x, y + h, x, y + h - r, r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.closePath()
}
function getPlayerFrameRect() {
return {
x: PLAYER_FRAME_MARGIN_X,
y: PLAYER_FRAME_MARGIN_Y,
w: SCREEN_WIDTH - PLAYER_FRAME_MARGIN_X * 2,
h: SCREEN_HEIGHT - PLAYER_FRAME_MARGIN_Y * 2,
r: PLAYER_FRAME_RADIUS,
}
}
function drawPlayerFrameShell(frame) {
ctx.save()
ctx.shadowColor = 'rgba(0,0,0,0.38)'
ctx.shadowBlur = 24
ctx.shadowOffsetY = 10
const bg = ctx.createLinearGradient(frame.x, frame.y, frame.x, frame.y + frame.h)
bg.addColorStop(0, 'rgba(20,10,42,0.30)')
bg.addColorStop(1, 'rgba(8,4,22,0.46)')
ctx.fillStyle = bg
roundRectPath(frame.x, frame.y, frame.w, frame.h, frame.r)
ctx.fill()
ctx.restore()
}
function drawPlayerFrameBorder(frame) {
ctx.save()
ctx.lineWidth = 3
ctx.strokeStyle = 'rgba(251,191,36,0.82)'
ctx.shadowColor = 'rgba(251,191,36,0.28)'
ctx.shadowBlur = 18
roundRectPath(frame.x, frame.y, frame.w, frame.h, frame.r)
ctx.stroke()
ctx.shadowBlur = 0
ctx.save()
roundRectPath(frame.x, frame.y, frame.w, frame.h, frame.r)
ctx.clip()
const hl = ctx.createLinearGradient(frame.x, frame.y, frame.x, frame.y + frame.h * 0.22)
hl.addColorStop(0, 'rgba(255,255,255,0.22)')
hl.addColorStop(1, 'rgba(255,255,255,0)')
ctx.fillStyle = hl
ctx.fillRect(frame.x, frame.y, frame.w, frame.h * 0.22)
ctx.restore()
ctx.restore()
}
// ─── 单个玩家画面渲染 ────────────────────────────────────────────────────────── // ─── 单个玩家画面渲染 ──────────────────────────────────────────────────────────
function renderPlayer(state, offsetX, roomId) { function renderPlayer(state, offsetX, roomId) {
const pid = state.playerId ?? 1 const pid = state.playerId ?? 1
const frame = getPlayerFrameRect()
const contentScale = Math.min(frame.w / SCREEN_WIDTH, frame.h / SCREEN_HEIGHT)
const contentW = SCREEN_WIDTH * contentScale
const contentH = SCREEN_HEIGHT * contentScale
const contentX = frame.x + (frame.w - contentW) / 2
const contentY = frame.y + (frame.h - contentH) / 2
// 从 grid 数据推算偶数行列数,动态调整泡泡大小 // 从 grid 数据推算偶数行列数,动态调整泡泡大小
// 偶数行(row 0)的列数决定了整个网格的列配置 // 偶数行(row 0)的列数决定了整个网格的列配置
...@@ -68,6 +138,14 @@ function renderPlayer(state, offsetX, roomId) { ...@@ -68,6 +138,14 @@ function renderPlayer(state, offsetX, roomId) {
ctx.save() ctx.save()
ctx.translate(offsetX, 0) ctx.translate(offsetX, 0)
drawPlayerFrameShell(frame)
ctx.save()
roundRectPath(frame.x, frame.y, frame.w, frame.h, frame.r)
ctx.clip()
ctx.translate(contentX, contentY)
ctx.scale(contentScale, contentScale)
drawPlayerGameBackground() drawPlayerGameBackground()
// 泡泡网格 // 泡泡网格
...@@ -100,13 +178,17 @@ function renderPlayer(state, offsetX, roomId) { ...@@ -100,13 +178,17 @@ function renderPlayer(state, offsetX, roomId) {
drawGameInfo(ctx, state.score ?? 0, state.isGameOver ?? false, roomId ?? state.roomId ?? '', state.nickname ?? '') drawGameInfo(ctx, state.score ?? 0, state.isGameOver ?? false, roomId ?? state.roomId ?? '', state.nickname ?? '')
ctx.restore() ctx.restore()
drawPlayerFrameBorder(frame)
ctx.restore()
} }
// ─── 多玩家分隔线 ────────────────────────────────────────────────────────────── // ─── 多玩家分隔线 ──────────────────────────────────────────────────────────────
function drawDivider(x) { function drawDivider(x) {
ctx.save() ctx.save()
const grad = ctx.createLinearGradient(x, 0, x, SCREEN_HEIGHT) const top = PLAYER_FRAME_MARGIN_Y + 26
const bottom = SCREEN_HEIGHT - PLAYER_FRAME_MARGIN_Y - 26
const grad = ctx.createLinearGradient(x, top, x, bottom)
grad.addColorStop(0, 'rgba(139,92,246,0)') grad.addColorStop(0, 'rgba(139,92,246,0)')
grad.addColorStop(0.2, 'rgba(139,92,246,0.5)') grad.addColorStop(0.2, 'rgba(139,92,246,0.5)')
grad.addColorStop(0.8, 'rgba(139,92,246,0.5)') grad.addColorStop(0.8, 'rgba(139,92,246,0.5)')
...@@ -115,8 +197,8 @@ function drawDivider(x) { ...@@ -115,8 +197,8 @@ function drawDivider(x) {
ctx.lineWidth = 2 ctx.lineWidth = 2
ctx.setLineDash([6, 6]) ctx.setLineDash([6, 6])
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x, 0) ctx.moveTo(x, top)
ctx.lineTo(x, SCREEN_HEIGHT) ctx.lineTo(x, bottom)
ctx.stroke() ctx.stroke()
ctx.setLineDash([]) ctx.setLineDash([])
ctx.restore() ctx.restore()
...@@ -126,32 +208,36 @@ function drawDivider(x) { ...@@ -126,32 +208,36 @@ function drawDivider(x) {
function drawVSDivider(x) { function drawVSDivider(x) {
ctx.save() ctx.save()
// 发光效果 const centerY = SCREEN_HEIGHT / 2
ctx.shadowColor = 'rgba(251,191,36,0.5)' const pillW = 58
ctx.shadowBlur = 20 const pillH = 70
const pillX = x - pillW / 2
const grad = ctx.createLinearGradient(x, 0, x, SCREEN_HEIGHT) const pillY = centerY - pillH / 2
grad.addColorStop(0, 'rgba(251,191,36,0)')
grad.addColorStop(0.15, 'rgba(251,191,36,0.8)') ctx.fillStyle = 'rgba(12,18,36,0.82)'
grad.addColorStop(0.5, 'rgba(251,191,36,1)') ctx.strokeStyle = 'rgba(103,232,249,0.9)'
grad.addColorStop(0.85, 'rgba(251,191,36,0.8)') ctx.lineWidth = 2
grad.addColorStop(1, 'rgba(251,191,36,0)') ctx.shadowColor = 'rgba(34,211,238,0.35)'
ctx.strokeStyle = grad ctx.shadowBlur = 18
ctx.lineWidth = 3 roundRectPath(pillX, pillY, pillW, pillH, 16)
ctx.beginPath() ctx.fill()
ctx.moveTo(x, SCREEN_HEIGHT * 0.15)
ctx.lineTo(x, SCREEN_HEIGHT * 0.85)
ctx.stroke() ctx.stroke()
ctx.shadowBlur = 0 ctx.shadowBlur = 0
ctx.fillStyle = 'rgba(125,211,252,0.95)'
ctx.fillRect(pillX + 8, pillY + 8, pillW - 16, 3)
// VS 文字 // V|S 文字
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
ctx.font = 'bold 48px Arial' ctx.font = 'bold 44px Arial'
ctx.fillStyle = 'rgba(251,191,36,0.9)' ctx.fillStyle = '#F8FAFC'
ctx.shadowColor = 'rgba(251,191,36,0.6)' ctx.strokeStyle = 'rgba(8,15,34,0.9)'
ctx.shadowBlur = 15 ctx.lineWidth = 4
ctx.fillText('VS', x, SCREEN_HEIGHT / 2) ctx.shadowColor = 'rgba(103,232,249,0.65)'
ctx.shadowBlur = 16
ctx.strokeText('V|S', x, centerY + 1)
ctx.fillText('V|S', x, centerY)
ctx.shadowBlur = 0 ctx.shadowBlur = 0
ctx.restore() ctx.restore()
} }
...@@ -212,6 +298,9 @@ function loop() { ...@@ -212,6 +298,9 @@ function loop() {
const roomId = getCurrentRoom() const roomId = getCurrentRoom()
const connStatus = getConnectionStatus() const connStatus = getConnectionStatus()
const playerCount = states.length || 1 const playerCount = states.length || 1
const totalWidth = SCREEN_WIDTH * Math.max(playerCount, 1)
const roomTimer = getRoomTimer()
let allGameOver = false
// 从任意一个玩家的 state 中提取屏幕比例和安全区域,在渲染前统一配置 // 从任意一个玩家的 state 中提取屏幕比例和安全区域,在渲染前统一配置
if (states.length > 0 && states[0].screenRatio) { if (states.length > 0 && states[0].screenRatio) {
...@@ -245,7 +334,7 @@ function loop() { ...@@ -245,7 +334,7 @@ function loop() {
ctx.restore() ctx.restore()
if (states.length > 0) { if (states.length > 0) {
const allGameOver = states.every(s => s.isGameOver) allGameOver = states.every(s => s.isGameOver)
if (allGameOver && states.length > 1) { if (allGameOver && states.length > 1) {
// 最终结果页使用干净背景,不再保留各玩家游戏盘面 // 最终结果页使用干净背景,不再保留各玩家游戏盘面
...@@ -257,43 +346,47 @@ function loop() { ...@@ -257,43 +346,47 @@ function loop() {
drawTeamResultOverlay(ctx, states, getPlayerTeam, totalWidth) drawTeamResultOverlay(ctx, states, getPlayerTeam, totalWidth)
ctx.restore() ctx.restore()
} else { } else {
// ── 按队伍分组渲染:A队左,B队右 ──────────────────────────────────────── // ── 按队伍分组渲染:A队左,B队右 ────────────────────────────────────────
let currentOffsetX = 0 let currentOffsetX = 0
const dividerXs = []
const hasBothTeams = teamAStates.length > 0 && teamBStates.length > 0
// 渲染A队玩家 // 渲染A队玩家
teamAStates.forEach((state, idx) => { teamAStates.forEach((state, idx) => {
const offsetX = currentOffsetX const offsetX = currentOffsetX
// 分隔线(A队内部) // 记录分隔线(A队内部)
if (idx > 0) drawDivider(offsetX) if (idx > 0) dividerXs.push(offsetX)
renderPlayer(state, offsetX, roomId) renderPlayer(state, offsetX, roomId)
currentOffsetX += SCREEN_WIDTH currentOffsetX += SCREEN_WIDTH
}) })
// 绘制 VS 分隔线(A队和B队之间)
if (teamAStates.length > 0 && teamBStates.length > 0) {
drawVSDivider(currentOffsetX)
}
// 渲染B队玩家 // 渲染B队玩家
teamBStates.forEach((state, idx) => { teamBStates.forEach((state, idx) => {
const offsetX = currentOffsetX const offsetX = currentOffsetX
// 分隔线(B队内部) // 记录分隔线(B队内部)
if (idx > 0) drawDivider(offsetX) if (idx > 0) dividerXs.push(offsetX)
renderPlayer(state, offsetX, roomId) renderPlayer(state, offsetX, roomId)
currentOffsetX += SCREEN_WIDTH currentOffsetX += SCREEN_WIDTH
}) })
dividerXs.forEach(drawDivider)
if (hasBothTeams) drawVSDivider(totalWidth / 2)
} }
} else { } else {
// ── 空闲等待画面 ────────────────────────────────────────────────────── // ── 空闲等待画面 ──────────────────────────────────────────────────────
drawIdleScreen(ctx, SCREEN_WIDTH, SCREEN_HEIGHT, roomId, connStatus, SCREEN_NAME, frameCount) drawIdleScreen(ctx, SCREEN_WIDTH, SCREEN_HEIGHT, roomId, connStatus, SCREEN_NAME, frameCount)
} }
if (states.length > 0 && !allGameOver && roomTimer.active) {
drawRoomTimerCard(ctx, roomTimer.remainingSec, totalWidth)
}
// ── 倒计时覆盖层(服务端驱动,所有端同步)────────────────────────────── // ── 倒计时覆盖层(服务端驱动,所有端同步)──────────────────────────────
drawCountdownOverlay(SCREEN_WIDTH * totalSlots) drawCountdownOverlay(SCREEN_WIDTH * totalSlots)
......
...@@ -6,6 +6,15 @@ ...@@ -6,6 +6,15 @@
*/ */
import { SCREEN_WIDTH, SCREEN_HEIGHT, SAFE_AREA_TOP } from '../constants.js' import { SCREEN_WIDTH, SCREEN_HEIGHT, SAFE_AREA_TOP } from '../constants.js'
const TEAM_NAME_MAP = {
A: '深耕队',
B: '致远队',
}
function getTeamDisplayName(team) {
return TEAM_NAME_MAP[team] || team
}
// ─── 工具 ───────────────────────────────────────────────────────────────────── // ─── 工具 ─────────────────────────────────────────────────────────────────────
function roundRectPath(ctx, x, y, w, h, r) { function roundRectPath(ctx, x, y, w, h, r) {
...@@ -154,6 +163,69 @@ function drawRoomCard(ctx, roomId) { ...@@ -154,6 +163,69 @@ function drawRoomCard(ctx, roomId) {
ctx.fillText(label, cx, boxY + 38) ctx.fillText(label, cx, boxY + 38)
} }
function formatRemainTime(remainingSec) {
const safeSec = Math.max(0, Math.floor(remainingSec || 0))
const minutes = String(Math.floor(safeSec / 60)).padStart(2, '0')
const seconds = String(safeSec % 60).padStart(2, '0')
return `${minutes}:${seconds}`
}
export function drawRoomTimerCard(ctx, remainingSec, totalWidth = SCREEN_WIDTH) {
const label = formatRemainTime(remainingSec)
const boxY = SAFE_AREA_TOP + 8
const boxH = 44
const r = 10
ctx.save()
ctx.font = 'bold 22px Arial'
const timeW = ctx.measureText(label).width
ctx.font = 'bold 11px Arial'
const titleW = ctx.measureText('时间').width
const boxW = Math.max(timeW, titleW) + 34
const boxX = totalWidth / 2 - boxW / 2
ctx.shadowColor = 'rgba(0,0,0,0.45)'
ctx.shadowBlur = 10
ctx.shadowOffsetY = 4
const bg = ctx.createLinearGradient(boxX, boxY, boxX, boxY + boxH)
bg.addColorStop(0, 'rgba(139,92,246,0.92)')
bg.addColorStop(1, 'rgba(124,58,237,0.97)')
ctx.fillStyle = bg
roundRectPath(ctx, boxX, boxY, boxW, boxH, r)
ctx.fill()
ctx.shadowBlur = 0
const hl = ctx.createLinearGradient(boxX, boxY, boxX + boxW, boxY)
hl.addColorStop(0, 'rgba(167,139,250,0.55)')
hl.addColorStop(0.5, 'rgba(196,181,253,0.88)')
hl.addColorStop(1, 'rgba(167,139,250,0.55)')
ctx.fillStyle = hl
ctx.fillRect(boxX + 4, boxY + 4, boxW - 8, 3)
ctx.strokeStyle = 'rgba(167,139,250,0.55)'
ctx.lineWidth = 1.5
roundRectPath(ctx, boxX, boxY, boxW, boxH, r)
ctx.stroke()
const cx = boxX + boxW / 2
ctx.textAlign = 'center'
ctx.textBaseline = 'alphabetic'
ctx.font = 'bold 11px Arial'
ctx.fillStyle = 'rgba(221,214,254,0.95)'
ctx.fillText('时间', cx, boxY + 18)
ctx.font = 'bold 22px Arial'
ctx.fillStyle = 'rgba(0,0,0,0.32)'
ctx.fillText(label, cx + 1, boxY + 39)
const tg = ctx.createLinearGradient(cx - 35, 0, cx + 35, 0)
tg.addColorStop(0, '#FDE68A')
tg.addColorStop(0.5, '#FCD34D')
tg.addColorStop(1, '#F59E0B')
ctx.fillStyle = tg
ctx.fillText(label, cx, boxY + 38)
ctx.restore()
}
// ─── 游戏结束大卡片 ─────────────────────────────────────────────────────────── // ─── 游戏结束大卡片 ───────────────────────────────────────────────────────────
function getStarCount(score) { function getStarCount(score) {
...@@ -362,7 +434,7 @@ export function drawTeamResultOverlay(ctx, playerStates, getPlayerTeam, totalWid ...@@ -362,7 +434,7 @@ export function drawTeamResultOverlay(ctx, playerStates, getPlayerTeam, totalWid
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
ctx.font = 'bold 42px Arial' ctx.font = 'bold 42px Arial'
const titleText = isDraw ? '平局!' : `${winner}胜利!` const titleText = isDraw ? '平局!' : `${getTeamDisplayName(winner)}胜利!`
const titleColor = isDraw ? '#FCD34D' : winner === 'A' ? '#8B5CF6' : '#EC4899' const titleColor = isDraw ? '#FCD34D' : winner === 'A' ? '#8B5CF6' : '#EC4899'
ctx.shadowColor = titleColor ctx.shadowColor = titleColor
ctx.shadowBlur = 25 ctx.shadowBlur = 25
...@@ -485,7 +557,7 @@ function drawTeamScoreBig(ctx, x, y, team, score, isWinner) { ...@@ -485,7 +557,7 @@ function drawTeamScoreBig(ctx, x, y, team, score, isWinner) {
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
ctx.font = 'bold 18px Arial' ctx.font = 'bold 18px Arial'
ctx.fillStyle = isWinner ? color : 'rgba(150,150,150,0.6)' ctx.fillStyle = isWinner ? color : 'rgba(150,150,150,0.6)'
ctx.fillText(`${team}队`, x, y - 28) ctx.fillText(getTeamDisplayName(team), x, y - 28)
// 分数(胜利方更大更亮,失败方灰色暗淡) // 分数(胜利方更大更亮,失败方灰色暗淡)
ctx.font = isWinner ? 'bold 56px Arial' : 'bold 42px Arial' ctx.font = isWinner ? 'bold 56px Arial' : 'bold 42px Arial'
...@@ -520,7 +592,7 @@ function drawTeamPlayerList(ctx, x, y, w, h, team, players, isWinner) { ...@@ -520,7 +592,7 @@ function drawTeamPlayerList(ctx, x, y, w, h, team, players, isWinner) {
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
ctx.font = 'bold 16px Arial' ctx.font = 'bold 16px Arial'
ctx.fillStyle = displayColor ctx.fillStyle = displayColor
ctx.fillText(`${team}成员`, x + w / 2, y + 12) ctx.fillText(`${getTeamDisplayName(team)}成员`, x + w / 2, y + 12)
// 分割线 // 分割线
ctx.strokeStyle = isWinner ? color + '40' : 'rgba(150,150,150,0.2)' ctx.strokeStyle = isWinner ? color + '40' : 'rgba(150,150,150,0.2)'
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
* 发送 → JSON.stringify({ event: string, data: any }) * 发送 → JSON.stringify({ event: string, data: any })
* 接收 ← JSON.stringify({ event: string, data: any }) * 接收 ← JSON.stringify({ event: string, data: any })
*/ */
import { setCurrentRoom, setGameState, getPlayerState, clearGameState, setPlayerGameOver, setAllGameOver, setPlayerTeam, setCountdown, clearCountdown } from './stateManager.js' import { setCurrentRoom, setGameState, getPlayerState, clearGameState, setPlayerGameOver, setAllGameOver, setPlayerTeam, setCountdown, clearCountdown, setRoomTimer, clearRoomTimer } from './stateManager.js'
/** /**
* WebSocket 服务器地址配置 * WebSocket 服务器地址配置
...@@ -109,6 +109,8 @@ function _dispatch(event, data) { ...@@ -109,6 +109,8 @@ function _dispatch(event, data) {
case 'screen:roomChanged': { case 'screen:roomChanged': {
const roomId = data?.roomId ?? null const roomId = data?.roomId ?? null
console.log('[Socket] screen:roomChanged, roomId:', roomId) console.log('[Socket] screen:roomChanged, roomId:', roomId)
clearCountdown()
clearRoomTimer()
setCurrentRoom(roomId) setCurrentRoom(roomId)
if (!roomId) clearGameState() if (!roomId) clearGameState()
break break
...@@ -186,6 +188,7 @@ function _dispatch(event, data) { ...@@ -186,6 +188,7 @@ function _dispatch(event, data) {
case 'room:gameStart': { case 'room:gameStart': {
console.log('[Socket] room:gameStart') console.log('[Socket] room:gameStart')
clearCountdown() clearCountdown()
setRoomTimer(data?.gameDuration ?? 0)
break break
} }
...@@ -195,6 +198,7 @@ function _dispatch(event, data) { ...@@ -195,6 +198,7 @@ function _dispatch(event, data) {
*/ */
case 'room:timeUp': { case 'room:timeUp': {
console.log('[Socket] room:timeUp,所有玩家游戏结束') console.log('[Socket] room:timeUp,所有玩家游戏结束')
clearRoomTimer()
setAllGameOver() setAllGameOver()
break break
} }
......
...@@ -14,6 +14,9 @@ const playerTeams = new Map() ...@@ -14,6 +14,9 @@ const playerTeams = new Map()
/** 倒计时状态 */ /** 倒计时状态 */
let countdownState = { active: false, value: 0 } let countdownState = { active: false, value: 0 }
/** 游戏进行中的房间计时状态 */
let roomTimerState = { active: false, durationSec: 0, startAtMs: 0 }
export function setCurrentRoom(roomId) { export function setCurrentRoom(roomId) {
currentRoomId = roomId currentRoomId = roomId
} }
...@@ -129,8 +132,50 @@ export function getCountdown() { ...@@ -129,8 +132,50 @@ export function getCountdown() {
return countdownState return countdownState
} }
/**
* 设置房间游戏计时
*/
export function setRoomTimer(durationSec, startAtMs = Date.now()) {
const safeDuration = Number(durationSec) || 0
if (safeDuration <= 0) {
roomTimerState = { active: false, durationSec: 0, startAtMs: 0 }
return
}
roomTimerState = {
active: true,
durationSec: safeDuration,
startAtMs,
}
}
/**
* 清除房间游戏计时
*/
export function clearRoomTimer() {
roomTimerState = { active: false, durationSec: 0, startAtMs: 0 }
}
/**
* 获取当前房间剩余时间
*/
export function getRoomTimer(now = Date.now()) {
if (!roomTimerState.active || roomTimerState.durationSec <= 0) {
return { active: false, remainingSec: 0, durationSec: 0 }
}
const elapsedSec = Math.max(0, Math.floor((now - roomTimerState.startAtMs) / 1000))
const remainingSec = Math.max(0, roomTimerState.durationSec - elapsedSec)
return {
active: true,
durationSec: roomTimerState.durationSec,
remainingSec,
}
}
export function clearGameState() { export function clearGameState() {
playerStates.clear() playerStates.clear()
playerTeams.clear() playerTeams.clear()
countdownState = { active: false, value: 0 } countdownState = { active: false, value: 0 }
roomTimerState = { active: false, durationSec: 0, startAtMs: 0 }
} }
const prisma = require('../prisma/client'); const prisma = require('../prisma/client');
const TEAM_NAME_MAP = {
A: '深耕队',
B: '致远队',
};
function getTeamDisplayName(team) {
return TEAM_NAME_MAP[team] || team;
}
/** /**
* 内存等待表:roomId → { totalSeats, joined: Set<ws>, players: Array } * 内存等待表:roomId → { totalSeats, joined: Set<ws>, players: Array }
* 房间满员或游戏开始后从表中移除 * 房间满员或游戏开始后从表中移除
...@@ -102,7 +111,7 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro ...@@ -102,7 +111,7 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro
ws.ctx.team = playerInfo.team; ws.ctx.team = playerInfo.team;
roomPlayerCounter.set(rid, 1); roomPlayerCounter.set(rid, 1);
console.log(`[Room] 创建房间 ${rid},总座位: ${seats},房主: ${playerInfo.nickname} (${playerInfo.team})`); console.log(`[Room] 创建房间 ${rid},总座位: ${seats},房主: ${playerInfo.nickname} (${getTeamDisplayName(playerInfo.team)})`);
ws.sendEvent('room:created', { ws.sendEvent('room:created', {
roomId: rid, roomId: rid,
totalSeats: seats, totalSeats: seats,
...@@ -172,10 +181,10 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro ...@@ -172,10 +181,10 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro
const teamBCount = waiting.players.filter(p => p.team === 'B').length; const teamBCount = waiting.players.filter(p => p.team === 'B').length;
if (finalTeam === 'A' && teamACount >= perTeamSeats) { if (finalTeam === 'A' && teamACount >= perTeamSeats) {
ws.sendEvent('error', { message: 'A队已满,请选择其他队伍', code: 'TEAM_FULL' }); ws.sendEvent('error', { message: `${getTeamDisplayName('A')}已满,请选择其他队伍`, code: 'TEAM_FULL' });
return; return;
} else if (finalTeam === 'B' && teamBCount >= perTeamSeats) { } else if (finalTeam === 'B' && teamBCount >= perTeamSeats) {
ws.sendEvent('error', { message: 'B队已满,请选择其他队伍', code: 'TEAM_FULL' }); ws.sendEvent('error', { message: `${getTeamDisplayName('B')}已满,请选择其他队伍`, code: 'TEAM_FULL' });
return; return;
} }
...@@ -203,7 +212,7 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro ...@@ -203,7 +212,7 @@ function registerRoomHandlers(ws, { broadcastToRoom, joinRoom, leaveAllRooms, ro
const joinedCount = waiting.joined.size; const joinedCount = waiting.joined.size;
const { totalSeats, players } = waiting; const { totalSeats, players } = waiting;
console.log(`[Room] 玩家加入房间 ${rid},playerId=${nextId},team=${playerInfo.team},当前 ${joinedCount}/${totalSeats}`); console.log(`[Room] 玩家加入房间 ${rid},playerId=${nextId},team=${getTeamDisplayName(playerInfo.team)},当前 ${joinedCount}/${totalSeats}`);
// 通知自己加入成功(含最终分配的队伍,可能因满员被调整) // 通知自己加入成功(含最终分配的队伍,可能因满员被调整)
ws.sendEvent('room:joined', { roomId: rid, joinedCount, totalSeats, playerId: nextId, players, myPlayerId: nextId, team: finalTeam }); ws.sendEvent('room:joined', { roomId: rid, joinedCount, totalSeats, playerId: nextId, players, myPlayerId: nextId, team: finalTeam });
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论