249 lines
8.2 KiB
GDScript
249 lines
8.2 KiB
GDScript
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,
|
||
|
|
})
|