fix(combat): restore retail power bar layers
Preserve the authored gray track, trained-Recklessness range, live bright charge meter, and independent desired-power thumb while keeping Speed and Power over gray side regions. Correct retail text justification value 2 to left alignment and retain direct RenderSurface decoding in the texture inspection tool used to verify the assets. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
0f2d98c501
commit
256c1930bd
18 changed files with 200 additions and 32 deletions
|
|
@ -66,6 +66,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
CombatAttackController attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
Func<bool> recklessnessTrained,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
|
|
@ -94,6 +95,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_powerControl.SetScalarPosition(_attacks.DesiredPower);
|
||||
_powerControl.ScalarChanged = _attacks.SetDesiredPower;
|
||||
_powerControl.ScalarFill = () => _attacks.PowerBarLevel;
|
||||
_powerControl.ScalarRangeVisible = recklessnessTrained;
|
||||
|
||||
BindAttackButton(_high, AttackHeight.High);
|
||||
BindAttackButton(_medium, AttackHeight.Medium);
|
||||
|
|
@ -105,8 +107,11 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_repeatAttacks.Label = labels.RepeatAttacks;
|
||||
_autoTarget.Label = labels.AutoTarget;
|
||||
_keepInView.Label = labels.KeepInView;
|
||||
SetStaticText(layout.FindElement(SpeedLabelId) as UiText, labels.Speed);
|
||||
SetStaticText(layout.FindElement(PowerLabelId) as UiText, labels.Power);
|
||||
UiText? speedLabel = layout.FindElement(SpeedLabelId) as UiText;
|
||||
UiText? powerLabel = layout.FindElement(PowerLabelId) as UiText;
|
||||
SetStaticText(speedLabel, labels.Speed, rightAligned: false);
|
||||
SetStaticText(powerLabel, labels.Power, rightAligned: true);
|
||||
ConfigurePowerRange(speedLabel, powerLabel);
|
||||
|
||||
_repeatAttacks.OnClick = () =>
|
||||
_setGameplay(_gameplay() with { AutoRepeatAttack = _repeatAttacks.Selected });
|
||||
|
|
@ -126,6 +131,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
CombatAttackController attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
Func<bool> recklessnessTrained,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
|
|
@ -134,6 +140,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
ArgumentNullException.ThrowIfNull(attacks);
|
||||
ArgumentNullException.ThrowIfNull(gameplay);
|
||||
ArgumentNullException.ThrowIfNull(setGameplay);
|
||||
ArgumentNullException.ThrowIfNull(recklessnessTrained);
|
||||
ArgumentNullException.ThrowIfNull(labels);
|
||||
ArgumentNullException.ThrowIfNull(setWindowVisible);
|
||||
|
||||
|
|
@ -151,7 +158,8 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
return new CombatUiController(
|
||||
layout, basic, advanced, power, high, medium, low,
|
||||
repeatAttacks, autoTarget, keepInView,
|
||||
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
|
||||
combat, attacks, gameplay, setGameplay, recklessnessTrained,
|
||||
labels, setWindowVisible);
|
||||
}
|
||||
|
||||
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
|
||||
|
|
@ -190,7 +198,23 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_keepInView.Selected = gameplay.ViewCombatTarget;
|
||||
}
|
||||
|
||||
private static void SetStaticText(UiText? text, string value)
|
||||
private void ConfigurePowerRange(UiText? speedLabel, UiText? powerLabel)
|
||||
{
|
||||
if (speedLabel is null || powerLabel is null) return;
|
||||
|
||||
// All three elements are siblings in gmCombatUI's basic page. Keeping
|
||||
// the meter between the authored text rectangles leaves the base gray
|
||||
// track behind Speed/Power while the dark/live red textures occupy the
|
||||
// semantic power range between them.
|
||||
float left = speedLabel.Left + speedLabel.Width - _powerControl.Left;
|
||||
float right = powerLabel.Left - _powerControl.Left;
|
||||
_powerControl.ScalarRangeLeft = Math.Clamp(left, 0f, _powerControl.Width);
|
||||
_powerControl.ScalarRangeWidth = Math.Max(
|
||||
0f, Math.Clamp(right, 0f, _powerControl.Width)
|
||||
- _powerControl.ScalarRangeLeft);
|
||||
}
|
||||
|
||||
private static void SetStaticText(UiText? text, string value, bool rightAligned)
|
||||
{
|
||||
if (text is null) return;
|
||||
// Retail UIElement_Text starts with zero margins; these 15px-high
|
||||
|
|
@ -198,6 +222,8 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
// generic 4px inset would leave less than one glyph row and clip them all.
|
||||
text.OneLine = true;
|
||||
text.Padding = 0f;
|
||||
text.Centered = false;
|
||||
text.RightAligned = rightAligned;
|
||||
UiText.Line[] line = [new UiText.Line(value, text.DefaultColor)];
|
||||
text.LinesProvider = () => line;
|
||||
}
|
||||
|
|
@ -212,6 +238,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_attacks.StateChanged -= OnAttackStateChanged;
|
||||
_powerControl.ScalarChanged = null;
|
||||
_powerControl.ScalarFill = () => null;
|
||||
_powerControl.ScalarRangeVisible = () => false;
|
||||
_high.OnPressed = null;
|
||||
_high.OnReleased = null;
|
||||
_medium.OnPressed = null;
|
||||
|
|
|
|||
|
|
@ -177,6 +177,16 @@ public static class DatWidgetFactory
|
|||
.OrderByDescending(child => child.ReadOrder)
|
||||
.FirstOrDefault();
|
||||
bar.ScalarFillSprite = fill is null ? 0u : DefaultImage(fill);
|
||||
|
||||
// gmCombatUI preserves a second authored meter child for the
|
||||
// trained-Recklessness range. The controller owns its visibility,
|
||||
// exactly like gmCombatUI::ListenToElementMessage @ 0x004CC430;
|
||||
// the widget only retains the media because it consumes DAT children.
|
||||
ElementInfo? scalarRange = meter?.Children.FirstOrDefault(
|
||||
child => child.Id == 0x100005EFu);
|
||||
bar.ScalarRangeSprite = scalarRange is null
|
||||
? 0u
|
||||
: DefaultImage(scalarRange);
|
||||
// gmCombatUI meter 0x10000050 grows from the Power end at the
|
||||
// right toward Speed at the left (retail screenshot oracle +
|
||||
// gmCombatUI::RecvNotice_SetPowerbarLevel 0x004CC0E0).
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ namespace AcDream.App.UI.Layout;
|
|||
|
||||
/// <summary>
|
||||
/// Horizontal text justification read from dat property 0x14 (UIElement HorizontalJustification).
|
||||
/// Values match the retail enum: 0=Left, 1=Center, 3/5=Right.
|
||||
/// Retail <c>CalcJustification @ 0x00467260</c> treats 1 as Center,
|
||||
/// 3/5 as Right, and every other value (including constructor default 2) as Left.
|
||||
/// </summary>
|
||||
public enum HJustify : byte { Left = 0, Center = 1, Right = 2 }
|
||||
|
||||
|
|
@ -348,7 +349,7 @@ public static class ElementReader
|
|||
{
|
||||
info.HJustify = horizontal.UnsignedValue switch
|
||||
{
|
||||
0u => HJustify.Left,
|
||||
0u or 2u => HJustify.Left,
|
||||
3u or 5u => HJustify.Right,
|
||||
_ => HJustify.Center,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -435,7 +435,8 @@ public static class LayoutImporter
|
|||
if (sd.Properties is not null)
|
||||
{
|
||||
// HorizontalJustification (0x14): EnumBaseProperty.
|
||||
// Retail values: 0=Left, 1=Center, 3=Right, 5=Right (treat 5 as Right per spec).
|
||||
// Retail CalcJustification @ 0x00467260: 1=Center, 3/5=Right,
|
||||
// every other value (including constructor default 2)=Left.
|
||||
// Only update if still at the default (Center); derived-wins handled in Merge.
|
||||
if (info.HJustify == HJustify.Center
|
||||
&& sd.Properties.TryGetValue(0x14u, out var hRaw)
|
||||
|
|
@ -443,11 +444,11 @@ public static class LayoutImporter
|
|||
{
|
||||
info.HJustify = hEnum.Value switch
|
||||
{
|
||||
0u => HJustify.Left,
|
||||
0u or 2u => HJustify.Left,
|
||||
1u => HJustify.Center,
|
||||
3u => HJustify.Right,
|
||||
5u => HJustify.Right,
|
||||
_ => HJustify.Center, // unknown → center (safe default)
|
||||
_ => HJustify.Left,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ public sealed record CombatRuntimeBindings(
|
|||
CombatState State,
|
||||
CombatAttackController Attacks,
|
||||
Func<GameplaySettings> Gameplay,
|
||||
Action<GameplaySettings> SetGameplay);
|
||||
Action<GameplaySettings> SetGameplay,
|
||||
Func<bool> RecklessnessTrained);
|
||||
|
||||
public sealed record ToolbarRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
|
|
@ -480,6 +481,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Combat.Attacks,
|
||||
_bindings.Combat.Gameplay,
|
||||
_bindings.Combat.SetGameplay,
|
||||
_bindings.Combat.RecklessnessTrained,
|
||||
labels,
|
||||
visible =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ public sealed class UiScrollbar : UiElement
|
|||
public Func<float?> ScalarFill { get; set; } = () => null;
|
||||
public uint ScalarFillSprite { get; set; }
|
||||
/// <summary>
|
||||
/// Optional authored texture beneath the live scalar fill. gmCombatUI uses
|
||||
/// element <c>0x100005EF</c> for the trained-Recklessness range.
|
||||
/// </summary>
|
||||
public uint ScalarRangeSprite { get; set; }
|
||||
public Func<bool> ScalarRangeVisible { get; set; } = () => false;
|
||||
public float ScalarRangeLeft { get; set; }
|
||||
public float ScalarRangeWidth { get; set; } = float.PositiveInfinity;
|
||||
/// <summary>
|
||||
/// Draw the scalar meter from the power end (right) toward the speed end
|
||||
/// (left). This is the authored gmCombatUI power meter direction.
|
||||
/// </summary>
|
||||
|
|
@ -113,9 +121,15 @@ public sealed class UiScrollbar : UiElement
|
|||
/// <summary>Returns the clipped scalar-meter span in local pixels.</summary>
|
||||
public static (float x, float width) ScalarFillRect(
|
||||
float totalWidth, float fill, bool fromRight)
|
||||
=> ScalarFillRect(0f, totalWidth, fill, fromRight);
|
||||
|
||||
/// <summary>Returns a clipped scalar-meter span inside an authored sub-range.</summary>
|
||||
public static (float x, float width) ScalarFillRect(
|
||||
float rangeLeft, float rangeWidth, float fill, bool fromRight)
|
||||
{
|
||||
float visibleWidth = totalWidth * Math.Clamp(fill, 0f, 1f);
|
||||
return (fromRight ? totalWidth - visibleWidth : 0f, visibleWidth);
|
||||
float safeWidth = MathF.Max(0f, rangeWidth);
|
||||
float visibleWidth = safeWidth * Math.Clamp(fill, 0f, 1f);
|
||||
return (fromRight ? rangeLeft + safeWidth - visibleWidth : rangeLeft, visibleWidth);
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
|
|
@ -124,14 +138,16 @@ public sealed class UiScrollbar : UiElement
|
|||
if (Horizontal)
|
||||
{
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
(float rangeLeft, float rangeWidth) = ScalarRangeRect();
|
||||
if (ScalarRangeVisible() && ScalarRangeSprite != 0)
|
||||
DrawTiled(ctx, resolve, ScalarRangeSprite,
|
||||
rangeLeft, 0f, rangeWidth, Height);
|
||||
if (ScalarFill() is float fill && ScalarFillSprite != 0)
|
||||
{
|
||||
var (fillX, visibleWidth) = ScalarFillRect(Width, fill, ScalarFillFromRight);
|
||||
if (ScalarFillFromRight)
|
||||
DrawTiledRightClipped(
|
||||
ctx, resolve, ScalarFillSprite, fillX, visibleWidth, Height);
|
||||
else
|
||||
DrawTiled(ctx, resolve, ScalarFillSprite, 0f, 0f, visibleWidth, Height);
|
||||
var (fillX, visibleWidth) = ScalarFillRect(
|
||||
rangeLeft, rangeWidth, fill, ScalarFillFromRight);
|
||||
DrawTiledClipped(ctx, resolve, ScalarFillSprite,
|
||||
rangeLeft, fillX, visibleWidth, Height);
|
||||
}
|
||||
float thumbWidth = ScalarThumbWidth(resolve);
|
||||
float travel = MathF.Max(0f, Width - thumbWidth);
|
||||
|
|
@ -196,14 +212,14 @@ public sealed class UiScrollbar : UiElement
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the rightmost portion of a full-width tiled scalar image. UVs
|
||||
/// start at the same offset they would have in the uncut image, matching
|
||||
/// retail meter clipping rather than restarting the wave at the clip edge.
|
||||
/// Draws a clipped portion of a tiled range without restarting the texture
|
||||
/// phase at the clip edge. This mirrors retail meter child clipping.
|
||||
/// </summary>
|
||||
private void DrawTiledRightClipped(
|
||||
private void DrawTiledClipped(
|
||||
UiRenderContext ctx,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id,
|
||||
float rangeLeft,
|
||||
float x,
|
||||
float w,
|
||||
float h)
|
||||
|
|
@ -211,7 +227,19 @@ public sealed class UiScrollbar : UiElement
|
|||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
ctx.DrawSprite(tex, x, 0f, w, h, x / tw, 0f, (x + w) / tw, h / th, Vector4.One);
|
||||
float u0 = (x - rangeLeft) / tw;
|
||||
float u1 = u0 + w / tw;
|
||||
ctx.DrawSprite(tex, x, 0f, w, h, u0, 0f, u1, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
private (float left, float width) ScalarRangeRect()
|
||||
{
|
||||
float left = Math.Clamp(ScalarRangeLeft, 0f, Width);
|
||||
float requestedWidth = float.IsPositiveInfinity(ScalarRangeWidth)
|
||||
? Width - left
|
||||
: MathF.Max(0f, ScalarRangeWidth);
|
||||
float width = Math.Clamp(requestedWidth, 0f, Width - left);
|
||||
return (left, width);
|
||||
}
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue