iter53-卡1:卷轴视口批: 关卡画面视口化(level-map.md 布局决策5落地)——视口承载件/锚点跟随/平滑滚动减动效/拖拽滚轮平移与clamp/指引件/探针26断言/golden两Shot冻结
This commit is contained in:
1 parent
fd9c8ad90e
commit
55f4de3ca4
22 files changed
+983
-152
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user