Grid/Base/WallSpawner.cs
using Sandbox;
using Sandbox.Sboku;
using Sandbox.Sboku.Arena;
using System;
using System.Collections.Generic;
using System.Numerics;
/// <summary>
/// WallSpawner – vastaa seinien ja ovien spawnaamisesta maailmaan.
/// Sisältää talo-id:n rakentamisen (flood fill), seinäpylväiden spawnauksen,
/// ovien spawnauksen sekä ulkorajan seinien spawnauksen.
/// </summary>
internal class WallSpawner
{
private readonly GridSpawner owner;
private GridMap Map => owner.Map;
private Model BoxModel => owner.BoxModel;
private Material WallMaterialOuter => owner.WallMaterialOuter;
private Material WallMaterialHouse => owner.WallMaterialHouse;
private Dictionary<int, Material> HouseWallMaterials => owner.HouseWallMaterials;
private int[,] HouseIds => owner.HouseIds;
private DebrisVisualPool DebrisPool => owner.DebrisPool;
private GameObject DustPrefab => owner.DustPrefab;
private int WallHeight => owner.WallHeight;
private float FloorZ => owner.FloorZ;
private const float BorderUVFactor = 2.5f;
public WallSpawner(GridSpawner owner)
{
this.owner = owner;
}
/// <summary>
/// Spawnaa seinäpylvään tai oven annettuun grid-koordinaattiin.
/// </summary>
public void SpawnWallsAndDoors(int x, int y)
{
if (!Networking.IsHost)
return;
if (Map.IsIndestructible(x, y))
return;
int columnHeight = Game.Random.Int(1, WallHeight);
var go = new GameObject($"Wall_{x}_{y}");
var dmg = go.Components.Create<WallColumnDamage>();
dmg.DebrisPool = DebrisPool;
dmg.DustPrefab = DustPrefab;
int houseId = HouseIds[x, y];
if (houseId != 0 && HouseWallMaterials.TryGetValue(houseId, out var mat))
dmg.WallMaterial = mat;
else
dmg.WallMaterial = WallMaterialHouse; // fallback
dmg.MaxHealth = 1000;
dmg.ColumnHeight = columnHeight;
dmg.GridSize = Map.GridSize;
dmg.IsIndestructible = false;
var renderer = go.Components.Create<ModelRenderer>();
renderer.Model = BoxModel;
dmg.WallMaterial.Set("g_vTexCoordScale", new Vector2(1f, MathF.Max(columnHeight * 1.5f, 2.0f)));
renderer.MaterialOverride = dmg.WallMaterial;
// 🔑 skaala
float scaleXY = Map.GridSize / BoxModel.Bounds.Size.x;
float scaleZ = (Map.GridSize * columnHeight) / BoxModel.Bounds.Size.z;
go.Transform.Scale = new Vector3(scaleXY, scaleXY, scaleZ);
// 🔑 GRID → WORLD (Z = 0!)
Vector3 pos = Map.GridToWorld(x, y, 0);
// 🔑 pylvään world-korkeus
float worldHeight = BoxModel.Bounds.Size.z * scaleZ;
// 🔑 ALAPINTA LATTIAAN
pos.z = FloorZ + worldHeight * 0.5f;
go.Transform.Position = pos;
var collider = go.Components.Create<BoxCollider>();
collider.Static = true;
/*var physics = go.Components.Create<ModelPhysics>();
physics.Enabled = true;
physics.MotionEnabled = false;*/
go.Tags.Add("solid");
if (Map.IsDoor(x, y))
{
var door = go.Components.Create<DoorComponent>();
door.GridX = x;
door.GridY = y;
door.Activate();
}
}
/// <summary>
/// Spawnaa ulkorajan seinät (pohjoinen, etelä, itä, länsi).
/// </summary>
public void SpawnBorderWalls()
{
if (!Networking.IsHost)
return;
const int height = 2;
float wallWorldHeight = Map.GridSize * height;
float scaleZ = wallWorldHeight / BoxModel.Bounds.Size.z;
float totalWidth = Map.Width * Map.GridSize;
float totalHeight = Map.Height * Map.GridSize;
float scaleX_Long = totalWidth / BoxModel.Bounds.Size.x;
float scaleY_Long = totalHeight / BoxModel.Bounds.Size.y;
float scaleX_Thin = Map.GridSize / BoxModel.Bounds.Size.x;
float scaleY_Thin = Map.GridSize / BoxModel.Bounds.Size.y;
// SOUTH
{
var center = Map.GridToWorld((int)(Map.Width / 2f - 0.5f), 0, 0);
SpawnBorder(
"Border_South",
center,
new Vector3(scaleX_Long, scaleY_Thin, scaleZ)
);
}
// NORTH
{
var center = Map.GridToWorld((int)(Map.Width / 2f - 0.5f), Map.Height - 1, 0);
SpawnBorder(
"Border_North",
center,
new Vector3(scaleX_Long, scaleY_Thin, scaleZ)
);
}
// WEST
{
var center = Map.GridToWorld(0, (int)(Map.Height / 2 - 0.5), 0);
SpawnBorder(
"Border_West",
center,
new Vector3(scaleX_Thin, scaleY_Long, scaleZ)
);
}
// EAST
{
var center = Map.GridToWorld(Map.Width - 1, (int)(Map.Height / 2 - 0.5), 0);
SpawnBorder(
"Border_East",
center,
new Vector3(scaleX_Thin, scaleY_Long, scaleZ)
);
}
}
/// <summary>
/// Spawnaa yksittäisen ulkorajan seinän annetulla nimellä, sijainnilla ja skaalalla.
/// </summary>
public void SpawnBorder(string name, Vector3 center, Vector3 scale)
{
var go = new GameObject(name);
var renderer = go.Components.Create<ModelRenderer>();
renderer.Model = BoxModel;
renderer.MaterialOverride = WallMaterialOuter;
go.Transform.Scale = scale;
float worldHeight = BoxModel.Bounds.Size.z * scale.z;
go.Transform.Position = new Vector3(
center.x,
center.y,
FloorZ + worldHeight * 0.5f
);
var collider = go.Components.Create<BoxCollider>();
collider.Static = true;
go.Tags.Add("solid");
// 🔥 TÄRKEÄ OSA — OIKEIN SÄÄDETTY TILING
float tilesX = Map.Width / BorderUVFactor;
float tilesY = 1;
WallMaterialOuter.Set("g_vTexCoordScale",
new Vector2(tilesX, tilesY));
}
/// <summary>
/// Rakentaa talo-id:t flood fill -algoritmilla.
/// Jokainen yhtenäinen seinä-alue ('-') saa oman id:n ja materiaalin.
/// </summary>
public void BuildHouseIds()
{
owner.HouseIds = new int[Map.Width, Map.Height];
int nextHouseId = 1;
for (int y = 0; y < Map.Height; y++)
{
for (int x = 0; x < Map.Width; x++)
{
if (HouseIds[x, y] != 0)
continue;
// talon seinä
if (Map.Cells[x, y] != '-')
continue;
FloodFillHouse(x, y, nextHouseId);
nextHouseId++;
}
}
// arvo materiaali per talo
for (int id = 1; id < nextHouseId; id++)
{
int matIndex = Game.Random.Int(1, owner.WallMaterials.Count - 1);
HouseWallMaterials[id] = Material.Load(owner.WallMaterials[matIndex]);
}
}
/// <summary>
/// Flood fill -algoritmi talo-id:n levittämiseksi yhtenäiselle seinäalueelle.
/// </summary>
public void FloodFillHouse(int startX, int startY, int houseId)
{
var stack = new Stack<(int x, int y)>();
stack.Push((startX, startY));
while (stack.Count > 0)
{
var (x, y) = stack.Pop();
if (!Map.InBounds(x, y))
continue;
if (HouseIds[x, y] != 0)
continue;
if (Map.Cells[x, y] != '-')
continue;
HouseIds[x, y] = houseId;
stack.Push((x + 1, y));
stack.Push((x - 1, y));
stack.Push((x, y + 1));
stack.Push((x, y - 1));
}
}
}