Add shop supplies and rebalance early economy

This commit is contained in:
Alexander Sellite 2026-07-25 22:15:39 -04:00
parent 6386c5e3e3
commit 6a5923fdb8
41 changed files with 1159 additions and 52 deletions

View file

@ -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}."

View file

@ -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

View file

@ -0,0 +1 @@
uid://dvvvjfah26pp3

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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,

View file

@ -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

16
items/catalog/coffee.tres Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")]

16
items/catalog/snack.tres Normal file
View file

@ -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

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M18 22h29v27H18z" fill="#f3d6a4" stroke="#17243a" stroke-width="5"/>
<path d="M47 28h6q7 0 7 7t-7 7h-6" fill="none" stroke="#17243a" stroke-width="5"/>
<path d="M25 17q-5-5 0-10M38 17q-5-5 0-10" fill="none" stroke="#54c7c0" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View file

@ -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

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M9 22h46v32H9z" fill="#72c7e8" stroke="#17243a" stroke-width="5"/>
<path d="M7 13h50v12H7z" fill="#fff3d4" stroke="#17243a" stroke-width="5"/>
<path d="M32 31v16M24 39h16" stroke="#ef6b88" stroke-width="6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View file

@ -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

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M20 9h25l3 7-3 39H20l-3-39z" fill="#ef6b88" stroke="#17243a" stroke-width="5"/>
<path d="M36 18l-10 17h8l-5 14 13-20h-8z" fill="#ffe26b" stroke="#17243a" stroke-width="3" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View file

@ -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

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect x="8" y="10" width="48" height="44" rx="5" fill="#54c7c0" stroke="#17243a" stroke-width="5"/>
<circle cx="31" cy="32" r="15" fill="#dff7f3" stroke="#17243a" stroke-width="4"/>
<path d="M31 32q7-8 15 0M31 32q5 6 10 0" fill="none" stroke="#ef6b88" stroke-width="3"/>
<circle cx="31" cy="32" r="3" fill="#17243a"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View file

@ -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

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path d="M12 17l8-7h30l5 9-5 35H17L9 46z" fill="#f7a84a" stroke="#17243a" stroke-width="5" stroke-linejoin="round"/>
<path d="M22 29h22M20 39h25" stroke="#fff3d4" stroke-width="5" stroke-linecap="round"/>
<path d="M50 10q-8 2-6 10 8 1 11-5z" fill="#fff3d4" stroke="#17243a" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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}

View file

@ -0,0 +1 @@
uid://c85ofvdqxyacg

View file

@ -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

View file

@ -0,0 +1 @@
uid://cvd5jj5wbab1k

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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:

View file

@ -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