Add-Sprite.ps1

function Add-Sprite
{
    <#
    .Synopsis
        Adds a sprite
    .Description
        Adds a Sprite to the current level, display it for the first time, and register it for collision detection.
    .Example
        # Adds walls surrounding the screen.
        Add-Sprite -X 0 -Y 0 -Width $game.Width -Height 1 -Type Wall # Top
        Add-Sprite -X 0 -Y $game.Height -Width $game.Width -Height 1 -Type Wall # Bottom
        Add-Sprite -X 0 -Y 1 -Width 1 -Height ($game.Height -1) -Type Wall # Left
        Add-Sprite -X $game.Width -Y 1 -Width 1 -Height ($game.Height - 1) -Type Wall #Right
    .Link
        Move-Sprite
    .Link
        New-Sprite
    .Link
        Remove-Sprite
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Games Must Use the Host")]
    [OutputType([Nullable],[PSObject])]
    param(
    # The type of the sprite. The sprite type is used to group sprites and handle specific collisions.
    [Parameter(Position=0,ValueFromPipelineByPropertyName)]
    [string]
    $Type,

    # The X coordinate of the sprite.
    # If the X coordinate would not be visible, the sprite will not be rendered
    [Parameter(ValueFromPipelineByPropertyName)]
    [int]
    $X,

    # The X coordinate of the sprite.
    # If the Y coordinate would not be visible, the sprite will not be rendered
    [Parameter(ValueFromPipelineByPropertyName)]
    [int]
    $Y,

    # The name of the sprite.
    # Giving a sprite a name will declare it as a global variable.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Name,

    # The sprite content. This can be used for sprites that are a single line.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Content,

    # The sprite color.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $Color,

    # The sprite background color.
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $BackgroundColor,

    # The width of the sprite.
    # Supplying this and -Height will make the sprite a rectangle.
    [Parameter(ValueFromPipelineByPropertyName)]
    [int]
    $Width,

    # The height of the sprite
    # Supplying this and -Width will make the sprite a rectangle.
    [Parameter(ValueFromPipelineByPropertyName)]
    [int]
    $Height,

    # Additional properties of the sprite.
    # This can contain any custom information.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Properties')]
    [Collections.IDictionary]
    $Property,

    # Additional methods for the sprite
    # These can be used to dynamically create sprite behavior.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Collections.IDictionary]
    $Method,

    # If set, this will find randomized empty space to add the sprite.
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('WhereEver')]
    [switch]
    $Anywhere,

    # If set, returns the variable of the sprite.
    [Parameter(ValueFromPipelineByPropertyName)]
    [switch]
    $PassThru
    )


    process {
        #region Prepare to Splat
        # Most of the sprite creation is handled by New-Sprite
        $toSplat = @{} + $PSBoundParameters # so copy all parameters
        foreach ($NotToSplat in @('PassThru', 'Anywhere')) { # and remove -PassThru and -Anywhere
            $toSplat.Remove($NotToSplat) # (which are not parameters of New-Sprite)
        }
        #endregion Prepare to Splat

        #region Determine Where anywhere Is
        if ($Anywhere) { # If we want to put a sprite -Anywhere
            $randomizer = [random]::new() # Create a random number
            if ($game.CurrentLevel.SpatialMap.Count) { # If the spatial map has been initialized
                $tryCount = 100 # we'll give it a hundred tries
                $foundASpot = $false # to see if we find a spot.
                :NextSpot do {
                    $X = $randomizer.Next(3,$game.Width - 3) # Pick a random X,Y
                    $Y = $randomizer.Next(3, $game.Height - 3)

                    $coordinates = @(if ($Width -and $Height) { # If provided a -Width and -Height, make a of coordinates
                        for ($ex = $x; $ex -lt ($x + $Width); $ex++) {
                            for ($ey = $y; $ey -lt ($y + $Height); $ey++) {
                                [PSCustomObject]@{X=$ex;Y=$ey}
                            }

                        }
                    } else {
                        [PSCustomObject]@{X=$x;Y=$y}
                    })
                    $tryCount--
                    if ($tryCount -le 0) {
                        break
                    }
                    foreach ($c in $coordinates) { # Walk over each coordinate
                        if (Find-Sprite -X $c.X -Y $c.Y) { # if any sprites were found
                            continue NextSpot # move to the next spot
                            # (this way if the map is cluttered near x,y,
                            # we pick a new spot as soon as we know it's bad)
                        }
                    }
                    $foundASpot = $true # If we made it to here, we've found a spot
                } while (-not $foundASpot)
                if ($tryCount -le 0) {return } # If the try count is depleted, return.
            } else
            {
                # If we didn't have a spatial map yet, just pick a random spot a bit from the edge.
                $X = $randomizer.Next(3, $game.Width - 3)
                $Y = $randomizer.Next(3, $game.Height - 3)
            }

            $toSplat.X = $X
            $toSplat.Y = $y
        }
        #endregion Determine Where anywhere Is

        # Now, create our sprite.
        $newSprite = New-Sprite @toSplat

        if ($Name) # If the sprite was named
        {
            $ExecutionContext.SessionState.PSVariable.Set("Global:$name",$newSprite) # set a global variable
        }

        # If the sprite would be within the field of view, and we're not initializing a level
        if ($newSprite.X -ge 0 -and $newSprite.X -le $Host.UI.RawUI.WindowSize.Width -and
            $newSprite.Y -ge 0 -and $newSprite.Y -le $Host.UI.RawUI.WindowSize.Height -and
            -not $game.CurrentLevel.Initializing
        ) {
            # Write the sprite to the screen.
            [Console]::Write('' + [char]0x1b + '[25l' + (Out-String -InputObject $newSprite -Width 1kb).Trim())
            # (this way, a lot of initial sprite draws can be buffered to improve performance)
        }

        #region Put the Sprite in its Place
        if ($game.CurrentLevel.Sprites.Add) {              # Assuming we have a sprite collection
            $newSpriteSpatialHash = $newSprite.SpatialHash # get the spatial hash of the sprite
            if ($game.CurrentLevel.SpatialMap -and         # If there is a spatial map
                $newSpriteSpatialHash                      # and we have at least one spatial hash
            )
            {
                foreach ($sh in $newSpriteSpatialHash) {   # Walk the spatial hashes
                    # (sprites could be in more than one)

                    # If the spatial hash wasn't already in the map
                    if (-not $game.CurrentLevel.SpatialMap.ContainsKey($sh)) {
                        # they may be off the gamespace and into the ether,
                        # but they may want a world bigger than their screen,
                        # so create a new spatial map bucket.
                        $game.CurrentLevel.SpatialMap[$sh] = [Collections.Generic.List[PSObject]]::new()
                    }
                    # Create a sprite reference.
                    $newSpriteRef =
                        [PSCustomObject]@{PSTypeName='PowerArcade.Sprite.Reference';Type=$type;SpriteID=$newSprite.SpriteID}

                    # Sprite References allow us to safely see the map in terms of a type and ID,
                    # and minimize the data we store.

                    # So we add the sprite reference to the spatial map, so it can be hit.
                    $game.CurrentLevel.SpatialMap[$sh].Add($newSpriteRef)
                }
            }

            $game.CurrentLevel.SpritesById[$newSprite.SpriteID] = $newSprite
            $game.CurrentLevel.Sprites.Add($newSprite)
        }
        #endregion Put the Sprite in its Place

        if (-not $game.CurrentLevel.Sprites.Add -or $PassThru) {
            $newSprite
        }
    }
}