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:
parent
9958458318
commit
2215c76c7e
22 changed files with 1265 additions and 46 deletions
|
|
@ -760,6 +760,7 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||
private AcDream.App.Combat.CombatAttackController? _combatAttackController;
|
||||
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
||||
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
|
||||
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
|
||||
|
|
@ -1979,11 +1980,19 @@ public sealed class GameWindow : IDisposable
|
|||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
|
||||
_samplerCache = new SamplerCache(_gl);
|
||||
|
||||
// Phase D.2b — retail-look UI (ACDREAM_RETAIL_UI=1). Wires the existing
|
||||
// UiHost retained-mode tree (dormant until now) + a first vitals panel.
|
||||
// Render-only: UiHost input is NOT yet bridged to the InputDispatcher
|
||||
// (next sub-phase), so the close button + window drag are inert. Coexists
|
||||
// with the ImGui devtools path (ACDREAM_DEVTOOLS=1), which is unchanged.
|
||||
// Retail ClientCombatSystem attack-request owner. This exists independently
|
||||
// of the retained UI so keyboard combat keeps the same press/hold/release
|
||||
// semantics even when the retail renderer is disabled.
|
||||
_combatAttackController = new AcDream.App.Combat.CombatAttackController(
|
||||
Combat,
|
||||
CanStartLiveCombatAttack,
|
||||
SendLiveCombatAttack,
|
||||
isDualWield: () => _playerController?.Motion.InterpretedState.CurrentStyle
|
||||
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
|
||||
playerReadyForAttack: IsPlayerReadyForCombatAttack,
|
||||
autoRepeatAttack: () => _persistedGameplay.AutoRepeatAttack);
|
||||
|
||||
// Phase D.2b retail-look retained UI (ACDREAM_RETAIL_UI=1).
|
||||
if (_options.RetailUi)
|
||||
{
|
||||
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
|
||||
|
|
@ -2116,6 +2125,9 @@ public sealed class GameWindow : IDisposable
|
|||
radarSnapshotProvider.BuildSnapshot,
|
||||
_selection,
|
||||
SetRetailUiLocked),
|
||||
Combat: new AcDream.App.UI.CombatRuntimeBindings(
|
||||
Combat,
|
||||
_combatAttackController),
|
||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||
Objects,
|
||||
() => Shortcuts,
|
||||
|
|
@ -8194,6 +8206,7 @@ public sealed class GameWindow : IDisposable
|
|||
// re-fire while their chord is held. K.1b adds the subscribers
|
||||
// that actually consume the events.
|
||||
_inputDispatcher?.Tick();
|
||||
_combatAttackController?.Tick();
|
||||
|
||||
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
|
||||
// on selection, then revert). No-op when nothing is flashing.
|
||||
|
|
@ -11730,6 +11743,12 @@ public sealed class GameWindow : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
// Retail attack actions consume their full transition stream: key-down
|
||||
// starts the power build and key-up commits it. Handle them before the
|
||||
// generic Press-only gate below.
|
||||
if (_combatAttackController?.HandleInputAction(action, activation) == true)
|
||||
return;
|
||||
|
||||
// Every other action fires on Press only (no Release / Hold side-
|
||||
// effects in the K.1b set). Filter out non-Press activations early
|
||||
// so subscribers that have Release-mode bindings don't accidentally
|
||||
|
|
@ -11853,18 +11872,6 @@ public sealed class GameWindow : IDisposable
|
|||
ToggleLiveCombatMode();
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatLowAttack:
|
||||
SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction.Low);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatMediumAttack:
|
||||
SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction.Medium);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatHighAttack:
|
||||
SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction.High);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.SelectLeft:
|
||||
PickAndStoreSelection(useImmediately: false);
|
||||
break;
|
||||
|
|
@ -11928,17 +11935,28 @@ public sealed class GameWindow : IDisposable
|
|||
_debugVm?.AddToast(text);
|
||||
}
|
||||
|
||||
private void SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction action)
|
||||
private bool IsPlayerReadyForCombatAttack()
|
||||
{
|
||||
if (_playerController is null)
|
||||
return false;
|
||||
var motion = _playerController.Motion.InterpretedState;
|
||||
return AcDream.Core.Combat.CombatInputPlanner.PlayerInReadyPositionForAttack(
|
||||
Combat.CurrentMode,
|
||||
motion.CurrentStyle,
|
||||
motion.ForwardCommand);
|
||||
}
|
||||
|
||||
private bool CanStartLiveCombatAttack()
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (!AcDream.Core.Combat.CombatInputPlanner.SupportsTargetedAttack(Combat.CurrentMode))
|
||||
{
|
||||
_debugVm?.AddToast("Enter melee or missile combat first");
|
||||
Console.WriteLine("combat: attack ignored; not in melee/missile combat mode");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint? target = GetSelectedOrClosestCombatTarget();
|
||||
|
|
@ -11946,21 +11964,32 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_debugVm?.AddToast("No monster target");
|
||||
Console.WriteLine("combat: attack ignored; no creature target found");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var height = AcDream.Core.Combat.CombatInputPlanner.HeightFor(action);
|
||||
const float FullBar = 1.0f;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SendLiveCombatAttack(
|
||||
AcDream.Core.Combat.AttackHeight height,
|
||||
float power)
|
||||
{
|
||||
if (!CanStartLiveCombatAttack())
|
||||
return false;
|
||||
|
||||
uint target = _selection.SelectedObjectId!.Value;
|
||||
power = Math.Clamp(power, 0f, 1f);
|
||||
if (Combat.CurrentMode == AcDream.Core.Combat.CombatMode.Missile)
|
||||
{
|
||||
_liveSession.SendMissileAttack(target.Value, height, FullBar);
|
||||
Console.WriteLine($"combat: missile attack target=0x{target.Value:X8} height={height} accuracy={FullBar:F2}");
|
||||
_liveSession!.SendMissileAttack(target, height, power);
|
||||
Console.WriteLine($"combat: missile attack target=0x{target:X8} height={height} accuracy={power:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_liveSession.SendMeleeAttack(target.Value, height, FullBar);
|
||||
Console.WriteLine($"combat: melee attack target=0x{target.Value:X8} height={height} power={FullBar:F2}");
|
||||
_liveSession!.SendMeleeAttack(target, height, power);
|
||||
Console.WriteLine($"combat: melee attack target=0x{target:X8} height={height} power={power:F2}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private uint? GetSelectedOrClosestCombatTarget()
|
||||
|
|
@ -13556,6 +13585,8 @@ public sealed class GameWindow : IDisposable
|
|||
// UI resource is still alive. UiHost disposes controllers before its renderer.
|
||||
_retailUiRuntime?.Dispose();
|
||||
_retailUiRuntime = null;
|
||||
_combatAttackController?.Dispose();
|
||||
_combatAttackController = null;
|
||||
_uiHost = null;
|
||||
_itemInteractionController?.Dispose();
|
||||
_itemInteractionController = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue