feat: add nearby social interactions

This commit is contained in:
ANG-Server
2026-07-21 22:19:41 +08:00
parent 0bab768b14
commit 9dee47492c
43 changed files with 1499 additions and 77 deletions

View File

@@ -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)

View File

@@ -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():

View File

@@ -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

View File

@@ -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():

View File

@@ -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

View File

@@ -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:

View File

@@ -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 := {

View File

@@ -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