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;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.View;
namespace Cthangover.CardBattle.UI
{
///
/// Container panel that manages a list of instances for one side
/// of the battle (player or enemy). Handles card creation, multi-row layout when enemy count exceeds
/// MAX_CARDS_ROW (10 per row), smooth animated repositioning, health-triggered death,
/// and the dissolve sequence.
/// Two instances are created by : a left-aligned player panel
/// and a right-aligned enemy panel.
///
public partial class BattleCardPanel : ModWidget
{
private const int MAX_CARDS_ROW = 10;
private const float BASE_CARD_HEIGHT = 280f;
[Export] private Control content;
///
/// Controls card alignment within the panel. "Left" for the player side,
/// "Right" for the enemy side. Affects .
///
[Export] public string AlignType { get; set; } = "Left";
[Export] private float cellOffset = 10f;
///
/// All character cards currently managed by this panel. Populated by
/// and modified by (removal) and (full clear).
///
public List CardList { get; } = new();
private Dictionary activeTweens = new();
///
/// When true, cards are laid out in multiple rows (up to 3) instead of a single row.
/// Set by when enemy count exceeds 6.
///
public bool UseRows { get; set; }
///
/// Fired when a card's death animation completes. subscribes
/// to record the defeated character in .
///
public event System.Action OnCardDead;
/// Builds the card grid container rows for one battle side.
protected override void Construct()
{
CthangoverTheme.Apply(this);
content ??= GetNodeOrNull("Content");
}
///
/// Clears the current card list and creates new instances
/// for each entry. Sets player/enemy team colors and subscribes
/// the health-change handler. Returns the created card list.
/// Called by with the full character roster for each side.
///
public List Init(IEnumerable cards, bool isPlayer, float cardScale)
{
CardList.Clear();
activeTweens.Clear();
int idx = 0;
foreach (var card in cards)
{
CardList.Add(Create(card, isPlayer, cardScale));
idx++;
}
Redraw(animate: false);
return CardList;
}
///
/// Instantiates a single , adds it to the content panel,
/// sets its scale, initializes it with card data, applies team colors, and hooks
/// the health-change event. Called by for each character.
///
public CharacterCardNode Create(Character card, bool isPlayer, float cardScale)
{
var behaviour = (CharacterCardNode)TscnScenes.LoadAndBuild("scenes/character_card.tscn");
behaviour.EnsureConstructed();
if (content != null)
content.AddChild(behaviour);
else
AddChild(behaviour);
behaviour.Scale = new Vector2(cardScale, cardScale);
behaviour.Init(card);
behaviour.IsPlayer = isPlayer;
behaviour.SetTeamColors();
if (card != null)
card.Attributes.Health.OnChange += CheckHealthAttribute;
return behaviour;
}
private void CheckHealthAttribute(float value, float basevalue)
{
if (value > 0)
return;
foreach (var cardItem in CardList.ToList())
{
if (cardItem.Card.Attributes.Health.Value <= 0)
Dead(cardItem);
}
}
///
/// Frees all card nodes from the scene tree, clears the card list, and triggers a redraw.
/// Called by when the battle scene is cleaned up.
///
public void ClearAll()
{
foreach (var card in CardList.ToList())
{
if (GodotObject.IsInstanceValid(card))
card.QueueFree();
}
CardList.Clear();
activeTweens.Clear();
Redraw(animate: false);
}
///
/// Recalculates and applies positions for all cards in the list. When
/// is true, cards tween to their new positions with a cubic ease-out; otherwise they
/// snap instantly. Called after cards are added, removed, or when a selection/unselection
/// changes the sort order.
///
public void Redraw(bool animate = true)
{
for (int i = 0; i < CardList.Count; i++)
{
var card = CardList[i];
if (card == null)
continue;
var (row, col) = GetRowCol(i);
SetCardPosition(card, i, row, col, animate);
}
}
private (int row, int col) GetRowCol(int index)
{
if (!UseRows || index < MAX_CARDS_ROW)
return (0, index);
if (index < MAX_CARDS_ROW * 2)
return (1, index - MAX_CARDS_ROW);
return (2, index - MAX_CARDS_ROW * 2);
}
private int CalcBaseZIndex(int row, int col)
{
return row * 20 + col;
}
private Vector2 CalcTargetPosition(CharacterCardNode card, int row, int col)
{
var panelSize = Size;
float effectiveWidth = card.Size.X * card.Scale.X;
float effectiveHeight = card.Size.Y * card.Scale.Y;
float y = row * (effectiveHeight + cellOffset);
if (AlignType == "Left")
return new Vector2((cellOffset + effectiveWidth) * col, y);
else
return new Vector2(panelSize.X - (cellOffset + effectiveWidth) * (col + 1), y);
}
///
/// Calculates how many rows are needed to display enemies
/// when is enabled. Returns 1, 2, or 3.
/// Called by to determine maximum card scale.
///
public int GetCurrentRowCount(int enemyCount)
{
if (!UseRows)
return 1;
if (enemyCount <= MAX_CARDS_ROW)
return 1;
if (enemyCount <= MAX_CARDS_ROW * 2)
return 2;
return 3;
}
///
/// Returns the maximum cards per row constant (MAX_CARDS_ROW = 10).
/// Used by for scale calculations.
///
public int GetMaxCardsPerRow()
{
return MAX_CARDS_ROW;
}
private void SetCardPosition(CharacterCardNode card, int index, int row, int col, bool animate)
{
if (card == null)
return;
var targetPos = CalcTargetPosition(card, row, col);
if (activeTweens.TryGetValue(card, out var prevTween) && prevTween != null && prevTween.IsValid())
prevTween.Kill();
if (animate)
{
var tween = CreateTween();
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
tween.TweenProperty(card, "position", targetPos, 0.3f);
activeTweens[card] = tween;
}
else
{
card.Position = targetPos;
activeTweens.Remove(card);
}
var baseZ = CalcBaseZIndex(row, col);
card.SetBaseZIndex(baseZ);
if (content != null)
content.MoveChild(card, index);
else
MoveChild(card, index);
card.UpdateInfo();
}
///
/// Marks the card as dead and starts the dissolve sequence.
/// Once the animation completes, the card is removed from ,
/// is fired, and the remaining cards are redrawn.
/// Triggered by the health-change handler when a character's health reaches zero.
///
public void Dead(CharacterCardNode characterCardNode)
{
if (characterCardNode == null || characterCardNode.IsDead)
return;
characterCardNode.IsDead = true;
var animator = new Cthangover.CardBattle.UI.CardDeathAnimation();
characterCardNode.AddChild(animator);
animator.StartAnimation(characterCardNode, () =>
{
OnCardDead?.Invoke(characterCardNode);
CardList.Remove(characterCardNode);
activeTweens.Remove(characterCardNode);
Redraw();
});
}
}
}