Initial WhaleTown V2 frontend

This commit is contained in:
2026-07-19 22:42:20 +08:00
commit 435c578dde
1421 changed files with 54486 additions and 0 deletions

View 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