10. 和 FPSMatch / 团队竞技联动(数值平衡)¶
做玩法服时,最常见的需求不是"加新枪",而是"把现有枪的数值按玩法血量校准"。这一节讲怎么做一套可复现、可校验的数值补丁。
10.1 为什么要校准¶
原版枪械数值是按"原版血量 + 原版战斗节奏"设计的。玩法服常见改动(例如玩家血量 32、无护甲或有特殊护甲)会让原数值失衡:
- 爆头倍率过高 → 一枪秒杀,没有交火过程;
- 霰弹"单颗弹丸伤害"口径 → 贴脸一枪毙命;
- 射速/移速组合 → 跑打流无敌。
校准的目标:让 TTK(Time To Kill,击杀所需时间)落在玩法想要的区间(例如 0.3~0.8 秒)。
10.2 只改 data,不动其它¶
数值补丁只覆盖 data/guns/<id>_data.json:
要改的:data/<ns>/data/guns/*.json ← 只动这里
不动的:index / display / geo_models / animations / textures / tacz_sounds
好处:文件小、可审查、不碰美术资源、出问题好回滚。
常改字段:
| 字段 | 用途 |
|---|---|
bullet.damage |
单发伤害(躯干击杀发数 = ceil(血量 / damage)) |
bullet.bullet_amount |
霰弹弹丸数(口径统一按"弹丸合计") |
bullet.extra_damage.head_shot_multiplier |
爆头倍率 |
bullet.extra_damage.damage_adjust |
距离衰减 |
ammo_amount |
弹匣容量 |
rpm |
射速(TTK 的另一个乘数) |
TTK 估算(单发武器):
击杀发数 = ceil(玩家血量 / 单发伤害) # 躯干
爆头发数 = ceil(玩家血量 / (单发伤害 × 爆头倍率))
TTK(秒) ≈ (击杀发数 - 1) × 60 / rpm
例:血量 32、伤害 9、rpm 600 → ceil(32/9)=4 发 → TTK ≈ 3 × 0.1 = 0.3s。
10.3 用脚本批量改(附最小示例)¶
手改十几把枪容易漏字段。用脚本做,并只改白名单内的字段:
import json, glob, os
RULE = { # 枪 id → 要改的字段
"m4a1": {"head": 2.0},
"ak47": {"head": 2.0},
"m870": {"damage": 7}, # 霰弹:按"弹丸合计"口径削弱
"aa12": {"damage": 6},
"glock_17": {"damage": 8},
}
def patch(path):
with open(path, encoding="utf-8") as f:
data = json.load(f)
gun_id = os.path.basename(path).replace("_data.json", "")
rule = RULE.get(gun_id)
if not rule:
return "skip"
before = json.dumps(data, sort_keys=True)
if "head" in rule:
data["bullet"]["extra_damage"]["head_shot_multiplier"] = rule["head"]
if "damage" in rule:
data["bullet"]["damage"] = rule["damage"]
changed = json.dumps(data, sort_keys=True) != before
if changed:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) # 干净 JSON,无注释
return "changed" if changed else "same"
for p in sorted(glob.glob("data/*/data/guns/*_data.json")):
print(p, "→", patch(p))
⚠️ 真实枪包的 JSON 可能带注释或尾逗号(非标准 JSON)。脚本改之前先确认能解析;不能解析就先清洗,或者只对"干净"的目标文件做补丁。
10.4 必须做校验(不然改坏了都不知道)¶
补丁脚本写完,配一个断言脚本:
import json, glob
def check(path):
data = json.load(open(path, encoding="utf-8"))
gun = path.split("/")[-1].replace("_data.json", "")
# ① 该改的改了
if gun in ("m4a1", "ak47"):
assert data["bullet"]["extra_damage"]["head_shot_multiplier"] == 2.0, gun
# ② 不该动的一个字节都没动(和"原包备份"逐字段比对)
# …这里放你保存的原包副本对比逻辑…
return "PASS"
for p in sorted(glob.glob("patched/**/*_data.json", recursive=True)):
print(check(p), p)
校验点: 1. 目标字段确实变了; 2. 其它字段与原包完全一致(逐字段 diff); 3. 数值在合理范围(伤害 > 0、倍率 < 10、rpm 在合理区间); 4. 所有枪的 JSON 都能解析。
10.5 两端同步与发布¶
① 生成补丁(只含 data/guns/*.json)
② 校验报告全 PASS
③ 服务端:覆盖 tacz/<枪包>/data/<ns>/data/guns/ → 重启/重载
④ 客户端:同样覆盖(或打一个"补丁包"叠加在枪包之上)→ 玩家重进游戏
⑤ 记录改动:枪 / 字段 / 原值 / 新值(方便回滚与公示)
| 坑 | 说明 |
|---|---|
| 只改客户端 | 服务端仍按旧数值判定伤害 → 玩家看到的与服务器算的不一致 |
| 只改服务端 | 客户端 HUD/预估显示旧值 → 手感怪 |
忘了 ammo_amount 与显示 |
改了弹匣容量但没看 HUD,玩法体感对不上 |
| 超模枪未处理 | 只调数值不够时,用黑名单禁枪(从 index/guns/ 移除或改 id) |
10.6 和 FPSMatch 类玩法的配合¶
玩法模组(如 FPSMatch)负责对局流程(队伍、计分、商店、HUD),枪包负责武器本身。两者配合时的注意点:
| 场景 | 注意 |
|---|---|
| 商店/装备系统按枪 id 发枪 | 枪包改 id 或删枪会让商店配置失效 |
| 玩法自己算伤害 | 若玩法侧有独立伤害逻辑,枪包的 damage 可能只影响部分场景,需实测 |
| 超模枪黑名单 | 玩法侧有黑名单配置的,枪包改动要同步维护 |
| 数值公示 | 服务器/社区通常会公开武器数值表,补丁后记得同步更新 |
💡 团队的实践:数值补丁 + "逐项 PASS/FAIL 校验报告" 一起交付,改数值这件事就变成可回滚、可复现的工程活了。