Files
mod_ff_battle/src/UI/FFMenuPanel.cs
T
iisaevandClaude Opus 5.5 00aa70e302 upd: sync local changes
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>
2026-10-16 16:18:14 +00:00

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();
}
}
}