functions/utility/New-MiniGameCollisionUtility.ps1

function New-MiniGameCollisionUtility {
    <#
    .SYNOPSIS
    This return a utility class for detecting collisions.
 
    .DESCRIPTION
    This return a utility class for detecting collisions.
 
    Use the New-MiniGameRunUtility or New-MiniGameObjectUtility for effective use.
 
    .OUTPUTS
 
     
    .EXAMPLE
 
    PS> (Use with New-MiniGameObjetUtility) - Add two movable blocks with radius collision method:
 
        $object = New-MiniGameObjectUtility -CollisionMethod Radius
        $block1 = $object.Add('Block 1')
        $block1.Rightmost()
        $block1.Move(-23,5)
        $block1.CollisionGroup()
        $block1.AddCanvas(@("###"," # ","###"))
 
        $block2 = $object.Add('Block 2')
        $block2.Rightmost()
        $block2.Move(-19,5)
        $block2.CollisionGroup()
        $block2.AddCanvas(@("###"," # ","###"))
 
        clear
        $block1.Redraw()
        $block2.Redraw()
 
        $script = {
            param($self, $other)
            Write-Host "`nI'm $($self.Id) colliding with $($other.Id)"
        }
        $block1.OnCollision($script)
        $block2.OnCollision($script)
        Start-Sleep -Seconds 2
 
        $block2.Move(-1, 0)
        $block2.Redraw()
        $block1.Redraw()
        $block1.Collisions()
        Start-Sleep -Seconds 2
 
        $block2.Move(-1, 0)
        $block2.Redraw()
        $block1.Redraw()
        $block1.Collisions()
        Start-Sleep -Seconds 2
 
        $block2.Move(-1, 0)
        $block2.Redraw()
        $block1.Redraw()
        $block2.Collisions()
        Start-Sleep -Seconds 2
 
        $object.Collisions()
        Start-Sleep -Seconds 2
 
 
        ####
        ####
        ####
         
 
    (Standalone Custom Use)
 
    NOTE:
    There is no drawing on Canvas, this only calculates positions.
    Use via the New-MiniGameRunUtility or New-MiniGameObjectUtility for drawing.
         
    PS> $collision = New-MiniGameCollisionUtility -Method Position
 
        $box1 = $collision.Add('Box 1')
        $box1.SetPos(0, 5)
        $box1.SetCanvas(@(
            '###',
            ' # ',
            '###'
        ))
        $box1.OnCollision({
            param($self, $colliders)
            Write-Host "`nI'm $($self.id) colliding with $($colliders.id)"
        })
 
        $box2 = $collision.Add('Box 2')
        $box2.SetPos(2, 5)
        $box2.SetCanvas(@(
            ' ##',
            '###',
            ' ##'
        ))
 
 
        # Should not collide with Box 2, since they are not overlapping.
        $box1.Collisions()
        Start-Sleep -Seconds 2
 
        # After moving one it should now collide.
        $box2.SetPos(1, 5)
        $box1.Collisions()
 
 
    .LINK
     
    #>


    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true
        )]
        [ValidateSet(
            'Position', 'Radius'
        )]
        [System.String]
        $Method
    )

    class MiniGameCollisionUtilityObjectBase {
        [System.String] $_group
        [System.Object] $_manager

        [System.Object] $ref
        [System.String] $id

        [System.Boolean] $_valid
        [System.String[]] $canvas
        [System.Numerics.Vector2] $pos 
    
        [System.Collections.Hashtable] $_onCollision = @{}
        [System.Collections.Hashtable] $_currentCollisions = @{} 

        # Set a reference object to provide in the OnCollisions-Function, instead of this object.
        [void] SetRef([System.Object] $ref) {
            $this.ref = $ref
        }
        [void] SetPos([System.Numerics.Vector2] $pos) {
            $this.SetPos($pos.X, $pos.Y)
        }
        [void] SetPos([Int32] $x, [Int32] $y) {
            $this.pos = [System.Numerics.Vector2]@{X = $x; Y = $y }
            $this._valid = $false
            $this._manager._processed[$this._group] = $false
        }
        [void] SetCanvas([System.String[]] $canvas) {
            $this.canvas = $canvas
            $this._valid = $false
            $this._manager._processed[$this._group] = $false
        }
        
        [void] OnCollision([scriptblock] $action) {
            $this._onCollision.Remove($action.Id.Guid)
            $this._onCollision.Add($action.Id.Guid, $action)
        }
        
        [void] InvokeCollisions() {
            if ($this._currentCollisions.Count -EQ 0) {
                return
            }

            foreach ($action in $this._onCollision.Values) {
                if ($null -NE $this.ref) {
                    $action.Invoke($this.ref, $this._currentCollisions.Values)
                }
                else {
                    $action.Invoke($this, $this._currentCollisions.Values)
                }
            }
        }
        [void] Collisions() {
            $this._manager.Calculate($this._group)
            $this.InvokeCollisions()
        }
    }

    class MiniGameCollisionUtilityBase {
        [System.Collections.Hashtable] $_groups = @{}
        [System.Collections.Hashtable] $_processed = @{}

        [MiniGameCollisionUtilityObjectBase] Add(
            [MiniGameCollisionUtilityObjectBase] $object,
            [System.String] $group
        ) {
            if (-NOT $this._groups.ContainsKey($group)) {
                $this._groups.Add($group, @{})
                $this._processed.Add($group, $false)
            }

            $this._groups[$group].Add($object.id, $object)
            
            $object._manager = $this
            $object._group = $group

            return $object
        }
        [MiniGameCollisionUtilityObjectBase] Add(
            [MiniGameCollisionUtilityObjectBase] $object
        ) {
            return $this.Add($object, '__default__')
        }


        [void] Collisions([System.String] $group) {
            $this.Calculate($group)

            # Invoke the collision handler on each object.
            foreach ($collisionObject in $this._groups[$group].Values) {
                $collisionObject.InvokeCollisions()
            }
        }
        [void] Collisions() {
            $this.Collisions('__default__')
        }
    }


    <#
     
        Find collisions based on a radius around the center
     
    #>


    class MiniGameCollisionUtilityObjectRadiusMethod : MiniGameCollisionUtilityObjectBase {
        [System.Single] $_radius = -1
        [System.Numerics.Vector2] $_center

        [void] SetRadius([System.Single] $radius) {
            $this._radius = $radius
            $this._valid = $false
        }

        [void] _process() {
            if ($this._valid) {
                return
            }

            $this._valid = $true
            $rowCount = $this.canvas.Count
            $rowMiddle = [System.Math]::Floor($rowCount / 2)

            $colCount = $this.canvas[$rowMiddle].Length
            $colMiddle = [System.Math]::Floor($colCount / 2)

            $this._center = [System.Numerics.Vector2]@{
                X = $this.pos.X + $colMiddle
                Y = $this.pos.Y + $rowMiddle
            }

            if (-1 -EQ $this._radius) {
                $this._radius = (($rowCount / 2) + ($colCount / 2)) / 2
            }
        }
    }

    class MiniGameCollisionUtilityRadiusMethod : MiniGameCollisionUtilityBase {
        [MiniGameCollisionUtilityObjectRadiusMethod] Add(
            [System.String] $id,
            [System.String] $group
        ) {
            return ([MiniGameCollisionUtilityBase]$this).Add( 
                [MiniGameCollisionUtilityObjectRadiusMethod]@{_manager = $this; id = $id }, $group
            )
        }
        [MiniGameCollisionUtilityObjectRadiusMethod] Add([System.String] $id) {
            return ([MiniGameCollisionUtilityBase]$this).Add( 
                [MiniGameCollisionUtilityObjectRadiusMethod]@{_manager = $this; id = $id }
            )
        }


        [void] Calculate([System.String] $group) {
            # If for the current state everything has been processed, don't calculate collisions again.
            # This will be invalidated, as soon as a property on a collision object changes.
            if ($this._processed[$group]) {
                return
            }
            else {
                $this._processed[$group] = $true
            }

            $collisonGroup = $this._groups[$group]
            foreach ($object in $collisonGroup.Values) {
                $object._currentCollisions = @{}
                $object._process()
            }

            foreach ($main in $collisonGroup.Values) {
                foreach ($other in $collisonGroup.Values) {
                    if ($main -EQ $other) {
                        continue
                    }
                    if ($main._currentCollisions.ContainsKey($other.id)) {
                        continue
                    }

                    $dist = [System.Numerics.Vector2]@{
                        X = $main._center.X - $other._center.X
                        Y = $main._center.Y - $other._center.Y
                    }

                    $dist = $dist.X * $dist.X + $dist.Y * $dist.Y
                    $rad = $main._radius * $main._radius + $other._radius * $other._radius
                    if ($dist -LT $rad) {
                        $main._currentCollisions.Add($other.id, $other)
                        $other._currentCollisions.Add($main.id, $main)
                    }
                }
            }
        }
    }



    <#
     
        Find collisions based on exact positions.
        (This doesn't work if the positions aren't integer values!)
 
    #>


    class MiniGameCollisionUtilityObjectPositionMethod : MiniGameCollisionUtilityObjectBase {
        [System.Numerics.Vector2[]] $_occupied

        [System.Numerics.Vector2[]] _process() {
            if ($this._valid) {
                return $this._occupied
            }

            $this._valid = $true
            $this._occupied = @()
            for ($row = 0; $row -LT $this.canvas.Count; $row++) {
                for ($col = 0; $col -LT $this.canvas[$row].Length; $col++) {
                    $char = $this.canvas[$row][$col]
                    if ([System.String]::IsNullOrWhiteSpace($char)) {
                        continue
                    }
                    $this._occupied += [System.Numerics.Vector2]@{
                        X = [System.Math]::round($this.pos.X + $col)
                        Y = [System.Math]::round($this.pos.Y + $row)
                    }
                }
            }

            return $this._occupied
        }
    }

    class MiniGameCollisionUtilityPositionMethod : MiniGameCollisionUtilityBase {
        [MiniGameCollisionUtilityObjectPositionMethod] Add(
            [System.String] $id,
            [System.String] $group
        ) {
            return ([MiniGameCollisionUtilityBase]$this).Add( 
                [MiniGameCollisionUtilityObjectPositionMethod]@{_manager = $this; id = $id }, $group
            )
        }
        [MiniGameCollisionUtilityObjectPositionMethod] Add([System.String] $id) {
            return ([MiniGameCollisionUtilityBase]$this).Add( 
                [MiniGameCollisionUtilityObjectPositionMethod]@{_manager = $this; id = $id }
            )
        }

        [void] Calculate([System.String] $group) {
            # If for the current state everything has been processed, don't calculate collisions again.
            # This will be invalidated, as soon as a property on a collision object changes.
            if ($this._processed[$group]) {
                return
            }
            else {
                $this._processed[$group] = $true
            }

            [System.Collections.Hashtable] $dections = @{}
            [System.Collections.Hashtable] $temp = @{}

            $collisonGroup = $this._groups[$group]
            foreach ($object in $collisonGroup.Values) {

                $object._currentCollisions = @{}
                foreach ($pos in $object._process()) {
                    # If two or more object occupy the same space,
                    # then they are colliding with each other
                    if ($temp.ContainsKey($pos)) {
                        $dections.Remove($pos)
                        $dections.Add($pos, $pos) 
                        $temp[$pos] += $object
                    }
                    else {
                        $temp.Add($pos, @($object))
                    }
                }
            }

            # Those are all postion on which a collision occured.
            # However multiple position can belong to the same object.
            foreach ($pos in $dections.Values) {

                # Temp has all the objects at the same position.
                # Multiple objects on the same position collide with each other.
                $objects = $temp[$pos]
                foreach ($main in $objects) {
                    foreach ($other in $objects) {
                        # Don't add a collision to itself!
                        if ($main -EQ $other) {
                            continue
                        }

                        # Collect all colliding objects in the collision object.
                        if (-NOT $main._currentCollisions.ContainsKey($other.id)) {
                            if ($null -NE $other.ref) {
                                $main._currentCollisions.Add($other.id, $other.ref)
                            }
                            else {
                                $main._currentCollisions.Add($other.id, $other)
                            }
                        }
                    }
                }
            }
        }
    }

    if ($Method -IEQ 'Radius') {
        return [MiniGameCollisionUtilityRadiusMethod]@{}
    }
    elseif ($Method -IEQ 'Position') {
        return [MiniGameCollisionUtilityPositionMethod]@{}
    }

}