From 0cc3ae970911fbbe4ff5dedefbe11d5a7a45d829 Mon Sep 17 00:00:00 2001 From: Voyager Date: Tue, 25 Aug 2026 02:05:52 -0400 Subject: [PATCH] add bulk staging to shop sales --- tests/economy_regression_validation.gd | 25 ++++-- ui/shop_sell_inventory.gd | 102 ++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index de0133b..ec80d1b 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -971,12 +971,21 @@ func _test_fishing_shop_sale_ui( reserved_catch.catch_id, ) assert(staged.is_empty()) - sell_inventory.call( - "_stage", - PlayerInventoryLayout.EntryKind.CATCH, - fish_catch.catch_id, + var sell_button := sell_inventory.get("_sell_button") as Button + var all_button := sell_inventory.get("_all_button") as Button + assert(sell_button != null and sell_button.text == "sell") + assert(all_button != null and all_button.text == "all") + assert(sell_button.get_parent() == all_button.get_parent()) + assert( + sell_button.size_flags_horizontal == Control.SIZE_EXPAND_FILL + and all_button.size_flags_horizontal == Control.SIZE_EXPAND_FILL ) + sell_inventory.call("_stage_all_eligible") assert(staged.has(PlayerInventoryLayout.catch_key(fish_catch.catch_id))) + assert(not staged.has( + PlayerInventoryLayout.catch_key(reserved_catch.catch_id) + )) + assert(all_button.disabled) assert((sell_inventory.get("_feedback") as Label).text.is_empty()) var source_grid := sell_inventory.get("_inventory_grid") as GeneralInventoryGrid var staged_source_found := false @@ -985,17 +994,19 @@ func _test_fishing_shop_sale_ui( staged_source_found = bool(source_slot.get("_staged")) break assert(staged_source_found) + var expected_all_payout := int(sell_inventory.call("_calculate_total")) + assert(expected_all_payout >= fish_catch.sale_value) var feedback := sell_inventory.get("_feedback") as Label var total_label := sell_inventory.get("_total_label") as Label - var sell_button := sell_inventory.get("_sell_button") as Button + var sale_buttons := sell_button.get_parent() as HBoxContainer assert(feedback.get_index() < total_label.get_parent().get_index()) - assert(total_label.get_parent().get_index() < sell_button.get_index()) + assert(total_label.get_parent().get_index() < sale_buttons.get_index()) _sale_result.clear() sell_inventory.call("_submit_sale") await process_frame assert(not _sale_result.is_empty() and bool(_sale_result[1])) assert(not player.inventory.contains_catch_id(fish_catch.catch_id)) - assert(player.wallet.get_balance() == balance_before + fish_catch.sale_value) + assert(player.wallet.get_balance() == balance_before + expected_all_payout) assert(not sale_service.is_local_sale_pending()) assert(reservations.release(reservation_id)) await _activate_pointer_control(shop_tabs[0] as Button, ui_viewport) diff --git a/ui/shop_sell_inventory.gd b/ui/shop_sell_inventory.gd index 9e39850..f46bc6d 100644 --- a/ui/shop_sell_inventory.gd +++ b/ui/shop_sell_inventory.gd @@ -15,6 +15,7 @@ var _tray_grid: GridContainer var _total_label: Label var _feedback: Label var _sell_button: Button +var _all_button: Button var _staged: Dictionary[String, Dictionary] = {} @@ -135,6 +136,67 @@ func _on_remove_requested(key: String) -> void: _refresh_tray() +func _stage_all_eligible() -> void: + if _layout == null: + return + var added_count := 0 + for entry: Dictionary in _layout.get_entries( + PlayerInventoryLayout.InventoryContainer.INVENTORY + ): + var kind := int(entry.get("kind", -1)) + var identity := StringName(str(entry.get("identity", ""))) + if identity.is_empty(): + continue + var key := ( + PlayerInventoryLayout.catch_key(identity) + if kind == PlayerInventoryLayout.EntryKind.CATCH + else PlayerInventoryLayout.item_key(identity) + ) + if _staged.has(key): + continue + var record := _eligible_staged_record(kind, identity) + if record.is_empty(): + continue + _staged[key] = record + added_count += 1 + _feedback.text = ( + "" + if added_count > 0 + else "no additional items are eligible for sale" + ) + _refresh_tray() + + +func _eligible_staged_record(kind: int, identity: StringName) -> Dictionary: + if kind == PlayerInventoryLayout.EntryKind.CATCH: + var fish_catch := _fish_inventory.get_catch_by_id(identity) + if ( + fish_catch == null + or fish_catch.is_favorited + or ( + _reservations != null + and _reservations.is_fish_reserved(identity) + ) + ): + return {} + return {"kind": kind, "identity": identity, "quantity": 1} + if kind != PlayerInventoryLayout.EntryKind.ITEM: + return {} + var item := _item_catalog.get_item_by_id(identity) + if not ItemResalePolicy.is_sellable(item): + return {} + var available := _bag.get_quantity(identity) + if _reservations != null: + available = _reservations.get_available_item_quantity(identity) + if available < 1: + return {} + return { + "kind": kind, + "identity": identity, + "quantity": available, + } + + func _submit_sale() -> void: if _staged.is_empty() or _network_sale == null: return @@ -154,6 +216,7 @@ func _submit_sale() -> void: func _on_sale_pending(_request_id: String) -> void: _sell_button.disabled = true + _all_button.disabled = true _feedback.text = "selling…" @@ -243,6 +306,10 @@ func _refresh_tray() -> void: or _network_sale == null or _network_sale.is_local_sale_pending() ) + _all_button.disabled = ( + _network_sale != null + and _network_sale.is_local_sale_pending() + ) or not _has_unstaged_eligible_entries() call_deferred("_configure_focus") @@ -268,9 +335,32 @@ func _configure_focus() -> void: if control != null and control.focus_mode != Control.FOCUS_NONE: controls.append(control) controls.append(_sell_button) + controls.append(_all_button) ControllerFocusNavigation.configure_spatial_neighbors(controls) +func _has_unstaged_eligible_entries() -> bool: + if _layout == null: + return false + for entry: Dictionary in _layout.get_entries( + PlayerInventoryLayout.InventoryContainer.INVENTORY + ): + var kind := int(entry.get("kind", -1)) + var identity := StringName(str(entry.get("identity", ""))) + var key := ( + PlayerInventoryLayout.catch_key(identity) + if kind == PlayerInventoryLayout.EntryKind.CATCH + else PlayerInventoryLayout.item_key(identity) + ) + if ( + not identity.is_empty() + and not _staged.has(key) + and not _eligible_staged_record(kind, identity).is_empty() + ): + return true + return false + + func _calculate_total() -> int: var total: int = 0 for record: Dictionary in _staged.values(): @@ -347,9 +437,19 @@ func _build_ui() -> void: total_row.add_child(_total_label) _sell_button = Button.new() _sell_button.text = "sell" + _sell_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL UtilityPageStyle.apply_ocean_button(_sell_button) _sell_button.pressed.connect(_submit_sale) - (tray as VBoxContainer).add_child(_sell_button) + var sale_buttons := HBoxContainer.new() + sale_buttons.add_theme_constant_override("separation", 8) + (tray as VBoxContainer).add_child(sale_buttons) + sale_buttons.add_child(_sell_button) + _all_button = Button.new() + _all_button.text = "all" + _all_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(_all_button) + _all_button.pressed.connect(_stage_all_eligible) + sale_buttons.add_child(_all_button) func _build_panel(parent: HBoxContainer, title_text: String) -> VBoxContainer: