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;
}
}