extends Control ## NEO-74: scrollable prototype recipe list with per-row Craft buttons. signal craft_requested(recipe_id: String) const PrototypeHudTheme := preload("res://scripts/prototype_hud_theme.gd") const HudThemeConstants := preload("res://scripts/prototype_hud_theme_constants.gd") var _item_defs_client: Node = null var _craft_buttons: Array[Button] = [] @onready var _scroll: ScrollContainer = $ScrollContainer @onready var _vbox: VBoxContainer = $ScrollContainer/VBoxContainer func _ready() -> void: if is_instance_valid(_scroll): _scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO resized.connect(_sync_vbox_width) func setup(item_defs_client: Node) -> void: _item_defs_client = item_defs_client func populate(recipes: Array) -> void: if not is_instance_valid(_vbox): return for child in _vbox.get_children(): child.queue_free() _craft_buttons.clear() for row_variant in recipes: if not row_variant is Dictionary: continue var recipe: Dictionary = row_variant var recipe_id: String = str(recipe.get("id", "")) if recipe_id.is_empty(): continue var row := VBoxContainer.new() row.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_theme_constant_override("separation", 4) var info := Label.new() info.size_flags_horizontal = Control.SIZE_EXPAND_FILL info.text = _format_recipe_line(recipe) _apply_recipe_row_label(info) row.add_child(info) var btn_row := HBoxContainer.new() btn_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL btn_row.alignment = BoxContainer.ALIGNMENT_END var craft_btn := Button.new() craft_btn.text = "Craft" PrototypeHudTheme.apply_to_button(craft_btn, false) craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id)) btn_row.add_child(craft_btn) row.add_child(btn_row) _vbox.add_child(row) _craft_buttons.append(craft_btn) call_deferred("_reset_scroll_after_populate") func set_craft_busy(busy: bool) -> void: for btn in _craft_buttons: if is_instance_valid(btn): btn.disabled = busy func _format_recipe_line(recipe: Dictionary) -> String: var recipe_id: String = str(recipe.get("id", "")) var display_name: String = str(recipe.get("displayName", recipe_id)) var inputs_line: String = _format_inputs(recipe.get("inputs", [])) return "%s (%s)\nneeds: %s" % [display_name, recipe_id, inputs_line] func _format_inputs(raw_inputs: Variant) -> String: if raw_inputs == null or not raw_inputs is Array: return "—" var parts: PackedStringArray = PackedStringArray() for row_variant in raw_inputs as Array: if not row_variant is Dictionary: continue var row: Dictionary = row_variant var item_id: String = str(row.get("itemId", "")) var qty: int = int(row.get("quantity", 0)) if item_id.is_empty() or qty <= 0: continue parts.append("%d× %s" % [qty, _item_display_name(item_id)]) if parts.is_empty(): return "—" return ", ".join(parts) func _item_display_name(item_id: String) -> String: if is_instance_valid(_item_defs_client) and _item_defs_client.has_method("display_name_for"): return str(_item_defs_client.call("display_name_for", item_id)) return item_id func _on_craft_pressed(recipe_id: String) -> void: craft_requested.emit(recipe_id) func _apply_recipe_row_label(label: Label) -> void: label.add_theme_font_size_override("font_size", HudThemeConstants.FONT_SIZE_SECONDARY) label.add_theme_constant_override("outline_size", HudThemeConstants.OUTLINE_SIZE) label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART func _sync_vbox_width() -> void: if not is_instance_valid(_scroll) or not is_instance_valid(_vbox): return var width: float = _scroll.size.x if width <= 0.0: return _vbox.custom_minimum_size.x = width func _reset_scroll_after_populate() -> void: _sync_vbox_width() if is_instance_valid(_scroll): _scroll.scroll_vertical = 0