feat: add nearby social interactions
This commit is contained in:
@@ -23,6 +23,7 @@ const COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH: int = 118
|
||||
const COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
||||
const NPC_NAMEPLATE_OFFSET_Y: float = -136.0
|
||||
const HIRED_PLAYER_NAMEPLATE_OFFSET_Y: float = -96.0
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@@ -38,6 +39,7 @@ var _isChangingScene: bool = false
|
||||
var _lastRecruitmentClickMsec: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_interactable")
|
||||
_align_service_occupants()
|
||||
_configure_static_companion_nameplates()
|
||||
_apply_spawn_point()
|
||||
@@ -45,6 +47,15 @@ func _ready() -> void:
|
||||
_connect_exit_area()
|
||||
_connect_recruitment_area()
|
||||
_connect_cafe_companion_events()
|
||||
_discover_destination()
|
||||
|
||||
func _discover_destination() -> void:
|
||||
var destination_id := SceneManager.get_next_destination_id()
|
||||
if destination_id.is_empty():
|
||||
destination_id = "cafe_entrance"
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("discover_destination"):
|
||||
socialManager.call("discover_destination", destination_id)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
@@ -56,6 +67,8 @@ func _align_service_occupants() -> void:
|
||||
cafeWhaleBaristaNpc.global_position = serviceIdlePoint01.global_position
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
if player.has_scene_position_override:
|
||||
return
|
||||
var spawnName: String = SceneManager.get_next_spawn_name()
|
||||
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
|
||||
var marker := $Markers.get_node_or_null(markerName) as Marker2D
|
||||
@@ -75,8 +88,7 @@ func _configure_camera() -> void:
|
||||
playerCamera.limit_smoothed = true
|
||||
|
||||
func _connect_exit_area() -> void:
|
||||
if not exitToWorkZoneArea.body_entered.is_connected(_on_exit_area_body_entered):
|
||||
exitToWorkZoneArea.body_entered.connect(_on_exit_area_body_entered)
|
||||
exitToWorkZoneArea.collision_mask = 0
|
||||
|
||||
func _connect_recruitment_area() -> void:
|
||||
cafeRecruitmentLogoArea.input_pickable = true
|
||||
@@ -92,7 +104,30 @@ func _connect_cafe_companion_events() -> void:
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
|
||||
|
||||
func _on_exit_area_body_entered(body: Node2D) -> void:
|
||||
if _isChangingScene or body != player:
|
||||
return
|
||||
|
||||
func get_interaction_actions(local_player: Node2D) -> Array[Dictionary]:
|
||||
if _isChangingScene or exitToWorkZoneArea == null:
|
||||
return []
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(exitToWorkZoneArea)
|
||||
var distance: float = anchor_position.distance_to(local_player.global_position)
|
||||
if distance > 160.0:
|
||||
return []
|
||||
return [{
|
||||
"id": "cafe_exit",
|
||||
"title": "离开咖啡馆",
|
||||
"distance": distance,
|
||||
"priority": 20,
|
||||
"callback": Callable(self, "_leave_to_work_zone"),
|
||||
}]
|
||||
|
||||
func get_interaction_marker_positions() -> Array[Vector2]:
|
||||
if exitToWorkZoneArea == null:
|
||||
return []
|
||||
return [InteractionAnchorUtil.get_position(exitToWorkZoneArea)]
|
||||
|
||||
func _leave_to_work_zone() -> void:
|
||||
if _isChangingScene:
|
||||
return
|
||||
_isChangingScene = true
|
||||
SceneManager.set_next_scene_position(WORK_ZONE_CAFE_RETURN_POSITION)
|
||||
|
||||
@@ -56,19 +56,10 @@ func _ready() -> void:
|
||||
call_deferred("_send_world_ready")
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
_try_start_private_chat()
|
||||
if Input.is_action_just_pressed("friend_request"):
|
||||
_try_request_friend()
|
||||
return
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("interact"):
|
||||
if _try_start_private_chat():
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
if event.is_action_pressed("friend_request") and _try_request_friend():
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
@@ -117,7 +108,7 @@ func _is_text_input_focused() -> bool:
|
||||
return false
|
||||
|
||||
func _on_interact_pressed(_data: Dictionary = {}) -> void:
|
||||
_try_start_private_chat()
|
||||
return
|
||||
|
||||
func _try_start_private_chat() -> bool:
|
||||
if _is_text_input_focused():
|
||||
|
||||
@@ -46,6 +46,7 @@ var _inventoryRequestInFlight: bool = false
|
||||
var _inventoryRequestReportsErrors: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
_leave_multiplayer_world()
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
@@ -54,8 +55,17 @@ func _ready() -> void:
|
||||
_build_room_decor_ui()
|
||||
_connect_room_decor_events()
|
||||
_fetch_room_decor_inventory()
|
||||
_discover_destination()
|
||||
set_process(false)
|
||||
|
||||
func _discover_destination() -> void:
|
||||
var destination_id := SceneManager.get_next_destination_id()
|
||||
if destination_id.is_empty():
|
||||
destination_id = "personal_room"
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("discover_destination"):
|
||||
socialManager.call("discover_destination", destination_id)
|
||||
|
||||
func _leave_multiplayer_world() -> void:
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
if chatManager != null and chatManager.has_method("leave_world"):
|
||||
@@ -73,6 +83,8 @@ func _exit_tree() -> void:
|
||||
saveManager.decor_save_failed.disconnect(_on_decor_save_failed)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT:
|
||||
@@ -91,7 +103,22 @@ func _process(_delta: float) -> void:
|
||||
if _draggedDecor != null:
|
||||
_update_dragged_decor_position()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _draggedDecor != null or (is_instance_valid(_inventoryPanel) and _inventoryPanel.visible)
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 500
|
||||
|
||||
func request_escape_close() -> void:
|
||||
if _draggedDecor != null:
|
||||
_finish_decor_drag()
|
||||
return
|
||||
if is_instance_valid(_inventoryPanel):
|
||||
_inventoryPanel.visible = false
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
if player.has_scene_position_override:
|
||||
return
|
||||
var spawnName: String = SceneManager.get_next_spawn_name()
|
||||
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
|
||||
var marker := $Markers.get_node_or_null(markerName) as Marker2D
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
class_name ScenePortal
|
||||
extends Area2D
|
||||
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
# ============================================================================
|
||||
# ScenePortal.gd - 场景入口传送组件
|
||||
# ============================================================================
|
||||
@@ -10,14 +12,30 @@ extends Area2D
|
||||
@export var targetSceneName: String = ""
|
||||
@export var targetSpawnName: String = ""
|
||||
@export var targetPosition: Vector2 = Vector2.ZERO
|
||||
@export var interactionTitle: String = "进入"
|
||||
@export var interactionDistance: float = 160.0
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
add_to_group("whaletown_interactable")
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
if not (body is PlayerController):
|
||||
return
|
||||
_change_scene()
|
||||
return
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > interactionDistance:
|
||||
return []
|
||||
var label := interactionTitle.strip_edges()
|
||||
if label.is_empty():
|
||||
label = "进入 %s" % targetSceneName
|
||||
return [{
|
||||
"id": "portal:%s" % get_instance_id(),
|
||||
"title": label,
|
||||
"distance": distance,
|
||||
"priority": 20,
|
||||
"callback": Callable(self, "_change_scene"),
|
||||
}]
|
||||
|
||||
func _change_scene() -> void:
|
||||
if targetSceneName.is_empty():
|
||||
|
||||
@@ -12,6 +12,8 @@ const CAMERA_LIMIT_LEFT: int = -1280
|
||||
const CAMERA_LIMIT_TOP: int = -960
|
||||
const CAMERA_LIMIT_RIGHT: int = 1280
|
||||
const CAMERA_LIMIT_BOTTOM: int = 960
|
||||
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
|
||||
const WELCOME_DIALOG_NAME: String = "WelcomeDialog"
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@@ -20,8 +22,35 @@ const CAMERA_LIMIT_BOTTOM: int = 960
|
||||
func _ready() -> void:
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
_discover_destination()
|
||||
call_deferred("_show_registration_welcome")
|
||||
|
||||
func _show_registration_welcome() -> void:
|
||||
var auth_manager: Node = get_node_or_null("/root/AuthManager")
|
||||
if auth_manager == null or not auth_manager.has_method("consume_registration_welcome"):
|
||||
return
|
||||
if not bool(auth_manager.call("consume_registration_welcome")):
|
||||
return
|
||||
var root: Window = get_tree().root
|
||||
if root.has_node(WELCOME_DIALOG_NAME):
|
||||
return
|
||||
var dialog: CanvasLayer = WELCOME_DIALOG_SCENE.instantiate() as CanvasLayer
|
||||
if dialog == null:
|
||||
return
|
||||
dialog.name = WELCOME_DIALOG_NAME
|
||||
root.add_child(dialog)
|
||||
|
||||
func _discover_destination() -> void:
|
||||
var destination_id := SceneManager.get_next_destination_id()
|
||||
if destination_id.is_empty():
|
||||
destination_id = "square_center"
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("discover_destination"):
|
||||
socialManager.call("discover_destination", destination_id)
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
if player.has_scene_position_override:
|
||||
return
|
||||
var spawnName: String = SceneManager.get_next_spawn_name()
|
||||
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
|
||||
var marker := $Markers.get_node_or_null(markerName) as Marker2D
|
||||
|
||||
@@ -31,6 +31,7 @@ var _isInsideMall: bool = false
|
||||
var _mallCanEnter: bool = true
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_interactable")
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
_ensure_mall_panel()
|
||||
@@ -38,12 +39,23 @@ func _ready() -> void:
|
||||
_ensure_mall_entrance_area()
|
||||
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
|
||||
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
|
||||
_discover_destination()
|
||||
|
||||
func _discover_destination() -> void:
|
||||
var destination_id := SceneManager.get_next_destination_id()
|
||||
if destination_id.is_empty():
|
||||
destination_id = "work_entrance"
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("discover_destination"):
|
||||
socialManager.call("discover_destination", destination_id)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
EventSystem.disconnect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
|
||||
EventSystem.disconnect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
if player.has_scene_position_override:
|
||||
return
|
||||
var spawnName: String = SceneManager.get_next_spawn_name()
|
||||
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
|
||||
var marker := $Markers.get_node_or_null(markerName) as Marker2D
|
||||
@@ -112,15 +124,14 @@ func _ensure_mall_entrance_area() -> void:
|
||||
|
||||
func _setup_mall_entrance_trigger(entrance: Area2D) -> void:
|
||||
_mallEntranceArea = entrance
|
||||
_mallEntranceArea.collision_mask = PLAYER_COLLISION_LAYER
|
||||
if not _mallEntranceArea.body_entered.is_connected(_on_mall_entrance_body_entered):
|
||||
_mallEntranceArea.body_entered.connect(_on_mall_entrance_body_entered)
|
||||
_mallEntranceArea.collision_mask = 0
|
||||
|
||||
func _on_object_interacted(data: Dictionary) -> void:
|
||||
var buildingId := str(data.get("buildingId", ""))
|
||||
if buildingId.is_empty():
|
||||
return
|
||||
if buildingId == "whale_super_mall":
|
||||
_enter_mall()
|
||||
return
|
||||
if buildingId == "virtual_whale_recruitment_board":
|
||||
_open_course_board()
|
||||
@@ -142,9 +153,7 @@ func _open_course_board() -> void:
|
||||
_courseBoardPanel.call("show_panel")
|
||||
|
||||
func _on_mall_entrance_body_entered(body: Node2D) -> void:
|
||||
if body != player or not _mallCanEnter:
|
||||
return
|
||||
_enter_mall()
|
||||
return
|
||||
|
||||
func _enter_mall() -> void:
|
||||
if _isInsideMall:
|
||||
|
||||
@@ -8,14 +8,30 @@ extends Area2D
|
||||
# ============================================================================
|
||||
|
||||
const INTERACTION_COLLISION_LAYER: int = 2
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
@export var buildingId: String = ""
|
||||
@export var buildingTitle: String = ""
|
||||
@export var buildingRole: String = ""
|
||||
@export var interactionDistance: float = 150.0
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = INTERACTION_COLLISION_LAYER
|
||||
collision_mask = 0
|
||||
add_to_group("whaletown_interactable")
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > interactionDistance:
|
||||
return []
|
||||
return [{
|
||||
"id": "building:%s" % buildingId,
|
||||
"title": "使用 %s" % (buildingTitle if not buildingTitle.is_empty() else "设施"),
|
||||
"distance": distance,
|
||||
"priority": 30,
|
||||
"callback": Callable(self, "interact"),
|
||||
}]
|
||||
|
||||
func interact() -> void:
|
||||
var payload := {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=12 format=4]
|
||||
[gd_scene load_steps=13 format=4]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/Maps/PersonalSpace.gd" id="1_personal_space"]
|
||||
[ext_resource type="Texture2D" path="res://assets/maps/personal_space/v1/base/personal_room_25d_sidewalls_wider_not_longer_v1.png" id="2_room_base"]
|
||||
@@ -8,6 +8,7 @@
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/PlayerHud.tscn" id="6_playerhud"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/FriendListPanel.tscn" id="7_friendpanel"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/SettingsPanel.tscn" id="8_settingspanel"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/MapPanel.tscn" id="9_mappanel"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_TopWall"]
|
||||
size = Vector2(882, 76)
|
||||
@@ -35,6 +36,8 @@ layer = 10
|
||||
|
||||
[node name="SettingsPanel" parent="UILayer" instance=ExtResource("8_settingspanel")]
|
||||
|
||||
[node name="MapPanel" parent="UILayer" instance=ExtResource("9_mappanel")]
|
||||
|
||||
[node name="StageBackdrop" type="ColorRect" parent="."]
|
||||
z_index = -200
|
||||
offset_left = -2000.0
|
||||
|
||||
@@ -18,6 +18,7 @@ class_name NPCController
|
||||
signal interaction_happened(text: String)
|
||||
|
||||
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
|
||||
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
||||
const NPC_TALKED_EVENT: String = "npc_talked"
|
||||
@@ -34,12 +35,14 @@ const NAMEPLATE_VISUAL_CHAR_WIDTH: int = 12
|
||||
@export_multiline var dialogue: String = "欢迎来到WhaleTown,我是镇长范鲸晶"
|
||||
@export var showNameplate: bool = false
|
||||
@export var nameplateOffsetY: float = -112.0
|
||||
@export var interactionDistance: float = 150.0
|
||||
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
|
||||
var _nameplate: Label
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_interactable")
|
||||
# 播放场景里配置好的待机动画,让不同 NPC 可以复用同一个控制器。
|
||||
if animation_player.has_animation("idle"):
|
||||
animation_player.play("idle")
|
||||
@@ -63,7 +66,20 @@ func interact() -> void:
|
||||
"npc_name": npcName,
|
||||
"dialogue": dialogue
|
||||
})
|
||||
interaction_happened.emit(dialogue)
|
||||
interaction_happened.emit(dialogue)
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > interactionDistance:
|
||||
return []
|
||||
return [{
|
||||
"id": "npc:%s" % get_instance_id(),
|
||||
"title": "与 %s 交谈" % npcName,
|
||||
"distance": distance,
|
||||
"priority": 40,
|
||||
"callback": Callable(self, "interact"),
|
||||
}]
|
||||
|
||||
# 在 NPC 头顶生成一次性聊天气泡。
|
||||
#
|
||||
|
||||
@@ -25,8 +25,11 @@ const DIRECTION_ROWS: Dictionary = {
|
||||
var lastDirection: String = "down"
|
||||
var _nameLabel: Label
|
||||
var _movementLocked: bool = false
|
||||
var has_scene_position_override: bool = false
|
||||
var _discoveryElapsed: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_local_player")
|
||||
_reset_movement_input_state()
|
||||
_apply_current_appearance()
|
||||
_subscribe_to_appearance_events()
|
||||
@@ -71,28 +74,40 @@ func _release_movement_actions() -> void:
|
||||
|
||||
func _check_spawn_position() -> void:
|
||||
var spawnPos: Variant = SceneManager.get_next_scene_position()
|
||||
if spawnPos != null:
|
||||
if spawnPos is Vector2:
|
||||
global_position = spawnPos
|
||||
_update_world_sort_z()
|
||||
has_scene_position_override = true
|
||||
_update_world_sort_z()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_movement(delta)
|
||||
_update_world_sort_z()
|
||||
_handle_interaction()
|
||||
_report_nearby_discoveries(delta)
|
||||
|
||||
func _handle_interaction() -> void:
|
||||
if _is_text_input_focused():
|
||||
# 统一交互由 /root/InteractionManager 收集附近候选并处理 E 键。
|
||||
return
|
||||
|
||||
func _report_nearby_discoveries(delta: float) -> void:
|
||||
_discoveryElapsed += delta
|
||||
if _discoveryElapsed < 0.35:
|
||||
return
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
EventSystem.emit_event(EventNames.INTERACT_PRESSED, {
|
||||
"player": self,
|
||||
"position": global_position,
|
||||
"direction": lastDirection
|
||||
})
|
||||
if ray_cast.is_colliding():
|
||||
var collider := ray_cast.get_collider()
|
||||
if collider and collider.has_method("interact"):
|
||||
collider.interact()
|
||||
_discoveryElapsed = 0.0
|
||||
var scene := get_tree().current_scene
|
||||
if scene == null:
|
||||
return
|
||||
var map_id: String = str({
|
||||
"Square": "whale_port",
|
||||
"WorkZone": "work_zone",
|
||||
"CafeInterior": "whale_cafe",
|
||||
"PersonalSpace": "personal_space",
|
||||
}.get(str(scene.name), ""))
|
||||
if map_id.is_empty():
|
||||
return
|
||||
var social_manager := get_node_or_null("/root/SocialManager")
|
||||
if social_manager != null and social_manager.has_method("discover_nearby_destinations"):
|
||||
social_manager.call("discover_nearby_destinations", map_id, global_position)
|
||||
|
||||
func _handle_movement(_delta: float) -> void:
|
||||
if _movementLocked:
|
||||
@@ -111,10 +126,7 @@ func _handle_movement(_delta: float) -> void:
|
||||
return
|
||||
|
||||
# 获取移动向量 (参考 docs/02-开发规范/输入映射配置.md)
|
||||
var direction := Input.get_vector(
|
||||
"move_left", "move_right",
|
||||
"move_up", "move_down"
|
||||
)
|
||||
var direction := _movement_direction()
|
||||
|
||||
# 应用移动
|
||||
if direction != Vector2.ZERO:
|
||||
@@ -133,6 +145,16 @@ func _handle_movement(_delta: float) -> void:
|
||||
"position": global_position
|
||||
})
|
||||
|
||||
func _movement_direction() -> Vector2:
|
||||
var interactionManager := get_node_or_null("/root/InteractionManager")
|
||||
if interactionManager != null and interactionManager.has_method("is_selection_active") and bool(interactionManager.call("is_selection_active")):
|
||||
# 有交互候选时方向键用于选择,保留 WASD 移动。
|
||||
return Vector2(
|
||||
(-1.0 if Input.is_key_pressed(KEY_A) else 0.0) + (1.0 if Input.is_key_pressed(KEY_D) else 0.0),
|
||||
(-1.0 if Input.is_key_pressed(KEY_W) else 0.0) + (1.0 if Input.is_key_pressed(KEY_S) else 0.0)
|
||||
).normalized()
|
||||
return Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||
|
||||
func _update_animation_state(direction: Vector2) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
|
||||
@@ -26,6 +26,7 @@ const CAFE_NAME_LABEL_VISUAL_MAX_WIDTH: int = 118
|
||||
const CAFE_NAME_LABEL_VISUAL_CHAR_WIDTH: int = 12
|
||||
const CAFE_NAME_LABEL_OFFSET_Y: float = -96.0
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
const DIRECTION_ROWS: Dictionary = {
|
||||
"down": 0,
|
||||
"up": 1,
|
||||
@@ -38,6 +39,7 @@ var _nameLabel: Label
|
||||
var _cafeCompanionTarget: CafeCompanionTarget
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_interactable")
|
||||
# 初始化时确保无物理处理
|
||||
set_physics_process(false)
|
||||
# 初始位置设为当前位置
|
||||
@@ -52,6 +54,53 @@ func _ready() -> void:
|
||||
if has_node("CollisionShape2D"):
|
||||
$CollisionShape2D.disabled = true
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
if userId.strip_edges().is_empty() or player == null:
|
||||
return []
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > 160.0:
|
||||
return []
|
||||
var display_name := username if not username.strip_edges().is_empty() else "玩家"
|
||||
return [
|
||||
{
|
||||
"id": "player_card:%s" % userId,
|
||||
"title": "查看 %s 的社区名片" % display_name,
|
||||
"distance": distance,
|
||||
"priority": 60,
|
||||
"callback": Callable(self, "_show_community_profile"),
|
||||
},
|
||||
{
|
||||
"id": "player_dm:%s" % userId,
|
||||
"title": "私聊 %s" % display_name,
|
||||
"distance": distance,
|
||||
"priority": 61,
|
||||
"callback": Callable(self, "_open_private_chat"),
|
||||
},
|
||||
{
|
||||
"id": "player_friend:%s" % userId,
|
||||
"title": "申请添加 %s 为好友" % display_name,
|
||||
"distance": distance,
|
||||
"priority": 62,
|
||||
"callback": Callable(self, "_request_friend"),
|
||||
},
|
||||
]
|
||||
|
||||
func _show_community_profile() -> void:
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("show_profile"):
|
||||
socialManager.call("show_profile", userId)
|
||||
|
||||
func _open_private_chat() -> void:
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("open_private_chat"):
|
||||
socialManager.call("open_private_chat", userId, username)
|
||||
|
||||
func _request_friend() -> void:
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("request_friend"):
|
||||
socialManager.call("request_friend", userId, username)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
|
||||
@@ -4,10 +4,25 @@ class_name DatawhaleHonorBoard
|
||||
const RANKING_PANEL_SCENE: PackedScene = preload("res://scenes/ui/datawhale_honor_ranking_panel.tscn")
|
||||
const RANKING_PANEL_NAME: String = "DatawhaleHonorRankingPanel"
|
||||
const INTERACTION_COLLISION_LAYER: int = 2
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = INTERACTION_COLLISION_LAYER
|
||||
collision_mask = 0
|
||||
add_to_group("whaletown_interactable")
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > 150.0:
|
||||
return []
|
||||
return [{
|
||||
"id": "honor_board:%s" % get_instance_id(),
|
||||
"title": "查看荣誉榜",
|
||||
"distance": distance,
|
||||
"priority": 35,
|
||||
"callback": Callable(self, "interact"),
|
||||
}]
|
||||
|
||||
func interact() -> void:
|
||||
var root: Window = get_tree().root
|
||||
|
||||
@@ -4,10 +4,25 @@ class_name NoticeBoard
|
||||
const NOTICE_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/notice_dialog.tscn")
|
||||
const NOTICE_DIALOG_NAME: String = "NoticeDialog"
|
||||
const INTERACTION_COLLISION_LAYER: int = 2
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = INTERACTION_COLLISION_LAYER
|
||||
collision_mask = 0
|
||||
add_to_group("whaletown_interactable")
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > 150.0:
|
||||
return []
|
||||
return [{
|
||||
"id": "notice_board:%s" % get_instance_id(),
|
||||
"title": "查看公告栏",
|
||||
"distance": distance,
|
||||
"priority": 35,
|
||||
"callback": Callable(self, "interact"),
|
||||
}]
|
||||
|
||||
func interact() -> void:
|
||||
var root: Window = get_tree().root
|
||||
|
||||
@@ -4,10 +4,25 @@ class_name WelcomeBoard
|
||||
const WELCOME_DIALOG_SCENE: PackedScene = preload("res://scenes/ui/welcome_dialog.tscn")
|
||||
const WELCOME_DIALOG_NAME: String = "WelcomeDialog"
|
||||
const INTERACTION_COLLISION_LAYER: int = 2
|
||||
const InteractionAnchorUtil = preload("res://_Core/utils/InteractionAnchor.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = INTERACTION_COLLISION_LAYER
|
||||
collision_mask = 0
|
||||
add_to_group("whaletown_interactable")
|
||||
|
||||
func get_interaction_actions(player: Node2D) -> Array[Dictionary]:
|
||||
var anchor_position: Vector2 = InteractionAnchorUtil.get_position(self)
|
||||
var distance: float = anchor_position.distance_to(player.global_position)
|
||||
if distance > 150.0:
|
||||
return []
|
||||
return [{
|
||||
"id": "welcome_board:%s" % get_instance_id(),
|
||||
"title": "查看新人引导",
|
||||
"distance": distance,
|
||||
"priority": 35,
|
||||
"callback": Callable(self, "interact"),
|
||||
}]
|
||||
|
||||
func interact() -> void:
|
||||
var root: Window = get_tree().root
|
||||
|
||||
@@ -680,6 +680,7 @@ func _registration_texture_rect(nodeName: String, texturePath: String, rect: Rec
|
||||
return textureRect
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
_auth_manager = get_node_or_null("/root/AuthManager")
|
||||
_chat_manager = get_node_or_null("/root/ChatManager")
|
||||
_scene_manager = get_node_or_null("/root/SceneManager")
|
||||
@@ -1625,6 +1626,15 @@ func _hide_skin_workshop() -> void:
|
||||
if is_instance_valid(skin_workshop_overlay):
|
||||
skin_workshop_overlay.hide()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return is_instance_valid(skin_workshop_overlay) and skin_workshop_overlay.visible
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 900
|
||||
|
||||
func request_escape_close() -> void:
|
||||
_hide_skin_workshop()
|
||||
|
||||
func _on_workshop_source_pressed() -> void:
|
||||
var err := DisplayServer.file_dialog_show(
|
||||
"选择角色参考图片",
|
||||
|
||||
@@ -30,6 +30,7 @@ var _resignConfirmButton: Button
|
||||
var _resignCancelButton: Button
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
chatFrame.visible = false
|
||||
purchaseButton.pressed.connect(_on_purchase_pressed)
|
||||
sendButton.pressed.connect(_on_send_pressed)
|
||||
@@ -238,6 +239,18 @@ func _on_close_pressed() -> void:
|
||||
_waitingForReply = false
|
||||
chatInput.release_focus()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return (is_instance_valid(_resignDialog) and _resignDialog.visible) or chatFrame.visible
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 850
|
||||
|
||||
func request_escape_close() -> void:
|
||||
if is_instance_valid(_resignDialog) and _resignDialog.visible:
|
||||
_hide_resign_dialog()
|
||||
return
|
||||
_on_close_pressed()
|
||||
|
||||
func _confirm_resign() -> void:
|
||||
var servicePointId := str(_resignTarget.get("service_point_id", "")).strip_edges()
|
||||
if servicePointId.is_empty():
|
||||
|
||||
@@ -32,6 +32,7 @@ var _isSubmitting: bool = false
|
||||
var _isFetchingModels: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
recruitmentFrame.visible = false
|
||||
_setup_protocol_options()
|
||||
_setup_employment_duration_options()
|
||||
@@ -398,6 +399,15 @@ func _on_close_pressed() -> void:
|
||||
fetchModelsButton.disabled = false
|
||||
tokenInput.clear()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return recruitmentFrame.visible
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 900
|
||||
|
||||
func request_escape_close() -> void:
|
||||
_on_close_pressed()
|
||||
|
||||
func _set_status(message: String, isError: bool) -> void:
|
||||
statusLabel.text = message
|
||||
statusLabel.add_theme_color_override("font_color", Color(0.68, 0.22, 0.18, 1) if isError else Color(0.32, 0.42, 0.48, 1))
|
||||
|
||||
@@ -125,6 +125,7 @@ var _send_failure_handled_by_ui: bool = false
|
||||
|
||||
# 准备就绪
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
_configure_mouse_focus()
|
||||
|
||||
# 初始隐藏聊天框
|
||||
@@ -182,6 +183,8 @@ func _get_settings_manager() -> Node:
|
||||
|
||||
# 处理全局输入
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
_handle_global_mouse_button_input(event as InputEventMouseButton)
|
||||
return
|
||||
@@ -366,6 +369,15 @@ func hide_chat(immediate: bool = false) -> void:
|
||||
_transition_tween.tween_property(chat_panel, "modulate:a", 0.0, CHAT_TRANSITION_DURATION)
|
||||
_transition_tween.finished.connect(_on_hide_transition_finished)
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _is_chat_visible
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 480
|
||||
|
||||
func request_escape_close() -> void:
|
||||
hide_chat()
|
||||
|
||||
# 创建隐藏计时器
|
||||
func _create_hide_timer() -> void:
|
||||
_hide_timer = Timer.new()
|
||||
|
||||
@@ -29,6 +29,7 @@ var _isOpen: bool = false
|
||||
var _transitionTween: Tween
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(false)
|
||||
@@ -39,6 +40,8 @@ func _notification(what: int) -> void:
|
||||
_positionPanel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
@@ -72,6 +75,15 @@ func hide_panel() -> void:
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 700
|
||||
|
||||
func request_escape_close() -> void:
|
||||
hide_panel()
|
||||
|
||||
func _buildUi() -> void:
|
||||
_overlay = ColorRect.new()
|
||||
_overlay.name = "courseBoardDimOverlay"
|
||||
|
||||
@@ -96,6 +96,7 @@ var _isLoading: bool = false
|
||||
var _debugShowRankSlots: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
get_tree().paused = true
|
||||
_disableChatUiMouseInput()
|
||||
_buildUi()
|
||||
@@ -109,12 +110,23 @@ func _exit_tree() -> void:
|
||||
_restoreChatUiMouseInput()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if event is InputEventKey:
|
||||
var keyEvent := event as InputEventKey
|
||||
if keyEvent.pressed and not keyEvent.echo and keyEvent.keycode == KEY_ESCAPE:
|
||||
_onClosePressed()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return is_inside_tree()
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 1000
|
||||
|
||||
func request_escape_close() -> void:
|
||||
_onClosePressed()
|
||||
|
||||
func _buildUi() -> void:
|
||||
_rootControl = Control.new()
|
||||
_rootControl.name = "RankingPanelRoot"
|
||||
|
||||
@@ -38,6 +38,7 @@ var _isOpen: bool = false
|
||||
var _hasRequestedFriendList: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
set_panel_open(false)
|
||||
@@ -60,6 +61,15 @@ func set_panel_open(open: bool) -> void:
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 500
|
||||
|
||||
func request_escape_close() -> void:
|
||||
set_panel_open(false)
|
||||
|
||||
func toggle_panel() -> void:
|
||||
set_panel_open(not _isOpen)
|
||||
|
||||
|
||||
@@ -37,13 +37,13 @@ const MAP_CONFIGS: Dictionary = {
|
||||
"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"},
|
||||
{"label": "码头", "destinationId": "square_dock", "icon": "anchor", "pos": Vector2(0.160, 0.395), "side": "right"},
|
||||
{"label": "总部", "destinationId": "square_headquarters", "icon": "home", "pos": Vector2(0.505, 0.188), "side": "right"},
|
||||
{"label": "广场", "destinationId": "square_center", "icon": "whale", "pos": Vector2(0.505, 0.515), "side": "right"},
|
||||
{"label": "小屋", "destinationId": "square_cottage", "icon": "home", "pos": Vector2(0.830, 0.400), "side": "right"},
|
||||
{"label": "工坊", "destinationId": "square_workshop", "icon": "tool", "pos": Vector2(0.752, 0.760), "side": "left"},
|
||||
{"label": "公告", "destinationId": "square_notice", "icon": "notice", "pos": Vector2(0.288, 0.838), "side": "right"},
|
||||
{"label": "入口", "destinationId": "square_work_zone_gate", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
|
||||
],
|
||||
},
|
||||
"WorkZone": {
|
||||
@@ -57,13 +57,13 @@ const MAP_CONFIGS: Dictionary = {
|
||||
"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"},
|
||||
{"label": "商城", "destinationId": "work_mall", "icon": "home", "pos": Vector2(0.500, 0.180), "side": "right"},
|
||||
{"label": "咖啡店", "destinationId": "work_cafe_gate", "icon": "home", "pos": Vector2(0.092, 0.620), "side": "right"},
|
||||
{"label": "任务", "destinationId": "work_jobs", "icon": "notice", "pos": Vector2(0.304, 0.600), "side": "right"},
|
||||
{"label": "课程", "destinationId": "work_courses", "icon": "notice", "pos": Vector2(0.694, 0.500), "side": "left"},
|
||||
{"label": "AI站", "destinationId": "work_ai", "icon": "tool", "pos": Vector2(0.676, 0.785), "side": "left"},
|
||||
{"label": "鲸币", "destinationId": "work_exchange", "icon": "whale", "pos": Vector2(0.920, 0.785), "side": "left"},
|
||||
{"label": "入口", "destinationId": "work_entrance", "icon": "gate", "pos": Vector2(0.500, 0.900), "side": "right"},
|
||||
],
|
||||
},
|
||||
"CafeInterior": {
|
||||
@@ -74,11 +74,19 @@ const MAP_CONFIGS: Dictionary = {
|
||||
"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"},
|
||||
{"label": "服务台", "destinationId": "cafe_counter", "icon": "whale", "pos": Vector2(0.500, 0.465), "side": "right"},
|
||||
{"label": "陪伴区", "destinationId": "cafe_companion", "icon": "notice", "pos": Vector2(0.240, 0.280), "side": "right"},
|
||||
{"label": "出口", "destinationId": "cafe_entrance", "icon": "gate", "pos": Vector2(0.500, 0.855), "side": "right"},
|
||||
],
|
||||
},
|
||||
"PersonalSpace": {
|
||||
"title": "我的房间",
|
||||
"worldSize": Vector2(1024, 768),
|
||||
"sourceNodes": [
|
||||
"RoomBase",
|
||||
],
|
||||
"markers": [],
|
||||
},
|
||||
}
|
||||
|
||||
var _panel: PanelContainer
|
||||
@@ -90,8 +98,13 @@ var _mapCamera: Camera2D
|
||||
var _titleLabel: Label
|
||||
var _markerNodes: Array[Control] = []
|
||||
var _isOpen: bool = false
|
||||
var _travelDestinations: Dictionary = {}
|
||||
var _travelLocked: bool = false
|
||||
var _destinationMenu: PopupMenu
|
||||
var _destinationMenuIds: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_build_ui()
|
||||
set_panel_open(false)
|
||||
@@ -107,6 +120,8 @@ func _notification(what: int) -> void:
|
||||
_anchor_panel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
@@ -123,10 +138,20 @@ func set_panel_open(open: bool) -> void:
|
||||
_panel.visible = _isOpen
|
||||
if _isOpen:
|
||||
_rebuild_minimap_world()
|
||||
_load_travel_destinations()
|
||||
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 600
|
||||
|
||||
func request_escape_close() -> void:
|
||||
set_panel_open(false)
|
||||
|
||||
func toggle_panel() -> void:
|
||||
set_panel_open(not _isOpen)
|
||||
|
||||
@@ -152,6 +177,9 @@ func _build_ui() -> void:
|
||||
|
||||
content.add_child(_build_header())
|
||||
content.add_child(_build_map_view())
|
||||
_destinationMenu = PopupMenu.new()
|
||||
_destinationMenu.id_pressed.connect(_on_destination_menu_selected)
|
||||
add_child(_destinationMenu)
|
||||
|
||||
func _build_header() -> Control:
|
||||
var header := HBoxContainer.new()
|
||||
@@ -173,6 +201,20 @@ func _build_header() -> Control:
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
header.add_child(title)
|
||||
|
||||
var destinationButton := Button.new()
|
||||
destinationButton.text = "目的地"
|
||||
destinationButton.tooltip_text = "查看全部快速传送目的地"
|
||||
destinationButton.custom_minimum_size = Vector2(76, 38)
|
||||
destinationButton.focus_mode = Control.FOCUS_NONE
|
||||
destinationButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
destinationButton.add_theme_font_size_override("font_size", 14)
|
||||
destinationButton.add_theme_color_override("font_color", ACCENT_COLOR)
|
||||
destinationButton.add_theme_stylebox_override("normal", _create_round_style(Color(0.902, 0.962, 0.995, 1.0), 14))
|
||||
destinationButton.add_theme_stylebox_override("hover", _create_round_style(Color(0.818, 0.925, 0.980, 1.0), 14))
|
||||
destinationButton.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
destinationButton.pressed.connect(func() -> void: _show_destination_menu(destinationButton))
|
||||
header.add_child(destinationButton)
|
||||
|
||||
var closeButton := Button.new()
|
||||
closeButton.text = "×"
|
||||
closeButton.tooltip_text = "关闭地图"
|
||||
@@ -263,6 +305,16 @@ func _create_marker(marker: Dictionary) -> Control:
|
||||
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 destinationId := str(marker.get("destinationId", "")).strip_edges()
|
||||
var destination: Dictionary = _travelDestinations.get(destinationId, {})
|
||||
var unlocked := destinationId.is_empty() or _travelDestinations.is_empty() or bool(destination.get("unlocked", false))
|
||||
button.disabled = not unlocked or _travelLocked
|
||||
button.tooltip_text = "点击快速传送" if unlocked else "首次到访后解锁"
|
||||
if not unlocked:
|
||||
button.modulate = Color(0.66, 0.69, 0.72, 0.72)
|
||||
button.pressed.connect(func() -> void: return)
|
||||
else:
|
||||
button.pressed.connect(func() -> void: _request_travel(destinationId))
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -281,7 +333,7 @@ func _create_marker(marker: Dictionary) -> Control:
|
||||
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.text = str(marker.get("label", "地点"))
|
||||
label.text = ("🔒 " if not unlocked else "") + 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)
|
||||
@@ -295,6 +347,117 @@ func _create_marker(marker: Dictionary) -> Control:
|
||||
)
|
||||
return button
|
||||
|
||||
func _load_travel_destinations() -> void:
|
||||
var api := get_node_or_null("/root/ApiClient")
|
||||
if api == null:
|
||||
return
|
||||
api.call("get_json", "/world/travel-destinations", func(success: bool, response: Dictionary, _error: Dictionary) -> void:
|
||||
if not success:
|
||||
return
|
||||
var data_variant: Variant = response.get("data", [])
|
||||
if not (data_variant is Array):
|
||||
return
|
||||
_travelDestinations.clear()
|
||||
for entry in data_variant as Array:
|
||||
if entry is Dictionary:
|
||||
var destination: Dictionary = entry
|
||||
_travelDestinations[str(destination.get("id", ""))] = destination
|
||||
_render_markers()
|
||||
, true)
|
||||
|
||||
func _show_destination_menu(origin: Control) -> void:
|
||||
if not is_instance_valid(_destinationMenu):
|
||||
return
|
||||
_destinationMenu.clear()
|
||||
_destinationMenuIds.clear()
|
||||
if _travelDestinations.is_empty():
|
||||
_destinationMenu.add_item("正在读取目的地…", 0)
|
||||
_destinationMenu.set_item_disabled(0, true)
|
||||
else:
|
||||
var destinations: Array[Dictionary] = []
|
||||
for destination_variant in _travelDestinations.values():
|
||||
if destination_variant is Dictionary:
|
||||
destinations.append(destination_variant as Dictionary)
|
||||
destinations.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
var map_order := {"whale_port": 0, "work_zone": 1, "whale_cafe": 2, "personal_space": 3}
|
||||
var order_a := int(map_order.get(str(a.get("mapId", "")), 9))
|
||||
var order_b := int(map_order.get(str(b.get("mapId", "")), 9))
|
||||
if order_a != order_b:
|
||||
return order_a < order_b
|
||||
return str(a.get("label", "")).naturalnocasecmp_to(str(b.get("label", ""))) < 0
|
||||
)
|
||||
var current_map := ""
|
||||
var menu_id := 1
|
||||
for destination in destinations:
|
||||
var map_id := str(destination.get("mapId", ""))
|
||||
if map_id != current_map:
|
||||
if not current_map.is_empty():
|
||||
_destinationMenu.add_separator()
|
||||
_destinationMenu.add_item("— %s —" % _map_label(map_id), menu_id)
|
||||
_destinationMenu.set_item_disabled(_destinationMenu.item_count - 1, true)
|
||||
menu_id += 1
|
||||
current_map = map_id
|
||||
var unlocked := bool(destination.get("unlocked", false))
|
||||
_destinationMenu.add_item(("" if unlocked else "🔒 ") + str(destination.get("label", "地点")), menu_id)
|
||||
_destinationMenu.set_item_disabled(_destinationMenu.item_count - 1, not unlocked or _travelLocked)
|
||||
_destinationMenuIds[menu_id] = str(destination.get("id", ""))
|
||||
menu_id += 1
|
||||
_destinationMenu.reset_size()
|
||||
_destinationMenu.position = Vector2i(origin.get_screen_position() + Vector2(0.0, origin.size.y))
|
||||
_destinationMenu.popup()
|
||||
|
||||
func _on_destination_menu_selected(menu_id: int) -> void:
|
||||
var destination_id := str(_destinationMenuIds.get(menu_id, ""))
|
||||
if not destination_id.is_empty():
|
||||
_request_travel(destination_id)
|
||||
|
||||
func _request_travel(destinationId: String) -> void:
|
||||
if destinationId.is_empty() or _travelLocked:
|
||||
return
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager == null or not socialManager.has_method("request_travel"):
|
||||
return
|
||||
_travelLocked = true
|
||||
_render_markers()
|
||||
socialManager.call("request_travel", destinationId, Callable(self, "_on_travel_authorized"))
|
||||
|
||||
func _on_travel_authorized(success: bool, destination: Dictionary) -> void:
|
||||
if not success:
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
_travelLocked = false
|
||||
_render_markers()
|
||||
return
|
||||
var mapId := str(destination.get("mapId", ""))
|
||||
var position := _scene_position_for_destination(mapId, Vector2(float(destination.get("x", 0.0)), float(destination.get("y", 0.0))))
|
||||
var sceneName: String = str({"whale_port": "square", "work_zone": "work_zone", "whale_cafe": "cafe_interior", "personal_space": "personal_space"}.get(mapId, ""))
|
||||
if sceneName.is_empty():
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
_travelLocked = false
|
||||
_render_markers()
|
||||
return
|
||||
set_panel_open(false)
|
||||
SceneManager.set_next_destination_id(str(destination.get("id", "")))
|
||||
SceneManager.set_next_scene_position(position)
|
||||
SceneManager.change_scene(sceneName)
|
||||
|
||||
func _scene_position_for_destination(map_id: String, map_position: Vector2) -> Vector2:
|
||||
# 服务端旅行目录使用小地图坐标;游戏场景则以地图中心为原点。
|
||||
var origin := {
|
||||
"whale_port": Vector2(1280, 960),
|
||||
"work_zone": Vector2(1280, 960),
|
||||
"whale_cafe": Vector2(768, 512),
|
||||
"personal_space": Vector2(768, 512),
|
||||
}.get(map_id, Vector2.ZERO) as Vector2
|
||||
return map_position - origin
|
||||
|
||||
func _map_label(map_id: String) -> String:
|
||||
return {
|
||||
"whale_port": "中心广场",
|
||||
"work_zone": "打工区",
|
||||
"whale_cafe": "鲸鱼咖啡馆",
|
||||
"personal_space": "我的房间",
|
||||
}.get(map_id, map_id)
|
||||
|
||||
func _anchor_panel() -> void:
|
||||
_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
_panel.offset_left = -PANEL_SIZE.x - PANEL_MARGIN_RIGHT
|
||||
|
||||
@@ -24,6 +24,7 @@ var _chatUiPrevMouseFilter: Control.MouseFilter = Control.MOUSE_FILTER_STOP
|
||||
var _chatUiMouseDisabled: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
get_tree().paused = true
|
||||
_disableChatUiMouseInput()
|
||||
|
||||
@@ -39,6 +40,15 @@ func _ready() -> void:
|
||||
func _exit_tree() -> void:
|
||||
_restoreChatUiMouseInput()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return is_inside_tree()
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 1000
|
||||
|
||||
func request_escape_close() -> void:
|
||||
_onClosePressed()
|
||||
|
||||
func _setupDots() -> void:
|
||||
for child in dotsContainer.get_children():
|
||||
child.queue_free()
|
||||
|
||||
@@ -8,7 +8,7 @@ extends Control
|
||||
# ============================================================================
|
||||
|
||||
const HUD_MARGIN: Vector2 = Vector2(16, 16)
|
||||
const SHORTCUT_BAR_SIZE: Vector2 = Vector2(350, 86)
|
||||
const SHORTCUT_BAR_SIZE: Vector2 = Vector2(406, 86)
|
||||
const PLAYER_PROFILE_BUTTON_SIZE: Vector2 = Vector2(210, 78)
|
||||
const PLAYER_AVATAR_SIZE: Vector2 = Vector2(58, 58)
|
||||
const HUD_SEPARATION: int = 18
|
||||
@@ -95,6 +95,7 @@ func _build_shortcut_bar() -> PanelContainer:
|
||||
row.add_child(_create_shortcut_button("task", "任务", _on_task_pressed))
|
||||
row.add_child(_create_shortcut_button("backpack", "背包", _on_backpack_pressed))
|
||||
row.add_child(_create_shortcut_button("friends", "好友", _on_friends_pressed))
|
||||
row.add_child(_create_shortcut_button("activity", "通知", _on_notifications_pressed))
|
||||
row.add_child(_create_shortcut_button("settings", "设置", _on_settings_pressed))
|
||||
|
||||
return panel
|
||||
@@ -379,6 +380,11 @@ func _on_map_pressed() -> void:
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.HUD_MAP_TOGGLE, {})
|
||||
|
||||
func _on_notifications_pressed() -> void:
|
||||
var socialManager := get_node_or_null("/root/SocialManager")
|
||||
if socialManager != null and socialManager.has_method("toggle_notifications"):
|
||||
socialManager.call("toggle_notifications")
|
||||
|
||||
func _on_task_pressed() -> void:
|
||||
_emit_status_message("任务入口稍后接入")
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const DEFAULT_SETTINGS: Dictionary = {
|
||||
"ui_scale": 1.00,
|
||||
"fullscreen": false,
|
||||
"show_interaction_hints": true,
|
||||
"show_interaction_points": false,
|
||||
"show_name_always": false,
|
||||
"show_chat_bubbles": true,
|
||||
"world_notifications": true,
|
||||
@@ -45,6 +46,7 @@ const DEFAULT_SETTINGS: Dictionary = {
|
||||
"friend_request_notifications": true,
|
||||
"allow_nearby_private": true,
|
||||
"allow_nearby_friend_requests": true,
|
||||
"allow_nearby_profile": true,
|
||||
"mute_ui_sfx": false,
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ var _transitionTween: Tween
|
||||
var _avatarUploadPending: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(false)
|
||||
@@ -95,6 +98,8 @@ func _notification(what: int) -> void:
|
||||
_position_panel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
@@ -136,6 +141,15 @@ func hide_panel(saveBeforeClose: bool = false) -> void:
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 700
|
||||
|
||||
func request_escape_close() -> void:
|
||||
hide_panel(true)
|
||||
|
||||
func _build_ui() -> void:
|
||||
_overlay = ColorRect.new()
|
||||
_overlay.name = "SettingsDimOverlay"
|
||||
@@ -344,6 +358,7 @@ func _render_basic_content() -> void:
|
||||
_content.add_child(_create_section("基础设置", [
|
||||
_create_toggle_row("window", "全屏显示", "fullscreen", "适合专注游玩,窗口模式方便调试"),
|
||||
_create_toggle_row("eye", "显示互动提示", "show_interaction_hints", "靠近 NPC、好友或公告板时显示按键提示"),
|
||||
_create_toggle_row("eye", "显示交互点", "show_interaction_points", "用白色圆圈直接标出当前地图内的交互目标"),
|
||||
_create_toggle_row("account", "始终显示名字", "show_name_always", "关闭时只在需要时显示玩家名称"),
|
||||
]))
|
||||
_content.add_child(_create_section("音频设置", [
|
||||
@@ -365,25 +380,27 @@ func _render_chat_content() -> void:
|
||||
_content.add_child(_create_section("聊天与社交", [
|
||||
_create_toggle_row("chat", "世界频道提醒", "world_notifications", "收到世界频道消息时保留轻提示"),
|
||||
_create_toggle_row("chat", "私聊提醒", "private_notifications", "好友或附近玩家私聊时提示"),
|
||||
_create_toggle_row("account", "好友请求提醒", "friend_request_notifications", "对方按 F 发起好友申请时显示在好友列表"),
|
||||
_create_toggle_row("account", "好友请求提醒", "friend_request_notifications", "附近玩家发起好友申请时显示在好友列表"),
|
||||
_create_toggle_row("chat", "显示聊天气泡", "show_chat_bubbles", "气泡发送会同时进入世界频道"),
|
||||
]))
|
||||
_content.add_child(_create_section("附近玩家权限", [
|
||||
_create_toggle_row("eye", "允许查看我的名片", "allow_nearby_profile", "关闭后陌生玩家无法在附近打开你的社区名片"),
|
||||
_create_toggle_row("chat", "允许附近私聊", "allow_nearby_private", "附近玩家按 E 可以发起悄悄话"),
|
||||
_create_toggle_row("account", "允许好友申请", "allow_nearby_friend_requests", "附近玩家按 F 可以发送好友申请"),
|
||||
_create_toggle_row("account", "允许好友申请", "allow_nearby_friend_requests", "附近玩家可通过互动列表发送好友申请"),
|
||||
]))
|
||||
|
||||
func _render_controls_content() -> void:
|
||||
_content.add_child(_create_section("操作说明", [
|
||||
_create_key_row("W A S D", "移动角色"),
|
||||
_create_key_row("方向键", "备用移动"),
|
||||
_create_key_row("E", "互动 / 附近私聊"),
|
||||
_create_key_row("F", "发送好友申请"),
|
||||
_create_key_row("方向键", "附近互动列表选择"),
|
||||
_create_key_row("E", "执行选中的互动"),
|
||||
_create_key_row("Esc", "关闭当前界面或结束当前互动"),
|
||||
_create_key_row("T", "打开聊天输入"),
|
||||
_create_key_row("Enter", "发送聊天消息"),
|
||||
]))
|
||||
_content.add_child(_create_section("操作辅助", [
|
||||
_create_toggle_row("eye", "显示互动提示", "show_interaction_hints", "靠近可交互目标时显示按键提示"),
|
||||
_create_toggle_row("eye", "显示交互点", "show_interaction_points", "用白色圆圈直接标出当前地图内的交互目标"),
|
||||
_create_hint_row("当前版本使用固定键位;这里的开关会控制地图内的互动提示显示。"),
|
||||
]))
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const GUIDE_PAGES: Array[Dictionary] = [
|
||||
"image_path": "res://assets/maps/square/v1/props/center_whale_fountain_v2_hd_clean.png",
|
||||
},
|
||||
{
|
||||
"text": "操作提示:\n\n- 按 [color=#ffaa00]E[/color] 键可以与 NPC、公告板和信息板互动。\n- 靠近目标后面向它,再按互动键。\n- 输入框获得焦点时,角色移动会暂停响应。",
|
||||
"text": "操作提示:\n\n- 按 [color=#ffaa00]E[/color] 键可以与 NPC、公告板和信息板互动。\n- 靠近目标后面向它,再按互动键。\n- 按 [color=#ffaa00]Esc[/color] 可以关闭当前界面或结束当前互动。\n- 输入框获得焦点时,角色移动会暂停响应。",
|
||||
"image_path": "res://assets/maps/square/v1/props/bottom_entrance_right_service_props_v2_hd_clean.png",
|
||||
},
|
||||
]
|
||||
@@ -38,6 +38,7 @@ var _guideCurrentPage: int = 0
|
||||
var _guideTween: Tween
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
_disableChatUiMouseInput()
|
||||
|
||||
var closeButton := find_child("CloseButton", true, false) as Button
|
||||
@@ -52,8 +53,20 @@ func _exit_tree() -> void:
|
||||
_restoreChatUiMouseInput()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
queue_free()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return is_inside_tree()
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 980
|
||||
|
||||
func request_escape_close() -> void:
|
||||
queue_free()
|
||||
|
||||
func _onClosePressed() -> void:
|
||||
queue_free()
|
||||
|
||||
@@ -62,6 +62,7 @@ var _transitionTween: Tween
|
||||
var _toastTween: Tween
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("whaletown_escape_dismissible")
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(false)
|
||||
@@ -82,6 +83,8 @@ func _notification(what: int) -> void:
|
||||
_position_panel()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if get_viewport().is_input_handled():
|
||||
return
|
||||
if not _isOpen:
|
||||
return
|
||||
if event is InputEventKey:
|
||||
@@ -120,6 +123,18 @@ func hide_panel() -> void:
|
||||
func is_panel_open() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func is_escape_dismissible() -> bool:
|
||||
return _isOpen
|
||||
|
||||
func get_escape_priority() -> int:
|
||||
return 700
|
||||
|
||||
func request_escape_close() -> void:
|
||||
if is_instance_valid(_confirmDialog) and _confirmDialog.visible:
|
||||
_hide_confirm_dialog()
|
||||
return
|
||||
hide_panel()
|
||||
|
||||
func _build_ui() -> void:
|
||||
_overlay = ColorRect.new()
|
||||
_overlay.name = "mallDimOverlay"
|
||||
|
||||
Reference in New Issue
Block a user