Arena polish: in-run HUD, no free loot, retire-with-winnings

- HUD arena status (top center): live wave number and run gold,
  driven by GameState signals, torn down cleanly on HUD.reset()
- Free floor weapons removed: the initial sword/shield spawn is gone
  and manually placed level.tscn weapons are deleted server-side at
  init. Gear now only enters play via the armory or enemy drops.
- Retire to Camp: new escape-menu button (shown only while alive in
  an active run) banks 100% of run gold and keeps all gear - the
  counterweight to death's 10% settlement. Exit Game and window close
  also bank winnings before quitting.
- CLAUDE.md rewritten for the gladiator arena era: core loop,
  autoloads, economy API, RPC patterns, verification workflow.
This commit is contained in:
2026-07-01 23:47:03 +01:00
parent 1562b37563
commit f5e0642b39
5 changed files with 253 additions and 158 deletions
+30
View File
@@ -39,6 +39,11 @@ signal loadout_changed(loadout: Dictionary)
func _ready() -> void:
load_save()
func _notification(what: int) -> void:
# Closing the window mid-run counts as walking out alive - bank the winnings
if what == NOTIFICATION_WM_CLOSE_REQUEST and run_active:
retire_run()
# --- Persistence ---------------------------------------------------------
func load_save() -> void:
@@ -186,6 +191,31 @@ func end_run() -> Dictionary:
run_ended.emit(last_run_stats)
return last_run_stats
## Walk out of the arena alive: bank 100% of run gold, keep all gear.
## The reward for retiring instead of dying (death only keeps 10%).
func retire_run() -> Dictionary:
if not run_active:
return last_run_stats
run_active = false
last_run_stats = {
"waves": current_wave,
"kills": kills_this_run,
"time": get_run_time(),
"gold_earned": run_gold,
"total_value": get_total_value(),
"kept": banked_gold + run_gold,
"retired": true,
}
banked_gold += run_gold
run_gold = 0
gold_changed.emit(banked_gold)
run_gold_changed.emit(run_gold)
save_game()
run_ended.emit(last_run_stats)
return last_run_stats
# --- Multiplayer crediting (server -> owning peer) ------------------------
# GameState is an autoload, so it exists at the same path on every peer with
# the server (peer 1) as its multiplayer authority. The server decides who
+14 -72
View File
@@ -100,11 +100,9 @@ func initialize_multiplayer():
Network.connect("player_connected", Callable(self, "_on_player_connected"))
multiplayer.peer_disconnected.connect(_remove_player)
# Initialize any manually placed weapons in the scene
_initialize_manual_weapons()
# Spawn initial weapons when server starts
_spawn_initial_weapons()
# Remove any manually placed weapons - free floor loot undermines the shop.
# Weapons only enter the arena via the armory or dropped by slain enemies.
_remove_manual_weapons_on_server()
# Spawn practice dummies
_spawn_practice_dummies()
@@ -154,81 +152,25 @@ func _cleanup_manual_weapons_on_client():
weapons_container.remove_child(weapon)
weapon.free()
func _initialize_manual_weapons():
"""Initialize any WorldWeapon nodes manually placed in the level scene"""
func _remove_manual_weapons_on_server():
"""Remove WorldWeapon nodes manually placed in level.tscn (server side).
Clients already remove their local copies in _cleanup_manual_weapons_on_client.
The arena economy requires gear to be bought or looted from slain enemies."""
if not multiplayer.is_server():
return
if not weapons_container:
return
# Find all WorldWeapon nodes in the weapons container
var manual_weapons = []
var removed = 0
for child in weapons_container.get_children():
if child is WorldWeapon:
manual_weapons.append(child)
if child is WorldWeapon and child.weapon_id == -1:
weapons_container.remove_child(child)
child.free()
removed += 1
if manual_weapons.is_empty():
print("[Server] No manually placed weapons found")
return
print("[Server] Found ", manual_weapons.size(), " manually placed weapon(s)")
# Initialize each manually placed weapon
for weapon in manual_weapons:
# Skip if already initialized (weapon_id != -1)
if weapon.weapon_id != -1:
continue
# Assign unique ID
_weapon_spawn_counter += 1
weapon.weapon_id = _weapon_spawn_counter
# Set deterministic name for networking
var old_name = weapon.name
weapon.name = "WorldWeapon_" + str(weapon.weapon_id)
# Track in active weapons
_active_weapons[weapon.weapon_id] = weapon
# Connect cleanup signal
weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon.weapon_id))
print("[Server] Initialized manual weapon '", old_name, "' with ID: ", weapon.weapon_id, " at position: ", weapon.global_position)
# Verify weapon_data is set
if not weapon.weapon_data:
push_error("Manual weapon '", old_name, "' has no WeaponData assigned!")
continue
func _spawn_initial_weapons():
if not multiplayer.is_server():
return
# Wait a frame for everything to be ready
await get_tree().process_frame
print("[Server] _spawn_initial_weapons - Connected peers: ", multiplayer.get_peers())
# Spawn a sword
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn sword with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_sword.tres",
Vector3(5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
# Spawn a shield
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn shield with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_shield.tres",
Vector3(-5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
if removed > 0:
print("[Server] Removed ", removed, " manually placed floor weapon(s)")
func _on_player_connected(peer_id, player_info):
print("[Server] _on_player_connected called for peer ", peer_id, " with info: ", player_info)