1557 lines
60 KiB
GDScript
1557 lines
60 KiB
GDScript
class_name PersonalSpace
|
||
extends Node2D
|
||
|
||
# ============================================================================
|
||
# PersonalSpace.gd - 个人空间场景控制器
|
||
# ============================================================================
|
||
# 独立于广场的私人房间空间。当前版本负责固定镜头、玩家出生点和
|
||
# 后续房间 manifest/装修系统的场景承载。
|
||
# ============================================================================
|
||
|
||
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
|
||
const CAMERA_LIMIT_LEFT: int = -538
|
||
const CAMERA_LIMIT_TOP: int = -358
|
||
const CAMERA_LIMIT_RIGHT: int = 538
|
||
const CAMERA_LIMIT_BOTTOM: int = 358
|
||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||
|
||
const TEXT_COLOR: Color = Color(0.102, 0.224, 0.376)
|
||
const PANEL_COLOR: Color = Color(0.970, 0.992, 1.0, 0.96)
|
||
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
|
||
const MUTED_COLOR: Color = Color(0.400, 0.500, 0.620)
|
||
const DECOR_TEXTURE_FILTER: int = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
const DECOR_COLLISION_LAYER: int = 1
|
||
const DECOR_COLLISION_MASK: int = 1
|
||
const ROOM_GRID_SIZE: float = 16.0
|
||
const FLOOR_PLACE_BOUNDS: Rect2 = Rect2(-426.0, -186.0, 852.0, 398.0)
|
||
const WALL_PLACE_BOUNDS: Rect2 = Rect2(-378.0, -258.0, 756.0, 72.0)
|
||
|
||
@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 _inventoryRequest: HTTPRequest
|
||
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 _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 _showPlacementGrid: 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()
|
||
_build_decor_layer()
|
||
_build_room_decor_requests()
|
||
_build_room_decor_ui()
|
||
_connect_room_decor_events()
|
||
_fetch_room_decor_inventory()
|
||
_discover_destination()
|
||
set_process(false)
|
||
|
||
func _discover_destination() -> void:
|
||
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 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
|
||
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)
|
||
_add_grid_zone(FLOOR_PLACE_BOUNDS, Color(0.050, 0.560, 0.900, 0.20), Color(0.050, 0.560, 0.900, 0.62))
|
||
_add_grid_zone(WALL_PLACE_BOUNDS, 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_requests() -> void:
|
||
_inventoryRequest = HTTPRequest.new()
|
||
_inventoryRequest.name = "roomDecorInventoryRequest"
|
||
_inventoryRequest.timeout = 12.0
|
||
_inventoryRequest.request_completed.connect(_on_inventory_request_completed)
|
||
add_child(_inventoryRequest)
|
||
|
||
func _build_room_decor_ui() -> void:
|
||
_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)
|
||
|
||
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 err := _inventoryRequest.request("%s%s" % [NetworkConfig.get_api_base_url(), endpoint], _auth_headers(), HTTPClient.METHOD_GET, "")
|
||
if err != OK:
|
||
_inventoryRequestInFlight = false
|
||
if _inventoryRequestReportsErrors:
|
||
_show_toast("家具背包请求发送失败")
|
||
_inventoryRequestReportsErrors = false
|
||
|
||
func _on_inventory_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||
_inventoryRequestInFlight = false
|
||
var reportErrors := _inventoryRequestReportsErrors
|
||
_inventoryRequestReportsErrors = false
|
||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||
if reportErrors:
|
||
_show_toast("家具背包读取失败")
|
||
return
|
||
var json := JSON.new()
|
||
if json.parse(body.get_string_from_utf8()) != OK:
|
||
if reportErrors:
|
||
_show_toast("家具背包响应解析失败")
|
||
return
|
||
var responseVariant: Variant = json.data
|
||
if not (responseVariant is Dictionary):
|
||
if reportErrors:
|
||
_show_toast("家具背包响应格式错误")
|
||
return
|
||
var response: Dictionary = responseVariant
|
||
if not bool(response.get("success", true)):
|
||
if reportErrors:
|
||
_show_toast(str(response.get("message", "家具背包读取失败")))
|
||
return
|
||
var dataVariant: Variant = response.get("data", {})
|
||
if dataVariant is Dictionary:
|
||
_apply_inventory_payload(dataVariant as Dictionary)
|
||
|
||
func _apply_inventory_payload(data: Dictionary) -> void:
|
||
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:
|
||
_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
|
||
|
||
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)
|
||
status.text = "已摆放" if bool(item.get("placed", false)) else "未摆放"
|
||
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||
status.add_theme_font_size_override("font_size", 14)
|
||
status.add_theme_color_override("font_color", ACCENT_COLOR if bool(item.get("placed", false)) else MUTED_COLOR)
|
||
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
content.add_child(status)
|
||
|
||
var icon := TextureRect.new()
|
||
icon.texture = _load_texture(str(item.get("icon", "")))
|
||
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||
icon.custom_minimum_size = Vector2(0, 82)
|
||
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
content.add_child(icon)
|
||
|
||
var nameLabel := Label.new()
|
||
nameLabel.text = str(item.get("name", decorId))
|
||
nameLabel.custom_minimum_size = Vector2(0, 36)
|
||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
nameLabel.add_theme_font_size_override("font_size", 17)
|
||
nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
content.add_child(nameLabel)
|
||
|
||
return card
|
||
|
||
func _render_placed_decors() -> void:
|
||
for decorIdVariant in _decorNodes.keys().duplicate():
|
||
var decorId := str(decorIdVariant)
|
||
var item: Dictionary = _decorItems.get(decorId, {})
|
||
if item.is_empty() or not bool(item.get("placed", false)):
|
||
var node := _decorNodes[decorId] as Node
|
||
if is_instance_valid(node):
|
||
node.queue_free()
|
||
_decorNodes.erase(decorId)
|
||
_remove_decor_collision(decorId)
|
||
for decorId in _decorItems.keys():
|
||
var item: Dictionary = _decorItems[decorId]
|
||
if bool(item.get("placed", false)):
|
||
_ensure_decor_node(str(decorId), item)
|
||
_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 _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
|
||
_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 = ""
|
||
_undoHistory.clear()
|
||
_redoHistory.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 _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 _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 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
|
||
_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:
|
||
return WALL_PLACE_BOUNDS if str(item.get("placement_surface", "floor")) == "wall" else FLOOR_PLACE_BOUNDS
|
||
|
||
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:
|
||
var collisionSize := _variant_to_vector2(item.get("collision_size", Vector2.ZERO))
|
||
if collisionSize == Vector2.ZERO:
|
||
_remove_decor_collision(decorId)
|
||
return
|
||
var collisionOffset := _variant_to_vector2(item.get("collision_offset", Vector2.ZERO))
|
||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||
var shapeNode: CollisionShape2D
|
||
if body == null or not is_instance_valid(body):
|
||
body = StaticBody2D.new()
|
||
body.name = "DecorCollision_%s" % decorId
|
||
body.collision_layer = DECOR_COLLISION_LAYER
|
||
body.collision_mask = DECOR_COLLISION_MASK
|
||
shapeNode = CollisionShape2D.new()
|
||
shapeNode.name = "CollisionShape2D"
|
||
var rectShape := RectangleShape2D.new()
|
||
shapeNode.shape = rectShape
|
||
body.add_child(shapeNode)
|
||
staticCollision.add_child(body)
|
||
_decorCollisionBodies[decorId] = body
|
||
else:
|
||
shapeNode = body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||
if shapeNode == null:
|
||
shapeNode = CollisionShape2D.new()
|
||
shapeNode.name = "CollisionShape2D"
|
||
shapeNode.shape = RectangleShape2D.new()
|
||
body.add_child(shapeNode)
|
||
var scale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
|
||
var rotation_degrees := _get_item_float(item, "rotation_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:
|
||
rectShape = RectangleShape2D.new()
|
||
shapeNode.shape = rectShape
|
||
rectShape.size = Vector2(max(8.0, collisionSize.x * scale), max(8.0, collisionSize.y * scale))
|
||
|
||
func _set_decor_collision_disabled(decorId: String, disabled: bool) -> void:
|
||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||
if body == null or not is_instance_valid(body):
|
||
return
|
||
var shapeNode := body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||
if shapeNode != null:
|
||
shapeNode.disabled = disabled
|
||
|
||
func _remove_decor_collision(decorId: String) -> void:
|
||
var body := _decorCollisionBodies.get(decorId, null) as Node
|
||
if body != null and is_instance_valid(body):
|
||
body.queue_free()
|
||
_decorCollisionBodies.erase(decorId)
|
||
|
||
func _variant_to_vector2(value: Variant) -> Vector2:
|
||
if value is Vector2:
|
||
return value
|
||
if value is Dictionary:
|
||
var dict: Dictionary = value
|
||
return Vector2(_to_float(dict.get("x", 0.0), 0.0), _to_float(dict.get("y", 0.0), 0.0))
|
||
return Vector2.ZERO
|
||
|
||
func _to_float(value: Variant, fallback: float) -> float:
|
||
match typeof(value):
|
||
TYPE_FLOAT:
|
||
return value
|
||
TYPE_INT:
|
||
return value
|
||
TYPE_STRING:
|
||
var text := str(value).strip_edges()
|
||
return fallback if text.is_empty() else text.to_float()
|
||
TYPE_BOOL:
|
||
return 1.0 if bool(value) else 0.0
|
||
_:
|
||
return fallback
|
||
|
||
func _to_int(value: Variant, fallback: int) -> int:
|
||
match typeof(value):
|
||
TYPE_INT:
|
||
return value
|
||
TYPE_FLOAT:
|
||
return roundi(value)
|
||
TYPE_STRING:
|
||
var text := str(value).strip_edges()
|
||
return fallback if text.is_empty() else text.to_int()
|
||
TYPE_BOOL:
|
||
return 1 if bool(value) else 0
|
||
_:
|
||
return fallback
|
||
|
||
func _get_item_float(item: Dictionary, key: String, fallback: float) -> float:
|
||
return _to_float(item.get(key, fallback), fallback)
|
||
|
||
func _get_item_int(item: Dictionary, key: String, fallback: int) -> int:
|
||
return _to_int(item.get(key, fallback), fallback)
|
||
|
||
func _is_authenticated() -> bool:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated"))
|
||
|
||
func _auth_headers() -> PackedStringArray:
|
||
var authManager := get_node_or_null("/root/AuthManager")
|
||
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else ""
|
||
return PackedStringArray([
|
||
"Content-Type: application/json",
|
||
"Authorization: Bearer %s" % accessToken,
|
||
])
|
||
|
||
func _load_texture(path: String) -> Texture2D:
|
||
if path.is_empty():
|
||
return null
|
||
if FileAccess.file_exists("%s.import" % path):
|
||
var texture := load(path) as Texture2D
|
||
if texture != null:
|
||
return texture
|
||
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
|
||
if image == null or image.is_empty():
|
||
return null
|
||
return ImageTexture.create_from_image(image)
|
||
|
||
func _get_event_system() -> Node:
|
||
return get_node_or_null("/root/EventSystem")
|
||
|
||
func _show_toast(message: String) -> void:
|
||
if not is_instance_valid(_toastLabel):
|
||
return
|
||
_toastLabel.text = message
|
||
_toastLabel.visible = true
|
||
var tween := create_tween()
|
||
tween.tween_interval(1.5)
|
||
tween.tween_callback(func() -> void:
|
||
if is_instance_valid(_toastLabel):
|
||
_toastLabel.visible = false
|
||
)
|
||
|
||
func _pulse_node(node: Node2D) -> void:
|
||
var originalScale := node.scale
|
||
var tween := create_tween()
|
||
tween.tween_property(node, "scale", originalScale * 1.08, 0.08)
|
||
tween.tween_property(node, "scale", originalScale, 0.10)
|
||
|
||
func _clear_children(node: Node) -> void:
|
||
for child in node.get_children():
|
||
child.queue_free()
|
||
|
||
func _create_panel_style(color: Color, radius: int, shadow: bool) -> StyleBoxFlat:
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = color
|
||
style.corner_radius_top_left = radius
|
||
style.corner_radius_top_right = radius
|
||
style.corner_radius_bottom_left = radius
|
||
style.corner_radius_bottom_right = radius
|
||
style.content_margin_left = 12
|
||
style.content_margin_top = 8
|
||
style.content_margin_right = 12
|
||
style.content_margin_bottom = 8
|
||
if shadow:
|
||
style.shadow_color = Color(0.082, 0.243, 0.380, 0.18)
|
||
style.shadow_size = 12
|
||
style.shadow_offset = Vector2(0, 4)
|
||
return style
|
||
|
||
func _create_inventory_card_style(hovered: bool) -> StyleBoxFlat:
|
||
var style := _create_panel_style(Color(0.982, 0.995, 1.0, 1.0), 18, true)
|
||
style.border_width_left = 2
|
||
style.border_width_top = 2
|
||
style.border_width_right = 2
|
||
style.border_width_bottom = 2
|
||
style.border_color = Color(0.650, 0.835, 0.965, 0.90) if not hovered else Color(0.086, 0.690, 0.960, 1.0)
|
||
style.shadow_color = Color(0.086, 0.314, 0.520, 0.14 if hovered else 0.08)
|
||
style.shadow_size = 14 if hovered else 8
|
||
style.content_margin_left = 0
|
||
style.content_margin_top = 0
|
||
style.content_margin_right = 0
|
||
style.content_margin_bottom = 0
|
||
return style
|