feat(selection): port retail polygon picking and vivid marker
Replace the projected Setup-sphere rectangle and independent physics-wall ray with retail's render-coupled picker: only visible server-object parts participate, each exact drawing sphere broad-phases the camera-eye ray, and first-in-DAT-order visual polygon hits globally outrank sphere fallbacks. Replace the devtools-only procedural triangles with the retained gameplay VividTargetIndicator using retail client-enum surfaces 1..4, radar-blip colorization, Setup selection-sphere framing, and the exact eight-pixel viewport clamp. Release build succeeds with zero warnings and all 5,886 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0f82a08f0a
commit
146a963aeb
26 changed files with 1302 additions and 1340 deletions
173
tests/AcDream.Core.Tests/Selection/RetailWorldPickerTests.cs
Normal file
173
tests/AcDream.Core.Tests/Selection/RetailWorldPickerTests.cs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.Core.Tests.Selection;
|
||||
|
||||
public sealed class RetailWorldPickerTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildRayStartsAtCameraViewpointRatherThanNearPlane()
|
||||
{
|
||||
var eye = new Vector3(10f, 20f, 30f);
|
||||
Matrix4x4 view = Matrix4x4.CreateLookAt(eye, eye - Vector3.UnitZ, Vector3.UnitY);
|
||||
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
MathF.PI / 3f, 4f / 3f, 0.1f, 1000f);
|
||||
|
||||
var ray = WorldPicker.BuildRay(400f, 300f, 800f, 600f, view, projection);
|
||||
|
||||
Assert.True(Vector3.Distance(eye, ray.Origin) < 0.0001f);
|
||||
Assert.True(Vector3.Distance(-Vector3.UnitZ, ray.Direction) < 0.0001f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PolygonHitGloballyBeatsNearerSphereFallback()
|
||||
{
|
||||
var sphereOnly = Part(1u, z: -3f, polygons: []);
|
||||
var polygon = Part(2u, z: -8f, polygons: [SquareAtLocalZ(0f)]);
|
||||
|
||||
RetailSelectionHit? hit = RetailWorldPicker.Pick(
|
||||
Vector3.Zero, -Vector3.UnitZ, [sphereOnly, polygon]);
|
||||
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(2u, hit.Value.ServerGuid);
|
||||
Assert.True(hit.Value.PolygonHit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SphereIsUsedOnlyWhenNoVisiblePolygonHits()
|
||||
{
|
||||
var part = Part(0xABCDu, z: -5f, polygons: []);
|
||||
|
||||
RetailSelectionHit? hit = RetailWorldPicker.Pick(
|
||||
Vector3.Zero, -Vector3.UnitZ, [part]);
|
||||
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(0xABCDu, hit.Value.ServerGuid);
|
||||
Assert.False(hit.Value.PolygonHit);
|
||||
Assert.Equal(3d, hit.Value.Distance, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopsAtFirstHitPolygonInDatOrderForEachPart()
|
||||
{
|
||||
var datOrdered = Part(
|
||||
10u,
|
||||
z: 0f,
|
||||
polygons: [SquareAtLocalZ(-10f), SquareAtLocalZ(-5f)],
|
||||
sphereCenter: new Vector3(0f, 0f, -7.5f),
|
||||
sphereRadius: 5f);
|
||||
var competitor = Part(20u, z: -7f, polygons: [SquareAtLocalZ(0f)]);
|
||||
|
||||
RetailSelectionHit? hit = RetailWorldPicker.Pick(
|
||||
Vector3.Zero, -Vector3.UnitZ, [datOrdered, competitor]);
|
||||
|
||||
// Scanning all polygons would incorrectly give guid 10 at 5 m. Retail
|
||||
// accepts its first DAT-order hit at 10 m, so guid 20 wins at 7 m.
|
||||
Assert.NotNull(hit);
|
||||
Assert.Equal(20u, hit.Value.ServerGuid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleSidedPolygonRejectsItsBackFace()
|
||||
{
|
||||
var front = new RetailSelectionPart(
|
||||
1u,
|
||||
0,
|
||||
Matrix4x4.Identity,
|
||||
new RetailSelectionMesh(
|
||||
new Vector3(0f, 0f, -5f), 2f,
|
||||
[SquareAtLocalZ(-5f, singleSided: true)]));
|
||||
|
||||
RetailSelectionHit? backHit = RetailWorldPicker.Pick(
|
||||
new Vector3(0f, 0f, -10f), Vector3.UnitZ, [front]);
|
||||
|
||||
Assert.NotNull(backHit); // drawing sphere remains the retail fallback
|
||||
Assert.False(backHit.Value.PolygonHit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InverseScaledPartPreservesWorldDistance()
|
||||
{
|
||||
var transform = Matrix4x4.CreateScale(2f)
|
||||
* Matrix4x4.CreateTranslation(0f, 0f, -10f);
|
||||
var part = new RetailSelectionPart(
|
||||
3u,
|
||||
0,
|
||||
transform,
|
||||
new RetailSelectionMesh(Vector3.Zero, 1f, [SquareAtLocalZ(0f)]));
|
||||
|
||||
RetailSelectionHit? hit = RetailWorldPicker.Pick(
|
||||
Vector3.Zero, -Vector3.UnitZ, [part]);
|
||||
|
||||
Assert.NotNull(hit);
|
||||
Assert.True(hit.Value.PolygonHit);
|
||||
Assert.Equal(10d, hit.Value.Distance, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipGuidAndNonServerPartsNeverCompete()
|
||||
{
|
||||
RetailSelectionHit? hit = RetailWorldPicker.Pick(
|
||||
Vector3.Zero,
|
||||
-Vector3.UnitZ,
|
||||
[Part(0u, -2f, [SquareAtLocalZ(0f)]), Part(7u, -4f, [SquareAtLocalZ(0f)])],
|
||||
skipServerGuid: 7u);
|
||||
|
||||
Assert.Null(hit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawingSphereRejectsRayWhichBeginsInsideLikeRetail()
|
||||
{
|
||||
bool hit = RetailWorldPicker.TryIntersectSphere(
|
||||
Vector3.Zero, Vector3.UnitX, Vector3.Zero, 2f, out _);
|
||||
|
||||
Assert.False(hit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvexEdgeTestRejectsAConcaveNotchRatherThanUsingEvenOddFill()
|
||||
{
|
||||
var concave = new RetailSelectionPolygon(
|
||||
[
|
||||
new(-2f, -2f, -5f),
|
||||
new( 2f, -2f, -5f),
|
||||
new( 2f, 2f, -5f),
|
||||
new( 0f, 0f, -5f),
|
||||
new(-2f, 2f, -5f),
|
||||
],
|
||||
SingleSided: false);
|
||||
|
||||
bool hit = RetailWorldPicker.TryIntersectPolygon(
|
||||
new Vector3(0f, 1f, 0f), -Vector3.UnitZ, concave, out _);
|
||||
|
||||
Assert.False(hit);
|
||||
}
|
||||
|
||||
private static RetailSelectionPart Part(
|
||||
uint guid,
|
||||
float z,
|
||||
IReadOnlyList<RetailSelectionPolygon> polygons,
|
||||
Vector3? sphereCenter = null,
|
||||
float sphereRadius = 2f)
|
||||
=> new(
|
||||
guid,
|
||||
0,
|
||||
Matrix4x4.CreateTranslation(0f, 0f, z),
|
||||
new RetailSelectionMesh(
|
||||
sphereCenter ?? Vector3.Zero,
|
||||
sphereRadius,
|
||||
polygons));
|
||||
|
||||
private static RetailSelectionPolygon SquareAtLocalZ(
|
||||
float z,
|
||||
bool singleSided = false)
|
||||
=> new(
|
||||
[
|
||||
new Vector3(-1f, -1f, z),
|
||||
new Vector3( 1f, -1f, z),
|
||||
new Vector3( 1f, 1f, z),
|
||||
new Vector3(-1f, 1f, z),
|
||||
],
|
||||
singleSided);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue