状态模型
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_beacon | object | 是 | 公开位置,以及可见时的携带状态。 |
objects | array | 是 | 己方实体,加上当前可见的地形和敌方实体。 |
events | array | 是 | 发给这名玩家的结算结果。 |
没有内容时,objects 和 events 是空数组 [],而不是干脆不出现。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。如果下一份状态里没有 status 或
carrier_id,把旧值丢掉,别留着接着用。
世界对象
objects 里每一项都以 kind 开头。
kind | 表示 | 身份 |
|---|---|---|
"CORE" | 一个 Core | id |
"UNIT" | 一个 Worker、Vanguard 或 Ranger | id |
"OBSTACLE" | 所有可见障碍格 | 单个坐标 |
"RESOURCE" | 所有可见且当前可用的资源点 | 单个坐标 |
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
{
"kind": "CORE",
"id": "2ea3c3dc-42b0-4b92-9754-7558bd4ff834",
"owner_username": "arena_hero",
"controlled": true,
"position": [12, 8],
"hp": 5,
"shield": 4,
"state": "NORMAL"
}
{
"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" | 是 |
id | UUID | 是 |
owner_username | 3–24 位小写字母、数字或下划线 | 是 |
controlled | boolean | 是 |
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
{
"kind": "UNIT",
"id": "9d3e4941-2816-4a39-a220-df8cd95e877d",
"controlled": true,
"position": [11, 8],
"hp": 2,
"unit_type": "WORKER",
"cargo": 1
}
| 字段 | 格式 | 必需 |
|---|---|---|
kind | "UNIT" | 是 |
id | UUID | 是 |
controlled | boolean | 是 |
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);
}
}
服务端发对象的顺序是确定的:
- 障碍批次;
- 资源批次;
- 己方 Core;
- 按 UUID 排序的己方 Unit;
- 按 UUID 排序的可见敌方 Core;
- 按 UUID 排序的可见敌方 Unit。
空的分组会直接跳过——正因为如此,数组下标永远不能当成对象的身份。