Expand bait progression and retune fishing

This commit is contained in:
Alexander Sellite 2026-08-08 00:34:06 -04:00
parent 2f30691808
commit 2a5e0a8f26
21 changed files with 555 additions and 80 deletions

View file

@ -9,13 +9,33 @@ const WORM_MAX_STACK: int = 10
const ITEM_PRICES: Dictionary[StringName, int] = {
&"worms": 1,
&"snails": 3,
&"shrimp": 7,
&"squid_chunks": 15,
&"whole_anchovy": 30,
&"whole_sardine": 60,
&"luminous_roe": 125,
&"coffee": 20,
&"energy_drink": 35,
&"snack": 30,
&"fish_finder": 60,
}
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
&"snails": 400,
&"shrimp": 1200,
&"squid_chunks": 3500,
&"whole_anchovy": 9000,
&"whole_sardine": 22000,
&"luminous_roe": 50000,
}
const ITEM_ORDER: Array[StringName] = [
&"worms",
&"snails",
&"shrimp",
&"squid_chunks",
&"whole_anchovy",
&"whole_sardine",
&"luminous_roe",
&"coffee",
&"energy_drink",
&"snack",
@ -31,14 +51,35 @@ static func get_stock_item_ids() -> Array[StringName]:
return ITEM_ORDER.duplicate()
static func get_purchase_quantity(item_id: StringName, owned: int) -> int:
if item_id == &"worms":
return maxi(WORM_MAX_STACK - owned, 0)
static func get_unlock_price(item_id: StringName) -> int:
return BAIT_UNLOCK_PRICES.get(item_id, -1)
static func get_purchase_quantity(
item_id: StringName,
owned: int,
maximum_stack: int = WORM_MAX_STACK,
) -> int:
if is_bait_topoff(item_id):
return maxi(maximum_stack - owned, 0)
return 1
static func is_bait_topoff(item_id: StringName) -> bool:
return item_id == &"worms"
return item_id == &"worms" or BAIT_UNLOCK_PRICES.has(item_id)
static func get_purchase_cost(
item_id: StringName,
quantity: int,
bait_unlocked: bool,
) -> int:
if quantity <= 0:
return -1
if is_bait_topoff(item_id) and item_id != &"worms" and not bait_unlocked:
return get_unlock_price(item_id)
var unit_price: int = get_price(item_id)
return unit_price * quantity if unit_price >= 0 else -1
static func purchase_one(
@ -49,22 +90,32 @@ static func purchase_one(
) -> 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)
var bait_topoff: bool = is_bait_topoff(item_id)
var quantity: int = get_purchase_quantity(
item_id,
bag.get_quantity(item_id),
item.max_stack if item != null else WORM_MAX_STACK,
)
var price: int = get_purchase_cost(
item_id,
quantity,
bag.is_bait_unlocked(item_id),
)
if (
price < 0
or item == null
or not item.is_valid()
or (item.category != ItemDataType.Category.CONSUMABLE and not is_bait_topoff(item_id))
or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff)
or not item.stackable
or (not item.usable and not is_bait_topoff(item_id))
or not bag.can_add_item(item_id, 1)
or (not item.usable and not bait_topoff)
or not bag.can_add_item(item_id, quantity)
or not wallet.can_afford(price)
):
return false
if not wallet.debit(price):
return false
if bag.add_item(item_id, 1):
if bag.add_item(item_id, quantity):
return true
if not wallet.credit(price):
push_error("Fishing Shop failed to roll back an item purchase.")

View file

@ -16,6 +16,12 @@ const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
# multipliers later without changing catch serialization or tier identity.
const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0]
const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0]
const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0]
const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0]
const ANCHOVY_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0]
const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0]
const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0]
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
# Legacy profile multiplier retained for serialized/profile compatibility.
# New encounters use the weighted quality/rarity/weight bands below.
@ -136,17 +142,34 @@ static func roll(
static func roll_weights_for_bait(active_bait_tags: Array[StringName]) -> Array[float]:
var bait_weights: Array[float] = rarity_weights_for_bait(active_bait_tags)
var weights: Array[float] = []
for quality: int in TIER_COUNT:
weights.append(bait_weights[quality] / BASE_ROLL_WEIGHTS[quality])
return weights
static func rarity_weights_for_bait(
active_bait_tags: Array[StringName],
) -> Array[float]:
var weights: Array[float] = []
if active_bait_tags.is_empty():
weights.assign([2.5, 0.0, 0.0, 0.0, 0.0])
weights.assign([100.0, 0.0, 0.0, 0.0, 0.0])
elif active_bait_tags.has(&"bait_rarity_6"):
weights.assign(LUMINOUS_ROE_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_5"):
weights.assign(SARDINE_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_4"):
weights.assign(ANCHOVY_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_3"):
weights.assign(SQUID_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_2"):
weights.assign(SHRIMP_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_1"):
weights.assign(SNAIL_ROLL_WEIGHTS)
else:
weights.assign([
WORM_ROLL_WEIGHTS[0] / BASE_ROLL_WEIGHTS[0],
WORM_ROLL_WEIGHTS[1] / BASE_ROLL_WEIGHTS[1],
WORM_ROLL_WEIGHTS[2] / BASE_ROLL_WEIGHTS[2],
WORM_ROLL_WEIGHTS[3] / BASE_ROLL_WEIGHTS[3],
WORM_ROLL_WEIGHTS[4] / BASE_ROLL_WEIGHTS[4],
])
# Unknown and legacy bait tags retain the original worms behavior.
weights.assign(WORM_ROLL_WEIGHTS)
return weights

View file

@ -81,9 +81,9 @@ func select_fish(
func _roll_rarity(context: FishingContextType) -> int:
var weights: Array[float] = [100.0, 0.0, 0.0, 0.0, 0.0]
if not context.active_bait_tags.is_empty():
weights = [80.0, 10.0, 5.0, 4.0, 1.0]
var weights: Array[float] = FishQualityType.rarity_weights_for_bait(
context.active_bait_tags
)
for index: int in range(weights.size()):
if index < rarity_weight_multipliers.size():
weights[index] *= maxf(rarity_weight_multipliers[index], 0.0)

View file

@ -100,14 +100,14 @@ enum FishingState {
@export_range(0.0, 1.0, 0.01) var withdrawal_surface_clearance: float = 0.4
@export_category("Timing")
const BITE_QUICK_MIN_SECONDS: float = 8.0
const BITE_QUICK_MAX_SECONDS: float = 24.0
const BITE_TYPICAL_MAX_SECONDS: float = 72.0
const BITE_LONG_MAX_SECONDS: float = 144.0
const BITE_MAX_SECONDS: float = 192.0
const BITE_QUICK_PROBABILITY: float = 0.25
const BITE_QUICK_MIN_SECONDS: float = 6.0
const BITE_QUICK_MAX_SECONDS: float = 18.0
const BITE_TYPICAL_MAX_SECONDS: float = 48.0
const BITE_LONG_MAX_SECONDS: float = 72.0
const BITE_MAX_SECONDS: float = 90.0
const BITE_QUICK_PROBABILITY: float = 0.30
const BITE_TYPICAL_PROBABILITY: float = 0.65
const BITE_LONG_PROBABILITY: float = 0.08
const BITE_LONG_PROBABILITY: float = 0.04
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0

View file

@ -5,10 +5,15 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const DEFAULT_UNLOCKED_BAIT_IDS: Array[StringName] = [&"worms"]
signal contents_changed
var _catalog: ItemCatalogType
var _items: Array[OwnedItemType] = []
var _unlocked_bait_ids: Array[StringName] = (
DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
)
func setup(catalog: ItemCatalogType) -> void:
@ -27,6 +32,8 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
owned.item_id = item_id
owned.quantity = quantity
_items.append(owned)
if item.is_bait() and not _unlocked_bait_ids.has(item_id):
_unlocked_bait_ids.append(item_id)
contents_changed.emit()
return true
@ -85,6 +92,46 @@ func get_all_items() -> Array[OwnedItemType]:
return result
func get_unlocked_bait_ids() -> Array[StringName]:
return _unlocked_bait_ids.duplicate()
func is_bait_unlocked(item_id: StringName) -> bool:
return _unlocked_bait_ids.has(item_id)
func get_unlocked_bait_items() -> Array[OwnedItemType]:
var result: Array[OwnedItemType] = []
for item_id: StringName in _unlocked_bait_ids:
var owned: OwnedItemType = get_owned_item(item_id)
if owned != null:
result.append(owned.duplicate_record())
continue
var empty_record := OwnedItemType.new()
empty_record.item_id = item_id
empty_record.quantity = 0
result.append(empty_record)
return result
func replace_unlocked_bait_ids(item_ids: Array[StringName]) -> bool:
var validated: Array[StringName] = DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
var seen: Dictionary[StringName, bool] = {}
for item_id: StringName in item_ids:
if item_id.is_empty() or seen.has(item_id):
return false
seen[item_id] = true
if validated.has(item_id):
continue
var item: ItemDataType = _resolve_valid_item(item_id)
if item == null or not item.is_bait():
return false
validated.append(item_id)
_unlocked_bait_ids = validated
contents_changed.emit()
return true
func replace_all_items(items: Array[OwnedItemType]) -> bool:
var validated: Array[OwnedItemType] = []
var seen: Dictionary[StringName, bool] = {}

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=10 format=3]
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=16 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"]
@ -9,7 +9,13 @@
[ext_resource type="Resource" path="res://items/catalog/cooler_expansion.tres" id="7_cooler"]
[ext_resource type="Resource" path="res://items/catalog/art_kit.tres" id="8_art_kit"]
[ext_resource type="Resource" path="res://items/catalog/worms.tres" id="9_worms"]
[ext_resource type="Resource" path="res://items/catalog/snails.tres" id="10_snails"]
[ext_resource type="Resource" path="res://items/catalog/shrimp.tres" id="11_shrimp"]
[ext_resource type="Resource" path="res://items/catalog/squid_chunks.tres" id="12_squid"]
[ext_resource type="Resource" path="res://items/catalog/whole_anchovy.tres" id="13_anchovy"]
[ext_resource type="Resource" path="res://items/catalog/whole_sardine.tres" id="14_sardine"]
[ext_resource type="Resource" path="res://items/catalog/luminous_roe.tres" id="15_roe"]
[resource]
script = ExtResource("1_script")
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms")]
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("13_anchovy"), ExtResource("14_sardine"), ExtResource("15_roe")]

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"luminous_roe"
display_name = "luminous roe"
description = "rare glowing roe with the strongest rare catch odds."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_6"])
stackable = true
max_stack = 10
hotbar_allowed = false

14
items/catalog/shrimp.tres Normal file
View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"shrimp"
display_name = "shrimp"
description = "lively bait that noticeably improves rare catch chances."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_2"])
stackable = true
max_stack = 10
hotbar_allowed = false

14
items/catalog/snails.tres Normal file
View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"snails"
display_name = "snails"
description = "slow-moving bait with a better chance for rarer catches."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_1"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"squid_chunks"
display_name = "squid chunks"
description = "rich cut bait that strongly favors rarer catches."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_3"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"whole_anchovy"
display_name = "whole anchovy"
description = "premium whole bait with high rare catch potential."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_4"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"whole_sardine"
display_name = "whole sardine"
description = "valuable whole bait tuned for exceptional catches."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_5"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -1,14 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[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/bait/64_bait_worms.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"worms"
display_name = "worms"
description = "common bait. fills your tacklebox at the fishing shop."
description = "basic bait that opens a small chance for rarer catches."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"worm"])
bait_tags = Array[StringName]([&"worm", &"bait_rarity_0"])
stackable = true
max_stack = 10
hotbar_allowed = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bn6815x0rrspw"
path="res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/bait/64_bait_worms.png"
dest_files=["res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.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

View file

@ -992,6 +992,7 @@ func _capture_assets() -> Dictionary:
return {
"wallet": _wallet.get_balance(),
"bag": _bag.get_all_items(),
"unlocked_bait_ids": _bag.get_unlocked_bait_ids(),
"catches": _inventory.get_all_catches(),
"next_sequence": _inventory.get_next_catch_sequence(),
"discovered": _collection_log.get_discovered_ids(),
@ -1002,6 +1003,7 @@ func _capture_assets() -> Dictionary:
func _restore_assets(snapshot: Dictionary) -> void:
_wallet.restore_balance(int(snapshot["wallet"]))
_bag.replace_all_items(snapshot["bag"])
_bag.replace_unlocked_bait_ids(snapshot["unlocked_bait_ids"])
_inventory.replace_all_catches(
snapshot["catches"], int(snapshot["next_sequence"])
)

View file

@ -56,6 +56,11 @@ static func validate_request(data: Variant) -> String:
var quantity: int = payload["quantity"]
var wallet_balance: int = payload["wallet_balance"]
var current_state: int = payload["current_state"]
if (
payload.has("bait_unlocked")
and typeof(payload["bait_unlocked"]) != TYPE_BOOL
):
return "Purchase could not be completed."
if (
request_id.is_empty()
or request_id.length() > MAX_ID_LENGTH

View file

@ -109,7 +109,15 @@ func is_local_purchase_pending() -> bool:
func request_supply(item_id: StringName) -> String:
var owned: int = _bag.get_quantity(item_id) if _bag != null else 0
var quantity: int = FishingShopStockType.get_purchase_quantity(item_id, owned)
var item: ItemDataType = (
_item_catalog.get_item_by_id(item_id)
if _item_catalog != null else null
)
var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id,
owned,
item.max_stack if item != null else FishingShopStockType.WORM_MAX_STACK,
)
return _request_purchase(
item_id,
NetworkShopProtocol.ProductCategory.SUPPLY,
@ -223,6 +231,13 @@ func _request_purchase(
"wallet_balance": _wallet.get_balance() if _wallet != null else 0,
"current_state": current_state,
}
if (
category == NetworkShopProtocol.ProductCategory.SUPPLY
and FishingShopStockType.is_bait_topoff(product_id)
):
request["bait_unlocked"] = (
_bag != null and _bag.is_bait_unlocked(product_id)
)
_pending_local_request = request.duplicate(true)
local_purchase_pending.emit(request_id)
if _session.is_host():
@ -317,16 +332,18 @@ func _build_authoritative_result(
var item: ItemDataType = _item_catalog.get_item_by_id(
product_id
) if _item_catalog != null else null
cost = FishingShopStockType.get_price(product_id)
var bait_topoff: bool = (
FishingShopStockType.is_bait_topoff(product_id)
)
if (
item == null
or not item.is_valid()
or product_id not in (
FishingShopStockType.get_stock_item_ids()
)
or (item.category != ItemDataType.Category.CONSUMABLE and not FishingShopStockType.is_bait_topoff(product_id))
or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff)
or not item.stackable
or (not item.usable and not FishingShopStockType.is_bait_topoff(product_id))
or (not item.usable and not bait_topoff)
or current_state >= item.max_stack
or current_state + quantity > item.max_stack
):
@ -336,10 +353,21 @@ func _build_authoritative_result(
else "Purchase could not be completed."
)
else:
if not FishingShopStockType.is_bait_topoff(product_id) and quantity != 1:
var expected_quantity: int = (
FishingShopStockType.get_purchase_quantity(
product_id, current_state, item.max_stack
)
)
if quantity != expected_quantity:
rejection = "Purchase could not be completed."
else:
cost *= quantity
cost = FishingShopStockType.get_purchase_cost(
product_id,
quantity,
bool(request.get("bait_unlocked", true)),
)
if cost < 0:
rejection = "Purchase could not be completed."
resulting_state = current_state + quantity
NetworkShopProtocol.ProductCategory.ROD:
rejection = "This item is not sold here."
@ -532,6 +560,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
return
var wallet_snapshot: int = _wallet.get_balance()
var bag_snapshot: Array[OwnedItemType] = _bag.get_all_items()
var bait_unlock_snapshot: Array[StringName] = (
_bag.get_unlocked_bait_ids()
)
var reel_snapshot: int = _upgrades.get_reel_speed_level()
var barrier_snapshot: int = _upgrades.get_barrier_power_level()
var cooler_snapshot: int = _cooler_capacity.get_level()
@ -539,6 +570,7 @@ func _apply_purchase_result(data: Dictionary) -> void:
var applied: bool = _apply_local_product(data)
if not applied or not _save_manager.save_if_dirty():
_bag.replace_all_items(bag_snapshot)
_bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
_upgrades.restore_levels(reel_snapshot, barrier_snapshot)
_cooler_capacity.restore_level(cooler_snapshot)
_art_unlocks.restore_mask(art_snapshot)
@ -590,9 +622,22 @@ func _validate_local_result(data: Dictionary) -> String:
return "Not enough fish coin."
match category:
NetworkShopProtocol.ProductCategory.SUPPLY:
if _bag.get_quantity(product_id) != expected_state:
var item: ItemDataType = (
_item_catalog.get_item_by_id(product_id)
if _item_catalog != null else null
)
var bait_unlocked: bool = _bag.is_bait_unlocked(product_id)
if (
item == null
or _bag.get_quantity(product_id) != expected_state
or int(data["quantity"]) != FishingShopStockType.get_purchase_quantity(
product_id, expected_state, item.max_stack
)
):
return "Purchase could not be completed."
if cost != FishingShopStockType.get_price(product_id) * int(data["quantity"]):
if cost != FishingShopStockType.get_purchase_cost(
product_id, int(data["quantity"]), bait_unlocked
):
return "Purchase could not be completed."
if not _bag.can_add_item(product_id, data["quantity"]):
return "Your Bag is full."

View file

@ -43,6 +43,7 @@ class LoadSnapshot:
var wallet_balance: int = 0
var next_catch_sequence: int = 1
var bag_items: Array[OwnedItemType] = []
var unlocked_bait_ids: Array[StringName] = []
var hotbar_slots: Array[StringName] = []
var fish_hotbar_slots: Array[StringName] = []
var selected_hotbar_slot: int = 0
@ -258,7 +259,10 @@ func load_player_data() -> bool:
var wallet_restored: bool = _wallet.restore_balance(
snapshot.wallet_balance
)
var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items)
var bag_restored: bool = (
_bag.replace_all_items(snapshot.bag_items)
and _bag.replace_unlocked_bait_ids(snapshot.unlocked_bait_ids)
)
var hotbar_restored: bool = _hotbar.replace_state(
snapshot.hotbar_slots,
snapshot.selected_hotbar_slot,
@ -492,6 +496,12 @@ func _build_save_dictionary() -> Dictionary:
if owned == null or not owned.is_valid():
return {}
serialized_items.append(owned.to_save_dict())
var serialized_unlocked_baits: Array[String] = []
for item_id: StringName in _bag.get_unlocked_bait_ids():
var item_data = _item_catalog.get_item_by_id(item_id)
if item_data == null or not item_data.is_valid() or not item_data.is_bait():
return {}
serialized_unlocked_baits.append(String(item_id))
var serialized_slots: Array[String] = []
for item_id: StringName in _hotbar.get_slots():
serialized_slots.append(String(item_id))
@ -524,6 +534,7 @@ func _build_save_dictionary() -> Dictionary:
},
"bag": {
"items": serialized_items,
"unlocked_bait_ids": serialized_unlocked_baits,
},
"hotbar": {
"selected_slot": _hotbar.get_selected_slot(),
@ -589,6 +600,10 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
or not inventory_data.has("next_catch_sequence")
or typeof(bag_data.get("items")) != TYPE_ARRAY
or (
bag_data.has("unlocked_bait_ids")
and typeof(bag_data.get("unlocked_bait_ids")) != TYPE_ARRAY
)
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
or not hotbar_data.has("selected_slot")
or not experience_data.has("total_experience")
@ -781,6 +796,34 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
seen_items[item_id] = true
snapshot.bag_items.append(owned)
var seen_unlocked_baits: Dictionary[StringName, bool] = {}
if bag_data.has("unlocked_bait_ids"):
var unlocked_bait_values: Array = bag_data["unlocked_bait_ids"]
for value: Variant in unlocked_bait_values:
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
continue
var bait_id: StringName = StringName(str(value))
var bait_data = _item_catalog.get_item_by_id(bait_id)
if (
bait_id.is_empty()
or seen_unlocked_baits.has(bait_id)
or bait_data == null
or not bait_data.is_valid()
or not bait_data.is_bait()
):
continue
seen_unlocked_baits[bait_id] = true
snapshot.unlocked_bait_ids.append(bait_id)
for owned: OwnedItemType in snapshot.bag_items:
var owned_item_data = _item_catalog.get_item_by_id(owned.item_id)
if (
owned_item_data != null
and owned_item_data.is_bait()
and not seen_unlocked_baits.has(owned.item_id)
):
seen_unlocked_baits[owned.item_id] = true
snapshot.unlocked_bait_ids.append(owned.item_id)
var slot_values: Array = hotbar_data["slots"]
snapshot.hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
snapshot.hotbar_slots.fill(StringName())
@ -1098,6 +1141,8 @@ func _restore_defaults() -> void:
)
_wallet.restore_balance(0)
_bag.replace_all_items(default_items)
var default_unlocked_baits: Array[StringName] = []
_bag.replace_unlocked_bait_ids(default_unlocked_baits)
_hotbar.replace_state(default_slots, 0)
_fishing_upgrades.reset_to_defaults()
_cooler_capacity.reset_to_defaults()

View file

@ -423,34 +423,71 @@ func _refresh_supplies() -> void:
button.icon = item.icon
button.expand_icon = true
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
var owned: int = _bag.get_quantity(item_id)
var bait_topoff: bool = FishingShopStockType.is_bait_topoff(item_id)
var bait_unlocked: bool = (
not bait_topoff or _bag.is_bait_unlocked(item_id)
)
var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id, owned, item.max_stack
)
var total_cost: int = FishingShopStockType.get_purchase_cost(
item_id, quantity, bait_unlocked
)
button.text = "%s\n$%d • owned %d" % [
item.display_name,
FishingShopStockType.get_price(item_id),
_bag.get_quantity(item_id),
owned,
]
if FishingShopStockType.is_bait_topoff(item_id):
button.text = "%s\n$%d each • %d/%d" % [
var tooltip_text: String = item.description
if bait_topoff:
if item.icon != null:
button.custom_minimum_size = Vector2(72, 72)
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.text = ""
else:
button.text = (
"%s\nunlock $%d" % [item.display_name, total_cost]
if not bait_unlocked
else "%s\n$%d each • %d/%d" % [
item.display_name,
FishingShopStockType.get_price(item_id),
_bag.get_quantity(item_id),
FishingShopStockType.WORM_MAX_STACK,
owned,
item.max_stack,
]
button.tooltip_text = item.description
)
tooltip_text = (
"%s\nunlock $%d • fills to %d/%d\n%s" % [
item.display_name,
total_cost,
item.max_stack,
item.max_stack,
item.description,
]
if not bait_unlocked
else "%s\n$%d each • %d/%d\n%s" % [
item.display_name,
FishingShopStockType.get_price(item_id),
owned,
item.max_stack,
item.description,
]
)
button.disabled = (
_transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or FishingShopStockType.get_purchase_quantity(item_id, _bag.get_quantity(item_id)) <= 0
or not _bag.can_add_item(item_id, FishingShopStockType.get_purchase_quantity(item_id, _bag.get_quantity(item_id)))
or not _wallet.can_afford(
FishingShopStockType.get_price(item_id) * FishingShopStockType.get_purchase_quantity(item_id, _bag.get_quantity(item_id))
)
or quantity <= 0
or total_cost < 0
or not _bag.can_add_item(item_id, quantity)
or not _wallet.can_afford(total_cost)
)
button.tooltip_text = (
"Purchases are unavailable in this session."
if _network_shop == null or not _network_shop.can_request_purchase()
else item.description
else tooltip_text
)
UtilityPageStyleType.apply_ocean_button(button)
button.pressed.connect(_purchase_supply.bind(item_id))
@ -585,11 +622,20 @@ func _purchase_supply(item_id: StringName) -> void:
_feedback.text = "unable to complete purchase."
return
var owned: int = _bag.get_quantity(item_id)
var quantity: int = FishingShopStockType.get_purchase_quantity(item_id, owned)
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
if item == null:
_feedback.text = "Purchase could not be completed."
return
var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id, owned, item.max_stack
)
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
_feedback.text = "Your Bag is full."
return
if not _wallet.can_afford(FishingShopStockType.get_price(item_id) * quantity):
var total_cost: int = FishingShopStockType.get_purchase_cost(
item_id, quantity, _bag.is_bait_unlocked(item_id)
)
if total_cost < 0 or not _wallet.can_afford(total_cost):
_feedback.text = "Not enough fish coin."
return
if _network_shop == null:

View file

@ -189,7 +189,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _lures_filter: Button = %LuresFilter
@onready var _tackle_empty: Label = %TackleEmpty
@onready var _tackle_detail_text: Label = %TackleDetailText
@onready var _tackle_equip_button: Button = %TackleEquipButton
@onready var _tackle_equip_button: NotepadInkActionType = %TackleEquipButton
@onready var _tackle_item_list: VBoxContainer = %TackleItemList
@onready var _bag_outer_wall: PanelContainer = %BagOuterWall
@onready var _bag_inner_liner: PanelContainer = %BagInnerLiner
@ -847,7 +847,7 @@ func mount_shop_cooler(
_set_content_interactive(true)
_update_cooler_water_mask()
set_process(true)
call_deferred("_focus_shop_cooler")
_cooler_sort_option.call_deferred("grab_focus")
return true
@ -1323,7 +1323,12 @@ func _refresh_tackle_box() -> void:
child.queue_free()
var matching_items: Array[OwnedItemType] = []
if _bag != null and _item_catalog != null:
for owned: OwnedItemType in _bag.get_all_items():
var available_items: Array[OwnedItemType] = (
_bag.get_unlocked_bait_items()
if _tackle_view == TackleView.BAIT
else _bag.get_all_items()
)
for owned: OwnedItemType in available_items:
var item: ItemDataType = _item_catalog.get_item_by_id(
owned.item_id
)
@ -1343,11 +1348,22 @@ func _refresh_tackle_box() -> void:
for owned: OwnedItemType in matching_items:
var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id)
var row := Button.new()
row.text = "%s ×%d" % [item.display_name, owned.quantity]
row.icon = item.icon
row.tooltip_text = "%s ×%d" % [item.display_name, owned.quantity]
if item.is_bait() and item.icon != null:
row.custom_minimum_size = Vector2(72, 72)
row.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
row.expand_icon = true
row.alignment = HORIZONTAL_ALIGNMENT_CENTER
else:
row.text = "%s ×%d" % [item.display_name, owned.quantity]
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
row.toggle_mode = true
row.button_pressed = owned.item_id == _selected_tackle_item_id
if item.is_bait() and item.icon != null:
_apply_tackle_bait_button_style(row)
_add_tackle_quantity_badge(row, owned.quantity)
else:
UtilityPageStyle.apply_ocean_button(row)
row.pressed.connect(_select_tackle_item.bind(owned.item_id))
_tackle_item_list.add_child(row)
@ -1360,6 +1376,68 @@ func _refresh_tackle_box() -> void:
_update_tackle_detail()
func _add_tackle_quantity_badge(row: Button, quantity: int) -> void:
var badge := Panel.new()
badge.name = "QuantityBadge"
badge.anchor_left = 1.0
badge.anchor_right = 1.0
badge.offset_left = -27.0
badge.offset_top = 3.0
badge.offset_right = -3.0
badge.offset_bottom = 27.0
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.z_index = 2
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
badge.add_theme_stylebox_override("panel", badge_style)
row.add_child(badge)
var quantity_label := Label.new()
quantity_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
quantity_label.offset_left = 1.0
quantity_label.offset_right = 1.0
quantity_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
quantity_label.text = str(quantity)
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
quantity_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
quantity_label.add_theme_font_size_override(
"font_size",
13 if quantity >= 100 else 15,
)
quantity_label.add_theme_color_override("font_color", Color("e7f5f4"))
badge.add_child(quantity_label)
func _apply_tackle_bait_button_style(button: Button) -> void:
var profile := BubbleMenuProfile.new()
var normal_style: StyleBoxFlat = profile.make_normal_style()
var hover_style: StyleBoxFlat = profile.make_hover_style()
var pressed_style: StyleBoxFlat = profile.make_pressed_style()
var disabled_style: StyleBoxFlat = profile.make_disabled_style()
for style: StyleBoxFlat in [
normal_style,
hover_style,
pressed_style,
disabled_style,
]:
style.set_corner_radius_all(36)
style.content_margin_left = 13.5
style.content_margin_top = 13.5
style.content_margin_right = 13.5
style.content_margin_bottom = 13.5
button.add_theme_stylebox_override("normal", normal_style)
button.add_theme_stylebox_override("hover", hover_style)
button.add_theme_stylebox_override("focus", hover_style)
button.add_theme_stylebox_override("pressed", pressed_style)
button.add_theme_stylebox_override("disabled", disabled_style)
button.add_theme_color_override("icon_normal_color", Color.WHITE)
button.add_theme_color_override("icon_hover_color", Color.WHITE)
button.add_theme_color_override("icon_focus_color", Color.WHITE)
button.add_theme_color_override("icon_pressed_color", Color.WHITE)
func _select_tackle_item(item_id: StringName) -> void:
_selected_tackle_item_id = item_id
_refresh_tackle_box()
@ -1373,6 +1451,7 @@ func _update_tackle_detail() -> void:
)
if item == null:
_tackle_equip_button.visible = false
_tackle_equip_button.persistent_mark = false
_tackle_detail_text.text = (
"Select bait for details."
if _tackle_view == TackleView.BAIT
@ -1387,28 +1466,28 @@ func _update_tackle_detail() -> void:
if _hotbar.get_item_id(slot_index) == item.item_id:
assigned_slot = slot_index
break
var state_text: String = (
"hotbar slot %d" % (assigned_slot + 1)
if assigned_slot >= 0
else "not assigned"
)
_tackle_detail_text.text = (
"%s\n\nType: %s\nQuantity: %d\n%s\n\n%s"
% [
var detail_lines: Array[String] = [
item.display_name,
item.get_category_name(),
quantity,
state_text,
item.description,
"",
"quantity: %d" % quantity,
]
)
if assigned_slot >= 0:
detail_lines.append("hotbar slot %d" % (assigned_slot + 1))
detail_lines.append("")
detail_lines.append(item.description)
_tackle_detail_text.text = "\n".join(detail_lines)
if item.is_bait():
var is_equipped: bool = (
_player != null and _player.active_bait_id == item.item_id
)
_tackle_equip_button.text = (
"dequip %s" % item.display_name
if _player != null and _player.active_bait_id == item.item_id
if is_equipped
else "equip %s" % item.display_name
)
_tackle_equip_button.persistent_mark = is_equipped
_tackle_equip_button.disabled = quantity <= 0
_tackle_equip_button.refresh_ink_state()
func _toggle_active_bait() -> void: