fix(combat): match retail live attack feedback
Keep AttackDone control statuses out of chat, preserve dead motion across zero-velocity position updates, render the authored power meter from right to left, and retain the rotatable viewer offset during target tracking. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
33cc9aa16a
commit
5276a83087
16 changed files with 231 additions and 67 deletions
|
|
@ -5854,23 +5854,11 @@ public sealed class GameWindow : IDisposable
|
|||
if (update.Velocity is { } svel)
|
||||
{
|
||||
rmState.Body.Velocity = svel;
|
||||
// Only use the < 0.2 m/s stop signal when velocity was
|
||||
// explicitly provided (i.e. server sent HasVelocity + tiny
|
||||
// value = "I'm definitely stopped"). Absent velocity field
|
||||
// carries no stop information for our ACE.
|
||||
if (svel.LengthSquared() < 0.04f)
|
||||
{
|
||||
rmState.Motion.StopCompletely();
|
||||
if (_animatedEntities.TryGetValue(entity.Id, out var aeForStop)
|
||||
&& aeForStop.Sequencer is not null)
|
||||
{
|
||||
uint curStyle = aeForStop.Sequencer.CurrentStyle;
|
||||
uint readyCmd = (curStyle & 0xFF000000u) != 0
|
||||
? ((curStyle & 0xFF000000u) | 0x01000003u)
|
||||
: 0x41000003u;
|
||||
aeForStop.Sequencer.SetCycle(curStyle, readyCmd);
|
||||
}
|
||||
}
|
||||
// Position updates carry physics velocity only. Retail
|
||||
// CPhysicsObj::MoveOrTeleport never derives an animation
|
||||
// command from it; UpdateMotion owns the interpreted state.
|
||||
// In particular, a dead creature legitimately broadcasts
|
||||
// zero velocity while its Dead cycle must remain persistent.
|
||||
}
|
||||
else if (!IsPlayerGuid(update.Guid) && rmState.HasServerVelocity
|
||||
&& !snapSuppressedByStick && rmState.Airborne)
|
||||
|
|
|
|||
|
|
@ -156,7 +156,8 @@ public sealed class RetailChaseCamera : ICamera
|
|||
// CameraSet::TrackTarget 0x00458280 and CameraManager::UpdateCamera
|
||||
// 0x00456826-0x00456915.
|
||||
Vector3 pivotWorld = playerPosition + new Vector3(0f, 0f, PivotHeight);
|
||||
Vector3 heading = ComputeTrackedHeading(pivotWorld, trackedTargetPoint)
|
||||
Vector3? trackedHeading = ComputeTrackedHeading(pivotWorld, trackedTargetPoint);
|
||||
Vector3 heading = trackedHeading
|
||||
?? ComputeHeading(
|
||||
avgVel,
|
||||
playerYaw + YawOffset,
|
||||
|
|
@ -164,15 +165,14 @@ public sealed class RetailChaseCamera : ICamera
|
|||
contactPlaneNormal,
|
||||
CameraDiagnostics.AlignToSlope);
|
||||
|
||||
// 3. Orthonormal heading-frame basis.
|
||||
var (forward, _, up) = BuildBasis(heading);
|
||||
|
||||
// 4. Target pose.
|
||||
float horizontal = Distance * MathF.Cos(Pitch);
|
||||
float vertical = Distance * MathF.Sin(Pitch);
|
||||
// viewer_offset = -horizontal along forward + vertical along up.
|
||||
Vector3 targetEye = pivotWorld + forward * (-horizontal) + up * vertical;
|
||||
Vector3 targetForward = Vector3.Normalize(pivotWorld - targetEye);
|
||||
// 3-4. Target pose. Retail TrackTarget changes the frame that
|
||||
// transforms viewer_offset; it does not replace that offset. CameraSet::Rotate
|
||||
// (0x00458310) therefore continues to orbit viewer_offset.x/y while the
|
||||
// target supplies the frame heading. Without this local rotation, enabling
|
||||
// Keep in View snaps the camera behind the target and disables RMB orbit.
|
||||
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
|
||||
(Vector3 targetEye, Vector3 targetForward) = ComputeDesiredPose(
|
||||
pivotWorld, heading, Distance, Pitch, viewerYawOffset);
|
||||
|
||||
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
|
||||
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
|
||||
|
|
@ -380,6 +380,35 @@ public sealed class RetailChaseCamera : ICamera
|
|||
: Vector3.Normalize(towardTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ports retail's heading-frame transform of <c>viewer_offset</c> in
|
||||
/// CameraManager::UpdateCamera (0x00456660), including the X/Y rotation
|
||||
/// retained by CameraSet::Rotate (0x00458310) while target tracking is active.
|
||||
/// The returned forward always looks back at the player pivot, matching
|
||||
/// LOOK_AT_PIVOT.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward) ComputeDesiredPose(
|
||||
Vector3 pivotWorld,
|
||||
Vector3 heading,
|
||||
float distance,
|
||||
float pitch,
|
||||
float viewerYawOffset)
|
||||
{
|
||||
var (frameForward, frameRight, frameUp) = BuildBasis(heading);
|
||||
|
||||
// AC's frame-local +Y points opposite our screen-right basis. Rotating
|
||||
// viewer_offset.x/y by +yaw therefore produces this boom direction.
|
||||
Vector3 boomForward = Vector3.Normalize(
|
||||
frameForward * MathF.Cos(viewerYawOffset)
|
||||
- frameRight * MathF.Sin(viewerYawOffset));
|
||||
|
||||
float horizontal = distance * MathF.Cos(pitch);
|
||||
float vertical = distance * MathF.Sin(pitch);
|
||||
Vector3 eye = pivotWorld - boomForward * horizontal + frameUp * vertical;
|
||||
Vector3 forward = Vector3.Normalize(pivotWorld - eye);
|
||||
return (eye, forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build an orthonormal basis with <c>forward = heading</c>. World
|
||||
/// up is <c>(0, 0, 1)</c>; if <c>heading</c> is near-parallel to it
|
||||
|
|
|
|||
|
|
@ -177,6 +177,10 @@ public static class DatWidgetFactory
|
|||
.OrderByDescending(child => child.ReadOrder)
|
||||
.FirstOrDefault();
|
||||
bar.ScalarFillSprite = fill is null ? 0u : DefaultImage(fill);
|
||||
// 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;
|
||||
return bar;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ public sealed class UiScrollbar : UiElement
|
|||
/// </summary>
|
||||
public Func<float?> ScalarFill { get; set; } = () => null;
|
||||
public uint ScalarFillSprite { get; set; }
|
||||
/// <summary>
|
||||
/// Draw the scalar meter from the power end (right) toward the speed end
|
||||
/// (left). This is the authored gmCombatUI power meter direction.
|
||||
/// </summary>
|
||||
public bool ScalarFillFromRight { get; set; }
|
||||
|
||||
/// <summary>Programmatically set retail scrollbar attribute 0x86 without broadcasting.</summary>
|
||||
public void SetScalarPosition(float position)
|
||||
|
|
@ -105,6 +110,14 @@ public sealed class UiScrollbar : UiElement
|
|||
return (y, h);
|
||||
}
|
||||
|
||||
/// <summary>Returns the clipped scalar-meter span in local pixels.</summary>
|
||||
public static (float x, float width) ScalarFillRect(
|
||||
float totalWidth, float fill, bool fromRight)
|
||||
{
|
||||
float visibleWidth = totalWidth * Math.Clamp(fill, 0f, 1f);
|
||||
return (fromRight ? totalWidth - visibleWidth : 0f, visibleWidth);
|
||||
}
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (SpriteResolve is not { } resolve) return;
|
||||
|
|
@ -113,8 +126,12 @@ public sealed class UiScrollbar : UiElement
|
|||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
if (ScalarFill() is float fill && ScalarFillSprite != 0)
|
||||
{
|
||||
float visibleWidth = Width * Math.Clamp(fill, 0f, 1f);
|
||||
DrawTiled(ctx, resolve, ScalarFillSprite, 0f, 0f, visibleWidth, Height);
|
||||
var (fillX, visibleWidth) = ScalarFillRect(Width, fill, ScalarFillFromRight);
|
||||
if (ScalarFillFromRight)
|
||||
DrawTiledRightClipped(
|
||||
ctx, resolve, ScalarFillSprite, fillX, visibleWidth, Height);
|
||||
else
|
||||
DrawTiled(ctx, resolve, ScalarFillSprite, 0f, 0f, visibleWidth, Height);
|
||||
}
|
||||
float thumbWidth = ScalarThumbWidth(resolve);
|
||||
float travel = MathF.Max(0f, Width - thumbWidth);
|
||||
|
|
@ -178,6 +195,25 @@ public sealed class UiScrollbar : UiElement
|
|||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the rightmost portion of a full-width tiled scalar image. UVs
|
||||
/// start at the same offset they would have in the uncut image, matching
|
||||
/// retail meter clipping rather than restarting the wave at the clip edge.
|
||||
/// </summary>
|
||||
private void DrawTiledRightClipped(
|
||||
UiRenderContext ctx,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id,
|
||||
float x,
|
||||
float w,
|
||||
float h)
|
||||
{
|
||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
ctx.DrawSprite(tex, x, 0f, w, h, x / tw, 0f, (x + w) / tw, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
if (Horizontal && ScalarChanged is not null)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue