Game/DailyGenConfig.cs
using System.Collections.Generic;

namespace BlockParty;

/// <summary>One non-baseline thing a generated daily can have. Each has a weighted chance of being
/// drawn and a cost against the day's spice budget, so interesting twists appear often but rarely
/// stack into an outlier day.</summary>
public enum DailyModifier
{
	ExtraBlocks,
	FewerBlocks,
	/// <summary>Allow one block type to appear twice.</summary>
	Duplicates,
	/// <summary>One or two blocks start at phase 1.</summary>
	PhaseStart,
}

public sealed record SpiceModifier( DailyModifier Id, int Weight, int Cost );

/// <summary>
/// One hand-authored obstacle layout the daily generator can pick. Curation is the safety net:
/// spawn positions are valid 40x40 block centres clear of the obstacles, the player spawn is safe,
/// and only sides that are fair for THIS layout's player spawn are listed as spike candidates
/// (never the surface the player starts standing on). Checked by the daily_validate_templates
/// ConCmd. Rects use the project's logical space: new RectF( left, bottom, right, top ), y-up,
/// arena 0..240 with 2-unit walls (valid block centres 22..218).
/// </summary>
public sealed class ObstacleTemplate
{
	public string Id { get; init; }
	/// <summary>Relative daily selection weight (1 = normal); zero excludes the template from normal rolls.</summary>
	public float Weight { get; init; }
	/// <summary>Allow a coin-flip mirrored variant across x=120 (only useful for asymmetric layouts).</summary>
	public bool AllowMirrorX { get; init; }
	/// <summary>Always place spikes exactly as authored: when set, every authored spiked surface is
	/// spiked every day. Off (the default) = spikes are rolled — each surface has an independent
	/// <see cref="DailyGenConfig.SpikeChancePercent"/> chance each day.</summary>
	public bool AuthoredSpikes { get; init; }
	public RectF[] Obstacles { get; init; } = System.Array.Empty<RectF>();
	/// <summary>Indices into <see cref="Obstacles"/> whose rect also blocks line of sight.</summary>
	public int[] VisionObstacles { get; init; } = System.Array.Empty<int>();
	/// <summary>Indices into <see cref="Obstacles"/> flagged as fences (solid only to blocks). Fence
	/// COLOUR is not template data — it rolls with the day's palette like every other colour.</summary>
	public int[] FenceObstacles { get; init; } = System.Array.Empty<int>();
	/// <summary>Indices into <see cref="Obstacles"/> flagged as GLASS (solid only to players).
	/// Layout-only like the fences; the glass colour rolls with the day's palette.</summary>
	public int[] GlassObstacles { get; init; } = System.Array.Empty<int>();
	public Vector2[] SpawnPositions { get; init; }
	/// <summary>Optional pinned blocks, aligned index-wise with <see cref="SpawnPositions"/> (null =
	/// free slot). Pinned blocks always spawn, independent of the day's rolled block count.</summary>
	public PinnedBlock[] SpawnPins { get; init; } = System.Array.Empty<PinnedBlock>();
	/// <summary>Candidate player starts; the run picks one from the run Rng when there's more than one.</summary>
	public Vector2[] PlayerSpawns { get; init; } = { new( 16, 40 ) };
	/// <summary>Spiked arena boundary sides — rolled per-side each day, unless
	/// <see cref="AuthoredSpikes"/> makes them always spiked as authored.</summary>
	public Direction[] SpikeableWalls { get; init; } = System.Array.Empty<Direction>();
	/// <summary>Spiked obstacle faces (obstacle index + outward sides) — same
	/// rolled-vs-<see cref="AuthoredSpikes"/> rule as <see cref="SpikeableWalls"/>.</summary>
	public (int Obstacle, Direction[] Sides)[] SpikeableObstacleSides { get; init; } = System.Array.Empty<(int, Direction[])>();
	/// <summary>Optional alternate-playfield regions (cosmetic); empty for now, scaffolded for later.</summary>
	public RectF[] AlternatePlayfieldRects { get; init; } = System.Array.Empty<RectF>();
	/// <summary>Collectible coin centres this template's days always place (mirrored with the layout
	/// on a mirrored day). Spawning coins consumes no generator RNG.</summary>
	public Vector2[] Coins { get; init; } = System.Array.Empty<Vector2>();
}

