Gladiator arena: gold economy, outfitting shop, permadeath runs

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).
This commit is contained in:
2026-07-01 23:38:59 +01:00
parent 97ebbb1618
commit 1562b37563
25 changed files with 1023 additions and 43 deletions
+148 -32
View File
@@ -11,15 +11,40 @@ class_name CharacterSheet
# Player reference
var player: Character = null
# Preview mode: renders from a loadout dictionary instead of a live player.
# Used by the outfitting screen at character creation.
# preview_data keys: "nick" (String), "main" (WeaponData or null), "offhand" (WeaponData or null)
var preview_mode: bool = false
var preview_data: Dictionary = {}
# Character defaults shown in preview (match player.gd export defaults)
const PREVIEW_MAX_HEALTH := 100.0
const PREVIEW_ATTACK_DAMAGE := 10.0
const PREVIEW_ATTACK_RANGE := 3.0
const PREVIEW_ATTACK_COOLDOWN := 0.5
const PREVIEW_DASH_COOLDOWN := 4.0
const PREVIEW_DASH_DURATION := 0.25
const PREVIEW_DASH_MULTIPLIER := 2.0
# Visibility
var is_visible: bool = false
func _ready():
# Start hidden
hide()
is_visible = false
if preview_mode:
_configure_preview_layout()
show()
is_visible = true
refresh_all()
else:
# Start hidden (in-game Tab toggle behavior)
hide()
is_visible = false
func _input(event):
# Embedded preview never toggles - it's part of the outfitting screen
if preview_mode:
return
# Close on Escape press if open
if event.is_action_pressed("quit") and is_visible:
toggle_sheet()
@@ -37,6 +62,35 @@ func set_player(p: Character):
if player:
refresh_all()
## Feed the sheet a loadout to preview (outfitting screen).
## Safe to call before or after the sheet enters the tree.
func set_preview(data: Dictionary):
preview_mode = true
preview_data = data
if is_inside_tree():
refresh_all()
## Re-shape the fullscreen overlay into an embeddable panel:
## no dark backdrop, panel fills the space its parent gives it.
func _configure_preview_layout():
mouse_filter = Control.MOUSE_FILTER_IGNORE
var backdrop = get_node_or_null("DarkBackground")
if backdrop:
backdrop.hide()
var panel = get_node_or_null("Panel")
if panel:
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.offset_left = 0
panel.offset_top = 0
panel.offset_right = 0
panel.offset_bottom = 0
var title = get_node_or_null("Panel/MarginContainer/VBoxContainer/TitleLabel")
if title:
title.text = "YOUR GLADIATOR"
var close_hint = get_node_or_null("Panel/MarginContainer/VBoxContainer/CloseHint")
if close_hint:
close_hint.hide()
## Toggle character sheet visibility
func toggle_sheet():
is_visible = !is_visible
@@ -53,7 +107,7 @@ func toggle_sheet():
## Refresh all data in the sheet
func refresh_all():
if not player:
if not player and not preview_mode:
return
_refresh_stats()
@@ -81,11 +135,40 @@ func _refresh_stats():
spacer1.custom_minimum_size = Vector2(0, 10)
stats_container.add_child(spacer1)
# Get player name
var player_id = player.name.to_int()
var player_name = "Player"
if Network.players.has(player_id):
player_name = Network.players[player_id]["nick"]
# Resolve stats from either the live player or the preview loadout
var player_name: String
var current_hp: int
var max_hp: int
var atk_damage: float
var atk_range: float
var atk_cooldown: float
if preview_mode:
player_name = preview_data.get("nick", "Gladiator")
if player_name.strip_edges() == "":
player_name = "Gladiator"
current_hp = int(PREVIEW_MAX_HEALTH)
max_hp = int(PREVIEW_MAX_HEALTH)
# Combat stats come from the selected main weapon, or bare fists
var main_weapon = preview_data.get("main")
if main_weapon is WeaponData:
atk_damage = main_weapon.damage
atk_range = main_weapon.attack_range
atk_cooldown = main_weapon.attack_cooldown
else:
atk_damage = PREVIEW_ATTACK_DAMAGE
atk_range = PREVIEW_ATTACK_RANGE
atk_cooldown = PREVIEW_ATTACK_COOLDOWN
else:
var player_id = player.name.to_int()
player_name = "Player"
if Network.players.has(player_id):
player_name = Network.players[player_id]["nick"]
current_hp = int(player.current_health)
max_hp = int(player.max_health)
atk_damage = player.attack_damage
atk_range = player.attack_range
atk_cooldown = player.attack_cooldown
_add_stat_label("Name: " + player_name)
_add_stat_label("Level: 1") # Hardcoded for now
@@ -93,23 +176,35 @@ func _refresh_stats():
# Health stats
_add_stat_label("=== HEALTH ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Current HP: " + str(int(player.current_health)))
_add_stat_label("Max HP: " + str(int(player.max_health)))
_add_stat_label("Health: " + str(int(player.get_health_percent() * 100)) + "%")
_add_stat_label("Current HP: " + str(current_hp))
_add_stat_label("Max HP: " + str(max_hp))
var hp_percent = int(float(current_hp) / max_hp * 100) if max_hp > 0 else 0
_add_stat_label("Health: " + str(hp_percent) + "%")
_add_stat_label("")
# Movement stats
# Movement stats (class constants - same for live and preview)
_add_stat_label("=== MOVEMENT ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Walk Speed: " + str(player.NORMAL_SPEED))
_add_stat_label("Sprint Speed: " + str(player.SPRINT_SPEED))
_add_stat_label("Jump Power: " + str(player.JUMP_VELOCITY))
_add_stat_label("Walk Speed: " + str(Character.NORMAL_SPEED))
_add_stat_label("Sprint Speed: " + str(Character.SPRINT_SPEED))
_add_stat_label("Jump Power: " + str(Character.JUMP_VELOCITY))
_add_stat_label("")
# Combat stats
_add_stat_label("=== COMBAT ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Base Damage: " + str(player.attack_damage))
_add_stat_label("Attack Range: " + str(player.attack_range))
_add_stat_label("Attack Cooldown: " + str(player.attack_cooldown) + "s")
if preview_mode and preview_data.get("main") is WeaponData:
_add_stat_label("Weapon Damage: " + str(atk_damage))
else:
_add_stat_label("Base Damage: " + str(atk_damage))
_add_stat_label("Attack Range: " + str(atk_range))
_add_stat_label("Attack Cooldown: " + str(atk_cooldown) + "s")
# Off-hand block bonus (preview only - handy when choosing a shield)
if preview_mode:
var offhand = preview_data.get("offhand")
if offhand is WeaponData and offhand.can_block:
_add_stat_label("")
_add_stat_label("=== DEFENSE ===", Color(0.8, 0.8, 0.8))
_add_stat_label("Block Reduction: " + str(int(offhand.block_reduction * 100)) + "%")
## Refresh equipped weapons
func _refresh_weapons():
@@ -143,12 +238,26 @@ func _refresh_weapons():
push_error("[CharacterSheet] Failed to load weapon_slot.tscn")
return
# Resolve weapon data from preview loadout or live player
var main_data: WeaponData = null
var offhand_data: WeaponData = null
if preview_mode:
if preview_data.get("main") is WeaponData:
main_data = preview_data["main"]
if preview_data.get("offhand") is WeaponData:
offhand_data = preview_data["offhand"]
else:
if player.equipped_weapon and player.equipped_weapon.weapon_data:
main_data = player.equipped_weapon.weapon_data
if player.equipped_offhand and player.equipped_offhand.weapon_data:
offhand_data = player.equipped_offhand.weapon_data
# Create main hand slot
var main_hand_slot = weapon_slot_scene.instantiate()
slots_container.add_child(main_hand_slot)
if player.equipped_weapon and player.equipped_weapon.weapon_data:
main_hand_slot.set_weapon(player.equipped_weapon.weapon_data, "Main Hand")
if main_data:
main_hand_slot.set_weapon(main_data, "Main Hand")
else:
main_hand_slot.clear_weapon()
@@ -156,8 +265,8 @@ func _refresh_weapons():
var off_hand_slot = weapon_slot_scene.instantiate()
slots_container.add_child(off_hand_slot)
if player.equipped_offhand and player.equipped_offhand.weapon_data:
off_hand_slot.set_weapon(player.equipped_offhand.weapon_data, "Off-Hand")
if offhand_data:
off_hand_slot.set_weapon(offhand_data, "Off-Hand")
else:
off_hand_slot.clear_weapon()
@@ -182,17 +291,24 @@ func _refresh_abilities():
spacer.custom_minimum_size = Vector2(0, 10)
abilities_container.add_child(spacer)
# Dash ability
# Dash ability (defaults in preview, live values in game)
var dash_cd = PREVIEW_DASH_COOLDOWN if preview_mode else player.dash_cooldown
var dash_dur = PREVIEW_DASH_DURATION if preview_mode else player.dash_duration
var dash_mult = PREVIEW_DASH_MULTIPLIER if preview_mode else player.dash_speed_multiplier
_add_stat_label("--- DASH (F) ---", Color(0.8, 0.8, 0.8))
_add_stat_label("Cooldown: " + str(player.dash_cooldown) + "s")
_add_stat_label("Duration: " + str(player.dash_duration) + "s")
_add_stat_label("Speed: " + str(player.dash_speed_multiplier) + "x")
_add_stat_label("Cooldown: " + str(dash_cd) + "s")
_add_stat_label("Duration: " + str(dash_dur) + "s")
_add_stat_label("Speed: " + str(dash_mult) + "x")
_add_stat_label("Description: Dash in movement direction")
var dash_remaining = player._dash_cooldown_timer
if dash_remaining > 0:
_add_stat_label("Ready in: " + str(ceil(dash_remaining)) + "s", Color(1.0, 0.5, 0.5))
else:
if preview_mode:
_add_stat_label("Status: READY", Color(0.0, 1.0, 0.0))
else:
var dash_remaining = player._dash_cooldown_timer
if dash_remaining > 0:
_add_stat_label("Ready in: " + str(ceil(dash_remaining)) + "s", Color(1.0, 0.5, 0.5))
else:
_add_stat_label("Status: READY", Color(0.0, 1.0, 0.0))
# Spacer
var spacer2 = Control.new()
@@ -201,7 +317,7 @@ func _refresh_abilities():
# Jump ability
_add_stat_label("--- JUMP (Space) ---", Color(0.8, 0.8, 0.8))
_add_stat_label("Power: " + str(player.JUMP_VELOCITY))
_add_stat_label("Power: " + str(Character.JUMP_VELOCITY))
_add_stat_label("Description: Jump into the air")
_add_stat_label("Status: ALWAYS READY", Color(0.0, 1.0, 0.0))
+36 -2
View File
@@ -10,6 +10,7 @@ var character_sheet: Control = null
var tab_hint: Control = null
var keybind_hint: Control = null
var escape_menu: Control = null
var run_results: Control = null
# Player reference
var local_player: Character = null
@@ -157,6 +158,26 @@ func show_hud():
func hide_hud():
hide()
## Tear down all UI components (used when returning to the menu after a run).
## The HUD is an autoload, so its children survive scene reloads unless freed.
func reset():
if local_player and is_instance_valid(local_player):
_disconnect_player_signals()
local_player = null
for child in get_children():
child.queue_free()
action_bar = null
unit_frame = null
target_frame = null
character_sheet = null
tab_hint = null
keybind_hint = null
escape_menu = null
run_results = null
print("[HUD] Reset - all UI components removed")
## Player signal callbacks
func _on_player_health_changed(old_health: float, new_health: float):
# Update unit frame
@@ -164,8 +185,21 @@ func _on_player_health_changed(old_health: float, new_health: float):
unit_frame.update_health(new_health, local_player.max_health)
func _on_player_died(killer_id: int):
print("[HUD] Player died")
# Could show death screen or respawn timer here
print("[HUD] Player died - ending run")
_show_run_results()
## End the local player's run and show the results screen.
## GameState.end_run() is guarded against double-calls (died can fire twice on host).
func _show_run_results():
if run_results and is_instance_valid(run_results):
return # Already showing
var stats = GameState.end_run()
var results_script = load("res://level/ui/scripts/run_results.gd")
run_results = results_script.new()
run_results.setup(stats)
add_child(run_results)
func _on_player_respawned():
print("[HUD] Player respawned")
+248
View File
@@ -0,0 +1,248 @@
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,
})
@@ -0,0 +1 @@
uid://ngicph2qsf0m
+134
View File
@@ -0,0 +1,134 @@
extends Control
class_name RunResults
## End-of-run results screen for the gladiator arena.
## Shown by the HUD when the local player dies. Displays survival stats and
## the death settlement (keep 10% of total value, floored at the fresh-start
## default), then returns the player to the menu/camp.
var stats: Dictionary = {}
func setup(run_stats: Dictionary):
stats = run_stats
func _ready():
# Full-screen dark backdrop
set_anchors_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
var backdrop = ColorRect.new()
backdrop.color = Color(0.0, 0.0, 0.0, 0.75)
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(backdrop)
# Centered panel
var center = CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
var panel = PanelContainer.new()
panel.custom_minimum_size = Vector2(460, 0)
center.add_child(panel)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 30)
margin.add_theme_constant_override("margin_right", 30)
margin.add_theme_constant_override("margin_top", 24)
margin.add_theme_constant_override("margin_bottom", 24)
panel.add_child(margin)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
margin.add_child(vbox)
# Title
var title = Label.new()
title.text = "YOU HAVE FALLEN"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 36)
title.add_theme_color_override("font_color", Color(0.9, 0.2, 0.2))
vbox.add_child(title)
var subtitle = Label.new()
subtitle.text = "The arena claims another gladiator"
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
subtitle.add_theme_font_size_override("font_size", 16)
subtitle.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
vbox.add_child(subtitle)
_add_spacer(vbox, 12)
# Survival stats
_add_header(vbox, "=== SURVIVAL ===")
_add_stat_row(vbox, "Waves Survived", str(int(stats.get("waves", 0))))
_add_stat_row(vbox, "Time in Arena", _format_time(stats.get("time", 0.0)))
_add_stat_row(vbox, "Kills", str(int(stats.get("kills", 0))))
_add_spacer(vbox, 12)
# Settlement
_add_header(vbox, "=== SETTLEMENT ===")
_add_stat_row(vbox, "Gold Earned This Run", str(int(stats.get("gold_earned", 0))))
_add_stat_row(vbox, "Total Value at Death", str(int(stats.get("total_value", 0))))
var kept = int(stats.get("kept", 0))
var kept_row = Label.new()
kept_row.text = "The arena takes its cut... you keep " + str(kept) + " gold"
kept_row.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
kept_row.add_theme_font_size_override("font_size", 18)
kept_row.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
vbox.add_child(kept_row)
_add_spacer(vbox, 16)
# Return button
var return_button = Button.new()
return_button.text = "Return to Camp"
return_button.custom_minimum_size = Vector2(0, 44)
return_button.pressed.connect(_on_return_pressed)
vbox.add_child(return_button)
# Make sure the mouse is usable on the overlay
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _add_header(parent: Control, text: String):
var label = Label.new()
label.text = text
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 16)
label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
parent.add_child(label)
func _add_stat_row(parent: Control, label_text: String, value_text: String):
var row = HBoxContainer.new()
var label = Label.new()
label.text = label_text
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.add_theme_font_size_override("font_size", 16)
row.add_child(label)
var value = Label.new()
value.text = value_text
value.add_theme_font_size_override("font_size", 16)
value.add_theme_color_override("font_color", Color(1.0, 0.8, 0.0))
row.add_child(value)
parent.add_child(row)
func _add_spacer(parent: Control, height: int):
var spacer = Control.new()
spacer.custom_minimum_size = Vector2(0, height)
parent.add_child(spacer)
func _format_time(seconds: float) -> String:
var total = int(seconds)
return "%dm %02ds" % [total / 60, total % 60]
func _on_return_pressed():
# Tear down the multiplayer session and go back to the menu.
# (Host quitting ends the session for connected clients - acceptable for now.)
multiplayer.multiplayer_peer = null
Network.players.clear()
if has_node("/root/HUD"):
get_node("/root/HUD").reset()
get_tree().reload_current_scene()
+1
View File
@@ -0,0 +1 @@
uid://i1s8oc7snqnm
+36
View File
@@ -5,6 +5,8 @@ class_name WeaponSlot
# References
@onready var icon_rect: TextureRect = $SlotPanel/MarginContainer/IconRect
var tooltip: PanelContainer = null
# Fallback name display for weapons without an icon texture
var _name_label: Label = null
# Weapon data
var weapon_data: WeaponData = null
@@ -53,9 +55,43 @@ func _update_display():
if weapon_data and weapon_data.icon:
icon_rect.texture = weapon_data.icon
icon_rect.modulate = Color.WHITE
_set_name_fallback("")
elif weapon_data:
# No icon on this weapon - show its name in the slot instead
icon_rect.texture = null
icon_rect.modulate = Color.WHITE
_set_name_fallback(weapon_data.weapon_name)
else:
icon_rect.texture = null
icon_rect.modulate = Color(0.3, 0.3, 0.3, 0.5) # Dim empty slot
_set_name_fallback("")
## Show/hide the fallback name label ("" hides it)
func _set_name_fallback(text: String):
if text == "":
if _name_label:
_name_label.hide()
return
if not _name_label:
_name_label = Label.new()
_name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_name_label.add_theme_font_size_override("font_size", 12)
_name_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.4))
_name_label.add_theme_color_override("font_outline_color", Color.BLACK)
_name_label.add_theme_constant_override("outline_size", 2)
_name_label.set_anchors_preset(Control.PRESET_FULL_RECT)
_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
var container = get_node_or_null("SlotPanel/MarginContainer")
if container:
container.add_child(_name_label)
else:
add_child(_name_label)
_name_label.text = text
_name_label.show()
## Show tooltip on mouse enter
func _on_mouse_entered():