Private/Bricks.ps1

using module .\Common.psm1

class Bricks
{
    static $kBallSpeed = 60
    static $kBarSpeed = 70
    static $kBarVelocityInfluenceAccelTime = 0.6
    static $kExplosionDuration = 0.6
    static $kExplosionPropagationDelayTime = [RandomDouble]::new(0.2, 0.3)

    static $foregroundColor = $null
    static $backgroundColor = $null
    static $windowWidth = 0
    static $windowHeight = 0

    static $characters = $null
    static $cellToCharacter = $null
    static $ball = $null
    static $bar = $null
    static $explosionsToAdd = $null
    static $explosions = $null

    static [void] Init($windowBuffer)
    {
        [Bricks]::foregroundColor = (Get-Host).UI.RawUI.ForegroundColor
        [Bricks]::backgroundColor = (Get-Host).UI.RawUI.BackgroundColor

        [Bricks]::CreateCharacters($windowBuffer)
        [Bricks]::CreateBall()
        [Bricks]::CreateBar()
        [Bricks]::explosionsToAdd = [System.Collections.Generic.List[PSObject]]::new()
        [Bricks]::explosions = [System.Collections.Generic.List[PSObject]]::new()
    }

    static [void] Term()
    {
        [Bricks]::characters = $null
        [Bricks]::ball = $null
        [Bricks]::bar = $null
        [Bricks]::explosions = $null
        [Bricks]::explosionsToAdd = $null
    }

    static [Boolean] Render([System.Management.Automation.Host.BufferCell[,]]$windowBuffer, [Double]$dt, [Double]$speed)
    {
        if (GetKeyHold ([ConsoleKey]::LeftArrow))
        {
            [Bricks]::bar.MoveLeft()
        }
        elseif (GetKeyHold ([ConsoleKey]::RightArrow))
        {
            [Bricks]::bar.MoveRight()
        }

        [Bricks]::bar.Update($dt)
        [Bricks]::ball.Update($dt * $speed)

        foreach ($explosion in [Bricks]::explosions)
        {
            $explosion.Update($dt * $speed)
        }
        foreach ($explosion in [Bricks]::explosionsToAdd)
        {
            [Bricks]::explosions.Add($explosion)
        }
        [Bricks]::explosionsToAdd.Clear()

        $isAllFinished = $true
        foreach ($character in [Bricks]::characters)
        {
            if (-not $character.isFinished)
            {
                $isAllFinished = $false
                break
            }
        }
        foreach ($explosion in [Bricks]::explosions)
        {
            if (-not $explosion.isFinished)
            {
                $isAllFinished = $false
                break
            }
        }

        if ($isAllFinished)
        {
            return $true
        }

        foreach ($character in [Bricks]::characters)
        {
            $character.Render($windowBuffer)
        }
        [Bricks]::ball.Render($windowBuffer)
        [Bricks]::bar.Render($windowBuffer)
        foreach ($explosion in [Bricks]::explosions)
        {
            $explosion.Render($windowBuffer)
        }

        return $false
    }

    static [void] CreateCharacters($windowBuffer)
    {
        [Bricks]::windowWidth = $windowBuffer.GetUpperBound(1)
        [Bricks]::windowHeight = $windowBuffer.GetUpperBound(0)

        [Bricks]::characters = CreateCharacters ([BricksCharacter]) $windowBuffer

        [Bricks]::cellToCharacter = New-Object 'Object[,]' ([Bricks]::windowWidth, [Bricks]::windowHeight)
        foreach ($character in [Bricks]::characters)
        {
            [Bricks]::cellToCharacter[$character.x, $character.y] = $character
            if ($character.IsTwoCell())
            {
                [Bricks]::cellToCharacter[($character.x+1), $character.y] = $character
            }
        }
    }

    static [void] CreateBall()
    {
        $x = [Int]([Bricks]::windowWidth / 2)
        $y = [Bricks]::windowHeight - 3
        [Bricks]::ball = [BricksBall]::new($x, $y)
    }

    static [void] CreateBar()
    {
        $width = 20
        $x = [Int](([Bricks]::windowWidth - $width) / 2)
        $y = [Int]([Bricks]::windowHeight - 2)
        [Bricks]::bar = [BricksBar]::new($x, $y, $width)
    }

    static [void] CreateExplosion($character, $isBackgroundColorAnimation)
    {
        [Bricks]::explosionsToAdd.Add([Explosion]::new($character, $isBackgroundColorAnimation))
    }

    static [Object] GetCharacter($x, $y)
    {
        if (($x -lt 0) -or ($x -ge ([Bricks]::windowWidth)))
        {
            return $null
        }

        if (($y -lt 0) -or ($y -ge ([Bricks]::windowHeight)))
        {
            return $null
        }

        $character = [Bricks]::cellToCharacter[$x, $y]
        if ($character -and $character.isFinished)
        {
            return $null
        }

        return $character
    }
}

class BricksCharacter : RenderItem
{
    BricksCharacter($cell, $cellTrailing, $x, $y)
        : base($cell, $cellTrailing, $x, $y)
    {
    }

    [void] Update($dt)
    {
    }
}

class BricksBall : RenderItem
{
    $velocity = @(0.0, 0.0)

    BricksBall($x, $y)
        : base((New-Object System.Management.Automation.Host.BufferCell(
            [char]0x2b24,
            [System.ConsoleColor]::Yellow,
            [Bricks]::backgroundColor,
            [System.Management.Automation.Host.BufferCellType]::Complete)),
            $null,
            $x, $y)
    {
        $this.SetVelocity(-1.0, -1.0)
    }

    [void] Update($dt)
    {
        # assume cell width and height ratio is 1:2
        $newX = $this.x + $this.velocity[0] * $dt
        $newY = $this.y + $this.velocity[1] * 0.5 * $dt

        $this.SetNewPos($newX, $newY)
    }

    [void] SetVelocity($x, $y)
    {
        $length = [Math]::Sqrt($x * $x + $y * $y);
        $this.velocity[0] = $x * [Bricks]::kBallSpeed / $length
        $this.velocity[1] = $y * [Bricks]::kBallSpeed / $length
    }

    [void] SetNewPos($newX, $newY)
    {
        $walls = @()
        $walls += $this.GetWalls($this.x, $newX, 0)
        $walls += $this.GetWalls($this.y, $newY, 1)
        $walls = $walls | Sort-Object -Property t

        $dx = $newX - $this.x
        $dy = $newY - $this.y
        $windowSize = [Bricks]::windowWidth, [Bricks]::windowHeight

        $iSearchCoord = [Int]$this.x, [Int]$this.y
        foreach ($wall in $walls)
        {
            $componentIndex = $wall.componentIndex
            $iSearchCoord[$componentIndex] = $wall.cellIndex

            $isHit = $false
            $barVelocityAndInfluence = $null
            if (($iSearchCoord[$componentIndex] -lt 0) -or ($iSearchCoord[$componentIndex] -ge $windowSize[$componentIndex]))
            {
                $isHit = $true
            }
            else
            {
                $character = [Bricks]::GetCharacter($iSearchCoord[0], $iSearchCoord[1])
                if ($character)
                {
                    $character.Finish()
                    [Bricks]::CreateExplosion($character, $true)
                    $isHit = $true
                }
                elseif ([Bricks]::bar.IsInside($iSearchCoord[0], $iSearchCoord[1]))
                {
                    $barVelocityAndInfluence = [Bricks]::bar.GetBallVelocityAndInfluence()
                    $isHit = $true
                }
            }

            if ($isHit)
            {
                $newVelocity = $this.velocity.Clone()
                $newVelocity[$componentIndex] = -$newVelocity[$componentIndex]
                if ($barVelocityAndInfluence)
                {
                    $barVelocityX = $barVelocityAndInfluence[0]
                    $barVelocityY = $barVelocityAndInfluence[1]
                    $barInfluence = $barVelocityAndInfluence[2]

                    $barVelocityY *= [Math]::Sign($newVelocity[1])

                    $newVelocity[0] = Lerp $newVelocity[0] $barVelocityX $barInfluence
                    $newVelocity[1] = Lerp $newVelocity[1] $barVelocityY $barInfluence
                }
                $this.SetVelocity($newVelocity[0], $newVelocity[1])

                $this.x = Limit ($this.x + $dx * $wall.t) 0 ([Bricks]::windowWidth-1)
                $this.y = Limit ($this.y + $dy * $wall.t) 0 ([Bricks]::windowHeight-1)
                return
            }
        }

        $this.x = $newX
        $this.y = $newY
    }

    [Object[]] GetWalls($currentCoord, $newCoord, $componentIndex)
    {
        $walls = @()
        $min = [Math]::Min($currentCoord, $newCoord)
        $max = [Math]::Max($currentCoord, $newCoord)
        $d = $newCoord - $currentCoord
        $sign = [Math]::Sign($d)

        $wallCoords = @(([Math]::Floor($min))..([Math]::Floor($max)))
        if ($wallCoords.Count -gt 1)
        {
            $wallCoords = $wallCoords[1..($wallCoords.Count-1)]
            foreach ($wallCoord in $wallCoords)
            {
                $t = ($wallCoord - $currentCoord) / $d
                $walls += @{componentIndex = $componentIndex; t = $t; cellIndex = ($wallCoord + $sign)}
            }
        }
        return $walls
    }
}

class BricksBar
{
    $x = 0
    $y = 0
    $width = 0
    $renderItems = $null
    $nextMoveDirection = 0
    $prevMoveDirection = 0
    $velocityInfluence = 0.0

