Add multi-select batch fish selling

This commit is contained in:
Alexander Sellite 2026-07-26 23:18:51 -04:00
parent 9277326164
commit e560cc0b3e
9 changed files with 747 additions and 272 deletions

View file

@ -8,12 +8,15 @@ enum Status {
INVALID_VALUE,
INVALID_BUYER,
INVALID_OFFER,
INVALID_SELECTION,
TRANSACTION_FAILED,
}
var success: bool = false
var status: Status = Status.TRANSACTION_FAILED
var catch_id: StringName
var catch_ids: Array[StringName] = []
var fish_count: int = 0
var fish_name: String = ""
var buyer_id: StringName
var buyer_display_name: String = ""
@ -32,14 +35,16 @@ func get_message() -> String:
Status.SUCCESS:
return sale_message
Status.NOT_FOUND:
return "Fish no longer exists."
return "fish no longer exists."
Status.FAVORITED:
return "Favorited fish cannot be sold."
return "favorited fish cannot be sold."
Status.INVALID_VALUE:
return "Invalid sale value."
return "invalid sale value."
Status.INVALID_BUYER:
return "Buyer is unavailable."
return "buyer is unavailable."
Status.INVALID_OFFER:
return "Invalid buyer offer."
return "invalid buyer offer."
Status.INVALID_SELECTION:
return "the fish selection is invalid."
_:
return "Transaction failed."
return "transaction failed."

View file

