hy-farm/godot/FarmGodot/scripts/ui/scene_hit_router.gd
2026-05-29 19:54:56 +08:00

354 lines
14 KiB
GDScript
Raw Permalink 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.

class_name SceneHitRouter
extends RefCounted
# 场景点击路由:
# - 负责给“纯展示贴图”补透明 Button 命中区,保持主 UI 图层不被鼠标事件污染。
# - 普通菜单按钮放在 SceneHitLayer随页面固定农场建筑命中区放在 FarmHitLayer随缩放根节点一起缩放。
# - 这里只做点击分发和按压动画,不写具体业务,业务通过 callbacks 回到 game_scene/panel。
const UiMotionScript := preload("res://scripts/ui/common/ui_motion.gd")
var _owner: Control
var _farm_scene_root: Control
var _pet_display: Control
var _scene_hit_layer: Control
var _farm_hit_layer: Control
var _add_land_hit: Button
var _callbacks: Dictionary = {}
var _farm_direct_targets: Array[Dictionary] = []
var _top_button_group_hits: Array[Button] = []
var _top_button_group_hits_enabled := true
var _hit_layers_enabled := true
var _visitor_mode := false
func setup(options: Dictionary) -> void:
_owner = options.get("owner") as Control
_farm_scene_root = options.get("farm_scene_root") as Control
_pet_display = options.get("pet_display") as Control
_callbacks = options.get("callbacks", {})
# 先建命中层,再关闭装饰节点事件阻挡,最后挂业务入口,保证层级始终在可点击元素上方。
_setup_layers()
_disable_decorative_mouse_blocks()
_call("keep_modal_layers_on_top")
_setup_farm_hits(options.get("farm_targets", {}))
_setup_scene_hits()
func scene_hit_layer() -> Control:
return _scene_hit_layer
func farm_hit_layer() -> Control:
return _farm_hit_layer
func add_land_hit() -> Button:
return _add_land_hit
func set_hit_layers_enabled(enabled: bool) -> void:
_hit_layers_enabled = enabled
if _scene_hit_layer != null:
_scene_hit_layer.visible = enabled
if _farm_hit_layer != null:
_farm_hit_layer.visible = enabled and not _visitor_mode
_apply_scene_hit_button_mode()
func set_visitor_mode(enabled: bool) -> void:
_visitor_mode = enabled
if _farm_hit_layer != null:
_farm_hit_layer.visible = _hit_layers_enabled and not _visitor_mode
_apply_scene_hit_button_mode()
func set_top_button_group_hits_enabled(enabled: bool) -> void:
_top_button_group_hits_enabled = enabled
for hit_button in _top_button_group_hits:
if not is_instance_valid(hit_button):
continue
hit_button.visible = enabled
hit_button.disabled = not enabled
hit_button.mouse_filter = Control.MOUSE_FILTER_STOP if enabled else Control.MOUSE_FILTER_IGNORE
_apply_scene_hit_button_mode()
func handle_direct_scene_hit(position: Vector2) -> bool:
if not _hit_layers_enabled:
return false
# Web/移动端部分透明 Button 会被缩放或弹窗层影响,这里保留一条直接坐标命中的兜底路径。
for entry in _direct_targets():
if _trigger_direct_node_hit(entry["path"], position, _callback(entry["callback"]), float(entry.get("grow", 10.0))):
return true
if _visitor_mode:
return false
for entry in _farm_direct_targets:
var target := entry.get("target") as Control
var callback: Callable = entry.get("callback", Callable())
var offset: Vector2 = entry.get("offset", Vector2.ZERO)
var size: Vector2 = entry.get("size", Vector2.ZERO)
var grow := float(entry.get("grow", 10.0))
if _trigger_direct_control_hit(target, position, callback, offset, size, grow):
return true
return false
func _setup_layers() -> void:
if _owner == null:
return
# 固定 UI 命中层挂主场景z_index 高于顶部/左侧菜单但低于 ModalLayer。
_scene_hit_layer = Control.new()
_scene_hit_layer.name = "SceneHitLayer"
_scene_hit_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_scene_hit_layer.z_index = 1000
_scene_hit_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
_owner.add_child(_scene_hit_layer)
if _farm_scene_root == null:
return
# 农场命中层挂缩放根节点,确保双指缩放后建筑热区和视觉位置一致。
_farm_hit_layer = Control.new()
_farm_hit_layer.name = "FarmHitLayer"
_farm_hit_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_farm_hit_layer.set_anchors_preset(Control.PRESET_FULL_RECT)
_farm_scene_root.add_child(_farm_hit_layer)
func _setup_farm_hits(targets: Dictionary) -> void:
# 建筑热区使用原版可点范围的近似矩形,偏移量按贴图视觉而不是节点边界调校。
var house_art := targets.get("house_art") as Control
if house_art != null:
var house_callback := _callback("open_house_upgrade_panel")
_create_farm_node_hit_button(
"HouseUpgradeHit",
house_art,
Vector2(52.0, 42.0),
Vector2(260.0, 238.0),
house_callback,
house_art
)
_register_farm_direct_target(house_art, Vector2(52.0, 42.0), Vector2(260.0, 238.0), house_callback, 12.0)
var dog_house_art := targets.get("dog_house_art") as Control
if dog_house_art != null:
var dog_callback := _callback("open_pet_panel")
_create_farm_node_hit_button(
"DogHouseHit",
dog_house_art,
Vector2(-10.0, -8.0),
dog_house_art.size + Vector2(24.0, 24.0),
dog_callback,
dog_house_art
)
_register_farm_direct_target(dog_house_art, Vector2(-10.0, -8.0), dog_house_art.size + Vector2(24.0, 24.0), dog_callback, 10.0)
var add_land_art := targets.get("add_land_art") as Control
if add_land_art != null:
var add_land_callback := _callback("open_land_extend_panel")
_add_land_hit = _create_farm_node_hit_button(
"AddLandHit",
add_land_art,
Vector2.ZERO,
add_land_art.size,
add_land_callback,
add_land_art
)
_register_farm_direct_target(add_land_art, Vector2.ZERO, add_land_art.size, add_land_callback, 10.0)
func _setup_scene_hits() -> void:
for entry in _button_targets():
_create_node_hit_button(entry["name"], entry["path"], _callback(entry["callback"]), bool(entry.get("allow_hidden", false)))
_apply_scene_hit_button_mode()
func _direct_targets() -> Array[Dictionary]:
# 直接坐标命中列表用于 _input 兜底;路径必须和场景树节点名保持同步。
return [
{"path": "TopLayer/Avatar", "callback": "open_player_info_panel", "grow": 8.0},
{"path": "TopLayer/HeadMask", "callback": "open_player_info_panel", "grow": 8.0},
{"path": "TopLayer/TopButtonGroup/TopLog", "callback": "open_log_panel"},
{"path": "TopLayer/TopButtonGroup/TopLandup", "callback": "open_land_upgrade_panel"},
{"path": "TopLayer/TopButtonGroup/TopPay", "callback": "open_pay_panel"},
{"path": "TopLayer/TopButtonGroup/TopPayGift", "callback": "open_level_gift_panel"},
{"path": "TopLayer/TopButtonGroup/TopLottery", "callback": "open_lottery_panel"},
{"path": "TopLayer/TopHide", "callback": "toggle_top_button_group"},
{"path": "LeftMenu/MarketButtonArt", "callback": "open_market_panel"},
{"path": "LeftMiddleMenu/Notice", "callback": "open_notice_panel"},
{"path": "LeftMiddleMenu/Sign", "callback": "open_sign_panel"},
{"path": "LeftMiddleMenu/RewardGift", "callback": "open_online_gift_panel"},
{"path": "ExchangeGold", "callback": "open_gold_exchange_panel"},
{"path": "BackHome", "callback": "back_to_own_farm", "grow": 8.0},
]
func _button_targets() -> Array[Dictionary]:
# 透明 Button 命中列表用于正常点击;新增入口优先加这里,再按需要加入 direct_targets。
return [
{"name": "AvatarHit", "path": "TopLayer/Avatar", "callback": "open_player_info_panel"},
{"name": "TopLogHit", "path": "TopLayer/TopButtonGroup/TopLog", "callback": "open_log_panel"},
{"name": "TopLandupHit", "path": "TopLayer/TopButtonGroup/TopLandup", "callback": "open_land_upgrade_panel"},
{"name": "TopPayHit", "path": "TopLayer/TopButtonGroup/TopPay", "callback": "open_pay_panel"},
{"name": "TopPayGiftHit", "path": "TopLayer/TopButtonGroup/TopPayGift", "callback": "open_level_gift_panel"},
{"name": "TopLotteryHit", "path": "TopLayer/TopButtonGroup/TopLottery", "callback": "open_lottery_panel"},
{"name": "TopHideHit", "path": "TopLayer/TopHide", "callback": "toggle_top_button_group"},
{"name": "NoticeHit", "path": "LeftMiddleMenu/Notice", "callback": "open_notice_panel"},
{"name": "SignHit", "path": "LeftMiddleMenu/Sign", "callback": "open_sign_panel"},
{"name": "RewardGiftHit", "path": "LeftMiddleMenu/RewardGift", "callback": "open_online_gift_panel"},
{"name": "ExchangeGoldHit", "path": "ExchangeGold", "callback": "open_gold_exchange_panel"},
{"name": "BackHomeHit", "path": "BackHome", "callback": "back_to_own_farm", "allow_hidden": true},
]
func _trigger_direct_node_hit(node_path: NodePath, position: Vector2, callback: Callable, grow := 10.0) -> bool:
if _owner == null or not callback.is_valid():
return false
if not _top_button_group_hits_enabled and str(node_path).begins_with("TopLayer/TopButtonGroup/"):
return false
var target := _owner.get_node_or_null(node_path) as Control
if target == null:
return false
return _trigger_direct_control_hit(target, position, callback, Vector2.ZERO, target.size, grow)
func _trigger_direct_control_hit(target: Control, position: Vector2, callback: Callable, offset := Vector2.ZERO, size := Vector2.ZERO, grow := 10.0) -> bool:
if _owner == null or target == null or not callback.is_valid():
return false
if not target.is_visible_in_tree():
return false
var hit_rect := _scaled_global_hit_rect(target, offset, size)
if not hit_rect.grow(grow).has_point(position):
return false
# grow 给 H5 触摸留容错,视觉上仍由原贴图负责反馈。
UiMotionScript.play_click_pulse(_owner, target)
callback.call()
return true
func _scaled_global_hit_rect(target: Control, offset: Vector2, size: Vector2) -> Rect2:
var global_rect := target.get_global_rect()
var target_size := target.size
var scale := Vector2(
global_rect.size.x / maxf(target_size.x, 1.0),
global_rect.size.y / maxf(target_size.y, 1.0)
)
var rect_size := size
if rect_size == Vector2.ZERO:
rect_size = target_size
return Rect2(global_rect.position + offset * scale, rect_size * scale)
func _disable_decorative_mouse_blocks() -> void:
if _owner == null:
return
# 这些贴图只参与展示,不能截断上层透明 Button 的点击。
for path in [
"TopLayer/TopBase",
"TopLayer/TopButtonGroup/TopPanelBack",
"TopLayer/TopButtonGroup/TopPanelFront",
"TopLayer/TopHide",
]:
var node := _owner.get_node_or_null(path) as Control
if node != null:
node.mouse_filter = Control.MOUSE_FILTER_IGNORE
func _create_node_hit_button(name: String, node_path: NodePath, callback: Callable, allow_hidden := false) -> void:
if _owner == null or _scene_hit_layer == null:
return
var target := _owner.get_node_or_null(node_path) as Control
if target == null:
return
if not allow_hidden and not target.is_visible_in_tree():
return
# 固定 UI 直接使用全局矩形创建命中按钮,不跟随农场缩放。
var hit_button := _new_hit_button(name, target.get_global_rect().position, target.get_global_rect().size)
hit_button.z_index = 1001
hit_button.pressed.connect(callback)
hit_button.button_down.connect(_play_press_scale.bind(target))
hit_button.button_up.connect(_play_release_scale.bind(target))
_scene_hit_layer.add_child(hit_button)
if str(node_path).begins_with("TopLayer/TopButtonGroup/"):
_top_button_group_hits.append(hit_button)
_apply_scene_hit_button_mode()
func _create_farm_node_hit_button(name: String, target: Control, offset: Vector2, size: Vector2, callback: Callable, press_target: Control = null) -> Button:
if target == null or _farm_hit_layer == null:
return null
var global_position := target.get_global_rect().position + offset
# 视觉坐标先取全局,再转回 FarmHitLayer 局部坐标,防止父节点缩放后命中区错位。
var local_position := _farm_hit_layer.get_global_transform().affine_inverse() * global_position
var hit_button := _new_hit_button(name, local_position, size)
hit_button.z_index = 1001
hit_button.pressed.connect(callback)
if press_target != null:
hit_button.button_down.connect(_play_press_scale.bind(press_target))
hit_button.button_up.connect(_play_release_scale.bind(press_target))
_farm_hit_layer.add_child(hit_button)
return hit_button
func _register_farm_direct_target(target: Control, offset: Vector2, size: Vector2, callback: Callable, grow := 10.0) -> void:
if target == null or not callback.is_valid():
return
_farm_direct_targets.append({
"target": target,
"offset": offset,
"size": size,
"callback": callback,
"grow": grow,
})
func _new_hit_button(button_name: String, position: Vector2, size: Vector2) -> Button:
# 热区按钮必须无文本无主题背景,只承担 mouse_filter=STOP 的点击截获职责。
# z_index 使用绝对值,避免挂在 SceneHitLayer 下时相对叠加到 ModalLayer 之上。
var hit_button := Button.new()
hit_button.name = button_name
hit_button.position = position
hit_button.size = size
hit_button.z_as_relative = false
hit_button.z_index = 1001
hit_button.focus_mode = Control.FOCUS_NONE
hit_button.text = ""
hit_button.mouse_filter = Control.MOUSE_FILTER_STOP
_call("apply_empty_button_style", [hit_button])
return hit_button
func _apply_scene_hit_button_mode() -> void:
if _scene_hit_layer == null:
return
for child in _scene_hit_layer.get_children():
var button := child as Button
if button == null:
continue
var is_back_home := button.name == "BackHomeHit"
var enabled := _hit_layers_enabled and ((is_back_home and _visitor_mode) or ((not is_back_home) and not _visitor_mode))
if button in _top_button_group_hits and not _top_button_group_hits_enabled:
enabled = false
button.visible = enabled
button.disabled = not enabled
button.mouse_filter = Control.MOUSE_FILTER_STOP if enabled else Control.MOUSE_FILTER_IGNORE
func _callback(name: String) -> Callable:
var callback: Callable = _callbacks.get(name, Callable())
return callback
func _call(name: String, args: Array = []) -> Variant:
var callback := _callback(name)
if not callback.is_valid():
return null
return callback.callv(args)
func _play_press_scale(target: Control) -> void:
UiMotionScript.play_press_scale(_owner, target)
func _play_release_scale(target: Control) -> void:
UiMotionScript.play_release_scale(_owner, target)