Grid/Base/GridObjectSpawner.cs
using Sandbox;
using Sandbox.Sboku;
using Sandbox.Sboku.Arena;
using System;
using System.Collections.Generic;
using System.Numerics;

/// <summary>
/// GridObjectSpawner – vastaa painikkeiden, ansojen, sumun ja propsien spawnaamisesta.
/// Sisältää myös huonerampin spawnauksen sekä kommentoituna säilytettävät indoor/outdoor item-metodit.
/// </summary>
internal class GridObjectSpawner
{
    private readonly GridSpawner owner;

    private GridMap Map => owner.Map;
    private Material FloorIndoorMaterial => owner.FloorIndoorMaterial;
    private float FloorZ => owner.FloorZ;
    private float SpawnHeight => owner.SpawnHeight;
    private GameObject InteriorFog => owner.InteriorFog;
    private string ButtonModelPath => owner.ButtonModelPath;
    private string TrapModelEmptyPath => owner.TrapModelEmptyPath;
    private string TrapModelPath => owner.TrapModelPath;
    private List<string> IndoorModels => owner.IndoorModels;
    private List<string> OutdoorModels => owner.OutdoorModels;

    private const int itemsPerRoom = 5;

    public GridObjectSpawner(GridSpawner owner)
    {
        this.owner = owner;
    }

    /// <summary>
    /// Spawnaa painikkeet ('0') lattialle gridin perusteella.
    /// </summary>
    public void SpawnButtons()
    {
        for (int y = 0; y < Map.Height; y++)
        {
            for (int x = 0; x < Map.Width; x++)
            {
                if (!Map.IsButton(x, y))
                    continue;

                var go = new GameObject($"Button_{x}_{y}");

                var model = Model.Load(ButtonModelPath);
                if (model == null)
                    continue;

                var renderer = go.Components.Create<ModelRenderer>();
                renderer.Model = model;
                renderer.MaterialOverride = FloorIndoorMaterial;

                // 🔑 SKAALA
                float scaleXY = Map.GridSize / model.Bounds.Size.x;
                go.Transform.Scale = new Vector3(scaleXY, scaleXY, scaleXY);

                // 🔑 GRID → WORLD (Z = 0!)
                Vector3 pos = Map.GridToWorld(x, y, 0);

                // 🔑 SIIRRETÄÄN LATTIAN PÄÄLLE
                pos.z = FloorZ + model.Bounds.Size.z * scaleXY * 0.5f + 1.0f;

                go.Transform.Position = pos;

                var collider = go.Components.Create<BoxCollider>();
                collider.Static = true;
                collider.IsTrigger = true;

                go.Tags.Add("button");

                var button = go.Components.Create<ButtonComponent>();
                button.GridX = x;
                button.GridY = y;

                go.NetworkSpawn();
            }
        }
    }

    /// <summary>
    /// Spawnaa ansat ('^') gridin perusteella.
    /// Jokaiselle anssalle luodaan visuaalinen malli, collider ja TrapComponent.
    /// </summary>
    public void SpawnTraps()
    {
        for (int y = 0; y < Map.Height; y++)
        {
            for (int x = 0; x < Map.Width; x++)
            {
                if (Map.Cells[x, y] != '^')
                    continue;



                var trapGO = new GameObject();
                trapGO.Name = $"Trap_{x}_{y}";
                trapGO.WorldPosition = Map.GridToWorld(x, y, SpawnHeight);
                Vector3 pos = Map.GridToWorld(x, y, SpawnHeight);
                pos.z -= SpawnHeight * 0.58f; // hieman lattian yläpuolelle
                trapGO.Transform.Position = pos;


                // Visuaalinen malli (alkutila = suljettu)
                var model = trapGO.AddComponent<ModelRenderer>();
                model.Model = Model.Load(TrapModelEmptyPath);
                model.MaterialOverride = FloorIndoorMaterial;

                // Collider (aktivoituu viereisestä ruudusta)
                var collider = trapGO.AddComponent<BoxCollider>();
                collider.Static = true; // estää liikkumisen painikkeen päällä
                collider.IsTrigger = true; // voidaan astua päälle

                // TrapComponent
                var trap = trapGO.AddComponent<TrapComponent>();
                trap.GridX = x;
                trap.GridY = y;
                trap.ClosedModelPath = TrapModelEmptyPath;
                trap.ActiveModelPath = TrapModelPath;

                trapGO.SetParent(owner.GameObject);
            }
        }
    }

