226 lines
8.3 KiB
GDScript
226 lines
8.3 KiB
GDScript
# 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)
|