Rendering/SpriteLayer.cs
using Sandbox.Rendering;

namespace BlockParty;

/// <summary>
/// Helper for building the layered 2D sprites each entity is composed of. Wraps the
/// engine's <see cref="SpriteRenderer"/> with the conventions this game needs:
/// point filtering (crisp pixels), 1 logical pixel = 1 world unit, and a small local-Z
/// offset so layers stack in a defined order within an entity.
/// </summary>
public static class SpriteLayer
{
	/// <summary>Local-Z spacing between an entity's stacked layers (avoids z-fighting).</summary>
	public const float LAYER_Z_STEP = 0.1f;

	/// <summary>The game-wide sprite opacity default — the single switch for the parked
	/// opaque-by-default policy (see the comment in <see cref="Add"/>). Everything that restores a
	/// sprite to "normal" opacity (e.g. Block.SetLayersTranslucent) must use this rather than a
	/// literal true, so no sprite can join the engine's opaque batch while the default is off.</summary>
	public const bool OPAQUE_DEFAULT = false;

	/// <summary>Returns a sprite center whose edges land on the logical pixel grid.</summary>
	public static Vector2 PixelAlignedCenter( Vector2 center, Vector2 size )
	{
		float left = MathF.Round( center.x - size.x * 0.5f );
		float bottom = MathF.Round( center.y - size.y * 0.5f );
		return new Vector2( left + size.x * 0.5f, bottom + size.y * 0.5f );
	}

	/// <summary>Turns a sprite in the view plane so its art-up points along <paramref name="up"/> (and its
	/// Size.x axis along <paramref name="up"/> rotated 90° clockwise). Use this instead of
	/// BillboardMode.None: the engine gives every translucent non-billboard sprite its own scene
	/// object, sorted by bounds distance outside the Z-sorted batch (drawn behind it). With the camera
	/// looking straight down -Z, a rolled billboard is the same flat quad. Positive billboard roll
	/// turns clockwise on screen.</summary>
	public static Rotation FlatRotation( Vector2 up ) => Rotation.FromRoll( MathF.Atan2( up.x, up.y ).RadianToDegree() );

	/// <summary>
	/// Add a sprite layer as a child of <paramref name="parent"/>.
	/// </summary>
	/// <param name="parent">Owning entity GameObject.</param>
	/// <param name="spritePath">Path to the baked .sprite resource (e.g. "sprites/player.sprite").</param>
	/// <param name="size">Quad size in logical pixels (= source art pixel dimensions).</param>
	/// <param name="startingAnim">Animation to start playing.</param>
	/// <param name="childOrder">Stacking order within the entity (higher = nearer camera).</param>
	public static SpriteRenderer Add( GameObject parent, string spritePath, Vector2 size, string startingAnim = "idle", int childOrder = 0 )
	{
		var go = new GameObject();
		go.Name = $"layer:{startingAnim}";
		go.SetParent( parent );
		// 0.1 spacing (not 0.01) so layers don't z-fight: ortho depth precision over the
		// camera's near/far range can't reliably separate 0.01-spaced layers.
		go.LocalPosition = new Vector3( 0, 0, childOrder * LAYER_Z_STEP );

		var sr = go.Components.Create<SpriteRenderer>();
		sr.Sprite = ResourceLibrary.Get<Sprite>( spritePath );
		sr.Size = size;
		sr.TextureFilter = FilterMode.Point;
		sr.Color = Color.White;
		sr.PlaybackSpeed = 1f;
		// Sort by depth (Z) so layering is driven by our DepthToZ, NOT scene/creation order.
		// Without this, draw order follows object creation; after stage churn (play→die→menu→
		// play) the order changes and e.g. the background rect ends up drawn on top of everything.
		sr.IsSorted = true;
		// TRANSLUCENT by default — for now. Opaque-by-default is where this wants to end up (nearly
		// all our sprites are hard-edged pixel art, and depth-writing sprites are ordered by the
		// depth buffer instead of the sort). Older engines drew the opaque and transparent sprite
		// batches in bounds-dependent order with no pass flags, so whichever drew second stomped the
		// other (the e4dd570 edge-blocker regression); the opaque batch's membership stays frozen at
		// the known-good set: player, walls/bands/teeth, glass borders/teeth, obstacle fills. Engine
		// 98907a9109 now puts the batches in the opaque/translucent passes, so flipping
		// OPAQUE_DEFAULT is unblocked but needs a playtest pass (an opaque sprite at alpha < 1 blends
		// only with what the opaque pass already drew). Translucent sites already declare themselves
		// explicitly (sr.Opaque = false + sr.AlphaCutoff = 0).
		sr.Opaque = OPAQUE_DEFAULT;
		if ( sr.Sprite is not null && !string.IsNullOrEmpty( startingAnim ) )
			sr.PlayAnimation( startingAnim );

		return sr;
	}
}