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
+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()