功能:記分板語音改為報比分、發球區、賽末點與獲勝隊伍

摘要:
- 得分播報改為「比分(發球方先報)+ 發球者左右發球區」
- 發球方到賽末點(再得 1 分即獲勝)時,比分後加唸「賽末點」
- 賽末點得分獲勝時,整段改播「<獲勝隊伍> 贏得比賽」
- 發球區左右一律用實際球場方向,取消上方隊伍鏡像;畫面「發球區」顯示同步改為不鏡像,與語音一致

根本原因:
- 現場記分需要即時聽到比分與發球位置,原本只唸「誰得分、誰發球」較不直覺
- 先前發球區對上方隊伍做鏡像,導致語音與實際球場方向相反

影響:
- src/App.tsx:recordPoint 計算發球方比分、發球區、賽末點與獲勝隊伍,重整 voiceAnnouncement 欄位
- src/pages/ScoreboardPage.tsx:語音組字改為「X比Y(賽末點)」「OO左/右邊發球」、獲勝改播「贏得比賽」;發球區顯示移除鏡像;語音設定「播報誰得分」更名「播報比分」

修法:
- 發球方分數先報;賽末點僅在發球方再得 1 分就獲勝時觸發;發球區統一用 getServiceCourt 實際方向

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:42:05 +08:00
parent 091e654bdb
commit 9eb60baa37
2 changed files with 19 additions and 12 deletions
+11 -4
View File
@@ -89,6 +89,7 @@ type VoiceAnnouncement = {
opponentScore: number opponentScore: number
serverName: string serverName: string
serverCourt: 'left' | 'right' serverCourt: 'left' | 'right'
matchPoint: boolean
winnerTeamName: string | null winnerTeamName: string | null
} }
@@ -773,13 +774,18 @@ function App() {
: getTeamDisplayName(rightTeam) : getTeamDisplayName(rightTeam)
: null : null
// 得分方接著發球,報分以發球方分數為先;左側隊伍的發球區需鏡像對應畫面 // 得分方接著發球,報分以發球方分數為先;發球區一律用實際球場左右,不做鏡像
const servingScore = side === 'left' ? nextScoreState.scoreLeft : nextScoreState.scoreRight const servingScore = side === 'left' ? nextScoreState.scoreLeft : nextScoreState.scoreRight
const opponentScore = side === 'left' ? nextScoreState.scoreRight : nextScoreState.scoreLeft const opponentScore = side === 'left' ? nextScoreState.scoreRight : nextScoreState.scoreLeft
const serverCourt = const serverCourt = getServiceCourt(servingScore)
// 只有發球方(剛得分那隊)再得 1 分就能贏,才算賽末點。
const matchPoint =
!reachedTarget &&
hasWonGame(
side === 'left' side === 'left'
? getMirroredCourt(getServiceCourt(servingScore)) ? { ...nextScoreState, scoreLeft: nextScoreState.scoreLeft + 1 }
: getServiceCourt(servingScore) : { ...nextScoreState, scoreRight: nextScoreState.scoreRight + 1 },
)
setScoreHistory((current) => [...current, { pointLog, scoreState }]) setScoreHistory((current) => [...current, { pointLog, scoreState }])
setPointLog(nextPointLog) setPointLog(nextPointLog)
@@ -790,6 +796,7 @@ function App() {
opponentScore, opponentScore,
serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side), serverName: getNextServerName(nextScoreState, leftTeam, rightTeam, side),
serverCourt, serverCourt,
matchPoint,
winnerTeamName, winnerTeamName,
}) })
+7 -7
View File
@@ -3,7 +3,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { import {
getCourtAssignments, getCourtAssignments,
getMirroredCourt,
getReceivingPlayer, getReceivingPlayer,
getServiceCourt, getServiceCourt,
getServingPlayer, getServingPlayer,
@@ -64,6 +63,7 @@ type ScoreboardPageProps = {
opponentScore: number opponentScore: number
serverName: string serverName: string
serverCourt: 'left' | 'right' serverCourt: 'left' | 'right'
matchPoint: boolean
winnerTeamName: string | null winnerTeamName: string | null
} | null } | null
targetDate: string targetDate: string
@@ -260,7 +260,11 @@ export function ScoreboardPage({
const parts: string[] = [] const parts: string[] = []
if (voiceSettings.announceScore) { if (voiceSettings.announceScore) {
parts.push(`${voiceAnnouncement.servingScore}${voiceAnnouncement.opponentScore}`) parts.push(
`${voiceAnnouncement.servingScore}${voiceAnnouncement.opponentScore}${
voiceAnnouncement.matchPoint ? ' 賽末點' : ''
}`,
)
} }
if (voiceSettings.announceServer && voiceAnnouncement.serverName) { if (voiceSettings.announceServer && voiceAnnouncement.serverName) {
@@ -484,11 +488,7 @@ export function ScoreboardPage({
onSwapPlayers={() => onSwapTeamPlayers('left')} onSwapPlayers={() => onSwapTeamPlayers('left')}
onSwapTeams={onSwapMatchup} onSwapTeams={onSwapMatchup}
score={scoreState.scoreLeft} score={scoreState.scoreLeft}
serviceCourt={ serviceCourt={scoreState.serving === 'left' ? servingCourt : null}
scoreState.serving === 'left' && servingCourt
? getMirroredCourt(servingCourt)
: null
}
showServingPrompt={scoreState.serving === null} showServingPrompt={scoreState.serving === null}
team={leftTeam} team={leftTeam}
teamSlot="top" teamSlot="top"