feat(迭代38卡① levelmap批): level_map_screen 三段审计指认落地——l1 视图直读拼文案迁装配层(main_adventure_flow 新增 level_map_status_payload 九键成品,视图 _refresh_status 改 apply_status 纯赋值,喂入点=组合根两处 open_choices 后,刷新时点不变)+l2 _layout BFS 布局缓存(键=nodes 内容等价+canvas.size,_content_size 经 _fit_canvas 派生链间接覆盖,四调用点零改动)+l3 绘制字体函数首单取三处共享;l4 改前 golden 基线新建(level_map.png+README 登记,跨跑次一致性实测 0.00%);验收=wr1 改后对拍 changed=0.00%+wr2 缓存命中/换代失效双绿(is_same 同实例判别,零生产计数器);撤销证明=回退缓存 hunk→V1/V2 红→还原复绿;呈现逐像素不变(契约 1);视图侧拼装 grep 清零(:278 tint 层号读非拼装)

This commit is contained in:
DKChan committed 2026-10-26 05:40:31 +08:00
1 parent b23fa50d81
commit b85b3b45e3
5 files changed
+97 -34

No files matched your search

+32
View File
@@ -120,6 +120,7 @@ static func _on_battle_end_continue(root: Variant) -> void:
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root.run_scope.pending_candidates = []
root._refresh_presentations()
return
@@ -171,6 +172,7 @@ static func _on_choices_presented(root: Variant, candidates: Array) -> void:
_enter_level_screen(root)
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
root.level_screen.open_choices(candidates, root.run_scope.runner)
root.level_screen.apply_status(level_map_status_payload(root.run_scope.runner))
root._refresh_presentations()
static func _on_route_chosen(root: Variant, node_id: int) -> void:
@@ -198,6 +200,36 @@ static func _exit_level_screen(root: Variant) -> void:
root._close_modal(root.level_screen)
root._sync_status_entry() # 同上
# 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。
# 打开本屏/兑付确认回执后由组合根取成品喂入 apply_status(刷新时点与原视图内自刷一致)。
static func level_map_status_payload(runner: LevelRunner) -> Dictionary:
var atk_bonus := int(runner.run_ledger.bonus.get("atk", 0)) # 加算区住 RunLedger
var ac_bonus := int(runner.run_ledger.bonus.get("armor_class", 0))
var hp_bonus := int(runner.run_ledger.bonus.get("max_hp", 0))
var atk_txt := "%d" % runner.effective_atk()
if atk_bonus > 0:
atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块)
var ac_txt := "%d" % runner.effective_ac()
if ac_bonus > 0:
ac_txt += " +%d" % ac_bonus
var parts: Array[String] = []
if atk_bonus > 0:
parts.append("攻 +%d" % atk_bonus)
if hp_bonus > 0:
parts.append("上限 +%d" % hp_bonus)
if ac_bonus > 0:
parts.append("AC +%d" % ac_bonus)
return {
"level_label": String(runner.level_row.get("display_name", "")),
"gp_text": "◈ %d" % int(runner.gp),
"hp_text": "HP %d/%d" % [int(runner.hero_hp), int(runner.effective_max_hp())],
"hp_bar_redraw": true,
"attr_text": "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt],
"bonus_text": " · ".join(parts),
"bonus_row_visible": not parts.is_empty(),
}
static func _on_abandon_requested(root: Variant) -> void:
# 放弃入口(冒险结构.md 已放开「MVP 无界面入口」后置条款;
# 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型,
+21 -32
View File
@@ -52,6 +52,7 @@ var _drawn_disp: Dictionary[int, Dictionary] = {} # 本停点实际绘制记
var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全)
var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点)
var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用)
var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效)
func _init() -> void:
@@ -274,7 +275,6 @@ func open_choices(cands: Array, runner: LevelRunner) -> void:
for cand in cands:
_cand_ids.append(_id_of(cand))
_fit_canvas(_runner.nodes())
_refresh_status()
_apply_level_tint(int(_runner.level_row.get("level_index", 0)))
visible = true
if not was_visible:
@@ -322,35 +322,17 @@ func _apply_level_tint(level_index: int) -> void:
Color(target.r, target.g, target.b, UiKit.BACKDROP_TILE_ALPHA), UiKit.motion(0.45)) # 0.14 弱质感档随 tint 保持
# 状态栏刷新(命令式纪律——组合根打开本屏/兑付确认回执后重查;零推送信号订阅)
func _refresh_status() -> void:
if _runner == null:
return
_level_label.text = String(_runner.level_row.get("display_name", ""))
_gp_value.text = "◈ %d" % int(_runner.gp)
var max_hp: int = _runner.effective_max_hp()
_hp_value.text = "HP %d/%d" % [int(_runner.hero_hp), int(max_hp)]
_hp_bar.queue_redraw()
var atk_bonus := int(_runner.run_ledger.bonus.get("atk", 0)) # 加算区住 RunLedger
var ac_bonus := int(_runner.run_ledger.bonus.get("armor_class", 0))
var atk_txt := "%d" % _runner.effective_atk()
if atk_bonus > 0:
atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块)
var ac_txt := "%d" % _runner.effective_ac()
if ac_bonus > 0:
ac_txt += " +%d" % ac_bonus
_attr_value.text = "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt]
var hp_bonus := int(_runner.run_ledger.bonus.get("max_hp", 0))
var parts: Array[String] = []
if atk_bonus > 0:
parts.append("攻 +%d" % atk_bonus)
if hp_bonus > 0:
parts.append("上限 +%d" % hp_bonus)
if ac_bonus > 0:
parts.append("AC +%d" % ac_bonus)
_bonus_value.text = " · ".join(parts)
# 状态栏投喂(命令式纪律——组合根经 level_map_status_payload 取成品后喂入;零推送信号订阅、零核心态拼装)
func apply_status(payload: Dictionary) -> void:
_level_label.text = String(payload["level_label"])
_gp_value.text = String(payload["gp_text"])
_hp_value.text = String(payload["hp_text"])
if bool(payload["hp_bar_redraw"]):
_hp_bar.queue_redraw()
_attr_value.text = String(payload["attr_text"])
_bonus_value.text = String(payload["bonus_text"])
# 特性持有行可见性(与加成行并框——任一非空整框显示;文案归组合根 set_feat_holdings)
_bonus_row.visible = not parts.is_empty() or _feat_value.text != ""
_bonus_row.visible = bool(payload["bonus_row_visible"]) or _feat_value.text != ""
# ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ----
@@ -444,6 +426,7 @@ func _draw_graph(canvas: Control) -> void:
_drawn_disp.clear() # 绘制记录随本帧重建——只反映实际发生的绘制(接线坏的判据面)
var nodes: Array[Variant] = _runner.nodes()
var pos := _layout(nodes, canvas)
var font := get_theme_default_font() # 绘制路径字体单次取用(三处 draw_string 共享——零语义差)
var current_id: int = _runner.current_node_id()
# 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析
var visited: Array[Variant] = _runner.visited_ids()
@@ -499,12 +482,11 @@ func _draw_graph(canvas: Control) -> void:
canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0,
Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint)
else:
var font := get_theme_default_font()
var label := String(NODE_LABELS.get(t, "?"))
canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE)
# 编号(右上角——与图标占位区零重叠;双色墨随态)
var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM
canvas.draw_string(get_theme_default_font(), Vector2(rect.end.x - 16.0, rect.position.y + 8.0),
canvas.draw_string(font, Vector2(rect.end.x - 16.0, rect.position.y + 8.0),
str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink)
# 类型文字标签(demo ④:节点下方 11px;当前位=「当前位置」金、候选=类型名+「 · 可选」烛光黄、
# 其余=类型名暖米弱化;文字+图标+形状三通道)
@@ -512,12 +494,16 @@ func _draw_graph(canvas: Control) -> void:
rec["draw_label"] = true
rec["label_text"] = String(disp["label_text"])
var type_ink := COL_ROOM_CURRENT if nid == current_id else (UiKit.CANDLE if cand_ids.has(nid) else UiKit.WARM_FAINT)
canvas.draw_string(get_theme_default_font(), Vector2(p.x - 44.0, rect.end.y + 15.0),
canvas.draw_string(font, Vector2(p.x - 44.0, rect.end.y + 15.0),
String(disp["label_text"]), HORIZONTAL_ALIGNMENT_CENTER, 88, 11, type_ink)
_drawn_disp[nid] = rec
func _layout(nodes: Array, canvas: Control) -> Dictionary:
# 缓存命中键 = nodes 内容等价(GDScript Array== 逐元素比较)+画布尺寸——布局是二者与
# _content_size(经 _fit_canvas 派生于 nodes)的纯函数,键间接覆盖派生链;换代即自然失效。
if _layout_cache.nodes == nodes and _layout_cache.canvas_size == canvas.size:
return _layout_cache.pos
var layer := {}
var slot := {}
_assign_layers(nodes, layer, slot)
@@ -526,6 +512,9 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary:
for node in nodes:
pos[node] = origin + Vector2(LAYER_BASE_X + int(layer[node]) * LAYER_STEP_X,
SLOT_BASE_Y + int(slot[node]) * SLOT_STEP_Y)
_layout_cache.nodes = nodes
_layout_cache.canvas_size = canvas.size
_layout_cache.pos = pos
return pos
+8 -2
View File
@@ -1,7 +1,8 @@
# 视觉 golden 基线(渲染闸的比对基准)
四张基线:棋盘三图(`board_L1_dungeon` / `board_L2_underdark` / `board_L3_deep_underdark`,
裁剪区 900×760)+ 启动态菜单(`menu_boot`,全屏)。比对判据与工具见 `tools/golden_compare.py`
五张基线:棋盘三图(`board_L1_dungeon` / `board_L2_underdark` / `board_L3_deep_underdark`,
裁剪区 900×760)+ 启动态菜单(`menu_boot`,全屏)+ 关卡画面(`level_map`,全屏)。
比对判据与工具见 `tools/golden_compare.py`
(逐像素灰度差 >16 的像素占比 ≤2%)。
## 时效判定纪律(迭代 29 T7 立)
@@ -27,3 +28,8 @@
字形——晚于 2026-10-08 建基提交,与本批零因果。呈报后用户裁「重制三张基线」,
经 `tools/golden_baseline_refresh.sh` 换代;复闸 changed = 0.00% / 0.00% / 0.00%,
启动态菜单 0.00%(未重制)。
- 2026-10-26(迭代 38 卡① l4):**新增基线 `level_map`**(改前建基——时点铁序)。
来源 = shot_probe `_snap("level")` 帧(入口停点出图后第 4 帧,淡入完成态),
跨跑次逐像素一致性实测 changed=0.00% 后落盘。承载迭代 38 卡① l1–l3 结构重构的
逐像素对拍(改后对拍 changed=0.00% 为验收主锚);对拍经 `tools/golden_compare.py`
直跑(render_gate 现行只对拍棋盘三图+menu_boot,闸件不在本批改动面)。
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

@@ -26,6 +26,7 @@ func _run() -> void:
_test_details_never_shown()
_test_label_three_states()
await _test_big_graph_fit(db)
await _test_layout_cache(db)
quit(Kit.finish("level_map_info_fog_test"))
@@ -189,3 +190,38 @@ func _test_big_graph_fit(db) -> void:
and bool(disp[boss_ids[0]]["boss_border"]) and bool(disp[boss_ids[1]]["boss_border"]))
Kit.check("大图呈现:入口停点远处战斗节点(句柄 32,层 6)类型入雾〔fv8〕", not bool(disp[32]["draw_icon"]))
screen.queue_free()
# ---- 布局缓存命中与换代失效〔levelmap批 W2〕----
# 真实入口 = open_choices 后的真实绘制路径(_draw_graph→_layout,与 hover→_candidate_at 链同一缓存层);
# 命中读数 = is_same 同实例判别(重算必产新字典、命中返回缓存原实例)——零生产计数器。
func _test_layout_cache(db) -> void:
var nodes := _big_graph_nodes()
var runner = _runner(db, nodes, [nodes[1]])
var screen = ScreenScript.new()
root.add_child(screen)
screen.open_choices([nodes[1]], runner)
await process_frame
await process_frame
var p1: Dictionary = screen.layout_probe(nodes)
var p2: Dictionary = screen.layout_probe(nodes)
Kit.check("布局缓存:同 nodes 两次查询返回同一实例(第二次命中缓存)〔levelmap批 V1〕",
is_same(p1, p2))
# 换代失效:节点集内容变 → 缓存键(内容等价)击穿 → 重算换写。
# 判别式 = 同一性:若缓存未随换代失效,新图查询将命中旧缓存返回同实例;重算必产新字典。
# (不回查旧图——换代后 _content_size 已随 open_choices 重拟合到新图,旧图混查属生产外用法。)
var chain: Array = [
{"id": 0, "type": "battle", "depth": 0, "next": [1]},
{"id": 1, "type": "boss", "depth": 1, "next": []},
]
runner.level_row["nodes"] = chain
screen.open_choices([chain[0]], runner)
await process_frame
await process_frame # 换代图真实绘制路径重算(缓存随内容变失效重写)
var pc1: Dictionary = screen.layout_probe(chain)
var pc2: Dictionary = screen.layout_probe(chain)
Kit.check("布局缓存换代:新图查询返回新实例非旧缓存(失效重算生效)〔levelmap批 V2〕",
not is_same(p1, pc1) and is_same(pc1, pc2)
and (pc1[chain[0]] as Vector2) != (p1[nodes[0]] as Vector2))
screen.queue_free()