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:
Erik 2026-07-12 21:14:14 +02:00
parent 0f2d98c501
commit 256c1930bd
18 changed files with 200 additions and 32 deletions

View file

@ -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;

View file

@ -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).

View file

@ -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,
};

View file

@ -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,
};
}