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

191 lines
8.1 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.
## 卷轴视口承载件(level-map.md 布局决策 5——关卡画面选路屏):画布宿主与裁切、平移状态
## 与 clamp、锚点跟随与平滑滚动、指引件(屏缘渐隐带+平移提示 chip)。
## 纯呈现态:平移位置零核心写、不进存档(重进本屏按锚点规则重新定位);跟随只随停点切换/
## 兑付回图发生(零自动推进)。拖拽/滚轮判定留画布 _gui_input(事件流同源——悬停/选路同源),
## 本件只管裁切/offset/clamp/跟随/指引;画布包络=画布实际尺寸(open_choices 拟合后同帧收敛)。
class_name LevelMapViewport
extends Control
const SCROLL_DUR := 0.3 # 平滑滚动时长(布局决策 5 provisional 0.3s 单段缓动)
const FADE_BAND_W := 70.0 # 屏缘渐隐带宽(布局决策 5/像素级判据②)
const CHIP_SIZE := Vector2(240, 30) # 平移提示 chip(像素级判据③——图区底部居中)
const CHIP_CORNER := 6 # chip 圆角(布局决策 5)
const CHIP_BOTTOM_GAP := 24.0 # chip 距视口底缘(效果图档同族取值)
const CHIP_TEXT := "◄ 拖拽画布 / 滚轮平移 ►" # 14px(FS_HINT 档——布局决策 5)
const EDGE_EPS := 0.5 # 包络边界判定容差(浮点平移累积)
var _canvas: Control
var _anchor_provider: Callable # () -> Vector2 锚点画布坐标(screen 提供——当前节点/入口组)
var _viewport_size := Vector2.ZERO # 窗口尺寸兜底(setup 注入设计基座——实况窗口以控件 size 为准,布局晚于构造)
var _offset := Vector2.ZERO: # 画布左上角在视口件坐标系的位置(纯呈现态——直驱画布 position)
set(value):
_offset = value
if _canvas != null:
_canvas.position = _offset
var _view_tween: Tween
var _guided_visible := false # 指引件可见态(画布超窗才暗示——浅图无平移余量即隐藏)
## 一次交齐注入(code-conventions §1.6 拆分交接:画布宿主、锚点来源、窗口兜底尺寸)
func setup(canvas: Control, anchor_provider: Callable, viewport_size: Vector2) -> void:
_canvas = canvas
_anchor_provider = anchor_provider
_viewport_size = viewport_size
clip_contents = true
add_child(canvas)
_build_guidance()
func _notification(what: int) -> void:
# 窗口实况尺寸落定/变化即重锚(首开布局晚于 open_choices 的锚定——隐藏子树容器排序
# 首个可见帧才传播,4.7 行为;实况窗口以本件 size 为准,落定即收敛锚点)
if what == NOTIFICATION_RESIZED and _canvas != null and _anchor_provider.is_valid():
reanchor(false)
## 锚点跟随(布局决策 5:锚点居中;smooth=停点切换/兑付回图平滑滚动,减动效开=跳终态;
## 重入先杀在飞 tween 再建新——快速连续停点切换防叠加)
func reanchor(smooth: bool) -> void:
if _canvas == null or not _anchor_provider.is_valid():
return
var target := _clamped_center_offset(_anchor_provider.call())
_kill_scroll_tween()
if not smooth or ArmedTimer.motion_still():
_offset = target
_sync_guidance()
return
_view_tween = create_tween()
_view_tween.tween_property(self, "_offset", target, UiKit.motion(SCROLL_DUR)) \
.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
_sync_guidance()
## 手动平移(画布 _gui_input 判定后调用——拖拽沿逐帧/滚轮步进;纯呈现零核心写)
func pan_by(delta: Vector2) -> void:
if _canvas == null:
return
_kill_scroll_tween()
var win := _window_size()
_offset = Vector2(
_axis_pan_offset(_offset.x, delta.x, _canvas.size.x, win.x),
_axis_pan_offset(_offset.y, delta.y, _canvas.size.y, win.y))
_sync_guidance()
## 视口中线的画布坐标(锚点居中判据投影面——浅图=画布中心,深图=夹紧域内当前注视点)
func viewport_center_canvas_pos() -> Vector2:
return _window_size() / 2.0 - _offset
## 视口是否钉在画布包络边界(任一超窗轴 offset 顶边——clamp 判据面)
func is_clamped_at_edge() -> bool:
var win := _window_size()
if _canvas == null:
return false
return _axis_at_edge(_offset.x, _canvas.size.x, win.x) \
or _axis_at_edge(_offset.y, _canvas.size.y, win.y)
func _kill_scroll_tween() -> void:
if _view_tween != null and _view_tween.is_valid():
_view_tween.kill()
_view_tween = null
## 单轴锚定 offset:画布不超窗=居中分支(夹紧域退化——布局决策 5);
## 超窗=锚点夹紧在画布包络内,锚点落视口中线(offset = 半窗 − 锚点)
func _axis_center_offset(anchor: float, canvas_len: float, half_win: float) -> float:
if canvas_len <= half_win * 2.0:
return half_win - canvas_len / 2.0
return half_win - clampf(anchor, half_win, canvas_len - half_win)
func _clamped_center_offset(anchor: Vector2) -> Vector2:
var half := _window_size() / 2.0
return Vector2(
_axis_center_offset(anchor.x, _canvas.size.x, half.x),
_axis_center_offset(anchor.y, _canvas.size.y, half.y))
## 单轴平移收敛:居中轴零平移余量不动;超窗轴 offset 收敛在 [半窗−画布, 0](不出露屏底空档)
func _axis_pan_offset(cur: float, delta: float, canvas_len: float, win_len: float) -> float:
if canvas_len <= win_len:
return cur
return clampf(cur + delta, win_len - canvas_len, 0.0)
func _axis_at_edge(offset: float, canvas_len: float, win_len: float) -> bool:
if canvas_len <= win_len:
return false
return offset >= -EDGE_EPS or offset <= win_len - canvas_len + EDGE_EPS
## 实况窗口优先(容器布局晚于构造——注入值只兜构造帧数学)
func _window_size() -> Vector2:
if size.x > 0.0 and size.y > 0.0:
return size
return _viewport_size
## 指引件(布局决策 5/像素级判据②③):屏缘渐隐带盖在画布之上+图区底部居中提示 chip;
## 可见性=画布超窗(_sync_guidance 随锚定/平移刷新)
func _build_guidance() -> void:
var band_l := TextureRect.new()
band_l.name = "FadeL"
band_l.texture = _fade_gradient(false)
band_l.set_anchors_preset(Control.PRESET_LEFT_WIDE)
band_l.offset_right = FADE_BAND_W
band_l.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
band_l.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(band_l)
var band_r := TextureRect.new()
band_r.name = "FadeR"
band_r.texture = _fade_gradient(true)
band_r.set_anchors_preset(Control.PRESET_RIGHT_WIDE)
band_r.offset_left = -FADE_BAND_W
band_r.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
band_r.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(band_r)
var chip := PanelContainer.new()
chip.name = "PanHint"
chip.add_theme_stylebox_override("panel",
UiKit.panel_flat(UiKit.HP_BAR_BACK, UiKit.STONE, CHIP_CORNER, 1, Vector4(12, 5, 12, 5)))
chip.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
chip.offset_left = -CHIP_SIZE.x / 2.0
chip.offset_right = CHIP_SIZE.x / 2.0
chip.offset_top = -CHIP_SIZE.y - CHIP_BOTTOM_GAP
chip.offset_bottom = -CHIP_BOTTOM_GAP
chip.custom_minimum_size = CHIP_SIZE
chip.mouse_filter = Control.MOUSE_FILTER_IGNORE
var chip_label := UiKit.label(CHIP_TEXT, UiKit.FS_HINT, false, UiKit.WARM)
chip_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
chip_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
chip.add_child(chip_label)
add_child(chip)
_sync_guidance()
## 屏缘渐隐渐变(色取屏底同族 BG_ROOT——代拍板:不透明端 0.9 单段衰减)
func _fade_gradient(transparent_at_left: bool) -> GradientTexture2D:
var grad := Gradient.new()
grad.offsets = PackedFloat32Array([0.0, 1.0])
grad.colors = PackedColorArray([Color(UiKit.BG_ROOT, 0.9), Color(UiKit.BG_ROOT, 0.0)])
var tex := GradientTexture2D.new()
tex.gradient = grad
tex.fill_from = Vector2(1, 0.5) if transparent_at_left else Vector2(0, 0.5)
tex.fill_to = Vector2(0, 0.5) if transparent_at_left else Vector2(1, 0.5)
tex.width = int(FADE_BAND_W)
return tex
func _sync_guidance() -> void:
var win := _window_size()
var over := _canvas != null and (_canvas.size.x > win.x or _canvas.size.y > win.y)
if over == _guided_visible:
return
_guided_visible = over
for nm in ["FadeL", "FadeR", "PanHint"]:
var node := get_node_or_null(NodePath(nm)) as Control
if node != null:
node.visible = over