Files
chameleon/Assets/Scripts/FirstPersonMode.cs
T
adminandClaude Fable 5 2243cc823e Refactor: extract shared helpers across scripts, trim remaining doc redundancy
Deduplication pass (44 extractions):
- New SmoothedLook: shared smoothed yaw/pitch look tick + view seeding
  (FirstPersonController, FreeCamController)
- New CharacterTeleport: disable-CC-around-teleport idiom (rescue,
  owner position apply, free-cam sync)
- New GeometryUtil: RescaleToUnitScale + TransformBounds (concealment
  meter/volume, skinned collider baker)
- Paint family: canonical map read/write shared between Net and
  Persistence, snapshot decode, backend stroke dispatch, RebuildBackend,
  ResolveSeed, chunked RPC send/gate/copy, CamoKnobs/HeightKnobs
  aggregates, SanitizeFileKey, NewMaskTexture, AdoptAlbedo, FindSlider
- Player/modes: GameMode SetPlayerControl/SetPainting/SetOrbitInputEnabled,
  InputUtil.GameplayKeyPressed, NearestClimbHit, ComputeXRay delegates to
  ComputeResolutionFreeze, FingerIK TryInPlaneAngle
- Match/misc: UnhookSessionCallbacks, SendEmptyNamedMessage, SwapScenes,
  RunSessionOpAsync, CovarianceEigen, CollectClusterPoints, MatchHud
  GUIStyle cache, HsvColorPicker rehook, VoxelCenter

Documentation pass: remove 6 triplicated doc blocks and 2 restating
inline comments left after the previous reduction.

Behavior-neutral; hot-path rasterizer loops intentionally untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:33:48 +02:00

104 lines
4.5 KiB
C#

using UnityEngine;
using Unity.Netcode;
/// <summary>
/// First-person mode. The player keeps walking (WASD, camera-relative) but the camera moves to the
/// player's eye (the CameraTarget) with free mouse-look, and the player's mesh is hidden so the body
/// isn't seen from the inside. The orbit camera input is disabled while here so it doesn't fight for
/// the cursor; the <see cref="FirstPersonController"/> drives its own dedicated CinemachineCamera.
/// </summary>
public class FirstPersonMode : GameMode
{
public override string DisplayName => "First Person";
public override void Enter()
{
// Player stays fully controllable; only the view changes.
SetPlayerControl(true);
SetPainting(false);
SetOrbitInputEnabled(false);
if (Manager.FirstPerson)
Manager.FirstPerson.SetActive(true);
}
public override void Exit()
{
// Modes are Enter-only, so first-person must undo its own camera takeover (and unhide the mesh)
// here; the next mode's Enter restores the player/painter/look-mode state.
if (Manager.FirstPerson)
Manager.FirstPerson.SetActive(false);
SetOrbitInputEnabled(true);
if (Manager.CameraInput)
{
// Carry the first-person facing into the orbit camera so the next orbit mode (Paint / Player
// Control) keeps the current view angle instead of snapping back to the orbit's last-used angle.
// Camera.main still shows the first-person view this frame (the brain hasn't blended yet).
Manager.CameraInput.SyncToView();
}
}
// --- Hunter shooting -------------------------------------------------------------------------------
// A hunter in first person shoots a single ray from the camera centre on left-click (with a cooldown),
// but only during the SEEK sub-phase. A hit on an un-found hider is reported to the server, which
// validates and marks the hider found. Gated on the local avatar actually being a hunter role variant,
// so the legacy lobby first-person view never shoots.
private float _nextShootTime;
private static readonly RaycastHit[] _hits = new RaycastHit[16];
public override void Tick()
{
if (!CanShoot()) return;
if (Time.time < _nextShootTime) return;
if (!InputUtil.LeftClickedThisFrame()) return;
_nextShootTime = Time.time + Mathf.Max(0f, MatchManager.Instance.ShootCooldown);
TryShoot();
}
private bool CanShoot()
{
var match = MatchManager.Instance;
if (!match || !match.MatchActive || match.LocalSubPhase != MatchManager.MatchSubPhase.Seek) return false;
if (!Manager.Player || !Manager.FirstPerson) return false;
var setup = Manager.Player.GetComponent<NetworkPlayerSetup>();
return setup && setup.ScopedModes && setup.Role == PlayerRole.Hunter;
}
private void TryShoot()
{
var match = MatchManager.Instance;
Transform cam = Manager.FirstPerson.transform; // FP vcam sits at the eye, facing the look direction
float range = Mathf.Max(0f, match.ShootRange);
int count = Physics.RaycastNonAlloc(cam.position, cam.forward, _hits, range, ~0, QueryTriggerInteraction.Ignore);
if (count == 0) return;
// Nearest-first so the shot stops at the first solid thing it meets (a wall blocks; a nearer avatar
// shields a farther one). The hunter's own body is skipped so looking down doesn't self-block.
System.Array.Sort(_hits, 0, count, RaycastDistanceComparer.Instance);
NetworkObject self = Manager.Player ? Manager.Player.GetComponent<NetworkObject>() : null;
for (int i = 0; i < count; i++)
{
var netObj = _hits[i].collider.GetComponentInParent<NetworkObject>();
if (netObj && self && netObj == self) continue;
if (!netObj || !netObj.IsPlayerObject) return; // first solid hit is world geometry -> blocked
var setup = netObj.GetComponent<NetworkPlayerSetup>();
if (setup && setup.Role == PlayerRole.Hider && !setup.IsFound)
match.ReportHit(netObj.OwnerClientId);
return; // the first non-self hit decides the shot
}
}
private sealed class RaycastDistanceComparer : System.Collections.Generic.IComparer<RaycastHit>
{
public static readonly RaycastDistanceComparer Instance = new RaycastDistanceComparer();
public int Compare(RaycastHit a, RaycastHit b) => a.distance.CompareTo(b.distance);
}
}