swb_player/PlayerBase.cs
using Sandbox.Events;
using Sandbox.Sboku;
using Sandbox.Sboku.Arena;
using Sandbox.Sboku.Shared;
using SWB.Shared;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SWB.Player;

[Group( "SWB" )]
[Title( "PlayerBase" )]
public partial class PlayerBase : Component, Component.INetworkSpawn, IPlayerBase, ISbokuTarget
{
	[Property] public GameObject Head { get; set; }
	[Property] public GameObject Body { get; set; }
	[Property] public SkinnedModelRenderer BodyRenderer { get; set; }
	[Property] public CameraComponent Camera { get; set; }
	[Property] public CameraComponent ViewModelCamera { get; set; }
	[Property] public PanelComponent RootDisplay { get; set; }
	[Property] public Voice Voice { get; set; }
	[RequireComponent]
	public UpgradeHolder UpgradeHolder { get; set; }

	[Sync] public bool IsBot { get; set; }
	public IInventory Inventory { get; set; }
	public ICameraMovement CameraMovement { get; set; }
	public bool IsFirstPerson => CameraMovement.IsFirstPerson;
	public string DisplayName => !IsBot ? (Network.Owner?.DisplayName ?? "Disconnected") : GameObject.Name;
	public SteamId SteamId => !IsBot ? Network.Owner.SteamId : new( 0 );
	public bool IsHost => !IsBot && Network.Owner.IsHost;
	public bool IsSpeaking => Voice.Amplitude > 0;

	public float InputSensitivity
	{
		get { return CameraMovement.InputSensitivity; }
		set { CameraMovement.InputSensitivity = value; }
	}
	public Angles EyeAnglesOffset
	{
		get { return CameraMovement.EyeAnglesOffset; }
		set { CameraMovement.EyeAnglesOffset = value; }
	}

	Guid IPlayerBase.Id { get => GameObject.Id; }

    protected override void OnAwake()
	{
		Inventory = Components.Create<Inventory>();
		CameraMovement = Components.GetInChildren<CameraMovement>();
		Voice = Components.GetInChildren<Voice>();

		if ( IsBot ) return;

		// Hack: Hide client until fully loaded in OnStart
		if ( !IsProxy )
		{
			WorldPosition = new( 0, 0, -999999 );
			Network.ClearInterpolation();
		}

		OnMovementAwake();
	}

	public void OnNetworkSpawn( Connection connection )
	{
		//ApplyClothes( connection );
	}

	protected override void OnStart()
	{
        if (!IsProxy && !IsBot)
        {
            InitClothingFromConnection();
        }
        if ( IsProxy || IsBot )
		{
			if ( Camera is not null )
				Camera.Enabled = false;

			if ( ViewModelCamera is not null )
				ViewModelCamera.Enabled = false;
		}

		if ( IsBot )
		{
			var screenPanel = Components.GetInChildrenOrSelf<ScreenPanel>();

			if ( screenPanel is not null )
				screenPanel.Enabled = false;
		}

		if ( !IsProxy )
			Respawn();

        var movement = Components.Get<CameraMovement>();
        if (movement is not null)
        {
            movement.Enabled = false;
        }
    }

    [Rpc.Owner]
	public void Kill()
	{
		if ( !IsAlive ) return;
		Health = 0;
		OnDeath( new() { AttackerId = GameObject.Id } );
	}

	[Rpc.Owner]
	public void Kick( string reason )
	{
		Log.Info( reason );
		Game.Disconnect();
	}

	[Rpc.Broadcast]
	public virtual void OnDeath( Shared.DamageInfo info )
	{
		if ( !IsValid ) return;
		var attackerGO = Scene.Directory.FindByGuid( info.AttackerId );

		if ( attackerGO is not null && !attackerGO.IsProxy )
		{
			var attacker = attackerGO.Components.Get<IPlayerBase>();

			if ( attacker is not null && attacker != this )
				attacker.Kills++;
		}

		if ( IsProxy ) return;

		Deaths++;
		CharacterController.Velocity = 0;
		Ragdoll( info.Force, info.Origin );
		Inventory.Clear();


        RespawnWithDelay( 2 );
    }

