Files
whale-town-front-v2/scenes/ui/MapPanel.gd

517 lines
18 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.
extends Control
# ============================================================================
# MapPanel.gd - 右上角小镇地图弹窗
# ============================================================================
# 监听 HUD 地图入口,展示当前区域全图与关键建筑标识。
# ============================================================================
const PANEL_SIZE: Vector2 = Vector2(548, 452)
const PANEL_MARGIN_TOP: float = 122.0
const PANEL_MARGIN_RIGHT: float = 56.0
const MAP_VIEW_SIZE: Vector2 = Vector2(500, 375)
const MAP_WORLD_SIZE: Vector2 = Vector2(2560, 1920)
const MAP_CAMERA_ZOOM: Vector2 = Vector2(MAP_VIEW_SIZE.x / MAP_WORLD_SIZE.x, MAP_VIEW_SIZE.y / MAP_WORLD_SIZE.y)
const MINIMAP_EXCLUDED_NODE_NAMES: Array[String] = [
"Characters",
"MultiplayerController",
]
const TEXT_COLOR: Color = Color(0.188, 0.294, 0.424)
const MUTED_COLOR: Color = Color(0.560, 0.639, 0.733)
const ACCENT_COLOR: Color = Color(0.258824, 0.627451, 0.913725)
const SOFT_BLUE: Color = Color(0.918, 0.961, 0.992, 0.94)
const PIN_BLUE: Color = Color(0.244, 0.572, 0.862)
const MAP_CONFIGS: Dictionary = {
"Square": {
"title": "小镇地图",
"sourceNodes": [
"HDGrassBase",
"GroundGrassLayer",
"PathBaseLayer",
"PathEdgeLayer",
"PathEdgeCornerLayer",
"PathEdgeJoinLayer",
"PathCurbLayer",
"YSortWorld",
],
"markers": [
{"label": "码头", "icon": "anchor", "pos": Vector2(0.160, 0.395), "side": "right"},
{"label": "总部", "icon": "home", "pos": Vector2(0.505, 0.188), "side": "right"},
{"label": "广场", "icon": "whale", "pos": Vector2(0.505, 0.515), "side": "right"},
{"label": "小屋", "icon": "home", "pos": Vector2(0.830, 0.400), "side": "right"},
{"label": "工坊", "icon": "tool", "pos": Vector2(0.752, 0.760), "side": "left"},
{"label": "公告", "icon": "notice", "pos": Vector2(0.288, 0.838), "side": "right"},
{"label": "入口", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
],
},
"WorkZone": {
"title": "打工区地图",
"sourceNodes": [
"StageBackdrop",
"RoadPavementLayer",
"RoadAsphaltLayer",
"RoadCrosswalkLayer",
"RoadMarkingLayer",
"YSortWorld",
],
"markers": [
{"label": "商城", "icon": "home", "pos": Vector2(0.500, 0.180), "side": "right"},
{"label": "咖啡店", "icon": "home", "pos": Vector2(0.092, 0.620), "side": "right"},
{"label": "任务", "icon": "notice", "pos": Vector2(0.304, 0.600), "side": "right"},
{"label": "课程", "icon": "notice", "pos": Vector2(0.694, 0.500), "side": "left"},
{"label": "AI站", "icon": "tool", "pos": Vector2(0.676, 0.785), "side": "left"},
{"label": "鲸币", "icon": "whale", "pos": Vector2(0.920, 0.785), "side": "left"},
{"label": "入口", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
],
},
"CafeInterior": {
"title": "鲸鱼咖啡馆",
"worldSize": Vector2(1536, 1024),
"sourceNodes": [
"StageBackdrop",
"CafeServiceHallBase",
],
"markers": [
{"label": "服务台", "icon": "whale", "pos": Vector2(0.500, 0.465), "side": "right"},
{"label": "陪伴区", "icon": "notice", "pos": Vector2(0.240, 0.280), "side": "right"},
{"label": "出口", "icon": "gate", "pos": Vector2(0.500, 0.855), "side": "right"},
],
},
}
var _panel: PanelContainer
var _mapFrame: PanelContainer
var _mapLayer: Control
var _mapViewport: SubViewport
var _mapWorld: Node2D
var _mapCamera: Camera2D
var _titleLabel: Label
var _markerNodes: Array[Control] = []
var _isOpen: bool = false
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_build_ui()
set_panel_open(false)
_subscribe_to_events()
func _exit_tree() -> void:
var eventSystem := _get_event_system()
if eventSystem != null:
eventSystem.call("disconnect_event", EventNames.HUD_MAP_TOGGLE, _on_map_toggle, self)
func _notification(what: int) -> void:
if what == NOTIFICATION_RESIZED and is_instance_valid(_panel):
_anchor_panel()
func _input(event: InputEvent) -> void:
if not _isOpen:
return
if event is InputEventKey:
var keyEvent := event as InputEventKey
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
set_panel_open(false)
get_viewport().set_input_as_handled()
func set_panel_open(open: bool) -> void:
_isOpen = open
visible = _isOpen
mouse_filter = Control.MOUSE_FILTER_STOP if _isOpen else Control.MOUSE_FILTER_IGNORE
if is_instance_valid(_panel):
_panel.visible = _isOpen
if _isOpen:
_rebuild_minimap_world()
func is_panel_open() -> bool:
return _isOpen
func toggle_panel() -> void:
set_panel_open(not _isOpen)
func _build_ui() -> void:
_panel = PanelContainer.new()
_panel.name = "MapCard"
_panel.custom_minimum_size = PANEL_SIZE
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
_panel.add_theme_stylebox_override("panel", _create_panel_style())
add_child(_panel)
_anchor_panel()
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_top", 20)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_bottom", 22)
_panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 12)
margin.add_child(content)
content.add_child(_build_header())
content.add_child(_build_map_view())
func _build_header() -> Control:
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 42)
header.alignment = BoxContainer.ALIGNMENT_CENTER
header.add_theme_constant_override("separation", 12)
var icon := MapGlyph.new()
icon.custom_minimum_size = Vector2(32, 32)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_child(icon)
var title := Label.new()
title.name = "TitleLabel"
_titleLabel = title
title.text = "小镇地图"
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.add_theme_color_override("font_color", TEXT_COLOR)
title.add_theme_font_size_override("font_size", 24)
header.add_child(title)
var closeButton := Button.new()
closeButton.text = "×"
closeButton.tooltip_text = "关闭地图"
closeButton.custom_minimum_size = Vector2(42, 42)
closeButton.focus_mode = Control.FOCUS_NONE
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
closeButton.add_theme_font_size_override("font_size", 28)
closeButton.add_theme_color_override("font_color", MUTED_COLOR)
closeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
closeButton.add_theme_color_override("font_pressed_color", ACCENT_COLOR.darkened(0.18))
closeButton.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
closeButton.add_theme_stylebox_override("normal", _create_round_style(SOFT_BLUE, 21))
closeButton.add_theme_stylebox_override("hover", _create_round_style(Color(0.858, 0.925, 0.980, 1.0), 21))
closeButton.add_theme_stylebox_override("pressed", _create_round_style(Color(0.778, 0.884, 0.965, 1.0), 21))
closeButton.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
closeButton.pressed.connect(func() -> void: set_panel_open(false))
header.add_child(closeButton)
return header
func _build_map_view() -> Control:
_mapFrame = PanelContainer.new()
_mapFrame.custom_minimum_size = MAP_VIEW_SIZE
_mapFrame.mouse_filter = Control.MOUSE_FILTER_STOP
_mapFrame.add_theme_stylebox_override("panel", _create_map_frame_style())
_mapLayer = Control.new()
_mapLayer.custom_minimum_size = MAP_VIEW_SIZE
_mapLayer.clip_contents = true
_mapLayer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_mapFrame.add_child(_mapLayer)
var viewportContainer := SubViewportContainer.new()
viewportContainer.name = "SquareMapViewportContainer"
viewportContainer.set_anchors_preset(Control.PRESET_FULL_RECT)
viewportContainer.stretch = true
viewportContainer.mouse_filter = Control.MOUSE_FILTER_IGNORE
_mapLayer.add_child(viewportContainer)
_mapViewport = SubViewport.new()
_mapViewport.name = "SquareMapViewport"
_mapViewport.size = Vector2i(int(MAP_VIEW_SIZE.x), int(MAP_VIEW_SIZE.y))
_mapViewport.render_target_update_mode = SubViewport.UPDATE_WHEN_VISIBLE
_mapViewport.world_2d = World2D.new()
viewportContainer.add_child(_mapViewport)
_mapWorld = Node2D.new()
_mapWorld.name = "RenderedMapWorld"
_mapViewport.add_child(_mapWorld)
_mapCamera = Camera2D.new()
_mapCamera.name = "MinimapCamera"
_mapCamera.position = Vector2.ZERO
_mapCamera.zoom = MAP_CAMERA_ZOOM
_mapCamera.enabled = true
_mapViewport.add_child(_mapCamera)
var wash := ColorRect.new()
wash.name = "MapSoftWash"
wash.set_anchors_preset(Control.PRESET_FULL_RECT)
wash.color = Color(1.0, 0.985, 0.930, 0.10)
wash.mouse_filter = Control.MOUSE_FILTER_IGNORE
_mapLayer.add_child(wash)
_render_markers()
return _mapFrame
func _render_markers() -> void:
for markerNode in _markerNodes:
if is_instance_valid(markerNode):
markerNode.queue_free()
_markerNodes.clear()
for marker in _current_markers():
var markerNode := _create_marker(marker)
_markerNodes.append(markerNode)
_mapLayer.add_child(markerNode)
func _create_marker(marker: Dictionary) -> Control:
var button := Button.new()
button.name = "Marker%s" % str(marker.get("label", ""))
button.text = ""
button.focus_mode = Control.FOCUS_NONE
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.custom_minimum_size = Vector2(82, 34)
button.add_theme_stylebox_override("normal", _create_marker_style(Color(1.0, 1.0, 1.0, 0.93), Color(0.650, 0.792, 0.910, 0.88)))
button.add_theme_stylebox_override("hover", _create_marker_style(Color(0.925, 0.966, 0.996, 0.98), ACCENT_COLOR))
button.add_theme_stylebox_override("pressed", _create_marker_style(Color(0.858, 0.925, 0.980, 0.98), ACCENT_COLOR.darkened(0.05)))
button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
var row := HBoxContainer.new()
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.set_anchors_preset(Control.PRESET_FULL_RECT)
row.offset_left = 8
row.offset_right = -9
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 5)
button.add_child(row)
var icon := MarkerGlyph.new()
icon.set("iconName", str(marker.get("icon", "pin")))
icon.custom_minimum_size = Vector2(20, 20)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
row.add_child(icon)
var label := Label.new()
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.text = str(marker.get("label", "地点"))
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_font_size_override("font_size", 14)
row.add_child(label)
var position := marker.get("pos", Vector2.ZERO) as Vector2
var buttonSize := button.custom_minimum_size
var xOffset := 8.0 if str(marker.get("side", "right")) == "right" else -buttonSize.x - 8.0
button.position = Vector2(
position.x * MAP_VIEW_SIZE.x + xOffset,
position.y * MAP_VIEW_SIZE.y - buttonSize.y * 0.5
)
return button
func _anchor_panel() -> void:
_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
_panel.offset_left = -PANEL_SIZE.x - PANEL_MARGIN_RIGHT
_panel.offset_top = PANEL_MARGIN_TOP
_panel.offset_right = -PANEL_MARGIN_RIGHT
_panel.offset_bottom = PANEL_MARGIN_TOP + PANEL_SIZE.y
_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
func _subscribe_to_events() -> void:
var eventSystem := _get_event_system()
if eventSystem == null:
push_warning("MapPanel: EventSystem autoload is not available.")
return
eventSystem.call("connect_event", EventNames.HUD_MAP_TOGGLE, _on_map_toggle, self)
func _on_map_toggle(_data: Variant = null) -> void:
toggle_panel()
func _rebuild_minimap_world() -> void:
if not is_instance_valid(_mapWorld):
return
_clear_children(_mapWorld)
var sourceScene := _find_source_scene()
if sourceScene == null:
push_warning("MapPanel: source scene for minimap is unavailable.")
return
_apply_scene_config(sourceScene)
for nodeName in _current_source_node_names(sourceScene):
var sourceNode := sourceScene.get_node_or_null(nodeName)
if sourceNode == null:
continue
var clone := sourceNode.duplicate(0)
if clone == null:
continue
_remove_excluded_minimap_nodes(clone)
_mapWorld.add_child(clone)
func _find_source_scene() -> Node:
var node: Node = self
while node != null:
if MAP_CONFIGS.has(str(node.name)):
return node
node = node.get_parent()
return get_tree().current_scene
func _apply_scene_config(sourceScene: Node) -> void:
var config := _map_config_for_scene(sourceScene)
if is_instance_valid(_titleLabel):
_titleLabel.text = str(config.get("title", "区域地图"))
_configure_map_camera(config)
_render_markers()
func _configure_map_camera(config: Dictionary) -> void:
if not is_instance_valid(_mapCamera):
return
var worldSize := config.get("worldSize", MAP_WORLD_SIZE) as Vector2
if worldSize.x <= 0.0 or worldSize.y <= 0.0:
worldSize = MAP_WORLD_SIZE
var zoomScale := minf(MAP_VIEW_SIZE.x / worldSize.x, MAP_VIEW_SIZE.y / worldSize.y)
_mapCamera.zoom = Vector2.ONE * zoomScale
func _current_source_node_names(sourceScene: Node) -> Array:
var config := _map_config_for_scene(sourceScene)
var nodeNames: Array = config.get("sourceNodes", [])
return nodeNames
func _current_markers() -> Array:
var config := _map_config_for_scene(_find_source_scene())
var markers: Array = config.get("markers", [])
return markers
func _map_config_for_scene(sourceScene: Node) -> Dictionary:
if sourceScene != null:
var sceneName := str(sourceScene.name)
if MAP_CONFIGS.has(sceneName):
return MAP_CONFIGS[sceneName]
return MAP_CONFIGS["Square"]
func _remove_excluded_minimap_nodes(root: Node) -> void:
for nodeName in MINIMAP_EXCLUDED_NODE_NAMES:
var node := root.find_child(nodeName, true, false)
if node != null and node.get_parent() != null:
node.get_parent().remove_child(node)
node.queue_free()
func _clear_children(container: Node) -> void:
for child in container.get_children():
container.remove_child(child)
child.queue_free()
func _create_panel_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(1.0, 1.0, 1.0, 0.948)
style.corner_radius_top_left = 28
style.corner_radius_top_right = 28
style.corner_radius_bottom_left = 28
style.corner_radius_bottom_right = 28
style.shadow_color = Color(0.12549, 0.282353, 0.407843, 0.18)
style.shadow_size = 22
style.shadow_offset = Vector2(0, 9)
return style
func _create_map_frame_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(0.918, 0.961, 0.992, 0.96)
style.border_color = Color(0.431, 0.733, 0.929, 0.86)
style.border_width_left = 2
style.border_width_top = 2
style.border_width_right = 2
style.border_width_bottom = 2
style.corner_radius_top_left = 16
style.corner_radius_top_right = 16
style.corner_radius_bottom_left = 16
style.corner_radius_bottom_right = 16
return style
func _create_marker_style(color: Color, borderColor: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = borderColor
style.border_width_left = 2
style.border_width_top = 2
style.border_width_right = 2
style.border_width_bottom = 2
style.corner_radius_top_left = 18
style.corner_radius_top_right = 18
style.corner_radius_bottom_left = 18
style.corner_radius_bottom_right = 18
style.shadow_color = Color(0.078, 0.223, 0.360, 0.14)
style.shadow_size = 7
style.shadow_offset = Vector2(0, 3)
return style
func _create_round_style(color: Color, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.corner_radius_top_left = radius
style.corner_radius_top_right = radius
style.corner_radius_bottom_left = radius
style.corner_radius_bottom_right = radius
return style
func _get_event_system() -> Node:
return get_node_or_null("/root/EventSystem")
class MapGlyph:
extends Control
func _draw() -> void:
var color := Color(0.258824, 0.627451, 0.913725)
var width := 2.2
draw_polyline(PackedVector2Array([
Vector2(6, 9), Vector2(12, 7), Vector2(19, 9), Vector2(26, 7),
Vector2(26, 25), Vector2(19, 27), Vector2(12, 25), Vector2(6, 27),
Vector2(6, 9)
]), color, width, true)
draw_line(Vector2(12, 8), Vector2(12, 24), color, 1.6, true)
draw_line(Vector2(19, 10), Vector2(19, 26), color, 1.6, true)
class MarkerGlyph:
extends Control
var iconName: String = "pin"
func _draw() -> void:
var color := Color(0.258824, 0.627451, 0.913725)
var center := size * 0.5
match iconName:
"anchor":
_draw_anchor(center, color)
"home":
_draw_home(center, color)
"whale":
_draw_whale(center, color)
"tool":
_draw_tool(center, color)
"notice":
_draw_notice(center, color)
"gate":
_draw_gate(center, color)
_:
draw_circle(center, 5.0, color)
func _draw_anchor(center: Vector2, color: Color) -> void:
draw_line(center + Vector2(0, -7), center + Vector2(0, 7), color, 2.0, true)
draw_circle(center + Vector2(0, -7), 2.8, color)
draw_arc(center + Vector2(0, 2), 7.0, 0.20, PI - 0.20, 24, color, 2.0, true)
draw_line(center + Vector2(-7, 3), center + Vector2(-10, 0), color, 2.0, true)
draw_line(center + Vector2(7, 3), center + Vector2(10, 0), color, 2.0, true)
func _draw_home(center: Vector2, color: Color) -> void:
draw_polyline(PackedVector2Array([
center + Vector2(-8, -1), center + Vector2(0, -8), center + Vector2(8, -1)
]), color, 2.0, true)
draw_rect(Rect2(center + Vector2(-6, -1), Vector2(12, 10)), color, false, 2.0)
draw_line(center + Vector2(-1, 9), center + Vector2(-1, 3), color, 2.0, true)
func _draw_whale(center: Vector2, color: Color) -> void:
draw_arc(center + Vector2(-1, 2), 8.0, PI, TAU, 28, color, 2.0, true)
draw_line(center + Vector2(6, 0), center + Vector2(11, -4), color, 2.0, true)
draw_line(center + Vector2(6, 0), center + Vector2(11, 4), color, 2.0, true)
draw_circle(center + Vector2(-4, 0), 1.2, color)
func _draw_tool(center: Vector2, color: Color) -> void:
draw_line(center + Vector2(-7, 7), center + Vector2(7, -7), color, 2.4, true)
draw_arc(center + Vector2(7, -7), 5.0, PI * 0.1, PI * 1.3, 18, color, 2.0, true)
draw_circle(center + Vector2(-7, 7), 2.4, color)
func _draw_notice(center: Vector2, color: Color) -> void:
draw_rect(Rect2(center + Vector2(-8, -7), Vector2(16, 13)), color, false, 2.0)
draw_line(center + Vector2(-5, -2), center + Vector2(5, -2), color, 1.7, true)
draw_line(center + Vector2(-5, 2), center + Vector2(2, 2), color, 1.7, true)
func _draw_gate(center: Vector2, color: Color) -> void:
draw_rect(Rect2(center + Vector2(-9, -3), Vector2(18, 11)), color, false, 2.0)
draw_line(center + Vector2(0, -3), center + Vector2(0, 8), color, 1.7, true)
draw_line(center + Vector2(-11, 8), center + Vector2(11, 8), color, 2.0, true)