using System; using System.Collections.Concurrent; using Godot; using System.Collections.Generic; using System.Threading.Tasks; using AceFieldNewHorizon.Scripts.Entities; using AceFieldNewHorizon.Scripts.Tiles; namespace AceFieldNewHorizon.Scripts.System; public partial class NaturalResourceGenerator : Node2D { public const string ChunkTrackerGroupName = "NrgTrackingTarget"; [Export] public GridManager Grid { get; set; } [Export] public BuildingRegistry Registry { get; set; } [Export] public int ChunkSize = 16; [Export] public int LoadDistance = 2; // Number of chunks to load in each direction [Export] public float StoneDensity = 0.1f; [Export] public float IronDensity = 0.03f; [Export] public int MinStoneVeinSize = 1; [Export] public int MaxStoneVeinSize = 5; [Export] public int MinIronVeinSize = 1; [Export] public int MaxIronVeinSize = 3; [Export] public int Seed; [Export] public bool SpawnEnemyNest = true; [Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0) [Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin private const string LogPrefix = "[NaturalGeneration]"; private RandomNumberGenerator _rng; private readonly Dictionary _loadedChunks = new(); private Vector2I _lastPlayerChunk = new(-1000, -1000); private Player _player; private readonly ConcurrentQueue _mainThreadActions = new(); private Task _generationTask; private bool _isRunning = true; public override void _Ready() { _rng = new RandomNumberGenerator(); _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); // Test if building registry is assigned if (Registry == null) { GD.PrintErr($"{LogPrefix} BuildingRegistry is not assigned!"); return; } // Test if enemy_nest is in the registry var testBuilding = Registry.GetBuilding("enemy_nest"); if (testBuilding == null) { GD.PrintErr($"{LogPrefix} 'enemy_nest' is not found in BuildingRegistry!"); } else { GD.Print($"{LogPrefix} Found enemy_nest in registry!"); } GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); if (SpawnEnemyNest) { GD.Print($"{LogPrefix} Attempting to spawn enemy nest..."); SpawnRandomEnemyNest(); } } public override void _Process(double delta) { // Process any pending main thread actions while (_mainThreadActions.TryDequeue(out var action)) { action.Invoke(); } // Rest of your existing _Process code _player = GetTree().GetFirstNodeInGroup(ChunkTrackerGroupName) as Player; if (_player != null) { UpdatePlayerPosition(_player.GlobalPosition); } else { GD.PrintErr($"{LogPrefix} Player not found in group: {ChunkTrackerGroupName}"); } } private void UpdatePlayerPosition(Vector2 playerPosition) { var playerChunk = WorldToChunkCoords(playerPosition); if (playerChunk == _lastPlayerChunk) return; _lastPlayerChunk = playerChunk; // Start generation in background task if not already running if (_generationTask == null || _generationTask.IsCompleted) { _generationTask = Task.Run(() => GenerateChunksAroundPlayer(playerChunk)); } } private void GenerateChunksAroundPlayer(Vector2I playerChunk) { var chunksToLoad = new List(); // Determine which chunks to load/unload for (int x = -LoadDistance; x <= LoadDistance; x++) { for (int y = -LoadDistance; y <= LoadDistance; y++) { var chunkPos = new Vector2I(playerChunk.X + x, playerChunk.Y + y); if (!_loadedChunks.ContainsKey(chunkPos)) { chunksToLoad.Add(chunkPos); } } } // Generate chunks in background foreach (var chunkPos in chunksToLoad) { if (!_isRunning) return; GenerateChunkInBackground(chunkPos); } } private void GenerateChunkInBackground(Vector2I chunkPos) { // Skip if chunk was loaded while we were waiting if (_loadedChunks.ContainsKey(chunkPos)) return; var chunkData = new ChunkData(); var chunkWorldPos = ChunkToWorldCoords(chunkPos); // Generate ground tiles for (int x = 0; x < ChunkSize; x++) { for (int y = 0; y < ChunkSize; y++) { var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); _mainThreadActions.Enqueue(() => PlaceTile("ground", cell)); } } // Generate stone veins for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); if (_rng.Randf() < StoneDensity) { var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize); PlaceVeinInBackground(cell, "stone", veinSize, chunkData.StoneTiles); } else if (_rng.Randf() < IronDensity) { var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize); PlaceVeinInBackground(cell, "ore_iron", veinSize, chunkData.IronTiles); } } } // Add chunk data to dictionary on main thread _mainThreadActions.Enqueue(() => { _loadedChunks[chunkPos] = chunkData; }); } private void PlaceVeinInBackground(Vector2I startCell, string tileType, int maxSize, List tileList) { var placed = new HashSet(); var queue = new Queue(); queue.Enqueue(startCell); int placedCount = 0; while (queue.Count > 0 && placedCount < maxSize && _isRunning) { var cell = queue.Dequeue(); if (placed.Contains(cell)) continue; // Schedule tile placement on main thread _mainThreadActions.Enqueue(() => { if (PlaceTile(tileType, cell)) { tileList.Add(cell); } }); placed.Add(cell); placedCount++; // Add adjacent cells var directions = new[] { Vector2I.Up, Vector2I.Right, Vector2I.Down, Vector2I.Left }; foreach (var dir in directions) { var newCell = cell + dir; if (!placed.Contains(newCell)) { queue.Enqueue(newCell); } } } } public override void _ExitTree() { // Clean up _isRunning = false; _generationTask?.Wait(); // Wait for current generation to finish base._ExitTree(); } private void GenerateChunk(Vector2I chunkPos) { GD.Print($"{LogPrefix} Generating chunk at {chunkPos}"); var chunkData = new ChunkData(); var chunkWorldPos = ChunkToWorldCoords(chunkPos); // First, place ground tiles for (int x = 0; x < ChunkSize; x++) { for (int y = 0; y < ChunkSize; y++) { var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); if (!PlaceTile("ground", cell)) { GD.PrintErr($"{LogPrefix} Failed to place ground at {cell}"); } } } // Then generate stone veins var stoneVeins = 0; for (var x = 0; x < ChunkSize; x++) { for (var y = 0; y < ChunkSize; y++) { var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); if (_rng.Randf() < StoneDensity) { var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize); GD.Print($"{LogPrefix} Attempting to place stone vein at {cell} with size {veinSize}"); PlaceVein(cell, "stone", veinSize, chunkData.StoneTiles); stoneVeins++; } } } GD.Print($"{LogPrefix} Placed {stoneVeins} stone veins in chunk {chunkPos}"); // Generate iron veins within stone int ironVeins = 0; foreach (var stoneCell in new List(chunkData.StoneTiles)) { if (_rng.Randf() < IronDensity) { var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize); GD.Print($"{LogPrefix} Attempting to place iron vein at {stoneCell} with size {veinSize}"); PlaceVein(stoneCell, "ore_iron", veinSize, chunkData.IronTiles); ironVeins++; } } GD.Print($"{LogPrefix} Placed {ironVeins} iron veins in chunk {chunkPos}"); _loadedChunks[chunkPos] = chunkData; } private void UnloadChunk(Vector2I chunkPos) { GD.Print($"{LogPrefix} Unloading chunk at {chunkPos}"); if (!_loadedChunks.TryGetValue(chunkPos, out var chunkData)) return; // Remove all tiles in this chunk var chunkWorldPos = ChunkToWorldCoords(chunkPos); for (int x = 0; x < ChunkSize; x++) { for (int y = 0; y < ChunkSize; y++) { var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); // Free a 1x1 area for each cell Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground); } } _loadedChunks.Remove(chunkPos); } private Vector2I WorldToChunkCoords(Vector2 worldPos) { var cell = GridUtils.WorldToGrid(worldPos); return new Vector2I( (int)Mathf.Floor((float)cell.X / ChunkSize), (int)Mathf.Floor((float)cell.Y / ChunkSize) ); } private Vector2I ChunkToWorldCoords(Vector2I chunkPos) { return new Vector2I( chunkPos.X * ChunkSize, chunkPos.Y * ChunkSize ); } private bool IsInLoadedChunk(Vector2I cell) { // Check the chunk where the cell is located var chunkPos = new Vector2I( (int)Mathf.Floor((float)cell.X / ChunkSize), (int)Mathf.Floor((float)cell.Y / ChunkSize) ); // Also check adjacent chunks since veins can cross chunk boundaries for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var checkPos = new Vector2I(chunkPos.X + dx, chunkPos.Y + dy); if (_loadedChunks.ContainsKey(checkPos)) { return true; } } } return false; } private void PlaceVein(Vector2I startCell, string tileType, int maxSize, List tileList) { GD.Print($"{LogPrefix} Starting to place vein of type {tileType} at {startCell} with max size {maxSize}"); var placed = new HashSet(); var queue = new Queue(); queue.Enqueue(startCell); int placedCount = 0; while (queue.Count > 0 && placedCount < maxSize) { var cell = queue.Dequeue(); // Skip if already placed or out of bounds if (placed.Contains(cell)) { GD.Print($"{LogPrefix} Skipping cell {cell} - already placed"); continue; } if (!IsInLoadedChunk(cell)) { GD.Print($"{LogPrefix} Skipping cell {cell} - out of bounds"); continue; } // Try to place the tile if (PlaceTile(tileType, cell)) { tileList.Add(cell); placed.Add(cell); placedCount++; // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} ({placedCount}/{maxSize})"); // Add adjacent cells to queue var directions = new[] { Vector2I.Up, Vector2I.Right, Vector2I.Down, Vector2I.Left }; foreach (var dir in directions) { var newCell = cell + dir; if (!placed.Contains(newCell)) { queue.Enqueue(newCell); } } } else { GD.Print($"{LogPrefix} Failed to place {tileType} at {cell}"); } } GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles"); } private void SpawnRandomEnemyNest() { // Generate a random position within the specified distance from origin var angle = _rng.Randf() * Mathf.Pi * 2; var distance = _rng.RandfRange(MinDistanceFromOrigin, MaxDistanceFromOrigin); var offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * distance; var nestPosition = new Vector2I((int)offset.X, (int)offset.Y); // Try to find a valid position for the nest int attempts = 0; const int maxAttempts = 10; while (attempts < maxAttempts) { if (PlaceTile("enemy_nest", nestPosition)) { GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}"); return; } // Try a different position if placement failed angle = _rng.Randf() * Mathf.Pi * 2; distance = _rng.RandfRange(MinDistanceFromOrigin, MaxDistanceFromOrigin); offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * distance; nestPosition = new Vector2I((int)offset.X, (int)offset.Y); attempts++; } GD.PrintErr($"{LogPrefix} Failed to place enemy nest after {maxAttempts} attempts"); } private bool PlaceTile(string tileType, Vector2I cell) { try { // For enemy nest, we want to place it on top of existing ground if (tileType != "enemy_nest") { // Original behavior for other tile types Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground); } else { // For enemy nest, check if there's ground below (skip for now) GD.Print($"{LogPrefix} Attempting placing nest at {cell}."); } var building = Registry.GetBuilding(tileType); if (building == null) { GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}"); return false; } // GD.Print($"{LogPrefix} Found building in registry: {tileType}"); var scene = building.Scene; if (scene == null) { GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}"); return false; } // GD.Print($"{LogPrefix} Scene loaded for {tileType}"); if (scene.Instantiate() is not BaseTile instance) { GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}"); return false; } // GD.Print($"{LogPrefix} Successfully instantiated {tileType}"); // Use the same positioning logic as PlacementManager var rotatedSize = building.GetRotatedSize(0f); var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); instance.ZIndex = (int)building.Layer; instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; instance.Grid = Grid; AddChild(instance); // For enemy nest, use Building layer var layer = tileType == "enemy_nest" ? GridLayer.Building : building.Layer; Grid.OccupyArea(cell, instance, building.Size, 0f, layer); // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} on layer {layer}"); return true; } catch (Exception e) { GD.PrintErr($"{LogPrefix} Error placing {tileType} at {cell}: {e.Message}"); GD.Print($"{LogPrefix} Stack trace: {e.StackTrace}"); return false; } } } public class ChunkData { public List StoneTiles { get; } = []; public List IronTiles { get; } = []; }