using System;
using System.Collections.Generic;
namespace Heroes.Battle
{
///
/// Pure-C# generator that turns (seed, terrain) into a BattlefieldConfig.
/// Does not touch the scene, GameObjects, sprites, or UnityEngine.Random.
/// Deterministic: the same seed + terrain + database always yields the same
/// config.
///
/// Mirrors VCMI's lib/battle/ObstaclePlacer.cpp flow
/// (https://github.com/vcmi/vcmi):
/// 1. Mark initial unit columns as reserved so obstacles never land on them.
/// 2. Place at most one absolute obstacle (VCMI: CObstacleInstance::ABSOLUTE_OBSTACLE).
/// Absolute obstacles sit on a fixed anchor baked into the definition; if that
/// anchor collides with reserved cells, the absolute obstacle is skipped.
/// 3. Place a randomized count of usual obstacles (CObstacleInstance::USUAL) by
/// picking a weighted-random obstacle and trying a handful of random anchors.
///
public static class BattlefieldGenerator
{
/// Left-most column reserved for attacker unit placement.
public const int UnitColumnLeft = 0;
/// Right-most column reserved for defender unit placement.
public const int UnitColumnRight = BattlefieldConfig.Width - 1;
public static BattlefieldConfig Generate(int seed, TerrainType terrain, ObstacleDatabase database)
{
return Generate(seed, terrain, database, 1, 1);
}
///
/// Same as but with an explicit
/// number of columns reserved for units on each side (2 when wide units are present,
/// so their inner hex never collides with an obstacle).
///
public static BattlefieldConfig Generate(
int seed,
TerrainType terrain,
ObstacleDatabase database,
int reservedColumnsLeft,
int reservedColumnsRight)
{
if (database == null) throw new ArgumentNullException(nameof(database));
var rng = new Random(seed);
var config = new BattlefieldConfig
{
Seed = seed,
Terrain = terrain,
BackgroundId = PickBackgroundId(rng, database, terrain),
};
bool[,] occupied = new bool[BattlefieldConfig.Width, BattlefieldConfig.Height];
ReserveUnitColumns(occupied, reservedColumnsLeft, reservedColumnsRight);
TryPlaceAbsoluteObstacle(rng, database, terrain, occupied, config);
PlaceUsualObstacles(rng, database, terrain, occupied, config);
return config;
}
private static void ReserveUnitColumns(bool[,] occupied, int reservedColumnsLeft, int reservedColumnsRight)
{
reservedColumnsLeft = Math.Max(1, reservedColumnsLeft);
reservedColumnsRight = Math.Max(1, reservedColumnsRight);
for (int y = 0; y < BattlefieldConfig.Height; y++)
{
for (int i = 0; i < reservedColumnsLeft; i++)
{
occupied[UnitColumnLeft + i, y] = true;
}
for (int i = 0; i < reservedColumnsRight; i++)
{
occupied[UnitColumnRight - i, y] = true;
}
}
}
private static void TryPlaceAbsoluteObstacle(
Random rng,
ObstacleDatabase database,
TerrainType terrain,
bool[,] occupied,
BattlefieldConfig config)
{
var candidates = new List();
foreach (var o in database.GetObstaclesForTerrain(terrain, ObstacleType.Absolute))
{
candidates.Add(o);
}
if (candidates.Count == 0) return;
if (rng.NextDouble() > database.absoluteObstacleChance) return;
var chosen = candidates[rng.Next(candidates.Count)];
int ax = chosen.absoluteAnchor.x;
int ay = chosen.absoluteAnchor.y;
if (!CanPlace(chosen, ax, ay, occupied)) return;
Commit(chosen, ax, ay, occupied);
config.Obstacles.Add(new PlacedObstacle
{
ObstacleId = chosen.ResolveId(),
X = ax,
Y = ay,
});
}
private static void PlaceUsualObstacles(
Random rng,
ObstacleDatabase database,
TerrainType terrain,
bool[,] occupied,
BattlefieldConfig config)
{
int count = RandomRange(rng, database.minObstacles, database.maxObstacles);
if (count <= 0) return;
var pool = new List();
foreach (var o in database.GetObstaclesForTerrain(terrain, ObstacleType.Usual))
{
if (o.weight > 0f) pool.Add(o);
}
if (pool.Count == 0) return;
int attempts = Math.Max(1, database.placementAttemptsPerObstacle);
for (int i = 0; i < count; i++)
{
var obstacle = PickWeighted(rng, pool);
if (obstacle == null) continue;
for (int a = 0; a < attempts; a++)
{
int ax = rng.Next(0, BattlefieldConfig.Width);
int ay = rng.Next(0, BattlefieldConfig.Height);
if (!CanPlace(obstacle, ax, ay, occupied)) continue;
Commit(obstacle, ax, ay, occupied);
config.Obstacles.Add(new PlacedObstacle
{
ObstacleId = obstacle.ResolveId(),
X = ax,
Y = ay,
});
break;
}
}
}
private static string PickBackgroundId(Random rng, ObstacleDatabase database, TerrainType terrain)
{
var bg = database.FindBackgrounds(terrain);
if (bg == null || bg.variants == null || bg.variants.Count == 0) return terrain.ToString();
int idx = rng.Next(bg.variants.Count);
return bg.ResolveVariantId(idx);
}
private static ObstacleDefinition PickWeighted(Random rng, List pool)
{
float total = 0f;
for (int i = 0; i < pool.Count; i++) total += pool[i].weight;
if (total <= 0f) return pool[rng.Next(pool.Count)];
float roll = (float)rng.NextDouble() * total;
float acc = 0f;
for (int i = 0; i < pool.Count; i++)
{
acc += pool[i].weight;
if (roll <= acc) return pool[i];
}
return pool[pool.Count - 1];
}
private static bool CanPlace(ObstacleDefinition obstacle, int ax, int ay, bool[,] occupied)
{
var cells = obstacle.blockedHexes;
if (cells == null || cells.Count == 0) return false;
for (int i = 0; i < cells.Count; i++)
{
int x = ax + cells[i].dx;
int y = ay + cells[i].dy;
if (x < 0 || x >= BattlefieldConfig.Width) return false;
if (y < 0 || y >= BattlefieldConfig.Height) return false;
if (occupied[x, y]) return false;
}
return true;
}
private static void Commit(ObstacleDefinition obstacle, int ax, int ay, bool[,] occupied)
{
var cells = obstacle.blockedHexes;
for (int i = 0; i < cells.Count; i++)
{
int x = ax + cells[i].dx;
int y = ay + cells[i].dy;
occupied[x, y] = true;
}
}
private static int RandomRange(Random rng, int minInclusive, int maxInclusive)
{
if (maxInclusive < minInclusive) (minInclusive, maxInclusive) = (maxInclusive, minInclusive);
return rng.Next(minInclusive, maxInclusive + 1);
}
}
}