| name | draw-aa-map |
| description | 创建或调整 erArk 的 AA 地图(data/map/*/Map),包括给区块新增房间、重排整张版面、修正竖墙错位。当需要画地图/改地图/加房间,或玩家反馈地图显示歪掉、排版错乱时使用。 |
Skill Instructions
你是 erArk 的 AA 地图排版助手。完整原理说明见 地图系统 的「AA 地图排版」一节。
第零步:先记住这条铁律
AA 地图的排版由字体的实际字宽决定,不是由 wcwidth.wcswidth 决定。
MapDrawLine.width 走 Script/Core/text_handle.py:65 get_text_index()(即 wcswidth),但它不参与行内排版。see_map_panel.py:97-109 只用全局 max_width 算出一个所有行共用的居中空格前缀,之后逐段原样输出,不做任何裁剪或补齐。因此行内对齐 100% 由字体字宽决定。
⚠️ 永远不要用"数列数"的方式判断对齐——这正是历史上重画后实机仍然歪掉的原因。必须用 tools/map_aa_check.py 按像素校验。
核心模型:U 单位
在 等距更纱黑体 SC 下字符分成两个互不通约的度量族:
| 度量族 | 代表字符 | 字号 20 下 |
|---|
| 框线族 | ═ ║ │ ┃ ┄ ┅ ┆ ┇ ─ ━、▓ ♂ ♀ ¤ ▁、U+2002 | 13 px |
| 框线族(全角) | 汉字、○ ● □ ▣ ♲ ◎ ◇、- ~ ← → ↗ ↘ | 26 px |
| ASCII 族 | ASCII 空格、~、-、数字、字母 | 14 px |
| ASCII 族(双宽) | ☸ ➷ ➹ ✪ | 28 px |
两族在字号 18/22/24/26/30 下碰巧同宽,在 14/16/20/40 下不同宽。当前 config.ini 反算出的字号正好是 20(font_size = int(window_width / text_width) * 2 = int(2000/190)*2),所以每出现一个 ASCII 空格就相对框线漂移 1px。
令 U = 框线字符宽度,只用恒为 1U / 2U 的字符即可在任意字号下对齐:
| 宽度 | 可用字符 |
|---|
| 1U | 框线 ═ ║ │ ┃ ┄ ┅ ┆ ┇ ─ ━、▓ ♂ ♀ ¤ ▁、U+2002 EN SPACE(留白)、≈ ∼ ∽ ⊙ ⊕ |
| 2U | 汉字、○ ● □ ▣ ♲ ◎ ◉ ◍ ◇ ◈、~ 〜 ﹏ ⌒、← → ↗ ↘ ↖ ↙、- _ < >、U+3000 |
| 禁用 | ASCII 空格、~、-、数字字母、☸ ➷ ➹ ✪ |
留白一律用 U+2002 EN SPACE 替代 ASCII 空格,这是消除大范围错位的关键。
U+2002 与普通空格肉眼完全一样,手抄/复制粘贴极易被静默换回 ASCII 空格。因此这类地图必须由脚本生成,绝不可手改 Map 文件。
实施步骤
第一步:摸清现状
python tools/map_aa_check.py <地图名>
python tools/map_aa_check.py --list-bad
同时读一遍目标 data/map/<区块>/Map 与 Map.json,弄清现有房间的相对位置与连通关系。参考画得比较规整的 data/map/仓储/Map、data/map/疗养庭院/Map。
第二步:设计列规划(用 U 数,不是字符数)
先把版面写成一张列规划表再动手,例如:
上排 0 ┃ 健身区上 26U ┃27 木桩房 18U ┃46 射击房 8U │55 靶道 32U ┃88
走廊 健身区@11U 走廊@46U 入口0@88U
下排 0 ┃ 健身区下 26U ┃27 模拟对战室 18U ┃46 休息室 12U ┃59 更衣室 8U ┃68 淋浴 6U ┃75 游泳池 12U ┃88
要点:
- 上下排尽量共用竖墙列位,观感更整齐。
- 每个房间格的内容宽度 = 该格的 U 数;汉字算 2U,框线/留白算 1U。
- 门(可通行)用虚线
┄ / ┆,实墙用 ═ / ║,必须与 Map.json 的 PathEdge 一致。
- 总宽必须 ≤
text_width(190),否则 Tk 会按字符软换行、整张图崩坏。
第三步:写生成脚本
不要手写 Map 文件。 以 tools/map_aa_build_training.py 为模板新建生成脚本,按 U 数拼装:
S = " "
E = "═"
rest = [
S + "▓¤" + S * 6 + "¤▓" + S,
S * 12,
S * 3 + "<mapbutton>休息室</mapbutton>" + S * 3,
S * 12,
]
open("data/map/<区块>/Map", "wb").write("\r\n".join(lines).encode("utf-8"))
脚本必须在写入前自检三件事,任一不满足就报错退出、不写文件:
- 每个含竖墙的行 U 数一致
- 全图不含 ASCII 空格
- 不含裸
<...>(会被富文本正则吞掉)
文件格式:UTF-8 无 BOM + CRLF + 末尾无换行。
第四步:像素校验(必做)
python tools/map_aa_check.py <地图名>
python tools/map_aa_check.py <地图名> --tol 2
它会在 10 个字号下逐字符累加像素位置,检查各竖墙行的总宽与每堵墙的位置是否一致,超差会指出具体哪行哪堵墙差多少,并以非零退出码结束。
期望:每个字号下含竖墙的行总宽只有一个值。
第五步:若同时新增了场景,补两个数据文件
- 新建
data/map/<区块>/<房间名>/Scene.json——照抄同类场景,字段含义见 地图系统.md。⚠️ 目录名必须与 <mapbutton> 里的名字完全一致,否则 map_handle.py:363 会无限递归抛 RecursionError。
data/map/<区块>/Map.json 的 PathEdge 加双向边。
通常不需要改任何 CSV、po 或 Python:constant.place_data 由 SceneTag 自动生成。
第六步:重建缓存(必做,否则改动 100% 不生效)
map_config.init_map_data() 只判断四个缓存文件是否全部存在,没有任何 mtime/hash 校验,不删缓存就完全不生效且无任何报错:
rm -f data/SceneData data/MapData data/PlaceData data/ScenePath
python init_data.py
⚠️ 第二层缓存:save_handle.py:496-516 只在场景增删或 scene_tag/scene_img/room_area 变化时才刷新存档里的 map_data。只改 AA 图而不动 Scene.json 时,老存档读出来仍是旧地图,必须用新档验证。
第七步:收尾
update.log 加一行(新增房间记「新增:(地图)…」,重排版面记「调整:(地图)…」)
- 若改动较大,在
plan/ 下留实施记录,可参考 plan/done/plan_01_训练场新增休息室.md
陷阱速查
| 陷阱 | 依据 |
|---|
<mapbutton> 标签零宽,只有中间的名字参与排版 | map_config.py:199 |
行尾空白被 str.rstrip() 吃掉,包括 U+2002——别靠行尾补齐 | map_config.py:243 |
行首公共空格被裁,但只认 ASCII 空格(lstrip(" ")),U+2002 不受影响 | map_config.py:246 |
裸 <...> 会被 re.sub(r"<[^>]+>","") 静默吞掉;要画尖括号用全角 <> | rich_text.py:233 |
未闭合的 <mapbutton> 会让该按钮及本行后续文本静默丢失 | map_config.py:203 |
按钮名 ≠ 子目录名 → RecursionError,不是友好报错 | map_handle.py:363 |
有 Map.json 就必须有同目录 Map,否则 FileNotFoundError | map_config.py:90 |
_pad_map_lines_to_max_width() 是死代码,从未被调用 | map_config.py:345 |
常见取舍
- 想保留某个 ASCII 族字符(如
➷➹ 的观感):可以,但要把它放在最右侧的格子里,这样左侧所有竖墙不受影响,只有该行最右端偏移 2px;然后用 --tol 2 校验并在文档里写明确切代价。
- 场景名是数字(如入口
0):无法改(是目录名)。把它放在没有竖墙的标签行上即可,不产生可见错位。
- 想给新房间加等级门禁:
Facility_open.csv 是按 SceneName 匹配的,加进去会连坐所有同名场景(项目里有 9 个"休息室")。需要单独门禁必须起独立 SceneName。
完成前自查