feat(combat): Phase L.1c wire live attack input
This commit is contained in:
parent
d1fb68f419
commit
4874d8595a
6 changed files with 367 additions and 11 deletions
|
|
@ -522,6 +522,11 @@ public sealed class GameWindow : IDisposable
|
|||
/// keys the render list; this parallel dictionary keys by server guid.
|
||||
/// </summary>
|
||||
private readonly Dictionary<uint, AcDream.Core.World.WorldEntity> _entitiesByServerGuid = new();
|
||||
private readonly Dictionary<uint, LiveEntityInfo> _liveEntityInfoByGuid = new();
|
||||
private uint? _selectedTargetGuid;
|
||||
private readonly record struct LiveEntityInfo(
|
||||
string? Name,
|
||||
AcDream.Core.Items.ItemType ItemType);
|
||||
private int _liveSpawnReceived; // diagnostics
|
||||
private int _liveSpawnHydrated;
|
||||
private int _liveDropReasonNoPos;
|
||||
|
|
@ -1662,6 +1667,9 @@ public sealed class GameWindow : IDisposable
|
|||
// UpdatePosition look like a 2m-residual soft-snap.
|
||||
_remoteDeadReckon.Remove(spawn.Guid);
|
||||
_remoteLastMove.Remove(spawn.Guid);
|
||||
_liveEntityInfoByGuid.Remove(spawn.Guid);
|
||||
if (_selectedTargetGuid == spawn.Guid)
|
||||
_selectedTargetGuid = null;
|
||||
}
|
||||
|
||||
// Log every spawn that arrives so we can inventory what the server
|
||||
|
|
@ -1674,12 +1682,19 @@ public sealed class GameWindow : IDisposable
|
|||
: "no-pos";
|
||||
string setupStr = spawn.SetupTableId is { } su ? $"0x{su:X8}" : "no-setup";
|
||||
string nameStr = spawn.Name is { Length: > 0 } n ? $"\"{n}\"" : "no-name";
|
||||
string itemTypeStr = spawn.ItemType is { } it ? $"0x{it:X8}" : "no-itemtype";
|
||||
int animPartCount = spawn.AnimPartChanges?.Count ?? 0;
|
||||
int texChangeCount = spawn.TextureChanges?.Count ?? 0;
|
||||
int subPalCount = spawn.SubPalettes?.Count ?? 0;
|
||||
Console.WriteLine(
|
||||
$"live: spawn guid=0x{spawn.Guid:X8} name={nameStr} setup={setupStr} pos={posStr} " +
|
||||
$"animParts={animPartCount} texChanges={texChangeCount} subPalettes={subPalCount}");
|
||||
$"itemType={itemTypeStr} animParts={animPartCount} texChanges={texChangeCount} subPalettes={subPalCount}");
|
||||
|
||||
_liveEntityInfoByGuid[spawn.Guid] = new LiveEntityInfo(
|
||||
spawn.Name,
|
||||
spawn.ItemType is { } rawItemType
|
||||
? (AcDream.Core.Items.ItemType)rawItemType
|
||||
: AcDream.Core.Items.ItemType.None);
|
||||
|
||||
// Target the statue specifically for full diagnostic dump: Name match
|
||||
// is cheap and gives us exactly one entity's worth of log regardless
|
||||
|
|
@ -5915,6 +5930,26 @@ public sealed class GameWindow : IDisposable
|
|||
_settingsPanel.IsVisible = !_settingsPanel.IsVisible;
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.SelectionClosestMonster:
|
||||
SelectClosestCombatTarget(showToast: true);
|
||||
break;
|
||||
|
||||
case AcDream.UI.Abstractions.Input.InputAction.CombatToggleCombat:
|
||||
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.EscapeKey:
|
||||
if (_cameraController?.IsFlyMode == true)
|
||||
_cameraController.ToggleFly(); // exit fly, release cursor
|
||||
|
|
@ -5932,6 +5967,123 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void ToggleLiveCombatMode()
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
return;
|
||||
|
||||
var nextMode = AcDream.Core.Combat.CombatInputPlanner.ToggleMode(Combat.CurrentMode);
|
||||
_liveSession.SendChangeCombatMode(nextMode);
|
||||
Combat.SetCombatMode(nextMode);
|
||||
string text = $"Combat mode {nextMode}";
|
||||
Console.WriteLine($"combat: {text}");
|
||||
_debugVm?.AddToast(text);
|
||||
}
|
||||
|
||||
private void SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction action)
|
||||
{
|
||||
if (_liveSession is null
|
||||
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
uint? target = GetSelectedOrClosestCombatTarget();
|
||||
if (target is null)
|
||||
{
|
||||
_debugVm?.AddToast("No monster target");
|
||||
Console.WriteLine("combat: attack ignored; no creature target found");
|
||||
return;
|
||||
}
|
||||
|
||||
var height = AcDream.Core.Combat.CombatInputPlanner.HeightFor(action);
|
||||
const float FullBar = 1.0f;
|
||||
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}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_liveSession.SendMeleeAttack(target.Value, height, FullBar);
|
||||
Console.WriteLine($"combat: melee attack target=0x{target.Value:X8} height={height} power={FullBar:F2}");
|
||||
}
|
||||
}
|
||||
|
||||
private uint? GetSelectedOrClosestCombatTarget()
|
||||
{
|
||||
if (_selectedTargetGuid is { } selected && IsLiveCreatureTarget(selected))
|
||||
return selected;
|
||||
|
||||
return SelectClosestCombatTarget(showToast: false);
|
||||
}
|
||||
|
||||
private uint? SelectClosestCombatTarget(bool showToast)
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity))
|
||||
return null;
|
||||
|
||||
uint? bestGuid = null;
|
||||
float bestDistanceSq = float.PositiveInfinity;
|
||||
foreach (var (guid, entity) in _entitiesByServerGuid)
|
||||
{
|
||||
if (!IsLiveCreatureTarget(guid))
|
||||
continue;
|
||||
|
||||
float distanceSq = System.Numerics.Vector3.DistanceSquared(
|
||||
entity.Position,
|
||||
playerEntity.Position);
|
||||
if (distanceSq >= bestDistanceSq)
|
||||
continue;
|
||||
|
||||
bestDistanceSq = distanceSq;
|
||||
bestGuid = guid;
|
||||
}
|
||||
|
||||
_selectedTargetGuid = bestGuid;
|
||||
if (bestGuid is { } selected)
|
||||
{
|
||||
string label = DescribeLiveEntity(selected);
|
||||
float distance = MathF.Sqrt(bestDistanceSq);
|
||||
Console.WriteLine($"combat: selected target 0x{selected:X8} {label} dist={distance:F1}");
|
||||
if (showToast)
|
||||
_debugVm?.AddToast($"Target {label}");
|
||||
}
|
||||
else if (showToast)
|
||||
{
|
||||
_debugVm?.AddToast("No monster target");
|
||||
Console.WriteLine("combat: no creature target found");
|
||||
}
|
||||
|
||||
return bestGuid;
|
||||
}
|
||||
|
||||
private bool IsLiveCreatureTarget(uint guid)
|
||||
{
|
||||
if (guid == _playerServerGuid)
|
||||
return false;
|
||||
if (!_entitiesByServerGuid.ContainsKey(guid))
|
||||
return false;
|
||||
if (!_liveEntityInfoByGuid.TryGetValue(guid, out var info))
|
||||
return false;
|
||||
|
||||
return (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0;
|
||||
}
|
||||
|
||||
private string DescribeLiveEntity(uint guid)
|
||||
{
|
||||
if (_liveEntityInfoByGuid.TryGetValue(guid, out var info)
|
||||
&& !string.IsNullOrWhiteSpace(info.Name))
|
||||
return info.Name!;
|
||||
return $"0x{guid:X8}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// K.1b: Tab handler extracted into a method so the dispatcher
|
||||
/// subscriber can call it. Same body as the previous Tab branch in
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue