forked from xiangwang25/whale-town-front-v2
Initial WhaleTown V2 frontend
This commit is contained in:
808
scenes/Maps/PersonalSpace.gd
Normal file
808
scenes/Maps/PersonalSpace.gd
Normal file
@@ -0,0 +1,808 @@
|
||||
class_name PersonalSpace
|
||||
extends Node2D
|
||||
|
||||
# ============================================================================
|
||||
# PersonalSpace.gd - 个人空间场景控制器
|
||||
# ============================================================================
|
||||
# 独立于广场的私人房间空间。当前版本负责固定镜头、玩家出生点和
|
||||
# 后续房间 manifest/装修系统的场景承载。
|
||||
# ============================================================================
|
||||
|
||||
const CAMERA_ZOOM: Vector2 = Vector2(1.65, 1.65)
|
||||
const CAMERA_LIMIT_LEFT: int = -538
|
||||
const CAMERA_LIMIT_TOP: int = -358
|
||||
const CAMERA_LIMIT_RIGHT: int = 538
|
||||
const CAMERA_LIMIT_BOTTOM: int = 358
|
||||
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
|
||||
|
||||
const TEXT_COLOR: Color = Color(0.102, 0.224, 0.376)
|
||||
const PANEL_COLOR: Color = Color(0.970, 0.992, 1.0, 0.96)
|
||||
const ACCENT_COLOR: Color = Color(0.086, 0.608, 0.922)
|
||||
const MUTED_COLOR: Color = Color(0.400, 0.500, 0.620)
|
||||
const DECOR_TEXTURE_FILTER: int = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
const DECOR_COLLISION_LAYER: int = 1
|
||||
const DECOR_COLLISION_MASK: int = 1
|
||||
|
||||
@onready var player: PlayerController = $YSortWorld/Characters/Players/Player
|
||||
@onready var playerCamera: Camera2D = $YSortWorld/Characters/Players/Player/Camera2D
|
||||
@onready var uiLayer: CanvasLayer = $UILayer
|
||||
@onready var ySortWorld: Node2D = $YSortWorld
|
||||
@onready var staticCollision: Node2D = $StaticCollision
|
||||
|
||||
var _decorLayer: Node2D
|
||||
var _inventoryRequest: HTTPRequest
|
||||
var _inventoryPanel: PanelContainer
|
||||
var _inventoryGrid: GridContainer
|
||||
var _dragSurface: Control
|
||||
var _toastLabel: Label
|
||||
var _selectedDecorId: String = ""
|
||||
var _draggedDecor: Sprite2D
|
||||
var _dragOffset: Vector2 = Vector2.ZERO
|
||||
var _decorItems: Dictionary = {}
|
||||
var _decorNodes: Dictionary = {}
|
||||
var _decorCollisionBodies: Dictionary = {}
|
||||
var _remoteDecorDefinitions: Dictionary = {}
|
||||
var _inventoryRequestInFlight: bool = false
|
||||
var _inventoryRequestReportsErrors: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_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()
|
||||
set_process(false)
|
||||
|
||||
func _leave_multiplayer_world() -> void:
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
if chatManager != null and chatManager.has_method("leave_world"):
|
||||
chatManager.call("leave_world", "personal_space")
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem != null:
|
||||
eventSystem.call("disconnect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager != null:
|
||||
if saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
|
||||
saveManager.decor_save_succeeded.disconnect(_on_decor_save_succeeded)
|
||||
if saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
|
||||
saveManager.decor_save_failed.disconnect(_on_decor_save_failed)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
if _draggedDecor != null and not mouseEvent.pressed:
|
||||
_finish_decor_drag()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _draggedDecor == null and mouseEvent.pressed and not _is_pointer_over_blocking_ui():
|
||||
if _try_begin_decor_drag(get_global_mouse_position()):
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _draggedDecor != null:
|
||||
_update_dragged_decor_position()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _draggedDecor != null:
|
||||
_update_dragged_decor_position()
|
||||
|
||||
func _apply_spawn_point() -> void:
|
||||
var spawnName: String = SceneManager.get_next_spawn_name()
|
||||
var markerName: String = spawnName if not spawnName.is_empty() else "DefaultSpawn"
|
||||
var marker := $Markers.get_node_or_null(markerName) as Marker2D
|
||||
if marker == null:
|
||||
marker = $Markers/DefaultSpawn
|
||||
player.global_position = marker.global_position
|
||||
|
||||
func _configure_camera() -> void:
|
||||
playerCamera.zoom = CAMERA_ZOOM
|
||||
playerCamera.position_smoothing_enabled = true
|
||||
playerCamera.limit_left = CAMERA_LIMIT_LEFT
|
||||
playerCamera.limit_top = CAMERA_LIMIT_TOP
|
||||
playerCamera.limit_right = CAMERA_LIMIT_RIGHT
|
||||
playerCamera.limit_bottom = CAMERA_LIMIT_BOTTOM
|
||||
playerCamera.limit_smoothed = true
|
||||
|
||||
func _build_decor_layer() -> void:
|
||||
_decorLayer = Node2D.new()
|
||||
_decorLayer.name = "RoomDecorLayer"
|
||||
_decorLayer.y_sort_enabled = true
|
||||
ySortWorld.add_child(_decorLayer)
|
||||
ySortWorld.move_child(_decorLayer, 0)
|
||||
|
||||
func _build_room_decor_requests() -> void:
|
||||
_inventoryRequest = HTTPRequest.new()
|
||||
_inventoryRequest.name = "roomDecorInventoryRequest"
|
||||
_inventoryRequest.timeout = 12.0
|
||||
_inventoryRequest.request_completed.connect(_on_inventory_request_completed)
|
||||
add_child(_inventoryRequest)
|
||||
|
||||
func _build_room_decor_ui() -> void:
|
||||
_inventoryPanel = PanelContainer.new()
|
||||
_inventoryPanel.visible = false
|
||||
_inventoryPanel.custom_minimum_size = Vector2(560, 560)
|
||||
_inventoryPanel.offset_left = 24
|
||||
_inventoryPanel.offset_top = 116
|
||||
_inventoryPanel.offset_right = 584
|
||||
_inventoryPanel.offset_bottom = 676
|
||||
_inventoryPanel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_inventoryPanel.add_theme_stylebox_override("panel", _create_panel_style(PANEL_COLOR, 26, true))
|
||||
uiLayer.add_child(_inventoryPanel)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 24)
|
||||
margin.add_theme_constant_override("margin_top", 22)
|
||||
margin.add_theme_constant_override("margin_right", 24)
|
||||
margin.add_theme_constant_override("margin_bottom", 22)
|
||||
_inventoryPanel.add_child(margin)
|
||||
|
||||
var root := VBoxContainer.new()
|
||||
root.add_theme_constant_override("separation", 14)
|
||||
margin.add_child(root)
|
||||
|
||||
var header := HBoxContainer.new()
|
||||
header.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
root.add_child(header)
|
||||
|
||||
var titleBox := VBoxContainer.new()
|
||||
titleBox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
titleBox.add_theme_constant_override("separation", 2)
|
||||
header.add_child(titleBox)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "背包"
|
||||
title.add_theme_font_size_override("font_size", 28)
|
||||
title.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
titleBox.add_child(title)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.text = "按住装饰品,拖到房间里松手摆放。"
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
hint.add_theme_font_size_override("font_size", 16)
|
||||
hint.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
titleBox.add_child(hint)
|
||||
|
||||
var closeButton := Button.new()
|
||||
closeButton.text = "×"
|
||||
closeButton.custom_minimum_size = Vector2(48, 48)
|
||||
closeButton.focus_mode = Control.FOCUS_NONE
|
||||
closeButton.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
closeButton.add_theme_font_size_override("font_size", 30)
|
||||
closeButton.add_theme_color_override("font_color", Color.WHITE)
|
||||
closeButton.add_theme_stylebox_override("normal", _create_panel_style(ACCENT_COLOR, 18, false))
|
||||
closeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.150, 0.680, 1.0, 1.0), 18, false))
|
||||
closeButton.add_theme_stylebox_override("pressed", _create_panel_style(Color(0.060, 0.420, 0.780, 1.0), 18, false))
|
||||
closeButton.pressed.connect(func() -> void:
|
||||
_inventoryPanel.visible = false
|
||||
)
|
||||
header.add_child(closeButton)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.custom_minimum_size = Vector2(0, 370)
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
root.add_child(scroll)
|
||||
|
||||
_inventoryGrid = GridContainer.new()
|
||||
_inventoryGrid.columns = 2
|
||||
_inventoryGrid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_inventoryGrid.add_theme_constant_override("h_separation", 14)
|
||||
_inventoryGrid.add_theme_constant_override("v_separation", 14)
|
||||
scroll.add_child(_inventoryGrid)
|
||||
|
||||
var removeButton := Button.new()
|
||||
removeButton.text = "收回选中家具"
|
||||
removeButton.custom_minimum_size = Vector2(0, 46)
|
||||
removeButton.add_theme_font_size_override("font_size", 17)
|
||||
removeButton.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
removeButton.add_theme_color_override("font_hover_color", ACCENT_COLOR)
|
||||
removeButton.add_theme_color_override("font_pressed_color", Color(0.060, 0.270, 0.540, 1.0))
|
||||
removeButton.add_theme_color_override("font_disabled_color", Color(0.520, 0.600, 0.680, 0.55))
|
||||
removeButton.add_theme_stylebox_override("normal", _create_panel_style(Color(0.900, 0.960, 1.0, 0.92), 16, false))
|
||||
removeButton.add_theme_stylebox_override("hover", _create_panel_style(Color(0.820, 0.925, 1.0, 1.0), 16, false))
|
||||
removeButton.pressed.connect(_remove_selected_decor)
|
||||
root.add_child(removeButton)
|
||||
|
||||
_toastLabel = Label.new()
|
||||
_toastLabel.visible = false
|
||||
_toastLabel.offset_left = 360
|
||||
_toastLabel.offset_top = 100
|
||||
_toastLabel.offset_right = 760
|
||||
_toastLabel.offset_bottom = 148
|
||||
_toastLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_toastLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_toastLabel.add_theme_font_size_override("font_size", 18)
|
||||
_toastLabel.add_theme_color_override("font_color", Color.WHITE)
|
||||
_toastLabel.add_theme_stylebox_override("normal", _create_panel_style(Color(0.028, 0.160, 0.310, 0.90), 18, false))
|
||||
uiLayer.add_child(_toastLabel)
|
||||
|
||||
_dragSurface = Control.new()
|
||||
_dragSurface.name = "RoomDecorDragSurface"
|
||||
_dragSurface.visible = false
|
||||
_dragSurface.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_dragSurface.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
_dragSurface.gui_input.connect(_on_drag_surface_gui_input)
|
||||
uiLayer.add_child(_dragSurface)
|
||||
|
||||
func _connect_room_decor_events() -> void:
|
||||
var eventSystem := _get_event_system()
|
||||
if eventSystem == null:
|
||||
push_warning("PersonalSpace: EventSystem autoload is not available.")
|
||||
else:
|
||||
eventSystem.call("connect_event", EventNames.HUD_BACKPACK_TOGGLE, _on_backpack_toggle_requested, self)
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager != null:
|
||||
if not saveManager.decor_save_succeeded.is_connected(_on_decor_save_succeeded):
|
||||
saveManager.decor_save_succeeded.connect(_on_decor_save_succeeded)
|
||||
if not saveManager.decor_save_failed.is_connected(_on_decor_save_failed):
|
||||
saveManager.decor_save_failed.connect(_on_decor_save_failed)
|
||||
|
||||
func _on_backpack_toggle_requested(_data: Variant = null) -> void:
|
||||
_toggle_inventory_panel()
|
||||
|
||||
func _toggle_inventory_panel() -> void:
|
||||
_inventoryPanel.visible = not _inventoryPanel.visible
|
||||
if _inventoryPanel.visible:
|
||||
_fetch_room_decor_inventory(true)
|
||||
|
||||
func _fetch_room_decor_inventory(reportErrors: bool = false) -> void:
|
||||
if _inventoryRequestInFlight:
|
||||
_inventoryRequestReportsErrors = _inventoryRequestReportsErrors or reportErrors
|
||||
return
|
||||
if not _is_authenticated():
|
||||
_apply_local_empty_inventory()
|
||||
if reportErrors:
|
||||
_show_toast("请先登录后使用家具背包")
|
||||
return
|
||||
_inventoryRequestInFlight = true
|
||||
_inventoryRequestReportsErrors = reportErrors
|
||||
var err := _inventoryRequest.request("%s/rooms/me/decor-placements" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_GET, "")
|
||||
if err != OK:
|
||||
_inventoryRequestInFlight = false
|
||||
if _inventoryRequestReportsErrors:
|
||||
_show_toast("家具背包请求发送失败")
|
||||
_inventoryRequestReportsErrors = false
|
||||
|
||||
func _on_inventory_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
_inventoryRequestInFlight = false
|
||||
var reportErrors := _inventoryRequestReportsErrors
|
||||
_inventoryRequestReportsErrors = false
|
||||
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
|
||||
if reportErrors:
|
||||
_show_toast("家具背包读取失败")
|
||||
return
|
||||
var json := JSON.new()
|
||||
if json.parse(body.get_string_from_utf8()) != OK:
|
||||
if reportErrors:
|
||||
_show_toast("家具背包响应解析失败")
|
||||
return
|
||||
var responseVariant: Variant = json.data
|
||||
if not (responseVariant is Dictionary):
|
||||
if reportErrors:
|
||||
_show_toast("家具背包响应格式错误")
|
||||
return
|
||||
var response: Dictionary = responseVariant
|
||||
if not bool(response.get("success", true)):
|
||||
if reportErrors:
|
||||
_show_toast(str(response.get("message", "家具背包读取失败")))
|
||||
return
|
||||
var dataVariant: Variant = response.get("data", {})
|
||||
if dataVariant is Dictionary:
|
||||
_apply_inventory_payload(dataVariant as Dictionary)
|
||||
|
||||
func _apply_inventory_payload(data: Dictionary) -> void:
|
||||
_decorItems.clear()
|
||||
_apply_remote_decor_definitions(data.get("definitions", []))
|
||||
var itemsVariant: Variant = data.get("items", [])
|
||||
if itemsVariant is Array:
|
||||
for itemVariant in itemsVariant:
|
||||
if itemVariant is Dictionary:
|
||||
var item: Dictionary = (itemVariant as Dictionary).duplicate(true)
|
||||
var decorId := str(item.get("decor_id", "")).strip_edges()
|
||||
if not decorId.is_empty():
|
||||
_decorItems[decorId] = _merge_decor_definition(item)
|
||||
_render_inventory_list()
|
||||
_render_placed_decors()
|
||||
|
||||
func _apply_local_empty_inventory() -> void:
|
||||
_decorItems.clear()
|
||||
_render_inventory_list()
|
||||
_render_placed_decors()
|
||||
|
||||
func _merge_decor_definition(item: Dictionary) -> Dictionary:
|
||||
var decorId := str(item.get("decor_id", "")).strip_edges()
|
||||
var definition: Dictionary = _remoteDecorDefinitions.get(decorId, {})
|
||||
for key in definition.keys():
|
||||
if not item.has(key):
|
||||
item[key] = definition[key]
|
||||
return item
|
||||
|
||||
func _apply_remote_decor_definitions(definitionsVariant: Variant) -> void:
|
||||
_remoteDecorDefinitions.clear()
|
||||
if not (definitionsVariant is Array):
|
||||
return
|
||||
for definitionVariant in definitionsVariant:
|
||||
if not (definitionVariant is Dictionary):
|
||||
continue
|
||||
var definition: Dictionary = (definitionVariant as Dictionary).duplicate(true)
|
||||
var decorId := str(definition.get("decor_id", "")).strip_edges()
|
||||
if decorId.is_empty():
|
||||
continue
|
||||
if definition.get("default_position", null) is Dictionary:
|
||||
var position: Dictionary = definition.get("default_position")
|
||||
definition["default_position"] = Vector2(
|
||||
_to_float(position.get("x", 0.0), 0.0),
|
||||
_to_float(position.get("y", 0.0), 0.0)
|
||||
)
|
||||
_remoteDecorDefinitions[decorId] = definition
|
||||
|
||||
func _render_inventory_list() -> void:
|
||||
_clear_children(_inventoryGrid)
|
||||
if _decorItems.is_empty():
|
||||
var empty := _create_empty_inventory_card()
|
||||
_inventoryGrid.add_child(empty)
|
||||
return
|
||||
for decorId in _decorItems.keys():
|
||||
_inventoryGrid.add_child(_create_inventory_card(str(decorId)))
|
||||
|
||||
func _create_empty_inventory_card() -> Control:
|
||||
var card := PanelContainer.new()
|
||||
card.custom_minimum_size = Vector2(500, 132)
|
||||
card.add_theme_stylebox_override("panel", _create_panel_style(Color(1, 1, 1, 0.76), 20, false))
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 18)
|
||||
margin.add_theme_constant_override("margin_top", 18)
|
||||
margin.add_theme_constant_override("margin_right", 18)
|
||||
margin.add_theme_constant_override("margin_bottom", 18)
|
||||
card.add_child(margin)
|
||||
var label := Label.new()
|
||||
label.text = "背包里还没有装饰品。去商城的空间分类购买后会出现在这里。"
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.add_theme_font_size_override("font_size", 17)
|
||||
label.add_theme_color_override("font_color", MUTED_COLOR)
|
||||
margin.add_child(label)
|
||||
return card
|
||||
|
||||
func _create_inventory_card(decorId: String) -> Button:
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
var card := Button.new()
|
||||
card.custom_minimum_size = Vector2(240, 176)
|
||||
card.focus_mode = Control.FOCUS_NONE
|
||||
card.mouse_default_cursor_shape = Control.CURSOR_DRAG
|
||||
card.add_theme_stylebox_override("normal", _create_inventory_card_style(false))
|
||||
card.add_theme_stylebox_override("hover", _create_inventory_card_style(true))
|
||||
card.add_theme_stylebox_override("pressed", _create_inventory_card_style(true))
|
||||
card.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
card.button_down.connect(func() -> void:
|
||||
_begin_inventory_decor_drag(decorId)
|
||||
)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 12)
|
||||
margin.add_theme_constant_override("margin_top", 12)
|
||||
margin.add_theme_constant_override("margin_right", 12)
|
||||
margin.add_theme_constant_override("margin_bottom", 12)
|
||||
margin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
card.add_child(margin)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
content.add_theme_constant_override("separation", 6)
|
||||
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
margin.add_child(content)
|
||||
|
||||
var status := Label.new()
|
||||
status.custom_minimum_size = Vector2(0, 22)
|
||||
status.text = "已摆放" if bool(item.get("placed", false)) else "未摆放"
|
||||
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
status.add_theme_font_size_override("font_size", 14)
|
||||
status.add_theme_color_override("font_color", ACCENT_COLOR if bool(item.get("placed", false)) else MUTED_COLOR)
|
||||
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(status)
|
||||
|
||||
var icon := TextureRect.new()
|
||||
icon.texture = _load_texture(str(item.get("icon", "")))
|
||||
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||
icon.custom_minimum_size = Vector2(0, 82)
|
||||
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(icon)
|
||||
|
||||
var nameLabel := Label.new()
|
||||
nameLabel.text = str(item.get("name", decorId))
|
||||
nameLabel.custom_minimum_size = Vector2(0, 36)
|
||||
nameLabel.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
nameLabel.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
nameLabel.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
nameLabel.add_theme_font_size_override("font_size", 17)
|
||||
nameLabel.add_theme_color_override("font_color", TEXT_COLOR)
|
||||
nameLabel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
content.add_child(nameLabel)
|
||||
|
||||
return card
|
||||
|
||||
func _render_placed_decors() -> void:
|
||||
for decorIdVariant in _decorNodes.keys().duplicate():
|
||||
var decorId := str(decorIdVariant)
|
||||
var item: Dictionary = _decorItems.get(decorId, {})
|
||||
if item.is_empty() or not bool(item.get("placed", false)):
|
||||
var node := _decorNodes[decorId] as Node
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_decorNodes.erase(decorId)
|
||||
_remove_decor_collision(decorId)
|
||||
for decorId in _decorItems.keys():
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
if bool(item.get("placed", false)):
|
||||
_ensure_decor_node(str(decorId), item)
|
||||
|
||||
func _begin_inventory_decor_drag(decorId: String) -> void:
|
||||
if not _decorItems.has(decorId):
|
||||
return
|
||||
_selectedDecorId = decorId
|
||||
var item := _ensure_decor_placed_for_drag(decorId)
|
||||
var node := _ensure_decor_node(decorId, item)
|
||||
if node == null:
|
||||
return
|
||||
_draggedDecor = node
|
||||
_dragOffset = Vector2.ZERO
|
||||
_set_decor_collision_disabled(decorId, true)
|
||||
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
|
||||
_inventoryPanel.visible = false
|
||||
_enable_drag_surface()
|
||||
_update_dragged_decor_position()
|
||||
_pulse_node(_draggedDecor)
|
||||
|
||||
func _ensure_decor_placed_for_drag(decorId: String) -> Dictionary:
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
if not bool(item.get("placed", false)):
|
||||
item["placed"] = true
|
||||
var defaultPosition := _variant_to_vector2(item.get("default_position", Vector2.ZERO))
|
||||
item["position_x"] = defaultPosition.x
|
||||
item["position_y"] = defaultPosition.y
|
||||
item["scale"] = _get_item_float(item, "default_scale", 1.0)
|
||||
item["z_index"] = _get_item_int(item, "default_z_index", 0)
|
||||
_decorItems[decorId] = item
|
||||
return item
|
||||
|
||||
func _ensure_decor_node(decorId: String, item: Dictionary) -> Sprite2D:
|
||||
var node := _decorNodes.get(decorId, null) as Sprite2D
|
||||
if node == null or not is_instance_valid(node):
|
||||
node = Sprite2D.new()
|
||||
node.name = "Decor_%s" % decorId
|
||||
node.texture_filter = DECOR_TEXTURE_FILTER
|
||||
node.centered = true
|
||||
node.set_meta("decor_id", decorId)
|
||||
_decorLayer.add_child(node)
|
||||
_decorNodes[decorId] = node
|
||||
else:
|
||||
node.texture_filter = DECOR_TEXTURE_FILTER
|
||||
node.texture = _load_texture(str(item.get("texture", item.get("icon", ""))))
|
||||
node.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0))
|
||||
var itemScale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
|
||||
node.scale = Vector2(itemScale, itemScale)
|
||||
node.z_index = _get_item_int(item, "z_index", _get_item_int(item, "default_z_index", 0))
|
||||
_sync_decor_collision(decorId, item)
|
||||
return node
|
||||
|
||||
func _try_begin_decor_drag(worldPosition: Vector2) -> bool:
|
||||
var bestNode: Sprite2D = null
|
||||
var bestZ := -100000
|
||||
for decorId in _decorNodes.keys():
|
||||
var node := _decorNodes[decorId] as Sprite2D
|
||||
if node == null or not is_instance_valid(node) or node.texture == null:
|
||||
continue
|
||||
if not _is_point_inside_sprite(node, worldPosition):
|
||||
continue
|
||||
if node.z_index >= bestZ:
|
||||
bestNode = node
|
||||
bestZ = node.z_index
|
||||
if bestNode == null:
|
||||
return false
|
||||
_draggedDecor = bestNode
|
||||
_selectedDecorId = str(bestNode.get_meta("decor_id", ""))
|
||||
_dragOffset = bestNode.global_position - worldPosition
|
||||
_set_decor_collision_disabled(_selectedDecorId, true)
|
||||
_draggedDecor.z_index = max(_draggedDecor.z_index, 50)
|
||||
_enable_drag_surface()
|
||||
return true
|
||||
|
||||
func _finish_decor_drag() -> void:
|
||||
if _draggedDecor == null:
|
||||
_disable_drag_surface()
|
||||
return
|
||||
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
|
||||
if _decorItems.has(decorId):
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
item["position_x"] = _draggedDecor.global_position.x
|
||||
item["position_y"] = _draggedDecor.global_position.y
|
||||
item["placed"] = true
|
||||
item["scale"] = _draggedDecor.scale.x
|
||||
item["z_index"] = _get_item_int(item, "default_z_index", 0)
|
||||
_decorItems[decorId] = item
|
||||
_draggedDecor.z_index = _get_item_int(item, "z_index", 0)
|
||||
_sync_decor_collision(decorId, item)
|
||||
_set_decor_collision_disabled(decorId, false)
|
||||
_save_decor_item(item)
|
||||
_render_inventory_list()
|
||||
_draggedDecor = null
|
||||
_disable_drag_surface()
|
||||
|
||||
func _update_dragged_decor_position() -> void:
|
||||
if _draggedDecor != null:
|
||||
_draggedDecor.global_position = get_global_mouse_position() + _dragOffset
|
||||
var decorId := str(_draggedDecor.get_meta("decor_id", ""))
|
||||
if not decorId.is_empty() and _decorItems.has(decorId):
|
||||
var item: Dictionary = _decorItems[decorId]
|
||||
item["position_x"] = _draggedDecor.global_position.x
|
||||
item["position_y"] = _draggedDecor.global_position.y
|
||||
_sync_decor_collision(decorId, item)
|
||||
|
||||
func _enable_drag_surface() -> void:
|
||||
if is_instance_valid(_dragSurface):
|
||||
_dragSurface.visible = true
|
||||
uiLayer.move_child(_dragSurface, uiLayer.get_child_count() - 1)
|
||||
set_process(true)
|
||||
|
||||
func _disable_drag_surface() -> void:
|
||||
if is_instance_valid(_dragSurface):
|
||||
_dragSurface.visible = false
|
||||
set_process(false)
|
||||
|
||||
func _on_drag_surface_gui_input(event: InputEvent) -> void:
|
||||
if _draggedDecor == null:
|
||||
_disable_drag_surface()
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
_update_dragged_decor_position()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseButton:
|
||||
var mouseEvent := event as InputEventMouseButton
|
||||
if mouseEvent.button_index == MOUSE_BUTTON_LEFT and not mouseEvent.pressed:
|
||||
_finish_decor_drag()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _is_pointer_over_blocking_ui() -> bool:
|
||||
var mousePosition := get_viewport().get_mouse_position()
|
||||
for child in uiLayer.get_children():
|
||||
if child == _dragSurface:
|
||||
continue
|
||||
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _control_blocks_pointer(control: Control, mousePosition: Vector2) -> bool:
|
||||
if not control.visible or not control.is_visible_in_tree():
|
||||
return false
|
||||
if control.mouse_filter != Control.MOUSE_FILTER_IGNORE and control.get_global_rect().has_point(mousePosition):
|
||||
return true
|
||||
for child in control.get_children():
|
||||
if child is Control and _control_blocks_pointer(child as Control, mousePosition):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _remove_selected_decor() -> void:
|
||||
if _selectedDecorId.is_empty() or not _decorItems.has(_selectedDecorId):
|
||||
_show_toast("请先选择一个家具")
|
||||
return
|
||||
var item: Dictionary = _decorItems[_selectedDecorId]
|
||||
item["placed"] = false
|
||||
_decorItems[_selectedDecorId] = item
|
||||
if _decorNodes.has(_selectedDecorId):
|
||||
var node := _decorNodes[_selectedDecorId] as Node
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_decorNodes.erase(_selectedDecorId)
|
||||
_remove_decor_collision(_selectedDecorId)
|
||||
_save_decor_item(item)
|
||||
_render_inventory_list()
|
||||
_show_toast("已收回家具")
|
||||
|
||||
func _save_decor_item(item: Dictionary) -> void:
|
||||
if not _is_authenticated():
|
||||
_show_toast("请先登录后保存摆放")
|
||||
return
|
||||
var saveManager := get_node_or_null("/root/RoomDecorSaveManager")
|
||||
if saveManager == null or not saveManager.has_method("enqueue_save"):
|
||||
_show_toast("家具保存服务不可用")
|
||||
return
|
||||
saveManager.call("enqueue_save", item)
|
||||
|
||||
func _on_decor_save_succeeded(savedItem: Dictionary) -> void:
|
||||
var item := _merge_decor_definition(savedItem.duplicate(true))
|
||||
var decorId := str(item.get("decor_id", ""))
|
||||
if not decorId.is_empty():
|
||||
_decorItems[decorId] = item
|
||||
|
||||
func _on_decor_save_failed(_item: Dictionary, message: String) -> void:
|
||||
_show_toast(message)
|
||||
|
||||
func _is_point_inside_sprite(node: Sprite2D, worldPosition: Vector2) -> bool:
|
||||
if node.texture == null:
|
||||
return false
|
||||
var localPoint := node.to_local(worldPosition)
|
||||
var size := node.texture.get_size()
|
||||
var rect := Rect2(-size * 0.5, size)
|
||||
return rect.has_point(localPoint)
|
||||
|
||||
func _sync_decor_collision(decorId: String, item: Dictionary) -> void:
|
||||
var collisionSize := _variant_to_vector2(item.get("collision_size", Vector2.ZERO))
|
||||
if collisionSize == Vector2.ZERO:
|
||||
_remove_decor_collision(decorId)
|
||||
return
|
||||
var collisionOffset := _variant_to_vector2(item.get("collision_offset", Vector2.ZERO))
|
||||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||||
var shapeNode: CollisionShape2D
|
||||
if body == null or not is_instance_valid(body):
|
||||
body = StaticBody2D.new()
|
||||
body.name = "DecorCollision_%s" % decorId
|
||||
body.collision_layer = DECOR_COLLISION_LAYER
|
||||
body.collision_mask = DECOR_COLLISION_MASK
|
||||
shapeNode = CollisionShape2D.new()
|
||||
shapeNode.name = "CollisionShape2D"
|
||||
var rectShape := RectangleShape2D.new()
|
||||
shapeNode.shape = rectShape
|
||||
body.add_child(shapeNode)
|
||||
staticCollision.add_child(body)
|
||||
_decorCollisionBodies[decorId] = body
|
||||
else:
|
||||
shapeNode = body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode == null:
|
||||
shapeNode = CollisionShape2D.new()
|
||||
shapeNode.name = "CollisionShape2D"
|
||||
shapeNode.shape = RectangleShape2D.new()
|
||||
body.add_child(shapeNode)
|
||||
var scale := _get_item_float(item, "scale", _get_item_float(item, "default_scale", 1.0))
|
||||
body.global_position = Vector2(_to_float(item.get("position_x", 0.0), 0.0), _to_float(item.get("position_y", 0.0), 0.0)) + collisionOffset * scale
|
||||
shapeNode.position = Vector2.ZERO
|
||||
var rectShape := shapeNode.shape as RectangleShape2D
|
||||
if rectShape == null:
|
||||
rectShape = RectangleShape2D.new()
|
||||
shapeNode.shape = rectShape
|
||||
rectShape.size = Vector2(max(8.0, collisionSize.x * scale), max(8.0, collisionSize.y * scale))
|
||||
|
||||
func _set_decor_collision_disabled(decorId: String, disabled: bool) -> void:
|
||||
var body := _decorCollisionBodies.get(decorId, null) as StaticBody2D
|
||||
if body == null or not is_instance_valid(body):
|
||||
return
|
||||
var shapeNode := body.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shapeNode != null:
|
||||
shapeNode.disabled = disabled
|
||||
|
||||
func _remove_decor_collision(decorId: String) -> void:
|
||||
var body := _decorCollisionBodies.get(decorId, null) as Node
|
||||
if body != null and is_instance_valid(body):
|
||||
body.queue_free()
|
||||
_decorCollisionBodies.erase(decorId)
|
||||
|
||||
func _variant_to_vector2(value: Variant) -> Vector2:
|
||||
if value is Vector2:
|
||||
return value
|
||||
if value is Dictionary:
|
||||
var dict: Dictionary = value
|
||||
return Vector2(_to_float(dict.get("x", 0.0), 0.0), _to_float(dict.get("y", 0.0), 0.0))
|
||||
return Vector2.ZERO
|
||||
|
||||
func _to_float(value: Variant, fallback: float) -> float:
|
||||
match typeof(value):
|
||||
TYPE_FLOAT:
|
||||
return value
|
||||
TYPE_INT:
|
||||
return value
|
||||
TYPE_STRING:
|
||||
var text := str(value).strip_edges()
|
||||
return fallback if text.is_empty() else text.to_float()
|
||||
TYPE_BOOL:
|
||||
return 1.0 if bool(value) else 0.0
|
||||
_:
|
||||
return fallback
|
||||
|
||||
func _to_int(value: Variant, fallback: int) -> int:
|
||||
match typeof(value):
|
||||
TYPE_INT:
|
||||
return value
|
||||
TYPE_FLOAT:
|
||||
return roundi(value)
|
||||
TYPE_STRING:
|
||||
var text := str(value).strip_edges()
|
||||
return fallback if text.is_empty() else text.to_int()
|
||||
TYPE_BOOL:
|
||||
return 1 if bool(value) else 0
|
||||
_:
|
||||
return fallback
|
||||
|
||||
func _get_item_float(item: Dictionary, key: String, fallback: float) -> float:
|
||||
return _to_float(item.get(key, fallback), fallback)
|
||||
|
||||
func _get_item_int(item: Dictionary, key: String, fallback: int) -> int:
|
||||
return _to_int(item.get(key, fallback), fallback)
|
||||
|
||||
func _is_authenticated() -> bool:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
return authManager != null and authManager.has_method("is_authenticated") and bool(authManager.call("is_authenticated"))
|
||||
|
||||
func _auth_headers() -> PackedStringArray:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else ""
|
||||
return PackedStringArray([
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer %s" % accessToken,
|
||||
])
|
||||
|
||||
func _load_texture(path: String) -> Texture2D:
|
||||
if path.is_empty():
|
||||
return null
|
||||
if FileAccess.file_exists("%s.import" % path):
|
||||
var texture := load(path) as Texture2D
|
||||
if texture != null:
|
||||
return texture
|
||||
var image := Image.load_from_file(ProjectSettings.globalize_path(path))
|
||||
if image == null or image.is_empty():
|
||||
return null
|
||||
return ImageTexture.create_from_image(image)
|
||||
|
||||
func _get_event_system() -> Node:
|
||||
return get_node_or_null("/root/EventSystem")
|
||||
|
||||
func _show_toast(message: String) -> void:
|
||||
if not is_instance_valid(_toastLabel):
|
||||
return
|
||||
_toastLabel.text = message
|
||||
_toastLabel.visible = true
|
||||
var tween := create_tween()
|
||||
tween.tween_interval(1.5)
|
||||
tween.tween_callback(func() -> void:
|
||||
if is_instance_valid(_toastLabel):
|
||||
_toastLabel.visible = false
|
||||
)
|
||||
|
||||
func _pulse_node(node: Node2D) -> void:
|
||||
var originalScale := node.scale
|
||||
var tween := create_tween()
|
||||
tween.tween_property(node, "scale", originalScale * 1.08, 0.08)
|
||||
tween.tween_property(node, "scale", originalScale, 0.10)
|
||||
|
||||
func _clear_children(node: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.queue_free()
|
||||
|
||||
func _create_panel_style(color: Color, radius: int, shadow: bool) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.corner_radius_top_left = radius
|
||||
style.corner_radius_top_right = radius
|
||||
style.corner_radius_bottom_left = radius
|
||||
style.corner_radius_bottom_right = radius
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_bottom = 8
|
||||
if shadow:
|
||||
style.shadow_color = Color(0.082, 0.243, 0.380, 0.18)
|
||||
style.shadow_size = 12
|
||||
style.shadow_offset = Vector2(0, 4)
|
||||
return style
|
||||
|
||||
func _create_inventory_card_style(hovered: bool) -> StyleBoxFlat:
|
||||
var style := _create_panel_style(Color(0.982, 0.995, 1.0, 1.0), 18, true)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.650, 0.835, 0.965, 0.90) if not hovered else Color(0.086, 0.690, 0.960, 1.0)
|
||||
style.shadow_color = Color(0.086, 0.314, 0.520, 0.14 if hovered else 0.08)
|
||||
style.shadow_size = 14 if hovered else 8
|
||||
style.content_margin_left = 0
|
||||
style.content_margin_top = 0
|
||||
style.content_margin_right = 0
|
||||
style.content_margin_bottom = 0
|
||||
return style
|
||||
Reference in New Issue
Block a user