using System;
namespace SceneTests.Components;
///
/// A camera effect contributing a fixed, deterministic offset - for testing the system's targeting,
/// falloff and scaling without the built-in effects' randomness.
///
public sealed class FixedOffsetEffect : CameraEffectSystem.BaseEffect
{
public Vector3 Offset = new( 0, 0, 10 );
public override void Evaluate( float scale, ref Vector3 position, ref Angles angles, ref float fieldOfView )
{
position += Offset * scale;
}
}
///
/// Pins the CameraEffectSystem contract: effect lifetimes, Stop and infinite effects, the global
/// Scale knob, per-camera targeting, epicenter falloff, and the deterministic envelope math of the
/// built-in punch and tilt.
///
[TestClass]
public class CameraEffectSystemTests
{
static CameraComponent CreateCamera( Scene scene, Vector3 position = default )
{
var go = scene.CreateObject();
go.WorldPosition = position;
return go.Components.Create();
}
///
/// Effects expire when their duration elapses, and Stop ends them early. A duration of zero runs
/// until stopped.
///
[TestMethod]
public void LifetimesAndStop()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var cam = CreateCamera( scene );
var timed = system.AddShake( cam, 4, 40, duration: 1f );
Assert.IsFalse( timed.IsDone );
timed.Update( 1.1f );
Assert.IsTrue( timed.IsDone, "Expired after its duration" );
var infinite = system.AddShake( cam, 4, 40, duration: 0f );
infinite.Update( 100f );
Assert.IsFalse( infinite.IsDone, "Infinite effects don't expire" );
infinite.Stop();
Assert.IsTrue( infinite.IsDone, "Stop ends an infinite effect" );
var stopped = system.AddShake( cam, 4, 40, duration: 10f );
stopped.Stop();
Assert.IsTrue( stopped.IsDone, "Stop ends a timed effect early" );
}
///
/// Scale zero silences every effect - the accessibility off-switch.
///
[TestMethod]
public void ScaleZeroSilences()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var cam = CreateCamera( scene );
system.Add( new FixedOffsetEffect { Camera = cam, Duration = 10f } );
system.Scale = 0f;
system.QueryOffsets( cam, out var position, out _, out _ );
Assert.AreEqual( Vector3.Zero, position );
system.Scale = 1f;
system.QueryOffsets( cam, out position, out _, out _ );
Assert.AreEqual( 10f, position.z, 0.001f );
system.Scale = 0.5f;
system.QueryOffsets( cam, out position, out _, out _ );
Assert.AreEqual( 5f, position.z, 0.001f, "Scale multiplies every effect" );
}
///
/// An effect targeting one camera contributes nothing to any other camera.
///
[TestMethod]
public void CameraTargeting()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var camA = CreateCamera( scene );
var camB = CreateCamera( scene );
system.Add( new FixedOffsetEffect { Camera = camA, Duration = 10f } );
system.QueryOffsets( camA, out var positionA, out _, out _ );
system.QueryOffsets( camB, out var positionB, out _, out _ );
Assert.AreEqual( 10f, positionA.z, 0.001f );
Assert.AreEqual( Vector3.Zero, positionB, "Targeted effects skip other cameras" );
}
///
/// Epicenter effects fall off linearly with camera distance - full at the epicenter, half way at
/// half the radius, nothing at and beyond the radius.
///
[TestMethod]
public void EpicenterFalloff()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var atCenter = CreateCamera( scene, Vector3.Zero );
var halfway = CreateCamera( scene, new Vector3( 256, 0, 0 ) );
var outside = CreateCamera( scene, new Vector3( 600, 0, 0 ) );
system.Add( new FixedOffsetEffect { Epicenter = Vector3.Zero, Radius = 512f, Duration = 10f } );
system.QueryOffsets( atCenter, out var position, out _, out _ );
Assert.AreEqual( 10f, position.z, 0.001f );
system.QueryOffsets( halfway, out position, out _, out _ );
Assert.AreEqual( 5f, position.z, 0.001f );
system.QueryOffsets( outside, out position, out _, out _ );
Assert.AreEqual( Vector3.Zero, position );
}
///
/// The punch envelope is deterministic - sin( t·3π·frequency ) · (1 - t). Half way through a
/// one-oscillation punch it sits at exactly half amplitude on the far side, and it returns to
/// zero at the end of its life.
///
[TestMethod]
public void PunchEnvelope()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var cam = CreateCamera( scene );
var punch = system.AddPunch( cam, Vector3.Up, amplitude: 8f, frequency: 1f, duration: 1f );
punch.Update( 0.5f );
system.QueryOffsets( cam, out var position, out _, out _ );
Assert.AreEqual( -4f, position.z, 0.01f, "sin(1.5π) · 0.5 · 8" );
punch.Update( 0.4999f );
system.QueryOffsets( cam, out position, out _, out _ );
Assert.AreEqual( 0f, position.z, 0.05f, "Lapses back to zero at the end" );
}
///
/// The tilt eases to exactly its full angle after the ease time and holds it. The angular punch
/// starts from zero.
///
[TestMethod]
public void TiltEase()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var cam = CreateCamera( scene );
var tilt = system.AddTilt( cam, new Angles( 5, 0, 15 ), duration: 100f, easeTime: 0.25f );
system.QueryOffsets( cam, out _, out var angles, out _ );
Assert.AreEqual( 0f, angles.roll, 0.001f, "Starts from zero" );
tilt.Update( 1f );
system.QueryOffsets( cam, out _, out angles, out _ );
Assert.AreEqual( 15f, angles.roll, 0.001f, "Held at full strength after easing in" );
Assert.AreEqual( 5f, angles.pitch, 0.001f );
}
///
/// The classic shake never throws the camera further than its amplitude, on any axis, at any
/// point in its life.
///
[TestMethod]
public void ShakeStaysWithinAmplitude()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var system = CameraEffectSystem.Get( scene );
var cam = CreateCamera( scene );
var amplitude = 4f;
var shake = system.AddShake( cam, amplitude, frequency: 40f, duration: 1f );
for ( int i = 0; i < 60; i++ )
{
shake.Update( 1f / 60f );
system.QueryOffsets( cam, out var position, out var angles, out _ );
Assert.IsTrue( MathF.Abs( position.x ) <= amplitude + 0.001f, $"x {position.x} exceeded amplitude" );
Assert.IsTrue( MathF.Abs( position.y ) <= amplitude + 0.001f, $"y {position.y} exceeded amplitude" );
Assert.IsTrue( MathF.Abs( position.z ) <= amplitude + 0.001f, $"z {position.z} exceeded amplitude" );
Assert.IsTrue( MathF.Abs( angles.roll ) <= amplitude * 0.25f + 0.001f, $"roll {angles.roll} exceeded amplitude/4" );
}
}
}