package com.jonathan.survivor; import java.util.ArrayList; import java.util.Random; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import com.jonathan.survivor.entity.Box; import com.jonathan.survivor.entity.GameObject; import com.jonathan.survivor.entity.InteractiveObject.InteractiveState; import com.jonathan.survivor.entity.ItemObject; import com.jonathan.survivor.entity.Tree; import com.jonathan.survivor.entity.Zombie; import com.jonathan.survivor.managers.GameObjectManager; import com.jonathan.survivor.math.Vector2; /** * Defines the geometry for a layer of terrain. A TerrainLevel contains these layers and manages them. * @author Clementine * */ public class TerrainLayer { /** Stores the row and column of the layer. Note that the row defines the layer's geometry. */ private int row, col; /** Stores the width and height of a layer. The width is measured from the left end of the layer to the right end. The height * goes from the bottom y-position to the top y-position of any x-position on the layer. If it is a cosine layer, it does not * go from the bottom-most point to the top-most point; it is the same regardless. */ public static final float LAYER_WIDTH = 20, LAYER_HEIGHT = 3.5f; /** Stores the height from any bottom point of the layer to its ground point where the user walks */ public static final float GROUND_HEIGHT = 0.5f; /** Stores the bottom height of objects on the layer. Increasing this places objects higher on the layer. */ public static final float OBJECT_HEIGHT = 1.0f; /** Stores the minimum horizontal spacing between GameObjects on a layer. */ public static final float OBJECT_SPACING = 2.0f; /** Stores the maximum slope a linear layer can have */ public static final float MAX_SLOPE = 0.33f; /** Stores the minimum slope a linear layer can have */ public static final float MIN_SLOPE = 0.1f; /** Stores the maximum amplitude a cosine layer can have. (Note that amplitude = half-height of cosine function) */ public static final float MAX_AMPLITUDE = 1; /** Stores the minimum amplitude a cosine layer can have. (Note that amplitude = half-height of cosine function) */ public static final float MIN_AMPLITUDE = 0.2f; /** Stores the b-value in a cosine function, the frequency, found by 2pi/period, where the period is the width of the cosine function. */ public static final float COSINE_FREQUENCY = 2 * (float) Math.PI / LAYER_WIDTH; /** Holds the distance in meters. Determines how close a GameObject has to be to the layer's edge be considered "near" the edge. Used in closeToEdge(...) */ public static final float EDGE_MARGIN = 2; /** Holds the probability rate (0: lowest chance, 1: highest chance) that a zombie gets spawned on the TerrainLayer. */ public static final float ZOMBIE_PROBABILITY_RATE = 0.5f; /** Stores the position of the bottom-left and bottom-right ends of the layer. */ private final Vector2 leftPoint, rightPoint; /** Stores the slope of the layer if it is linear */ private float slope; /** Stores the amplitude of the layer if it is a cosine function */ private float amplitude; /** Stores the 'h' variable of the cosine function if the layer is a cosine function */ private float cosineXOffset; /** Stores the 'k' variable of the cosine function if the layer is a cosine function */ private float cosineYOffset; public enum TerrainType { CONSTANT, LINEAR, COSINE } /** Stores the type of the terrain layer */ private TerrainType terrainType; public enum TerrainDirection { RIGHT, LEFT }; /** Stores whether the terrain goes from left to right or from right to left. */ private TerrainDirection terrainDirection; /** Stores the world seed used to randomly generate the geometry of the layer. */ private int worldSeed; /** Stores the GameObjectManager used to fetch GameObjects to populate the TerrainLayer with objects. */ private GameObjectManager goManager; /** Stores the random object used to define the terrain geometry of the layer. */ private Random terrainRand; /** Stores the random object used to define the objects stacked on the layer. */ private Random objectRand; /** Stores the profile used to create the TerrainLayer. Specifies the world seed, and the GameObjects already scavenged on each layer. */ private Profile profile; /** Stores an array of all the GameObjects that are on this layer. If gameObjectsStored==true, the array is already populated. Helper array to avoid GC. */ private Array<GameObject> gameObjects = new Array<GameObject>(); /** Stores true if the gameObjects array has already been populated with the correct objects. Prevents having to re-populate the array every frame. */ private boolean gameObjectsStored = false; /** Holds arrays containing the different types of GameObjects on the layer. */ private Array<Tree> trees = new Array<Tree>(); private Array<Box> boxes = new Array<Box>(); private Array<Zombie> zombies = new Array<Zombie>(); /** Stores an array of all the ItemObjects that have been dropped on this TerrainLayer. These items can be picked up. */ private Array<ItemObject> itemObjects = new Array<ItemObject>(); /** Constructor used to create a terrain layer. * * @param row The row of the layer * @param col The column of the layer * @param startX The starting x-position of the layer, specified as the left-most x-position of the layer if terrainDirection = RIGHT or the right-most * x-position of the layer if terrainDirection == LEFT. * @param startY The starting y-position of the layer, specified as the bottom y-position of either end of the layer. * @param terrainDirection The direction the terrain faces. If TerrainDirection.RIGHT is specified, the (startX,startY) parameters specify the bottom-left * end-point of the layer * @param profile Profile where the layer retrieves the world seed, along with information on which GameObjects have already been scavenged. * @param goManager The GameObjectManager which manages the World's GameObjects. Used to populate the layer with objects. */ public TerrainLayer(int row, int col, float startX, float startY, TerrainDirection terrainDirection, Profile profile, GameObjectManager goManager) { //Stores the row and column of the layer this.row = row; this.col = col; //Creates new Vector2s to store the left end-point of the layer and the right end-point of the layer. leftPoint = new Vector2(); rightPoint = new Vector2(); //Sets the start position of the layer, essentially specifying where in world coordinates the layer should be placed. setStartPosition(startX, startY, terrainDirection); //Stores the profile used to revert the layer back to its save-game state. this.profile = profile; //Stores the world seed in its respective member variable. this.worldSeed = profile.getWorldSeed(); //Stores the GameObjectManager instance used by the world in order to populate the terrain with GameObjects. this.goManager = goManager; //Creates a new Random object to define the geometry of the layer and the objects it contains. terrainRand = new Random(); objectRand = new Random(); //Resets the layer by computing its geometry and the objects placed on it. resetLayer(); } /** Resets the layer. Re-computes the geometry of the layer and the objects it contains depending on its row and column. Must be called any time the * layer is re-purposed to fit another row or column. */ public void resetLayer() { //Resets the terrain geometry of the layer resetTerrain(); //Resets the objects placed on the layer. resetObjects(); } /** Resets and re-calculates the terrain geometry according to the world seed and the column number of the layer. */ public void resetTerrain() { //Stores the seed used for the terrain's geometry. Only the column and the worldSeed are used so that layers in the same column will have the same geometry. int terrainSeed = col * worldSeed; //Sets the seed of the terrainRand instance to the new seed. Numbers will be generated to define the geometry of the layer. terrainRand.setSeed(terrainSeed); //Gets a random float from [0,1] to define the type of the terrain. float randType = terrainRand.nextFloat(); //If the random number is above this value if(randType > 0.5f) //Original: 0.5f { //Make the terrain layer have a COSINE geometry by setting its type to COSINE. terrainType = TerrainType.COSINE; //Generates a random amplitude for the cosine function between MIN_AMPLITUDE and MAX_AMPLITUDE. amplitude = MIN_AMPLITUDE + (terrainRand.nextFloat() * (MAX_AMPLITUDE - MIN_AMPLITUDE)); //If a new random float is greater than '0.5', make the amplitude negative to flip the cosine function. if(terrainRand.nextFloat() > 0.5f) //Flip the cosine function upside down. amplitude *= -1; //If the terrain goes from left to right if(terrainDirection == TerrainDirection.RIGHT) { //Set the right end-point of the layer to the left end-point, plus the width of the layer. Needs to be set since only leftPoint contains a valid value. rightPoint.set(leftPoint).add(LAYER_WIDTH, 0); } //If the terrain goes from right to left else { //Set the left end-point of the layer to be the right end-point, minus layer's width. Needs to be initialized since only 'leftPoint' contains a valid value. leftPoint.set(rightPoint).sub(LAYER_WIDTH, 0); } //Offsets the cosine function by the x-position of the left of the layer. This places the cosine function at the right place in the world. cosineXOffset = leftPoint.x; //Offsets the cosine function by the y-position of either end point of the layer, minus the cosine's amplitude. This places the cosine function at the right yPos. cosineYOffset = leftPoint.y - amplitude; } //Else, if the random float is greater than this value else if(randType > 0.2f) { //Make the terrain a linear terrain, modeled by a linear equation. terrainType = TerrainType.LINEAR; //Generate a random slope for the layer. slope = MIN_SLOPE + (terrainRand.nextFloat() * (MAX_SLOPE-MIN_SLOPE)); //if(terrainRand.nextFloat() > 0.5f) //slope *= -1; //If the terrain goes from left to right if(terrainDirection == TerrainDirection.RIGHT) { //Find the x-position of the right end point of the layer by adding the layer width to the left end-point's x-position. float x = leftPoint.x + LAYER_WIDTH; //Find the y-position of the right end point of the layer using y = ax + b, by multiplying the line's slope by the x position from the y-intercept, which //is LAYER_WIDTH, since the left end-point's x-position is the origin, and leftPoint.y is the y-intercept. float y = slope * LAYER_WIDTH + leftPoint.y; //Set the right end-point of the layer to the calculated position. rightPoint.set(x, y); } //If the terrain goes from right to left else { //Find the x-position of the left end point of the layer by subtracting the layer width from the right end-point's x-position. float x = rightPoint.x - LAYER_WIDTH; //Find the y-position of the left end point of the layer using y = ax + b, by multiplying the line's slope by the x position from the y-intercept, which //is x = -LAYER_WIDTH, since the right end-point's x-position is the origin, and leftPoint.x is at x = 0. float y = slope * -LAYER_WIDTH + rightPoint.y; //Sets the left end-point's position for the layer to the computed position above. leftPoint.set(x, y); } } //Else, if we are here, the random number stored in randType:float dictates that the layer should be a constant function. else { //Set the terrain type of the layer to be a constant function. terrainType = TerrainType.CONSTANT; //Set the slope of the line to zero. slope = 0; //If the terrain goes from left to right if(terrainDirection == TerrainDirection.RIGHT) { //Set the right end-point of the layer to the left end-point, plus the width of the layer. Needs to be set since only leftPoint contains a valid value. rightPoint.set(leftPoint).add(LAYER_WIDTH, 0); } //If the terrain goes from right to left else { //Set the left end-point of the layer to be the right end-point, minus layer's width. Needs to be initialized since only 'leftPoint' contains a valid value. leftPoint.set(rightPoint).sub(LAYER_WIDTH, 0); } } } /** Resets the objects placed on the layer. This essentially places the correct objects on the layer depending on its column and row. */ public void resetObjects() { //The object seed is determined by the row and the column of the layer. To make sure every world's layers have different objects, we //add the world seed to the sum of the row and the column. int objectSeed = row + col + worldSeed; //Sets the seed of the objectRand instance to the new seed. Numbers will be generated to define the which objects are on the layer. objectRand.setSeed(objectSeed); //Stores a list of objectIds for the GameObjects that have already been scavenged on this layer. ArrayList<Integer> scavengedObjects = profile.getScavengedLayerObjects(row, col); //Stores the index of the object being placed on the layer. Starts at 0 for the left-most object, and increments by one for each object. int objectIndex = 0; //Cycles from the left-most x-point of the layer, plus an offset, and increments by the object spacing until the end x-point is reached. for(float x = leftPoint.x + OBJECT_SPACING; x < rightPoint.x; x += OBJECT_SPACING) { //Stores a random number between 0 and 1 to determine which object will be placed next on the layer. float randObject = objectRand.nextFloat(); if(randObject > 0.5f) { //If the object we want to place has not been scavenged, place it on the TerrainLayer. if(!scavengedObjects.contains(objectIndex)) { //Retrieves a tree GameObject from a pool inside the GameObjectManager. Tree tree = goManager.getGameObject(Tree.class); //Tell the tree that it has just spawned tree.setInteractiveState(InteractiveState.SPAWN); //Sets the terrain cell to the layer's row and column so that the tree knows which layer it is in. tree.setTerrainCell(row, col); //Positions the tree at the current x-position, and the correct y-position according to the object height at the given x-position. tree.setPosition(x, getObjectHeight(x)); //Set the object id of the tree to the current object index. Used to identify a scavenged GameObject in save data. tree.setObjectId(objectIndex); //Add the tree GameObject to the array of trees held by the layer. trees.add(tree); } //Increment the object index for the next GameObject to be placed on the layer. objectIndex++; //Increment x by the width of the tree so that the next object doesn't overlap. x += Tree.COLLIDER_WIDTH / 2; } else if(randObject > 0.45f) { //If the object we want to place has not yet been scavenged, place it on the TerrainLayer. if(!scavengedObjects.contains(objectIndex)) { //Retrieves a box GameObject from a pool inside the GameObjectManager. Box box = goManager.getGameObject(Box.class); //Tell the box that it has just spawned box.setInteractiveState(InteractiveState.SPAWN); //Sets the terrain cell to the layer's row and column so that the box knows which layer it is in. box.setTerrainCell(row, col); //Positions the box at the current x-position, and the correct y-position according to the object height at the given x-position. box.setPosition(x, getObjectHeight(x)); //Set the object id of the box to the current object index. Used to identify a scavenged GameObject in save data. box.setObjectId(objectIndex); //Add the box GameObject to the array of boxes held by the layer. boxes.add(box); } //Increment the object index for the next GameObject to be placed on the layer. objectIndex++; //Increment x by the width of the box's collider so that the next object doesn't overlap with this box. x += Box.COLLIDER_WIDTH / 2; } } //Generates a new random number between 0 and 1 which dictates whether or not a zombie will be spawned in the center of the TerrainLayer. float randZombie = objectRand.nextFloat(); //If the random number is less than the zombie probability rate, place a zombie on the layer. However, the layer has to be checked to make sure that a zombie can spawn on it. if(randZombie < ZOMBIE_PROBABILITY_RATE && canSpawnZombie()) { //If the zombie has not yet been killed on the TerrainLayer, spawn him there. if(!scavengedObjects.contains(objectIndex)) { //Retrieves a Zombie GameObject from the GameObjectManager. Zombie zombie = goManager.getGameObject(Zombie.class); //Sets the zombie's terrain cell to the layer's row and column so that the zombie knows which layer it is in. zombie.setTerrainCell(row, col); //Places the zombie at the center of the TerrainLayer. zombie.setPosition(getCenterX() - 2, getGroundHeight(getCenterX() - 2)); //Set the object id of the box to the current object index. Used to identify a scavenged GameObject in save data. zombie.setObjectId(objectIndex); //Add the zombie into the list of zombies inside the layer. zombies.add(zombie); } //Increment the object index for the next GameObject that is placed on the layer. objectIndex++; } } /** Returns true if a zombie can be spawned on this layer. The only reason it could not spawn is if this layer is the one where the player has spawned. */ private boolean canSpawnZombie() { //Retrieves the terrain coordinates where the player is first spawned. int playerSpawnRow = profile.getTerrainRowOffset() + TerrainLevel.NUM_LAYER_ROWS/2; int playerSpawnCol = profile.getTerrainColOffset() + TerrainLevel.NUM_LAYER_COLS/2; //If this layer is the same layer where the player is spawned if(row == playerSpawnRow && col == playerSpawnCol) { //Return false, since a zombie should not spawn right next to the player when the game starts. return false; } //If this statement is reached, the zombie can be spawned on this layer. In fact, this layer is not the one where the player first spawns. return true; } /** Frees the GameObjects stored inside the layer. Puts them back into the GameObjectManager's pools, so that they can be reused. Called when the layer * should be re-purposed into another layer. */ public void freeGameObjects() { //Cycles through the array of tree GameObjects belonging to the layer for(int i = 0; i < trees.size; i++) //Frees the trees back into the pool of the GameObjectManager for later reuse. goManager.freeGameObject(trees.get(i), Tree.class); //Cycles through the array of box GameObjects belonging to the layer for(int i = 0; i < boxes.size; i++) //Frees the boxes back into the pool of the GameObjectManager for later reuse. goManager.freeGameObject(boxes.get(i), Box.class); //Cycles through the array of Zombies, which must be freed back into the GameObjectManager's internal pools. for(int i = 0; i < zombies.size; i++) //Frees the Zombies back into their respective pool inside the GameObjectManager so that they can be reused once more Zombies are spawned. goManager.freeGameObject(zombies.get(i), Zombie.class); //Cycles through the array of ItemObjects, which must be freed back into the GameObjectManager's internal pools. for(int i = 0; i < itemObjects.size; i++) //Frees the ItemObjects back into their respective pool inside the GameObjectManager so that they can be reused once more ItemObjects are dropped. goManager.freeGameObject(itemObjects.get(i), ItemObject.class); //Clears the array of all the GameObjects contained by the TerrainLayer so that they can be re-populated with new GameObjects when resetLayer() is called. trees.clear(); boxes.clear(); zombies.clear(); itemObjects.clear(); //Clears the array which contains a list of all the GameObjects on this layer. Allows us to repopulate the array once the layer is reused. gameObjects.clear(); //Tells the getGameObjects() that it has to re-populate its gameObjects array since there we just cleared it. gameObjectsStored = false; } /** Adds the given GameObject to the list of GameObjects contained by the TerrainLayer. This way, the GameObjectRenderer will know to render this GameObject. */ public void addGameObject(GameObject gameObject) { //If the GameObject we want to add is an ItemObject, then a collectible has just been dropped on the ground. if(gameObject instanceof ItemObject) //Add the ItemObject to the list of Item GameObjects contained by this TerrainLayer. itemObjects.add((ItemObject)gameObject); //Else, if the GameObject to add to the layer is a zombie else if(gameObject instanceof Zombie) //Adds the zombie to the list of zombies contained in the layer. zombies.add((Zombie)gameObject); //Add the GameObject to the list of all GameObjects contained in the TerrainLayer. gameObjects.add(gameObject); } /** Removes the given GameObject from the list of GameObjects contained by the TerrainLayer. The GameObjectRenderer will know that it should not render the GameObject. */ public void removeGameObject(GameObject gameObject) { //If the GameObject we want to add is an ItemObject, then a collectible has just been removed from the ground. NEVER CALLED. Pre-mature implementation. if(gameObject instanceof ItemObject) { //Removes the ItemObject from the list of Item GameObjects contained by this TerrainLayer. itemObjects.removeValue((ItemObject)gameObject, true); } //Else, if the GameObject to remove is a zombie else if(gameObject instanceof Zombie) { //Removes the zombie from the list of zombies contained in the layer. zombies.removeValue((Zombie)gameObject, true); } //Removes the GameObject from the list of all GameObjects contained in the TerrainLayer. gameObjects.removeValue(gameObject, true); } /** Returns an array of all GameObjects contained in this layer. */ public Array<GameObject> getGameObjects() { //If the gameObjects array hasn't yet been populated if(!gameObjectsStored) { //Add all the GameObjects from the layer into the gameObjects array. gameObjects.addAll(trees); gameObjects.addAll(boxes); gameObjects.addAll(zombies); gameObjects.addAll(itemObjects); //Tell the layer that all of its GameObjects have been stored inside the array, to avoid doing so every frame. gameObjectsStored = true; } //Returns the array containing all of the layer's GameObjects. return gameObjects; } /** Returns an array consisting of all the Tree GameObjects that are on this layer. */ public Array<Tree> getTrees() { //Returns the trees array, which contains all of the trees held by the layer. return trees; } /** Returns an array containing all the Box GameObjects that are on this layer. */ public Array<Box> getBoxes() { //Returns the boxes array, which contains all of the boxes held by the layer. return boxes; } /** Returns an array containing all the Zombies GameObjects that are on this layer. */ public Array<Zombie> getZombies() { //Returns the 'zombies' array, which contains all of the zombies held by the layer. return zombies; } /** Returns an array consisting of all the Item GameObjects that have been dropped on this layer and have yet to be picked up. */ public Array<ItemObject> getItemObjects() { //Returns the itemObjects array, which contains all of the itemObjects held by the layer. return itemObjects; } /** Returns true if the given GameObject is close to the edge of this TerrainLayer. */ public boolean closeToEdge(GameObject gameObject) { //Stores the gameObject's x-position. The y-position is irrelevant in check if the GameObject is close to the layer's edge. float xPos = gameObject.getX(); //If the GameObject is within 'EDGE_MARGIN' distance from either end point of the layer, it is considered "close to the edge" if(rightPoint.x - xPos < EDGE_MARGIN || xPos - leftPoint.x < EDGE_MARGIN) { //Thus, return true, since the GameObject is close to the edge return true; } //If this statement is reached, the GameObject is not close to the edge of the layer. Thus, return false. return false; } /** Gets the world x-position at the center of the terrain layer */ public float getCenterX() { //Returns the x-position of the left end-point, plus half the width of the layer. return leftPoint.x + LAYER_WIDTH/2; } /** Gets the height of the ground at the center of the layer in world units. */ public float getCenterGroundHeight() { //Returns the ground height at the center of the layer. return getGroundHeight(getCenterX()); } /** Gets the ground height at any given x-position of the layer in world units. */ public float getGroundHeight(float xPos) { //Returns the height of the bottom of the layer, plus the height of the ground. return getBottomLayerHeight(xPos) + GROUND_HEIGHT; } /** Gets the GameObject height at any given x-position on the layer in world units. */ public float getObjectHeight(float xPos) { //Returns the height of the bottom of the layer, plus the height of the GameObjects from the bottom of the layer. return getBottomLayerHeight(xPos) + OBJECT_HEIGHT; } /** Returns the y-position of the top of the layer at any given x-position in world units. */ public float getTopLayerHeight(float xPos) { //Returns the y-position of the bottom of the layer at the given x-position, plus the height of the layer, which is the distance from //the bottom to the top of the layer at any x-position. return getBottomLayerHeight(xPos) + LAYER_HEIGHT; } /** Retrieves the height of the bottom portion of the layer at a specified x-position. */ public float getBottomLayerHeight(float xPos) { //If the x-position is beyond the bounds of the layer, throw an exception. if(xPos < leftPoint.x || xPos > rightPoint.x) throw new IllegalArgumentException("X-Position out of bounds of level layer (" + row + ", " + col + "). Position " + xPos + " not between " + leftPoint.x + " and " + rightPoint.x); //If the terrain is modeled using a cosine function if(terrainType == TerrainType.COSINE) { //Return the y-position of the layer at a specified x-position using the equation y = acos(b(x - h)) + k. This is the bottom portion since cosineYOffset //specifies the y-position of the bottom-most point of the layer. return amplitude * MathUtils.cos(COSINE_FREQUENCY * (xPos - cosineXOffset)) + cosineYOffset; } else if(terrainType == TerrainType.LINEAR) { //Retrieve and return the y-position of the bottom portion of the layer at a given x using y = ax + b, where 'b' is the y-position of the left end-point. return slope * (xPos - leftPoint.x) + leftPoint.y; } //Else, if we are here, the layer is modeled using a constant function. Thus, the y-position of any point on the bottom half of the layer is left/rightPoint.y return leftPoint.y; } /** Sets the cell coordinates of the layer. The resetLayer() method must be called after this. */ public void setCell(int row, int col) { this.row = row; this.col = col; } /** Sets the row of the layer. The resetLayer() method must be called after this. */ public void setRow(int row) { this.row = row; } /** Gets the row of the layer. */ public int getRow() { return row; } /** Sets the column of the layer. The resetLayer() method must be called after this. */ public void setCol(int col) { this.col = col; } /** Gets the column placement of the layer. */ public int getCol() { return col; } /** Sets the start position of the terrain layer. Note that the specified position must either be the coordinates for the bottom-left or bottom-right position * of the layer in world coordinates. If terrainDirection == TerrainDirection.RIGHT, the left coordinate must be specified. If TerrainDirection.LEFT is passed, * the right end-position must be specified. */ public void setStartPosition(float startX, float startY, TerrainDirection terrainDirection) { //Stores the terrainDirection passed from the method. this.terrainDirection = terrainDirection; //If the desired terrain direction is from left to right if(terrainDirection == TerrainDirection.RIGHT) { //The (startX,startY) parameters specify the bottom-left point of the layer. leftPoint.set(startX, startY); } //Else, if the desired terrain direction is from right to left else { //The (startX,startY) parameters specify the bottom-right end-point of the layer. rightPoint.set(startX, startY); } } /** Returns the bottom-left end point of the layer in world coordinates. */ public Vector2 getLeftPoint() { return leftPoint; } /** Returns the bottom-right end point of the layer in world coordinates. */ public Vector2 getRightPoint() { return rightPoint; } /** Gets the terrain type of the layer, dictating what type of equation models its geometry. */ public TerrainType getTerrainType() { return terrainType; } public String toString() { return "Left Point: " + leftPoint + ", Right Point: " + rightPoint; } }