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