using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.App.Tests.Rendering;
///
/// Hand-built-fixture tests for — no dat
/// access needed, since a can be
/// constructed entirely in memory. Installed-DAT coverage for the RESOLUTION
/// half (ChargenPreviewEntityBuilder.TryBuildAnimated actually finding
/// the idle DID against real dat data) lives in
/// ChargenPreviewEntityBuilderTests.
///
public sealed class ChargenPreviewAnimatorTests
{
private static Animation MakeTwoFrameAnim(Vector3 frame0Origin, Vector3 frame1Origin)
{
var anim = new Animation();
var pf0 = new AnimationFrame(1);
pf0.Frames.Add(new Frame { Origin = frame0Origin, Orientation = Quaternion.Identity });
var pf1 = new AnimationFrame(1);
pf1.Frames.Add(new Frame { Origin = frame1Origin, Orientation = Quaternion.Identity });
anim.PartFrames.Add(pf0);
anim.PartFrames.Add(pf1);
return anim;
}
private static ChargenPreviewAnimatedBuild MakeBuild(Animation? idleAnimation, int idleLow = 0, int idleHigh = 1)
{
const uint gfxObjId = 0x0100_0001u;
var restMeshRefs = new List
{
new(gfxObjId, Matrix4x4.CreateTranslation(new Vector3(99f, 99f, 99f))), // distinct from any idle frame, so tests can tell them apart.
};
var drawableParts = new List
{
new(SetupPartIndex: 0, GfxObjId: gfxObjId, DefaultScale: Vector3.One, SurfaceOverrides: null),
};
var entity = new WorldEntity
{
Id = ChargenPreviewEntityBuilder.PreviewRenderId,
ServerGuid = ChargenPreviewEntityBuilder.PreviewServerGuid,
SourceGfxObjOrSetupId = 0x0200_0001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = restMeshRefs,
};
return new ChargenPreviewAnimatedBuild
{
Entity = entity,
DrawableParts = drawableParts,
RestMeshRefs = restMeshRefs,
IdleAnimation = idleAnimation,
IdleLowFrame = idleLow,
IdleHighFrame = idleHigh,
};
}
[Fact]
public void Constructor_WithIdleAnimation_SeedsFrameZeroPose_NotTheRestPose()
{
// Retail's true default is the idle loop PLAYING, not the rest pose
// — see ChargenPreviewEntityBuilder's class doc.
var origin0 = new Vector3(1f, 0f, 0f);
var origin1 = new Vector3(5f, 0f, 0f);
var build = MakeBuild(MakeTwoFrameAnim(origin0, origin1));
var animator = new ChargenPreviewAnimator(build);
Assert.False(animator.IsZoomedIn);
Assert.Equal(origin0, animator.Entity.MeshRefs[0].PartTransform.Translation);
}
[Fact]
public void Constructor_WithNoIdleAnimation_KeepsTheRestPoseFallback()
{
var build = MakeBuild(idleAnimation: null);
var animator = new ChargenPreviewAnimator(build);
Assert.Equal(new Vector3(99f, 99f, 99f), animator.Entity.MeshRefs[0].PartTransform.Translation);
}
[Fact]
public void Tick_AdvancesTheIdleFrame_InterpolatingBetweenFrames()
{
var origin0 = new Vector3(0f, 0f, 0f);
var origin1 = new Vector3(10f, 0f, 0f);
var build = MakeBuild(MakeTwoFrameAnim(origin0, origin1));
var animator = new ChargenPreviewAnimator(build);
// 30fps, half a frame's worth of elapsed time -> currFrame 0.5, lerp halfway.
animator.Tick(1f / 60f);
Assert.Equal(5f, animator.Entity.MeshRefs[0].PartTransform.Translation.X, 3);
}
[Fact]
public void SetZoomedIn_True_SwapsToTheFrozenRestPoseImmediately()
{
var build = MakeBuild(MakeTwoFrameAnim(new Vector3(1f, 0f, 0f), new Vector3(5f, 0f, 0f)));
var animator = new ChargenPreviewAnimator(build);
animator.SetZoomedIn(true);
Assert.True(animator.IsZoomedIn);
Assert.Equal(new Vector3(99f, 99f, 99f), animator.Entity.MeshRefs[0].PartTransform.Translation);
}
[Fact]
public void Tick_WhileZoomedIn_DoesNotAdvanceTheFrozenPose()
{
var build = MakeBuild(MakeTwoFrameAnim(new Vector3(1f, 0f, 0f), new Vector3(5f, 0f, 0f)));
var animator = new ChargenPreviewAnimator(build);
animator.SetZoomedIn(true);
animator.Tick(10f); // large elapsed time — must still be a no-op while zoomed in.
Assert.Equal(new Vector3(99f, 99f, 99f), animator.Entity.MeshRefs[0].PartTransform.Translation);
}
[Fact]
public void SetZoomedIn_False_RestartsTheIdleLoopAtFrameZero()
{
var origin0 = new Vector3(1f, 0f, 0f);
var origin1 = new Vector3(5f, 0f, 0f);
var build = MakeBuild(MakeTwoFrameAnim(origin0, origin1));
var animator = new ChargenPreviewAnimator(build);
animator.Tick(1f / 30f); // advance to frame 1.
animator.SetZoomedIn(true);
animator.SetZoomedIn(false); // gmCG3DView::StartAnimation restarts the clip (clear-then-append).
Assert.Equal(origin0, animator.Entity.MeshRefs[0].PartTransform.Translation);
}
[Fact]
public void SetZoomedIn_SameStateTwice_IsANoOp()
{
var build = MakeBuild(MakeTwoFrameAnim(new Vector3(1f, 0f, 0f), new Vector3(5f, 0f, 0f)));
var animator = new ChargenPreviewAnimator(build);
animator.Tick(1f / 60f); // partway through frame 0->1.
Vector3 beforeX = animator.Entity.MeshRefs[0].PartTransform.Translation;
animator.SetZoomedIn(false); // already not zoomed in — must not restart the loop.
Assert.Equal(beforeX, animator.Entity.MeshRefs[0].PartTransform.Translation);
}
}