NEO-122: Keep quest HUD fresh after accept.
Merge accept-response quest row into the HUD snapshot immediately, re-apply when a stale GET returns not_started, and queue pending sync when progress GET is already in flight.pull/161/head
parent
bfded69b3b
commit
91bdb18357
|
|
@ -2,6 +2,7 @@ extends Node
|
||||||
|
|
||||||
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
|
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
|
||||||
|
|
||||||
|
const QuestProgressClient := preload("res://scripts/quest_progress_client.gd")
|
||||||
const GATHER_QUEST_ID := "prototype_quest_gather_intro"
|
const GATHER_QUEST_ID := "prototype_quest_gather_intro"
|
||||||
const ACCEPT_IDLE_HINT := "Quest accept: — (Q gather / Shift+Q next)"
|
const ACCEPT_IDLE_HINT := "Quest accept: — (Q gather / Shift+Q next)"
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ var _accept_label: Label = null
|
||||||
var _last_snapshot: Dictionary = {}
|
var _last_snapshot: Dictionary = {}
|
||||||
var _progress_error: String = ""
|
var _progress_error: String = ""
|
||||||
var _defs_error: String = ""
|
var _defs_error: String = ""
|
||||||
|
var _accept_quest_patch: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -76,18 +78,27 @@ func try_accept_key_input(event: InputEvent) -> bool:
|
||||||
func _on_progress_received(snapshot: Dictionary) -> void:
|
func _on_progress_received(snapshot: Dictionary) -> void:
|
||||||
_progress_error = ""
|
_progress_error = ""
|
||||||
_last_snapshot = snapshot.duplicate(true)
|
_last_snapshot = snapshot.duplicate(true)
|
||||||
|
_reapply_accept_quest_patch_if_needed()
|
||||||
_render_progress_label()
|
_render_progress_label()
|
||||||
|
|
||||||
|
|
||||||
func _on_sync_failed(reason: String) -> void:
|
func _on_sync_failed(reason: String) -> void:
|
||||||
_progress_error = reason
|
_progress_error = reason
|
||||||
_last_snapshot = {}
|
_last_snapshot = {}
|
||||||
|
_accept_quest_patch = {}
|
||||||
_render_progress_label()
|
_render_progress_label()
|
||||||
|
|
||||||
|
|
||||||
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)):
|
||||||
_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)
|
||||||
|
if quest_variant is Dictionary:
|
||||||
|
_accept_quest_patch = (quest_variant as Dictionary).duplicate(true)
|
||||||
|
_last_snapshot = QuestProgressClient.merge_quest_row_into_snapshot(
|
||||||
|
_last_snapshot, _accept_quest_patch
|
||||||
|
)
|
||||||
|
_render_progress_label()
|
||||||
else:
|
else:
|
||||||
var rc := str(result.get("reasonCode", "")).strip_edges()
|
var rc := str(result.get("reasonCode", "")).strip_edges()
|
||||||
if rc.is_empty():
|
if rc.is_empty():
|
||||||
|
|
@ -200,6 +211,21 @@ func _quest_row(quest_id: String) -> Dictionary:
|
||||||
return row as Dictionary
|
return row as Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
func _reapply_accept_quest_patch_if_needed() -> void:
|
||||||
|
if _accept_quest_patch.is_empty():
|
||||||
|
return
|
||||||
|
var patch_id := str(_accept_quest_patch.get("questId", "")).strip_edges()
|
||||||
|
if patch_id.is_empty():
|
||||||
|
return
|
||||||
|
var row: Dictionary = _quest_row(patch_id)
|
||||||
|
if str(row.get("status", "not_started")) == "not_started":
|
||||||
|
_last_snapshot = QuestProgressClient.merge_quest_row_into_snapshot(
|
||||||
|
_last_snapshot, _accept_quest_patch
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_accept_quest_patch = {}
|
||||||
|
|
||||||
|
|
||||||
func _display_name(quest_id: String) -> String:
|
func _display_name(quest_id: String) -> String:
|
||||||
if not is_instance_valid(_defs_client):
|
if not is_instance_valid(_defs_client):
|
||||||
return quest_id
|
return quest_id
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ const SCHEMA_VERSION := 1
|
||||||
var _sync_http: Node
|
var _sync_http: Node
|
||||||
var _accept_http: Node
|
var _accept_http: Node
|
||||||
var _sync_busy: bool = false
|
var _sync_busy: bool = false
|
||||||
|
var _sync_pending: bool = false
|
||||||
var _accept_busy: bool = false
|
var _accept_busy: bool = false
|
||||||
var _current_accept_quest_id: String = ""
|
var _current_accept_quest_id: String = ""
|
||||||
|
|
||||||
|
|
@ -36,7 +37,12 @@ func is_accept_busy() -> bool:
|
||||||
|
|
||||||
func request_sync_from_server() -> void:
|
func request_sync_from_server() -> void:
|
||||||
if _sync_busy:
|
if _sync_busy:
|
||||||
|
_sync_pending = true
|
||||||
return
|
return
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
|
func _start_sync_request() -> void:
|
||||||
_sync_busy = true
|
_sync_busy = true
|
||||||
var url := "%s/game/players/%s/quest-progress" % [_base_root(), _player_path_segment()]
|
var url := "%s/game/players/%s/quest-progress" % [_base_root(), _player_path_segment()]
|
||||||
var err: Error = _sync_http.request(url)
|
var err: Error = _sync_http.request(url)
|
||||||
|
|
@ -45,6 +51,7 @@ func request_sync_from_server() -> void:
|
||||||
push_warning("QuestProgressClient: %s" % reason)
|
push_warning("QuestProgressClient: %s" % reason)
|
||||||
_sync_busy = false
|
_sync_busy = false
|
||||||
quest_sync_failed.emit(reason)
|
quest_sync_failed.emit(reason)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
||||||
|
|
||||||
## Returns [code]true[/code] when the HTTP POST was queued.
|
## Returns [code]true[/code] when the HTTP POST was queued.
|
||||||
|
|
@ -127,6 +134,40 @@ static func parse_quest_accept_json(text: String) -> Variant:
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
static func merge_quest_row_into_snapshot(snapshot: Dictionary, row: Dictionary) -> Dictionary:
|
||||||
|
var qid := str(row.get("questId", "")).strip_edges()
|
||||||
|
if qid.is_empty():
|
||||||
|
return snapshot.duplicate(true) if not snapshot.is_empty() else {}
|
||||||
|
var merged: Dictionary
|
||||||
|
if snapshot.is_empty():
|
||||||
|
merged = {"schemaVersion": SCHEMA_VERSION, "playerId": "", "quests": []}
|
||||||
|
else:
|
||||||
|
merged = snapshot.duplicate(true)
|
||||||
|
var quests: Array = []
|
||||||
|
var quests_variant: Variant = merged.get("quests", null)
|
||||||
|
if quests_variant is Array:
|
||||||
|
for row_variant in quests_variant as Array:
|
||||||
|
quests.append(row_variant)
|
||||||
|
var replaced := false
|
||||||
|
for i in quests.size():
|
||||||
|
var existing: Variant = quests[i]
|
||||||
|
if existing is Dictionary and str((existing as Dictionary).get("questId", "")) == qid:
|
||||||
|
quests[i] = row.duplicate(true)
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
if not replaced:
|
||||||
|
quests.append(row.duplicate(true))
|
||||||
|
merged["quests"] = quests
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
func _try_flush_pending_sync() -> void:
|
||||||
|
if not _sync_pending or _sync_busy:
|
||||||
|
return
|
||||||
|
_sync_pending = false
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
func _on_sync_request_completed(
|
func _on_sync_request_completed(
|
||||||
result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
|
result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|
@ -135,16 +176,19 @@ func _on_sync_request_completed(
|
||||||
var reason := "HTTP failed (result=%s)" % result
|
var reason := "HTTP failed (result=%s)" % result
|
||||||
push_warning("QuestProgressClient: %s" % reason)
|
push_warning("QuestProgressClient: %s" % reason)
|
||||||
quest_sync_failed.emit(reason)
|
quest_sync_failed.emit(reason)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
if response_code == 404:
|
if response_code == 404:
|
||||||
var reason404 := "HTTP 404 (player unknown)"
|
var reason404 := "HTTP 404 (player unknown)"
|
||||||
push_warning("QuestProgressClient: %s" % reason404)
|
push_warning("QuestProgressClient: %s" % reason404)
|
||||||
quest_sync_failed.emit(reason404)
|
quest_sync_failed.emit(reason404)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
if response_code < 200 or response_code >= 300:
|
if response_code < 200 or response_code >= 300:
|
||||||
var reason_code := "HTTP %s" % response_code
|
var reason_code := "HTTP %s" % response_code
|
||||||
push_warning("QuestProgressClient: %s" % reason_code)
|
push_warning("QuestProgressClient: %s" % reason_code)
|
||||||
quest_sync_failed.emit(reason_code)
|
quest_sync_failed.emit(reason_code)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
var text := body.get_string_from_utf8()
|
var text := body.get_string_from_utf8()
|
||||||
var snapshot: Variant = parse_quest_progress_json(text)
|
var snapshot: Variant = parse_quest_progress_json(text)
|
||||||
|
|
@ -152,8 +196,10 @@ func _on_sync_request_completed(
|
||||||
var reason_json := "non-JSON body or schemaVersion mismatch"
|
var reason_json := "non-JSON body or schemaVersion mismatch"
|
||||||
push_warning("QuestProgressClient: %s" % reason_json)
|
push_warning("QuestProgressClient: %s" % reason_json)
|
||||||
quest_sync_failed.emit(reason_json)
|
quest_sync_failed.emit(reason_json)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
quest_progress_received.emit(snapshot as Dictionary)
|
quest_progress_received.emit(snapshot as Dictionary)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
||||||
|
|
||||||
func _on_accept_request_completed(
|
func _on_accept_request_completed(
|
||||||
|
|
|
||||||
|
|
@ -194,3 +194,83 @@ func test_request_accept_shows_busy_feedback() -> void:
|
||||||
# Assert
|
# Assert
|
||||||
assert_bool(started).is_false()
|
assert_bool(started).is_false()
|
||||||
assert_str(accept_label.text).contains("busy")
|
assert_str(accept_label.text).contains("busy")
|
||||||
|
|
||||||
|
|
||||||
|
func _not_started_snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"playerId": "dev-local-1",
|
||||||
|
"quests":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"questId": QuestHudController.GATHER_QUEST_ID,
|
||||||
|
"status": "not_started",
|
||||||
|
"currentStepIndex": 0,
|
||||||
|
"objectiveCounters": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func test_accept_success_merges_quest_row_into_progress_label() -> void:
|
||||||
|
# Arrange
|
||||||
|
var progress_client: Node = _make_progress_client(NoopHttpTransport.new())
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
var progress_label := Label.new()
|
||||||
|
auto_free(controller)
|
||||||
|
auto_free(progress_label)
|
||||||
|
add_child(controller)
|
||||||
|
await get_tree().process_frame
|
||||||
|
controller.set("_progress_client", progress_client)
|
||||||
|
controller.set("_progress_label", progress_label)
|
||||||
|
controller.set("_last_snapshot", _not_started_snapshot())
|
||||||
|
# Act
|
||||||
|
(
|
||||||
|
controller
|
||||||
|
. call(
|
||||||
|
"_on_accept_result_received",
|
||||||
|
QuestHudController.GATHER_QUEST_ID,
|
||||||
|
{
|
||||||
|
"accepted": true,
|
||||||
|
"quest":
|
||||||
|
{
|
||||||
|
"questId": QuestHudController.GATHER_QUEST_ID,
|
||||||
|
"status": "active",
|
||||||
|
"currentStepIndex": 0,
|
||||||
|
"objectiveCounters": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Assert
|
||||||
|
assert_str(progress_label.text).contains("active step")
|
||||||
|
|
||||||
|
|
||||||
|
func test_stale_sync_reapplies_accept_quest_patch() -> void:
|
||||||
|
# Arrange
|
||||||
|
var progress_client: Node = _make_progress_client(NoopHttpTransport.new())
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
var progress_label := Label.new()
|
||||||
|
auto_free(controller)
|
||||||
|
auto_free(progress_label)
|
||||||
|
add_child(controller)
|
||||||
|
await get_tree().process_frame
|
||||||
|
controller.set("_progress_client", progress_client)
|
||||||
|
controller.set("_progress_label", progress_label)
|
||||||
|
controller.set("_last_snapshot", _not_started_snapshot())
|
||||||
|
(
|
||||||
|
controller
|
||||||
|
. set(
|
||||||
|
"_accept_quest_patch",
|
||||||
|
{
|
||||||
|
"questId": QuestHudController.GATHER_QUEST_ID,
|
||||||
|
"status": "active",
|
||||||
|
"currentStepIndex": 0,
|
||||||
|
"objectiveCounters": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Act
|
||||||
|
controller.call("_on_progress_received", _not_started_snapshot())
|
||||||
|
# Assert
|
||||||
|
assert_str(progress_label.text).contains("active step")
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,33 @@ class PendingAcceptHttpTransport:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PendingSyncHttpTransport:
|
||||||
|
extends Node
|
||||||
|
signal request_completed(
|
||||||
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||||||
|
)
|
||||||
|
var request_count: int = 0
|
||||||
|
var response_code: int = 200
|
||||||
|
var body_json: String = ""
|
||||||
|
|
||||||
|
func request(
|
||||||
|
_url: String,
|
||||||
|
_custom_headers: PackedStringArray = PackedStringArray(),
|
||||||
|
_method: HTTPClient.Method = HTTPClient.METHOD_GET,
|
||||||
|
_request_data: String = ""
|
||||||
|
) -> Error:
|
||||||
|
request_count += 1
|
||||||
|
return OK
|
||||||
|
|
||||||
|
func complete_pending() -> void:
|
||||||
|
request_completed.emit(
|
||||||
|
HTTPRequest.RESULT_SUCCESS,
|
||||||
|
response_code,
|
||||||
|
PackedStringArray(),
|
||||||
|
body_json.to_utf8_buffer()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func test_parse_not_started_quest_row() -> void:
|
func test_parse_not_started_quest_row() -> void:
|
||||||
# Arrange
|
# Arrange
|
||||||
var json := _not_started_progress_json()
|
var json := _not_started_progress_json()
|
||||||
|
|
@ -287,3 +314,48 @@ func test_request_accept_returns_false_when_accept_busy() -> void:
|
||||||
assert_that(first).is_true()
|
assert_that(first).is_true()
|
||||||
assert_that(second).is_false()
|
assert_that(second).is_false()
|
||||||
pending.complete_pending()
|
pending.complete_pending()
|
||||||
|
|
||||||
|
|
||||||
|
func test_merge_quest_row_into_snapshot_replaces_existing_row() -> void:
|
||||||
|
# Arrange
|
||||||
|
var snapshot: Dictionary = {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"playerId": "dev-local-1",
|
||||||
|
"quests":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"questId": PROTOTYPE_QUEST_GATHER_ID,
|
||||||
|
"status": "not_started",
|
||||||
|
"currentStepIndex": 0,
|
||||||
|
"objectiveCounters": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
var active_row: Dictionary = {
|
||||||
|
"questId": PROTOTYPE_QUEST_GATHER_ID,
|
||||||
|
"status": "active",
|
||||||
|
"currentStepIndex": 0,
|
||||||
|
"objectiveCounters": {},
|
||||||
|
}
|
||||||
|
# Act
|
||||||
|
var merged: Dictionary = QuestProgressClient.merge_quest_row_into_snapshot(snapshot, active_row)
|
||||||
|
# Assert
|
||||||
|
var c: Node = QuestProgressClient.new()
|
||||||
|
auto_free(c)
|
||||||
|
var row: Dictionary = c.call("quest_row", PROTOTYPE_QUEST_GATHER_ID, merged)
|
||||||
|
assert_that(str(row.get("status", ""))).is_equal("active")
|
||||||
|
|
||||||
|
|
||||||
|
func test_request_sync_queues_while_busy_and_flushes_after_completion() -> void:
|
||||||
|
# Arrange
|
||||||
|
var transport := PendingSyncHttpTransport.new()
|
||||||
|
transport.body_json = _not_started_progress_json()
|
||||||
|
var noop := NoopHttpTransport.new()
|
||||||
|
var c := _make_client(transport, noop)
|
||||||
|
# Act
|
||||||
|
c.call("request_sync_from_server")
|
||||||
|
c.call("request_sync_from_server")
|
||||||
|
# Assert
|
||||||
|
assert_that(transport.request_count).is_equal(1)
|
||||||
|
transport.complete_pending()
|
||||||
|
assert_that(transport.request_count).is_equal(2)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ Seven commits landed after the first review: code-review follow-up (quest-defini
|
||||||
|
|
||||||
3. ~~**GdUnit CI exit 101 (orphans)** — Rewrote **`quest_hud_controller_test.gd`** with `MockHttpTransport` / `auto_free()` on all nodes; full suite **247 tests, 0 orphans, exit 0**. **Done.**
|
3. ~~**GdUnit CI exit 101 (orphans)** — Rewrote **`quest_hud_controller_test.gd`** with `MockHttpTransport` / `auto_free()` on all nodes; full suite **247 tests, 0 orphans, exit 0**. **Done.**
|
||||||
|
|
||||||
|
4. ~~**Quest HUD stale after accept** — Merge accept-response `quest` row into snapshot immediately; re-apply patch when stale GET returns `not_started`; queue pending sync when GET busy. **Done.**
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue