fix(combat): charge power bar from speed

Read the authored meter direction instead of inferring it from the combat element id, so the bright charge texture grows left-to-right from Speed. Keep the user-approved dark-red middle baseline visible independently and record its exact retail Recklessness visibility edge.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 21:25:24 +02:00
parent 256c1930bd
commit 707add8539
13 changed files with 50 additions and 49 deletions

View file

@ -2135,8 +2135,7 @@ public sealed class GameWindow : IDisposable
Combat,
_combatAttackController,
() => _persistedGameplay,
SetRetailCombatGameplay,
() => LocalPlayer.GetSkill(0x32u)?.Status >= 2u),
SetRetailCombatGameplay),
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
Objects,
() => Shortcuts,

View file

@ -105,7 +105,6 @@ public static class FixtureProvider
attacks,
() => gameplay,
value => gameplay = value,
() => true,
new CombatUiLabels(
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
"High", "Medium", "Low"),

View file

@ -66,7 +66,6 @@ public sealed class CombatUiController : IRetainedPanelController
CombatAttackController attacks,
Func<GameplaySettings> gameplay,
Action<GameplaySettings> setGameplay,
Func<bool> recklessnessTrained,
CombatUiLabels labels,
Action<bool> setWindowVisible)
{
@ -95,7 +94,6 @@ 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);
@ -131,7 +129,6 @@ public sealed class CombatUiController : IRetainedPanelController
CombatAttackController attacks,
Func<GameplaySettings> gameplay,
Action<GameplaySettings> setGameplay,
Func<bool> recklessnessTrained,
CombatUiLabels labels,
Action<bool> setWindowVisible)
{
@ -140,7 +137,6 @@ public sealed class CombatUiController : IRetainedPanelController
ArgumentNullException.ThrowIfNull(attacks);
ArgumentNullException.ThrowIfNull(gameplay);
ArgumentNullException.ThrowIfNull(setGameplay);
ArgumentNullException.ThrowIfNull(recklessnessTrained);
ArgumentNullException.ThrowIfNull(labels);
ArgumentNullException.ThrowIfNull(setWindowVisible);
@ -158,8 +154,7 @@ public sealed class CombatUiController : IRetainedPanelController
return new CombatUiController(
layout, basic, advanced, power, high, medium, low,
repeatAttacks, autoTarget, keepInView,
combat, attacks, gameplay, setGameplay, recklessnessTrained,
labels, setWindowVisible);
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
}
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
@ -238,7 +233,6 @@ 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

@ -178,19 +178,22 @@ public static class DatWidgetFactory
.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.
// gmCombatUI preserves a second authored meter child for its dark
// red interior range. The widget retains the media because horizontal
// scrollbars consume their 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).
bar.ScalarFillFromRight = meter?.Id == 0x10000050u;
// UIElement_Meter::UIElement_Meter @ 0x0046F4C0 defaults direction
// 1; DrawChildren @ 0x0046FBD0 clips that direction left-to-right.
// Direction 3 is the horizontal reverse. Read authored attribute
// 0x6F instead of inferring direction from the combat element id.
bar.ScalarFillFromRight = meter is not null
&& meter.TryGetEffectiveProperty(0x6Fu, out UiPropertyValue direction)
&& direction.Kind == UiPropertyKind.Enum
&& direction.UnsignedValue == 3u;
return bar;
}

View file

@ -40,8 +40,7 @@ public sealed record CombatRuntimeBindings(
CombatState State,
CombatAttackController Attacks,
Func<GameplaySettings> Gameplay,
Action<GameplaySettings> SetGameplay,
Func<bool> RecklessnessTrained);
Action<GameplaySettings> SetGameplay);
public sealed record ToolbarRuntimeBindings(
ClientObjectTable Objects,
@ -481,7 +480,6 @@ public sealed class RetailUiRuntime : IDisposable
_bindings.Combat.Attacks,
_bindings.Combat.Gameplay,
_bindings.Combat.SetGameplay,
_bindings.Combat.RecklessnessTrained,
labels,
visible =>
{

View file

@ -42,11 +42,10 @@ 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.
/// Optional authored texture beneath the live scalar fill. gmCombatUI's
/// dark-red interior media comes from element <c>0x100005EF</c>.
/// </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>
@ -139,7 +138,7 @@ public sealed class UiScrollbar : UiElement
{
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
(float rangeLeft, float rangeWidth) = ScalarRangeRect();
if (ScalarRangeVisible() && ScalarRangeSprite != 0)
if (ScalarRangeSprite != 0)
DrawTiled(ctx, resolve, ScalarRangeSprite,
rangeLeft, 0f, rangeWidth, Height);
if (ScalarFill() is float fill && ScalarFillSprite != 0)