♻️ Optimizations of the various system

🍱 Retexture of the enemy portal
This commit is contained in:
2025-08-31 18:26:45 +08:00
parent 09511b37c9
commit b424aafeab
16 changed files with 497 additions and 399 deletions

View File

@@ -42,14 +42,14 @@
"layer": 1, "layer": 1,
"size": [3, 3] "size": [3, 3]
}, },
"enemy_nest": { "enemy_portal": {
"scene": "res://Scenes/Tiles/EnemyNest.tscn", "scene": "res://Scenes/Tiles/EnemyPortalTile.tscn",
"cost": {}, "cost": {},
"durability": 200, "durability": 200,
"buildTime": 0.0, "buildTime": 0.0,
"allowedRotations": [0], "allowedRotations": [0],
"layer": 1, "layer": 1,
"size": [1, 1] "size": [1, 2]
}, },
"ground": { "ground": {
"scene": "res://Scenes/Tiles/GroundTile.tscn", "scene": "res://Scenes/Tiles/GroundTile.tscn",

View File

@@ -1,20 +0,0 @@
[gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"]
[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyNestTile.cs" id="1_4g0ff"]
[ext_resource type="Texture2D" uid="uid://vwfs68ftvjr4" path="res://Scenes/Tiles/EnemyNest.jpg" id="1_id484"]
[ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_pka71"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"]
size = Vector2(54, 54)
[node name="EnemyNest" type="StaticBody2D"]
script = ExtResource("1_4g0ff")
EnemyScene = ExtResource("2_pka71")
TileId = "enemy_nest"
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.056, 0.056)
texture = ExtResource("1_id484")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_id484")

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://vwfs68ftvjr4" uid="uid://dv2xwfyshxdtp"
path="res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex" path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://Scenes/Tiles/EnemyNest.jpg" source_file="res://Scenes/Tiles/EnemyPortalTile.png"
dest_files=["res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex"] dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"]
[params] [params]

View File

@@ -0,0 +1,21 @@
[gd_scene load_steps=4 format=3 uid="uid://dup2su0s3ybcy"]
[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"]
[ext_resource type="Texture2D" uid="uid://dv2xwfyshxdtp" path="res://Scenes/Tiles/EnemyPortalTile.png" id="3_i4us4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"]
size = Vector2(54, 79)
[node name="EnemyPortal" type="StaticBody2D"]
collision_layer = 0
collision_mask = 0
script = ExtResource("1_o543x")
TileId = "enemy_portal"
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.1, 0.1)
texture = ExtResource("3_i4us4")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, -0.5)
shape = SubResource("RectangleShape2D_id484")

View File

@@ -5,266 +5,9 @@ using AceFieldNewHorizon.Scripts.System;
namespace AceFieldNewHorizon.Scripts.Entities; namespace AceFieldNewHorizon.Scripts.Entities;
public partial class Enemy : CharacterBody2D public partial class Enemy : BaseEnemy
{ {
public const string EnemyGroupName = "Enemy"; // All the base functionality is now in BaseEnemy
// This class is kept for backward compatibility and can be used to add
[Export] public float MoveSpeed = 150.0f; // specific behaviors for the basic enemy type if needed
[Export] public float DetectionRadius = 300.0f;
[Export] public float AttackRadius = 80.0f;
[Export] public int Damage = 10;
[Export] public float AttackCooldown = 1.0f;
[Export] public int MaxHealth = 100;
[Export] public bool ShowDamageNumbers = true;
private BaseTile _targetTile;
private ReactorTile _reactor;
private float _attackTimer = 0;
private Area2D _detectionArea;
private Area2D _attackArea;
private int _currentHealth;
private ProgressBar _healthBar;
private GridManager _gridManager;
// Track collisions with potential targets
private readonly HashSet<BaseTile> _collidingTiles = [];
public int CurrentHealth
{
get => _currentHealth;
private set
{
_currentHealth = Mathf.Clamp(value, 0, MaxHealth);
UpdateHealthBar();
if (_currentHealth <= 0)
{
Die();
}
}
}
public bool IsDead => _currentHealth <= 0;
public override void _Ready()
{
_currentHealth = MaxHealth;
_gridManager = DependencyInjection.Container.GetInstance<GridManager>();
// Find the reactor in the scene
_reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile;
// Create health bar
_healthBar = new ProgressBar
{
MaxValue = MaxHealth,
Value = _currentHealth,
Size = new Vector2(40, 4),
ShowPercentage = false,
Visible = false
};
var healthBarContainer = new Control();
healthBarContainer.AddChild(_healthBar);
AddChild(healthBarContainer);
healthBarContainer.Position = new Vector2(-20, -20);
// Create detection area for finding targets
_detectionArea = new Area2D();
var collisionShape = new CollisionShape2D();
var shape = new CircleShape2D();
shape.Radius = DetectionRadius;
collisionShape.Shape = shape;
_detectionArea.AddChild(collisionShape);
AddChild(_detectionArea);
_attackArea = GetNodeOrNull<Area2D>("AttackArea");
// Connect signals
_detectionArea.BodyEntered += OnBodyEnteredDetection;
_detectionArea.BodyExited += OnBodyExitedDetection;
if (_attackArea != null)
{
_attackArea.BodyEntered += OnBodyEntered;
_attackArea.BodyExited += OnBodyExited;
}
AddToGroup(EnemyGroupName);
}
public override void _Process(double delta)
{
if (IsDead) return;
// Find the best target if we don't have one
if (_targetTile == null || _targetTile.IsDestroyed || !_targetTile.IsInsideTree())
{
UpdateTarget();
}
// Move towards current target
if (_targetTile != null)
{
var direction = GlobalPosition.DirectionTo(_targetTile.GlobalPosition);
Velocity = direction * MoveSpeed;
LookAt(_targetTile.GlobalPosition);
MoveAndSlide();
}
// Attack any colliding tiles
if (_attackTimer > 0)
{
_attackTimer -= (float)delta;
}
else if (_collidingTiles.Count > 0)
{
// Attack the first colliding tile
foreach (var tile in _collidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
TryAttackTile(tile);
break;
}
}
}
}
private void UpdateTarget()
{
// If we have a valid target in collision, use that
foreach (var tile in _collidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
_targetTile = tile;
return;
}
}
// Otherwise find the reactor
if (_reactor != null && !_reactor.IsDestroyed && _reactor.IsInsideTree())
{
_targetTile = _reactor;
}
else
{
_targetTile = null;
}
}
private void TryAttackTile(BaseTile tile)
{
if (IsDead || tile == null || tile.IsDestroyed || !tile.IsInsideTree())
return;
_attackTimer = AttackCooldown;
bool wasDestroyed = tile.TakeDamage(Damage);
GD.Print($"Attacking {tile.Name} for {Damage} damage. Was destroyed: {wasDestroyed}");
if (wasDestroyed)
{
_collidingTiles.Remove(tile);
if (_targetTile == tile)
{
_targetTile = null;
}
}
}
private void OnBodyEntered(Node2D body)
{
if (body is BaseTile { IsDestroyed: false } tile && !body.IsInGroup("Hostile"))
{
GD.Print($"[Enemy] {body.Name} Entered attack range");
_collidingTiles.Add(tile);
if (_targetTile == null || _targetTile.IsDestroyed || !_targetTile.IsInsideTree())
{
_targetTile = tile;
}
// Attack immediately on collision
TryAttackTile(tile);
}
}
private void OnBodyExited(Node2D body)
{
if (body.GetParent() is BaseTile tile)
{
_collidingTiles.Remove(tile);
if (_targetTile == tile)
{
_targetTile = null;
}
}
}
private void OnBodyEnteredDetection(Node2D body)
{
// Keep this for initial target detection if needed
}
private void OnBodyExitedDetection(Node2D body)
{
// Keep this for target cleanup if needed
}
public void TakeDamage(int damage, Vector2? hitPosition = null)
{
if (IsDead) return;
CurrentHealth -= damage;
// Show damage number (optional)
if (ShowDamageNumbers)
{
var damageLabel = new Label
{
Text = $"-{damage}",
Position = hitPosition ?? GlobalPosition,
ZIndex = 1000
};
GetTree().CurrentScene.AddChild(damageLabel);
// Animate and remove damage number
var tween = CreateTween();
tween.TweenProperty(damageLabel, "position:y", damageLabel.Position.Y - 30, 0.5f);
tween.TweenCallback(Callable.From(() => damageLabel.QueueFree())).SetDelay(0.5f);
}
// Visual feedback
var originalModulate = Modulate;
Modulate = new Color(1, 0.5f, 0.5f); // Flash red
var tweenFlash = CreateTween();
tweenFlash.TweenProperty(this, "modulate", originalModulate, 0.2f);
}
private void UpdateHealthBar()
{
if (_healthBar != null)
{
_healthBar.Value = _currentHealth;
_healthBar.Visible = _currentHealth < MaxHealth; // Only show when damaged
}
}
private void Die()
{
// Play death animation/sound
// You can add a death animation here
// Disable collisions and hide
SetProcess(false);
SetPhysicsProcess(false);
Hide();
// Queue free after a delay (for any death animation/sound to play)
var timer = new Timer();
AddChild(timer);
timer.Timeout += () => QueueFree();
timer.Start(0.5f);
}
} }

