functions/utility/New-MiniGameDrawUtility.ps1
|
function New-MiniGameDrawUtility { <# .SYNOPSIS This return a utility class for drawing text on the screen. - Use New-MiniGame for creating a game loop. - The draw utility only draws on the screen, but does not handle any game logic. .DESCRIPTION This return a utility class for drawing text on the screen. Text can be single or multiline by passing a [System.String[]]-Array. By default X=0,Y=0 points to the top-rightmost-corner. Both Axis can be inverted. Foreground and Background Colors can be changed to: - predefined colors Red, Green, Yellow, etc. - a 8-Bit value of predefined colors. - passing RGB values for a specific color. Can defined custom boundaries as the canvas. (Defaults to full screen) Inside the main boundary, sub-boundaries can be created for: - checking objects to be out of bounds. - drawing objects relative to some sub-boundary. .OUTPUTS .EXAMPLE Create a new MiniGameDrawUtility object and draw a text in color on the screen: PS> $canvas = New-MiniGameDrawUtility $canvas.ForeGround.setColor("Red") $canvas.draw(10, 1, "Hello World 1") $canvas.draw(10, 2, "Hello World 2") $canvas.ForeGround.Reset() # Only apply color once to the next draw. $canvas.ForeGround.onceColor("Cyan") $canvas.draw(10, 4, "Hello World 3") $canvas.draw(10, 5, "Hello World 4") # Undraw text on the position. # $canvas.undraw(10, 25, "Hello World 3") # Draw multiline text. $canvas.draw(10, 7, @( "Hello World 4", "Hello World 5" )) .LINK #> [CmdletBinding()] param ( [Parameter( Position = 0, Mandatory = $false )] [System.Int32] $MaximumX, [Parameter( Position = 1, Mandatory = $false )] [System.Int32] $MaximumY, [Parameter( Position = 2, Mandatory = $false )] [System.Int32] $MinimumX = 0, [Parameter( Position = 3, Mandatory = $false )] [System.Int32] $MinimumY = 0, # Inverts drawing on the Y-Axis. Normally Y=0 points to the top of the screen. # This is usefull for having elements fly from the bottom upwards. [Parameter()] [switch] $InvertY, # Inverts drawing on the X-Axis. Normally X=0 points to the left side of the screen. # Might not be useful but complement to InvertY $InvertX, # This automatically trims empty spaces from inputet strings. [Parameter( Mandatory = $false )] [System.Boolean] $TrimSpaces = $true ) # If no values are provided, use the current buffer size. # TODO: Maybe make it dynamic when the screen size changes. if ($null -EQ $PSBoundParameters['MaximumX'] -OR $MaximumX -LT 0) { $MaximumX = $host.UI.RawUI.BufferSize.Width } if ($null -EQ $PSBoundParameters['MaximumY'] -OR $MaximumY -LT 0) { $MaximumY = $host.UI.RawUI.BufferSize.Height } class MiniGameDrawUtilityGraphics { [System.String] $_mode = $null [System.String] $_code = $null [System.Boolean] $_autoReset = $false [System.Object] $_colorCodes = @{ FOREGROUND = $PSStyle.Foreground BACKGROUND = $PSStyle.Background } [void] Reset() { $this._code = $null $this._autoReset = $false } [void] _draw() { if ($null -EQ $this._code) { return } [System.Console]::Write($this._code) if ($this._autoReset) { $this.Reset() } } MiniGameDrawUtilityGraphics([System.String] $mode) { $this._mode = $mode } <# Methods for setting colors Once: Resets automatically after draw has been called Set: Keeps color permanent, until call of Reset() or setting another color. #> [void] setColor([System.String] $color) { $this._code = $this._colorCodes."$($this._mode)"."$color" if ($null -EQ $this._code) { throw "Color $color not found in `$PSStyle." } } [void] OnceColor([System.String] $color) { $this.setColor($color) $this._autoReset = $true } [void] Once8Bit([System.Byte] $byte) { $this.set8Bit($byte) $this._autoReset = $true } [void] Set8Bit([System.Byte] $byte) { $mode = $this._mode -EQ "FOREGROUND" ? 38 : 48 $this._code = "`e[$($mode);5;${byte}m" } [void] OnceRGB([System.Byte[]]$rgb) { $this.setRGB($rgb[0], $rgb[1], $rgb[2]) $this._autoReset = $true } [void] OnceRGB( [System.Byte] $red, [System.Byte] $green, [System.Byte] $blue ) { $this.setRGB($red, $blue, $green) $this._autoReset = $true } [void] SetRGB([System.Byte[]] $rgb) { $this.SetRGB($rgb[0], $rgb[1], $rgb[2]) } [void] SetRGB( [System.Byte] $red, [System.Byte] $green, [System.Byte] $blue ) { $mode = $this._mode -EQ "FOREGROUND" ? 38 : 48 $this._code = "`e[$($mode);2;${red};${green};${blue}m" } } <# This defines a boundary on the terminal. Boundaries can be used for: - Out-Of-Bounds Checks - Drawing inside a boundary Each boundary can have an offset, which allows multiple different boundaries. The canvases __default__ boundary has an offset of X=0;Y=0. #> class MiniGameDrawUtilityBounds { [System.String] $Name [System.Numerics.Vector2] $Offset [System.Numerics.Vector2] $Minimum [System.Numerics.Vector2] $Maximum [System.Numerics.Vector2] $AbsMinimum [System.Numerics.Vector2] $AbsMaximum [MiniGameDrawUtilityBounds] Init() { $this.AbsMinimum = [System.Numerics.Vector2]@{ X = $this.Offset.X + $this.Minimum.X Y = $this.Offset.Y + $this.Minimum.Y } $this.AbsMaximum = [System.Numerics.Vector2]@{ X = $this.Offset.X + $this.Maximum.X Y = $this.Offset.Y + $this.Maximum.Y } return $this } [System.Boolean] IsOutside( [System.Int32] $x, [System.Int32] $y ) { return $this.Offset.X + $x -GT $this.AbsMaximum.X -OR $this.Offset.Y + $y -GT $this.AbsMaximum.Y -OR $this.Offset.X + $x -LT $this.AbsMinimum.X -OR $this.Offset.Y + $y -LT $this.AbsMinimum.Y } [System.Numerics.Vector2] RandomPos() { $randomX = Get-Random -Minimum ($this.Minimum.X + 0) -Maximum ($this.Maximum.X) $randomY = Get-Random -Minimum ($this.Minimum.Y + 0) -Maximum ($this.Maximum.Y) return [System.Numerics.Vector2]@{ X = [System.Math]::Round($randomX) Y = [System.Math]::Round($randomY) } } [System.Numerics.Vector2] AbsRandomPos([System.Numerics.Vector2] $pos) { $randomX = Get-Random -Minimum ($this.AbsMinimum.X + 0) -Maximum ($this.AbsMaximum.X + 1) $randomY = Get-Random -Minimum ($this.AbsMinimum.Y + 0) -Maximum ($this.AbsMaximum.Y + 1) return [System.Numerics.Vector2]@{ X = [System.Math]::Round($randomX) Y = [System.Math]::Round($randomY) } } } class MiniGameDrawUtilityBoundsManager { # All boundaries are set relative to the canvas __default__ boundary. [MiniGameDrawUtilityBounds] $Current [System.Collections.Hashtable] $Bounds = @{} [MiniGameDrawUtilityBounds] $Default = [MiniGameDrawUtilityBounds]@{ Name = '__default__' Offset = [System.Numerics.Vector2]::Zero Minimum = [System.Numerics.Vector2]@{X = $MinimumX; Y = $MinimumY } Maximum = [System.Numerics.Vector2]@{X = $MaximumX; Y = $MaximumY } } MiniGameDrawUtilityBoundsManager() { $this.Current = $this.Default $this.Bounds.Add($this.Current.Name, $this.Current) } <# Adds boundaries inside the __default__ canvas boundary. #> [MiniGameDrawUtilityBounds] Get([System.String] $name) { return $this.Bounds[$name] } [void] Add( [System.String] $name, [System.Int32] $minX, [System.Int32] $minY, [System.Int32] $maxX, [System.Int32] $maxY ) { $bound = [MiniGameDrawUtilityBounds]@{ Name = $name Offset = $this.Default.Minimum Minimum = [System.Numerics.Vector2]@{X = $minX; Y = $minY } Maximum = [System.Numerics.Vector2]@{X = $maxX; Y = $maxY } } $this.Bounds.Remove($name) $this.Bounds.Add($name, $bound.Init()) } [void] Add( [System.String] $name, [System.Numerics.Vector2] $min, [System.Numerics.Vector2] $max ) { $this.Add($name, $min.X, $min.Y, $max.X, $max.Y) } [void] Set([System.String] $name) { $this.Current = $this.Bounds[$name] } [void] Reset() { $this.Set($this.Default.Name) } <# Out of Bounds checks. #> [System.Boolean] IsOutside( [System.String] $name, [System.Int32] $x, [System.Int32] $y ) { return $this.Bounds[$name].IsOutside($x, $y) } [System.Boolean] IsOutside( [System.Int32] $x, [System.Int32] $y ) { return $this.IsOutside($this.Current.name, $X, $Y) } [System.Boolean] IsOutside( [System.String] $name, [System.Numerics.Vector2] $pos ) { return $this.IsOutside($name, $pos.X, $pos.Y) } [System.Boolean] IsOutside( [System.Numerics.Vector2] $pos ) { return $this.IsOutside($this.Current.name, $pos.X, $pos.Y) } <# These methods are used to move a vector on the screen relative to the bounds. #> [System.Numerics.Vector2] Move( [System.Numerics.Vector2] $position, [System.Int32] $x, [System.Int32] $y ) { return $this.Move( [System.Numerics.Vector2]@{ X = $position.X + $x; Y = $position.Y + $y } ) } [System.Numerics.Vector2] Move( [System.Numerics.Vector2] $position, [System.Numerics.Vector2] $move ) { $position.Add($move) if ($position.X -LT $this.Current.Minimum.X) { $position.X = $this.Current.Maximum.X } elseif ($position.X -GT $this.Current.Maximum.X) { $position.X = $this.Current.Minimum.X } if ($position.Y -LT $this.Current.Minimum.Y) { $position.Y = $this.Current.Maximum.Y } elseif ($position.Y -GT $this.Current.Maximum.Y) { $position.Y = $this.Current.Minimum.Y } return $position } } <# ////////////////////////////////////////////////////////////////////////////////////// This is the main class for drawing text on the screen. - It uses the MiniGameDrawUtilityGraphics class for setting colors and drawing text. - The MiniGameDrawUtilityBoundsManager class is used for managing boundaries. - The default boundary is the full screen. #> class MiniGameDrawUtility { [System.Boolean] $InvertX = $InvertX.IsPresent [System.Boolean] $InvertY = $InvertY.IsPresent [System.Boolean] $_trimSpaces = $TrimSpaces [MiniGameDrawUtilityGraphics] $ForeGround = [MiniGameDrawUtilityGraphics]::new("FOREGROUND") [MiniGameDrawUtilityGraphics] $BackGround = [MiniGameDrawUtilityGraphics]::new("BACKGROUND") [MiniGameDrawUtilityBoundsManager] $Bounds = [MiniGameDrawUtilityBoundsManager]@{} [void] Clear() { [System.Console]::Clear() } [void] ResetGraphics() { $this.ForeGround.Reset() $this.BackGround.Reset() } <# These methods draw text on the screen, offset from the right side. #> [void] DrawRight( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.DrawRight($pos.X, $pos.Y, $text) } [void] DrawRight( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this.Draw(($this.Maximum.X - $this.Minimum.X) - $text[0].Length - $x, $y, $text) } [void] UndrawRight( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.Undraw($pos.X, $pos.Y, $text) } [void] UndrawRight( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this.Undraw(($this.Maximum.X - $this.Minimum.X) - $text[0].Length - $x, $y, $text) } <# These methods draw text on the screen, offset from the bottom side. - Y=0 draws the first line of text at the bottom of the screen. any following lines are outside of the screen. For multiline text, the $y needs to be adjusted by the number of lines. - Y=1 draws two lines with - the first line being one above the bottom of the screen. - the second line being at the bottom of the screen. #> [void] DrawBottom( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this.Draw($x, $this.Maximum.Y - $y - 1, $text) } [void] DrawBottom( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.DrawBottom($pos.X, $pos.Y, $text) } [void] UndrawBottom( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this.Undraw($x, $this.Maximum.Y - $y - 1, $text) } [void] UndrawBottom( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.UndrawBottom($pos.X, $pos.Y, $text) } <# These methods are used to draw text on the screen. NOTE: When TrimSpaces=$true, drawing empty spaces won't have effect. Either the element on screen - has to be overdrawn. - or undraw() has to be used. #> [void] Draw( [System.String[]] $text ) { $this.Draw(0, 0, $text) } [void] Draw( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.Draw($pos.X, $pos.Y, $text) } [void] Draw( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this.ForeGround._draw() $this.BackGround._draw() $this._internalDraw($x, $y, $text, $false) [System.Console]::Write("`e[0m") } <# These methods are used to delete text from the screen. #> [void] Undraw( [System.String[]] $text ) { $this.Undraw(0, 0, $text) } [void] Undraw( [System.Numerics.Vector2] $pos, [System.String[]] $text ) { $this.Undraw($pos.X, $pos.Y, $text) } [void] Undraw( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text ) { $this._internalDraw($x, $y, $text, $true) } <# This is an internal method for drawing. #> [void] _internalDraw( [System.Int32] $x, [System.Int32] $y, [System.String[]] $text, [System.Boolean] $undraw ) { # These are the starting points for drawing the text. $canvasX = $this.Minimum.X + $x $canvasY = $this.Minimum.y + $y if ($this.InvertY) { $canvasY = $this.Maximum.Y - ($canvasY + 1) } if ($this.InvertX) { $canvasX = $this.Maximum.X - ($canvasX + 1) } # - In normal mode: (0,0) should be top most of screen and (0,maxY-1) should be bottom of the screen. # - In inverted mode: (0,0) should be the bottom most of the screen and (0,minY-1) should be the topmost of the screen. <# # 0 is topmost of screen. # If it goes out of the screen upwards only the lowest part gets drawn at Y=-3 line 0: 0 line 1: /|\ line 2: A ObjectHeight: 3 - drawing starts from line 0 to 2 At Y= 0 => the whole object is drawn At Y=-1 => 2 Parts of object is drawn At Y=-2 => 1 Part of object is drawn At Y=-3 => 0 Parts of object is drawn #> <# # Max is bottommost of screen. # If it goes out of the screen downwards only the upper part gets drawn at Y=Max-1 line 0: 0 line 1: /|\ line 2: A ScreenMax is 13 (Example) ObjectHeight: 3 - drawing starts from line 0 to 2 At Y= 9 => the whole object is drawn At Y=10 => 2 Parts of object is drawn At Y=11 => 1 Part of object is drawn At Y=12 => 0 Parts of object is drawn #> for ($index = 0; $index -LT $text.Count; $index++) { $line = $text[$index] $drawY = $canvasY + $index $drawX = $canvasX if ($drawY -LT $this.Minimum.Y) { continue } elseif ($drawY -GE $this.Maximum.Y) { continue } # This removes whitespace, but offsets the drawnX by the removed whitespaces. if ($this._trimSpaces) { $line = $line.TrimStart() $drawX += $text[$index].Length - $line.Length $line = $line.TrimEnd() } # Skip drawing when fully outside of the left screen. if (($drawX + $line.Length) -LT $this.Minimum.X) { continue } # Handle line only being partially outside of the left screen. elseif ($drawX -LT $this.Minimum.X) { # 0 is leftmost of the screen # If it goes out of the screen to the left the rightmost part is drawn. $diff = $this.Minimum.X - $drawX $line = $line.Substring($diff) # DrawX needs to be adjusted to be still inside of the screen. $drawX = $this.Minimum.X } # Skip drawing when fully outside of the right screen. if ($drawX -GE $this.Maximum.X) { continue } # Handle line only being partially outside of the right screen. elseif (($drawX + $line.Length) -GT $this.Maximum.X) { # MaxX-1 is the rightmost of the screen # If it goes out of the screen to the right, the leftmost part is drawn. $line = $line.Substring(0, $this.Maximum.X - $drawX) } if ($undraw) { [System.Console]::SetCursorPosition($drawX, $drawY) [System.Console]::Write(" " * $line.Length) } else { [System.Console]::SetCursorPosition($drawX, $drawY) [System.Console]::Write($line) } } } } if ( $null -EQ (Get-TypeData -TypeName 'MiniGameDrawUtility').Members.Minimum ) { Update-TypeData -TypeName 'MiniGameDrawUtility' -MemberName 'Minimum' -Value { return $this.Bounds.Current.Minimum } -MemberType ScriptProperty } if ( $null -EQ (Get-TypeData -TypeName 'MiniGameDrawUtility').Members.Maximum ) { Update-TypeData -TypeName 'MiniGameDrawUtility' -MemberName 'Maximum' -Value { return $this.Bounds.Current.Maximum } -MemberType ScriptProperty } return [MiniGameDrawUtility]@{} } |