# CLAUDE.md Guidance for working in this Unity project. **These rules are mandatory** — follow them exactly when reading, writing, or refactoring any C# script. --- ## 1. Project overview First-person survival/exploration game built in **Unity 6000.3.8f1** with **URP**. The game is **online co-op multiplayer** (a single-player build is just a host with no clients — there is no separate offline mode). Core systems already in place: first-person locomotion, camera, stats (health/hunger/thirst), inventory + hotbar, crafting, cooking, harvesting/tools, procedural scatter placement, audio (footsteps by surface + music), seasons, and a full UI/menu/settings stack. **Networking stack** (see §3): - **FishNet 4.7.2** (in `Assets/FishNet/`) — the netcode. - **Steamworks.NET** + **FishySteamworks** transport — Steam relay (SteamNetworkingSockets, no port forwarding). Sessions are joined by a shareable code (host SteamID64) or Steam invite. Context for current work: - **This is final, production-quality code — not a prototype.** Aim for clean, well-named, well-architected code that respects the patterns in this document. No quick hacks, no "temporary" shortcuts; if something is worth doing, do it properly. - **Multiplayer co-op is a hard constraint** — when touching any player, world or shared system, assume two+ players exist (see §3). - **No player arms / viewmodel** — feedback comes from camera, audio and UI only. - Animations use **DOTween** (tweens, not Animator) wherever possible. - Shaders are **hand-written HLSL/ShaderLab** — do not propose Shader Graph. --- ## 2. Architecture — the golden rule **Everything talks through the static event bus `PlayerEvents`** (`Assets/GAME/Script/Player/Events/PlayerEvents.cs`). - `PlayerEvents` is the source of truth for the **local** player's cross-system state (`IsDead`, `IsGrounded`, `IsInventoryOpen`, `IsPaused`, `InputLocked`, …). Here "shared" means shared *between systems on one client* — never *between players* (see §3). - Producers call a `Raise*` method; the raiser updates the matching `IsX` state, logs (under the `PLAYER_EVENT_LOG` define) and invokes the event. - Consumers subscribe to the `event` in `OnEnable` and **unsubscribe in `OnDisable`**. - **Never** reach across systems with `FindObjectOfType` or hard references to read state that the bus already exposes. Add a new event/state to `PlayerEvents` instead — **except** per-player networked state, which belongs on a `NetworkBehaviour` (SyncVar/RPC), not the bus (see §3). **Each script owns one concern.** A script reads its own inputs/state from the bus, does its job, and raises events for others. Do not let a script mutate another system's internal data directly. If two systems need to coordinate, route it through `PlayerEvents`. Data that is authored in the editor lives in **ScriptableObjects** (`ItemData`, `CraftingRecipe`, `SurfaceSoundProfile`, `ScatterProfile`, `MusicPlaylist`, `ToolDropTable`, …). Logic lives in `MonoBehaviour`s. Keep them separate. Singletons exist where a single instance is genuinely global (`PlayerInventory.Instance`, `PlayerStats.Instance`, …); guard every access for `null` — they may not exist yet or in every scene. --- ## 3. Multiplayer — the second golden rule The game runs on **FishNet** (co-op, **client-authoritative** — friendly play, *not* anti-cheat). Networking code lives in `Assets/GAME/Script/Network/`. When you write or change anything that involves a player, the world, or shared state, obey these rules. ### `PlayerEvents` is the LOCAL player's bus only The static `PlayerEvents` (state + events) represents **the local owned player**, never remote players. Only owner-side code may call `Raise*`. Remote player copies must **never** drive the static bus — they only update their own instance state (e.g. via SyncVar hooks). Per-player networked state (health, position, …) belongs on the player's `NetworkBehaviour` (SyncVar / RPC), not in `PlayerEvents`. ### Owner gating `PlayerNetworkController` (on the player root) disables every owner-only component (input, locomotion, camera, tools, **stats**, inventory, hotbar, interactor, audio, lifecycle) and owner-only object (cameras, audio listener, tool holder) on non-owners, then raises `LocalPlayerSpawned`. **Any new player component that simulates or reads local input must be added to that `ownerOnlyBehaviours` list.** Scene/UI code must bind to the *local* player — get it from `PlayerInventory.Instance` / `PlayerStats.Instance` (owner-only singletons), **never** `FindObjectOfType()`, which also matches disabled remote puppets (their root GameObject stays active) and targets the wrong player. ### Networking a player system — the canonical pattern (mirror `PlayerInventory`/`PlayerStats`) - Make it a `NetworkBehaviour`. **No destructive `Awake` singleton guard** (it would destroy spawned remote players). Set the `Instance` only for the owner, in `OnStartNetwork` (`if (base.Owner.IsLocalClient) Instance = this;`), clear it in `OnStopNetwork`. - Owner simulates locally; replicate a lightweight snapshot via SyncVar (server writes, pushed from the owner through a `[ServerRpc]`, throttled + immediate on key events). - SyncVar `OnChange` updates remote copies' local state only — guard `if (base.IsOwner) return;` and never touch `PlayerEvents` there. ### World state World objects (pickables, harvestables) are **not** NetworkObjects. They use the `WorldObject` + `WorldObjectRegistry` pattern: one networked registry per type per scene owns a synced set of inactive ids + server-side authority (health, loot, drops) via RPC. Add new harvestable/pickable-like world state here, not as per-object NetworkObjects. `ItemData` cannot cross the wire — map items to ids through `ItemDatabase` (**Tools ▸ Ashwild ▸ Rebuild Item Database** after adding items). ### Shared interactables (cooking stations, future chests/benches) Anything two players can use at once needs server-authoritative state + RPCs — local-only MonoBehaviour state will let players clobber each other. `CookingStation` is the reference: the server owns the simulation (fuel burn + cooking timers), replicates a compact view (`SyncList` + a `SyncVar` log count), and mutates only through `[ServerRpc(RequireOwnership = false)]` requests that refund the item on failure via `PlayerInventory.GrantItemFromServer`. Mirror it for chests/benches. ### Not yet networked — do not rely on these in co-op These still run locally and will desync between clients until migrated: - **`GrassClearer`** — modifies the terrain detail layer locally, no RPC → others keep the grass. - **`SeasonManager`** — the global `_Season` shader value is not synced → visual desync. ### Reference implementations Copy these patterns rather than inventing new ones: `PlayerInventory.cs` and `PlayerStats.cs` (networked player system), `HarvestableRegistry.cs` / `WorldObjectRegistry.cs` (world state), `CookingStation.cs` (server-authoritative shared interactable), `PlayerNetworkController.cs` (owner gating). --- ## 4. Folder map (`Assets/GAME/Script/`) | Folder | Namespace | Responsibility | | --- | --- | --- | | `Player/Events/` | `Ashwild.Player` | `PlayerEvents` — the central bus. Start here to understand the game. | | `Player/Input/` | `Ashwild.Player` | `PlayerInputRouter` — the **only** Input System receiver. | | `Player/Locomotion/` | `Ashwild.Player` | Movement, ground/slope checks, jump, slide, crouch. | | `Player/Camera/` | `Ashwild.Player` | First-person camera, look, head height. | | `Player/Tools/` | `Ashwild.Player` | Tool equip/swing logic and tool holder. | | `Player/` (root) | `Ashwild.Player` | `PlayerStats`, `PlayerInteractor`, `PlayerLifecycle`, `PlayerAudio`. | | `Network/` | `Ashwild.Network` | FishNet layer — session host/join, player spawn, owner gating, `WorldObject`/registry world-state sync. | | `Inventory/` | `Ashwild.Inventory` | Inventory model, slots, hotbar, pickables, inventory UI. | | `Crafting/` | `Ashwild.Crafting` | Recipes, ingredients, crafting manager and its UI. | | `Cooking/` | `Ashwild.Cooking` | Cooking stations (food + fuel → cooked results). | | `Harvesting/` | `Ashwild.Harvesting` | Harvestables, tools, resource drops, scatter placement. | | `Audio/` | `Ashwild.Audio` | Music manager/playlist, surface sound profiles. | | `Environment/` | `Ashwild.Environment` | Season manager. | | `GrassClearer/` | `Ashwild.GrassClearer` | Detail/grass removal around placed objects. | | `Interaction/` | `Ashwild.Interaction` | `IInteractable` and interaction contracts. | | `Settings/` | `Ashwild.Settings` | Settings manager, panels and sub-panels, brightness. | | `UI/` | `Ashwild.UI` | HUD, panels, notifications, crosshair, death/splash/pause screens. | | `Integrations/` | `Ashwild.Integrations` | Third-party integrations (e.g. Discord Rich Presence). | | `Editor/` | `Ashwild.EditorTools` | Custom inspectors, property drawers, tooling (editor-only). | ### Namespaces — one per top-level system folder Every script lives in a namespace **`Ashwild.`** — flat, one per top-level system folder. Sub-folders do **not** get their own namespace: everything under `Player/` (incl. `Player/Camera/`, `Player/Input/`, `Player/Tools/`, `Player/HeldItems/`) is `Ashwild.Player`. This flatness is deliberate — it keeps cross-references short and avoids a sub-namespace shadowing a Unity type (a namespace `Ashwild.Player.Camera` would mask `UnityEngine.Camera` inside it). Rules when adding or moving a script: - New file → wrap it in the namespace of its top-level folder. Add `using Ashwild.X;` for every other Ashwild namespace whose types it references. - **`Editor/` is the one exception to the `Ashwild.` rule** — its namespace is **`Ashwild.EditorTools`**, *not* `Ashwild.Editor`, because a namespace segment named `Editor` shadows the `UnityEditor.Editor` base type that every custom inspector derives from (`CS0118`). - **Never** put `using Ashwild.EditorTools;` in a runtime (non-`Editor/`) script. Editor code compiles into the separate `Assembly-CSharp-Editor` assembly, which runtime code cannot reference (`CS0234`). Editor scripts may freely `using` runtime namespaces, not the reverse. --- ## 5. Code conventions — non-negotiable ### 5.1 XML summaries Every **class, method, and non-obvious public member** gets a `/// ` written in **English**, describing intent (the *why*), not restating the signature. ```csharp /// /// Adds an item to the inventory, stacking into existing slots first. /// Returns false (and logs) when the item is null or no room remains. /// public bool AddItem(ItemData item, int quantity = 1) { ... } ``` **Use the 3-line form** — opening `/// `, the text, and the closing `/// ` each on their own line; never collapse a summary onto one line. Scope (this matches the real codebase — see `PlayerInventory`/`PlayerStats`): - **Always:** every class, every method (incl. Unity lifecycle and event handlers), and any public member whose intent isn't obvious from its name. - **Not required:** trivial getters / `=>` passthroughs and `[SerializeField]` fields — group the latter under `[Header("...")]` and clarify individual ones with `[Tooltip("...")]` (or a summary) only when the name alone doesn't convey intent. ### 5.1b No in-body comments — explain in the summary above the method **Never write explanatory comments *inside* a method body.** All rationale — the *why*, the gotchas, the edge cases — goes in the `/// ` above the method (extend it with as much prose as needed). The body stays pure code. If a block feels like it needs an inline comment to be understood, that's a signal to either name things better or extract it into its own well-summarized method. The only inline text allowed in a body is a `// TODO:` / `// HACK:` marker for tracked future work — not an explanation of what the current code does. ```csharp // ❌ wrong — explanation lives inside the body private void Step() { // Subtract instead of zeroing so the leftover carries into the next step. distanceTravelled -= stepDistance; } /// /// Fires a footstep and carries the leftover distance into the next step. Subtracting /// (instead of zeroing) keeps the cadence even at sprint speed, where the per-frame /// overshoot would otherwise randomly stretch steps and drift out of sync. /// private void Step() { distanceTravelled -= stepDistance; } ``` ### 5.2 Specific `#region`s — never one generic blob Group members into **named, specific** regions reflecting their role. Do **not** dump everything under a single `#region Methods`. Typical regions for a MonoBehaviour: ```csharp #region Serialized Fields #region State #region Unity Lifecycle // Awake / OnEnable / OnDisable / Start / Update #region Event Handlers // bus callbacks #region Public API #region Internal Helpers #endregion ``` Pick regions that match what the script actually does (e.g. `#region Ground Check`, `#region Surface Detection`, `#region Playback`). The goal: a reader scans the region names and knows the script's shape instantly. ### 5.3 Event subscription lifecycle Subscribe in `OnEnable`, unsubscribe in `OnDisable` — always paired, same order. Never subscribe without a matching unsubscribe (leaks + double-fire after reload). ### 5.4 DOTween hygiene Cache tweeners in fields and `Kill()` them in `OnDestroy` (and before restarting them). A tween targeting a destroyed object throws — never leave one running on teardown. ### 5.5 Defensive logging instead of crashes Guard nullable inputs and log a clear, prefixed error with the **context object** so clicking the console message selects the culprit in the hierarchy: ```csharp if (itemData == null) { Debug.LogError($"[Pickable] '{name}' has no ItemData assigned — cannot pick up.", this); return; } ``` Prefix logs with `[ClassName]`. Pass `this` as the second arg whenever possible. ### 5.6 Input naming — avoid the `On` trap The `PlayerInput` component uses **Send Messages**, so Unity invokes `On` methods by reflection on the player's GameObject and its components. **Only `PlayerInputRouter` may declare `On` methods.** Any other component with a method named like an input action (e.g. `OnHotbarScroll`) will be invoked by the Input System and crash on a signature mismatch. Name bus handlers `Handle` instead. ### 5.7 General practices - `private` fields exposed to the inspector use `[SerializeField] private`, grouped under `[Header("...")]`. No public fields just to show them in the inspector. - One class per file; file name matches the type. - Prefer `OnEnable/OnDisable` over `Start/OnDestroy` for subscription symmetry. - No magic numbers in logic that designers tune — promote them to serialized fields. - Don't add `FindObjectOfType` in hot paths (`Update`/`FixedUpdate`); cache references. - Keep `Update`/`FixedUpdate` cheap; cache expensive lookups (see `PlayerAudio`'s surface cache for the pattern). --- ## 6. Canonical script template ```csharp using UnityEngine; /// /// One-sentence description of this component's single responsibility. /// [DisallowMultipleComponent] public class ExampleSystem : MonoBehaviour { #region Serialized Fields [Header("Tuning")] [SerializeField] private float duration = 1f; #endregion #region State private bool isActive; #endregion #region Unity Lifecycle /// /// Subscribes to the bus events this system reacts to. /// private void OnEnable() { PlayerEvents.Died += HandleDied; } /// /// Unsubscribes — must mirror OnEnable exactly. /// private void OnDisable() { PlayerEvents.Died -= HandleDied; } #endregion #region Event Handlers /// /// Resets the system when the player dies. /// private void HandleDied() => isActive = false; #endregion #region Public API /// /// Starts the effect; ignored if already running. /// public void Activate() { if (isActive) return; isActive = true; } #endregion } ``` --- ## 7. Before you finish - New cross-system communication → add an event to `PlayerEvents`, don't bypass the bus. - Touched a `MonoBehaviour` that subscribes? Verify `OnEnable`/`OnDisable` stay paired. - Touched a player system? Confirm it's owner-gated (in `ownerOnlyBehaviours`) and that a remote copy never drives `PlayerEvents` or simulates itself (see §3). - New networked prefab? Add a `NetworkObject` and register it in `DefaultPrefabObjects`. - Added a tween? Verify it's killed on teardown. - The project must compile with **zero errors**; treat new warnings as something to fix or justify. - Editor-only code stays under `Editor/`.