diff --git a/client/scripts/autoloads/ui_strings.gd b/client/scripts/autoloads/ui_strings.gd index 7c790909f..b7c2dd443 100644 --- a/client/scripts/autoloads/ui_strings.gd +++ b/client/scripts/autoloads/ui_strings.gd @@ -47,11 +47,11 @@ func reload() -> void: _loaded = false _load_strings() -## Parse subset of YAML: one-level-nested key-value pairs. -## Returns flat Dictionary with dotted keys: { "section.key": "value" }. +## Parse YAML with arbitrary nesting depth. +## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }. static func _parse_yaml(text: String) -> Dictionary: var strings := {} - var current_section := "" + var stack: Array = [] # [[indent, key], ...] for line in text.split("\n"): var stripped := line.strip_edges(false, true) if stripped.is_empty() or stripped.begins_with("#"): @@ -63,10 +63,30 @@ static func _parse_yaml(text: String) -> Dictionary: continue var key := content.substr(0, colon_pos).strip_edges() var val := content.substr(colon_pos + 1).strip_edges() - if val.length() >= 2 and val.begins_with("\"") and val.ends_with("\""): - val = val.substr(1, val.length() - 2) - if indent == 0: - current_section = key - elif indent >= 2 and not current_section.is_empty(): - strings[current_section + "." + key] = val + # Trailing comment without a value — treat as section header + if val.begins_with("#"): + val = "" + # Pop sections at same or deeper indent + while stack.size() > 0 and stack.back()[0] >= indent: + stack.pop_back() + if val.is_empty(): + # Section header — push onto stack + stack.push_back([indent, key]) + else: + # Leaf value — extract from quotes or strip inline comment + if val.begins_with("\""): + var end_quote := val.find("\"", 1) + if end_quote > 0: + val = val.substr(1, end_quote - 1) + else: + val = val.substr(1) + else: + var comment_pos := val.find(" #") + if comment_pos >= 0: + val = val.substr(0, comment_pos).strip_edges() + var dotted_key := "" + for entry in stack: + dotted_key += entry[1] + "." + dotted_key += key + strings[dotted_key] = val return strings diff --git a/client/tests/test_ui_strings.gd b/client/tests/test_ui_strings.gd index 9b7bea73c..68d60153a 100644 --- a/client/tests/test_ui_strings.gd +++ b/client/tests/test_ui_strings.gd @@ -54,19 +54,59 @@ func test_parse_multiple_keys_per_section() -> void: assert_that(result["hud.health"]).is_equal("Health") +# -- YAML parser: nested sections -- + +func test_parse_nested_sections() -> void: + var yaml := "states:\n unknown:\n label: \"Unknown\"\n description: \"No prior contact.\"" + var result := UIStrings._parse_yaml(yaml) + assert_that(result.has("states.unknown.label")).is_true() + assert_that(result["states.unknown.label"]).is_equal("Unknown") + assert_that(result["states.unknown.description"]).is_equal("No prior contact.") + +func test_parse_nested_multiple_subsections() -> void: + var yaml := "states:\n a:\n x: \"1\"\n b:\n x: \"2\"" + var result := UIStrings._parse_yaml(yaml) + assert_that(result["states.a.x"]).is_equal("1") + assert_that(result["states.b.x"]).is_equal("2") + +func test_parse_mixed_flat_and_nested() -> void: + var yaml := "hud:\n health: \"Condition\"\n health_values:\n full: \"Normal\"\n critical: \"Critical\"" + var result := UIStrings._parse_yaml(yaml) + assert_that(result["hud.health"]).is_equal("Condition") + assert_that(result["hud.health_values.full"]).is_equal("Normal") + assert_that(result["hud.health_values.critical"]).is_equal("Critical") + +func test_parse_inline_comment_quoted() -> void: + var yaml := "hud:\n prefix: \"\" # No prefix" + var result := UIStrings._parse_yaml(yaml) + assert_that(result.has("hud.prefix")).is_true() + assert_that(result["hud.prefix"]).is_equal("") + +func test_parse_inline_comment_unquoted() -> void: + var yaml := "hud:\n mode: Standard # default mode" + var result := UIStrings._parse_yaml(yaml) + assert_that(result["hud.mode"]).is_equal("Standard") + +func test_parse_quoted_empty_string() -> void: + var yaml := "section:\n key: \"\"" + var result := UIStrings._parse_yaml(yaml) + assert_that(result.has("section.key")).is_true() + assert_that(result["section.key"]).is_equal("") + + # -- Autoload: registration and file loading -- func test_autoload_registered() -> void: assert_that(UIStrings).is_not_null() func test_autoload_loaded_strings() -> void: - assert_that(UIStrings.has_key("hud.mode_label")).is_true() + assert_that(UIStrings.has_key("hud.health")).is_true() func test_get_text_returns_value() -> void: - assert_that(UIStrings.get_text("hud.mode_label")).is_equal("Mode") + assert_that(UIStrings.get_text("hud.health")).is_equal("Condition") -func test_get_text_interaction_prefix() -> void: - assert_that(UIStrings.get_text("interaction.prompt_prefix")).is_equal("E") +func test_get_text_interaction_verb() -> void: + assert_that(UIStrings.get_text("interaction_verbs.talk")).is_equal("Talk") func test_get_text_missing_key_returns_key() -> void: var result := UIStrings.get_text("nonexistent.key") @@ -76,7 +116,13 @@ func test_get_all_keys_not_empty() -> void: assert_that(UIStrings.get_all_keys().size()).is_greater(0) func test_has_key_true_for_existing() -> void: - assert_that(UIStrings.has_key("interaction.talk")).is_true() + assert_that(UIStrings.has_key("interaction_verbs.talk")).is_true() func test_has_key_false_for_missing() -> void: assert_that(UIStrings.has_key("nonexistent.key")).is_false() + +func test_nested_key_exists() -> void: + assert_that(UIStrings.has_key("relationship_states.unknown.label")).is_true() + +func test_nested_key_value() -> void: + assert_that(UIStrings.get_text("relationship_states.unknown.label")).is_equal("Unknown") diff --git a/client/ui/hud.gd b/client/ui/hud.gd index ea4d1590b..ba4bf70c6 100644 --- a/client/ui/hud.gd +++ b/client/ui/hud.gd @@ -13,12 +13,20 @@ func _ready() -> void: func update_from_hud_data(data: Dictionary) -> void: # Update perception mode display if data.has("perception_mode"): - perception_label.text = UIStrings.get_text("hud.mode_label") + ": " + data.perception_mode.capitalize() + var prefix := UIStrings.get_text("hud.perception_mode_prefix") + if prefix.is_empty(): + perception_label.text = data.perception_mode.capitalize() + else: + perception_label.text = prefix + ": " + data.perception_mode.capitalize() # Update time display (per D-031) if data.has("time"): - time_label.text = UIStrings.get_text("hud.time_label") + ": " + data.time + var prefix := UIStrings.get_text("hud.time_prefix") + if prefix.is_empty(): + time_label.text = data.time + else: + time_label.text = prefix + ": " + data.time # Update player health (from player data, not hud_data) func update_health(health: int) -> void: - health_label.text = UIStrings.get_text("hud.health_label") + ": " + str(health) + health_label.text = UIStrings.get_text("hud.health") + ": " + str(health) diff --git a/client/ui/interaction_prompt.gd b/client/ui/interaction_prompt.gd index 9975c4109..dfa569d0c 100644 --- a/client/ui/interaction_prompt.gd +++ b/client/ui/interaction_prompt.gd @@ -40,7 +40,7 @@ func _show_prompt(interaction: Dictionary) -> void: # v0.1: pick first verb (sorted by priority from server) var verb_label: String = verbs[0].get("label", "") - var display_text: String = "%s - %s" % [UIStrings.get_text("interaction.prompt_prefix"), verb_label] + var display_text: String = "E - %s" % verb_label if _current_target_id != target_id or prompt_label.text != display_text: prompt_label.text = display_text