跳到主要内容

状态模型

state.data 就是这名玩家此刻能看到的全部内容,每来一条新消息就顶掉上一份。

如何读取状态

规则客户端行为
收到新消息替换上一份 PlayerState,不要合并数组。
读取对象先看 kind,再按该类型读取字段。
判断归属Core 用 owner_username 表示所属玩家;Core 和 Unit 的 controlled 表示你是否能控制它。
某个字段缺失它的值未知或不适用。服务端不会发送 null
最小状态消息
{
"type": "state",
"data": {
"status": "ACTIVE",
"resources": 5,
"population": 1,
"champion_beacon": {"position": [0, 0]},
"objects": [
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"owner_username": "arena_hero",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 5,
"state": "NORMAL"
},
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 0
}
],
"events": []
}
}

需要机器可读的定义,看 AsyncAPI schema

PlayerState

字段格式必需含义
status"ACTIVE""RESPAWNING"玩家有存活的 Core,还是正在等待出生点重试。
respawn_at_tick正 int64仅重生中找不到合法出生点后,下一次尝试部署的 Tick。
resources非负整数Core 里的资源,上限为 max(10, population × 5);Worker 身上的 cargo 另算。
population非负整数存活的己方 Unit 数,不含 Core。
champion_beaconobject公开位置,以及可见时的携带状态。
objectsarray己方实体,加上当前可见的地形和敌方实体。
eventsarray发给这名玩家的结算结果。

没有内容时,objectsevents 是空数组 [],而不是干脆不出现。Core 被摧毁后通常 会在同一个 Tick 重生,因此只有首次进入世界或暂时找不到合法出生点时才会发布 RESPAWNING。期间资源和人口字段照样在,但在 CORE_RESPAWNED 到来之前没有 Core。

可以用 population 估算下一个 Unit 的价格: round_half_up(base_price × (13/10)^k),其中 k = max(0, floor((population - 20) / 5) + 1)。服务端会在同 Tick 自毁和战斗结算后 重新计算,因此最终以生产结果事件为准。

Champion Beacon

位置永远公开,其余的就看你能不能看见了。

视野外

{
"position": [120, 85]
}

你只知道它在哪儿,别的一概不知——躺在地上还是被人拿着,都看不出来。

可见且在地面

{
"position": [120, 85],
"status": "GROUND"
}

这时候没有 carrier_id

可见且被携带

{
"position": [120, 85],
"status": "CARRIED",
"carrier_id": "9d3e4941-2816-4a39-a220-df8cd95e877d"
}

carrier_id 指的是携带它的那个 Core 或 Unit。如果下一份状态里没有 statuscarrier_id,把旧值丢掉,别留着接着用。

世界对象

objects 里每一项都以 kind 开头。

kind表示身份
"CORE"一个 Coreid
"UNIT"一个 Worker、Vanguard 或 Rangerid
"OBSTACLE"所有可见障碍格单个坐标
"RESOURCE"所有可见且当前可用的资源点单个坐标
按 kind 分发
for (const object of state.objects) {
if (object.kind === 'CORE') handleCore(object);
else if (object.kind === 'UNIT') handleUnit(object);
else handleTerrain(object);
}

地形

{
"kind": "OBSTACLE",
"positions": [[4, 7], [4, 8], [5, 8]]
}
字段格式含义
kind"OBSTACLE""RESOURCE"可见地图要素类型。
positions非空 [x, y] 数组可见格,先按 x、再按 y 排序。

同一种可见要素的所有位置都合并成一项。某个 kind 整个不出现,就说明当前没有一个 位置可见。这些批次没有 id、没有 controlled、没有 HP,也没有资源数量。

OBSTACLE 位置是永久地形。RESOURCE 位置表示当前可用,而不是永久地形记忆。它可能 是自然资源点,也可能是死亡 Worker 留下的 Cargo 资源堆。一次成功采集会消耗自然点; 资源堆如果只取走一部分,同一个位置仍会继续出现。之后的补充可能在区块内其他位置生成 新的自然资源点。

Core

正常 Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"owner_username": "arena_hero",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "NORMAL"
}
迁移中的 Core
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"owner_username": "arena_hero",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "MOVING",
"move_direction": "RIGHT",
"move_progress": 2,
"move_required_ticks": 4,
"destination": [13, 8]
}
字段格式必需
kind"CORE"
idUUID
owner_username3–24 位小写字母、数字或下划线
controlledboolean
position[x, y]是;迁移期间仍是起点
hp非负整数
shield非负整数
state"NORMAL""MOVING"
move_direction方向字符串仅迁移中
move_progress正整数仅迁移中
move_required_ticks正整数仅迁移中;当前为 4
destination[x, y]仅迁移中

owner_username 是没有 @ 前缀的原始值;界面显示时写成 @${owner_username}。正常状态的 Core 一个移动字段都没有。看得见的敌方 Core 也会公开同样的 owner_username,但不会公开内部 owner UUID、邮箱或其他账号资料。

Unit

己方 Worker
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 1
}
字段格式必需
kind"UNIT"
idUUID
controlledboolean
position[x, y]
hp非负整数
unit_type"WORKER""VANGUARD""RANGER"
cargo非负整数仅己方 Worker

敌方 Worker 的 cargo 对你不可见。Vanguard 和 Ranger 则根本不带 cargo 字段, 自己的也不带。

视野

数据何时出现隐藏字段
己方 Core 和 Unit始终Core 有公开的 owner_username;Unit 不带 owner 字段
敌方 Core所在格当前可见owner_username 公开;内部 owner UUID 和账号资料隐藏
敌方 Unit所在格当前可见所属玩家;敌方 Worker 的 cargo
障碍与资源点所在格当前可见资源数量
Beacon 位置始终
Beacon 状态和携带者Beacon 所在格当前可见视野外时两个字段都没有

这里没有任何「上次看见」的时间戳。记住的障碍一直有效,但记住的资源点在重新看见 之前可能已经过期。两类探索记忆都要和服务端当前状态分开;不要把视野外的旧资源坐标 当成当前仍可用。

更新状态

每收到一份新状态,就把实体映射重建一次:

const entities = new Map();

for (const object of nextState.objects) {
if (object.kind === 'CORE' || object.kind === 'UNIT') {
entities.set(object.id, object);
}
}

服务端发对象的顺序是确定的:

  1. 障碍批次;
  2. 资源批次;
  3. 己方 Core;
  4. 按 UUID 排序的己方 Unit;
  5. 按 UUID 排序的可见敌方 Core;
  6. 按 UUID 排序的可见敌方 Unit。

空的分组会直接跳过——正因为如此,数组下标永远不能当成对象的身份。