@
feat(D.2b): chat polish — typing fix, opacity, scrollbar 3-slice, retail channel menu Visual-iteration batch (decomp-grounded), each fix verified against the retail screenshots: - typing: UiElement.HitTest aborted on ClickThrough BEFORE walking children, so the ClickThrough UiDatElement panels blocked hit-testing to the input/transcript inside them. Check ClickThrough AFTER the child walk (it only gates whether THIS element claims the hit). Restores input focus + typing. - opacity: UiElement.Opacity + a UiRenderContext alpha stack applied to sprite/rect draws (text bypasses it, stays sharp); chat frame Opacity=0.75 → translucent chat. - brown sliver: grow the transcript panel up 9px to cover the dropped resize-bar strip. - scrollbar: real 3-slice thumb (caps 0x06004C60/66 + tiled mid) + tiled track. - max/min: shifted one button-width left of the scrollbar (dat right-anchors collide). - system text now green (retail ChatMessageType 5; was yellow). - word-wrap: transcript lines wrap to the panel width (greedy, ports GlyphList::Recalculate). - channel menu reworked to retail gmMainChatUI::InitTalkFocusMenu: "Chat" button + a TWO-COLUMN popup of the 14 talk-focus items (Squelch, Tell to Selected, Chat to All, Tell to Fellows, ...) on a tan panel; channel items set the active outbound channel. Build + 392 App tests green. Visual confirmation in progress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
parent
0ec36f6197
commit
1da697ec2a
7 changed files with 329 additions and 126 deletions
|
|
@ -5,32 +5,44 @@ using AcDream.UI.Abstractions;
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Chat channel selector (the "Chat ▸" button). Port of retail
|
||||
/// <c>UIElement_Menu</c> as used by <c>gmMainChatUI::InitTalkFocusMenu @0x4cdc50</c>:
|
||||
/// a button whose label is the active channel; clicking opens a popup of channels;
|
||||
/// selecting one calls <c>SetTalkFocus</c> (here: <see cref="OnChannelChanged"/>).
|
||||
/// Chat channel selector — the "Chat" button + its talk-focus popup. Mirrors retail
|
||||
/// <c>gmMainChatUI::InitTalkFocusMenu @0x4cdc50</c>: the button is labelled "Chat";
|
||||
/// clicking opens a TWO-COLUMN popup of 14 talk-focus items (Squelch, Tell to Selected,
|
||||
/// Chat to All, Tell to Fellows, …). Selecting a channel item sets the active outbound
|
||||
/// channel (retail <c>SetTalkFocus</c>; here <see cref="OnChannelChanged"/>). The items
|
||||
/// are code-populated exactly as retail populates them, not a dat-layout port.
|
||||
/// </summary>
|
||||
public sealed class UiChannelMenu : UiElement
|
||||
{
|
||||
public readonly record struct Item(string Label, ChatChannelKind Channel);
|
||||
/// <summary>One menu row: its label + the channel it selects (null = special/no-op
|
||||
/// item such as Squelch or Tell-to-Selected, deferred).</summary>
|
||||
public readonly record struct Item(string Label, ChatChannelKind? Channel);
|
||||
|
||||
/// <summary>Retail talk-focus channels (subset acdream's ChatInputParser routes).</summary>
|
||||
public static readonly Item[] Channels =
|
||||
/// <summary>The 14 retail talk-focus items in retail order — left column rows 0–6,
|
||||
/// right column rows 7–13 (matching the live retail menu).</summary>
|
||||
public static readonly Item[] Items =
|
||||
{
|
||||
new("Say", ChatChannelKind.Say),
|
||||
new("General", ChatChannelKind.General),
|
||||
new("Trade", ChatChannelKind.Trade),
|
||||
new("LFG", ChatChannelKind.Lfg),
|
||||
new("Fellowship", ChatChannelKind.Fellowship),
|
||||
new("Allegiance", ChatChannelKind.Allegiance),
|
||||
new("Patron", ChatChannelKind.Patron),
|
||||
new("Vassals", ChatChannelKind.Vassals),
|
||||
new("Monarch", ChatChannelKind.Monarch),
|
||||
new("Roleplay", ChatChannelKind.Roleplay),
|
||||
new("Society", ChatChannelKind.Society),
|
||||
new("Olthoi", ChatChannelKind.Olthoi),
|
||||
new("Squelch (ignore)", null), // 0 special (squelch — deferred)
|
||||
new("Tell to Selected", null), // 1 special (selected target — deferred)
|
||||
new("Chat to All", ChatChannelKind.Say), // 2
|
||||
new("Tell to Fellows", ChatChannelKind.Fellowship), // 3
|
||||
new("Tell to General Chat", ChatChannelKind.General), // 4
|
||||
new("Tell to LFG Chat", ChatChannelKind.Lfg), // 5
|
||||
new("Tell to Society Chat", ChatChannelKind.Society), // 6
|
||||
new("Tell to Monarch", ChatChannelKind.Monarch), // 7
|
||||
new("Tell to Patron", ChatChannelKind.Patron), // 8
|
||||
new("Tell to Vassals", ChatChannelKind.Vassals), // 9
|
||||
new("Tell to Allegiance", ChatChannelKind.Allegiance), // 10
|
||||
new("Tell to Trade Chat", ChatChannelKind.Trade), // 11
|
||||
new("Tell to Roleplay Chat", ChatChannelKind.Roleplay), // 12
|
||||
new("Tell to Olthoi Chat", ChatChannelKind.Olthoi), // 13
|
||||
};
|
||||
|
||||
private const int Rows = 7; // items per column
|
||||
private const float ItemH = 16f; // row height
|
||||
private const float ColW = 150f; // column width (fits "Tell to Roleplay Chat")
|
||||
|
||||
/// <summary>The channel the player's typed text currently goes to.</summary>
|
||||
public ChatChannelKind Selected { get; private set; } = ChatChannelKind.Say;
|
||||
public Action<ChatChannelKind>? OnChannelChanged { get; set; }
|
||||
|
||||
|
|
@ -39,41 +51,40 @@ public sealed class UiChannelMenu : UiElement
|
|||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public uint NormalSprite { get; set; }
|
||||
public uint PressedSprite { get; set; }
|
||||
public Vector4 TextColor { get; set; } = new(1f, 0.85f, 0.4f, 1f);
|
||||
public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f);
|
||||
/// <summary>Popup panel fill — the retail talk-focus menu is a warm tan/orange.</summary>
|
||||
public Vector4 PopupColor { get; set; } = new(0.56f, 0.40f, 0.18f, 0.97f);
|
||||
|
||||
private bool _open;
|
||||
private const float ItemH = 16f;
|
||||
private const float PopupW = 90f;
|
||||
private static float PopupW => 2 * ColW;
|
||||
private static float PopupH => Rows * ItemH;
|
||||
|
||||
public UiChannelMenu() { CapturesPointerDrag = true; }
|
||||
|
||||
private string Label => FindLabel(Selected);
|
||||
private static string FindLabel(ChatChannelKind k)
|
||||
{
|
||||
foreach (var it in Channels) if (it.Channel == k) return it.Label;
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// Button face + the "Chat" label (retail labels the talk-focus button "Chat").
|
||||
if (SpriteResolve is { } resolve)
|
||||
{
|
||||
var (tex, _, _) = resolve(_open ? PressedSprite : NormalSprite);
|
||||
if (tex != 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
DrawLabel(ctx, Label + " >", 2f, (Height - LineH()) * 0.5f);
|
||||
DrawLabel(ctx, "Chat", 4f, (Height - LineH()) * 0.5f);
|
||||
|
||||
if (_open)
|
||||
if (!_open) return;
|
||||
|
||||
// Two-column popup opening UPWARD from the button (chat sits at screen bottom).
|
||||
float top = -PopupH;
|
||||
ctx.DrawRect(0, top, PopupW, PopupH, PopupColor);
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
float h = Channels.Length * ItemH;
|
||||
float top = -h; // popup opens UPWARD (chat sits at screen bottom)
|
||||
ctx.DrawRect(0, top, MathF.Max(Width, PopupW), h, new(0f, 0f, 0f, 0.85f));
|
||||
for (int i = 0; i < Channels.Length; i++)
|
||||
DrawLabel(ctx, Channels[i].Label, 2f, top + i * ItemH);
|
||||
int col = i / Rows, row = i % Rows;
|
||||
DrawLabel(ctx, Items[i].Label, 4f + col * ColW, top + row * ItemH);
|
||||
}
|
||||
}
|
||||
|
||||
private float LineH() => DatFont?.LineHeight ?? Font?.LineHeight ?? 14f;
|
||||
|
||||
private void DrawLabel(UiRenderContext ctx, string s, float x, float y)
|
||||
{
|
||||
if (DatFont is { } df) ctx.DrawStringDat(df, s, x, y, TextColor);
|
||||
|
|
@ -81,29 +92,30 @@ public sealed class UiChannelMenu : UiElement
|
|||
}
|
||||
|
||||
protected override bool OnHitTest(float lx, float ly)
|
||||
=> _open ? (lx >= 0 && lx < MathF.Max(Width, PopupW)
|
||||
&& ly >= -Channels.Length * ItemH && ly < Height)
|
||||
=> _open ? (lx >= 0 && lx < PopupW && ly >= -PopupH && ly < Height)
|
||||
: base.OnHitTest(lx, ly);
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (e.Type == UiEventType.MouseDown)
|
||||
if (e.Type != UiEventType.MouseDown) return false;
|
||||
|
||||
float lx = e.Data1, ly = e.Data2;
|
||||
if (_open && ly < 0) // clicked an item in the upward popup
|
||||
{
|
||||
float ly = e.Data2;
|
||||
if (_open && ly < 0)
|
||||
int col = lx < ColW ? 0 : 1;
|
||||
int row = (int)((ly + PopupH) / ItemH);
|
||||
int idx = col * Rows + row;
|
||||
if (row >= 0 && row < Rows && idx >= 0 && idx < Items.Length
|
||||
&& Items[idx].Channel is { } ch)
|
||||
{
|
||||
int idx = (int)((ly + Channels.Length * ItemH) / ItemH);
|
||||
if (idx >= 0 && idx < Channels.Length)
|
||||
{
|
||||
Selected = Channels[idx].Channel;
|
||||
OnChannelChanged?.Invoke(Selected);
|
||||
}
|
||||
_open = false;
|
||||
return true;
|
||||
Selected = ch;
|
||||
OnChannelChanged?.Invoke(ch);
|
||||
}
|
||||
_open = !_open;
|
||||
_open = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
_open = !_open; // toggle on button click
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue