Turns the sandbox into a survival roguelike loop: outfit your gladiator with banked gold, enter the arena, earn gold from kills and wave clears, die, keep 10% of your total value (floored at the 500 fresh-start). Economy (GameState autoload): - banked_gold, owned items, and loadout persist to user://save.cfg - Kill gold (basic 10g, armed 25g) credited server-side to the killer - Wave-clear bonus (25 + 5/wave) for all players; wave synced to peers - Death settlement liquidates gear, guarded against double-fire Character creation / shop: - Menu reframed: armory shop + live Character Sheet loadout preview - Character Sheet gains preview mode (renders from loadout, no player) - Buy/Equip with hand rules; two-handers and off-hands displace each other symmetrically (shop and in-game pickups) - Chosen loadout auto-equips on spawn via existing RPC path Combat feel: - Attacks snap to camera facing and lock direction for the swing - Dash commits to its direction for the full duration - Weapon slots show name text when no icon is set - Hitbox/hurtbox debug meshes hidden by default (H toggles) Death is final: players no longer respawn; a results screen shows waves, time, kills, and the settlement, then returns to camp. Armed enemies no longer respawn (were an infinite gold farm and respawned invisible).
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,
|
|
})
|