@@ -22,6 +22,8 @@ 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 ROOM_PLACE_BOUNDS : Rect2 = Rect2 ( - 426.0 , - 186.0 , 852.0 , 398.0 )
@ onready var player : PlayerController = $ YSortWorld / Characters / Players / Player
@ onready var playerCamera : Camera2D = $ YSortWorld / Characters / Players / Player / Camera2D
@@ -35,18 +37,48 @@ 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 _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 _decorCollisionBodies : Dictionary = { }
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 _undoHistory : Array [ Dictionary ] = [ ]
var _redoHistory : Array [ Dictionary ] = [ ]
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 ( )
@@ -59,6 +91,8 @@ func _ready() -> void:
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 "
@@ -81,11 +115,15 @@ func _exit_tree() -> void:
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 is_instance_valid ( _inventoryRequest ) :
_inventoryRequest . cancel_request ( )
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
@@ -104,17 +142,23 @@ func _process(_delta: float) -> void:
_update_dragged_decor_position ( )
func is_escape_dismissible ( ) - > bool :
return _draggedDecor != null or ( is_instance_valid ( _inventoryPanel ) and _inventoryPanel . visible )
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 :
_finish _decor_drag ( )
_cancel _decor_drag ( )
return
if is_instance_valid ( _inventoryPanel ) :
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 :
@@ -126,6 +170,12 @@ func _apply_spawn_point() -> void:
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
@@ -150,6 +200,8 @@ func _build_room_decor_requests() -> void:
add_child ( _inventoryRequest )
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 )
@@ -223,19 +275,6 @@ func _build_room_decor_ui() -> void:
_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
@@ -257,6 +296,146 @@ func _build_room_decor_ui() -> void:
_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 )
_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 ( _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 :
@@ -264,18 +443,27 @@ func _connect_room_decor_events() -> void:
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 _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 )
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 :
if _inventoryPanel . visible and not _inventoryLoaded :
_fetch_room_decor_inventory ( true )
func _fetch_room_decor_inventory ( reportErrors : bool = false ) - > void :
@@ -289,7 +477,8 @@ func _fetch_room_decor_inventory(reportErrors: bool = false) -> void:
return
_inventoryRequestInFlight = true
_inventoryRequestReportsErrors = reportErrors
var err : = _inventoryRequest . request ( " %s /rooms/me /decor-placements " % NetworkConfig . get_api_base_url ( ) , _auth_headers ( ) , HTTPClient . METHOD_GET , " " )
var endpoint : String = " /rooms/%s /decor-placements " % _visitorOwnerId if _visitorMode else " /rooms/me/decor-placements "
var err : = _inventoryRequest . request ( " %s %s " % [ NetworkConfig . get_api_base_url ( ) , endpoint ] , _auth_headers ( ) , HTTPClient . METHOD_GET , " " )
if err != OK :
_inventoryRequestInFlight = false
if _inventoryRequestReportsErrors :
@@ -324,7 +513,16 @@ func _on_inventory_request_completed(result: int, responseCode: int, _headers: P
_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 :
@@ -334,13 +532,23 @@ func _apply_inventory_payload(data: Dictionary) -> void:
var decorId : = str ( item . get ( " decor_id " , " " ) ) . strip_edges ( )
if not decorId . is_empty ( ) :
_decorItems [ decorId ] = _merge_decor_definition ( item )
if not _visitorMode :
_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 ( )
@@ -370,6 +578,8 @@ func _apply_remote_decor_definitions(definitionsVariant: Variant) -> void:
_remoteDecorDefinitions [ decorId ] = definition
func _render_inventory_list ( ) - > void :
if _visitorMode :
return
_clear_children ( _inventoryGrid )
if _decorItems . is_empty ( ) :
var empty : = _create_empty_inventory_card ( )
@@ -471,11 +681,13 @@ func _render_placed_decors() -> void:
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 _decorItems . has ( decorId ) :
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 :
@@ -487,6 +699,7 @@ func _begin_inventory_decor_drag(decorId: String) -> void:
_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 :
@@ -497,6 +710,7 @@ func _ensure_decor_placed_for_drag(decorId: String) -> Dictionary:
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
@@ -517,11 +731,14 @@ func _ensure_decor_node(decorId: String, item: Dictionary) -> Sprite2D:
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 ( ) :
@@ -537,10 +754,13 @@ func _try_begin_decor_drag(worldPosition: Vector2) -> bool:
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 :
@@ -550,23 +770,30 @@ func _finish_decor_drag() -> void:
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
var snapped_position : = _snap_to_room_grid ( _draggedDecor . global_position )
_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 )
_save_d ecor_item( item )
_r ecord _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 :
_draggedDecor . global_position = get_global_mouse_position ( ) + _dragOffset
_draggedDecor . global_position = _constrain_room_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 ]
@@ -574,6 +801,21 @@ func _update_dragged_decor_position() -> void:
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
@@ -618,10 +860,14 @@ func _control_blocks_pointer(control: Control, mousePosition: Vector2) -> bool:
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 ) :
@@ -630,28 +876,433 @@ func _remove_selected_decor() -> void:
node . queue_free ( )
_decorNodes . erase ( _selectedDecorId )
_remove_decor_collision ( _selectedDecorId )
_save_d ecor_item( item )
_r ecord _item_change ( before , item )
_mark_decor_dirty ( _selectedDecorId )
_render_inventory_list ( )
_refresh_decor_selection ( )
_update_editor_toolbar ( )
_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
if decorId . is_empty ( ) or not _saveAwaitingIds . has ( decorId ) :
return
_serverDecorItems [ decorId ] = item . duplicate ( true )
_saveAwaitingIds . erase ( decorId )
if _decorItems . has ( decorId ) and _placement_signature ( _decorItems [ decorId ] ) == _placement_signature ( item ) :
_dirtyDecorIds . erase ( decorId )
else :
_dirtyDecorIds [ decorId ] = true
_finalize_save_if_ready ( )
func _on_decor_save_failed ( _item : Dictionary , message : String ) - > void :
_show_toast ( message )
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 = " "
_undoHistory . clear ( )
_redoHistory . clear ( )
_resetArmed = false
_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 = " "
_undoHistory . clear ( )
_redoHistory . clear ( )
_resetArmed = false
_exitAfterSave = false
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 ( _finishEditButton ) :
_finishEditButton . visible = _editMode
_finishEditButton . disabled = _saveInProgress
if is_instance_valid ( _undoButton ) :
_undoButton . visible = _editMode
_undoButton . disabled = not _can_edit_layout ( ) or _undoHistory . is_empty ( )
if is_instance_valid ( _redoButton ) :
_redoButton . visible = _editMode
_redoButton . disabled = not _can_edit_layout ( ) or _redoHistory . is_empty ( )
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 _record_item_change ( before : Dictionary , after : Dictionary ) - > void :
if before == after :
return
var decor_id : String = str ( after . get ( " decor_id " , before . get ( " decor_id " , " " ) ) ) . strip_edges ( )
if decor_id . is_empty ( ) :
return
_undoHistory . append ( {
" decor_id " : decor_id ,
" before " : before . duplicate ( true ) ,
" after " : after . duplicate ( true ) ,
} )
_redoHistory . clear ( )
_update_editor_toolbar ( )
func _record_bulk_change ( before_items : Dictionary , after_items : Dictionary ) - > void :
if before_items == after_items :
return
_undoHistory . append ( {
" before_items " : _duplicate_item_map ( before_items ) ,
" after_items " : _duplicate_item_map ( after_items ) ,
} )
_redoHistory . clear ( )
_update_editor_toolbar ( )
func _undo_last_change ( ) - > void :
if not _can_edit_layout ( ) or _undoHistory . is_empty ( ) :
return
var entry : Dictionary = _undoHistory . pop_back ( )
_apply_history_entry ( entry , false )
_redoHistory . append ( entry )
_update_editor_toolbar ( )
func _redo_last_change ( ) - > void :
if not _can_edit_layout ( ) or _redoHistory . is_empty ( ) :
return
var entry : Dictionary = _redoHistory . pop_back ( )
_apply_history_entry ( entry , true )
_undoHistory . append ( entry )
_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 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
_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 _constrain_room_position ( position : Vector2 ) - > Vector2 :
return Vector2 (
clampf ( position . x , ROOM_PLACE_BOUNDS . position . x , ROOM_PLACE_BOUNDS . end . x ) ,
clampf ( position . y , ROOM_PLACE_BOUNDS . position . y , ROOM_PLACE_BOUNDS . end . y )
)
func _snap_to_room_grid ( position : Vector2 ) - > Vector2 :
var constrained : = _constrain_room_position ( position )
return Vector2 (
roundf ( constrained . x / ROOM_GRID_SIZE ) * ROOM_GRID_SIZE ,
roundf ( constrained . y / ROOM_GRID_SIZE ) * ROOM_GRID_SIZE
)
func _is_point_inside_sprite ( node : Sprite2D , worldPosition : Vector2 ) - > bool :
if node . texture == null :
@@ -689,7 +1340,9 @@ func _sync_decor_collision(decorId: String, item: Dictionary) -> void:
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 ( " posi tion_x " , 0.0 ) , 0.0 ) , _to_float ( item . get ( " position_y " , 0.0 ) , 0.0 ) ) + collisionOffset * scale
var rotation_degrees : = _get_item _float( item , " rota tion_degrees " , _get_item_float ( item , " default_rotation_degrees " , 0.0 ) )
body . rotation_degrees = rotation_degrees
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 . rotated ( deg_to_rad ( rotation_degrees ) ) * scale
shapeNode . position = Vector2 . ZERO
var rectShape : = shapeNode . shape as RectangleShape2D
if rectShape == null :