diff --git a/art/exported/characters/base/netfishing_base_character.glb b/art/exported/characters/base/netfishing_base_character.glb index c7fcf83..679fd69 100644 Binary files a/art/exported/characters/base/netfishing_base_character.glb and b/art/exported/characters/base/netfishing_base_character.glb differ diff --git a/art/source/characters/base/netfishing_character_source.blend b/art/source/characters/base/netfishing_character_source.blend index 2b345d1..2d419f7 100644 Binary files a/art/source/characters/base/netfishing_character_source.blend and b/art/source/characters/base/netfishing_character_source.blend differ diff --git a/art/source/characters/base/netfishing_character_source.blend1 b/art/source/characters/base/netfishing_character_source.blend1 index c6f3af2..72e072b 100644 Binary files a/art/source/characters/base/netfishing_character_source.blend1 and b/art/source/characters/base/netfishing_character_source.blend1 differ diff --git a/economy/fishing_shop_stock.gd b/economy/fishing_shop_stock.gd index c07c763..e74232e 100644 --- a/economy/fishing_shop_stock.gd +++ b/economy/fishing_shop_stock.gd @@ -6,6 +6,8 @@ const ItemDataType = preload("res://items/item_data.gd") const PlayerBagType = preload("res://inventory/player_bag.gd") const PlayerWalletType = preload("res://economy/player_wallet.gd") const WORM_MAX_STACK: int = 10 +const FISH_FINDER_ID: StringName = &"fish_finder" +const BATTERIES_ID: StringName = &"batteries" const ITEM_PRICES: Dictionary[StringName, int] = { &"worms": 1, @@ -15,10 +17,12 @@ const ITEM_PRICES: Dictionary[StringName, int] = { &"whole_anchovy": 30, &"whole_sardine": 60, &"luminous_roe": 125, + &"the_standby": 750, &"coffee": 20, &"energy_drink": 35, &"snack": 30, - &"fish_finder": 60, + FISH_FINDER_ID: 500, + BATTERIES_ID: 15, } const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = { &"snails": 400, @@ -36,10 +40,12 @@ const ITEM_ORDER: Array[StringName] = [ &"whole_anchovy", &"whole_sardine", &"luminous_roe", + &"the_standby", + FISH_FINDER_ID, &"coffee", &"energy_drink", &"snack", - &"fish_finder", + BATTERIES_ID, ] @@ -69,6 +75,20 @@ static func is_bait_topoff(item_id: StringName) -> bool: return item_id == &"worms" or BAIT_UNLOCK_PRICES.has(item_id) +static func is_permanent_unlock( + item_id: StringName, + item: ItemDataType, +) -> bool: + return ( + item != null + and (item.is_lure() or item_id == FISH_FINDER_ID) + ) + + +static func is_passive_supply(item_id: StringName) -> bool: + return item_id == BATTERIES_ID + + static func get_purchase_cost( item_id: StringName, quantity: int, @@ -92,6 +112,7 @@ static func purchase_one( return false var item: ItemDataType = catalog.get_item_by_id(item_id) var bait_topoff: bool = is_bait_topoff(item_id) + var permanent_unlock: bool = is_permanent_unlock(item_id, item) var quantity: int = get_purchase_quantity( item_id, bag.get_quantity(item_id), @@ -106,9 +127,18 @@ static func purchase_one( price < 0 or item == null or not item.is_valid() - or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff) - or not item.stackable - or (not item.usable and not bait_topoff) + or ( + item.category != ItemDataType.Category.CONSUMABLE + and not bait_topoff + and not permanent_unlock + ) + or (not item.stackable and not permanent_unlock) + or ( + not item.usable + and not bait_topoff + and not permanent_unlock + and not is_passive_supply(item_id) + ) or not bag.can_add_item(item_id, quantity) or not wallet.can_afford(price) ): diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd index 2981491..28ca93d 100644 --- a/fish/fish_quality.gd +++ b/fish/fish_quality.gd @@ -19,8 +19,8 @@ const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0] const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0] const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0] const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0] -const ANCHOVY_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] -const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] +const SARDINE_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] +const MINNOW_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] # Legacy profile multiplier retained for serialized/profile compatibility. @@ -158,9 +158,9 @@ static func rarity_weights_for_bait( elif active_bait_tags.has(&"bait_rarity_6"): weights.assign(LUMINOUS_ROE_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_5"): - weights.assign(SARDINE_ROLL_WEIGHTS) + weights.assign(MINNOW_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_4"): - weights.assign(ANCHOVY_ROLL_WEIGHTS) + weights.assign(SARDINE_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_3"): weights.assign(SQUID_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_2"): diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index b275bcb..cadd214 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -47,6 +47,7 @@ const WorldWeatherServiceType = preload( ) signal status_changed(status: String) +signal local_speech_requested(message: String) signal catch_display_changed( progress: float, chase_progress: float, @@ -64,6 +65,7 @@ signal showcase_changed( visible: bool, ) signal bite_activated +signal bite_prompt_changed(is_visible: bool) signal ready_for_equipment_refresh signal fish_showcase_toggle_requested signal art_ui_toggle_requested @@ -175,6 +177,8 @@ var _network_auto_click_accumulator: float = 0.0 var _network_active_barrier_index: int = -1 var _bite_rng: RandomNumberGenerator = RandomNumberGenerator.new() var _pending_cleanup_message: String = "" +var _bite_confirmation_pending: bool = false +var _bite_confirmation_requested: bool = false func _ready() -> void: @@ -251,6 +255,9 @@ func setup( _network_fishing.local_bite_started.connect( _on_network_bite_started ) + _network_fishing.local_bite_pending.connect( + _on_network_bite_pending + ) _network_fishing.local_snapshot_received.connect( _on_network_fishing_snapshot ) @@ -514,14 +521,25 @@ func _unhandled_input(event: InputEvent) -> void: return if ( active_item != null - and active_item.category == ItemDataType.Category.CONSUMABLE + and ( + active_item.category == ItemDataType.Category.CONSUMABLE + or ( + active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID + ) + ) ): if _network_item_use != null: _network_item_use.request_use(active_item.item_id) - elif _item_effects.use_consumable(active_item, _local_bag): + elif _item_effects.use_item(active_item, _local_bag): status_changed.emit(_item_effects.get_feedback( active_item.item_id )) + elif ( + active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID + ): + local_speech_requested.emit( + PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE + ) get_viewport().set_input_as_handled() return if not has_active_fishing_rod(): @@ -552,6 +570,10 @@ func _unhandled_input(event: InputEvent) -> void: else: return FishingState.WAITING_FOR_BITE: + if _bite_confirmation_pending: + confirm_pending_bite() + get_viewport().set_input_as_handled() + return _withdrawal_input_held = true _start_reeling_audio() if _network_fishing != null: @@ -568,6 +590,9 @@ func _unhandled_input(event: InputEvent) -> void: _confirm_cast() get_viewport().set_input_as_handled() elif state == FishingState.WAITING_FOR_BITE: + if _bite_confirmation_pending: + get_viewport().set_input_as_handled() + return _withdrawal_input_held = false _stop_reeling_audio() if _network_fishing != null: @@ -682,6 +707,28 @@ func is_fighting() -> bool: return state == FishingState.FIGHTING +func confirm_pending_bite() -> void: + if state != FishingState.WAITING_FOR_BITE or not _bite_confirmation_pending: + return + _bite_confirmation_requested = true + if _network_fishing != null: + _network_primary_input_held = false + _network_input_resend_elapsed = 0.0 + _network_fishing.submit_local_input(false, true) + else: + _activate_bite(true) + + +func _set_bite_confirmation_pending(is_pending: bool) -> void: + if _bite_confirmation_pending == is_pending: + if not is_pending: + _bite_confirmation_requested = false + return + _bite_confirmation_pending = is_pending + _bite_confirmation_requested = false + bite_prompt_changed.emit(is_pending) + + func is_returning() -> bool: return state == FishingState.RETURNING @@ -791,6 +838,7 @@ func _on_cast_completed() -> void: _consume_active_bait() state = FishingState.WAITING_FOR_BITE + _active_player.set_fishing_visual(true) _state_time_remaining = roll_bite_wait_time() * ( _item_effects.get_bite_time_multiplier() if _item_effects != null @@ -845,6 +893,8 @@ func roll_bite_wait_time() -> float: func _update_waiting_for_bite(delta: float) -> void: + if _bite_confirmation_pending: + return if ( _withdrawal_input_held and not Input.is_action_pressed("fish_primary") @@ -939,7 +989,7 @@ func _get_withdrawable_distance() -> float: return landing_offset.length() - withdrawal_cancel_distance -func _activate_bite() -> void: +func _activate_bite(confirmation_override: bool = false) -> void: if state != FishingState.WAITING_FOR_BITE: return @@ -951,7 +1001,19 @@ func _activate_bite() -> void: ): _cancel_attempt() return + if ( + not confirmation_override + and _active_lure_has_effect(&"deferred_fight") + ): + _state_time_remaining = 0.0 + _withdrawal_input_held = false + _set_bite_confirmation_pending(true) + status_changed.emit("") + _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) + _presentation.show_bite() + return + _set_bite_confirmation_pending(false) state = FishingState.FIGHTING _active_player.set_fighting_visual(true) _state_time_remaining = 0.0 @@ -1043,6 +1105,7 @@ func _on_catch_completed() -> void: return _stop_fight_audio() _active_player.set_fighting_visual(false) + _active_player.set_fishing_visual(false) state = FishingState.SHOWING_CATCH _showcase_ready = false _showcase_outcome_completed = false @@ -1163,10 +1226,12 @@ func _cleanup_attempt( cooldown_message: String = "", visual_outcome: StringName = &"", ) -> void: + _set_bite_confirmation_pending(false) _stop_fight_audio() _stop_reeling_audio() if _active_player != null: _active_player.set_fighting_visual(false) + _active_player.set_fishing_visual(false) if not visual_outcome.is_empty(): if state == FishingState.RETURNING: return @@ -1196,6 +1261,7 @@ func _finalize_attempt_cleanup(cooldown_message: String) -> void: _showcase_restore_generation += 1 if _active_player != null: _active_player.set_fighting_visual(false) + _active_player.set_fishing_visual(false) _active_player.end_catch_showcase() _active_player.set_movement_enabled(true) _active_player = null @@ -1410,6 +1476,21 @@ func _get_active_bait_tags() -> Array[StringName]: return item.bait_tags.duplicate() if item != null and item.is_bait() else [] +func _active_lure_has_effect(effect_id: StringName) -> bool: + if ( + _local_player == null + or _item_catalog == null + or _local_bag == null + or _local_player.active_lure_id.is_empty() + or not _local_bag.owns_item(_local_player.active_lure_id) + ): + return false + var lure: ItemDataType = _item_catalog.get_item_by_id( + _local_player.active_lure_id + ) + return lure != null and lure.is_lure() and effect_id in lure.lure_effects + + func _build_network_evidence() -> Dictionary: var rarity_multipliers: Array[float] = [] for rarity: int in range(FishDataType.Rarity.size()): @@ -1421,6 +1502,7 @@ func _build_network_evidence() -> Dictionary: var item: ItemDataType = _get_active_item() return { "bait_id": str(_active_player.active_bait_id), + "lure_id": str(_active_player.active_lure_id), "rod_id": str(item.item_id) if item != null else "", "reel_speed": _active_player.reel_speed * ( _fishing_upgrades.get_reel_speed_multiplier() @@ -1441,6 +1523,13 @@ func _on_network_item_use_finished( accepted: bool, message: String, ) -> void: + if ( + not accepted + and message == PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE + ): + status_changed.emit("") + local_speech_requested.emit(message) + return status_changed.emit(message) if accepted: refresh_active_item_status() @@ -1455,6 +1544,8 @@ func _on_network_cast_accepted( _cast_target = target _bobber_water_position = target state = FishingState.WAITING_FOR_BITE + if _active_player != null: + _active_player.set_fishing_visual(true) _network_primary_input_held = false _network_input_resend_elapsed = 0.0 _presentation.show_withdrawal_position(_bobber_water_position) @@ -1478,10 +1569,24 @@ func _on_network_cast_rejected(message: String) -> void: _cleanup_attempt(visible_message, &"invalid") +func _on_network_bite_pending(_attempt_id: String) -> void: + if state != FishingState.WAITING_FOR_BITE: + return + _stop_reeling_audio() + _withdrawal_input_held = false + _network_primary_input_held = false + _network_input_resend_elapsed = 0.0 + _set_bite_confirmation_pending(true) + status_changed.emit("") + _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) + _presentation.show_bite() + + func _on_network_bite_started(_attempt_id: String) -> void: if state != FishingState.WAITING_FOR_BITE: return _stop_reeling_audio() + _set_bite_confirmation_pending(false) state = FishingState.FIGHTING if _active_player != null: _active_player.set_fighting_visual(true) @@ -1512,7 +1617,10 @@ func _resend_network_input_state(delta: float) -> void: _network_input_resend_elapsed, NETWORK_INPUT_RESEND_INTERVAL_SECONDS, ) - _network_fishing.submit_local_input(_network_primary_input_held, false) + _network_fishing.submit_local_input( + false if _bite_confirmation_pending else _network_primary_input_held, + _bite_confirmation_pending and _bite_confirmation_requested, + ) func _on_network_fishing_snapshot(snapshot: Dictionary) -> void: @@ -1577,6 +1685,7 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void: _pending_catch = fish_catch if _active_player != null: _active_player.set_fighting_visual(false) + _active_player.set_fishing_visual(false) state = FishingState.SHOWING_CATCH _showcase_ready = false _showcase_outcome_completed = false diff --git a/items/catalog/batteries.tres b/items/catalog/batteries.tres new file mode 100644 index 0000000..9f26650 --- /dev/null +++ b/items/catalog/batteries.tres @@ -0,0 +1,15 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] + +[resource] +script = ExtResource("1_item") +item_id = &"batteries" +display_name = "batteries" +description = "replacement batteries for the fish finder." +category = 3 +stackable = true +max_stack = 99 +usable = false +equippable = false +hotbar_allowed = false diff --git a/items/catalog/fish_finder.tres b/items/catalog/fish_finder.tres index 4c6d4a2..a3fb6c4 100644 --- a/items/catalog/fish_finder.tres +++ b/items/catalog/fish_finder.tres @@ -7,10 +7,11 @@ script = ExtResource("1") item_id = &"fish_finder" display_name = "fish finder" -description = "temporarily shortens bite waits and improves rarity weights." -category = 3 +description = "uses one battery to temporarily shorten bite waits and improve rarity weights." +category = 1 icon = ExtResource("2") -stackable = true -max_stack = 99 +stackable = false +max_stack = 1 usable = true +equippable = true hotbar_allowed = true diff --git a/items/catalog/item_catalog.tres b/items/catalog/item_catalog.tres index 2cd0546..f8c0f5d 100644 --- a/items/catalog/item_catalog.tres +++ b/items/catalog/item_catalog.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemCatalog" load_steps=16 format=3] +[gd_resource type="Resource" script_class="ItemCatalog" load_steps=18 format=3] [ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"] [ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"] @@ -15,7 +15,9 @@ [ext_resource type="Resource" path="res://items/catalog/whole_anchovy.tres" id="13_anchovy"] [ext_resource type="Resource" path="res://items/catalog/whole_sardine.tres" id="14_sardine"] [ext_resource type="Resource" path="res://items/catalog/luminous_roe.tres" id="15_roe"] +[ext_resource type="Resource" path="res://items/catalog/the_standby.tres" id="16_standby"] +[ext_resource type="Resource" path="res://items/catalog/batteries.tres" id="17_batteries"] [resource] script = ExtResource("1_script") -items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("13_anchovy"), ExtResource("14_sardine"), ExtResource("15_roe")] +items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("13_anchovy"), ExtResource("14_sardine"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries")] diff --git a/items/catalog/the_standby.tres b/items/catalog/the_standby.tres new file mode 100644 index 0000000..1fcc92e --- /dev/null +++ b/items/catalog/the_standby.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] + +[resource] +script = ExtResource("1_item") +item_id = &"the_standby" +display_name = "The Standby" +description = "holds a bite until you're ready to start the fight." +category = 7 +lure_effects = Array[StringName]([&"deferred_fight"]) +stackable = false +max_stack = 1 +usable = false +equippable = true +hotbar_allowed = false diff --git a/items/catalog/whole_anchovy.tres b/items/catalog/whole_anchovy.tres index f640941..0aee243 100644 --- a/items/catalog/whole_anchovy.tres +++ b/items/catalog/whole_anchovy.tres @@ -5,8 +5,8 @@ [resource] script = ExtResource("1") item_id = &"whole_anchovy" -display_name = "whole anchovy" -description = "premium whole bait with high rare catch potential." +display_name = "sardine" +description = "oily schooling bait with high rare catch potential." category = 2 bait_tags = Array[StringName]([&"bait_rarity_4"]) stackable = true diff --git a/items/catalog/whole_sardine.tres b/items/catalog/whole_sardine.tres index b5ccb52..d03ac81 100644 --- a/items/catalog/whole_sardine.tres +++ b/items/catalog/whole_sardine.tres @@ -5,8 +5,8 @@ [resource] script = ExtResource("1") item_id = &"whole_sardine" -display_name = "whole sardine" -description = "valuable whole bait tuned for exceptional catches." +display_name = "minnow" +description = "valuable live bait tuned for exceptional catches." category = 2 bait_tags = Array[StringName]([&"bait_rarity_5"]) stackable = true diff --git a/items/item_data.gd b/items/item_data.gd index 9ba078e..a34cec0 100644 --- a/items/item_data.gd +++ b/items/item_data.gd @@ -17,6 +17,7 @@ enum Category { @export_multiline var description: String @export var category: Category = Category.UTILITY @export var bait_tags: Array[StringName] = [] +@export var lure_effects: Array[StringName] = [] @export var icon: Texture2D @export var stackable: bool = false @export_range(1, 999, 1) var max_stack: int = 1 @@ -40,3 +41,7 @@ func get_category_name() -> String: func is_bait() -> bool: return category == Category.BAIT and not bait_tags.is_empty() + + +func is_lure() -> bool: + return category == Category.LURE and equippable diff --git a/network/network_fishing_attempt.gd b/network/network_fishing_attempt.gd index 7ad71ae..3a1b1de 100644 --- a/network/network_fishing_attempt.gd +++ b/network/network_fishing_attempt.gd @@ -22,8 +22,10 @@ var target: Vector3 var bobber_position: Vector3 var fish_id: StringName var bait_tags: Array[StringName] = [] +var lure_effects: Array[StringName] = [] var encounter_seed: int = 0 var bite_time_remaining: float = 0.0 +var bite_confirmation_pending: bool = false var withdrawal_progress: float = 0.0 var reel_speed: float = 0.0 var barrier_damage: int = 1 diff --git a/network/network_fishing_protocol.gd b/network/network_fishing_protocol.gd index 547b255..2e9a342 100644 --- a/network/network_fishing_protocol.gd +++ b/network/network_fishing_protocol.gd @@ -55,6 +55,8 @@ static func validate_cast_request(data: Variant) -> String: return "Malformed fishing request." if payload.has("bait_id") and typeof(payload["bait_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]: return "Malformed fishing request." + if payload.has("lure_id") and typeof(payload["lure_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]: + return "Malformed fishing request." var request_id: String = payload["request_id"] var session_id: String = payload["session_id"] var origin: Array = payload["origin"] @@ -89,6 +91,7 @@ static func validate_cast_request(data: Variant) -> String: or discovered.size() > MAX_EVIDENCE_FISH_IDS or str(payload["rod_id"]).is_empty() or str(payload["rod_id"]).length() > 96 + or str(payload.get("lure_id", "")).length() > 96 ): return "Fishing request values are outside allowed limits." for value: Variant in rarity: diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index a15b929..af84bdd 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -24,6 +24,7 @@ const MIN_CAST_INTERVAL: float = 0.25 signal local_cast_accepted(attempt_id: String, target: Vector3) signal local_cast_rejected(message: String) signal local_bite_started(attempt_id: String) +signal local_bite_pending(attempt_id: String) signal local_snapshot_received(snapshot: Dictionary) signal local_catch_received(fish_catch: FishCatch) signal local_attempt_ended(outcome: StringName, message: String) @@ -111,6 +112,7 @@ func request_local_cast( "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", "")), } if _session.is_host(): _handle_cast_request(_session.get_local_peer_id(), data) @@ -176,12 +178,17 @@ func _process(delta: float) -> void: continue match attempt.phase: NetworkFishingAttempt.Phase.WAITING_FOR_BITE: + if attempt.bite_confirmation_pending: + continue _update_waiting_attempt(attempt, delta) if not _attempts.has(peer_id): continue attempt.bite_time_remaining -= delta if attempt.bite_time_remaining <= 0.0: - _start_bite(attempt) + if &"deferred_fight" in attempt.lure_effects: + _set_bite_pending(attempt) + else: + _start_bite(attempt) NetworkFishingAttempt.Phase.PENDING_CAPACITY: if now >= attempt.capacity_deadline: _cancel_attempt(peer_id, "Fishing attempt ended.") @@ -296,6 +303,23 @@ 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 lure_id: StringName = StringName(str(data.get("lure_id", ""))) + var lure: ItemDataType = ( + _item_catalog.get_item_by_id(lure_id) + if not lure_id.is_empty() and _item_catalog != null + else null + ) + if ( + not lure_id.is_empty() + and ( + lure == null + or not lure.is_lure() + or 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)): _record_and_reject(peer_id, request_id, "No bait available.") return @@ -310,6 +334,10 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void: attempt.bobber_position = authoritative_target attempt.fish_id = selected_fish.id attempt.bait_tags = _bait_tags_for_request(data) + attempt.lure_effects.clear() + if lure != null: + for effect_id: StringName in lure.lure_effects: + attempt.lure_effects.append(effect_id) attempt.reel_speed = float(data["reel_speed"]) * ( effects.get_reel_multiplier() if effects != null else 1.0 ) @@ -437,6 +465,23 @@ func _bait_tags_for_request(data: Dictionary) -> Array[StringName]: return bait.bait_tags.duplicate() if bait != null and bait.is_bait() else [] +func _set_bite_pending(attempt: NetworkFishingAttempt) -> void: + if ( + attempt.phase != NetworkFishingAttempt.Phase.WAITING_FOR_BITE + or attempt.bite_confirmation_pending + ): + return + attempt.bite_confirmation_pending = true + attempt.bite_time_remaining = 0.0 + attempt.input_held = false + var data: Dictionary = { + "attempt_id": attempt.attempt_id, + "owner_peer_id": attempt.owner_peer_id, + } + _apply_bite_pending(data) + receive_bite_pending.rpc(data) + + func _start_bite(attempt: NetworkFishingAttempt) -> void: if attempt.phase != NetworkFishingAttempt.Phase.WAITING_FOR_BITE: return @@ -444,6 +489,7 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void: if fish == null or fish.catch_profile == null: _cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.") return + attempt.bite_confirmation_pending = false attempt.phase = NetworkFishingAttempt.Phase.FIGHTING attempt.encounter_seed = _new_seed() var selector := FishSelectorType.new() @@ -496,6 +542,14 @@ func _handle_fishing_input(peer_id: int, data: Dictionary) -> void: return attempt.last_input_sequence = int(data["sequence"]) attempt.input_held = bool(data["held"]) + if ( + attempt.phase == NetworkFishingAttempt.Phase.WAITING_FOR_BITE + and attempt.bite_confirmation_pending + ): + if bool(data["pressed"]): + attempt.input_held = false + _start_bite(attempt) + return if attempt.phase != NetworkFishingAttempt.Phase.FIGHTING: return attempt.controller.set_reel_input(attempt.input_held) @@ -978,6 +1032,21 @@ func _apply_cast_accepted(data: Dictionary) -> void: presentation.show_cast(origin, target) +@rpc("authority", "call_remote", "reliable", 0) +func receive_bite_pending(data: Dictionary) -> void: + _apply_bite_pending(data) + + +func _apply_bite_pending(data: Dictionary) -> void: + if ( + typeof(data.get("attempt_id")) != TYPE_STRING + or typeof(data.get("owner_peer_id")) != TYPE_INT + ): + return + if int(data["owner_peer_id"]) == _session.get_local_peer_id(): + local_bite_pending.emit(str(data["attempt_id"])) + + @rpc("authority", "call_remote", "reliable", 0) func receive_bite_started(data: Dictionary) -> void: _apply_bite_started(data) diff --git a/network/network_item_use_service.gd b/network/network_item_use_service.gd index 654b45b..34b0530 100644 --- a/network/network_item_use_service.gd +++ b/network/network_item_use_service.gd @@ -50,9 +50,19 @@ func request_use(item_id: StringName) -> String: if not _pending_local.is_empty(): local_item_use_finished.emit(false, "An item use is already pending.") return "" + var consumed_item_id: StringName = _consumed_item_id(item_id) + if item_id == PlayerItemEffects.FISH_FINDER_ID and ( + _local_bag == null + or not _local_bag.owns_item(item_id) + or _local_bag.get_quantity(consumed_item_id) < 1 + ): + local_item_use_finished.emit( + false, PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE + ) + return "" if ( _reservations != null - and _reservations.get_available_item_quantity(item_id) < 1 + and _reservations.get_available_item_quantity(consumed_item_id) < 1 ): local_item_use_finished.emit(false, "Reserved in a letter.") return "" @@ -73,7 +83,7 @@ func request_use(item_id: StringName) -> String: "request_id": request_id, "session_id": _session.get_session_id(), "item_id": str(item_id), - "quantity": _local_bag.get_quantity(item_id), + "quantity": _local_bag.get_quantity(consumed_item_id), } _pending_local = data.duplicate(true) local_item_use_pending.emit(request_id) @@ -109,14 +119,28 @@ func _handle_use_request(peer_id: int, data: Dictionary) -> void: var item: ItemData = _catalog.get_item_by_id(item_id) var avatar: Player = _spawn_service.get_avatar(peer_id) var duration: float = 0.0 + var is_fish_finder: bool = ( + item_id == PlayerItemEffects.FISH_FINDER_ID + and item != null + and item.category == ItemData.Category.TOOL + and item.usable + and item.equippable + ) if error.is_empty() and ( item == null - or item.category != ItemData.Category.CONSUMABLE + or ( + item.category != ItemData.Category.CONSUMABLE + and not is_fish_finder + ) or not item.usable ): error = "That item cannot be used now." if error.is_empty() and int(data["quantity"]) < 1: - error = "You do not have that item." + error = ( + PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE + if is_fish_finder + else "You do not have that item." + ) if error.is_empty() and ( avatar == null or avatar.is_water_recovery_active() @@ -180,9 +204,10 @@ func _apply_result(data: Dictionary) -> void: _acknowledge(data, false, str(data["message"])) return var item_id := StringName(str(data["item_id"])) + var consumed_item_id: StringName = _consumed_item_id(item_id) if ( _reservations != null - and _reservations.get_available_item_quantity(item_id) < 1 + and _reservations.get_available_item_quantity(consumed_item_id) < 1 ): _pending_local.clear() local_item_use_finished.emit(false, "Reserved in a letter.") @@ -190,8 +215,8 @@ func _apply_result(data: Dictionary) -> void: return var bag_snapshot := _local_bag.get_all_items() if ( - _local_bag.get_quantity(item_id) < 1 - or not _local_bag.remove_item(item_id, 1) + _local_bag.get_quantity(consumed_item_id) < 1 + or not _local_bag.remove_item(consumed_item_id, 1) or not _save_manager.save_if_dirty() ): _local_bag.replace_all_items(bag_snapshot) @@ -409,3 +434,11 @@ func _new_id(prefix: String) -> String: return "%s:%s" % [ prefix, Crypto.new().generate_random_bytes(16).hex_encode(), ] + + +func _consumed_item_id(item_id: StringName) -> StringName: + return ( + PlayerItemEffects.BATTERIES_ID + if item_id == PlayerItemEffects.FISH_FINDER_ID + else item_id + ) diff --git a/network/network_shop_service.gd b/network/network_shop_service.gd index fd7240e..a91c5bc 100644 --- a/network/network_shop_service.gd +++ b/network/network_shop_service.gd @@ -335,15 +335,29 @@ func _build_authoritative_result( var bait_topoff: bool = ( FishingShopStockType.is_bait_topoff(product_id) ) + var permanent_unlock: bool = ( + FishingShopStockType.is_permanent_unlock(product_id, item) + ) if ( item == null or not item.is_valid() or product_id not in ( FishingShopStockType.get_stock_item_ids() ) - or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff) - or not item.stackable - or (not item.usable and not bait_topoff) + or ( + item.category != ItemDataType.Category.CONSUMABLE + and not bait_topoff + and not permanent_unlock + ) + or (not item.stackable and not permanent_unlock) + or ( + not item.usable + and not bait_topoff + and not permanent_unlock + and not FishingShopStockType.is_passive_supply( + product_id + ) + ) or current_state >= item.max_stack or current_state + quantity > item.max_stack ): diff --git a/player/fishing_rod_attachment.tscn b/player/fishing_rod_attachment.tscn index af895f5..b7f18f0 100644 --- a/player/fishing_rod_attachment.tscn +++ b/player/fishing_rod_attachment.tscn @@ -13,12 +13,12 @@ roughness = 0.8 bone_name = "hand.R" [node name="FishingRod" type="Node3D" parent="."] -rotation_degrees = Vector3(322.6, -31.2, 248.9) +rotation_degrees = Vector3(65, 0, 0) [node name="RodMesh" type="MeshInstance3D" parent="FishingRod"] -position = Vector3(0, 0.42, 0) +position = Vector3(0, 0.5, 0) mesh = SubResource("FishingRodMesh") material_override = SubResource("FishingRodMaterial") [node name="FishingRodTip" type="Marker3D" parent="FishingRod"] -position = Vector3(0, 0.95, 0) +position = Vector3(0, 1.1, 0) diff --git a/player/player.gd b/player/player.gd index f006af4..8413be3 100644 --- a/player/player.gd +++ b/player/player.gd @@ -41,6 +41,10 @@ const CHARACTER_IDLE_SIT_ANIMATION: StringName = &"idle_sit" const CHARACTER_IDLE_SIT_SHOW_ANIMATION: StringName = &"idle_sit_show" const CHARACTER_WALKING_ANIMATION: StringName = &"walking" const CHARACTER_WALKING_SHOW_ANIMATION: StringName = &"walking_show" +const CHARACTER_FISHING_ANIMATION: StringName = &"fishing" +const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit" +const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting" +const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit" const FIGHTING_EYES_ID: String = "alligator_eyes" const BASE_REEL_SPEED: float = 0.16 # The target Android handheld exposes its physical right trigger through @@ -53,7 +57,9 @@ var appearance_snapshot: Dictionary = ( ) var animalese_voice_id: String = "natural" var active_bait_id: StringName = StringName() +var active_lure_id: StringName = StringName() signal active_bait_changed(item_id: StringName) +signal active_lure_changed(item_id: StringName) func equip_bait(item: ItemDataType) -> bool: @@ -73,6 +79,23 @@ func unequip_bait() -> void: active_bait_changed.emit(active_bait_id) +func equip_lure(item: ItemDataType) -> bool: + if item == null or not item.is_lure() or bag == null: + return false + if not bag.owns_item(item.item_id): + return false + active_lure_id = item.item_id + active_lure_changed.emit(active_lure_id) + return true + + +func unequip_lure() -> void: + if active_lure_id.is_empty(): + return + active_lure_id = StringName() + active_lure_changed.emit(active_lure_id) + + func apply_appearance_snapshot(snapshot: Dictionary) -> void: if CharacterCustomizationCatalog.validate_snapshot(snapshot): appearance_snapshot = snapshot.duplicate(true) @@ -93,6 +116,16 @@ func set_fighting_visual(active: bool) -> void: return _fighting_visual_active = active _apply_presented_appearance() + _character_animation_name = &"" + _update_character_animation() + + +func set_fishing_visual(active: bool) -> void: + if _fishing_visual_active == active: + return + _fishing_visual_active = active + _character_animation_name = &"" + _update_character_animation() func _apply_presented_appearance() -> void: @@ -246,6 +279,7 @@ var _sitting_intent_sequence: int = -1 var _held_fish_visible: bool = false var _showcase_animation_active: bool = false var _fighting_visual_active: bool = false +var _fishing_visual_active: bool = false var _fishing_rod: Node3D var _fishing_rod_tip: Marker3D var _controller_mapping_manager: ControllerMappingManagerType @@ -268,6 +302,11 @@ func _on_bag_contents_changed() -> void: and bag.get_quantity(active_bait_id) <= 0 ): unequip_bait() + if ( + not active_lure_id.is_empty() + and not bag.owns_item(active_lure_id) + ): + unequip_lure() func set_controller_mapping_manager( @@ -527,6 +566,30 @@ func _update_character_animation() -> void: &"show", &"show_loop", ] + elif _fighting_visual_active: + if _sitting: + requested_animation = [ + CHARACTER_FIGHTING_SIT_ANIMATION, + CHARACTER_IDLE_SIT_ANIMATION, + &"idle_sit_loop", + ] + else: + requested_animation = [ + CHARACTER_FIGHTING_ANIMATION, + CHARACTER_IDLE_ANIMATION, + ] + elif _fishing_visual_active: + if _sitting: + requested_animation = [ + CHARACTER_FISHING_SIT_ANIMATION, + CHARACTER_IDLE_SIT_ANIMATION, + &"idle_sit_loop", + ] + else: + requested_animation = [ + CHARACTER_FISHING_ANIMATION, + CHARACTER_IDLE_ANIMATION, + ] elif _sitting: if _held_fish_visible: requested_animation = [ diff --git a/player/player_ground_shadow.gd b/player/player_ground_shadow.gd index 9a4bb6f..0efa948 100644 --- a/player/player_ground_shadow.gd +++ b/player/player_ground_shadow.gd @@ -54,7 +54,7 @@ func _physics_process(delta: float) -> void: _visual.transparency = 1.0 - _opacity -func _place_on_surface(position: Vector3, normal: Vector3) -> void: +func _place_on_surface(surface_position: Vector3, normal: Vector3) -> void: var tangent := Vector3.RIGHT - normal * Vector3.RIGHT.dot(normal) if tangent.length_squared() < 0.001: tangent = Vector3.FORWARD - normal * Vector3.FORWARD.dot(normal) @@ -62,5 +62,5 @@ func _place_on_surface(position: Vector3, normal: Vector3) -> void: var bitangent := tangent.cross(normal).normalized() _placement.global_transform = Transform3D( Basis(tangent, normal, bitangent), - position + normal * surface_offset, + surface_position + normal * surface_offset, ) diff --git a/progression/player_item_effects.gd b/progression/player_item_effects.gd index d7542fc..1a73c43 100644 --- a/progression/player_item_effects.gd +++ b/progression/player_item_effects.gd @@ -10,6 +10,8 @@ const COFFEE_ID: StringName = &"coffee" const ENERGY_DRINK_ID: StringName = &"energy_drink" const SNACK_ID: StringName = &"snack" const FISH_FINDER_ID: StringName = &"fish_finder" +const BATTERIES_ID: StringName = &"batteries" +const FISH_FINDER_DEAD_MESSAGE: String = "hmm... batteries are dead..." const COFFEE_DURATION: float = 90.0 const ENERGY_DRINK_DURATION: float = 90.0 const SNACK_DURATION: float = 60.0 @@ -45,16 +47,35 @@ func use_consumable( item: ItemDataType, bag: PlayerBagType, ) -> bool: + return use_item(item, bag) + + +func use_item( + item: ItemDataType, + bag: PlayerBagType, +) -> bool: + var is_fish_finder: bool = ( + item != null + and item.item_id == FISH_FINDER_ID + and item.category == ItemDataType.Category.TOOL + and item.equippable + ) if ( item == null or bag == null - or item.category != ItemDataType.Category.CONSUMABLE + or ( + item.category != ItemDataType.Category.CONSUMABLE + and not is_fish_finder + ) or not item.usable or not _remaining.has(item.item_id) or not bag.owns_item(item.item_id) ): return false - if not bag.remove_item(item.item_id, 1): + var consumed_item_id: StringName = ( + BATTERIES_ID if is_fish_finder else item.item_id + ) + if not bag.remove_item(consumed_item_id, 1): return false _remaining[item.item_id] = _get_duration(item.item_id) effects_changed.emit() diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 2d4e5fb..69d35e9 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -34,6 +34,10 @@ const CLOCK_SIZE := Vector2(116.0, 34.0) const CLOCK_EDGE_MARGIN: float = 10.0 const WEATHER_ICON_SIZE := Vector2(34.0, 34.0) const WEATHER_ICON_GAP: float = 6.0 +const FISH_FINDER_EFFECT_ICON_SIZE := Vector2(18.0, 18.0) +const FISH_FINDER_EFFECT_ICON: Texture2D = preload( + "res://items/icons/consumables/64_consumable_fishfinder.png" +) const CHAT_SHOW_ICON: Texture2D = preload( "res://ui/icons/pictograms/arrow_light_right_more.png" ) @@ -86,6 +90,7 @@ var _animalese_voice: AnimaleseVoiceType var _clock_panel: PanelContainer var _clock_label: Label var _weather_icon: WeatherIconType +var _fish_finder_effect_icon: TextureRect var _speech: Dictionary[int, Dictionary] = {} var _draft_save_timer: Timer var _opacity_tween: Tween @@ -110,6 +115,7 @@ var _dock_right: bool = false var _mobile_mode: bool = false var _world_time: WorldTimeServiceType var _world_weather: WorldWeatherServiceType +var _item_effects: PlayerItemEffects func _ready() -> void: @@ -129,6 +135,7 @@ func setup( settings: PlayerSettingsManager, world_time: WorldTimeServiceType, world_weather: WorldWeatherServiceType, + item_effects: PlayerItemEffects, ) -> void: _service = service _session = session @@ -138,6 +145,14 @@ func setup( _settings = settings _world_time = world_time _world_weather = world_weather + _item_effects = item_effects + if ( + _fishing_spot != null + and not _fishing_spot.local_speech_requested.is_connected( + show_local_speech + ) + ): + _fishing_spot.local_speech_requested.connect(show_local_speech) if ( _world_time != null and not _world_time.time_changed.is_connected( @@ -361,6 +376,15 @@ func _build_ui() -> void: "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) _clock_panel.add_child(_clock_label) + _fish_finder_effect_icon = TextureRect.new() + _fish_finder_effect_icon.name = "FishFinderEffectIcon" + _fish_finder_effect_icon.texture = FISH_FINDER_EFFECT_ICON + _fish_finder_effect_icon.size = FISH_FINDER_EFFECT_ICON_SIZE + _fish_finder_effect_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + _fish_finder_effect_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + _fish_finder_effect_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + _fish_finder_effect_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_fish_finder_effect_icon) _weather_icon = WeatherIconType.new() _weather_icon.name = "WorldWeatherIcon" _weather_icon.size = WEATHER_ICON_SIZE @@ -800,6 +824,24 @@ func _on_message(message: Dictionary) -> void: if int(message["kind"]) != NetworkChatProtocol.Kind.PLAYER: return var peer_id: int = message["sender_peer_id"] + _show_speech_bubble( + peer_id, + str(message["body"]), + str(message.get("sender_fingerprint", "")), + ) + + +func show_local_speech(body: String) -> void: + if body.strip_edges().is_empty() or _session == null: + return + _show_speech_bubble(_session.get_local_peer_id(), body, "") + + +func _show_speech_bubble( + peer_id: int, + body: String, + fingerprint: String, +) -> void: _on_peer_removed(peer_id) var bubble := PanelContainer.new() bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -808,7 +850,7 @@ func _on_message(message: Dictionary) -> void: var pointer := _create_speech_pointer() bubble.add_child(pointer) var label := Label.new() - label.text = str(message["body"]) + label.text = body label.custom_minimum_size = Vector2( SPEECH_BUBBLE_WIDTH - 20.0, 0.0, @@ -844,7 +886,7 @@ func _on_message(message: Dictionary) -> void: + reveal_seconds + SPEECH_SECONDS ), - "fingerprint": str(message.get("sender_fingerprint", "")), + "fingerprint": fingerprint, } @@ -891,6 +933,7 @@ func _refresh_visibility() -> void: _panel.hide() _clock_panel.hide() _weather_icon.hide() + _fish_finder_effect_icon.hide() _collapse_button.hide() _height_button.hide() _unread_indicator.hide() @@ -898,6 +941,10 @@ func _refresh_visibility() -> void: return _clock_panel.show() _weather_icon.show() + _fish_finder_effect_icon.visible = ( + _item_effects != null + and _item_effects.is_active(PlayerItemEffects.FISH_FINDER_ID) + ) _collapse_button.show() var collapsed := _presentation_state == PresentationState.COLLAPSED _panel.show() @@ -1232,6 +1279,11 @@ func _layout_presentation(animate: bool) -> void: clock_x, CLOCK_EDGE_MARGIN, ) + var fish_finder_effect_position := Vector2( + clock_position.x + + (CLOCK_SIZE.x - FISH_FINDER_EFFECT_ICON_SIZE.x) * 0.5, + clock_position.y + CLOCK_SIZE.y + 2.0, + ) var weather_icon_x: float = ( clock_position.x - WEATHER_ICON_GAP - WEATHER_ICON_SIZE.x if _dock_right @@ -1255,6 +1307,8 @@ func _layout_presentation(animate: bool) -> void: _panel.size = target_size _clock_panel.position = clock_position _clock_panel.size = CLOCK_SIZE + _fish_finder_effect_icon.position = fish_finder_effect_position + _fish_finder_effect_icon.size = FISH_FINDER_EFFECT_ICON_SIZE _weather_icon.position = weather_icon_position _weather_icon.size = WEATHER_ICON_SIZE _collapse_button.position = collapse_position @@ -1275,6 +1329,12 @@ func _layout_presentation(animate: bool) -> void: clock_position, UIMotion.CHAT_RESIZE_DURATION, ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + _height_tween.tween_property( + _fish_finder_effect_icon, + "position", + fish_finder_effect_position, + UIMotion.CHAT_RESIZE_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) _height_tween.tween_property( _weather_icon, "position", diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 563e692..03fdf16 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -25,6 +25,7 @@ const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") signal menu_visibility_changed(is_open: bool) signal sell_fish_requested @@ -39,6 +40,22 @@ enum CloseReason { TEARDOWN, } +enum ShopSection { + UPGRADES, + BAIT, + SNACKS, + EQUIPMENT, + ART_SUPPLIES, +} + +const SHOP_SECTION_LABELS: Array[String] = [ + "Upgrades", + "Bait", + "Snacks", + "Equipment", + "Art Supplies", +] + @onready var _wallet_label: Label = %WalletLabel @onready var _shop_panel: PanelContainer = %ShopPanel @onready var _shop_cooler_page: Control = %ShopCoolerPage @@ -47,6 +64,10 @@ enum CloseReason { @onready var _buy_mode_button: Button = %BuyModeButton @onready var _sell_mode_button: Button = %SellModeButton @onready var _feedback: Label = %Feedback +@onready var _shop_tab_bar: HBoxContainer = %ShopTabBar +@onready var _upgrades_content: VBoxContainer = %Upgrades +@onready var _supplies_content: VBoxContainer = %Supplies +@onready var _stock_title: Label = %StockTitle @onready var _reel_level: Label = %ReelLevel @onready var _reel_effect: Label = %ReelEffect @onready var _reel_cost: Label = %ReelCost @@ -81,11 +102,14 @@ var _transaction_in_progress: bool = false var _closing: bool = false var _cooler_page_active: bool = false var _cooler_modal_open: bool = false +var _shop_section: ShopSection = ShopSection.UPGRADES +var _shop_tabs: Array[OrganizerTab] = [] func _ready() -> void: UtilityPageStyleType.apply_page(self) _apply_shop_styles() + _build_shop_tabs() %CloseButton.pressed.connect(close_shop) _buy_mode_button.pressed.connect(_focus_buy_page) _sell_mode_button.pressed.connect(_request_shop_cooler) @@ -107,12 +131,39 @@ func _request_shop_cooler() -> void: func _focus_buy_page() -> void: _feedback.text = "" + if _shop_section == ShopSection.UPGRADES: + _reel_purchase.grab_focus() + return for child: Node in _supplies_list.get_children(): var stock_button := child as Button if stock_button != null and not stock_button.disabled: stock_button.grab_focus() return - _reel_purchase.grab_focus() + _buy_mode_button.grab_focus() + + +func _build_shop_tabs() -> void: + for section_index: int in range(ShopSection.size()): + var tab: OrganizerTab = OrganizerTabType.new() + tab.text = SHOP_SECTION_LABELS[section_index] + tab.palette_index = section_index + tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tab.pressed.connect(_select_shop_section.bind(section_index, true)) + _shop_tab_bar.add_child(tab) + _shop_tabs.append(tab) + _select_shop_section(ShopSection.UPGRADES, false) + + +func _select_shop_section(section_index: int, focus_content: bool) -> void: + _shop_section = section_index as ShopSection + var showing_upgrades: bool = _shop_section == ShopSection.UPGRADES + _upgrades_content.visible = showing_upgrades + _supplies_content.visible = not showing_upgrades + for tab_index: int in range(_shop_tabs.size()): + _shop_tabs[tab_index].set_selected(tab_index == section_index, true) + _refresh_supplies() + if focus_content and visible: + _focus_buy_page() func _apply_shop_styles() -> void: @@ -236,6 +287,7 @@ func open_shop() -> bool: _feedback.text = "" deactivate_shop_cooler_page() show() + _select_shop_section(ShopSection.UPGRADES, false) _refresh_all() _buy_mode_button.grab_focus() menu_visibility_changed.emit(true) @@ -413,10 +465,12 @@ func _refresh_supplies() -> void: for child: Node in _supplies_list.get_children(): _supplies_list.remove_child(child) child.queue_free() - _add_stock_section("supplies") + if _shop_section == ShopSection.UPGRADES: + return + _stock_title.text = SHOP_SECTION_LABELS[int(_shop_section)] for item_id: StringName in FishingShopStockType.get_stock_item_ids(): var item: ItemDataType = _item_catalog.get_item_by_id(item_id) - if item == null: + if item == null or not _item_belongs_in_current_section(item): continue var button := Button.new() button.custom_minimum_size = Vector2(195, 54) @@ -492,17 +546,32 @@ func _refresh_supplies() -> void: UtilityPageStyleType.apply_ocean_button(button) button.pressed.connect(_purchase_supply.bind(item_id)) _supplies_list.add_child(button) - _add_stock_section("art kit") - _add_art_kit_button() - _add_stock_section("markers") - for product_id: StringName in ArtShopStockType.MARKER_PRODUCTS: - _add_art_upgrade_button(product_id) - _add_stock_section("brushes") - for product_id: StringName in ArtShopStockType.BRUSH_PRODUCTS: - _add_art_upgrade_button(product_id) - _add_stock_section("grids") - for product_id: StringName in ArtShopStockType.GRID_PRODUCTS: - _add_art_upgrade_button(product_id) + if _shop_section == ShopSection.ART_SUPPLIES: + _add_stock_section("art kit") + _add_art_kit_button() + _add_stock_section("markers") + for product_id: StringName in ArtShopStockType.MARKER_PRODUCTS: + _add_art_upgrade_button(product_id) + _add_stock_section("brushes") + for product_id: StringName in ArtShopStockType.BRUSH_PRODUCTS: + _add_art_upgrade_button(product_id) + _add_stock_section("grids") + for product_id: StringName in ArtShopStockType.GRID_PRODUCTS: + _add_art_upgrade_button(product_id) + + +func _item_belongs_in_current_section(item: ItemDataType) -> bool: + match _shop_section: + ShopSection.BAIT: + return item.is_bait() + ShopSection.SNACKS: + return item.category == ItemDataType.Category.CONSUMABLE + ShopSection.EQUIPMENT: + return item.category in [ + ItemDataType.Category.TOOL, + ItemDataType.Category.LURE, + ] + return false func _add_stock_section(title: String) -> void: diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 80f6e21..5732baf 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -143,12 +143,19 @@ text = "" horizontal_alignment = 1 theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) +[node name="ShopTabBar" type="HBoxContainer" parent="ShopPanel/Margin/Layout"] +unique_name_in_owner = true +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + [node name="Body" type="HBoxContainer" parent="ShopPanel/Margin/Layout"] layout_mode = 2 size_flags_vertical = 3 -theme_override_constants/separation = 12 +theme_override_constants/separation = 0 [node name="Upgrades" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] +unique_name_in_owner = true custom_minimum_size = Vector2(270, 0) layout_mode = 2 size_flags_horizontal = 3 @@ -160,12 +167,14 @@ text = "fishing upgrades" theme_override_font_sizes/font_size = 23 [node name="Supplies" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] +unique_name_in_owner = true custom_minimum_size = Vector2(210, 0) layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/separation = 6 -[node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"] +[node name="StockTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"] +unique_name_in_owner = true layout_mode = 2 text = "shop stock" theme_override_font_sizes/font_size = 23 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index a9af18e..c2dfca2 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -91,6 +91,7 @@ const FISHING_PANEL_SHOWCASE_TOP_OFFSET: float = -174.0 const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _status_label: Label = %StatusLabel +@onready var _bite_prompt_button: Button = %BitePromptButton @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD @onready var _active_bait_button: Button = %ActiveBaitButton @onready var _active_bait_quantity_badge: Panel = %ActiveBaitQuantityBadge @@ -190,6 +191,7 @@ var _settings_manager: PlayerSettingsManagerType func _ready() -> void: + _bite_prompt_button.pressed.connect(_on_bite_prompt_pressed) _apply_active_bait_indicator_style() _refresh_active_bait_indicator() # Reward feedback must remain above full-screen canonical menus. Keeping the @@ -307,7 +309,7 @@ func setup( _experience.experience_awarded.connect(_on_experience_awarded) _chat_ui.setup( network_chat_service, network_session, spawn_service, player, - fishing_spot, settings_manager, world_time, world_weather + fishing_spot, settings_manager, world_time, world_weather, item_effects ) _title_settings_panel.setup_network_profile( network_profile, network_session @@ -316,6 +318,7 @@ func setup( network_profile, network_session ) fishing_spot.status_changed.connect(_on_fishing_status_changed) + 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) @@ -1590,6 +1593,7 @@ func _refresh_fishing_panel_visibility() -> void: var has_content: bool = ( not _status_label.text.strip_edges().is_empty() or not _showcase_details.text.strip_edges().is_empty() + or _bite_prompt_button.visible ) _fishing_panel.visible = ( _gameplay_ui_enabled @@ -1597,6 +1601,16 @@ func _refresh_fishing_panel_visibility() -> void: ) +func _on_bite_prompt_changed(is_visible: bool) -> void: + _bite_prompt_button.visible = is_visible + _refresh_fishing_panel_visibility() + + +func _on_bite_prompt_pressed() -> void: + if _fishing_spot != null: + _fishing_spot.confirm_pending_bite() + + func _on_catch_display_changed( progress: float, chase_progress: float, diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index ed97ac5..1cbbc4a 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -279,6 +279,16 @@ text = "" horizontal_alignment = 1 theme_override_font_sizes/font_size = 16 +[node name="BitePromptButton" type="Button" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(0, 42) +layout_mode = 2 +focus_mode = 0 +mouse_default_cursor_shape = 2 +text = "you got a bite!" +theme_override_font_sizes/font_size = 22 + [node name="ShowcaseDetails" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 3d99289..bb1b418 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -189,6 +189,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0) @onready var _cooler_combined_offer_value: Label = %CoolerCombinedOfferValue @onready var _favorite_bubble: NotepadInkActionType = %FavoriteBubble @onready var _sell_bubble: NotepadInkActionType = %SellBubble +@onready var _sell_all_bubble: NotepadInkActionType = %SellAllBubble @onready var _bag_page: Control = %BagPage @onready var _tackle_box_page: Control = %TackleBoxPage @onready var _inventory_sub_tabs: HBoxContainer = %InventorySubTabs @@ -392,7 +393,7 @@ func _ready() -> void: ) _bait_filter.pressed.connect(_set_tackle_view.bind(TackleView.BAIT)) _lures_filter.pressed.connect(_set_tackle_view.bind(TackleView.LURES)) - _tackle_equip_button.pressed.connect(_toggle_active_bait) + _tackle_equip_button.pressed.connect(_toggle_active_tackle) _logbook_tab.pressed.connect( _show_section.bind(Section.LOGBOOK) ) @@ -409,6 +410,7 @@ func _ready() -> void: _sell_button.pressed.connect(_on_sell_pressed) _favorite_bubble.pressed.connect(_on_favorite_pressed) _sell_bubble.pressed.connect(_on_sell_pressed) + _sell_all_bubble.pressed.connect(_on_sell_all_pressed) _confirm_sale_button.pressed.connect(_on_confirm_sale_pressed) _cancel_sale_button.pressed.connect(_close_sale_confirmation) _configure_sale_confirmation_focus() @@ -593,6 +595,8 @@ func setup( _bag.contents_changed.connect(_on_bag_changed) if not _player.active_bait_changed.is_connected(_on_active_bait_changed): _player.active_bait_changed.connect(_on_active_bait_changed) + if not _player.active_lure_changed.is_connected(_on_active_lure_changed): + _player.active_lure_changed.connect(_on_active_lure_changed) if not _hotbar.slots_changed.is_connected(_on_hotbar_changed): _hotbar.slots_changed.connect(_on_hotbar_changed) if not _cooler_capacity.capacity_changed.is_connected( @@ -754,6 +758,8 @@ func _try_enter_notepad_controller_ownership() -> bool: actions.append(_favorite_bubble) if not _sell_bubble.disabled: actions.append(_sell_bubble) + if not _sell_all_bubble.disabled: + actions.append(_sell_all_bubble) elif _current_section == Section.TACKLE_BOX: if not focus_owner.has_meta(&"controller_tackle_item_id"): return false @@ -1943,21 +1949,30 @@ func _refresh_tackle_box() -> void: var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id) var row := Button.new() row.icon = item.icon - row.tooltip_text = "%s ×%d" % [item.display_name, owned.quantity] - if item.is_bait() and item.icon != null: + row.tooltip_text = ( + "%s ×%d" % [item.display_name, owned.quantity] + if item.is_bait() + else item.display_name + ) + var compact_tackle_item: bool = item.is_bait() or item.is_lure() + if compact_tackle_item: row.custom_minimum_size = Vector2(72, 72) row.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN - row.expand_icon = true + row.expand_icon = item.icon != null row.alignment = HORIZONTAL_ALIGNMENT_CENTER + row.text = "" if item.icon != null else item.display_name + if item.icon == null: + row.add_theme_font_size_override("font_size", 12) else: row.text = "%s ×%d" % [item.display_name, owned.quantity] row.alignment = HORIZONTAL_ALIGNMENT_LEFT row.toggle_mode = true row.set_meta(&"controller_tackle_item_id", owned.item_id) row.button_pressed = owned.item_id == _selected_tackle_item_id - if item.is_bait() and item.icon != null: + if compact_tackle_item: _apply_tackle_bait_button_style(row) - _add_tackle_quantity_badge(row, owned.quantity) + if item.is_bait(): + _add_tackle_quantity_badge(row, owned.quantity) else: UtilityPageStyle.apply_ocean_button(row) row.pressed.connect(_select_tackle_item.bind(owned.item_id)) @@ -2053,7 +2068,7 @@ func _update_tackle_detail() -> void: else "Select a lure for details." ) return - _tackle_equip_button.visible = item.is_bait() + _tackle_equip_button.visible = item.is_bait() or item.is_lure() var quantity: int = _bag.get_quantity(item.item_id) if _bag != null else 0 var assigned_slot: int = -1 if _hotbar != null: @@ -2061,19 +2076,22 @@ func _update_tackle_detail() -> void: if _hotbar.get_item_id(slot_index) == item.item_id: assigned_slot = slot_index break - var detail_lines: Array[String] = [ - item.display_name, - "", - "quantity: %d" % quantity, - ] + var detail_lines: Array[String] = [item.display_name, ""] + if item.is_bait(): + detail_lines.append("quantity: %d" % quantity) if assigned_slot >= 0: detail_lines.append("hotbar slot %d" % (assigned_slot + 1)) detail_lines.append("") detail_lines.append(item.description) _tackle_detail_text.text = "\n".join(detail_lines) - if item.is_bait(): + if item.is_bait() or item.is_lure(): var is_equipped: bool = ( - _player != null and _player.active_bait_id == item.item_id + _player != null + and ( + _player.active_bait_id == item.item_id + if item.is_bait() + else _player.active_lure_id == item.item_id + ) ) _tackle_equip_button.text = ( "dequip %s" % item.display_name @@ -2085,16 +2103,24 @@ func _update_tackle_detail() -> void: _tackle_equip_button.refresh_ink_state() -func _toggle_active_bait() -> void: +func _toggle_active_tackle() -> void: if _player == null or _item_catalog == null: return var item: ItemDataType = _item_catalog.get_item_by_id(_selected_tackle_item_id) - if item == null or not item.is_bait(): + if item == null: return - if _player.active_bait_id == item.item_id: - _player.unequip_bait() + if item.is_bait(): + if _player.active_bait_id == item.item_id: + _player.unequip_bait() + else: + _player.equip_bait(item) + elif item.is_lure(): + if _player.active_lure_id == item.item_id: + _player.unequip_lure() + else: + _player.equip_lure(item) else: - _player.equip_bait(item) + return _update_tackle_detail() @@ -2102,6 +2128,10 @@ func _on_active_bait_changed(_item_id: StringName) -> void: _update_tackle_detail() +func _on_active_lure_changed(_item_id: StringName) -> void: + _update_tackle_detail() + + func _apply_cooler_control_styles() -> void: var profile: BubbleMenuProfile = _inventory_tab.profile if profile == null: @@ -2426,6 +2456,9 @@ func _update_shell_layout() -> void: _sell_bubble.custom_minimum_size = ( Vector2(74.0, 42.0) if compact else Vector2(118.0, 50.0) ) + _sell_all_bubble.custom_minimum_size = ( + Vector2(96.0, 38.0) if compact else Vector2(122.0, 44.0) + ) _favorite_bubble.position = ( Vector2(144.0, 148.0) if compact else Vector2(26.0, 378.0) ) @@ -2438,12 +2471,21 @@ func _update_shell_layout() -> void: _sell_bubble.size = ( Vector2(74.0, 42.0) if compact else Vector2(118.0, 50.0) ) + _sell_all_bubble.position = ( + Vector2(102.0, 190.0) if compact else Vector2(78.0, 426.0) + ) + _sell_all_bubble.size = ( + Vector2(96.0, 38.0) if compact else Vector2(122.0, 44.0) + ) _favorite_bubble.add_theme_font_size_override( "font_size", 11 if compact else 17, ) _sell_bubble.add_theme_font_size_override( "font_size", 11 if compact else 17, ) + _sell_all_bubble.add_theme_font_size_override( + "font_size", 11 if compact else 15, + ) _update_sort_direction_text() _bag_page.size = reference_size _bag_page.position = Vector2.ZERO @@ -2946,7 +2988,11 @@ func _set_content_interactive(interactive: bool) -> void: var detail_interactive: bool = ( cooler_interactive and _detail_constellation.visible ) - for action: NotepadInkActionType in [_favorite_bubble, _sell_bubble]: + for action: NotepadInkActionType in [ + _favorite_bubble, + _sell_bubble, + _sell_all_bubble, + ]: var action_interactive: bool = detail_interactive and not action.disabled action.focus_mode = ( Control.FOCUS_ALL @@ -3595,6 +3641,11 @@ func _configure_cooler_fish_focus() -> void: not _favorite_bubble.disabled and index + columns >= controls.size() ) + else control.get_path_to(_sell_all_bubble) + if ( + not _sell_all_bubble.disabled + and index + columns >= controls.size() + ) else control.get_path_to( controls[mini(index + columns, controls.size() - 1)] ) @@ -3620,6 +3671,18 @@ func _configure_cooler_fish_focus() -> void: _sell_bubble.get_path_to(_close_button) ) _sell_bubble.focus_neighbor_top = _sell_bubble.get_path_to(focused_node) + _sell_bubble.focus_neighbor_bottom = ( + _sell_bubble.get_path_to(_sell_all_bubble) + ) + _sell_all_bubble.focus_neighbor_left = ( + _sell_all_bubble.get_path_to(_favorite_bubble) + ) + _sell_all_bubble.focus_neighbor_right = ( + _sell_all_bubble.get_path_to(_close_button) + ) + _sell_all_bubble.focus_neighbor_top = ( + _sell_all_bubble.get_path_to(_sell_bubble) + ) func _compare_catches(left: FishCatchType, right: FishCatchType) -> bool: @@ -3684,6 +3747,7 @@ func _close_detail_constellation() -> void: _sell_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE _favorite_bubble.refresh_ink_state() _sell_bubble.refresh_ink_state() + _refresh_cooler_notepad_action_interactivity() _configure_cooler_fish_focus() @@ -3750,6 +3814,7 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: _sell_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE _favorite_bubble.refresh_ink_state() _sell_bubble.refresh_ink_state() + _refresh_cooler_notepad_action_interactivity() _configure_cooler_fish_focus() return _cooler_detail_texture.texture = fish_catch.fish.display_texture @@ -3797,7 +3862,24 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: and not _transitioning and not _page_transitioning ) - for action: NotepadInkActionType in [_favorite_bubble, _sell_bubble]: + _refresh_cooler_notepad_action_interactivity() + _configure_cooler_fish_focus() + + +func _refresh_cooler_notepad_action_interactivity() -> void: + var detail_interactive: bool = ( + _detail_constellation.visible + and _current_section == Section.COOLER + and (visible or _shop_cooler_context_active) + and not _transitioning + and not _page_transitioning + and not _sale_confirmation.visible + ) + for action: NotepadInkActionType in [ + _favorite_bubble, + _sell_bubble, + _sell_all_bubble, + ]: var action_interactive: bool = detail_interactive and not action.disabled action.focus_mode = ( Control.FOCUS_ALL @@ -3810,7 +3892,6 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: else Control.MOUSE_FILTER_IGNORE ) action.refresh_ink_state() - _configure_cooler_fish_focus() func _update_sale_summary() -> void: @@ -3818,6 +3899,7 @@ func _update_sale_summary() -> void: var buyer_id: StringName = ( active_buyer.id if active_buyer != null else StringName() ) + _update_sell_all_action(active_buyer, buyer_id) var offer_label: String = _get_offer_label(active_buyer) var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() var selected_count: int = selected_ids.size() @@ -3927,6 +4009,55 @@ func _update_sale_summary() -> void: _sale_unavailable.visible = not _sale_unavailable.text.is_empty() +func _update_sell_all_action( + active_buyer: FishBuyerProfileType, + buyer_id: StringName, +) -> void: + var sell_all_ids: Array[StringName] = _get_sell_all_catch_ids() + var sale_available: bool = ( + not _sale_in_progress + and ( + _network_sale_service == null + or not _network_sale_service.is_local_sale_pending() + ) + and _network_sale_service != null + and not buyer_id.is_empty() + and _network_sale_service.can_request_sale(buyer_id) + and _sale_service != null + and active_buyer != null + and not sell_all_ids.is_empty() + ) + if sale_available: + var preview: FishSaleResultType = _sale_service.preview_batch( + sell_all_ids, + active_buyer, + ) + sale_available = preview.is_success() + _sell_all_bubble.text = "sell all fish" + _sell_all_bubble.disabled = not sale_available + _sell_all_bubble.persistent_mark = false + _sell_all_bubble.refresh_ink_state() + + +func _get_sell_all_catch_ids() -> Array[StringName]: + var catch_ids: Array[StringName] = [] + if _inventory == null: + return catch_ids + for fish_catch: FishCatchType in _inventory.get_all_catches(): + if ( + fish_catch == null + or not fish_catch.is_valid() + or fish_catch.is_favorited + or ( + _reservations != null + and _reservations.is_fish_reserved(fish_catch.catch_id) + ) + ): + continue + catch_ids.append(fish_catch.catch_id) + return catch_ids + + func _on_favorite_pressed() -> void: var focused_id: StringName = _fish_selection.get_focused_id() if _inventory == null or focused_id.is_empty(): @@ -3951,6 +4082,18 @@ func _on_favorite_pressed() -> void: func _on_sell_pressed() -> void: + _begin_sale_confirmation(_fish_selection.get_selected_ids()) + + +func _on_sell_all_pressed() -> void: + var catch_ids: Array[StringName] = _get_sell_all_catch_ids() + if catch_ids.is_empty(): + _transaction_feedback.text = "No sellable fish in the cooler." + return + _begin_sale_confirmation(catch_ids) + + +func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void: var active_buyer: FishBuyerProfileType = _get_active_sale_buyer() var buyer_id: StringName = ( active_buyer.id if active_buyer != null else StringName() @@ -3973,16 +4116,15 @@ func _on_sell_pressed() -> void: "Selling is not supported by this server." ) return - var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() if ( _inventory == null or _sale_service == null or active_buyer == null - or selected_ids.is_empty() + or catch_ids.is_empty() ): return var preview: FishSaleResultType = _sale_service.preview_batch( - selected_ids, + catch_ids, active_buyer ) if not preview.is_success(): @@ -3994,7 +4136,7 @@ func _on_sell_pressed() -> void: ) _refresh_inventory() return - _confirmation_catch_ids = selected_ids.duplicate() + _confirmation_catch_ids = catch_ids.duplicate() _confirmation_buyer = active_buyer _confirmation_buyer_id = active_buyer.id _confirmation_generation = _menu_generation diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index f990a93..5fdee45 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -470,6 +470,16 @@ offset_right = 256.0 offset_bottom = 464.0 text = "sell fish" +[node name="SellAllBubble" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation" instance=ExtResource("12_ink_action")] +unique_name_in_owner = true +custom_minimum_size = Vector2(122, 44) +layout_mode = 0 +offset_left = 78.0 +offset_top = 426.0 +offset_right = 200.0 +offset_bottom = 470.0 +text = "sell all fish" + [node name="BagPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true visible = false