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
+59
View File
@@ -42,6 +42,9 @@ func _ready():
# Add quick-fill preset buttons
_create_preset_buttons()
# Reshape the menu into the gladiator outfitting screen
_setup_outfitting_menu()
# Create or find weapons container
if has_node("WeaponsContainer"):
weapons_container = get_node("WeaponsContainer")
@@ -89,6 +92,9 @@ func initialize_multiplayer():
_multiplayer_initialized = true
# Begin this player's arena run (starts the survival clock + gold tracking)
GameState.start_run()
if multiplayer.is_server():
print("[Level] Running server initialization")
Network.connect("player_connected", Callable(self, "_on_player_connected"))
@@ -377,6 +383,13 @@ func _spawn_player_local(id: int, spawn_pos: Vector3):
else:
push_warning("[Level] HUD autoload not found! Make sure to restart Godot to register the new autoload.")
# Equip the loadout chosen in the outfitting screen (broadcasts to all peers)
var loadout = GameState.get_selected_loadout()
if loadout.get("main", "") != "":
player.rpc("equip_weapon_from_world", loadout["main"])
if loadout.get("offhand", "") != "":
player.rpc("equip_weapon_from_world", loadout["offhand"])
var skin_enum = player_info["skin"]
player.set_player_skin(skin_enum)
# rpc("sync_player_skin", id, skin_enum)
@@ -470,6 +483,40 @@ func _on_send_pressed() -> void:
func msg_rpc(nick, msg):
chat.text += str(nick, " : ", msg, "\n")
# ---------- OUTFITTING MENU ----------
## Turn the plain host/join menu into the gladiator camp:
## menu options on the left, armory shop + character sheet preview on the right.
func _setup_outfitting_menu():
# Retitle and shrink the header so it fits the narrowed left column
var title = menu.get_node_or_null("MainContainer/Label")
if title:
title.text = "SURVIVAL OF\nTHE SNIPPEST"
title.add_theme_font_size_override("font_size", 42)
# Narrow the existing menu column to the left third of the screen
var main_container = menu.get_node_or_null("MainContainer")
if main_container:
main_container.anchor_right = 0.34
# Rename buttons to fit the arena fiction
var host_button = menu.get_node_or_null("MainContainer/MainMenu/Buttons/Host")
if host_button:
host_button.text = "ENTER ARENA"
var join_button = menu.get_node_or_null("MainContainer/MainMenu/Buttons/Join")
if join_button:
join_button.text = "JOIN FRIEND"
# Armory + preview fill the rest of the screen
var outfitting_script = load("res://level/ui/scripts/outfitting_screen.gd")
var outfitting = outfitting_script.new()
outfitting.name = "OutfittingScreen"
outfitting.anchor_left = 0.35
outfitting.anchor_top = 0.04
outfitting.anchor_right = 0.985
outfitting.anchor_bottom = 0.96
menu.add_child(outfitting)
outfitting.set_nick_input(nick_input)
# ---------- PRESET BUTTONS ----------
func _create_preset_buttons():
# Create a container for preset buttons
@@ -795,6 +842,18 @@ func _spawn_armed_enemy_local(enemy_name: String, spawn_pos: Vector3, main_weapo
enemies_container.add_child(enemy, true)
print("[Peer ", multiplayer.get_unique_id(), "] Armed enemy ", enemy_name, " spawned successfully")
# Server credits kill gold for armed enemies spawned outside the wave spawner
if multiplayer.is_server():
enemy.died.connect(_on_level_enemy_died.bind(enemy))
## Kill-gold payout for enemies spawned directly by the level (not the wave spawner).
## Practice dummies are deliberately NOT connected - they respawn and would be farmable.
func _on_level_enemy_died(killer_id: int, enemy: Node):
if not multiplayer.is_server():
return
if killer_id > 0 and Network.players.has(killer_id) and enemy is BaseEnemy:
GameState.server_credit_kill(killer_id, enemy.gold_reward)
## Spawn initial armed enemies when server starts
func _spawn_armed_enemies():
if not multiplayer.is_server():