feat(combat): port retail basic combat bar

Mount authored gmCombatUI, share one press/hold/release request state machine across DAT buttons and keybindings, and recover the exact 1.0s/0.8s power timing from matching retail x86. The same timer fixes jump charge, while ready-stance, response queueing, auto-repeat, layout binding, migration, and conformance coverage keep behavior architectural rather than panel-local.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 14:03:28 +02:00
parent 9958458318
commit 2215c76c7e
22 changed files with 1265 additions and 46 deletions

View file

@ -26,7 +26,7 @@ namespace AcDream.UI.Abstractions.Input;
/// </summary>
public sealed class KeyBindings
{
private const int CurrentSchemaVersion = 2;
private const int CurrentSchemaVersion = 3;
private readonly List<Binding> _bindings = new();
@ -244,9 +244,12 @@ public sealed class KeyBindings
// Melee mode (active when MeleeCombat scope pushed).
b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseAttackPower));
b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseAttackPower));
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatLowAttack));
b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatMediumAttack));
b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatHighAttack));
// Retail HandleCombatAction consumes key-down AND key-up for attack
// height actions: down starts charging, up ends the request. Hold is
// our binding representation for that transition pair.
b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatLowAttack, ActivationType.Hold));
b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatMediumAttack, ActivationType.Hold));
b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatHighAttack, ActivationType.Hold));
// Missile + Magic + Spell-tab — same chords; resolved by scope at
// runtime per InputDispatcher's stack lookup. Add the bindings;
// subscribers arrive in Phase L when CombatState.CurrentMode is
@ -420,6 +423,7 @@ public sealed class KeyBindings
}
var chord = new KeyChord(silkKey, mods, device);
action = MigrateLegacyQuickSlotIntent(version, action, chord, activation);
activation = MigrateCombatAttackActivation(version, action, activation);
loaded.Add(new(chord, action, activation));
}
}
@ -516,6 +520,25 @@ public sealed class KeyBindings
: action;
}
/// <summary>
/// Schema v2 stored the three attack-height commands as one-shot Press
/// bindings. Retail requires both press and release, so old and customized
/// chords retain their key while upgrading to Hold transition semantics.
/// </summary>
private static ActivationType MigrateCombatAttackActivation(
int version,
InputAction action,
ActivationType activation)
{
if (version >= 3 || activation != ActivationType.Press)
return activation;
return action is InputAction.CombatLowAttack
or InputAction.CombatMediumAttack
or InputAction.CombatHighAttack
? ActivationType.Hold
: activation;
}
private static ModifierMask ParseModifiers(JsonElement bindingEl)
{
if (!bindingEl.TryGetProperty("mod", out var modEl)) return ModifierMask.None;