- 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.
7.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Survival of the Snippest — a 3D multiplayer gladiator arena survival roguelike built in Godot 4.5. Outfit a gladiator with banked gold, enter the colosseum, survive escalating enemy waves, earn gold from kills and wave clears, and either retire alive (keep everything) or die (keep 10% of your total value, floored at the 500-gold fresh start). Uses ENet client-server networking.
Key Commands
Running the Game
- Open the project in Godot Editor and press F5, or:
godot --path . res://level/scenes/level.tscn - Headless script-error check:
godot --headless --path . --quit
Testing Multiplayer Locally
- Run one instance, click ENTER ARENA to host (port 8080)
- Run a second instance, click JOIN FRIEND to connect (IP 127.0.0.1)
In-Game Debug Keys
N— start next enemy wave (server only)H— toggle hitbox/hurtbox debug visualization (off by default)
The Core Loop
- Outfitting screen (main menu): armory shop on the left/middle, Character Sheet in preview mode on the right showing the pending loadout live. Buy weapons with banked gold; hand rules enforced (two-handers and off-hands displace each other).
- Arena run: chosen loadout auto-equips on spawn. Waves spawn on a timer; kills pay gold (server-credited to the killer's peer), wave clears pay a bonus to all players.
- Run end:
- Death (permadeath, no respawn): results screen; keep
max(500, 10% of total value); gear and loadout liquidated. - Retire (escape menu, or quitting alive): bank 100% of run gold, keep all gear.
- Death (permadeath, no respawn): results screen; keep
Architecture
Autoloads (singletons)
- Network (
level/scripts/network.gd) — ENet setup, host/join,playersdict keyed by peer_id ({"nick": String, "skin": Character.SkinColor}). Server = peer 1, MAX_PLAYERS = 10. - GameState (
level/scripts/game_state.gd) — the economy. Persistsbanked_gold,owned_items,selected_loadouttouser://save.cfg. Per-run:run_gold,current_wave,kills_this_run,run_active. Key API:buy_item,equip_loadout_item,start_run,end_run(death settlement, double-call guarded),retire_run(bank everything),server_credit_kill/gold(server → owning peer RPCs),server_sync_wave. No compile-time dependency on Network — keep it that way (testable standalone via--script). - HUD (
level/ui/scripts/hud_manager.gd) — builds all in-run UI for the local player (action bar, unit frame, character sheet, arena status wave/gold readout, escape menu, run results on death).reset()tears everything down when returning to the menu — autoload children survive scene reloads, so always reset beforereload_current_scene().
Class hierarchy
- BaseUnit (
base_unit.gd) — health, damage, death, respawn, passive regen. Server validates all damage. NOTE:diedcan fire twice on the host (direct emit + call_local sync) — guard anything expensive hooked to it. - Character (
player.gd, class_name Character) — player: movement, dash, attacks, blocking, weapon equip/drop/pickup.can_respawn = false(death ends the run). Attacks snap the body to camera facing and lock direction for startup+active (_lock_facing_to_camera); dash locks direction for its duration. - BaseEnemy (
base_enemy.gd) — targeting,gold_reward(basic 10, armed 25). Enemies are server-authoritative (authority = 1).- BasicEnemy — melee chaser;
can_respawn = false, despawns after death, 40% health orb drop. - ArmedEnemy — player-model enemy that seeks/equips weapons and drops them on death.
can_respawn = false(respawning would be a gold farm). Practice dummies DO respawn and deliberately pay no gold.
- BasicEnemy — melee chaser;
Weapons
- WeaponData (
weapon_data.gd) — Resource inlevel/resources/*.tres: combat stats, hand type (MAIN_HAND / OFF_HAND / TWO_HAND), block stats,cost/tierfor the shop,icon(optional — weapon slots fall back to name text when missing). - BaseWeapon — equipped instance on a hand bone; WorldWeapon — pickable RigidBody3D.
- No free floor weapons: manually placed WorldWeapons in level.tscn are removed at init on the
server (
_remove_manual_weapons_on_server). Gear enters play via the shop or enemy drops only. - The shop catalog is auto-discovered: any WeaponData
.tresinlevel/resources/appears in the armory (outfitting_screen.gd).
UI
character_sheet.gdhas two modes: live player (Tab in-game) andpreview_mode(embedded in the outfitting screen, rendering from a loadout dictionary). Keep both paths working.- UI is largely built in code; match that style for new components.
RPC Patterns
@rpc("any_peer", "reliable")— server-validated actions (damage, heal); server checksmultiplayer.is_server()inside.@rpc("any_peer", "call_local", "reliable")— state sync to all peers (spawns, equips, health).@rpc("authority", ...)— server-initiated (GameState credits, wave sync).- Use
rpc_id(peer_id, ...)for targeted sync (late-join). When the server targets itself (peer 1), call the method directly instead ofrpc_id(1, ...). - Late-join sync lives in
level.gd _on_player_connected— new networked object types must be added there (players, weapons, enemies, orbs, equipped gear all have precedents).
Scene Structure (level.tscn)
- Containers the code expects by name:
PlayersContainer,WeaponsContainer,EnemiesContainer,OrbsContainer,PlayerSpawnPoints,EnemySpawner(spawn points are its children namedEnemySpawnPoint*). - Networked nodes are named deterministically for lookup: players by peer_id,
WorldWeapon_<id>,Enemy_<wave>_<n>,ArmedEnemy_<n>,HealthOrb_<id>.
Physics Layers
1 player · 2 world · 3 weapon · 4 hitbox · 5 hurtbox
Input Actions (project.godot)
move_left/right/forward/backward (WASD), jump (Space), shift (sprint), dash (F),
attack (LMB), block (RMB), pickup (E), toggle_character_sheet (Tab),
toggle_chat (F12), quit (Esc).
Development Guidelines
Git Commits
When creating git commits, do NOT include "🤖 Generated with [Claude Code]" or "Co-Authored-By: Claude" in commit messages. Keep commit messages clean and professional.
Verification
- Always run
godot --headless --path . --quitafter script changes and check for SCRIPT ERROR. (Pre-existing "invalid UID" warnings on mesh/theme assets are known noise.) - Economy logic can be tested standalone: a SceneTree script that instantiates
game_state.gdand exercises the API (--scriptmode; autoload_readydoesn't fire there, callload_save()manually). Restore the default save afterwards. - Test multiplayer with 2 instances (host + client); check late joins and death/retire flows.
Adding Content
- New weapon: create a WeaponData
.tresinlevel/resources/withcost/tier— the shop picks it up automatically. - New enemy: extend BaseEnemy, set
gold_reward, decidecan_respawn(respawning enemies must not pay gold), add scene to the EnemySpawner pool or spawn via level.gd (connect itsdiedsignal for gold there). - Multiplayer state changes: server validates, then syncs via RPC; check
is_multiplayer_authority()before processing input.