Files
whale-town-front-v2/scenes/Maps/PersonalSpace.gd
2026-07-21 23:07:31 +08:00

836 lines
32 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class_name 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:
add_to_group("whaletown_escape_dismissible")
_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()
_discover_destination()
set_process(false)
func _discover_destination() -> void:
var destination_id := SceneManager.get_next_destination_id()
if destination_id.is_empty():
destination_id = "personal_room"
var socialManager := get_node_or_null("/root/SocialManager")
if socialManager != null and socialManager.has_method("discover_destination"):
socialManager.call("discover_destination", destination_id)
func _leave_multiplayer_world() -> void:
var chatManager := get_node_or_null("/root/ChatManager")
if chatManager != null and chatManager.has_method("leave_world"):
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 get_viewport().is_input_handled():
return
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 is_escape_dismissible() -> bool:
return _draggedDecor != null or (is_instance_valid(_inventoryPanel) and _inventoryPanel.visible)
func get_escape_priority() -> int:
return 500
func request_escape_close() -> void:
if _draggedDecor != null:
_finish_decor_drag()
return
if is_instance_valid(_inventoryPanel):
_inventoryPanel.visible = false
func _apply_spawn_point() -> void:
if player.has_scene_position_override:
return
var spawnName: String = SceneManager.get_next_spawn_name()
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
var marker := $Markers.get_node_or_null(markerName) as Marker2D
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