View File

@@ -0,0 +1,283 @@
using System.Collections.Generic;
using AceFieldNewHorizon.Scripts.System;
using AceFieldNewHorizon.Scripts.Tiles;
using Godot;
namespace AceFieldNewHorizon.Scripts.Entities;
public abstract partial class BaseEnemy : CharacterBody2D
{
public const string EnemyGroupName = "Enemy";
[Export] public float MoveSpeed = 150.0f;
[Export] public float DetectionRadius = 300.0f;
[Export] public float AttackRadius = 80.0f;
[Export] public int Damage = 10;
[Export] public float AttackCooldown = 1.0f;
[Export] public int MaxHealth = 100;
[Export] public bool ShowDamageNumbers = true;
protected BaseTile TargetTile;
protected ReactorTile Reactor;
protected float AttackTimer = 0;
protected Area2D DetectionArea;
protected Area2D AttackArea;
protected int CurrentHealthValue;
protected ProgressBar HealthBar;
protected GridManager Grid;
// Track collisions with potential targets
protected readonly HashSet<BaseTile> CollidingTiles = [];
public int CurrentHealth
{
get => CurrentHealthValue;
protected set
{
CurrentHealthValue = Mathf.Clamp(value, 0, MaxHealth);
UpdateHealthBar();
if (CurrentHealthValue <= 0)
{
Die();
}
}
}
public bool IsDead => CurrentHealthValue <= 0;
public override void _Ready()
{
CurrentHealthValue = MaxHealth;
Grid = DependencyInjection.Container.GetInstance<GridManager>();
Reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile;
InitializeHealthBar();
InitializeDetectionArea();
InitializeAttackArea();
AddToGroup(EnemyGroupName);
}
protected virtual void InitializeHealthBar()
{
HealthBar = new ProgressBar
{
MaxValue = MaxHealth,
Value = CurrentHealthValue,
Size = new Vector2(40, 4),
ShowPercentage = false,
Visible = false
};
var healthBarContainer = new Control();
healthBarContainer.AddChild(HealthBar);
AddChild(healthBarContainer);
healthBarContainer.Position = new Vector2(-20, -20);
}
protected virtual void InitializeDetectionArea()
{
DetectionArea = new Area2D();
var collisionShape = new CollisionShape2D();
var shape = new CircleShape2D();
shape.Radius = DetectionRadius;
collisionShape.Shape = shape;
DetectionArea.AddChild(collisionShape);
AddChild(DetectionArea);
DetectionArea.BodyEntered += OnBodyEnteredDetection;
DetectionArea.BodyExited += OnBodyExitedDetection;
}
protected virtual void InitializeAttackArea()
{
AttackArea = GetNodeOrNull<Area2D>("AttackArea");
if (AttackArea != null)
{
AttackArea.BodyEntered += OnBodyEntered;
AttackArea.BodyExited += OnBodyExited;
}
}
public override void _Process(double delta)
{
if (IsDead) return;
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
{
UpdateTarget();
}
MoveTowardsTarget();
HandleAttacks(delta);
}
protected virtual void MoveTowardsTarget()
{
if (TargetTile != null)
{
var direction = GlobalPosition.DirectionTo(TargetTile.GlobalPosition);
Velocity = direction * MoveSpeed;
LookAt(TargetTile.GlobalPosition);
MoveAndSlide();
}
}
protected virtual void HandleAttacks(double delta)
{
if (AttackTimer > 0)
{
AttackTimer -= (float)delta;
}
else if (CollidingTiles.Count > 0)
{
foreach (var tile in CollidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
TryAttackTile(tile);
break;
}
}
}
}
protected virtual void UpdateTarget()
{
// If we have a valid target in collision, use that
foreach (var tile in CollidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
TargetTile = tile;
return;
}
}
// Otherwise find the reactor
if (Reactor != null && !Reactor.IsDestroyed && Reactor.IsInsideTree())
{
TargetTile = Reactor;
}
else
{
TargetTile = null;
}
}
protected virtual void TryAttackTile(BaseTile tile)
{
if (IsDead || tile == null || tile.IsDestroyed || !tile.IsInsideTree())
return;
AttackTimer = AttackCooldown;
bool wasDestroyed = tile.TakeDamage(Damage);
GD.Print($"Attacking {tile.Name} for {Damage} damage. Was destroyed: {wasDestroyed}");
if (wasDestroyed)
{
CollidingTiles.Remove(tile);
if (TargetTile == tile)
{
TargetTile = null;
}
}
}
protected virtual void OnBodyEntered(Node2D body)
{
if (body is BaseTile { IsDestroyed: false } tile && !body.IsInGroup("Hostile"))
{
GD.Print($"[Enemy] {body.Name} Entered attack range");
CollidingTiles.Add(tile);
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
{
TargetTile = tile;
}
// Attack immediately on collision
TryAttackTile(tile);
}
}
protected virtual void OnBodyExited(Node2D body)
{
if (body.GetParent() is BaseTile tile)
{
CollidingTiles.Remove(tile);
if (TargetTile == tile)
{
TargetTile = null;
}
}
}
protected virtual void OnBodyEnteredDetection(Node2D body)
{
// Can be overridden by derived classes
}
protected virtual void OnBodyExitedDetection(Node2D body)
{
// Can be overridden by derived classes
}
public virtual void TakeDamage(int damage, Vector2? hitPosition = null)
{
if (IsDead) return;
CurrentHealth -= damage;
// Show damage number (optional)
if (ShowDamageNumbers)
{
var damageLabel = new Label
{
Text = $"-{damage}",
Position = hitPosition ?? GlobalPosition,
ZIndex = 1000
};
GetTree().CurrentScene.AddChild(damageLabel);
// Animate and remove damage number
var tween = CreateTween();
tween.TweenProperty(damageLabel, "position:y", damageLabel.Position.Y - 30, 0.5f);
tween.TweenCallback(Callable.From(() => damageLabel.QueueFree())).SetDelay(0.5f);
}
// Visual feedback
var originalModulate = Modulate;
Modulate = new Color(1, 0.5f, 0.5f); // Flash red
var tweenFlash = CreateTween();
tweenFlash.TweenProperty(this, "modulate", originalModulate, 0.2f);
}
protected virtual void UpdateHealthBar()
{
if (HealthBar != null)
{
HealthBar.Value = CurrentHealthValue;
HealthBar.Visible = CurrentHealthValue < MaxHealth; // Only show when damaged
}
}
protected virtual void Die()
{
// Play death animation/sound
// You can add a death animation here
// Disable collisions and hide
SetProcess(false);
SetPhysicsProcess(false);
Hide();
// Queue free after a delay (for any death animation/sound to play)
var timer = new Timer();
AddChild(timer);
timer.Timeout += () => QueueFree();
timer.Start(0.5f);
}
}

