Voxel/Stages.cs
namespace Monolith;

public enum ShapeKind
{
	/// <summary>Solid box. Size decides whether it reads as a cube, a slab or a spire.</summary>
	Box,
	/// <summary>Solid ellipsoid inscribed in the size box.</summary>
	Sphere,
	/// <summary>Torus lying flat in XY. All edges, calves constantly.</summary>
	Ring,
	/// <summary>Box with a regular grid of shafts bored through all three axes.</summary>
	Lattice,
	/// <summary>Several overlapping blobs drifting together.</summary>
	Cluster,
}

public sealed class StageDef
{
	public string Name;
	public ShapeKind Shape;
	public Vector3Int Size;

	/// <summary>Flavour line shown when the stage condenses.</summary>
	public string Tagline;
}

/// <summary>
/// The ladder of shapes. Clearing one condenses the next.
///
/// Sizes are the REAL tuning, derived from a simulated greedy upgrade buyer: each archetype is
/// around 600 cubes at stage 1, growing by <see cref="Tuning.StageSizeGrowth"/> per stage so
/// that stage 100 lands just inside <see cref="Tuning.StageMaxAxis"/> and a min-maxing player
/// takes roughly 18 minutes for the full ladder.
/// </summary>
public static class Stages
{
	public static readonly StageDef[] All =
	{
		new()
		{
			Name = "The Seed",
			Shape = ShapeKind.Box,
			Size = new Vector3Int( 7, 7, 6 ),
			Tagline = "A small thing. Break it.",
		},
		new()
		{
			Name = "The Slab",
			Shape = ShapeKind.Box,
			Size = new Vector3Int( 12, 12, 2 ),
			Tagline = "Wide and thin. Cut underneath it.",
		},
		new()
		{
			Name = "The Spire",
			Shape = ShapeKind.Box,
			Size = new Vector3Int( 5, 5, 12 ),
			Tagline = "Take the base and the rest follows.",
		},
		new()
		{
			Name = "The Sphere",
			Shape = ShapeKind.Sphere,
			Size = new Vector3Int( 9, 9, 9 ),
			Tagline = "No flat faces to hide behind.",
		},
		new()
		{
			Name = "The Ring",
			Shape = ShapeKind.Ring,
			Size = new Vector3Int( 14, 14, 5 ),
			Tagline = "All edge, no core.",
		},
		new()
		{
			Name = "The Lattice",
			Shape = ShapeKind.Lattice,
			Size = new Vector3Int( 11, 11, 11 ),
			Tagline = "Thin members. Everything wants to fall.",
		},
		new()
		{
			Name = "The Monolith",
			Shape = ShapeKind.Box,
			Size = new Vector3Int( 7, 7, 6 ),
			Tagline = "The real one.",
		},
		new()
		{
			Name = "The Cluster",
			Shape = ShapeKind.Cluster,
			Size = new Vector3Int( 14, 14, 9 ),
			Tagline = "It never was one piece.",
		},
	};

	/// <summary>
	/// The eight authored shapes are archetypes. The ladder cycles through them forever,
	/// growing each time round, so stage 100 is the same shape vocabulary at a far larger
	/// scale rather than 92 shapes nobody wrote. See <see cref="Tuning.PrestigeStageRequirement"/>
	/// for where Collapse becomes available.
	/// </summary>
	public static StageDef Get( int index )
	{
		if ( All.Length == 0 )
			return null;

		if ( index < 0 ) index = 0;
		return All[index % All.Length];
	}

	/// <summary>How many full cycles of the archetype list we are into.</summary>
	public static int CycleOf( int stageIndex ) => Math.Max( 0, stageIndex ) / All.Length;

	/// <summary>
	/// Grows smoothly **per stage**, not per cycle. A per-cycle step made every eighth stage a
	/// wall and the seven between it trivial; a gentle per-stage curve keeps each one feeling
	/// slightly meatier than the last. See <see cref="Tuning.StageSizeGrowth"/> for the pacing
	/// arithmetic behind the constant.
	/// </summary>
	public static Vector3Int SizeFor( int stageIndex )
	{
		var def = Get( stageIndex );
		if ( def == null )
			return new Vector3Int( 20, 20, 20 );

		if ( stageIndex <= 0 )
			return def.Size;

		float scale = MathF.Pow( Tuning.StageSizeGrowth, stageIndex );
		int max = Tuning.StageMaxAxis;

		return new Vector3Int(
			Math.Clamp( (int)(def.Size.x * scale), 8, max ),
			Math.Clamp( (int)(def.Size.y * scale), 8, max ),
			Math.Clamp( (int)(def.Size.z * scale), 8, max ) );
	}

	public static string NameFor( int stageIndex )
	{
		var def = Get( stageIndex );
		if ( def == null ) return "Monolith";

		int cycle = CycleOf( stageIndex );
		if ( cycle <= 0 )
			return def.Name;

		// Greek suffix per cycle reads better than "+3" and suggests escalation.
		string[] tiers = { "", " II", " III", " IV", " V", " VI", " VII", " VIII", " IX", " X" };
		return cycle < tiers.Length ? def.Name + tiers[cycle] : $"{def.Name} +{cycle}";
	}

	// ---------------------------------------------------------------- volatile cubes

	/// <summary>
	/// Density of volatile blocks, driven by the local player's Instability upgrade. Set by
	/// <see cref="PlayerProgress"/>; kept as a static so the mesher and the hit test cannot
	/// disagree about which blocks are volatile.
	///
	/// **Multiplayer caveat:** this is a per-player value applied to a shared world, so on the
	/// shared Monolith two players with different Instability see different volatile blocks.
	/// Fine for co-op against a rock, and it is written down here rather than discovered later.
	/// Making it authoritative would mean seeding it from the host per world.
	/// </summary>
	public static float VolatileChance { get; set; } = Tuning.VolatileChanceBase;

	/// <summary>
	/// Volatile blocks render bright red and detonate hard, but only on a **direct hit**. They
	/// are a pure hash of position, so nothing is stored per voxel and nothing is sent over the
	/// wire: changing the density simply changes how many of the same ordering qualify.
	/// </summary>
	public static bool IsVolatile( int x, int y, int z )
	{
		uint h = (uint)(x * 73856093) ^ (uint)(y * 19349663) ^ (uint)(z * 83492791);

		// Avalanche the low bits, otherwise a plain modulo on a lattice of coordinates picks
		// out regular planes rather than a scatter.
		h ^= h >> 13;
		h *= 0x5bd1e995;
		h ^= h >> 15;

		return h % 10000u < (uint)(VolatileChance * 10000f);
	}

	// ---------------------------------------------------------------- shape fill

	/// <summary>True if this voxel is solid for the given shape. Pure function of position,
	/// so host and clients generate byte-identical worlds with no data transfer.</summary>
	public static bool IsSolid( ShapeKind shape, Vector3Int size, int x, int y, int z )
	{
		switch ( shape )
		{
			case ShapeKind.Sphere:
				return InEllipsoid( size, x, y, z, 1f );

			case ShapeKind.Ring:
				return InRing( size, x, y, z );

			case ShapeKind.Lattice:
				return InLattice( x, y, z );

			case ShapeKind.Cluster:
				return InCluster( size, x, y, z );

			default:
				return true;
		}
	}

	private static bool InEllipsoid( Vector3Int size, int x, int y, int z, float scale )
	{
		float rx = size.x * 0.5f * scale;
		float ry = size.y * 0.5f * scale;
		float rz = size.z * 0.5f * scale;

		float dx = (x - size.x * 0.5f + 0.5f) / rx;
		float dy = (y - size.y * 0.5f + 0.5f) / ry;
		float dz = (z - size.z * 0.5f + 0.5f) / rz;

		return dx * dx + dy * dy + dz * dz <= 1f;
	}

	private static bool InRing( Vector3Int size, int x, int y, int z )
	{
		float cx = size.x * 0.5f - 0.5f;
		float cy = size.y * 0.5f - 0.5f;
		float cz = size.z * 0.5f - 0.5f;

		// Major radius sits between the centre and the outer edge; minor radius fills the rest.
		float major = MathF.Min( size.x, size.y ) * 0.32f;
		float minor = MathF.Min( MathF.Min( size.x, size.y ) * 0.16f, size.z * 0.5f );

		float dx = x - cx;
		float dy = y - cy;
		float dz = z - cz;

		float planar = MathF.Sqrt( dx * dx + dy * dy ) - major;
		return planar * planar + dz * dz <= minor * minor;
	}

	private static bool InLattice( int x, int y, int z )
	{
		// Bore square shafts through all three axes on a regular pitch, leaving a frame.
		const int pitch = 12;
		const int hole = 7;

		bool holeX = (y % pitch) < hole && (z % pitch) < hole;
		bool holeY = (x % pitch) < hole && (z % pitch) < hole;
		bool holeZ = (x % pitch) < hole && (y % pitch) < hole;

		return !(holeX || holeY || holeZ);
	}

	/// <summary>Blob centres as fractions of the size box. Fixed, not random, so every client
	/// generates the same cluster with nothing sent over the wire.</summary>
	private static readonly Vector3[] ClusterCentres =
	{
		new( 0.30f, 0.32f, 0.50f ),
		new( 0.70f, 0.38f, 0.44f ),
		new( 0.50f, 0.68f, 0.56f ),
		new( 0.24f, 0.70f, 0.40f ),
		new( 0.78f, 0.72f, 0.62f ),
	};

	private static bool InCluster( Vector3Int size, int x, int y, int z )
	{
		float radius = MathF.Min( size.x, size.y ) * 0.19f;

		foreach ( var c in ClusterCentres )
		{
			float dx = x - c.x * size.x;
			float dy = y - c.y * size.y;
			float dz = z - c.z * size.z;

			if ( dx * dx + dy * dy + dz * dz <= radius * radius )
				return true;
		}

		return false;
	}
}