Score System v2 状态、计算与兼容契约
Score System v2 状态、计算与兼容契约
本文负责当前比分模型、算法、普通/自由模式隔离及持久化兼容。源码入口:比分模型、MatchScoreService、ScorePageViewModel、共享数据。
当前实现状态:旧比分写入路径清理已完成。现有 Core.Models.Game 已持有 MatchScoreState;普通对局进度继续由 Games 保存 Score System v2 半场结果,自由对局则由同一状态内独立的 FreeScore 保存完全手动的比分,两套数据互不写入。后台 ScorePageViewModel 根据 CurrentGame.GameProgress 分流命令:普通进度写入当前 V2 半场,自由进度重复累加预设或打开真实 v3 布局手动编辑器。ScoreSurWindow / ScoreHunWindow / ScoreGlobalWindow 继续使用既有 CurrentGame.MatchScore 绑定路径,派生属性会按当前模式选择 V2 或自由状态。Team.Score、ScoreWindowViewModel 总分字段和 FrontedWindowService 动态比分控件都不是权威来源。
Score System v2 的核心目标是把权威比分状态放回现有 Core.Models.Game,让比分可以随对局导入、导出、回溯,并能在 SharedDataService.NewGame() 创建新对局时像 MapV2Dictionary 一样从上一局 CurrentGame 延续必要状态。
本文中的“小比分(MinorScore)”指每半场、每个比分系统 Game 或全场累计的分值。首次说明后,本文会使用“小比分”或 MinorScore。未来代码模型命名应优先使用 MinorScore,避免使用含糊的英文泛称。
1. 权威状态边界
当前比分在 Game.MatchScore,不在页面 VM、WindowService 或控件中保存第二份数据库。旧 Team.Score 仅保留 JSON/DTO 兼容;默认布局已绑定 MatchScore。旧布局若仍绑定 Team.Score 不会自动获得新派生结果;使用当前默认布局或显式修改绑定,不能通过运行时回写旧镜像掩盖差异。
2. 核心设计决策
权威比分状态必须由现有 Core.Models.Game 持有。
ISharedDataService
└─ CurrentGame
├─ SurTeam / HunTeam
├─ GameProgress
├─ MapV2Dictionary
└─ MatchScoreState <-- Score System v2 权威比分状态这里需要区分两个“Game”:
| 名称 | 含义 |
|---|---|
现有 Core.Models.Game | 应用当前对局记录对象,保存队伍、BP 状态、地图、Ban、选手数据,并保存 MatchScoreState。 |
| Score System v2 的 Game | 比分系统领域术语,指 Game 1、Game 2、Game 3 Overtime 这样的计分单元,每个 Game 包含 First Half 和 Second Half。Identity V 赛事语境中常用 “Game {x} First Half / Second Half”。 |
文档中的 Game 是比分系统领域术语;实现时为避免与现有 Core.Models.Game 冲突,使用 ScoreGame 作为类型名。
不应把权威比分状态放在:
| 不应存放处 | 原因 |
|---|---|
Team | 队伍信息应可跨对局复用;比分记录属于某场比赛/某个对局进程,不是队伍静态属性。 |
ScorePageViewModel | 页面 ViewModel 是 UI 控制层,不能作为比分数据库。 |
ScoreWindowViewModel | 前台窗口 ViewModel 应暴露绑定状态,不应独占计算结果。 |
FrontedWindowService | 服务可以操作当前比分,但不应通过控件属性保存比分。 |
| 前台 UI 控件 | 控件只能显示状态,不能成为状态来源。 |
Team.Score 暂时不能从模型层立即删除,因为旧 JSON 和旧 DTO 仍可能包含 Team.Score.Win、Team.Score.Tie、Team.Score.GameScores。它不再是运行时兼容镜像,MatchScoreState 不会把派生比分写回 Team.Score。新模型不以它作为权威写入点,后台 ScorePage 也不提供手动 Team.Score 累加入口;CutSceneWindow 和 GameDataWindow v3 默认布局已改为读取 CurrentGame.MatchScore.CurrentSurTeamMajorText / CurrentGame.MatchScore.CurrentHunTeamMajorText。
3. 新模型概念
3.1 MatchScoreState
MatchScoreState 由 Game 持有,表示整场比赛的比分状态。
| 责任 | 说明 |
|---|---|
| 权威存储 | 保存所有已记录半场结果、队伍/阵营映射和可序列化比分数据。 |
| 可序列化 | 作为 Game 的一部分导入导出,支持赛后回溯。 |
| 可延续 | 新建 Game 时可从上一局 CurrentGame.MatchScore clone 或 carry。 |
| 派生显示字段 | 暴露前台和后台可绑定字段,避免 UI 层重复计算。 |
| 重算入口 | 在半场结果、队伍换边、BO 模式或进度变化后统一重算派生值。 |
已实现结构:
MatchScoreState
├─ Games: collection<ScoreGame>
├─ CurrentGameScore: ScoreGame? [JsonIgnore]
├─ CurrentHalf: ScoreHalf? [JsonIgnore]
├─ HomeMajorWin
├─ HomeMajorTie
├─ AwayMajorWin
├─ AwayMajorTie
├─ HomeMajorText
├─ AwayMajorText
├─ HomeTotalMinorScore
├─ AwayTotalMinorScore
├─ CurrentSurTeamTotalMinorScore
├─ CurrentHunTeamTotalMinorScore
├─ CurrentSurTeamMinorHalfScoreText
├─ CurrentHunTeamMinorHalfScoreText
├─ CurrentSurTeamMinorScoreText (Game-level 权威)
├─ CurrentHunTeamMinorScoreText (Game-level 权威)
├─ CurrentSurTeamMinorGameScoreText (已弃用别名,已从设计器隐藏)
├─ CurrentHunTeamMinorGameScoreText (已弃用别名,已从设计器隐藏)
├─ CurrentSurTeamMajorText
└─ CurrentHunTeamMajorText派生字段使用重算后写入的 observable 属性,并以 [JsonIgnore] 排除在持久化之外。Games 内的半场结果、记录时主客队映射和 ScoreGameKey 是持久化数据。
前台小比分严格分为三层:
| 层级 | 字段 | 语义 |
|---|---|---|
| Half-level | CurrentSurTeamMinorHalfScoreText / CurrentHunTeamMinorHalfScoreText | 当前 GameProgress 对应的单独 ScoreHalf,按当前 Sur/Hun 队伍的 Home/Away 身份读取。 |
| Game-level | CurrentSurTeamMinorScoreText / CurrentHunTeamMinorScoreText | 当前 ScoreGame 从 FirstHalf 累计到当前 Half 的已记录小比分。 |
| Match-level | CurrentSurTeamTotalMinorScore / CurrentHunTeamTotalMinorScore | 整场比赛所有已记录半场的小比分累计,按当前 Sur/Hun 队伍身份读取。 |
CurrentSurTeamMinorGameScoreText 和 CurrentHunTeamMinorGameScoreText 已弃用([Obsolete] + [FrontedBindingIgnore]),仅作为兼容旧绑定路径的别名保留,等价于 CurrentSurTeamMinorScoreText / CurrentHunTeamMinorScoreText;已从设计器绑定浏览器隐藏,运行时仍可解析旧路径。CurrentSurTeamMinorScoreText / CurrentHunTeamMinorScoreText 为 Game-level 权威字段,旧 v3 package 与旧布局的绑定路径即使用此名称。两套字段只有一份权威计算结果。CurrentGameScore 与 CurrentHalf 不是持久化状态,而是直接使用 GetGame(_currentDisplayProgress, _currentDisplayIsBo3Mode) 和 GetHalf(_currentDisplayProgress, _currentDisplayIsBo3Mode) 解析,避免复制 GameProgress 映射。
例如:
FirstHalf = 5:0
SecondHalf = 2:2
当前 SecondHalf 时:
HalfScore = 2:2
GameScore = 7:2
MatchTotal = 整场所有已记录 Half 的累计这里的 CurrentSurTeam... / CurrentHunTeam... 始终是 Team 视角,不是固定的 Sur/Hun camp 字段直取。Half-level 和 Game-level 都通过 HomeMinorScore / AwayMinorScore 及当前队伍 TeamType 映射读取,因此换边后历史比分仍归属正确队伍。
3.2 ScoreGame
ScoreGame 表示比分系统中的一个 Game,例如 Game 1 Normal、Game 3 Overtime。每个 ScoreGame 由一上一下两个半场组成。
| 字段 | 说明 |
|---|---|
GameNumber | Game 编号,范围 1..5。 |
GameKind | Normal 或 Overtime,实现类型为 ScoreGameKind。 |
FirstHalf | 第一半。 |
SecondHalf | 第二半。 |
HomeMinorScore / AwayMinorScore | 两半都有结果时计算出的 Game-level MinorScore。 |
MajorResult | 两半都有结果时计算出的 Game major result,实现类型为 ScoreGameMajorResult。 |
这个 ScoreGame 只有在 FirstHalf.Result != null 且 SecondHalf.Result != null 时才完整。未完整时,不参与大比分计算。
3.3 ScoreHalf
ScoreHalf 表示某个 ScoreGame 的一半。
| 字段 | 说明 |
|---|---|
GameProgress | 对应当前半场进度。 |
Result | 可空 GameResult;null 表示未记录或已清空。 |
SurTeamTypeWhenRecorded | 记录时求生者侧对应主队还是客队,计算主客得分时使用。 |
HunTeamTypeWhenRecorded | 记录时监管者侧对应主队还是客队,计算主客得分时使用。 |
SurMinorScore / HunMinorScore | 从 GameResult 派生出的求生者/监管者小比分。 |
HomeMinorScore / AwayMinorScore | 根据记录时求生/监管侧与主客队映射派生出的主客小比分。 |
半场必须保存“记录时”的队伍/阵营映射,不能只在显示时读取当前 SurTeam / HunTeam。原因是比分记录需要可回溯;后续换边或导入队伍信息不应改变过去半场的历史得分归属。
3.4 ScoreGameKey
ScoreGameKey 用于稳定定位一个 ScoreGame。
| 字段 | 说明 |
|---|---|
GameNumber | 1..5。 |
GameKind | Normal / Overtime,实现类型为 ScoreGameKind。 |
示例:
ScoreGame 3 Normal -> { GameNumber: 3, GameKind: Normal }
ScoreGame 3 Overtime -> { GameNumber: 3, GameKind: Overtime }
ScoreGame 5 Overtime -> { GameNumber: 5, GameKind: Overtime }3.4.1 GlobalScoreRow cell 布局
ScoreGlobalWindow 的 v3 GlobalScoreRow 不再根据 MajorGameGap / HalfGameGap 自动展开比分格。布局 JSON 中的父级 GlobalScoreRowControlConfig 保存队伍方向和默认样式,Cells 中的每个 GlobalScoreCellConfig 通过 GameNumber、GameKind、HalfKind 指向一个 ScoreHalf,并用相对父行的 X/Y/Width/Height 定位。运行时只用这些 key 从 CurrentGame.MatchScore.Games 解析显示值;缺失半场或 Result == null 时显示 -。BO3 与 BO5 的差异由通用 Canvas state 保存,因此 BO3 state 可以有独立的 cell 列表和相对坐标。
3.5 IMatchScoreService / MatchScoreService
IMatchScoreService 是操作层,所有命令都作用于 ISharedDataService.CurrentGame.MatchScore。服务不拥有权威状态。
| 命令 | 行为 |
|---|---|
CurrentHalf / GetHalf(...) | 根据 CurrentGame.GameProgress 定位当前 ScoreHalf。 |
SetCurrentHalfResult(GameResult result) | 写入当前半场结果,并记录当时主客队阵营映射。 |
ClearCurrentHalfResult() | 把当前半场结果设为 null。 |
Recalculate() | 从当前 BO 模式可见的 ScoreGame 重新计算大比分、总小比分和前台派生字段。内部调用 MatchScoreState.Recalculate(bool isBo3Mode),统计范围由 BO3/BO5 可见性决定,不依赖当前进度。 |
RefreshCurrentProgress() | 结合当前 GameProgress、当前求生/监管队伍和 BO3/BO5 状态刷新局内显示字段。 |
ScorePageViewModel
└─ IMatchScoreService.SetCurrentHalfResult(...)
└─ ISharedDataService.CurrentGame.MatchScore
└─ Recalculate()
├─ 通知后台页面刷新
├─ 通知 ScoreSurWindow / ScoreHunWindow 绑定刷新
└─ 通知 ScoreGlobalWindow 绑定刷新4. GameProgress 映射
GameProgress.Free 不对应任何 V2 半场,因此 GetGame() / GetHalf() 仍返回 null。自由模式通过 MatchScoreState.FreeScore 进入独立手动计分流程,不伪造 V2 的当前 Game 或 Half。
GameProgress | Score System v2 mapping |
|---|---|
Game1FirstHalf / Game1SecondHalf | ScoreGame 1 Normal |
Game2FirstHalf / Game2SecondHalf | ScoreGame 2 Normal |
Game3FirstHalf / Game3SecondHalf | ScoreGame 3 Normal |
Game3OvertimeFirstHalf / Game3OvertimeSecondHalf | ScoreGame 3 Overtime |
Game4FirstHalf / Game4SecondHalf | ScoreGame 4 Normal |
Game5FirstHalf / Game5SecondHalf | ScoreGame 5 Normal |
Game5OvertimeFirstHalf / Game5OvertimeSecondHalf | ScoreGame 5 Overtime |
Free | 不映射 V2 半场;使用独立 FreeScore。 |
映射必须显式维护。当前 GameProgress enum 中 Game4FirstHalf 与 Game3OvertimeFirstHalf 共用数值 6,Game4SecondHalf 与 Game3OvertimeSecondHalf 共用数值 7。实现中 MatchScoreState.GetGame(progress) 在缺少上下文时保守按 BO5 第四局解析,MatchScoreService 会结合 ISharedDataService.IsBo3Mode 调用带上下文的解析来区分“Game 3 Overtime”和“Game 4 Normal”。
5. 计分规则
以下计分规则基于当前项目实现,不声明为官方赛事规则。
GameResult | 求生者小比分 | 监管者小比分 |
|---|---|---|
Escape4 | 5 | 0 |
Escape3 | 3 | 1 |
Tie | 2 | 2 |
Out3 | 1 | 3 |
Out4 | 0 | 5 |
5.1 Null 结果行为
ScoreHalf.Result == null 表示未记录或已清空。
| 场景 | 行为 |
|---|---|
| 小比分合计 | 不参与小比分合计。 |
| 大比分计算 | 不参与大比分计算。 |
| 全局比分格 | 显示 -。 |
| 全局比分格阵营图标 | 隐藏。 |
| 局内小比分 | 当前半场所在 Game 的所有已记录半场(含当前半场)小比分累计;当前半场无结果时仅累计之前已记录的半场;当前半场为第一半且未记录时显示 0。 |
空结果不等价于平局,也不等价于 0:0 已记录结果。
5.2 半场派生
ScoreHalf.Result
-> SurMinorScore / HunMinorScore
-> 根据 SurTeamTypeWhenRecorded / HunTeamTypeWhenRecorded
-> HomeMinorScore / AwayMinorScore如果记录时主队是求生者:
HomeMinorScore = SurMinorScore
AwayMinorScore = HunMinorScore如果记录时主队是监管者:
HomeMinorScore = HunMinorScore
AwayMinorScore = SurMinorScore5.3 ScoreGame 派生
ScoreGame 只有两半都非空时才计算 Home/Away MinorScore 和 Game major result:
HomeMinorScore = FirstHalf.HomeMinorScore + SecondHalf.HomeMinorScore
AwayMinorScore = FirstHalf.AwayMinorScore + SecondHalf.AwayMinorScore
if HomeMinorScore > AwayMinorScore:
majorResult = HomeWin
else if AwayMinorScore > HomeMinorScore:
majorResult = AwayWin
else:
majorResult = Tie如果任一半为 null,该 ScoreGame 不完整:
| 派生项 | 行为 |
|---|---|
HomeMinorScore / AwayMinorScore | 不作为完整 ScoreGame 的小比分输出。 |
| 大比分胜/平 | 不计算。 |
| 全场总小比分 | 只累加已记录半场的小比分;空半场不累加。 |
大比分统计范围
MatchScoreState.Recalculate(bool isBo3Mode) 统计当前 BO 模式下可见的 ScoreGame,不依赖当前 GameProgress,也不使用 Games 的索引位置或 GameNumber 当作遍历边界。
foreach game in Games where ScoreGameVisibility.IsVisibleInBoMode(game.Key, isBo3Mode):
count MajorResult from game
count recorded half minor scores from gameBO3 可见范围是 Game 1、Game 2、Game 3、Game 3 Overtime。BO5 可见范围是 Game 1、Game 2、Game 3、Game 4、Game 5、Game 5 Overtime。即使当前进度是 GameProgress.Free,重算也会按传入 BO 模式重新派生总分;没有可见已记录结果时,总分会归零。
5.4 全场派生
MatchScoreState 从所有 ScoreHalf / ScoreGame 派生以下字段:
| 字段 | 说明 |
|---|---|
HomeMajorWin / HomeMajorTie | 主队大比分胜/平。 |
AwayMajorWin / AwayMajorTie | 客队大比分胜/平。 |
HomeMajorText / AwayMajorText | 前台大比分文本,建议保持当前 W{Win} D{Tie} 风格。 |
HomeTotalMinorScore / AwayTotalMinorScore | 所有已记录半场的主客小比分合计。 |
CurrentSurTeamTotalMinorScore / CurrentHunTeamTotalMinorScore | 将全场主客总小比分按当前求生者/监管者阵营映射后的合计;换边时随当前队伍映射刷新。 |
CurrentSurTeamMinorHalfScoreText / CurrentHunTeamMinorHalfScoreText | 当前单独 Half 的小比分文本;当前 Half 未录入时显示 -,合法的 0 分显示 0。 |
CurrentSurTeamMinorScoreText / CurrentHunTeamMinorScoreText | 当前 ScoreGame 从 FirstHalf 累计到当前 Half 的小比分文本。当前 SecondHalf 未录入时,仍可显示已录入的 FirstHalf 累计。 |
CurrentSurTeamMinorGameScoreText / CurrentHunTeamMinorGameScoreText | 已弃用别名([Obsolete] + [FrontedBindingIgnore]),等价于对应的 *MinorScoreText;已从设计器隐藏,运行时仍可解析旧绑定路径,将在后续版本移除。 |
CurrentSurTeamMajorText | 当前求生者队伍对应的大比分文本。 |
CurrentHunTeamMajorText | 当前监管者队伍对应的大比分文本。 |
6. ScoreSurWindow / ScoreHunWindow 行为
ScoreSurWindow 和 ScoreHunWindow 已迁移为 v3 renderer。内置默认 layout 已从旧的 Team.Score 字段切换到 CurrentGame.MatchScore 派生字段:
| 窗口 | 旧绑定 | 当前默认绑定 |
|---|---|---|
ScoreSurWindow | CurrentGame.SurTeam.Score.MajorPointsOnFront、CurrentGame.SurTeam.Score.GameScores | CurrentGame.MatchScore.CurrentSurTeamMajorText、CurrentGame.MatchScore.CurrentSurTeamMinorScoreText |
ScoreHunWindow | CurrentGame.HunTeam.Score.MajorPointsOnFront、CurrentGame.HunTeam.Score.GameScores | CurrentGame.MatchScore.CurrentHunTeamMajorText、CurrentGame.MatchScore.CurrentHunTeamMinorScoreText |
局内比分窗口显示规则(旧布局继续使用兼容的 Game-level 字段;新布局可分别选择 Half-level 和 Game-level 字段):
| 当前半场 | 显示小比分 |
|---|---|
| 当前半场为第一半 | 显示第一半已记录的 MinorScore(按当前阵营映射);未记录显示 0。 |
| 当前半场为第二半 | 显示同 Game 内第一半 + 第二半已记录 MinorScore 之和(按当前阵营映射);第二半未记录时只累计第一半。 |
Free 进度 | 显示自由状态中按当前阵营映射的可手动编辑当前小分。 |
| 其他无对应 Game 的进度 | 显示 0。 |
累计小比分按当前阵营映射。每个 ScoreHalf 的 HomeMinorScore / AwayMinorScore 由记录时阵营派生,多半场累加以 Home/Away 为稳定身份,再按当前阵营映射到求生者/监管者窗口;记录后发生换边时,历史得分归属仍正确(与全局比分格的阵营映射方式一致)。
例:第一半录入 Escape4(5:0),切到第二半未录入时显示 5:0;第二半录入 Tie(2:2)后显示 7:2。
在相同数据下,Half-level 始终只显示当前半场:第二半未录入显示 -:-,录入 Tie 后显示 2:2。这与 Game-level 在未录入第二半时仍显示第一半累计 5:0 的行为不同,二者差异是预期契约。
7. 后台 ScorePage 行为
后台 ScorePage 的编辑对象由 CurrentGame.GameProgress 决定。页面按钮只表达“给当前半场写入一个结果”,不再直接累加 Team.Score.GameScores。
| 按钮 | 新行为 |
|---|---|
Escape4 | SetCurrentHalfResult(GameResult.Escape4) |
Escape3 | SetCurrentHalfResult(GameResult.Escape3) |
Tie | SetCurrentHalfResult(GameResult.Tie) |
Out3 | SetCurrentHalfResult(GameResult.Out3) |
Out4 | SetCurrentHalfResult(GameResult.Out4) |
Clear | ClearCurrentHalfResult(),即把当前半场结果设为 null。 |
新模型不需要 IsGameFinished 作为核心概念。是否完成由 GameResult != null 表达。不要为旧交互重新创建第二份完成状态。
当前后台实现不再暴露普通 UI 的 IsGameFinished、手动 Game/half 选择、手动 Team.Score 累加或“同步至前台”流程。清除当前半场比分按钮位于旧“小比分清零”按钮位置,会把全局 CurrentGame.GameProgress 对应半场的 ScoreHalf.Result 设为 null;后台预览表和 ScoreGlobalWindow 都由 CurrentGame.MatchScore 绑定自动刷新。
后台 ScorePage 的预览表是导播检查用只读 UI。普通进度从 CurrentGame.MatchScore.Games 重建;自由进度则从 FreeScore.GlobalGames 重建。每行同时显示该半场的小比分,以及当前 BO 模式下按场次、上下半场顺序累计到该行的主客队总小比分;未记录半场不增加累计值,后续半场不影响之前行的累计值,用户排序只改变展示顺序。自由进度仅累计 IsCompleted 半场,预览累计值独立于手动编辑的 FreeScore.Home.TotalMinorScore / Away.TotalMinorScore,不会反写手动总分。两者都使用 ScoreGameKey 区分 Game 3 Overtime 与 Game 4,不把 GameProgress 原始数值作为唯一判断依据。自由全局比分通过独立编辑器修改,预览表本身不承担编辑职责。
自由对局模式下,比分页面隐藏预览表,经典后台也隐藏预览表入口;若抽屉已经打开,进入自由模式时立即关闭。自由比分继续通过手动编辑器查看和修改。
7.1 自由模式控制
比分模型传播时只由源字段或汇总通知触发上层计算,派生属性通知继续用于绑定,不重复驱动整场重算。比分页只在整场显示更新后的 CurrentHalf 通知刷新预览;页面命令涉及多个字段时,合并到命令结束后刷新一次。页面刷新本身不再调用比分重算。MatchScoreService 初始化时建立当前进度和 BO 上下文。前台 GlobalScoreRow 同样监听完成显示更新的通知,并复用现有比分格,只更新文本和阵营图标状态,避免每条属性通知都重建控件、模板和图标。
自由模式标题旁显示警示说明,V2 ToggleStyled 结果组折叠,五个结果按钮改为可重复点击的累加预设。每次预设按照当前求生者/监管者与主客队映射,同时增加对应队伍的当前小分和总小分。大比分结算比较主客当前小分:胜者增加一个胜场,同分时双方各增加一个平场,随后只清零当前小分。自由总重置会清除当前小分、总小分、胜/平以及全部自由 ScoreGlobal 单格,不修改 Games。
局内比分与 ScoreGlobal 的手动编辑入口只在自由模式显示。编辑器使用活动布局包和真实 v3 renderer;写入立即反映到前台绑定,关闭不回滚。切出自由模式或替换当前 Game 时,已打开的编辑器立即关闭并解除订阅。
8. ScoreGlobalWindow 行为
ScoreGlobalWindow 已迁移到 v3 renderer,默认布局位于 Resources/FrontedLayouts/ScoreGlobalWindow.json。它绑定现有 Core.Models.Game 持有的 MatchScoreState,不依赖 ScoreWindowViewModel 独有字段或 FrontedWindowService 直接变更控件。
当前状态流:
ScorePage button
-> IMatchScoreService
-> CurrentGame.MatchScore
-> ScoreGlobalWindow binding全局比分格规则:
| 状态 | 文本 | 阵营图标 |
|---|---|---|
GameResult == null | - | 隐藏 |
| 有结果且该队当半为求生者 | 对应小比分 | 显示求生者图标 |
| 有结果且该队当半为监管者 | 对应小比分 | 显示监管者图标 |
全局比分格表示 ScoreGame 内部的 ScoreHalf 结果,由内置 v3 控件 GlobalScoreRow 的 Cells 显式配置。总分显示从 MatchScoreState 派生,不再从 ScoreWindowViewModel 独有字段或 FrontedWindowService UI mutation 派生。每个 cell 用 ScoreGameKey 和 ScoreHalfKind 定位比分,避免依赖 GameProgress 原始数值。阵营图标颜色由 CampIconColor 控制,支持黑/白两种填充色;运行时基于原始阵营图标资源的 alpha 直接填充颜色,不需要维护额外黑色图标资源。ScoreGlobalWindow/BaseCanvas 使用 Designer v3 通用 Canvas BO states:BO5 是 root/default state,BO3 是 BoModeStates["Bo3"],背景、总分位置、父行框和 cell 列表都由对应 state 决定;窗口订阅 IsBo3ModeChanged 后会重新应用 v3 布局,让 BO3/BO5 state 即时刷新。
自由模式下,GlobalScoreRow 改读 FreeScore.GlobalGames。每个半场独立保存完成状态、主客整数比分与双方阵营图标;未完成时显示 - 并隐藏图标,取消完成不会删除隐藏值,重新完成后恢复。首次完成初始化为 0:0 和当前主客阵营;预设阵营与赛果只做一次性填充,之后数字和双方图标可形成非标准组合。自由总小分不由这些单格求和。
9. 导入、导出与新建对局
MatchScoreState 是现有 Core.Models.Game 的一部分,因此:
| 场景 | 行为 |
|---|---|
| 导出对局 | 序列化 Game.MatchScore,包括 Games 与 FreeScore。 |
| 导入对局 | 从 JSON 恢复 V2 与自由状态;旧 JSON 缺少 FreeScore 时创建空自由状态,不从 Team.Score 或 V2 推断。 |
| 新建对局 | 从旧 CurrentGame.MatchScore clone/carry 到新 Game;V2 与自由状态均深复制。 |
| 回溯对局 | 不依赖页面 ViewModel 是否还存在,不依赖前台窗口是否打开。 |
新建对局的 carry 行为和当前地图状态类似:SharedDataService.NewGame() 先 clone 整个 CurrentGame.MatchScore,再构造新的 Core.Models.Game。新旧对局的 V2 与自由状态内容相同但可变实例彼此独立。
10. 兼容策略
| 旧能力 | 迁移期策略 |
|---|---|
Team.Score.GameScores | 仅作为旧 JSON/旧 DTO 反序列化兼容字段保留;运行时不再从 MatchScoreState 派生写回。后台 ScorePageViewModel 和默认 v3 布局不把它作为权威写入点。 |
Team.Score.MajorPointsOnFront | 仅随旧 Team.Score.Win / Tie 自身格式化;局内比分 v3 默认布局读取 MatchScoreState 派生字段。 |
ScorePageViewModel.GameGlobalInfoRecord | 已移除。 |
ScoreWindowViewModel.TotalMainGameScore / TotalAwayGameScore | 已移除;总小比分直接绑定 CurrentGame.MatchScore.HomeTotalMinorScore / AwayTotalMinorScore。 |
FrontedWindowService.SetGlobalScore* / ResetGlobalScore | 已完全移除;全局比分窗口状态由 CurrentGame.MatchScore 驱动。 |
12. 重要警告
- 不要把权威比分状态存到 UI 控件里。
- 不要继续让
ScorePageViewModel作为比分数据库。 - 不要把新的 v3 前台控件绑定到
ScoreWindowViewModel独有字段。 - 不要把
MatchScoreState派生值同步回Team.Score;旧字段只用于旧数据兼容。 GameProgress.Free不能写入 V2 当前半场;只能写入独立FreeScore,切换模式时不得互相同步或推断。Game3Overtime*与Game4*当前 enum 数值重叠,映射实现必须显式处理上下文。
13. 已确定边界与待清理项
| 问题 | 当前建议 |
|---|---|
Free 模式比分语义 | 使用独立持久化自由状态,允许预设累加、任意整数和真实布局手动编辑,不写入 V2 Games。 |
| BO3 中第三场加赛与 BO5 第四场的持久化 key | 使用 ScoreGameKey,避免只靠 GameProgress 数值。 |
旧 Team.Score 字段何时删除 | 等旧 DTO 和 legacy 导入路径完全收口后再删除。旧记录中的 Team.Score 无法安全还原完整 per-Game/per-Half 历史;导入器不会伪造半场结果。 |
| 全局比分 v3 控件类型 | 已新增内置 GlobalScoreRow,通过统一 V3 Control API(FrontedV3ControlBase + [FrontedV3Control] + AddFrontedV3Control<TControl>())注册并由 JSON ControlType = "GlobalScoreRow" 使用。 |
