Files
SurvivalOfTheSnippest/CLAUDE.md
T
Scottlg f5e0642b39 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.
2026-07-01 23:47:03 +01:00

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

  1. Run one instance, click ENTER ARENA to host (port 8080)
  2. 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

  1. 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).
  2. 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.
  3. 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.

Architecture

Autoloads (singletons)

  • Network (level/scripts/network.gd) — ENet setup, host/join, players dict keyed by peer_id ({"nick": String, "skin": Character.SkinColor}). Server = peer 1, MAX_PLAYERS = 10.
  • GameState (level/scripts/game_state.gd) — the economy. Persists banked_gold, owned_items, selected_loadout to user://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 before reload_current_scene().

Class hierarchy

  • BaseUnit (base_unit.gd) — health, damage, death, respawn, passive regen. Server validates all damage. NOTE: died can 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.

Weapons

  • WeaponData (weapon_data.gd) — Resource in level/resources/*.tres: combat stats, hand type (MAIN_HAND / OFF_HAND / TWO_HAND), block stats, cost/tier for 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 .tres in level/resources/ appears in the armory (outfitting_screen.gd).

UI

  • character_sheet.gd has two modes: live player (Tab in-game) and preview_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 checks multiplayer.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 of rpc_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 named EnemySpawnPoint*).
  • 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 . --quit after 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.gd and exercises the API (--script mode; autoload _ready doesn't fire there, call load_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 .tres in level/resources/ with cost/tier — the shop picks it up automatically.
  • New enemy: extend BaseEnemy, set gold_reward, decide can_respawn (respawning enemies must not pay gold), add scene to the EnemySpawner pool or spawn via level.gd (connect its died signal for gold there).
  • Multiplayer state changes: server validates, then syncs via RPC; check is_multiplayer_authority() before processing input.