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
parent
0e13a10b79
commit
7e3507088b
|
|
@ -20,6 +20,7 @@ var _progress_error: String = ""
|
|||
var _defs_error: String = ""
|
||||
var _accept_quest_patch: Dictionary = {}
|
||||
var _previous_status_by_quest: Dictionary = {}
|
||||
var _quest_was_active_in_session: Dictionary = {}
|
||||
var _last_reward_quest_id: String = ""
|
||||
var _last_reward_summary: Dictionary = {}
|
||||
|
||||
|
|
@ -58,6 +59,10 @@ func setup(
|
|||
_defs_client.connect(
|
||||
"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_accept_feedback_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:
|
||||
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))
|
||||
var quest_variant: Variant = result.get("quest", null)
|
||||
if quest_variant is Dictionary:
|
||||
|
|
@ -144,6 +150,17 @@ func _on_definitions_sync_failed(reason: String) -> void:
|
|||
_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:
|
||||
var boot_seed := _previous_status_by_quest.is_empty()
|
||||
var transitions: Array = []
|
||||
|
|
@ -157,8 +174,13 @@ func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void:
|
|||
if qid.is_empty():
|
||||
continue
|
||||
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"))
|
||||
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)
|
||||
if not summary.is_empty():
|
||||
transitions.append({"questId": qid, "summary": summary})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ extends GdUnitTestSuite
|
|||
const QuestHudController := preload("res://scripts/quest_hud_controller.gd")
|
||||
const QuestProgressClient := preload("res://scripts/quest_progress_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"
|
||||
|
||||
|
|
@ -545,6 +546,21 @@ func test_completion_transition_paints_reward_label_with_skill_xp() -> void:
|
|||
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:
|
||||
# Arrange
|
||||
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")
|
||||
|
||||
|
||||
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:
|
||||
# Arrange
|
||||
var controller: Node = QuestHudController.new()
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
- **`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.
|
||||
- **`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.
|
||||
|
||||
## 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:
|
||||
- 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`**.
|
||||
- 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.
|
||||
|
||||
### 3. Reward label render
|
||||
|
|
|
|||
Loading…
Reference in New Issue