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 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 const ROOM_GRID_SIZE: float = 16.0 const FLOOR_PLACE_BOUNDS: Rect2 = Rect2(-426.0, -176.0, 852.0, 388.0) const WALL_PLACE_BOUNDS: Rect2 = Rect2(-378.0, -258.0, 756.0, 66.0) const RoomDecorEditHistory = preload("res://_Core/room_decor/RoomDecorEditHistory.gd") const RoomDecorCollisionRegistry = preload("res://_Core/room_decor/RoomDecorCollisionRegistry.gd") @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 _gridOverlay: Node2D var _inventoryPanel: PanelContainer var _inventoryGrid: GridContainer var _dragSurface: Control var _toastLabel: Label var _roomToolbar: PanelContainer var _roomTitleLabel: Label var _layoutStatusLabel: Label var _escHintLabel: Label var _editButton: Button var _backpackButton: Button var _gridButton: Button var _undoButton: Button var _redoButton: Button var _rotateButton: Button var _collectButton: Button var _resetButton: Button var _saveButton: Button var _finishEditButton: Button var _unsavedDialog: PanelContainer var _selectedDecorId: String = "" var _draggedDecor: Sprite2D var _dragOffset: Vector2 = Vector2.ZERO var _dragOriginalItem: Dictionary = {} var _decorItems: Dictionary = {} var _serverDecorItems: Dictionary = {} var _decorNodes: Dictionary = {} var _decorCollisionRegistry: RefCounted var _remoteDecorDefinitions: Dictionary = {} var _inventoryRequestInFlight: bool = false var _inventoryRequestReportsErrors: bool = false var _inventoryLoaded: bool = false var _visitorMode: bool = false var _visitorOwnerId: String = "" var _visitorOwnerName: String = "" var _editMode: bool = false var _showPlacementGrid: bool = false var _editHistory: RefCounted = RoomDecorEditHistory.new() var _dirtyDecorIds: Dictionary = {} var _saveAwaitingIds: Dictionary = {} var _saveFailedIds: Dictionary = {} var _saveInProgress: bool = false var _exitAfterSave: bool = false var _resetArmed: bool = false func _ready() -> void: add_to_group("whaletown_escape_dismissible") _configure_visit_mode() _leave_multiplayer_world() _apply_spawn_point() _configure_camera() _decorCollisionRegistry = RoomDecorCollisionRegistry.new(staticCollision, DECOR_COLLISION_LAYER, DECOR_COLLISION_MASK) _build_decor_layer() _build_room_decor_ui() _connect_room_decor_events() _fetch_room_decor_inventory() _discover_destination() set_process(false) func _discover_destination() -> void: if _visitorMode: return 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) if saveManager.decor_revision_conflict.is_connected(_on_decor_revision_conflict): saveManager.decor_revision_conflict.disconnect(_on_decor_revision_conflict) func _input(event: InputEvent) -> void: if get_viewport().is_input_handled(): return if event is InputEventMouseButton: if not _can_edit_layout(): return 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) or (is_instance_valid(_unsavedDialog) and _unsavedDialog.visible) or _editMode func get_escape_priority() -> int: return 500 func request_escape_close() -> void: if _draggedDecor != null: _cancel_decor_drag() return if is_instance_valid(_inventoryPanel) and _inventoryPanel.visible: _inventoryPanel.visible = false return if is_instance_valid(_unsavedDialog) and _unsavedDialog.visible: _unsavedDialog.visible = false return if _editMode: _request_finish_edit() 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_visit_mode() -> void: var context: Dictionary = SceneManager.get_room_visit_context() _visitorOwnerId = str(context.get("owner_user_id", "")).strip_edges() _visitorOwnerName = str(context.get("owner_nickname", "")).strip_edges() _visitorMode = not _visitorOwnerId.is_empty() 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) _build_placement_grid() func _build_placement_grid() -> void: _gridOverlay = Node2D.new() _gridOverlay.name = "RoomPlacementGrid" _gridOverlay.z_index = -30 _gridOverlay.visible = false ySortWorld.add_child(_gridOverlay) ySortWorld.move_child(_gridOverlay, 0) _rebuild_placement_grid() func _rebuild_placement_grid() -> void: if _gridOverlay == null: return _clear_children(_gridOverlay) _add_grid_zone(_placement_bounds_for_surface("floor"), Color(0.050, 0.560, 0.900, 0.20), Color(0.050, 0.560, 0.900, 0.62)) _add_grid_zone(_placement_bounds_for_surface("wall"), Color(0.620, 0.330, 0.860, 0.20), Color(0.620, 0.330, 0.860, 0.62)) func _add_grid_zone(bounds: Rect2, line_color: Color, edge_color: Color) -> void: if _gridOverlay == null: return var x: float = ceilf(bounds.position.x / ROOM_GRID_SIZE) * ROOM_GRID_SIZE while x < bounds.end.x: _add_grid_line(Vector2(x, bounds.position.y), Vector2(x, bounds.end.y), line_color, 1.0) x += ROOM_GRID_SIZE var y: float = ceilf(bounds.position.y / ROOM_GRID_SIZE) * ROOM_GRID_SIZE while y < bounds.end.y: _add_grid_line(Vector2(bounds.position.x, y), Vector2(bounds.end.x, y), line_color, 1.0) y += ROOM_GRID_SIZE _add_grid_line(bounds.position, Vector2(bounds.end.x, bounds.position.y), edge_color, 2.0) _add_grid_line(Vector2(bounds.end.x, bounds.position.y), bounds.end, edge_color, 2.0) _add_grid_line(bounds.end, Vector2(bounds.position.x, bounds.end.y), edge_color, 2.0) _add_grid_line(Vector2(bounds.position.x, bounds.end.y), bounds.position, edge_color, 2.0) func _add_grid_line(from: Vector2, to: Vector2, color: Color, width: float) -> void: var line := Line2D.new() line.points = PackedVector2Array([from, to]) line.default_color = color line.width = width line.antialiased = true _gridOverlay.add_child(line) func _build_room_decor_ui() -> void: _build_room_toolbar() _build_unsaved_exit_dialog() _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) _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 _build_room_toolbar() -> void: _roomToolbar = PanelContainer.new() _roomToolbar.mouse_filter = Control.MOUSE_FILTER_STOP _roomToolbar.add_theme_stylebox_override("panel", _create_panel_style(Color(0.970, 0.992, 1.0, 0.94), 18, true)) if _visitorMode: _roomToolbar.set_anchors_preset(Control.PRESET_CENTER_TOP) _roomToolbar.offset_left = -430 _roomToolbar.offset_top = 26 _roomToolbar.offset_right = 430 _roomToolbar.offset_bottom = 88 else: _roomToolbar.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) _roomToolbar.offset_left = -540 _roomToolbar.offset_top = -96 _roomToolbar.offset_right = 540 _roomToolbar.offset_bottom = -28 uiLayer.add_child(_roomToolbar) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 16) margin.add_theme_constant_override("margin_top", 8) margin.add_theme_constant_override("margin_right", 16) margin.add_theme_constant_override("margin_bottom", 8) _roomToolbar.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) margin.add_child(row) _roomTitleLabel = Label.new() _roomTitleLabel.size_flags_horizontal = Control.SIZE_EXPAND_FILL _roomTitleLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _roomTitleLabel.add_theme_font_size_override("font_size", 19) _roomTitleLabel.add_theme_color_override("font_color", TEXT_COLOR) _roomTitleLabel.text = "%s的个人空间 · 只读参观" % (_visitorOwnerName if not _visitorOwnerName.is_empty() else "这位玩家") if _visitorMode else "我的个人空间" row.add_child(_roomTitleLabel) if _visitorMode: row.add_child(_create_toolbar_button("离开参观", _leave_visited_room)) return _layoutStatusLabel = Label.new() _layoutStatusLabel.custom_minimum_size = Vector2(128, 0) _layoutStatusLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _layoutStatusLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _layoutStatusLabel.add_theme_font_size_override("font_size", 14) _layoutStatusLabel.add_theme_color_override("font_color", MUTED_COLOR) row.add_child(_layoutStatusLabel) _escHintLabel = Label.new() _escHintLabel.text = "Esc 结束编辑" _escHintLabel.custom_minimum_size = Vector2(92, 0) _escHintLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _escHintLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _escHintLabel.add_theme_font_size_override("font_size", 13) _escHintLabel.add_theme_color_override("font_color", MUTED_COLOR) row.add_child(_escHintLabel) _editButton = _create_toolbar_button("编辑布局", func() -> void: _set_edit_mode(true)) _backpackButton = _create_toolbar_button("背包", _toggle_inventory_panel) _gridButton = _create_toolbar_button("网格:关", _toggle_placement_grid) _undoButton = _create_toolbar_button("撤销", _undo_last_change) _redoButton = _create_toolbar_button("重做", _redo_last_change) _rotateButton = _create_toolbar_button("旋转", _rotate_selected_decor) _collectButton = _create_toolbar_button("收回", _remove_selected_decor) _resetButton = _create_toolbar_button("重置布局", _request_reset_layout) _saveButton = _create_toolbar_button("保存", _save_layout) _finishEditButton = _create_toolbar_button("完成编辑", _request_finish_edit) row.add_child(_editButton) row.add_child(_backpackButton) row.add_child(_gridButton) row.add_child(_undoButton) row.add_child(_redoButton) row.add_child(_rotateButton) row.add_child(_collectButton) row.add_child(_resetButton) row.add_child(_saveButton) row.add_child(_finishEditButton) _update_editor_toolbar() func _build_unsaved_exit_dialog() -> void: _unsavedDialog = PanelContainer.new() _unsavedDialog.name = "UnsavedLayoutDialog" _unsavedDialog.visible = false _unsavedDialog.mouse_filter = Control.MOUSE_FILTER_STOP _unsavedDialog.set_anchors_preset(Control.PRESET_CENTER) _unsavedDialog.offset_left = -250 _unsavedDialog.offset_top = -132 _unsavedDialog.offset_right = 250 _unsavedDialog.offset_bottom = 132 _unsavedDialog.add_theme_stylebox_override("panel", _create_panel_style(Color(0.965, 0.990, 1.0, 0.99), 24, true)) uiLayer.add_child(_unsavedDialog) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 28) margin.add_theme_constant_override("margin_top", 26) margin.add_theme_constant_override("margin_right", 28) margin.add_theme_constant_override("margin_bottom", 24) _unsavedDialog.add_child(margin) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 14) margin.add_child(content) var title := Label.new() title.text = "布局还没有保存" title.add_theme_font_size_override("font_size", 24) title.add_theme_color_override("font_color", TEXT_COLOR) content.add_child(title) var detail := Label.new() detail.text = "你可以先保存本次摆放,或者放弃未保存的改动。" detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.add_theme_font_size_override("font_size", 16) detail.add_theme_color_override("font_color", MUTED_COLOR) content.add_child(detail) var actions := HBoxContainer.new() actions.alignment = BoxContainer.ALIGNMENT_END actions.add_theme_constant_override("separation", 10) content.add_child(actions) var continueButton := _create_toolbar_button("继续编辑", func() -> void: _hide_unsaved_dialog()) var discardButton := _create_toolbar_button("放弃改动", _discard_and_finish_edit) discardButton.add_theme_color_override("font_hover_color", Color(0.850, 0.300, 0.340)) var saveExitButton := _create_toolbar_button("保存并退出", func() -> void: _save_layout(true)) saveExitButton.add_theme_color_override("font_color", Color.WHITE) saveExitButton.add_theme_stylebox_override("normal", _create_panel_style(ACCENT_COLOR, 12, false)) saveExitButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.150, 0.680, 1.0, 1.0), 12, false)) actions.add_child(continueButton) actions.add_child(discardButton) actions.add_child(saveExitButton) func _create_toolbar_button(label_text: String, callback: Callable) -> Button: var button := Button.new() button.text = label_text button.custom_minimum_size = Vector2(74, 38) button.focus_mode = Control.FOCUS_NONE button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button.add_theme_font_size_override("font_size", 15) button.add_theme_color_override("font_color", TEXT_COLOR) button.add_theme_color_override("font_hover_color", ACCENT_COLOR) button.add_theme_color_override("font_disabled_color", Color(0.52, 0.60, 0.68, 0.65)) button.add_theme_stylebox_override("normal", _create_panel_style(Color(0.900, 0.960, 1.0, 0.92), 12, false)) button.add_theme_stylebox_override("hover", _create_panel_style(Color(0.820, 0.925, 1.0, 1.0), 12, false)) button.pressed.connect(callback) return button 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 not _visitorMode and 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) if not saveManager.decor_revision_conflict.is_connected(_on_decor_revision_conflict): saveManager.decor_revision_conflict.connect(_on_decor_revision_conflict) func _on_backpack_toggle_requested(_data: Variant = null) -> void: if _visitorMode: _show_toast("参观模式不能编辑布局") return if not _editMode: _show_toast("请先点击“编辑布局”") return _toggle_inventory_panel() func _toggle_inventory_panel() -> void: if not _can_edit_layout(): _show_toast("请先进入布局编辑") return _inventoryPanel.visible = not _inventoryPanel.visible if _inventoryPanel.visible and not _inventoryLoaded: _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 endpoint: String = "/rooms/%s/decor-placements" % _visitorOwnerId if _visitorMode else "/rooms/me/decor-placements" var api_client := get_node_or_null("/root/ApiClient") if api_client == null or not api_client.has_method("get_json"): _inventoryRequestInFlight = false if _inventoryRequestReportsErrors: _show_toast("家具背包服务不可用") _inventoryRequestReportsErrors = false return api_client.call("get_json", endpoint, _on_inventory_request_completed, true) func _on_inventory_request_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void: _inventoryRequestInFlight = false var reportErrors := _inventoryRequestReportsErrors _inventoryRequestReportsErrors = false if not success: if reportErrors: _show_toast(str(error_info.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: if _editMode and not _dirtyDecorIds.is_empty(): return _decorItems.clear() if _visitorMode: var owner_variant: Variant = data.get("owner", {}) if owner_variant is Dictionary: var owner: Dictionary = owner_variant _visitorOwnerName = str(owner.get("nickname", owner.get("username", _visitorOwnerName))).strip_edges() if is_instance_valid(_roomTitleLabel): _roomTitleLabel.text = "%s的个人空间 · 只读参观" % (_visitorOwnerName if not _visitorOwnerName.is_empty() else "这位玩家") _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) if not _visitorMode: var save_manager := get_node_or_null("/root/RoomDecorSaveManager") if save_manager != null and save_manager.has_method("set_layout_revision"): save_manager.call("set_layout_revision", int(data.get("revision", 0)), true) _serverDecorItems = _duplicate_item_map(_decorItems) _dirtyDecorIds.clear() _saveFailedIds.clear() _inventoryLoaded = true _render_inventory_list() _render_placed_decors() _update_editor_toolbar() func _apply_local_empty_inventory() -> void: _decorItems.clear() _serverDecorItems.clear() _dirtyDecorIds.clear() _inventoryLoaded = true _render_inventory_list() _render_placed_decors() _update_editor_toolbar() 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 _rebuild_placement_grid() func _render_inventory_list() -> void: if _visitorMode: return _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) var surface_label := "墙面挂饰" if str(item.get("placement_surface", "floor")) == "wall" else "地面家具" status.text = "%s · %s" % [surface_label, "已摆放" 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) _refresh_decor_selection() func _begin_inventory_decor_drag(decorId: String) -> void: if not _can_edit_layout() or not _decorItems.has(decorId): return _selectedDecorId = decorId _dragOriginalItem = (_decorItems[decorId] as Dictionary).duplicate(true) 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() _refresh_decor_selection() _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["rotation_degrees"] = _get_item_int(item, "default_rotation_degrees", 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.rotation_degrees = _get_item_float(item, "rotation_degrees", _get_item_float(item, "default_rotation_degrees", 0.0)) 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: if not _can_edit_layout(): return false 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", "")) _dragOriginalItem = (_decorItems.get(_selectedDecorId, {}) as Dictionary).duplicate(true) _dragOffset = bestNode.global_position - worldPosition _set_decor_collision_disabled(_selectedDecorId, true) _draggedDecor.z_index = max(_draggedDecor.z_index, 50) _enable_drag_surface() _refresh_decor_selection() _update_editor_toolbar() 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] var snapped_position := _snap_to_room_grid(_draggedDecor.global_position, item) _draggedDecor.global_position = snapped_position item["position_x"] = snapped_position.x item["position_y"] = snapped_position.y item["placed"] = true item["scale"] = _draggedDecor.scale.x item["rotation_degrees"] = _draggedDecor.rotation_degrees 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) _record_item_change(_dragOriginalItem, item) _mark_decor_dirty(decorId) _render_inventory_list() _draggedDecor = null _dragOriginalItem.clear() _disable_drag_surface() _refresh_decor_selection() _update_editor_toolbar() func _update_dragged_decor_position() -> void: if _draggedDecor != null: var decorId := str(_draggedDecor.get_meta("decor_id", "")) if not decorId.is_empty() and _decorItems.has(decorId): var item: Dictionary = _decorItems[decorId] _draggedDecor.global_position = _constrain_room_position(get_global_mouse_position() + _dragOffset, item) item["position_x"] = _draggedDecor.global_position.x item["position_y"] = _draggedDecor.global_position.y _sync_decor_collision(decorId, item) func _cancel_decor_drag() -> void: if _draggedDecor == null: _disable_drag_surface() return var decor_id: String = str(_draggedDecor.get_meta("decor_id", "")) if not decor_id.is_empty() and not _dragOriginalItem.is_empty(): _decorItems[decor_id] = _dragOriginalItem.duplicate(true) _render_placed_decors() _render_inventory_list() _draggedDecor = null _dragOriginalItem.clear() _disable_drag_surface() _refresh_decor_selection() _update_editor_toolbar() 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 not _can_edit_layout(): _show_toast("请先进入编辑布局") return if _selectedDecorId.is_empty() or not _decorItems.has(_selectedDecorId): _show_toast("请先选择一个家具") return var item: Dictionary = _decorItems[_selectedDecorId] var before: Dictionary = item.duplicate(true) 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) _record_item_change(before, item) _mark_decor_dirty(_selectedDecorId) _render_inventory_list() _refresh_decor_selection() _update_editor_toolbar() _show_toast("已收回家具") func _on_decor_save_succeeded(savedItem: Dictionary) -> void: var item := _merge_decor_definition(savedItem.duplicate(true)) var decorId := str(item.get("decor_id", "")) if decorId.is_empty() or not _saveAwaitingIds.has(decorId): return _serverDecorItems[decorId] = item.duplicate(true) _decorItems[decorId] = item.duplicate(true) _saveAwaitingIds.erase(decorId) _dirtyDecorIds.erase(decorId) _saveFailedIds.erase(decorId) _render_placed_decors() _render_inventory_list() _finalize_save_if_ready() func _on_decor_revision_conflict(_current_revision: int, message: String) -> void: _saveAwaitingIds.clear() _saveFailedIds.clear() _saveInProgress = false _exitAfterSave = false _dirtyDecorIds.clear() _editHistory.call("clear") _update_editor_toolbar() _show_toast(message) _fetch_room_decor_inventory(true) func _can_edit_layout() -> bool: return not _visitorMode and _editMode and not _saveInProgress func _set_edit_mode(enabled: bool) -> void: if _visitorMode: return if not enabled: _request_finish_edit() return if _editMode: return _editMode = true _selectedDecorId = "" _editHistory.call("clear") _resetArmed = false _showPlacementGrid = false _update_placement_grid_visibility() _refresh_decor_selection() _update_editor_toolbar() _show_toast("布局编辑已开启;改动会在点击保存后提交") func _request_finish_edit() -> void: if not _editMode: return if _draggedDecor != null: _finish_decor_drag() if _saveInProgress: _show_toast("正在保存布局,请稍候") return if _has_unsaved_changes(): _show_unsaved_exit_dialog() return _end_edit_session() func _end_edit_session() -> void: _editMode = false _selectedDecorId = "" _editHistory.call("clear") _resetArmed = false _exitAfterSave = false _showPlacementGrid = false _update_placement_grid_visibility() if is_instance_valid(_inventoryPanel): _inventoryPanel.visible = false _hide_unsaved_dialog() _refresh_decor_selection() _update_editor_toolbar() _show_toast("布局编辑已完成") func _show_unsaved_exit_dialog() -> void: if is_instance_valid(_unsavedDialog): _unsavedDialog.visible = true func _hide_unsaved_dialog() -> void: if is_instance_valid(_unsavedDialog): _unsavedDialog.visible = false func _discard_and_finish_edit() -> void: if _saveInProgress: return _decorItems = _duplicate_item_map(_serverDecorItems) _dirtyDecorIds.clear() _saveFailedIds.clear() _render_inventory_list() _render_placed_decors() _end_edit_session() func _has_unsaved_changes() -> bool: return not _dirtyDecorIds.is_empty() func _update_editor_toolbar() -> void: if _visitorMode: return var has_selection := not _selectedDecorId.is_empty() and _decorItems.has(_selectedDecorId) var selected_is_placed := has_selection and bool((_decorItems[_selectedDecorId] as Dictionary).get("placed", false)) var dirty_count := _dirtyDecorIds.size() if is_instance_valid(_editButton): _editButton.visible = not _editMode if is_instance_valid(_backpackButton): _backpackButton.visible = _editMode _backpackButton.disabled = _saveInProgress if is_instance_valid(_gridButton): _gridButton.visible = _editMode _gridButton.disabled = _saveInProgress _gridButton.text = "网格:开" if _showPlacementGrid else "网格:关" if is_instance_valid(_finishEditButton): _finishEditButton.visible = _editMode _finishEditButton.disabled = _saveInProgress if is_instance_valid(_undoButton): _undoButton.visible = _editMode _undoButton.disabled = not _can_edit_layout() or not bool(_editHistory.call("can_undo")) if is_instance_valid(_redoButton): _redoButton.visible = _editMode _redoButton.disabled = not _can_edit_layout() or not bool(_editHistory.call("can_redo")) if is_instance_valid(_rotateButton): _rotateButton.visible = _editMode _rotateButton.disabled = not _can_edit_layout() or not selected_is_placed if is_instance_valid(_collectButton): _collectButton.visible = _editMode _collectButton.disabled = not _can_edit_layout() or not selected_is_placed if is_instance_valid(_resetButton): _resetButton.visible = _editMode _resetButton.disabled = not _can_edit_layout() if is_instance_valid(_saveButton): _saveButton.visible = _editMode _saveButton.text = "保存" if dirty_count == 0 else "保存(%d)" % dirty_count _saveButton.disabled = not _editMode or _saveInProgress or dirty_count == 0 if is_instance_valid(_layoutStatusLabel): _layoutStatusLabel.visible = true var selected_name := str((_decorItems.get(_selectedDecorId, {}) as Dictionary).get("name", "")) if has_selection else "" var selected_prefix := ("选中:%s · " % selected_name) if not selected_name.is_empty() else "" if _saveInProgress: _layoutStatusLabel.text = "正在保存 %d 项…" % _saveAwaitingIds.size() elif not _saveFailedIds.is_empty(): _layoutStatusLabel.text = "%d 项保存失败,可重试" % _saveFailedIds.size() elif dirty_count > 0: _layoutStatusLabel.text = "%s%d 项未保存" % [selected_prefix, dirty_count] elif _editMode: _layoutStatusLabel.text = "%s草稿已保存" % selected_prefix else: _layoutStatusLabel.text = "布局已保存" if is_instance_valid(_escHintLabel): _escHintLabel.visible = _editMode _escHintLabel.text = "Esc 结束编辑" if _editMode else "" func _toggle_placement_grid() -> void: if not _can_edit_layout(): return _showPlacementGrid = not _showPlacementGrid _update_placement_grid_visibility() _update_editor_toolbar() _show_toast("已显示摆放网格(蓝色:地面,紫色:墙面)" if _showPlacementGrid else "已隐藏摆放网格") func _update_placement_grid_visibility() -> void: if is_instance_valid(_gridOverlay): _gridOverlay.visible = _editMode and _showPlacementGrid func _record_item_change(before: Dictionary, after: Dictionary) -> void: if bool(_editHistory.call("record_item", before, after)): _update_editor_toolbar() func _record_bulk_change(before_items: Dictionary, after_items: Dictionary) -> void: if bool(_editHistory.call("record_bulk", before_items, after_items)): _update_editor_toolbar() func _undo_last_change() -> void: if not _can_edit_layout() or not bool(_editHistory.call("can_undo")): return var entry: Dictionary = _editHistory.call("take_undo") _apply_history_entry(entry, false) _update_editor_toolbar() func _redo_last_change() -> void: if not _can_edit_layout() or not bool(_editHistory.call("can_redo")): return var entry: Dictionary = _editHistory.call("take_redo") _apply_history_entry(entry, true) _update_editor_toolbar() func _apply_history_entry(entry: Dictionary, use_after: bool) -> void: var key := "after_items" if use_after else "before_items" var bulk_variant: Variant = entry.get(key, null) if bulk_variant is Dictionary: var bulk_items: Dictionary = bulk_variant as Dictionary for decor_id_variant in bulk_items.keys(): var decor_id := str(decor_id_variant) var item_variant: Variant = bulk_items.get(decor_id_variant, {}) if item_variant is Dictionary: _decorItems[decor_id] = (item_variant as Dictionary).duplicate(true) _mark_decor_dirty(decor_id) _selectedDecorId = "" else: var item_key := "after" if use_after else "before" var item_variant: Variant = entry.get(item_key, {}) if not (item_variant is Dictionary): return var item: Dictionary = (item_variant as Dictionary).duplicate(true) var decor_id: String = str(item.get("decor_id", "")).strip_edges() if decor_id.is_empty(): return _decorItems[decor_id] = item _selectedDecorId = decor_id _mark_decor_dirty(decor_id) _render_placed_decors() _render_inventory_list() _refresh_decor_selection() func _rotate_selected_decor() -> void: if not _can_edit_layout() or _selectedDecorId.is_empty() or not _decorItems.has(_selectedDecorId): return var item: Dictionary = _decorItems[_selectedDecorId] if not bool(item.get("placed", false)): _show_toast("请先摆放选中的家具") return var steps_variant: Variant = item.get("rotation_steps", [0]) if not (steps_variant is Array): return var steps: Array = steps_variant as Array if steps.size() <= 1: _show_toast("该家具的方向已锁定") return var current_rotation: int = _get_item_int(item, "rotation_degrees", _get_item_int(item, "default_rotation_degrees", 0)) var current_index: int = 0 for index in range(steps.size()): if _to_int(steps[index], 0) == current_rotation: current_index = index break var before: Dictionary = item.duplicate(true) item["rotation_degrees"] = _to_int(steps[(current_index + 1) % steps.size()], 0) _decorItems[_selectedDecorId] = item _render_placed_decors() _record_item_change(before, item) _mark_decor_dirty(_selectedDecorId) _refresh_decor_selection() func _request_reset_layout() -> void: if not _can_edit_layout(): return if not _resetArmed: _resetArmed = true _show_toast("再次点击“重置布局”确认;家具不会丢失") get_tree().create_timer(3.0).timeout.connect(func() -> void: _resetArmed = false) return _resetArmed = false var save_manager := get_node_or_null("/root/RoomDecorSaveManager") if save_manager != null and save_manager.has_method("clear_pending_saves"): save_manager.call("clear_pending_saves") var before_items := _duplicate_item_map(_decorItems) for decor_id_variant in _decorItems.keys(): var decor_id := str(decor_id_variant) var item: Dictionary = (_decorItems[decor_id] as Dictionary).duplicate(true) item["placed"] = false var default_position := _variant_to_vector2(item.get("default_position", Vector2.ZERO)) item["position_x"] = default_position.x item["position_y"] = default_position.y item["scale"] = _get_item_float(item, "default_scale", 1.0) item["rotation_degrees"] = _get_item_int(item, "default_rotation_degrees", 0) item["z_index"] = _get_item_int(item, "default_z_index", 0) _decorItems[decor_id] = item _mark_decor_dirty(decor_id) var after_items := _duplicate_item_map(_decorItems) _record_bulk_change(before_items, after_items) _selectedDecorId = "" _render_placed_decors() _render_inventory_list() _refresh_decor_selection() _update_editor_toolbar() _show_toast("布局已重置为草稿;点击保存后生效") func _save_layout(exit_after_save: bool = false) -> void: if not _editMode: return if _saveInProgress: _show_toast("正在保存布局,请稍候") return if _dirtyDecorIds.is_empty(): _show_toast("当前没有需要保存的改动") if exit_after_save: _end_edit_session() return if not _is_authenticated(): _show_toast("请先登录后保存摆放") return var save_manager := get_node_or_null("/root/RoomDecorSaveManager") if save_manager == null or not save_manager.has_method("enqueue_save"): _show_toast("家具保存服务不可用") return var save_ids: Array[String] = [] for decor_id_variant in _dirtyDecorIds.keys(): var decor_id := str(decor_id_variant) if _decorItems.has(decor_id): save_ids.append(decor_id) if save_ids.is_empty(): _dirtyDecorIds.clear() _update_editor_toolbar() return _saveInProgress = true _exitAfterSave = exit_after_save _hide_unsaved_dialog() _saveAwaitingIds.clear() _saveFailedIds.clear() for decor_id in save_ids: _saveAwaitingIds[decor_id] = true _update_editor_toolbar() for decor_id in save_ids: var item: Dictionary = (_decorItems[decor_id] as Dictionary).duplicate(true) var accepted: bool = bool(save_manager.call("enqueue_save", item)) if not accepted and _saveAwaitingIds.has(decor_id): _saveAwaitingIds.erase(decor_id) _saveFailedIds[decor_id] = "保存请求未能加入队列" _finalize_save_if_ready() func _on_decor_save_failed(item: Dictionary, message: String) -> void: var decor_id := str(item.get("decor_id", "")).strip_edges() if decor_id.is_empty() or not _saveAwaitingIds.has(decor_id): if not message.is_empty(): _show_toast(message) return _saveAwaitingIds.erase(decor_id) _saveFailedIds[decor_id] = message _dirtyDecorIds[decor_id] = true _finalize_save_if_ready() func _finalize_save_if_ready() -> void: if not _saveInProgress or not _saveAwaitingIds.is_empty(): return _saveInProgress = false var exit_when_saved := _exitAfterSave and _saveFailedIds.is_empty() and _dirtyDecorIds.is_empty() _exitAfterSave = false _update_editor_toolbar() if not _saveFailedIds.is_empty(): _show_toast("%d 项家具未保存,可再次点击保存重试" % _saveFailedIds.size()) return _show_toast("布局已保存") if exit_when_saved: _end_edit_session() func _mark_decor_dirty(decor_id: String) -> void: if decor_id.is_empty() or not _decorItems.has(decor_id): return var server_variant: Variant = _serverDecorItems.get(decor_id, null) if server_variant is Dictionary and _placement_signature(_decorItems[decor_id]) == _placement_signature(server_variant as Dictionary): _dirtyDecorIds.erase(decor_id) _saveFailedIds.erase(decor_id) else: _dirtyDecorIds[decor_id] = true _update_editor_toolbar() func _placement_signature(item: Dictionary) -> String: return "%d|%.3f|%.3f|%.3f|%d|%d" % [ 1 if bool(item.get("placed", false)) else 0, _get_item_float(item, "position_x", 0.0), _get_item_float(item, "position_y", 0.0), _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0)), _get_item_int(item, "rotation_degrees", _get_item_int(item, "default_rotation_degrees", 0)), _get_item_int(item, "z_index", _get_item_int(item, "default_z_index", 0)), ] func _duplicate_item_map(source: Dictionary) -> Dictionary: var duplicate: Dictionary = {} for decor_id_variant in source.keys(): var item_variant: Variant = source.get(decor_id_variant, {}) if item_variant is Dictionary: duplicate[str(decor_id_variant)] = (item_variant as Dictionary).duplicate(true) return duplicate func _refresh_decor_selection() -> void: for decor_id_variant in _decorNodes.keys(): var decor_id := str(decor_id_variant) var node := _decorNodes.get(decor_id, null) as Sprite2D if node == null or not is_instance_valid(node): continue var frame := node.get_node_or_null("SelectionFrame") as Line2D var collision_preview := node.get_node_or_null("CollisionPreview") as Line2D var show_frame := _editMode and decor_id == _selectedDecorId and node.texture != null if not show_frame: if frame != null: frame.queue_free() if collision_preview != null: collision_preview.queue_free() continue if frame == null: frame = Line2D.new() frame.name = "SelectionFrame" frame.antialiased = true frame.default_color = Color(0.040, 0.740, 1.0, 0.96) frame.z_index = 20 node.add_child(frame) var half_size: Vector2 = node.texture.get_size() * 0.5 frame.points = PackedVector2Array([ Vector2(-half_size.x, -half_size.y), Vector2(half_size.x, -half_size.y), Vector2(half_size.x, half_size.y), Vector2(-half_size.x, half_size.y), Vector2(-half_size.x, -half_size.y), ]) frame.width = 3.0 / maxf(absf(node.scale.x), 0.1) frame.default_color = Color(1.0, 0.750, 0.180, 0.98) if node == _draggedDecor else Color(0.040, 0.740, 1.0, 0.96) var item: Dictionary = _decorItems.get(decor_id, {}) as Dictionary var collision_size := _variant_to_vector2(item.get("collision_size", Vector2.ZERO)) if node == _draggedDecor and collision_size != Vector2.ZERO: if collision_preview == null: collision_preview = Line2D.new() collision_preview.name = "CollisionPreview" collision_preview.antialiased = true collision_preview.z_index = 21 node.add_child(collision_preview) var collision_half := collision_size * 0.5 collision_preview.position = _variant_to_vector2(item.get("collision_offset", Vector2.ZERO)) collision_preview.points = PackedVector2Array([ Vector2(-collision_half.x, -collision_half.y), Vector2(collision_half.x, -collision_half.y), Vector2(collision_half.x, collision_half.y), Vector2(-collision_half.x, collision_half.y), Vector2(-collision_half.x, -collision_half.y), ]) collision_preview.width = 2.0 / maxf(absf(node.scale.x), 0.1) collision_preview.default_color = Color(1.0, 0.860, 0.260, 0.92) elif collision_preview != null: collision_preview.queue_free() func _leave_visited_room() -> void: if not SceneManager.return_from_room_visit(): SceneManager.change_scene("square") func _placement_bounds_for_item(item: Dictionary) -> Rect2: var surface := str(item.get("placement_surface", "floor")) return _placement_bounds_from_variant(item.get("placement_bounds", null), WALL_PLACE_BOUNDS if surface == "wall" else FLOOR_PLACE_BOUNDS) func _placement_bounds_for_surface(surface: String) -> Rect2: for definition_variant in _remoteDecorDefinitions.values(): if not (definition_variant is Dictionary): continue var definition: Dictionary = definition_variant as Dictionary if str(definition.get("placement_surface", "floor")) == surface: return _placement_bounds_from_variant(definition.get("placement_bounds", null), WALL_PLACE_BOUNDS if surface == "wall" else FLOOR_PLACE_BOUNDS) return WALL_PLACE_BOUNDS if surface == "wall" else FLOOR_PLACE_BOUNDS func _placement_bounds_from_variant(value: Variant, fallback: Rect2) -> Rect2: if not (value is Dictionary): return fallback var bounds: Dictionary = value as Dictionary var min_x := _to_float(bounds.get("min_x", fallback.position.x), fallback.position.x) var max_x := _to_float(bounds.get("max_x", fallback.end.x), fallback.end.x) var min_y := _to_float(bounds.get("min_y", fallback.position.y), fallback.position.y) var max_y := _to_float(bounds.get("max_y", fallback.end.y), fallback.end.y) if min_x >= max_x or min_y >= max_y: return fallback return Rect2(min_x, min_y, max_x - min_x, max_y - min_y) func _constrain_room_position(position: Vector2, item: Dictionary) -> Vector2: var bounds := _placement_bounds_for_item(item) return Vector2( clampf(position.x, bounds.position.x, bounds.end.x), clampf(position.y, bounds.position.y, bounds.end.y) ) func _snap_to_room_grid(position: Vector2, item: Dictionary) -> Vector2: var constrained := _constrain_room_position(position, item) var snapped := Vector2( roundf(constrained.x / ROOM_GRID_SIZE) * ROOM_GRID_SIZE, roundf(constrained.y / ROOM_GRID_SIZE) * ROOM_GRID_SIZE ) return _constrain_room_position(snapped, item) 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: if _decorCollisionRegistry != null: _decorCollisionRegistry.call("sync", decorId, item) func _set_decor_collision_disabled(decorId: String, disabled: bool) -> void: if _decorCollisionRegistry != null: _decorCollisionRegistry.call("set_disabled", decorId, disabled) func _remove_decor_collision(decorId: String) -> void: if _decorCollisionRegistry != null: _decorCollisionRegistry.call("remove", 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 _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