    /// <summary>
    /// Asettaa sisätilasumun (volumetric fog) kattamaan kaikki sisätilat.
    /// </summary>
    public void SpawnInteriorFog()
    {
        if (InteriorFog == null)
        {
            Log.Warning("[GridSpawner] InteriorFog not assigned");
            return;
        }

        int minX = int.MaxValue;
        int minY = int.MaxValue;
        int maxX = int.MinValue;
        int maxY = int.MinValue;

        for (int y = 0; y < Map.Height; y++)
        {
            for (int x = 0; x < Map.Width; x++)
            {
                if (Map.Cells[x, y] != '#')
                {
                    minX = Math.Min(minX, x);
                    minY = Math.Min(minY, y);
                    maxX = Math.Max(maxX, x);
                    maxY = Math.Max(maxY, y);
                }
            }
        }

        if (minX > maxX)
            return;

        // 🔹 Laajennetaan yhdellä grid-ruudulla joka suuntaan
        minX = Math.Max(0, minX - 1);
        minY = Math.Max(0, minY - 1);
        maxX = Math.Min(Map.Width - 1, maxX + 1);
        maxY = Math.Min(Map.Height - 1, maxY + 1);

        Vector3 minWorld = Map.GridToWorld(minX, minY, SpawnHeight);
        Vector3 maxWorld = Map.GridToWorld(maxX, maxY, SpawnHeight);

        Vector3 center = (minWorld + maxWorld) * 0.5f;

        Vector3 size = new Vector3(
            Math.Abs(maxWorld.x - minWorld.x),
            Math.Abs(maxWorld.y - minWorld.y),
            256f
        );

        var vol = InteriorFog.GetComponent<VolumetricFogVolume>(false)
                  ?? InteriorFog.AddComponent<VolumetricFogVolume>();

        InteriorFog.WorldPosition = center;
        vol.Bounds = new BBox(-size * 0.5f, size * 0.5f);
        vol.Strength = 0.2f;
    }

    /// <summary>
    /// Spawnaa sisätilojen esineitä (kommentoituna, ei käytössä).
    /// </summary>
    // public void SpawnIndoorItems(GridMap map)
    // {
    //     if (!Networking.IsHost)
    //         return;
    //
    //     if (IndoorModels == null || IndoorModels.Count == 0)
    //         return;
    //
    //     var rooms = map.FindRooms();
    //
    //     foreach (var room in rooms)
    //     {
    //         var used = new HashSet<(int, int)>();
    //         int spawned = 0;
    //         int safety = 0;
    //
    //         while (spawned < itemsPerRoom && safety < 200)
    //         {
    //             safety++;
    //
    //             int x = Game.Random.Int(room.minX, room.maxX);
    //             int y = Game.Random.Int(room.minY, room.maxY);
    //
    //             if (!map.IsInterior(x, y))
    //                 continue;
    //
    //             if (used.Contains((x, y)))
    //                 continue;
    //
    //             Vector3 wallNormal = Vector3.Zero;
    //
    //             if (map.IsWall(x + 1, y)) wallNormal = Vector3.Right;
    //             else if (map.IsWall(x - 1, y)) wallNormal = Vector3.Left;
    //             else if (map.IsWall(x, y + 1)) wallNormal = Vector3.Forward;
    //             else if (map.IsWall(x, y - 1)) wallNormal = Vector3.Backward;
    //             else continue;
    //
    //             used.Add((x, y));
    //
    //             string modelPath = IndoorModels[
    //                 Game.Random.Int(0, IndoorModels.Count - 1)
    //             ];
    //
    //             var model = Model.Load(modelPath);
    //             if (model == null)
    //                 continue;
    //
    //             Vector3 basePos = map.GridToWorld(x, y, 0f);
    //
    //             var go = new GameObject(Scene);
    //
    //             // 🔥 Aseta XY gridistä, Z suoraan lattian pintaan
    //             go.Transform.Position = new Vector3(
    //                 basePos.x,
    //                 basePos.y,
    //                 FloorZ
    //             );
    //
    //             go.Transform.Rotation = Rotation.LookAt(-wallNormal, Vector3.Up);
    //
    //             var renderer = go.Components.Create<ModelRenderer>();
    //             renderer.Model = model;
    //
    //             var localBounds = model.Bounds;
    //
    //             // 🔥 seinään kiinni
    //             float depth = localBounds.Size.y * 0.5f;
    //             go.Transform.Position -= go.Transform.Rotation.Forward * depth;
    //
    //             // 🔥 NOSTO SUORAAN LATTIASTA
    //             float bottomOffset = -localBounds.Mins.z;
    //             go.Transform.Position += Vector3.Up * bottomOffset;
    //
    //             var collider = go.Components.Create<BoxCollider>();
    //             collider.Scale = localBounds.Size;
    //
    //             spawned++;
    //         }
    //     }
    // }

