Compare commits
4 Commits
c72353716f
...
master
Author | SHA1 | Date | |
---|---|---|---|
2c5e0459ad | |||
f2c243ecf6 | |||
b424aafeab | |||
09511b37c9 |
@@ -33,14 +33,23 @@
|
||||
"layer": 1,
|
||||
"size": [1, 1]
|
||||
},
|
||||
"enemy_nest": {
|
||||
"scene": "res://Scenes/Tiles/EnemyNest.tscn",
|
||||
"reactor": {
|
||||
"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": {},
|
||||
"durability": 200,
|
||||
"buildTime": 0.0,
|
||||
"allowedRotations": [0],
|
||||
"layer": 1,
|
||||
"size": [1, 1]
|
||||
"size": [1, 2]
|
||||
},
|
||||
"ground": {
|
||||
"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"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dlhpiyxtmp707"
|
||||
path="res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.ctex"
|
||||
uid="uid://x4u6oatvsm8y"
|
||||
path="res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Scenes/Entities/Enemy.jpg"
|
||||
dest_files=["res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.ctex"]
|
||||
source_file="res://Scenes/Entities/Enemy.png"
|
||||
dest_files=["res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"]
|
||||
|
||||
[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"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_jajit"]
|
||||
size = Vector2(60, 74)
|
||||
[ext_resource type="Texture2D" uid="uid://x4u6oatvsm8y" path="res://Scenes/Entities/Enemy.png" id="2_jajit"]
|
||||
|
||||
[node name="Enemy" type="CharacterBody2D"]
|
||||
collision_layer = 2
|
||||
collision_mask = 3
|
||||
script = ExtResource("1_jajit")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("RectangleShape2D_jajit")
|
||||
[node name="CollisionShape2D" type="CollisionPolygon2D" parent="."]
|
||||
polygon = PackedVector2Array(-2, -21, 3, -21, 24, 6, 24, 10, 20, 14, -21, 14, -24, 11, -24, 7)
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
scale = Vector2(0.1, 0.1)
|
||||
texture = ExtResource("1_8q37v")
|
||||
scale = Vector2(0.05, 0.05)
|
||||
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="."]
|
||||
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)
|
||||
Inventory = NodePath("")
|
||||
|
||||
[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"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://vwfs68ftvjr4"
|
||||
path="res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex"
|
||||
uid="uid://fg03qxqphp7n"
|
||||
path="res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://Scenes/Tiles/EnemyNest.jpg"
|
||||
dest_files=["res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex"]
|
||||
source_file="res://Scenes/Tiles/ReactorTile.png"
|
||||
dest_files=["res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"]
|
||||
|
||||
[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 AceFieldNewHorizon.Scripts.System;
|
||||
|
||||
namespace AceFieldNewHorizon.Scripts.Entities;
|
||||
|
||||
public partial class Enemy : CharacterBody2D
|
||||
public partial class Enemy : BaseEnemy
|
||||
{
|
||||
public const string EnemyGroupName = "Enemy";
|
||||
|
||||
[Export] public float MoveSpeed = 150.0f;
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// All the base functionality is now in BaseEnemy
|
||||
// This class is kept for backward compatibility and can be used to add
|
||||
// specific behaviors for the basic enemy type if needed
|
||||
}
|
||||
|
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 Godot;
|
||||
|
||||
|
@@ -43,11 +43,29 @@ public partial class GridManager : Node
|
||||
|
||||
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);
|
||||
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))
|
||||
_layers[layer].Remove(cell);
|
||||
var (building, buildingSize, buildingRotation) = _layers[layer][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 VBoxContainer _resourceDisplay;
|
||||
private readonly Dictionary<string, Label> _resourceLabels = new();
|
||||
private VBoxContainer _pickupLogContainer;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_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>();
|
||||
if (_resourceManager == null)
|
||||
{
|
||||
@@ -21,6 +30,7 @@ public partial class Hud : CanvasLayer
|
||||
}
|
||||
|
||||
_resourceManager.OnResourceChanged += UpdateResourceDisplay;
|
||||
_resourceManager.OnResourcePickedUp += OnResourcePickedUp;
|
||||
|
||||
// Initialize display with current resources
|
||||
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()
|
||||
{
|
||||
if (_resourceManager != null)
|
||||
{
|
||||
_resourceManager.OnResourceChanged -= UpdateResourceDisplay;
|
||||
_resourceManager.OnResourcePickedUp -= OnResourcePickedUp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -55,18 +55,21 @@ public partial class NaturalResourceGenerator : Node2D
|
||||
return;
|
||||
}
|
||||
|
||||
// Test if enemy_nest is in the registry
|
||||
var testBuilding = Registry.GetBuilding("enemy_nest");
|
||||
// Test if enemy_portal is in the registry
|
||||
var testBuilding = Registry.GetBuilding("enemy_portal");
|
||||
if (testBuilding == null)
|
||||
{
|
||||
GD.PrintErr($"{LogPrefix} 'enemy_nest' is not found in BuildingRegistry!");
|
||||
GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!");
|
||||
}
|
||||
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} Spawning the core reactor...");
|
||||
SpawnCoreReactor();
|
||||
|
||||
if (SpawnEnemyNest)
|
||||
{
|
||||
@@ -284,9 +287,9 @@ public partial class NaturalResourceGenerator : Node2D
|
||||
|
||||
// Remove all tiles in this chunk
|
||||
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);
|
||||
// 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");
|
||||
}
|
||||
|
||||
private void SpawnCoreReactor()
|
||||
{
|
||||
// Place the reactor tile at the center of the map
|
||||
PlaceTile("reactor", new Vector2I(0, 0));
|
||||
}
|
||||
|
||||
private void SpawnRandomEnemyNest()
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Try to find a valid position for the nest
|
||||
int attempts = 0;
|
||||
var attempts = 0;
|
||||
const int maxAttempts = 10;
|
||||
|
||||
while (attempts < maxAttempts)
|
||||
{
|
||||
if (PlaceTile("enemy_nest", nestPosition))
|
||||
if (PlaceTile("enemy_portal", nestPosition))
|
||||
{
|
||||
GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}");
|
||||
return;
|
||||
@@ -428,25 +437,18 @@ public partial class NaturalResourceGenerator : Node2D
|
||||
{
|
||||
try
|
||||
{
|
||||
// For enemy nest, we want to place it on top of existing ground
|
||||
if (tileType != "enemy_nest")
|
||||
{
|
||||
// Original behavior for other tile types
|
||||
Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For enemy nest, check if there's ground below (skip for now)
|
||||
GD.Print($"{LogPrefix} Attempting placing nest at {cell}.");
|
||||
}
|
||||
|
||||
var building = Registry.GetBuilding(tileType);
|
||||
if (building == null)
|
||||
{
|
||||
GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}");
|
||||
return false;
|
||||
}
|
||||
// GD.Print($"{LogPrefix} Found building in registry: {tileType}");
|
||||
|
||||
// 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;
|
||||
if (scene == null)
|
||||
@@ -454,29 +456,24 @@ public partial class NaturalResourceGenerator : Node2D
|
||||
GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}");
|
||||
return false;
|
||||
}
|
||||
// GD.Print($"{LogPrefix} Scene loaded for {tileType}");
|
||||
|
||||
if (scene.Instantiate() is not BaseTile instance)
|
||||
{
|
||||
GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}");
|
||||
return false;
|
||||
}
|
||||
// GD.Print($"{LogPrefix} Successfully instantiated {tileType}");
|
||||
|
||||
// Use the same positioning logic as PlacementManager
|
||||
// Calculate position with proper offset based on building size
|
||||
var rotatedSize = building.GetRotatedSize(0f);
|
||||
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f);
|
||||
|
||||
instance.ZIndex = (int)building.Layer;
|
||||
instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset;
|
||||
instance.Grid = Grid;
|
||||
AddChild(instance);
|
||||
|
||||
// For enemy nest, use Building layer
|
||||
var layer = tileType == "enemy_nest" ? GridLayer.Building : building.Layer;
|
||||
Grid.OccupyArea(cell, instance, building.Size, 0f, layer);
|
||||
// Occupy the appropriate area based on building size
|
||||
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer);
|
||||
|
||||
// GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} on layer {layer}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@@ -203,7 +203,6 @@ public partial class PlacementManager : Node2D
|
||||
var scene = building.Scene;
|
||||
|
||||
_ghostBuilding = (BaseTile)scene.Instantiate();
|
||||
_ghostBuilding.Grid = Grid;
|
||||
_ghostBuilding.SetGhostMode(true);
|
||||
_ghostBuilding.RotationDegrees = _currentRotation;
|
||||
_ghostBuilding.ZAsRelative = false;
|
||||
@@ -251,7 +250,6 @@ public partial class PlacementManager : Node2D
|
||||
// Create the building instance
|
||||
var scene = building.Scene;
|
||||
var buildingInstance = (BaseTile)scene.Instantiate();
|
||||
buildingInstance.Grid = Grid;
|
||||
buildingInstance.RotationDegrees = _currentRotation;
|
||||
buildingInstance.ZIndex = (int)building.Layer;
|
||||
buildingInstance.Position = _ghostBuilding.Position;
|
||||
@@ -430,32 +428,28 @@ public static class GridManagerExtensions
|
||||
public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid,
|
||||
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 (int x = 0; x < 100; x++) // Arbitrary max size
|
||||
for (var y = 0; y < 100; y++)
|
||||
{
|
||||
for (int y = 0; y < 100; y++)
|
||||
{
|
||||
var checkCell = new Vector2I(cell.X - x, cell.Y - y);
|
||||
if (grid.GetTileAtCell(checkCell, layer) == building)
|
||||
{
|
||||
// Found the top-left corner, now find the size
|
||||
var size = Vector2I.One;
|
||||
// Search right
|
||||
while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
|
||||
building)
|
||||
size.X++;
|
||||
// Search down
|
||||
while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
|
||||
building)
|
||||
size.Y++;
|
||||
var checkCell = new Vector2I(cell.X - x, cell.Y - y);
|
||||
if (grid.GetTileAtCell(checkCell, layer) != building) continue;
|
||||
// Found the top-left corner, now find the size
|
||||
var size = Vector2I.One;
|
||||
// Search right
|
||||
while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
|
||||
building)
|
||||
size.X++;
|
||||
// Search down
|
||||
while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
|
||||
building)
|
||||
size.Y++;
|
||||
|
||||
// Get rotation from the first cell
|
||||
var rotation = 0f; // You'll need to store rotation in GridManager to make this work
|
||||
return (checkCell, size, rotation);
|
||||
}
|
||||
}
|
||||
// Get rotation from the first cell
|
||||
var rotation = 0f; // You'll need to store rotation in Grid to make this work
|
||||
return (checkCell, size, rotation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,10 @@ public partial class ResourceManager : Node
|
||||
// Event for when resource amounts change
|
||||
[Signal]
|
||||
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()
|
||||
{
|
||||
@@ -43,6 +47,7 @@ public partial class ResourceManager : Node
|
||||
}
|
||||
|
||||
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
|
||||
EmitSignal(nameof(OnResourcePickedUp), resourceId, amount);
|
||||
}
|
||||
|
||||
// Remove resources of a specific type
|
||||
|
@@ -9,30 +9,46 @@ namespace AceFieldNewHorizon.Scripts.Tiles;
|
||||
public partial class BaseTile : Node2D
|
||||
{
|
||||
[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 Sprite2D _sprite;
|
||||
private ColorRect _progressOverlay;
|
||||
private Action _onConstructionComplete;
|
||||
|
||||
private Tween _damageTween;
|
||||
|
||||
public bool IsConstructing;
|
||||
public bool IsConstructed;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Grid = DependencyInjection.Container.GetInstance<GridManager>();
|
||||
Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
|
||||
|
||||
_collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D");
|
||||
_sprite = GetNodeOrNull<Sprite2D>("Sprite2D");
|
||||
_progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay");
|
||||
if (_progressOverlay != null)
|
||||
_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)
|
||||
{
|
||||
// Don't modify collision for constructing buildings
|
||||
if (IsConstructing) return;
|
||||
|
||||
|
||||
if (_collisionShape != null)
|
||||
_collisionShape.Disabled = true;
|
||||
|
||||
@@ -50,6 +66,81 @@ public partial class BaseTile : Node2D
|
||||
_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
|
||||
public void StartConstruction(float buildTime, Action onComplete = null)
|
||||
{
|
||||
@@ -57,7 +148,7 @@ public partial class BaseTile : Node2D
|
||||
if (_collisionShape != null)
|
||||
_collisionShape.Disabled = true;
|
||||
|
||||
if (_progressOverlay == null || _sprite?.Texture == null)
|
||||
if (_progressOverlay == null || _sprite?.Texture == null)
|
||||
{
|
||||
IsConstructing = false;
|
||||
onComplete?.Invoke();
|
||||
@@ -95,15 +186,15 @@ public partial class BaseTile : Node2D
|
||||
|
||||
// Fade out the overlay
|
||||
await FadeOutOverlay(0.5f);
|
||||
|
||||
|
||||
// Construction complete - restore full opacity and enable collision
|
||||
if (_sprite != null)
|
||||
_sprite.Modulate = Colors.White;
|
||||
|
||||
|
||||
IsConstructing = false;
|
||||
if (_collisionShape != null)
|
||||
_collisionShape.Disabled = false;
|
||||
|
||||
|
||||
_onConstructionComplete?.Invoke();
|
||||
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");
|
||||
sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker
|
||||
sprite.ZIndex = -10;
|
||||
|
||||
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
|
||||
_attackTimer = AttackCooldown;
|
||||
|
||||
GD.Print("Turret attacking enemy!");
|
||||
GD.Print("[Turret] Turret firing!");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user