@ -25,24 +25,53 @@ func can_sell(
catch_id: StringName,
buyer: FishBuyerProfileType,
) -> bool:
return _validate_sale(catch_id, buyer).is_success()
return preview_batch([catch_id], buyer).is_success()
func can_sell_batch(
catch_ids: Array[StringName],
buyer: FishBuyerProfileType,
) -> bool:
return preview_batch(catch_ids, buyer).is_success()
func preview_batch(
catch_ids: Array[StringName],
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
return _validate_batch(catch_ids, buyer)
func sell(
catch_id: StringName,
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
var result: FishSaleResultType = _validate_sale(catch_id, buyer)
return sell_batch([catch_id], buyer)
func sell_batch(
catch_ids: Array[StringName],
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
var result: FishSaleResultType = _validate_batch(catch_ids, buyer)
if not result.is_success():
return result
var removed_catch: FishCatchType = _inventory.remove_catch_by_id(catch_id)
if removed_catch == null:
var inventory_snapshot: Array[FishCatchType] = _inventory.get_all_catches()
var next_sequence_snapshot: int = _inventory.get_next_catch_sequence()
var removed_catches: Array[FishCatchType] = (
_inventory.remove_catches_by_ids(result.catch_ids)
)
if removed_catches.size() != result.catch_ids.size():
result.success = false
result.status = FishSaleResultType.Status.NOT_FOUND
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
return result
if not _wallet.credit(result.payout):
_inventory.add_catch(removed_catch)
if not _inventory.replace_all_catches(
inventory_snapshot,
next_sequence_snapshot
):
push_error("Unable to roll back a failed fish batch sale.")
result.success = false
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
return result
@ -50,27 +79,22 @@ func sell(
return result
func _validate_sale(
catch_id: StringName,
func _validate_batch(
catch_ids: Array[StringName],
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
var result := FishSaleResultType.new()
result.catch_id = catch_id
if (
catch_id.is_empty()
or _inventory == null
or _wallet == null
):
result.status = FishSaleResultType.Status.NOT_FOUND
result.catch_ids = catch_ids.duplicate()
result.fish_count = catch_ids.size()
if _inventory == null or _wallet == null:
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
return result
if catch_ids.is_empty():
result.status = FishSaleResultType.Status.INVALID_SELECTION
return result
if buyer == null or not buyer.is_valid():
result.status = FishSaleResultType.Status.INVALID_BUYER
return result
var fish_catch: FishCatchType = _inventory.get_catch_by_id(catch_id)
if fish_catch == null:
result.status = FishSaleResultType.Status.NOT_FOUND
return result
result.fish_name = fish_catch.fish.display_name
result.buyer_id = buyer.id
result.buyer_display_name = buyer.display_name
result.buyer_animal_name = (
@ -78,21 +102,55 @@ func _validate_sale(
if not buyer.animal_name_plural.is_empty()
else buyer.display_name
)
result.base_value = fish_catch.sale_value
result.payout = buyer.get_offer(result.base_value)
if fish_catch.is_favorited:
var seen_ids: Dictionary[StringName, bool] = {}
var contains_favorite: bool = false
const MAX_SAFE_TOTAL: int = 9223372036854775807
for catch_id: StringName in catch_ids:
if catch_id.is_empty() or seen_ids.has(catch_id):
result.status = FishSaleResultType.Status.INVALID_SELECTION
return result
seen_ids[catch_id] = true
var fish_catch: FishCatchType = _inventory.get_catch_by_id(catch_id)
if fish_catch == null:
result.status = FishSaleResultType.Status.NOT_FOUND
return result
if fish_catch.is_favorited:
contains_favorite = true
if fish_catch.sale_value < 0:
result.status = FishSaleResultType.Status.INVALID_VALUE
return result
var offer: int = buyer.get_offer(fish_catch.sale_value)
if offer < 0:
result.status = FishSaleResultType.Status.INVALID_OFFER
return result
if (
result.base_value > MAX_SAFE_TOTAL - fish_catch.sale_value
or result.payout > MAX_SAFE_TOTAL - offer
):
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
return result
result.base_value += fish_catch.sale_value
result.payout += offer
if result.fish_count == 1:
result.catch_id = fish_catch.catch_id
result.fish_name = fish_catch.fish.display_name
if contains_favorite:
result.status = FishSaleResultType.Status.FAVORITED
elif fish_catch.sale_value < 0:
result.status = FishSaleResultType.Status.INVALID_VALUE
elif result.payout < 0:
result.status = FishSaleResultType.Status.INVALID_OFFER
elif not _wallet.can_credit(result.payout):
return result
if not _wallet.can_credit(result.payout):
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
else:
result.status = FishSaleResultType.Status.SUCCESS
result.success = true
return result
result.status = FishSaleResultType.Status.SUCCESS
result.success = true
if result.fish_count == 1:
result.sale_message = buyer.get_sale_message(
result.fish_name,
result.payout
)
else:
result.sale_message = "you sold %d fish to the %s for $%d." % [
result.fish_count,
result.buyer_animal_name,
result.payout,
]
return result

View file

@ -95,18 +95,40 @@ func contains_catch_id(catch_id: StringName) -> bool:
func remove_catch_by_id(catch_id: StringName) -> FishCatchType:
if catch_id.is_empty():
return null
for index: int in range(_catches.size()):
var fish_catch: FishCatchType = _catches[index]
if fish_catch == null or fish_catch.catch_id != catch_id:
continue
_catches.remove_at(index)
contents_changed.emit(
fish_catch.fish_id,
get_count(fish_catch.fish_id)
)
catches_changed.emit()
return fish_catch
return null
var removed: Array[FishCatchType] = remove_catches_by_ids([catch_id])
return removed.front() if removed.size() == 1 else null
func remove_catches_by_ids(
catch_ids: Array[StringName],
) -> Array[FishCatchType]:
var removed: Array[FishCatchType] = []
if catch_ids.is_empty():
return removed
var requested_ids: Dictionary[StringName, bool] = {}
for catch_id: StringName in catch_ids:
if catch_id.is_empty() or requested_ids.has(catch_id):
return removed
requested_ids[catch_id] = true
for catch_id: StringName in catch_ids:
var fish_catch: FishCatchType = get_catch_by_id(catch_id)
if fish_catch == null:
removed.clear()
return removed
removed.append(fish_catch)
var remaining: Array[FishCatchType] = []
var affected_species: Dictionary[StringName, bool] = {}
for fish_catch: FishCatchType in _catches:
if requested_ids.has(fish_catch.catch_id):
affected_species[fish_catch.fish_id] = true
else:
remaining.append(fish_catch)
_catches = remaining
for fish_id: StringName in affected_species:
contents_changed.emit(fish_id, get_count(fish_id))
catches_changed.emit()
return removed
func set_catch_favorited(

106
ui/fish_batch_selection.gd Normal file
View file

@ -0,0 +1,106 @@
class_name FishBatchSelection
extends RefCounted
var _selected_ids: Dictionary[StringName, bool] = {}
var _visible_ids: Array[StringName] = []
var _focused_id: StringName
var _anchor_id: StringName
func set_visible_order(visible_ids: Array[StringName]) -> void:
_visible_ids = visible_ids.duplicate()
var visible_set: Dictionary[StringName, bool] = {}
for catch_id: StringName in _visible_ids:
if not catch_id.is_empty():
visible_set[catch_id] = true
for catch_id: StringName in _selected_ids.keys():
if not visible_set.has(catch_id):
_selected_ids.erase(catch_id)
if not visible_set.has(_focused_id):
_focused_id = StringName()
if not visible_set.has(_anchor_id):
_anchor_id = StringName()
func apply_click(
catch_id: StringName,
ctrl_pressed: bool,
shift_pressed: bool,
) -> void:
if catch_id.is_empty() or not _visible_ids.has(catch_id):
return
if not shift_pressed or _anchor_id.is_empty():
if ctrl_pressed:
if _selected_ids.has(catch_id):
_selected_ids.erase(catch_id)
else:
_selected_ids[catch_id] = true
else:
_selected_ids.clear()
_selected_ids[catch_id] = true
_focused_id = catch_id
_anchor_id = catch_id
return
var anchor_index: int = _visible_ids.find(_anchor_id)
var target_index: int = _visible_ids.find(catch_id)
if anchor_index < 0 or target_index < 0:
select_only(catch_id)
return
if not ctrl_pressed:
_selected_ids.clear()
var first_index: int = mini(anchor_index, target_index)
var last_index: int = maxi(anchor_index, target_index)
for index: int in range(first_index, last_index + 1):
_selected_ids[_visible_ids[index]] = true
_focused_id = catch_id
func select_only(catch_id: StringName) -> void:
_selected_ids.clear()
if catch_id.is_empty() or not _visible_ids.has(catch_id):
_focused_id = StringName()
_anchor_id = StringName()
return
_selected_ids[catch_id] = true
_focused_id = catch_id
_anchor_id = catch_id
func clear() -> void:
_selected_ids.clear()
_focused_id = StringName()
_anchor_id = StringName()
func remove_ids(catch_ids: Array[StringName]) -> void:
for catch_id: StringName in catch_ids:
_selected_ids.erase(catch_id)
if _focused_id == catch_id:
_focused_id = StringName()
if _anchor_id == catch_id:
_anchor_id = StringName()
func get_selected_ids() -> Array[StringName]:
var selected_in_order: Array[StringName] = []
for catch_id: StringName in _visible_ids:
if _selected_ids.has(catch_id):
selected_in_order.append(catch_id)
return selected_in_order
func is_selected(catch_id: StringName) -> bool:
return _selected_ids.has(catch_id)
func get_selected_count() -> int:
return _selected_ids.size()
func get_focused_id() -> StringName:
return _focused_id
func get_anchor_id() -> StringName:
return _anchor_id

View file

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

View file

@ -24,6 +24,9 @@ const PlayerCoolerCapacityType = preload(
const ShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
signal menu_visibility_changed(is_open: bool)
@ -42,6 +45,7 @@ enum CloseReason {
@onready var _fish_texture: TextureRect = %FishTexture
@onready var _fish_name: Label = %FishName
@onready var _fish_details: Label = %FishDetails
@onready var _selection_summary: Label = %SelectionSummary
@onready var _sell_button: Button = %SellButton
@onready var _feedback: Label = %Feedback
@onready var _reel_level: Label = %ReelLevel
@ -73,14 +77,16 @@ 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 _fish_selection := FishBatchSelectionType.new()
var _confirmation_catch_ids: Array[StringName] = []
var _confirmation_generation: int = -1
var _prior_movement_enabled: bool = true
var _prior_camera_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _snapshot_stored: bool = false
var _mouse_snapshot_stored: bool = false
var _generation: int = 0
var _selection_input_generation: int = 0
var _transaction_in_progress: bool = false
var _closing: bool = false
@ -88,6 +94,7 @@ var _closing: bool = false
func _ready() -> void:
%CloseButton.pressed.connect(close_shop)
_fish_list.item_selected.connect(_on_fish_selected)
_fish_list.item_clicked.connect(_on_fish_clicked)
_sell_button.pressed.connect(_open_sale_confirmation)
_confirm_sale.pressed.connect(_on_confirm_sale)
_cancel_sale.pressed.connect(_close_sale_confirmation)
@ -120,6 +127,7 @@ func setup(
_bag = bag
_item_catalog = item_catalog
_cooler_capacity = cooler_capacity
_fish_selection.clear()
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):
@ -189,7 +197,7 @@ func close_shop(
_generation += 1
_transaction_in_progress = false
_close_sale_confirmation()
_selected_catch_id = StringName()
_fish_selection.clear()
hide()
get_viewport().gui_release_focus()
if _fishing_spot != null and is_instance_valid(_fishing_spot):
@ -242,9 +250,9 @@ func _refresh_all() -> void:
func _refresh_wallet() -> void:
_wallet_label.text = (
"Wallet: $%d" % _wallet.get_balance()
"wallet: $%d" % _wallet.get_balance()
if _wallet != null
else "Wallet: $0"
else "wallet: $0"
)
@ -253,7 +261,7 @@ 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" % [
_sales_title.text = "cooler %d / %d • full base value" % [
catches.size(),
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
]
@ -261,11 +269,21 @@ func _refresh_fish_list() -> void:
func(a: FishCatchType, b: FishCatchType) -> bool:
return a.catch_sequence > b.catch_sequence
)
var selected_index: int = -1
var visible_ids: Array[StringName] = []
for fish_catch: FishCatchType in catches:
if fish_catch != null and fish_catch.is_valid():
visible_ids.append(fish_catch.catch_id)
_fish_selection.set_visible_order(visible_ids)
for fish_catch: FishCatchType in catches:
if fish_catch == null or not fish_catch.is_valid():
continue
var marker: String = "" if fish_catch.is_favorited else ""
var marker: String = ""
if _fish_selection.is_selected(fish_catch.catch_id):
marker += ""
if fish_catch.catch_id == _fish_selection.get_focused_id():
marker += ""
if fish_catch.is_favorited:
marker += ""
var index: int = _fish_list.add_item(
"%s%s%.2f lb"
% [marker, fish_catch.fish.display_name, fish_catch.weight_lb],
@ -277,16 +295,15 @@ func _refresh_fish_list() -> void:
"%s%s"
% [
fish_catch.fish.get_rarity_name(),
"Favorited" if fish_catch.is_favorited else "Available",
"favorited" if fish_catch.is_favorited else "available",
]
)
if fish_catch.catch_id == _selected_catch_id:
selected_index = index
_fish_empty.visible = _fish_list.item_count == 0
if selected_index >= 0:
_fish_list.select(selected_index)
elif not _selected_catch_id.is_empty():
_selected_catch_id = StringName()
_fish_list.deselect_all()
for index: int in range(_fish_list.item_count):
var catch_id := StringName(str(_fish_list.get_item_metadata(index)))
if _fish_selection.is_selected(catch_id):
_fish_list.select(index, false)
func _refresh_selected_fish() -> void:
@ -295,27 +312,72 @@ func _refresh_selected_fish() -> void:
fish_catch.fish.display_texture if fish_catch != null else null
)
_fish_name.text = (
fish_catch.fish.display_name if fish_catch != null else "Select a fish"
fish_catch.fish.display_name if fish_catch != null else "select a fish"
)
if fish_catch == null:
_fish_details.text = "Choose one individual fish from your Cooler."
_sell_button.disabled = true
_fish_details.text = "choose one individual fish from your cooler."
_update_sale_summary()
return
var offer: int = _buyer.get_offer(fish_catch.sale_value)
var individual_preview: FishSaleResultType = (
_sale_service.preview_batch([fish_catch.catch_id], _buyer)
if _sale_service != null
else null
)
var offer: int = (
individual_preview.payout if individual_preview != null else -1
)
_fish_details.text = (
"%.2f lb • %s\nBase value: $%d\nMain-shop offer: $%d%s"
"%.2f lb • %s\nbase value: $%d\nmain-shop offer: $%d%s"
% [
fish_catch.weight_lb,
fish_catch.fish.get_rarity_name(),
fish_catch.sale_value,
offer,
"\nFavorited fish cannot be sold."
"\nfavorited fish cannot be sold."
if fish_catch.is_favorited
else "",
]
)
_update_sale_summary()
func _update_sale_summary() -> void:
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
var selected_count: int = selected_ids.size()
_sell_button.text = (
"sell fish"
if selected_count <= 1
else "sell %d fish" % selected_count
)
if selected_count == 0:
_selection_summary.text = "no fish selected"
_sell_button.disabled = true
return
var preview: FishSaleResultType = (
_sale_service.preview_batch(selected_ids, _buyer)
if _sale_service != null
else null
)
_selection_summary.text = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
if (
preview != null
and (
preview.is_success()
or preview.status == FishSaleResultType.Status.FAVORITED
)
):
_selection_summary.text += "\ntotal offer: $%d" % preview.payout
if preview != null and preview.status == FishSaleResultType.Status.FAVORITED:
_selection_summary.text += (
"\nfavorited fish must be removed from the selection."
)
_sell_button.disabled = (
fish_catch.is_favorited
preview == null
or not preview.is_success()
or _transaction_in_progress
or _closing
)
@ -326,7 +388,7 @@ func _refresh_upgrades() -> void:
return
var reel_level: int = _upgrades.get_reel_speed_level()
var reel_cost: int = _upgrades.get_next_reel_speed_cost()
_reel_level.text = "Level %d" % reel_level
_reel_level.text = "level %d" % reel_level
_reel_purchase.disabled = (
reel_cost < 0
or _transaction_in_progress
@ -335,7 +397,7 @@ func _refresh_upgrades() -> void:
)
if reel_cost < 0:
_reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier()
_reel_cost.text = "MAX"
_reel_cost.text = "max"
else:
_reel_effect.text = (
"%.2f×%.2f×"
@ -345,10 +407,10 @@ func _refresh_upgrades() -> void:
]
)
_reel_cost.text = "$%d" % reel_cost
_reel_purchase.text = "MAX" if reel_cost < 0 else "Purchase"
_reel_purchase.text = "max" if reel_cost < 0 else "purchase"
var barrier_level: int = _upgrades.get_barrier_power_level()
var barrier_cost: int = _upgrades.get_next_barrier_power_cost()
_barrier_level.text = "Level %d" % barrier_level
_barrier_level.text = "level %d" % barrier_level
_barrier_purchase.disabled = (
barrier_cost < 0
or _transaction_in_progress
@ -357,7 +419,7 @@ func _refresh_upgrades() -> void:
)
if barrier_cost < 0:
_barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage()
_barrier_cost.text = "MAX"
_barrier_cost.text = "max"
else:
_barrier_effect.text = (
"%d damage → %d damage"
@ -367,7 +429,7 @@ func _refresh_upgrades() -> void:
]
)
_barrier_cost.text = "$%d" % barrier_cost
_barrier_purchase.text = "MAX" if barrier_cost < 0 else "Purchase"
_barrier_purchase.text = "max" if barrier_cost < 0 else "purchase"
func _refresh_supplies() -> void:
@ -385,7 +447,7 @@ func _refresh_supplies() -> void:
button.icon = item.icon
button.expand_icon = true
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.text = "%s\n$%dOwned %d" % [
button.text = "%s\n$%downed %d" % [
item.display_name,
FishingShopStockType.get_price(item_id),
_bag.get_quantity(item_id),
@ -409,7 +471,7 @@ func _refresh_cooler_capacity() -> void:
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_level.text = "level %d" % level
_cooler_purchase.disabled = (
cost < 0
or _transaction_in_progress
@ -418,46 +480,99 @@ func _refresh_cooler_capacity() -> void:
)
if cost < 0:
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity()
_cooler_cost.text = "MAX"
_cooler_purchase.text = "MAX"
_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"
_cooler_purchase.text = "purchase"
func _on_fish_selected(index: int) -> void:
if index < 0 or index >= _fish_list.item_count:
return
_selected_catch_id = StringName(str(_fish_list.get_item_metadata(index)))
var input_generation: int = _selection_input_generation
call_deferred(
"_apply_keyboard_fish_selection",
index,
input_generation
)
func _apply_keyboard_fish_selection(
index: int,
input_generation: int,
) -> void:
if (
input_generation != _selection_input_generation
or index < 0
or index >= _fish_list.item_count
):
return
var catch_id := StringName(str(_fish_list.get_item_metadata(index)))
_fish_selection.select_only(catch_id)
_feedback.text = ""
_refresh_fish_list()
_refresh_selected_fish()
func _on_fish_clicked(
index: int,
_position: Vector2,
mouse_button_index: int,
) -> void:
if (
mouse_button_index != MOUSE_BUTTON_LEFT
or index < 0
or index >= _fish_list.item_count
):
return
_selection_input_generation += 1
var catch_id := StringName(str(_fish_list.get_item_metadata(index)))
_fish_selection.apply_click(
catch_id,
Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT)
)
_feedback.text = ""
_refresh_fish_list()
_refresh_selected_fish()
func _open_sale_confirmation() -> void:
var fish_catch: FishCatchType = _get_selected_catch()
if not _is_transaction_context_valid() or fish_catch == null:
_feedback.text = "This fish is no longer available."
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
if not _is_transaction_context_valid() or selected_ids.is_empty():
_feedback.text = "the fish selection is no longer available."
_refresh_all()
return
if fish_catch.is_favorited:
_feedback.text = "Favorited fish cannot be sold."
var preview: FishSaleResultType = _sale_service.preview_batch(
selected_ids,
_buyer
)
if not preview.is_success():
_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
return
_confirmation_catch_id = fish_catch.catch_id
var offer: int = _buyer.get_offer(fish_catch.sale_value)
_confirmation_catch_ids = selected_ids.duplicate()
_confirmation_generation = _generation
_confirmation_text.text = (
"Sell this %.2f lb %s to the Fishing Shop for $%d?"
% [fish_catch.weight_lb, fish_catch.fish.display_name, offer]
"sell %d fish to the fishing shop for $%d?"
% [preview.fish_count, preview.payout]
)
_confirmation.show()
_cancel_sale.grab_focus()
func _close_sale_confirmation() -> void:
_confirmation_catch_id = StringName()
_confirmation_catch_ids.clear()
_confirmation_generation = -1
_confirmation.hide()
@ -465,18 +580,23 @@ func _on_confirm_sale() -> void:
if _transaction_in_progress:
return
var transaction_generation: int = _generation
var catch_id: StringName = _confirmation_catch_id
var catch_ids: Array[StringName] = _confirmation_catch_ids.duplicate()
var confirmation_generation: int = _confirmation_generation
_close_sale_confirmation()
if (
catch_id.is_empty()
catch_ids.is_empty()
or not _is_transaction_context_valid()
or transaction_generation != _generation
or confirmation_generation != transaction_generation
or _buyer.id != MAIN_SHOP_BUYER_ID
):
_feedback.text = "Unable to complete sale."
_feedback.text = "unable to complete sale."
return
_transaction_in_progress = true
var result: FishSaleResultType = _sale_service.sell(catch_id, _buyer)
var result: FishSaleResultType = _sale_service.sell_batch(
catch_ids,
_buyer
)
_transaction_in_progress = false
if (
transaction_generation != _generation
@ -484,8 +604,15 @@ func _on_confirm_sale() -> void:
):
return
if result.is_success():
_feedback.text = "Fish sold for $%d." % result.payout
_selected_catch_id = StringName()
_feedback.text = (
"fish sold for $%d."
if result.fish_count == 1
else "%d fish sold for $%d." % [
result.fish_count,
result.payout,
]
)
_fish_selection.remove_ids(catch_ids)
else:
_feedback.text = result.get_message()
_refresh_all()
@ -501,14 +628,14 @@ func _purchase_barrier_power() -> void:
func _purchase_supply(item_id: StringName) -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "Unable to complete purchase."
_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."
_feedback.text = "bag cannot accept this item."
return
if not _wallet.can_afford(price):
_feedback.text = "Not enough money."
_feedback.text = "not enough money."
return
var transaction_generation: int = _generation
_transaction_in_progress = true
@ -522,21 +649,21 @@ func _purchase_supply(item_id: StringName) -> void:
if transaction_generation != _generation or not visible:
return
_feedback.text = (
"Item purchased." if purchased else "Unable to complete purchase."
"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."
_feedback.text = "unable to complete purchase."
return
var cost: int = _cooler_capacity.get_next_cost()
if cost < 0:
_feedback.text = "Maximum level reached."
_feedback.text = "maximum level reached."
return
if not _wallet.can_afford(cost):
_feedback.text = "Not enough money."
_feedback.text = "not enough money."
return
var transaction_generation: int = _generation
_transaction_in_progress = true
@ -545,16 +672,16 @@ func _purchase_cooler_capacity() -> void:
if transaction_generation != _generation or not visible:
return
_feedback.text = (
"Upgrade purchased."
"upgrade purchased."
if purchased
else "Unable to complete purchase."
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."
_feedback.text = "unable to complete purchase."
return
var cost: int = (
_upgrades.get_next_reel_speed_cost()
@ -562,10 +689,10 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
else _upgrades.get_next_barrier_power_cost()
)
if cost < 0:
_feedback.text = "Maximum level reached."
_feedback.text = "maximum level reached."
return
if not _wallet.can_afford(cost):
_feedback.text = "Not enough money."
_feedback.text = "not enough money."
return
var transaction_generation: int = _generation
_transaction_in_progress = true
@ -578,17 +705,18 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
if transaction_generation != _generation or not visible:
return
_feedback.text = (
"Upgrade purchased."
"upgrade purchased."
if purchased
else "Unable to complete purchase."
else "unable to complete purchase."
)
_refresh_all()
func _get_selected_catch() -> FishCatchType:
if _inventory == null or _selected_catch_id.is_empty():
var focused_id: StringName = _fish_selection.get_focused_id()
if _inventory == null or focused_id.is_empty():
return null
return _inventory.get_catch_by_id(_selected_catch_id)
return _inventory.get_catch_by_id(focused_id)
func _is_transaction_context_valid() -> bool:
@ -607,12 +735,22 @@ func _is_transaction_context_valid() -> bool:
func _on_inventory_changed() -> void:
_refresh_fish_list()
if (
not _confirmation_catch_id.is_empty()
and _inventory.get_catch_by_id(_confirmation_catch_id) == null
):
_close_sale_confirmation()
_feedback.text = "This fish is no longer available."
if not _confirmation_catch_ids.is_empty():
var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids,
_buyer
)
if (
_confirmation_generation != _generation
or not preview.is_success()
):
_close_sale_confirmation()
_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
_refresh_selected_fish()

View file

@ -63,7 +63,7 @@ theme_override_constants/separation = 8
[node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Header"]
layout_mode = 2
size_flags_horizontal = 3
text = "Fishing Shop"
text = "fishing shop"
theme_override_font_sizes/font_size = 24
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
@ -78,12 +78,12 @@ texture_filter = 1
[node name="WalletLabel" type="Label" parent="ShopPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
text = "Wallet: $0"
text = "wallet: $0"
[node name="CloseButton" type="Button" parent="ShopPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
text = "Close"
text = "close"
custom_minimum_size = Vector2(72, 30)
[node name="Feedback" type="Label" parent="ShopPanel/Margin/Layout"]
@ -107,7 +107,7 @@ 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"
text = "sell individual cooler fish • full base value"
theme_override_font_sizes/font_size = 17
[node name="SalesBody" type="HSplitContainer" parent="ShopPanel/Margin/Layout/Body/FishSales"]
@ -133,7 +133,7 @@ layout_mode = 2
[node name="FishEmpty" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"]
unique_name_in_owner = true
layout_mode = 2
text = "Your Cooler is empty."
text = "your cooler is empty."
horizontal_alignment = 1
[node name="FishList" type="ItemList" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"]
@ -142,6 +142,7 @@ layout_mode = 2
size_flags_vertical = 3
fixed_icon_size = Vector2i(48, 36)
icon_mode = 1
select_mode = 1
same_column_width = true
[node name="FishDetail" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"]
@ -170,22 +171,30 @@ texture_filter = 1
[node name="FishName" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
text = "Select a fish"
text = "select a fish"
horizontal_alignment = 1
theme_override_font_sizes/font_size = 19
[node name="FishDetails" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
text = "Choose one individual fish from your Cooler."
text = "choose one individual fish from your cooler."
horizontal_alignment = 1
autowrap_mode = 2
[node name="SelectionSummary" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
text = "no fish selected"
horizontal_alignment = 1
autowrap_mode = 2
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
[node name="SellButton" type="Button" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "Sell Selected"
text = "sell selected"
[node name="Upgrades" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"]
custom_minimum_size = Vector2(270, 0)
@ -194,7 +203,7 @@ theme_override_constants/separation = 8
[node name="UpgradeTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
text = "Fishing Upgrades"
text = "fishing upgrades"
theme_override_font_sizes/font_size = 17
[node name="Supplies" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"]
@ -204,7 +213,7 @@ theme_override_constants/separation = 6
[node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"]
layout_mode = 2
text = "Supplies"
text = "supplies"
theme_override_font_sizes/font_size = 17
[node name="Scroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Supplies"]
@ -246,13 +255,13 @@ size_flags_horizontal = 3
[node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
layout_mode = 2
text = "Reel Speed"
text = "reel speed"
theme_override_font_sizes/font_size = 17
[node name="ReelLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "Level 0"
text = "level 0"
[node name="ReelEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
unique_name_in_owner = true
@ -267,7 +276,7 @@ text = "$50"
[node name="ReelPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"]
unique_name_in_owner = true
layout_mode = 2
text = "Purchase"
text = "purchase"
[node name="BarrierCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
@ -297,13 +306,13 @@ size_flags_horizontal = 3
[node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
layout_mode = 2
text = "Barrier Power"
text = "barrier power"
theme_override_font_sizes/font_size = 17
[node name="BarrierLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "Level 0"
text = "level 0"
[node name="BarrierEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
unique_name_in_owner = true
@ -318,7 +327,7 @@ text = "$100"
[node name="BarrierPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"]
unique_name_in_owner = true
layout_mode = 2
text = "Purchase"
text = "purchase"
[node name="CoolerCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
@ -348,13 +357,13 @@ 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"
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"
text = "level 0"
[node name="CoolerEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"]
unique_name_in_owner = true
@ -369,7 +378,7 @@ 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"
text = "purchase"
[node name="SaleConfirmation" type="PanelContainer" parent="."]
unique_name_in_owner = true
@ -413,10 +422,10 @@ theme_override_constants/separation = 8
[node name="ConfirmSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"]
unique_name_in_owner = true
layout_mode = 2
text = "Sell Fish"
text = "sell fish"
theme_type_variation = &"DangerButton"
[node name="CancelSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"]
unique_name_in_owner = true
layout_mode = 2
text = "Cancel"
text = "cancel"

View file

@ -22,6 +22,9 @@ const ItemDragSourceType = preload("res://ui/item_drag_source.gd")
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
signal menu_visibility_changed(is_open: bool)
@ -68,6 +71,7 @@ enum CloseReason {
@onready var _detail_texture: TextureRect = %DetailTexture
@onready var _detail_name: Label = %DetailName
@onready var _detail_data: Label = %DetailData
@onready var _selection_summary: Label = %SelectionSummary
@onready var _favorite_button: Button = %FavoriteButton
@onready var _sell_button: Button = %SellButton
@onready var _sale_unavailable: Label = %SaleUnavailable
@ -94,7 +98,7 @@ var _cooler_capacity: PlayerCoolerCapacityType
var _current_section: Section = Section.COOLER
var _sort_mode: SortMode = SortMode.CATCH_ORDER
var _sort_descending: bool = true
var _selected_catch_id: StringName
var _fish_selection := FishBatchSelectionType.new()
var _selected_bag_item_id: StringName
var _prior_movement_enabled: bool = true
var _prior_camera_input_enabled: bool = true
@ -102,9 +106,10 @@ var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _control_snapshot_stored: bool = false
var _mouse_snapshot_stored: bool = false
var _menu_generation: int = 0
var _confirmation_catch_id: StringName
var _confirmation_catch_ids: Array[StringName] = []
var _confirmation_buyer: FishBuyerProfileType
var _confirmation_buyer_id: StringName
var _confirmation_generation: int = -1
var _sale_in_progress: bool = false
@ -123,9 +128,9 @@ func _ready() -> void:
_sell_button.pressed.connect(_on_sell_pressed)
_confirm_sale_button.pressed.connect(_on_confirm_sale_pressed)
_cancel_sale_button.pressed.connect(_close_sale_confirmation)
_sort_option.add_item("Catch order", SortMode.CATCH_ORDER)
_sort_option.add_item("Name", SortMode.NAME)
_sort_option.add_item("Rarity", SortMode.RARITY)
_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_sort_option.add_item("name", SortMode.NAME)
_sort_option.add_item("rarity", SortMode.RARITY)
_sort_option.select(SortMode.CATCH_ORDER)
_update_sort_direction_text()
_show_section(_current_section)
@ -157,6 +162,7 @@ func setup(
_hotbar = hotbar
_item_catalog = item_catalog
_cooler_capacity = cooler_capacity
_fish_selection.clear()
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):
@ -234,6 +240,13 @@ func close_menu(
return
get_viewport().gui_cancel_drag()
_close_sale_confirmation()
if reason in [
CloseReason.BITE_STARTED,
CloseReason.WATER_RECOVERY,
CloseReason.SESSION_END,
CloseReason.TEARDOWN,
]:
_fish_selection.clear()
var closing_generation: int = _menu_generation
visible = false
get_viewport().gui_release_focus()
@ -249,6 +262,7 @@ func close_menu(
func close_for_water_recovery() -> void:
_fish_selection.clear()
close_menu(CloseReason.WATER_RECOVERY, false)
@ -257,13 +271,14 @@ func close_for_game_menu() -> void:
func close_for_session_end() -> void:
_fish_selection.clear()
close_menu(CloseReason.SESSION_END, false)
func _exit_tree() -> void:
if visible:
close_menu(CloseReason.TEARDOWN, false)
_selected_catch_id = StringName()
_fish_selection.clear()
_menu_generation += 1
@ -340,11 +355,11 @@ func _on_sort_direction_pressed() -> void:
func _update_sort_direction_text() -> void:
match _sort_mode:
SortMode.CATCH_ORDER:
_sort_direction.text = "Newest first" if _sort_descending else "Oldest first"
_sort_direction.text = "newest first" if _sort_descending else "oldest first"
SortMode.NAME:
_sort_direction.text = "ZA" if _sort_descending else "AZ"
_sort_direction.text = "za" if _sort_descending else "az"
SortMode.RARITY:
_sort_direction.text = "High to low" if _sort_descending else "Low to high"
_sort_direction.text = "high to low" if _sort_descending else "low to high"
func _refresh_all() -> void:
@ -386,9 +401,9 @@ func _refresh_bag() -> void:
item.get_category_name(),
]
card.tooltip_text = (
"Drag to a hotbar slot."
"drag to a hotbar slot."
if item.hotbar_allowed
else "This item cannot be assigned to the hotbar."
else "this item cannot be assigned to the hotbar."
)
card.setup(item.item_id, item.display_name, item.icon)
card.pressed.connect(_select_bag_item.bind(item.item_id))
@ -427,19 +442,19 @@ func _update_bag_detail() -> void:
_bag_detail_texture.texture = item.icon if item != null else null
_bag_detail_name.text = item.display_name if item != null else ""
_bag_detail_data.text = (
"%s\nQuantity: %d\n%s\n%s"
"%s\nquantity: %d\n%s\n%s"
% [
item.get_category_name(),
quantity,
item.description,
(
"Can be assigned to the hotbar."
"can be assigned to the hotbar."
if item.hotbar_allowed
else "Cannot be assigned to the hotbar."
else "cannot be assigned to the hotbar."
),
]
if item != null
else "Select a Bag item for details."
else "select a bag item for details."
)
@ -470,8 +485,8 @@ func _refresh_economy_summary() -> void:
if _inventory != null
else 0
)
_wallet_balance.text = "Wallet: $%d" % balance
_held_value.text = "Held fish base value: $%d" % held_total
_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,
@ -493,16 +508,18 @@ func _refresh_inventory() -> void:
catches.append(fish_catch)
catches.sort_custom(_compare_catches)
_inventory_empty.visible = catches.is_empty()
var visible_ids: Array[StringName] = []
for fish_catch: FishCatchType in catches:
visible_ids.append(fish_catch.catch_id)
_fish_selection.set_visible_order(visible_ids)
var selected: FishCatchType
if _inventory != null:
selected = _inventory.get_catch(_selected_catch_id)
if selected == null and not catches.is_empty():
selected = catches.front()
_selected_catch_id = selected.catch_id
selected = _inventory.get_catch(_fish_selection.get_focused_id())
for fish_catch: FishCatchType in catches:
_inventory_grid.add_child(_create_inventory_card(fish_catch))
_update_inventory_detail(selected)
_update_sale_summary()
_update_sort_direction_text()
@ -527,11 +544,17 @@ func _create_inventory_card(fish_catch: FishCatchType) -> Button:
card.theme_type_variation = &"CardButton"
card.custom_minimum_size = Vector2(132.0, 118.0)
card.toggle_mode = true
card.button_pressed = fish_catch.catch_id == _selected_catch_id
card.pressed.connect(_select_catch.bind(fish_catch.catch_id))
var is_selected: bool = _fish_selection.is_selected(fish_catch.catch_id)
var is_focused: bool = (
fish_catch.catch_id == _fish_selection.get_focused_id()
)
card.button_pressed = is_selected
card.pressed.connect(_on_catch_card_pressed.bind(fish_catch.catch_id))
_apply_inventory_card_styles(
card,
UIPalette.get_rarity_color(fish_catch.fish.rarity)
UIPalette.get_rarity_color(fish_catch.fish.rarity),
is_selected,
is_focused
)
var content := VBoxContainer.new()
@ -573,34 +596,83 @@ func _create_inventory_card(fish_catch: FishCatchType) -> Button:
favorite_marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
favorite_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_child(favorite_marker)
if is_selected:
var selected_marker := Label.new()
selected_marker.text = ""
selected_marker.add_theme_color_override(
"font_color",
UIPalette.SUCCESS
)
selected_marker.add_theme_font_size_override("font_size", 18)
selected_marker.set_anchors_preset(Control.PRESET_TOP_LEFT)
selected_marker.offset_left = 8.0
selected_marker.offset_top = 5.0
selected_marker.offset_right = 30.0
selected_marker.offset_bottom = 29.0
selected_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_child(selected_marker)
if is_focused:
var focus_marker := Label.new()
focus_marker.text = ""
focus_marker.add_theme_color_override(
"font_color",
UIPalette.PRIMARY
)
focus_marker.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
focus_marker.offset_left = -26.0
focus_marker.offset_top = -25.0
focus_marker.offset_right = -8.0
focus_marker.offset_bottom = -7.0
focus_marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
focus_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
card.add_child(focus_marker)
return card
func _apply_inventory_card_styles(
card: Button,
rarity_color: Color,
is_selected: bool,
is_focused: bool,
) -> void:
card.add_theme_stylebox_override(
"normal",
_create_inventory_card_style(rarity_color, 0)
_create_inventory_card_style(
rarity_color,
0,
is_selected,
is_focused
)
)
card.add_theme_stylebox_override(
"hover",
_create_inventory_card_style(rarity_color, 1)
_create_inventory_card_style(
rarity_color,
1,
is_selected,
is_focused
)
)
card.add_theme_stylebox_override(
"focus",
_create_inventory_card_style(rarity_color, 1)
_create_inventory_card_style(
rarity_color,
1,
is_selected,
true
)
)
card.add_theme_stylebox_override(
"pressed",
_create_inventory_card_style(rarity_color, 2)
_create_inventory_card_style(rarity_color, 2, true, is_focused)
)
func _create_inventory_card_style(
rarity_color: Color,
state_index: int,
is_selected: bool,
is_focused: bool,
) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
match state_index:
@ -609,9 +681,13 @@ func _create_inventory_card_style(
2:
style.bg_color = UIPalette.PRIMARY.darkened(0.48)
_:
style.bg_color = UIPalette.ELEVATED_PANEL
style.bg_color = (
UIPalette.PRIMARY.darkened(0.55)
if is_selected
else UIPalette.ELEVATED_PANEL
)
style.border_color = rarity_color
var border_width: int = 4 if state_index == 2 else 2
var border_width: int = 4 if state_index == 2 or is_focused else 2
style.set_border_width_all(border_width)
style.set_corner_radius_all(8)
style.content_margin_left = 7.0
@ -621,13 +697,18 @@ func _create_inventory_card_style(
return style
func _select_catch(catch_id: StringName) -> void:
func _on_catch_card_pressed(catch_id: StringName) -> void:
if _inventory == null:
return
var selected: FishCatchType = _inventory.get_catch(catch_id)
if selected == null:
return
_selected_catch_id = catch_id
_fish_selection.apply_click(
catch_id,
Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT)
)
_transaction_feedback.text = ""
_refresh_inventory()
@ -638,26 +719,28 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
_detail_name.text = ""
_detail_data.text = ""
_favorite_button.disabled = true
_favorite_button.text = "Favorite"
_sell_button.disabled = true
_sale_unavailable.text = ""
_sale_unavailable.visible = false
_favorite_button.text = "favorite"
return
_detail_texture.texture = fish_catch.fish.display_texture
_detail_texture.visible = fish_catch.fish.display_texture != null
_detail_name.text = fish_catch.fish.display_name
var individual_preview: FishSaleResultType = (
_sale_service.preview_batch([fish_catch.catch_id], _default_buyer)
if _sale_service != null
else null
)
var buyer_offer: int = (
_default_buyer.get_offer(fish_catch.sale_value)
if _default_buyer != null
individual_preview.payout
if individual_preview != null
else -1
)
var buyer_offer_text: String = "Buyer unavailable"
if buyer_offer >= 0:
var buyer_offer_text: String = "buyer unavailable"
if buyer_offer >= 0 and _default_buyer != null:
buyer_offer_text = "%s offer: $%d" % [
_default_buyer.display_name,
buyer_offer,
]
_detail_data.text = "%.2f lb\n%s\nBase value: $%d\n%s" % [
_detail_data.text = "%.2f lb\n%s\nbase value: $%d\n%s" % [
fish_catch.weight_lb,
fish_catch.fish.get_rarity_name(),
fish_catch.sale_value,
@ -665,35 +748,68 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
]
_favorite_button.disabled = false
_favorite_button.text = (
"Unfavorite" if fish_catch.is_favorited else "Favorite"
"unfavorite" if fish_catch.is_favorited else "favorite"
)
var valid_sale_value: bool = fish_catch.sale_value >= 0
var valid_buyer: bool = (
_default_buyer != null
and _default_buyer.is_valid()
and buyer_offer >= 0
func _update_sale_summary() -> void:
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
var selected_count: int = selected_ids.size()
_sell_button.text = (
"sell fish"
if selected_count == 1
else "sell %d fish" % selected_count
)
_sell_button.disabled = (
fish_catch.is_favorited
or not valid_sale_value
or not valid_buyer
if selected_count == 0:
_selection_summary.text = "no fish selected"
_sell_button.text = "sell fish"
_sell_button.disabled = true
_sale_unavailable.text = ""
_sale_unavailable.visible = false
return
var preview: FishSaleResultType = (
_sale_service.preview_batch(selected_ids, _default_buyer)
if _sale_service != null
else null
)
if fish_catch.is_favorited:
_sale_unavailable.text = "Favorited fish cannot be sold."
elif not valid_sale_value:
_sale_unavailable.text = "Invalid sale value."
elif not valid_buyer:
_sale_unavailable.text = "Buyer is unavailable."
var count_text: String = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_summary.text = count_text
if (
preview != null
and preview.payout >= 0
and _default_buyer != null
and (
preview.is_success()
or preview.status == FishSaleResultType.Status.FAVORITED
)
):
_selection_summary.text += "\n%s total offer: $%d" % [
_default_buyer.display_name,
preview.payout,
]
_sell_button.disabled = preview == null or not preview.is_success()
if preview != null and preview.status == FishSaleResultType.Status.FAVORITED:
_sale_unavailable.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
)
elif preview != null and not preview.is_success():
_sale_unavailable.text = preview.get_message()
else:
_sale_unavailable.text = ""
_sale_unavailable.visible = not _sale_unavailable.text.is_empty()
func _on_favorite_pressed() -> void:
if _inventory == null or _selected_catch_id.is_empty():
var focused_id: StringName = _fish_selection.get_focused_id()
if _inventory == null or focused_id.is_empty():
return
var fish_catch: FishCatchType = _inventory.get_catch_by_id(
_selected_catch_id
focused_id
)
if fish_catch == null:
_refresh_inventory()
@ -712,46 +828,40 @@ func _on_favorite_pressed() -> void:
func _on_sell_pressed() -> void:
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
if (
_inventory == null
or _sale_service == null
or _default_buyer == null
or _selected_catch_id.is_empty()
or selected_ids.is_empty()
):
return
var fish_catch: FishCatchType = _inventory.get_catch_by_id(
_selected_catch_id
var preview: FishSaleResultType = _sale_service.preview_batch(
selected_ids,
_default_buyer
)
if fish_catch == null:
_transaction_feedback.text = "Fish no longer exists."
if not preview.is_success():
_transaction_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
_refresh_inventory()
return
if fish_catch.is_favorited:
_transaction_feedback.text = "Favorited fish cannot be sold."
return
if fish_catch.sale_value < 0:
_transaction_feedback.text = "Invalid sale value."
return
if not _default_buyer.is_valid():
_transaction_feedback.text = "Buyer is unavailable."
return
var buyer_offer: int = _default_buyer.get_offer(
fish_catch.sale_value
)
if buyer_offer < 0:
_transaction_feedback.text = "Invalid buyer offer."
return
_confirmation_catch_id = fish_catch.catch_id
_confirmation_catch_ids = selected_ids.duplicate()
_confirmation_buyer = _default_buyer
_confirmation_buyer_id = _default_buyer.id
_confirmation_generation = _menu_generation
var fish_label: String = "fish"
_confirmation_message.text = (
"Sell this %.2f lb %s to the %s for $%d?\nBase value: $%d"
"sell %d %s to the %s for $%d?\ncombined base value: $%d"
% [
fish_catch.weight_lb,
fish_catch.fish.display_name,
preview.fish_count,
fish_label,
_get_buyer_display_group(_default_buyer),
buyer_offer,
fish_catch.sale_value,
preview.payout,
preview.base_value,
]
)
_sale_confirmation.visible = true
@ -763,31 +873,39 @@ func _on_confirm_sale_pressed() -> void:
if (
_sale_in_progress
or _sale_service == null
or _confirmation_catch_id.is_empty()
or _confirmation_catch_ids.is_empty()
or _confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
or _confirmation_generation != _menu_generation
):
return
_sale_in_progress = true
var requested_catch_id: StringName = _confirmation_catch_id
var requested_catch_ids: Array[StringName] = (
_confirmation_catch_ids.duplicate()
)
var transaction_generation: int = _menu_generation
var transaction_buyer: FishBuyerProfileType = _confirmation_buyer
_confirm_sale_button.disabled = true
var result: FishSaleResultType = _sale_service.sell(
requested_catch_id,
_confirmation_buyer
var result: FishSaleResultType = _sale_service.sell_batch(
requested_catch_ids,
transaction_buyer
)
_close_sale_confirmation()
_transaction_feedback.text = result.get_message()
_sale_in_progress = false
if result.is_success() and _selected_catch_id == requested_catch_id:
_selected_catch_id = StringName()
if transaction_generation != _menu_generation or not visible:
return
_transaction_feedback.text = result.get_message()
if result.is_success():
_fish_selection.remove_ids(requested_catch_ids)
_refresh_all()
_inventory_tab.grab_focus()
func _close_sale_confirmation() -> void:
_confirmation_catch_id = StringName()
_confirmation_catch_ids.clear()
_confirmation_buyer = null
_confirmation_buyer_id = StringName()
_confirmation_generation = -1
_sale_confirmation.visible = false
_confirmation_message.text = ""
_confirm_sale_button.disabled = false
@ -796,25 +914,35 @@ func _close_sale_confirmation() -> void:
func _revalidate_confirmation() -> void:
if not _sale_confirmation.visible:
return
var fish_catch: FishCatchType
if _inventory != null:
fish_catch = _inventory.get_catch_by_id(_confirmation_catch_id)
if fish_catch == null:
if (
_confirmation_generation != _menu_generation
or _confirmation_catch_ids.is_empty()
or _sale_service == null
or _confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
):
_close_sale_confirmation()
_transaction_feedback.text = "Fish no longer exists."
elif fish_catch.is_favorited:
_close_sale_confirmation()
_transaction_feedback.text = "Favorited fish cannot be sold."
elif fish_catch.sale_value < 0:
_close_sale_confirmation()
_transaction_feedback.text = "Invalid sale value."
elif (
_transaction_feedback.text = "sale selection is no longer available."
return
var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids,
_confirmation_buyer
)
if (
not preview.is_success()
or (
_confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
or not _confirmation_buyer.is_valid()
)
):
_close_sale_confirmation()
_transaction_feedback.text = "Buyer is unavailable."
_transaction_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
func _get_buyer_display_group(
@ -840,9 +968,9 @@ func _refresh_logbook() -> void:
valid_species.append(fish)
_logbook_empty.visible = valid_species.is_empty()
_logbook_empty.text = (
"No fish catalog configured."
"no fish catalog configured."
if valid_species.is_empty()
else "No species discovered yet."
else "no species discovered yet."
)
if not valid_species.is_empty() and _collection_log != null:
var any_discovered: bool = false
@ -881,12 +1009,12 @@ func _create_logbook_card(fish: FishDataType) -> Control:
var owned_count: int = (
_inventory.get_count(fish.id) if _inventory != null else 0
)
details.text = "%s\nOwned: %d" % [
details.text = "%s\nowned: %d" % [
fish.get_rarity_name(),
owned_count,
]
else:
details.text = "Undiscovered"
details.text = "undiscovered"
details.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(details)
return card
@ -904,7 +1032,7 @@ func _create_texture_frame(
texture_frame.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
texture_frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
if texture == null:
texture_frame.tooltip_text = "Display texture unavailable"
texture_frame.tooltip_text = "display texture unavailable"
return texture_frame

View file

@ -56,39 +56,39 @@ layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
theme_override_font_sizes/font_size = 22
text = "Player Menu"
text = "player menu"
[node name="WalletBalance" type="Label" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
text = "Wallet: $0"
text = "wallet: $0"
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
[node name="InventoryTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Cooler"
text = "cooler"
custom_minimum_size = Vector2(88, 30)
[node name="BagTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Bag"
text = "bag"
custom_minimum_size = Vector2(76, 30)
[node name="LogbookTab" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
toggle_mode = true
text = "Logbook"
text = "logbook"
custom_minimum_size = Vector2(88, 30)
[node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"]
unique_name_in_owner = true
layout_mode = 2
text = "Close"
text = "close"
custom_minimum_size = Vector2(68, 30)
[node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"]
@ -123,7 +123,7 @@ theme_override_constants/separation = 6
[node name="SortLabel" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2
text = "Sort:"
text = "sort:"
[node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
unique_name_in_owner = true
@ -134,7 +134,7 @@ custom_minimum_size = Vector2(130, 30)
unique_name_in_owner = true
layout_mode = 2
custom_minimum_size = Vector2(112, 30)
text = "Newest first"
text = "newest first"
[node name="SortSpacer" type="Control" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
layout_mode = 2
@ -149,7 +149,7 @@ 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
text = "Held fish base value: $0"
text = "held fish base value: $0"
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
[node name="InventoryBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"]
@ -176,7 +176,7 @@ theme_override_constants/separation = 6
[node name="InventoryEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
unique_name_in_owner = true
layout_mode = 2
text = "Your Cooler is empty."
text = "your cooler is empty."
horizontal_alignment = 1
[node name="InventoryScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"]
@ -228,17 +228,25 @@ layout_mode = 2
autowrap_mode = 2
horizontal_alignment = 1
[node name="SelectionSummary" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
text = "no fish selected"
autowrap_mode = 2
horizontal_alignment = 1
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
[node name="FavoriteButton" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "Favorite"
text = "favorite"
[node name="SellButton" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
unique_name_in_owner = true
layout_mode = 2
disabled = true
text = "Sell"
text = "sell"
theme_type_variation = &"DangerButton"
[node name="SaleUnavailable" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"]
@ -262,7 +270,7 @@ theme_override_constants/separation = 7
[node name="BagHint" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection"]
layout_mode = 2
text = "Drag hotbar-compatible items onto a slot below. Right-click a slot to clear it."
text = "drag hotbar-compatible items onto a slot below. right-click a slot to clear it."
horizontal_alignment = 1
[node name="BagBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/BagSection"]
@ -289,7 +297,7 @@ theme_override_constants/separation = 6
[node name="BagEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "Your Bag is empty."
text = "your bag is empty."
horizontal_alignment = 1
[node name="BagScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"]
@ -337,7 +345,7 @@ horizontal_alignment = 1
[node name="BagDetailData" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"]
unique_name_in_owner = true
layout_mode = 2
text = "Select a Bag item for details."
text = "select a bag item for details."
horizontal_alignment = 1
autowrap_mode = 2
@ -355,7 +363,7 @@ theme_override_constants/separation = 7
[node name="LogbookEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/LogbookSection"]
unique_name_in_owner = true
layout_mode = 2
text = "No species discovered yet."
text = "no species discovered yet."
horizontal_alignment = 1
[node name="LogbookScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/LogbookSection"]
@ -412,12 +420,12 @@ alignment = 1
[node name="ConfirmSaleButton" type="Button" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"]
unique_name_in_owner = true
layout_mode = 2
text = "Confirm"
text = "confirm"
theme_type_variation = &"DangerButton"
custom_minimum_size = Vector2(120, 38)
[node name="CancelSaleButton" type="Button" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"]
unique_name_in_owner = true
layout_mode = 2
text = "Cancel"
text = "cancel"
custom_minimum_size = Vector2(120, 38)