    BricksBar($x, $y, $width)
    {
        $this.x = $x
        $this.y = $y
        $this.width = $width

        $color = [System.ConsoleColor]::Blue
        $firstCell = New-Object System.Management.Automation.Host.BufferCell(
            "(",
            $color,
            [Bricks]::backgroundColor,
            [System.Management.Automation.Host.BufferCellType]::Complete)

        $lastCell = New-Object System.Management.Automation.Host.BufferCell(
            ")",
            $color,
            [Bricks]::backgroundColor,
            [System.Management.Automation.Host.BufferCellType]::Complete)

        $middleCell = New-Object System.Management.Automation.Host.BufferCell(
            " ",
            [Bricks]::foregroundColor,
            $color,
            [System.Management.Automation.Host.BufferCellType]::Complete)

        $this.renderItems = New-Object System.Collections.ArrayList
        $this.renderItems.Add([RenderItem]::new($firstCell, $null, $x, $y)) | Out-Null
        for ($i = 1; $i -lt ($width-1); ++$i)
        {
            $this.renderItems.Add([RenderItem]::new($middleCell, $null, ($x + $i), $y)) | Out-Null
        }
        $this.renderItems.Add([RenderItem]::new($lastCell, $null, $x + ($width-1), $y)) | Out-Null
    }

    [void] Render($windowBuffer)
    {
        foreach ($item in $this.renderItems)
        {
            $item.Render($windowBuffer)
        }
    }

    [void] Update($dt)
    {
        if ($this.nextMoveDirection -eq 0)
        {
            $this.velocityInfluence = 0.0
        }
        elseif (($this.prevMoveDirection * $this.nextMoveDirection) -lt 0)
        {
            # Moved in opposite direction
            $this.velocityInfluence = 0.0
        }
        else
        {
            $this.velocityInfluence += $dt / [Bricks]::kBarVelocityInfluenceAccelTime
            $this.velocityInfluence = [Math]::Min($this.velocityInfluence, 1.0)
        }

        $this.x += $this.nextMoveDirection * [Bricks]::kBarSpeed * $dt
        $this.prevMoveDirection = $this.nextMoveDirection
        $this.nextMoveDirection = 0

        $maxX = [Bricks]::windowWidth - $this.width - 1
        if (($this.x -lt 0) -or ($this.x -gt $maxX))
        {
            $this.velocityInfluence = 0.0
        }
        $this.x = [Math]::Max($this.x, 0)
        $this.x = [Math]::Min($this.x, $maxX)

        for ($i = 0; $i -lt $this.width; ++$i)
        {
            $this.renderItems[$i].x = $this.x + $i
        }
    }

    [void] MoveLeft()
    {
        $this.nextMoveDirection = -1
    }

    [void] MoveRight()
    {
        $this.nextMoveDirection = 1
    }

    [Boolean] IsInside($x, $y)
    {
        if ($y -ne $this.y)
        {
            return $false
        }

        if ($x -lt $this.x)
        {
            return $false
        }

        if ($x -ge ($this.x + $this.width))
        {
            return $false
        }

        return $true
    }

    [Double[]] GetBallVelocityAndInfluence()
    {
        $velocityX = [Math]::Sign($this.prevMoveDirection) * [Bricks]::kBallSpeed
        $velocityY = 0.5 * [Bricks]::kBallSpeed
        return $velocityX, $velocityY, $this.velocityInfluence
    }
}


class Explosion : RenderItem
{
    $timer = 0.0
    $colorAnimation = $null
    $isBackgroundColorAnimation = $true
    $propagationDelayTime = 0.0

    static $kColors = @(
        [System.ConsoleColor]::Yellow,
        [System.ConsoleColor]::Yellow,
        [System.ConsoleColor]::Blue,
        [System.ConsoleColor]::DarkBlue
    )

    Explosion($character, $isBackgroundColorAnimation)
        : base(
            $character.cells[0],
            $character.cells[1],
            $character.x, $character.y)
    {
        $this.colorAnimation = [LinearAnimation]::new([Explosion]::kColors, [Bricks]::kExplosionDuration)
        $this.isBackgroundColorAnimation = $isBackgroundColorAnimation
        $this.propagationDelayTime = [Bricks]::kExplosionPropagationDelayTime.Get()
    }

    [void] Update($dt)
    {
        if ($this.isFinished)
        {
            return
        }

        $this.colorAnimation.Update($dt)
        $color = $this.colorAnimation.GetValue()
        if ($this.isBackgroundColorAnimation)
        {
            $this.SetCell($null, $null, $color)
        }
        else
        {
            $this.SetCell($null, $color, $null)
        }

        if ($this.colorAnimation.GetRatio() -gt $this.propagationDelayTime)
        {
            foreach ($offsetX in @(-1..1))
            {
                foreach ($offsetY in @(-1..1))
                {
                    $character = [Bricks]::GetCharacter($this.x + $offsetX, $this.y + $offsetY)
                    if ($character)
                    {
                        $character.Finish()
                        [Bricks]::CreateExplosion($character, $false)
                    }
                }
            }
        }

        if ($this.colorAnimation.IsFinished())
        {
            $this.Finish()
        }
    }
}