## 营地场景图视图(交互式营地):10 层贴图铺底+七物件命中区+点选反馈四态+顶栏预留位。 ## 布局唯一事实源 = db.presentation.camp_scene_layout(构造注入,本视图禁自载表路径); ## 子节点序 = 表内数组序 = z 序(Control 根+TextureRect 层——禁 CanvasLayer/z_index/Sprite2D)。 ## 输入单点:根 _gui_input 集中命中判定(层节点全 IGNORE),点击只发 object_pressed 信号,路由归组合根。 ## 零核心写调用:视图不读 MetaProgression,禁用态经 _apply_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_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.expand_mode = TextureRect.EXPAND_IGNORE_SIZE # 允许节点小于贴图原尺寸(门扇 240×211→矩形 138 高) 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 = &""