Common.psm1

Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
namespace PSClearHost {
public class Native
{
    [DllImport("user32.dll")]
    public static extern short GetAsyncKeyState(int virtualKeyCode);
    public static void ClearBuffer(
        System.Management.Automation.Host.BufferCell[,] buffer,
        System.ConsoleColor foregroundColor,
        System.ConsoleColor backgroundColor)
    {
        int width = buffer.GetUpperBound(1);
        int height = buffer.GetUpperBound(0);
        var clearCell = new System.Management.Automation.Host.BufferCell(
            ' ',
            foregroundColor,
            backgroundColor,
            System.Management.Automation.Host.BufferCellType.Complete);
        for (int x = 0; x < width; ++x)
        {
            for (int y = 0; y < height; ++y)
            {
                buffer[y, x] = clearCell;
            }
        }
    }
}
}
"@


class RenderItem
{
    [System.Collections.Generic.List[System.Management.Automation.Host.BufferCell]]$cells = $null
    [Double]$x = 0.0
    [Double]$y = 0.0
    [Boolean]$isFinished = $false

    RenderItem($cell, $cellTrailing, $x, $y)
    {
        $this.cells = [System.Collections.Generic.List[System.Management.Automation.Host.BufferCell]]::new()
        $this.cells.Add($cell)
        if ($cellTrailing)
        {
            $this.cells.Add($cellTrailing)
        }
        $this.x = $x
        $this.y = $y
    }

    [Boolean] IsTwoCell()
    {
        return ($this.cells.Count -eq 2)
    }

    [void] Finish()
    {
        $this.isFinished = $true
    }

    [void] Render([System.Management.Automation.Host.BufferCell[,]]$buffer)
    {
        if ($this.isFinished)
        {
            return
        }

        $ix = [Int]$this.x
        $iy = [Int]$this.y

        $buffer[$iy, $ix] = $this.cells[0]
        if ($this.cells.Count -gt 1)
        {
            $buffer[$iy, ($ix+1)] = $this.cells[1]
        }
    }

    [void] SetCell($character, $foregroundColor, $backgroundColor)
    {
        foreach ($i in @(0..($this.cells.Count-1)))
        {
            $newCell = $this.cells[$i]
            if ($null -ne $character)
            {
                $newCell.Character = $character
                $newCell.BufferCellType = [System.Management.Automation.Host.BufferCellType]::Complete
            }
            if ($null -ne $foregroundColor)
            {
                $newCell.ForegroundColor = $foregroundColor
            }
            if ($null -ne $backgroundColor)
            {
                $newCell.BackgroundColor = $backgroundColor
            }
            $this.cells[$i] = $newCell
        }
    }
}

class LinearAnimation
{
    [Object[]]$values = $null
    [Double]$duration = 0.0
    [Double]$timer = 0.0

    LinearAnimation($values, $duration)
    {
        $this.values = $values
        $this.duration = $duration
    }

    [void] Update($dt)
    {
        $this.timer += $dt
    }

    [Object] GetValue()
    {
        [Int]$index = $this.values.Length * ($this.timer / $this.duration)
        $index = [Math]::Min($index, $this.values.Length-1)

        return $this.values[$index]
    }

    [Double] GetRatio()
    {
        return [Math]::Min($this.timer / $this.duration, 1.0)
    }

    [Boolean] IsFinished()
    {
        return ($this.timer -ge $this.duration)
    }
}

class RandomDouble
{
    static [System.Random]$rand = (New-Object System.Random)
    [Double]$min = 0.0
    [Double]$diff = 0.0

    RandomDouble($min, $max)
    {
        $this.min = $min
        $this.diff = $max - $min
    }

    [Double] Get()
    {
        return $this.min + [RandomDouble]::rand.NextDouble() * $this.diff
    }
}

class RandomInt
{
    [Int]$min = 0
    [Int]$max = 0

    RandomInt($min, $max)
    {
        $this.min = $min
        $this.max = $max + 1
    }

    [Int] Get()
    {
        return [RandomDouble]::rand.Next($this.min, $this.max)
    }
}

