using System; using System.Threading.Tasks; using AceFieldNewHorizon.Scripts.System; using Godot; using Vector2 = Godot.Vector2; namespace AceFieldNewHorizon.Scripts.Tiles; public partial class BaseTile : Node2D { [Export] public string TileId { 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(); Registry = DependencyInjection.Container.GetInstance(); _collisionShape = GetNodeOrNull("CollisionShape2D"); _sprite = GetNodeOrNull("Sprite2D"); _progressOverlay = GetNodeOrNull("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; if (_sprite != null) _sprite.Modulate = canPlace ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f); } public virtual void FinalizePlacement() { if (_collisionShape != null) _collisionShape.Disabled = false; if (_sprite != null) _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(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(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) { IsConstructing = true; if (_collisionShape != null) _collisionShape.Disabled = true; if (_progressOverlay == null || _sprite?.Texture == null) { IsConstructing = false; onComplete?.Invoke(); return; } _onConstructionComplete = onComplete; var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize); // Set initial transparency for construction if (_sprite != null) _sprite.Modulate = new Color(1, 1, 1, 0.8f); _progressOverlay.Visible = true; _progressOverlay.Modulate = Colors.White; _progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue async void RunProgress() { var elapsed = 0f; while (elapsed < buildTime) { await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); elapsed += (float)GetProcessDeltaTime(); var ratio = Mathf.Clamp(elapsed / buildTime, 0, 1); // Fill from bottom to top, aligned with sprite center pivot _progressOverlay.Size = new Vector2(texSize.X, texSize.Y * ratio); _progressOverlay.Position = new Vector2( -texSize.X / 2, // align left edge texSize.Y / 2 - _progressOverlay.Size.Y // move upward ); } // 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; } RunProgress(); } private async Task FadeOutOverlay(float duration) { var elapsed = 0f; var startAlpha = _progressOverlay.Modulate.A; while (elapsed < duration) { await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); elapsed += (float)GetProcessDeltaTime(); var alpha = Mathf.Lerp(startAlpha, 0f, elapsed / duration); _progressOverlay.Modulate = new Color(1, 1, 1, alpha); } _progressOverlay.Visible = false; _progressOverlay.Modulate = Colors.White; // Reset alpha for next use } }