Fix enemy turn hanging when a lethal hit frees the target widget: UpdateInfo skips freed widgets, animations finish early and restore the attacker only while it is alive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
265 lines
10 KiB
C#
265 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using Cthangover.Core.UI;
|
|
using Cthangover.Core.UI.Theming;
|
|
using Godot;
|
|
|
|
using Cthangover.Mods.Interface.Base.Lists;
|
|
using Cthangover.Mods.Interface.Theming;
|
|
namespace Cthangover.FFBattle.UI
|
|
{
|
|
/// <summary>
|
|
/// Data object for a single menu entry. <see cref="Key"/> drives the action
|
|
/// dispatch in <see cref="FFBattleCore.MatchMenuAction"/> (values like
|
|
/// <c>"attack"</c>, <c>"action"</c>, <c>"item"</c>, <c>"back"</c>).
|
|
/// <see cref="Data"/> carries arbitrary payload: an
|
|
/// <see cref="ActionCharacter"/> for action entries or an
|
|
/// <see cref="IItem"/> for item entries.
|
|
/// </summary>
|
|
public class FFMenuEntry
|
|
{
|
|
/// <summary>Dispatch key used by <see cref="FFBattleCore"/> to determine the next step.</summary>
|
|
public string Key;
|
|
/// <summary>Display label shown in the menu.</summary>
|
|
public string Label;
|
|
/// <summary>Whether this entry can be selected (greyed out if false).</summary>
|
|
public bool Enabled = true;
|
|
/// <summary>Payload data: <see cref="ActionCharacter"/> for actions, <see cref="IItem"/> for items.</summary>
|
|
public object Data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Vertical scrolling menu panel with a blinking cursor, keyboard navigation,
|
|
/// and mouse hover/click support. Used as both the main battle menu and the
|
|
/// action/item sub-menu (a separate instance). Each menu is populated via
|
|
/// <see cref="ShowMenu"/> with a list of <see cref="FFMenuEntry"/> objects.
|
|
/// Selection wraps around but skips disabled entries. Raises
|
|
/// <see cref="OnItemSelected"/> on confirm, <see cref="OnCancelled"/> on
|
|
/// right-click or Escape (handled by <see cref="FFBattleController"/>).
|
|
/// </summary>
|
|
public partial class FFMenuPanel : ModWidget
|
|
{
|
|
private ColorRect _background;
|
|
private Panel _border;
|
|
private Label _cursorLabel;
|
|
private Label _caption;
|
|
private StyleBoxFlat _rowActiveStyle;
|
|
private StyleBoxFlat _rowIdleStyle;
|
|
private List<Label> _itemLabels = new();
|
|
private Tween _cursorTween;
|
|
private int _selectedIndex;
|
|
private bool _visible;
|
|
|
|
private const float ITEM_HEIGHT = 34f;
|
|
private const float PADDING = 18f;
|
|
private const float CURSOR_OFFSET_X = 6f;
|
|
private const float CURSOR_OFFSET_Y = 7f;
|
|
|
|
/// <summary>The entries currently displayed in the menu.</summary>
|
|
public List<FFMenuEntry> Entries { get; private set; } = new();
|
|
/// <summary>
|
|
/// 0-based index of the highlighted entry. Setting it moves the
|
|
/// cursor and clamps to the valid range.
|
|
/// </summary>
|
|
public int SelectedIndex
|
|
{
|
|
get => _selectedIndex;
|
|
set
|
|
{
|
|
var clamped = Mathf.Clamp(value, 0, Entries.Count - 1);
|
|
if (clamped == _selectedIndex)
|
|
return;
|
|
_selectedIndex = clamped;
|
|
MoveCursor(_selectedIndex);
|
|
}
|
|
}
|
|
|
|
/// <summary>Raised when an entry is confirmed (Enter/Space or left click).</summary>
|
|
public event System.Action<int> OnItemSelected;
|
|
/// <summary>Raised when the highlighted entry changes (keyboard or mouse hover); carries the new index.</summary>
|
|
public event System.Action<int> OnSelectionChanged;
|
|
/// <summary>Raised when the menu is cancelled (right click, or Escape handled externally).</summary>
|
|
public event System.Action OnCancelled;
|
|
|
|
/// <summary>Builds the command menu list (Attack/Defence/Item/...) used during the player turn.</summary>
|
|
protected override void Construct()
|
|
{
|
|
CthangoverTheme.Apply(this);
|
|
|
|
_background = GetNode<ColorRect>("Background");
|
|
_background.Color = UiTokens.Surface2;
|
|
|
|
_border = GetNode<Panel>("Border");
|
|
_border.AddThemeStyleboxOverride("panel",
|
|
UiKit.Frame(UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.White, 0f), UiTokens.AccentLine, 2));
|
|
|
|
_cursorLabel = GetNode<Label>("Cursor");
|
|
_cursorLabel.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
|
|
|
_caption = GetNode<Label>("Caption");
|
|
_caption.AddThemeColorOverride("font_color", UiTokens.Muted);
|
|
|
|
_rowActiveStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.AccentTrace, UiTokens.Alpha(UiTokens.White, 0f));
|
|
_rowIdleStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.White, 0f), UiTokens.Alpha(UiTokens.White, 0f));
|
|
|
|
GuiInput += OnPanelGuiInput;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the menu with the given entries. Creates labels, wires hover/click
|
|
/// handlers, sizes the panel to fit, positions the cursor at index 0, and
|
|
/// makes the panel visible.
|
|
/// </summary>
|
|
public void ShowMenu(List<FFMenuEntry> entries, string title = null)
|
|
{
|
|
ClearEntries();
|
|
Entries = entries;
|
|
|
|
float width = 220f;
|
|
float totalHeight = PADDING * 2 + ITEM_HEIGHT * entries.Count;
|
|
Size = new Vector2(width, totalHeight);
|
|
|
|
for (int i = 0; i < entries.Count; i++)
|
|
{
|
|
var label = new Label();
|
|
label.Text = entries[i].Label;
|
|
label.AddThemeFontSizeOverride("font_size", 17);
|
|
label.AddThemeColorOverride("font_color", entries[i].Enabled
|
|
? UiTokens.Ink
|
|
: UiTokens.Faint);
|
|
label.AddThemeStyleboxOverride("normal", _rowIdleStyle);
|
|
label.MouseFilter = MouseFilterEnum.Stop;
|
|
label.Position = new Vector2(PADDING + 24, PADDING + i * ITEM_HEIGHT + CURSOR_OFFSET_Y);
|
|
label.Size = new Vector2(width - PADDING * 2 - 24, ITEM_HEIGHT);
|
|
label.ClipText = true;
|
|
|
|
int idx = i;
|
|
label.GuiInput += (evt) =>
|
|
{
|
|
if (evt is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
|
|
{
|
|
if (entries[idx].Enabled)
|
|
{
|
|
_selectedIndex = idx;
|
|
MoveCursor(idx);
|
|
OnItemSelected?.Invoke(idx);
|
|
}
|
|
}
|
|
};
|
|
label.MouseEntered += () =>
|
|
{
|
|
if (entries[idx].Enabled)
|
|
{
|
|
SelectedIndex = idx;
|
|
}
|
|
};
|
|
|
|
AddChild(label);
|
|
_itemLabels.Add(label);
|
|
}
|
|
|
|
_selectedIndex = 0;
|
|
MoveCursor(0);
|
|
_cursorLabel.Visible = true;
|
|
_caption.Text = title ?? string.Empty;
|
|
_caption.Visible = !string.IsNullOrEmpty(title);
|
|
_visible = true;
|
|
Visible = true;
|
|
}
|
|
|
|
/// <summary>Hides the menu, caption and cursor. Does not clear entries — call <see cref="ShowMenu"/> to repopulate.</summary>
|
|
public void HideMenu()
|
|
{
|
|
_visible = false;
|
|
Visible = false;
|
|
_cursorLabel.Visible = false;
|
|
_caption.Visible = false;
|
|
}
|
|
|
|
/// <summary>Moves selection down, wrapping around and skipping disabled entries.</summary>
|
|
public void SelectNext()
|
|
{
|
|
if (!_visible || Entries.Count == 0)
|
|
return;
|
|
|
|
var next = _selectedIndex;
|
|
for (int i = 0; i < Entries.Count; i++)
|
|
{
|
|
next = (next + 1) % Entries.Count;
|
|
if (Entries[next].Enabled)
|
|
break;
|
|
}
|
|
SelectedIndex = next;
|
|
}
|
|
|
|
/// <summary>Moves selection up, wrapping around and skipping disabled entries.</summary>
|
|
public void SelectPrevious()
|
|
{
|
|
if (!_visible || Entries.Count == 0)
|
|
return;
|
|
|
|
var prev = _selectedIndex;
|
|
for (int i = 0; i < Entries.Count; i++)
|
|
{
|
|
prev = (prev - 1 + Entries.Count) % Entries.Count;
|
|
if (Entries[prev].Enabled)
|
|
break;
|
|
}
|
|
SelectedIndex = prev;
|
|
}
|
|
|
|
/// <summary>Invokes <see cref="OnItemSelected"/> for the currently highlighted entry if it is enabled.</summary>
|
|
public void ConfirmSelection()
|
|
{
|
|
if (!_visible || Entries.Count == 0)
|
|
return;
|
|
|
|
if (_selectedIndex >= 0 && _selectedIndex < Entries.Count && Entries[_selectedIndex].Enabled)
|
|
OnItemSelected?.Invoke(_selectedIndex);
|
|
}
|
|
|
|
private void MoveCursor(int index)
|
|
{
|
|
_cursorTween?.Kill();
|
|
_cursorLabel.Position = new Vector2(CURSOR_OFFSET_X, PADDING + index * ITEM_HEIGHT + CURSOR_OFFSET_Y);
|
|
|
|
_cursorTween = CreateTween();
|
|
_cursorTween.SetLoops();
|
|
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X + 3f, 0.3f);
|
|
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X, 0.3f);
|
|
|
|
for (var i = 0; i < _itemLabels.Count; i++)
|
|
{
|
|
var enabled = i < Entries.Count && Entries[i].Enabled;
|
|
var selected = i == index && enabled;
|
|
_itemLabels[i].AddThemeStyleboxOverride("normal", selected ? _rowActiveStyle : _rowIdleStyle);
|
|
_itemLabels[i].AddThemeColorOverride("font_color",
|
|
selected ? UiTokens.AccentBright : enabled ? UiTokens.Ink : UiTokens.Faint);
|
|
}
|
|
|
|
OnSelectionChanged?.Invoke(index);
|
|
}
|
|
|
|
private void OnPanelGuiInput(InputEvent evt)
|
|
{
|
|
if (evt is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Right)
|
|
{
|
|
OnCancelled?.Invoke();
|
|
}
|
|
}
|
|
|
|
private void ClearEntries()
|
|
{
|
|
foreach (var label in _itemLabels)
|
|
label.QueueFree();
|
|
_itemLabels.Clear();
|
|
}
|
|
|
|
/// <summary>Frees the menu entries and disconnects selection handlers when the panel closes.</summary>
|
|
protected override void Destruct()
|
|
{
|
|
ClearEntries();
|
|
_cursorTween?.Kill();
|
|
}
|
|
}
|
|
}
|