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
|
|
@ -141,6 +141,33 @@ public class RetailChaseCameraTests
|
|||
Assert.Null(RetailChaseCamera.ComputeTrackedHeading(pivot, pivot));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DesiredPose_TrackedHeadingWithNoOrbit_PlacesBoomBehindTargetDirection()
|
||||
{
|
||||
var pivot = new Vector3(10f, 20f, 1.5f);
|
||||
|
||||
var (eye, forward) = RetailChaseCamera.ComputeDesiredPose(
|
||||
pivot, Vector3.UnitX, distance: 5f, pitch: 0f, viewerYawOffset: 0f);
|
||||
|
||||
Assert.Equal(new Vector3(5f, 20f, 1.5f), eye);
|
||||
Assert.Equal(Vector3.UnitX, forward);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DesiredPose_TrackedHeadingRetainsViewerOffsetOrbitAndLooksAtPivot()
|
||||
{
|
||||
var pivot = new Vector3(10f, 20f, 1.5f);
|
||||
|
||||
var (eye, forward) = RetailChaseCamera.ComputeDesiredPose(
|
||||
pivot, Vector3.UnitX, distance: 5f, pitch: 0f,
|
||||
viewerYawOffset: MathF.PI / 2f);
|
||||
|
||||
Assert.Equal(10f, eye.X, 5);
|
||||
Assert.Equal(15f, eye.Y, 5);
|
||||
Assert.Equal(1.5f, eye.Z, 5);
|
||||
Assert.Equal(Vector3.Normalize(pivot - eye), forward);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Basis_HorizontalHeading_IsOrthonormalAndRightHanded()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public sealed class CombatLayoutConformanceTests
|
|||
Assert.Equal(0x060074CAu, power.TrackSprite);
|
||||
Assert.Equal(0x06001923u, power.ThumbSprite);
|
||||
Assert.Equal(0x06001200u, power.ScalarFillSprite);
|
||||
Assert.True(power.ScalarFillFromRight);
|
||||
|
||||
var repeat = Assert.IsType<UiButton>(
|
||||
layout.FindElement(CombatUiController.RepeatAttacksId));
|
||||
|
|
|
|||
|
|
@ -490,6 +490,7 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(0x06001919u, bar.TrackSprite);
|
||||
Assert.Equal(0x06001923u, bar.ThumbSprite);
|
||||
Assert.Equal(0x06001200u, bar.ScalarFillSprite);
|
||||
Assert.True(bar.ScalarFillFromRight);
|
||||
}
|
||||
|
||||
private static ElementInfo TextInfo(params (uint Id, UiPropertyValue Value)[] properties)
|
||||
|
|
|
|||
|
|
@ -102,4 +102,16 @@ public class UiScrollbarTests
|
|||
|
||||
Assert.True(bar.OnEvent(new UiEvent(0u, bar, UiEventType.MouseUp, Data1: 45)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 100f, 0f)]
|
||||
[InlineData(0.5f, 50f, 50f)]
|
||||
[InlineData(1f, 0f, 100f)]
|
||||
public void ScalarFillRect_CombatPower_GrowsRightToLeft(
|
||||
float fill, float expectedX, float expectedWidth)
|
||||
{
|
||||
var (x, width) = UiScrollbar.ScalarFillRect(100f, fill, fromRight: true);
|
||||
Assert.Equal(expectedX, x, 3);
|
||||
Assert.Equal(expectedWidth, width, 3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,23 +91,20 @@ public sealed class CombatChatTranslatorTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void AttackDone_NonZeroError_EmitsErrorLine()
|
||||
public void AttackDone_NonZeroControlStatus_EmitsNothing()
|
||||
{
|
||||
var (chat, combat, _) = Setup();
|
||||
combat.OnAttackDone(attackSequence: 7, weenieError: 0x1234u);
|
||||
combat.OnAttackDone(attackSequence: 7, weenieError: 0x0036u);
|
||||
|
||||
var entry = Assert.Single(chat.Snapshot());
|
||||
Assert.Equal(ChatKind.Combat, entry.Kind);
|
||||
Assert.Equal(CombatLineKind.Error, entry.CombatKind);
|
||||
Assert.Contains("Attack sequence finished with", entry.Text);
|
||||
Assert.Contains("0x1234", entry.Text);
|
||||
// Retail HandleAttackDoneEvent consumes this only as control state.
|
||||
// ACE deliberately uses ActionCancelled to reset the meter and sends
|
||||
// a separate WeenieError event for any player-visible failure.
|
||||
Assert.Empty(chat.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttackDone_ZeroError_EmitsNothing()
|
||||
{
|
||||
// Holtburger silently logs a debug-tag line; we omit since chat
|
||||
// panel doesn't surface debug entries.
|
||||
var (chat, combat, _) = Setup();
|
||||
combat.OnAttackDone(attackSequence: 7, weenieError: 0u);
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ public sealed class WeenieErrorMessagesTests
|
|||
WeenieErrorMessages.Format(0x050F, null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0036u, "Action cancelled!")]
|
||||
[InlineData(0x003Du, "You charged too far!")]
|
||||
public void Format_CombatMovementErrors(uint code, string expected)
|
||||
=> Assert.Equal(expected, WeenieErrorMessages.Format(code, null));
|
||||
|
||||
// ── unknown codes — graceful fallback preserves debug info ───────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue