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.
}