Initial WhaleTown V2 frontend
This commit is contained in:
285
scenes/Maps/CafeInterior.gd
Normal file
285
scenes/Maps/CafeInterior.gd
Normal file
@@ -0,0 +1,285 @@
|
||||
class_name CafeInterior
|
||||
extends Node2D
|
||||
|
||||
# ============================================================================
|
||||
# CafeInterior.gd - 鲸鱼咖啡馆室内场景控制器
|
||||
# ============================================================================
|
||||
# 承载咖啡馆挂机服务大厅,负责玩家出生点、相机边界和返回工作区。
|
||||
# ============================================================================
|
||||
|
||||
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
|
||||
const CAMERA_LIMIT_LEFT: int = -768
|
||||
const CAMERA_LIMIT_TOP: int = -512
|
||||
const CAMERA_LIMIT_RIGHT: int = 768
|
||||
const CAMERA_LIMIT_BOTTOM: int = 512
|
||||
const WORK_ZONE_CAFE_RETURN_POSITION: Vector2 = Vector2(-1093, 560)
|
||||
const CAFE_DOOR_POSITION: Vector2 = Vector2(0, 392)
|
||||
const WORLD_TEXT_THEME = preload("res://assets/ui/world_text_theme.tres")
|
||||
const COMPANION_NAMEPLATE_RENDER_SCALE: float = 0.5
|
||||
const COMPANION_NAMEPLATE_FONT_SIZE: int = 24
|
||||
const COMPANION_NAMEPLATE_VISUAL_HEIGHT: int = 22
|
||||
const COMPANION_NAMEPLATE_VISUAL_MIN_WIDTH: int = 76
|
||||
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
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@onready var hiredPlayerCompanionTarget: CafeCompanionTarget = $YSortWorld/Characters/Players/Player/CafeHiredCompanionTarget
|
||||
@onready var hiredPlayerNameplate: Label = $YSortWorld/Characters/Players/Player/CafeHiredCompanionNameplate
|
||||
@onready var exitToWorkZoneArea: Area2D = $InteractionAreas/ExitToWorkZoneArea
|
||||
@onready var cafeRecruitmentLogoArea: Area2D = $InteractionAreas/CafeRecruitmentLogoArea
|
||||
@onready var serviceIdlePoint01: Marker2D = $Markers/ServiceIdlePoints/ServiceIdlePoint01
|
||||
@onready var cafeWhaleBaristaNpc: NPCController = $YSortWorld/Characters/CafeWhaleBaristaNpc
|
||||
@onready var cafeCompanionNameplate: Label = $YSortWorld/Characters/CafeWhaleBaristaNpc/CafeCompanionNameplate
|
||||
|
||||
var _isChangingScene: bool = false
|
||||
var _lastRecruitmentClickMsec: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
_align_service_occupants()
|
||||
_configure_static_companion_nameplates()
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
_connect_exit_area()
|
||||
_connect_recruitment_area()
|
||||
_connect_cafe_companion_events()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CAFE_COMPANION_EMPLOYMENT_RESIGNED, _on_cafe_companion_employment_resigned, self)
|
||||
|
||||
func _align_service_occupants() -> void:
|
||||
cafeWhaleBaristaNpc.global_position = serviceIdlePoint01.global_position
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
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
|
||||
if marker == null:
|
||||
marker = $Markers/DefaultSpawn
|
||||
player.global_position = marker.global_position
|
||||
|
||||
func _configure_camera() -> void:
|
||||
playerCamera.enabled = true
|
||||
playerCamera.make_current()
|
||||
playerCamera.zoom = CAMERA_ZOOM
|
||||
playerCamera.position_smoothing_enabled = true
|
||||
playerCamera.limit_left = CAMERA_LIMIT_LEFT
|
||||
playerCamera.limit_top = CAMERA_LIMIT_TOP
|
||||
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
|
||||
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
|
||||
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)
|
||||
|
||||
func _connect_recruitment_area() -> void:
|
||||
cafeRecruitmentLogoArea.input_pickable = true
|
||||
if not cafeRecruitmentLogoArea.input_event.is_connected(_on_recruitment_area_input_event):
|
||||
cafeRecruitmentLogoArea.input_event.connect(_on_recruitment_area_input_event)
|
||||
set_process_unhandled_input(true)
|
||||
|
||||
func _connect_cafe_companion_events() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem == null:
|
||||
return
|
||||
eventSystem.call("connect_event", EventNames.CAFE_COMPANION_AGENT_REGISTERED, _on_cafe_companion_agent_registered, self)
|
||||
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
|
||||
_isChangingScene = true
|
||||
SceneManager.set_next_scene_position(WORK_ZONE_CAFE_RETURN_POSITION)
|
||||
SceneManager.change_scene("work_zone")
|
||||
|
||||
func _on_recruitment_area_input_event(_viewport: Viewport, event: InputEvent, _shapeIdx: int) -> void:
|
||||
if not _is_primary_mouse_click(event):
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
_try_emit_recruitment_selected()
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not _is_primary_mouse_click(event):
|
||||
return
|
||||
if not _area_contains_global_point(cafeRecruitmentLogoArea, get_global_mouse_position()):
|
||||
return
|
||||
get_viewport().set_input_as_handled()
|
||||
_try_emit_recruitment_selected()
|
||||
|
||||
func _try_emit_recruitment_selected() -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _lastRecruitmentClickMsec > 0 and now - _lastRecruitmentClickMsec < 120:
|
||||
return
|
||||
_lastRecruitmentClickMsec = now
|
||||
_emit_recruitment_selected()
|
||||
|
||||
func _emit_recruitment_selected() -> void:
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem == null:
|
||||
return
|
||||
eventSystem.call("emit_event", EventNames.CAFE_COMPANION_RECRUITMENT_SELECTED, {
|
||||
"cafe_id": "whale_cafe",
|
||||
"terminal_id": "front_reception_whale_logo",
|
||||
"title": "咖啡店陪伴招聘",
|
||||
"anchor_position": cafeRecruitmentLogoArea.global_position,
|
||||
})
|
||||
|
||||
func _is_primary_mouse_click(event: InputEvent) -> bool:
|
||||
if not (event is InputEventMouseButton):
|
||||
return false
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
return mouseEvent.button_index == MOUSE_BUTTON_LEFT and mouseEvent.pressed
|
||||
|
||||
func _area_contains_global_point(area: Area2D, globalPoint: Vector2) -> bool:
|
||||
for child in area.get_children():
|
||||
var shapeNode := child as CollisionShape2D
|
||||
if shapeNode == null or shapeNode.disabled or shapeNode.shape == null:
|
||||
continue
|
||||
if _shape_contains_point(shapeNode, globalPoint):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _shape_contains_point(shapeNode: CollisionShape2D, globalPoint: Vector2) -> bool:
|
||||
var localPoint := shapeNode.to_local(globalPoint)
|
||||
var shape := shapeNode.shape
|
||||
if shape is RectangleShape2D:
|
||||
var rectangle := shape as RectangleShape2D
|
||||
return Rect2(rectangle.size * -0.5, rectangle.size).has_point(localPoint)
|
||||
if shape is CircleShape2D:
|
||||
var circle := shape as CircleShape2D
|
||||
return localPoint.length() <= circle.radius
|
||||
return false
|
||||
|
||||
func _on_cafe_companion_agent_registered(data: Dictionary) -> void:
|
||||
var companionData := _extract_companion_data(data)
|
||||
if companionData.is_empty():
|
||||
return
|
||||
|
||||
var servicePointId := str(companionData.get("service_point_id", "")).strip_edges()
|
||||
var servicePointMarker := _service_point_marker(servicePointId)
|
||||
if servicePointMarker == null:
|
||||
push_warning("CafeInterior: 服务点不存在,无法摆放被雇佣玩家: %s" % servicePointId)
|
||||
return
|
||||
|
||||
var personaName := str(companionData.get("persona_name", "陪伴机器人")).strip_edges()
|
||||
player.global_position = servicePointMarker.global_position
|
||||
player.velocity = Vector2.ZERO
|
||||
player.lastDirection = "down"
|
||||
player.set_movement_locked(true)
|
||||
player.call("_play_idle_animation")
|
||||
player.call("_update_world_sort_z")
|
||||
_configure_hired_player_companion_target(companionData, servicePointId, personaName)
|
||||
_configure_hired_player_nameplate(personaName)
|
||||
|
||||
func _on_cafe_companion_employment_resigned(_data: Dictionary) -> void:
|
||||
player.set_movement_locked(false)
|
||||
_disable_hired_player_companion_target()
|
||||
hiredPlayerNameplate.visible = false
|
||||
player.global_position = CAFE_DOOR_POSITION
|
||||
player.velocity = Vector2.ZERO
|
||||
player.lastDirection = "down"
|
||||
player.call("_play_idle_animation")
|
||||
player.call("_update_world_sort_z")
|
||||
|
||||
func _extract_companion_data(data: Dictionary) -> Dictionary:
|
||||
var companionVariant: Variant = data.get("companion", {})
|
||||
if companionVariant is Dictionary:
|
||||
return companionVariant
|
||||
if data.has("service_point_id"):
|
||||
return data
|
||||
return {}
|
||||
|
||||
func _service_point_marker(servicePointId: String) -> Marker2D:
|
||||
if servicePointId.is_empty():
|
||||
return null
|
||||
return $Markers/ServiceIdlePoints.get_node_or_null(servicePointId) as Marker2D
|
||||
|
||||
func _configure_hired_player_companion_target(companionData: Dictionary, servicePointId: String, personaName: String) -> void:
|
||||
hiredPlayerCompanionTarget.servicePointId = servicePointId
|
||||
hiredPlayerCompanionTarget.companionId = str(companionData.get("id", "")).strip_edges()
|
||||
if hiredPlayerCompanionTarget.companionId.is_empty():
|
||||
hiredPlayerCompanionTarget.companionId = str(companionData.get("companion_id", "")).strip_edges()
|
||||
hiredPlayerCompanionTarget.companionType = str(companionData.get("companion_type", "hired_player")).strip_edges()
|
||||
hiredPlayerCompanionTarget.personaName = personaName
|
||||
hiredPlayerCompanionTarget.ownerUserId = str(companionData.get("owner_user_id", "")).strip_edges()
|
||||
hiredPlayerCompanionTarget.employmentEndsAt = str(companionData.get("employment_ends_at", "")).strip_edges()
|
||||
hiredPlayerCompanionTarget.selfTarget = true
|
||||
hiredPlayerCompanionTarget.input_pickable = true
|
||||
|
||||
var shapeNode := hiredPlayerCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode != null:
|
||||
shapeNode.disabled = false
|
||||
|
||||
func _disable_hired_player_companion_target() -> void:
|
||||
hiredPlayerCompanionTarget.input_pickable = false
|
||||
hiredPlayerCompanionTarget.selfTarget = false
|
||||
var shapeNode := hiredPlayerCompanionTarget.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode != null:
|
||||
shapeNode.disabled = true
|
||||
|
||||
func _configure_hired_player_nameplate(personaName: String) -> void:
|
||||
_configure_companion_nameplate(hiredPlayerNameplate, personaName, HIRED_PLAYER_NAMEPLATE_OFFSET_Y)
|
||||
hiredPlayerNameplate.visible = true
|
||||
|
||||
func _configure_static_companion_nameplates() -> void:
|
||||
_configure_companion_nameplate(cafeCompanionNameplate, cafeWhaleBaristaNpc.npcName, NPC_NAMEPLATE_OFFSET_Y)
|
||||
cafeCompanionNameplate.visible = true
|
||||
|
||||
func _configure_companion_nameplate(label: Label, personaName: String, offsetY: float) -> void:
|
||||
var displayName := personaName.strip_edges()
|
||||
if displayName.is_empty():
|
||||
displayName = "陪伴机器人"
|
||||
var visualWidth := _companion_nameplate_visual_width(displayName)
|
||||
var renderWidth := ceili(float(visualWidth) / COMPANION_NAMEPLATE_RENDER_SCALE)
|
||||
var renderHeight := ceili(float(COMPANION_NAMEPLATE_VISUAL_HEIGHT) / COMPANION_NAMEPLATE_RENDER_SCALE)
|
||||
|
||||
label.theme = WORLD_TEXT_THEME
|
||||
label.text = displayName
|
||||
label.visible = true
|
||||
label.z_index = 30
|
||||
label.scale = Vector2.ONE * COMPANION_NAMEPLATE_RENDER_SCALE
|
||||
label.position = Vector2(float(visualWidth) * -0.5, offsetY)
|
||||
label.custom_minimum_size = Vector2(renderWidth, renderHeight)
|
||||
label.size = label.custom_minimum_size
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.clip_text = true
|
||||
label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.add_theme_color_override("font_color", Color(0.12, 0.20, 0.24, 1.0))
|
||||
label.add_theme_color_override("font_shadow_color", Color(1.0, 1.0, 1.0, 0.85))
|
||||
label.add_theme_constant_override("shadow_offset_x", 0)
|
||||
label.add_theme_constant_override("shadow_offset_y", 1)
|
||||
label.add_theme_font_size_override("font_size", COMPANION_NAMEPLATE_FONT_SIZE)
|
||||
label.add_theme_stylebox_override("normal", _create_companion_nameplate_style())
|
||||
|
||||
func _companion_nameplate_visual_width(displayName: String) -> int:
|
||||
var estimatedWidth := displayName.length() * COMPANION_NAMEPLATE_VISUAL_CHAR_WIDTH + 28
|
||||
return clampi(estimatedWidth, COMPANION_NAMEPLATE_VISUAL_MIN_WIDTH, COMPANION_NAMEPLATE_VISUAL_MAX_WIDTH)
|
||||
|
||||
func _create_companion_nameplate_style() -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(1.0, 0.988, 0.955, 0.96)
|
||||
style.border_color = Color(0.18, 0.34, 0.38, 0.92)
|
||||
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 = 12
|
||||
style.corner_radius_top_right = 12
|
||||
style.corner_radius_bottom_left = 12
|
||||
style.corner_radius_bottom_right = 12
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_bottom = 4
|
||||
style.shadow_color = Color(0.05, 0.08, 0.09, 0.18)
|
||||
style.shadow_size = 4
|
||||
style.shadow_offset = Vector2(0, 2)
|
||||
return style
|
||||
1
scenes/Maps/CafeInterior.gd.uid
Normal file
1
scenes/Maps/CafeInterior.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccesagjisqodl
|
||||
539
scenes/Maps/MapMultiplayerController.gd
Normal file
539
scenes/Maps/MapMultiplayerController.gd
Normal file
@@ -0,0 +1,539 @@
|
||||
extends Node
|
||||
|
||||
# ============================================================================
|
||||
# MapMultiplayerController.gd - 地图远端玩家同步
|
||||
# ============================================================================
|
||||
# 负责把本地玩家位置发送给聊天 WebSocket,并把后端位置广播渲染为远端玩家。
|
||||
# ============================================================================
|
||||
|
||||
const REMOTE_PLAYER_SCENE: PackedScene = preload("res://scenes/characters/remote_player.tscn")
|
||||
const CHAT_BUBBLE_SCENE: PackedScene = preload("res://scenes/ui/ChatBubble.tscn")
|
||||
const DEFAULT_MAP_ID: String = "whale_port"
|
||||
const POSITION_SEND_INTERVAL: float = 0.12
|
||||
const MIN_POSITION_DELTA: float = 4.0
|
||||
const CHAT_BUBBLE_LAYER_NAME: String = "WorldChatBubbleLayer"
|
||||
const CHAT_BUBBLE_TARGET_OFFSET: Vector2 = Vector2(0, -84)
|
||||
const PRIVATE_CHAT_INTERACTION_DISTANCE: float = 150.0
|
||||
const PRIVATE_CHAT_FORWARD_DOT_THRESHOLD: float = 0.25
|
||||
|
||||
@export var map_id: String = DEFAULT_MAP_ID
|
||||
@export var local_player_path: NodePath = NodePath("../Characters/Players/Player")
|
||||
@export var remote_players_root_path: NodePath = NodePath("../Characters/Players/RemotePlayers")
|
||||
|
||||
var _local_player: Node2D
|
||||
var _remote_players_root: Node2D
|
||||
var _remote_players: Dictionary = {}
|
||||
var _pending_remote_players: Dictionary = {}
|
||||
var _last_sent_position: Vector2 = Vector2.INF
|
||||
var _last_sent_at_msec: int = 0
|
||||
var _last_private_chat_interaction_msec: int = 0
|
||||
var _last_friend_request_interaction_msec: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
_local_player = get_node_or_null(local_player_path) as Node2D
|
||||
_remote_players_root = get_node_or_null(remote_players_root_path) as Node2D
|
||||
|
||||
if _local_player == null:
|
||||
push_warning("MapMultiplayerController: local player not found.")
|
||||
if _remote_players_root == null:
|
||||
push_warning("MapMultiplayerController: remote players root not found.")
|
||||
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("MapMultiplayerController: EventSystem autoload is not available.")
|
||||
return
|
||||
|
||||
eventSystem.call("connect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self)
|
||||
eventSystem.call("connect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self)
|
||||
eventSystem.call("connect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self)
|
||||
eventSystem.call("connect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
|
||||
eventSystem.call("connect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self)
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
return
|
||||
|
||||
eventSystem.call("disconnect_event", EventNames.PLAYER_MOVED, _on_local_player_moved, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_LOGIN_SUCCESS, _on_chat_login_success, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, _on_remote_players_snapshot_ready, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_POSITION_UPDATED, _on_remote_player_position_updated, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_JOINED, _on_remote_player_joined, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_PLAYER_LEFT, _on_remote_player_left, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_READY, _on_remote_skin_ready, self)
|
||||
eventSystem.call("disconnect_event", EventNames.REMOTE_SKIN_FAILED, _on_remote_skin_failed, self)
|
||||
eventSystem.call("disconnect_event", EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
|
||||
eventSystem.call("disconnect_event", EventNames.INTERACT_PRESSED, _on_interact_pressed, self)
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
|
||||
func _get_chat_manager() -> Node:
|
||||
return get_node_or_null("/root/ChatManager")
|
||||
|
||||
func _get_settings_manager() -> Node:
|
||||
return get_node_or_null("/root/SettingsManager")
|
||||
|
||||
func _find_chat_ui() -> Control:
|
||||
var current: Node = self
|
||||
while current != null:
|
||||
var chat_ui := current.get_node_or_null("UILayer/ChatUI") as Control
|
||||
if chat_ui != null:
|
||||
return chat_ui
|
||||
current = current.get_parent()
|
||||
return null
|
||||
|
||||
func _is_text_input_focused() -> bool:
|
||||
var focusOwner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focusOwner == null or not focusOwner.is_inside_tree() or not focusOwner.is_visible_in_tree():
|
||||
return false
|
||||
if focusOwner is LineEdit:
|
||||
var lineEdit := focusOwner as LineEdit
|
||||
return lineEdit.editable
|
||||
if focusOwner is TextEdit:
|
||||
var textEdit := focusOwner as TextEdit
|
||||
return textEdit.editable
|
||||
return false
|
||||
|
||||
func _on_interact_pressed(_data: Dictionary = {}) -> void:
|
||||
_try_start_private_chat()
|
||||
|
||||
func _try_start_private_chat() -> bool:
|
||||
if _is_text_input_focused():
|
||||
return false
|
||||
if not _settings_bool("allow_nearby_private", true):
|
||||
return false
|
||||
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_private_chat_interaction_msec > 0 and now - _last_private_chat_interaction_msec < 160:
|
||||
return false
|
||||
|
||||
var target := _find_private_chat_target()
|
||||
if target == null:
|
||||
return false
|
||||
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
return false
|
||||
|
||||
_last_private_chat_interaction_msec = now
|
||||
eventSystem.call("emit_event", EventNames.CHAT_PRIVATE_TARGET_SELECTED, {
|
||||
"userId": str(target.get("userId")),
|
||||
"username": str(target.get("username"))
|
||||
})
|
||||
return true
|
||||
|
||||
func _try_request_friend() -> bool:
|
||||
if _is_text_input_focused():
|
||||
return false
|
||||
if not _settings_bool("allow_nearby_friend_requests", true):
|
||||
return false
|
||||
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_friend_request_interaction_msec > 0 and now - _last_friend_request_interaction_msec < 220:
|
||||
return false
|
||||
|
||||
var target := _find_private_chat_target()
|
||||
if target == null:
|
||||
return false
|
||||
|
||||
var chat_ui := _find_chat_ui()
|
||||
if chat_ui == null or not chat_ui.has_method("request_friend_with_target"):
|
||||
return false
|
||||
|
||||
_last_friend_request_interaction_msec = now
|
||||
return bool(chat_ui.call(
|
||||
"request_friend_with_target",
|
||||
str(target.get("userId")),
|
||||
str(target.get("username"))
|
||||
))
|
||||
|
||||
func _on_chat_login_success(_data: Dictionary) -> void:
|
||||
_send_world_ready()
|
||||
|
||||
func _on_local_player_moved(data: Dictionary) -> void:
|
||||
var position_variant: Variant = data.get("position", null)
|
||||
if position_variant is Vector2:
|
||||
_send_position(position_variant as Vector2)
|
||||
|
||||
func _send_world_ready() -> void:
|
||||
if _local_player == null:
|
||||
return
|
||||
var chatManager := _get_chat_manager()
|
||||
if chatManager == null or not chatManager.has_method("mark_world_ready"):
|
||||
return
|
||||
chatManager.call("mark_world_ready", _get_map_id(), _local_player.global_position)
|
||||
|
||||
func _send_position(position: Vector2, force: bool = false) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
if not force and now - _last_sent_at_msec < int(POSITION_SEND_INTERVAL * 1000.0):
|
||||
return
|
||||
if not force and _last_sent_position != Vector2.INF and _last_sent_position.distance_to(position) < MIN_POSITION_DELTA:
|
||||
return
|
||||
|
||||
var chatManager := _get_chat_manager()
|
||||
if chatManager == null:
|
||||
return
|
||||
if chatManager.has_method("can_attempt_chat_connection") and not bool(chatManager.call("can_attempt_chat_connection")):
|
||||
return
|
||||
|
||||
_last_sent_position = position
|
||||
_last_sent_at_msec = now
|
||||
chatManager.call("update_player_position", position.x, position.y, _get_map_id())
|
||||
|
||||
func _on_remote_player_joined(data: Dictionary) -> void:
|
||||
if not _is_event_for_current_map(data):
|
||||
return
|
||||
var user_id := str(data.get("userId", "")).strip_edges()
|
||||
if user_id.is_empty():
|
||||
return
|
||||
var remote_player := _ensure_remote_player(user_id, data)
|
||||
var position_variant: Variant = data.get("position", null)
|
||||
if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"):
|
||||
remote_player.call("update_position", position_variant as Vector2)
|
||||
|
||||
func _on_remote_players_snapshot_ready(data: Dictionary) -> void:
|
||||
if not _is_event_for_current_map(data):
|
||||
return
|
||||
|
||||
var seen_user_ids: Dictionary = {}
|
||||
var players_variant: Variant = data.get("players", [])
|
||||
if players_variant is Array:
|
||||
for player_variant in players_variant:
|
||||
if not (player_variant is Dictionary):
|
||||
continue
|
||||
var player_data: Dictionary = player_variant
|
||||
var user_id := str(player_data.get("userId", "")).strip_edges()
|
||||
if user_id.is_empty() or _is_current_user(user_id, str(player_data.get("username", ""))):
|
||||
continue
|
||||
seen_user_ids[user_id] = true
|
||||
var remote_player := _ensure_remote_player(user_id, player_data)
|
||||
var position_variant: Variant = player_data.get("position", null)
|
||||
if remote_player != null and position_variant is Vector2 and remote_player.has_method("update_position"):
|
||||
remote_player.call("update_position", position_variant as Vector2)
|
||||
|
||||
for user_id_variant in _remote_players.keys().duplicate():
|
||||
var user_id := str(user_id_variant)
|
||||
if seen_user_ids.has(user_id):
|
||||
continue
|
||||
var remote_player := _remote_players[user_id] as Node
|
||||
_remote_players.erase(user_id)
|
||||
if is_instance_valid(remote_player):
|
||||
remote_player.queue_free()
|
||||
for user_id_variant in _pending_remote_players.keys().duplicate():
|
||||
var pendingUserId := str(user_id_variant)
|
||||
if not seen_user_ids.has(pendingUserId):
|
||||
_pending_remote_players.erase(pendingUserId)
|
||||
|
||||
func _on_remote_player_position_updated(data: Dictionary) -> void:
|
||||
if not _is_event_for_current_map(data):
|
||||
return
|
||||
var user_id := str(data.get("userId", "")).strip_edges()
|
||||
if user_id.is_empty():
|
||||
return
|
||||
if _is_current_user(user_id, str(data.get("username", ""))):
|
||||
return
|
||||
|
||||
var position_variant: Variant = data.get("position", null)
|
||||
if not (position_variant is Vector2):
|
||||
return
|
||||
|
||||
var remote_player := _ensure_remote_player(user_id, data)
|
||||
if remote_player != null and remote_player.has_method("update_position"):
|
||||
remote_player.call("update_position", position_variant as Vector2)
|
||||
|
||||
func _on_remote_player_left(data: Dictionary) -> void:
|
||||
if not _is_event_for_current_map(data):
|
||||
return
|
||||
var user_id := str(data.get("userId", "")).strip_edges()
|
||||
if user_id.is_empty():
|
||||
return
|
||||
_pending_remote_players.erase(user_id)
|
||||
if not _remote_players.has(user_id):
|
||||
return
|
||||
|
||||
var remote_player := _remote_players[user_id] as Node
|
||||
_remote_players.erase(user_id)
|
||||
if is_instance_valid(remote_player):
|
||||
remote_player.queue_free()
|
||||
|
||||
func _on_chat_message_received(data: Dictionary) -> void:
|
||||
if not bool(data.get("show_bubble", false)):
|
||||
return
|
||||
if not _settings_bool("show_chat_bubbles", true):
|
||||
return
|
||||
|
||||
var content := str(data.get("content", "")).strip_edges()
|
||||
if content.is_empty():
|
||||
return
|
||||
|
||||
var target := _resolve_chat_bubble_target(data)
|
||||
if target == null:
|
||||
return
|
||||
|
||||
_show_chat_bubble(target, content)
|
||||
|
||||
func _ensure_remote_player(user_id: String, data: Dictionary) -> Node2D:
|
||||
if _is_current_user(user_id, str(data.get("username", ""))):
|
||||
return null
|
||||
var appearanceManager := get_node_or_null("/root/AppearanceManager")
|
||||
var skinId := str(data.get("skin_id", data.get("skinId", ""))).strip_edges()
|
||||
var skinAssetVariant: Variant = data.get("skin_asset", data.get("skinAsset", {}))
|
||||
var skinAsset: Dictionary = skinAssetVariant if skinAssetVariant is Dictionary else {}
|
||||
if appearanceManager != null and appearanceManager.has_method("is_remote_skin_ready"):
|
||||
if not bool(appearanceManager.call("is_remote_skin_ready", skinId, skinAsset)):
|
||||
_queue_pending_remote_player(user_id, data)
|
||||
if _remote_players.has(user_id):
|
||||
var loadingPlayer := _remote_players[user_id] as Node2D
|
||||
if is_instance_valid(loadingPlayer):
|
||||
loadingPlayer.hide()
|
||||
if appearanceManager.has_method("request_remote_skin"):
|
||||
appearanceManager.call("request_remote_skin", skinId, skinAsset)
|
||||
return null
|
||||
if _remote_players.has(user_id):
|
||||
var existing_player := _remote_players[user_id] as Node2D
|
||||
if is_instance_valid(existing_player):
|
||||
_apply_remote_player_metadata(existing_player, data)
|
||||
existing_player.show()
|
||||
return existing_player
|
||||
|
||||
if _remote_players_root == null:
|
||||
return null
|
||||
|
||||
var remote_player := REMOTE_PLAYER_SCENE.instantiate() as Node2D
|
||||
if remote_player == null:
|
||||
return null
|
||||
|
||||
remote_player.name = "RemotePlayer_%s" % user_id
|
||||
remote_player.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
_remote_players_root.add_child(remote_player)
|
||||
_remote_players[user_id] = remote_player
|
||||
|
||||
if remote_player.has_method("setup"):
|
||||
var setup_data := data.duplicate(true)
|
||||
if not setup_data.has("position"):
|
||||
setup_data["position"] = {"x": _local_player.global_position.x if _local_player != null else 0.0, "y": _local_player.global_position.y if _local_player != null else 0.0}
|
||||
remote_player.call("setup", setup_data)
|
||||
|
||||
return remote_player
|
||||
|
||||
func _queue_pending_remote_player(userId: String, data: Dictionary) -> void:
|
||||
var merged: Dictionary = (_pending_remote_players.get(userId, {}) as Dictionary).duplicate(true)
|
||||
for key in data.keys():
|
||||
merged[key] = data[key]
|
||||
_pending_remote_players[userId] = merged
|
||||
|
||||
func _on_remote_skin_ready(data: Dictionary) -> void:
|
||||
var skinId := str(data.get("skin_id", "")).strip_edges()
|
||||
if skinId.is_empty():
|
||||
return
|
||||
for userIdVariant in _pending_remote_players.keys().duplicate():
|
||||
var userId := str(userIdVariant)
|
||||
var pendingData: Dictionary = _pending_remote_players.get(userId, {})
|
||||
var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges()
|
||||
if pendingSkinId != skinId:
|
||||
continue
|
||||
_pending_remote_players.erase(userId)
|
||||
var remotePlayer := _ensure_remote_player(userId, pendingData)
|
||||
var positionVariant: Variant = pendingData.get("position", null)
|
||||
if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"):
|
||||
remotePlayer.call("update_position", positionVariant as Vector2)
|
||||
|
||||
func _on_remote_skin_failed(data: Dictionary) -> void:
|
||||
var skinId := str(data.get("skin_id", "")).strip_edges()
|
||||
if skinId.is_empty():
|
||||
return
|
||||
for userIdVariant in _pending_remote_players.keys().duplicate():
|
||||
var userId := str(userIdVariant)
|
||||
var pendingData: Dictionary = _pending_remote_players.get(userId, {})
|
||||
var pendingSkinId := str(pendingData.get("skin_id", pendingData.get("skinId", ""))).strip_edges()
|
||||
if pendingSkinId != skinId:
|
||||
continue
|
||||
_pending_remote_players.erase(userId)
|
||||
var fallbackData := pendingData.duplicate(true)
|
||||
fallbackData["skin_id"] = ""
|
||||
fallbackData["skinId"] = ""
|
||||
fallbackData.erase("skin_asset")
|
||||
fallbackData.erase("skinAsset")
|
||||
var remotePlayer := _ensure_remote_player(userId, fallbackData)
|
||||
var positionVariant: Variant = pendingData.get("position", null)
|
||||
if remotePlayer != null and positionVariant is Vector2 and remotePlayer.has_method("update_position"):
|
||||
remotePlayer.call("update_position", positionVariant as Vector2)
|
||||
|
||||
func _find_private_chat_target() -> Node2D:
|
||||
if _local_player == null or _remote_players.is_empty():
|
||||
return null
|
||||
|
||||
var local_position := _local_player.global_position
|
||||
var facing_direction := _get_local_facing_direction()
|
||||
var best_forward_target: Node2D = null
|
||||
var best_forward_distance := INF
|
||||
var best_any_target: Node2D = null
|
||||
var best_any_distance := INF
|
||||
|
||||
for remote_player_variant in _remote_players.values():
|
||||
var remote_player := remote_player_variant as Node2D
|
||||
if not is_instance_valid(remote_player):
|
||||
continue
|
||||
|
||||
var offset := remote_player.global_position - local_position
|
||||
var distance := offset.length()
|
||||
if distance > PRIVATE_CHAT_INTERACTION_DISTANCE:
|
||||
continue
|
||||
|
||||
if distance < best_any_distance:
|
||||
best_any_distance = distance
|
||||
best_any_target = remote_player
|
||||
|
||||
if distance <= 0.01:
|
||||
continue
|
||||
|
||||
var forward_dot := facing_direction.dot(offset.normalized())
|
||||
if forward_dot >= PRIVATE_CHAT_FORWARD_DOT_THRESHOLD and distance < best_forward_distance:
|
||||
best_forward_distance = distance
|
||||
best_forward_target = remote_player
|
||||
|
||||
return best_forward_target if best_forward_target != null else best_any_target
|
||||
|
||||
func _get_local_facing_direction() -> Vector2:
|
||||
if _local_player != null:
|
||||
var last_direction := str(_local_player.get("lastDirection"))
|
||||
match last_direction:
|
||||
"up":
|
||||
return Vector2.UP
|
||||
"down":
|
||||
return Vector2.DOWN
|
||||
"left":
|
||||
return Vector2.LEFT
|
||||
"right":
|
||||
return Vector2.RIGHT
|
||||
_:
|
||||
pass
|
||||
return Vector2.DOWN
|
||||
|
||||
func _apply_remote_player_metadata(remote_player: Node2D, data: Dictionary) -> void:
|
||||
var username := str(data.get("username", "")).strip_edges()
|
||||
if not username.is_empty():
|
||||
remote_player.set("username", username)
|
||||
if data.has("skin_id") or data.has("skinId") or data.has("skin_asset") or data.has("skinAsset") or data.has("avatar_id") or data.has("avatarId") or data.has("cafe_companion") or data.has("cafeCompanion"):
|
||||
if remote_player.has_method("setup"):
|
||||
remote_player.call("setup", data)
|
||||
|
||||
func _resolve_chat_bubble_target(data: Dictionary) -> Node2D:
|
||||
var from_user := str(data.get("from_user", data.get("username", ""))).strip_edges()
|
||||
var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges()
|
||||
var is_self := bool(data.get("is_self", false))
|
||||
|
||||
if is_self or (not user_id.is_empty() and _is_current_user(user_id, from_user)) or _is_current_username(from_user):
|
||||
return _local_player
|
||||
|
||||
if not user_id.is_empty() and _remote_players.has(user_id):
|
||||
var remote_by_id := _remote_players[user_id] as Node2D
|
||||
if is_instance_valid(remote_by_id):
|
||||
return remote_by_id
|
||||
|
||||
if from_user.is_empty():
|
||||
return null
|
||||
|
||||
for remote_player_variant in _remote_players.values():
|
||||
var remote_player := remote_player_variant as Node2D
|
||||
if not is_instance_valid(remote_player):
|
||||
continue
|
||||
var remote_username := str(remote_player.get("username")).strip_edges()
|
||||
if not remote_username.is_empty() and remote_username == from_user:
|
||||
return remote_player
|
||||
|
||||
return null
|
||||
|
||||
func _show_chat_bubble(target: Node2D, text: String) -> void:
|
||||
var bubble := CHAT_BUBBLE_SCENE.instantiate() as Control
|
||||
if bubble == null:
|
||||
return
|
||||
|
||||
var bubble_layer := _get_chat_bubble_layer()
|
||||
bubble_layer.add_child(bubble)
|
||||
if bubble.has_method("set_text"):
|
||||
bubble.call("set_text", text, target, CHAT_BUBBLE_TARGET_OFFSET)
|
||||
|
||||
func _get_chat_bubble_layer() -> CanvasLayer:
|
||||
return _get_or_create_canvas_layer(CHAT_BUBBLE_LAYER_NAME, 20)
|
||||
|
||||
func _get_or_create_canvas_layer(layerName: String, layerIndex: int) -> CanvasLayer:
|
||||
var root := get_tree().root
|
||||
var layer := root.get_node_or_null(layerName) as CanvasLayer
|
||||
if layer != null:
|
||||
return layer
|
||||
|
||||
layer = CanvasLayer.new()
|
||||
layer.name = layerName
|
||||
layer.layer = layerIndex
|
||||
root.add_child(layer)
|
||||
return layer
|
||||
|
||||
func _get_map_id() -> String:
|
||||
var configured_map_id := map_id.strip_edges()
|
||||
return configured_map_id if not configured_map_id.is_empty() else DEFAULT_MAP_ID
|
||||
|
||||
func _is_event_for_current_map(data: Dictionary) -> bool:
|
||||
var event_map_id := str(data.get("mapId", data.get("map_id", ""))).strip_edges()
|
||||
return event_map_id.is_empty() or event_map_id == _get_map_id()
|
||||
|
||||
func _is_current_user(user_id: String, username: String = "") -> bool:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager == null:
|
||||
return false
|
||||
|
||||
var user: Dictionary = authManager.call("get_current_user")
|
||||
var current_id := str(user.get("id", user.get("userId", user.get("user_id", "")))).strip_edges()
|
||||
if not current_id.is_empty() and user_id == current_id:
|
||||
return true
|
||||
|
||||
var current_username := str(user.get("username", "")).strip_edges()
|
||||
return not current_username.is_empty() and not username.strip_edges().is_empty() and current_username == username.strip_edges()
|
||||
|
||||
func _is_current_username(username: String) -> bool:
|
||||
var normalized_username := username.strip_edges()
|
||||
if normalized_username.is_empty():
|
||||
return false
|
||||
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager != null:
|
||||
var user: Dictionary = authManager.call("get_current_user")
|
||||
var current_username := str(user.get("username", "")).strip_edges()
|
||||
if not current_username.is_empty() and current_username == normalized_username:
|
||||
return true
|
||||
|
||||
var chatManager := _get_chat_manager()
|
||||
if chatManager != null:
|
||||
var chat_username := str(chatManager.get("_current_username")).strip_edges()
|
||||
if not chat_username.is_empty() and chat_username == normalized_username:
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func _settings_bool(key: String, defaultValue: bool) -> bool:
|
||||
var settingsManager := _get_settings_manager()
|
||||
if settingsManager != null and settingsManager.has_method("get_bool"):
|
||||
return bool(settingsManager.call("get_bool", key))
|
||||
return defaultValue
|
||||
1
scenes/Maps/MapMultiplayerController.gd.uid
Normal file
1
scenes/Maps/MapMultiplayerController.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dxupavbgcw3tw
|
||||
808
scenes/Maps/PersonalSpace.gd
Normal file
808
scenes/Maps/PersonalSpace.gd
Normal file
@@ -0,0 +1,808 @@
|
||||
class_name PersonalSpace
|
||||
extends Node2D
|
||||
|
||||
# ============================================================================
|
||||
# PersonalSpace.gd - 个人空间场景控制器
|
||||
# ============================================================================
|
||||
# 独立于广场的私人房间空间。当前版本负责固定镜头、玩家出生点和
|
||||
# 后续房间 manifest/装修系统的场景承载。
|
||||
# ============================================================================
|
||||
|
||||
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
|
||||
const CAMERA_LIMIT_LEFT: int = -538
|
||||
const CAMERA_LIMIT_TOP: int = -358
|
||||
const CAMERA_LIMIT_RIGHT: int = 538
|
||||
const CAMERA_LIMIT_BOTTOM: int = 358
|
||||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||||
|
||||
const TEXT_COLOR: Color = Color(0.102, 0.224, 0.376)
|
||||
const PANEL_COLOR: Color = Color(0.970, 0.992, 1.0, 0.96)
|
||||
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
|
||||
const MUTED_COLOR: Color = Color(0.400, 0.500, 0.620)
|
||||
const DECOR_TEXTURE_FILTER: int = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
const DECOR_COLLISION_LAYER: int = 1
|
||||
const DECOR_COLLISION_MASK: int = 1
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@onready var uiLayer: CanvasLayer = $UILayer
|
||||
@onready var ySortWorld: Node2D = $YSortWorld
|
||||
@onready var staticCollision: Node2D = $StaticCollision
|
||||
|
||||
var _decorLayer: Node2D
|
||||
var _inventoryRequest: HTTPRequest
|
||||
var _inventoryPanel: PanelContainer
|
||||
var _inventoryGrid: GridContainer
|
||||
var _dragSurface: Control
|
||||
var _toastLabel: Label
|
||||
var _selectedDecorId: String = ""
|
||||
var _draggedDecor: Sprite2D
|
||||
var _dragOffset: Vector2 = Vector2.ZERO
|
||||
var _decorItems: Dictionary = {}
|
||||
var _decorNodes: Dictionary = {}
|
||||
var _decorCollisionBodies: Dictionary = {}
|
||||
var _remoteDecorDefinitions: Dictionary = {}
|
||||
var _inventoryRequestInFlight: bool = false
|
||||
var _inventoryRequestReportsErrors: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_leave_multiplayer_world()
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
_build_decor_layer()
|
||||
_build_room_decor_requests()
|
||||
_build_room_decor_ui()
|
||||
_connect_room_decor_events()
|
||||
_fetch_room_decor_inventory()
|
||||
set_process(false)
|
||||
|
||||
func _leave_multiplayer_world() -> void:
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
if chatManager != null and chatManager.has_method("leave_world"):
|
||||
chatManager.call("leave_world", "personal_space")
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager != null:
|
||||
if saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
|
||||
saveManager.decor_save_succeeded.disconnect(_on_decor_save_succeeded)
|
||||
if saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
|
||||
saveManager.decor_save_failed.disconnect(_on_decor_save_failed)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
if _draggedDecor != null and not mouseEvent.pressed:
|
||||
_finish_decor_drag()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _draggedDecor == null and mouseEvent.pressed and not _is_pointer_over_blocking_ui():
|
||||
if _try_begin_decor_drag(get_global_mouse_position()):
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _draggedDecor != null:
|
||||
_update_dragged_decor_position()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _draggedDecor != null:
|
||||
_update_dragged_decor_position()
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
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
|
||||
if marker == null:
|
||||
marker = $Markers/DefaultSpawn
|
||||
player.global_position = marker.global_position
|
||||
|
||||
func _configure_camera() -> void:
|
||||
playerCamera.zoom = CAMERA_ZOOM
|
||||
playerCamera.position_smoothing_enabled = true
|
||||
playerCamera.limit_left = CAMERA_LIMIT_LEFT
|
||||
playerCamera.limit_top = CAMERA_LIMIT_TOP
|
||||
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
|
||||
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
|
||||
playerCamera.limit_smoothed = true
|
||||
|
||||
func _build_decor_layer() -> void:
|
||||
_decorLayer = Node2D.new()
|
||||
_decorLayer.name = "RoomDecorLayer"
|
||||
_decorLayer.y_sort_enabled = true
|
||||
ySortWorld.add_child(_decorLayer)
|
||||
ySortWorld.move_child(_decorLayer, 0)
|
||||
|
||||
func _build_room_decor_requests() -> void:
|
||||
_inventoryRequest = HTTPRequest.new()
|
||||
_inventoryRequest.name = "roomDecorInventoryRequest"
|
||||
_inventoryRequest.timeout = 12.0
|
||||
_inventoryRequest.request_completed.connect(_on_inventory_request_completed)
|
||||
add_child(_inventoryRequest)
|
||||
|
||||
func _build_room_decor_ui() -> void:
|
||||
_inventoryPanel = PanelContainer.new()
|
||||
_inventoryPanel.visible = false
|
||||
_inventoryPanel.custom_minimum_size = Vector2(560, 560)
|
||||
_inventoryPanel.offset_left = 24
|
||||
_inventoryPanel.offset_top = 116
|
||||
_inventoryPanel.offset_right = 584
|
||||
_inventoryPanel.offset_bottom = 676
|
||||
_inventoryPanel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_inventoryPanel.add_theme_stylebox_override("panel", _create_panel_style(PANEL_COLOR, 26, true))
|
||||
uiLayer.add_child(_inventoryPanel)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 24)
|
||||
margin.add_theme_constant_override("margin_top", 22)
|
||||
margin.add_theme_constant_override("margin_right", 24)
|
||||
margin.add_theme_constant_override("margin_bottom", 22)
|
||||
_inventoryPanel.add_child(margin)
|
||||
|
||||
var root := VBoxContainer.new()
|
||||
root.add_theme_constant_override("separation", 14)
|
||||
margin.add_child(root)
|
||||
|
||||
var header := HBoxContainer.new()
|
||||
header.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
root.add_child(header)
|
||||
|
||||
var titleBox := VBoxContainer.new()
|
||||
titleBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
titleBox.add_theme_constant_override("separation", 2)
|
||||
header.add_child(titleBox)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "背包"
|
||||
title.add_theme_font_size_override("font_size", 28)
|
||||
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
titleBox.add_child(title)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.text = "按住装饰品,拖到房间里松手摆放。"
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
hint.add_theme_font_size_override("font_size", 16)
|
||||
hint.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
titleBox.add_child(hint)
|
||||
|
||||
var closeButton := Button.new()
|
||||
closeButton.text = "×"
|
||||
closeButton.custom_minimum_size = Vector2(48, 48)
|
||||
closeButton.focus_mode = Control.FOCUS_NONE
|
||||
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
closeButton.add_theme_font_size_override("font_size", 30)
|
||||
closeButton.add_theme_color_override("font_color", Color.WHITE)
|
||||
closeButton.add_theme_stylebox_override("normal", _create_panel_style(ACCENT_COLOR, 18, false))
|
||||
closeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.150, 0.680, 1.0, 1.0), 18, false))
|
||||
closeButton.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.060, 0.420, 0.780, 1.0), 18, false))
|
||||
closeButton.pressed.connect(func() -> void:
|
||||
_inventoryPanel.visible = false
|
||||
)
|
||||
header.add_child(closeButton)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(0, 370)
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
root.add_child(scroll)
|
||||
|
||||
_inventoryGrid = GridContainer.new()
|
||||
_inventoryGrid.columns = 2
|
||||
_inventoryGrid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_inventoryGrid.add_theme_constant_override("h_separation", 14)
|
||||
_inventoryGrid.add_theme_constant_override("v_separation", 14)
|
||||
scroll.add_child(_inventoryGrid)
|
||||
|
||||
var removeButton := Button.new()
|
||||
removeButton.text = "收回选中家具"
|
||||
removeButton.custom_minimum_size = Vector2(0, 46)
|
||||
removeButton.add_theme_font_size_override("font_size", 17)
|
||||
removeButton.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
removeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||||
removeButton.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0))
|
||||
removeButton.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
|
||||
removeButton.add_theme_stylebox_override("normal", _create_panel_style(Color(0.900, 0.960, 1.0, 0.92), 16, false))
|
||||
removeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.820, 0.925, 1.0, 1.0), 16, false))
|
||||
removeButton.pressed.connect(_remove_selected_decor)
|
||||
root.add_child(removeButton)
|
||||
|
||||
_toastLabel = Label.new()
|
||||
_toastLabel.visible = false
|
||||
_toastLabel.offset_left = 360
|
||||
_toastLabel.offset_top = 100
|
||||
_toastLabel.offset_right = 760
|
||||
_toastLabel.offset_bottom = 148
|
||||
_toastLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_toastLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_toastLabel.add_theme_font_size_override("font_size", 18)
|
||||
_toastLabel.add_theme_color_override("font_color", Color.WHITE)
|
||||
_toastLabel.add_theme_stylebox_override("normal", _create_panel_style(Color(0.028, 0.160, 0.310, 0.90), 18, false))
|
||||
uiLayer.add_child(_toastLabel)
|
||||
|
||||
_dragSurface = Control.new()
|
||||
_dragSurface.name = "RoomDecorDragSurface"
|
||||
_dragSurface.visible = false
|
||||
_dragSurface.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_dragSurface.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_dragSurface.gui_input.connect(_on_drag_surface_gui_input)
|
||||
uiLayer.add_child(_dragSurface)
|
||||
|
||||
func _connect_room_decor_events() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("PersonalSpace: EventSystem autoload is not available.")
|
||||
else:
|
||||
eventSystem.call("connect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager != null:
|
||||
if not saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
|
||||
saveManager.decor_save_succeeded.connect(_on_decor_save_succeeded)
|
||||
if not saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
|
||||
saveManager.decor_save_failed.connect(_on_decor_save_failed)
|
||||
|
||||
func _on_backpack_toggle_requested(_data: Variant = null) -> void:
|
||||
_toggle_inventory_panel()
|
||||
|
||||
func _toggle_inventory_panel() -> void:
|
||||
_inventoryPanel.visible = not _inventoryPanel.visible
|
||||
if _inventoryPanel.visible:
|
||||
_fetch_room_decor_inventory(true)
|
||||
|
||||
func _fetch_room_decor_inventory(reportErrors: bool = false) -> void:
|
||||
if _inventoryRequestInFlight:
|
||||
_inventoryRequestReportsErrors = _inventoryRequestReportsErrors or reportErrors
|
||||
return
|
||||
if not _is_authenticated():
|
||||
_apply_local_empty_inventory()
|
||||
if reportErrors:
|
||||
_show_toast("请先登录后使用家具背包")
|
||||
return
|
||||
_inventoryRequestInFlight = true
|
||||
_inventoryRequestReportsErrors = reportErrors
|
||||
var err := _inventoryRequest.request("%s/rooms/me/decor-placements" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_GET, "")
|
||||
if err != OK:
|
||||
_inventoryRequestInFlight = false
|
||||
if _inventoryRequestReportsErrors:
|
||||
_show_toast("家具背包请求发送失败")
|
||||
_inventoryRequestReportsErrors = false
|
||||
|
||||
func _on_inventory_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
_inventoryRequestInFlight = false
|
||||
var reportErrors := _inventoryRequestReportsErrors
|
||||
_inventoryRequestReportsErrors = false
|
||||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||||
if reportErrors:
|
||||
_show_toast("家具背包读取失败")
|
||||
return
|
||||
var json := JSON.new()
|
||||
if json.parse(body.get_string_from_utf8()) != OK:
|
||||
if reportErrors:
|
||||
_show_toast("家具背包响应解析失败")
|
||||
return
|
||||
var responseVariant: Variant = json.data
|
||||
if not (responseVariant is Dictionary):
|
||||
if reportErrors:
|
||||
_show_toast("家具背包响应格式错误")
|
||||
return
|
||||
var response: Dictionary = responseVariant
|
||||
if not bool(response.get("success", true)):
|
||||
if reportErrors:
|
||||
_show_toast(str(response.get("message", "家具背包读取失败")))
|
||||
return
|
||||
var dataVariant: Variant = response.get("data", {})
|
||||
if dataVariant is Dictionary:
|
||||
_apply_inventory_payload(dataVariant as Dictionary)
|
||||
|
||||
func _apply_inventory_payload(data: Dictionary) -> void:
|
||||
_decorItems.clear()
|
||||
_apply_remote_decor_definitions(data.get("definitions", []))
|
||||
var itemsVariant: Variant = data.get("items", [])
|
||||
if itemsVariant is Array:
|
||||
for itemVariant in itemsVariant:
|
||||
if itemVariant is Dictionary:
|
||||
var item: Dictionary = (itemVariant as Dictionary).duplicate(true)
|
||||
var decorId := str(item.get("decor_id", "")).strip_edges()
|
||||
if not decorId.is_empty():
|
||||
_decorItems[decorId] = _merge_decor_definition(item)
|
||||
_render_inventory_list()
|
||||
_render_placed_decors()
|
||||
|
||||
func _apply_local_empty_inventory() -> void:
|
||||
_decorItems.clear()
|
||||
_render_inventory_list()
|
||||
_render_placed_decors()
|
||||
|
||||
func _merge_decor_definition(item: Dictionary) -> Dictionary:
|
||||
var decorId := str(item.get("decor_id", "")).strip_edges()
|
||||
var definition: Dictionary = _remoteDecorDefinitions.get(decorId, {})
|
||||
for key in definition.keys():
|
||||
if not item.has(key):
|
||||
item[key] = definition[key]
|
||||
return item
|
||||
|
||||
func _apply_remote_decor_definitions(definitionsVariant: Variant) -> void:
|
||||
_remoteDecorDefinitions.clear()
|
||||
if not (definitionsVariant is Array):
|
||||
return
|
||||
for definitionVariant in definitionsVariant:
|
||||
if not (definitionVariant is Dictionary):
|
||||
continue
|
||||
var definition: Dictionary = (definitionVariant as Dictionary).duplicate(true)
|
||||
var decorId := str(definition.get("decor_id", "")).strip_edges()
|
||||
if decorId.is_empty():
|
||||
continue
|
||||
if definition.get("default_position", null) is Dictionary:
|
||||
var position: Dictionary = definition.get("default_position")
|
||||
definition["default_position"] = Vector2(
|
||||
_to_float(position.get("x", 0.0), 0.0),
|
||||
_to_float(position.get("y", 0.0), 0.0)
|
||||
)
|
||||
_remoteDecorDefinitions[decorId] = definition
|
||||
|
||||
func _render_inventory_list() -> void:
|
||||
_clear_children(_inventoryGrid)
|
||||
if _decorItems.is_empty():
|
||||
var empty := _create_empty_inventory_card()
|
||||
_inventoryGrid.add_child(empty)
|
||||
return
|
||||
for decorId in _decorItems.keys():
|
||||
_inventoryGrid.add_child(_create_inventory_card(str(decorId)))
|
||||
|
||||
func _create_empty_inventory_card() -> Control:
|
||||
var card := PanelContainer.new()
|
||||
card.custom_minimum_size = Vector2(500, 132)
|
||||
card.add_theme_stylebox_override("panel", _create_panel_style(Color(1, 1, 1, 0.76), 20, false))
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 18)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 18)
|
||||
card.add_child(margin)
|
||||
var label := Label.new()
|
||||
label.text = "背包里还没有装饰品。去商城的空间分类购买后会出现在这里。"
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", 17)
|
||||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
margin.add_child(label)
|
||||
return card
|
||||
|
||||
func _create_inventory_card(decorId: String) -> Button:
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
var card := Button.new()
|
||||
card.custom_minimum_size = Vector2(240, 176)
|
||||
card.focus_mode = Control.FOCUS_NONE
|
||||
card.mouse_default_cursor_shape = Control.CURSOR_DRAG
|
||||
card.add_theme_stylebox_override("normal", _create_inventory_card_style(false))
|
||||
card.add_theme_stylebox_override("hover", _create_inventory_card_style(true))
|
||||
card.add_theme_stylebox_override("pressed", _create_inventory_card_style(true))
|
||||
card.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
card.button_down.connect(func() -> void:
|
||||
_begin_inventory_decor_drag(decorId)
|
||||
)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 12)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_right", 12)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
card.add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
content.add_theme_constant_override("separation", 6)
|
||||
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
margin.add_child(content)
|
||||
|
||||
var status := Label.new()
|
||||
status.custom_minimum_size = Vector2(0, 22)
|
||||
status.text = "已摆放" if bool(item.get("placed", false)) else "未摆放"
|
||||
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
status.add_theme_font_size_override("font_size", 14)
|
||||
status.add_theme_color_override("font_color", ACCENT_COLOR if bool(item.get("placed", false)) else MUTED_COLOR)
|
||||
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(status)
|
||||
|
||||
var icon := TextureRect.new()
|
||||
icon.texture = _load_texture(str(item.get("icon", "")))
|
||||
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
icon.custom_minimum_size = Vector2(0, 82)
|
||||
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(icon)
|
||||
|
||||
var nameLabel := Label.new()
|
||||
nameLabel.text = str(item.get("name", decorId))
|
||||
nameLabel.custom_minimum_size = Vector2(0, 36)
|
||||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
nameLabel.add_theme_font_size_override("font_size", 17)
|
||||
nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(nameLabel)
|
||||
|
||||
return card
|
||||
|
||||
func _render_placed_decors() -> void:
|
||||
for decorIdVariant in _decorNodes.keys().duplicate():
|
||||
var decorId := str(decorIdVariant)
|
||||
var item: Dictionary = _decorItems.get(decorId, {})
|
||||
if item.is_empty() or not bool(item.get("placed", false)):
|
||||
var node := _decorNodes[decorId] as Node
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_decorNodes.erase(decorId)
|
||||
_remove_decor_collision(decorId)
|
||||
for decorId in _decorItems.keys():
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
if bool(item.get("placed", false)):
|
||||
_ensure_decor_node(str(decorId), item)
|
||||
|
||||
func _begin_inventory_decor_drag(decorId: String) -> void:
|
||||
if not _decorItems.has(decorId):
|
||||
return
|
||||
_selectedDecorId = decorId
|
||||
var item := _ensure_decor_placed_for_drag(decorId)
|
||||
var node := _ensure_decor_node(decorId, item)
|
||||
if node == null:
|
||||
return
|
||||
_draggedDecor = node
|
||||
_dragOffset = Vector2.ZERO
|
||||
_set_decor_collision_disabled(decorId, true)
|
||||
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
|
||||
_inventoryPanel.visible = false
|
||||
_enable_drag_surface()
|
||||
_update_dragged_decor_position()
|
||||
_pulse_node(_draggedDecor)
|
||||
|
||||
func _ensure_decor_placed_for_drag(decorId: String) -> Dictionary:
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
if not bool(item.get("placed", false)):
|
||||
item["placed"] = true
|
||||
var defaultPosition := _variant_to_vector2(item.get("default_position", Vector2.ZERO))
|
||||
item["position_x"] = defaultPosition.x
|
||||
item["position_y"] = defaultPosition.y
|
||||
item["scale"] = _get_item_float(item, "default_scale", 1.0)
|
||||
item["z_index"] = _get_item_int(item, "default_z_index", 0)
|
||||
_decorItems[decorId] = item
|
||||
return item
|
||||
|
||||
func _ensure_decor_node(decorId: String, item: Dictionary) -> Sprite2D:
|
||||
var node := _decorNodes.get(decorId, null) as Sprite2D
|
||||
if node == null or not is_instance_valid(node):
|
||||
node = Sprite2D.new()
|
||||
node.name = "Decor_%s" % decorId
|
||||
node.texture_filter = DECOR_TEXTURE_FILTER
|
||||
node.centered = true
|
||||
node.set_meta("decor_id", decorId)
|
||||
_decorLayer.add_child(node)
|
||||
_decorNodes[decorId] = node
|
||||
else:
|
||||
node.texture_filter = DECOR_TEXTURE_FILTER
|
||||
node.texture = _load_texture(str(item.get("texture", item.get("icon", ""))))
|
||||
node.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0))
|
||||
var itemScale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
|
||||
node.scale = Vector2(itemScale, itemScale)
|
||||
node.z_index = _get_item_int(item, "z_index", _get_item_int(item, "default_z_index", 0))
|
||||
_sync_decor_collision(decorId, item)
|
||||
return node
|
||||
|
||||
func _try_begin_decor_drag(worldPosition: Vector2) -> bool:
|
||||
var bestNode: Sprite2D = null
|
||||
var bestZ := -100000
|
||||
for decorId in _decorNodes.keys():
|
||||
var node := _decorNodes[decorId] as Sprite2D
|
||||
if node == null or not is_instance_valid(node) or node.texture == null:
|
||||
continue
|
||||
if not _is_point_inside_sprite(node, worldPosition):
|
||||
continue
|
||||
if node.z_index >= bestZ:
|
||||
bestNode = node
|
||||
bestZ = node.z_index
|
||||
if bestNode == null:
|
||||
return false
|
||||
_draggedDecor = bestNode
|
||||
_selectedDecorId = str(bestNode.get_meta("decor_id", ""))
|
||||
_dragOffset = bestNode.global_position - worldPosition
|
||||
_set_decor_collision_disabled(_selectedDecorId, true)
|
||||
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
|
||||
_enable_drag_surface()
|
||||
return true
|
||||
|
||||
func _finish_decor_drag() -> void:
|
||||
if _draggedDecor == null:
|
||||
_disable_drag_surface()
|
||||
return
|
||||
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
|
||||
if _decorItems.has(decorId):
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
item["position_x"] = _draggedDecor.global_position.x
|
||||
item["position_y"] = _draggedDecor.global_position.y
|
||||
item["placed"] = true
|
||||
item["scale"] = _draggedDecor.scale.x
|
||||
item["z_index"] = _get_item_int(item, "default_z_index", 0)
|
||||
_decorItems[decorId] = item
|
||||
_draggedDecor.z_index = _get_item_int(item, "z_index", 0)
|
||||
_sync_decor_collision(decorId, item)
|
||||
_set_decor_collision_disabled(decorId, false)
|
||||
_save_decor_item(item)
|
||||
_render_inventory_list()
|
||||
_draggedDecor = null
|
||||
_disable_drag_surface()
|
||||
|
||||
func _update_dragged_decor_position() -> void:
|
||||
if _draggedDecor != null:
|
||||
_draggedDecor.global_position = get_global_mouse_position() + _dragOffset
|
||||
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
|
||||
if not decorId.is_empty() and _decorItems.has(decorId):
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
item["position_x"] = _draggedDecor.global_position.x
|
||||
item["position_y"] = _draggedDecor.global_position.y
|
||||
_sync_decor_collision(decorId, item)
|
||||
|
||||
func _enable_drag_surface() -> void:
|
||||
if is_instance_valid(_dragSurface):
|
||||
_dragSurface.visible = true
|
||||
uiLayer.move_child(_dragSurface, uiLayer.get_child_count() - 1)
|
||||
set_process(true)
|
||||
|
||||
func _disable_drag_surface() -> void:
|
||||
if is_instance_valid(_dragSurface):
|
||||
_dragSurface.visible = false
|
||||
set_process(false)
|
||||
|
||||
func _on_drag_surface_gui_input(event: InputEvent) -> void:
|
||||
if _draggedDecor == null:
|
||||
_disable_drag_surface()
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
_update_dragged_decor_position()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index == MOUSE_BUTTON_LEFT and not mouseEvent.pressed:
|
||||
_finish_decor_drag()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _is_pointer_over_blocking_ui() -> bool:
|
||||
var mousePosition := get_viewport().get_mouse_position()
|
||||
for child in uiLayer.get_children():
|
||||
if child == _dragSurface:
|
||||
continue
|
||||
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _control_blocks_pointer(control: Control, mousePosition: Vector2) -> bool:
|
||||
if not control.visible or not control.is_visible_in_tree():
|
||||
return false
|
||||
if control.mouse_filter != Control.MOUSE_FILTER_IGNORE and control.get_global_rect().has_point(mousePosition):
|
||||
return true
|
||||
for child in control.get_children():
|
||||
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _remove_selected_decor() -> void:
|
||||
if _selectedDecorId.is_empty() or not _decorItems.has(_selectedDecorId):
|
||||
_show_toast("请先选择一个家具")
|
||||
return
|
||||
var item: Dictionary = _decorItems[_selectedDecorId]
|
||||
item["placed"] = false
|
||||
_decorItems[_selectedDecorId] = item
|
||||
if _decorNodes.has(_selectedDecorId):
|
||||
var node := _decorNodes[_selectedDecorId] as Node
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_decorNodes.erase(_selectedDecorId)
|
||||
_remove_decor_collision(_selectedDecorId)
|
||||
_save_decor_item(item)
|
||||
_render_inventory_list()
|
||||
_show_toast("已收回家具")
|
||||
|
||||
func _save_decor_item(item: Dictionary) -> void:
|
||||
if not _is_authenticated():
|
||||
_show_toast("请先登录后保存摆放")
|
||||
return
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager == null or not saveManager.has_method("enqueue_save"):
|
||||
_show_toast("家具保存服务不可用")
|
||||
return
|
||||
saveManager.call("enqueue_save", item)
|
||||
|
||||
func _on_decor_save_succeeded(savedItem: Dictionary) -> void:
|
||||
var item := _merge_decor_definition(savedItem.duplicate(true))
|
||||
var decorId := str(item.get("decor_id", ""))
|
||||
if not decorId.is_empty():
|
||||
_decorItems[decorId] = item
|
||||
|
||||
func _on_decor_save_failed(_item: Dictionary, message: String) -> void:
|
||||
_show_toast(message)
|
||||
|
||||
func _is_point_inside_sprite(node: Sprite2D, worldPosition: Vector2) -> bool:
|
||||
if node.texture == null:
|
||||
return false
|
||||
var localPoint := node.to_local(worldPosition)
|
||||
var size := node.texture.get_size()
|
||||
var rect := Rect2(-size * 0.5, size)
|
||||
return rect.has_point(localPoint)
|
||||
|
||||
func _sync_decor_collision(decorId: String, item: Dictionary) -> void:
|
||||
var collisionSize := _variant_to_vector2(item.get("collision_size", Vector2.ZERO))
|
||||
if collisionSize == Vector2.ZERO:
|
||||
_remove_decor_collision(decorId)
|
||||
return
|
||||
var collisionOffset := _variant_to_vector2(item.get("collision_offset", Vector2.ZERO))
|
||||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||||
var shapeNode: CollisionShape2D
|
||||
if body == null or not is_instance_valid(body):
|
||||
body = StaticBody2D.new()
|
||||
body.name = "DecorCollision_%s" % decorId
|
||||
body.collision_layer = DECOR_COLLISION_LAYER
|
||||
body.collision_mask = DECOR_COLLISION_MASK
|
||||
shapeNode = CollisionShape2D.new()
|
||||
shapeNode.name = "CollisionShape2D"
|
||||
var rectShape := RectangleShape2D.new()
|
||||
shapeNode.shape = rectShape
|
||||
body.add_child(shapeNode)
|
||||
staticCollision.add_child(body)
|
||||
_decorCollisionBodies[decorId] = body
|
||||
else:
|
||||
shapeNode = body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode == null:
|
||||
shapeNode = CollisionShape2D.new()
|
||||
shapeNode.name = "CollisionShape2D"
|
||||
shapeNode.shape = RectangleShape2D.new()
|
||||
body.add_child(shapeNode)
|
||||
var scale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
|
||||
body.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0)) + collisionOffset * scale
|
||||
shapeNode.position = Vector2.ZERO
|
||||
var rectShape := shapeNode.shape as RectangleShape2D
|
||||
if rectShape == null:
|
||||
rectShape = RectangleShape2D.new()
|
||||
shapeNode.shape = rectShape
|
||||
rectShape.size = Vector2(max(8.0, collisionSize.x * scale), max(8.0, collisionSize.y * scale))
|
||||
|
||||
func _set_decor_collision_disabled(decorId: String, disabled: bool) -> void:
|
||||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||||
if body == null or not is_instance_valid(body):
|
||||
return
|
||||
var shapeNode := body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode != null:
|
||||
shapeNode.disabled = disabled
|
||||
|
||||
func _remove_decor_collision(decorId: String) -> void:
|
||||
var body := _decorCollisionBodies.get(decorId, null) as Node
|
||||
if body != null and is_instance_valid(body):
|
||||
body.queue_free()
|
||||
_decorCollisionBodies.erase(decorId)
|
||||
|
||||
func _variant_to_vector2(value: Variant) -> Vector2:
|
||||
if value is Vector2:
|
||||
return value
|
||||
if value is Dictionary:
|
||||
var dict: Dictionary = value
|
||||
return Vector2(_to_float(dict.get("x", 0.0), 0.0), _to_float(dict.get("y", 0.0), 0.0))
|
||||
return Vector2.ZERO
|
||||
|
||||
func _to_float(value: Variant, fallback: float) -> float:
|
||||
match typeof(value):
|
||||
TYPE_FLOAT:
|
||||
return value
|
||||
TYPE_INT:
|
||||
return value
|
||||
TYPE_STRING:
|
||||
var text := str(value).strip_edges()
|
||||
return fallback if text.is_empty() else text.to_float()
|
||||
TYPE_BOOL:
|
||||
return 1.0 if bool(value) else 0.0
|
||||
_:
|
||||
return fallback
|
||||
|
||||
func _to_int(value: Variant, fallback: int) -> int:
|
||||
match typeof(value):
|
||||
TYPE_INT:
|
||||
return value
|
||||
TYPE_FLOAT:
|
||||
return roundi(value)
|
||||
TYPE_STRING:
|
||||
var text := str(value).strip_edges()
|
||||
return fallback if text.is_empty() else text.to_int()
|
||||
TYPE_BOOL:
|
||||
return 1 if bool(value) else 0
|
||||
_:
|
||||
return fallback
|
||||
|
||||
func _get_item_float(item: Dictionary, key: String, fallback: float) -> float:
|
||||
return _to_float(item.get(key, fallback), fallback)
|
||||
|
||||
func _get_item_int(item: Dictionary, key: String, fallback: int) -> int:
|
||||
return _to_int(item.get(key, fallback), fallback)
|
||||
|
||||
func _is_authenticated() -> bool:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated"))
|
||||
|
||||
func _auth_headers() -> PackedStringArray:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else ""
|
||||
return PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer %s" % accessToken,
|
||||
])
|
||||
|
||||
func _load_texture(path: String) -> Texture2D:
|
||||
if path.is_empty():
|
||||
return null
|
||||
if FileAccess.file_exists("%s.import" % path):
|
||||
var texture := load(path) as Texture2D
|
||||
if texture != null:
|
||||
return texture
|
||||
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
|
||||
if image == null or image.is_empty():
|
||||
return null
|
||||
return ImageTexture.create_from_image(image)
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
|
||||
func _show_toast(message: String) -> void:
|
||||
if not is_instance_valid(_toastLabel):
|
||||
return
|
||||
_toastLabel.text = message
|
||||
_toastLabel.visible = true
|
||||
var tween := create_tween()
|
||||
tween.tween_interval(1.5)
|
||||
tween.tween_callback(func() -> void:
|
||||
if is_instance_valid(_toastLabel):
|
||||
_toastLabel.visible = false
|
||||
)
|
||||
|
||||
func _pulse_node(node: Node2D) -> void:
|
||||
var originalScale := node.scale
|
||||
var tween := create_tween()
|
||||
tween.tween_property(node, "scale", originalScale * 1.08, 0.08)
|
||||
tween.tween_property(node, "scale", originalScale, 0.10)
|
||||
|
||||
func _clear_children(node: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func _create_panel_style(color: Color, radius: int, shadow: bool) -> 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
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_bottom = 8
|
||||
if shadow:
|
||||
style.shadow_color = Color(0.082, 0.243, 0.380, 0.18)
|
||||
style.shadow_size = 12
|
||||
style.shadow_offset = Vector2(0, 4)
|
||||
return style
|
||||
|
||||
func _create_inventory_card_style(hovered: bool) -> StyleBoxFlat:
|
||||
var style := _create_panel_style(Color(0.982, 0.995, 1.0, 1.0), 18, true)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.650, 0.835, 0.965, 0.90) if not hovered else Color(0.086, 0.690, 0.960, 1.0)
|
||||
style.shadow_color = Color(0.086, 0.314, 0.520, 0.14 if hovered else 0.08)
|
||||
style.shadow_size = 14 if hovered else 8
|
||||
style.content_margin_left = 0
|
||||
style.content_margin_top = 0
|
||||
style.content_margin_right = 0
|
||||
style.content_margin_bottom = 0
|
||||
return style
|
||||
1
scenes/Maps/PersonalSpace.gd.uid
Normal file
1
scenes/Maps/PersonalSpace.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c1ndorvawxclg
|
||||
32
scenes/Maps/ScenePortal.gd
Normal file
32
scenes/Maps/ScenePortal.gd
Normal file
@@ -0,0 +1,32 @@
|
||||
class_name ScenePortal
|
||||
extends Area2D
|
||||
|
||||
# ============================================================================
|
||||
# ScenePortal.gd - 场景入口传送组件
|
||||
# ============================================================================
|
||||
# 提供轻量级场景跳转能力,用于广场、小区和个人空间之间的入口。
|
||||
# ============================================================================
|
||||
|
||||
@export var targetSceneName: String = ""
|
||||
@export var targetSpawnName: String = ""
|
||||
@export var targetPosition: Vector2 = Vector2.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
func _on_body_entered(body: Node2D) -> void:
|
||||
if not (body is PlayerController):
|
||||
return
|
||||
_change_scene()
|
||||
|
||||
func _change_scene() -> void:
|
||||
if targetSceneName.is_empty():
|
||||
push_warning("ScenePortal: targetSceneName is empty.")
|
||||
return
|
||||
|
||||
if not targetSpawnName.is_empty():
|
||||
SceneManager.set_next_spawn_name(targetSpawnName)
|
||||
elif targetPosition != Vector2.ZERO:
|
||||
SceneManager.set_next_scene_position(targetPosition)
|
||||
|
||||
SceneManager.change_scene(targetSceneName)
|
||||
1
scenes/Maps/ScenePortal.gd.uid
Normal file
1
scenes/Maps/ScenePortal.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dr6uk7m4fsr4l
|
||||
42
scenes/Maps/Square.gd
Normal file
42
scenes/Maps/Square.gd
Normal file
@@ -0,0 +1,42 @@
|
||||
class_name Square
|
||||
extends Node2D
|
||||
|
||||
# ============================================================================
|
||||
# Square.gd - 广场场景控制器
|
||||
# ============================================================================
|
||||
# 负责广场玩家出生点和玩家相机边界配置,避免运行时看到地图外区域。
|
||||
# ============================================================================
|
||||
|
||||
const CAMERA_ZOOM: Vector2 = Vector2(2.0, 2.0)
|
||||
const CAMERA_LIMIT_LEFT: int = -1280
|
||||
const CAMERA_LIMIT_TOP: int = -960
|
||||
const CAMERA_LIMIT_RIGHT: int = 1280
|
||||
const CAMERA_LIMIT_BOTTOM: int = 960
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@onready var overviewCamera: Camera2D = $Camera2D
|
||||
|
||||
func _ready() -> void:
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
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
|
||||
if marker == null:
|
||||
marker = $Markers/DefaultSpawn
|
||||
player.global_position = marker.global_position
|
||||
|
||||
func _configure_camera() -> void:
|
||||
overviewCamera.enabled = false
|
||||
playerCamera.enabled = true
|
||||
playerCamera.make_current()
|
||||
playerCamera.zoom = CAMERA_ZOOM
|
||||
playerCamera.position_smoothing_enabled = true
|
||||
playerCamera.limit_left = CAMERA_LIMIT_LEFT
|
||||
playerCamera.limit_top = CAMERA_LIMIT_TOP
|
||||
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
|
||||
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
|
||||
playerCamera.limit_smoothed = true
|
||||
1
scenes/Maps/Square.gd.uid
Normal file
1
scenes/Maps/Square.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://biu0igl6133q5
|
||||
176
scenes/Maps/WorkZone.gd
Normal file
176
scenes/Maps/WorkZone.gd
Normal file
@@ -0,0 +1,176 @@
|
||||
class_name WorkZone
|
||||
extends Node2D
|
||||
|
||||
# ============================================================================
|
||||
# WorkZone.gd - 打工区场景控制器
|
||||
# ============================================================================
|
||||
# 承载赛博打工区概念地图,负责玩家出生点和相机边界配置。
|
||||
# ============================================================================
|
||||
|
||||
const CAMERA_ZOOM: Vector2 = Vector2(2.0, 2.0)
|
||||
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 MALL_PANEL_SCENE: PackedScene = preload("res://scenes/ui/mall/MallPanel.tscn")
|
||||
const COURSE_BOARD_PANEL_SCENE: PackedScene = preload("res://scenes/ui/CourseBoardPanel.tscn")
|
||||
const MALL_ENTRANCE_POSITION: Vector2 = Vector2(-4, -418)
|
||||
const MALL_ENTRANCE_SIZE: Vector2 = Vector2(112, 34)
|
||||
const MALL_EXIT_POSITION: Vector2 = Vector2(-4, -310)
|
||||
const MALL_EXIT_REOPEN_COOLDOWN: float = 0.45
|
||||
const PLAYER_COLLISION_LAYER: int = 1
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@onready var uiLayer: CanvasLayer = $UILayer
|
||||
|
||||
var _mallPanel: Control
|
||||
var _courseBoardPanel: Control
|
||||
var _mallEntranceArea: Area2D
|
||||
var _isInsideMall: bool = false
|
||||
var _mallCanEnter: bool = true
|
||||
|
||||
func _ready() -> void:
|
||||
_apply_spawn_point()
|
||||
_configure_camera()
|
||||
_ensure_mall_panel()
|
||||
_ensure_course_board_panel()
|
||||
_ensure_mall_entrance_area()
|
||||
EventSystem.connect_event(EventNames.OBJECT_INTERACTED, _on_object_interacted, self)
|
||||
EventSystem.connect_event(EventNames.MALL_CLOSED, _on_mall_closed, self)
|
||||
|
||||
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:
|
||||
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
|
||||
if marker == null:
|
||||
marker = $Markers/DefaultSpawn
|
||||
player.global_position = marker.global_position
|
||||
|
||||
func _configure_camera() -> void:
|
||||
playerCamera.zoom = CAMERA_ZOOM
|
||||
playerCamera.position_smoothing_enabled = true
|
||||
playerCamera.limit_left = CAMERA_LIMIT_LEFT
|
||||
playerCamera.limit_top = CAMERA_LIMIT_TOP
|
||||
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
|
||||
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
|
||||
playerCamera.limit_smoothed = true
|
||||
|
||||
func _ensure_mall_panel() -> void:
|
||||
var existingPanel := uiLayer.get_node_or_null("MallPanel") as Control
|
||||
if existingPanel != null:
|
||||
_mallPanel = existingPanel
|
||||
return
|
||||
_mallPanel = MALL_PANEL_SCENE.instantiate() as Control
|
||||
_mallPanel.name = "MallPanel"
|
||||
uiLayer.add_child(_mallPanel)
|
||||
|
||||
func _ensure_course_board_panel() -> void:
|
||||
var existingPanel := uiLayer.get_node_or_null("CourseBoardPanel") as Control
|
||||
if existingPanel != null:
|
||||
_courseBoardPanel = existingPanel
|
||||
return
|
||||
_courseBoardPanel = COURSE_BOARD_PANEL_SCENE.instantiate() as Control
|
||||
_courseBoardPanel.name = "CourseBoardPanel"
|
||||
uiLayer.add_child(_courseBoardPanel)
|
||||
|
||||
func _ensure_mall_entrance_area() -> void:
|
||||
var areas := get_node_or_null("InteractionAreas") as Node2D
|
||||
if areas == null:
|
||||
areas = Node2D.new()
|
||||
areas.name = "InteractionAreas"
|
||||
add_child(areas)
|
||||
|
||||
var entrance := areas.get_node_or_null("MallEntranceArea") as WorkZoneBuildingArea
|
||||
if entrance == null:
|
||||
entrance = WorkZoneBuildingArea.new()
|
||||
entrance.name = "MallEntranceArea"
|
||||
areas.add_child(entrance)
|
||||
|
||||
entrance.buildingId = "whale_super_mall"
|
||||
entrance.buildingTitle = "鲸鱼商城"
|
||||
entrance.buildingRole = "mall"
|
||||
entrance.position = MALL_ENTRANCE_POSITION
|
||||
|
||||
var shape := entrance.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shape == null:
|
||||
shape = CollisionShape2D.new()
|
||||
shape.name = "CollisionShape2D"
|
||||
entrance.add_child(shape)
|
||||
|
||||
var rectangle := shape.shape as RectangleShape2D
|
||||
if rectangle == null:
|
||||
rectangle = RectangleShape2D.new()
|
||||
shape.shape = rectangle
|
||||
shape.position = Vector2.ZERO
|
||||
rectangle.size = MALL_ENTRANCE_SIZE
|
||||
_setup_mall_entrance_trigger(entrance)
|
||||
|
||||
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)
|
||||
|
||||
func _on_object_interacted(data: Dictionary) -> void:
|
||||
var buildingId := str(data.get("buildingId", ""))
|
||||
if buildingId.is_empty():
|
||||
return
|
||||
if buildingId == "whale_super_mall":
|
||||
return
|
||||
if buildingId == "virtual_whale_recruitment_board":
|
||||
_open_course_board()
|
||||
return
|
||||
if not [
|
||||
"whale_job_center",
|
||||
"ai_service_station",
|
||||
"whale_coin_exchange",
|
||||
"virtual_whale_recruitment_board",
|
||||
].has(buildingId):
|
||||
return
|
||||
|
||||
var title := str(data.get("title", buildingId))
|
||||
var role := str(data.get("role", ""))
|
||||
print("WorkZone interaction ready: %s (%s)" % [title, role])
|
||||
|
||||
func _open_course_board() -> void:
|
||||
if is_instance_valid(_courseBoardPanel) and _courseBoardPanel.has_method("show_panel"):
|
||||
_courseBoardPanel.call("show_panel")
|
||||
|
||||
func _on_mall_entrance_body_entered(body: Node2D) -> void:
|
||||
if body != player or not _mallCanEnter:
|
||||
return
|
||||
_enter_mall()
|
||||
|
||||
func _enter_mall() -> void:
|
||||
if _isInsideMall:
|
||||
return
|
||||
_isInsideMall = true
|
||||
_set_player_mall_transition_state(true)
|
||||
if is_instance_valid(_mallPanel) and _mallPanel.has_method("show_panel"):
|
||||
_mallPanel.call("show_panel")
|
||||
|
||||
func _on_mall_closed(_data: Dictionary = {}) -> void:
|
||||
if not _isInsideMall:
|
||||
return
|
||||
_isInsideMall = false
|
||||
_mallCanEnter = false
|
||||
player.global_position = MALL_EXIT_POSITION
|
||||
if player.has_method("_reset_movement_input_state"):
|
||||
player.call("_reset_movement_input_state")
|
||||
_set_player_mall_transition_state(false)
|
||||
await get_tree().create_timer(MALL_EXIT_REOPEN_COOLDOWN).timeout
|
||||
_mallCanEnter = true
|
||||
|
||||
func _set_player_mall_transition_state(isInsideMall: bool) -> void:
|
||||
if not is_instance_valid(player):
|
||||
return
|
||||
player.visible = not isInsideMall
|
||||
player.set_physics_process(not isInsideMall)
|
||||
player.velocity = Vector2.ZERO
|
||||
if player.has_method("_reset_movement_input_state"):
|
||||
player.call("_reset_movement_input_state")
|
||||
1
scenes/Maps/WorkZone.gd.uid
Normal file
1
scenes/Maps/WorkZone.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://6de5gvosic2s
|
||||
30
scenes/Maps/WorkZoneBuildingArea.gd
Normal file
30
scenes/Maps/WorkZoneBuildingArea.gd
Normal file
@@ -0,0 +1,30 @@
|
||||
class_name WorkZoneBuildingArea
|
||||
extends Area2D
|
||||
|
||||
# ============================================================================
|
||||
# WorkZoneBuildingArea.gd - 打工区建筑交互入口
|
||||
# ============================================================================
|
||||
# 为打工区建筑提供统一交互数据,玩家面对入口按交互键时由 PlayerController 调用。
|
||||
# ============================================================================
|
||||
|
||||
const INTERACTION_COLLISION_LAYER: int = 2
|
||||
|
||||
@export var buildingId: String = ""
|
||||
@export var buildingTitle: String = ""
|
||||
@export var buildingRole: String = ""
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = INTERACTION_COLLISION_LAYER
|
||||
collision_mask = 0
|
||||
|
||||
func interact() -> void:
|
||||
var payload := {
|
||||
"buildingId": buildingId,
|
||||
"title": buildingTitle,
|
||||
"role": buildingRole,
|
||||
"nodePath": get_path(),
|
||||
}
|
||||
var eventSystem := get_node_or_null("/root/EventSystem")
|
||||
if eventSystem != null:
|
||||
eventSystem.call("emit_event", EventNames.OBJECT_INTERACTED, payload)
|
||||
print("WorkZone building interacted: %s - %s" % [buildingTitle, buildingRole])
|
||||
1
scenes/Maps/WorkZoneBuildingArea.gd.uid
Normal file
1
scenes/Maps/WorkZoneBuildingArea.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6catye0hyjwp
|
||||
360
scenes/Maps/cafe_interior.tscn
Normal file
360
scenes/Maps/cafe_interior.tscn
Normal file
@@ -0,0 +1,360 @@
|
||||
[gd_scene format=3 uid="uid://c2flhkqk5icab"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ccesagjisqodl" path="res://scenes/Maps/CafeInterior.gd" id="1_script"]
|
||||
[ext_resource type="Texture2D" path="res://output/novamailio-imagegen/cafe_interior_scene/whale_cafe_large_service_hall_base_v4.png" id="2_map"]
|
||||
[ext_resource type="PackedScene" uid="uid://b2f8e24plwqgj" path="res://scenes/characters/player.tscn" id="3_player"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/ChatUI.tscn" id="4_chatui"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/PlayerHud.tscn" id="5_playerhud"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/FriendListPanel.tscn" id="6_friendpanel"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/SettingsPanel.tscn" id="7_settings"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/characters/cafe_whale_barista_npc.tscn" id="8_barista_npc"]
|
||||
[ext_resource type="Script" uid="uid://d1sfqui0pqalf" path="res://scenes/characters/CafeCompanionTarget.gd" id="9_cafe_companion_target"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/CafeCompanionPanel.tscn" id="10_cafe_companion_panel"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/CafeCompanionRecruitmentPanel.tscn" id="11_cafe_recruitment_panel"]
|
||||
[ext_resource type="Script" uid="uid://dxupavbgcw3tw" path="res://scenes/Maps/MapMultiplayerController.gd" id="12_multiplayer"]
|
||||
[ext_resource type="Theme" uid="uid://brk6ca2npglqc" path="res://assets/ui/world_text_theme.tres" id="13_world_text_theme"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/MapPanel.tscn" id="14_mappanel"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="Style_CafeCompanionNameplate"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.988, 0.955, 0.96)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.18, 0.34, 0.38, 0.92)
|
||||
corner_radius_top_left = 12
|
||||
corner_radius_top_right = 12
|
||||
corner_radius_bottom_right = 12
|
||||
corner_radius_bottom_left = 12
|
||||
shadow_color = Color(0.05, 0.08, 0.09, 0.18)
|
||||
shadow_size = 4
|
||||
shadow_offset = Vector2(0, 2)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_BaristaChatTarget"]
|
||||
size = Vector2(112, 148)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_TopWall"]
|
||||
size = Vector2(1580, 72)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_BottomWall"]
|
||||
size = Vector2(1572, 72)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_LeftWall"]
|
||||
size = Vector2(80, 1100)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_RightWall"]
|
||||
size = Vector2(47, 721)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_FrontCounterBand"]
|
||||
size = Vector2(1063, 64)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_BackEquipmentBand"]
|
||||
size = Vector2(1300, 92)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_LeftServiceFixture"]
|
||||
size = Vector2(208, 309)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_RightServiceFixture"]
|
||||
size = Vector2(202, 329)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_ExitArea"]
|
||||
size = Vector2(420, 96)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_CafeRecruitmentLogo"]
|
||||
size = Vector2(135, 63)
|
||||
|
||||
[node name="CafeInterior" type="Node2D" unique_id=6364941]
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="UILayer" type="CanvasLayer" parent="." unique_id=915800518]
|
||||
layer = 10
|
||||
|
||||
[node name="ChatUI" parent="UILayer" unique_id=1173101039 instance=ExtResource("4_chatui")]
|
||||
|
||||
[node name="PlayerHud" parent="UILayer" unique_id=1564092911 instance=ExtResource("5_playerhud")]
|
||||
|
||||
[node name="MapPanel" parent="UILayer" instance=ExtResource("14_mappanel")]
|
||||
|
||||
[node name="FriendListPanel" parent="UILayer" unique_id=916180939 instance=ExtResource("6_friendpanel")]
|
||||
|
||||
[node name="SettingsPanel" parent="UILayer" unique_id=783599923 instance=ExtResource("7_settings")]
|
||||
|
||||
[node name="CafeCompanionPanel" parent="UILayer" unique_id=365240236 instance=ExtResource("10_cafe_companion_panel")]
|
||||
|
||||
[node name="CafeCompanionRecruitmentPanel" parent="UILayer" unique_id=1274369176 instance=ExtResource("11_cafe_recruitment_panel")]
|
||||
|
||||
[node name="StageBackdrop" type="ColorRect" parent="." unique_id=120901466]
|
||||
z_index = -200
|
||||
offset_left = -2000.0
|
||||
offset_top = -1600.0
|
||||
offset_right = 2000.0
|
||||
offset_bottom = 1600.0
|
||||
mouse_filter = 2
|
||||
color = Color(0.065, 0.075, 0.09, 1)
|
||||
|
||||
[node name="CafeServiceHallBase" type="Sprite2D" parent="." unique_id=106640887]
|
||||
z_index = -100
|
||||
texture_filter = 1
|
||||
texture = ExtResource("2_map")
|
||||
|
||||
[node name="YSortWorld" type="Node2D" parent="." unique_id=1472764725]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Characters" type="Node2D" parent="YSortWorld" unique_id=2141328072]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Players" type="Node2D" parent="YSortWorld/Characters" unique_id=1853391519]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Player" parent="YSortWorld/Characters/Players" unique_id=616484753 instance=ExtResource("3_player")]
|
||||
position = Vector2(0, 325)
|
||||
|
||||
[node name="CafeHiredCompanionNameplate" type="Label" parent="YSortWorld/Characters/Players/Player" unique_id=1594219674]
|
||||
visible = false
|
||||
z_index = 30
|
||||
custom_minimum_size = Vector2(188, 44)
|
||||
offset_left = -47.0
|
||||
offset_top = -96.0
|
||||
offset_right = -47.0
|
||||
offset_bottom = -96.0
|
||||
scale = Vector2(0.5, 0.5)
|
||||
theme = ExtResource("13_world_text_theme")
|
||||
theme_override_colors/font_color = Color(0.12, 0.2, 0.24, 1)
|
||||
theme_override_colors/font_shadow_color = Color(1, 1, 1, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 0
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 24
|
||||
theme_override_styles/normal = SubResource("Style_CafeCompanionNameplate")
|
||||
text = "陪伴机器人"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CafeHiredCompanionTarget" type="Area2D" parent="YSortWorld/Characters/Players/Player" unique_id=1604517960]
|
||||
position = Vector2(0, -58)
|
||||
collision_layer = 2
|
||||
collision_mask = 0
|
||||
script = ExtResource("9_cafe_companion_target")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="YSortWorld/Characters/Players/Player/CafeHiredCompanionTarget" unique_id=658877351]
|
||||
shape = SubResource("Shape_BaristaChatTarget")
|
||||
disabled = true
|
||||
|
||||
[node name="RemotePlayers" type="Node2D" parent="YSortWorld/Characters/Players" unique_id=2071839484]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="CafeWhaleBaristaNpc" parent="YSortWorld/Characters" unique_id=1188569491 instance=ExtResource("8_barista_npc")]
|
||||
position = Vector2(-472, -291)
|
||||
|
||||
[node name="CafeCompanionNameplate" type="Label" parent="YSortWorld/Characters/CafeWhaleBaristaNpc" unique_id=885952340]
|
||||
z_index = 30
|
||||
custom_minimum_size = Vector2(152, 44)
|
||||
offset_left = -38.0
|
||||
offset_top = -136.0
|
||||
offset_right = -38.0
|
||||
offset_bottom = -136.0
|
||||
scale = Vector2(0.5, 0.5)
|
||||
theme = ExtResource("13_world_text_theme")
|
||||
theme_override_colors/font_color = Color(0.12, 0.2, 0.25, 1)
|
||||
theme_override_colors/font_shadow_color = Color(1, 1, 1, 0.85)
|
||||
theme_override_constants/shadow_offset_x = 0
|
||||
theme_override_constants/shadow_offset_y = 1
|
||||
theme_override_font_sizes/font_size = 24
|
||||
theme_override_styles/normal = SubResource("Style_CafeCompanionNameplate")
|
||||
text = "海盐拿铁"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CafeCompanionTarget" type="Area2D" parent="YSortWorld/Characters/CafeWhaleBaristaNpc" unique_id=1439390611]
|
||||
position = Vector2(0, -58)
|
||||
script = ExtResource("9_cafe_companion_target")
|
||||
servicePointId = "ServiceIdlePoint01"
|
||||
companionId = "cafe_companion_npc"
|
||||
personaName = "海盐拿铁"
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="YSortWorld/Characters/CafeWhaleBaristaNpc/CafeCompanionTarget" unique_id=292882204]
|
||||
shape = SubResource("Shape_BaristaChatTarget")
|
||||
|
||||
[node name="MultiplayerController" type="Node" parent="YSortWorld" unique_id=1171758081]
|
||||
script = ExtResource("12_multiplayer")
|
||||
map_id = "whale_cafe"
|
||||
|
||||
[node name="StaticCollision" type="Node2D" parent="." unique_id=329016366]
|
||||
|
||||
[node name="CafeBoundary" type="StaticBody2D" parent="StaticCollision" unique_id=810981314]
|
||||
|
||||
[node name="TopWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=83767543]
|
||||
position = Vector2(14, -501)
|
||||
shape = SubResource("Shape_TopWall")
|
||||
|
||||
[node name="BottomWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=578059512]
|
||||
position = Vector2(3, 472)
|
||||
shape = SubResource("Shape_BottomWall")
|
||||
|
||||
[node name="LeftWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=1817681571]
|
||||
position = Vector2(-782, -5)
|
||||
shape = SubResource("Shape_LeftWall")
|
||||
|
||||
[node name="RightWall" type="CollisionShape2D" parent="StaticCollision/CafeBoundary" unique_id=19691881]
|
||||
position = Vector2(761, 196)
|
||||
shape = SubResource("Shape_RightWall")
|
||||
|
||||
[node name="BarCounterCollision" type="StaticBody2D" parent="StaticCollision" unique_id=2101575249]
|
||||
|
||||
[node name="FrontCounterBand" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=236568381]
|
||||
position = Vector2(2.5, -123)
|
||||
shape = SubResource("Shape_FrontCounterBand")
|
||||
|
||||
[node name="BackEquipmentBand" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=302153346]
|
||||
position = Vector2(0, -435)
|
||||
shape = SubResource("Shape_BackEquipmentBand")
|
||||
|
||||
[node name="LeftServiceFixture" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=1759728122]
|
||||
position = Vector2(-641, -283.5)
|
||||
shape = SubResource("Shape_LeftServiceFixture")
|
||||
|
||||
[node name="RightServiceFixture" type="CollisionShape2D" parent="StaticCollision/BarCounterCollision" unique_id=1239925157]
|
||||
position = Vector2(636, -294.5)
|
||||
shape = SubResource("Shape_RightServiceFixture")
|
||||
|
||||
[node name="PropCollision" type="StaticBody2D" parent="StaticCollision" unique_id=729484024]
|
||||
|
||||
[node name="CafeReceptionDeskCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1834206100]
|
||||
polygon = PackedVector2Array(-119, 60, -107, 43, 117, 43, 112, 74, 86, 116, -104, 105)
|
||||
|
||||
[node name="LeftSelfServiceKiosk01Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1994436927]
|
||||
polygon = PackedVector2Array(-521, 164, -465, 160, -462, 240, -510, 243, -525, 219)
|
||||
|
||||
[node name="LeftSelfServiceKiosk02Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1428046087]
|
||||
polygon = PackedVector2Array(-450, 153, -404, 166, -397, 242, -433, 240, -449, 224)
|
||||
|
||||
[node name="RightSelfServiceKiosk01Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1839282295]
|
||||
polygon = PackedVector2Array(411, 160, 443, 166, 439, 232, 398, 242, 390, 205, 400, 193)
|
||||
|
||||
[node name="RightSelfServiceKiosk02Collision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1823238674]
|
||||
polygon = PackedVector2Array(463, 161, 517, 162, 519, 226, 510, 243, 468, 235)
|
||||
|
||||
[node name="LeftCornerPlantSignCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=57617661]
|
||||
polygon = PackedVector2Array(-747, 325, -750, 203, -679, 208, -637, 296, -602, 329, -594, 411, -645, 422, -739, 410)
|
||||
|
||||
[node name="RightCornerPlantSignCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1013557406]
|
||||
polygon = PackedVector2Array(629, 291, 653, 247, 740, 193, 747, 325, 739, 410, 658, 421, 619, 392)
|
||||
|
||||
[node name="LeftQueueTopRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=443030549]
|
||||
position = Vector2(8, -35)
|
||||
polygon = PackedVector2Array(-382, 4, -282, 5, -282, 14, -382, 13)
|
||||
|
||||
[node name="LeftQueueLeftRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=782461653]
|
||||
position = Vector2(-18, -2)
|
||||
polygon = PackedVector2Array(-360, 18, -352, -25, -365, 205, -376, 207)
|
||||
|
||||
[node name="LeftQueueRightRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=785942935]
|
||||
position = Vector2(-6, -37)
|
||||
polygon = PackedVector2Array(-286, 8, -275, 8, -290, 217, -293, 248)
|
||||
|
||||
[node name="LeftQueueBottomRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=141845763]
|
||||
position = Vector2(-330, -326)
|
||||
polygon = PackedVector2Array(-377, 204, -370, 207, -352, 386, -409, 460, -426, 358, -406, 202)
|
||||
|
||||
[node name="RightQueueTopRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=332551755]
|
||||
position = Vector2(2, -35)
|
||||
polygon = PackedVector2Array(282, 5, 381, 5, 381, 14, 282, 14)
|
||||
|
||||
[node name="RightQueueLeftRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=1268670145]
|
||||
position = Vector2(-2, -40)
|
||||
polygon = PackedVector2Array(276, 8, 287, 8, 308, 258, 294, 254, 291, 193)
|
||||
|
||||
[node name="RightQueueRightRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=350292682]
|
||||
position = Vector2(13, 3)
|
||||
polygon = PackedVector2Array(355, -27, 363, 18, 387, 205, 374, 203)
|
||||
|
||||
[node name="RightQueueBottomRopeCollision" type="CollisionPolygon2D" parent="StaticCollision/PropCollision" unique_id=2067709655]
|
||||
position = Vector2(300, -242)
|
||||
polygon = PackedVector2Array(383, 116, 433, 92, 483, 231, 433, 355, 391, 297, 377, 232, 382, 191)
|
||||
|
||||
[node name="InteractionAreas" type="Node2D" parent="." unique_id=1414044788]
|
||||
|
||||
[node name="ExitToWorkZoneArea" type="Area2D" parent="InteractionAreas" unique_id=1132514494]
|
||||
position = Vector2(0, 455)
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/ExitToWorkZoneArea" unique_id=1808842525]
|
||||
shape = SubResource("Shape_ExitArea")
|
||||
|
||||
[node name="CafeRecruitmentLogoArea" type="Area2D" parent="InteractionAreas" unique_id=1787759997]
|
||||
position = Vector2(0, 90)
|
||||
collision_layer = 2
|
||||
collision_mask = 0
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/CafeRecruitmentLogoArea" unique_id=521534737]
|
||||
position = Vector2(-5.5, -16.5)
|
||||
shape = SubResource("Shape_CafeRecruitmentLogo")
|
||||
|
||||
[node name="Markers" type="Node2D" parent="." unique_id=388355020]
|
||||
|
||||
[node name="DefaultSpawn" type="Marker2D" parent="Markers" unique_id=1348428752]
|
||||
position = Vector2(0, 392)
|
||||
|
||||
[node name="ServiceIdlePoints" type="Node2D" parent="Markers" unique_id=917409132]
|
||||
|
||||
[node name="ServiceIdlePoint01" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=579915137]
|
||||
position = Vector2(-472, -291)
|
||||
|
||||
[node name="ServiceIdlePoint02" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=783535284]
|
||||
position = Vector2(-376, -291)
|
||||
|
||||
[node name="ServiceIdlePoint03" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=904507716]
|
||||
position = Vector2(-280, -291)
|
||||
|
||||
[node name="ServiceIdlePoint04" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1203682930]
|
||||
position = Vector2(-184, -291)
|
||||
|
||||
[node name="ServiceIdlePoint05" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=705534018]
|
||||
position = Vector2(-90, -291)
|
||||
|
||||
[node name="ServiceIdlePoint06" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=227830370]
|
||||
position = Vector2(0, -291)
|
||||
|
||||
[node name="ServiceIdlePoint07" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1258667067]
|
||||
position = Vector2(92, -291)
|
||||
|
||||
[node name="ServiceIdlePoint08" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=868571064]
|
||||
position = Vector2(185, -291)
|
||||
|
||||
[node name="ServiceIdlePoint09" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=510403492]
|
||||
position = Vector2(277, -291)
|
||||
|
||||
[node name="ServiceIdlePoint10" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=527786790]
|
||||
position = Vector2(371, -291)
|
||||
|
||||
[node name="ServiceIdlePoint11" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=29178558]
|
||||
position = Vector2(-484, -223)
|
||||
|
||||
[node name="ServiceIdlePoint12" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=61920657]
|
||||
position = Vector2(-384, -223)
|
||||
|
||||
[node name="ServiceIdlePoint13" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=882725816]
|
||||
position = Vector2(-287, -223)
|
||||
|
||||
[node name="ServiceIdlePoint14" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=748399169]
|
||||
position = Vector2(-191, -223)
|
||||
|
||||
[node name="ServiceIdlePoint15" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=197622030]
|
||||
position = Vector2(-96, -223)
|
||||
|
||||
[node name="ServiceIdlePoint16" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1362126027]
|
||||
position = Vector2(-2, -223)
|
||||
|
||||
[node name="ServiceIdlePoint17" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=2008866192]
|
||||
position = Vector2(92, -223)
|
||||
|
||||
[node name="ServiceIdlePoint18" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=69828930]
|
||||
position = Vector2(188, -223)
|
||||
|
||||
[node name="ServiceIdlePoint19" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=294392180]
|
||||
position = Vector2(281, -223)
|
||||
|
||||
[node name="ServiceIdlePoint20" type="Marker2D" parent="Markers/ServiceIdlePoints" unique_id=1006333228]
|
||||
position = Vector2(378, -223)
|
||||
97
scenes/Maps/personal_space.tscn
Normal file
97
scenes/Maps/personal_space.tscn
Normal file
@@ -0,0 +1,97 @@
|
||||
[gd_scene load_steps=12 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"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/characters/player.tscn" id="3_player"]
|
||||
[ext_resource type="Script" path="res://scenes/Maps/ScenePortal.gd" id="4_portal"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/ui/ChatUI.tscn" id="5_chatui"]
|
||||
[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"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_TopWall"]
|
||||
size = Vector2(882, 76)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_LeftWall"]
|
||||
size = Vector2(133, 518)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_RightWall"]
|
||||
size = Vector2(133, 518)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="Shape_ExitPortal"]
|
||||
size = Vector2(105, 57)
|
||||
|
||||
[node name="PersonalSpace" type="Node2D"]
|
||||
script = ExtResource("1_personal_space")
|
||||
|
||||
[node name="UILayer" type="CanvasLayer" parent="."]
|
||||
layer = 10
|
||||
|
||||
[node name="ChatUI" parent="UILayer" instance=ExtResource("5_chatui")]
|
||||
|
||||
[node name="PlayerHud" parent="UILayer" instance=ExtResource("6_playerhud")]
|
||||
|
||||
[node name="FriendListPanel" parent="UILayer" instance=ExtResource("7_friendpanel")]
|
||||
|
||||
[node name="SettingsPanel" parent="UILayer" instance=ExtResource("8_settingspanel")]
|
||||
|
||||
[node name="StageBackdrop" type="ColorRect" parent="."]
|
||||
z_index = -200
|
||||
offset_left = -2000.0
|
||||
offset_top = -1400.0
|
||||
offset_right = 2000.0
|
||||
offset_bottom = 1400.0
|
||||
color = Color(0.118, 0.149, 0.18, 1)
|
||||
|
||||
[node name="RoomBase" type="Sprite2D" parent="."]
|
||||
z_index = -100
|
||||
texture_filter = 2
|
||||
scale = Vector2(0.7, 0.7)
|
||||
texture = ExtResource("2_room_base")
|
||||
|
||||
[node name="YSortWorld" type="Node2D" parent="."]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Characters" type="Node2D" parent="YSortWorld"]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Players" type="Node2D" parent="YSortWorld/Characters"]
|
||||
y_sort_enabled = true
|
||||
|
||||
[node name="Player" parent="YSortWorld/Characters/Players" instance=ExtResource("3_player")]
|
||||
position = Vector2(0, 217)
|
||||
|
||||
[node name="StaticCollision" type="Node2D" parent="."]
|
||||
|
||||
[node name="RoomBoundary" type="StaticBody2D" parent="StaticCollision"]
|
||||
|
||||
[node name="TopWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
|
||||
position = Vector2(0, -210)
|
||||
shape = SubResource("Shape_TopWall")
|
||||
|
||||
[node name="LeftWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
|
||||
position = Vector2(-483, 14)
|
||||
shape = SubResource("Shape_LeftWall")
|
||||
|
||||
[node name="RightWall" type="CollisionShape2D" parent="StaticCollision/RoomBoundary"]
|
||||
position = Vector2(483, 14)
|
||||
shape = SubResource("Shape_RightWall")
|
||||
|
||||
[node name="InteractionAreas" type="Node2D" parent="."]
|
||||
|
||||
[node name="ExitToSquareArea" type="Area2D" parent="InteractionAreas"]
|
||||
position = Vector2(0, 273)
|
||||
script = ExtResource("4_portal")
|
||||
targetSceneName = "square"
|
||||
targetSpawnName = "NeighborhoodTeleport"
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionAreas/ExitToSquareArea"]
|
||||
shape = SubResource("Shape_ExitPortal")
|
||||
|
||||
[node name="Markers" type="Node2D" parent="."]
|
||||
|
||||
[node name="DefaultSpawn" type="Marker2D" parent="Markers"]
|
||||
position = Vector2(0, 200)
|
||||
|
||||
[node name="FromNeighborhood" type="Marker2D" parent="Markers"]
|
||||
position = Vector2(0, 214)
|
||||
1459
scenes/Maps/square.tscn
Normal file
1459
scenes/Maps/square.tscn
Normal file
File diff suppressed because one or more lines are too long
424
scenes/Maps/work_zone.tscn
Normal file
424
scenes/Maps/work_zone.tscn
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user