- 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.
131 lines
7.3 KiB
Markdown
131 lines
7.3 KiB
Markdown
# 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.
|