Sboku/BotAdapter.cs
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Events;
using Sandbox.Sboku.Arena;
using Sandbox.Sboku.Shared;
using Sandbox.Shared;
using SWB.Base;
using SWB.Demo;
using SWB.Demo.Clothing;
using SWB.Player;
using SWB.Shared;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Sboku;

[Title("Bot Adapter")]
[Group("Sboku Arena")]
public class BotAdapter : SbokuBase,
    IPlayerBase,
    IGameEventHandler<Weapon.NoAmmoLeftEvent>,
    IGameEventHandler<Weapon.ReloadFinished>
{
    [Sync]
    public int BotIndex { get; set; }

    // ==============================
    // INVENTORY / WEAPON
    // ==============================
   
    public override ISbokuWeapon Weapon => sbokuWeapon;
    private ISbokuWeapon sbokuWeapon;

    public IInventory Inventory { get; set; }

    public void GiveWeapon(string className)
    {
        var weapon = WeaponRegistry.Instance.Get(className);
        if (weapon == null)
        {
            Log.Error($"[BotAdapter] Weapon '{className}' not found");
            return;
        }

        Inventory.AddClone(weapon.GameObject, true);
        SetAmmo(weapon.Primary.AmmoType, 999);
        sbokuWeapon = new WeaponAdapter(weapon);
    }

    // ==============================
    // LIFECYCLE
    // ==============================

    protected override void OnStart()
    {
        Log.Info($"[BOT {BotIndex}] OnStart | IsProxy={IsProxy} | Scene={Scene.Name}");

        if (IsProxy)
            return;

        Health = MaxHealth;
        Inventory = Components.Create<Inventory>();

        Camera = AddComponent<CameraComponent>(false);
        ViewModelCamera = AddComponent<CameraComponent>(false);

        var weapon = WeaponRegistry.Instance.Weapons.Values
            .ElementAt(Game.Random.Next(WeaponRegistry.Instance.Weapons.Count));

        GiveWeapon(weapon.ClassName);

        // ==============================
        // 🎨 VAATTEET (VISUAALIT)
        // ==============================

        var smr = GetComponentInChildren<SkinnedModelRenderer>();

        if (smr != null)
        {
            BodyRenderer = ClothingBuilder.Build(
                smr,
                "", // head
                "hat/Racer_Helmet/racer_helmet",
                "shirt/Jumpsuit/prison_jumpsuit",
                null,
                "",
                "",
                null
            );
        }
    
        CreateHitCollider(); // 🔥 LISÄÄ TÄMÄ

    }

    private void CreateHitCollider()
    {
        var hit = Scene.CreateObject();
        hit.Name = "HitCollider";
        hit.Parent = GameObject;

        // Suunnilleen rinnan kohdalle
        hit.LocalPosition = new Vector3(0, 0, 36f);
        hit.LocalRotation = Rotation.Identity;

        var sphere = hit.Components.Create<SphereCollider>();
        sphere.Radius = 24f;
        sphere.IsTrigger = false;

        // 🔑 TÄRKEIN RIVI
        hit.Tags.Add(TagsHelper.Player);
    }

    protected override void OnUpdate()
    {
        if (!IsAlive)
        {
            Enabled = false;
            return;
        }

        if (Head != null)
        {
            Head.WorldRotation = EyeAngles.ToRotation();
        }

        base.OnUpdate();
    }

    // ==============================
    // INPUT → SWB
    // ==============================

    public bool IsAttackDown(string type)
        => IsAlive && IsShooting;

    public bool IsAttackPressed(string type)
        => IsAlive && IsShooting;

    public bool IsReloadDown()
        => IsReloading;

    public void ShakeScreen(ScreenShake shake) { }

    // ==============================
    // WEAPON EVENTS
    // ==============================

    public void OnGameEvent(Weapon.NoAmmoLeftEvent e)
        => Reload();

    public void OnGameEvent(Weapon.ReloadFinished e)
        => OnReloadFinish();

    // ==============================
    // IPlayerBase – PROPERTIES
    // ==============================

    public bool IsFirstPerson => false;
    public bool IsBot => true;
    public bool IsAlive => Health > 0;

    public CameraComponent Camera { get; set; }
    public CameraComponent ViewModelCamera { get; set; }

    public float InputSensitivity { get; set; }
    public Angles EyeAnglesOffset { get; set; }

    [Sync] public bool IsCrouching { get; set; }
    [Sync] public bool IsRunning { get; set; }

    [Sync] public int Health { get; set; }
    [Sync] public int Kills { get; set; }
    [Sync] public int Deaths { get; set; }

    Vector3 IPlayerBase.Velocity => Character?.Velocity ?? Vector3.Zero;
    bool IPlayerBase.IsOnGround => Character?.IsOnGround ?? true;
    Guid IPlayerBase.Id => GameObject.Id;

    // ==============================
    // DAMAGE
    // ==============================

    public void TakeDamage(SWB.Shared.DamageInfo info)
    {
        if (IsProxy || !IsAlive)
            return;

        Health -= (int)info.Damage;

        if (Health <= 0)
        {
            Die(info);
        }
    }


    private void Die(SWB.Shared.DamageInfo info)
    {
       
        Health = 0;
        Deaths++;

        ResetState();

        // 🔑 KUTSU BROADCASTIA
        OnDeathBroadcast(
            info.Force,
            info.Origin
        );
    }
    [Rpc.Broadcast]
    private void OnDeathBroadcast(Vector3 force, Vector3 origin)
    {
        SpawnRagdoll(force, origin);

        // 🔑 POISTA ELÄVÄ BOTTI KAIKILTA
        GameObject.Destroy();
    }

    private void SpawnRagdoll(Vector3 force, Vector3 origin)
    {
        Log.Info("SPawndagdoll");
        var ragdoll = Scene.CreateObject();
        ragdoll.Name = "BotRagdoll";

        ragdoll.WorldPosition = GameObject.WorldPosition;
        ragdoll.WorldRotation = GameObject.WorldRotation;

        // =========================
        // RENDERER
        // =========================
        var smr = ragdoll.Components.Create<SkinnedModelRenderer>();
        smr.Model = BodyRenderer.Model;

        // 🔑 VAATTEET UUDELLE MODELILLE
        BodyRenderer = ClothingBuilder.Build(
                smr,
                "", // head
                "hat/Racer_Helmet/racer_helmet",
                "shirt/Jumpsuit/prison_jumpsuit",
                null,
                "",
                "",
                null
            );

        // =========================
        // PHYSICS
        // =========================
        var phys = ragdoll.Components.Create<ModelPhysics>();
        phys.Renderer = smr;
        phys.Enabled = true;

        if (phys.PhysicsGroup != null)
        {
            foreach (var body in phys.PhysicsGroup.Bodies)
                body.ApplyImpulseAt(origin, force);
        }

        // =========================
        // VARMISTA EI ANIMAATIOTA
        // =========================
        foreach (var anim in ragdoll.Components.GetAll<CitizenAnimationHelper>())
            anim.Enabled = false;
    }


    // ==============================
    // CHARACTER / VIEW
    // ==============================

    [Property] public GameObject Head { get; set; }
    [Property] public GameObject Body { get; set; }
    [Property] public SkinnedModelRenderer BodyRenderer { get; set; }
    [Property] public CitizenAnimationHelper AnimationHelper { get; set; }
    [Property] public ModelPhysics RagdollPhysics { get; set; }

    [Property] public int MaxHealth { get; set; } = 100;

    [Sync] public Vector3 EyeOffset { get; set; } = Vector3.Zero;

    [Sync]
    public override Angles EyeAngles
    {
        get => eyeAngles;
        set => eyeAngles = value;
    }
    private Angles eyeAngles;

    public override Vector3 EyePos => Head.WorldPosition + EyeOffset;

    // ==============================
    // ANIMATION
    // ==============================

    protected override void UpdateAnimations(Vector3 wishVelocity, Rotation rotation)
    {
        if (AnimationHelper == null)
            return;

        AnimationHelper.WithWishVelocity(wishVelocity);
        AnimationHelper.WithVelocity(Character.Velocity);
        AnimationHelper.AimAngle = rotation;
        AnimationHelper.IsGrounded = Character.IsOnGround;
        AnimationHelper.WithLook(rotation.Forward);
        AnimationHelper.MoveStyle = CitizenAnimationHelper.MoveStyles.Run;
    }

    // ==============================
    // AMMO
    // ==============================

    [Sync] public NetDictionary<string, int> Ammo { get; set; } = new();

    public int AmmoCount(string type)
        => Ammo.TryGetValue(type, out var a) ? a : 0;

    public void SetAmmo(string type, int amount)
        => Ammo[type] = amount;

    public int TakeAmmo(string type, int amount)
    {
        var available = AmmoCount(type);
        amount = Math.Min(amount, available);
        SetAmmo(type, available - amount);
        return amount;
    }


    protected override List<ISbokuCondition> GetConditions()
    {
        return new()
    {
        new FindNearestAlivePlayerCondition( this ),
        // muut ehdot
    };
    }

}