diff --git a/client/scripts/main.gd b/client/scripts/main.gd index ccdc12a..7ae5185 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -82,9 +82,9 @@ var _inventory_error: String = "" var _interactables_catalog: Array = [] var _last_progression_snapshot: Dictionary = {} var _progression_error: String = "" -var _pending_gather_feedback: bool = false -var _pending_gather_interactable_id: String = "" var _gather_pre_scrap_qty: int = 0 +var _gather_pending_interactable_id: String = "" +var _gather_awaiting_inventory_finalize: bool = false @onready var _camera: Camera3D = $World/IsometricFollowCamera/Camera3D @onready var _world: Node3D = $World @@ -375,8 +375,8 @@ func _on_inventory_sync_failed(reason: String) -> void: _inventory_error = reason _last_inventory_snapshot = {} _render_inventory_label() - if _pending_gather_feedback: - _clear_pending_gather_feedback() + if _gather_awaiting_inventory_finalize: + _clear_gather_session() _render_gather_feedback_label("Gather: inventory refresh failed") @@ -528,27 +528,33 @@ func _render_gather_feedback_label(text: String = "Gather: —") -> void: _gather_feedback_label.text = text -func _clear_pending_gather_feedback() -> void: - _pending_gather_feedback = false - _pending_gather_interactable_id = "" +func _clear_gather_session() -> void: + _gather_pending_interactable_id = "" + _gather_awaiting_inventory_finalize = false func _on_interaction_result_for_gather( interactable_id: String, allowed: bool, reason_code: String ) -> void: - if _pending_gather_feedback and interactable_id != _pending_gather_interactable_id: - _clear_pending_gather_feedback() + if _gather_pending_interactable_id.is_empty(): + return + if interactable_id != _gather_pending_interactable_id: + if _gather_awaiting_inventory_finalize: + return + _clear_gather_session() return if not PrototypeInteractablePicker.is_resource_node_id(_interactables_catalog, interactable_id): + _clear_gather_session() return if not allowed: - _clear_pending_gather_feedback() + _clear_gather_session() var rc := reason_code.strip_edges() if rc.is_empty(): _render_gather_feedback_label("Gather: denied (no reasonCode)") else: _render_gather_feedback_label("Gather: denied — %s" % rc) return + _gather_awaiting_inventory_finalize = true _request_inventory_refresh() if ( is_instance_valid(_skill_progression_client) @@ -558,12 +564,14 @@ func _on_interaction_result_for_gather( func _on_interaction_request_failed_for_gather(interactable_id: String, _reason: String) -> void: - if not _pending_gather_feedback: + if _gather_pending_interactable_id.is_empty(): return - if interactable_id != _pending_gather_interactable_id: - _clear_pending_gather_feedback() + if interactable_id != _gather_pending_interactable_id: + if not _gather_awaiting_inventory_finalize: + _clear_gather_session() return - _clear_pending_gather_feedback() + if not _gather_awaiting_inventory_finalize: + _clear_gather_session() func _total_bag_qty(item_id: String, snapshot: Dictionary = {}) -> int: @@ -585,10 +593,10 @@ func _total_bag_qty(item_id: String, snapshot: Dictionary = {}) -> int: func _finalize_pending_gather_feedback() -> void: - if not _pending_gather_feedback: + if not _gather_awaiting_inventory_finalize: return var pre_qty: int = _gather_pre_scrap_qty - _clear_pending_gather_feedback() + _clear_gather_session() var new_qty: int = _total_bag_qty(SCRAP_METAL_BULK_ID) var delta: int = new_qty - pre_qty if delta <= 0: @@ -837,8 +845,8 @@ func _forward_interact_resource_nearest() -> void: _interactables_catalog, _player.global_position ) _gather_pre_scrap_qty = _total_bag_qty(SCRAP_METAL_BULK_ID) - _pending_gather_interactable_id = node_id - _pending_gather_feedback = true + _gather_pending_interactable_id = node_id + _gather_awaiting_inventory_finalize = false if _interaction_client.has_method("post_interact_id"): _interaction_client.call("post_interact_id", node_id) diff --git a/client/test/gather_feedback_refresh_test.gd b/client/test/gather_feedback_refresh_test.gd index a9172a8..87768ac 100644 --- a/client/test/gather_feedback_refresh_test.gd +++ b/client/test/gather_feedback_refresh_test.gd @@ -43,8 +43,8 @@ class GatherRefreshHarness: extends Node var inventory_sync_calls: int = 0 var skill_sync_calls: int = 0 - var pending_gather: bool = false var pending_gather_interactable_id: String = "" + var awaiting_inventory_finalize: bool = false var _inventory: SpySyncClient var _skill: SpySyncClient var _ix: Node @@ -61,23 +61,29 @@ class GatherRefreshHarness: func queue_resource_gather(interactable_id: String) -> void: pending_gather_interactable_id = interactable_id - pending_gather = true + awaiting_inventory_finalize = false - func _clear_pending_gather() -> void: - pending_gather = false + func _clear_gather_session() -> void: pending_gather_interactable_id = "" + awaiting_inventory_finalize = false func _on_interaction_result( interactable_id: String, allowed: bool, _reason_code: String ) -> void: - if pending_gather and interactable_id != pending_gather_interactable_id: - _clear_pending_gather() + if pending_gather_interactable_id.is_empty(): + return + if interactable_id != pending_gather_interactable_id: + if awaiting_inventory_finalize: + return + _clear_gather_session() return if not Picker.is_resource_node_id([], interactable_id): + _clear_gather_session() return if not allowed: - _clear_pending_gather() + _clear_gather_session() return + awaiting_inventory_finalize = true _inventory.request_sync_from_server() _skill.request_sync_from_server() @@ -136,6 +142,25 @@ func test_terminal_result_clears_orphaned_gather_pending() -> void: # Act harness.emit_terminal_allowed() # Assert - assert_that(harness.pending_gather).is_false() + assert_that(harness.pending_gather_interactable_id).is_empty() + assert_that(harness.awaiting_inventory_finalize).is_false() assert_that(harness._inventory.sync_calls).is_equal(0) assert_that(harness._skill.sync_calls).is_equal(0) + + +func test_terminal_after_gather_success_keeps_awaiting_inventory() -> void: + # Arrange + var transport := MockInteractTransport.new() + var harness := GatherRefreshHarness.new() + auto_free(transport) + auto_free(harness) + add_child(harness) + harness.setup(self, transport) + # Act + harness.post_resource_alpha() + harness.emit_terminal_allowed() + # Assert + assert_that(harness.awaiting_inventory_finalize).is_true() + assert_that(harness.pending_gather_interactable_id).is_equal("prototype_resource_node_alpha") + assert_that(harness._inventory.sync_calls).is_equal(1) + assert_that(harness._skill.sync_calls).is_equal(1) diff --git a/docs/reviews/2026-05-24-NEO-73.md b/docs/reviews/2026-05-24-NEO-73.md index 9994ec4..5fd77e1 100644 --- a/docs/reviews/2026-05-24-NEO-73.md +++ b/docs/reviews/2026-05-24-NEO-73.md @@ -44,7 +44,8 @@ The branch adds **`skill_progression_client.gd`**, **`prototype_interactable_pic - ~~Nit: **`Gather: ok — inventory refresh failed`** (`main.gd` **`_on_inventory_sync_failed`**) reads as success; consider **`Gather: inventory refresh failed`** for clarity.~~ **Done.** - Nit: Rapid **R** while a gather POST is in flight overwrites **`_gather_pre_scrap_qty`**; delta can be wrong if two responses complete out of order. Prototype-acceptable; last-wins POST coalescing already exists on the interaction client. -- ~~**Bugbot:** Gather feedback flag orphaned when interaction coalesced (**E** overwrites pending **R** POST) — stale **`_pending_gather_feedback`** could paint spurious delta on manual **I**.~~ **Done.** Track **`_pending_gather_interactable_id`**; clear pending when a different interactable completes or fails. +- ~~**Bugbot:** Gather feedback flag orphaned when interaction coalesced (**E** overwrites pending **R** POST) — stale **`_pending_gather_feedback`** could paint spurious delta on manual **I**.~~ **Done.** Track **`_gather_pending_interactable_id`**; clear session only when gather never completed. +- ~~**Bugbot:** Coalesced POST clears gather feedback before inventory arrives — terminal result after successful **R** cleared pending before inventory GET returned, dropping **`GatherFeedbackLabel`** delta.~~ **Done.** **`_gather_awaiting_inventory_finalize`** preserves session until inventory sync paints delta. - Nit: **`KNOWN_RESOURCE_NODE_IDS`** in **`prototype_interactable_picker.gd`** duplicates the frozen four-node table — intentional catalog-empty fallback; keep in sync with **`E3_M1`** if ids ever change (they are frozen). ## Verification