using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.GameContent;
using Terraria.Graphics;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only compositor for short, screen-space Reaper impacts. Gameplay remains
/// server authoritative; this class only combines camera impulses and capped flashes.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperVfxDirector : ModSystem
{
private const float MaximumShakeStrength = 12f;
private const float MaximumFlashOpacity = 0.78f;
private const float MaximumCinematicZoom = 1.11f;
private const float MinimumCinematicZoom = 0.985f;
private const float MaximumCinematicBias = 22f;
private static int shakeFrames;
private static int shakeDuration;
private static float shakeStrength;
private static Vector2 shakeDirection = Vector2.UnitY;
private static float shakePhase;
private static int flashFrames;
private static int flashDuration;
private static float flashOpacity;
private static Color flashColor = Color.White;
private static float edgeDarkness;
private static CinematicCameraState[] cinematicStates = [];
public override void OnWorldUnload() => Clear();
public override void Unload() => Clear();
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
Clear();
return;
}
if (shakeFrames > 0)
shakeFrames--;
else
shakeStrength = 0f;
if (flashFrames > 0)
flashFrames--;
else
{
flashOpacity = 0f;
edgeDarkness = 0f;
}
EnsureCinematicStorage();
for (int index = 0; index < cinematicStates.Length; index++)
{
ref CinematicCameraState state = ref cinematicStates[index];
if (state.Active && Main.GameUpdateCount > state.LastReportTick + 2UL)
state = default;
}
}
public override void ModifyTransformMatrix(ref SpriteViewMatrix transform)
{
if (Main.dedServ || Main.gameMenu)
return;
ComposeCinematicCamera(out float zoomScale, out _);
if (Math.Abs(zoomScale - 1f) <= 0.0001f)
return;
transform.Zoom *= zoomScale;
}
public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
{
int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
if (index < 0)
index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
if (index < 0)
index = layers.Count;
layers.Insert(index, new LegacyGameInterfaceLayer(
"SoulHarvest: Reaper Impact Composite",
DrawImpactOverlay,
InterfaceScaleType.None));
}
/// <summary>
/// Adds a global impact without distance falloff. Ultimate timelines use this so
/// every client which receives the action sees the same full-strength punctuation.
/// Concurrent calls use maxima instead of unbounded addition.
/// </summary>
public static void TriggerGlobalImpact(
Vector2 direction,
float strength,
int cameraFrames,
Color color,
float opacity,
int colorFrames,
float vignette = 0.28f)
{
if (Main.dedServ || Main.gameMenu)
return;
direction = direction.SafeNormalize(Vector2.UnitY);
strength = MathHelper.Clamp(strength, 0f, MaximumShakeStrength);
cameraFrames = Math.Clamp(cameraFrames, 0, 30);
opacity = MathHelper.Clamp(opacity, 0f, MaximumFlashOpacity);
colorFrames = Math.Clamp(colorFrames, 0, 30);
// Bright full-viewport overlays read as a white-screen defect when rapid
// melee or passive events overlap. Keep their camera punch and vignette,
// but suppress the bright color plane entirely.
Vector3 rgb = color.ToVector3();
float luminance = rgb.X * 0.2126f + rgb.Y * 0.7152f + rgb.Z * 0.0722f;
if (luminance >= 0.64f)
{
opacity = 0f;
colorFrames = 0;
}
if (strength >= shakeStrength || shakeFrames <= 1)
{
shakeStrength = strength;
shakeDirection = direction;
shakeDuration = Math.Max(1, cameraFrames);
shakeFrames = cameraFrames;
shakePhase += 1.731f;
}
else
{
shakeFrames = Math.Max(shakeFrames, cameraFrames);
shakeDuration = Math.Max(shakeDuration, shakeFrames);
shakeDirection = (shakeDirection + direction * 0.45f).SafeNormalize(shakeDirection);
}
// A camera-only impact must not create, extend or recolor an unrelated
// full-screen flash that is already active (for example an overlapping
// ultimate). Unique-skill cues intentionally pass zero here.
if (opacity > 0.001f && colorFrames > 0)
{
if (opacity >= flashOpacity || flashFrames <= 1)
{
flashColor = color;
flashOpacity = opacity;
flashDuration = Math.Max(1, colorFrames);
flashFrames = colorFrames;
}
else
{
flashFrames = Math.Max(flashFrames, colorFrames);
flashDuration = Math.Max(flashDuration, flashFrames);
flashColor = Color.Lerp(flashColor, color, 0.25f);
}
}
edgeDarkness = Math.Max(edgeDarkness, MathHelper.Clamp(vignette, 0f, 0.62f));
}
/// <summary>
/// Reports an ultimate's normalized visual clock. The resulting camera move is
/// a push-in, held composition and capped rebound; it never pauses or changes
/// gameplay time. Concurrent ultimates use the strongest safe zoom instead of
/// multiplying their transforms together.
/// </summary>
public static void ReportCinematicCamera(
int owner,
int actionId,
float normalizedTime,
Vector2 direction,
float intensity = 1f)
{
if (Main.dedServ || Main.gameMenu || owner < 0 || owner >= Main.maxPlayers)
return;
EnsureCinematicStorage();
ref CinematicCameraState state = ref cinematicStates[owner];
if (state.Active && state.ActionId == actionId && state.LastReportTick == Main.GameUpdateCount
&& normalizedTime < state.NormalizedTime)
{
return;
}
state.Active = true;
state.ActionId = actionId;
state.NormalizedTime = MathHelper.Clamp(float.IsFinite(normalizedTime) ? normalizedTime : 0f, 0f, 1f);
state.Direction = direction.SafeNormalize(Vector2.UnitX);
state.Intensity = MathHelper.Clamp(float.IsFinite(intensity) ? intensity : 1f, 0f, 1f);
state.LastReportTick = Main.GameUpdateCount;
}
/// <summary>
/// Dispatches a synchronized visual description without granting it authority
/// to create damage. Skill cues and Void direct-hit cues are supported now; the
/// same event shape is kept for other normal, special and ultimate storyboards.
/// </summary>
internal static bool Submit(in ReaperVisualEvent visualEvent)
{
if (Main.dedServ || Main.gameMenu
|| visualEvent.Owner < 0 || visualEvent.Owner >= Main.maxPlayers)
{
return false;
}
if (visualEvent.ActionKind == ReaperVisualActionKind.NormalAttack
&& visualEvent.Form == ReaperFormId.Void)
{
ReaperVoidHitVisualSystem.ReportHit(
visualEvent.Owner,
visualEvent.Stage,
visualEvent.ActionId,
visualEvent.Target,
visualEvent.Origin,
visualEvent.Direction,
visualEvent.Seed);
return true;
}
if (visualEvent.ActionKind != ReaperVisualActionKind.Skill)
return false;
Vector2 center = visualEvent.Origin;
if (visualEvent.Target >= 0 && visualEvent.Target < Main.maxNPCs)
{
NPC target = Main.npc[visualEvent.Target];
if (target.active)
center = target.Center;
}
ReaperSkillVisualSystem.ReportSkill(
visualEvent.Owner,
visualEvent.Form,
visualEvent.Stage,
visualEvent.Skill,
visualEvent.SkillLevel,
visualEvent.TimelinePhase,
center,
visualEvent.Direction,
visualEvent.ActionId,
visualEvent.Seed);
return true;
}
/// <summary>Applies the current composite shake to the local camera.</summary>
public static void ApplyCameraImpulse(ref Vector2 screenPosition)
{
if (Main.dedServ)
return;
ComposeCinematicCamera(out _, out Vector2 cinematicBias);
screenPosition += cinematicBias;
if (shakeFrames <= 0 || shakeStrength <= 0.01f)
return;
float remaining = shakeFrames / (float)Math.Max(1, shakeDuration);
float envelope = remaining * remaining;
float tick = (float)Main.GameUpdateCount + shakePhase;
Vector2 tangent = shakeDirection.RotatedBy(MathHelper.PiOver2);
float along = (float)Math.Sin(tick * 2.31f) * shakeStrength * envelope;
float across = (float)Math.Sin(tick * 3.77f + 0.9f) * shakeStrength * envelope * 0.52f;
screenPosition += shakeDirection * along + tangent * across;
}
private static bool DrawImpactOverlay()
{
if (Main.gameMenu || flashFrames <= 0 || flashOpacity <= 0.001f)
return true;
float remaining = flashFrames / (float)Math.Max(1, flashDuration);
float opacity = flashOpacity * remaining * remaining;
Rectangle viewport = new(
0,
0,
Math.Max(1, Main.screenWidth),
Math.Max(1, Main.screenHeight));
Texture2D pixel = TextureAssets.MagicPixel.Value;
SpriteBatch batch = Main.spriteBatch;
batch.Draw(pixel, viewport, flashColor * opacity);
float edge = edgeDarkness * MathHelper.Clamp(remaining * 1.45f, 0f, 1f);
if (edge > 0.001f)
batch.Draw(pixel, viewport, Color.Black * (edge * 0.12f));
return true;
}
private static void Clear()
{
shakeFrames = 0;
shakeDuration = 0;
shakeStrength = 0f;
shakeDirection = Vector2.UnitY;
shakePhase = 0f;
flashFrames = 0;
flashDuration = 0;
flashOpacity = 0f;
flashColor = Color.White;
edgeDarkness = 0f;
cinematicStates = [];
}
private static void ComposeCinematicCamera(out float zoomScale, out Vector2 cameraBias)
{
zoomScale = 1f;
cameraBias = Vector2.Zero;
if (cinematicStates.Length == 0)
return;
float strongestPositiveZoom = 0f;
float strongestRebound = 0f;
float strongestBias = 0f;
for (int index = 0; index < cinematicStates.Length; index++)
{
ref CinematicCameraState state = ref cinematicStates[index];
if (!state.Active || Main.GameUpdateCount > state.LastReportTick + 2UL)
continue;
EvaluateCinematicCurve(state.NormalizedTime, out float zoomDelta, out float bias);
zoomDelta *= state.Intensity;
bias *= state.Intensity;
if (zoomDelta >= 0f)
strongestPositiveZoom = Math.Max(strongestPositiveZoom, zoomDelta);
else
strongestRebound = Math.Min(strongestRebound, zoomDelta);
float biasMagnitude = Math.Abs(bias);
if (biasMagnitude > strongestBias)
{
strongestBias = biasMagnitude;
cameraBias = state.Direction * bias;
}
}
float selectedZoom = strongestPositiveZoom > 0.0001f
? strongestPositiveZoom
: strongestRebound;
zoomScale = MathHelper.Clamp(1f + selectedZoom, MinimumCinematicZoom, MaximumCinematicZoom);
if (cameraBias.LengthSquared() > MaximumCinematicBias * MaximumCinematicBias)
cameraBias = cameraBias.SafeNormalize(Vector2.Zero) * MaximumCinematicBias;
}
private static void EvaluateCinematicCurve(float progress, out float zoomDelta, out float bias)
{
progress = MathHelper.Clamp(progress, 0f, 1f);
if (progress < 0.12f)
{
float amount = Smooth(progress / 0.12f);
zoomDelta = MathHelper.Lerp(0f, 0.060f, amount);
bias = MathHelper.Lerp(0f, 9f, amount);
return;
}
if (progress < 0.76f)
{
zoomDelta = 0.060f;
bias = 9f;
return;
}
if (progress < 0.90f)
{
float amount = Smooth((progress - 0.76f) / 0.14f);
zoomDelta = MathHelper.Lerp(0.060f, 0.105f, amount);
bias = MathHelper.Lerp(9f, 19f, amount);
return;
}
if (progress < 0.965f)
{
float amount = Smooth((progress - 0.90f) / 0.065f);
zoomDelta = MathHelper.Lerp(0.105f, -0.015f, amount);
bias = MathHelper.Lerp(19f, -7f, amount);
return;
}
float rebound = Smooth((progress - 0.965f) / 0.035f);
zoomDelta = MathHelper.Lerp(-0.015f, 0f, rebound);
bias = MathHelper.Lerp(-7f, 0f, rebound);
}
private static float Smooth(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static void EnsureCinematicStorage()
{
if (cinematicStates.Length != Main.maxPlayers)
cinematicStates = new CinematicCameraState[Main.maxPlayers];
}
private struct CinematicCameraState
{
public bool Active;
public int ActionId;
public float NormalizedTime;
public Vector2 Direction;
public float Intensity;
public ulong LastReportTick;
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.GameContent;
using Terraria.Graphics;
using Terraria.ModLoader;
using Terraria.UI;
namespace SoulHarvest.Common;
/// <summary>
/// Client-only compositor for short, screen-space Reaper impacts. Gameplay remains
/// server authoritative; this class only combines camera impulses and capped flashes.
/// </summary>
[Autoload(Side = ModSide.Client)]
public sealed class ReaperVfxDirector : ModSystem
{
private const float MaximumShakeStrength = 12f;
private const float MaximumFlashOpacity = 0.78f;
private const float MaximumCinematicZoom = 1.11f;
private const float MinimumCinematicZoom = 0.985f;
private const float MaximumCinematicBias = 22f;
private static int shakeFrames;
private static int shakeDuration;
private static float shakeStrength;
private static Vector2 shakeDirection = Vector2.UnitY;
private static float shakePhase;
private static int flashFrames;
private static int flashDuration;
private static float flashOpacity;
private static Color flashColor = Color.White;
private static float edgeDarkness;
private static CinematicCameraState[] cinematicStates = [];
public override void OnWorldUnload() => Clear();
public override void Unload() => Clear();
public override void PostUpdateEverything()
{
if (Main.gameMenu)
{
Clear();
return;
}
if (shakeFrames > 0)
shakeFrames--;
else
shakeStrength = 0f;
if (flashFrames > 0)
flashFrames--;
else
{
flashOpacity = 0f;
edgeDarkness = 0f;
}
EnsureCinematicStorage();
for (int index = 0; index < cinematicStates.Length; index++)
{
ref CinematicCameraState state = ref cinematicStates[index];
if (state.Active && Main.GameUpdateCount > state.LastReportTick + 2UL)
state = default;
}
}
public override void ModifyTransformMatrix(ref SpriteViewMatrix transform)
{
if (Main.dedServ || Main.gameMenu)
return;
ComposeCinematicCamera(out float zoomScale, out _);
if (Math.Abs(zoomScale - 1f) <= 0.0001f)
return;
transform.Zoom *= zoomScale;
}
public override void ModifyInterfaceLayers(List<GameInterfaceLayer> layers)
{
int index = layers.FindIndex(layer => layer.Name == "Vanilla: Resource Bars");
if (index < 0)
index = layers.FindIndex(layer => layer.Name == "Vanilla: Mouse Text");
if (index < 0)
index = layers.Count;
layers.Insert(index, new LegacyGameInterfaceLayer(
"SoulHarvest: Reaper Impact Composite",
DrawImpactOverlay,
InterfaceScaleType.None));
}
/// <summary>
/// Adds a global impact without distance falloff. Ultimate timelines use this so
/// every client which receives the action sees the same full-strength punctuation.
/// Concurrent calls use maxima instead of unbounded addition.
/// </summary>
public static void TriggerGlobalImpact(
Vector2 direction,
float strength,
int cameraFrames,
Color color,
float opacity,
int colorFrames,
float vignette = 0.28f)
{
if (Main.dedServ || Main.gameMenu)
return;
direction = direction.SafeNormalize(Vector2.UnitY);
strength = MathHelper.Clamp(strength, 0f, MaximumShakeStrength);
cameraFrames = Math.Clamp(cameraFrames, 0, 30);
opacity = MathHelper.Clamp(opacity, 0f, MaximumFlashOpacity);
colorFrames = Math.Clamp(colorFrames, 0, 30);
// Bright full-viewport overlays read as a white-screen defect when rapid
// melee or passive events overlap. Keep their camera punch and vignette,
// but suppress the bright color plane entirely.
Vector3 rgb = color.ToVector3();
float luminance = rgb.X * 0.2126f + rgb.Y * 0.7152f + rgb.Z * 0.0722f;
if (luminance >= 0.64f)
{
opacity = 0f;
colorFrames = 0;
}
if (strength >= shakeStrength || shakeFrames <= 1)
{
shakeStrength = strength;
shakeDirection = direction;
shakeDuration = Math.Max(1, cameraFrames);
shakeFrames = cameraFrames;
shakePhase += 1.731f;
}
else
{
shakeFrames = Math.Max(shakeFrames, cameraFrames);
shakeDuration = Math.Max(shakeDuration, shakeFrames);
shakeDirection = (shakeDirection + direction * 0.45f).SafeNormalize(shakeDirection);
}
// A camera-only impact must not create, extend or recolor an unrelated
// full-screen flash that is already active (for example an overlapping
// ultimate). Unique-skill cues intentionally pass zero here.
if (opacity > 0.001f && colorFrames > 0)
{
if (opacity >= flashOpacity || flashFrames <= 1)
{
flashColor = color;
flashOpacity = opacity;
flashDuration = Math.Max(1, colorFrames);
flashFrames = colorFrames;
}
else
{
flashFrames = Math.Max(flashFrames, colorFrames);
flashDuration = Math.Max(flashDuration, flashFrames);
flashColor = Color.Lerp(flashColor, color, 0.25f);
}
}
edgeDarkness = Math.Max(edgeDarkness, MathHelper.Clamp(vignette, 0f, 0.62f));
}
/// <summary>
/// Reports an ultimate's normalized visual clock. The resulting camera move is
/// a push-in, held composition and capped rebound; it never pauses or changes
/// gameplay time. Concurrent ultimates use the strongest safe zoom instead of
/// multiplying their transforms together.
/// </summary>
public static void ReportCinematicCamera(
int owner,
int actionId,
float normalizedTime,
Vector2 direction,
float intensity = 1f)
{
if (Main.dedServ || Main.gameMenu || owner < 0 || owner >= Main.maxPlayers)
return;
EnsureCinematicStorage();
ref CinematicCameraState state = ref cinematicStates[owner];
if (state.Active && state.ActionId == actionId && state.LastReportTick == Main.GameUpdateCount
&& normalizedTime < state.NormalizedTime)
{
return;
}
state.Active = true;
state.ActionId = actionId;
state.NormalizedTime = MathHelper.Clamp(float.IsFinite(normalizedTime) ? normalizedTime : 0f, 0f, 1f);
state.Direction = direction.SafeNormalize(Vector2.UnitX);
state.Intensity = MathHelper.Clamp(float.IsFinite(intensity) ? intensity : 1f, 0f, 1f);
state.LastReportTick = Main.GameUpdateCount;
}
/// <summary>
/// Dispatches a synchronized visual description without granting it authority
/// to create damage. Skill cues and Void direct-hit cues are supported now; the
/// same event shape is kept for other normal, special and ultimate storyboards.
/// </summary>
internal static bool Submit(in ReaperVisualEvent visualEvent)
{
if (Main.dedServ || Main.gameMenu
|| visualEvent.Owner < 0 || visualEvent.Owner >= Main.maxPlayers)
{
return false;
}
if (visualEvent.ActionKind == ReaperVisualActionKind.NormalAttack
&& visualEvent.Form == ReaperFormId.Void)
{
ReaperVoidHitVisualSystem.ReportHit(
visualEvent.Owner,
visualEvent.Stage,
visualEvent.ActionId,
visualEvent.Target,
visualEvent.Origin,
visualEvent.Direction,
visualEvent.Seed);
return true;
}
if (visualEvent.ActionKind != ReaperVisualActionKind.Skill)
return false;
Vector2 center = visualEvent.Origin;
if (visualEvent.Target >= 0 && visualEvent.Target < Main.maxNPCs)
{
NPC target = Main.npc[visualEvent.Target];
if (target.active)
center = target.Center;
}
ReaperSkillVisualSystem.ReportSkill(
visualEvent.Owner,
visualEvent.Form,
visualEvent.Stage,
visualEvent.Skill,
visualEvent.SkillLevel,
visualEvent.TimelinePhase,
center,
visualEvent.Direction,
visualEvent.ActionId,
visualEvent.Seed);
return true;
}
/// <summary>Applies the current composite shake to the local camera.</summary>
public static void ApplyCameraImpulse(ref Vector2 screenPosition)
{
if (Main.dedServ)
return;
ComposeCinematicCamera(out _, out Vector2 cinematicBias);
screenPosition += cinematicBias;
if (shakeFrames <= 0 || shakeStrength <= 0.01f)
return;
float remaining = shakeFrames / (float)Math.Max(1, shakeDuration);
float envelope = remaining * remaining;
float tick = (float)Main.GameUpdateCount + shakePhase;
Vector2 tangent = shakeDirection.RotatedBy(MathHelper.PiOver2);
float along = (float)Math.Sin(tick * 2.31f) * shakeStrength * envelope;
float across = (float)Math.Sin(tick * 3.77f + 0.9f) * shakeStrength * envelope * 0.52f;
screenPosition += shakeDirection * along + tangent * across;
}
private static bool DrawImpactOverlay()
{
if (Main.gameMenu || flashFrames <= 0 || flashOpacity <= 0.001f)
return true;
float remaining = flashFrames / (float)Math.Max(1, flashDuration);
float opacity = flashOpacity * remaining * remaining;
Rectangle viewport = new(
0,
0,
Math.Max(1, Main.screenWidth),
Math.Max(1, Main.screenHeight));
Texture2D pixel = TextureAssets.MagicPixel.Value;
SpriteBatch batch = Main.spriteBatch;
batch.Draw(pixel, viewport, flashColor * opacity);
float edge = edgeDarkness * MathHelper.Clamp(remaining * 1.45f, 0f, 1f);
if (edge > 0.001f)
batch.Draw(pixel, viewport, Color.Black * (edge * 0.12f));
return true;
}
private static void Clear()
{
shakeFrames = 0;
shakeDuration = 0;
shakeStrength = 0f;
shakeDirection = Vector2.UnitY;
shakePhase = 0f;
flashFrames = 0;
flashDuration = 0;
flashOpacity = 0f;
flashColor = Color.White;
edgeDarkness = 0f;
cinematicStates = [];
}
private static void ComposeCinematicCamera(out float zoomScale, out Vector2 cameraBias)
{
zoomScale = 1f;
cameraBias = Vector2.Zero;
if (cinematicStates.Length == 0)
return;
float strongestPositiveZoom = 0f;
float strongestRebound = 0f;
float strongestBias = 0f;
for (int index = 0; index < cinematicStates.Length; index++)
{
ref CinematicCameraState state = ref cinematicStates[index];
if (!state.Active || Main.GameUpdateCount > state.LastReportTick + 2UL)
continue;
EvaluateCinematicCurve(state.NormalizedTime, out float zoomDelta, out float bias);
zoomDelta *= state.Intensity;
bias *= state.Intensity;
if (zoomDelta >= 0f)
strongestPositiveZoom = Math.Max(strongestPositiveZoom, zoomDelta);
else
strongestRebound = Math.Min(strongestRebound, zoomDelta);
float biasMagnitude = Math.Abs(bias);
if (biasMagnitude > strongestBias)
{
strongestBias = biasMagnitude;
cameraBias = state.Direction * bias;
}
}
float selectedZoom = strongestPositiveZoom > 0.0001f
? strongestPositiveZoom
: strongestRebound;
zoomScale = MathHelper.Clamp(1f + selectedZoom, MinimumCinematicZoom, MaximumCinematicZoom);
if (cameraBias.LengthSquared() > MaximumCinematicBias * MaximumCinematicBias)
cameraBias = cameraBias.SafeNormalize(Vector2.Zero) * MaximumCinematicBias;
}
private static void EvaluateCinematicCurve(float progress, out float zoomDelta, out float bias)
{
progress = MathHelper.Clamp(progress, 0f, 1f);
if (progress < 0.12f)
{
float amount = Smooth(progress / 0.12f);
zoomDelta = MathHelper.Lerp(0f, 0.060f, amount);
bias = MathHelper.Lerp(0f, 9f, amount);
return;
}
if (progress < 0.76f)
{
zoomDelta = 0.060f;
bias = 9f;
return;
}
if (progress < 0.90f)
{
float amount = Smooth((progress - 0.76f) / 0.14f);
zoomDelta = MathHelper.Lerp(0.060f, 0.105f, amount);
bias = MathHelper.Lerp(9f, 19f, amount);
return;
}
if (progress < 0.965f)
{
float amount = Smooth((progress - 0.90f) / 0.065f);
zoomDelta = MathHelper.Lerp(0.105f, -0.015f, amount);
bias = MathHelper.Lerp(19f, -7f, amount);
return;
}
float rebound = Smooth((progress - 0.965f) / 0.035f);
zoomDelta = MathHelper.Lerp(-0.015f, 0f, rebound);
bias = MathHelper.Lerp(-7f, 0f, rebound);
}
private static float Smooth(float value)
{
value = MathHelper.Clamp(value, 0f, 1f);
return value * value * (3f - 2f * value);
}
private static void EnsureCinematicStorage()
{
if (cinematicStates.Length != Main.maxPlayers)
cinematicStates = new CinematicCameraState[Main.maxPlayers];
}
private struct CinematicCameraState
{
public bool Active;
public int ActionId;
public float NormalizedTime;
public Vector2 Direction;
public float Intensity;
public ulong LastReportTick;
}
}