NEO-142: add faction standing client and HUD wiring
Introduce faction-standing GET client, standing label in HudRoot, readable faction_gate_blocked accept feedback, reputationGrants reward lines, and GdUnit tests.pull/182/head
parent
fb9a8ab8b2
commit
c8c706f839
|
|
@ -23,6 +23,7 @@
|
|||
[ext_resource type="Script" path="res://scripts/encounter_progress_client.gd" id="24_enc_prog"]
|
||||
[ext_resource type="Script" uid="uid://bneo122qstprog01" path="res://scripts/quest_progress_client.gd" id="25_quest_prog"]
|
||||
[ext_resource type="Script" uid="uid://bneo122qstdefs01" path="res://scripts/quest_definitions_client.gd" id="26_quest_defs"]
|
||||
[ext_resource type="Script" uid="uid://bneo142factionst01" path="res://scripts/faction_standing_client.gd" id="27_faction_standing"]
|
||||
[ext_resource type="PackedScene" path="res://assets/district/prototype_district_environment.glb" id="22_district_env"]
|
||||
[ext_resource type="Script" path="res://scripts/prototype_district_art.gd" id="23_district_art"]
|
||||
|
||||
|
|
@ -1126,6 +1127,9 @@ script = ExtResource("25_quest_prog")
|
|||
[node name="QuestDefinitionsClient" type="Node" parent="." unique_id=2500014]
|
||||
script = ExtResource("26_quest_defs")
|
||||
|
||||
[node name="FactionStandingClient" type="Node" parent="." unique_id=2500015]
|
||||
script = ExtResource("27_faction_standing")
|
||||
|
||||
[node name="RecipeDefinitionsClient" type="Node" parent="." unique_id=2500009]
|
||||
script = ExtResource("17_recipe_defs")
|
||||
|
||||
|
|
@ -1271,6 +1275,18 @@ autowrap_mode = 3
|
|||
text = "Quest rewards:
|
||||
—"
|
||||
|
||||
[node name="FactionStandingLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000028]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_colors/font_color = Color(0.85, 0.88, 0.98, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.08, 0.08, 0.1, 1)
|
||||
theme_override_constants/outline_size = 8
|
||||
theme_override_font_sizes/font_size = 22
|
||||
autowrap_mode = 3
|
||||
text = "Faction standing:
|
||||
Grid Operators: 0
|
||||
Rust Collective: 0"
|
||||
|
||||
[node name="NpcStateLabel" type="Label" parent="UICanvas/HudRootScroll/HudRoot" unique_id=9000020]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
extends Node
|
||||
|
||||
## NEO-142: prototype HTTP client for [code]GET /game/players/{id}/faction-standing[/code] (NEO-139).
|
||||
|
||||
signal faction_standing_received(snapshot: Dictionary)
|
||||
signal faction_standing_sync_failed(reason: String)
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const GRID_OPERATORS_ID := "prototype_faction_grid_operators"
|
||||
const RUST_COLLECTIVE_ID := "prototype_faction_rust_collective"
|
||||
|
||||
const _DISPLAY_NAMES: Dictionary = {
|
||||
GRID_OPERATORS_ID: "Grid Operators",
|
||||
RUST_COLLECTIVE_ID: "Rust Collective",
|
||||
}
|
||||
|
||||
@export var base_url: String = "http://127.0.0.1:5253"
|
||||
@export var dev_player_id: String = "dev-local-1"
|
||||
@export var injected_http: Node = null
|
||||
|
||||
var _http: Node
|
||||
var _busy: bool = false
|
||||
var _sync_pending: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if injected_http != null:
|
||||
_http = injected_http
|
||||
else:
|
||||
_http = HTTPRequest.new()
|
||||
add_child(_http)
|
||||
if _http is HTTPRequest:
|
||||
(_http as HTTPRequest).timeout = 30.0
|
||||
@warning_ignore("unsafe_method_access")
|
||||
_http.request_completed.connect(_on_request_completed)
|
||||
|
||||
|
||||
func request_sync_from_server() -> void:
|
||||
if _busy:
|
||||
_sync_pending = true
|
||||
return
|
||||
_start_sync_request()
|
||||
|
||||
|
||||
func _start_sync_request() -> void:
|
||||
_busy = true
|
||||
var url := "%s/game/players/%s/faction-standing" % [_base_root(), _player_path_segment()]
|
||||
var err: Error = _http.request(url)
|
||||
if err != OK:
|
||||
var reason := "GET failed to start (%s)" % err
|
||||
push_warning("FactionStandingClient: %s" % reason)
|
||||
_busy = false
|
||||
faction_standing_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()
|
||||
|
||||
|
||||
static func prototype_faction_ids_in_order() -> Array:
|
||||
return [GRID_OPERATORS_ID, RUST_COLLECTIVE_ID]
|
||||
|
||||
|
||||
static func display_name_for(faction_id: String) -> String:
|
||||
var fid := faction_id.strip_edges()
|
||||
if _DISPLAY_NAMES.has(fid):
|
||||
return str(_DISPLAY_NAMES[fid])
|
||||
return _title_case_token(fid)
|
||||
|
||||
|
||||
static func standing_for(faction_id: String, snapshot: Dictionary = {}) -> int:
|
||||
var row: Dictionary = faction_row(faction_id, snapshot)
|
||||
if row.is_empty():
|
||||
return 0
|
||||
return int(row.get("standing", 0))
|
||||
|
||||
|
||||
static func faction_row(faction_id: String, snapshot: Dictionary = {}) -> Dictionary:
|
||||
var factions: Variant = snapshot.get("factions", null)
|
||||
if factions == null or not factions is Array:
|
||||
return {}
|
||||
var fid := faction_id.strip_edges()
|
||||
for row_variant in factions as Array:
|
||||
if not row_variant is Dictionary:
|
||||
continue
|
||||
var row: Dictionary = row_variant
|
||||
if str(row.get("id", "")) == fid:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
static func parse_faction_standing_json(text: String) -> Variant:
|
||||
var parsed: Variant = JSON.parse_string(text)
|
||||
if not parsed is Dictionary:
|
||||
return null
|
||||
var root: Dictionary = parsed
|
||||
if int(root.get("schemaVersion", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if str(root.get("playerId", "")).strip_edges().is_empty():
|
||||
return null
|
||||
var factions: Variant = root.get("factions", null)
|
||||
if factions == null or not factions is Array:
|
||||
return null
|
||||
return root
|
||||
|
||||
|
||||
static func _title_case_token(token: String) -> String:
|
||||
var trimmed := token.strip_edges()
|
||||
if trimmed.is_empty():
|
||||
return trimmed
|
||||
return trimmed.substr(0, 1).to_upper() + trimmed.substr(1)
|
||||
|
||||
|
||||
func _base_root() -> String:
|
||||
return base_url.strip_edges().rstrip("/")
|
||||
|
||||
|
||||
func _player_path_segment() -> String:
|
||||
return dev_player_id.strip_edges()
|
||||
|
||||
|
||||
func _on_request_completed(
|
||||
result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
|
||||
) -> void:
|
||||
_busy = false
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
var reason := "HTTP failed (result=%s)" % result
|
||||
push_warning("FactionStandingClient: %s" % reason)
|
||||
faction_standing_sync_failed.emit(reason)
|
||||
_try_flush_pending_sync()
|
||||
return
|
||||
if response_code == 404:
|
||||
var reason404 := "HTTP 404 (player unknown)"
|
||||
push_warning("FactionStandingClient: %s" % reason404)
|
||||
faction_standing_sync_failed.emit(reason404)
|
||||
_try_flush_pending_sync()
|
||||
return
|
||||
if response_code < 200 or response_code >= 300:
|
||||
var reason_code := "HTTP %s" % response_code
|
||||
push_warning("FactionStandingClient: %s" % reason_code)
|
||||
faction_standing_sync_failed.emit(reason_code)
|
||||
_try_flush_pending_sync()
|
||||
return
|
||||
var text := body.get_string_from_utf8()
|
||||
var snapshot: Variant = parse_faction_standing_json(text)
|
||||
if snapshot == null:
|
||||
var reason_json := "non-JSON body or schemaVersion mismatch"
|
||||
push_warning("FactionStandingClient: %s" % reason_json)
|
||||
faction_standing_sync_failed.emit(reason_json)
|
||||
_try_flush_pending_sync()
|
||||
return
|
||||
faction_standing_received.emit(snapshot as Dictionary)
|
||||
_try_flush_pending_sync()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bneo142factionst01
|
||||
|
|
@ -137,6 +137,7 @@ var _dev_pulse_bootstrap_attempted: bool = false
|
|||
@onready var _quest_progress_label: Label = _hud_root.get_node("QuestProgressLabel")
|
||||
@onready var _quest_accept_feedback_label: Label = _hud_root.get_node("QuestAcceptFeedbackLabel")
|
||||
@onready var _quest_reward_delivery_label: Label = _hud_root.get_node("QuestRewardDeliveryLabel")
|
||||
@onready var _faction_standing_label: Label = _hud_root.get_node("FactionStandingLabel")
|
||||
@onready var _npc_state_label: Label = _hud_root.get_node("NpcStateLabel")
|
||||
@onready var _telegraph_label: Label = _hud_root.get_node("TelegraphLabel")
|
||||
@onready var _cooldown_slots_label: Label = _hud_root.get_node("CooldownSlotsLabel")
|
||||
|
|
@ -155,6 +156,7 @@ var _dev_pulse_bootstrap_attempted: bool = false
|
|||
@onready var _encounter_progress_client: Node = $EncounterProgressClient
|
||||
@onready var _quest_progress_client: Node = $QuestProgressClient
|
||||
@onready var _quest_defs_client: Node = $QuestDefinitionsClient
|
||||
@onready var _faction_standing_client: Node = $FactionStandingClient
|
||||
@onready var _recipe_defs_client: Node = $RecipeDefinitionsClient
|
||||
@onready var _craft_client: Node = $CraftClient
|
||||
@onready var _target_client: Node = $TargetSelectionClient
|
||||
|
|
@ -867,7 +869,9 @@ func _setup_quest_progress_sync() -> void:
|
|||
_quest_reward_delivery_label,
|
||||
_item_defs_client,
|
||||
_inventory_client,
|
||||
_skill_progression_client
|
||||
_skill_progression_client,
|
||||
_faction_standing_client,
|
||||
_faction_standing_label
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,21 @@ func quests_snapshot() -> Array:
|
|||
return _quests.duplicate(true)
|
||||
|
||||
|
||||
func faction_gate_rules_for(quest_id: String) -> Array:
|
||||
var qid := quest_id.strip_edges()
|
||||
for row_variant in _quests:
|
||||
if not row_variant is Dictionary:
|
||||
continue
|
||||
var row: Dictionary = row_variant
|
||||
if str(row.get("id", "")) != qid:
|
||||
continue
|
||||
var rules: Variant = row.get("factionGateRules", null)
|
||||
if rules is Array:
|
||||
return (rules as Array).duplicate(true)
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
func _base_root() -> String:
|
||||
return base_url.strip_edges().rstrip("/")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@ extends Node
|
|||
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
|
||||
## NEO-131: quest completion reward label on in-session completed transition.
|
||||
## NEO-132: emits quest_completion_reward_transition for economy HUD refresh.
|
||||
## NEO-142: faction standing label + readable faction_gate_blocked accept deny.
|
||||
|
||||
signal quest_completion_reward_transition(quest_id: String, summary: Dictionary)
|
||||
|
||||
const QuestProgressClient := preload("res://scripts/quest_progress_client.gd")
|
||||
const FactionStandingClient := preload("res://scripts/faction_standing_client.gd")
|
||||
const GATHER_QUEST_ID := "prototype_quest_gather_intro"
|
||||
const FACTION_GATE_BLOCKED_REASON := "faction_gate_blocked"
|
||||
const ACCEPT_IDLE_HINT := "Quest accept: — (Q gather / Shift+Q next)"
|
||||
const ACCEPT_SENDING_HINT := "Quest accept: sending…"
|
||||
const REWARD_HEADER := "Quest rewards:"
|
||||
const FACTION_STANDING_HEADER := "Faction standing:"
|
||||
|
||||
var _progress_client: Node = null
|
||||
var _defs_client: Node = null
|
||||
|
|
@ -28,6 +32,10 @@ var _last_reward_quest_id: String = ""
|
|||
var _last_reward_summary: Dictionary = {}
|
||||
var _inventory_client: Node = null
|
||||
var _skill_progression_client: Node = null
|
||||
var _faction_standing_client: Node = null
|
||||
var _faction_standing_label: Label = null
|
||||
var _last_faction_snapshot: Dictionary = {}
|
||||
var _faction_standing_error: String = ""
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -39,19 +47,25 @@ func setup(
|
|||
reward_label: Label = null,
|
||||
item_defs_client: Node = null,
|
||||
inventory_client: Node = null,
|
||||
skill_progression_client: Node = null
|
||||
skill_progression_client: Node = null,
|
||||
faction_standing_client: Node = null,
|
||||
faction_standing_label: Label = null
|
||||
) -> void:
|
||||
_progress_client = progress_client
|
||||
_defs_client = defs_client
|
||||
_item_defs_client = item_defs_client
|
||||
_inventory_client = inventory_client
|
||||
_skill_progression_client = skill_progression_client
|
||||
_faction_standing_client = faction_standing_client
|
||||
_faction_standing_label = faction_standing_label
|
||||
_progress_label = progress_label
|
||||
_accept_label = accept_label
|
||||
_reward_label = reward_label
|
||||
if apply_http_config.is_valid():
|
||||
apply_http_config.call(progress_client)
|
||||
apply_http_config.call(defs_client)
|
||||
if is_instance_valid(_faction_standing_client):
|
||||
apply_http_config.call(_faction_standing_client)
|
||||
if _progress_client.has_signal("quest_progress_received"):
|
||||
_progress_client.connect("quest_progress_received", Callable(self, "_on_progress_received"))
|
||||
if _progress_client.has_signal("quest_sync_failed"):
|
||||
|
|
@ -70,11 +84,22 @@ func setup(
|
|||
)
|
||||
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"))
|
||||
if is_instance_valid(_faction_standing_client):
|
||||
if _faction_standing_client.has_signal("faction_standing_received"):
|
||||
_faction_standing_client.connect(
|
||||
"faction_standing_received", Callable(self, "_on_faction_standing_received")
|
||||
)
|
||||
if _faction_standing_client.has_signal("faction_standing_sync_failed"):
|
||||
_faction_standing_client.connect(
|
||||
"faction_standing_sync_failed", Callable(self, "_on_faction_standing_sync_failed")
|
||||
)
|
||||
_render_progress_label()
|
||||
_render_accept_feedback_label()
|
||||
_render_reward_label()
|
||||
_render_faction_standing_label()
|
||||
if _defs_client.has_method("request_sync_from_server"):
|
||||
_defs_client.call("request_sync_from_server")
|
||||
_refresh_faction_standing()
|
||||
request_progress_refresh()
|
||||
|
||||
|
||||
|
|
@ -133,7 +158,9 @@ func _on_accept_result_received(quest_id: String, result: Dictionary) -> void:
|
|||
_render_progress_label()
|
||||
else:
|
||||
var rc := str(result.get("reasonCode", "")).strip_edges()
|
||||
if rc.is_empty():
|
||||
if rc == FACTION_GATE_BLOCKED_REASON:
|
||||
_render_accept_feedback_label(_format_faction_gate_blocked_deny(quest_id))
|
||||
elif rc.is_empty():
|
||||
_render_accept_feedback_label("Quest accept: denied (no reasonCode)")
|
||||
else:
|
||||
_render_accept_feedback_label("Quest accept: denied — %s" % rc)
|
||||
|
|
@ -217,6 +244,85 @@ func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void:
|
|||
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()
|
||||
_refresh_faction_standing()
|
||||
|
||||
|
||||
func _refresh_faction_standing() -> void:
|
||||
if (
|
||||
is_instance_valid(_faction_standing_client)
|
||||
and _faction_standing_client.has_method("request_sync_from_server")
|
||||
):
|
||||
_faction_standing_client.call("request_sync_from_server")
|
||||
|
||||
|
||||
func _on_faction_standing_received(snapshot: Dictionary) -> void:
|
||||
_faction_standing_error = ""
|
||||
_last_faction_snapshot = snapshot.duplicate(true)
|
||||
_render_faction_standing_label()
|
||||
|
||||
|
||||
func _on_faction_standing_sync_failed(reason: String) -> void:
|
||||
_faction_standing_error = reason
|
||||
_render_faction_standing_label()
|
||||
|
||||
|
||||
func _render_faction_standing_label() -> void:
|
||||
if not is_instance_valid(_faction_standing_label):
|
||||
return
|
||||
var lines: PackedStringArray = [FACTION_STANDING_HEADER]
|
||||
if not _faction_standing_error.is_empty():
|
||||
lines.append("error — %s" % _faction_standing_error)
|
||||
for faction_id in FactionStandingClient.prototype_faction_ids_in_order():
|
||||
var fid := str(faction_id)
|
||||
var standing: int = FactionStandingClient.standing_for(fid, _last_faction_snapshot)
|
||||
var label := FactionStandingClient.display_name_for(fid)
|
||||
lines.append(" %s: %d" % [label, standing])
|
||||
_faction_standing_label.text = "\n".join(lines)
|
||||
|
||||
|
||||
func _format_faction_gate_blocked_deny(quest_id: String) -> String:
|
||||
var rules: Array = _faction_gate_rules_for(quest_id)
|
||||
if rules.is_empty():
|
||||
return (
|
||||
"Quest accept: denied — faction standing too low (%s)" % FACTION_GATE_BLOCKED_REASON
|
||||
)
|
||||
var first_rule: Variant = rules[0]
|
||||
if not first_rule is Dictionary:
|
||||
return (
|
||||
"Quest accept: denied — faction standing too low (%s)" % FACTION_GATE_BLOCKED_REASON
|
||||
)
|
||||
var rule: Dictionary = first_rule
|
||||
var faction_id := str(rule.get("factionId", "")).strip_edges()
|
||||
var min_standing: int = int(rule.get("minStanding", 0))
|
||||
if faction_id.is_empty():
|
||||
return (
|
||||
"Quest accept: denied — faction standing too low (%s)" % FACTION_GATE_BLOCKED_REASON
|
||||
)
|
||||
var faction_label := _faction_display_name(faction_id)
|
||||
var current_standing: int = FactionStandingClient.standing_for(
|
||||
faction_id, _last_faction_snapshot
|
||||
)
|
||||
return (
|
||||
"Quest accept: denied — %s standing %d required (have %d)"
|
||||
% [faction_label, min_standing, current_standing]
|
||||
)
|
||||
|
||||
|
||||
func _faction_gate_rules_for(quest_id: String) -> Array:
|
||||
if not is_instance_valid(_defs_client):
|
||||
return []
|
||||
if not _defs_client.has_method("faction_gate_rules_for"):
|
||||
return []
|
||||
return _defs_client.call("faction_gate_rules_for", quest_id) as Array
|
||||
|
||||
|
||||
func _faction_display_name(faction_id: String) -> String:
|
||||
if (
|
||||
is_instance_valid(_faction_standing_client)
|
||||
and _faction_standing_client.has_method("display_name_for")
|
||||
):
|
||||
return str(_faction_standing_client.call("display_name_for", faction_id))
|
||||
return FactionStandingClient.display_name_for(faction_id)
|
||||
|
||||
|
||||
func _refresh_economy_hud() -> void:
|
||||
|
|
@ -335,6 +441,17 @@ func _format_reward_summary_lines(summary: Dictionary) -> PackedStringArray:
|
|||
if skill_id.is_empty() or amount <= 0:
|
||||
continue
|
||||
lines.append(" %s +%d XP" % [_title_case_token(skill_id), amount])
|
||||
var rep_variant: Variant = summary.get("reputationGrants", null)
|
||||
if rep_variant is Array:
|
||||
for grant_variant in rep_variant as Array:
|
||||
if not grant_variant is Dictionary:
|
||||
continue
|
||||
var grant: Dictionary = grant_variant
|
||||
var faction_id := str(grant.get("factionId", "")).strip_edges()
|
||||
var amount: int = int(grant.get("amount", 0))
|
||||
if faction_id.is_empty() or amount <= 0:
|
||||
continue
|
||||
lines.append(" %s +%d rep" % [_faction_display_name(faction_id), amount])
|
||||
if lines.is_empty():
|
||||
lines.append(" (no grants)")
|
||||
return lines
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
extends GdUnitTestSuite
|
||||
|
||||
## NEO-142: `FactionStandingClient` GET parse + failure signals.
|
||||
|
||||
const FactionStandingClient := preload("res://scripts/faction_standing_client.gd")
|
||||
|
||||
var _standing_capture: Dictionary = {}
|
||||
|
||||
|
||||
func _capture_standing(snapshot: Dictionary) -> void:
|
||||
_standing_capture = snapshot
|
||||
|
||||
|
||||
class MockHttpTransport:
|
||||
extends Node
|
||||
signal request_completed(
|
||||
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||||
)
|
||||
var last_url: String = ""
|
||||
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:
|
||||
last_url = url
|
||||
request_completed.emit(
|
||||
HTTPRequest.RESULT_SUCCESS,
|
||||
response_code,
|
||||
PackedStringArray(),
|
||||
body_json.to_utf8_buffer()
|
||||
)
|
||||
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 _default_standing_json() -> String:
|
||||
return (
|
||||
'{"schemaVersion":1,"playerId":"dev-local-1","factions":['
|
||||
+ '{"id":"prototype_faction_grid_operators","standing":0},'
|
||||
+ '{"id":"prototype_faction_rust_collective","standing":0}'
|
||||
+ "]}"
|
||||
)
|
||||
|
||||
|
||||
static func _post_operator_chain_standing_json() -> String:
|
||||
return (
|
||||
'{"schemaVersion":1,"playerId":"dev-local-1","factions":['
|
||||
+ '{"id":"prototype_faction_rust_collective","standing":0},'
|
||||
+ '{"id":"prototype_faction_grid_operators","standing":15}'
|
||||
+ "]}"
|
||||
)
|
||||
|
||||
|
||||
func _make_client(transport: Node) -> Node:
|
||||
var c: Node = FactionStandingClient.new()
|
||||
c.set("injected_http", transport)
|
||||
auto_free(transport)
|
||||
auto_free(c)
|
||||
add_child(c)
|
||||
return c
|
||||
|
||||
|
||||
func test_parse_faction_standing_json_reads_grid_operators_row() -> void:
|
||||
# Arrange
|
||||
var json := _post_operator_chain_standing_json()
|
||||
# Act
|
||||
var snapshot: Variant = FactionStandingClient.parse_faction_standing_json(json)
|
||||
# Assert
|
||||
assert_that(snapshot is Dictionary).is_true()
|
||||
assert_that(
|
||||
FactionStandingClient.standing_for(
|
||||
FactionStandingClient.GRID_OPERATORS_ID, snapshot as Dictionary
|
||||
)
|
||||
).is_equal(15)
|
||||
|
||||
|
||||
func test_parse_faction_standing_json_rejects_bad_schema() -> void:
|
||||
# Arrange
|
||||
var json := '{"schemaVersion":2,"playerId":"dev-local-1","factions":[]}'
|
||||
# Act
|
||||
var snapshot: Variant = FactionStandingClient.parse_faction_standing_json(json)
|
||||
# Assert
|
||||
assert_that(snapshot).is_null()
|
||||
|
||||
|
||||
func test_standing_for_defaults_to_zero_when_row_missing() -> void:
|
||||
# Arrange
|
||||
var snapshot := {"schemaVersion": 1, "playerId": "dev-local-1", "factions": []}
|
||||
# Act
|
||||
var standing: int = FactionStandingClient.standing_for(
|
||||
FactionStandingClient.GRID_OPERATORS_ID, snapshot
|
||||
)
|
||||
# Assert
|
||||
assert_that(standing).is_equal(0)
|
||||
|
||||
|
||||
func test_display_name_for_prototype_factions() -> void:
|
||||
# Arrange
|
||||
# Act
|
||||
var grid_label := FactionStandingClient.display_name_for(
|
||||
FactionStandingClient.GRID_OPERATORS_ID
|
||||
)
|
||||
var rust_label := FactionStandingClient.display_name_for(
|
||||
FactionStandingClient.RUST_COLLECTIVE_ID
|
||||
)
|
||||
# Assert
|
||||
assert_str(grid_label).is_equal("Grid Operators")
|
||||
assert_str(rust_label).is_equal("Rust Collective")
|
||||
|
||||
|
||||
func test_request_sync_emits_faction_standing_received() -> void:
|
||||
# Arrange
|
||||
_standing_capture = {}
|
||||
var transport := MockHttpTransport.new()
|
||||
transport.body_json = _default_standing_json()
|
||||
var c := _make_client(transport)
|
||||
c.connect("faction_standing_received", Callable(self, "_capture_standing"))
|
||||
monitor_signals(c)
|
||||
# Act
|
||||
c.call("request_sync_from_server")
|
||||
# Assert
|
||||
await assert_signal(c).is_emitted("faction_standing_received", any())
|
||||
assert_that(_standing_capture.has("factions")).is_true()
|
||||
assert_that(transport.last_url).contains("/faction-standing")
|
||||
|
||||
|
||||
func test_http_404_emits_faction_standing_sync_failed() -> void:
|
||||
# Arrange
|
||||
var transport := MockHttpTransport.new()
|
||||
transport.response_code = 404
|
||||
var c := _make_client(transport)
|
||||
monitor_signals(c)
|
||||
# Act
|
||||
c.call("request_sync_from_server")
|
||||
# Assert
|
||||
await assert_signal(c).is_emitted(
|
||||
"faction_standing_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 = _default_standing_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)
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
extends GdUnitTestSuite
|
||||
|
||||
## NEO-142: faction standing label + faction_gate_blocked accept deny HUD tests.
|
||||
|
||||
const QuestHudController := preload("res://scripts/quest_hud_controller.gd")
|
||||
const QuestDefinitionsClient := preload("res://scripts/quest_definitions_client.gd")
|
||||
const FactionStandingClient := preload("res://scripts/faction_standing_client.gd")
|
||||
|
||||
const GRID_CONTRACT_QUEST_ID := "prototype_quest_grid_contract"
|
||||
|
||||
|
||||
class MockHttpTransport:
|
||||
extends Node
|
||||
signal request_completed(
|
||||
result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray
|
||||
)
|
||||
var body_json: String = ""
|
||||
|
||||
func request(
|
||||
_url: String,
|
||||
_custom_headers: PackedStringArray = PackedStringArray(),
|
||||
_method: HTTPClient.Method = HTTPClient.Method.GET,
|
||||
_request_data: String = ""
|
||||
) -> Error:
|
||||
request_completed.emit(
|
||||
HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), body_json.to_utf8_buffer()
|
||||
)
|
||||
return OK
|
||||
|
||||
|
||||
class MockFactionSyncClient:
|
||||
extends Node
|
||||
|
||||
var sync_count: int = 0
|
||||
|
||||
func request_sync_from_server() -> void:
|
||||
sync_count += 1
|
||||
|
||||
func display_name_for(faction_id: String) -> String:
|
||||
return FactionStandingClient.display_name_for(faction_id)
|
||||
|
||||
|
||||
static func _post_operator_chain_standing_snapshot() -> Dictionary:
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"playerId": "dev-local-1",
|
||||
"factions":
|
||||
[
|
||||
{"id": FactionStandingClient.GRID_OPERATORS_ID, "standing": 15},
|
||||
{"id": FactionStandingClient.RUST_COLLECTIVE_ID, "standing": 0},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
static func _grid_contract_defs_json() -> String:
|
||||
return (
|
||||
'{"schemaVersion":1,"quests":['
|
||||
+ '{"id":"prototype_quest_grid_contract","displayName":"Grid Contract",'
|
||||
+ '"prerequisiteQuestIds":["prototype_quest_operator_chain"],'
|
||||
+ '"factionGateRules":[{"factionId":"prototype_faction_grid_operators","minStanding":15}],'
|
||||
+ '"steps":[]}]}'
|
||||
)
|
||||
|
||||
|
||||
func test_faction_standing_label_paints_grid_operators_standing() -> void:
|
||||
# Arrange
|
||||
var controller: Node = QuestHudController.new()
|
||||
var standing_label := Label.new()
|
||||
auto_free(controller)
|
||||
auto_free(standing_label)
|
||||
add_child(controller)
|
||||
controller.set("_faction_standing_label", standing_label)
|
||||
# Act
|
||||
controller.call("_on_faction_standing_received", _post_operator_chain_standing_snapshot())
|
||||
# Assert
|
||||
assert_str(standing_label.text).contains("Faction standing:")
|
||||
assert_str(standing_label.text).contains("Grid Operators: 15")
|
||||
assert_str(standing_label.text).contains("Rust Collective: 0")
|
||||
|
||||
|
||||
func test_faction_standing_sync_error_shows_error_line() -> void:
|
||||
# Arrange
|
||||
var controller: Node = QuestHudController.new()
|
||||
var standing_label := Label.new()
|
||||
auto_free(controller)
|
||||
auto_free(standing_label)
|
||||
add_child(controller)
|
||||
controller.set("_faction_standing_label", standing_label)
|
||||
# Act
|
||||
controller.call("_on_faction_standing_sync_failed", "HTTP 503")
|
||||
# Assert
|
||||
assert_str(standing_label.text).contains("error — HTTP 503")
|
||||
assert_str(standing_label.text).contains("Grid Operators: 0")
|
||||
|
||||
|
||||
func test_faction_gate_blocked_deny_uses_readable_gate_copy() -> void:
|
||||
# Arrange
|
||||
var defs_transport := MockHttpTransport.new()
|
||||
defs_transport.body_json = _grid_contract_defs_json()
|
||||
var defs_client: Node = QuestDefinitionsClient.new()
|
||||
defs_client.set("injected_http", defs_transport)
|
||||
auto_free(defs_transport)
|
||||
auto_free(defs_client)
|
||||
add_child(defs_client)
|
||||
defs_client.call("request_sync_from_server")
|
||||
var controller: Node = QuestHudController.new()
|
||||
var accept_label := Label.new()
|
||||
auto_free(controller)
|
||||
auto_free(accept_label)
|
||||
add_child(controller)
|
||||
controller.set("_defs_client", defs_client)
|
||||
controller.set("_accept_label", accept_label)
|
||||
controller.call(
|
||||
"_on_faction_standing_received", {"schemaVersion": 1, "playerId": "dev-local-1", "factions": []}
|
||||
)
|
||||
# Act
|
||||
var deny_text: String = str(
|
||||
controller.call("_format_faction_gate_blocked_deny", GRID_CONTRACT_QUEST_ID)
|
||||
)
|
||||
# Assert
|
||||
assert_str(deny_text).contains("Grid Operators standing 15 required (have 0)")
|
||||
|
||||
|
||||
func test_faction_gate_blocked_deny_falls_back_without_gate_rules() -> void:
|
||||
# Arrange
|
||||
var controller: Node = QuestHudController.new()
|
||||
auto_free(controller)
|
||||
add_child(controller)
|
||||
# Act
|
||||
var deny_text: String = str(
|
||||
controller.call("_format_faction_gate_blocked_deny", GRID_CONTRACT_QUEST_ID)
|
||||
)
|
||||
# Assert
|
||||
assert_str(deny_text).contains("faction standing too low")
|
||||
assert_str(deny_text).contains("faction_gate_blocked")
|
||||
|
||||
|
||||
func test_accept_result_received_paints_readable_faction_gate_blocked() -> void:
|
||||
# Arrange
|
||||
var defs_transport := MockHttpTransport.new()
|
||||
defs_transport.body_json = _grid_contract_defs_json()
|
||||
var defs_client: Node = QuestDefinitionsClient.new()
|
||||
defs_client.set("injected_http", defs_transport)
|
||||
auto_free(defs_transport)
|
||||
auto_free(defs_client)
|
||||
add_child(defs_client)
|
||||
defs_client.call("request_sync_from_server")
|
||||
var controller: Node = QuestHudController.new()
|
||||
var accept_label := Label.new()
|
||||
auto_free(controller)
|
||||
auto_free(accept_label)
|
||||
add_child(controller)
|
||||
controller.set("_defs_client", defs_client)
|
||||
controller.set("_accept_label", accept_label)
|
||||
controller.set("_progress_client", Node.new())
|
||||
controller.call(
|
||||
"_on_faction_standing_received", {"schemaVersion": 1, "playerId": "dev-local-1", "factions": []}
|
||||
)
|
||||
# Act
|
||||
controller.call(
|
||||
"_on_accept_result_received",
|
||||
GRID_CONTRACT_QUEST_ID,
|
||||
{"schemaVersion": 1, "accepted": false, "reasonCode": "faction_gate_blocked"}
|
||||
)
|
||||
# Assert
|
||||
assert_str(accept_label.text).contains("Grid Operators standing 15 required (have 0)")
|
||||
|
||||
|
||||
func test_completion_transition_requests_faction_standing_refresh() -> void:
|
||||
# Arrange
|
||||
var controller: Node = QuestHudController.new()
|
||||
var faction_client := MockFactionSyncClient.new()
|
||||
auto_free(controller)
|
||||
auto_free(faction_client)
|
||||
add_child(controller)
|
||||
add_child(faction_client)
|
||||
controller.set("_faction_standing_client", faction_client)
|
||||
var active_snapshot := {
|
||||
"schemaVersion": 1,
|
||||
"playerId": "dev-local-1",
|
||||
"quests":
|
||||
[
|
||||
{
|
||||
"questId": "prototype_quest_operator_chain",
|
||||
"status": "active",
|
||||
"currentStepIndex": 3,
|
||||
"objectiveCounters": {},
|
||||
}
|
||||
],
|
||||
}
|
||||
var completed_snapshot := {
|
||||
"schemaVersion": 1,
|
||||
"playerId": "dev-local-1",
|
||||
"quests":
|
||||
[
|
||||
{
|
||||
"questId": "prototype_quest_operator_chain",
|
||||
"status": "completed",
|
||||
"currentStepIndex": 3,
|
||||
"objectiveCounters": {},
|
||||
"completedAt": "2026-06-07T14:00:00Z",
|
||||
"completionRewardSummary":
|
||||
{
|
||||
"itemGrants": [],
|
||||
"skillXpGrants": [],
|
||||
"reputationGrants":
|
||||
[{"factionId": FactionStandingClient.GRID_OPERATORS_ID, "amount": 15}],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
# Act
|
||||
controller.call("_on_progress_received", active_snapshot)
|
||||
controller.call("_on_progress_received", completed_snapshot)
|
||||
# Assert
|
||||
assert_int(faction_client.sync_count).is_equal(1)
|
||||
|
|
@ -159,6 +159,13 @@ func _completed_operator_chain_with_summary_snapshot() -> Dictionary:
|
|||
{
|
||||
"itemGrants": [{"itemId": "survey_drone_kit", "quantity": 1}],
|
||||
"skillXpGrants": [{"skillId": "salvage", "amount": 50}],
|
||||
"reputationGrants":
|
||||
[
|
||||
{
|
||||
"factionId": "prototype_faction_grid_operators",
|
||||
"amount": 15,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
|
|
@ -235,6 +242,7 @@ func test_completion_transition_paints_item_grant_with_display_name() -> void:
|
|||
# Assert
|
||||
assert_str(reward_label.text).contains("Survey Drone Kit ×1")
|
||||
assert_str(reward_label.text).contains("Salvage +50 XP")
|
||||
assert_str(reward_label.text).contains("Grid Operators +15 rep")
|
||||
|
||||
|
||||
func test_definitions_ready_repaints_reward_label_with_display_name() -> void:
|
||||
|
|
|
|||
Loading…
Reference in New Issue