Grid/Base/PrisonConcreteBlockSpawner.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Sandbox.Sboku;

public sealed class PrisonConcreteBlockSpawner : Component
{
    [Property] public int MaxBlocks { get; set; } = 32;
    [Property] public int MaxSpawnMarkers { get; set; } = 8;
    [Property] public float BlockScaleMultiplier { get; set; } = 1f;
    [Property] public int WallHealth { get; set; } = 62;
    [Property] public float WallZOffset { get; set; } = -256f;
    [Property] public float GroundProbeHeight { get; set; } = 900f;
    [Property] public float GroundProbeDepth { get; set; } = 2000f;
    [Property] public float GroundPadding { get; set; } = 0f;

    private bool spawned;

    protected override void OnStart()
    {
        if (spawned || !Networking.IsHost)
            return;

        SpawnBlocks();
        SpawnMarkers();
        spawned = true;
    }

    private void SpawnBlocks()
    {
        var model = Model.Load("models/dev/box.vmdl");
        var material = Material.Load("assets/materials/concrete_layers/concrete_layers.vmat");

        var blockPositions = BuildBlockPositions();

        int count = 0;
        foreach (var localPos in blockPositions)
        {
            if (count >= MaxBlocks)
                break;

            SpawnBlock(model, material, localPos);
            count++;
        }
    }

    private IEnumerable<Vector3> BuildBlockPositions()
    {
        const int columns = 8;
        const int rows = 4;
        const float xStart = -448f;
        const float yStart = -224f;
        const float xStep = 128f;
        const float yStep = 128f;
        const float jitter = 36f;

        var positions = new List<Vector3>(columns * rows);
        var rng = new Random();

        for (int row = 0; row < rows; row++)
        {
            for (int col = 0; col < columns; col++)
            {
                var x = xStart + col * xStep + Game.Random.Float(-jitter, jitter);
                var y = yStart + row * yStep + Game.Random.Float(-jitter, jitter);
                positions.Add(new Vector3(x, y, 0f));
            }
        }

        return positions.OrderBy(_ => rng.Next()).ToArray();
    }

    private void SpawnMarkers()
    {
        var markerPositions = new[]
        {
            new Vector3(-256, -256, 0),
            new Vector3(0, -256, 0),
            new Vector3(256, -256, 0),
            new Vector3(-256, 0, 0),
            new Vector3(256, 0, 0),
            new Vector3(-256, 256, 0),
            new Vector3(0, 256, 0),
            new Vector3(256, 256, 0),
        };

        int count = 0;
        foreach (var localPos in markerPositions)
        {
            if (count >= MaxSpawnMarkers)
                break;

            var go = new GameObject($"PrisonSpawn_{count}");
            go.SetParent(GameObject, false);
            go.Transform.Position = GameObject.Transform.Position + localPos;
            go.Transform.Rotation = Rotation.Identity;

            go.Components.Create<PrisonSpawnMarker>();
            go.NetworkSpawn();
            count++;
        }
    }

    private void SpawnBlock(Model model, Material material, Vector3 localPosition)
    {
        if (model == null)
            return;

        var go = new GameObject("ConcreteBlock");
        go.SetParent(GameObject, false);

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

        int wallColumns = Game.Random.Int(1, 3);
        var gridSpawner = Scene.GetComponentInChildren<GridSpawner>();
        float gridSize = gridSpawner?.Map?.GridSize ?? 128f;
        float scaleXY = (gridSize * BlockScaleMultiplier) / MathF.Max(model.Bounds.Size.x, model.Bounds.Size.y);
        float scaleZ = (gridSize * wallColumns) / model.Bounds.Size.z;
        go.Transform.Scale = new Vector3(scaleXY, scaleXY, scaleZ);

        var pos = GameObject.Transform.Position + localPosition;
        var traceStart = pos + Vector3.Up * GroundProbeHeight;
        var traceEnd = pos + Vector3.Down * GroundProbeDepth;
        var tr = Scene.Trace.Ray(traceStart, traceEnd)
            .WithoutTags("solid", "player", "trigger", "viewmodel")
            .Run();

        if (tr.Hit)
            pos = tr.HitPosition;

        float worldHeight = model.Bounds.Size.z * scaleZ;
        pos.z = pos.z + worldHeight * 0.5f + GroundPadding + WallZOffset;
        go.Transform.Position = pos;

        go.Transform.Rotation = Rotation.FromYaw(Game.Random.Float(0f, 360f));

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

        var wall = go.Components.Create<WallColumnDamage>();
        wall.DebrisPool = gridSpawner?.DebrisPool;
        wall.DustPrefab = gridSpawner?.DustPrefab;
        wall.WallMaterial = material;
        wall.MaxHealth = WallHealth;
        wall.GridSize = gridSize;
        wall.ColumnHeight = wallColumns;
        wall.IsIndestructible = false;

        go.Tags.Add("solid");
        go.NetworkSpawn();
    }
}