NEO-131: Fix Bugbot boot-seed and item-defs reward repaint.

Track in-session active quests so first successful GET after failed
syncs can paint rewards; repaint reward label on item definitions_ready.
pull/170/head
VinPropane 2026-06-07 23:06:39 -04:00
parent 0e13a10b79
commit 7e3507088b
3 changed files with 76 additions and 3 deletions

View File

@ -20,6 +20,7 @@ var _progress_error: String = ""
var _defs_error: String = "" var _defs_error: String = ""
var _accept_quest_patch: Dictionary = {} var _accept_quest_patch: Dictionary = {}
var _previous_status_by_quest: Dictionary = {} var _previous_status_by_quest: Dictionary = {}
var _quest_was_active_in_session: Dictionary = {}
var _last_reward_quest_id: String = "" var _last_reward_quest_id: String = ""
var _last_reward_summary: Dictionary = {} var _last_reward_summary: Dictionary = {}
@ -58,6 +59,10 @@ func setup(
_defs_client.connect( _defs_client.connect(
"definitions_sync_failed", Callable(self, "_on_definitions_sync_failed") "definitions_sync_failed", Callable(self, "_on_definitions_sync_failed")
) )
if is_instance_valid(_item_defs_client) and _item_defs_client.has_signal("definitions_ready"):
_item_defs_client.connect(
"definitions_ready", Callable(self, "_on_item_definitions_ready")
)
_render_progress_label() _render_progress_label()
_render_accept_feedback_label() _render_accept_feedback_label()
_render_reward_label() _render_reward_label()
@ -109,6 +114,7 @@ func _on_sync_failed(reason: String) -> void:
func _on_accept_result_received(quest_id: String, result: Dictionary) -> void: func _on_accept_result_received(quest_id: String, result: Dictionary) -> void:
if bool(result.get("accepted", false)): if bool(result.get("accepted", false)):
_mark_quest_active_in_session(quest_id)
_render_accept_feedback_label("Quest accept: %s accepted" % _display_name(quest_id)) _render_accept_feedback_label("Quest accept: %s accepted" % _display_name(quest_id))
var quest_variant: Variant = result.get("quest", null) var quest_variant: Variant = result.get("quest", null)
if quest_variant is Dictionary: if quest_variant is Dictionary:
@ -144,6 +150,17 @@ func _on_definitions_sync_failed(reason: String) -> void:
_render_progress_label() _render_progress_label()
func _on_item_definitions_ready(_definitions_by_id: Dictionary) -> void:
if not _last_reward_quest_id.is_empty():
_render_reward_label()
func _mark_quest_active_in_session(quest_id: String) -> void:
var qid := quest_id.strip_edges()
if not qid.is_empty():
_quest_was_active_in_session[qid] = true
func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void: func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void:
var boot_seed := _previous_status_by_quest.is_empty() var boot_seed := _previous_status_by_quest.is_empty()
var transitions: Array = [] var transitions: Array = []
@ -157,8 +174,13 @@ func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void:
if qid.is_empty(): if qid.is_empty():
continue continue
var new_status := str(row.get("status", "not_started")) var new_status := str(row.get("status", "not_started"))
if new_status == "active":
_mark_quest_active_in_session(qid)
var prev_status := str(_previous_status_by_quest.get(qid, "not_started")) var prev_status := str(_previous_status_by_quest.get(qid, "not_started"))
if not boot_seed and prev_status != "completed" and new_status == "completed": var can_detect := (
not boot_seed or bool(_quest_was_active_in_session.get(qid, false))
)
if can_detect and prev_status != "completed" and new_status == "completed":
var summary := QuestProgressClient.completion_reward_summary(qid, snapshot) var summary := QuestProgressClient.completion_reward_summary(qid, snapshot)
if not summary.is_empty(): if not summary.is_empty():
transitions.append({"questId": qid, "summary": summary}) transitions.append({"questId": qid, "summary": summary})

View File

@ -5,6 +5,7 @@ extends GdUnitTestSuite
const QuestHudController := preload("res://scripts/quest_hud_controller.gd") const QuestHudController := preload("res://scripts/quest_hud_controller.gd")
const QuestProgressClient := preload("res://scripts/quest_progress_client.gd") const QuestProgressClient := preload("res://scripts/quest_progress_client.gd")
const QuestDefinitionsClient := preload("res://scripts/quest_definitions_client.gd") const QuestDefinitionsClient := preload("res://scripts/quest_definitions_client.gd")
const ItemDefsClient := preload("res://scripts/item_definitions_client.gd")
const OPERATOR_CHAIN_QUEST_ID := "prototype_quest_operator_chain" const OPERATOR_CHAIN_QUEST_ID := "prototype_quest_operator_chain"
@ -545,6 +546,21 @@ func test_completion_transition_paints_reward_label_with_skill_xp() -> void:
assert_str(reward_label.text).contains("Salvage +25 XP") assert_str(reward_label.text).contains("Salvage +25 XP")
func test_first_success_paints_reward_when_quest_was_active_in_session() -> void:
# Arrange
var controller: Node = QuestHudController.new()
var reward_label := Label.new()
auto_free(controller)
auto_free(reward_label)
add_child(controller)
controller.set("_reward_label", reward_label)
controller.call("_mark_quest_active_in_session", QuestHudController.GATHER_QUEST_ID)
# Act — first successful GET already completed (missed intermediate snapshots)
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
# Assert
assert_str(reward_label.text).contains("Salvage +25 XP")
func test_boot_completed_snapshot_does_not_paint_reward_label() -> void: func test_boot_completed_snapshot_does_not_paint_reward_label() -> void:
# Arrange # Arrange
var controller: Node = QuestHudController.new() var controller: Node = QuestHudController.new()
@ -641,6 +657,41 @@ func test_definitions_ready_repaints_reward_label_with_display_name() -> void:
assert_str(reward_label.text).not_contains("prototype_quest_gather_intro") assert_str(reward_label.text).not_contains("prototype_quest_gather_intro")
func _survey_drone_item_defs_json() -> String:
return (
'{"schemaVersion":1,"items":[{"id":"survey_drone_kit",'
+ '"displayName":"Survey Drone Kit","inventorySlotKind":"bag"}]}'
)
func test_item_definitions_ready_repaints_reward_item_display_name() -> void:
# Arrange
var item_transport := MockHttpTransport.new()
item_transport.body_json = _survey_drone_item_defs_json()
var item_defs: Node = ItemDefsClient.new()
item_defs.set("injected_http", item_transport)
auto_free(item_transport)
auto_free(item_defs)
add_child(item_defs)
await get_tree().process_frame
var controller: Node = QuestHudController.new()
var reward_label := Label.new()
auto_free(controller)
auto_free(reward_label)
add_child(controller)
controller.set("_reward_label", reward_label)
controller.set("_item_defs_client", item_defs)
controller.call("_on_progress_received", _active_operator_chain_snapshot())
controller.call("_on_progress_received", _completed_operator_chain_with_summary_snapshot())
assert_str(reward_label.text).contains("survey_drone_kit")
# Act
item_defs.call("request_sync_from_server")
controller.call("_on_item_definitions_ready", {})
# Assert
assert_str(reward_label.text).contains("Survey Drone Kit ×1")
assert_str(reward_label.text).not_contains("survey_drone_kit ×1")
func test_sync_failure_shows_progress_error_and_idle_reward_label() -> void: func test_sync_failure_shows_progress_error_and_idle_reward_label() -> void:
# Arrange # Arrange
var controller: Node = QuestHudController.new() var controller: Node = QuestHudController.new()

View File

@ -61,7 +61,7 @@
- **`quest_progress_client.gd`:** **`completion_reward_summary`** static helper; parse preserves NEO-129 nested summary on completed rows. - **`quest_progress_client.gd`:** **`completion_reward_summary`** static helper; parse preserves NEO-129 nested summary on completed rows.
- **`quest_hud_controller.gd`:** transition-only detection (**`_previous_status_by_quest`**); **`QuestRewardDeliveryLabel`** render with item-def display names + title-case skill XP lines; sync error idle state. - **`quest_hud_controller.gd`:** transition-only detection (**`_previous_status_by_quest`**); **`QuestRewardDeliveryLabel`** render with item-def display names + title-case skill XP lines; sync error idle state.
- **`main.gd` / `main.tscn`:** **`QuestRewardDeliveryLabel`** after accept feedback; **`ItemDefinitionsClient`** threaded into controller **`setup`**. - **`main.gd` / `main.tscn`:** **`QuestRewardDeliveryLabel`** after accept feedback; **`ItemDefinitionsClient`** threaded into controller **`setup`**.
- **Tests:** `quest_progress_client_test.gd` (summary parse); `quest_hud_controller_test.gd` (transition, boot idempotency, sync error, item grant display name, `definitions_ready` reward repaint). - **Tests:** `quest_progress_client_test.gd` (summary parse); `quest_hud_controller_test.gd` (transition, boot idempotency, first-success-after-active, sync error, item grant display name, quest/item `definitions_ready` reward repaint).
- **Docs:** `client/README.md`; `docs/manual-qa/NEO-131.md`; E7.M2 module + backlog alignment. - **Docs:** `client/README.md`; `docs/manual-qa/NEO-131.md`; E7.M2 module + backlog alignment.
## Technical approach ## Technical approach
@ -95,7 +95,7 @@ Operator chain adds item line: **`survey_drone_kit` ×1** + **`salvage` 50** XP.
- On **`_on_progress_received`**, before overwriting snapshot: - On **`_on_progress_received`**, before overwriting snapshot:
- For each quest row in new snapshot, compare **`status`** to previous. - For each quest row in new snapshot, compare **`status`** to previous.
- When previous ∉ **`{completed}`** and new **`status == "completed"`** with non-empty **`completionRewardSummary`**, set **`_last_reward_quest_id`** + **`_last_reward_summary`**. - When previous ∉ **`{completed}`** and new **`status == "completed"`** with non-empty **`completionRewardSummary`**, set **`_last_reward_quest_id`** + **`_last_reward_summary`**.
- On boot first snapshot: seed **`_previous_status_by_quest`** without firing transition (no “replay” grants). - On boot first snapshot: seed **`_previous_status_by_quest`** without firing transition unless the quest was **`active` in-session** (accept or prior active row) — covers failed GET then first success already **`completed`**; Godot restart idempotency unchanged.
- On sync failure: clear **`_last_reward_summary`** paint to **`—`**; preserve NEO-122 progress error lines. - On sync failure: clear **`_last_reward_summary`** paint to **`—`**; preserve NEO-122 progress error lines.
### 3. Reward label render ### 3. Reward label render