class_name HomeDecorController extends Node const DecorCatalogType = preload("res://homes/decor_catalog.gd") const DRAG_RESPONSE: float = 22.0 const DRAG_POSITION_EPSILON: float = 0.005 var _player: Player var _hotbar: PlayerHotbar var _bag: PlayerBag var _home_state: PlayerHomeState var _network_home: NetworkHomeService var _home_world: HomeWorldService var _fishing_spot: FishingSpot var _toolbar: HomeDecorToolbar var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID var _selected_instance_id: String = "" var _drag_instance_id: String = "" var _drag_owner_fingerprint: String = "" var _drag_pointer_offset: Vector3 = Vector3.ZERO var _drag_original_position: Vector3 = Vector3.ZERO var _drag_original_yaw: float = 0.0 var _drag_target_position: Vector3 = Vector3.ZERO var _drag_target_yaw: float = 0.0 var _drag_target_valid: bool = false func setup( player: Player, hotbar: PlayerHotbar, bag: PlayerBag, home_state: PlayerHomeState, network_home: NetworkHomeService, home_world: HomeWorldService, fishing_spot: FishingSpot, toolbar: HomeDecorToolbar, ) -> void: _player = player _hotbar = hotbar _bag = bag _home_state = home_state _network_home = network_home _home_world = home_world _fishing_spot = fishing_spot _toolbar = toolbar if not _network_home.local_space_changed.is_connected( _on_local_space_changed ): _network_home.local_space_changed.connect(_on_local_space_changed) if not _home_state.changed.is_connected(_on_home_changed): _home_state.changed.connect(_on_home_changed) if not _toolbar.placement_mode_requested.is_connected( _on_placement_mode_requested ): _toolbar.placement_mode_requested.connect( _on_placement_mode_requested ) _toolbar.rotate_requested.connect(_rotate_selected) _toolbar.store_requested.connect(_store_selected) _toolbar.cancel_requested.connect(_clear_selection) _toolbar.set_mode(_mode) _refresh_toolbar() func begin_decorating() -> void: _refresh_toolbar() if _toolbar.visible: _toolbar.report_status( "use held decor to place it; click or drag placed decor" ) func _input(event: InputEvent) -> void: if _drag_instance_id.is_empty(): return var pointer_motion := event as InputEventMouseMotion if pointer_motion != null: _update_drag(pointer_motion.position) get_viewport().set_input_as_handled() return var pointer_button := event as InputEventMouseButton if ( pointer_button != null and pointer_button.button_index == MOUSE_BUTTON_LEFT and not pointer_button.pressed ): _finish_drag() get_viewport().set_input_as_handled() func _unhandled_input(event: InputEvent) -> void: var pointer_button := event as InputEventMouseButton if ( pointer_button != null and pointer_button.button_index == MOUSE_BUTTON_LEFT ): if not pointer_button.pressed or not _can_decorate(): return if _begin_drag(pointer_button.position): get_viewport().set_input_as_handled() return var pointer_decor_id: StringName = _get_selected_decor_id() if ( not pointer_decor_id.is_empty() and _bag.get_quantity(pointer_decor_id) > 0 ): _place_carried_decor(pointer_decor_id) get_viewport().set_input_as_handled() return if not _can_decorate() or not event.is_action_pressed("fish_primary"): return if event is InputEventKey and event.echo: return var decor_id: StringName = _get_selected_decor_id() if not decor_id.is_empty() and _bag.get_quantity(decor_id) > 0: _place_carried_decor(decor_id) get_viewport().set_input_as_handled() func _process(delta: float) -> void: if _drag_instance_id.is_empty(): return if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _finish_drag() return if not _drag_target_valid: return if not _can_decorate(): _cancel_drag() return var current: Dictionary = _home_world.get_decor_preview_transform( _drag_owner_fingerprint, _drag_instance_id ) if current.is_empty(): _cancel_drag() return var current_position: Vector3 = current["position"] var current_yaw: float = float(current["yaw"]) var blend: float = 1.0 - exp(-DRAG_RESPONSE * delta) _home_world.set_decor_preview_transform( _drag_owner_fingerprint, _drag_instance_id, current_position.lerp(_drag_target_position, blend), lerp_angle(current_yaw, _drag_target_yaw, blend), ) func _begin_drag(screen_position: Vector2) -> bool: var owner_fingerprint: String = ( _network_home.get_local_home_owner_fingerprint() ) var camera: Camera3D = _player.get_active_gameplay_camera() var clicked_instance: String = _home_world.find_decor_instance_at_screen( owner_fingerprint, camera, screen_position ) if clicked_instance.is_empty(): return false var placement: Dictionary = _home_state.get_placement(clicked_instance) if placement.is_empty(): return false var original_position: Vector3 = _placement_position(placement) if not original_position.is_finite(): return false if not _home_world.set_decor_manipulation_active( owner_fingerprint, clicked_instance, true ): return false var pointer_position: Vector3 = _home_world.get_floor_position_from_screen( owner_fingerprint, camera, screen_position ) _drag_instance_id = clicked_instance _drag_owner_fingerprint = owner_fingerprint _drag_original_position = original_position _drag_original_yaw = float(placement.get("yaw", 0.0)) _drag_target_position = original_position _drag_target_yaw = _drag_original_yaw _drag_target_valid = true _drag_pointer_offset = ( original_position - pointer_position if pointer_position.is_finite() else Vector3.ZERO ) _drag_pointer_offset.y = 0.0 _select_instance(clicked_instance) _toolbar.report_status("drag decor, then release to place it") return true func _update_drag(screen_position: Vector2) -> void: if not _can_decorate(): _cancel_drag() return var camera: Camera3D = _player.get_active_gameplay_camera() var pointer_position: Vector3 = _home_world.get_floor_position_from_screen( _drag_owner_fingerprint, camera, screen_position ) if not pointer_position.is_finite(): # Keep the most recent valid target when the pointer briefly leaves the # RV floor. Releasing there should place at that target, not throw the # decor all the way back to where the drag began. _toolbar.report_status("keep the pointer over the RV floor") return var placement: Dictionary = _home_state.get_placement(_drag_instance_id) if placement.is_empty(): _clear_selection() return var candidate: Vector3 = pointer_position + _drag_pointer_offset candidate.y = 0.0 var normalized: Dictionary = _network_home.get_valid_local_decor_transform( _drag_instance_id, candidate, float(placement.get("yaw", _drag_original_yaw)), _mode, ) if normalized.is_empty(): # A densely furnished area may have no nearby solution. Preserve the # last valid preview so the interaction remains stable while the player # moves the pointer toward open space. _toolbar.report_status("holding at the nearest available position") return _drag_target_position = normalized["position"] _drag_target_yaw = float(normalized["yaw"]) _drag_target_valid = true _toolbar.report_status("release to place decor") func _finish_drag() -> void: var instance_id: String = _drag_instance_id var owner_fingerprint: String = _drag_owner_fingerprint var original_position: Vector3 = _drag_original_position var original_yaw: float = _drag_original_yaw var target_position: Vector3 = _drag_target_position var target_yaw: float = _drag_target_yaw var target_valid: bool = _drag_target_valid var placement: Dictionary = _home_state.get_placement(instance_id) var decor_id := StringName(str(placement.get("decor_id", ""))) if ( not target_valid or decor_id.is_empty() or not _home_world.is_decor_placement_clear_of_players( owner_fingerprint, decor_id, target_position, target_yaw ) ): _home_world.set_decor_preview_transform( owner_fingerprint, instance_id, original_position, original_yaw, ) _home_world.set_decor_manipulation_active( owner_fingerprint, instance_id, false ) _reset_drag_state() _toolbar.report_status( "a player is in the way" if target_valid else "that space is not available" ) return _home_world.set_decor_preview_transform( owner_fingerprint, instance_id, target_position, target_yaw ) if ( original_position.distance_to(target_position) <= DRAG_POSITION_EPSILON and is_zero_approx(angle_difference(original_yaw, target_yaw)) ): _home_world.set_decor_manipulation_active( owner_fingerprint, instance_id, false ) _reset_drag_state() _select_instance(instance_id) return if _network_home.update_local_decor( instance_id, target_position, target_yaw, _mode ): _home_world.set_decor_manipulation_active( owner_fingerprint, instance_id, false ) _reset_drag_state() _toolbar.report_status("decor moved") return _home_world.set_decor_preview_transform( owner_fingerprint, instance_id, original_position, original_yaw, ) _home_world.set_decor_manipulation_active( owner_fingerprint, instance_id, false ) _reset_drag_state() _toolbar.report_status("that space is not available") func _cancel_drag() -> void: if _drag_instance_id.is_empty(): return _home_world.set_decor_preview_transform( _drag_owner_fingerprint, _drag_instance_id, _drag_original_position, _drag_original_yaw, ) _home_world.set_decor_manipulation_active( _drag_owner_fingerprint, _drag_instance_id, false ) _reset_drag_state() func _reset_drag_state() -> void: _drag_instance_id = "" _drag_owner_fingerprint = "" _drag_pointer_offset = Vector3.ZERO _drag_original_position = Vector3.ZERO _drag_original_yaw = 0.0 _drag_target_position = Vector3.ZERO _drag_target_yaw = 0.0 _drag_target_valid = false static func _placement_position(placement: Dictionary) -> Vector3: if placement.is_empty(): return Vector3.INF var values: Array = placement.get("position", []) if values.size() != 3: return Vector3.INF return Vector3( float(values[0]), float(values[1]), float(values[2]) ) func _place_carried_decor(decor_id: StringName) -> void: var owner_fingerprint: String = ( _network_home.get_local_home_owner_fingerprint() ) var forward: Vector3 = -_player.global_basis.z forward.y = 0.0 forward = forward.normalized() var global_position: Vector3 = _player.global_position + forward * 1.45 var local_position: Vector3 = _home_world.interior_global_to_local( owner_fingerprint, global_position ) var instance_id: String = _network_home.place_local_decor( decor_id, local_position, _player.global_rotation.y, _mode ) if instance_id.is_empty(): _toolbar.report_status("make room in front of you to place this decor") return _select_instance(instance_id) _toolbar.report_status("decor placed") func _select_instance(instance_id: String) -> void: var placement: Dictionary = _home_state.get_placement(instance_id) if placement.is_empty(): _clear_selection() return _selected_instance_id = instance_id var decor_id := StringName(str(placement.get("decor_id", ""))) _toolbar.set_selection( instance_id, DecorCatalogType.get_display_name(decor_id) ) func _rotate_selected() -> void: if not _can_decorate() or _selected_instance_id.is_empty(): return var placement: Dictionary = _home_state.get_placement( _selected_instance_id ) if placement.is_empty(): _clear_selection() return var values: Array = placement.get("position", []) if values.size() != 3: return var position := Vector3( float(values[0]), float(values[1]), float(values[2]) ) if _network_home.update_local_decor( _selected_instance_id, position, float(placement.get("yaw", 0.0)) + PI * 0.5, _mode, ): _toolbar.report_status("decor rotated") else: _toolbar.report_status("not enough room to rotate here") func _store_selected() -> void: if _selected_instance_id.is_empty(): return if _network_home.remove_local_decor(_selected_instance_id): _clear_selection() _toolbar.report_status("decor packed into storage") else: _toolbar.report_status("inventory and storage are full") func _clear_selection() -> void: _cancel_drag() _selected_instance_id = "" if _toolbar != null: _toolbar.set_selection("") func _on_placement_mode_requested(mode: int) -> void: _mode = mode as PlayerHomeState.PlacementMode func _on_local_space_changed( _space_id: StringName, _owner_fingerprint: String, ) -> void: _clear_selection() _refresh_toolbar() func _on_home_changed() -> void: if ( not _selected_instance_id.is_empty() and _home_state.get_placement(_selected_instance_id).is_empty() ): _clear_selection() _refresh_toolbar() func _refresh_toolbar() -> void: if _toolbar == null: return _toolbar.set_active(_can_decorate()) if not _toolbar.visible: _clear_selection() func _can_decorate() -> bool: return ( _player != null and _network_home != null and _network_home.can_local_decorate() and _fishing_spot != null and _fishing_spot.can_open_fishing_shop() and _player.is_movement_enabled() ) func _get_selected_decor_id() -> StringName: if ( _hotbar == null or _hotbar.get_selected_assignment_kind() != PlayerHotbar.AssignmentKind.ITEM ): return StringName() var item_id: StringName = _hotbar.get_selected_item_id() return item_id if DecorCatalogType.has_product(item_id) else StringName()