Grid/Base/GridMap.cs
using System;
using System.Collections.Generic;
using System.Numerics;

public sealed class GridMap
{
    public int Width { get; }
    public int Height { get; }
    public char[,] Cells { get; }

    public float GridSize { get; set; } = 128f;
    public Vector3 CenterWorld { get; private set; }

    public GridMap(string gridText)
    {
        string[] lines = gridText.Split(
            new[] { "\r\n", "\n" },
            StringSplitOptions.RemoveEmptyEntries
        );

        int width = 0;
        foreach (var line in lines)
            if (line.Length > width)
                width = line.Length;

        Width = width;
        Height = lines.Length;
        Cells = new char[Width, Height];

        for (int y = 0; y < Height; y++)
        {
            var line = lines[y];
            for (int x = 0; x < Width; x++)
                Cells[x, y] = x < line.Length ? line[x] : ' ';
        }

        CenterWorld = GridToWorld(Width / 2, Height / 2, 0f);
    }

    // =========================
    // COORDINATES
    // =========================

    public Vector3 GridToWorld(int x, int y, float z)
    {
        float half = GridSize * 0.5f;

        float worldX = (x - Width * 0.5f) * GridSize + half;
        float worldY = (y - Height * 0.5f) * GridSize + half;

        return new Vector3(worldX, worldY, z);
    }

    public (int x, int y) WorldToGrid(Vector3 worldPos)
    {
        int x = (int)MathF.Floor(worldPos.x / GridSize + Width * 0.5f);
        int y = (int)MathF.Floor(worldPos.y / GridSize + Height * 0.5f);
        return (x, y);
    }

    // =========================
    // BASIC
    // =========================

    public bool InBounds(int x, int y)
        => x >= 0 && y >= 0 && x < Width && y < Height;

    public char GetCell(int x, int y)
        => InBounds(x, y) ? Cells[x, y] : '#';

    public bool SetCell(int x, int y, char value)
    {
        if (!InBounds(x, y))
            return false;

        Cells[x, y] = value;
        return true;
    }

    // =========================
    // CELL TYPES (NEW MODEL)
    // =========================

    public bool IsOutside(int x, int y) => GetCell(x, y) == '.';
    public bool IsInterior(int x, int y) => GetCell(x, y) == ',';

    public bool IsWall(int x, int y)
    {
        char c = GetCell(x, y);
        return c == '-' || c == '#' || c == 'B';
    }

    public bool IsOuterWall(int x, int y) => GetCell(x, y) == '#';
    public bool IsInnerWall(int x, int y) => GetCell(x, y) == '-';

    /// <summary>
    /// Räjähdettävä heikko seinä – tuhoutuu yhdestä osumasta, ketjureaktio.
    /// </summary>
    public bool IsExplosiveWall(int x, int y) => GetCell(x, y) == 'B';

    public bool IsDoor(int x, int y) => GetCell(x, y) == '+';
    public bool IsTrap(int x, int y) => GetCell(x, y) == '^';
    public bool IsEnemy(int x, int y) => GetCell(x, y) == 'X';
    public bool IsSpawn(int x, int y) => GetCell(x, y) == '@';

    // =========================
    // 🔧 BACKWARD COMPATIBILITY
    // =========================

    // Vanha spawneri odottaa tätä
    public bool IsButton(int x, int y)
        => GetCell(x, y) == '0';

    // Vanha spawneri käyttää tätä seinien spawniin
    public bool IsWallLike(int x, int y)
    {
        char c = GetCell(x, y);
        return c == '#' || c == '-' || c == '+' || c == 'B';
    }

    // =========================
    // WALKABILITY / GAMEPLAY
    // =========================

    public bool IsWalkable(int x, int y)
    {
        if (!InBounds(x, y))
            return false;

        char c = Cells[x, y];
        return c == '.' || c == ',' || c == '+' || c == '@' || c == 'X';
        // Huom: 'B' ei ole käveltävä – se on seinä
    }

    public bool IsIndestructible(int x, int y)
        => IsOuterWall(x, y);

    // =========================
    // HELPERS
    // =========================

    public bool TouchesWall(int x, int y)
    {
        return IsWall(x - 1, y) ||
               IsWall(x + 1, y) ||
               IsWall(x, y - 1) ||
               IsWall(x, y + 1);
    }

    public bool AdjacentTo(int x, int y, params char[] targets)
    {
        foreach (var t in targets)
        {
            if (GetCell(x - 1, y) == t ||
                GetCell(x + 1, y) == t ||
                GetCell(x, y - 1) == t ||
                GetCell(x, y + 1) == t)
                return true;
        }
        return false;
    }

    public (int x, int y)[] GetAllCellsOfType(char cellType)
    {
        var list = new List<(int, int)>();
        for (int y = 0; y < Height; y++)
            for (int x = 0; x < Width; x++)
                if (Cells[x, y] == cellType)
                    list.Add((x, y));

        return list.ToArray();
    }

    public (int x, int y)[] GetAllSpawnPoints(char spawnChar = '@')
        => GetAllCellsOfType(spawnChar);

    public List<(int minX, int minY, int maxX, int maxY)> FindRooms()
    {
        var visited = new bool[Width, Height];
        var rooms = new List<(int, int, int, int)>();

        for (int x = 0; x < Width; x++)
        {
            for (int y = 0; y < Height; y++)
            {
                if (visited[x, y] || !IsInterior(x, y))
                    continue;

                int minX = x, maxX = x;
                int minY = y, maxY = y;

                var stack = new Stack<(int, int)>();
                stack.Push((x, y));
                visited[x, y] = true;

                while (stack.Count > 0)
                {
                    var (cx, cy) = stack.Pop();

                    minX = Math.Min(minX, cx);
                    maxX = Math.Max(maxX, cx);
                    minY = Math.Min(minY, cy);
                    maxY = Math.Max(maxY, cy);

                    foreach (var (nx, ny) in new (int, int)[]
                    {
                    (cx+1, cy),
                    (cx-1, cy),
                    (cx, cy+1),
                    (cx, cy-1)
                    })
                    {
                        if (nx < 0 || ny < 0 || nx >= Width || ny >= Height)
                            continue;

                        if (visited[nx, ny] || !IsInterior(nx, ny))
                            continue;

                        visited[nx, ny] = true;
                        stack.Push((nx, ny));
                    }
                }

                rooms.Add((minX, minY, maxX, maxY));
            }
        }

        Log.Info($"Rooms found: {rooms.Count}");
        return rooms;
    }

}