iter53-卡1:卷轴视口批: 关卡画面视口化(level-map.md 布局决策5落地)——视口承载件/锚点跟随/平滑滚动减动效/拖拽滚轮平移与clamp/指引件/探针26断言/golden两Shot冻结

This commit is contained in:
DKChan committed 2026-11-06 00:08:18 +08:00
1 parent fd9c8ad90e
commit 55f4de3ca4
22 files changed
+983 -152

No files matched your search

+122 -20
View File
@@ -31,6 +31,8 @@ const SLOT_STEP_Y := 58 # 同层槽位纵向步距
const HIT_PAD := 6.0 # 图上命中外扩(指宽容差)
const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落)
const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(对暗面板可辨;值单点收 UiKit)
const DRAG_THRESHOLD := 8.0 # 拖动距离阈值(布局决策 5 provisional——超阈值进拖拽态,拖拽不触发选路)
const WHEEL_STEP := 60.0 # 滚轮横滚单档步进(provisional 实现期定值;方向映射=代拍板:下/右=向右前进向)
## 内部状态件(u2 子域拆件——契约 9 职责轴):_MapState=选路屏运行态与图绘制态(单一变化原因=
## 选路呈现)。layer_decor 域字典承载换层装饰三件(图标贴图/石纹面/tint 层号——装饰域归并形态)。
@@ -39,6 +41,7 @@ class _MapState extends RefCounted:
var runner = null
var title: Label
var canvas: Control
var viewport: LevelMapViewport # 卷轴视口承载件(布局决策 5——画布宿主/裁切/clamp/跟随/指引)
var level_label: Label
var gp_value: Label
var hp_value: Label
@@ -54,6 +57,8 @@ class _MapState extends RefCounted:
var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命中合法集;探针直读冻结——住宿主)
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它;探针直读冻结)
var _hover_id := -1 # 悬停预承诺候选(-1 = 无;探针直读冻结)
var _drag_active := false # 拖拽态(超阈值至抬杆——拖拽不触发选路,布局决策 5)
var _drag_press_pos := Vector2(-1, -1) # 拖距判定基准(按下点画布局部坐标;-1 = 无按下跟踪)
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开;探针直读冻结)
var status_badge: Button # 状态面板入口角标(顶栏 GP 徽右侧;连接归组合根;探针直读冻结)
var _layout_cache := {"nodes": [], "canvas_size": Vector2.ZERO, "pos": {}} # 布局缓存(_layout 纯函数于 (nodes, canvas)——键=nodes 内容等价+画布尺寸,图态换代或画布尺寸变即自然失效;探针直读冻结)
@@ -74,15 +79,15 @@ func _init() -> void:
base.add_child(tex)
_map.layer_decor.noise_rect = tex # 世界侧 tint 承载面(换层色板渐变挂点——§2.7 切片档)
base.add_child(UiKit.vignette_rect())
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
base.add_child(center)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 10)
center.add_child(box)
_build_top_bar(box)
_build_canvas(box)
_build_status_bar(box)
# 三段纵排页(布局决策 5 卷轴视口:顶栏/标题在上、图区视口扩张充盈、底栏在下——
# 视口为窗口屏为框,画布随图生长可超屏)
var page := VBoxContainer.new()
page.set_anchors_preset(Control.PRESET_FULL_RECT)
page.add_theme_constant_override("separation", 10)
base.add_child(page)
_build_top_bar(page)
_build_canvas(page)
_build_status_bar(page)
visible = false
@@ -122,14 +127,21 @@ func _build_top_bar(box: VBoxContainer) -> void:
box.add_child(_map.title)
## 节点图画布域(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击)
## 节点图区(卷轴视口承载——布局决策 5):视口件为窗口(裁切/offset/clamp/跟随/指引),
## 画布为其子件;图上点击 = 唯一提交通道(布局决策 2),拖拽/滚轮判定留在画布 _gui_input
## (事件流同源——悬停/选路同源,视口件不做父子事件路由)
func _build_canvas(box: VBoxContainer) -> void:
_map.canvas = Control.new()
_map.canvas.custom_minimum_size = Vector2(620, 240)
_map.canvas.mouse_filter = Control.MOUSE_FILTER_STOP
_map.canvas.draw.connect(_draw_graph.bind(_map.canvas))
_map.canvas.gui_input.connect(_on_canvas_input)
box.add_child(_map.canvas)
_map.canvas.mouse_exited.connect(_on_canvas_mouse_exited)
_map.viewport = LevelMapViewport.new()
_map.viewport.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_map.viewport.size_flags_vertical = Control.SIZE_EXPAND_FILL
box.add_child(_map.viewport)
_map.viewport.setup(_map.canvas, _viewport_anchor, Vector2(1280, 800)) # 兜底尺寸=设计基座(实况窗口以布局为准)
## 底部状态栏域(合并单条:亮米面板一站读齐=头像+生命+属性+本关加成/流派件——demo ④;
@@ -297,6 +309,9 @@ func open_choices(cands: Array, runner: LevelRunner) -> void:
_fit_canvas(_map.runner.nodes())
_apply_level_tint(int(_map.runner.level_row.get("level_index", 0)))
visible = true
# 视口锚定(布局决策 5):入口停点(尚无当前节点)=首锚即达;其余停点/兑付回图=平滑跟随
# (停点切换与兑付确认回图——减动效开由视口件跳终态)
_map.viewport.reanchor(_map.runner.current_node_id() >= 0)
if not was_visible:
# 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定)
modulate.a = 0.0
@@ -323,6 +338,7 @@ func _fit_canvas(nodes: Array) -> void:
SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 24.0) # +24 = 类型文字标签行
_content_size = _content_size.max(Vector2(620, 256)) # 下限(抬高容纳标签行)
_map.canvas.custom_minimum_size = _content_size
_map.canvas.size = _content_size # 同帧收敛(视口锚点/包络数学与 _layout 同源读画布尺寸——容器延迟收敛不等锚点)
# 换层色板渐变挂点(art-bible §2.7 下探过渡切片档——关卡结算确认→下一关→独立屏淡入链路上的
@@ -356,20 +372,70 @@ func apply_status(payload: Dictionary) -> void:
# ---- 图上命中(布局决策 2——唯一提交通道;探针 press_candidate 同路径) ----
# 拖拽与点击以拖动距离阈值区分(布局决策 5):按下起跟踪拖距,超阈值进拖拽态逐帧平移视口,
# 抬杆不提交;未超阈值的完整点击(按下→抬杆)=提交沿——命中候选即提交,语义零改动。
func _on_canvas_input(ev: InputEvent) -> void:
if _map.runner == null or not visible:
return
if ev is InputEventMouseMotion:
var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立)
if nid != _hover_id:
_hover_id = nid
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
if ev is InputEventMouseButton and ev.pressed and _wheel_pan_step(ev.button_index) != 0.0:
_map.viewport.pan_by(Vector2(_wheel_pan_step(ev.button_index), 0))
elif ev is InputEventMouseMotion:
_on_canvas_motion(ev)
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT:
_on_canvas_click(ev)
## 悬停预承诺与拖拽态推进沿(画布局部坐标——gui_input 事件流同源)
func _on_canvas_motion(ev: InputEventMouseMotion) -> void:
if _drag_active:
_map.viewport.pan_by(ev.relative) # 拖拽态逐帧平移(视口件执行 clamp——本屏只判定)
return
if _drag_press_pos.x >= 0.0 and ev.position.distance_to(_drag_press_pos) >= DRAG_THRESHOLD:
_drag_active = true
_drag_press_pos = Vector2(-1, -1)
if _hover_id != -1: # 进拖拽态清悬停预承诺(拖拽非选路意图)
_hover_id = -1
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_ARROW
_map.canvas.queue_redraw()
elif ev is InputEventMouseButton and ev.button_index == MOUSE_BUTTON_LEFT and ev.pressed:
var nid2 := _candidate_at(ev.position)
if nid2 >= 0:
_submit_choice(nid2)
_map.viewport.pan_by(ev.relative) # 跨阈值帧位移计入平移(拖距全长=平移全长)
return
var nid := _candidate_at(ev.position) # gui_input 事件坐标 = 接收控件局部坐标(push_input 同成立)
if nid != _hover_id:
_hover_id = nid
_map.canvas.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if nid >= 0 else Control.CURSOR_ARROW
_map.canvas.queue_redraw()
## 点击沿与拖拽抬杆(未超阈值的完整点击=提交沿——布局决策 2 语义零改动)
func _on_canvas_click(ev: InputEventMouseButton) -> void:
if ev.pressed:
_drag_press_pos = ev.position
_drag_active = false
return
_drag_press_pos = Vector2(-1, -1)
if _drag_active:
_drag_active = false # 拖拽抬杆=平移结束,不触发选路提交(布局决策 5)
return
var nid2 := _candidate_at(ev.position)
if nid2 >= 0:
_submit_choice(nid2)
## 滚轮横滚步进(代拍板:下/右=向右前进向——视线移向深层,画布左移;上/左反向;非滚轮=0)
func _wheel_pan_step(button_index: int) -> float:
if button_index == MOUSE_BUTTON_WHEEL_DOWN or button_index == MOUSE_BUTTON_WHEEL_RIGHT:
return -WHEEL_STEP
if button_index == MOUSE_BUTTON_WHEEL_UP or button_index == MOUSE_BUTTON_WHEEL_LEFT:
return WHEEL_STEP
return 0.0
## 拖距跟踪器生命周期清理(按下即武装——鼠标离画布且未入拖拽态时解除,
## 防丢失 release 沿后的残留 armed 态把后续 motion 误判成拖拽激活)
func _on_canvas_mouse_exited() -> void:
if not _drag_active:
_drag_press_pos = Vector2(-1, -1)
# 候选命中:位置 = 布局公式(与 _draw_graph 同源),矩形 = 节点方块 + 外扩容差;非候选格不响应
@@ -413,6 +479,42 @@ func press_candidate(index: int) -> void:
_submit_choice(nid)
# ---- 视口锚点(布局决策 5:当前节点居中;入口停点尚无当前节点=锚入口组包围盒居中) ----
## 锚点画布坐标提供方(构造注入视口件——本屏供数,视口件只消费)
func _viewport_anchor() -> Vector2:
if _map.runner == null:
return _content_size / 2.0
var nodes: Array[Variant] = _map.runner.nodes()
var pos := _layout(nodes, _map.canvas)
var cur_id: int = _map.runner.current_node_id()
if cur_id < 0:
return _entry_group_center(nodes, pos) # 入口停点(尚未走出第一步)
var cur := _node_by_id(cur_id, nodes)
if cur.is_empty():
return _content_size / 2.0
return pos[cur]
func _entry_group_center(nodes: Array, pos: Dictionary) -> Vector2:
# 入口组 = 全部无入边节点(与 _assign_layers 入口判据同源);多入口取包围盒居中
var inbound := {}
for node in nodes:
for tid in _out_ids(node, nodes):
inbound[tid] = true
var lo := Vector2(INF, INF)
var hi := Vector2(-INF, -INF)
for node in nodes:
if inbound.has(_id_of(node)):
continue
var p: Vector2 = pos[node]
lo = lo.min(p)
hi = hi.max(p)
if lo.x > hi.x:
return _content_size / 2.0 # 空图兜底(无入口——不达路径,防御位)
return (lo + hi) / 2.0
func _id_of(node: Dictionary) -> int:
# 编号显示经行进器读面(编号空间解析单一出口——单链 = 下标 / 图 = node_depth)
return _map.runner.node_id_of(node)