    /// <summary>
    /// Spawnaa ulkotilojen esineitä (kommentoituna, ei käytössä).
    /// </summary>
    // public void SpawnOutdoorItems(GridMap map, int amount)
    // {
    //     if (!Networking.IsHost)
    //         return;
    //
    //     if (OutdoorModels == null || OutdoorModels.Count == 0)
    //         return;
    //
    //     const float wallChance = 0.7f;
    //
    //     int spawned = 0;
    //     int safety = 0;
    //     int maxAttempts = amount * 50;
    //
    //     while (spawned < amount && safety < maxAttempts)
    //     {
    //         safety++;
    //
    //         int x = Game.Random.Int(0, map.Width - 1);
    //         int y = Game.Random.Int(0, map.Height - 1);
    //
    //         if (map.IsInterior(x, y))
    //             continue;
    //
    //         if (map.IsWallLike(x, y))
    //             continue;
    //
    //         Vector3 wallNormal = Vector3.Zero;
    //         bool spawnOnWall = Game.Random.Float() <= wallChance;
    //
    //         if (spawnOnWall)
    //         {
    //             if (map.IsWall(x + 1, y) && !map.IsInterior(x + 1, y))
    //                 wallNormal = Vector3.Right;
    //             else if (map.IsWall(x - 1, y) && !map.IsInterior(x - 1, y))
    //                 wallNormal = Vector3.Left;
    //             else if (map.IsWall(x, y + 1) && !map.IsInterior(x, y + 1))
    //                 wallNormal = Vector3.Forward;
    //             else if (map.IsWall(x, y - 1) && !map.IsInterior(x, y - 1))
    //                 wallNormal = Vector3.Backward;
    //             else
    //                 continue;
    //         }
    //
    //         string modelPath = OutdoorModels[
    //             Game.Random.Int(0, OutdoorModels.Count - 1)
    //         ];
    //
    //         var model = Model.Load(modelPath);
    //         if (model == null)
    //             continue;
    //
    //         var basePos = map.GridToWorld(x, y, 0f);
    //
    //         var go = new GameObject(Scene);
    //         go.Transform.Position = basePos;
    //
    //         if (spawnOnWall)
    //             go.Transform.Rotation = Rotation.LookAt(-wallNormal, Vector3.Up);
    //         else
    //             go.Transform.Rotation = Rotation.FromYaw(Game.Random.Float(0, 360));
    //
    //         var renderer = go.Components.Create<ModelRenderer>();
    //         renderer.Model = model;
    //
    //         if (spawnOnWall)
    //         {
    //             float depth = renderer.Bounds.Size.y * 0.5f;
    //             go.Transform.Position -= go.Transform.Rotation.Forward * depth;
    //         }
    //
    //         var bounds = renderer.Bounds;
    //         float bottom = bounds.Mins.z;
    //         float lift = basePos.z - bottom;
    //
    //         go.Transform.Position += Vector3.Up * lift;
    //
    //         var collider = go.Components.Create<BoxCollider>();
    //         collider.Scale = renderer.Bounds.Size;
    //
    //         spawned++;
    //     }
    // }

    /// <summary>
    /// Spawnaa rampin huoneeseen seinän viereen.
    /// Arpoo seinän ja suunnan, asettaa mallin ja colliderin.
    /// </summary>
    public void SpawnRoomRamp(
        GridMap map,
        int minX,
        int minY,
        int maxX,
        int maxY,
        string modelPath,
        float wallHeight
    )
    {
        if (!Networking.IsHost)
            return;

        var model = Model.Load(modelPath);
        if (model is null)
            return;

        // Arvotaan seinä
        int side = Game.Random.Int(0, 3);
        // 0 = bottom
        // 1 = top
        // 2 = left
        // 3 = right

        int rampLengthCells;
        Vector3 startWorld;
        Rotation rotation;

        if (side == 0) // bottom wall
        {
            rampLengthCells = maxX - minX + 1;
            int x = Game.Random.Int(minX, maxX);
            startWorld = map.GridToWorld(x, minY + 1, 0f);
            rotation = Rotation.FromPitch(-35f); // nousee ylöspäin
        }
        else if (side == 1) // top wall
        {
            rampLengthCells = maxX - minX + 1;
            int x = Game.Random.Int(minX, maxX);
            startWorld = map.GridToWorld(x, maxY - 1, 0f);
            rotation = Rotation.FromYaw(180) * Rotation.FromPitch(-35f);
        }
        else if (side == 2) // left wall
        {
            rampLengthCells = maxY - minY + 1;
            int y = Game.Random.Int(minY, maxY);
            startWorld = map.GridToWorld(minX + 1, y, 0f);
            rotation = Rotation.FromYaw(90) * Rotation.FromPitch(-35f);
        }
        else // right wall
        {
            rampLengthCells = maxY - minY + 1;
            int y = Game.Random.Int(minY, maxY);
            startWorld = map.GridToWorld(maxX - 1, y, 0f);
            rotation = Rotation.FromYaw(-90) * Rotation.FromPitch(-35f);
        }

        float rampLength = map.GridSize;
        float rampHeight = wallHeight;

        var go = new GameObject(owner.Scene);
        go.Transform.Position = startWorld + Vector3.Up * (rampHeight * 0.5f);
        go.Transform.Rotation = rotation;

        var renderer = go.Components.Create<ModelRenderer>();
        renderer.Model = model;

        // Skaalaa mallin 1x1x1 rampiksi
        var bounds = model.Bounds.Size;

        go.Transform.Scale = new Vector3(
            rampLength / bounds.x,
            map.GridSize / bounds.y,
            rampHeight / bounds.z
        );

        // Staattinen collider
        var physics = go.Components.Create<ModelPhysics>();
        physics.Model = renderer.Model;
        physics.Enabled = true;
        physics.MotionEnabled = false;
    }
}