using System.Collections.Generic; using System.Linq; using Cthangover.Core.Characters; using Cthangover.Core.Scenes; using Cthangover.Core.UI; using Cthangover.Core.UI.Theming; using Godot; namespace Cthangover.FFBattle.UI { /// /// Panel that arranges player character widgets horizontally at the bottom of /// the battle screen. Creates one per player /// character, wires click and health-change handlers, and positions them using /// fixed horizontal spacing via . When a player's HP drops /// to zero, the widget plays its death animation and is removed; the remaining /// widgets are repositioned. Exposes for win/loss checks. /// public partial class FFPlayerPanel : ModWidget { private const float CELL_SPACING = 20f; /// All player character widgets, including dead ones until their death animation completes. public List Widgets { get; } = new(); /// Raised when a player widget is clicked — routed to for character selection. public event System.Action OnWidgetClicked; protected override void Construct() { CthangoverTheme.Apply(this); } /// /// Creates instances for each player, scales /// them uniformly, wires click and health-change handlers, and calls /// to position them horizontally. /// public void Init(Character[] players, float scale) { foreach (var child in Widgets) child.QueueFree(); Widgets.Clear(); for (int i = 0; i < players.Length; i++) { var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn"); widget.EnsureConstructed(); widget.Scale = new Vector2(scale, scale); widget.Init(players[i]); widget.IsPlayer = true; widget.MouseFilter = MouseFilterEnum.Stop; widget.GuiInput += (evt) => { if (evt is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) OnWidgetClicked?.Invoke(widget); }; AddChild(widget); Widgets.Add(widget); players[i].Attributes.Health.OnChange += (value, baseValue) => { widget.UpdateInfo(); if (value <= 0 && !widget.IsDead) { widget.PlayDeathAnimation(() => { RemoveWidget(widget); Redraw(); }); } }; } Redraw(); } /// Repositions all widgets in a horizontal row with fixed CELL_SPACING between them. public void Redraw() { for (int i = 0; i < Widgets.Count; i++) { var widget = Widgets[i]; if (widget == null) continue; var effWidth = widget.Size.X * widget.Scale.X; var effHeight = widget.Size.Y * widget.Scale.Y; widget.Position = new Vector2(i * (effWidth + CELL_SPACING) + CELL_SPACING, Size.Y - effHeight - CELL_SPACING); } } /// Returns true if any widget is alive and has positive health. public bool HasAlive() { return Widgets.Any(w => !w.IsDead && w.Card?.Attributes?.Health?.Value > 0); } /// Frees all widget instances and clears the widget list. Called on battle cleanup. public void ClearAll() { foreach (var widget in Widgets.ToList()) { if (GodotObject.IsInstanceValid(widget)) widget.QueueFree(); } Widgets.Clear(); Redraw(); } /// Removes a widget from the panel, frees it, and repositions remaining widgets. public void RemoveWidget(FFCharacterWidget widget) { Widgets.Remove(widget); widget.QueueFree(); Redraw(); } } }