战斗裁定权
在联机世界里,回合由服务器裁定,客户端只负责绘制。
这句话写起来容易,做到很难。多数号称"服务器裁定"的战斗系统,对结果是裁定的,对通往结果的一路却含糊不清——而玩家真正看着的,正是这一路。如果伤害由服务器决定,而"这一击是否效果绝佳"由客户端决定,那么看同一场战斗的两个玩家,会看到两场不同的战斗。
所以服务器不发送结果。它逐条发送发生了什么,而客户端的工作就是把那些原样演出来。

这张截图里没有一样东西来自本地判断。野生的绿毛虫是服务器挑的,它的等级随之而来,右侧四个行动是客户端被允许提供的全部——每一个都变成一个请求,而不是一个结果。底部的标记(32ms · Server: US-East)和顶部的频道条属于同一个会话:这是一条连接,不是在大厅上拧了一场战斗。
一个回合会传回什么
每个提交的行动都以一条回合记录返回。有意思的是其中有多少是叙述而不是数字:
| 字段 | 客户端拿它做什么 |
|---|---|
PlayerMoveName、EnemyMoveName | 说出真正裁定了的招式名称 |
PlayerDamageDealt、EnemyDamageDealt | 实际落下的伤害 |
PlayerHpRemaining、EnemyHpRemaining | 服务器声明还剩多少体力 |
PlayerFainted、EnemyFainted | 谁倒下了 |
BattleEnded | 终局:胜、负、捕获或逃走 |
FleeAttempted、FleeSucceeded、FleeChance、RunAwayAttempts | 逃走的完整经过 |
EnemySubstituteMonsterId、EnemySubstituteSpeciesId | 训练师的怪物倒下时,是谁上场 |
TurnNumber | 服务器的回合计数,不是客户端的 |
注意体力这几个字段。客户端不会从 它以为自己拥有的体力里减去伤害——它采用服务器声明的那个数字。两者并不是一回事,而在出现分歧时只有其中一个能活下来。
也注意替补字段。训练师的怪物倒下时,服务器会说出哪一只上场,带 id 和种族。早先的版本只说发生了替换,而"有谁上场了"不足以把它画出来。
拒绝是一等的答案
有三个字段,专为最要紧的情形而存在:
| 字段 | 含义 |
|---|---|
Refused | 服务器拒绝裁定这个回合 |
RefusalReason | 为什么,用文字说明 |
RefusalFactor | 它无法建模的那个具体事物 |
当服务器遇到自己没有建模的因素——某个效果、某种道具交互、某个它不认识的版本里的规则——它会拒绝这个回合,并点名是什么把它挡住了。它不会以中立方式裁定过去。
这就是设计的全部,而替代方案比看上去更糟。一个把未知因素悄悄裁定为"没有效果"的服务器,会产出一场跑到终点、却以无人可见的方式出错的战斗。玩家看着一个招式什么都没做,于是断定这个招式很弱。拒绝是吵闹的;猜测是安静的,而代价昂贵的那种失败恰恰是安静的那种。
拒绝会展示给玩家。它绝不会被翻译成"那就在客户端裁定这场战斗"。只要有一个回合在本地裁定,服务器的记录与玩家的游戏就已经分叉,而后续没有任何环节还能说出哪一个是真的。
这换来了什么
因为回合在服务端裁定,一整类作弊手段干脆没有着力面:
- 野生遭遇由服务器生成——种族、等级、个体值和闪光。客户端无法一直重掷到自己满意。
- 捕获在服务端验证。 捕获判定不属于客户端。
- 伤害浮动、会心一击和能力等级都是服务器的判定。
- PP、挣扎和道具使用由服务器扣除,所以客户端花不出自己没有的东西。
诚实的边界
服务器裁定权的范围仅限联机模式。Core 的离线战斗路径未被触碰:同一个引擎运行单机游戏的方式和以前完全一样,任何地方都没有服务器。这条边界是刻意的:联机层是一个按需启用的模块,从不启用它的购买者也从不 为这一切付费。