Files
gplan-2/game/presentation/views/route_overlay.gd
T

226 lines
8.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)