Compare commits
	
		
			4 Commits
		
	
	
		
			c72353716f
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2c5e0459ad | |||
| f2c243ecf6 | |||
| b424aafeab | |||
| 09511b37c9 | 
| @@ -33,14 +33,23 @@ | |||||||
| 	"layer": 1, | 	"layer": 1, | ||||||
| 	"size": [1, 1] | 	"size": [1, 1] | ||||||
|   }, |   }, | ||||||
|   "enemy_nest": { |   "reactor": { | ||||||
| 	"scene": "res://Scenes/Tiles/EnemyNest.tscn", | 	"scene": "res://Scenes/Tiles/ReactorTile.tscn", | ||||||
|  | 	"cost": {}, | ||||||
|  | 	"durability": 1000, | ||||||
|  | 	"buildTime": 5.0, | ||||||
|  | 	"allowedRotations": [0], | ||||||
|  | 	"layer": 1, | ||||||
|  | 	"size": [3, 3] | ||||||
|  |   }, | ||||||
|  |   "enemy_portal": { | ||||||
|  | 	"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", | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Entities/Enemy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Scenes/Entities/Enemy.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 807 KiB | 
| @@ -2,16 +2,16 @@ | |||||||
| 
 | 
 | ||||||
| importer="texture" | importer="texture" | ||||||
| type="CompressedTexture2D" | type="CompressedTexture2D" | ||||||
| uid="uid://dlhpiyxtmp707" | uid="uid://x4u6oatvsm8y" | ||||||
| path="res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.ctex" | path="res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex" | ||||||
| metadata={ | metadata={ | ||||||
| "vram_texture": false | "vram_texture": false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| [deps] | [deps] | ||||||
| 
 | 
 | ||||||
| source_file="res://Scenes/Entities/Enemy.jpg" | source_file="res://Scenes/Entities/Enemy.png" | ||||||
| dest_files=["res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.ctex"] | dest_files=["res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"] | ||||||
| 
 | 
 | ||||||
| [params] | [params] | ||||||
| 
 | 
 | ||||||
| @@ -1,19 +1,21 @@ | |||||||
| [gd_scene load_steps=4 format=3 uid="uid://b3ffcucytwmk"] | [gd_scene load_steps=3 format=3 uid="uid://b3ffcucytwmk"] | ||||||
|  |  | ||||||
| [ext_resource type="Texture2D" uid="uid://dlhpiyxtmp707" path="res://Scenes/Entities/Enemy.jpg" id="1_8q37v"] |  | ||||||
| [ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"] | [ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://x4u6oatvsm8y" path="res://Scenes/Entities/Enemy.png" id="2_jajit"] | ||||||
| [sub_resource type="RectangleShape2D" id="RectangleShape2D_jajit"] |  | ||||||
| size = Vector2(60, 74) |  | ||||||
|  |  | ||||||
| [node name="Enemy" type="CharacterBody2D"] | [node name="Enemy" type="CharacterBody2D"] | ||||||
| collision_layer = 2 | collision_layer = 2 | ||||||
| collision_mask = 3 | collision_mask = 3 | ||||||
| script = ExtResource("1_jajit") | script = ExtResource("1_jajit") | ||||||
|  |  | ||||||
| [node name="CollisionShape2D" type="CollisionShape2D" parent="."] | [node name="CollisionShape2D" type="CollisionPolygon2D" parent="."] | ||||||
| shape = SubResource("RectangleShape2D_jajit") | polygon = PackedVector2Array(-2, -21, 3, -21, 24, 6, 24, 10, 20, 14, -21, 14, -24, 11, -24, 7) | ||||||
|  |  | ||||||
| [node name="Sprite2D" type="Sprite2D" parent="."] | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
| scale = Vector2(0.1, 0.1) | scale = Vector2(0.05, 0.05) | ||||||
| texture = ExtResource("1_8q37v") | texture = ExtResource("2_jajit") | ||||||
|  |  | ||||||
|  | [node name="AttackArea" type="Area2D" parent="."] | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionPolygon2D" parent="AttackArea"] | ||||||
|  | polygon = PackedVector2Array(-3, -24, 4, -24, 27, 5, 27, 14, 20, 19, -21, 19, -27, 14, -27, 5) | ||||||
|   | |||||||
| @@ -14,9 +14,8 @@ script = ExtResource("2_oss8w") | |||||||
| [node name="PlacementSystem" type="Node2D" parent="."] | [node name="PlacementSystem" type="Node2D" parent="."] | ||||||
| script = ExtResource("2_sxhdm") | script = ExtResource("2_sxhdm") | ||||||
|  |  | ||||||
| [node name="Player" parent="." node_paths=PackedStringArray("Inventory") instance=ExtResource("3_oss8w")] | [node name="Player" parent="." instance=ExtResource("3_oss8w")] | ||||||
| scale = Vector2(0.35, 0.35) | scale = Vector2(0.35, 0.35) | ||||||
| Inventory = NodePath("") |  | ||||||
|  |  | ||||||
| [node name="HUD" parent="." instance=ExtResource("8_hud_scene")] | [node name="HUD" parent="." instance=ExtResource("8_hud_scene")] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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") |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/EnemyPortalTile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Scenes/Tiles/EnemyPortalTile.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 431 KiB | 
							
								
								
									
										34
									
								
								Scenes/Tiles/EnemyPortalTile.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Scenes/Tiles/EnemyPortalTile.png.import
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://dv2xwfyshxdtp" | ||||||
|  | path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Tiles/EnemyPortalTile.png" | ||||||
|  | dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										23
									
								
								Scenes/Tiles/EnemyPortalTile.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Scenes/Tiles/EnemyPortalTile.tscn
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | [gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"] | ||||||
|  | [ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_nh7ff"] | ||||||
|  | [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") | ||||||
|  | EnemyScene = ExtResource("2_nh7ff") | ||||||
|  | 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") | ||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/ReactorTile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Scenes/Tiles/ReactorTile.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 544 KiB | 
| @@ -2,16 +2,16 @@ | |||||||
| 
 | 
 | ||||||
| importer="texture" | importer="texture" | ||||||
| type="CompressedTexture2D" | type="CompressedTexture2D" | ||||||
| uid="uid://vwfs68ftvjr4" | uid="uid://fg03qxqphp7n" | ||||||
| path="res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex" | path="res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex" | ||||||
| metadata={ | metadata={ | ||||||
| "vram_texture": false | "vram_texture": false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| [deps] | [deps] | ||||||
| 
 | 
 | ||||||
| source_file="res://Scenes/Tiles/EnemyNest.jpg" | source_file="res://Scenes/Tiles/ReactorTile.png" | ||||||
| dest_files=["res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex"] | dest_files=["res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"] | ||||||
| 
 | 
 | ||||||
| [params] | [params] | ||||||
| 
 | 
 | ||||||
							
								
								
									
										26
									
								
								Scenes/Tiles/ReactorTile.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Scenes/Tiles/ReactorTile.tscn
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | [gd_scene load_steps=4 format=3 uid="uid://w6ni678js7cu"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://c4k3ottt7j3b1" path="res://Scripts/Tiles/ReactorTile.cs" id="1_yldg2"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://fg03qxqphp7n" path="res://Scenes/Tiles/ReactorTile.png" id="3_fk1vt"] | ||||||
|  |  | ||||||
|  | [sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"] | ||||||
|  | size = Vector2(54, 54) | ||||||
|  |  | ||||||
|  | [node name="ReactorTile" type="StaticBody2D"] | ||||||
|  | script = ExtResource("1_yldg2") | ||||||
|  | TileId = "reactor" | ||||||
|  |  | ||||||
|  | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|  | scale = Vector2(0.3, 0.3) | ||||||
|  | texture = ExtResource("3_fk1vt") | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] | ||||||
|  | scale = Vector2(3, 3) | ||||||
|  | shape = SubResource("RectangleShape2D_8o613") | ||||||
|  |  | ||||||
|  | [node name="ProgressOverlay" type="ColorRect" parent="."] | ||||||
|  | offset_left = -81.0 | ||||||
|  | offset_top = -81.0 | ||||||
|  | offset_right = -27.0 | ||||||
|  | offset_bottom = -27.0 | ||||||
|  | scale = Vector2(3, 3) | ||||||
| @@ -1,193 +1,13 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using AceFieldNewHorizon.Scripts.Tiles; | ||||||
| using Godot; | using Godot; | ||||||
|  | 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 AttackRange = 50.0f; |  | ||||||
|     [Export] public int Damage = 10; |  | ||||||
|     [Export] public float AttackCooldown = 1.0f; |  | ||||||
|     [Export] public int MaxHealth = 100; |  | ||||||
|     [Export] public bool ShowDamageNumbers = true; |  | ||||||
|      |  | ||||||
|     private Player _player; |  | ||||||
|     private float _attackTimer = 0; |  | ||||||
|     private bool _isPlayerInRange = false; |  | ||||||
|     private Area2D _detectionArea; |  | ||||||
|     private int _currentHealth; |  | ||||||
|     private ProgressBar _healthBar; |  | ||||||
|      |  | ||||||
|     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; |  | ||||||
|          |  | ||||||
|         // 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); // Adjust position as needed |  | ||||||
|          |  | ||||||
|         // Create detection area |  | ||||||
|         _detectionArea = new Area2D(); |  | ||||||
|         var collisionShape = new CollisionShape2D(); |  | ||||||
|         var shape = new CircleShape2D(); |  | ||||||
|         shape.Radius = DetectionRadius; |  | ||||||
|         collisionShape.Shape = shape; |  | ||||||
|         _detectionArea.AddChild(collisionShape); |  | ||||||
|         AddChild(_detectionArea); |  | ||||||
|          |  | ||||||
|         // Connect signals |  | ||||||
|         _detectionArea.BodyEntered += OnBodyEnteredDetection; |  | ||||||
|         _detectionArea.BodyExited += OnBodyExitedDetection; |  | ||||||
|          |  | ||||||
|         AddToGroup(EnemyGroupName); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     public override void _Process(double delta) |  | ||||||
|     { |  | ||||||
|         if (IsDead) return; |  | ||||||
|          |  | ||||||
|         if (_player != null && _isPlayerInRange) |  | ||||||
|         { |  | ||||||
|             // Face the player |  | ||||||
|             LookAt(_player.GlobalPosition); |  | ||||||
|              |  | ||||||
|             // Move towards player if not in attack range |  | ||||||
|             var direction = GlobalPosition.DirectionTo(_player.GlobalPosition); |  | ||||||
|             var distance = GlobalPosition.DistanceTo(_player.GlobalPosition); |  | ||||||
|              |  | ||||||
|             if (distance > AttackRange) |  | ||||||
|             { |  | ||||||
|                 Velocity = direction * MoveSpeed; |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 Velocity = Vector2.Zero; |  | ||||||
|                 TryAttackPlayer(delta); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             MoveAndSlide(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     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); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     private void TryAttackPlayer(double delta) |  | ||||||
|     { |  | ||||||
|         if (IsDead) return; |  | ||||||
|          |  | ||||||
|         _attackTimer += (float)delta; |  | ||||||
|          |  | ||||||
|         if (_attackTimer >= AttackCooldown) |  | ||||||
|         { |  | ||||||
|             _attackTimer = 0; |  | ||||||
|             // Here you can implement the attack logic |  | ||||||
|             // For example: _player.TakeDamage(Damage); |  | ||||||
|             GD.Print("Enemy attacks player!"); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     private void OnBodyEnteredDetection(Node2D body) |  | ||||||
|     { |  | ||||||
|         if (body is Player player) |  | ||||||
|         { |  | ||||||
|             _player = player; |  | ||||||
|             _isPlayerInRange = true; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     private void OnBodyExitedDetection(Node2D body) |  | ||||||
|     { |  | ||||||
|         if (body == _player) |  | ||||||
|         { |  | ||||||
|             _isPlayerInRange = false; |  | ||||||
|             Velocity = Vector2.Zero; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										283
									
								
								Scripts/Entities/EnemyBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								Scripts/Entities/EnemyBase.cs
									
									
									
									
									
										Normal 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 = 3.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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Entities/EnemyBase.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Scripts/Entities/EnemyBase.cs.uid
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | uid://6oduws4kbdlf | ||||||
| @@ -1,4 +1,3 @@ | |||||||
| using System; |  | ||||||
| using AceFieldNewHorizon.Scripts.System; | using AceFieldNewHorizon.Scripts.System; | ||||||
| using Godot; | using Godot; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,11 +8,20 @@ public partial class Hud : CanvasLayer | |||||||
| 	private ResourceManager _resourceManager; | 	private ResourceManager _resourceManager; | ||||||
| 	private VBoxContainer _resourceDisplay; | 	private VBoxContainer _resourceDisplay; | ||||||
| 	private readonly Dictionary<string, Label> _resourceLabels = new(); | 	private readonly Dictionary<string, Label> _resourceLabels = new(); | ||||||
|  | 	private VBoxContainer _pickupLogContainer; | ||||||
|  |  | ||||||
| 	public override void _Ready() | 	public override void _Ready() | ||||||
| 	{ | 	{ | ||||||
| 		_resourceDisplay = GetNode<VBoxContainer>("ResourceDisplay"); | 		_resourceDisplay = GetNode<VBoxContainer>("ResourceDisplay"); | ||||||
| 		 | 		 | ||||||
|  | 		_pickupLogContainer = new VBoxContainer(); | ||||||
|  | 		_pickupLogContainer.Name = "PickupLogContainer"; | ||||||
|  | 		_pickupLogContainer.SetAnchorsPreset(Control.LayoutPreset.BottomLeft); | ||||||
|  | 		_pickupLogContainer.OffsetLeft = 10; | ||||||
|  | 		_pickupLogContainer.OffsetBottom = -10; // Negative offset from bottom anchor | ||||||
|  | 		_pickupLogContainer.GrowVertical = Control.GrowDirection.Begin; // Make it grow upwards | ||||||
|  | 		AddChild(_pickupLogContainer); | ||||||
|  | 		 | ||||||
| 		_resourceManager = DependencyInjection.Container.GetInstance<ResourceManager>();  | 		_resourceManager = DependencyInjection.Container.GetInstance<ResourceManager>();  | ||||||
| 		if (_resourceManager == null) | 		if (_resourceManager == null) | ||||||
| 		{ | 		{ | ||||||
| @@ -21,6 +30,7 @@ public partial class Hud : CanvasLayer | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		_resourceManager.OnResourceChanged += UpdateResourceDisplay; | 		_resourceManager.OnResourceChanged += UpdateResourceDisplay; | ||||||
|  | 		_resourceManager.OnResourcePickedUp += OnResourcePickedUp; | ||||||
| 		 | 		 | ||||||
| 		// Initialize display with current resources | 		// Initialize display with current resources | ||||||
| 		foreach (var entry in _resourceManager.GetAllResources()) | 		foreach (var entry in _resourceManager.GetAllResources()) | ||||||
| @@ -54,11 +64,39 @@ public partial class Hud : CanvasLayer | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	private void OnResourcePickedUp(string resourceId, int amount) | ||||||
|  | 	{ | ||||||
|  | 		AddPickupLogEntry($"Picked up {amount} {resourceId}!"); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void AddPickupLogEntry(string message) | ||||||
|  | 	{ | ||||||
|  | 		var logLabel = new Label(); | ||||||
|  | 		logLabel.Text = message; | ||||||
|  | 		_pickupLogContainer.AddChild(logLabel); | ||||||
|  |  | ||||||
|  | 		var timer = new Timer(); | ||||||
|  | 		timer.WaitTime = 3.0f; | ||||||
|  | 		timer.OneShot = true; | ||||||
|  | 		timer.Autostart = true; | ||||||
|  | 		logLabel.AddChild(timer); // Add timer as child of the label | ||||||
|  | 		timer.Timeout += () => OnLogEntryTimeout(logLabel); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void OnLogEntryTimeout(Label logLabel) | ||||||
|  | 	{ | ||||||
|  | 		// Start fading out the label | ||||||
|  | 		var tween = logLabel.CreateTween(); | ||||||
|  | 		tween.TweenProperty(logLabel, "modulate", new Color(1, 1, 1, 0), 0.5f); // Fade out over 0.5 seconds | ||||||
|  | 		tween.TweenCallback(Callable.From(() => logLabel.QueueFree())); // Free after fading | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	public override void _ExitTree() | 	public override void _ExitTree() | ||||||
| 	{ | 	{ | ||||||
| 		if (_resourceManager != null) | 		if (_resourceManager != null) | ||||||
| 		{ | 		{ | ||||||
| 			_resourceManager.OnResourceChanged -= UpdateResourceDisplay; | 			_resourceManager.OnResourceChanged -= UpdateResourceDisplay; | ||||||
|  | 			_resourceManager.OnResourcePickedUp -= OnResourcePickedUp; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -55,18 +55,21 @@ 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}"); | ||||||
|  |          | ||||||
|  |         GD.Print($"{LogPrefix} Spawning the core reactor..."); | ||||||
|  |         SpawnCoreReactor(); | ||||||
|      |      | ||||||
|         if (SpawnEnemyNest) |         if (SpawnEnemyNest) | ||||||
|         { |         { | ||||||
| @@ -284,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 | ||||||
| @@ -393,6 +396,12 @@ public partial class NaturalResourceGenerator : Node2D | |||||||
|         GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles"); |         GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private void SpawnCoreReactor() | ||||||
|  |     { | ||||||
|  |         // Place the reactor tile at the center of the map | ||||||
|  |         PlaceTile("reactor", new Vector2I(0, 0)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private void SpawnRandomEnemyNest() |     private void SpawnRandomEnemyNest() | ||||||
|     { |     { | ||||||
|         // Generate a random position within the specified distance from origin |         // Generate a random position within the specified distance from origin | ||||||
| @@ -402,12 +411,12 @@ public partial class NaturalResourceGenerator : Node2D | |||||||
|         var nestPosition = new Vector2I((int)offset.X, (int)offset.Y); |         var nestPosition = new Vector2I((int)offset.X, (int)offset.Y); | ||||||
|          |          | ||||||
|         // Try to find a valid position for the nest |         // Try to find a valid position for the nest | ||||||
|         int attempts = 0; |         var attempts = 0; | ||||||
|         const int maxAttempts = 10; |         const int maxAttempts = 10; | ||||||
|          |          | ||||||
|         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; | ||||||
| @@ -428,25 +437,18 @@ public partial class NaturalResourceGenerator : Node2D | |||||||
|     { |     { | ||||||
|         try |         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); |             var building = Registry.GetBuilding(tileType); | ||||||
|             if (building == null) |             if (building == null) | ||||||
|             { |             { | ||||||
|                 GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}"); |                 GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}"); | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|             // GD.Print($"{LogPrefix} Found building in registry: {tileType}"); |  | ||||||
|  |             // Free area for ground layer if needed | ||||||
|  |             if (building.Layer == GridLayer.Ground) | ||||||
|  |                 Grid.FreeArea(cell, building.Size, 0f, GridLayer.Ground); | ||||||
|  |             else | ||||||
|  |                 GD.Print($"{LogPrefix} Attempting placing building tile {tileType} at {cell}."); | ||||||
|  |  | ||||||
|             var scene = building.Scene; |             var scene = building.Scene; | ||||||
|             if (scene == null) |             if (scene == null) | ||||||
| @@ -454,29 +456,24 @@ public partial class NaturalResourceGenerator : Node2D | |||||||
|                 GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}"); |                 GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}"); | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|             // GD.Print($"{LogPrefix} Scene loaded for {tileType}"); |  | ||||||
|  |  | ||||||
|             if (scene.Instantiate() is not BaseTile instance) |             if (scene.Instantiate() is not BaseTile instance) | ||||||
|             { |             { | ||||||
|                 GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}"); |                 GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}"); | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|             // GD.Print($"{LogPrefix} Successfully instantiated {tileType}"); |  | ||||||
|  |  | ||||||
|             // Use the same positioning logic as PlacementManager |             // Calculate position with proper offset based on building size | ||||||
|             var rotatedSize = building.GetRotatedSize(0f); |             var rotatedSize = building.GetRotatedSize(0f); | ||||||
|             var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); |             var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); | ||||||
|              |              | ||||||
|             instance.ZIndex = (int)building.Layer; |             instance.ZIndex = (int)building.Layer; | ||||||
|             instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; |             instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; | ||||||
|             instance.Grid = Grid; |  | ||||||
|             AddChild(instance); |             AddChild(instance); | ||||||
|              |              | ||||||
|             // For enemy nest, use Building layer |             // Occupy the appropriate area based on building size | ||||||
|             var layer = tileType == "enemy_nest" ? GridLayer.Building : building.Layer; |             Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); | ||||||
|             Grid.OccupyArea(cell, instance, building.Size, 0f, layer); |  | ||||||
|              |              | ||||||
|             // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} on layer {layer}"); |  | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|         catch (Exception e) |         catch (Exception e) | ||||||
|   | |||||||
| @@ -203,7 +203,6 @@ public partial class PlacementManager : Node2D | |||||||
|             var scene = building.Scene; |             var scene = building.Scene; | ||||||
|  |  | ||||||
|             _ghostBuilding = (BaseTile)scene.Instantiate(); |             _ghostBuilding = (BaseTile)scene.Instantiate(); | ||||||
|             _ghostBuilding.Grid = Grid; |  | ||||||
|             _ghostBuilding.SetGhostMode(true); |             _ghostBuilding.SetGhostMode(true); | ||||||
|             _ghostBuilding.RotationDegrees = _currentRotation; |             _ghostBuilding.RotationDegrees = _currentRotation; | ||||||
|             _ghostBuilding.ZAsRelative = false; |             _ghostBuilding.ZAsRelative = false; | ||||||
| @@ -251,7 +250,6 @@ public partial class PlacementManager : Node2D | |||||||
|             // Create the building instance |             // Create the building instance | ||||||
|             var scene = building.Scene; |             var scene = building.Scene; | ||||||
|             var buildingInstance = (BaseTile)scene.Instantiate(); |             var buildingInstance = (BaseTile)scene.Instantiate(); | ||||||
|             buildingInstance.Grid = Grid; |  | ||||||
|             buildingInstance.RotationDegrees = _currentRotation; |             buildingInstance.RotationDegrees = _currentRotation; | ||||||
|             buildingInstance.ZIndex = (int)building.Layer; |             buildingInstance.ZIndex = (int)building.Layer; | ||||||
|             buildingInstance.Position = _ghostBuilding.Position; |             buildingInstance.Position = _ghostBuilding.Position; | ||||||
| @@ -430,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); | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,6 +13,10 @@ public partial class ResourceManager : Node | |||||||
|     // Event for when resource amounts change |     // Event for when resource amounts change | ||||||
|     [Signal] |     [Signal] | ||||||
|     public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount); |     public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount); | ||||||
|  |      | ||||||
|  |     // Event for when resources are picked up (added) | ||||||
|  |     [Signal] | ||||||
|  |     public delegate void OnResourcePickedUpEventHandler(string resourceId, int amount); | ||||||
|  |  | ||||||
|     public override void _Ready() |     public override void _Ready() | ||||||
|     { |     { | ||||||
| @@ -43,6 +47,7 @@ public partial class ResourceManager : Node | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); |         EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); | ||||||
|  |         EmitSignal(nameof(OnResourcePickedUp), resourceId, amount); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Remove resources of a specific type |     // Remove resources of a specific type | ||||||
|   | |||||||
| @@ -9,30 +9,46 @@ namespace AceFieldNewHorizon.Scripts.Tiles; | |||||||
| public partial class BaseTile : Node2D | public partial class BaseTile : Node2D | ||||||
| { | { | ||||||
|     [Export] public string TileId { get; set; } |     [Export] public string TileId { get; set; } | ||||||
|     [Export] public GridManager Grid { get; set; } |  | ||||||
|  |     protected GridManager Grid { get; set; } | ||||||
|  |     protected BuildingRegistry Registry { get; set; } | ||||||
|  |  | ||||||
|  |     public int MaxDurability { get; private set; } | ||||||
|  |     public int CurrentDurability { get; private set; } | ||||||
|  |     public bool IsDestroyed { get; private set; } | ||||||
|  |  | ||||||
|     private CollisionShape2D _collisionShape; |     private CollisionShape2D _collisionShape; | ||||||
|     private Sprite2D _sprite; |     private Sprite2D _sprite; | ||||||
|     private ColorRect _progressOverlay; |     private ColorRect _progressOverlay; | ||||||
|     private Action _onConstructionComplete; |     private Action _onConstructionComplete; | ||||||
|      |     private Tween _damageTween; | ||||||
|  |  | ||||||
|     public bool IsConstructing; |     public bool IsConstructing; | ||||||
|     public bool IsConstructed; |     public bool IsConstructed; | ||||||
|  |  | ||||||
|     public override void _Ready() |     public override void _Ready() | ||||||
|     { |     { | ||||||
|  |         Grid = DependencyInjection.Container.GetInstance<GridManager>(); | ||||||
|  |         Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>(); | ||||||
|  |  | ||||||
|         _collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D"); |         _collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D"); | ||||||
|         _sprite = GetNodeOrNull<Sprite2D>("Sprite2D"); |         _sprite = GetNodeOrNull<Sprite2D>("Sprite2D"); | ||||||
|         _progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay"); |         _progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay"); | ||||||
|         if (_progressOverlay != null) |         if (_progressOverlay != null) | ||||||
|             _progressOverlay.Visible = false; |             _progressOverlay.Visible = false; | ||||||
|  |  | ||||||
|  |         // Get durability from BuildingRegistry | ||||||
|  |         var buildingData = Registry?.GetBuilding(TileId); | ||||||
|  |         MaxDurability = buildingData?.Durability ?? 100; // Default to 100 if not found | ||||||
|  |         CurrentDurability = MaxDurability; | ||||||
|  |         IsDestroyed = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public virtual void SetGhostMode(bool canPlace) |     public virtual void SetGhostMode(bool canPlace) | ||||||
|     { |     { | ||||||
|         // Don't modify collision for constructing buildings |         // Don't modify collision for constructing buildings | ||||||
|         if (IsConstructing) return; |         if (IsConstructing) return; | ||||||
|              |  | ||||||
|         if (_collisionShape != null) |         if (_collisionShape != null) | ||||||
|             _collisionShape.Disabled = true; |             _collisionShape.Disabled = true; | ||||||
|  |  | ||||||
| @@ -50,6 +66,81 @@ public partial class BaseTile : Node2D | |||||||
|             _sprite.Modulate = Colors.White; |             _sprite.Modulate = Colors.White; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public virtual bool TakeDamage(int damage) | ||||||
|  |     { | ||||||
|  |         if (IsDestroyed || IsConstructing) return false; | ||||||
|  |  | ||||||
|  |         GD.Print($"[Tile] {TileId} {GetInstanceId()} took {damage} damage"); | ||||||
|  |  | ||||||
|  |         CurrentDurability = Mathf.Max(0, CurrentDurability - damage); | ||||||
|  |  | ||||||
|  |         // Visual feedback for taking damage | ||||||
|  |         ShowDamageEffect(); | ||||||
|  |  | ||||||
|  |         if (CurrentDurability <= 0) | ||||||
|  |         { | ||||||
|  |             Destroy(); | ||||||
|  |             return true; // Tile was destroyed | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; // Tile is still alive | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void ShowDamageEffect() | ||||||
|  |     { | ||||||
|  |         if (_sprite == null) return; | ||||||
|  |  | ||||||
|  |         // Cancel any existing tween | ||||||
|  |         _damageTween?.Kill(); | ||||||
|  |         _damageTween = CreateTween(); | ||||||
|  |  | ||||||
|  |         // Flash red briefly | ||||||
|  |         _damageTween.TweenProperty(_sprite, "modulate", Colors.Red, 0.1f); | ||||||
|  |         _damageTween.TweenProperty(_sprite, "modulate", Colors.White, 0.1f); | ||||||
|  |  | ||||||
|  |         // Shake effect | ||||||
|  |         var originalPosition = _sprite.Position; | ||||||
|  |         _damageTween.TweenMethod( | ||||||
|  |             Callable.From<Vector2>(pos => _sprite.Position = pos), | ||||||
|  |             originalPosition + new Vector2(-2, -2), | ||||||
|  |             originalPosition + new Vector2(2, 2), | ||||||
|  |             0.05f | ||||||
|  |         ).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut); | ||||||
|  |  | ||||||
|  |         _damageTween.TweenMethod( | ||||||
|  |             Callable.From<Vector2>(pos => _sprite.Position = pos), | ||||||
|  |             originalPosition + new Vector2(2, 2), | ||||||
|  |             originalPosition, | ||||||
|  |             0.05f | ||||||
|  |         ).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public virtual void Destroy() | ||||||
|  |     { | ||||||
|  |         if (IsDestroyed) return; | ||||||
|  |  | ||||||
|  |         IsDestroyed = true; | ||||||
|  |  | ||||||
|  |         // Disable collision using SetDeferred to avoid physics update conflicts | ||||||
|  |         _collisionShape?.SetDeferred("disabled", true); | ||||||
|  |  | ||||||
|  |         // Fade out and remove | ||||||
|  |         var tween = CreateTween(); | ||||||
|  |         tween.TweenProperty(this, "modulate:a", 0f, 0.3f); | ||||||
|  |         tween.TweenCallback(Callable.From(() => CallDeferred("queue_free"))); | ||||||
|  |  | ||||||
|  |         // Emit signal or call method when tile is destroyed | ||||||
|  |         CallDeferred(nameof(OnTileDestroyed)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnTileDestroyed() | ||||||
|  |     { | ||||||
|  |         // 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 | ||||||
|     public void StartConstruction(float buildTime, Action onComplete = null) |     public void StartConstruction(float buildTime, Action onComplete = null) | ||||||
|     { |     { | ||||||
| @@ -57,7 +148,7 @@ public partial class BaseTile : Node2D | |||||||
|         if (_collisionShape != null) |         if (_collisionShape != null) | ||||||
|             _collisionShape.Disabled = true; |             _collisionShape.Disabled = true; | ||||||
|  |  | ||||||
|         if (_progressOverlay == null || _sprite?.Texture == null)  |         if (_progressOverlay == null || _sprite?.Texture == null) | ||||||
|         { |         { | ||||||
|             IsConstructing = false; |             IsConstructing = false; | ||||||
|             onComplete?.Invoke(); |             onComplete?.Invoke(); | ||||||
| @@ -95,15 +186,15 @@ public partial class BaseTile : Node2D | |||||||
|  |  | ||||||
|             // Fade out the overlay |             // Fade out the overlay | ||||||
|             await FadeOutOverlay(0.5f); |             await FadeOutOverlay(0.5f); | ||||||
|              |  | ||||||
|             // Construction complete - restore full opacity and enable collision |             // Construction complete - restore full opacity and enable collision | ||||||
|             if (_sprite != null) |             if (_sprite != null) | ||||||
|                 _sprite.Modulate = Colors.White; |                 _sprite.Modulate = Colors.White; | ||||||
|                  |  | ||||||
|             IsConstructing = false; |             IsConstructing = false; | ||||||
|             if (_collisionShape != null) |             if (_collisionShape != null) | ||||||
|                 _collisionShape.Disabled = false; |                 _collisionShape.Disabled = false; | ||||||
|                  |  | ||||||
|             _onConstructionComplete?.Invoke(); |             _onConstructionComplete?.Invoke(); | ||||||
|             IsConstructed = true; |             IsConstructed = true; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,74 +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(); |  | ||||||
|          |  | ||||||
|         // 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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										129
									
								
								Scripts/Tiles/EnemyPortalTile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								Scripts/Tiles/EnemyPortalTile.cs
									
									
									
									
									
										Normal 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; | ||||||
|  |  | ||||||
|  | 	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, 6), // 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; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -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(); | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								Scripts/Tiles/ReactorTile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Scripts/Tiles/ReactorTile.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | namespace AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
|  | public partial class ReactorTile : BaseTile | ||||||
|  | { | ||||||
|  |     public static readonly string ReactorGroupName = "Reactor"; | ||||||
|  |  | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         base._Ready(); | ||||||
|  |          | ||||||
|  |         AddToGroup(ReactorGroupName); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Tiles/ReactorTile.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Scripts/Tiles/ReactorTile.cs.uid
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | uid://c4k3ottt7j3b1 | ||||||
| @@ -121,7 +121,7 @@ public partial class TurretTile : BaseTile | |||||||
|             // Reset attack cooldown |             // Reset attack cooldown | ||||||
|             _attackTimer = AttackCooldown; |             _attackTimer = AttackCooldown; | ||||||
|              |              | ||||||
|             GD.Print("Turret attacking enemy!"); |             GD.Print("[Turret] Turret firing!"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user