diff --git a/economy/buyers/pelicans.tres b/economy/buyers/pelicans.tres index d126545..a9867c3 100644 --- a/economy/buyers/pelicans.tres +++ b/economy/buyers/pelicans.tres @@ -8,5 +8,5 @@ id = &"pelicans" display_name = "Pelicans" animal_name_singular = "pelican" animal_name_plural = "pelicans" -payout_multiplier = 0.7 +payout_multiplier = 0.25 sale_message_template = "You sold your {fish} to the pelicans for ${payout}." diff --git a/economy/fishing_shop_stock.gd b/economy/fishing_shop_stock.gd new file mode 100644 index 0000000..3d5e867 --- /dev/null +++ b/economy/fishing_shop_stock.gd @@ -0,0 +1,58 @@ +class_name FishingShopStock +extends RefCounted + +const ItemCatalogType = preload("res://items/item_catalog.gd") +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 ITEM_PRICES: Dictionary[StringName, int] = { + &"coffee": 20, + &"energy_drink": 35, + &"snack": 30, + &"fish_finder": 60, +} +const ITEM_ORDER: Array[StringName] = [ + &"coffee", + &"energy_drink", + &"snack", + &"fish_finder", +] + + +static func get_price(item_id: StringName) -> int: + return ITEM_PRICES.get(item_id, -1) + + +static func get_stock_item_ids() -> Array[StringName]: + return ITEM_ORDER.duplicate() + + +static func purchase_one( + item_id: StringName, + wallet: PlayerWalletType, + bag: PlayerBagType, + catalog: ItemCatalogType, +) -> bool: + if wallet == null or bag == null or catalog == null: + return false + var price: int = get_price(item_id) + var item: ItemDataType = catalog.get_item_by_id(item_id) + if ( + price < 0 + or item == null + or not item.is_valid() + or item.category != ItemDataType.Category.CONSUMABLE + or not item.stackable + or not item.usable + or not bag.can_add_item(item_id, 1) + or not wallet.can_afford(price) + ): + return false + if not wallet.debit(price): + return false + if bag.add_item(item_id, 1): + return true + if not wallet.credit(price): + push_error("Fishing Shop failed to roll back an item purchase.") + return false diff --git a/economy/fishing_shop_stock.gd.uid b/economy/fishing_shop_stock.gd.uid new file mode 100644 index 0000000..951bd91 --- /dev/null +++ b/economy/fishing_shop_stock.gd.uid @@ -0,0 +1 @@ +uid://dvvvjfah26pp3 diff --git a/fish/fish_selector.gd b/fish/fish_selector.gd index 7764ad3..3c1656b 100644 --- a/fish/fish_selector.gd +++ b/fish/fish_selector.gd @@ -8,6 +8,7 @@ const FishPoolType = preload("res://fish/fish_pool.gd") const FishingContextType = preload("res://fishing/fishing_context.gd") var undiscovered_weight_multiplier: float = 1.5 +var rarity_weight_multipliers: Array[float] = [] var use_deterministic_test_seed: bool = false var deterministic_test_seed: int = 24680 var selection_seed: int = 0 @@ -45,6 +46,8 @@ func select_fish( final_weight *= fish.availability.get_bait_weight_multiplier(context) if not collection_log.has_discovered(fish.id): final_weight *= maxf(undiscovered_weight_multiplier, 0.0) + if fish.rarity >= 0 and fish.rarity < rarity_weight_multipliers.size(): + final_weight *= maxf(rarity_weight_multipliers[fish.rarity], 0.0) if final_weight <= 0.0: continue eligible_fish.append(fish) diff --git a/fish/species/bass/bass.tres b/fish/species/bass/bass.tres index 65c2801..d9ffd1c 100644 --- a/fish/species/bass/bass.tres +++ b/fish/species/bass/bass.tres @@ -25,7 +25,7 @@ weight_max_lb = 8.0 display_scale_min = 0.9 display_scale_max = 1.5 display_scale_curve = 0.9 -sell_value_min = 12 -sell_value_max = 45 +sell_value_min = 7 +sell_value_max = 7 sell_value_curve = 0.9 display_texture = ExtResource("4_texture") diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres index 0f47849..f3eb81d 100644 --- a/fish/species/bluegill/bluegill.tres +++ b/fish/species/bluegill/bluegill.tres @@ -19,6 +19,6 @@ display_scale_min = 0.7 display_scale_max = 1.1 display_scale_curve = 1.0 sell_value_min = 3 -sell_value_max = 12 +sell_value_max = 3 sell_value_curve = 1.0 display_texture = ExtResource("4_texture") diff --git a/fish/species/carp/carp.tres b/fish/species/carp/carp.tres index e6794f5..571cf69 100644 --- a/fish/species/carp/carp.tres +++ b/fish/species/carp/carp.tres @@ -25,7 +25,7 @@ weight_max_lb = 18.0 display_scale_min = 1.0 display_scale_max = 1.8 display_scale_curve = 0.85 -sell_value_min = 25 -sell_value_max = 90 +sell_value_min = 10 +sell_value_max = 10 sell_value_curve = 0.85 display_texture = ExtResource("4_texture") diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd index 7043508..350bafa 100644 --- a/fishing/catch_controller.gd +++ b/fishing/catch_controller.gd @@ -128,6 +128,13 @@ func set_reel_input(held: bool) -> void: _auto_click_accumulator = 0.0 +func set_effective_stats(reel_speed: float, click_power: int) -> void: + if state == CatchState.IDLE: + return + _reel_speed = maxf(reel_speed, 0.0) + _click_power = maxi(click_power, 1) + + func handle_primary_pressed() -> void: if state not in [CatchState.REELING, CatchState.BLOCKED_BY_BARRIER]: return diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index ca82d34..3198fb0 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -16,6 +16,12 @@ const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) +const PlayerItemEffectsType = preload( + "res://progression/player_item_effects.gd" +) +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) const PlayerType = preload("res://player/player.gd") const FishableWaterRegionType = preload( "res://world/fishable_water_region.gd" @@ -96,6 +102,8 @@ var _local_bag: PlayerBagType var _local_hotbar: PlayerHotbarType var _item_catalog: ItemCatalogType var _fishing_upgrades: PlayerFishingUpgradesType +var _item_effects: PlayerItemEffectsType +var _cooler_capacity: PlayerCoolerCapacityType var _active_player: PlayerType var _state_time_remaining: float = 0.0 var _cast_charge: float = 0.0 @@ -137,6 +145,8 @@ func setup( local_hotbar: PlayerHotbarType, item_catalog: ItemCatalogType, fishing_upgrades: PlayerFishingUpgradesType, + item_effects: PlayerItemEffectsType, + cooler_capacity: PlayerCoolerCapacityType, ) -> void: _local_player = local_player _local_inventory = local_inventory @@ -145,6 +155,18 @@ func setup( _local_hotbar = local_hotbar _item_catalog = item_catalog _fishing_upgrades = fishing_upgrades + _item_effects = item_effects + _cooler_capacity = cooler_capacity + if not _local_inventory.catches_changed.is_connected( + _on_cooler_availability_changed + ): + _local_inventory.catches_changed.connect( + _on_cooler_availability_changed + ) + if not _cooler_capacity.capacity_changed.is_connected( + _on_cooler_capacity_changed + ): + _cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed) func can_open_player_menu() -> bool: @@ -322,6 +344,10 @@ func _process(delta: float) -> void: FishingState.WAITING_FOR_BITE: _update_waiting_for_bite(delta) FishingState.FIGHTING: + _catch_controller.set_effective_stats( + _get_effective_reel_speed(), + _get_effective_barrier_damage() + ) if not Input.is_action_pressed("fish_primary"): _catch_controller.set_reel_input(false) FishingState.COOLDOWN: @@ -347,10 +373,29 @@ func _unhandled_input(event: InputEvent) -> void: FishingState.READY: if not _new_cast_press_armed: return + var active_item: ItemDataType = _get_active_item() + if ( + active_item != null + and active_item.category == ItemDataType.Category.CONSUMABLE + ): + if _item_effects.use_consumable(active_item, _local_bag): + status_changed.emit( + _item_effects.get_feedback(active_item.item_id) + ) + get_viewport().set_input_as_handled() + return if not has_active_fishing_rod(): status_changed.emit("Select a fishing rod to cast.") get_viewport().set_input_as_handled() return + if ( + _is_cooler_full() + ): + status_changed.emit( + "Cooler full. Sell fish before casting again." + ) + get_viewport().set_input_as_handled() + return _begin_aiming(_local_player) FishingState.WAITING_FOR_BITE: _withdrawal_input_held = true @@ -411,10 +456,7 @@ func has_active_fishing_rod() -> bool: or _item_catalog == null ): return false - var item_id: StringName = _local_hotbar.get_selected_item_id() - if item_id.is_empty() or not _local_bag.owns_item(item_id): - return false - var item: ItemDataType = _item_catalog.get_item_by_id(item_id) + var item: ItemDataType = _get_active_item() return ( item != null and item.is_valid() @@ -424,6 +466,38 @@ func has_active_fishing_rod() -> bool: ) +func _get_active_item() -> ItemDataType: + if ( + _local_bag == null + or _local_hotbar == null + or _item_catalog == null + ): + return null + var item_id: StringName = _local_hotbar.get_selected_item_id() + if item_id.is_empty() or not _local_bag.owns_item(item_id): + return null + var item: ItemDataType = _item_catalog.get_item_by_id(item_id) + return item if item != null and item.is_valid() else null + + +func _is_cooler_full() -> bool: + return ( + _cooler_capacity != null + and _local_inventory != null + and _local_inventory.get_all_catches().size() + >= _cooler_capacity.get_capacity() + ) + + +func _on_cooler_availability_changed() -> void: + if state == FishingState.READY and not _is_cooler_full(): + refresh_active_item_status() + + +func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: + _on_cooler_availability_changed() + + func is_fighting() -> bool: return state == FishingState.FIGHTING @@ -472,6 +546,13 @@ func _on_cast_completed() -> void: return _selection_context = _build_fishing_context(_selected_water_region) _fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier + _fish_selector.rarity_weight_multipliers = [] + for rarity: int in range(FishDataType.Rarity.size()): + _fish_selector.rarity_weight_multipliers.append( + _item_effects.get_rarity_weight_multiplier(rarity) + if _item_effects != null + else 1.0 + ) _fish_selector.use_deterministic_test_seed = use_deterministic_selection_seed _fish_selector.deterministic_test_seed = deterministic_selection_seed _fish_selector.begin_roll() @@ -485,7 +566,11 @@ func _on_cast_completed() -> void: return state = FishingState.WAITING_FOR_BITE - _state_time_remaining = wait_time + _state_time_remaining = wait_time * ( + _item_effects.get_bite_time_multiplier() + if _item_effects != null + else 1.0 + ) _withdrawal_progress = 0.0 _withdrawal_input_held = false _bobber_water_position = _cast_target @@ -618,16 +703,8 @@ func _activate_bite() -> void: _presentation.begin_reeling() _catch_controller.start_encounter( _selected_fish.catch_profile, - _active_player.reel_speed * ( - _fishing_upgrades.get_reel_speed_multiplier() - if _fishing_upgrades != null - else 1.0 - ), - ( - _fishing_upgrades.get_barrier_damage() - if _fishing_upgrades != null - else _active_player.click_power - ) + _get_effective_reel_speed(), + _get_effective_barrier_damage() ) _catch_controller.set_reel_input( Input.is_action_pressed("fish_primary") @@ -635,6 +712,34 @@ func _activate_bite() -> void: _presentation.show_bite() +func _get_effective_reel_speed() -> float: + if _active_player == null: + return 0.0 + return _active_player.reel_speed * ( + _fishing_upgrades.get_reel_speed_multiplier() + if _fishing_upgrades != null + else 1.0 + ) * ( + _item_effects.get_reel_multiplier() + if _item_effects != null + else 1.0 + ) + + +func _get_effective_barrier_damage() -> int: + if _active_player == null: + return 1 + return ( + _fishing_upgrades.get_barrier_damage() + if _fishing_upgrades != null + else _active_player.click_power + ) + ( + _item_effects.get_barrier_bonus() + if _item_effects != null + else 0 + ) + + func _on_catch_encounter_updated( progress: float, chase_progress: float, diff --git a/inventory/player_bag.gd b/inventory/player_bag.gd index 0d6112a..1e885ee 100644 --- a/inventory/player_bag.gd +++ b/inventory/player_bag.gd @@ -16,19 +16,13 @@ func setup(catalog: ItemCatalogType) -> void: func add_item(item_id: StringName, quantity: int = 1) -> bool: - var item: ItemDataType = _resolve_valid_item(item_id) - if item == null or quantity <= 0: + if not can_add_item(item_id, quantity): return false + var item: ItemDataType = _resolve_valid_item(item_id) var existing: OwnedItemType = get_owned_item(item_id) if existing != null: - if not item.stackable: - return false - if existing.quantity + quantity > item.max_stack: - return false existing.quantity += quantity else: - if quantity > (item.max_stack if item.stackable else 1): - return false var owned := OwnedItemType.new() owned.item_id = item_id owned.quantity = quantity @@ -37,6 +31,19 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool: return true +func can_add_item(item_id: StringName, quantity: int = 1) -> bool: + var item: ItemDataType = _resolve_valid_item(item_id) + if item == null or quantity <= 0: + return false + var existing: OwnedItemType = get_owned_item(item_id) + if existing != null: + return ( + item.stackable + and existing.quantity + quantity <= item.max_stack + ) + return quantity <= (item.max_stack if item.stackable else 1) + + func remove_item(item_id: StringName, quantity: int = 1) -> bool: if quantity <= 0: return false diff --git a/items/catalog/coffee.tres b/items/catalog/coffee.tres new file mode 100644 index 0000000..4e7d343 --- /dev/null +++ b/items/catalog/coffee.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/coffee.svg" id="2"] + +[resource] +script = ExtResource("1") +item_id = &"coffee" +display_name = "Coffee" +description = "Temporarily increases ground movement speed." +category = 3 +icon = ExtResource("2") +stackable = true +max_stack = 99 +usable = true +hotbar_allowed = true diff --git a/items/catalog/cooler_expansion.tres b/items/catalog/cooler_expansion.tres new file mode 100644 index 0000000..28114f6 --- /dev/null +++ b/items/catalog/cooler_expansion.tres @@ -0,0 +1,17 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/cooler_expansion.svg" id="2"] + +[resource] +script = ExtResource("1") +item_id = &"cooler_expansion" +display_name = "Cooler Expansion" +description = "Permanently increases the number of fish the Cooler can hold." +category = 4 +icon = ExtResource("2") +stackable = false +max_stack = 1 +usable = false +equippable = false +hotbar_allowed = false diff --git a/items/catalog/energy_drink.tres b/items/catalog/energy_drink.tres new file mode 100644 index 0000000..6594dbe --- /dev/null +++ b/items/catalog/energy_drink.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/energy_drink.svg" id="2"] + +[resource] +script = ExtResource("1") +item_id = &"energy_drink" +display_name = "Energy Drink" +description = "Temporarily increases reeling progress." +category = 3 +icon = ExtResource("2") +stackable = true +max_stack = 99 +usable = true +hotbar_allowed = true diff --git a/items/catalog/fish_finder.tres b/items/catalog/fish_finder.tres new file mode 100644 index 0000000..310b16a --- /dev/null +++ b/items/catalog/fish_finder.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/fish_finder.svg" id="2"] + +[resource] +script = ExtResource("1") +item_id = &"fish_finder" +display_name = "Fish Finder" +description = "Temporarily shortens bite waits and improves rarity weights." +category = 3 +icon = ExtResource("2") +stackable = true +max_stack = 99 +usable = true +hotbar_allowed = true diff --git a/items/catalog/item_catalog.tres b/items/catalog/item_catalog.tres index f4e3986..3cccb42 100644 --- a/items/catalog/item_catalog.tres +++ b/items/catalog/item_catalog.tres @@ -1,8 +1,13 @@ -[gd_resource type="Resource" script_class="ItemCatalog" load_steps=3 format=3] +[gd_resource type="Resource" script_class="ItemCatalog" load_steps=8 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"] +[ext_resource type="Resource" path="res://items/catalog/coffee.tres" id="3_coffee"] +[ext_resource type="Resource" path="res://items/catalog/energy_drink.tres" id="4_energy"] +[ext_resource type="Resource" path="res://items/catalog/snack.tres" id="5_snack"] +[ext_resource type="Resource" path="res://items/catalog/fish_finder.tres" id="6_finder"] +[ext_resource type="Resource" path="res://items/catalog/cooler_expansion.tres" id="7_cooler"] [resource] script = ExtResource("1_script") -items = [ExtResource("2_rod")] +items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler")] diff --git a/items/catalog/snack.tres b/items/catalog/snack.tres new file mode 100644 index 0000000..f81edca --- /dev/null +++ b/items/catalog/snack.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/snack.svg" id="2"] + +[resource] +script = ExtResource("1") +item_id = &"snack" +display_name = "Snack" +description = "Temporarily adds one barrier damage." +category = 3 +icon = ExtResource("2") +stackable = true +max_stack = 99 +usable = true +hotbar_allowed = true diff --git a/items/icons/placeholder/coffee.svg b/items/icons/placeholder/coffee.svg new file mode 100644 index 0000000..d30c96a --- /dev/null +++ b/items/icons/placeholder/coffee.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/items/icons/placeholder/coffee.svg.import b/items/icons/placeholder/coffee.svg.import new file mode 100644 index 0000000..f8cbaa9 --- /dev/null +++ b/items/icons/placeholder/coffee.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddx3pbgh2ele5" +path="res://.godot/imported/coffee.svg-aecf16cc8c38e5e0f432cd5395949bcf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/placeholder/coffee.svg" +dest_files=["res://.godot/imported/coffee.svg-aecf16cc8c38e5e0f432cd5395949bcf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/items/icons/placeholder/cooler_expansion.svg b/items/icons/placeholder/cooler_expansion.svg new file mode 100644 index 0000000..105bff7 --- /dev/null +++ b/items/icons/placeholder/cooler_expansion.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/items/icons/placeholder/cooler_expansion.svg.import b/items/icons/placeholder/cooler_expansion.svg.import new file mode 100644 index 0000000..fbcc85c --- /dev/null +++ b/items/icons/placeholder/cooler_expansion.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d115jiqughf40" +path="res://.godot/imported/cooler_expansion.svg-a3d63bc21ba230007f1039372b518c8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/placeholder/cooler_expansion.svg" +dest_files=["res://.godot/imported/cooler_expansion.svg-a3d63bc21ba230007f1039372b518c8f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/items/icons/placeholder/energy_drink.svg b/items/icons/placeholder/energy_drink.svg new file mode 100644 index 0000000..da04ac7 --- /dev/null +++ b/items/icons/placeholder/energy_drink.svg @@ -0,0 +1,4 @@ + + + + diff --git a/items/icons/placeholder/energy_drink.svg.import b/items/icons/placeholder/energy_drink.svg.import new file mode 100644 index 0000000..0e79f6a --- /dev/null +++ b/items/icons/placeholder/energy_drink.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3elrkttv1f1h" +path="res://.godot/imported/energy_drink.svg-8d7bc90bd15610c7852e4cf522a5b1ca.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/placeholder/energy_drink.svg" +dest_files=["res://.godot/imported/energy_drink.svg-8d7bc90bd15610c7852e4cf522a5b1ca.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/items/icons/placeholder/fish_finder.svg b/items/icons/placeholder/fish_finder.svg new file mode 100644 index 0000000..7bbc0b1 --- /dev/null +++ b/items/icons/placeholder/fish_finder.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/items/icons/placeholder/fish_finder.svg.import b/items/icons/placeholder/fish_finder.svg.import new file mode 100644 index 0000000..df63a31 --- /dev/null +++ b/items/icons/placeholder/fish_finder.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cxsaix6vp7t6u" +path="res://.godot/imported/fish_finder.svg-33ab7d731566e03c2f84e185d3503b7d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/placeholder/fish_finder.svg" +dest_files=["res://.godot/imported/fish_finder.svg-33ab7d731566e03c2f84e185d3503b7d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/items/icons/placeholder/snack.svg b/items/icons/placeholder/snack.svg new file mode 100644 index 0000000..edff1ec --- /dev/null +++ b/items/icons/placeholder/snack.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/items/icons/placeholder/snack.svg.import b/items/icons/placeholder/snack.svg.import new file mode 100644 index 0000000..5ad06b4 --- /dev/null +++ b/items/icons/placeholder/snack.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://i611n7iwr8i6" +path="res://.godot/imported/snack.svg-dfbab25d47c1c79c46c1d7fcf9b21cfe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/placeholder/snack.svg" +dest_files=["res://.godot/imported/snack.svg-dfbab25d47c1c79c46c1d7fcf9b21cfe.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/main/main.gd b/main/main.gd index f781ef4..0deb188 100644 --- a/main/main.gd +++ b/main/main.gd @@ -62,7 +62,8 @@ func _ready() -> void: _player.bag, _player.hotbar, item_catalog, - _player.fishing_upgrades + _player.fishing_upgrades, + _player.cooler_capacity ) _save_manager.set_autosave_enabled(false) _fishing_spot.setup( @@ -72,7 +73,9 @@ func _ready() -> void: _player.bag, _player.hotbar, item_catalog, - _player.fishing_upgrades + _player.fishing_upgrades, + _player.item_effects, + _player.cooler_capacity ) _game_ui.setup( _player, @@ -88,7 +91,9 @@ func _ready() -> void: item_catalog, main_shop_buyer_profile, _player.fishing_upgrades, - _shop_interaction + _shop_interaction, + _player.item_effects, + _player.cooler_capacity ) _water_recovery.setup( _player, @@ -210,6 +215,8 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void: func _set_gameplay_active(active: bool) -> void: _gameplay_started = active + if not active: + _player.item_effects.reset_all() _player.set_movement_enabled(active) _player.set_camera_input_enabled(active) _fishing_spot.set_gameplay_input_enabled(active) diff --git a/player/player.gd b/player/player.gd index 4022b12..847492c 100644 --- a/player/player.gd +++ b/player/player.gd @@ -11,6 +11,12 @@ const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) +const PlayerItemEffectsType = preload( + "res://progression/player_item_effects.gd" +) +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) class ShowcaseCameraSnapshot: extends RefCounted @@ -75,6 +81,8 @@ class ShowcaseCameraSnapshot: @onready var bag: PlayerBagType = %Bag @onready var hotbar: PlayerHotbarType = %Hotbar @onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades +@onready var item_effects: PlayerItemEffectsType = %ItemEffects +@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity @onready var _cast_origin: Marker3D = %CastOrigin @onready var _fishing_rod: Node3D = %FishingRod @onready var _fishing_rod_tip: Marker3D = %FishingRodTip @@ -141,7 +149,9 @@ func _physics_process(delta: float) -> void: var input_strength: float = minf(input_vector.length(), 1.0) move_direction = move_direction.normalized() - var speed: float = _get_current_speed() + var speed: float = ( + _get_current_speed() * item_effects.get_movement_multiplier() + ) velocity.x = move_direction.x * speed * input_strength velocity.z = move_direction.z * speed * input_strength diff --git a/player/player.tscn b/player/player.tscn index ead43d0..977040d 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=18 format=3] [ext_resource type="Script" path="res://player/player.gd" id="1_script"] [ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"] @@ -8,6 +8,8 @@ [ext_resource type="Script" path="res://inventory/player_bag.gd" id="6_bag"] [ext_resource type="Script" path="res://inventory/player_hotbar.gd" id="7_hotbar"] [ext_resource type="Script" path="res://progression/player_fishing_upgrades.gd" id="8_upgrades"] +[ext_resource type="Script" path="res://progression/player_item_effects.gd" id="9_effects"] +[ext_resource type="Script" path="res://progression/player_cooler_capacity.gd" id="10_capacity"] [sub_resource type="CapsuleShape3D" id="PlayerShape"] radius = 0.45 @@ -103,6 +105,14 @@ script = ExtResource("7_hotbar") unique_name_in_owner = true script = ExtResource("8_upgrades") +[node name="ItemEffects" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("9_effects") + +[node name="CoolerCapacity" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("10_capacity") + [node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"] unique_name_in_owner = true position = Vector3(0, 1.45, -1.15) diff --git a/playtest/README-PLAYTEST.txt b/playtest/README-PLAYTEST.txt index da39761..060f1cf 100644 --- a/playtest/README-PLAYTEST.txt +++ b/playtest/README-PLAYTEST.txt @@ -49,6 +49,10 @@ FISHING - Accessibility auto-click can be enabled in Settings. - Approach the bright Fishing Shop booth and press E to open it. - The Fishing Shop buys one Cooler fish at a time for full base value. +- Buy Coffee, Energy Drinks, Snacks, and Fish Finders as Bag supplies. +- Drag supplies from Bag to the hotbar and left-click in READY to use one. +- Coffee improves movement; other supplies temporarily improve fishing. +- The Cooler starts at 12 fish; buy permanent capacity expansions at the shop. - Reel Speed upgrades increase authoritative green reeling progress. - Barrier Power upgrades increase damage per valid barrier action. - Shop upgrades persist in the local progression save. diff --git a/progression/player_cooler_capacity.gd b/progression/player_cooler_capacity.gd new file mode 100644 index 0000000..1e77784 --- /dev/null +++ b/progression/player_cooler_capacity.gd @@ -0,0 +1,65 @@ +class_name PlayerCoolerCapacity +extends Node + +const PlayerWalletType = preload("res://economy/player_wallet.gd") + +signal capacity_changed(level: int, capacity: int) + +const CAPACITIES: Array[int] = [12, 18, 24, 32, 40] +const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850] +const MAX_LEVEL: int = 4 + +var _capacity_level: int = 0 + + +func get_level() -> int: + return _capacity_level + + +func get_capacity() -> int: + return CAPACITIES[_capacity_level] + + +func get_next_capacity() -> int: + if _capacity_level >= MAX_LEVEL: + return -1 + return CAPACITIES[_capacity_level + 1] + + +func get_next_cost() -> int: + if _capacity_level >= MAX_LEVEL: + return -1 + return EXPANSION_COSTS[_capacity_level] + + +func can_purchase(wallet: PlayerWalletType) -> bool: + var cost: int = get_next_cost() + return wallet != null and cost >= 0 and wallet.can_afford(cost) + + +func purchase(wallet: PlayerWalletType) -> bool: + if not can_purchase(wallet): + return false + var cost: int = get_next_cost() + if not wallet.debit(cost): + return false + _capacity_level += 1 + capacity_changed.emit(_capacity_level, get_capacity()) + return true + + +func restore_level(level: int) -> bool: + var validated: int = clampi(level, 0, MAX_LEVEL) + var changed: bool = _capacity_level != validated + _capacity_level = validated + if changed: + capacity_changed.emit(_capacity_level, get_capacity()) + return true + + +func reset_to_defaults() -> void: + restore_level(0) + + +func to_save_data() -> Dictionary: + return {"capacity_level": _capacity_level} diff --git a/progression/player_cooler_capacity.gd.uid b/progression/player_cooler_capacity.gd.uid new file mode 100644 index 0000000..2dda316 --- /dev/null +++ b/progression/player_cooler_capacity.gd.uid @@ -0,0 +1 @@ +uid://c85ofvdqxyacg diff --git a/progression/player_item_effects.gd b/progression/player_item_effects.gd new file mode 100644 index 0000000..f0ee063 --- /dev/null +++ b/progression/player_item_effects.gd @@ -0,0 +1,142 @@ +class_name PlayerItemEffects +extends Node + +const ItemDataType = preload("res://items/item_data.gd") +const PlayerBagType = preload("res://inventory/player_bag.gd") + +signal effects_changed + +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 COFFEE_DURATION: float = 90.0 +const ENERGY_DRINK_DURATION: float = 90.0 +const SNACK_DURATION: float = 60.0 +const FISH_FINDER_DURATION: float = 120.0 +const COFFEE_MOVEMENT_MULTIPLIER: float = 1.10 +const ENERGY_REEL_MULTIPLIER: float = 1.15 +const SNACK_BARRIER_BONUS: int = 1 +const FISH_FINDER_BITE_MULTIPLIER: float = 0.80 + +var _remaining: Dictionary[StringName, float] = { + COFFEE_ID: 0.0, + ENERGY_DRINK_ID: 0.0, + SNACK_ID: 0.0, + FISH_FINDER_ID: 0.0, +} + + +func _process(delta: float) -> void: + var changed: bool = false + for item_id: StringName in _remaining: + var previous: float = _remaining[item_id] + if previous <= 0.0: + continue + var next: float = maxf(previous - delta, 0.0) + _remaining[item_id] = next + if next == 0.0: + changed = true + if changed: + effects_changed.emit() + + +func use_consumable( + item: ItemDataType, + bag: PlayerBagType, +) -> bool: + if ( + item == null + or bag == null + or item.category != ItemDataType.Category.CONSUMABLE + 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): + return false + _remaining[item.item_id] = _get_duration(item.item_id) + effects_changed.emit() + return true + + +func reset_all() -> void: + var changed: bool = false + for item_id: StringName in _remaining: + changed = changed or _remaining[item_id] > 0.0 + _remaining[item_id] = 0.0 + if changed: + effects_changed.emit() + + +func get_remaining(item_id: StringName) -> float: + return maxf(_remaining.get(item_id, 0.0), 0.0) + + +func is_active(item_id: StringName) -> bool: + return get_remaining(item_id) > 0.0 + + +func get_movement_multiplier() -> float: + return COFFEE_MOVEMENT_MULTIPLIER if is_active(COFFEE_ID) else 1.0 + + +func get_reel_multiplier() -> float: + return ENERGY_REEL_MULTIPLIER if is_active(ENERGY_DRINK_ID) else 1.0 + + +func get_barrier_bonus() -> int: + return SNACK_BARRIER_BONUS if is_active(SNACK_ID) else 0 + + +func get_bite_time_multiplier() -> float: + return ( + FISH_FINDER_BITE_MULTIPLIER + if is_active(FISH_FINDER_ID) + else 1.0 + ) + + +func get_rarity_weight_multiplier(rarity: int) -> float: + if not is_active(FISH_FINDER_ID): + return 1.0 + match rarity: + 0: + return 1.0 + 1: + return 1.10 + 2: + return 1.15 + 3: + return 1.20 + _: + return 1.25 + + +func get_feedback(item_id: StringName) -> String: + match item_id: + COFFEE_ID: + return "Coffee active: movement speed increased." + ENERGY_DRINK_ID: + return "Energy Drink active: reeling speed increased." + SNACK_ID: + return "Snack active: barrier damage increased." + FISH_FINDER_ID: + return "Fish Finder active: bites and uncommon catches improved." + _: + return "" + + +func _get_duration(item_id: StringName) -> float: + match item_id: + COFFEE_ID: + return COFFEE_DURATION + ENERGY_DRINK_ID: + return ENERGY_DRINK_DURATION + SNACK_ID: + return SNACK_DURATION + FISH_FINDER_ID: + return FISH_FINDER_DURATION + _: + return 0.0 diff --git a/progression/player_item_effects.gd.uid b/progression/player_item_effects.gd.uid new file mode 100644 index 0000000..2866bb2 --- /dev/null +++ b/progression/player_item_effects.gd.uid @@ -0,0 +1 @@ +uid://cvd5jj5wbab1k diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 2acab87..41a5428 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -14,8 +14,11 @@ const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) -const SAVE_VERSION: int = 3 +const SAVE_VERSION: int = 4 const BASIC_ROD_ID: StringName = &"basic_fishing_rod" const SAVE_PATH: String = "user://player_save.json" const TEMP_PATH: String = "user://player_save.json.tmp" @@ -34,6 +37,7 @@ class LoadSnapshot: var selected_hotbar_slot: int = 0 var reel_speed_level: int = 0 var barrier_power_level: int = 0 + var cooler_capacity_level: int = 0 @export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5 @@ -46,6 +50,7 @@ var _bag: PlayerBagType var _hotbar: PlayerHotbarType var _item_catalog: ItemCatalogType var _fishing_upgrades: PlayerFishingUpgradesType +var _cooler_capacity: PlayerCoolerCapacityType var _autosave_timer: Timer var _is_configured: bool = false var _is_restoring: bool = false @@ -70,6 +75,7 @@ func setup( hotbar: PlayerHotbarType, item_catalog: ItemCatalogType, fishing_upgrades: PlayerFishingUpgradesType, + cooler_capacity: PlayerCoolerCapacityType, ) -> void: _inventory = inventory _collection_log = collection_log @@ -79,6 +85,7 @@ func setup( _hotbar = hotbar _item_catalog = item_catalog _fishing_upgrades = fishing_upgrades + _cooler_capacity = cooler_capacity _is_configured = ( _inventory != null and _collection_log != null @@ -88,6 +95,7 @@ func setup( and _hotbar != null and _item_catalog != null and _fishing_upgrades != null + and _cooler_capacity != null ) if not _is_configured: push_error("PlayerSaveManager setup is missing required references.") @@ -114,6 +122,12 @@ func setup( _on_upgrades_changed ): _fishing_upgrades.upgrades_changed.connect(_on_upgrades_changed) + if not _cooler_capacity.capacity_changed.is_connected( + _on_cooler_capacity_changed + ): + _cooler_capacity.capacity_changed.connect( + _on_cooler_capacity_changed + ) func load_player_data() -> bool: @@ -183,6 +197,9 @@ func load_player_data() -> bool: snapshot.reel_speed_level, snapshot.barrier_power_level ) + var cooler_restored: bool = _cooler_capacity.restore_level( + snapshot.cooler_capacity_level + ) _is_restoring = false if ( not inventory_restored @@ -191,6 +208,7 @@ func load_player_data() -> bool: or not bag_restored or not hotbar_restored or not upgrades_restored + or not cooler_restored ): push_error("Validated player save could not be restored.") return false @@ -400,6 +418,7 @@ func _build_save_dictionary() -> Dictionary: "slots": serialized_slots, }, "upgrades": _fishing_upgrades.to_save_data(), + "cooler": _cooler_capacity.to_save_data(), } @@ -418,12 +437,17 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: var bag_data: Dictionary = save_data["bag"] var hotbar_data: Dictionary = save_data["hotbar"] var upgrades_data: Dictionary = {} + var cooler_data: Dictionary = {} if typeof(save_data.get("upgrades")) == TYPE_DICTIONARY: upgrades_data = save_data["upgrades"] else: push_warning( "Saved fishing upgrades were missing or invalid; using defaults." ) + if typeof(save_data.get("cooler")) == TYPE_DICTIONARY: + cooler_data = save_data["cooler"] + else: + push_warning("Saved Cooler data was missing or invalid; using defaults.") if ( not wallet_data.has("balance") or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY @@ -600,6 +624,11 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: PlayerFishingUpgradesType.MAX_BARRIER_POWER_LEVEL, "barrier_power_level" ) + snapshot.cooler_capacity_level = _read_upgrade_level( + cooler_data.get("capacity_level"), + PlayerCoolerCapacityType.MAX_LEVEL, + "cooler.capacity_level" + ) return snapshot @@ -619,6 +648,8 @@ func _migrate_save( migrated = _migrate_version_1_to_2(migrated) 2: migrated = _migrate_version_2_to_3(migrated) + 3: + migrated = _migrate_version_3_to_4(migrated) _: return {} if migrated.is_empty(): @@ -659,6 +690,13 @@ func _migrate_version_2_to_3(data: Dictionary) -> Dictionary: return migrated +func _migrate_version_3_to_4(data: Dictionary) -> Dictionary: + var migrated: Dictionary = data.duplicate(true) + migrated["save_version"] = 4 + migrated["cooler"] = {"capacity_level": 0} + return migrated + + func _mark_dirty() -> void: if ( _is_restoring @@ -693,6 +731,13 @@ func _on_upgrades_changed( _mark_dirty() +func _on_cooler_capacity_changed( + _level: int, + _capacity: int, +) -> void: + _mark_dirty() + + func _on_autosave_timeout() -> void: if _is_dirty: save_now() @@ -748,6 +793,7 @@ func _restore_defaults() -> void: _bag.replace_all_items(default_items) _hotbar.replace_state(default_slots, 0) _fishing_upgrades.reset_to_defaults() + _cooler_capacity.reset_to_defaults() _is_restoring = false _is_dirty = false diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 6220950..16774a3 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -9,11 +9,18 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd") const FishSaleResultType = preload("res://economy/fish_sale_result.gd") const FishSaleServiceType = preload("res://economy/fish_sale_service.gd") const PlayerWalletType = preload("res://economy/player_wallet.gd") +const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") +const ItemCatalogType = preload("res://items/item_catalog.gd") +const ItemDataType = preload("res://items/item_data.gd") +const PlayerBagType = preload("res://inventory/player_bag.gd") const FishingSpotType = preload("res://fishing/fishing_spot.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) const PlayerType = preload("res://player/player.gd") +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) @@ -30,6 +37,7 @@ enum CloseReason { @onready var _wallet_label: Label = %WalletLabel @onready var _fish_list: ItemList = %FishList +@onready var _sales_title: Label = %SalesTitle @onready var _fish_empty: Label = %FishEmpty @onready var _fish_texture: TextureRect = %FishTexture @onready var _fish_name: Label = %FishName @@ -44,6 +52,11 @@ enum CloseReason { @onready var _barrier_effect: Label = %BarrierEffect @onready var _barrier_cost: Label = %BarrierCost @onready var _barrier_purchase: Button = %BarrierPurchase +@onready var _supplies_list: VBoxContainer = %SuppliesList +@onready var _cooler_level: Label = %CoolerLevel +@onready var _cooler_effect: Label = %CoolerEffect +@onready var _cooler_cost: Label = %CoolerCost +@onready var _cooler_purchase: Button = %CoolerPurchase @onready var _confirmation: PanelContainer = %SaleConfirmation @onready var _confirmation_text: Label = %ConfirmationText @onready var _confirm_sale: Button = %ConfirmSale @@ -57,6 +70,9 @@ var _buyer: FishBuyerProfileType var _upgrades: PlayerFishingUpgradesType var _fishing_spot: FishingSpotType var _interaction: ShopInteractionType +var _bag: PlayerBagType +var _item_catalog: ItemCatalogType +var _cooler_capacity: PlayerCoolerCapacityType var _selected_catch_id: StringName var _confirmation_catch_id: StringName var _prior_movement_enabled: bool = true @@ -77,6 +93,7 @@ func _ready() -> void: _cancel_sale.pressed.connect(_close_sale_confirmation) _reel_purchase.pressed.connect(_purchase_reel_speed) _barrier_purchase.pressed.connect(_purchase_barrier_power) + _cooler_purchase.pressed.connect(_purchase_cooler_capacity) func setup( @@ -88,6 +105,9 @@ func setup( upgrades: PlayerFishingUpgradesType, fishing_spot: FishingSpotType, interaction: ShopInteractionType, + bag: PlayerBagType, + item_catalog: ItemCatalogType, + cooler_capacity: PlayerCoolerCapacityType, ) -> void: _player = player _inventory = inventory @@ -97,12 +117,21 @@ func setup( _upgrades = upgrades _fishing_spot = fishing_spot _interaction = interaction + _bag = bag + _item_catalog = item_catalog + _cooler_capacity = cooler_capacity if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) if not _wallet.balance_changed.is_connected(_on_wallet_changed): _wallet.balance_changed.connect(_on_wallet_changed) if not _upgrades.upgrades_changed.is_connected(_on_upgrades_changed): _upgrades.upgrades_changed.connect(_on_upgrades_changed) + if not _bag.contents_changed.is_connected(_on_bag_changed): + _bag.contents_changed.connect(_on_bag_changed) + if not _cooler_capacity.capacity_changed.is_connected( + _on_cooler_capacity_changed + ): + _cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed) _refresh_all() @@ -207,6 +236,8 @@ func _refresh_all() -> void: _refresh_fish_list() _refresh_selected_fish() _refresh_upgrades() + _refresh_supplies() + _refresh_cooler_capacity() func _refresh_wallet() -> void: @@ -222,6 +253,10 @@ func _refresh_fish_list() -> void: var catches: Array[FishCatchType] = ( _inventory.get_all_catches() if _inventory != null else [] ) + _sales_title.text = "Cooler %d / %d • Full base value" % [ + catches.size(), + _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, + ] catches.sort_custom( func(a: FishCatchType, b: FishCatchType) -> bool: return a.catch_sequence > b.catch_sequence @@ -335,6 +370,65 @@ func _refresh_upgrades() -> void: _barrier_purchase.text = "MAX" if barrier_cost < 0 else "Purchase" +func _refresh_supplies() -> void: + if _supplies_list == null: + return + for child: Node in _supplies_list.get_children(): + _supplies_list.remove_child(child) + child.queue_free() + for item_id: StringName in FishingShopStockType.get_stock_item_ids(): + var item: ItemDataType = _item_catalog.get_item_by_id(item_id) + if item == null: + continue + var button := Button.new() + button.custom_minimum_size = Vector2(195, 54) + button.icon = item.icon + button.expand_icon = true + button.alignment = HORIZONTAL_ALIGNMENT_LEFT + button.text = "%s\n$%d • Owned %d" % [ + item.display_name, + FishingShopStockType.get_price(item_id), + _bag.get_quantity(item_id), + ] + button.tooltip_text = item.description + button.disabled = ( + _transaction_in_progress + or _closing + or not _bag.can_add_item(item_id, 1) + or not _wallet.can_afford( + FishingShopStockType.get_price(item_id) + ) + ) + button.pressed.connect(_purchase_supply.bind(item_id)) + _supplies_list.add_child(button) + + +func _refresh_cooler_capacity() -> void: + if _cooler_capacity == null: + return + var level: int = _cooler_capacity.get_level() + var cost: int = _cooler_capacity.get_next_cost() + var next_capacity: int = _cooler_capacity.get_next_capacity() + _cooler_level.text = "Level %d" % level + _cooler_purchase.disabled = ( + cost < 0 + or _transaction_in_progress + or _closing + or not _cooler_capacity.can_purchase(_wallet) + ) + if cost < 0: + _cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity() + _cooler_cost.text = "MAX" + _cooler_purchase.text = "MAX" + else: + _cooler_effect.text = "%d → %d fish" % [ + _cooler_capacity.get_capacity(), + next_capacity, + ] + _cooler_cost.text = "$%d" % cost + _cooler_purchase.text = "Purchase" + + func _on_fish_selected(index: int) -> void: if index < 0 or index >= _fish_list.item_count: return @@ -405,6 +499,59 @@ func _purchase_barrier_power() -> void: _purchase_upgrade(false) +func _purchase_supply(item_id: StringName) -> void: + if _transaction_in_progress or not _is_transaction_context_valid(): + _feedback.text = "Unable to complete purchase." + return + var price: int = FishingShopStockType.get_price(item_id) + if not _bag.can_add_item(item_id, 1): + _feedback.text = "Bag cannot accept this item." + return + if not _wallet.can_afford(price): + _feedback.text = "Not enough money." + return + var transaction_generation: int = _generation + _transaction_in_progress = true + var purchased: bool = FishingShopStockType.purchase_one( + item_id, + _wallet, + _bag, + _item_catalog + ) + _transaction_in_progress = false + if transaction_generation != _generation or not visible: + return + _feedback.text = ( + "Item purchased." if purchased else "Unable to complete purchase." + ) + _refresh_all() + + +func _purchase_cooler_capacity() -> void: + if _transaction_in_progress or not _is_transaction_context_valid(): + _feedback.text = "Unable to complete purchase." + return + var cost: int = _cooler_capacity.get_next_cost() + if cost < 0: + _feedback.text = "Maximum level reached." + return + if not _wallet.can_afford(cost): + _feedback.text = "Not enough money." + return + var transaction_generation: int = _generation + _transaction_in_progress = true + var purchased: bool = _cooler_capacity.purchase(_wallet) + _transaction_in_progress = false + if transaction_generation != _generation or not visible: + return + _feedback.text = ( + "Upgrade purchased." + if purchased + else "Unable to complete purchase." + ) + _refresh_all() + + func _purchase_upgrade(is_reel_speed: bool) -> void: if _transaction_in_progress or not _is_transaction_context_valid(): _feedback.text = "Unable to complete purchase." @@ -472,6 +619,8 @@ func _on_inventory_changed() -> void: func _on_wallet_changed(_balance: int, _delta: int) -> void: _refresh_wallet() _refresh_upgrades() + _refresh_supplies() + _refresh_cooler_capacity() func _on_upgrades_changed( @@ -481,6 +630,15 @@ func _on_upgrades_changed( _refresh_upgrades() +func _on_bag_changed() -> void: + _refresh_supplies() + + +func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: + _refresh_cooler_capacity() + _refresh_fish_list() + + func _apply_mouse_close_policy(reason: CloseReason) -> void: if not _mouse_snapshot_stored: return diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 8561d75..4ecc8b5 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=6 format=3] +[gd_scene load_steps=7 format=3] [ext_resource type="Script" path="res://ui/fishing_shop.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/reel_speed_upgrade.svg" id="3_reel"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/barrier_power_upgrade.svg" id="4_barrier"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/fish_coin.svg" id="5_coin"] +[ext_resource type="Texture2D" path="res://items/icons/placeholder/cooler_expansion.svg" id="6_cooler"] [node name="FishingShop" type="Control"] unique_name_in_owner = true @@ -37,10 +38,10 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -450.0 -offset_top = -260.0 -offset_right = 450.0 -offset_bottom = 260.0 +offset_left = -480.0 +offset_top = -310.0 +offset_right = 480.0 +offset_bottom = 310.0 grow_horizontal = 2 grow_vertical = 2 @@ -98,12 +99,13 @@ size_flags_vertical = 3 theme_override_constants/separation = 12 [node name="FishSales" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] -custom_minimum_size = Vector2(500, 0) +custom_minimum_size = Vector2(420, 0) layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/separation = 6 [node name="SalesTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales"] +unique_name_in_owner = true layout_mode = 2 text = "Sell individual Cooler fish • Full base value" theme_override_font_sizes/font_size = 17 @@ -111,11 +113,11 @@ theme_override_font_sizes/font_size = 17 [node name="SalesBody" type="HSplitContainer" parent="ShopPanel/Margin/Layout/Body/FishSales"] layout_mode = 2 size_flags_vertical = 3 -split_offset = 265 +split_offset = 220 theme_override_constants/separation = 8 [node name="ListPanel" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"] -custom_minimum_size = Vector2(250, 0) +custom_minimum_size = Vector2(205, 0) layout_mode = 2 [node name="ListMargin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel"] @@ -143,7 +145,7 @@ icon_mode = 1 same_column_width = true [node name="FishDetail" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"] -custom_minimum_size = Vector2(220, 0) +custom_minimum_size = Vector2(185, 0) layout_mode = 2 [node name="DetailMargin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail"] @@ -159,7 +161,7 @@ theme_override_constants/separation = 6 [node name="FishTexture" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] unique_name_in_owner = true -custom_minimum_size = Vector2(190, 100) +custom_minimum_size = Vector2(160, 90) layout_mode = 2 expand_mode = 1 stretch_mode = 5 @@ -186,7 +188,7 @@ disabled = true text = "Sell Selected" [node name="Upgrades" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] -custom_minimum_size = Vector2(310, 0) +custom_minimum_size = Vector2(270, 0) layout_mode = 2 theme_override_constants/separation = 8 @@ -195,6 +197,27 @@ layout_mode = 2 text = "Fishing Upgrades" theme_override_font_sizes/font_size = 17 +[node name="Supplies" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] +custom_minimum_size = Vector2(210, 0) +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"] +layout_mode = 2 +text = "Supplies" +theme_override_font_sizes/font_size = 17 + +[node name="Scroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Supplies"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 + +[node name="SuppliesList" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/Supplies/Scroll"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 5 + [node name="ReelCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"] layout_mode = 2 @@ -210,7 +233,7 @@ layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"] -custom_minimum_size = Vector2(60, 60) +custom_minimum_size = Vector2(48, 48) layout_mode = 2 texture = ExtResource("3_reel") expand_mode = 1 @@ -261,7 +284,7 @@ layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"] -custom_minimum_size = Vector2(60, 60) +custom_minimum_size = Vector2(48, 48) layout_mode = 2 texture = ExtResource("4_barrier") expand_mode = 1 @@ -297,6 +320,57 @@ unique_name_in_owner = true layout_mode = 2 text = "Purchase" +[node name="CoolerCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"] +layout_mode = 2 + +[node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard"] +layout_mode = 2 +theme_override_constants/margin_left = 9 +theme_override_constants/margin_top = 9 +theme_override_constants/margin_right = 9 +theme_override_constants/margin_bottom = 9 + +[node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] +custom_minimum_size = Vector2(48, 48) +layout_mode = 2 +texture = ExtResource("6_cooler") +expand_mode = 1 +stretch_mode = 5 +texture_filter = 1 + +[node name="Data" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] +layout_mode = 2 +text = "Cooler Capacity" +theme_override_font_sizes/font_size = 17 + +[node name="CoolerLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] +unique_name_in_owner = true +layout_mode = 2 +text = "Level 0" + +[node name="CoolerEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] +unique_name_in_owner = true +layout_mode = 2 +text = "12 → 18 fish" + +[node name="CoolerCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] +unique_name_in_owner = true +layout_mode = 2 +text = "$75" + +[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] +unique_name_in_owner = true +layout_mode = 2 +text = "Purchase" + [node name="SaleConfirmation" type="PanelContainer" parent="."] unique_name_in_owner = true visible = false diff --git a/ui/game_ui.gd b/ui/game_ui.gd index bb5e715..883d746 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -23,6 +23,12 @@ const PlayerFishingUpgradesType = preload( const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const PlayerItemEffectsType = preload( + "res://progression/player_item_effects.gd" +) +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) @onready var _status_label: Label = %StatusLabel @onready var _catch_track: Control = %CatchTrack @@ -40,6 +46,7 @@ const ShopInteractionType = preload( @onready var _hotbar_ui: HotbarUIType = %Hotbar @onready var _fishing_shop: FishingShopType = %FishingShop @onready var _shop_prompt: PanelContainer = %ShopPrompt +@onready var _effect_status: Label = %EffectStatus var _showcase_active: bool = false var _player_menu_open: bool = false @@ -47,6 +54,7 @@ var _gameplay_ui_enabled: bool = false var _fishing_spot: FishingSpotType var _system_menu_open: bool = false var _shop_open: bool = false +var _item_effects: PlayerItemEffectsType func setup( @@ -64,8 +72,11 @@ func setup( main_shop_buyer: FishBuyerProfileType, fishing_upgrades: PlayerFishingUpgradesType, shop_interaction: ShopInteractionType, + item_effects: PlayerItemEffectsType, + cooler_capacity: PlayerCoolerCapacityType, ) -> void: _fishing_spot = fishing_spot + _item_effects = item_effects fishing_spot.status_changed.connect(_on_fishing_status_changed) fishing_spot.catch_display_changed.connect(_on_catch_display_changed) fishing_spot.showcase_changed.connect(_on_showcase_changed) @@ -83,7 +94,8 @@ func setup( fishing_spot, bag, hotbar, - item_catalog + item_catalog, + cooler_capacity ) _hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot) _fishing_shop.setup( @@ -94,11 +106,42 @@ func setup( main_shop_buyer, fishing_upgrades, fishing_spot, - shop_interaction + shop_interaction, + bag, + item_catalog, + cooler_capacity ) _fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) +func _process(_delta: float) -> void: + if _item_effects == null or not _gameplay_ui_enabled: + _effect_status.hide() + return + var parts: Array[String] = [] + for item_id: StringName in [ + PlayerItemEffectsType.COFFEE_ID, + PlayerItemEffectsType.ENERGY_DRINK_ID, + PlayerItemEffectsType.SNACK_ID, + PlayerItemEffectsType.FISH_FINDER_ID, + ]: + var remaining: float = _item_effects.get_remaining(item_id) + if remaining <= 0.0: + continue + var label: String = { + PlayerItemEffectsType.COFFEE_ID: "Coffee", + PlayerItemEffectsType.ENERGY_DRINK_ID: "Energy", + PlayerItemEffectsType.SNACK_ID: "Snack", + PlayerItemEffectsType.FISH_FINDER_ID: "Finder", + }.get(item_id, "") + parts.append( + "%s %d:%02d" + % [label, int(remaining) / 60, int(remaining) % 60] + ) + _effect_status.text = " ".join(parts) + _effect_status.visible = not parts.is_empty() + + func close_player_menu() -> void: _player_menu.close_menu() diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 3c4eabe..16171df 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -167,6 +167,20 @@ horizontal_alignment = 1 [node name="FishingShop" parent="." instance=ExtResource("8_shop")] unique_name_in_owner = true +[node name="EffectStatus" type="Label" parent="."] +unique_name_in_owner = true +visible = false +z_index = 54 +offset_left = 18.0 +offset_top = 18.0 +offset_right = 500.0 +offset_bottom = 44.0 +mouse_filter = 2 +theme = ExtResource("3_theme") +theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9) +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 + [node name="ScreenFade" type="ColorRect" parent="."] unique_name_in_owner = true visible = false diff --git a/ui/player_menu.gd b/ui/player_menu.gd index c5fb8a5..ceaee4f 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -19,6 +19,9 @@ const OwnedItemType = preload("res://items/owned_item.gd") const PlayerBagType = preload("res://inventory/player_bag.gd") const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const ItemDragSourceType = preload("res://ui/item_drag_source.gd") +const PlayerCoolerCapacityType = preload( + "res://progression/player_cooler_capacity.gd" +) signal menu_visibility_changed(is_open: bool) @@ -59,6 +62,7 @@ enum CloseReason { @onready var _sort_option: OptionButton = %SortOption @onready var _sort_direction: Button = %SortDirection @onready var _held_value: Label = %HeldValue +@onready var _cooler_count: Label = %CoolerCount @onready var _inventory_empty: Label = %InventoryEmpty @onready var _inventory_grid: GridContainer = %InventoryGrid @onready var _detail_texture: TextureRect = %DetailTexture @@ -86,6 +90,7 @@ var _fishing_spot: FishingSpotType var _bag: PlayerBagType var _hotbar: PlayerHotbarType var _item_catalog: ItemCatalogType +var _cooler_capacity: PlayerCoolerCapacityType var _current_section: Section = Section.COOLER var _sort_mode: SortMode = SortMode.CATCH_ORDER var _sort_descending: bool = true @@ -138,6 +143,7 @@ func setup( bag: PlayerBagType, hotbar: PlayerHotbarType, item_catalog: ItemCatalogType, + cooler_capacity: PlayerCoolerCapacityType, ) -> void: _player = player _inventory = inventory @@ -150,6 +156,7 @@ func setup( _bag = bag _hotbar = hotbar _item_catalog = item_catalog + _cooler_capacity = cooler_capacity if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) if not _collection_log.fish_discovered.is_connected(_on_fish_discovered): @@ -160,6 +167,10 @@ func setup( _fishing_spot.bite_activated.connect(_on_bite_activated) if not _bag.contents_changed.is_connected(_on_bag_changed): _bag.contents_changed.connect(_on_bag_changed) + if not _cooler_capacity.capacity_changed.is_connected( + _on_cooler_capacity_changed + ): + _cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed) _refresh_all() @@ -461,6 +472,14 @@ func _refresh_economy_summary() -> void: ) _wallet_balance.text = "Wallet: $%d" % balance _held_value.text = "Held fish base value: $%d" % held_total + _cooler_count.text = "%d / %d" % [ + _inventory.get_all_catches().size() if _inventory != null else 0, + _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, + ] + + +func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: + _refresh_economy_summary() func _refresh_inventory() -> void: diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index dbb1ea3..1fe08dc 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -140,6 +140,12 @@ text = "Newest first" layout_mode = 2 size_flags_horizontal = 3 +[node name="CoolerCount" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] +unique_name_in_owner = true +layout_mode = 2 +text = "0 / 12" +theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) + [node name="HeldValue" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] unique_name_in_owner = true layout_mode = 2