/// <summary>
/// All tuning knobs for daily-challenge level generation. IMPORTANT: any change to these tables
/// changes every past and future generated day (and breaks stored daily replays — the regenerated
/// layout stops matching their recorded level hash, so they grey out), so config changes ship
/// together with a LEADERBOARD_VERSION bump — the daily seed folds the version in, which re-rolls
/// the boards cleanly. Tables are plain arrays drawn in
/// listed order; never replace them with dictionaries (iteration order is load-bearing).
/// </summary>
public static class DailyGenConfig
{
	/// <summary>Relative draw weight per block type. Types not listed never appear in dailies.
	/// TUNE: classics common, mechanically loud types rarer.</summary>
	public static readonly (BlockType Type, int Weight)[] TypeWeights =
	{
		(BlockType.Dragon, Weight: 7),
		(BlockType.Squid, Weight: 6),
		(BlockType.Smile, Weight: 6),
		(BlockType.Spikey, Weight: 7),
		(BlockType.Sad, Weight: 7),
		(BlockType.Wind, Weight: 3),
		(BlockType.Magnet, Weight: 2),
		(BlockType.Shade, Weight: 5),
		(BlockType.Sticky, Weight: 5),
		(BlockType.Stasis, Weight: 5),
		(BlockType.Shockwave, Weight: 3),
		(BlockType.Slicer, Weight: 5),
		(BlockType.Teleport, Weight: 5),
		(BlockType.Mimic, Weight: 4),
		(BlockType.Summoner, Weight: 4),
		(BlockType.Wisp, Weight: 2),
		(BlockType.Venom, Weight: 4),
		(BlockType.Hunter, Weight: 4),
		(BlockType.Siren, Weight: 3),
		(BlockType.Reverse, Weight: 3),
		(BlockType.DragonBounce, Weight: 4),
		(BlockType.Laser, Weight: 3),
		(BlockType.Bullet, Weight: 5),
		(BlockType.LaserPierce, Weight: 4),
		(BlockType.StraightLaser, Weight: 3),
		(BlockType.SadPierce, Weight: 3),
	};

	/// <summary>Type pairs that never share a day (drawing one removes the other from the pool).
	/// TUNE: combos that compound into unfair pressure.</summary>
	public static readonly (BlockType A, BlockType B)[] ForbiddenPairs =
	{
		(BlockType.Hunter, BlockType.Siren),
		//(BlockType.Hunter, BlockType.Wisp),
		//(BlockType.Siren, BlockType.Sticky),
		//(BlockType.Siren, BlockType.Magnet),
		//(BlockType.Siren, BlockType.Reverse),
		//(BlockType.Laser, BlockType.StraightLaser),
		//(BlockType.Laser, BlockType.LaserPierce),
		//(BlockType.LaserPierce, BlockType.StraightLaser),
		//(BlockType.Laser, BlockType.Bullet),
	};

	/// <summary>Types that never appear twice even on a Duplicates day.</summary>
	public static readonly BlockType[] DuplicateBannedTypes =
	{
		BlockType.Siren, BlockType.Summoner, BlockType.Reverse,
	};

	public static readonly (int Count, int Weight)[] BlockCountWeights =
	{
		(Count: 3, Weight: 10),
		(Count: 4, Weight: 20),
		(Count: 5, Weight: 20),
		(Count: 6, Weight: 10),
		(Count: 7, Weight: 5),
	};

	/// <summary>Attempt limits rolled when the independent limited-attempts chance succeeds.</summary>
	public static readonly (int Attempts, int Weight)[] AttemptWeights =
	{
		(Attempts: 15, Weight: 1),
		(Attempts: 10, Weight: 2),
		(Attempts: 5, Weight: 8),
		(Attempts: 3, Weight: 8),
		(Attempts: 2, Weight: 6),
		(Attempts: 1, Weight: 10),
	};

	/// <summary>Independent chance to limit attempts for the day.</summary>
	public const int LimitedAttemptsChancePercent = 85;

	/// <summary>Independent chance to force a character for the day.</summary>
	public const int ForcedCharacterChancePercent = 55;

