fix(vt): round 3 item 5 — jump-charge ceiling moves from storage load to execution
Retail's real 2000 ms jump-charge ceiling (refs/vtank/decompiled/bi.cs:502-505, bi.a) is enforced at the moment a jump STARTS charging, not by the storage format — metaf's own NJump (metaf_monolithic.py:11708-11820) and VTank's .nav both round-trip the authored value unclamped. The prior slice-1 port misread this as a LOAD-time clamp: MetafSerializer's .af "jmp" parser and the legacy-JSON route migration both clamped JumpChargeMilliseconds on read, silently rewriting an authored 5000 ms waypoint down to 2000 ms even when the route is never executed. Removed both load-time clamps; NavigationController.TickJump now clamps the EFFECTIVE charge duration (Math.Clamp(..., 0, 2000)) only at the one place retail actually enforces it — the charge-hold comparison during execution — leaving the stored/authored value untouched. Renamed MetafSerializerTests.JumpNodeClampsChargeMillisecondsTo2000 to JumpNodeLoadPreservesAuthoredChargeMillisecondsAboveRetailCeiling (now asserts the 5000 ms value survives the .af load) and added a save+load round-trip test and a Navigation execution test asserting the jump releases at ~2000 ms of in-game charging despite a 5000 ms authored value. Mutation: reverted MetafSerializer.cs/MossTankRouteProfileStore.cs/ Navigation.cs to HEAD (keeping only the new/changed tests) and ran the three new/renamed tests — all three failed (load clamped to 2000, execution never released before 5000 ms) — confirming they exercise the bug before the fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
0398913538
commit
7aee57fa22
5 changed files with 110 additions and 23 deletions
|
|
@ -498,8 +498,18 @@ public sealed class MetafSerializerTests
|
|||
Assert.Equal("Some Monster", nav.FollowTargetName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round 3 item 5: the retail 2000 ms jump-charge ceiling
|
||||
/// (refs/vtank/decompiled/bi.cs:502-505) is EXECUTION-time behavior
|
||||
/// (NavigationController.TickJump), not a storage-format limit — metaf
|
||||
/// and VTank both round-trip the authored value verbatim
|
||||
/// (metaf_monolithic.py:11708-11820, NJump — no clamp at all). An
|
||||
/// authored 5000 ms waypoint must therefore survive an .af load
|
||||
/// unchanged; renamed from JumpNodeClampsChargeMillisecondsTo2000,
|
||||
/// which pinned the pre-fix (wrong) load-time clamp.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void JumpNodeClampsChargeMillisecondsTo2000()
|
||||
public void JumpNodeLoadPreservesAuthoredChargeMillisecondsAboveRetailCeiling()
|
||||
{
|
||||
const string af = """
|
||||
NAV: j once
|
||||
|
|
@ -509,7 +519,7 @@ public sealed class MetafSerializerTests
|
|||
Assert.True(MetafSerializer.TryLoadNav(af, target, NoOpSpellCatalog.Instance, out string error), error);
|
||||
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||
Assert.Equal(RouteWaypointType.Jump, waypoint.Type);
|
||||
Assert.Equal(2000, waypoint.JumpChargeMilliseconds);
|
||||
Assert.Equal(5000, waypoint.JumpChargeMilliseconds);
|
||||
Assert.True(waypoint.JumpRun);
|
||||
Assert.Equal(90f, waypoint.JumpHeadingDegrees);
|
||||
}
|
||||
|
|
@ -528,6 +538,31 @@ public sealed class MetafSerializerTests
|
|||
Assert.False(waypoint.JumpRun);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round 3 item 5 companion: SaveNav must round-trip the same
|
||||
/// above-ceiling value back out unchanged (the save side never had a
|
||||
/// clamp; this pins that save+load together preserve it).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void JumpNodeSaveThenLoadRoundTripsChargeMillisecondsAboveRetailCeiling()
|
||||
{
|
||||
var source = new NavigationSettings { Mode = RouteMode.Once };
|
||||
source.Waypoints.Add(new RouteWaypoint
|
||||
{
|
||||
Type = RouteWaypointType.Jump,
|
||||
JumpHeadingDegrees = 90f,
|
||||
JumpRun = true,
|
||||
JumpChargeMilliseconds = 5000,
|
||||
});
|
||||
|
||||
string af = MetafSerializer.SaveNav(source);
|
||||
|
||||
var target = new NavigationSettings();
|
||||
Assert.True(MetafSerializer.TryLoadNav(af, target, NoOpSpellCatalog.Instance, out string error), error);
|
||||
RouteWaypoint waypoint = Assert.Single(target.Waypoints);
|
||||
Assert.Equal(5000, waypoint.JumpChargeMilliseconds);
|
||||
}
|
||||
|
||||
private static void AssertProfilesEqual(MetaProfile expected, MetaProfile actual)
|
||||
{
|
||||
Assert.Equal(expected.Rules.Count, actual.Rules.Count);
|
||||
|
|
|
|||
|
|
@ -379,6 +379,37 @@ public sealed class NavigationTests
|
|||
Assert.False(controller.Tick(0.01d, canAct: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round 3 item 5: retail's real jump-charge ceiling
|
||||
/// (refs/vtank/decompiled/bi.cs:502-505) applies at EXECUTION, not at
|
||||
/// storage load — an authored 5000 ms waypoint (which now survives an
|
||||
/// .af/legacy-JSON load unclamped) must still release the jump after
|
||||
/// at most 2000 ms of in-game charging, not wait for the full 5000.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void JumpChargeExecutionClampsAtRetailTwoThousandMillisecondCeiling()
|
||||
{
|
||||
var automation = new FakeAutomation
|
||||
{
|
||||
// Already aligned to the jump heading so the very first tick
|
||||
// starts charging immediately (no turn phase to account for).
|
||||
NavigationSnapshot = Snapshot(Position(0d, 0d, heading: 90f)),
|
||||
};
|
||||
RouteWaypoint jump = Waypoint(RouteWaypointType.Jump, Position(0d, 0d));
|
||||
jump.JumpHeadingDegrees = 90f;
|
||||
jump.JumpChargeMilliseconds = 5000;
|
||||
NavigationController controller = Controller(automation, RouteMode.Once, jump);
|
||||
|
||||
// 1.9 s elapsed < the 2000 ms ceiling: still charging.
|
||||
Assert.True(controller.Tick(1.9d, canAct: true));
|
||||
Assert.True(automation.Intents[^1].Jump);
|
||||
|
||||
// +0.2 s -> 2.1 s total, past the 2000 ms ceiling even though the
|
||||
// AUTHORED 5000 ms charge has not elapsed: released.
|
||||
Assert.True(controller.Tick(0.2d, canAct: true));
|
||||
Assert.False(automation.Intents[^1].Jump);
|
||||
}
|
||||
|
||||
// Campaign VT slice 1 Part A round 2: the route (.af) file now carries
|
||||
// ONLY Mode/Waypoints/FollowTarget — Enabled/Priority/
|
||||
// MinimumDistanceMeters/FollowAroundCorners/OpenDoors/Door* are real
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue