extends Control class_name OutfittingScreen ## Gladiator outfitting screen shown on the main menu. ## Left: the armory shop (buy and equip gear with banked gold). ## Right: the character sheet in preview mode, live-updating as you change loadout. ## Reads/writes GameState (banked_gold, owned_items, selected_loadout). const WEAPONS_DIR := "res://level/resources/" # Catalog of WeaponData found on disk, sorted by tier then cost var _catalog: Array[WeaponData] = [] var _catalog_paths: Dictionary = {} # WeaponData -> resource path var _gold_label: Label = null var _items_container: VBoxContainer = null var _sheet: Control = null # CharacterSheet in preview mode var _nick_input: LineEdit = null # menu's nickname field (for preview name) func _ready(): _load_catalog() _build_ui() GameState.gold_changed.connect(func(_g): _refresh()) GameState.loadout_changed.connect(func(_l): _refresh()) _refresh() ## The menu's nickname LineEdit, so the preview shows the entered name func set_nick_input(nick_input: LineEdit): _nick_input = nick_input if _nick_input: _nick_input.text_changed.connect(func(_t): _update_preview()) # --- Catalog -------------------------------------------------------------- func _load_catalog(): _catalog.clear() _catalog_paths.clear() var dir = DirAccess.open(WEAPONS_DIR) if not dir: push_error("[Outfitting] Cannot open " + WEAPONS_DIR) return for file in dir.get_files(): # Exported builds list resources as .tres.remap var file_name = file.trim_suffix(".remap") if not file_name.ends_with(".tres"): continue var path = WEAPONS_DIR + file_name var res = load(path) if res is WeaponData: _catalog.append(res) _catalog_paths[res] = path _catalog.sort_custom(func(a, b): if a.tier != b.tier: return a.tier < b.tier return a.cost < b.cost ) print("[Outfitting] Loaded ", _catalog.size(), " shop items") # --- UI construction -------------------------------------------------------- func _build_ui(): var hbox = HBoxContainer.new() hbox.set_anchors_preset(Control.PRESET_FULL_RECT) hbox.add_theme_constant_override("separation", 16) add_child(hbox) # ----- Shop panel (left) ----- var shop_panel = PanelContainer.new() shop_panel.custom_minimum_size = Vector2(420, 0) shop_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL shop_panel.size_flags_stretch_ratio = 0.85 hbox.add_child(shop_panel) var shop_margin = MarginContainer.new() shop_margin.add_theme_constant_override("margin_left", 16) shop_margin.add_theme_constant_override("margin_right", 16) shop_margin.add_theme_constant_override("margin_top", 12) shop_margin.add_theme_constant_override("margin_bottom", 12) shop_panel.add_child(shop_margin) var shop_vbox = VBoxContainer.new() shop_vbox.add_theme_constant_override("separation", 8) shop_margin.add_child(shop_vbox) var shop_title = Label.new() shop_title.text = "THE ARMORY" shop_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER shop_title.add_theme_font_size_override("font_size", 26) shop_title.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0)) shop_vbox.add_child(shop_title) _gold_label = Label.new() _gold_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _gold_label.add_theme_font_size_override("font_size", 20) _gold_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2)) shop_vbox.add_child(_gold_label) var hint = Label.new() hint.text = "Buy gear, equip it, then enter the arena.\nDie and the arena takes 90% of everything you're worth." hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER hint.add_theme_font_size_override("font_size", 13) hint.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7)) shop_vbox.add_child(hint) shop_vbox.add_child(HSeparator.new()) var scroll = ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED shop_vbox.add_child(scroll) _items_container = VBoxContainer.new() _items_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL _items_container.add_theme_constant_override("separation", 6) scroll.add_child(_items_container) # ----- Character sheet preview (right) ----- var sheet_scene = load("res://level/ui/scenes/character_sheet.tscn") if sheet_scene: _sheet = sheet_scene.instantiate() _sheet.preview_mode = true # Must be set before _ready runs _sheet.size_flags_horizontal = Control.SIZE_EXPAND_FILL _sheet.size_flags_stretch_ratio = 1.0 _sheet.custom_minimum_size = Vector2(480, 0) hbox.add_child(_sheet) else: push_error("[Outfitting] Failed to load character_sheet.tscn") # --- Refresh --------------------------------------------------------------- func _refresh(): if _gold_label: _gold_label.text = "Gold: " + str(GameState.banked_gold) _rebuild_items() _update_preview() func _rebuild_items(): if not _items_container: return for child in _items_container.get_children(): child.queue_free() for weapon in _catalog: _items_container.add_child(_make_item_row(weapon)) func _make_item_row(weapon: WeaponData) -> Control: var path: String = _catalog_paths[weapon] var owned := GameState.owns_item(path) var selected := GameState.is_loadout_item_selected(path) var row = PanelContainer.new() var margin = MarginContainer.new() margin.add_theme_constant_override("margin_left", 8) margin.add_theme_constant_override("margin_right", 8) margin.add_theme_constant_override("margin_top", 6) margin.add_theme_constant_override("margin_bottom", 6) row.add_child(margin) var hbox = HBoxContainer.new() hbox.add_theme_constant_override("separation", 10) margin.add_child(hbox) # Name + stat line var info = VBoxContainer.new() info.size_flags_horizontal = Control.SIZE_EXPAND_FILL hbox.add_child(info) var name_label = Label.new() name_label.text = weapon.weapon_name + (" [" + _hand_label(weapon) + "]") name_label.add_theme_font_size_override("font_size", 16) name_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4) if selected else (Color(0.85, 0.85, 1.0) if owned else Color.WHITE)) info.add_child(name_label) var stats_label = Label.new() var stat_bits: Array[String] = ["DMG " + str(weapon.damage), "CD " + str(weapon.attack_cooldown) + "s"] if weapon.can_block: stat_bits.append("Block " + str(int(weapon.block_reduction * 100)) + "%") stats_label.text = " | ".join(stat_bits) stats_label.add_theme_font_size_override("font_size", 12) stats_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6)) info.add_child(stats_label) # Action button var button = Button.new() button.custom_minimum_size = Vector2(120, 36) if not owned: button.text = str(weapon.cost) + "g Buy" button.disabled = not GameState.can_afford(weapon.cost) button.pressed.connect(_on_buy_pressed.bind(path, weapon.cost)) elif selected: button.text = "Equipped" button.pressed.connect(_on_unequip_pressed.bind(weapon)) else: button.text = "Equip" button.pressed.connect(_on_equip_pressed.bind(path)) hbox.add_child(button) return row func _hand_label(weapon: WeaponData) -> String: match weapon.hand_type: WeaponData.Hand.OFF_HAND: return "Off-Hand" WeaponData.Hand.TWO_HAND: return "Two-Hand" _: return "Main Hand" # --- Actions ---------------------------------------------------------------- func _on_buy_pressed(path: String, cost: int): if GameState.buy_item(path, cost): # Auto-equip fresh purchases - you bought it to use it GameState.equip_loadout_item(path) # _refresh happens via gold_changed/loadout_changed signals func _on_equip_pressed(path: String): GameState.equip_loadout_item(path) func _on_unequip_pressed(weapon: WeaponData): var slot = "offhand" if weapon.hand_type == WeaponData.Hand.OFF_HAND else "main" GameState.unequip_loadout_slot(slot) # --- Preview ---------------------------------------------------------------- func _update_preview(): if not _sheet: return var loadout = GameState.get_selected_loadout() var main_data = null var offhand_data = null if loadout.get("main", "") != "": main_data = load(loadout["main"]) if loadout.get("offhand", "") != "": offhand_data = load(loadout["offhand"]) var nick = "Gladiator" if _nick_input and _nick_input.text.strip_edges() != "": nick = _nick_input.text.strip_edges() _sheet.set_preview({ "nick": nick, "main": main_data, "offhand": offhand_data, })