	/// <summary>Relative draw weight per character when the independent forced-character chance succeeds (forced regardless of
	/// unlock state; a daily victory then unlocks the character as usual). Every character is
	/// available; lower weight = rarer. Explicit list so adding new characters later is a
	/// deliberate, version-bumped choice. TUNE.</summary>
	public static readonly (string CharacterId, int Weight)[] CharacterWeights =
	{
		("original", Weight: 1),
		("wrapper", Weight: 4),
		("swapper", Weight: 6),
		("gunner", Weight: 6),
		("flipper", Weight: 6),
		("bird", Weight: 5),
		("spring", Weight: 6),
		("swarm", Weight: 6),
		("grappler", Weight: 6),
		("climber", Weight: 6),
		("blinker", Weight: 6),
		("rewind", Weight: 5),
		("shifter", Weight: 6),
		("solar", Weight: 5),
		("mimic", Weight: 6),
		("twins", Weight: 5),
	};

	/// <summary>How many spice points a day gets: most days 0-1 twists, occasionally 2, rarely 3.</summary>
	public static readonly (int Points, int Weight)[] SpiceBudgetWeights =
	{
		(Points: 0, Weight: 30),
		(Points: 1, Weight: 50),
		(Points: 2, Weight: 15),
		(Points: 3, Weight: 10),
	};

	/// <summary>Every modifier currently costs 1, so the spice budget IS the twist count; a future
	/// especially nasty modifier can cost 2+ to hog a whole high-budget day.</summary>
	public static readonly SpiceModifier[] Modifiers =
	{
		new( DailyModifier.ExtraBlocks, Weight: 10, Cost: 1 ),
		//new( DailyModifier.FewerBlocks, Weight: 10, Cost: 1 ),
		new( DailyModifier.Duplicates, Weight: 7, Cost: 1 ),
		new( DailyModifier.PhaseStart, Weight: 10, Cost: 1 ),
	};

	/// <summary>Modifier pairs that never land on the same day.</summary>
	public static readonly (DailyModifier A, DailyModifier B)[] ExclusiveModifiers =
	{
		(A: DailyModifier.ExtraBlocks, B: DailyModifier.FewerBlocks),
	};

	/// <summary>Every day, each of the template's spike candidates independently rolls this chance
	/// to actually be spiked (<see cref="ObstacleTemplate.AuthoredSpikes"/> templates skip the roll
	/// and always spike everything).</summary>
	public const int SpikeChancePercent = 10;

	/// <summary>Percent chance a day rolls a generated alternate-playfield region (one of the
	/// generator's layout families — bands, boxes, frames, checkers, stairs … — randomly mirrored,
	/// drawn in a second pattern+colour). Purely cosmetic, so it's an independent roll rather than a
	/// spice modifier. Templates that author their own
	/// <see cref="ObstacleTemplate.AlternatePlayfieldRects"/> skip this roll.</summary>
	public const int AltPlayfieldChancePercent = 40;

	/// <summary>Percent chance a day rolls ambient background particles (rain / snow-style squares
	/// falling from the top edge, tinted to the day's palette — see
	/// <see cref="LevelCosmeticsRandomizer.RollBackgroundParticles"/>). Purely cosmetic: the emitter
	/// runs on the Background Rng stream and never touches the sim, and the roll's draw count is
	/// constant whichever way this gate lands, so tuning it never disturbs draw alignment.</summary>
	public const int BackgroundParticlesChancePercent = 35;

	/// <summary>Last-resort layout when no positive-weight template can be selected.</summary>
	public static readonly ObstacleTemplate FallbackTemplate = new()
	{
		Id = "__fallback",
		Weight = 0,
		SpawnPositions = new Vector2[]
		{
			new( 218, 218 ), new( 178, 218 ), new( 138, 218 ), new( 98, 218 ), new( 58, 218 ),
			new( 58, 158 ), new( 138, 158 ), new( 218, 158 ),
		},
		PlayerSpawns = new Vector2[] { new( 16, 40 ) },
		SpikeableWalls = new[] { Direction.Up, Direction.Right, Direction.Left },
	};

	// The template library lives in Assets/daily-templates/*.json (authored from
	// the level editor's Save Template button, loaded by DailyTemplates) — same generation-input
	// contract as the tables above: any file change re-rolls every day and rides a version bump.
}