function Play($playerClass, $speed, $debugSkipRender = $false)
{
    ClearKeyHold ([ConsoleKey]::Q)
    $prevCursorVisible = [Console]::CursorVisible
    [Console]::CursorVisible = $false

    $framerate = 60.0
    $dt = 1.0 / $framerate
    $vsyncTimer = New-Object System.Diagnostics.StopWatch
    $dtTimer = New-Object System.Diagnostics.StopWatch

    $windowSize = $host.UI.RawUI.WindowSize
    $windowPosition = $host.UI.RawUI.WindowPosition
    $windowWidth = $windowSize.Width
    $windowHeight = $windowSize.Height

    $windowRect = New-Object System.Management.Automation.Host.Rectangle(
        $windowPosition.X,
        $windowPosition.Y,
        ($windowPosition.X + $windowWidth),
        ($windowPosition.Y + $windowHeight))

    $windowBuffer = $host.UI.RawUI.GetBufferContents($windowRect)

    $playerClass::Init($windowBuffer)

    $quit = $false
    while ($true)
    {
        ClearBuffer $windowBuffer

        $dtTimer.Stop()
        if ($dtTimer.Elapsed.TotalSeconds -ne 0)
        {
            $dt = $dtTimer.Elapsed.TotalSeconds
        }
        $dtTimer.Reset()
        $dtTimer.Start()

        $finished = $playerClass::Render($windowBuffer, $dt, $speed)
        if (GetKeyHold ([ConsoleKey]::Q))
        {
            $finished = $true
            $quit = $true
        }
        $vsyncTimer.Stop()

        if ($vsyncTimer.Elapsed.TotalMilliseconds -ne 0)
        {
            $sleepMilliseconds = [Math]::Max((1000.0/$framerate) - $vsyncTimer.Elapsed.TotalMilliseconds, 0)
            if ($sleepMilliseconds -gt 0)
            {
                Start-Sleep -Millisecond $sleepMilliseconds
            }
        }

        $vsyncTimer.Reset()
        $vsyncTimer.Start()
        if (-not $debugSkipRender)
        {
            $host.UI.RawUI.SetBufferContents($windowPosition, $windowBuffer)
        }

        if ($finished)
        {
            $vsyncTimer.Stop()
            $dtTimer.Stop()
            break
        }
    }

    $playerClass::Term()

    if (-not $quit)
    {
        Start-Sleep -Millisecond 500
    }

    Clear-Host
    $host.UI.RawUI.FlushInputBuffer()
    [Console]::CursorVisible = $prevCursorVisible
}

function CreateCharacters($characterClass, $windowBuffer)
{
    $characters = [System.Collections.Generic.List[PSObject]]::new()

    $width = $windowBuffer.GetUpperBound(1)
    $height = $windowBuffer.GetUpperBound(0)
    $bgColor = $host.UI.RawUI.BackgroundColor

    foreach ($x in @(0..($width-1)))
    {
        foreach ($y in @(0..($height-1)))
        {
            $cell = $windowBuffer[$y, $x]
            if ($cell.BufferCellType -eq [System.Management.Automation.Host.BufferCellType]::Trailing)
            {
                continue
            }

            if (($cell.Character -ne " ") -or
                ($cell.BackgroundColor -ne $bgColor))
            {
                $cellTrailing = $null
                if ($cell.BufferCellType -eq [System.Management.Automation.Host.BufferCellType]::Leading)
                {
                    $cellTrailing = $windowBuffer[$y, ($x+1)]
                }
                $character = $characterClass::new($cell, $cellTrailing, $x, $y)
                $characters.Add($character)
            }
        }
    }

    $characters
}

function ClearBuffer([System.Management.Automation.Host.BufferCell[,]]$buffer)
{
    $bgColor = $host.UI.RawUI.BackgroundColor
    [PSClearHost.Native]::ClearBuffer($buffer, $bgColor, $bgColor)
}

function GetKeyHold([ConsoleKey]$consoleKey)
{
    $state = [PSClearHost.Native]::GetAsyncKeyState([Int]$consoleKey)
    [Boolean]$state
}

function ClearKeyHold([ConsoleKey]$consoleKey)
{
    [PSClearHost.Native]::GetAsyncKeyState([Int]$consoleKey) | Out-Null
}

function Limit($x, $min, $max)
{
    [Math]::Min([Math]::Max($x, $min), $max)
}

function Lerp($a, $b, $t)
{
    $a * (1.0 - $t) + $b * $t
}