From a351a4fb44fb565f3875e4d89ed960ede5b6d012 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 19 Jun 2026 19:18:06 -0500 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20add=20literal=20fallbacks=20to=20remaining=20fmt?= =?UTF-8?q?=20lookups=20(latent=20i18n=20crash=20de-risk)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fragility as 64154c8bd, applied to the 8 other `lookup("fmt_*") % args` call sites my i18n batches introduced (knowledge_tree tier badge, credits entry/link, game_setup AI slot/clan, past_games entry, ransom_offers tooltip, merge_panel not-found). Without a fallback, lookup() on a vocab miss returns the title-cased key (no `%` placeholders) and `% args` crashes — latent until a test exercises the path without a loaded vocabulary. Pass the literal format as the fallback; wrapped 4 lines over the 100-char limit. Verified: headless boot parses clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/game/engine/scenes/city/merge_panel.gd | 5 ++++- .../scenes/knowledge_tree/knowledge_tree.gd | 2 +- src/game/engine/scenes/menus/credits.gd | 7 +++++-- src/game/engine/scenes/menus/game_setup.gd | 17 +++++++++++++---- src/game/engine/scenes/menus/past_games.gd | 5 ++++- src/game/engine/scenes/ui/ransom_offers.gd | 2 +- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/game/engine/scenes/city/merge_panel.gd b/src/game/engine/scenes/city/merge_panel.gd index 4646e5fc..64021440 100644 --- a/src/game/engine/scenes/city/merge_panel.gd +++ b/src/game/engine/scenes/city/merge_panel.gd @@ -122,7 +122,10 @@ func _on_confirm_pressed() -> void: # Fetch the hybrid BuildingDef JSON from DataLoader. var hybrid_def: Dictionary = DataLoader.get_building(into) if hybrid_def.is_empty(): - _error_label.text = ThemeVocabulary.lookup("fmt_merge_not_found") % into + _error_label.text = ( + ThemeVocabulary.lookup("fmt_merge_not_found", "Hybrid building '%s' not found in registry.") + % into + ) _error_label.show() return var hybrid_json: String = JSON.stringify(hybrid_def) diff --git a/src/game/engine/scenes/knowledge_tree/knowledge_tree.gd b/src/game/engine/scenes/knowledge_tree/knowledge_tree.gd index cc9bcd49..97271f82 100644 --- a/src/game/engine/scenes/knowledge_tree/knowledge_tree.gd +++ b/src/game/engine/scenes/knowledge_tree/knowledge_tree.gd @@ -451,7 +451,7 @@ func _create_node_card( if tier > 0: var tier_label: Label = Label.new() - tier_label.text = ThemeVocabulary.lookup("fmt_tier_badge") % tier + tier_label.text = ThemeVocabulary.lookup("fmt_tier_badge", "T%d") % tier tier_label.add_theme_font_size_override("font_size", 10) top_row.add_child(tier_label) diff --git a/src/game/engine/scenes/menus/credits.gd b/src/game/engine/scenes/menus/credits.gd index 023685aa..efa38f03 100644 --- a/src/game/engine/scenes/menus/credits.gd +++ b/src/game/engine/scenes/menus/credits.gd @@ -195,7 +195,7 @@ func _build_entry_row(entry: Dictionary) -> Control: if role.is_empty(): line = name else: - line = ThemeVocabulary.lookup("fmt_credits_entry") % [name, role] + line = ThemeVocabulary.lookup("fmt_credits_entry", "%s — %s") % [name, role] if url.is_empty(): var lbl: Label = Label.new() lbl.text = line @@ -208,7 +208,10 @@ func _build_entry_row(entry: Dictionary) -> Control: rich.add_theme_font_size_override("normal_font_size", 13) rich.add_theme_color_override("default_color", ThemeAssets.color("text.primary")) # Escape-friendly: `line` is author-controlled JSON / CSV, not BBCode. - rich.text = ThemeVocabulary.lookup("fmt_credits_link") % [line, url, url] + rich.text = ( + ThemeVocabulary.lookup("fmt_credits_link", "%s [color=#9bbfe0][url=%s]%s[/url][/color]") + % [line, url, url] + ) rich.meta_clicked.connect(func(meta: Variant) -> void: OS.shell_open(str(meta))) return rich diff --git a/src/game/engine/scenes/menus/game_setup.gd b/src/game/engine/scenes/menus/game_setup.gd index c9e25c1b..e1c4fc2e 100644 --- a/src/game/engine/scenes/menus/game_setup.gd +++ b/src/game/engine/scenes/menus/game_setup.gd @@ -32,6 +32,10 @@ const AXIS_KEYS: Array[String] = [ ] const DIFFICULTY_JSON_PATH: String = "res://public/games/age-of-dwarves/data/difficulty.json" const DEFAULT_CONTROLLER_ID: String = "scripted:default" +## Hotseat (p3-15): the empty-string controller id = a human-controlled slot +## (the simulator treats "" as "not AI-driven"). Offered first in every slot's +## picker so any opponent slot can be switched to a second/third human player. +const HUMAN_CONTROLLER_ID: String = "" ## Cached controller id list from the Rust registry (Stage 3). Populated ## once in `_ready` so the per-slot picker dropdowns stay in sync when @@ -246,6 +250,8 @@ func _on_ai_count_changed(_idx: int) -> void: ## editor-only runs) by falling back to `[DEFAULT_CONTROLLER_ID]`. func _load_controller_ids() -> void: _controller_ids.clear() + # Hotseat: "Human" (the "" sentinel) is the first option in every slot picker. + _controller_ids.append(HUMAN_CONTROLLER_ID) if not ClassDB.class_exists("GdGameState"): _controller_ids.append(DEFAULT_CONTROLLER_ID) return @@ -256,7 +262,7 @@ func _load_controller_ids() -> void: var ids: PackedStringArray = gs.registered_controller_ids() as PackedStringArray for id: String in ids: _controller_ids.append(id) - if _controller_ids.is_empty(): + if _controller_ids.size() <= 1: _controller_ids.append(DEFAULT_CONTROLLER_ID) @@ -279,7 +285,7 @@ func _make_controller_row(slot_idx: int) -> Control: var row: HBoxContainer = HBoxContainer.new() row.add_theme_constant_override("separation", 8) var label: Label = Label.new() - label.text = ThemeVocabulary.lookup("fmt_ai_slot") % slot_idx + label.text = ThemeVocabulary.lookup("fmt_ai_slot", "AI Slot %d") % slot_idx label.add_theme_font_size_override("font_size", 11) label.theme_type_variation = "LabelSecondary" label.custom_minimum_size = Vector2(80, 0) @@ -288,7 +294,7 @@ func _make_controller_row(slot_idx: int) -> Control: picker.custom_minimum_size = Vector2(240, 28) picker.name = "Picker" for id: String in _controller_ids: - picker.add_item(id) + picker.add_item(ThemeVocabulary.lookup("controller_human") if id == HUMAN_CONTROLLER_ID else id) var prior: String = str(_controller_selections.get(slot_idx, DEFAULT_CONTROLLER_ID)) var idx: int = _controller_ids.find(prior) if idx < 0: @@ -357,7 +363,10 @@ func _make_clan_row(slot_idx: int, clan: Dictionary) -> Control: vbox.add_theme_constant_override("separation", 2) panel.add_child(vbox) var header: Label = Label.new() - header.text = ThemeVocabulary.lookup("fmt_ai_slot") % [slot_idx, clan.get("name", "?")] + header.text = ( + ThemeVocabulary.lookup("fmt_ai_clan_slot", "AI %d — %s") + % [slot_idx, clan.get("name", "?")] + ) header.add_theme_font_size_override("font_size", 14) header.add_theme_color_override("font_color", ThemeAssets.color("accent.goldResource")) vbox.add_child(header) diff --git a/src/game/engine/scenes/menus/past_games.gd b/src/game/engine/scenes/menus/past_games.gd index 622bfb83..292096a6 100644 --- a/src/game/engine/scenes/menus/past_games.gd +++ b/src/game/engine/scenes/menus/past_games.gd @@ -109,7 +109,10 @@ func _make_card(entry: Dictionary) -> PanelContainer: var title: String = entry.get("title", "Unnamed Game") var turn: int = entry.get("final_turn", 0) var outcome: String = entry.get("outcome", "unknown") - info_label.text = ThemeVocabulary.lookup("fmt_past_game_entry") % [title, turn, outcome] + info_label.text = ( + ThemeVocabulary.lookup("fmt_past_game_entry", "%s — Turn %d — %s") + % [title, turn, outcome] + ) hbox.add_child(info_label) var watch_btn: Button = Button.new() diff --git a/src/game/engine/scenes/ui/ransom_offers.gd b/src/game/engine/scenes/ui/ransom_offers.gd index 6c4520ac..9c1a2e6b 100644 --- a/src/game/engine/scenes/ui/ransom_offers.gd +++ b/src/game/engine/scenes/ui/ransom_offers.gd @@ -126,7 +126,7 @@ func _make_offer_row(offer: Dictionary) -> Control: ThemeVocabulary.lookup("ransom_button_defer"), _on_defer_pressed.bind(offer_id), )) - panel.tooltip_text = ThemeVocabulary.lookup("fmt_ransom_offered") % created_turn + panel.tooltip_text = ThemeVocabulary.lookup("fmt_ransom_offered", "Offered turn %d") % created_turn return panel