View File

@@ -0,0 +1 @@
uid://6oduws4kbdlf

View File

@@ -43,11 +43,29 @@ public partial class GridManager : Node
public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building)
{ {
// Get all cells that should be occupied by this building
var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation);
foreach (var cell in occupiedCells)
// Create a list to store cells that should be removed
var cellsToRemove = new List<Vector2I>();
// First, find all cells that match this building's position and size
foreach (var cell in _layers[layer].Keys.ToList())
{ {
if (_layers[layer].ContainsKey(cell)) var (building, buildingSize, buildingRotation) = _layers[layer][cell];
_layers[layer].Remove(cell); var buildingCells = GridUtils.GetOccupiedCells(cell, buildingSize, buildingRotation);
// If any of the building's cells match our target area, mark all of its cells for removal
if (buildingCells.Any(c => occupiedCells.Contains(c)))
{
cellsToRemove.AddRange(buildingCells);
}
}
// Remove all marked cells
foreach (var cell in cellsToRemove.Distinct())
{
_layers[layer].Remove(cell);
} }
} }

View File

@@ -55,15 +55,15 @@ public partial class NaturalResourceGenerator : Node2D
return; return;
} }
// Test if enemy_nest is in the registry // Test if enemy_portal is in the registry
var testBuilding = Registry.GetBuilding("enemy_nest"); var testBuilding = Registry.GetBuilding("enemy_portal");
if (testBuilding == null) if (testBuilding == null)
{ {
GD.PrintErr($"{LogPrefix} 'enemy_nest' is not found in BuildingRegistry!"); GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!");
} }
else else
{ {
GD.Print($"{LogPrefix} Found enemy_nest in registry!"); GD.Print($"{LogPrefix} Found enemy_portal in registry!");
} }
GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}");
@@ -287,9 +287,9 @@ public partial class NaturalResourceGenerator : Node2D
// Remove all tiles in this chunk // Remove all tiles in this chunk
var chunkWorldPos = ChunkToWorldCoords(chunkPos); var chunkWorldPos = ChunkToWorldCoords(chunkPos);
for (int x = 0; x < ChunkSize; x++) for (var x = 0; x < ChunkSize; x++)
{ {
for (int y = 0; y < ChunkSize; y++) for (var y = 0; y < ChunkSize; y++)
{ {
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
// Free a 1x1 area for each cell // Free a 1x1 area for each cell
@@ -416,7 +416,7 @@ public partial class NaturalResourceGenerator : Node2D
while (attempts < maxAttempts) while (attempts < maxAttempts)
{ {
if (PlaceTile("enemy_nest", nestPosition)) if (PlaceTile("enemy_portal", nestPosition))
{ {
GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}"); GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}");
return; return;

View File

@@ -428,32 +428,28 @@ public static class GridManagerExtensions
public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid, public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid,
Vector2I cell, GridLayer layer) Vector2I cell, GridLayer layer)
{ {
if (grid.GetTileAtCell(cell, layer) is { } building) if (grid.GetTileAtCell(cell, layer) is not { } building) return null;
// Find the top-left position of the building
for (var x = 0; x < 100; x++) // Arbitrary max size
{ {
// Find the top-left position of the building for (var y = 0; y < 100; y++)
for (int x = 0; x < 100; x++) // Arbitrary max size
{ {
for (int y = 0; y < 100; y++) var checkCell = new Vector2I(cell.X - x, cell.Y - y);
{ if (grid.GetTileAtCell(checkCell, layer) != building) continue;
var checkCell = new Vector2I(cell.X - x, cell.Y - y); // Found the top-left corner, now find the size
if (grid.GetTileAtCell(checkCell, layer) == building) var size = Vector2I.One;
{ // Search right
// Found the top-left corner, now find the size while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
var size = Vector2I.One; building)
// Search right size.X++;
while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == // Search down
building) while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
size.X++; building)
// Search down size.Y++;
while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
building)
size.Y++;
// Get rotation from the first cell // Get rotation from the first cell
var rotation = 0f; // You'll need to store rotation in GridManager to make this work var rotation = 0f; // You'll need to store rotation in Grid to make this work
return (checkCell, size, rotation); return (checkCell, size, rotation);
}
}
} }
} }

View File

@@ -136,6 +136,9 @@ public partial class BaseTile : Node2D
protected virtual void OnTileDestroyed() protected virtual void OnTileDestroyed()
{ {
// Can be overridden by derived classes for custom destruction behavior // Can be overridden by derived classes for custom destruction behavior
var cell = GridUtils.WorldToGrid(Position);
var buildingInfo = Registry.GetBuilding(TileId);
Grid.FreeArea(cell, buildingInfo.Size, Rotation, buildingInfo.Layer);
} }
// Building progress visualization // Building progress visualization

View File

@@ -1,77 +0,0 @@
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class EnemyNestTile : BaseTile
{
[Export] public PackedScene EnemyScene;
[Export] public int MaxEnemies = 5;
[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts
[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn
[Export] public bool Active = true;
private Timer _spawnTimer;
private int _currentEnemyCount = 0;
public override void _Ready()
{
base._Ready();
// Add to Hostile group to prevent enemies from attacking their own nest
AddToGroup("Hostile");
// Create and configure the timer
_spawnTimer = new Timer
{
Autostart = true,
WaitTime = SpawnDelay
};
AddChild(_spawnTimer);
_spawnTimer.Timeout += OnSpawnTimerTimeout;
}
private void OnSpawnTimerTimeout()
{
if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies)
return;
// Check if we can spawn more enemies
var enemies = GetTree().GetNodesInGroup("Enemy");
_currentEnemyCount = enemies.Count;
if (_currentEnemyCount >= MaxEnemies)
return;
// Spawn a new enemy
var enemy = EnemyScene.Instantiate<Enemy>();
if (enemy != null)
{
GetParent().AddChild(enemy);
// Calculate a random position within the spawn radius
var randomAngle = GD.Randf() * Mathf.Pi * 2;
var randomOffset = new Vector2(
Mathf.Cos(randomAngle) * SpawnRadius,
Mathf.Sin(randomAngle) * SpawnRadius
);
enemy.GlobalPosition = GlobalPosition + randomOffset;
_currentEnemyCount++;
// Connect to the enemy's death signal if available
enemy.TreeExiting += () => OnEnemyDied();
}
}
private void OnEnemyDied()
{
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
}
public void SetActive(bool active)
{
Active = active;
_spawnTimer.Paused = !active;
}
}

View File

@@ -0,0 +1,129 @@
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class EnemyPortalTile : BaseTile
{
[Export] public PackedScene EnemyScene;
[Export] public int MaxEnemies = 5;
[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts
[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn
[Export] public bool Active = true;
private Timer _spawnTimer;
private int _currentEnemyCount = 0;
public override void _Ready()
{
base._Ready();
// Add to Hostile group to prevent enemies from attacking their own nest
AddToGroup("Hostile");
// Create and configure the timer
_spawnTimer = new Timer
{
Autostart = true,
WaitTime = SpawnDelay
};
AddChild(_spawnTimer);
_spawnTimer.Timeout += OnSpawnTimerTimeout;
var sprite = GetNode<Sprite2D>("Sprite2D");
// Create and configure the shadow sprite
var shadow = new Sprite2D
{
Texture = sprite.Texture,
Scale = sprite.Scale * 1.05f, // Slightly larger than the original
Modulate = new Color(0, 0, 0, 0.5f), // Slightly more transparent
Position = new Vector2(0, 12), // Closer to the sprite (reduced from 30)
ZIndex = -1
};
AddChild(shadow);
// Create floating animation
const float floatOffset = 5.0f;
const float floatDuration = 2.0f;
var tween = CreateTween().SetLoops();
tween.TweenProperty(sprite, "position:y", -floatOffset, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(sprite, "position:y", floatOffset, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(sprite, "position:y", 0, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
// Animate shadow
tween.Parallel().TweenProperty(shadow, "position:y", 12 - (floatOffset * 0.3f), floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.02f, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.6f, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "position:y", 12 + (floatOffset * 0.4f), floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.08f, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.4f, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
}
private void OnSpawnTimerTimeout()
{
if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies)
return;
// Check if we can spawn more enemies
var enemies = GetTree().GetNodesInGroup("Enemy");
_currentEnemyCount = enemies.Count;
if (_currentEnemyCount >= MaxEnemies)
return;
// Spawn a new enemy
var enemy = EnemyScene.Instantiate<Enemy>();
if (enemy != null)
{
GetParent().AddChild(enemy);
// Calculate a random position within the spawn radius
var randomAngle = GD.Randf() * Mathf.Pi * 2;
var randomOffset = new Vector2(
Mathf.Cos(randomAngle) * SpawnRadius,
Mathf.Sin(randomAngle) * SpawnRadius
);
enemy.GlobalPosition = GlobalPosition + randomOffset;
_currentEnemyCount++;
// Connect to the enemy's death signal if available
enemy.TreeExiting += () => OnEnemyDied();
}
}
private void OnEnemyDied()
{
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
}
public void SetActive(bool active)
{
Active = active;
_spawnTimer.Paused = !active;
}
}

View File

@@ -8,6 +8,7 @@ public partial class GroundTile : BaseTile
{ {
var sprite = GetNode<Sprite2D>("Sprite2D"); var sprite = GetNode<Sprite2D>("Sprite2D");
sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker
sprite.ZIndex = -10;
base._Ready(); base._Ready();
} }