493 lines
17 KiB
C#
493 lines
17 KiB
C#
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<Vector2I, ChunkData> _loadedChunks = new();
|
|
private Vector2I _lastPlayerChunk = new(-1000, -1000);
|
|
|
|
private Player _player;
|
|
|
|
private readonly ConcurrentQueue<Action> _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<Vector2I>();
|
|
|
|
// 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<Vector2I> tileList)
|
|
{
|
|
var placed = new HashSet<Vector2I>();
|
|
var queue = new Queue<Vector2I>();
|
|
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<Vector2I>(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<Vector2I> tileList)
|
|
{
|
|
GD.Print($"{LogPrefix} Starting to place vein of type {tileType} at {startCell} with max size {maxSize}");
|
|
|
|
var placed = new HashSet<Vector2I>();
|
|
var queue = new Queue<Vector2I>();
|
|
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<Vector2I> StoneTiles { get; } = [];
|
|
public List<Vector2I> IronTiles { get; } = [];
|
|
} |