Compare commits
8 Commits
bc2e9d6045
...
33cfc62c66
| Author | SHA1 | Date |
|---|---|---|
|
|
33cfc62c66 | |
|
|
fa9396323e | |
|
|
e2991a2926 | |
|
|
acf3d079d2 | |
|
|
726926b901 | |
|
|
89c8f3034c | |
|
|
63156c353f | |
|
|
a1947d8f05 |
|
|
@ -246,11 +246,39 @@ Full checklist: [`docs/manual-qa/NEO-122.md`](../docs/manual-qa/NEO-122.md).
|
||||||
- **HUD:**
|
- **HUD:**
|
||||||
- **`UICanvas/HudRootScroll/HudRoot/QuestRewardDeliveryLabel`** — **`Quest rewards: —`** until a quest newly becomes **`completed`** in-session; then quest display name + grant lines.
|
- **`UICanvas/HudRootScroll/HudRoot/QuestRewardDeliveryLabel`** — **`Quest rewards: —`** until a quest newly becomes **`completed`** in-session; then quest display name + grant lines.
|
||||||
- Item lines use **`ItemDefinitionsClient.display_name_for`** (NEO-110 loot precedent); skill XP lines use title-case **`skillId`** + amount (e.g. **`Salvage +25 XP`**).
|
- Item lines use **`ItemDefinitionsClient.display_name_for`** (NEO-110 loot precedent); skill XP lines use title-case **`skillId`** + amount (e.g. **`Salvage +25 XP`**).
|
||||||
- **Refresh:** reuses NEO-122 triggers (boot hydrate + gather/craft/defeat/accept). **Transition-only** — boot with already-completed quests does not replay grant copy (NEO-123 idempotency).
|
- **Refresh:** reuses NEO-122 triggers (boot hydrate + gather/craft/defeat/accept). **Transition-only** — boot with already-completed quests does not replay grant copy (NEO-123 idempotency). **Economy HUD** (inventory + skills) refreshes on completion transition via NEO-132 signal from `quest_hud_controller.gd`.
|
||||||
- **Errors:** failed quest-progress GET shows **`sync error — …`** on **`QuestProgressLabel`**; reward label **`—`** (NEO-122 pattern).
|
- **Errors:** failed quest-progress GET shows **`sync error — …`** on **`QuestProgressLabel`**; reward label **`—`** (NEO-122 pattern).
|
||||||
|
|
||||||
Full checklist: [`docs/manual-qa/NEO-131.md`](../docs/manual-qa/NEO-131.md).
|
Full checklist: [`docs/manual-qa/NEO-131.md`](../docs/manual-qa/NEO-131.md).
|
||||||
|
|
||||||
|
## End-to-end quest reward loop (NEO-132)
|
||||||
|
|
||||||
|
Epic 7 Slice 2 capstone — complete all four prototype quests **in Godot** and verify quest completion bundles (skill XP + item grants) apply **once** with idempotent replay.
|
||||||
|
|
||||||
|
**Flow:** fresh server restart → NEO-123 four-quest path → per-completion **`QuestRewardDeliveryLabel`** + economy HUD verification → Godot restart idempotency.
|
||||||
|
|
||||||
|
| Step | Input / trigger | HUD / server outcome |
|
||||||
|
|------|-----------------|----------------------|
|
||||||
|
| Boot | Godot **F5** + fresh server | Four quests **`not started`**; reward label **`—`**; skill xp **0** |
|
||||||
|
| Gather intro | **Q** + gather ×3 scrap | Salvage Run **`completed`**; **`Salvage +25 XP`** reward line; salvage xp **+25** |
|
||||||
|
| Combat intro | **Shift+Q** + defeat ×3 NPCs | Clear the Pocket **`completed`**; encounter loot on **`EncounterCompleteLabel`**; **`Salvage +25 XP`** reward line; salvage xp **+25** |
|
||||||
|
| Refine intro | **Shift+Q** + craft **`refine_scrap_standard`** | Refine Stock **`completed`**; **`Refine +25 XP`** reward line; refine xp **+25** |
|
||||||
|
| Operator chain | **Shift+Q** + chain objectives | Operator Chain **`completed`**; **`Survey Drone Kit ×1`** + **`Salvage +50 XP`**; bag contains kit |
|
||||||
|
| Idempotency | Stop Godot + **F5** (server still running) | Quests still **`completed`**; reward label **`—`**; economy counts unchanged |
|
||||||
|
| Duplicate accept | **Q** / **Shift+Q** on finished quests | Deny; no duplicate skill XP or items |
|
||||||
|
|
||||||
|
**Reward vs encounter loot:** quest completion bundles (`completionRewardBundle`) surface on **`QuestRewardDeliveryLabel`**; encounter pocket loot remains on **`EncounterCompleteLabel`** (E7M2 freeze — encounter loot unchanged).
|
||||||
|
|
||||||
|
**Economy refresh:** on in-session quest completion transition, **`InventoryLabel`** + **`SkillProgressionLabel`** auto-refresh (NEO-132); reward label paint remains transition-only (NEO-131).
|
||||||
|
|
||||||
|
**Cross-links:** [NEO-124](../docs/plans/NEO-124-implementation-plan.md) catalog freeze · [NEO-126](../docs/plans/NEO-126-implementation-plan.md) delivery store · [NEO-127](../docs/plans/NEO-127-implementation-plan.md) router · [NEO-129](../docs/plans/NEO-129-implementation-plan.md) HTTP summary · [NEO-131](../docs/manual-qa/NEO-131.md) reward HUD · [NEO-123](../docs/manual-qa/NEO-123.md) onboarding flow.
|
||||||
|
|
||||||
|
**Scripts:** `quest_hud_controller.gd` (completion transition signal → economy refresh), `quest_progress_client.gd`, `inventory_client.gd`, `skill_progression_client.gd` — wired from `main.gd`.
|
||||||
|
|
||||||
|
**Preconditions:** **Server restart** before capstone run resets quest progress, reward delivery store, inventory, and skill progression.
|
||||||
|
|
||||||
|
Full capstone checklist: [`docs/manual-qa/NEO-132.md`](../docs/manual-qa/NEO-132.md).
|
||||||
|
|
||||||
## End-to-end onboarding quest loop (NEO-123)
|
## End-to-end onboarding quest loop (NEO-123)
|
||||||
|
|
||||||
Epic 7 Slice 1 capstone — complete all four prototype quests (three onboarding intros + operator chain) **in Godot** without Bruno.
|
Epic 7 Slice 1 capstone — complete all four prototype quests (three onboarding intros + operator chain) **in Godot** without Bruno.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const SCHEMA_VERSION := 1
|
||||||
|
|
||||||
var _http: Node
|
var _http: Node
|
||||||
var _busy: bool = false
|
var _busy: bool = false
|
||||||
|
var _sync_pending: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -29,7 +30,12 @@ func _ready() -> void:
|
||||||
|
|
||||||
func request_sync_from_server() -> void:
|
func request_sync_from_server() -> void:
|
||||||
if _busy:
|
if _busy:
|
||||||
|
_sync_pending = true
|
||||||
return
|
return
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
|
func _start_sync_request() -> void:
|
||||||
_busy = true
|
_busy = true
|
||||||
var url := "%s/game/players/%s/inventory" % [_base_root(), _player_path_segment()]
|
var url := "%s/game/players/%s/inventory" % [_base_root(), _player_path_segment()]
|
||||||
var err: Error = _http.request(url)
|
var err: Error = _http.request(url)
|
||||||
|
|
@ -38,6 +44,14 @@ func request_sync_from_server() -> void:
|
||||||
push_warning("InventoryClient: %s" % reason)
|
push_warning("InventoryClient: %s" % reason)
|
||||||
_busy = false
|
_busy = false
|
||||||
inventory_sync_failed.emit(reason)
|
inventory_sync_failed.emit(reason)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
||||||
|
|
||||||
|
func _try_flush_pending_sync() -> void:
|
||||||
|
if not _sync_pending or _busy:
|
||||||
|
return
|
||||||
|
_sync_pending = false
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
func _base_root() -> String:
|
func _base_root() -> String:
|
||||||
|
|
@ -66,16 +80,19 @@ func _on_request_completed(
|
||||||
var reason := "HTTP failed (result=%s)" % result
|
var reason := "HTTP failed (result=%s)" % result
|
||||||
push_warning("InventoryClient: %s" % reason)
|
push_warning("InventoryClient: %s" % reason)
|
||||||
inventory_sync_failed.emit(reason)
|
inventory_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("InventoryClient: %s" % reason404)
|
push_warning("InventoryClient: %s" % reason404)
|
||||||
inventory_sync_failed.emit(reason404)
|
inventory_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("InventoryClient: %s" % reason_code)
|
push_warning("InventoryClient: %s" % reason_code)
|
||||||
inventory_sync_failed.emit(reason_code)
|
inventory_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_inventory_json(text)
|
var snapshot: Variant = parse_inventory_json(text)
|
||||||
|
|
@ -83,5 +100,7 @@ func _on_request_completed(
|
||||||
var reason_json := "non-JSON body or schemaVersion mismatch"
|
var reason_json := "non-JSON body or schemaVersion mismatch"
|
||||||
push_warning("InventoryClient: %s" % reason_json)
|
push_warning("InventoryClient: %s" % reason_json)
|
||||||
inventory_sync_failed.emit(reason_json)
|
inventory_sync_failed.emit(reason_json)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
inventory_received.emit(snapshot as Dictionary)
|
inventory_received.emit(snapshot as Dictionary)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
|
||||||
|
|
@ -865,7 +865,9 @@ func _setup_quest_progress_sync() -> void:
|
||||||
_quest_accept_feedback_label,
|
_quest_accept_feedback_label,
|
||||||
Callable(self, "_apply_authority_http_config_to_client"),
|
Callable(self, "_apply_authority_http_config_to_client"),
|
||||||
_quest_reward_delivery_label,
|
_quest_reward_delivery_label,
|
||||||
_item_defs_client
|
_item_defs_client,
|
||||||
|
_inventory_client,
|
||||||
|
_skill_progression_client
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ extends Node
|
||||||
|
|
||||||
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
|
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
|
||||||
## NEO-131: quest completion reward label on in-session completed transition.
|
## NEO-131: quest completion reward label on in-session completed transition.
|
||||||
|
## NEO-132: emits quest_completion_reward_transition for economy HUD refresh.
|
||||||
|
|
||||||
|
signal quest_completion_reward_transition(quest_id: String, summary: Dictionary)
|
||||||
|
|
||||||
const QuestProgressClient := preload("res://scripts/quest_progress_client.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"
|
||||||
|
|
@ -23,6 +26,8 @@ var _previous_status_by_quest: Dictionary = {}
|
||||||
var _quest_was_active_in_session: 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 = {}
|
||||||
|
var _inventory_client: Node = null
|
||||||
|
var _skill_progression_client: Node = null
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -32,11 +37,15 @@ func setup(
|
||||||
accept_label: Label,
|
accept_label: Label,
|
||||||
apply_http_config: Callable,
|
apply_http_config: Callable,
|
||||||
reward_label: Label = null,
|
reward_label: Label = null,
|
||||||
item_defs_client: Node = null
|
item_defs_client: Node = null,
|
||||||
|
inventory_client: Node = null,
|
||||||
|
skill_progression_client: Node = null
|
||||||
) -> void:
|
) -> void:
|
||||||
_progress_client = progress_client
|
_progress_client = progress_client
|
||||||
_defs_client = defs_client
|
_defs_client = defs_client
|
||||||
_item_defs_client = item_defs_client
|
_item_defs_client = item_defs_client
|
||||||
|
_inventory_client = inventory_client
|
||||||
|
_skill_progression_client = skill_progression_client
|
||||||
_progress_label = progress_label
|
_progress_label = progress_label
|
||||||
_accept_label = accept_label
|
_accept_label = accept_label
|
||||||
_reward_label = reward_label
|
_reward_label = reward_label
|
||||||
|
|
@ -205,6 +214,22 @@ func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void:
|
||||||
_last_reward_summary = (summary_variant as Dictionary).duplicate(true)
|
_last_reward_summary = (summary_variant as Dictionary).duplicate(true)
|
||||||
else:
|
else:
|
||||||
_last_reward_summary = {}
|
_last_reward_summary = {}
|
||||||
|
if not _last_reward_quest_id.is_empty() and not _last_reward_summary.is_empty():
|
||||||
|
quest_completion_reward_transition.emit(_last_reward_quest_id, _last_reward_summary)
|
||||||
|
_refresh_economy_hud()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_economy_hud() -> void:
|
||||||
|
if (
|
||||||
|
is_instance_valid(_inventory_client)
|
||||||
|
and _inventory_client.has_method("request_sync_from_server")
|
||||||
|
):
|
||||||
|
_inventory_client.call("request_sync_from_server")
|
||||||
|
if (
|
||||||
|
is_instance_valid(_skill_progression_client)
|
||||||
|
and _skill_progression_client.has_method("request_sync_from_server")
|
||||||
|
):
|
||||||
|
_skill_progression_client.call("request_sync_from_server")
|
||||||
|
|
||||||
|
|
||||||
func _quest_ids_in_render_order(snapshot: Dictionary) -> Array:
|
func _quest_ids_in_render_order(snapshot: Dictionary) -> Array:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const SCHEMA_VERSION := 1
|
||||||
|
|
||||||
var _http: Node
|
var _http: Node
|
||||||
var _busy: bool = false
|
var _busy: bool = false
|
||||||
|
var _sync_pending: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -29,7 +30,12 @@ func _ready() -> void:
|
||||||
|
|
||||||
func request_sync_from_server() -> void:
|
func request_sync_from_server() -> void:
|
||||||
if _busy:
|
if _busy:
|
||||||
|
_sync_pending = true
|
||||||
return
|
return
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
|
func _start_sync_request() -> void:
|
||||||
_busy = true
|
_busy = true
|
||||||
var url := "%s/game/players/%s/skill-progression" % [_base_root(), _player_path_segment()]
|
var url := "%s/game/players/%s/skill-progression" % [_base_root(), _player_path_segment()]
|
||||||
var err: Error = _http.request(url)
|
var err: Error = _http.request(url)
|
||||||
|
|
@ -38,6 +44,14 @@ func request_sync_from_server() -> void:
|
||||||
push_warning("SkillProgressionClient: %s" % reason)
|
push_warning("SkillProgressionClient: %s" % reason)
|
||||||
_busy = false
|
_busy = false
|
||||||
progression_sync_failed.emit(reason)
|
progression_sync_failed.emit(reason)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
||||||
|
|
||||||
|
func _try_flush_pending_sync() -> void:
|
||||||
|
if not _sync_pending or _busy:
|
||||||
|
return
|
||||||
|
_sync_pending = false
|
||||||
|
_start_sync_request()
|
||||||
|
|
||||||
|
|
||||||
func skill_row(skill_id: String, snapshot: Dictionary = {}) -> Dictionary:
|
func skill_row(skill_id: String, snapshot: Dictionary = {}) -> Dictionary:
|
||||||
|
|
@ -82,16 +96,19 @@ func _on_request_completed(
|
||||||
var reason := "HTTP failed (result=%s)" % result
|
var reason := "HTTP failed (result=%s)" % result
|
||||||
push_warning("SkillProgressionClient: %s" % reason)
|
push_warning("SkillProgressionClient: %s" % reason)
|
||||||
progression_sync_failed.emit(reason)
|
progression_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("SkillProgressionClient: %s" % reason404)
|
push_warning("SkillProgressionClient: %s" % reason404)
|
||||||
progression_sync_failed.emit(reason404)
|
progression_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("SkillProgressionClient: %s" % reason_code)
|
push_warning("SkillProgressionClient: %s" % reason_code)
|
||||||
progression_sync_failed.emit(reason_code)
|
progression_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_progression_json(text)
|
var snapshot: Variant = parse_progression_json(text)
|
||||||
|
|
@ -99,5 +116,7 @@ func _on_request_completed(
|
||||||
var reason_json := "non-JSON body or schemaVersion mismatch"
|
var reason_json := "non-JSON body or schemaVersion mismatch"
|
||||||
push_warning("SkillProgressionClient: %s" % reason_json)
|
push_warning("SkillProgressionClient: %s" % reason_json)
|
||||||
progression_sync_failed.emit(reason_json)
|
progression_sync_failed.emit(reason_json)
|
||||||
|
_try_flush_pending_sync()
|
||||||
return
|
return
|
||||||
progression_received.emit(snapshot as Dictionary)
|
progression_received.emit(snapshot as Dictionary)
|
||||||
|
_try_flush_pending_sync()
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,33 @@ class MockHttpTransport:
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func _empty_inventory_json() -> String:
|
static func _empty_inventory_json() -> String:
|
||||||
var bag_parts: PackedStringArray = PackedStringArray()
|
var bag_parts: PackedStringArray = PackedStringArray()
|
||||||
for i in range(24):
|
for i in range(24):
|
||||||
|
|
@ -111,3 +138,17 @@ func test_malformed_json_emits_inventory_sync_failed() -> void:
|
||||||
c.call("request_sync_from_server")
|
c.call("request_sync_from_server")
|
||||||
# Assert
|
# Assert
|
||||||
await assert_signal(c).is_emitted("inventory_sync_failed", any())
|
await assert_signal(c).is_emitted("inventory_sync_failed", any())
|
||||||
|
|
||||||
|
|
||||||
|
func test_request_sync_queues_while_busy_and_flushes_after_completion() -> void:
|
||||||
|
# Arrange
|
||||||
|
var transport := PendingSyncHttpTransport.new()
|
||||||
|
transport.body_json = _empty_inventory_json()
|
||||||
|
var c := _make_client(transport)
|
||||||
|
# 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)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
extends GdUnitTestSuite
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
## NEO-131: quest completion reward label HUD tests.
|
## NEO-131: quest completion reward label HUD tests.
|
||||||
|
## NEO-132: quest_completion_reward_transition signal for economy refresh.
|
||||||
|
|
||||||
const QuestHudController := preload("res://scripts/quest_hud_controller.gd")
|
const QuestHudController := preload("res://scripts/quest_hud_controller.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 ItemDefsClient := preload("res://scripts/item_definitions_client.gd")
|
||||||
|
const InventoryClient := preload("res://scripts/inventory_client.gd")
|
||||||
|
const SkillProgressionClient := preload("res://scripts/skill_progression_client.gd")
|
||||||
|
|
||||||
const OPERATOR_CHAIN_QUEST_ID := "prototype_quest_operator_chain"
|
const OPERATOR_CHAIN_QUEST_ID := "prototype_quest_operator_chain"
|
||||||
|
|
||||||
|
|
@ -37,6 +40,38 @@ class StubItemDefsClient:
|
||||||
return item_id
|
return item_id
|
||||||
|
|
||||||
|
|
||||||
|
class MockEconomySyncClient:
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var sync_count: int = 0
|
||||||
|
|
||||||
|
func request_sync_from_server() -> void:
|
||||||
|
sync_count += 1
|
||||||
|
|
||||||
|
|
||||||
|
class PendingSyncHttpTransport:
|
||||||
|
extends Node
|
||||||
|
signal request_completed(
|
||||||
|
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||||||
|
)
|
||||||
|
var request_count: int = 0
|
||||||
|
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, 200, PackedStringArray(), body_json.to_utf8_buffer()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _make_defs_client(transport: Node) -> Node:
|
func _make_defs_client(transport: Node) -> Node:
|
||||||
var c: Node = QuestDefinitionsClient.new()
|
var c: Node = QuestDefinitionsClient.new()
|
||||||
c.set("injected_http", transport)
|
c.set("injected_http", transport)
|
||||||
|
|
@ -272,3 +307,98 @@ func test_sync_failure_shows_progress_error_and_idle_reward_label() -> void:
|
||||||
# Assert
|
# Assert
|
||||||
assert_str(progress_label.text).contains("error — HTTP 503")
|
assert_str(progress_label.text).contains("error — HTTP 503")
|
||||||
assert_str(reward_label.text).is_equal("Quest rewards:\n—")
|
assert_str(reward_label.text).is_equal("Quest rewards:\n—")
|
||||||
|
|
||||||
|
|
||||||
|
func test_completion_transition_emits_economy_refresh_signal() -> void:
|
||||||
|
# Arrange
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
auto_free(controller)
|
||||||
|
add_child(controller)
|
||||||
|
monitor_signals(controller)
|
||||||
|
# Act
|
||||||
|
controller.call("_on_progress_received", _active_gather_snapshot())
|
||||||
|
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||||||
|
# Assert
|
||||||
|
await assert_signal(controller).is_emitted("quest_completion_reward_transition", any(), any())
|
||||||
|
|
||||||
|
|
||||||
|
func test_completion_transition_invokes_economy_refresh_callback() -> void:
|
||||||
|
# Arrange
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
var inventory_client := MockEconomySyncClient.new()
|
||||||
|
var skill_client := MockEconomySyncClient.new()
|
||||||
|
auto_free(controller)
|
||||||
|
auto_free(inventory_client)
|
||||||
|
auto_free(skill_client)
|
||||||
|
add_child(controller)
|
||||||
|
add_child(inventory_client)
|
||||||
|
add_child(skill_client)
|
||||||
|
controller.set("_inventory_client", inventory_client)
|
||||||
|
controller.set("_skill_progression_client", skill_client)
|
||||||
|
# Act
|
||||||
|
controller.call("_on_progress_received", _active_gather_snapshot())
|
||||||
|
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||||||
|
# Assert
|
||||||
|
assert_int(inventory_client.sync_count).is_equal(1)
|
||||||
|
assert_int(skill_client.sync_count).is_equal(1)
|
||||||
|
|
||||||
|
|
||||||
|
func test_boot_completed_does_not_emit_economy_refresh_signal() -> void:
|
||||||
|
# Arrange
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
auto_free(controller)
|
||||||
|
add_child(controller)
|
||||||
|
monitor_signals(controller)
|
||||||
|
# Act
|
||||||
|
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||||||
|
# Assert
|
||||||
|
await (assert_signal(controller).wait_until(200).is_not_emitted(
|
||||||
|
"quest_completion_reward_transition"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
func test_completion_transition_queues_economy_refresh_while_clients_busy() -> void:
|
||||||
|
# Arrange
|
||||||
|
var inv_transport := PendingSyncHttpTransport.new()
|
||||||
|
var bag_parts: PackedStringArray = PackedStringArray()
|
||||||
|
for i in range(24):
|
||||||
|
bag_parts.append('{"slotIndex":%d,"quantity":0}' % i)
|
||||||
|
inv_transport.body_json = (
|
||||||
|
(
|
||||||
|
'{"schemaVersion":1,"playerId":"dev-local-1","bagSlots":[%s],'
|
||||||
|
+ '"equipmentSlots":[{"slotIndex":0,"quantity":0}]}'
|
||||||
|
)
|
||||||
|
% ", ".join(bag_parts)
|
||||||
|
)
|
||||||
|
var skill_transport := PendingSyncHttpTransport.new()
|
||||||
|
skill_transport.body_json = (
|
||||||
|
'{"schemaVersion":1,"playerId":"dev-local-1","skills":'
|
||||||
|
+ '[{"id":"salvage","xp":0,"level":1}]}'
|
||||||
|
)
|
||||||
|
var inventory_client: Node = InventoryClient.new()
|
||||||
|
var skill_client: Node = SkillProgressionClient.new()
|
||||||
|
inventory_client.set("injected_http", inv_transport)
|
||||||
|
skill_client.set("injected_http", skill_transport)
|
||||||
|
var controller: Node = QuestHudController.new()
|
||||||
|
auto_free(inv_transport)
|
||||||
|
auto_free(skill_transport)
|
||||||
|
auto_free(inventory_client)
|
||||||
|
auto_free(skill_client)
|
||||||
|
auto_free(controller)
|
||||||
|
add_child(inventory_client)
|
||||||
|
add_child(skill_client)
|
||||||
|
add_child(controller)
|
||||||
|
controller.set("_inventory_client", inventory_client)
|
||||||
|
controller.set("_skill_progression_client", skill_client)
|
||||||
|
inventory_client.call("request_sync_from_server")
|
||||||
|
skill_client.call("request_sync_from_server")
|
||||||
|
# Act
|
||||||
|
controller.call("_on_progress_received", _active_gather_snapshot())
|
||||||
|
controller.call("_on_progress_received", _completed_gather_with_summary_snapshot())
|
||||||
|
# Assert
|
||||||
|
assert_int(inv_transport.request_count).is_equal(1)
|
||||||
|
assert_int(skill_transport.request_count).is_equal(1)
|
||||||
|
inv_transport.complete_pending()
|
||||||
|
skill_transport.complete_pending()
|
||||||
|
assert_int(inv_transport.request_count).is_equal(2)
|
||||||
|
assert_int(skill_transport.request_count).is_equal(2)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,33 @@ class MockHttpTransport:
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func _trio_progression_json() -> String:
|
static func _trio_progression_json() -> String:
|
||||||
return (
|
return (
|
||||||
'{"schemaVersion":1,"playerId":"dev-local-1","skills":['
|
'{"schemaVersion":1,"playerId":"dev-local-1","skills":['
|
||||||
|
|
@ -95,3 +122,17 @@ func test_http_404_emits_progression_sync_failed() -> void:
|
||||||
c.call("request_sync_from_server")
|
c.call("request_sync_from_server")
|
||||||
# Assert
|
# Assert
|
||||||
await assert_signal(c).is_emitted("progression_sync_failed", "HTTP 404 (player unknown)")
|
await assert_signal(c).is_emitted("progression_sync_failed", "HTTP 404 (player unknown)")
|
||||||
|
|
||||||
|
|
||||||
|
func test_request_sync_queues_while_busy_and_flushes_after_completion() -> void:
|
||||||
|
# Arrange
|
||||||
|
var transport := PendingSyncHttpTransport.new()
|
||||||
|
transport.body_json = _trio_progression_json()
|
||||||
|
var c := _make_client(transport)
|
||||||
|
# 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)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
| **Module ID** | E7.M2 |
|
| **Module ID** | E7.M2 |
|
||||||
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
||||||
| **Stage target** | Prototype |
|
| **Stage target** | Prototype |
|
||||||
| **Status** | Planned — Slice 2 backlog [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md): **E7M2-01** [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132). **NEO-43 prep landed:** `MissionRewardSkillXpGrant` for bundle skill XP (`mission_reward`). Upstream: E7.M1 **Ready**, E2.M2 grant stack, E3.M3 inventory. |
|
| **Status** | Ready — Slice 2 backlog [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md): **E7M2-01** [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132) **landed**. **NEO-43 prep landed:** `MissionRewardSkillXpGrant` for bundle skill XP (`mission_reward`). Upstream: E7.M1 **Ready**, E2.M2 grant stack, E3.M3 inventory. |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
|
@ -79,3 +79,5 @@ Epic 7 **Slice 2** — `reward_delivery`, `unlock_granted`; no double-claim on r
|
||||||
**NEO-130 (E7M2-07 telemetry hooks):** comment-only **`reward_delivery`** + **`unlock_granted`** stub hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`** — first-time **`TryRecord`** success and future **`UnlockGrant`** apply region respectively; no production ingest. See [NEO-130 implementation plan](../../plans/NEO-130-implementation-plan.md); [server README — Reward telemetry hooks (NEO-130)](../../../server/README.md#reward-telemetry-hooks-neo-130).
|
**NEO-130 (E7M2-07 telemetry hooks):** comment-only **`reward_delivery`** + **`unlock_granted`** stub hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`** — first-time **`TryRecord`** success and future **`UnlockGrant`** apply region respectively; no production ingest. See [NEO-130 implementation plan](../../plans/NEO-130-implementation-plan.md); [server README — Reward telemetry hooks (NEO-130)](../../../server/README.md#reward-telemetry-hooks-neo-130).
|
||||||
|
|
||||||
**NEO-131 (E7M2-08 client HUD):** Godot **`QuestRewardDeliveryLabel`** + **`quest_hud_controller.gd`** transition detection paints **`completionRewardSummary`** grant lines when a quest newly becomes **`completed`** in-session (parse via **`quest_progress_client.gd`**). See [NEO-131 implementation plan](../../plans/NEO-131-implementation-plan.md); [client README — Quest completion reward HUD (NEO-131)](../../../client/README.md#quest-completion-reward-hud-neo-131).
|
**NEO-131 (E7M2-08 client HUD):** Godot **`QuestRewardDeliveryLabel`** + **`quest_hud_controller.gd`** transition detection paints **`completionRewardSummary`** grant lines when a quest newly becomes **`completed`** in-session (parse via **`quest_progress_client.gd`**). See [NEO-131 implementation plan](../../plans/NEO-131-implementation-plan.md); [client README — Quest completion reward HUD (NEO-131)](../../../client/README.md#quest-completion-reward-hud-neo-131).
|
||||||
|
|
||||||
|
**NEO-132 (E7M2-09 client capstone):** playable four-quest reward delivery loop — [`NEO-132` manual QA](../../manual-qa/NEO-132.md); [client README — End-to-end quest reward loop (NEO-132)](../../../client/README.md#end-to-end-quest-reward-loop-neo-132); economy HUD refresh on completion transition via **`quest_completion_reward_transition`** signal. Epic 7 Slice 2 client capstone complete.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -110,9 +110,9 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|
||||||
| E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Ready |
|
| E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Ready |
|
||||||
|
|
||||||
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) **landed**; label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. **E7M1-12 / NEO-123** client capstone landed — [`NEO-123` manual QA](../../manual-qa/NEO-123.md), [client README — End-to-end onboarding quest loop (NEO-123)](../../../client/README.md#end-to-end-onboarding-quest-loop-neo-123), plan [NEO-123](../../plans/NEO-123-implementation-plan.md). Epic 7 Slice 1 client capstone complete.
|
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) **landed**; label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. **E7M1-12 / NEO-123** client capstone landed — [`NEO-123` manual QA](../../manual-qa/NEO-123.md), [client README — End-to-end onboarding quest loop (NEO-123)](../../../client/README.md#end-to-end-onboarding-quest-loop-neo-123), plan [NEO-123](../../plans/NEO-123-implementation-plan.md). Epic 7 Slice 1 client capstone complete.
|
||||||
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned |
|
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Ready |
|
||||||
|
|
||||||
**E7.M2 note:** Epic 7 **Slice 2** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132); label **`E7.M2`**. See [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md), [E7_M2_RewardAndUnlockRouter.md](E7_M2_RewardAndUnlockRouter.md). Upstream: E7.M1 **Ready**, E2.M2 grant stack ([NEO-43](https://linear.app/neon-sprawl/issue/NEO-43)), E3.M3 inventory **Ready**. Client capstone **E7M2-09** [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132). Register row stays **Planned** until capstone lands.
|
**E7.M2 note:** Epic 7 **Slice 2** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) → [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132) **landed**; label **`E7.M2`**. See [E7M2-prototype-backlog.md](../../plans/E7M2-prototype-backlog.md), [E7_M2_RewardAndUnlockRouter.md](E7_M2_RewardAndUnlockRouter.md). Upstream: E7.M1 **Ready**, E2.M2 grant stack ([NEO-43](https://linear.app/neon-sprawl/issue/NEO-43)), E3.M3 inventory **Ready**. **E7M2-09 / NEO-132** client capstone landed — [`NEO-132` manual QA](../../manual-qa/NEO-132.md), [client README — End-to-end quest reward loop (NEO-132)](../../../client/README.md#end-to-end-quest-reward-loop-neo-132), plan [NEO-132](../../plans/NEO-132-implementation-plan.md). Epic 7 Slice 2 client capstone complete.
|
||||||
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
|
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
|
||||||
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |
|
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ Adjust gathers/crafts if bag counts differ; counters and craft denies are author
|
||||||
- Chain terminal step is **`inventory_has_item`** — no token consume in Slice 1.
|
- Chain terminal step is **`inventory_has_item`** — no token consume in Slice 1.
|
||||||
- **`POST /game/__dev/combat-targets-fixture`** is **out of scope** — clears encounter progress unsuitable for idempotency ([server README — dev combat-target fixture](../../server/README.md#dev-combat-target-fixture-brunomanual-qa)).
|
- **`POST /game/__dev/combat-targets-fixture`** is **out of scope** — clears encounter progress unsuitable for idempotency ([server README — dev combat-target fixture](../../server/README.md#dev-combat-target-fixture-brunomanual-qa)).
|
||||||
- Epic 7 Slice 1 AC: 3 onboarding + 1 chain complete without duped completion ([epic_07 Definition of Done](../decomposition/epics/epic_07_quest_faction.md#epic-7-definition-of-done)).
|
- Epic 7 Slice 1 AC: 3 onboarding + 1 chain complete without duped completion ([epic_07 Definition of Done](../decomposition/epics/epic_07_quest_faction.md#epic-7-definition-of-done)).
|
||||||
|
- **Slice 2 reward verification:** quest completion bundles + economy idempotency — [NEO-132 manual QA](NEO-132.md) (supersedes for reward delivery AC).
|
||||||
|
|
||||||
## Acceptance
|
## Acceptance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,5 @@
|
||||||
|
|
||||||
- [NEO-122](NEO-122.md) — quest progress + accept HUD unchanged except new label below accept feedback.
|
- [NEO-122](NEO-122.md) — quest progress + accept HUD unchanged except new label below accept feedback.
|
||||||
- [NEO-110](NEO-110.md) — encounter loot label still independent from quest completion bundles.
|
- [NEO-110](NEO-110.md) — encounter loot label still independent from quest completion bundles.
|
||||||
|
|
||||||
|
**Full-flow capstone:** [NEO-132](NEO-132.md) extends this checklist with economy HUD verification across all four quest completion bundles.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
# NEO-132 — Manual QA checklist
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Key | NEO-132 |
|
||||||
|
| Title | E7M2-09: Playable quest reward delivery capstone (Godot) |
|
||||||
|
| Linear | https://linear.app/neon-sprawl/issue/NEO-132/e7m2-09-playable-quest-reward-delivery-capstone-godot |
|
||||||
|
| Plan | `docs/plans/NEO-132-implementation-plan.md` |
|
||||||
|
| Branch | `NEO-132-playable-quest-reward-delivery-capstone` |
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- **Fresh dev player:** stop any running server, then start a new instance so in-memory quest progress, **`IRewardDeliveryStore`**, inventory, skill progression, encounter progress, NPC HP, and resource-node depletion reset.
|
||||||
|
- **No Bruno/curl** for this checklist — accept uses **Q** / **Shift+Q**; gameplay uses **R**, **Tab** + **1**, and Economy HUD **Craft** buttons only.
|
||||||
|
- NEO-124–NEO-131 reward spine and NEO-123 onboarding capstone landed on `main`.
|
||||||
|
- **`Economy HUD`** toggle **on** for inventory + skill verification.
|
||||||
|
|
||||||
|
## Quest completion reward freeze (E7M2-01)
|
||||||
|
|
||||||
|
| Quest id | Display name | Bundle grants | Expected **`QuestRewardDeliveryLabel`** |
|
||||||
|
|----------|--------------|---------------|----------------------------------------|
|
||||||
|
| `prototype_quest_gather_intro` | Intro: Salvage Run | `salvage` **+25** XP | `Salvage +25 XP` |
|
||||||
|
| `prototype_quest_combat_intro` | Intro: Clear the Pocket | `salvage` **+25** XP | `Salvage +25 XP` |
|
||||||
|
| `prototype_quest_refine_intro` | Intro: Refine Stock | `refine` **+25** XP | `Refine +25 XP` |
|
||||||
|
| `prototype_quest_operator_chain` | Operator Chain | `survey_drone_kit` ×1 + `salvage` **+50** XP | `Survey Drone Kit ×1` + `Salvage +50 XP` |
|
||||||
|
|
||||||
|
**Encounter loot (unchanged — E5.M3):** combat pocket still grants **`scrap_metal_bulk` ×10** + **`contract_handoff_token` ×1** on **`EncounterCompleteLabel`**, not quest bundles.
|
||||||
|
|
||||||
|
## Skill XP verification method
|
||||||
|
|
||||||
|
Record **`SkillProgressionLabel`** salvage/refine **`xp`** values **immediately before** each quest completion transition and **after** the HUD refreshes. Assert the **delta** matches the freeze table (+25 or +50 for salvage; +25 for refine). Gather/craft activity XP may also change totals — deltas per quest completion are authoritative.
|
||||||
|
|
||||||
|
## Expected HUD progression (extends NEO-123)
|
||||||
|
|
||||||
|
| Phase | `QuestProgressLabel` | `QuestRewardDeliveryLabel` | Economy HUD |
|
||||||
|
|-------|----------------------|----------------------------|-------------|
|
||||||
|
| Boot | All four **`not started`** | `Quest rewards:` / `—` | salvage/refine xp **0** (fresh server) |
|
||||||
|
| Gather intro **`completed`** | Salvage Run **`completed`** | `Intro: Salvage Run` + `Salvage +25 XP` | salvage xp **+25** delta |
|
||||||
|
| Combat intro **`completed`** | Clear the Pocket **`completed`** | `Intro: Clear the Pocket` + `Salvage +25 XP` | salvage xp **+25** delta |
|
||||||
|
| Refine intro **`completed`** | Refine Stock **`completed`** | `Intro: Refine Stock` + `Refine +25 XP` | refine xp **+25** delta |
|
||||||
|
| Operator chain **`completed`** | Operator Chain **`completed`** | `Survey Drone Kit ×1` + `Salvage +50 XP` | bag has **`survey_drone_kit`**; salvage xp **+50** delta |
|
||||||
|
| Godot restart | All four still **`completed`** | `—` (no transition replay) | counts **unchanged** |
|
||||||
|
| Duplicate accepts | Unchanged | Unchanged | counts **unchanged** |
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
Follow [NEO-123](NEO-123.md) for accept order, anchors, material math, and quest steps **1–15**. **Extend** each completion with reward + economy checks below.
|
||||||
|
|
||||||
|
### Boot
|
||||||
|
|
||||||
|
1. Start server: `cd server/NeonSprawl.Server && dotnet run`.
|
||||||
|
2. Run Godot main scene (**F5**). Confirm four quests **`not started`**, **`QuestRewardDeliveryLabel`** shows **`Quest rewards:`** / **`—`**, and **`SkillProgressionLabel`** shows salvage/refine xp **0** (fresh server).
|
||||||
|
|
||||||
|
### Intro: Salvage Run (gather + skill XP reward)
|
||||||
|
|
||||||
|
3. Press **Q**; gather at **gamma** **(12, 0)** once (+3 scrap) until **`Intro: Salvage Run: completed`**.
|
||||||
|
4. Verify **`QuestRewardDeliveryLabel`**: `Intro: Salvage Run` + **`Salvage +25 XP`**.
|
||||||
|
5. Verify **`SkillProgressionLabel`**: salvage xp increased by **25** vs value recorded before completion (activity gather XP may also apply — quest bundle delta is **+25**).
|
||||||
|
|
||||||
|
### Intro: Clear the Pocket (combat + skill XP reward)
|
||||||
|
|
||||||
|
6. Press **Shift+Q**; defeat all three prototype NPCs (**Tab** + **1**, any order).
|
||||||
|
7. Verify **`EncounterCompleteLabel`** still lists encounter loot (**`scrap_metal_bulk ×10`**, **`contract_handoff_token ×1`**) — separate from quest bundle.
|
||||||
|
8. Verify **`Intro: Clear the Pocket: completed`** on **`QuestProgressLabel`**.
|
||||||
|
9. Verify **`QuestRewardDeliveryLabel`**: `Intro: Clear the Pocket` + **`Salvage +25 XP`**.
|
||||||
|
10. Verify **`SkillProgressionLabel`**: salvage xp **+25** delta since step 5.
|
||||||
|
|
||||||
|
### Intro: Refine Stock (craft + skill XP reward)
|
||||||
|
|
||||||
|
11. Press **Shift+Q**; craft **`refine_scrap_standard`** when **≥5** scrap in bag.
|
||||||
|
12. Verify **`Intro: Refine Stock: completed`**.
|
||||||
|
13. Verify **`QuestRewardDeliveryLabel`**: `Intro: Refine Stock` + **`Refine +25 XP`**.
|
||||||
|
14. Verify **`SkillProgressionLabel`**: refine xp **+25** delta since step 10.
|
||||||
|
|
||||||
|
### Operator Chain (item + skill XP reward)
|
||||||
|
|
||||||
|
15. Press **Shift+Q**; complete chain steps 1–4 per [NEO-123](NEO-123.md) (gather ×5 → refine → stim → token held).
|
||||||
|
16. Verify **`Operator Chain: completed`**.
|
||||||
|
17. Verify **`QuestRewardDeliveryLabel`**: **`Survey Drone Kit ×1`** (or raw id fallback) + **`Salvage +50 XP`**.
|
||||||
|
18. Verify **`InventoryLabel`** lists **`survey_drone_kit`** (display name when item defs loaded).
|
||||||
|
19. Verify **`SkillProgressionLabel`**: salvage xp **+50** delta since step 14.
|
||||||
|
|
||||||
|
### Capstone snapshot (record for idempotency)
|
||||||
|
|
||||||
|
20. **Record** final state:
|
||||||
|
- **`QuestProgressLabel`**: all four **`completed`**
|
||||||
|
- **`SkillProgressionLabel`**: salvage + refine xp totals
|
||||||
|
- **`InventoryLabel`**: includes **`survey_drone_kit`**; note scrap/plate/token/stim counts
|
||||||
|
|
||||||
|
### Idempotency (no duplicate reward delivery)
|
||||||
|
|
||||||
|
21. **Godot restart:** stop Godot (**Shift+F5**); **F5** again **without** stopping the server.
|
||||||
|
22. Verify all four quests still **`completed`**; **`QuestRewardDeliveryLabel`** is **`—`** (transition-only).
|
||||||
|
23. Verify **`SkillProgressionLabel`** and **`InventoryLabel`** counts **match step 20** exactly.
|
||||||
|
24. Press **Q** on gather intro. Verify **`Quest accept: denied — already_completed`** (or **`already_active`**).
|
||||||
|
25. Press **Shift+Q**. Verify **`Quest accept: no eligible quest`** (or equivalent deny).
|
||||||
|
26. Verify economy HUD counts **still match step 20** — no duplicate skill XP or items.
|
||||||
|
|
||||||
|
### Regression
|
||||||
|
|
||||||
|
27. Component reward HUD: [NEO-131 manual QA](NEO-131.md).
|
||||||
|
28. Slice 1 quest chain: [NEO-123 manual QA](NEO-123.md).
|
||||||
|
29. Encounter loot independence: [NEO-110 manual QA](NEO-110.md).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Shift+Q catalog order** matches NEO-123 — combat intro before refine intro after gather completes.
|
||||||
|
- Quest completion bundles are **idempotent** server-side (`{playerId}:quest_complete:{questId}`); Godot restart must not replay grant copy on **`QuestRewardDeliveryLabel`** (NEO-131 transition-only).
|
||||||
|
- Economy HUD auto-refreshes on quest completion transition (NEO-132); no manual **I** required for reward verification.
|
||||||
|
- Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim ([epic_07 Slice 2](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing)).
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- [ ] Steps 1–26 completable in one session without Bruno/curl.
|
||||||
|
- [ ] All four quest completion bundles visible on **`QuestRewardDeliveryLabel`** during in-session transitions.
|
||||||
|
- [ ] Skill XP deltas and **`survey_drone_kit`** in bag match freeze table.
|
||||||
|
- [ ] Godot restart + duplicate accepts leave economy counts unchanged (no double-claim).
|
||||||
|
- [ ] Epic 7 Slice 2 AC satisfied in Godot.
|
||||||
|
|
@ -296,6 +296,8 @@ Combat intro bundle is **skill XP only** — encounter loot already granted **`c
|
||||||
- [ ] Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim.
|
- [ ] Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim.
|
||||||
- [ ] Re-read [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing).
|
- [ ] Re-read [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing).
|
||||||
|
|
||||||
|
**Landed ([NEO-132](https://linear.app/neon-sprawl/issue/NEO-132)):** capstone manual QA [`NEO-132`](../../manual-qa/NEO-132.md); [client README — End-to-end quest reward loop (NEO-132)](../../client/README.md#end-to-end-quest-reward-loop-neo-132); economy HUD refresh on quest completion transition; plan [NEO-132](../../plans/NEO-132-implementation-plan.md).
|
||||||
|
|
||||||
**Client counterpart:** this story **is** the Slice 2 client capstone.
|
**Client counterpart:** this story **is** the Slice 2 client capstone.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# NEO-132 — Implementation plan
|
||||||
|
|
||||||
|
## Story reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|--------|--------|
|
||||||
|
| **Key** | NEO-132 |
|
||||||
|
| **Title** | E7M2-09: Playable quest reward delivery capstone (Godot) |
|
||||||
|
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-132/e7m2-09-playable-quest-reward-delivery-capstone-godot |
|
||||||
|
| **Module** | [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-09** |
|
||||||
|
| **Branch** | `NEO-132-playable-quest-reward-delivery-capstone` |
|
||||||
|
| **Client deps** | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) quest completion reward HUD (**Done** on `main`); [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) Slice 1 capstone flow; [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) quest progress HUD |
|
||||||
|
| **Server deps** | [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124)–[NEO-130](https://linear.app/neon-sprawl/issue/NEO-130) reward spine (**landed on `main`**) — catalog, delivery store, router, quest-state wiring, HTTP **`completionRewardSummary`** |
|
||||||
|
| **Pattern** | Capstone integration — [NEO-123](NEO-123-implementation-plan.md) docs + manual QA primary; minimal gap-fill only if capstone QA fails on `main` |
|
||||||
|
| **Blocked by** | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) (**Done**) |
|
||||||
|
| **Server counterpart** | NEO-124–NEO-130 server spine; Bruno-only verification is not prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) |
|
||||||
|
|
||||||
|
## Kickoff clarifications
|
||||||
|
|
||||||
|
| Topic | Question | Agent recommendation | Answer |
|
||||||
|
|--------|----------|----------------------|--------|
|
||||||
|
| **Implementation scope** | Docs-only vs client code? | **Docs + manual QA primary**; fix integration gaps only if capstone QA fails on `main` — [NEO-123](NEO-123-implementation-plan.md) precedent; NEO-131 deferred economy HUD refresh to this capstone. | **Adopted** — docs-primary |
|
||||||
|
| **Capstone path** | Extend full NEO-123 vs shorter reward-focused path? | **Extend full NEO-123 four-quest flow** — add **`QuestRewardDeliveryLabel`** + economy HUD verification at each completion; covers skill-only, item+skill, and idempotency across all grant kinds in the freeze table. | **Adopted** — full NEO-123 extend |
|
||||||
|
|
||||||
|
**Additional defaults (no kickoff question — settled by backlog / landed code):**
|
||||||
|
|
||||||
|
- **Session baseline:** fresh **server restart** before Godot **F5** — resets in-memory quest progress, **`IRewardDeliveryStore`**, inventory, skill progression, encounter state, and resource-node depletion; zero Bruno/curl in main checklist.
|
||||||
|
- **Economy HUD:** toggle **on** for inventory + skill verification (NEO-75 / NEO-123 precedent).
|
||||||
|
- **Reward HUD:** transition-only paint per NEO-131 — boot with already-completed quests does not replay grant copy.
|
||||||
|
- **Encounter loot vs quest bundles:** combat pocket **`scrap_metal_bulk` ×10** + **`contract_handoff_token` ×1** remain on **`EncounterCompleteLabel`** (NEO-110); quest completion bundles are skill XP and/or **`survey_drone_kit`** on **`QuestRewardDeliveryLabel`** (E7M2 freeze).
|
||||||
|
- **Server changes:** none unless capstone QA exposes an authoritative bug (unlikely — covered by server reward router tests).
|
||||||
|
|
||||||
|
## Goal, scope, and out-of-scope
|
||||||
|
|
||||||
|
**Goal:** Prove Epic 7 Slice 2 acceptance **in Godot**: quest completion grants items + skill XP **once**; idempotent on replay; satisfies [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing) and [Definition of Done](../decomposition/epics/epic_07_quest_faction.md#epic-7-definition-of-done).
|
||||||
|
|
||||||
|
**In scope (from Linear + [E7M2-09](E7M2-prototype-backlog.md#e7m2-09--playable-quest-reward-delivery-capstone-godot)):**
|
||||||
|
|
||||||
|
- **`docs/manual-qa/NEO-132.md`**: numbered **single-session** capstone — extend [NEO-123](../manual-qa/NEO-123.md) four-quest flow; at each quest **`completed`** transition verify **`QuestRewardDeliveryLabel`**, **`SkillProgressionLabel`**, and **`InventoryLabel`** (where applicable); **idempotency** via Godot restart + duplicate accept keys with economy counts unchanged.
|
||||||
|
- **`client/README.md`**: **End-to-end quest reward loop (NEO-132)** section — integration flow table; cross-links NEO-124–NEO-131 and Slice 1 capstone.
|
||||||
|
- **Module alignment** (on story completion): update [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) status, [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M2 row, [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M2 note, and [E7M2-prototype-backlog.md](E7M2-prototype-backlog.md) E7M2-09 checkboxes; mark Epic 7 Slice 2 client capstone complete.
|
||||||
|
- **Integration fixes only if capstone QA fails** on current `main` wiring (economy HUD stale after quest completion, reward label gap, idempotency breach, etc.).
|
||||||
|
|
||||||
|
**Out of scope (from Linear + backlog):**
|
||||||
|
|
||||||
|
- Encounter loot migration; faction reputation; quest VFX art.
|
||||||
|
- Bruno-only verification as prototype-complete proof.
|
||||||
|
- New server HTTP routes or reward router logic beyond NEO-127/NEO-128.
|
||||||
|
- Skill-definitions client; unlock/flag grant kinds (deferred pre-production).
|
||||||
|
|
||||||
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
|
- [ ] Human completes **`docs/manual-qa/NEO-132.md`** with server + client.
|
||||||
|
- [ ] Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim.
|
||||||
|
- [ ] Re-read [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing).
|
||||||
|
|
||||||
|
## Implementation reconciliation (shipped)
|
||||||
|
|
||||||
|
- **`docs/manual-qa/NEO-132.md`** — four-quest capstone extending NEO-123 with reward HUD + economy verification + idempotency steps.
|
||||||
|
- **`client/README.md`** — **End-to-end quest reward loop (NEO-132)** section with flow table and cross-links.
|
||||||
|
- **`client/scripts/quest_hud_controller.gd`** — **`quest_completion_reward_transition`** signal + **`_refresh_economy_hud()`** on in-session completion (inventory + skill GET).
|
||||||
|
- **`client/scripts/main.gd`** — passes **`InventoryClient`** + **`SkillProgressionClient`** into quest HUD **`setup`**.
|
||||||
|
- **`docs/manual-qa/NEO-123.md`**, **`docs/manual-qa/NEO-131.md`** — cross-links to NEO-132 capstone.
|
||||||
|
- **`docs/plans/E7M2-prototype-backlog.md`**, **`E7_M2_RewardAndUnlockRouter.md`**, **`documentation_and_implementation_alignment.md`**, **`module_dependency_register.md`** — E7M2-09 landed; E7.M2 **Ready**; Epic 7 Slice 2 client capstone complete.
|
||||||
|
|
||||||
|
## Technical approach
|
||||||
|
|
||||||
|
### 1. Reward freeze reference (E7M2-01)
|
||||||
|
|
||||||
|
| Quest id | Quest completion bundle (server) | Expected **`QuestRewardDeliveryLabel`** | Economy HUD asserts |
|
||||||
|
|----------|----------------------------------|----------------------------------------|---------------------|
|
||||||
|
| `prototype_quest_gather_intro` | `salvage` **+25** XP | `Salvage +25 XP` | **`SkillProgressionLabel`**: salvage XP increases by **25** |
|
||||||
|
| `prototype_quest_combat_intro` | `salvage` **+25** XP | `Salvage +25 XP` | salvage XP **+25** (cumulative **50** after gather + combat) |
|
||||||
|
| `prototype_quest_refine_intro` | `refine` **+25** XP | `Refine +25 XP` | refine XP **+25** |
|
||||||
|
| `prototype_quest_operator_chain` | `survey_drone_kit` ×1 + `salvage` **+50** XP | `Survey Drone Kit ×1` + `Salvage +50 XP` | bag contains **`survey_drone_kit`**; salvage XP **+50** (cumulative **100** if all four completed in one session) |
|
||||||
|
|
||||||
|
**Idempotency key (server):** `{playerId}:quest_complete:{questId}` — replay **`completed`** status skips router apply ([NEO-128](NEO-128-implementation-plan.md)).
|
||||||
|
|
||||||
|
### 2. Capstone manual QA script (`docs/manual-qa/NEO-132.md`)
|
||||||
|
|
||||||
|
**Preconditions:** Same as [NEO-123](../manual-qa/NEO-123.md) — fresh server restart, Godot gameplay only, **`Economy HUD`** on.
|
||||||
|
|
||||||
|
**Flow:** Inherit NEO-123 accept order (**Q** gather → **Shift+Q** combat → **Shift+Q** refine → **Shift+Q** operator chain), material math, and combat/gather anchors. **Extend** each completion step with:
|
||||||
|
|
||||||
|
1. **`QuestRewardDeliveryLabel`** — grant lines on in-session transition only (NEO-131).
|
||||||
|
2. **`SkillProgressionLabel`** — record salvage/refine XP after each completion (note baseline at boot).
|
||||||
|
3. **`InventoryLabel`** — after operator chain completion, assert **`survey_drone_kit`** present (display name when item defs loaded).
|
||||||
|
|
||||||
|
**Idempotency (reward-specific):**
|
||||||
|
|
||||||
|
1. After all four quests **`completed`**, record final **`SkillProgressionLabel`** salvage/refine totals and bag rows (including **`survey_drone_kit`**).
|
||||||
|
2. **Godot restart** (server still running): reward label **`—`**; quest statuses still **`completed`**; economy counts **unchanged** from step 1.
|
||||||
|
3. **Duplicate accepts:** **Q** / **Shift+Q** deny per NEO-123; economy counts still unchanged.
|
||||||
|
|
||||||
|
**Regression pointers:** [NEO-131](../manual-qa/NEO-131.md) component reward HUD, [NEO-123](../manual-qa/NEO-123.md) Slice 1 capstone, [NEO-110](../manual-qa/NEO-110.md) encounter loot independence.
|
||||||
|
|
||||||
|
### 3. Client README (`client/README.md`)
|
||||||
|
|
||||||
|
Add **End-to-end quest reward loop (NEO-132)** section after [Quest completion reward HUD (NEO-131)](../../client/README.md#quest-completion-reward-hud-neo-131), mirroring NEO-123 capstone section:
|
||||||
|
|
||||||
|
- One-line Epic 7 Slice 2 capstone goal.
|
||||||
|
- Flow table: fresh restart → four-quest NEO-123 path → per-completion reward HUD + economy verification → idempotency.
|
||||||
|
- Cross-links: NEO-124 catalog freeze, NEO-126 delivery store, NEO-127 router, NEO-129 HTTP summary, NEO-131 HUD, NEO-123 onboarding flow.
|
||||||
|
- Pointer to **`docs/manual-qa/NEO-132.md`**.
|
||||||
|
|
||||||
|
### 4. Module / backlog alignment (on land)
|
||||||
|
|
||||||
|
When capstone QA passes:
|
||||||
|
|
||||||
|
- [E7M2-prototype-backlog.md](E7M2-prototype-backlog.md) — E7M2-09 AC checkboxes.
|
||||||
|
- [E7_M2_RewardAndUnlockRouter.md](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) — module status **Ready**; Slice 2 client capstone note.
|
||||||
|
- [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) — E7.M2 row: NEO-132 landed; Epic 7 Slice 2 client capstone complete.
|
||||||
|
- [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) — E7.M2 **Ready** when capstone lands.
|
||||||
|
|
||||||
|
### 5. Integration fixes (conditional)
|
||||||
|
|
||||||
|
Run capstone QA on `main` (or branch with NEO-131 merged). If any step fails:
|
||||||
|
|
||||||
|
| Symptom | Likely fix surface |
|
||||||
|
|---------|-------------------|
|
||||||
|
| **`SkillProgressionLabel`** stale after combat intro complete | `main.gd` — add skill GET refresh on quest-progress GET when combat quest completes (defeat path currently refreshes encounter + quest only) |
|
||||||
|
| **`InventoryLabel`** missing **`survey_drone_kit`** after operator chain | `main.gd` — refresh inventory on quest completion transition (operator chain completes via token check without new gather/craft) |
|
||||||
|
| Reward label wrong/missing | `quest_hud_controller.gd` — unlikely if NEO-131 GdUnit passes |
|
||||||
|
| Economy counts increase on Godot restart + replay | Server bug — file issue; client displays GET only |
|
||||||
|
| Duplicate delivery on replay | Server **`IRewardDeliveryStore`** / router — file issue |
|
||||||
|
|
||||||
|
Document any fix in **Implementation reconciliation** and add regression note to manual QA.
|
||||||
|
|
||||||
|
## Files to add
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `docs/plans/NEO-132-implementation-plan.md` | This plan. |
|
||||||
|
| `docs/manual-qa/NEO-132.md` | Single-session four-quest capstone with reward HUD + economy verification + idempotency steps. |
|
||||||
|
|
||||||
|
## Files to modify
|
||||||
|
|
||||||
|
| Path | Rationale |
|
||||||
|
|------|-----------|
|
||||||
|
| `client/README.md` | **End-to-end quest reward loop (NEO-132)** section with flow table and cross-links. |
|
||||||
|
| `docs/manual-qa/NEO-123.md` | Cross-link to NEO-132 Slice 2 capstone; note reward/economy verification defers to NEO-132. |
|
||||||
|
| `docs/manual-qa/NEO-131.md` | Cross-link to NEO-132 capstone as full-flow superset. |
|
||||||
|
| `docs/plans/E7M2-prototype-backlog.md` | E7M2-09 AC checkboxes when capstone lands. |
|
||||||
|
| `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | Module status **Ready** + capstone landed note on completion. |
|
||||||
|
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M2 row: NEO-132 landed; Slice 2 complete. |
|
||||||
|
| `docs/decomposition/modules/module_dependency_register.md` | E7.M2 **Ready** when capstone lands. |
|
||||||
|
|
||||||
|
**Conditional (only if capstone QA fails):** `client/scripts/main.gd` (economy HUD refresh on quest completion transition) — minimal gap-fill; document paths in reconciliation.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
| Test file | What it covers |
|
||||||
|
|-----------|----------------|
|
||||||
|
| `client/test/quest_reward_hud_test.gd` | **Add:** `quest_completion_reward_transition` emitted on in-session completion; not emitted on boot-completed snapshot. |
|
||||||
|
| *(server unchanged)* | Reward spine covered by **`RewardRouterOperationsTests`**, **`QuestStateOperationsTests`**, **`QuestProgressApiTests`**. Capstone proof is **`docs/manual-qa/NEO-132.md`** human QA. |
|
||||||
|
|
||||||
|
## Open questions / risks
|
||||||
|
|
||||||
|
| Question / risk | Agent recommendation | Status |
|
||||||
|
|-----------------|----------------------|--------|
|
||||||
|
| **Economy HUD auto-refresh gaps** | Add **`quest_completion_reward_transition`** signal + **`main.gd`** inventory/skill refresh — combat and operator-chain paths lacked auto-refresh. | **adopted** |
|
||||||
|
| **NEO-123 vs NEO-132 relationship** | NEO-132 **supersedes** as Slice 2 capstone for reward idempotency; keep NEO-123 as Slice 1 regression. | **adopted** |
|
||||||
|
| **Manual I key in QA script** | Prefer asserting auto-refresh after each completion; use **I** only as documented workaround if docs-primary QA passes with manual refresh and auto-fix is out of scope. | **deferred** |
|
||||||
|
| **Cumulative skill XP math** | Script records **delta** per completion from **`SkillProgressionLabel`**, not absolute totals — avoids coupling to unrelated XP sources. | **adopted** |
|
||||||
|
| **Docs-only fails capstone** | Economy refresh hook landed proactively; capstone QA confirms end-to-end. | **adopted** |
|
||||||
Loading…
Reference in New Issue