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

310 lines
11 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.
## 营地场景图视图(交互式营地):10 层贴图铺底+七物件命中区+点选反馈四态+顶栏预留位。
## 布局唯一事实源 = db.presentation.camp_scene_layout(构造注入,本视图禁自载表路径);
## 子节点序 = 表内数组序 = z 序(Control 根+TextureRect 层——禁 CanvasLayer/z_index/Sprite2D)。
## 输入单点:根 _gui_input 集中命中判定(层节点全 IGNORE),点击只发 object_pressed 信号,路由归组合根。
## 零核心写调用:视图不读 MetaProgression,禁用态由组合根经 set_feedback_state 推送。
class_name CampSceneView
extends Control
signal object_pressed(key: StringName) ## 命中区点击确认(press+release 同区且非禁用;组合根路由开界面/动线)
## 名牌文案(camp.md §场景构成绑定表——七可点件按界面名逐字)
const _HOTSPOT_NAMES := {
&"gate": "冒险", &"forge": "锻造工坊", &"chest": "秘宝匣", &"dummy": "天赋树",
&"board": "奖励格图鉴", &"notebook": "生涯记录", &"campfire": "离开营地",
}
## 层键→纹理路径(单点 = UiKit.TEX_CAMP_*,本表只引用不自写路径;缺位 load_tex→null→程序化占位)
const _TEX_BY_KEY := {
&"bg": UiKit.TEX_CAMP_BG, &"wall": UiKit.TEX_CAMP_WALL, &"board": UiKit.TEX_CAMP_BOARD,
&"gate": UiKit.TEX_CAMP_GATE, &"dummy": UiKit.TEX_CAMP_DUMMY, &"forge": UiKit.TEX_CAMP_FORGE,
&"tent": UiKit.TEX_CAMP_TENT, &"chest": UiKit.TEX_CAMP_CHEST,
&"notebook": UiKit.TEX_CAMP_NOTEBOOK, &"campfire": UiKit.TEX_CAMP_CAMPFIRE,
}
## 禁用态去饱和着色器(modulate 只能调明度——降明度+去饱和走 canvas_item shader,零新素材)
const _DESAT_SHADER_SRC := "
shader_type canvas_item;
uniform float strength : hint_range(0.0, 1.0) = 1.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
float g = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
tex.rgb = mix(tex.rgb, vec3(g) * 0.72, strength);
COLOR = tex * COLOR;
}
"
enum FeedbackState { NORMAL, HOVER, PRESSED, DISABLED } ## 点选反馈四态(camp.md §点选反馈)
var _layout: CampSceneLayoutTable # 布局表(坐标唯一事实源)
var _layer_nodes: Dictionary[StringName, Control] = {} # 层键→TextureRect/ColorRect 占位(子节点序=z 序)
var _hotspot_rects: Dictionary[StringName, Rect2] = {} # 命中键→Rect2(视图局部坐标=表坐标)
var _hotspot_layer: Dictionary[StringName, StringName] = {} # 命中键→层键
var _base_states: Dictionary[StringName, int] = {} # 命中键→FeedbackState 值(持久基态——组合根置 DISABLED/NORMAL)
var _outline: Panel # 悬停轮廓亮边
var _name_label: Label # 物件名牌(下方 8px 居中,画面内钳制)
var _hover_key := &"" # 当前悬停命中键(&""=无)
var _press_key := &"" # 当前按住命中键
var _float_tween: Tween # 悬停浮动补间(逐级整像素吸附,禁 _process 逐帧移 UI)
var _desat_mat: ShaderMaterial # 禁用态共享材质(strength 恒 1,材质挂/摘承载开关)
var _hud_slot: Control # 顶栏预留位(卡⑤ set_hud_band 填值)
var hud_band: Control # 顶栏 HUD 栏句柄(卡⑤ CampHudBand——装配后 bus 目标册取用面)
func _init() -> void:
visible = false # 登记模态初始隐(开合走栈——模态纪律;boot 态场景根不得盖菜单)
func setup_layers(layout: CampSceneLayoutTable) -> void:
if layout == null:
push_error("[CampSceneView] 布局表缺位——组合根须注入 db.presentation.camp_scene_layout")
return
_layout = layout
mouse_filter = Control.MOUSE_FILTER_STOP # 场景图=营地语境主交互面(模态层在兄弟位先截事件)
_build_desat_material()
for spec in layout.layers:
var node: Control = _build_layer_node(spec)
add_child(node)
_layer_nodes[spec.key] = node
_build_outline()
_build_name_label()
_build_hud_slot(layout)
mouse_exited.connect(_on_mouse_exited) # 悬出根控件=清 hover/press(防态悬挂)
func bind_hotspots(layout: CampSceneLayoutTable) -> void:
if layout == null:
return
for spec in layout.layers:
if spec.is_clickable():
_hotspot_rects[spec.hotspot_key] = spec.hotspot
_hotspot_layer[spec.hotspot_key] = spec.key
_base_states[spec.hotspot_key] = FeedbackState.NORMAL
func get_hotspot_rect(key: StringName) -> Rect2:
var rect: Rect2 = _hotspot_rects.get(key, Rect2())
return rect
func set_feedback_state(key: StringName, state: FeedbackState) -> void:
if not _base_states.has(key):
return
_base_states[key] = state # 持久基态只收 NORMAL/DISABLED;HOVER/PRESSED 为视图内瞬态
_apply_state(key, _base_state(key))
func set_hud_band(band: Control) -> void:
if _hud_slot != null:
_hud_slot.add_child(band)
hud_band = band
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
_handle_motion(event as InputEventMouseMotion)
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
_handle_button(event as InputEventMouseButton)
accept_event()
func _handle_motion(event: InputEventMouseMotion) -> void:
var key: StringName = _topmost_hotspot_at(event.position)
if key != _hover_key:
if _hover_key != &"":
_apply_state(_hover_key, _base_state(_hover_key))
_hover_key = key
if key != &"":
_apply_state(key, FeedbackState.HOVER)
if _press_key != &"" and key != _press_key:
_apply_state(_press_key, _base_state(_press_key)) # 按住拖出=取消按压
_press_key = &""
func _handle_button(event: InputEventMouseButton) -> void:
var hit: StringName = _topmost_hotspot_at(event.position)
if event.pressed:
if hit != &"":
_press_key = hit
_apply_state(hit, FeedbackState.PRESSED)
return
if _press_key == &"":
return
if hit == _press_key and not _is_disabled(_press_key):
object_pressed.emit(_press_key)
_apply_state(_press_key, _base_state(_press_key))
_press_key = &""
func _build_layer_node(spec: CampSceneLayerSpec) -> Control:
var node: Control = null
var tex: Texture2D = UiKit.load_tex(_TEX_BY_KEY.get(spec.key, ""))
if tex != null:
var tr := TextureRect.new()
tr.texture = tex
tr.stretch_mode = TextureRect.STRETCH_SCALE
node = tr
else:
var ph := ColorRect.new() # 程序化占位(素材缺位=冷检出半同步态,结构在位冒烟安全)
ph.color = Color(1.0, 1.0, 1.0, 0.06)
node = ph
node.name = "Layer_%s" % spec.key
node.position = spec.texture_rect.position
node.size = spec.texture_rect.size
node.mouse_filter = Control.MOUSE_FILTER_IGNORE
return node
func _build_desat_material() -> void:
_desat_mat = ShaderMaterial.new()
var shader := Shader.new()
shader.code = _DESAT_SHADER_SRC
_desat_mat.shader = shader
func _build_outline() -> void:
_outline = Panel.new()
var sb := StyleBoxFlat.new()
sb.draw_center = false
sb.border_color = UiKit.CANDLE
sb.set_border_width_all(2)
sb.set_corner_radius_all(4)
_outline.add_theme_stylebox_override("panel", sb)
_outline.mouse_filter = Control.MOUSE_FILTER_IGNORE
_outline.visible = false
add_child(_outline)
func _build_name_label() -> void:
_name_label = UiKit.label("", UiKit.FS_HINT, false, UiKit.INK_ON_DEEP)
var plate := StyleBoxFlat.new()
plate.bg_color = UiKit.BG_ROOT
plate.set_corner_radius_all(4)
plate.content_margin_left = 8.0
plate.content_margin_right = 8.0
plate.content_margin_top = 2.0
plate.content_margin_bottom = 2.0
_name_label.add_theme_stylebox_override("normal", plate)
_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE # 名牌不挡命中区
_name_label.visible = false
add_child(_name_label)
func _build_hud_slot(layout: CampSceneLayoutTable) -> void:
_hud_slot = Control.new()
_hud_slot.name = "HudBandSlot"
_hud_slot.set_anchors_preset(Control.PRESET_TOP_WIDE)
_hud_slot.custom_minimum_size = Vector2(0, layout.hud_band_h) # 48px 浮层覆盖,不挤压画面
_hud_slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_hud_slot)
func _topmost_hotspot_at(pos: Vector2) -> StringName:
if _layout == null:
return &""
var idx: int = _layout.layers.size() - 1 # 逆序=自最上 z 向下
while idx >= 0:
var spec: CampSceneLayerSpec = _layout.layers[idx]
if spec.is_clickable() and _hotspot_rects.get(spec.hotspot_key, Rect2()).has_point(pos):
return spec.hotspot_key
idx -= 1
return &""
func _base_state(key: StringName) -> FeedbackState:
var st: int = _base_states.get(key, FeedbackState.NORMAL)
return FeedbackState.DISABLED if st == FeedbackState.DISABLED else FeedbackState.NORMAL
func _is_disabled(key: StringName) -> bool:
return _base_state(key) == FeedbackState.DISABLED
func _apply_state(key: StringName, state: FeedbackState) -> void:
var layer_key: StringName = _hotspot_layer.get(key, &"")
var node: Control = _layer_nodes.get(layer_key, null)
if node == null:
return
var rect: Rect2 = _hotspot_rects.get(key, Rect2())
var base_y: float = node.position.y - _float_offset_of(key) # 还原到基准位再施新态
_set_float_offset(key, 0)
if _float_tween != null:
_float_tween.kill()
_float_tween = null
match state:
FeedbackState.NORMAL:
node.material = null
_outline.visible = false
_name_label.visible = false
FeedbackState.HOVER:
_apply_hover(key, node, rect)
FeedbackState.PRESSED:
node.position.y = base_y + 1.0 # 按下=位移 1px
FeedbackState.DISABLED:
_apply_disabled(node)
func _apply_hover(key: StringName, node: Control, rect: Rect2) -> void:
if _is_disabled(key):
_apply_disabled(node)
return
node.material = null
_outline.position = rect.position
_outline.size = rect.size
_outline.visible = true
_show_name_plate(key, rect)
_run_float(key) # 悬停浮动(整像素逐级吸附)
func _apply_disabled(node: Control) -> void:
node.material = _desat_mat # 降明度+去饱和(shader;仅锻炉/宝箱会有此态)
_outline.visible = false
_name_label.visible = false
func _run_float(key: StringName) -> void:
_float_tween = create_tween()
for step in 4:
var off := -1 - step
_float_tween.tween_callback(_set_float_offset.bind(key, off)).set_delay(UiKit.motion(0.03))
# 悬离时由 _apply_state(NORMAL) 先归零位——本补间只承载升浮
func _set_float_offset(key: StringName, off: int) -> void:
var layer_key: StringName = _hotspot_layer.get(key, &"")
var node: Control = _layer_nodes.get(layer_key, null)
if node == null or _layout == null:
return
for spec in _layout.layers:
if spec.key == layer_key:
node.position.y = spec.texture_rect.position.y + float(off)
return
func _float_offset_of(key: StringName) -> float:
var layer_key: StringName = _hotspot_layer.get(key, &"")
var node: Control = _layer_nodes.get(layer_key, null)
if node == null or _layout == null:
return 0.0
for spec in _layout.layers:
if spec.key == layer_key:
return node.position.y - spec.texture_rect.position.y
return 0.0
func _show_name_plate(key: StringName, rect: Rect2) -> void:
_name_label.text = String(_HOTSPOT_NAMES.get(key, ""))
_name_label.visible = true
_name_label.reset_size()
var pos := Vector2.ZERO
pos.x = clampf(rect.get_center().x - _name_label.size.x / 2.0, 0.0, _layout.canvas.x - _name_label.size.x)
pos.y = minf(rect.end.y + 8.0, _layout.canvas.y - _name_label.size.y) # 贴底物件防出画(画面内钳制)
_name_label.position = pos
func _on_mouse_exited() -> void:
if _hover_key != &"":
_apply_state(_hover_key, _base_state(_hover_key))
_hover_key = &""
if _press_key != &"":
_apply_state(_press_key, _base_state(_press_key))
_press_key = &""