Category: 更新日志

版本迭代与开发进度记录

  • 版本 1.0.374 开发日志

    构建 1.0.374(code 374)· 2026-10-02 14:55:54 · sha256 c845e48e24079e88…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 14:51:07 | 本包:kushan-1.0.374.apk(1.0.374 / code 374,sha256 c845e48e24079e88…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-DRESS-TIER  (2026-10-02 14:34:50)
    
    # TASK-DRESS-TIER —— 装扮档位(场合 × 社会地位 → 档位与构件)落地 + 取证
    
    ## 判据(68 号)
    - **D-12 装扮档位参数化**:立绘读 `dress_spec.json` tiers,界面代码零硬编码 | `grep` 界面脚本无饰具名硬编码
    - **B-16 场合互斥生效**:街面绝不出现内室构件;内室绝不穿罩袍 | 街面截图 + 内室截图对照
    - 关联:**D-10 外出罩袍外形** / **D-11 内外两形立绘**(这两条的「形」要美术层,见下「边界」)
    
    ## 本轮做了什么(此前客户端**零消费** `dress_spec.json`:只有 tools 读它)
    1. **新模块** `scripts/dress_tier.gd`(123 行,纯算不画,单一职责)
       - 读真源 `data/dress_spec.json`(70 号 §四 外出罩袍+内室香妃式饰具;用户令 2026-09-23)
       - 场合:房间 `type` ∈ 参数 `dress.inner_room_types` ⇒ 内室,否则外出
       - 档位:参数 `dress.tier_from_status`(`地位:档位` 映射)+ `dress.max_tier` 封顶
       - 适用性别:参数 `dress.apply_gender`(默认 female ⇒ 只有女眷出装束行)
       - **档名/构件名/规则文字全部读数据文件**,代码里没有一件饰具名(满足 D-12 后半句)
    2. **呈现**:NPC 卡片(`scripts/npc_panel.gd`)新增「装束」一节(`_dress_text()`)
    3. **参数**(`data/params.json`,组 `装扮`):`dress.inner_room_types` / `dress.tier_from_status` / `dress.max_tier` / `dress.apply_gender`
    4. **文案**(`data/i18n.json`):`npc.panel.dress_title` / `dress_na` / `dress_tier_n`
    
    ## 真机/渲染原文(`KUSHAN_DRESS` 自报,`KUSHAN_DRESS_DUMP=1` 门控;命令:headless 跑 `--render-panel npccard:<cid>`)
    ```
    cid=10   gender=female status=4 room=6   rtype=interior tier=4
            text=内室(家内/内院)(隐秘(深宫禁锢):蒙眼睛·勒嘴(封口)·双手在后(背缚 + 腕链)·双手合十被反绑)
    cid=6    gender=female status=3 room=587 rtype=interior tier=3
            text=内室(家内/内院)(高阶(勋贵/宗教世家):束腰·脚链 + 三寸金莲·臂环·长手链(链束双腕)·大腿环 + 贞操带·乳环 + 金链·肚脐红宝石)
    cid=17   gender=female status=1 room=5   rtype=court    tier=1
            text=内室(家内/内院)(常备(平民内室):束腰·脚链 + 三寸金莲·臂环·长手链(链束双腕))
    cid=120  gender=female status=1 room=19  rtype=street   tier=1
            text=外出(街面)·罩袍覆身、面纱只露双目、手不外露;男子须有男性亲属陪同(L-02)
    cid=77   gender=female status=1 room=522 rtype=street   tier=1   text=(同外出口径)
    ```
    ⇒ **B-16 场合互斥在数据/规则层成立**:街面**只**给罩袍、内室**只**给饰具,两套互斥;档位按地位分(1/3/4 档各不同)。
    
    ## 本轮抓到的两个真问题(都已修)
    1. **`room_id` 是 float**:JSON 数字在 GDScript 里是 float ⇒ `str(6.0)` = `"6.0"` 与房键 `"6"` 不匹配 ⇒ 判定**一律落「外出」**(我第一版就是这结果,被自报逮到)。
       修法=`str(int(room_id))` 归一(`dress_tier.gd` + 自报同源)。**教训**:客户端所有「拿 id 当字典键」的地方都要按 `str(int(...))` 归一。
    2. **i18n 护栏抓真问题**:`dress_tier.gd` 里 `"%d 档"` 是玩家可见字面量 ⇒ 外置成 `npc.panel.dress_tier_n`(这正是 C1.1 存在的意义)。
    
    ## 边界(未做,不谎报)
    - **D-10「外出罩袍外形」/D-11「内外两形立绘」仍是美术缺口**:`assets/portraits/` 里**没有**罩袍/饰具图层(`find` 无 `veil/chador/cloak/overlay` 命中),
      `dress_spec.modes.*.art` 也写着「新增外出外形一层;既有 111 张立绘不重画」 ⇒ 本轮只把**档位与构件名算对并呈现**,
    
    ### TASK-D0078-UI-THEME  (2026-10-02 14:05:45)
    
    # TASK-D0078-UI-THEME —— 四界面同主题目视核对(68 号 D-007 / D-008)
    
    真机:三星 R3CR704Q72V · **1.0.372** · 960×2616(真屏 id `4630947232161729155`)· 房 19「富贵街北」(香妃镇)
    取图方式:**真点**(轨迹球物理 `(880,2525)` → 菜单自报各项中心 → 点其物理中心),非 QA 脚本跑
    
    ## 目视四图(`md5` 各不相同 ⇒ 确实换了界面)
    | 图 | 界面 | md5 前 8 | 在哪取的 |
    |---|---|---|---|
    | `d007_room.png` | 房间页 | `a783fe4a` | 进房后直接截 |
    | `d008_shop.png` | 商店 | `9df36a75` | 菜单「🛒 商店」中心逻辑 `(519.5,883)`→物理 `(692,1177)` |
    | `d008_dialogue_say.png` | 对话(旅伴聊天面板) | `29485006` | 菜单「🧭 旅伴」中心逻辑 `(519.5,547)`→物理 `(692,729)` |
    | `d008_battle2.png` | 战斗(遭遇战·选敌) | `2d359718` | 菜单「⚔ 遭遇战」中心逻辑 `(519.5,967)`→物理 `(692,1289)` |
    
    菜单全 12 项自报(`MENU_DUMP=1` 原文,含新上的 B-14「📜 告示」):
    ```
    生命 51/50 | 克什 300.00 · 第纳尔 0.00 | 声望 +0 | 1620 冬   (519.5,354.5)
    五维 外交 2 | 军事 3 | 管理 6 | 谋略 2 | 学识 3            (519.5,451.5)
    🧭 旅伴  (519.5,547)   🎒 背包 (519.5,631)   📜 任务 (519.5,715)   🗺 世界地图 (519.5,799)
    🛒 商店  (519.5,883)   ⚔ 遭遇战 (519.5,967)  📜 告示 (519.5,1051)  💾 保存进度 (519.5,1135)
    设置 (519.5,1219)      🚪 退出游戏 (519.5,1303)
    ```
    
    ## D-008 判定:**同主题(已验·目视)**
    四张图都是「同一套纸面板 + 金/木色键面 + 同一字体」的外观;代码侧同源:
    - 四个界面全部经**同一套 UI 助手**与**同一个主题色表**出画:`scripts/room/room_layout.gd`(房间页,`theme.*` 10 处、UI 助手 16 处)、
      `scripts/battle_view.gd`(战斗,UI 助手 17 处)、`scripts/room/room_chat.gd`(对话/聊天)、商店面板同走 `UIConf`/`UI`。
    - 字体单一真源=参数 `ui.font.path`(`ui.font.enabled` 总开关)⇒ 四界面不可能各自换字体。
    - 因此判据「对话/商店/战斗套用同一主题」**成立**。
    
    老实说的两点:
    1. 战斗那张取的是**遭遇战的选敌面板**(在打斗界面的入口);真正回合内画面见前几轮 `evidence/TASK-B014-*`、`TASK-ROOM-POPUP-SHELL` 的真机图。
    2. 本机 `tools/qa_shot_ocr.py` 这次没吐出文字(OCR 链不可用)⇒ 文字证据改由 **`MENU_DUMP` 自报 + 截图** 双轨给出。
    
    ## D-007 判定:**未达标(半验)**——实测数字如下
    `KUSHAN_ROOM_LINES` 自报(逻辑坐标,窗口逻辑高 ≈ 2616/1.3333 ≈ 1962):
    - 主卡片 `框y=17 框高=1079` ⇒ 卡片自屏顶起、占到屏高 **55%**(17→1096)。
    - 卡内:房间名 `y=50 h=53`、描述 `y=129 h=18`、状态条 `y≈354`、五维 `y≈451`,随后是人物/出口区。
    - 69 号 §六 要求:**顶部状态条** + **上部 38–42% 场景图区** + **中部 33–36% 文本区** + **下部 22–26% 选项两列**。
    - **差异**:现布局**没有独立的「上部场景图区」**(场景图=全屏背景),文字卡片自顶部起贯通到 55%,
      状态条/五维/人物/出口**全在卡内**,没有「中部文本 / 下部选项两列」的竖向分区。
    
    ### TASK-B014-NOTICE  (2026-10-02 13:56:01)
    
    # TASK-B014-NOTICE —— 68 号 B-14「告示牌(律令入口)」落地 + 真机验证(2026-10-02 · 1.0.371)
    
    ## 判据(68 号 B-14 原文)
    > 镇上立告示木牌,点开列出当前生效律令 | 验法:真机点告示 → 截图
    
    ## 本轮做了什么(此前**客户端零实现**:`scripts/` 里 grep 不到「告示/律令」)
    1. **新面板** `scripts/room/notice_panel.gd`(一功能一文件):把「当前生效律令」画进房间交互区。
       - 真源:`data/laws_xiangfei.json`(70 号 §二《律令政策真源》的 `laws` 数组)+ 参数 `law.enabled_ids`(生效编号)。
       - **只读不裁**:名单读参数、正文读数据文件,面板零硬编码(文案走 I18n、条数上限走参数、样式走 UI/UIConf)。
    2. **入口(镇上才出)**:`scripts/room/room_layout.gd` 新增 `notice_here()` —— 按**所在地类型**判(`locations.type`),
       类型名单走参数 `law.notice_loc_types`(默认 `town,city,village,castle,district`);主菜单里多一项「📜 告示」。
    3. **参数**(`data/params.json`,与 `law.*` 同组):`law.notice_loc_types`(str)、`law.notice_max`(int,默认 12,min1/max40)。
    4. **文案**(`data/i18n.json`):`label.tag.notice` =「📜 告示」;`ui.room.t181~t184` = 面板标题/空表/页脚/日志。
       踩坑记:起初插到 `t017~t020`,**那几个号早被占用**(`t017` 已是「房间不存在」)⇒ JSON 允许重复键、`I18n` 取到旧值;
       改取**空闲号 t181~t184** 才对(教训:加 i18n 键前先查该命名空间的已用号)。
    
    ## 真机验证(**1.0.372** · 三星 R3CR704Q72V)—— 参数改名 `law.notice_*`→`notice.*` 后**重出包复验**
    (1.0.371 那次也 PASS,但包内参数是改名前的旧键;最终口径以本段的 1.0.372 为准:10/10 律令+页脚,附件 `b14_notice_room19_372.png`/`logcat_menuitems_372.txt`)
    - 房间:`KUSHAN_ROOM_LINES … 文="富贵街北"`(香妃镇 19 号房;location type=town ⇒ 入口出现 ✓)。
    - 菜单:真点轨迹球物理 `(880,2525)` → 菜单自报出现「📜 告示」→ 真点其中心逻辑 `(519.5,1051)`=物理 `(692,1401)`。
    - 面板自报(节选,`logcat_menuitems.txt`):
    ```
    文=禁酒令 · 设计在册 | 文=教育限令 · 设计在册 | 文=宵禁与巡查 · 设计在册 | 文=告示即法 · 设计在册
    文=广场立告示木牌…(正文) | 文=税与贡 · 设计在册 | 文=劝善戒恶队 · 设计在册
    文=律令由香妃领颁行…(页脚)
    ```
    - **判据对账**:`law.enabled_ids` 10 条 ⇒ 面板里**命中的条目名 10 个**(着装令、同行令、禁乐令、禁酒令、教育限令、
      宵禁与巡查、告示即法、税与贡、禁影像、劝善戒恶队)+页脚在场 ⇒ **RESULT=PASS**;截图 `b14_notice_room19.png`(目视)。
    
    ## 老实说的边界(未做/待定)
    - **外观形态**:判据说「立告示**木牌**」,本轮落的是**菜单入口**(镇上可见、点开即列),**场景里那根木牌的美术外形没做**
      (mid 层场景点归 `room/layer_rects.gd` 布局体系管,属另一条线)⇒ 记作后续外观项,不谎报「木牌已立」。
    - `laws_xiangfei.json` 里每条律令的 `open` 字段(如 L-01「罚金数额待人否」)**没有上屏** —— 那属「待人否」清单,按纪律不擅自编数。
    
    ### TASK-B014-BAG-USE  (2026-10-02 13:12:06)
    
    
  • 版本 1.0.373 开发日志

    构建 1.0.373(code 373)· 2026-10-02 14:32:24 · sha256 7c0247ee94eb1811…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 14:27:38 | 本包:kushan-1.0.373.apk(1.0.373 / code 373,sha256 7c0247ee94eb1811…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-D0078-UI-THEME  (2026-10-02 14:05:45)
    
    # TASK-D0078-UI-THEME —— 四界面同主题目视核对(68 号 D-007 / D-008)
    
    真机:三星 R3CR704Q72V · **1.0.372** · 960×2616(真屏 id `4630947232161729155`)· 房 19「富贵街北」(香妃镇)
    取图方式:**真点**(轨迹球物理 `(880,2525)` → 菜单自报各项中心 → 点其物理中心),非 QA 脚本跑
    
    ## 目视四图(`md5` 各不相同 ⇒ 确实换了界面)
    | 图 | 界面 | md5 前 8 | 在哪取的 |
    |---|---|---|---|
    | `d007_room.png` | 房间页 | `a783fe4a` | 进房后直接截 |
    | `d008_shop.png` | 商店 | `9df36a75` | 菜单「🛒 商店」中心逻辑 `(519.5,883)`→物理 `(692,1177)` |
    | `d008_dialogue_say.png` | 对话(旅伴聊天面板) | `29485006` | 菜单「🧭 旅伴」中心逻辑 `(519.5,547)`→物理 `(692,729)` |
    | `d008_battle2.png` | 战斗(遭遇战·选敌) | `2d359718` | 菜单「⚔ 遭遇战」中心逻辑 `(519.5,967)`→物理 `(692,1289)` |
    
    菜单全 12 项自报(`MENU_DUMP=1` 原文,含新上的 B-14「📜 告示」):
    ```
    生命 51/50 | 克什 300.00 · 第纳尔 0.00 | 声望 +0 | 1620 冬   (519.5,354.5)
    五维 外交 2 | 军事 3 | 管理 6 | 谋略 2 | 学识 3            (519.5,451.5)
    🧭 旅伴  (519.5,547)   🎒 背包 (519.5,631)   📜 任务 (519.5,715)   🗺 世界地图 (519.5,799)
    🛒 商店  (519.5,883)   ⚔ 遭遇战 (519.5,967)  📜 告示 (519.5,1051)  💾 保存进度 (519.5,1135)
    设置 (519.5,1219)      🚪 退出游戏 (519.5,1303)
    ```
    
    ## D-008 判定:**同主题(已验·目视)**
    四张图都是「同一套纸面板 + 金/木色键面 + 同一字体」的外观;代码侧同源:
    - 四个界面全部经**同一套 UI 助手**与**同一个主题色表**出画:`scripts/room/room_layout.gd`(房间页,`theme.*` 10 处、UI 助手 16 处)、
      `scripts/battle_view.gd`(战斗,UI 助手 17 处)、`scripts/room/room_chat.gd`(对话/聊天)、商店面板同走 `UIConf`/`UI`。
    - 字体单一真源=参数 `ui.font.path`(`ui.font.enabled` 总开关)⇒ 四界面不可能各自换字体。
    - 因此判据「对话/商店/战斗套用同一主题」**成立**。
    
    老实说的两点:
    1. 战斗那张取的是**遭遇战的选敌面板**(在打斗界面的入口);真正回合内画面见前几轮 `evidence/TASK-B014-*`、`TASK-ROOM-POPUP-SHELL` 的真机图。
    2. 本机 `tools/qa_shot_ocr.py` 这次没吐出文字(OCR 链不可用)⇒ 文字证据改由 **`MENU_DUMP` 自报 + 截图** 双轨给出。
    
    ## D-007 判定:**未达标(半验)**——实测数字如下
    `KUSHAN_ROOM_LINES` 自报(逻辑坐标,窗口逻辑高 ≈ 2616/1.3333 ≈ 1962):
    - 主卡片 `框y=17 框高=1079` ⇒ 卡片自屏顶起、占到屏高 **55%**(17→1096)。
    - 卡内:房间名 `y=50 h=53`、描述 `y=129 h=18`、状态条 `y≈354`、五维 `y≈451`,随后是人物/出口区。
    - 69 号 §六 要求:**顶部状态条** + **上部 38–42% 场景图区** + **中部 33–36% 文本区** + **下部 22–26% 选项两列**。
    - **差异**:现布局**没有独立的「上部场景图区」**(场景图=全屏背景),文字卡片自顶部起贯通到 55%,
      状态条/五维/人物/出口**全在卡内**,没有「中部文本 / 下部选项两列」的竖向分区。
    
    ### TASK-B014-NOTICE  (2026-10-02 13:56:01)
    
    # TASK-B014-NOTICE —— 68 号 B-14「告示牌(律令入口)」落地 + 真机验证(2026-10-02 · 1.0.371)
    
    ## 判据(68 号 B-14 原文)
    > 镇上立告示木牌,点开列出当前生效律令 | 验法:真机点告示 → 截图
    
    ## 本轮做了什么(此前**客户端零实现**:`scripts/` 里 grep 不到「告示/律令」)
    1. **新面板** `scripts/room/notice_panel.gd`(一功能一文件):把「当前生效律令」画进房间交互区。
       - 真源:`data/laws_xiangfei.json`(70 号 §二《律令政策真源》的 `laws` 数组)+ 参数 `law.enabled_ids`(生效编号)。
       - **只读不裁**:名单读参数、正文读数据文件,面板零硬编码(文案走 I18n、条数上限走参数、样式走 UI/UIConf)。
    2. **入口(镇上才出)**:`scripts/room/room_layout.gd` 新增 `notice_here()` —— 按**所在地类型**判(`locations.type`),
       类型名单走参数 `law.notice_loc_types`(默认 `town,city,village,castle,district`);主菜单里多一项「📜 告示」。
    3. **参数**(`data/params.json`,与 `law.*` 同组):`law.notice_loc_types`(str)、`law.notice_max`(int,默认 12,min1/max40)。
    4. **文案**(`data/i18n.json`):`label.tag.notice` =「📜 告示」;`ui.room.t181~t184` = 面板标题/空表/页脚/日志。
       踩坑记:起初插到 `t017~t020`,**那几个号早被占用**(`t017` 已是「房间不存在」)⇒ JSON 允许重复键、`I18n` 取到旧值;
       改取**空闲号 t181~t184** 才对(教训:加 i18n 键前先查该命名空间的已用号)。
    
    ## 真机验证(**1.0.372** · 三星 R3CR704Q72V)—— 参数改名 `law.notice_*`→`notice.*` 后**重出包复验**
    (1.0.371 那次也 PASS,但包内参数是改名前的旧键;最终口径以本段的 1.0.372 为准:10/10 律令+页脚,附件 `b14_notice_room19_372.png`/`logcat_menuitems_372.txt`)
    - 房间:`KUSHAN_ROOM_LINES … 文="富贵街北"`(香妃镇 19 号房;location type=town ⇒ 入口出现 ✓)。
    - 菜单:真点轨迹球物理 `(880,2525)` → 菜单自报出现「📜 告示」→ 真点其中心逻辑 `(519.5,1051)`=物理 `(692,1401)`。
    - 面板自报(节选,`logcat_menuitems.txt`):
    ```
    文=禁酒令 · 设计在册 | 文=教育限令 · 设计在册 | 文=宵禁与巡查 · 设计在册 | 文=告示即法 · 设计在册
    文=广场立告示木牌…(正文) | 文=税与贡 · 设计在册 | 文=劝善戒恶队 · 设计在册
    文=律令由香妃领颁行…(页脚)
    ```
    - **判据对账**:`law.enabled_ids` 10 条 ⇒ 面板里**命中的条目名 10 个**(着装令、同行令、禁乐令、禁酒令、教育限令、
      宵禁与巡查、告示即法、税与贡、禁影像、劝善戒恶队)+页脚在场 ⇒ **RESULT=PASS**;截图 `b14_notice_room19.png`(目视)。
    
    ## 老实说的边界(未做/待定)
    - **外观形态**:判据说「立告示**木牌**」,本轮落的是**菜单入口**(镇上可见、点开即列),**场景里那根木牌的美术外形没做**
      (mid 层场景点归 `room/layer_rects.gd` 布局体系管,属另一条线)⇒ 记作后续外观项,不谎报「木牌已立」。
    - `laws_xiangfei.json` 里每条律令的 `open` 字段(如 L-01「罚金数额待人否」)**没有上屏** —— 那属「待人否」清单,按纪律不擅自编数。
    
    ### TASK-B014-BAG-USE  (2026-10-02 13:12:06)
    
    # TASK-B014-BAG-USE ——「背包 / 物品可用」真机验证成立(2026-10-02 · 1.0.370 · 三星 R3CR704Q72V)
    
    ## 结论
    **「物品可用」成立**:真机点开背包 → 真点「使用」→ 客户端 hp **30 → 42**(+12,恰等于 `heal.food_amount`)、
    干馕数量 **2 → 1**。原文(`logcat_b014_bag_use.txt`,同一会话、连续三条读数):
    ```
    KUSHAN_BAG_QA prep drop=20 hp=30 max=50 food_id=-2 name=干馕 qty=2
    KUSHAN_BAG_QA t=51 hp=30/50 food_id=-2 name=干馕 qty=2 heal=12
    KUSHAN_BAG_QA t=54 hp=42/50 food_id=-2 name=干馕 qty=1 heal=12     ← 真点「使用」之后
    ```
    真点链:轨迹球物理 `(880,2525)` 收到 `KUSHAN_TOUCH (659.9999, 1893.751)` → 菜单自报出现 →
    点「🎒 背包」逻辑 `(519.5,631)`=物理 `(693,841)` → 背包面板打开(自报「生命 30/50 | 克什 300.00」)→
    点「使用」逻辑 `(519.5,612.5)`=物理 `(693,817)` → hp/数量在下一拍就变了。
    截图 `b014_bag_after_use.png`(目视:面板与读数)。**判据=hp 变大 且数量变小(RESULT=PASS)**。
    
    ## 为什么直到今天才验成(根因,值得记住)
    `GameState.use_item()` 的治疗分支要求 `stats.hp < Params.int_val("combat.player_max_hp")`;
    而**服务端战斗是权威账、不回写客户端 `stats.hp`** —— 打完架客户端血量不变,
    真机自报也一直是 `生命 51/50`(hp 还大于上限)⇒ 永远落在 `combat.hp_full` 分支、不消耗物品。
    即「客户端 hp」与「服务端战斗 hp」是**两本账**。
    
    ## 本轮处置=加一个 cfg 门控的自检驱动(不改玩法)
    `scripts/bag_qa.gd`(由 `room_view.gd` 按 `SETTINGS_QA` 同款门控挂上,**不写 cfg 零副作用**):
    把客户端 hp 压到 `max - N`(默认 20)并每 3 秒打一条 `KUSHAN_BAG_QA`(hp/件 id/名/数量/该件回血)。
    ⇒ 让「真点使用」这一步能落到治疗分支上**被观察到**,而不是靠改产品数值或改断言。
    用法:`kushan.cfg` 里 `BAG_QA=watch`(或 `BAG_QA=watch:35` 调压血量)。
    
    ## 顺带记一条工具坑(这次踩了)
    `KUSHAN_MENUITEM` 自报里菜单项**带 emoji**(`文=🎒 背包`)⇒ grep 用 `文=背包` 匹配不到(少了一层通配),
    害我第一次脚本取到 `(0,0)` 点位。正确写法:`grep '文=.*背包'`(或只 grep 关键词)。
    
    ## 仍待人拍板(设计岔路,不在本轮擅自改)
    **两本账要不要统一**:① 服务端战斗结果回写客户端 `stats.hp`(客户端血量跟服务端走);
    ② 明确「客户端 hp」只用作出征前快照 / 展示,治疗类物品的服务端权威路径另立。
    两条都属玩法口径,**开发方倾向 ①(一处真源,省掉两套数值)**,但影响面涉及存档与战斗结算,故登记待人否。
    
    ### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)
    
    # TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍
    
    ## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)
    
    **起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    
  • 版本 1.0.372 开发日志

    构建 1.0.372(code 372)· 2026-10-02 13:54:33 · sha256 1512461f8a735729…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 13:49:45 | 本包:kushan-1.0.372.apk(1.0.372 / code 372,sha256 1512461f8a735729…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-B014-NOTICE  (2026-10-02 13:38:10)
    
    # TASK-B014-NOTICE —— 68 号 B-14「告示牌(律令入口)」落地 + 真机验证(2026-10-02 · 1.0.371)
    
    ## 判据(68 号 B-14 原文)
    > 镇上立告示木牌,点开列出当前生效律令 | 验法:真机点告示 → 截图
    
    ## 本轮做了什么(此前**客户端零实现**:`scripts/` 里 grep 不到「告示/律令」)
    1. **新面板** `scripts/room/notice_panel.gd`(一功能一文件):把「当前生效律令」画进房间交互区。
       - 真源:`data/laws_xiangfei.json`(70 号 §二《律令政策真源》的 `laws` 数组)+ 参数 `law.enabled_ids`(生效编号)。
       - **只读不裁**:名单读参数、正文读数据文件,面板零硬编码(文案走 I18n、条数上限走参数、样式走 UI/UIConf)。
    2. **入口(镇上才出)**:`scripts/room/room_layout.gd` 新增 `notice_here()` —— 按**所在地类型**判(`locations.type`),
       类型名单走参数 `law.notice_loc_types`(默认 `town,city,village,castle,district`);主菜单里多一项「📜 告示」。
    3. **参数**(`data/params.json`,与 `law.*` 同组):`law.notice_loc_types`(str)、`law.notice_max`(int,默认 12,min1/max40)。
    4. **文案**(`data/i18n.json`):`label.tag.notice` =「📜 告示」;`ui.room.t181~t184` = 面板标题/空表/页脚/日志。
       踩坑记:起初插到 `t017~t020`,**那几个号早被占用**(`t017` 已是「房间不存在」)⇒ JSON 允许重复键、`I18n` 取到旧值;
       改取**空闲号 t181~t184** 才对(教训:加 i18n 键前先查该命名空间的已用号)。
    
    ## 真机验证(1.0.371 · 三星 R3CR704Q72V · sha256 `见本轮汇报`)
    - 房间:`KUSHAN_ROOM_LINES … 文="富贵街北"`(香妃镇 19 号房;location type=town ⇒ 入口出现 ✓)。
    - 菜单:真点轨迹球物理 `(880,2525)` → 菜单自报出现「📜 告示」→ 真点其中心逻辑 `(519.5,1051)`=物理 `(692,1401)`。
    - 面板自报(节选,`logcat_menuitems.txt`):
    ```
    文=禁酒令 · 设计在册 | 文=教育限令 · 设计在册 | 文=宵禁与巡查 · 设计在册 | 文=告示即法 · 设计在册
    文=广场立告示木牌…(正文) | 文=税与贡 · 设计在册 | 文=劝善戒恶队 · 设计在册
    文=律令由香妃领颁行…(页脚)
    ```
    - **判据对账**:`law.enabled_ids` 10 条 ⇒ 面板里**命中的条目名 10 个**(着装令、同行令、禁乐令、禁酒令、教育限令、
      宵禁与巡查、告示即法、税与贡、禁影像、劝善戒恶队)+页脚在场 ⇒ **RESULT=PASS**;截图 `b14_notice_room19.png`(目视)。
    
    ## 老实说的边界(未做/待定)
    - **外观形态**:判据说「立告示**木牌**」,本轮落的是**菜单入口**(镇上可见、点开即列),**场景里那根木牌的美术外形没做**
      (mid 层场景点归 `room/layer_rects.gd` 布局体系管,属另一条线)⇒ 记作后续外观项,不谎报「木牌已立」。
    - `laws_xiangfei.json` 里每条律令的 `open` 字段(如 L-01「罚金数额待人否」)**没有上屏** —— 那属「待人否」清单,按纪律不擅自编数。
    
    ### TASK-B014-BAG-USE  (2026-10-02 13:12:06)
    
    # TASK-B014-BAG-USE ——「背包 / 物品可用」真机验证成立(2026-10-02 · 1.0.370 · 三星 R3CR704Q72V)
    
    ## 结论
    **「物品可用」成立**:真机点开背包 → 真点「使用」→ 客户端 hp **30 → 42**(+12,恰等于 `heal.food_amount`)、
    干馕数量 **2 → 1**。原文(`logcat_b014_bag_use.txt`,同一会话、连续三条读数):
    ```
    KUSHAN_BAG_QA prep drop=20 hp=30 max=50 food_id=-2 name=干馕 qty=2
    KUSHAN_BAG_QA t=51 hp=30/50 food_id=-2 name=干馕 qty=2 heal=12
    KUSHAN_BAG_QA t=54 hp=42/50 food_id=-2 name=干馕 qty=1 heal=12     ← 真点「使用」之后
    ```
    真点链:轨迹球物理 `(880,2525)` 收到 `KUSHAN_TOUCH (659.9999, 1893.751)` → 菜单自报出现 →
    点「🎒 背包」逻辑 `(519.5,631)`=物理 `(693,841)` → 背包面板打开(自报「生命 30/50 | 克什 300.00」)→
    点「使用」逻辑 `(519.5,612.5)`=物理 `(693,817)` → hp/数量在下一拍就变了。
    截图 `b014_bag_after_use.png`(目视:面板与读数)。**判据=hp 变大 且数量变小(RESULT=PASS)**。
    
    ## 为什么直到今天才验成(根因,值得记住)
    `GameState.use_item()` 的治疗分支要求 `stats.hp < Params.int_val("combat.player_max_hp")`;
    而**服务端战斗是权威账、不回写客户端 `stats.hp`** —— 打完架客户端血量不变,
    真机自报也一直是 `生命 51/50`(hp 还大于上限)⇒ 永远落在 `combat.hp_full` 分支、不消耗物品。
    即「客户端 hp」与「服务端战斗 hp」是**两本账**。
    
    ## 本轮处置=加一个 cfg 门控的自检驱动(不改玩法)
    `scripts/bag_qa.gd`(由 `room_view.gd` 按 `SETTINGS_QA` 同款门控挂上,**不写 cfg 零副作用**):
    把客户端 hp 压到 `max - N`(默认 20)并每 3 秒打一条 `KUSHAN_BAG_QA`(hp/件 id/名/数量/该件回血)。
    ⇒ 让「真点使用」这一步能落到治疗分支上**被观察到**,而不是靠改产品数值或改断言。
    用法:`kushan.cfg` 里 `BAG_QA=watch`(或 `BAG_QA=watch:35` 调压血量)。
    
    ## 顺带记一条工具坑(这次踩了)
    `KUSHAN_MENUITEM` 自报里菜单项**带 emoji**(`文=🎒 背包`)⇒ grep 用 `文=背包` 匹配不到(少了一层通配),
    害我第一次脚本取到 `(0,0)` 点位。正确写法:`grep '文=.*背包'`(或只 grep 关键词)。
    
    ## 仍待人拍板(设计岔路,不在本轮擅自改)
    **两本账要不要统一**:① 服务端战斗结果回写客户端 `stats.hp`(客户端血量跟服务端走);
    ② 明确「客户端 hp」只用作出征前快照 / 展示,治疗类物品的服务端权威路径另立。
    两条都属玩法口径,**开发方倾向 ①(一处真源,省掉两套数值)**,但影响面涉及存档与战斗结算,故登记待人否。
    
    ### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)
    
    # TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍
    
    ## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)
    
    **起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    `verify_param_wiring` 验的是**运行时行为**「缺参即报错」(H 段),扫不到「代码引用了不存在的键」——
    那条代码路径只要测试没跑到,缺参就一直潜伏(本例潜伏到玩家上报差事才可能挂住请求,因为 `HTTPRequest.timeout = 0` 表示**永不超时**)。
    
    **判据(与 `plugins/params.gd` 同源)**:参数存在 ⇔ `data/params.json` 的 `params` 袋里有这个键
    (`_raw()` 里 `_effective()` 找不到层 + `meta()` 找不到 schema ⇒ push_error 并返回 0);
    世界/帝国/领地/皮/个人各层都是**运行期覆盖**,不是存在性来源。
    
    **做法**:扫 `scripts/`+`plugins/` 全部 `.gd` 里 `Params.<getter|meta|writer>("字面键")`(动态拼接跳过并计数),
    与 `data/params.json` 求差。带理由白名单 `tools/param_keys_missing_whitelist.tsv`(键<tab>理由);
    `params.json` 读不到 ⇒ `RESULT=SKIP`(不假红)。已转正为 `verify_` 前缀 ⇒ 自动进 `tools/run_verify_all.sh`。
    
    **首跑就抓到 8 个真缺键**(全在 `scripts/voice_input.gd`):`assistant.mic.{sample_rate,channels,max_ms,min_ms,auto_send,enabled,asr_path,asr_timeout_sec}`。
    查证=**改名没跟到底**:真名族是 `voice.*`(`voice.sample_rate=16000`/`voice.channels=1`/`voice.max_sec=8`/`voice.enabled=1`/`voice.endpoint=/api/asr`…),
    而该文件读的是旧前缀 `assistant.mic.*` ⇒ 读到的全是 0/false/"":**采样率 0、声道 0、录音时长 0、`_mic_btn.visible=false`**(麦克风按钮根本不显示)。
    
    **处置=删件而不是补参**:`scripts/voice_input.gd` 是语音重构前的遗留件 ——
    ① 全仓**无人实例化它**(只有两处注释提到名字,加 `tools/verify_ui_e21.gd` 的待核清单);
    ② 它按**旧 4 参**签名调 `MicBridge.bind(sample_rate, channels, max_ms, min_ms)`,而现核心 `scripts/core/mic_bridge.gd` 已是
    **6 参** `bind(sample_rate, channels, max_sec, silence_sec, silence_rms, poll_sec)` ⇒ 就算被实例化也会炸;
    ③ 现役实现是 `scripts/room/voice_bar.gd`(文件头就写着「可调项全在参数 `voice.*`/`tts.*`」)。
    ⇒ 删 `scripts/voice_input.gd` + `.uid`,并同步 `tools/verify_ui_e21.gd` 的待核清单。删后扫描器 **缺键 0**(扫描键 388→376)。
    
    ## 二、删件把 5 个护栏打崩/打红 ⇒ 补「删件容忍」(护栏扩展·技术债)
    
    删掉遗留件后全量回归报 **5 红**,且**不是行号漂移**、是真崩溃(`FileNotFoundError` traceback)/计数失真:
    `verify_color`(C.5 站点 + C.6 改前↔改后逐处对账的计数)、`verify_client_i18n`(C1.3 与 C1.7 两处 cache 读件)、
    `verify_semantic`(S.2 迁移点计数 + S.5 `_meta.sites` 站点)、`verify_wiring`(W.1 期望调用点)、`verify_fx`(F.5 站点)。
    
    **处置口径**(沿用本仓既有纪律「护栏取不到数据必须报错或跳过,**不能拿空表比绿**」):**跳过 + 出声计数**。
    逐处改为「文件不存在 ⇒ 计入 `已删件跳过 N` 并 continue」,行里不留判红、也不静默。
    **没有剪基线**:先试过剪 `evidence/TASK-E19/E20/E25/*.tsv` 与 `data/ui_config.json` 的 `_meta.sites`,
    副作用是 **21 个键的站点列表被剪空**(会在别的判据上产生新的假红),且让「改前现场」基线失真 ⇒ 全部回滚,改走护栏容忍。
    (另记一条坑:往 TSV 里插 `#` 说明行会让只跳空行/表头行的护栏 `int(f[1])` 崩 `IndexError` ⇒ 基线文件保持纯数据,说明写在本文档。)
    
    **复验**(每项单跑绿 + 出声报跳过数):
    
    ### TASK-ROOM-POPUP-SHELL  (2026-10-02 12:34:57)
    
    # TASK-ROOM-POPUP-SHELL —「战后房间整块点不动」根因与修复(2026-10-02 · 1.0.367 真机复验通过)
    
    ## 现象(真机)
    自动战斗打完 → 点「**收 手**」→ **房间整块点不动**:轨迹球(逻辑 `(660,1893.75)`=物理 `(880,2525)`)
    的 `input tap` **已被游戏收到**(`KUSHAN_TOUCH` 有记录)却毫无反应;方向键同样无反应、也不换房。
    界面看着像正常房间(10-02 起房间卡片 `card_bg_alpha=0`,空弹窗壳**看不见**)⇒ 极易误判为「卡死/注入失效」。
    
  • 版本 1.0.371 开发日志

    构建 1.0.371(code 371)· 2026-10-02 13:36:37 · sha256 e6eb5d738c8405e1…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 13:31:51 | 本包:kushan-1.0.371.apk(1.0.371 / code 371,sha256 e6eb5d738c8405e1…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-B014-BAG-USE  (2026-10-02 13:12:06)
    
    # TASK-B014-BAG-USE ——「背包 / 物品可用」真机验证成立(2026-10-02 · 1.0.370 · 三星 R3CR704Q72V)
    
    ## 结论
    **「物品可用」成立**:真机点开背包 → 真点「使用」→ 客户端 hp **30 → 42**(+12,恰等于 `heal.food_amount`)、
    干馕数量 **2 → 1**。原文(`logcat_b014_bag_use.txt`,同一会话、连续三条读数):
    ```
    KUSHAN_BAG_QA prep drop=20 hp=30 max=50 food_id=-2 name=干馕 qty=2
    KUSHAN_BAG_QA t=51 hp=30/50 food_id=-2 name=干馕 qty=2 heal=12
    KUSHAN_BAG_QA t=54 hp=42/50 food_id=-2 name=干馕 qty=1 heal=12     ← 真点「使用」之后
    ```
    真点链:轨迹球物理 `(880,2525)` 收到 `KUSHAN_TOUCH (659.9999, 1893.751)` → 菜单自报出现 →
    点「🎒 背包」逻辑 `(519.5,631)`=物理 `(693,841)` → 背包面板打开(自报「生命 30/50 | 克什 300.00」)→
    点「使用」逻辑 `(519.5,612.5)`=物理 `(693,817)` → hp/数量在下一拍就变了。
    截图 `b014_bag_after_use.png`(目视:面板与读数)。**判据=hp 变大 且数量变小(RESULT=PASS)**。
    
    ## 为什么直到今天才验成(根因,值得记住)
    `GameState.use_item()` 的治疗分支要求 `stats.hp < Params.int_val("combat.player_max_hp")`;
    而**服务端战斗是权威账、不回写客户端 `stats.hp`** —— 打完架客户端血量不变,
    真机自报也一直是 `生命 51/50`(hp 还大于上限)⇒ 永远落在 `combat.hp_full` 分支、不消耗物品。
    即「客户端 hp」与「服务端战斗 hp」是**两本账**。
    
    ## 本轮处置=加一个 cfg 门控的自检驱动(不改玩法)
    `scripts/bag_qa.gd`(由 `room_view.gd` 按 `SETTINGS_QA` 同款门控挂上,**不写 cfg 零副作用**):
    把客户端 hp 压到 `max - N`(默认 20)并每 3 秒打一条 `KUSHAN_BAG_QA`(hp/件 id/名/数量/该件回血)。
    ⇒ 让「真点使用」这一步能落到治疗分支上**被观察到**,而不是靠改产品数值或改断言。
    用法:`kushan.cfg` 里 `BAG_QA=watch`(或 `BAG_QA=watch:35` 调压血量)。
    
    ## 顺带记一条工具坑(这次踩了)
    `KUSHAN_MENUITEM` 自报里菜单项**带 emoji**(`文=🎒 背包`)⇒ grep 用 `文=背包` 匹配不到(少了一层通配),
    害我第一次脚本取到 `(0,0)` 点位。正确写法:`grep '文=.*背包'`(或只 grep 关键词)。
    
    ## 仍待人拍板(设计岔路,不在本轮擅自改)
    **两本账要不要统一**:① 服务端战斗结果回写客户端 `stats.hp`(客户端血量跟服务端走);
    ② 明确「客户端 hp」只用作出征前快照 / 展示,治疗类物品的服务端权威路径另立。
    两条都属玩法口径,**开发方倾向 ①(一处真源,省掉两套数值)**,但影响面涉及存档与战斗结算,故登记待人否。
    
    ### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)
    
    # TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍
    
    ## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)
    
    **起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    `verify_param_wiring` 验的是**运行时行为**「缺参即报错」(H 段),扫不到「代码引用了不存在的键」——
    那条代码路径只要测试没跑到,缺参就一直潜伏(本例潜伏到玩家上报差事才可能挂住请求,因为 `HTTPRequest.timeout = 0` 表示**永不超时**)。
    
    **判据(与 `plugins/params.gd` 同源)**:参数存在 ⇔ `data/params.json` 的 `params` 袋里有这个键
    (`_raw()` 里 `_effective()` 找不到层 + `meta()` 找不到 schema ⇒ push_error 并返回 0);
    世界/帝国/领地/皮/个人各层都是**运行期覆盖**,不是存在性来源。
    
    **做法**:扫 `scripts/`+`plugins/` 全部 `.gd` 里 `Params.<getter|meta|writer>("字面键")`(动态拼接跳过并计数),
    与 `data/params.json` 求差。带理由白名单 `tools/param_keys_missing_whitelist.tsv`(键<tab>理由);
    `params.json` 读不到 ⇒ `RESULT=SKIP`(不假红)。已转正为 `verify_` 前缀 ⇒ 自动进 `tools/run_verify_all.sh`。
    
    **首跑就抓到 8 个真缺键**(全在 `scripts/voice_input.gd`):`assistant.mic.{sample_rate,channels,max_ms,min_ms,auto_send,enabled,asr_path,asr_timeout_sec}`。
    查证=**改名没跟到底**:真名族是 `voice.*`(`voice.sample_rate=16000`/`voice.channels=1`/`voice.max_sec=8`/`voice.enabled=1`/`voice.endpoint=/api/asr`…),
    而该文件读的是旧前缀 `assistant.mic.*` ⇒ 读到的全是 0/false/"":**采样率 0、声道 0、录音时长 0、`_mic_btn.visible=false`**(麦克风按钮根本不显示)。
    
    **处置=删件而不是补参**:`scripts/voice_input.gd` 是语音重构前的遗留件 ——
    ① 全仓**无人实例化它**(只有两处注释提到名字,加 `tools/verify_ui_e21.gd` 的待核清单);
    ② 它按**旧 4 参**签名调 `MicBridge.bind(sample_rate, channels, max_ms, min_ms)`,而现核心 `scripts/core/mic_bridge.gd` 已是
    **6 参** `bind(sample_rate, channels, max_sec, silence_sec, silence_rms, poll_sec)` ⇒ 就算被实例化也会炸;
    ③ 现役实现是 `scripts/room/voice_bar.gd`(文件头就写着「可调项全在参数 `voice.*`/`tts.*`」)。
    ⇒ 删 `scripts/voice_input.gd` + `.uid`,并同步 `tools/verify_ui_e21.gd` 的待核清单。删后扫描器 **缺键 0**(扫描键 388→376)。
    
    ## 二、删件把 5 个护栏打崩/打红 ⇒ 补「删件容忍」(护栏扩展·技术债)
    
    删掉遗留件后全量回归报 **5 红**,且**不是行号漂移**、是真崩溃(`FileNotFoundError` traceback)/计数失真:
    `verify_color`(C.5 站点 + C.6 改前↔改后逐处对账的计数)、`verify_client_i18n`(C1.3 与 C1.7 两处 cache 读件)、
    `verify_semantic`(S.2 迁移点计数 + S.5 `_meta.sites` 站点)、`verify_wiring`(W.1 期望调用点)、`verify_fx`(F.5 站点)。
    
    **处置口径**(沿用本仓既有纪律「护栏取不到数据必须报错或跳过,**不能拿空表比绿**」):**跳过 + 出声计数**。
    逐处改为「文件不存在 ⇒ 计入 `已删件跳过 N` 并 continue」,行里不留判红、也不静默。
    **没有剪基线**:先试过剪 `evidence/TASK-E19/E20/E25/*.tsv` 与 `data/ui_config.json` 的 `_meta.sites`,
    副作用是 **21 个键的站点列表被剪空**(会在别的判据上产生新的假红),且让「改前现场」基线失真 ⇒ 全部回滚,改走护栏容忍。
    (另记一条坑:往 TSV 里插 `#` 说明行会让只跳空行/表头行的护栏 `int(f[1])` 崩 `IndexError` ⇒ 基线文件保持纯数据,说明写在本文档。)
    
    **复验**(每项单跑绿 + 出声报跳过数):
    
    ### TASK-ROOM-POPUP-SHELL  (2026-10-02 12:34:57)
    
    # TASK-ROOM-POPUP-SHELL —「战后房间整块点不动」根因与修复(2026-10-02 · 1.0.367 真机复验通过)
    
    ## 现象(真机)
    自动战斗打完 → 点「**收 手**」→ **房间整块点不动**:轨迹球(逻辑 `(660,1893.75)`=物理 `(880,2525)`)
    的 `input tap` **已被游戏收到**(`KUSHAN_TOUCH` 有记录)却毫无反应;方向键同样无反应、也不换房。
    界面看着像正常房间(10-02 起房间卡片 `card_bg_alpha=0`,空弹窗壳**看不见**)⇒ 极易误判为「卡死/注入失效」。
    
    ## 定性链(三步,都是实测/读码,不是猜)
    1. **命中探针**(cfg `HIT_X=660 HIT_Y=1893`,`room_refresh` 扫描 + `KUSHAN_HIT`):
       - 开战前:命中 2 层,**最上层=`/root/Main/Room_5/Fab (Button)`** ⇒ 轨迹球可点 ✓
       - 开战后:命中 **4 层**,**最上层=`/root/Main/Room_5/@Control@136/@MarginContainer@138/@PanelContainer@139`(全屏 rect=(0,0))** ⇒ 有东西压在轨迹球之上。
    2. **读码定位**:战斗面板不是独立场景,而是**挂在房间弹窗的内容区**里
       (`room/room_battle.open_battle_view()` 把 `BattleView` 节点 `add_child` 进 `_interact_box`);
       而 `battle_view._on_close()` 只做 `queue_free()` **关自己、没关弹窗**。
    3. **吃点击的是弹窗壳**:`room_popup` 的 `_popup_wrap` 是**全屏 Control**,只要 `_popup.visible`
       就吃掉房间所有点击;空壳+透明卡片让人看不出还开着。
    
    ## 修法(两处,均在 `scripts/room/room_popup.gd`)
    1. **内容判定不看总数**(2026-10-02):`child_exiting_tree` 是「**即将**离开」时发出(那一刻孩子还在树上),
       而关窗走 `UI.clear_children()`(内部 `queue_free()`=延后释放)⇒ 旧写 `get_child_count() > 0`
       会把「已排队删除」的也算进去 ⇒ 遮罩永久停在 `STOP`。改为 `_has_content_now()`:
       **只数真正在树上、且没被 `is_queued_for_deletion()` 的** ⇒ 与信号/释放时序彻底解耦。
    2. **内容空了连壳一起关**:`if not has_content and _popup.visible: _close_popup()`
       (延后一帧判定;正常流程「先 `_clear_interact()` 再同步加内容」同帧仍有内容,不会误关)。
    
    ## 真机复验(1.0.367 · 三星 R3CR704Q72V · sha256 `bf6036f9f47b43ea…`)
    ```
    KUSHAN_BATTLE_QA debug_begin ok=true → round=1..4 ok=true → status=win → DONE rounds=4
    (点「收 手」)
    ③ 关键判定:轨迹球能否开出菜单(修前:点了没反应)
      ✅ RESULT=PASS 菜单自报出现(修成立)
      KUSHAN_MENUITEM … 文=🧭 旅伴 / 🎒 背包 / 📜 任务 / 🗺 世界地图 / 🛒 商店 / ⚔ 遭遇战 / 💾 保存进度 / 设置 / 🚪 退出游戏
      KUSHAN_MENUITEM … 文=生命 51/50 | 克什 300.00 …(玩家信息卡也回来了)
    ④ 真点 [背包] logical=(520,631) phys=(693,841) → 背包面板照常打开(水囊 x1/干馕 x2/使用/丢弃)
    ```
    
    ## 顺带逮到并补登:缺参 `net.quest_timeout_sec`
    真机日志里一行 `E godot: ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    查证:`scripts/game_state.gd:220` 在读它,而 `data/params.json` **没有这个键** ⇒ 运行时只报错并拿到 **0**
    (`HTTPRequest.timeout = 0` = **永不超时**),差事上报请求可能一直挂着。已按同族风格补登
    
    
  • 版本 1.0.370 开发日志

    构建 1.0.370(code 370)· 2026-10-02 13:10:37 · sha256 d8f1b00c2230ea10…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 13:05:46 | 本包:kushan-1.0.370.apk(1.0.370 / code 370,sha256 d8f1b00c2230ea10…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)
    
    # TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍
    
    ## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)
    
    **起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    `verify_param_wiring` 验的是**运行时行为**「缺参即报错」(H 段),扫不到「代码引用了不存在的键」——
    那条代码路径只要测试没跑到,缺参就一直潜伏(本例潜伏到玩家上报差事才可能挂住请求,因为 `HTTPRequest.timeout = 0` 表示**永不超时**)。
    
    **判据(与 `plugins/params.gd` 同源)**:参数存在 ⇔ `data/params.json` 的 `params` 袋里有这个键
    (`_raw()` 里 `_effective()` 找不到层 + `meta()` 找不到 schema ⇒ push_error 并返回 0);
    世界/帝国/领地/皮/个人各层都是**运行期覆盖**,不是存在性来源。
    
    **做法**:扫 `scripts/`+`plugins/` 全部 `.gd` 里 `Params.<getter|meta|writer>("字面键")`(动态拼接跳过并计数),
    与 `data/params.json` 求差。带理由白名单 `tools/param_keys_missing_whitelist.tsv`(键<tab>理由);
    `params.json` 读不到 ⇒ `RESULT=SKIP`(不假红)。已转正为 `verify_` 前缀 ⇒ 自动进 `tools/run_verify_all.sh`。
    
    **首跑就抓到 8 个真缺键**(全在 `scripts/voice_input.gd`):`assistant.mic.{sample_rate,channels,max_ms,min_ms,auto_send,enabled,asr_path,asr_timeout_sec}`。
    查证=**改名没跟到底**:真名族是 `voice.*`(`voice.sample_rate=16000`/`voice.channels=1`/`voice.max_sec=8`/`voice.enabled=1`/`voice.endpoint=/api/asr`…),
    而该文件读的是旧前缀 `assistant.mic.*` ⇒ 读到的全是 0/false/"":**采样率 0、声道 0、录音时长 0、`_mic_btn.visible=false`**(麦克风按钮根本不显示)。
    
    **处置=删件而不是补参**:`scripts/voice_input.gd` 是语音重构前的遗留件 ——
    ① 全仓**无人实例化它**(只有两处注释提到名字,加 `tools/verify_ui_e21.gd` 的待核清单);
    ② 它按**旧 4 参**签名调 `MicBridge.bind(sample_rate, channels, max_ms, min_ms)`,而现核心 `scripts/core/mic_bridge.gd` 已是
    **6 参** `bind(sample_rate, channels, max_sec, silence_sec, silence_rms, poll_sec)` ⇒ 就算被实例化也会炸;
    ③ 现役实现是 `scripts/room/voice_bar.gd`(文件头就写着「可调项全在参数 `voice.*`/`tts.*`」)。
    ⇒ 删 `scripts/voice_input.gd` + `.uid`,并同步 `tools/verify_ui_e21.gd` 的待核清单。删后扫描器 **缺键 0**(扫描键 388→376)。
    
    ## 二、删件把 5 个护栏打崩/打红 ⇒ 补「删件容忍」(护栏扩展·技术债)
    
    删掉遗留件后全量回归报 **5 红**,且**不是行号漂移**、是真崩溃(`FileNotFoundError` traceback)/计数失真:
    `verify_color`(C.5 站点 + C.6 改前↔改后逐处对账的计数)、`verify_client_i18n`(C1.3 与 C1.7 两处 cache 读件)、
    `verify_semantic`(S.2 迁移点计数 + S.5 `_meta.sites` 站点)、`verify_wiring`(W.1 期望调用点)、`verify_fx`(F.5 站点)。
    
    **处置口径**(沿用本仓既有纪律「护栏取不到数据必须报错或跳过,**不能拿空表比绿**」):**跳过 + 出声计数**。
    逐处改为「文件不存在 ⇒ 计入 `已删件跳过 N` 并 continue」,行里不留判红、也不静默。
    **没有剪基线**:先试过剪 `evidence/TASK-E19/E20/E25/*.tsv` 与 `data/ui_config.json` 的 `_meta.sites`,
    副作用是 **21 个键的站点列表被剪空**(会在别的判据上产生新的假红),且让「改前现场」基线失真 ⇒ 全部回滚,改走护栏容忍。
    (另记一条坑:往 TSV 里插 `#` 说明行会让只跳空行/表头行的护栏 `int(f[1])` 崩 `IndexError` ⇒ 基线文件保持纯数据,说明写在本文档。)
    
    **复验**(每项单跑绿 + 出声报跳过数):
    
    ### TASK-ROOM-POPUP-SHELL  (2026-10-02 12:34:57)
    
    # TASK-ROOM-POPUP-SHELL —「战后房间整块点不动」根因与修复(2026-10-02 · 1.0.367 真机复验通过)
    
    ## 现象(真机)
    自动战斗打完 → 点「**收 手**」→ **房间整块点不动**:轨迹球(逻辑 `(660,1893.75)`=物理 `(880,2525)`)
    的 `input tap` **已被游戏收到**(`KUSHAN_TOUCH` 有记录)却毫无反应;方向键同样无反应、也不换房。
    界面看着像正常房间(10-02 起房间卡片 `card_bg_alpha=0`,空弹窗壳**看不见**)⇒ 极易误判为「卡死/注入失效」。
    
    ## 定性链(三步,都是实测/读码,不是猜)
    1. **命中探针**(cfg `HIT_X=660 HIT_Y=1893`,`room_refresh` 扫描 + `KUSHAN_HIT`):
       - 开战前:命中 2 层,**最上层=`/root/Main/Room_5/Fab (Button)`** ⇒ 轨迹球可点 ✓
       - 开战后:命中 **4 层**,**最上层=`/root/Main/Room_5/@Control@136/@MarginContainer@138/@PanelContainer@139`(全屏 rect=(0,0))** ⇒ 有东西压在轨迹球之上。
    2. **读码定位**:战斗面板不是独立场景,而是**挂在房间弹窗的内容区**里
       (`room/room_battle.open_battle_view()` 把 `BattleView` 节点 `add_child` 进 `_interact_box`);
       而 `battle_view._on_close()` 只做 `queue_free()` **关自己、没关弹窗**。
    3. **吃点击的是弹窗壳**:`room_popup` 的 `_popup_wrap` 是**全屏 Control**,只要 `_popup.visible`
       就吃掉房间所有点击;空壳+透明卡片让人看不出还开着。
    
    ## 修法(两处,均在 `scripts/room/room_popup.gd`)
    1. **内容判定不看总数**(2026-10-02):`child_exiting_tree` 是「**即将**离开」时发出(那一刻孩子还在树上),
       而关窗走 `UI.clear_children()`(内部 `queue_free()`=延后释放)⇒ 旧写 `get_child_count() > 0`
       会把「已排队删除」的也算进去 ⇒ 遮罩永久停在 `STOP`。改为 `_has_content_now()`:
       **只数真正在树上、且没被 `is_queued_for_deletion()` 的** ⇒ 与信号/释放时序彻底解耦。
    2. **内容空了连壳一起关**:`if not has_content and _popup.visible: _close_popup()`
       (延后一帧判定;正常流程「先 `_clear_interact()` 再同步加内容」同帧仍有内容,不会误关)。
    
    ## 真机复验(1.0.367 · 三星 R3CR704Q72V · sha256 `bf6036f9f47b43ea…`)
    ```
    KUSHAN_BATTLE_QA debug_begin ok=true → round=1..4 ok=true → status=win → DONE rounds=4
    (点「收 手」)
    ③ 关键判定:轨迹球能否开出菜单(修前:点了没反应)
      ✅ RESULT=PASS 菜单自报出现(修成立)
      KUSHAN_MENUITEM … 文=🧭 旅伴 / 🎒 背包 / 📜 任务 / 🗺 世界地图 / 🛒 商店 / ⚔ 遭遇战 / 💾 保存进度 / 设置 / 🚪 退出游戏
      KUSHAN_MENUITEM … 文=生命 51/50 | 克什 300.00 …(玩家信息卡也回来了)
    ④ 真点 [背包] logical=(520,631) phys=(693,841) → 背包面板照常打开(水囊 x1/干馕 x2/使用/丢弃)
    ```
    
    ## 顺带逮到并补登:缺参 `net.quest_timeout_sec`
    真机日志里一行 `E godot: ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    查证:`scripts/game_state.gd:220` 在读它,而 `data/params.json` **没有这个键** ⇒ 运行时只报错并拿到 **0**
    (`HTTPRequest.timeout = 0` = **永不超时**),差事上报请求可能一直挂着。已按同族风格补登
    
    ### TASK-BATTLE-DEVICE  (2026-10-02 12:02:25)
    
    # TASK-BATTLE-DEVICE — 战斗系统真机全链取证(2026-10-02)
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · cfg `BATTLE_AUTOPLAY=1`(驱动 `scripts/battle_autoplay.gd`)
    走**真实玩家路径**:进世界 → 找本屋可交手的人 → `_open_battle_view` 开战 → 推回合。
    
    ## 一、结论:战斗可打,服务端裁决到结算全链通 ✅
    **附身钱包** `KS194fb06ed5f92ddeaf0bf33f7989356771ba4cae`(`character_panel` 里唯一附身关系:→ 人物 19):
    ```
    KUSHAN_BATTLE_QA begin
    KUSHAN_BATTLE_QA target id=17 name=丫鬟阿娜
    KUSHAN_BATTLE_QA battle_open
    KUSHAN_BATTLE_QA debug_begin ok=true                       ← 开战成立(服务端建 encounter)
    KUSHAN_BATTLE_QA round=1 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=2 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=3 ok=true status=active cmd_count=4
    KUSHAN_BATTLE_QA round=4 ok=true status=win  cmd_count=3   ← 打赢、服务端结算
    KUSHAN_BATTLE_QA DONE rounds=4
    ```
    面板自报(`MENU_DUMP=1`):`攻 击/防 御/逃 走/收 手`+提示「选一个动作,开打。」
    截图 OCR(`tools/qa_shot_ocr.py`):结算横幅「**你击败了丫鬟阿娜**」+「收 手」。
    ⇒ B-013「战斗可打」有真机硬证(本轮)。
    
    ## 二、开战的前提:钱包必须**附身**某个人物(领域模型)
    同一驱动、同一台机,换**未附身**钱包(`local_trade_qa`/`KSbda5…`)时:
    ```
    KUSHAN_BATTLE_QA debug_begin ok=false
    KUSHAN_BATTLE_QA round=1 ok=false status= cmd_count=0
    KUSHAN_BATTLE_QA DONE rounds=1
    ```
    服务端原话(直打 `POST /api/combat/start`):
    ```
    {"success":false,"error":"该钱包没有附身任何 NPC,无法战斗"}
    ```
    **根因**:`gateway.py::combat_start` 用 `SELECT cp.character_id FROM character_panel cp WHERE cp.wallet_player=%s` 取 attacker,
    未命中即拒。**库内实测只有 1 条附身关系**(`character_panel` id=1:character 19 / `wallet_player=KS194fb06…` / `wallet_npc=KB19xxd_npc`)。
    **不是缺陷** —— 是领域模型「玩家一律附身 NPC」的直接体现;但**驱动侧原来只打 `ok=false` 不打原因**,会让排查白跑(本轮已补,见下)。
    
  • 版本 1.0.369 开发日志

    构建 1.0.369(code 369)· 2026-10-02 13:01:36 · sha256 6788ab4deec7a8ef…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 12:56:47 | 本包:kushan-1.0.369.apk(1.0.369 / code 369,sha256 6788ab4deec7a8ef…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-GUARD-DELETE-TOLERANT  (2026-10-02 12:55:47)
    
    # TASK-GUARD-DELETE-TOLERANT(2026-10-02)——「参数键存在性」新护栏 + 5 个护栏的删件容忍
    
    ## 一、补上真实盲区:代码引用的参数键,没人静态扫(→ 新护栏 `tools/verify_param_keys_missing.py`)
    
    **起因(真机逮到)**:1.0.366/367 真机日志有 `ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    `verify_param_wiring` 验的是**运行时行为**「缺参即报错」(H 段),扫不到「代码引用了不存在的键」——
    那条代码路径只要测试没跑到,缺参就一直潜伏(本例潜伏到玩家上报差事才可能挂住请求,因为 `HTTPRequest.timeout = 0` 表示**永不超时**)。
    
    **判据(与 `plugins/params.gd` 同源)**:参数存在 ⇔ `data/params.json` 的 `params` 袋里有这个键
    (`_raw()` 里 `_effective()` 找不到层 + `meta()` 找不到 schema ⇒ push_error 并返回 0);
    世界/帝国/领地/皮/个人各层都是**运行期覆盖**,不是存在性来源。
    
    **做法**:扫 `scripts/`+`plugins/` 全部 `.gd` 里 `Params.<getter|meta|writer>("字面键")`(动态拼接跳过并计数),
    与 `data/params.json` 求差。带理由白名单 `tools/param_keys_missing_whitelist.tsv`(键<tab>理由);
    `params.json` 读不到 ⇒ `RESULT=SKIP`(不假红)。已转正为 `verify_` 前缀 ⇒ 自动进 `tools/run_verify_all.sh`。
    
    **首跑就抓到 8 个真缺键**(全在 `scripts/voice_input.gd`):`assistant.mic.{sample_rate,channels,max_ms,min_ms,auto_send,enabled,asr_path,asr_timeout_sec}`。
    查证=**改名没跟到底**:真名族是 `voice.*`(`voice.sample_rate=16000`/`voice.channels=1`/`voice.max_sec=8`/`voice.enabled=1`/`voice.endpoint=/api/asr`…),
    而该文件读的是旧前缀 `assistant.mic.*` ⇒ 读到的全是 0/false/"":**采样率 0、声道 0、录音时长 0、`_mic_btn.visible=false`**(麦克风按钮根本不显示)。
    
    **处置=删件而不是补参**:`scripts/voice_input.gd` 是语音重构前的遗留件 ——
    ① 全仓**无人实例化它**(只有两处注释提到名字,加 `tools/verify_ui_e21.gd` 的待核清单);
    ② 它按**旧 4 参**签名调 `MicBridge.bind(sample_rate, channels, max_ms, min_ms)`,而现核心 `scripts/core/mic_bridge.gd` 已是
    **6 参** `bind(sample_rate, channels, max_sec, silence_sec, silence_rms, poll_sec)` ⇒ 就算被实例化也会炸;
    ③ 现役实现是 `scripts/room/voice_bar.gd`(文件头就写着「可调项全在参数 `voice.*`/`tts.*`」)。
    ⇒ 删 `scripts/voice_input.gd` + `.uid`,并同步 `tools/verify_ui_e21.gd` 的待核清单。删后扫描器 **缺键 0**(扫描键 388→376)。
    
    ## 二、删件把 5 个护栏打崩/打红 ⇒ 补「删件容忍」(护栏扩展·技术债)
    
    删掉遗留件后全量回归报 **5 红**,且**不是行号漂移**、是真崩溃(`FileNotFoundError` traceback)/计数失真:
    `verify_color`(C.5 站点 + C.6 改前↔改后逐处对账的计数)、`verify_client_i18n`(C1.3 与 C1.7 两处 cache 读件)、
    `verify_semantic`(S.2 迁移点计数 + S.5 `_meta.sites` 站点)、`verify_wiring`(W.1 期望调用点)、`verify_fx`(F.5 站点)。
    
    **处置口径**(沿用本仓既有纪律「护栏取不到数据必须报错或跳过,**不能拿空表比绿**」):**跳过 + 出声计数**。
    逐处改为「文件不存在 ⇒ 计入 `已删件跳过 N` 并 continue」,行里不留判红、也不静默。
    **没有剪基线**:先试过剪 `evidence/TASK-E19/E20/E25/*.tsv` 与 `data/ui_config.json` 的 `_meta.sites`,
    副作用是 **21 个键的站点列表被剪空**(会在别的判据上产生新的假红),且让「改前现场」基线失真 ⇒ 全部回滚,改走护栏容忍。
    (另记一条坑:往 TSV 里插 `#` 说明行会让只跳空行/表头行的护栏 `int(f[1])` 崩 `IndexError` ⇒ 基线文件保持纯数据,说明写在本文档。)
    
    **复验**(每项单跑绿 + 出声报跳过数):
    
    ### TASK-ROOM-POPUP-SHELL  (2026-10-02 12:34:57)
    
    # TASK-ROOM-POPUP-SHELL —「战后房间整块点不动」根因与修复(2026-10-02 · 1.0.367 真机复验通过)
    
    ## 现象(真机)
    自动战斗打完 → 点「**收 手**」→ **房间整块点不动**:轨迹球(逻辑 `(660,1893.75)`=物理 `(880,2525)`)
    的 `input tap` **已被游戏收到**(`KUSHAN_TOUCH` 有记录)却毫无反应;方向键同样无反应、也不换房。
    界面看着像正常房间(10-02 起房间卡片 `card_bg_alpha=0`,空弹窗壳**看不见**)⇒ 极易误判为「卡死/注入失效」。
    
    ## 定性链(三步,都是实测/读码,不是猜)
    1. **命中探针**(cfg `HIT_X=660 HIT_Y=1893`,`room_refresh` 扫描 + `KUSHAN_HIT`):
       - 开战前:命中 2 层,**最上层=`/root/Main/Room_5/Fab (Button)`** ⇒ 轨迹球可点 ✓
       - 开战后:命中 **4 层**,**最上层=`/root/Main/Room_5/@Control@136/@MarginContainer@138/@PanelContainer@139`(全屏 rect=(0,0))** ⇒ 有东西压在轨迹球之上。
    2. **读码定位**:战斗面板不是独立场景,而是**挂在房间弹窗的内容区**里
       (`room/room_battle.open_battle_view()` 把 `BattleView` 节点 `add_child` 进 `_interact_box`);
       而 `battle_view._on_close()` 只做 `queue_free()` **关自己、没关弹窗**。
    3. **吃点击的是弹窗壳**:`room_popup` 的 `_popup_wrap` 是**全屏 Control**,只要 `_popup.visible`
       就吃掉房间所有点击;空壳+透明卡片让人看不出还开着。
    
    ## 修法(两处,均在 `scripts/room/room_popup.gd`)
    1. **内容判定不看总数**(2026-10-02):`child_exiting_tree` 是「**即将**离开」时发出(那一刻孩子还在树上),
       而关窗走 `UI.clear_children()`(内部 `queue_free()`=延后释放)⇒ 旧写 `get_child_count() > 0`
       会把「已排队删除」的也算进去 ⇒ 遮罩永久停在 `STOP`。改为 `_has_content_now()`:
       **只数真正在树上、且没被 `is_queued_for_deletion()` 的** ⇒ 与信号/释放时序彻底解耦。
    2. **内容空了连壳一起关**:`if not has_content and _popup.visible: _close_popup()`
       (延后一帧判定;正常流程「先 `_clear_interact()` 再同步加内容」同帧仍有内容,不会误关)。
    
    ## 真机复验(1.0.367 · 三星 R3CR704Q72V · sha256 `bf6036f9f47b43ea…`)
    ```
    KUSHAN_BATTLE_QA debug_begin ok=true → round=1..4 ok=true → status=win → DONE rounds=4
    (点「收 手」)
    ③ 关键判定:轨迹球能否开出菜单(修前:点了没反应)
      ✅ RESULT=PASS 菜单自报出现(修成立)
      KUSHAN_MENUITEM … 文=🧭 旅伴 / 🎒 背包 / 📜 任务 / 🗺 世界地图 / 🛒 商店 / ⚔ 遭遇战 / 💾 保存进度 / 设置 / 🚪 退出游戏
      KUSHAN_MENUITEM … 文=生命 51/50 | 克什 300.00 …(玩家信息卡也回来了)
    ④ 真点 [背包] logical=(520,631) phys=(693,841) → 背包面板照常打开(水囊 x1/干馕 x2/使用/丢弃)
    ```
    
    ## 顺带逮到并补登:缺参 `net.quest_timeout_sec`
    真机日志里一行 `E godot: ERROR: [Params] 缺参(键不在任何层,也不在出厂 schema): net.quest_timeout_sec`。
    查证:`scripts/game_state.gd:220` 在读它,而 `data/params.json` **没有这个键** ⇒ 运行时只报错并拿到 **0**
    (`HTTPRequest.timeout = 0` = **永不超时**),差事上报请求可能一直挂着。已按同族风格补登
    
    ### TASK-BATTLE-DEVICE  (2026-10-02 12:02:25)
    
    # TASK-BATTLE-DEVICE — 战斗系统真机全链取证(2026-10-02)
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · cfg `BATTLE_AUTOPLAY=1`(驱动 `scripts/battle_autoplay.gd`)
    走**真实玩家路径**:进世界 → 找本屋可交手的人 → `_open_battle_view` 开战 → 推回合。
    
    ## 一、结论:战斗可打,服务端裁决到结算全链通 ✅
    **附身钱包** `KS194fb06ed5f92ddeaf0bf33f7989356771ba4cae`(`character_panel` 里唯一附身关系:→ 人物 19):
    ```
    KUSHAN_BATTLE_QA begin
    KUSHAN_BATTLE_QA target id=17 name=丫鬟阿娜
    KUSHAN_BATTLE_QA battle_open
    KUSHAN_BATTLE_QA debug_begin ok=true                       ← 开战成立(服务端建 encounter)
    KUSHAN_BATTLE_QA round=1 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=2 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=3 ok=true status=active cmd_count=4
    KUSHAN_BATTLE_QA round=4 ok=true status=win  cmd_count=3   ← 打赢、服务端结算
    KUSHAN_BATTLE_QA DONE rounds=4
    ```
    面板自报(`MENU_DUMP=1`):`攻 击/防 御/逃 走/收 手`+提示「选一个动作,开打。」
    截图 OCR(`tools/qa_shot_ocr.py`):结算横幅「**你击败了丫鬟阿娜**」+「收 手」。
    ⇒ B-013「战斗可打」有真机硬证(本轮)。
    
    ## 二、开战的前提:钱包必须**附身**某个人物(领域模型)
    同一驱动、同一台机,换**未附身**钱包(`local_trade_qa`/`KSbda5…`)时:
    ```
    KUSHAN_BATTLE_QA debug_begin ok=false
    KUSHAN_BATTLE_QA round=1 ok=false status= cmd_count=0
    KUSHAN_BATTLE_QA DONE rounds=1
    ```
    服务端原话(直打 `POST /api/combat/start`):
    ```
    {"success":false,"error":"该钱包没有附身任何 NPC,无法战斗"}
    ```
    **根因**:`gateway.py::combat_start` 用 `SELECT cp.character_id FROM character_panel cp WHERE cp.wallet_player=%s` 取 attacker,
    未命中即拒。**库内实测只有 1 条附身关系**(`character_panel` id=1:character 19 / `wallet_player=KS194fb06…` / `wallet_npc=KB19xxd_npc`)。
    **不是缺陷** —— 是领域模型「玩家一律附身 NPC」的直接体现;但**驱动侧原来只打 `ok=false` 不打原因**,会让排查白跑(本轮已补,见下)。
    
  • 版本 1.0.368 开发日志

    构建 1.0.368(code 368)· 2026-10-02 12:33:57 · sha256 030d6e14c924610e…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 12:29:08 | 本包:kushan-1.0.368.apk(1.0.368 / code 368,sha256 030d6e14c924610e…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-BATTLE-DEVICE  (2026-10-02 12:02:25)
    
    # TASK-BATTLE-DEVICE — 战斗系统真机全链取证(2026-10-02)
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · cfg `BATTLE_AUTOPLAY=1`(驱动 `scripts/battle_autoplay.gd`)
    走**真实玩家路径**:进世界 → 找本屋可交手的人 → `_open_battle_view` 开战 → 推回合。
    
    ## 一、结论:战斗可打,服务端裁决到结算全链通 ✅
    **附身钱包** `KS194fb06ed5f92ddeaf0bf33f7989356771ba4cae`(`character_panel` 里唯一附身关系:→ 人物 19):
    ```
    KUSHAN_BATTLE_QA begin
    KUSHAN_BATTLE_QA target id=17 name=丫鬟阿娜
    KUSHAN_BATTLE_QA battle_open
    KUSHAN_BATTLE_QA debug_begin ok=true                       ← 开战成立(服务端建 encounter)
    KUSHAN_BATTLE_QA round=1 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=2 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=3 ok=true status=active cmd_count=4
    KUSHAN_BATTLE_QA round=4 ok=true status=win  cmd_count=3   ← 打赢、服务端结算
    KUSHAN_BATTLE_QA DONE rounds=4
    ```
    面板自报(`MENU_DUMP=1`):`攻 击/防 御/逃 走/收 手`+提示「选一个动作,开打。」
    截图 OCR(`tools/qa_shot_ocr.py`):结算横幅「**你击败了丫鬟阿娜**」+「收 手」。
    ⇒ B-013「战斗可打」有真机硬证(本轮)。
    
    ## 二、开战的前提:钱包必须**附身**某个人物(领域模型)
    同一驱动、同一台机,换**未附身**钱包(`local_trade_qa`/`KSbda5…`)时:
    ```
    KUSHAN_BATTLE_QA debug_begin ok=false
    KUSHAN_BATTLE_QA round=1 ok=false status= cmd_count=0
    KUSHAN_BATTLE_QA DONE rounds=1
    ```
    服务端原话(直打 `POST /api/combat/start`):
    ```
    {"success":false,"error":"该钱包没有附身任何 NPC,无法战斗"}
    ```
    **根因**:`gateway.py::combat_start` 用 `SELECT cp.character_id FROM character_panel cp WHERE cp.wallet_player=%s` 取 attacker,
    未命中即拒。**库内实测只有 1 条附身关系**(`character_panel` id=1:character 19 / `wallet_player=KS194fb06…` / `wallet_npc=KB19xxd_npc`)。
    **不是缺陷** —— 是领域模型「玩家一律附身 NPC」的直接体现;但**驱动侧原来只打 `ok=false` 不打原因**,会让排查白跑(本轮已补,见下)。
    
    ## 三、顺手补的诊断(本轮改码)
    `scripts/battle_autoplay.gd`:`debug_begin`/`round` 两行**都加上 `error=`**。
    改前只有 `ok=false`,无从区分「没网关卡 / 服务端拒绝」,实测为此多查了一轮(先怀疑驱动、再怀疑网关、最后才直打端点拿到原话)。
    
    ### TASK-B010-SETTINGS  (2026-10-02 11:51:06)
    
    # TASK-B010-SETTINGS — 设置页四项「改 → 冷启 → 仍生效」真机验收(2026-10-02)
    
    **口径**:41 号 2.4「字号/皮肤/音量/清缓存+语音助手开关,重启仍生效」。
    **驱动**:`scripts/settings_qa.gd`(调设置页自己的 handler,零新逻辑;`SETTINGS_QA=run` 改、`=verify` 冷启回读)。
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`。
    
    ## ① run(改一遍)
    ```
    KUSHAN_SETTINGS_QA 字号 [run] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [run] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [run] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [run] 期望=0 实得=0 PASS      (清前=12 清后=0)
    KUSHAN_SETTINGS_QA 语音开关 [run·翻回] 期望=true 实得=true PASS
    ```
    
    ## ② verify(**冷启后**回读 —— 本项要证的就是这一档)
    ```
    KUSHAN_SETTINGS_QA 字号 [verify] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [verify] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [verify] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [verify] 期望=0 实得=0 PASS
    ```
    ⇒ **四项(字号/皮肤/音量/缓存)跨冷启持久化成立** ✓(持久化落点 `user://settings.json`,即应用私有目录)。
    两轮日志时间戳:run `11:48:57`(pid 21288)→ verify `11:49:25`(**pid 21579,另一个进程** ⇒ 确为重启后回读,不是同一进程内自证)。
    
    ## 遗留
    - **语音助手开关**只在 `run` 档打了(`[run·翻回]`),`verify` 档**不回读该项** ⇒ 「开关本身跨重启」未单独立证(其落点同 `settings.json`)。
    - `user://settings.json` 在**发布包**里属应用私有目录(`run-as` 不可用)⇒ 现场未直读文件原文,以上以驱动回读为准。
    
    ## 附带订正
    `scripts/settings_qa.gd` 头注沿用旧话「本机**游戏模式拦注入触摸**」——该判断已于 2026-10-02 被推翻
    (真因是坐标用错:`input tap` 要**物理**=逻辑×窗口宽/720;三条硬证见 `evidence/TASK-B014-TOUCH/touch_proof.txt`)。本轮已改注释。
    
    ### TASK-B011-TRADE-SAY  (2026-10-02 11:48:39)
    
    # TASK-B011-TRADE-SAY — 「对话成交」真机验收(商品匹配/买/卖/余额变化)
    
    **口径**:交易走对话、不走面板(41 号 §九十四 S4 / 用户令 2026-10-01);钱货服务端裁决(`/api/trade/*`),
    本地只镜像。**本件验的就是玩家那句人话** —— `TRADE_QA_SAY` 把句子喂进 `_send_chat`(与玩家手打同一入口)。
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`(伯克·祖米热,在 670 巴扎馕摊,本屋掌柜 21 阿依古丽);
    货架真源 `shop_21_8` 蜂蜜馕 单价 5.00(服务端含税报 5.10)。
    
    ## 一、负例:零余额钱包(KS0b828…,0.00)
    ```
    KUSHAN_SAY_QA 本屋掌柜=21 要说的话=买 蜂蜜馕 一件
    KUSHAN_DT 到手=[买 蜂蜜馕 一件] 买词=true 卖词=false 对象=npc:21     ← 按名匹配到本屋货架 ✓
    KUSHAN_DT 买回执={"error":"余额不足:需 5.10,有 0.00","success":false,"tx_id":null}   ← 服务端原话 ✓
    KUSHAN_SAY_QA 事后钱=300.0 背包件数=2 ; final ksc=300.0 bag=2
    ```
    **库侧**:`wallet_accounts` 0.00 不变;`npc_transaction_log` 无新行(最近仍是 10-01 23:07 的 76/77/78)⇒ **零状态变更** ✓
    
    ## 二、正例:有余额钱包(KS972f…,1018.50)——「买」→「卖」
    ```
    说=[买 蜂蜜馕 一件] 事前钱=300.0 背包件数=2
    KUSHAN_DT 买回执={"action":"buy","item_name":"蜂蜜馕","quantity":1,"unit_price":5.1,"total_din":"5.1",
                      "ksc_remaining":"1013.4","stock_left":17,"success":true,"tx_id":"tx-buy-1790912836763-d26f48"}
    KUSHAN_DT bag_buy rows=3 total=4 ; 事后钱=1013.4 背包件数=3
    说=[卖 蜂蜜馕 一件]
    KUSHAN_DT sell_key=[shop_21_8] 反查=shop_21_8 镜像名=[蜂蜜馕] id=0 原键=[shop_21_8]   ← 镜像键反查货架键 ✓
    KUSHAN_DT 卖回执={"action":"sell","item_name":"蜂蜜馕","quantity":1,"total_din":"3.06",
                      "ksc_remaining":"1016.46","stock_left":17,"success":true,"tx_id":"tx-sell-1790912839778-75852f"}
    KUSHAN_DT bag_sell rows=2 total=3 ; 事后钱=1016.46 背包件数=2
    KUSHAN_SAY_QA DONE
    ```
    **库侧对账(硬数字)**:
    - 余额 `KS972f…` **1018.50 → 1016.46**(−5.10 买 +3.06 卖 = −2.04 ✓)
    - `npc_transaction_log` 新增 **79** `buy` 蜂蜜馕×1 `total_ksc=5.10` `committed` 11:47:16、**80** `sell` 蜂蜜馕×1 `3.06` `committed` 11:47:19 ✓
    - `character_inventory`:买时新建行 qty=1(id 59),卖出按**归零删行**(B-057 修法)⇒ 该钱包当时零行;为证落物独立复验:
      直打 `POST /api/trade/buy`(同钱包同货)⇒ 立刻回读得到 **id=60 KS972f… shop_21_8 qty=1** ✓(先前的「零行」是买后即卖的正常结果,**不是缺陷** —— 教训 #63 同款:先复验再定性)
    
    ## 三、判定
    | 子判据 | 结果 | 证据 |
    | --- | --- | --- |
    | 商品列表(按名匹配本屋货架) | ✅ | `买词=true 对象=npc:21` + `sell_key=[shop_21_8] 反查=…镜像名=[蜂蜜馕]` |
    
    ### TASK-B012-QUEST  (2026-10-02 11:39:20)
    
    # TASK-B012-QUEST — 任务环真机全链验收(接取→进度→交差)+ 交差 500 修复(2026-10-02)
    
  • 版本 1.0.367 开发日志

    构建 1.0.367(code 367)· 2026-10-02 12:25:09 · sha256 bf6036f9f47b43ea…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 12:20:21 | 本包:kushan-1.0.367.apk(1.0.367 / code 367,sha256 bf6036f9f47b43ea…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-BATTLE-DEVICE  (2026-10-02 12:02:25)
    
    # TASK-BATTLE-DEVICE — 战斗系统真机全链取证(2026-10-02)
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · cfg `BATTLE_AUTOPLAY=1`(驱动 `scripts/battle_autoplay.gd`)
    走**真实玩家路径**:进世界 → 找本屋可交手的人 → `_open_battle_view` 开战 → 推回合。
    
    ## 一、结论:战斗可打,服务端裁决到结算全链通 ✅
    **附身钱包** `KS194fb06ed5f92ddeaf0bf33f7989356771ba4cae`(`character_panel` 里唯一附身关系:→ 人物 19):
    ```
    KUSHAN_BATTLE_QA begin
    KUSHAN_BATTLE_QA target id=17 name=丫鬟阿娜
    KUSHAN_BATTLE_QA battle_open
    KUSHAN_BATTLE_QA debug_begin ok=true                       ← 开战成立(服务端建 encounter)
    KUSHAN_BATTLE_QA round=1 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=2 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=3 ok=true status=active cmd_count=4
    KUSHAN_BATTLE_QA round=4 ok=true status=win  cmd_count=3   ← 打赢、服务端结算
    KUSHAN_BATTLE_QA DONE rounds=4
    ```
    面板自报(`MENU_DUMP=1`):`攻 击/防 御/逃 走/收 手`+提示「选一个动作,开打。」
    截图 OCR(`tools/qa_shot_ocr.py`):结算横幅「**你击败了丫鬟阿娜**」+「收 手」。
    ⇒ B-013「战斗可打」有真机硬证(本轮)。
    
    ## 二、开战的前提:钱包必须**附身**某个人物(领域模型)
    同一驱动、同一台机,换**未附身**钱包(`local_trade_qa`/`KSbda5…`)时:
    ```
    KUSHAN_BATTLE_QA debug_begin ok=false
    KUSHAN_BATTLE_QA round=1 ok=false status= cmd_count=0
    KUSHAN_BATTLE_QA DONE rounds=1
    ```
    服务端原话(直打 `POST /api/combat/start`):
    ```
    {"success":false,"error":"该钱包没有附身任何 NPC,无法战斗"}
    ```
    **根因**:`gateway.py::combat_start` 用 `SELECT cp.character_id FROM character_panel cp WHERE cp.wallet_player=%s` 取 attacker,
    未命中即拒。**库内实测只有 1 条附身关系**(`character_panel` id=1:character 19 / `wallet_player=KS194fb06…` / `wallet_npc=KB19xxd_npc`)。
    **不是缺陷** —— 是领域模型「玩家一律附身 NPC」的直接体现;但**驱动侧原来只打 `ok=false` 不打原因**,会让排查白跑(本轮已补,见下)。
    
    ## 三、顺手补的诊断(本轮改码)
    `scripts/battle_autoplay.gd`:`debug_begin`/`round` 两行**都加上 `error=`**。
    改前只有 `ok=false`,无从区分「没网关卡 / 服务端拒绝」,实测为此多查了一轮(先怀疑驱动、再怀疑网关、最后才直打端点拿到原话)。
    
    ### TASK-B010-SETTINGS  (2026-10-02 11:51:06)
    
    # TASK-B010-SETTINGS — 设置页四项「改 → 冷启 → 仍生效」真机验收(2026-10-02)
    
    **口径**:41 号 2.4「字号/皮肤/音量/清缓存+语音助手开关,重启仍生效」。
    **驱动**:`scripts/settings_qa.gd`(调设置页自己的 handler,零新逻辑;`SETTINGS_QA=run` 改、`=verify` 冷启回读)。
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`。
    
    ## ① run(改一遍)
    ```
    KUSHAN_SETTINGS_QA 字号 [run] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [run] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [run] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [run] 期望=0 实得=0 PASS      (清前=12 清后=0)
    KUSHAN_SETTINGS_QA 语音开关 [run·翻回] 期望=true 实得=true PASS
    ```
    
    ## ② verify(**冷启后**回读 —— 本项要证的就是这一档)
    ```
    KUSHAN_SETTINGS_QA 字号 [verify] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [verify] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [verify] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [verify] 期望=0 实得=0 PASS
    ```
    ⇒ **四项(字号/皮肤/音量/缓存)跨冷启持久化成立** ✓(持久化落点 `user://settings.json`,即应用私有目录)。
    两轮日志时间戳:run `11:48:57`(pid 21288)→ verify `11:49:25`(**pid 21579,另一个进程** ⇒ 确为重启后回读,不是同一进程内自证)。
    
    ## 遗留
    - **语音助手开关**只在 `run` 档打了(`[run·翻回]`),`verify` 档**不回读该项** ⇒ 「开关本身跨重启」未单独立证(其落点同 `settings.json`)。
    - `user://settings.json` 在**发布包**里属应用私有目录(`run-as` 不可用)⇒ 现场未直读文件原文,以上以驱动回读为准。
    
    ## 附带订正
    `scripts/settings_qa.gd` 头注沿用旧话「本机**游戏模式拦注入触摸**」——该判断已于 2026-10-02 被推翻
    (真因是坐标用错:`input tap` 要**物理**=逻辑×窗口宽/720;三条硬证见 `evidence/TASK-B014-TOUCH/touch_proof.txt`)。本轮已改注释。
    
    ### TASK-B011-TRADE-SAY  (2026-10-02 11:48:39)
    
    # TASK-B011-TRADE-SAY — 「对话成交」真机验收(商品匹配/买/卖/余额变化)
    
    **口径**:交易走对话、不走面板(41 号 §九十四 S4 / 用户令 2026-10-01);钱货服务端裁决(`/api/trade/*`),
    本地只镜像。**本件验的就是玩家那句人话** —— `TRADE_QA_SAY` 把句子喂进 `_send_chat`(与玩家手打同一入口)。
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`(伯克·祖米热,在 670 巴扎馕摊,本屋掌柜 21 阿依古丽);
    货架真源 `shop_21_8` 蜂蜜馕 单价 5.00(服务端含税报 5.10)。
    
    ## 一、负例:零余额钱包(KS0b828…,0.00)
    ```
    KUSHAN_SAY_QA 本屋掌柜=21 要说的话=买 蜂蜜馕 一件
    KUSHAN_DT 到手=[买 蜂蜜馕 一件] 买词=true 卖词=false 对象=npc:21     ← 按名匹配到本屋货架 ✓
    KUSHAN_DT 买回执={"error":"余额不足:需 5.10,有 0.00","success":false,"tx_id":null}   ← 服务端原话 ✓
    KUSHAN_SAY_QA 事后钱=300.0 背包件数=2 ; final ksc=300.0 bag=2
    ```
    **库侧**:`wallet_accounts` 0.00 不变;`npc_transaction_log` 无新行(最近仍是 10-01 23:07 的 76/77/78)⇒ **零状态变更** ✓
    
    ## 二、正例:有余额钱包(KS972f…,1018.50)——「买」→「卖」
    ```
    说=[买 蜂蜜馕 一件] 事前钱=300.0 背包件数=2
    KUSHAN_DT 买回执={"action":"buy","item_name":"蜂蜜馕","quantity":1,"unit_price":5.1,"total_din":"5.1",
                      "ksc_remaining":"1013.4","stock_left":17,"success":true,"tx_id":"tx-buy-1790912836763-d26f48"}
    KUSHAN_DT bag_buy rows=3 total=4 ; 事后钱=1013.4 背包件数=3
    说=[卖 蜂蜜馕 一件]
    KUSHAN_DT sell_key=[shop_21_8] 反查=shop_21_8 镜像名=[蜂蜜馕] id=0 原键=[shop_21_8]   ← 镜像键反查货架键 ✓
    KUSHAN_DT 卖回执={"action":"sell","item_name":"蜂蜜馕","quantity":1,"total_din":"3.06",
                      "ksc_remaining":"1016.46","stock_left":17,"success":true,"tx_id":"tx-sell-1790912839778-75852f"}
    KUSHAN_DT bag_sell rows=2 total=3 ; 事后钱=1016.46 背包件数=2
    KUSHAN_SAY_QA DONE
    ```
    **库侧对账(硬数字)**:
    - 余额 `KS972f…` **1018.50 → 1016.46**(−5.10 买 +3.06 卖 = −2.04 ✓)
    - `npc_transaction_log` 新增 **79** `buy` 蜂蜜馕×1 `total_ksc=5.10` `committed` 11:47:16、**80** `sell` 蜂蜜馕×1 `3.06` `committed` 11:47:19 ✓
    - `character_inventory`:买时新建行 qty=1(id 59),卖出按**归零删行**(B-057 修法)⇒ 该钱包当时零行;为证落物独立复验:
      直打 `POST /api/trade/buy`(同钱包同货)⇒ 立刻回读得到 **id=60 KS972f… shop_21_8 qty=1** ✓(先前的「零行」是买后即卖的正常结果,**不是缺陷** —— 教训 #63 同款:先复验再定性)
    
    ## 三、判定
    | 子判据 | 结果 | 证据 |
    | --- | --- | --- |
    | 商品列表(按名匹配本屋货架) | ✅ | `买词=true 对象=npc:21` + `sell_key=[shop_21_8] 反查=…镜像名=[蜂蜜馕]` |
    
    ### TASK-B012-QUEST  (2026-10-02 11:39:20)
    
    # TASK-B012-QUEST — 任务环真机全链验收(接取→进度→交差)+ 交差 500 修复(2026-10-02)
    
  • 版本 1.0.366 开发日志

    构建 1.0.366(code 366)· 2026-10-02 12:15:24 · sha256 a165f2058217e7d0…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 12:10:39 | 本包:kushan-1.0.366.apk(1.0.366 / code 366,sha256 a165f2058217e7d0…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-BATTLE-DEVICE  (2026-10-02 12:02:25)
    
    # TASK-BATTLE-DEVICE — 战斗系统真机全链取证(2026-10-02)
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · cfg `BATTLE_AUTOPLAY=1`(驱动 `scripts/battle_autoplay.gd`)
    走**真实玩家路径**:进世界 → 找本屋可交手的人 → `_open_battle_view` 开战 → 推回合。
    
    ## 一、结论:战斗可打,服务端裁决到结算全链通 ✅
    **附身钱包** `KS194fb06ed5f92ddeaf0bf33f7989356771ba4cae`(`character_panel` 里唯一附身关系:→ 人物 19):
    ```
    KUSHAN_BATTLE_QA begin
    KUSHAN_BATTLE_QA target id=17 name=丫鬟阿娜
    KUSHAN_BATTLE_QA battle_open
    KUSHAN_BATTLE_QA debug_begin ok=true                       ← 开战成立(服务端建 encounter)
    KUSHAN_BATTLE_QA round=1 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=2 ok=true status=active cmd_count=2
    KUSHAN_BATTLE_QA round=3 ok=true status=active cmd_count=4
    KUSHAN_BATTLE_QA round=4 ok=true status=win  cmd_count=3   ← 打赢、服务端结算
    KUSHAN_BATTLE_QA DONE rounds=4
    ```
    面板自报(`MENU_DUMP=1`):`攻 击/防 御/逃 走/收 手`+提示「选一个动作,开打。」
    截图 OCR(`tools/qa_shot_ocr.py`):结算横幅「**你击败了丫鬟阿娜**」+「收 手」。
    ⇒ B-013「战斗可打」有真机硬证(本轮)。
    
    ## 二、开战的前提:钱包必须**附身**某个人物(领域模型)
    同一驱动、同一台机,换**未附身**钱包(`local_trade_qa`/`KSbda5…`)时:
    ```
    KUSHAN_BATTLE_QA debug_begin ok=false
    KUSHAN_BATTLE_QA round=1 ok=false status= cmd_count=0
    KUSHAN_BATTLE_QA DONE rounds=1
    ```
    服务端原话(直打 `POST /api/combat/start`):
    ```
    {"success":false,"error":"该钱包没有附身任何 NPC,无法战斗"}
    ```
    **根因**:`gateway.py::combat_start` 用 `SELECT cp.character_id FROM character_panel cp WHERE cp.wallet_player=%s` 取 attacker,
    未命中即拒。**库内实测只有 1 条附身关系**(`character_panel` id=1:character 19 / `wallet_player=KS194fb06…` / `wallet_npc=KB19xxd_npc`)。
    **不是缺陷** —— 是领域模型「玩家一律附身 NPC」的直接体现;但**驱动侧原来只打 `ok=false` 不打原因**,会让排查白跑(本轮已补,见下)。
    
    ## 三、顺手补的诊断(本轮改码)
    `scripts/battle_autoplay.gd`:`debug_begin`/`round` 两行**都加上 `error=`**。
    改前只有 `ok=false`,无从区分「没网关卡 / 服务端拒绝」,实测为此多查了一轮(先怀疑驱动、再怀疑网关、最后才直打端点拿到原话)。
    
    ### TASK-B010-SETTINGS  (2026-10-02 11:51:06)
    
    # TASK-B010-SETTINGS — 设置页四项「改 → 冷启 → 仍生效」真机验收(2026-10-02)
    
    **口径**:41 号 2.4「字号/皮肤/音量/清缓存+语音助手开关,重启仍生效」。
    **驱动**:`scripts/settings_qa.gd`(调设置页自己的 handler,零新逻辑;`SETTINGS_QA=run` 改、`=verify` 冷启回读)。
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`。
    
    ## ① run(改一遍)
    ```
    KUSHAN_SETTINGS_QA 字号 [run] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [run] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [run] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [run] 期望=0 实得=0 PASS      (清前=12 清后=0)
    KUSHAN_SETTINGS_QA 语音开关 [run·翻回] 期望=true 实得=true PASS
    ```
    
    ## ② verify(**冷启后**回读 —— 本项要证的就是这一档)
    ```
    KUSHAN_SETTINGS_QA 字号 [verify] 期望=1.2 实得=1.2 PASS
    KUSHAN_SETTINGS_QA 皮肤 [verify] 期望=非空且=UI.skin().display_name() 实得=鎏金御用 PASS
    KUSHAN_SETTINGS_QA 音量 [verify] 期望=0.42 实得=0.42 PASS
    KUSHAN_SETTINGS_QA 清缓存 [verify] 期望=0 实得=0 PASS
    ```
    ⇒ **四项(字号/皮肤/音量/缓存)跨冷启持久化成立** ✓(持久化落点 `user://settings.json`,即应用私有目录)。
    两轮日志时间戳:run `11:48:57`(pid 21288)→ verify `11:49:25`(**pid 21579,另一个进程** ⇒ 确为重启后回读,不是同一进程内自证)。
    
    ## 遗留
    - **语音助手开关**只在 `run` 档打了(`[run·翻回]`),`verify` 档**不回读该项** ⇒ 「开关本身跨重启」未单独立证(其落点同 `settings.json`)。
    - `user://settings.json` 在**发布包**里属应用私有目录(`run-as` 不可用)⇒ 现场未直读文件原文,以上以驱动回读为准。
    
    ## 附带订正
    `scripts/settings_qa.gd` 头注沿用旧话「本机**游戏模式拦注入触摸**」——该判断已于 2026-10-02 被推翻
    (真因是坐标用错:`input tap` 要**物理**=逻辑×窗口宽/720;三条硬证见 `evidence/TASK-B014-TOUCH/touch_proof.txt`)。本轮已改注释。
    
    ### TASK-B011-TRADE-SAY  (2026-10-02 11:48:39)
    
    # TASK-B011-TRADE-SAY — 「对话成交」真机验收(商品匹配/买/卖/余额变化)
    
    **口径**:交易走对话、不走面板(41 号 §九十四 S4 / 用户令 2026-10-01);钱货服务端裁决(`/api/trade/*`),
    本地只镜像。**本件验的就是玩家那句人话** —— `TRADE_QA_SAY` 把句子喂进 `_send_chat`(与玩家手打同一入口)。
    
    **设备/包**:三星 `R3CR704Q72V` · 1.0.365 · `AUTO_ENTER=59`(伯克·祖米热,在 670 巴扎馕摊,本屋掌柜 21 阿依古丽);
    货架真源 `shop_21_8` 蜂蜜馕 单价 5.00(服务端含税报 5.10)。
    
    ## 一、负例:零余额钱包(KS0b828…,0.00)
    ```
    KUSHAN_SAY_QA 本屋掌柜=21 要说的话=买 蜂蜜馕 一件
    KUSHAN_DT 到手=[买 蜂蜜馕 一件] 买词=true 卖词=false 对象=npc:21     ← 按名匹配到本屋货架 ✓
    KUSHAN_DT 买回执={"error":"余额不足:需 5.10,有 0.00","success":false,"tx_id":null}   ← 服务端原话 ✓
    KUSHAN_SAY_QA 事后钱=300.0 背包件数=2 ; final ksc=300.0 bag=2
    ```
    **库侧**:`wallet_accounts` 0.00 不变;`npc_transaction_log` 无新行(最近仍是 10-01 23:07 的 76/77/78)⇒ **零状态变更** ✓
    
    ## 二、正例:有余额钱包(KS972f…,1018.50)——「买」→「卖」
    ```
    说=[买 蜂蜜馕 一件] 事前钱=300.0 背包件数=2
    KUSHAN_DT 买回执={"action":"buy","item_name":"蜂蜜馕","quantity":1,"unit_price":5.1,"total_din":"5.1",
                      "ksc_remaining":"1013.4","stock_left":17,"success":true,"tx_id":"tx-buy-1790912836763-d26f48"}
    KUSHAN_DT bag_buy rows=3 total=4 ; 事后钱=1013.4 背包件数=3
    说=[卖 蜂蜜馕 一件]
    KUSHAN_DT sell_key=[shop_21_8] 反查=shop_21_8 镜像名=[蜂蜜馕] id=0 原键=[shop_21_8]   ← 镜像键反查货架键 ✓
    KUSHAN_DT 卖回执={"action":"sell","item_name":"蜂蜜馕","quantity":1,"total_din":"3.06",
                      "ksc_remaining":"1016.46","stock_left":17,"success":true,"tx_id":"tx-sell-1790912839778-75852f"}
    KUSHAN_DT bag_sell rows=2 total=3 ; 事后钱=1016.46 背包件数=2
    KUSHAN_SAY_QA DONE
    ```
    **库侧对账(硬数字)**:
    - 余额 `KS972f…` **1018.50 → 1016.46**(−5.10 买 +3.06 卖 = −2.04 ✓)
    - `npc_transaction_log` 新增 **79** `buy` 蜂蜜馕×1 `total_ksc=5.10` `committed` 11:47:16、**80** `sell` 蜂蜜馕×1 `3.06` `committed` 11:47:19 ✓
    - `character_inventory`:买时新建行 qty=1(id 59),卖出按**归零删行**(B-057 修法)⇒ 该钱包当时零行;为证落物独立复验:
      直打 `POST /api/trade/buy`(同钱包同货)⇒ 立刻回读得到 **id=60 KS972f… shop_21_8 qty=1** ✓(先前的「零行」是买后即卖的正常结果,**不是缺陷** —— 教训 #63 同款:先复验再定性)
    
    ## 三、判定
    | 子判据 | 结果 | 证据 |
    | --- | --- | --- |
    | 商品列表(按名匹配本屋货架) | ✅ | `买词=true 对象=npc:21` + `sell_key=[shop_21_8] 反查=…镜像名=[蜂蜜馕]` |
    
    ### TASK-B012-QUEST  (2026-10-02 11:39:20)
    
    # TASK-B012-QUEST — 任务环真机全链验收(接取→进度→交差)+ 交差 500 修复(2026-10-02)
    
  • 版本 1.0.365 开发日志

    构建 1.0.365(code 365)· 2026-10-02 03:04:55 · sha256 4e60243267cebb92…

    下载:公网 · Tailscale · 局域网

    开发日志(本轮摘要)

    # 贵霜帝国 · 内网开发版 · 本轮开发记录
    
    生成时间:2026-10-02 03:00:43 | 本包:kushan-1.0.365.apk(1.0.365 / code 365,sha256 4e60243267cebb92…)
    
    ## 最近开发任务证据(evidence/*/readme.md,新→旧,取 10 份)
    
    ### TASK-B037-RECUR  (2026-10-02 02:30:59)
    
    # TASK-B037-RECUR · B-037 结案 + 同类复发(**已收口**)
    
    **日期**:2026-10-02 02:40 | **包**:1.0.358(真机 `R3CR704Q72V`)
    
    ## 一、B-037 原判据的处置:**对象已不存在**
    `scripts/room/shop_panel.gd` 已随 41 号 §九十四 S3(b) 退场(文件实测不存在;`room_view.gd` 已不再引用)⇒ 「商店面板点不动」在当前代码里**无法复现**(面板本身没了)。**不因此只写「消解」就完事** —— 该缺陷的机制是**复发类**:
    `room_layout.gd` 内已有 4 处带「真机实测」注释的 `mouse_filter = MOUSE_FILTER_IGNORE` 处置(全屏底色/场景图/描述层…),说明「装饰层默认 STOP 把点击吃掉」在这个仓反复出现过。
    
    ## 二、复发网(新工具,**故意不叫 `verify_`**)
    `tools/scan_click_eaters.py` —— 扫房间界面里 `TextureRect/RichTextLabel/ColorRect/Panel/Label.new()` 的**非按钮变量**,看其后 40 行内是否显式 `MOUSE_FILTER_IGNORE`,列出候选。
    按仓库纪律(假阳性未清零前不得带 `verify_` 前缀、不进全量闸门),它是**给人看的候选清单**,输出落 `scan_click_eaters.txt`。
    
    **扫描结果与逐条裁决**(29 个文件,3 处已显式 IGNORE):
    ```
    ① scripts/room/map_screen.gd:120  bg = ColorRect.new()      → 不修:全屏地图遮罩,**应该**吃掉点击(挡住下层房间)
    ② scripts/room/room_person.gd:55  tr = TextureRect.new()     → ★修:立绘是装饰,父面板 p 才接交谈
    ③ scripts/room/room_refresh.gd:260 rt = RichTextLabel.new()  → 不修:出口「点文字走过去」的点击目标
    ```
    
    ## 三、本轮实修(②)+顺手收拾(遗留探针)
    1. `scripts/room/room_person.gd`:立绘 `TextureRect` 补 `mouse_filter = Control.MOUSE_FILTER_IGNORE`
       —— 父面板 `p` 已是 `STOP` 且 `gui_input` 接交谈;子层吃点击 ⇒ 表现就是「点立绘/名字(立绘范围内)没反应」,与 B-037 同机制。
    2. `scripts/room/room_refresh.gd:82`:**遗留探针**原先「每进一间房无条件扫屏打日志」(其上游注释写「验完即删」却一直在)⇒ 改 cfg 门控(`HIT_X/HIT_Y` 或 `MENU_DUMP=1` 才跑),生产零影响。
    
    ## 四、真机取证(1.0.358)
    ```
    (a) cfg 带 HIT_X/HIT_Y=470,760 ⇒ 探针行数=7   ← 探针功能保留,可继续做命中排查
    (b) cfg 不带                    ⇒ 探针行数=0   ← 门控生效,生产不再白扫屏/刷日志
    KUSHAN_HIT 点(470.0, 760.0) 命中 4 层,最上层=/root/Main/RoomView/@MarginContainer@105/@VB…(面板在顶层,正常)
       候选含 /root/Main/RoomView/@TextureRect@113(场景图层的 TextureRect **确实参与命中** ⇒ 该机制在真机确实存在)
    ```
    截图:`room358.png`(同包同 cfg,房间正常渲染)。
    
    ## 五、未验(如实记)
    - 「点立绘/名字」的**手指点击**未在本机取证(该机折叠屏外侧触控注入不可用,历史 E-010/B-061 同因);本轮以**代码判据+命中层证据**收口。
    - ①③ 两处按裁决**不改**(理由见上),若日后真机发现同点位点不动,按本卡口径复跑 `scan_click_eaters.py` 复核。
    
    ### TASK-MAP-MODULE-DEPS  (2026-10-02 02:24:20)
    
    # TASK-MAP-MODULE-DEPS · MAP-08 单向依赖(**已收口**)
    
    **日期**:2026-10-02 02:20 | **口径**:68 号 MAP-08(行数项 2026-09-26 已作废 ⇒ 见 G-010「只查职责不查行数」)
    
    ## 新护栏 `tools/verify_map_module_deps.py`(已自动纳入全量)
    判据:四模块内部引用**只能是** `map_screen → {map_nav, map_data, map_render}`;基础件不得反向引用 `map_screen`、彼此不得互引、不得成环。
    
    **正测(现仓)**:
    ```
       map_nav     → (无)
       map_data    → (无)
       map_render  → (无)
       map_screen  → map_data、map_nav、map_render
       ✔ 单向成立:3 条边全部向下,无反向边、无环 / RESULT=PASS
    ```
    
    **反测(临时往 `map_nav.gd` 注入 `preload("res://scripts/room/map_screen.gd")`,跑完 `git checkout` 还原)**:
    ```
       ✗ map_nav ↔ map_screen(成环)/ ✗ map_screen ↔ map_nav(成环)/ RESULT=FAIL(2 处)
    ```
    
    护栏**非空过**(正反两测均如预期)。**职责项**(一功能一文件)由既有 `tools/verify_one_job_per_file.py` 把关,本件不重复。
    
    **行数(仅记录,不作判据)**:`map_nav` 97/`map_data` 167/`map_render` 62/`map_screen` 267 行。
    
    ### TASK-MAP-WALK  (2026-10-02 02:15:43)
    
    # TASK-MAP-WALK · 四层地图真机验收(MAP-01/02/03/04/07 · **已收口**)
    
    **日期**:2026-10-02 01:20-02:05 | **包**:1.0.356(真机 `R3CR704Q72V`,真屏 display 3 `4630947232161729155`)
    **工具**:`tools/qa_map_walk.sh`(三层取图 + 切换)、`tools/qa_map_back_ab.sh`(退层 + 开关 A/B)、`tools/map_shot_metrics.py`(量图)
    **驱动**:cfg 通道 `MAP_QA_LAYER` / `MAP_QA_STEPS`(`jump|back|down|layer:N`)——**不用 Params**:安卓上 `user://` 是应用私有目录,推参数够不着(技能 §8 实测),故把自检接到**外部 kushan.cfg**(与 `AUTO_ENTER`/`TRADE_QA_SAY` 同一条)。
    
    ## 关键读数(原始输出)
    
    **MAP-01 全屏不裁切**(`tools/map_shot_metrics.py`,基准 `map.safe_px=24`):
    ```
    L1.png (960,2616) 四边留白={'左':32,'右':33,'上':32,'下':1457} 内容带=7 均亮=22.7 ⇒ 四边留白达标
    L2.png (960,2616) 四边留白={'左':32,'右':33,'上':32,'下':1721} 内容带=8 均亮=22.2 ⇒ 四边留白达标
    L3.png (960,2616) 四边留白={'左':32,'右':33,'上':32,'下':1997} 内容带=6 均亮=20.6 ⇒ 四边留白达标
    ```
    机身实测:`mDisplayCutout insets=Rect(0,0-960,93)`(顶部挖孔带 93px)、`navigationBars mFrame=[0,2490][960,2616]`;**两条系统栏在本 App 内 `mVisible=false`**(沉浸)⇒ 地图内容(上边距 32px、底边距 ≥1457px)既不压系统栏也不进挖孔带。
    
    **MAP-02 四层可进可退**(同一跑内,层号 1→2→3;`back` 两步 3→2→1):
    ```
    KUSHAN_MAP_LAYER 落层 want=1(回总图)      KUSHAN_MAP_QA 落层 want=1 now=1
    KUSHAN_MAP_LAYER 落层 want=2 lid=2 rid=0 ok=true   KUSHAN_MAP_QA 落层 want=2 now=2
    KUSHAN_MAP_LAYER 落层 want=3 lid=2 rid=100 ok=true KUSHAN_MAP_QA 落层 want=3 now=3
    KUSHAN_MAP_BLOCKS L1 rooms=287 locs=10 blocks=10 key0_type=4
    KUSHAN_MAP_BLOCKS L2 lid=2
    KUSHAN_MAP_QA step=1 cmd=back 前层=3 / KUSHAN_MAP_BACK ok=true 层后=2
    KUSHAN_MAP_QA step=2 cmd=back 前层=2 / KUSHAN_MAP_BACK ok=true 层后=1
    ```
    三层截图 **md5 互不相同**(`b20e4c78` / `ac64a08a` / `6aa265f1`)= 视图真的随层变了。
    
    **MAP-03 总图⇄场景切换**(点两次「切换」= 调按钮同一函数 `_on_jump`):
    ```
    step=1 cmd=jump 前层=2 / KUSHAN_MAP_JUMP 层前=2 / step=1 cmd=jump 后层=1
    step=2 cmd=jump 前层=1 / KUSHAN_MAP_JUMP 层前=1 / step=2 cmd=jump 后层=1   ← 第二次无效(见 B-072)
    ```
    帧序列 `J2=b20e4c78(L1) / J3=ac64a08a(L2) / J4..J6=b20e4c78(L1)` = 同一次运行里两种视图都拍到。
    
    **MAP-04 三层画法一致**:三层内容带 7/8/6、均由同一 `map_render` 模块出图(`show_tiles`/`show_rows` 同一套块状画法,代码同一处),三层截图各具(见上 md5)。
    
    **MAP-07 可回退**(同一包,只改 cfg):
    ```
    abA_开关关.png md5=bb8fc1b6 四边留白={'左':0,'右':1,'上':0,'下':1} 均亮=25.0(世界视图)
    
    ### TASK-MAP-05-06  (2026-10-02 00:06:35)
    
    # TASK-MAP-05-06 · 地图线两项评测(**已收口**)
    
    **日期**:2026-10-02 00:05-00:15 | 来源:68 号 §补录十九(四层地图)
    
    ## MAP-05 · 客户端 L3 楼栋房间集合 == `tools/map_building.py` 同栋集合
    **口径**:抽样 10 栋**比集合**(脚本),不看截图。**本脚本比全部 67 栋**(比抽样更强),抽样 10 栋逐条打印。
    
    **新护栏** `tools/verify_map_building_rooms.py`(自动纳入全量):
    - **从客户端源码真读**判据:正则取 `scripts/room/map_data.gd` 的 `const STREET_TYPES := [...]`
      (不另抄一份,否则护栏自己就是一个新的漂移源);
    - 字尾表从 **params** 读:`map.tail_street`/`map.tail_inner`(`data/params.json` 的 `params` 袋内,点号键);
    - 按**客户端判据**重算(街面=type ∈ 表 或 名字 `·` 后段以字尾结尾;楼内=type==interior 且名字不以街面字结尾,
      无 type 时客户端另有 `tail_inner` 兜底——本脚本照抄);再与工具 `map_building.buildings()` 比集合。