feat(ui): port retail jump power bar
Import gmFloatyPowerBarUI LayoutDesc 0x21000072, teach the meter factory its stateful single-image shape, and project the movement-owned retail jump charge through a focused retained controller. Preserve authored resize constraints and state-managed visibility, with named-decomp pseudocode plus controller, movement, and production-DAT conformance coverage. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
27619b2328
commit
db03b4bda8
18 changed files with 3045 additions and 10 deletions
|
|
@ -311,7 +311,9 @@ public static class DatWidgetFactory
|
|||
.OrderBy(c => c.ReadOrder)
|
||||
.ToList();
|
||||
|
||||
if (containers.Count >= 2)
|
||||
if (containers.Count >= 2
|
||||
&& HasThreeSliceShape(containers[0])
|
||||
&& HasThreeSliceShape(containers[1]))
|
||||
{
|
||||
// Vitals 3-slice shape: two Type-3 containers each holding 3 grandchild images
|
||||
// (left-cap / center-tile / right-cap). Back is the lower ReadOrder; front is higher.
|
||||
|
|
@ -325,7 +327,7 @@ public static class DatWidgetFactory
|
|||
m.FrontTile = ft;
|
||||
m.FrontRight = fr;
|
||||
}
|
||||
else if (containers.Count == 1)
|
||||
else if (containers.Count == 1 && containers[0].StateMedia.ContainsKey(""))
|
||||
{
|
||||
// Single-image shape used by the toolbar selected-object meters
|
||||
// (health 0x100001A1, mana 0x100001A2).
|
||||
|
|
@ -354,15 +356,50 @@ public static class DatWidgetFactory
|
|||
m.FrontTile = containers[0].StateMedia.TryGetValue("", out var fm) ? fm.File : 0u;
|
||||
m.FrontRight = 0;
|
||||
}
|
||||
else if (containers.Any(HasStatefulFill))
|
||||
{
|
||||
// Stateful single-image shape used by gmPowerbarUI (LayoutDesc
|
||||
// 0x21000072). The meter's DirectState is the empty track. One
|
||||
// Type-3 child supplies named Jump/Melee/Missile/DDD state media;
|
||||
// another carries the optional recklessness range as DirectState.
|
||||
// gmPowerbarUI::PostInit @ 0x004DA4E0 hides that range, so it is not
|
||||
// promoted into a meter slice here.
|
||||
m.BackLeft = 0;
|
||||
m.BackTile = info.StateMedia.TryGetValue("", out var track) ? track.File : 0u;
|
||||
m.BackRight = 0;
|
||||
m.FrontLeft = 0;
|
||||
m.FrontTile = 0;
|
||||
m.FrontRight = 0;
|
||||
|
||||
foreach (ElementInfo container in containers)
|
||||
{
|
||||
foreach (var (stateId, state) in container.States)
|
||||
{
|
||||
if (stateId == UiStateInfo.DirectStateId
|
||||
|| !container.StateMedia.TryGetValue(state.Name, out var media))
|
||||
continue;
|
||||
m.ConfigureStateFill(stateId, media.File);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Count == 0: no Type-3 containers at all — genuinely malformed meter dat.
|
||||
Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 slice containers (expected 1 or 2) — bars may render as solid-color fallback.");
|
||||
Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback.");
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private static bool HasThreeSliceShape(ElementInfo container)
|
||||
=> container.Children.Count(c =>
|
||||
c.StateMedia.TryGetValue("", out var media) && media.File != 0) >= 3;
|
||||
|
||||
private static bool HasStatefulFill(ElementInfo container)
|
||||
=> container.States.Any(pair =>
|
||||
pair.Key != UiStateInfo.DirectStateId
|
||||
&& container.StateMedia.TryGetValue(pair.Value.Name, out var media)
|
||||
&& media.File != 0);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the (left, tile, right) sprite ids for a 3-slice container,
|
||||
/// extracting them from the container's image children that carry a DirectState
|
||||
|
|
|
|||
91
src/AcDream.App/UI/Layout/JumpPowerbarController.cs
Normal file
91
src/AcDream.App/UI/Layout/JumpPowerbarController.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using AcDream.App.Input;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Projects <see cref="PlayerMovementController.JumpCharge"/> into the imported
|
||||
/// retail <c>gmFloatyPowerBarUI</c>. The movement controller owns the clock; this
|
||||
/// controller owns only DAT state, fill, and window visibility.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail references: <c>gmPowerbarUI::RecvNotice_BeginPowerbar</c>
|
||||
/// (0x004DA730), <c>RecvNotice_SetPowerbarLevel</c> (0x004DA550),
|
||||
/// <c>RecvNotice_FinishPowerbar</c> (0x004DA580), and
|
||||
/// <c>ClientCombatSystem::CommenceJump</c> (0x0056AF90).
|
||||
/// </remarks>
|
||||
public sealed class JumpPowerbarController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000072u;
|
||||
public const uint MeterId = 0x10000034u;
|
||||
public const uint JumpModeState = 0x10000042u;
|
||||
|
||||
private readonly UiMeter _meter;
|
||||
private readonly Func<JumpChargeSnapshot> _snapshot;
|
||||
private readonly Action<bool> _setWindowVisible;
|
||||
private float _power;
|
||||
private bool _isCharging;
|
||||
private bool _hasSynced;
|
||||
private bool _disposed;
|
||||
|
||||
private JumpPowerbarController(
|
||||
UiMeter meter,
|
||||
Func<JumpChargeSnapshot> snapshot,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
_meter = meter;
|
||||
_snapshot = snapshot;
|
||||
_setWindowVisible = setWindowVisible;
|
||||
_meter.Fill = () => _power;
|
||||
SyncVisibility(force: true);
|
||||
}
|
||||
|
||||
public static JumpPowerbarController? Bind(
|
||||
ImportedLayout layout,
|
||||
Func<JumpChargeSnapshot> snapshot,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
ArgumentNullException.ThrowIfNull(setWindowVisible);
|
||||
|
||||
if (layout.FindElement(MeterId) is not UiMeter meter
|
||||
|| !meter.TrySetRetailState(JumpModeState))
|
||||
return null;
|
||||
|
||||
return new JumpPowerbarController(meter, snapshot, setWindowVisible);
|
||||
}
|
||||
|
||||
/// <summary>Applies the latest movement-owned charge snapshot.</summary>
|
||||
public void Tick() => SyncVisibility(force: false);
|
||||
|
||||
/// <summary>
|
||||
/// Re-applies the state-driven visibility after the window has been mounted.
|
||||
/// This covers a jump key already held during retained-UI construction.
|
||||
/// </summary>
|
||||
public void SyncVisibility() => SyncVisibility(force: true);
|
||||
|
||||
private void SyncVisibility(bool force)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
JumpChargeSnapshot state = _snapshot();
|
||||
_power = state.IsCharging ? Math.Clamp(state.Power, 0f, 1f) : 0f;
|
||||
|
||||
if (force || !_hasSynced || state.IsCharging != _isCharging)
|
||||
{
|
||||
_isCharging = state.IsCharging;
|
||||
_hasSynced = true;
|
||||
_setWindowVisible(_isCharging);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnShown() => SyncVisibility(force: false);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_power = 0f;
|
||||
_meter.Fill = () => null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue