# VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-17(分支节点图批——关卡节点地图 v1.0 图版选路呈现) # 节点图选路覆盖层(模态):兑付确认后由组合根打开——整幅节点图(方块地牢观感:节点 = 地牢房间 # 方块、连线 = 通道)+ 当前位置高亮 + 可选下一跳亮边 + 候选按钮。零自动推进的玩家停点语义: # 唯一前进通道 = 点候选按钮 → route_chosen(node_id) → 组合根转 runner.pick_next_node。 # 高内聚边界:只读 runner 成文出口(nodes()/visited_ids()/node_index/next_candidate_ids())做渲染, # 无任何写面;图形为程序化占位质量(占位纪律)。 # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——台账 119;推翻 冒险结构.md「MVP 无界面入口」 # 后置条款):选路停点 = 天然非战斗期(接口契约:战斗中放弃一律拒绝),按钮常驻本层底部; # 可见性归组合根(只在冒险模式开——放弃的语义对象是"整局冒险";调试两层无节点图天然不可达)。 class_name RouteOverlay extends Control signal route_chosen(node_id: int) signal abandon_requested const COL_ROOM_DONE := UiKit.GOLD_DIM # 已到——暗金 const COL_ROOM_CURRENT := UiKit.GOLD # 当前——奖励金 const COL_ROOM_FUTURE := UiKit.DARK_UPCOMING # 未达——暗 const COL_PASSAGE := UiKit.STONE # 通道——石灰 const COL_PASSAGE_LIT := UiKit.GOLD_LIT # 候选通道——亮金 const COL_BOSS_BORDER := UiKit.FLAG_RED # Boss 边框——旗朱 const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21 var _runner = null var _btn_box: VBoxContainer var _title: Label var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) func _init() -> void: var dim := ColorRect.new() dim.color = Color(0, 0, 0, 0.6) dim.set_anchors_preset(Control.PRESET_FULL_RECT) add_child(dim) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) var box := VBoxContainer.new() box.add_theme_constant_override("separation", 10) center.add_child(box) _title = UiKit.label("选择下一跳", 18, true) _title.add_theme_color_override("font_color", UiKit.WARM) _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_title) var canvas := Control.new() canvas.custom_minimum_size = Vector2(620, 240) canvas.draw.connect(_draw_graph.bind(canvas)) box.add_child(canvas) _btn_box = VBoxContainer.new() _btn_box.add_theme_constant_override("separation", 6) box.add_child(_btn_box) abandon_btn = UiKit.button("🏳 放弃冒险(同失败结算——已获大奖励保留)") abandon_btn.add_theme_color_override("font_color", UiKit.FLAG_RED) abandon_btn.pressed.connect(_emit_abandon) abandon_btn.visible = false box.add_child(abandon_btn) visible = false # 放弃入口可见性(组合根在冒险启动时开、默认关——语义对象 = 整局冒险) func set_abandon_available(v: bool) -> void: abandon_btn.visible = v func _emit_abandon() -> void: abandon_requested.emit() # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染) func open_choices(cands: Array, runner) -> void: _runner = runner for child in _btn_box.get_children(): child.queue_free() _title.text = "选择下一跳(岔路口)" if cands.size() > 1 else "继续前进" for cand in cands: var nid := _id_of(cand) var t := String(cand.get("type", "")) var label := "节点 %d · %s" % [nid, _type_name(t)] if cands.size() > 1 else "继续前进(%s)" % _type_name(t) var btn := UiKit.button(label) btn.pressed.connect(_emit_choice.bind(nid)) _btn_box.add_child(btn) visible = true # 探针复用:按候选序号点击(真实按钮路径) func press_candidate(index: int) -> void: var btns: Array = _btn_box.get_children() if index >= 0 and index < btns.size(): btns[index].pressed.emit() func _emit_choice(nid: int) -> void: visible = false route_chosen.emit(nid) func _id_of(node: Dictionary) -> int: # 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth) return _runner.node_id_of(node) func _type_name(t: String) -> String: match t: "battle": return "战斗房" "reward": return "奖励点" "rest": return "恢复点" "boss": return "Boss 房" "shop": return "商店房" return t # ---- 整图自绘(程序化占位——分层布局:层 = 入口 BFS 深度,同层垂直排布)---- func _draw_graph(canvas: Control) -> void: if _runner == null or not visible: return var nodes: Array = _runner.nodes() var pos := _layout(nodes, canvas) var current_id: int = _runner.current_node_id() # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 var visited: Array = _runner.visited_ids() var cand_ids: Array = _runner.next_candidate_ids() # 连线(通道)先行画(方块覆盖其上) for node in nodes: var from: Vector2 = pos[node] for tid in _out_ids(node, nodes): var target := _node_by_id(tid, nodes) if target.is_empty(): continue var to: Vector2 = pos[target] var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) var col := COL_PASSAGE_LIT if lit else COL_PASSAGE var width := 3.0 if lit else 1.5 _draw_passage(canvas, from, to, col, width) # 房间方块 for node in nodes: var p: Vector2 = pos[node] var t := String(node.get("type", "")) var nid := _id_of(node) var col := COL_ROOM_FUTURE if visited.has(nid): col = COL_ROOM_DONE if nid == current_id: col = COL_ROOM_CURRENT var rect := Rect2(p - Vector2(26, 18), Vector2(52, 36)) canvas.draw_rect(rect, col) if t == "boss": canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) elif cand_ids.has(nid): canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 2.0) var font := ThemeDB.fallback_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) canvas.draw_string(font, p + Vector2(10, 14), str(nid), HORIZONTAL_ALIGNMENT_LEFT, -1, 10, UiKit.WARM_ROUTE_SUB) func _layout(nodes: Array, canvas: Control) -> Dictionary: # 分层:layer = 入口 BFS 深度;同层 slot 垂直排布 var layer := {} var slot := {} var layer_counts := {} var entry := _entry(nodes) var queue: Array = [] if not entry.is_empty(): queue.append(entry) layer[entry] = 0 while not queue.is_empty(): var cur: Dictionary = queue.pop_front() var l: int = int(layer[cur]) layer_counts[l] = int(layer_counts.get(l, 0)) + 1 slot[cur] = int(layer_counts[l]) - 1 for tid in _out_ids(cur, nodes): var target := _node_by_id(tid, nodes) if target.is_empty() or layer.has(target): continue layer[target] = l + 1 queue.append(target) # 未达节点(理论不可达——校验已拦)兜底排末层 var max_layer := 0 for k in layer.keys(): max_layer = maxi(max_layer, int(layer[k])) for node in nodes: if not layer.has(node): layer[node] = max_layer + 1 slot[node] = int(layer_counts.get(layer[node], 0)) layer_counts[layer[node]] = int(layer_counts.get(layer[node], 0)) + 1 var pos := {} var origin := (canvas.size - Vector2(620, 240)) / 2.0 for node in nodes: pos[node] = origin + Vector2(70 + int(layer[node]) * 122, 52 + int(slot[node]) * 58) return pos func _entry(nodes: Array) -> Dictionary: var inbound := {} for node in nodes: for tid in _out_ids(node, nodes): inbound[tid] = true for node in nodes: if not inbound.has(_id_of(node)): return node return {} func _out_ids(node: Dictionary, _nodes: Array) -> Array: return _runner.node_out_ids(node) func _node_by_id(nid: int, nodes: Array) -> Dictionary: for node in nodes: if _id_of(node) == nid: return node return {} # 通道画法:房间方块边缘到边缘的水平-折线(占位观感——地牢通道) func _draw_passage(canvas: Control, from: Vector2, to: Vector2, col: Color, width: float) -> void: var a := from + Vector2(26, 0) var b := to - Vector2(26, 0) if b.x < a.x: return var mid_x := (a.x + b.x) / 2.0 var pts := PackedVector2Array([a, Vector2(mid_x, a.y), Vector2(mid_x, b.y), b]) canvas.draw_polyline(pts, col, width)