LineGrow.razor
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Collections.Generic;
@inherits PanelComponent

<root>
    <div class="game-screen">

        <div class="board @(DeathFlash ? "death-flash" : "board-normal")"
             style="width: @(BoardW)px; height: @(BoardH)px;">

            @* ── Obstacles ── *@
            @foreach ( var obs in Obstacles )
            {
                var ox = obs.X * CellPx + 2;
                var oy = obs.Y * CellPx + 2;
                var os = CellPx - 4;
                <div class="obstacle" style="left: @(ox)px; top: @(oy)px; width: @(os)px; height: @(os)px;"></div>
            }

            @* ── Line segments between consecutive cell centres ── *@
            @foreach ( var seg in GetLineSegments() )
            {
                <div class="line-seg"
                     style="left: @(seg.Left)px; top: @(seg.Top)px; width: @(seg.Width)px; height: @(seg.Height)px; opacity: @(seg.Alpha.ToString("F2"));">
                </div>
            }

            @* ── Head orb ── *@
            @if ( Snake.Count > 0 )
            {
                var hx = Snake[0].X * CellPx + HalfCell - 8;
                var hy = Snake[0].Y * CellPx + HalfCell - 8;
                <div class="head-orb" style="left: @(hx)px; top: @(hy)px;"></div>
            }

            @* ── Particles ── *@
            @foreach ( var p in Particles )
            {
                <div class="particle"
                     style="left: @(p.X)px; top: @(p.Y)px; opacity: @(p.Alpha.ToString("F2"));">
                </div>
            }

            @* ── HUD ── *@
            <div class="hud-pill">
                <div class="hud-item">
                    <span class="hud-label">LENGTH</span>
                    <span class="hud-value">@Snake.Count</span>
                </div>
                <div class="hud-sep"></div>
                <div class="hud-item">
                    <span class="hud-label">BEST</span>
                    <span class="hud-value">@HighScore</span>
                </div>
            </div>

            @* ── Overlays ── *@
            @if ( Phase != GamePhase.Playing || IsPaused )
            {
                <div class="overlay">
                    @if ( IsPaused )
                    {
                        <p class="overlay-title">Paused</p>
                        <p class="overlay-hint">Esc to resume</p>
                        <button class="quit-btn" onclick=@QuitToMenu>Quit</button>
                    }
                    else if ( Phase == GamePhase.Idle )
                    {
                        <p class="overlay-title">Line Grow</p>
                        <p class="overlay-hint">WASD to start</p>
                    }
                    else if ( Phase == GamePhase.Dead )
                    {
                        <p class="overlay-title">Dead</p>
                        <p class="overlay-score">@Snake.Count</p>
                        <p class="overlay-hint">Press any direction to retry</p>
                    }
                </div>
            }
        </div>

    </div>
</root>

@code {

    public enum Direction { Up, Down, Left, Right }
    public enum GamePhase { Idle, Playing, Dead }

    const int Cols     = 30;
    const int Rows     = 22;
    const int CellPx   = 30;
    const int HalfCell = CellPx / 2;
    const int LineHalf = 5;
    int BoardW => Cols * CellPx;
    int BoardH => Rows * CellPx;

    // How many obstacles to scatter. Scales with grid size but stays manageable.
    const int ObstacleCount = 18;

    // Safe zone around the snake's starting position — no obstacles placed here.
    // Starting snake is at (5,11),(6,11),(7,11) heading right, so clear a 5-cell radius.
    const int SafeX = 7;
    const int SafeY = 11;
    const int SafeRadius = 5;

    GamePhase Phase = GamePhase.Idle;
    bool IsPaused;
    bool DeathFlash;
    int HighScore;

    List<(int X, int Y)> Snake     = new();
    List<(int X, int Y)> Obstacles = new List<(int X, int Y)>();
    Direction Dir        = Direction.Right;
    Direction PendingDir = Direction.Right;

    float TickAccum;
    const float TickInterval = 0.12f;

    // ── Particles ──────────────────────────────────────────────
    class Particle
    {
        public float X, Y;
        public float Vx, Vy;
        public float Alpha;
        public float Life;
        public float MaxLife;
    }
    List<Particle> Particles = new();

    // ── Segment data ────────────────────────────────────────────
    struct LineSeg { public float Left, Top, Width, Height, Alpha; }

    IEnumerable<LineSeg> GetLineSegments()
    {
        int count = Snake.Count;
        for ( int i = 0; i < count - 1; i++ )
        {
            var a = Snake[i];
            var b = Snake[i + 1];
            float ax = a.X * CellPx + HalfCell;
            float ay = a.Y * CellPx + HalfCell;
            float bx = b.X * CellPx + HalfCell;
            float by = b.Y * CellPx + HalfCell;

            float alpha = 1f - (float)i / count * 0.6f;

            if ( ax == bx )
            {
                float top = Math.Min( ay, by );
                float bot = Math.Max( ay, by );
                yield return new LineSeg { Left = ax - LineHalf, Top = top, Width = LineHalf * 2, Height = bot - top, Alpha = alpha };
            }
            else
            {
                float left  = Math.Min( ax, bx );
                float right = Math.Max( ax, bx );
                yield return new LineSeg { Left = left, Top = ay - LineHalf, Width = right - left, Height = LineHalf * 2, Alpha = alpha };
            }
        }
    }

    // ── Obstacle generation ─────────────────────────────────────
    List<(int X, int Y)> GenerateObstacles( Random rng )
    {
        var result   = new List<(int X, int Y)>();
        var occupied = new HashSet<(int X, int Y)>();

        // Mark starting snake cells as occupied
        occupied.Add( (X: 5, Y: 11) );
        occupied.Add( (X: 6, Y: 11) );
        occupied.Add( (X: 7, Y: 11) );

        int attempts = 0;
        while ( result.Count < ObstacleCount && attempts < 2000 )
        {
            attempts++;
            int x = rng.Next( 0, Cols );
            int y = rng.Next( 0, Rows );

            // Keep a clear zone around the spawn
            int dx = x - SafeX;
            int dy = y - SafeY;
            if ( dx * dx + dy * dy <= SafeRadius * SafeRadius ) continue;

            if ( occupied.Contains( (X: x, Y: y) ) ) continue;

            // Don't cluster — require at least 1 cell gap from existing obstacles
            bool tooClose = false;
            foreach ( var o in result )
            {
                int odx = o.X - x;
                int ody = o.Y - y;
                if ( odx * odx + ody * ody < 4 ) { tooClose = true; break; }
            }
            if ( tooClose ) continue;

            result.Add( (X: x, Y: y) );
            occupied.Add( (X: x, Y: y) );
        }
        return result;
    }

    // ── Lifecycle ───────────────────────────────────────────────
    protected override void OnStart()
    {
        base.OnStart();
        Panel.Style.PointerEvents = PointerEvents.All;
        ResetGame();
    }

    protected override void OnUpdate()
    {
        HandleInput();

        if ( Phase == GamePhase.Playing && !IsPaused )
        {
            TickAccum += Time.Delta;
            if ( TickAccum >= TickInterval )
            {
                TickAccum -= TickInterval;
                Tick();
            }
        }

        bool anyParticle = false;
        for ( int i = Particles.Count - 1; i >= 0; i-- )
        {
            var p = Particles[i];
            p.Life -= Time.Delta;
            if ( p.Life <= 0f ) { Particles.RemoveAt( i ); continue; }
            p.X    += p.Vx * Time.Delta;
            p.Y    += p.Vy * Time.Delta;
            p.Alpha = p.Life / p.MaxLife;
            anyParticle = true;
        }
        if ( anyParticle ) StateHasChanged();
    }

    // ── Input ───────────────────────────────────────────────────
    void HandleInput()
    {
        if ( LineGrowInput.Escape )
        {
            if ( Phase == GamePhase.Playing )
            {
                IsPaused = !IsPaused;
                StateHasChanged();
            }
            return;
        }

        if ( IsPaused ) return;

        if ( Phase != GamePhase.Playing )
        {
            if ( LineGrowInput.AnyDir ) StartGame();
            return;
        }

        if ( LineGrowInput.Up )    TrySetDir( Direction.Up );
        if ( LineGrowInput.Down )  TrySetDir( Direction.Down );
        if ( LineGrowInput.Left )  TrySetDir( Direction.Left );
        if ( LineGrowInput.Right ) TrySetDir( Direction.Right );
    }

    void TrySetDir( Direction d )
    {
        var opp = Dir switch
        {
            Direction.Up    => Direction.Down,
            Direction.Down  => Direction.Up,
            Direction.Left  => Direction.Right,
            Direction.Right => Direction.Left,
            _               => Dir
        };
        if ( d != opp ) PendingDir = d;
    }

    // ── Game tick ───────────────────────────────────────────────
    void Tick()
    {
        Dir = PendingDir;
        var head = Snake[0];
        var next = Dir switch
        {
            Direction.Up    => (X: head.X, Y: head.Y - 1),
            Direction.Down  => (X: head.X, Y: head.Y + 1),
            Direction.Left  => (X: head.X - 1, Y: head.Y),
            Direction.Right => (X: head.X + 1, Y: head.Y),
            _               => head
        };

        // Wall collision
        if ( next.X < 0 || next.X >= Cols || next.Y < 0 || next.Y >= Rows ) { Die(); return; }

        // Self collision
        for ( int i = 1; i < Snake.Count; i++ )
            if ( Snake[i].X == next.X && Snake[i].Y == next.Y ) { Die(); return; }

        // Obstacle collision
        foreach ( var obs in Obstacles )
            if ( obs.X == next.X && obs.Y == next.Y ) { Die(); return; }

        Snake.Insert( 0, next );
        if ( Snake.Count > HighScore ) HighScore = Snake.Count;

        SpawnParticles( next.X * CellPx + HalfCell, next.Y * CellPx + HalfCell, 4, 30f, 0.4f );
        StateHasChanged();
    }

    // ── Particles ───────────────────────────────────────────────
    void SpawnParticles( float cx, float cy, int count, float speed, float life )
    {
        var rng = new Random();
        for ( int i = 0; i < count; i++ )
        {
            float angle = (float)(rng.NextDouble() * Math.PI * 2);
            float spd   = (float)(rng.NextDouble() * speed + speed * 0.3f);
            float lt    = (float)(rng.NextDouble() * life * 0.5f + life * 0.5f);
            Particles.Add( new Particle
            {
                X = cx - 3f, Y = cy - 3f,
                Vx = (float)Math.Cos( angle ) * spd,
                Vy = (float)Math.Sin( angle ) * spd,
                Alpha = 1f, Life = lt, MaxLife = lt
            } );
        }
        if ( Particles.Count > 150 )
            Particles.RemoveRange( 0, Particles.Count - 150 );
    }

    void Die()
    {
        Phase = GamePhase.Dead;
        DeathFlash = true;
        if ( Snake.Count > 0 )
        {
            var h = Snake[0];
            SpawnParticles( h.X * CellPx + HalfCell, h.Y * CellPx + HalfCell, 24, 80f, 0.7f );
        }
        StateHasChanged();
    }

    // ── Game state ───────────────────────────────────────────────
    void ResetGame()
    {
        Snake = new List<(int X, int Y)> { (X: 7, Y: 11), (X: 6, Y: 11), (X: 5, Y: 11) };
        Dir = Direction.Right; PendingDir = Direction.Right;
        DeathFlash = false; IsPaused = false;
        Particles.Clear();
        Obstacles.Clear();
        Phase = GamePhase.Idle;
        StateHasChanged();
    }

    void StartGame()
    {
        var rng = new Random();
        Snake = new List<(int X, int Y)> { (X: 7, Y: 11), (X: 6, Y: 11), (X: 5, Y: 11) };
        Dir = Direction.Right; PendingDir = Direction.Right;
        DeathFlash = false; IsPaused = false;
        Particles.Clear();
        Obstacles = GenerateObstacles( rng );
        Phase = GamePhase.Playing;
        StateHasChanged();
    }

    void QuitToMenu() { Game.Overlay.ShowPauseMenu(); }
}