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