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

大屏幕效果ok版本

上级 c8fd4e62
差异被折叠。
...@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论