diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index cdd4d4f..e0db860 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -67,7 +67,6 @@ signal showcase_changed( signal bite_activated signal bite_prompt_changed(is_visible: bool) signal ready_for_equipment_refresh -signal art_ui_toggle_requested enum FishingState { READY, @@ -510,13 +509,6 @@ func _unhandled_input(event: InputEvent) -> void: if not _new_cast_press_armed: return var active_item: ItemDataType = _get_active_item() - if ( - active_item != null - and active_item.item_id == ArtShopStockType.ART_KIT_ITEM_ID - ): - art_ui_toggle_requested.emit() - get_viewport().set_input_as_handled() - return if ( active_item != null and ( diff --git a/main/main.gd b/main/main.gd index 5b48e79..4647005 100644 --- a/main/main.gd +++ b/main/main.gd @@ -557,6 +557,7 @@ func _initialize_application(dedicated: bool) -> void: _player_spawn_service, _fishing_spot, _player.inventory, + _player.bag, _player.collection_log, _player.cooler_capacity, _player.experience, @@ -1747,13 +1748,17 @@ func _on_active_hotbar_item_changed( and item.category == ItemDataType.Category.ROD and _player.bag.owns_item(item_id) ) + var active_is_art_kit: bool = ( + item_id == ArtShopStockType.ART_KIT_ITEM_ID + and item != null + and _player.bag.owns_item(item_id) + ) _player.set_active_item_is_rod(active_is_rod, true) _player.set_active_art_kit( item.icon if item != null else null, - item_id == ArtShopStockType.ART_KIT_ITEM_ID - and item != null - and _player.bag.owns_item(item_id), + active_is_art_kit, ) + _game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit) _network_item_use.submit_local_equipped(item_id, active_is_rod or ( item != null and _player.bag.owns_item(item_id) )) diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index af84bdd..7303f12 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -33,6 +33,7 @@ var _session: NetworkSession var _spawn_service: PlayerSpawnService var _fishing_spot: FishingSpot var _local_inventory: FishInventory +var _local_bag: PlayerBag var _local_collection: CollectionLog var _local_capacity: PlayerCoolerCapacity var _local_experience: PlayerExperienceType @@ -47,6 +48,7 @@ var _result_acknowledgements: Dictionary[String, String] = {} var _last_cast_time: Dictionary[int, float] = {} var _last_input_time: Dictionary[int, float] = {} var _remote_presentations: Dictionary[int, RemoteFishingPresentation] = {} +var _pending_local_bait_by_request: Dictionary[String, StringName] = {} var _snapshot_accumulator: float = 0.0 var _local_input_sequence: int = 0 @@ -56,6 +58,7 @@ func setup( spawn_service: PlayerSpawnService, fishing_spot: FishingSpot, local_inventory: FishInventory, + local_bag: PlayerBag, local_collection: CollectionLog, local_capacity: PlayerCoolerCapacity, local_experience: PlayerExperienceType, @@ -68,6 +71,7 @@ func setup( _spawn_service = spawn_service _fishing_spot = fishing_spot _local_inventory = local_inventory + _local_bag = local_bag _local_collection = local_collection _local_capacity = local_capacity _local_experience = local_experience @@ -97,6 +101,18 @@ func request_local_cast( ): local_cast_rejected.emit("Fishing attempt ended.") return "" + var bait_id := StringName(str(evidence.get("bait_id", ""))) + if not bait_id.is_empty() and ( + _local_bag == null or not _local_bag.owns_item(bait_id) + ): + local_cast_rejected.emit("No bait available.") + return "" + var lure_id := StringName(str(evidence.get("lure_id", ""))) + if not lure_id.is_empty() and ( + _local_bag == null or not _local_bag.owns_item(lure_id) + ): + local_cast_rejected.emit("Lure is unavailable.") + return "" var request_id: String = _new_id("cast") var data: Dictionary = { "request_id": request_id, @@ -111,12 +127,14 @@ func request_local_cast( "rarity_multipliers": evidence.get("rarity_multipliers", []), "discovered_fish_ids": evidence.get("discovered_fish_ids", []), "capacity_available": bool(evidence.get("capacity_available", false)), - "bait_id": str(evidence.get("bait_id", "")), - "lure_id": str(evidence.get("lure_id", "")), + "bait_id": str(bait_id), + "lure_id": str(lure_id), } if _session.is_host(): _handle_cast_request(_session.get_local_peer_id(), data) else: + if not bait_id.is_empty(): + _pending_local_bait_by_request[request_id] = bait_id submit_cast_request.rpc_id(1, data) return request_id @@ -303,6 +321,14 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void: return var bait_id: StringName = StringName(str(data.get("bait_id", ""))) var avatar_bag: PlayerBag = avatar.bag + var owns_authoritative_bag: bool = ( + peer_id == _session.get_local_peer_id() + ) + var bait: ItemDataType = ( + _item_catalog.get_item_by_id(bait_id) + if not bait_id.is_empty() and _item_catalog != null + else null + ) var lure_id: StringName = StringName(str(data.get("lure_id", ""))) var lure: ItemDataType = ( _item_catalog.get_item_by_id(lure_id) @@ -314,13 +340,28 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void: and ( lure == null or not lure.is_lure() - or avatar_bag == null - or not avatar_bag.owns_item(lure_id) + or ( + owns_authoritative_bag + and ( + avatar_bag == null + or not avatar_bag.owns_item(lure_id) + ) + ) ) ): _record_and_reject(peer_id, request_id, "Lure is unavailable.") return - if not bait_id.is_empty() and (avatar_bag == null or not avatar_bag.remove_item(bait_id, 1)): + if not bait_id.is_empty() and ( + bait == null + or not bait.is_bait() + or ( + owns_authoritative_bag + and ( + avatar_bag == null + or not avatar_bag.remove_item(bait_id, 1) + ) + ) + ): _record_and_reject(peer_id, request_id, "No bait available.") return var attempt := NetworkFishingAttempt.new() @@ -1025,6 +1066,20 @@ func _apply_cast_accepted(data: Dictionary) -> void: # On the host this replaces the same authoritative value with itself. if not _session.is_host(): _attempts[peer_id] = attempt + var pending_bait_id: StringName = ( + _pending_local_bait_by_request.get( + attempt.request_id, + StringName(), + ) + ) + _pending_local_bait_by_request.erase(attempt.request_id) + if not pending_bait_id.is_empty() and ( + _local_bag == null + or not _local_bag.remove_item(pending_bait_id, 1) + ): + cancel_local_attempt("No bait available.") + local_cast_rejected.emit("No bait available.") + return local_cast_accepted.emit(attempt.attempt_id, target) else: var presentation := _get_remote_presentation(peer_id) @@ -1093,7 +1148,8 @@ func _send_cast_rejected( @rpc("authority", "call_remote", "reliable", 0) -func receive_cast_rejected(_request_id: String, message: String) -> void: +func receive_cast_rejected(request_id: String, message: String) -> void: + _pending_local_bait_by_request.erase(request_id) local_cast_rejected.emit(message) diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 6cab7f7..874cc4e 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -296,11 +296,20 @@ func _run() -> void: await process_frame assert(not service.is_active() and not toolbar.visible) assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID)) + player.hotbar.select_slot(0) var fishing_spot := main.get_node("%FishingSpot") as FishingSpot - fishing_spot.art_ui_toggle_requested.emit() await process_frame + assert(not fishing_spot.has_signal(&"art_ui_toggle_requested")) assert(service.is_active() and toolbar.visible) assert(service.get_grid_size() == 128) + assert(service.handle_input(world_click, true)) + assert(service.is_active() and toolbar.visible) + player.hotbar.select_slot(1) + await process_frame + assert(not service.is_active() and not toolbar.visible) + assert(service.handle_input(paint_key, true)) + await process_frame + assert(service.is_active() and toolbar.visible) print("Art tools validation: PASS") var session := main.get_node("%NetworkSession") as NetworkSession diff --git a/tests/fishing_multiplayer_validation.gd b/tests/fishing_multiplayer_validation.gd index 524a426..2dae5e3 100644 --- a/tests/fishing_multiplayer_validation.gd +++ b/tests/fishing_multiplayer_validation.gd @@ -50,6 +50,7 @@ func _run_host() -> void: ) as PlayerSpawnService var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id) assert(remote_avatar != null) + assert(remote_avatar.bag.get_quantity(&"worms") == 0) remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1) var remote_visuals := remote_avatar.get_node("Visuals") as Node3D remote_visuals.rotation.y = PI * 0.5 @@ -178,6 +179,12 @@ func _run_client() -> void: ) var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType var player := main.get("_player") as Player + var item_catalog := main.get("item_catalog") as ItemCatalog + var worms: ItemData = item_catalog.get_item_by_id(&"worms") + if player.bag.get_quantity(&"worms") == 0: + assert(player.bag.add_item(&"worms", 1)) + assert(player.equip_bait(worms)) + var worms_before_cast: int = player.bag.get_quantity(&"worms") var character_animation_player := player.get_node( "Visuals/CharacterRig/AnimationPlayer" ) as AnimationPlayer @@ -265,6 +272,7 @@ func _run_client() -> void: ] ) assert(service.has_local_attempt()) + assert(player.bag.get_quantity(&"worms") == worms_before_cast - 1) var fishing_deadline: int = Time.get_ticks_msec() + 5000 while ( Time.get_ticks_msec() < fishing_deadline diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index cb360fe..34923ec 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -241,6 +241,7 @@ func set_available(value: bool) -> void: func close_chat() -> void: var ownership_was_active: bool = _opened or _input_lock_applied _opened = false + _set_status("") _entry.virtual_keyboard_enabled = false _entry.release_focus() _entry.hide() @@ -743,6 +744,9 @@ func _send() -> void: if _send_pending: return var body := _entry.text + if body.strip_edges().is_empty(): + close_chat() + return if _handle_chat_command(body): return _send_pending = true @@ -1114,6 +1118,9 @@ func _update_speech() -> void: if camera.is_position_behind(world_position): bubble.hide() continue + if _is_speech_world_occluded(camera, avatar, world_position): + bubble.hide() + continue var screen_position := ( camera.unproject_position(world_position) / _output_scale ) @@ -1154,6 +1161,38 @@ func _update_speech() -> void: bubble.show() +func _is_speech_world_occluded( + camera: Camera3D, + speaker: Player, + world_position: Vector3, +) -> bool: + var world := camera.get_world_3d() + if world == null: + return false + var excluded: Array[RID] = [] + if _player != null: + excluded.append(_player.get_rid()) + if speaker != _player: + excluded.append(speaker.get_rid()) + var query := PhysicsRayQueryParameters3D.create( + camera.global_position, + world_position, + ) + query.collide_with_areas = false + query.collide_with_bodies = true + for _attempt: int in range(8): + query.exclude = excluded + var hit: Dictionary = world.direct_space_state.intersect_ray(query) + if hit.is_empty(): + return false + var collider := hit.get("collider") as CollisionObject3D + if collider is Player: + excluded.append(collider.get_rid()) + continue + return true + return false + + func _on_draft_changed(_value: String) -> void: if _settings == null: return diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 874bf5a..a9db30d 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -342,7 +342,6 @@ func setup( fishing_spot.bite_prompt_changed.connect(_on_bite_prompt_changed) fishing_spot.catch_display_changed.connect(_on_catch_display_changed) fishing_spot.showcase_changed.connect(_on_showcase_changed) - fishing_spot.art_ui_toggle_requested.connect(_toggle_surface_drawing) _player_menu.menu_visibility_changed.connect( _on_player_menu_visibility_changed ) @@ -1113,6 +1112,16 @@ func _toggle_surface_drawing() -> void: _surface_drawing.activate(_drawing_pointer_window_position()) +func set_surface_drawing_hotbar_selected(is_selected: bool) -> void: + if _surface_drawing == null: + return + if is_selected: + if not _surface_drawing.is_active() and _surface_drawing.can_activate(): + _surface_drawing.activate(_drawing_pointer_window_position()) + elif _surface_drawing.is_active(): + _surface_drawing.deactivate() + + func _drawing_pointer_window_position() -> Vector2: if _virtual_mouse_active: return _virtual_mouse_window_position