卡④T2(迭代47·营地场景图批): CampSceneView场景视图+camp_scene_layout.tres布局表+V14单测——场景视图/布局坐标/V14三面落地

- schema两件: CampSceneLayerSpec(层键+贴图矩形+命中矩形+命中键)+CampSceneLayoutTable(canvas/hud_band_h/gate_gap/wall_band/layers+validate()按面拆四查:身份贴图/命中边界/命中互斥/围墙带包络)
- 数据表camp_scene_layout.tres: 10层坐标唯一事实源(布局草案值+§5已裁:布告板(845,268,130,140)/门扇(520,250,240,220)门洞对齐/帐篷300x170;贴图矩形=contain终稿实测尺寸锚定:底中心/顶锚/底锚按物件语义,campfire命中(560,640,160,80)只取下半)
- ConfigDB接线: 表注册+表现域容器camp_scene_layout字段+装载期validate()逐问题_fatal(fail-fast红线:命中区越画面/相交/侵顶栏=数据致命)
- CampSceneView: setup_layers/bind_hotspots/get_hotspot_rect/set_feedback_state/set_hud_band五公开口+object_pressed信号; Control根+TextureRect层子节点序=z序(禁CanvasLayer/z_index/Sprite2D); 根单点_gui_input命中判定(层全IGNORE); 四态反馈:悬停=Candle轮廓+名牌(FS_HINT/INK_ON_DEEP/BG_ROOT板,画面内钳制)+Tween逐级整像素浮动/按下=位移1px/禁用=ShaderMaterial去饱和+降明度(零新素材,modulate只有明度轴); 素材缺位程序化占位冒烟安全; 命中键=七物件绑定(gate/forge/dummy/chest/notebook/board/campfire)
- V14单测camp_scene_layout_test.gd(11断言): 校验函数通过/七键集/两两不相交/不越画面不侵顶栏/篝火只下半/门扇对齐缺口/布告板挂墙带/10层序/ConfigDB装载链——全表关系谓词零坐标字面量; run_all manifest登记expect=0
- 验证: --editor --quit刷类缓存后V14孤跑绿(11项)+视图孤检绿(9项:子节点序/合成点击信号/悬停名牌/拖出取消/禁用不发/HUD槽——临时件不入库,T5探针承载正式验收); S9/S13归基线(gdstyle新违规4条已按职责轴拆函数清偿:validate按面四拆/_gui_input三拆/setup_layers构建段三拆/循环内分配提_build_layer_node)
This commit is contained in:
DKChan committed 2026-11-01 17:27:42 +08:00
1 parent aa4aa1dd20
commit 4b5e3f27e8
12 files changed
+566 -2

No files matched your search

+303
View File
@@ -0,0 +1,303 @@
## 营地场景图视图(交互式营地):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 填值)
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)
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 = &""
@@ -0,0 +1 @@
uid://btsrctv5ddx16