Source: ray/castCone.js

/**
 * Cast ray in a cone to find closest intersections with tested mapped objects.
 *
 * @method Raycaster.Ray#castCone
 * @memberof Raycaster.Ray
 * @instance
 * @since 0.7.0
 *
 * @param {object} [options] - options that may include:
 * @param {object[]} [options.objects = Raycaster.mappedObjects] - Array of game objects to test. If not provided test all mapped game objects.
 *
 * @return {Phaser.Geom.Point[]} Array of points of ray's closest intersections with tested objects. Additionally each point contains reference to hit mapped object and it's segment if available.
 */
export function castCone(options = {}) {
    let originalAngle = this.angle;
    let intersections = [];
    let maps = [];
    let rayTargets = [];
    let testedObjects = [];
    let cone = this.cone;
    let minAngle = 0;
    let maxAngle = 0;
    let angleOffset = 0;
    let startTime = performance.now();
    //reset stats
    this._stats = {
        method: 'castCone',
        rays: 0,
        testedMappedObjects: 0,
        hitMappedObjects: 0,
        segments: 0,
        time: 0
    };

    //set cone
    if(options.cone !== undefined)
        cone = options.cone;
    if(options.coneDeg !== undefined)
        cone = Phaser.Math.DegToRad(options.coneDeg);

    //set cone min and max angle
    minAngle = this.angle - cone / 2;
    maxAngle = this.angle + cone / 2;

    //add min and max angle points
    this.setAngle(minAngle);
    rayTargets.push({
        point: this._ray.getPointB(),
        angle: minAngle,
        angleOffsetDeg: Phaser.Math.RadToDeg(-cone / 2)
    });

    this.setAngle(maxAngle);
    rayTargets.push({
        point: this._ray.getPointB(),
        angle: maxAngle,
        angleOffsetDeg: Phaser.Math.RadToDeg(cone / 2)
    });

    //if no objects to cast ray were passed, use raycasters mapped objects
    if(!options.objects) {
        if(this._raycaster)
            options.objects = this._raycaster.mappedObjects;
        else
            return intersections;
    }

    //if bounding box is defined add bounding box points to 
    if(this._raycaster && this._raycaster.boundingBox) {
        for(let point of this._raycaster.boundingBox.points) {

            let angle = Phaser.Math.Angle.Between(this.origin.x, this.origin.y, point.x, point.y);
            let angleOffsetDeg = Phaser.Math.Angle.ShortestBetween(Phaser.Math.RadToDeg(angle), Phaser.Math.RadToDeg(originalAngle));

            if(Math.abs(angleOffsetDeg) < Phaser.Math.RadToDeg(cone / 2)) {
                rayTargets.push({
                    point: point,
                    angle: angle,
                    angleOffsetDeg: -angleOffsetDeg
                });
            }
        }
    }

    for(let i=0, iLength = options.objects.length; i < iLength; i++) {
        let object = options.objects[i];
        //if bound in range
        if(!this.boundsInRange(object))
            continue;
        
        testedObjects.push(object);

        let map, boundingBox;
        if(object.type === 'body' || object.type === 'composite')
            map = object.raycasterMap;
        else
            map = object.data.get('raycasterMap');

        //get slightly enlarged bounding box due to fridge cases, when ray "glanced" border box's corner (v0.10.1)
        boundingBox = map.getBoundingBox();
        boundingBox.setTo(boundingBox.x - 0.1, boundingBox.y - 0.1, boundingBox.width + 0.2, boundingBox.height + 0.2);

        map._boundingBox = boundingBox;

        maps.push(map);
        //get points and angles
        for(let point of map.getPoints(this)) {

            let angle = Phaser.Math.Angle.Between(this.origin.x, this.origin.y, point.x, point.y);
            let angleOffsetDeg = Phaser.Math.Angle.ShortestBetween(Phaser.Math.RadToDeg(angle), Phaser.Math.RadToDeg(originalAngle));

            if(Math.abs(angleOffsetDeg) < Phaser.Math.RadToDeg(cone / 2)) {
                rayTargets.push({
                    point: point,
                    angle: Phaser.Math.Angle.Between(this.origin.x, this.origin.y, point.x, point.y),
                    angleOffsetDeg: -angleOffsetDeg
                });
            }
        }

        //get objects intersections
        for(let j = i+1, jLength = options.objects.length; j < jLength; j++){
            let objectB = options.objects[j];
            let mapB;
            if(objectB.type === 'body' || objectB.type === 'composite')
                mapB = objectB.raycasterMap;
            else
                mapB = objectB.data.get('raycasterMap');
            //check if bounding boxes overlap
            if(!Phaser.Geom.Intersects.RectangleToRectangle(map.getBoundingBox(), mapB.getBoundingBox()))
                continue;
            
            //find objects intersections
            for(let segmentA of map.getSegments(this)) {
                for(let segmentB of mapB.getSegments(this)) {
                    let intersection = [];
                    if(!Phaser.Geom.Intersects.LineToLine(segmentA, segmentB, intersection))
                        continue;
                    let angle = Phaser.Math.Angle.Between(this.origin.x, this.origin.y, intersection.x, intersection.y);
                    let angleOffsetDeg = Phaser.Math.Angle.ShortestBetween(Phaser.Math.RadToDeg(angle), Phaser.Math.RadToDeg(originalAngle));

                    if(Math.abs(angleOffsetDeg) < Phaser.Math.RadToDeg(cone / 2)) {
                        rayTargets.push({
                            point: new Phaser.Geom.Point(intersection.x, intersection.y),
                            angle: Phaser.Math.Angle.Between(this.origin.x, this.origin.y, intersection.x, intersection.y),
                            angleOffsetDeg: -angleOffsetDeg
                        });
                    }
                }
            }
        }
    }

    //sort target points by angle
    rayTargets.sort(function(a, b){
        //if rays towards points have the same angles promote closer one
        if(a.angle == b.angle) {
            if(Phaser.Math.Distance.Between(this.origin.x, this.origin.y, a.point.x, a.point.y) > Phaser.Math.Distance.Between(this.origin.x, this.origin.y, b.point.x, b.point.y))
                return 1;
            else
                return -1;
        }

        return a.angleOffsetDeg - b.angleOffsetDeg;
    }.bind(this));

    let previousTarget = {
        angle: false
    };

    //cast rays
    for(let target of rayTargets){
        //if current target is the same as previous one skip loop
        if(target.angle === previousTarget.angle) {
            continue;
        }

        previousTarget = target;
        
        this.setAngle(target.angle);
        let intersection = this.cast({
            objects: testedObjects,
            target: target.point,
            internal: true
        });

        if(intersection) {
            //if intersection hits target point check if ray "glanced" mapped object.
            let castSides = false;
            if(this.round) {
                let roundedTarget = new Phaser.Geom.Point(Math.round(target.point.x), Math.round(target.point.y));
                castSides = Phaser.Geom.Point.Equals(roundedTarget, intersection)
            }
            else {
                castSides = Phaser.Geom.Point.Equals(target.point, intersection);
            }
            
            if(!castSides) {
                //castSides = false;
            }
            else if(!target.point.neighbours || target.point.neighbours.length < 2) {
                //castSides = true;
            }
            //check if ray and at least one line between target point and it's neighbours are parallel
            else if(Phaser.Math.Angle.Normalize(this.angle - Phaser.Math.Angle.BetweenPoints(this.origin, target.point.neighbours[0])) < 0.0001
                || Phaser.Math.Angle.Normalize(this.angle - Phaser.Math.Angle.BetweenPoints(this.origin, target.point.neighbours[1])) < 0.0001) {
                //castSides = true;
            }
            //check if ray crossed more than 1 points of triangle created by tatget point and it's neighbours
            else {
                let triangleIntersections = [];
                if(!target.point.neighboursTriangle) {
                    target.point.neighboursTriangle = new Phaser.Geom.Triangle(target.point.x, target.point.y, target.point.neighbours[0].x, target.point.neighbours[0].y, target.point.neighbours[1].x, target.point.neighbours[1].y);
                }

                Phaser.Geom.Intersects.GetTriangleToLine(target.point.neighboursTriangle, this._ray, triangleIntersections);
                
                //if point of intersection of ray and tirangle are close to arget point, assume ray "glanced" triangle.
                for(let triangleIntersection of triangleIntersections) {
                    if(Math.abs(target.point.x - triangleIntersection.x) > 0.0001 && Math.abs(target.point.y - triangleIntersection.y) > 0.0001) {
                        castSides = false;
                        break;
                    }
                }
            }
            
            //if ray "glanced" mapped object cast two additional rays
            if(castSides) {
                this.setAngle(target.angle - 0.0001);
                let intersectionA = this.cast({
                    objects: testedObjects,
                    internal: true
                });

                if(intersectionA) {
                    intersections.push(intersectionA);
                }

                intersections.push(intersection);

                this.setAngle(target.angle + 0.0001);
                let intersectionB = this.cast({
                    objects: testedObjects,
                    internal: true
                });

                if(intersectionB) {
                    intersections.push(intersectionB);
                }

                continue;
            }

            intersections.push(intersection);
        }
    }

    this.setAngle(originalAngle);
    this.intersections = intersections;
    if(this.autoSlice)
        this.slicedIntersections = this.slice(intersections, false);
    
    this._stats.time = performance.now() - startTime;

    this.drawDebug(intersections);

    return intersections;
}