diff --git a/Scenes/Entities/Bullet.png b/Scenes/Entities/Bullet.png new file mode 100644 index 0000000..0ae014f Binary files /dev/null and b/Scenes/Entities/Bullet.png differ diff --git a/Scenes/Entities/Bullet.png.import b/Scenes/Entities/Bullet.png.import new file mode 100644 index 0000000..c1bb22d --- /dev/null +++ b/Scenes/Entities/Bullet.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5mb8tu15rc2p" +path="res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Scenes/Entities/Bullet.png" +dest_files=["res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.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 diff --git a/Scenes/Entities/Bullet.tscn b/Scenes/Entities/Bullet.tscn new file mode 100644 index 0000000..9af3377 --- /dev/null +++ b/Scenes/Entities/Bullet.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=4 format=3 uid="uid://erqawdsydh6a"] + +[ext_resource type="Texture2D" uid="uid://b5mb8tu15rc2p" path="res://Scenes/Entities/Bullet.png" id="1_fi8au"] +[ext_resource type="Script" uid="uid://vgx2a8gm7l8b" path="res://Scripts/Entities/Bullet.cs" id="1_k5b1m"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_fi8au"] +size = Vector2(44, 12) + +[node name="Bullet" type="Area2D"] +collision_mask = 2 +script = ExtResource("1_k5b1m") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.03, 0.03) +texture = ExtResource("1_fi8au") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_fi8au") diff --git a/Scenes/Entities/Enemy.tscn b/Scenes/Entities/Enemy.tscn index eb6ae57..9e8e0df 100644 --- a/Scenes/Entities/Enemy.tscn +++ b/Scenes/Entities/Enemy.tscn @@ -7,6 +7,8 @@ size = Vector2(60, 74) [node name="Enemy" type="CharacterBody2D"] +collision_layer = 2 +collision_mask = 3 script = ExtResource("1_jajit") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] diff --git a/Scenes/Entities/Player.tscn b/Scenes/Entities/Player.tscn index 342e57f..79b7b09 100644 --- a/Scenes/Entities/Player.tscn +++ b/Scenes/Entities/Player.tscn @@ -4,6 +4,7 @@ [ext_resource type="Texture2D" uid="uid://jye6c2ehuxtg" path="res://Scenes/Entities/Player.png" id="1_ucweq"] [node name="Player" type="CharacterBody2D"] +collision_mask = 3 script = ExtResource("1_08t41") MinZoom = 0.1 MaxZoom = 5.0 diff --git a/Scenes/Tiles/GroundTile.tscn b/Scenes/Tiles/GroundTile.tscn index 1636e8c..95f26af 100644 --- a/Scenes/Tiles/GroundTile.tscn +++ b/Scenes/Tiles/GroundTile.tscn @@ -8,6 +8,7 @@ size = Vector2(54, 54) [node name="GroundTile" type="StaticBody2D"] collision_layer = 0 +collision_mask = 0 script = ExtResource("1_mqsaf") TileId = "ground" diff --git a/Scenes/Tiles/OreIronTile.tscn b/Scenes/Tiles/OreIronTile.tscn index 6fb6877..a77f10d 100644 --- a/Scenes/Tiles/OreIronTile.tscn +++ b/Scenes/Tiles/OreIronTile.tscn @@ -8,6 +8,7 @@ size = Vector2(54, 54) [node name="OreIronTile" type="StaticBody2D"] collision_layer = 0 +collision_mask = 0 script = ExtResource("1_exnim") TileId = "ore_iron" diff --git a/Scenes/Tiles/StoneTile.tscn b/Scenes/Tiles/StoneTile.tscn index 8cc06ec..6ad125e 100644 --- a/Scenes/Tiles/StoneTile.tscn +++ b/Scenes/Tiles/StoneTile.tscn @@ -8,6 +8,7 @@ size = Vector2(54, 54) [node name="StoneTile" type="StaticBody2D"] collision_layer = 0 +collision_mask = 0 script = ExtResource("1_rndy8") TileId = "stone" diff --git a/Scenes/Tiles/TurretTile.tscn b/Scenes/Tiles/TurretTile.tscn index 6209fc7..772997f 100644 --- a/Scenes/Tiles/TurretTile.tscn +++ b/Scenes/Tiles/TurretTile.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=5 format=3 uid="uid://dbup2pvjl8het"] +[gd_scene load_steps=6 format=3 uid="uid://dbup2pvjl8het"] [ext_resource type="Script" uid="uid://n5g6i0uovxfk" path="res://Scripts/Tiles/TurretTile.cs" id="1_j3157"] [ext_resource type="Texture2D" uid="uid://ckssi7soymu7g" path="res://Scenes/Tiles/TurretTile.png" id="2_7ljeh"] +[ext_resource type="PackedScene" uid="uid://erqawdsydh6a" path="res://Scenes/Entities/Bullet.tscn" id="2_gfad6"] [ext_resource type="Texture2D" uid="uid://dmbbkwgff7dej" path="res://Scenes/Tiles/TurretTileBarrel.png" id="3_gfad6"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_pndcb"] @@ -9,6 +10,8 @@ size = Vector2(54, 54) [node name="Turret" type="StaticBody2D"] script = ExtResource("1_j3157") +BulletScene = ExtResource("2_gfad6") +BarrelTipPath = NodePath("Barrel/Marker2D") TileId = "turret" [node name="Sprite2D" type="Sprite2D" parent="."] @@ -21,6 +24,10 @@ scale = Vector2(0.08, 0.08) texture = ExtResource("3_gfad6") offset = Vector2(0, -54) +[node name="Marker2D" type="Marker2D" parent="Barrel"] +position = Vector2(0, 37.5) +scale = Vector2(12.5, 12.5) + [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("RectangleShape2D_pndcb") diff --git a/Scripts/Entities/Bullet.cs b/Scripts/Entities/Bullet.cs new file mode 100644 index 0000000..f3aafdd --- /dev/null +++ b/Scripts/Entities/Bullet.cs @@ -0,0 +1,97 @@ +using Godot; + +namespace AceFieldNewHorizon.Scripts.Entities; + +public partial class Bullet : Area2D +{ + [Export] public float Speed = 400.0f; + [Export] public int Damage = 10; + [Export] public float MaxDistance = 1000.0f; + + private Vector2 _direction = Vector2.Right; + private Vector2 _startPosition; + private float _distanceTraveled = 0f; + private bool _hasHit = false; + private uint _ignoreCollisionLayer = 0; // Layer to ignore (will be set by turret) + + public void Initialize(Vector2 direction, Vector2 position, float rotation, uint ignoreLayer = 0) + { + _direction = direction.Normalized(); + Position = position; + Rotation = rotation; + _startPosition = position; + _ignoreCollisionLayer = ignoreLayer; + + // Connect the area entered signal + BodyEntered += OnBodyEntered; + AreaEntered += OnAreaEntered; + } + + public override void _PhysicsProcess(double delta) + { + if (_hasHit) return; + + // Move the bullet + var movement = _direction * Speed * (float)delta; + Position += movement; + _distanceTraveled += movement.Length(); + + // Check if bullet has traveled max distance + if (_distanceTraveled >= MaxDistance) + { + QueueFree(); + return; + } + } + + private void OnBodyEntered(Node2D body) + { + HandleCollision(body); + } + + private void OnAreaEntered(Area2D area) + { + HandleCollision(area); + } + + private void HandleCollision(Node2D node) + { + if (_hasHit) return; + + // Skip collision if it's on the ignore layer + if (node is PhysicsBody2D physicsBody && + (physicsBody.CollisionLayer & _ignoreCollisionLayer) != 0) + { + return; + } + + _hasHit = true; + + // If we hit an enemy, deal damage + if (node is Enemy enemy) + { + // Get the global position where the bullet hit + var hitPosition = GlobalPosition; + enemy.TakeDamage(Damage, hitPosition); + } + + // Optional: Add hit effect here + // CreateHitEffect(); + + // Remove the bullet + QueueFree(); + } + + private void CreateHitEffect() + { + // You can add a hit effect here if desired + // For example, a small explosion or impact sprite + } + + public override void _ExitTree() + { + // Clean up signal connections + BodyEntered -= OnBodyEntered; + AreaEntered -= OnAreaEntered; + } +} diff --git a/Scripts/Entities/Bullet.cs.uid b/Scripts/Entities/Bullet.cs.uid new file mode 100644 index 0000000..f394714 --- /dev/null +++ b/Scripts/Entities/Bullet.cs.uid @@ -0,0 +1 @@ +uid://vgx2a8gm7l8b diff --git a/Scripts/Entities/Enemy.cs b/Scripts/Entities/Enemy.cs index 2cfa232..29a0ea3 100644 --- a/Scripts/Entities/Enemy.cs +++ b/Scripts/Entities/Enemy.cs @@ -11,14 +11,52 @@ public partial class Enemy : CharacterBody2D [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(); @@ -37,6 +75,8 @@ public partial class Enemy : CharacterBody2D public override void _Process(double delta) { + if (IsDead) return; + if (_player != null && _isPlayerInRange) { // Face the player @@ -60,8 +100,68 @@ public partial class Enemy : CharacterBody2D } } + 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) diff --git a/Scripts/Tiles/EnemyNestTile.cs b/Scripts/Tiles/EnemyNestTile.cs index 94e8498..e8a935b 100644 --- a/Scripts/Tiles/EnemyNestTile.cs +++ b/Scripts/Tiles/EnemyNestTile.cs @@ -1,3 +1,4 @@ +using AceFieldNewHorizon.Scripts.Entities; using Godot; namespace AceFieldNewHorizon.Scripts.Tiles; @@ -5,31 +6,69 @@ 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(); + _spawnTimer = new Timer + { + Autostart = true, + WaitTime = SpawnDelay + }; AddChild(_spawnTimer); _spawnTimer.Timeout += OnSpawnTimerTimeout; - _spawnTimer.Start(1.0f); // Start with 1 second interval } private void OnSpawnTimerTimeout() { - if (EnemyScene != null) + 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(); + if (enemy != null) { - var enemy = EnemyScene.Instantiate(); GetParent().AddChild(enemy); - // Position the enemy at the nest's position - if (enemy is Node2D enemy2D) - { - enemy2D.GlobalPosition = GlobalPosition; - } + // 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; + } } \ No newline at end of file diff --git a/Scripts/Tiles/TurretTile.cs b/Scripts/Tiles/TurretTile.cs index 2dd42bb..350db40 100644 --- a/Scripts/Tiles/TurretTile.cs +++ b/Scripts/Tiles/TurretTile.cs @@ -1,22 +1,43 @@ +using System.Linq; +using AceFieldNewHorizon.Scripts.Entities; using Godot; namespace AceFieldNewHorizon.Scripts.Tiles; public partial class TurretTile : BaseTile { - private Sprite2D _spriteBarrel; + [Export] public PackedScene BulletScene; + + [Export] public float RotationSpeed = 2.0f; // Radians per second + [Export] public float AttackRange = 300.0f; + [Export] public float AttackCooldown = 0.5f; + [Export] public int Damage = 10; + [Export] public float BulletSpeed = 400.0f; + [Export] public NodePath BarrelTipPath; + + private Node2D _spriteBarrel; + private Node2D _barrelTip; + private float _attackTimer = 0; + private bool _hasTarget = false; public override void _Ready() { base._Ready(); + + _spriteBarrel = GetNodeOrNull("Barrel"); + _barrelTip = GetNodeOrNull(BarrelTipPath); - _spriteBarrel = GetNodeOrNull("Barrel"); + if (_barrelTip == null && _spriteBarrel != null) + { + // If no barrel tip is specified, use the end of the barrel sprite + _barrelTip = _spriteBarrel.GetNodeOrNull("BarrelTip"); + } } public override void SetGhostMode(bool canPlace) { base.SetGhostMode(canPlace); - + if (_spriteBarrel != null) _spriteBarrel.Modulate = canPlace ? new Color(0, 1, 0, 0.5f) @@ -26,8 +47,81 @@ public partial class TurretTile : BaseTile public override void FinalizePlacement() { base.FinalizePlacement(); - + if (_spriteBarrel != null) _spriteBarrel.Modulate = Colors.White; } + + public override void _Process(double delta) + { + if (!IsConstructed) return; + + // Update attack cooldown + if (_attackTimer > 0) + { + _attackTimer -= (float)delta; + } + + // Find the nearest enemy + var enemies = GetTree().GetNodesInGroup(Enemy.EnemyGroupName) + .OfType() + .Where(e => e.GlobalPosition.DistanceTo(GlobalPosition) <= AttackRange) + .OrderBy(e => e.GlobalPosition.DistanceTo(GlobalPosition)) + .ToList(); + + if (enemies.Count > 0) + { + var nearestEnemy = enemies[0]; + _hasTarget = true; + + // Calculate target angle + var direction = (nearestEnemy.GlobalPosition - _spriteBarrel.GlobalPosition).Normalized(); + var targetAngle = Mathf.Atan2(direction.Y, direction.X); + + // Smoothly rotate towards target + _spriteBarrel.Rotation = Mathf.LerpAngle(_spriteBarrel.Rotation, targetAngle, (float)delta * RotationSpeed); + + // Check if we're facing the target and can attack + if (Mathf.Abs(Mathf.Wrap(targetAngle - _spriteBarrel.Rotation, -Mathf.Pi, Mathf.Pi)) < 0.1f) + TryAttack(nearestEnemy.GlobalPosition); + } + else + { + _hasTarget = false; + } + } + + private void TryAttack(Vector2 targetPosition) + { + if (_attackTimer <= 0 && BulletScene != null && _barrelTip != null) + { + // Create bullet instance + var bullet = BulletScene.Instantiate(); + + // Calculate direction and rotation + var direction = (targetPosition - _barrelTip.GlobalPosition).Normalized(); + var bulletRotation = direction.Angle(); + + // Set bullet position and rotation + GetTree().CurrentScene.AddChild(bullet); + bullet.GlobalPosition = _barrelTip.GlobalPosition; + bullet.Rotation = bulletRotation; // Use the calculated rotation + + // Initialize bullet with direction and damage + bullet.Initialize( + direction, + bullet.GlobalPosition, + bulletRotation, // Pass the calculated rotation + 1 // Pass the turret's collision layer to ignore + ); + bullet.Damage = Damage; + bullet.Speed = BulletSpeed; + bullet.MaxDistance = AttackRange * 1.5f; // Bullets can travel slightly further than attack range + + // Reset attack cooldown + _attackTimer = AttackCooldown; + + GD.Print("Turret attacking enemy!"); + } + } } \ No newline at end of file