	public async virtual void RespawnWithDelay( float delay )
	{
		await GameTask.DelaySeconds( delay );
		// 🔑 Pelaaja spawnaa uudelleen omasta spawnistaan — ei Game Overia, ei elämärajoja.
		// Game Over dispatchaus poistettu: yksittäisen pelaajan kuolema ei lopeta koko peliä.
		// Scene.Dispatch<RoundManager.GameOver>(new());
        Respawn();
    }

	[Rpc.Broadcast]
	public void RespawnWithDelayBroadCast( float delay )
	{
		RespawnWithDelay( delay );
	}

	[Rpc.Broadcast]
	public void RespawnBroadCast()
	{
		Respawn();
	}

	public virtual void Respawn()
	{
		// Hack: Delaying with just 1ms fixes the 1 frame body at death position.
		async void UnragdollWithDelay()
		{
			await GameTask.Delay( 1 );
			Unragdoll();
		}
		UnragdollWithDelay();

		Inventory.Clear();
		dmgTable.Clear();
		Health = MaxHealth;

		var spawnLocation = GetSpawnLocation();
		WorldPosition = spawnLocation.Position;
		EyeAngles = spawnLocation.Rotation.Angles();
		Network.ClearInterpolation();

		if ( IsBot )
		{
			Body.WorldRotation = new Angles( 0, EyeAngles.ToRotation().Yaw(), 0 ).ToRotation();
			AnimationHelper.WithLook( EyeAngles.ToRotation().Forward, 1f, 0.75f, 0.5f );
		}

    }

    protected void ResetPlayerState()
    {
        Inventory.Clear();
        dmgTable.Clear();
        Health = MaxHealth;
    }
	public virtual Transform GetSpawnLocation()
	{
		var prisonMarkers = Scene.GetAllComponents<PrisonSpawnMarker>()
			.Where( marker => marker.IsValid && marker.PreferForPlayers )
			.Select( marker => new Transform(
				marker.GameObject.Transform.World.Position + Vector3.Up * 64f,
				marker.GameObject.Transform.World.Rotation
			) )
			.ToList();

		if ( prisonMarkers.Any() )
		{
			var rand = new Random();
			return prisonMarkers.ElementAt( rand.Next( 0, prisonMarkers.Count ) );
		}

		var spawnPoints = Scene.Components.GetAll<SpawnPoint>();

		if ( !spawnPoints.Any() )
			return new Transform();

		var randFallback = new Random();
		var randomSpawnPoint = spawnPoints.ElementAt( randFallback.Next( 0, spawnPoints.Count() ) );

		return randomSpawnPoint.Transform.World;
	}

	protected override void OnUpdate()
	{
		if ( IsBot ) return;
		if ( !IsProxy ) ViewModelCamera.Enabled = IsFirstPerson && IsAlive;
     
        // 🔑 TÄMÄ on se hetki kun client saa vaatteet
        if (IsProxy && !clothesApplied && !string.IsNullOrEmpty(clothingJSON))
        {
            ApplyClothesJson(clothingJSON);
            clothesApplied = true;
        }
        if ( IsAlive )
			OnMovementUpdate();

		HandleFlinch();
		HandleScreenShake();
		UpdateClothes();
        
	}

    

    protected override void OnFixedUpdate()
	{
		if (IsRagdolled) return;
        if ( !IsAlive || IsBot ) return;
		OnMovementFixedUpdate();
        HandleDamage();
    }

    public static PlayerBase GetLocal()
	{
		var players = GetAll();
		return players.FirstOrDefault( ( player ) => !player.IsProxy && !player.IsBot, null );
	}

	public static IEnumerable<PlayerBase> GetAll()
	{
		return Game.ActiveScene.GetAllComponents<PlayerBase>();
	}
    
	public bool IsEnemy => true;

    public bool IsAttackPressed(string type)
		=> Input.Pressed(type);
	public bool IsAttackDown(string type)
		=> Input.Down(type);
    public bool IsReloadDown()
        => Input.Down(InputButtonHelper.Reload);
}